diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index bc1fb8562..819beb288 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -8,6 +8,9 @@ inputs: project-id: description: 'WalletConnect project id' required: true + cache-key: + description: 'Cache key to use for caching' + required: true runs: using: "composite" @@ -34,4 +37,4 @@ runs: with: path: | products.tar - key: ${{ runner.os }}-deriveddata-${{ github.event.pull_request.head.sha }} \ No newline at end of file + key: ${{ runner.os }}-deriveddata-${{ inputs.cache-key }}-${{ github.event.pull_request.head.sha }} \ No newline at end of file diff --git a/.github/actions/run_tests_without_building/action.yml b/.github/actions/run_tests_without_building/action.yml index 682a1b87d..f8d0a3743 100644 --- a/.github/actions/run_tests_without_building/action.yml +++ b/.github/actions/run_tests_without_building/action.yml @@ -24,6 +24,10 @@ inputs: gm-dapp-project-secret: description: 'GM DApp Project Secret' required: false + js-client-api-host: + description: 'JS Client Api Host' + required: false + runs: using: "composite" @@ -51,7 +55,7 @@ runs: - name: Run integration tests if: inputs.type == 'integration-tests' shell: bash - run: make integration_tests RELAY_HOST=${{ inputs.relay-endpoint }} PROJECT_ID=${{ inputs.project-id }} CAST_HOST=${{ inputs.notify-endpoint }} GM_DAPP_PROJECT_ID=${{ inputs.gm-dapp-project-id }} GM_DAPP_PROJECT_SECRET=${{ inputs.gm-dapp-project-secret }} + run: make integration_tests RELAY_HOST=${{ inputs.relay-endpoint }} PROJECT_ID=${{ inputs.project-id }} CAST_HOST=${{ inputs.notify-endpoint }} GM_DAPP_PROJECT_ID=${{ inputs.gm-dapp-project-id }} GM_DAPP_PROJECT_SECRET=${{ inputs.gm-dapp-project-secret }} JS_CLIENT_API_HOST=${{ inputs.js-client-api-host }} # Relay Integration tests - name: Run Relay integration tests @@ -71,6 +75,11 @@ runs: shell: bash run: make notify_tests RELAY_HOST=${{ inputs.relay-endpoint }} PROJECT_ID=${{ inputs.project-id }} CAST_HOST=${{ inputs.notify-endpoint }} GM_DAPP_PROJECT_ID=${{ inputs.gm-dapp-project-id }} GM_DAPP_PROJECT_SECRET=${{ inputs.gm-dapp-project-secret }} + - name: Run x-platform protocol tests + if: inputs.type == 'x-platform-protocol-tests' + shell: bash + run: make x_platform_protocol_tests RELAY_HOST=${{ inputs.relay-endpoint }} PROJECT_ID=${{ inputs.project-id }} JS_CLIENT_API_HOST=${{ inputs.js-client-api-host }} + # Slack notification for failing smoke and relay tests - name: Slack Notification for Failure if: failure() && (inputs.type == 'smoke-tests' || inputs.type == 'relay-tests') diff --git a/.github/workflows/build_artifacts.yml b/.github/workflows/build_artifacts.yml index cc8adb7a5..94bff827b 100644 --- a/.github/workflows/build_artifacts.yml +++ b/.github/workflows/build_artifacts.yml @@ -11,11 +11,13 @@ on: description: 'WalletConnect project id' required: true push: - branches: [ main ] + branches: + - main jobs: build: - runs-on: macos-12 + runs-on: + group: apple-silicon timeout-minutes: 15 steps: @@ -44,4 +46,15 @@ jobs: - uses: actions/upload-artifact@v3 with: name: main-derivedData - path: products.tar \ No newline at end of file + path: products.tar + if-no-files-found: error + + # Slack notification for failing smoke and relay tests + - name: Slack Notification for Failure + if: failure() + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + text: CI pipeline for preparing arifacts failed to build main branch or failed to upload artifact. Check the logs for more details. + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 704d88a64..ef5f81953 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,8 @@ jobs: prepare: needs: authorize - runs-on: macos-12 + runs-on: + group: apple-silicon steps: - uses: actions/checkout@v3 with: @@ -31,10 +32,12 @@ jobs: - uses: ./.github/actions/build with: project-id: ${{ secrets.PROJECT_ID }} + cache-key: ci test: needs: prepare - runs-on: macos-12 + runs-on: + group: apple-silicon timeout-minutes: 15 strategy: fail-fast: false @@ -50,7 +53,7 @@ jobs: with: path: | products.tar - key: ${{ runner.os }}-deriveddata-${{ github.event.pull_request.head.sha }} + key: ${{ runner.os }}-deriveddata-ci-${{ github.event.pull_request.head.sha }} - name: Untar DerivedDataCache shell: bash @@ -66,7 +69,7 @@ jobs: - name: Run integration tests if: matrix.type == 'integration-tests' shell: bash - run: make integration_tests RELAY_HOST=relay.walletconnect.com PROJECT_ID=${{ secrets.PROJECT_ID }} CAST_HOST=cast.walletconnect.com GM_DAPP_PROJECT_ID=${{ secrets.GM_DAPP_PROJECT_ID }} GM_DAPP_PROJECT_SECRET=${{ secrets.GM_DAPP_PROJECT_SECRET }} + run: make integration_tests RELAY_HOST=relay.walletconnect.com PROJECT_ID=${{ secrets.PROJECT_ID }} CAST_HOST=notify.walletconnect.com GM_DAPP_PROJECT_ID=${{ secrets.GM_DAPP_PROJECT_ID }} GM_DAPP_PROJECT_SECRET=${{ secrets.GM_DAPP_PROJECT_SECRET }} JS_CLIENT_API_HOST=test-automation-api.walletconnect.com # Relay Integration tests - name: Run Relay integration tests diff --git a/.github/workflows/deploy_pages.yml b/.github/workflows/deploy_pages.yml index 55c70520a..9901d0c0d 100644 --- a/.github/workflows/deploy_pages.yml +++ b/.github/workflows/deploy_pages.yml @@ -27,7 +27,8 @@ jobs: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} - runs-on: macos-12 + runs-on: + group: apple-silicon steps: - name: Checkout uses: actions/checkout@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9a58e1a2f..3174bdc93 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,12 +8,12 @@ on: workflow_dispatch: jobs: - build: + build: runs-on: macos-12 steps: - uses: actions/checkout@v3 - + - uses: actions/cache@v3 with: path: | @@ -26,12 +26,13 @@ jobs: - name: Release shell: bash - env: + env: MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} GH_TOKEN: ${{ secrets.GH_TOKEN }} GH_USER: ${{ secrets.GH_USER }} APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }} APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }} APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }} + WALLETAPP_SENTRY_DSN: ${{ secrets.WALLETAPP_SENTRY_DSN }} run: | - make release_all APPLE_ID=${{ secrets.APPLE_ID }} TOKEN=$(echo -n $GH_USER:$GH_TOKEN | base64) PROJECT_ID=${{ secrets.RELEASE_PROJECT_ID }} + make release_all APPLE_ID=${{ secrets.APPLE_ID }} TOKEN=$(echo -n $GH_USER:$GH_TOKEN | base64) PROJECT_ID=${{ secrets.RELEASE_PROJECT_ID }} WALLETAPP_SENTRY_DSN=${{ secrets.WALLETAPP_SENTRY_DSN }} MIXPANEL_TOKEN=${{secrets.MIXPANEL_TOKEN}} diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/NotifyTests.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/NotifyTests.xcscheme new file mode 100644 index 000000000..b94c9e014 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/NotifyTests.xcscheme @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect-Package.xcscheme index bcf1ba2ab..14b8eeb45 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect-Package.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect-Package.xcscheme @@ -488,8 +488,13 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES" - shouldAutocreateTestPlan = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES"> + + + + diff --git a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectPush.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectNotify.xcscheme similarity index 55% rename from Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectPush.xcscheme rename to .swiftpm/xcode/xcshareddata/xcschemes/WalletConnectNotify.xcscheme index c779cb9d1..9b01e10d1 100644 --- a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectPush.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectNotify.xcscheme @@ -1,7 +1,7 @@ + LastUpgradeVersion = "1430" + version = "1.7"> @@ -14,24 +14,10 @@ buildForAnalyzing = "YES"> - - - - + BlueprintIdentifier = "WalletConnectNotify" + BuildableName = "WalletConnectNotify" + BlueprintName = "WalletConnectNotify" + ReferencedContainer = "container:"> @@ -40,19 +26,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> - - - - - - + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> + BlueprintIdentifier = "WalletConnectNotify" + BuildableName = "WalletConnectNotify" + BlueprintName = "WalletConnectNotify" + ReferencedContainer = "container:"> diff --git a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectEcho.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectPush.xcscheme similarity index 76% rename from Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectEcho.xcscheme rename to .swiftpm/xcode/xcshareddata/xcschemes/WalletConnectPush.xcscheme index 341890963..43b6f9bc1 100644 --- a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectEcho.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectPush.xcscheme @@ -1,7 +1,7 @@ + LastUpgradeVersion = "1430" + version = "1.7"> @@ -14,10 +14,10 @@ buildForAnalyzing = "YES"> + BlueprintIdentifier = "WalletConnectPush" + BuildableName = "WalletConnectPush" + BlueprintName = "WalletConnectPush" + ReferencedContainer = "container:"> @@ -26,9 +26,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> - - + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> + BlueprintIdentifier = "WalletConnectPush" + BuildableName = "WalletConnectPush" + BlueprintName = "WalletConnectPush" + ReferencedContainer = "container:"> diff --git a/Configuration.xcconfig b/Configuration.xcconfig index fd6cd8553..361714e46 100644 --- a/Configuration.xcconfig +++ b/Configuration.xcconfig @@ -9,4 +9,11 @@ RELAY_HOST = relay.walletconnect.com // Uncomment next line and paste dapp's project secret to run all the notify tests // GM_DAPP_PROJECT_SECRET = GM_DAPP_PROJECT_SECRET -CAST_HOST = cast.walletconnect.com +// Uncomment next line and paste js client's api host to run x-platform tests +// JS_CLIENT_API_HOST = JS_CLIENT_API_HOST + +// WALLETAPP_SENTRY_DSN = WALLETAPP_SENTRY_DSN + +// MIXPANEL_TOKEN = MIXPANEL_TOKEN + +CAST_HOST = notify.walletconnect.com diff --git a/Example/DApp/Info.plist b/Example/DApp/Info.plist index ec622dae1..92cddbe74 100644 --- a/Example/DApp/Info.plist +++ b/Example/DApp/Info.plist @@ -2,6 +2,15 @@ + LSApplicationQueriesSchemes + + metamask + trust + safe + zerion + rainbow + spot + CFBundleVersion 7 CFBundleShortVersionString diff --git a/Example/DApp/SceneDelegate.swift b/Example/DApp/SceneDelegate.swift index cb82e0aa3..759bb2df1 100644 --- a/Example/DApp/SceneDelegate.swift +++ b/Example/DApp/SceneDelegate.swift @@ -24,9 +24,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { WalletConnectModal.configure( projectId: InputConfig.projectId, - metadata: metadata + metadata: metadata, + accentColor: .green ) - + setupWindow(scene: scene) } diff --git a/Example/DApp/Sign/Accounts/AccountsViewController.swift b/Example/DApp/Sign/Accounts/AccountsViewController.swift index ad29010ae..631a0725a 100644 --- a/Example/DApp/Sign/Accounts/AccountsViewController.swift +++ b/Example/DApp/Sign/Accounts/AccountsViewController.swift @@ -1,6 +1,6 @@ import UIKit import WalletConnectSign -import WalletConnectPush +import WalletConnectNotify import Combine struct AccountDetails { @@ -14,7 +14,7 @@ final class AccountsViewController: UIViewController, UITableViewDataSource, UIT let session: Session var accountsDetails: [AccountDetails] = [] var onDisconnect: (() -> Void)? - var pushSubscription: PushSubscription? + var notifySubscription: NotifySubscription? private var publishers = [AnyCancellable]() private let accountsView: AccountsView = { @@ -54,21 +54,7 @@ final class AccountsViewController: UIViewController, UITableViewDataSource, UIT } } } - - func proposePushSubscription() { - let account = session.namespaces.values.first!.accounts.first! - - Task(priority: .high){ try! await Push.dapp.propose(account: account, topic: session.pairingTopic)} - Push.dapp.proposalResponsePublisher.sink { result in - switch result { - case .success(let subscription): - self.pushSubscription = subscription - case .failure(let error): - print(error) - } - }.store(in: &publishers) - } - + @objc private func disconnect() { Task { diff --git a/Example/DApp/Sign/ResponseViewController.swift b/Example/DApp/Sign/ResponseViewController.swift index eec81065f..9d817014b 100644 --- a/Example/DApp/Sign/ResponseViewController.swift +++ b/Example/DApp/Sign/ResponseViewController.swift @@ -26,10 +26,10 @@ class ResponseViewController: UIViewController { let record = Sign.instance.getSessionRequestRecord(id: response.id)! switch response.result { case .response(let response): - responseView.nameLabel.text = "Received Response\n\(record.method)" + responseView.nameLabel.text = "Received Response\n\(record.request.method)" responseView.descriptionLabel.text = try! response.get(String.self).description case .error(let error): - responseView.nameLabel.text = "Received Error\n\(record.method)" + responseView.nameLabel.text = "Received Error\n\(record.request.method)" responseView.descriptionLabel.text = error.message } responseView.dismissButton.addTarget(self, action: #selector(dismissSelf), for: .touchUpInside) diff --git a/Example/DApp/Sign/SignCoordinator.swift b/Example/DApp/Sign/SignCoordinator.swift index 55344a902..763cd2463 100644 --- a/Example/DApp/Sign/SignCoordinator.swift +++ b/Example/DApp/Sign/SignCoordinator.swift @@ -45,13 +45,6 @@ final class SignCoordinator { presentResponse(for: response) }.store(in: &publishers) - Sign.instance.sessionSettlePublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] session in - let vc = showAccountsScreen(session) - vc.proposePushSubscription() - }.store(in: &publishers) - if let session = Sign.instance.getSessions().first { _ = showAccountsScreen(session) } else { diff --git a/Example/ExampleApp.xcodeproj/IntegrationTests.xctestplan b/Example/ExampleApp.xcodeproj/IntegrationTests.xctestplan index 64d647c69..0fb2db965 100644 --- a/Example/ExampleApp.xcodeproj/IntegrationTests.xctestplan +++ b/Example/ExampleApp.xcodeproj/IntegrationTests.xctestplan @@ -15,6 +15,10 @@ "key" : "RELAY_HOST", "value" : "$(RELAY_HOST)" }, + { + "key" : "JS_CLIENT_API_HOST", + "value" : "$(JS_CLIENT_API_HOST)" + }, { "key" : "GM_DAPP_PROJECT_SECRET", "value" : "$(GM_DAPP_PROJECT_SECRET)" diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index a9217f3c9..5bf6b7261 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -32,8 +32,14 @@ 847BD1E7298A806800076C90 /* NotificationsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847BD1E2298A806800076C90 /* NotificationsInteractor.swift */; }; 847BD1E8298A806800076C90 /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847BD1E3298A806800076C90 /* NotificationsView.swift */; }; 847BD1EB298A87AB00076C90 /* SubscriptionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847BD1EA298A87AB00076C90 /* SubscriptionsViewModel.swift */; }; - 847CF3AF28E3141700F1D760 /* WalletConnectPush in Frameworks */ = {isa = PBXBuildFile; productRef = 847CF3AE28E3141700F1D760 /* WalletConnectPush */; settings = {ATTRIBUTES = (Required, ); }; }; - 849D7A93292E2169006A2BD4 /* PushTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849D7A92292E2169006A2BD4 /* PushTests.swift */; }; + 847F08012A25DBFF00B2A5A4 /* XPlatformW3WTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847F08002A25DBFF00B2A5A4 /* XPlatformW3WTests.swift */; }; + 8487A9442A836C2A0003D5AF /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 8487A9432A836C2A0003D5AF /* Sentry */; }; + 8487A9462A836C3F0003D5AF /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 8487A9452A836C3F0003D5AF /* Sentry */; }; + 8487A9482A83AD680003D5AF /* LoggingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8487A9472A83AD680003D5AF /* LoggingService.swift */; }; + 84943C7B2A9BA206007EBAC2 /* Mixpanel in Frameworks */ = {isa = PBXBuildFile; productRef = 84943C7A2A9BA206007EBAC2 /* Mixpanel */; }; + 84943C7D2A9BA328007EBAC2 /* Mixpanel in Frameworks */ = {isa = PBXBuildFile; productRef = 84943C7C2A9BA328007EBAC2 /* Mixpanel */; }; + 84943C7F2A9BA48C007EBAC2 /* ProfilingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84943C7E2A9BA48C007EBAC2 /* ProfilingService.swift */; }; + 849D7A93292E2169006A2BD4 /* NotifyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849D7A92292E2169006A2BD4 /* NotifyTests.swift */; }; 84A6E3C32A386BBC008A0571 /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A6E3C22A386BBC008A0571 /* Publisher.swift */; }; 84AA01DB28CF0CD7005D48D8 /* XCTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA01DA28CF0CD7005D48D8 /* XCTest.swift */; }; 84B8154E2991099000FAD54E /* BuildConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B8154D2991099000FAD54E /* BuildConfiguration.swift */; }; @@ -62,14 +68,6 @@ 84DDB4ED28ABB663003D66ED /* WalletConnectAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 84DDB4EC28ABB663003D66ED /* WalletConnectAuth */; }; 84E6B84A29787A8000428BAF /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E6B84929787A8000428BAF /* NotificationService.swift */; }; 84E6B84E29787A8000428BAF /* PNDecryptionService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 84E6B84729787A8000428BAF /* PNDecryptionService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 84E6B85429787AAE00428BAF /* WalletConnectPush in Frameworks */ = {isa = PBXBuildFile; productRef = 84E6B85329787AAE00428BAF /* WalletConnectPush */; }; - 84E6B8582981624F00428BAF /* PushRequestModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E6B8572981624F00428BAF /* PushRequestModule.swift */; }; - 84E6B85B298162EF00428BAF /* PushRequestPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E6B85A298162EF00428BAF /* PushRequestPresenter.swift */; }; - 84E6B85D298162F700428BAF /* PushRequestRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E6B85C298162F700428BAF /* PushRequestRouter.swift */; }; - 84E6B85F2981630000428BAF /* PushRequestInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E6B85E2981630000428BAF /* PushRequestInteractor.swift */; }; - 84E6B8612981630C00428BAF /* PushRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E6B8602981630C00428BAF /* PushRequestView.swift */; }; - 84E6B86329816A7900428BAF /* WalletConnectPush in Frameworks */ = {isa = PBXBuildFile; productRef = 84E6B86229816A7900428BAF /* WalletConnectPush */; }; - 84E6B8652981720400428BAF /* WalletConnectPush in Frameworks */ = {isa = PBXBuildFile; productRef = 84E6B8642981720400428BAF /* WalletConnectPush */; }; 84FE684628ACDB4700C893FF /* RequestParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FE684528ACDB4700C893FF /* RequestParams.swift */; }; A507BE1A29E8032E0038EF70 /* EIP55Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A507BE1929E8032E0038EF70 /* EIP55Tests.swift */; }; A50C036528AAD32200FE72D3 /* ClientDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50C036428AAD32200FE72D3 /* ClientDelegate.swift */; }; @@ -172,6 +170,10 @@ A5A4FC772840C12C00BBEC1E /* RegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A4FC762840C12C00BBEC1E /* RegressionTests.swift */; }; A5A8E47E293A1CFE00FEB97D /* DefaultSignerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59CF4F5292F83D50031A42F /* DefaultSignerFactory.swift */; }; A5A8E480293A1D0000FEB97D /* DefaultSignerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59CF4F5292F83D50031A42F /* DefaultSignerFactory.swift */; }; + A5B6C0F12A6EAB0800927332 /* WalletConnectNotify in Frameworks */ = {isa = PBXBuildFile; productRef = A5B6C0F02A6EAB0800927332 /* WalletConnectNotify */; }; + A5B6C0F32A6EAB1700927332 /* WalletConnectNotify in Frameworks */ = {isa = PBXBuildFile; productRef = A5B6C0F22A6EAB1700927332 /* WalletConnectNotify */; }; + A5B6C0F52A6EAB2800927332 /* WalletConnectNotify in Frameworks */ = {isa = PBXBuildFile; productRef = A5B6C0F42A6EAB2800927332 /* WalletConnectNotify */; }; + A5B6C0F72A6EAB3200927332 /* WalletConnectNotify in Frameworks */ = {isa = PBXBuildFile; productRef = A5B6C0F62A6EAB3200927332 /* WalletConnectNotify */; }; A5BB7F9F28B69B7100707FC6 /* SignCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BB7F9E28B69B7100707FC6 /* SignCoordinator.swift */; }; A5BB7FA128B69F3400707FC6 /* AuthCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BB7FA028B69F3400707FC6 /* AuthCoordinator.swift */; }; A5BB7FA328B6A50400707FC6 /* WalletConnectAuth in Frameworks */ = {isa = PBXBuildFile; productRef = A5BB7FA228B6A50400707FC6 /* WalletConnectAuth */; }; @@ -379,11 +381,14 @@ 847BD1E2298A806800076C90 /* NotificationsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsInteractor.swift; sourceTree = ""; }; 847BD1E3298A806800076C90 /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; 847BD1EA298A87AB00076C90 /* SubscriptionsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsViewModel.swift; sourceTree = ""; }; + 847F08002A25DBFF00B2A5A4 /* XPlatformW3WTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPlatformW3WTests.swift; sourceTree = ""; }; + 8487A92E2A7BD2F30003D5AF /* XPlatformProtocolTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = XPlatformProtocolTests.xctestplan; path = ../XPlatformProtocolTests.xctestplan; sourceTree = ""; }; + 8487A9472A83AD680003D5AF /* LoggingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingService.swift; sourceTree = ""; }; + 84943C7E2A9BA48C007EBAC2 /* ProfilingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilingService.swift; sourceTree = ""; }; 849A4F18298281E300E61ACE /* WalletAppRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WalletAppRelease.entitlements; sourceTree = ""; }; 849A4F19298281F100E61ACE /* PNDecryptionServiceRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PNDecryptionServiceRelease.entitlements; sourceTree = ""; }; - 849D7A92292E2169006A2BD4 /* PushTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushTests.swift; sourceTree = ""; }; + 849D7A92292E2169006A2BD4 /* NotifyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotifyTests.swift; sourceTree = ""; }; 84A6E3C22A386BBC008A0571 /* Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Publisher.swift; sourceTree = ""; }; - 84A6E3C42A38A5A3008A0571 /* NotifyTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = NotifyTests.xctestplan; path = ../NotifyTests.xctestplan; sourceTree = ""; }; 84AA01DA28CF0CD7005D48D8 /* XCTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTest.swift; sourceTree = ""; }; 84B8154D2991099000FAD54E /* BuildConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildConfiguration.swift; sourceTree = ""; }; 84B8154F2991217900FAD54E /* PushMessagesModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushMessagesModule.swift; sourceTree = ""; }; @@ -414,11 +419,6 @@ 84E6B84729787A8000428BAF /* PNDecryptionService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PNDecryptionService.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 84E6B84929787A8000428BAF /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 84E6B84B29787A8000428BAF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 84E6B8572981624F00428BAF /* PushRequestModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushRequestModule.swift; sourceTree = ""; }; - 84E6B85A298162EF00428BAF /* PushRequestPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushRequestPresenter.swift; sourceTree = ""; }; - 84E6B85C298162F700428BAF /* PushRequestRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushRequestRouter.swift; sourceTree = ""; }; - 84E6B85E2981630000428BAF /* PushRequestInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushRequestInteractor.swift; sourceTree = ""; }; - 84E6B8602981630C00428BAF /* PushRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushRequestView.swift; sourceTree = ""; }; 84F568C1279582D200D0A289 /* Signer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signer.swift; sourceTree = ""; }; 84F568C32795832A00D0A289 /* EthereumTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthereumTransaction.swift; sourceTree = ""; }; 84FE684528ACDB4700C893FF /* RequestParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestParams.swift; sourceTree = ""; }; @@ -617,6 +617,7 @@ CF6704DE29E59DDC003326A4 /* XCUIElementQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCUIElementQuery.swift; sourceTree = ""; }; CF6704E029E5A014003326A4 /* XCTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestCase.swift; sourceTree = ""; }; CF79389D29EDD9DC00441B4F /* RelayIntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = RelayIntegrationTests.xctestplan; sourceTree = ""; }; + CFF161B82A69719F00004342 /* WalletConnect-Package.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "WalletConnect-Package.xctestplan"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -635,8 +636,11 @@ C5CFBECC2AA99D5D00378D41 /* Starscream in Frameworks */, 8448F1D427E4726F0000B866 /* WalletConnect in Frameworks */, CF25F28B2A432488009C7E49 /* WalletConnectModal in Frameworks */, + A5B6C0F12A6EAB0800927332 /* WalletConnectNotify in Frameworks */, A54195A52934E83F0035AD19 /* Web3 in Frameworks */, - 84E6B8652981720400428BAF /* WalletConnectPush in Frameworks */, + 8487A9442A836C2A0003D5AF /* Sentry in Frameworks */, + A5D85228286333E300DAF5C3 /* Starscream in Frameworks */, + 84943C7B2A9BA206007EBAC2 /* Mixpanel in Frameworks */, A573C53929EC365000E3CBFD /* HDWalletKit in Frameworks */, A5BB7FA328B6A50400707FC6 /* WalletConnectAuth in Frameworks */, ); @@ -646,7 +650,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 84E6B86329816A7900428BAF /* WalletConnectPush in Frameworks */, + A5B6C0F72A6EAB3200927332 /* WalletConnectNotify in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -678,10 +682,10 @@ A5E03DFF2864662500888481 /* WalletConnect in Frameworks */, A561C80529DFCD4500DF540D /* WalletConnectSync in Frameworks */, A50DF19D2A25084A0036EA6C /* WalletConnectHistory in Frameworks */, - 847CF3AF28E3141700F1D760 /* WalletConnectPush in Frameworks */, A5C8BE85292FE20B006CC85C /* Web3 in Frameworks */, 84DDB4ED28ABB663003D66ED /* WalletConnectAuth in Frameworks */, C5DD5BE1294E09E3008FD3A4 /* Web3Wallet in Frameworks */, + A5B6C0F32A6EAB1700927332 /* WalletConnectNotify in Frameworks */, A573C53B29EC365800E3CBFD /* HDWalletKit in Frameworks */, A5E03E01286466EA00888481 /* WalletConnectChat in Frameworks */, ); @@ -696,9 +700,12 @@ C5133A78294125CC00A8314C /* Web3 in Frameworks */, 84536D7429EEBCF0008EA8DB /* Web3Inbox in Frameworks */, C5B2F7052970573D000DBA0E /* SolanaSwift in Frameworks */, + 8487A9462A836C3F0003D5AF /* Sentry in Frameworks */, C55D349929630D440004314A /* Web3Wallet in Frameworks */, - 84E6B85429787AAE00428BAF /* WalletConnectPush in Frameworks */, + C56EE255293F569A004840D1 /* Starscream in Frameworks */, + A5B6C0F52A6EAB2800927332 /* WalletConnectNotify in Frameworks */, C56EE27B293F56F8004840D1 /* WalletConnectAuth in Frameworks */, + 84943C7D2A9BA328007EBAC2 /* Mixpanel in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -715,10 +722,11 @@ 764E1D3326F8D3FC00A1FB15 = { isa = PBXGroup; children = ( + CFF161B82A69719F00004342 /* WalletConnect-Package.xctestplan */, CF79389D29EDD9DC00441B4F /* RelayIntegrationTests.xctestplan */, 845AA7D929BA1EBA00F33739 /* IntegrationTests.xctestplan */, - 84A6E3C42A38A5A3008A0571 /* NotifyTests.xctestplan */, 845AA7DC29BB424800F33739 /* SmokeTests.xctestplan */, + 8487A92E2A7BD2F30003D5AF /* XPlatformProtocolTests.xctestplan */, A5A8E479293A1C4400FEB97D /* Shared */, A5F48A0528E43D3F0034CBFB /* Configuration.xcconfig */, 84CE6453279FFE1100142511 /* Wallet.entitlements */, @@ -830,10 +838,26 @@ path = Models; sourceTree = ""; }; + 847F07FE2A25DBC700B2A5A4 /* XPlatform */ = { + isa = PBXGroup; + children = ( + 847F07FF2A25DBDB00B2A5A4 /* Web3Wallet */, + ); + path = XPlatform; + sourceTree = ""; + }; + 847F07FF2A25DBDB00B2A5A4 /* Web3Wallet */ = { + isa = PBXGroup; + children = ( + 847F08002A25DBFF00B2A5A4 /* XPlatformW3WTests.swift */, + ); + path = Web3Wallet; + sourceTree = ""; + }; 849D7A91292E2115006A2BD4 /* Push */ = { isa = PBXGroup; children = ( - 849D7A92292E2169006A2BD4 /* PushTests.swift */, + 849D7A92292E2169006A2BD4 /* NotifyTests.swift */, 84A6E3C22A386BBC008A0571 /* Publisher.swift */, ); path = Push; @@ -949,18 +973,6 @@ path = PNDecryptionService; sourceTree = ""; }; - 84E6B8592981625A00428BAF /* PushRequest */ = { - isa = PBXGroup; - children = ( - 84E6B8572981624F00428BAF /* PushRequestModule.swift */, - 84E6B85A298162EF00428BAF /* PushRequestPresenter.swift */, - 84E6B85C298162F700428BAF /* PushRequestRouter.swift */, - 84E6B85E2981630000428BAF /* PushRequestInteractor.swift */, - 84E6B8602981630C00428BAF /* PushRequestView.swift */, - ); - path = PushRequest; - sourceTree = ""; - }; A50F3944288005A700064555 /* Types */ = { isa = PBXGroup; children = ( @@ -1410,6 +1422,7 @@ A5E03DEE286464DB00888481 /* IntegrationTests */ = { isa = PBXGroup; children = ( + 847F07FE2A25DBC700B2A5A4 /* XPlatform */, A5321C292A25035A006CADC3 /* History */, A561C80129DFCCD300DF540D /* Sync */, 849D7A91292E2115006A2BD4 /* Push */, @@ -1543,7 +1556,6 @@ C5F32A2A2954812900A6476E /* ConnectionDetails */, C56EE236293F566A004840D1 /* Scan */, C56EE22A293F5668004840D1 /* Wallet */, - 84E6B8592981625A00428BAF /* PushRequest */, 847BD1E9298A807000076C90 /* Notifications */, 84B815592991217F00FAD54E /* PushMessages */, ); @@ -1652,6 +1664,8 @@ C56EE281293F5757004840D1 /* SceneDelegate.swift */, 84DB38F22983CDAE00BFEE37 /* PushRegisterer.swift */, A51811972A52E21A00A52B15 /* ConfigurationService.swift */, + 8487A9472A83AD680003D5AF /* LoggingService.swift */, + 84943C7E2A9BA48C007EBAC2 /* ProfilingService.swift */, ); path = ApplicationLayer; sourceTree = ""; @@ -1800,10 +1814,12 @@ 8448F1D327E4726F0000B866 /* WalletConnect */, A5BB7FA228B6A50400707FC6 /* WalletConnectAuth */, A54195A42934E83F0035AD19 /* Web3 */, - 84E6B8642981720400428BAF /* WalletConnectPush */, A573C53829EC365000E3CBFD /* HDWalletKit */, CF25F28A2A432488009C7E49 /* WalletConnectModal */, C5CFBECB2AA99D5D00378D41 /* Starscream */, + 8487A9432A836C2A0003D5AF /* Sentry */, + A5B6C0F02A6EAB0800927332 /* WalletConnectNotify */, + 84943C7A2A9BA206007EBAC2 /* Mixpanel */, ); productName = DApp; productReference = 84CE641C27981DED00142511 /* DApp.app */; @@ -1823,7 +1839,7 @@ ); name = PNDecryptionService; packageProductDependencies = ( - 84E6B86229816A7900428BAF /* WalletConnectPush */, + A5B6C0F62A6EAB3200927332 /* WalletConnectNotify */, ); productName = PNDecryptionService; productReference = 84E6B84729787A8000428BAF /* PNDecryptionService.appex */; @@ -1890,12 +1906,12 @@ A5E03DFE2864662500888481 /* WalletConnect */, A5E03E00286466EA00888481 /* WalletConnectChat */, 84DDB4EC28ABB663003D66ED /* WalletConnectAuth */, - 847CF3AE28E3141700F1D760 /* WalletConnectPush */, A5C8BE84292FE20B006CC85C /* Web3 */, C5DD5BE0294E09E3008FD3A4 /* Web3Wallet */, A561C80429DFCD4500DF540D /* WalletConnectSync */, A573C53A29EC365800E3CBFD /* HDWalletKit */, A50DF19C2A25084A0036EA6C /* WalletConnectHistory */, + A5B6C0F22A6EAB1700927332 /* WalletConnectNotify */, ); productName = IntegrationTests; productReference = A5E03DED286464DB00888481 /* IntegrationTests.xctest */; @@ -1922,9 +1938,11 @@ C5133A77294125CC00A8314C /* Web3 */, C55D349829630D440004314A /* Web3Wallet */, C5B2F7042970573D000DBA0E /* SolanaSwift */, - 84E6B85329787AAE00428BAF /* WalletConnectPush */, 84536D7329EEBCF0008EA8DB /* Web3Inbox */, A573C53C29EC366500E3CBFD /* HDWalletKit */, + 8487A9452A836C3F0003D5AF /* Sentry */, + A5B6C0F42A6EAB2800927332 /* WalletConnectNotify */, + 84943C7C2A9BA328007EBAC2 /* Mixpanel */, ); productName = ChatWallet; productReference = C56EE21B293F55ED004840D1 /* WalletApp.app */; @@ -2000,6 +2018,8 @@ A58EC60F299D57B800F3452A /* XCRemoteSwiftPackageReference "swiftui-async-button" */, A561C7FE29DF32CE00DF540D /* XCRemoteSwiftPackageReference "HDWallet" */, C5CFBECA2AA99D5D00378D41 /* XCRemoteSwiftPackageReference "Starscream" */, + 8487A9422A836C2A0003D5AF /* XCRemoteSwiftPackageReference "sentry-cocoa" */, + 84943C792A9BA206007EBAC2 /* XCRemoteSwiftPackageReference "mixpanel-swift" */, ); productRefGroup = 764E1D3D26F8D3FC00A1FB15 /* Products */; projectDirPath = ""; @@ -2237,8 +2257,9 @@ A518B31428E33A6500A2CE93 /* InputConfig.swift in Sources */, A541959E2934BFEF0035AD19 /* CacaoSignerTests.swift in Sources */, A59CF4F6292F83D50031A42F /* DefaultSignerFactory.swift in Sources */, + 847F08012A25DBFF00B2A5A4 /* XPlatformW3WTests.swift in Sources */, A5E03E03286466F400888481 /* ChatTests.swift in Sources */, - 849D7A93292E2169006A2BD4 /* PushTests.swift in Sources */, + 849D7A93292E2169006A2BD4 /* NotifyTests.swift in Sources */, 845B8D8C2934B36C0084A966 /* Account.swift in Sources */, 84D2A66628A4F51E0088AE09 /* AuthTests.swift in Sources */, 84FE684628ACDB4700C893FF /* RequestParams.swift in Sources */, @@ -2263,9 +2284,7 @@ buildActionMask = 2147483647; files = ( C53AA4362941251C008EA57C /* DefaultSignerFactory.swift in Sources */, - 84E6B8582981624F00428BAF /* PushRequestModule.swift in Sources */, C55D3480295DD7140004314A /* AuthRequestPresenter.swift in Sources */, - 84E6B85B298162EF00428BAF /* PushRequestPresenter.swift in Sources */, A51811A02A52E83100A52B15 /* SettingsPresenter.swift in Sources */, 847BD1DA2989492500076C90 /* MainRouter.swift in Sources */, C5F32A2E2954814A00A6476E /* ConnectionDetailsRouter.swift in Sources */, @@ -2316,8 +2335,6 @@ A51811A12A52E83100A52B15 /* SettingsRouter.swift in Sources */, C56EE279293F56D7004840D1 /* Color.swift in Sources */, 847BD1E6298A806800076C90 /* NotificationsRouter.swift in Sources */, - 84E6B8612981630C00428BAF /* PushRequestView.swift in Sources */, - 84E6B85F2981630000428BAF /* PushRequestInteractor.swift in Sources */, C55D3483295DD7140004314A /* AuthRequestView.swift in Sources */, C56EE243293F566D004840D1 /* ScanView.swift in Sources */, 84310D05298BC980000C15B6 /* MainInteractor.swift in Sources */, @@ -2350,13 +2367,14 @@ C5B2F6F929705293000DBA0E /* SessionRequestPresenter.swift in Sources */, A57879712A4EDC8100F8D10B /* TextFieldView.swift in Sources */, 84DB38F32983CDAE00BFEE37 /* PushRegisterer.swift in Sources */, + 84943C7F2A9BA48C007EBAC2 /* ProfilingService.swift in Sources */, C5B2F6FB297055B0000DBA0E /* ETHSigner.swift in Sources */, C56EE274293F56D7004840D1 /* SceneViewController.swift in Sources */, 847BD1E5298A806800076C90 /* NotificationsPresenter.swift in Sources */, C55D3496295DFA750004314A /* WelcomeInteractor.swift in Sources */, - 84E6B85D298162F700428BAF /* PushRequestRouter.swift in Sources */, C5B2F6FC297055B0000DBA0E /* SOLSigner.swift in Sources */, A518119F2A52E83100A52B15 /* SettingsModule.swift in Sources */, + 8487A9482A83AD680003D5AF /* LoggingService.swift in Sources */, C55D348D295DD8CA0004314A /* PasteUriView.swift in Sources */, C5F32A2C2954814200A6476E /* ConnectionDetailsModule.swift in Sources */, C56EE249293F566D004840D1 /* ScanInteractor.swift in Sources */, @@ -3073,6 +3091,22 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 8487A9422A836C2A0003D5AF /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/getsentry/sentry-cocoa.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 8.0.0; + }; + }; + 84943C792A9BA206007EBAC2 /* XCRemoteSwiftPackageReference "mixpanel-swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/mixpanel/mixpanel-swift"; + requirement = { + branch = master; + kind = branch; + }; + }; A5434021291E6A270068F706 /* XCRemoteSwiftPackageReference "solana-swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/flypaper0/solana-swift"; @@ -3128,25 +3162,29 @@ isa = XCSwiftPackageProductDependency; productName = Web3Inbox; }; - 847CF3AE28E3141700F1D760 /* WalletConnectPush */ = { + 8487A9432A836C2A0003D5AF /* Sentry */ = { isa = XCSwiftPackageProductDependency; - productName = WalletConnectPush; + package = 8487A9422A836C2A0003D5AF /* XCRemoteSwiftPackageReference "sentry-cocoa" */; + productName = Sentry; }; - 84DDB4EC28ABB663003D66ED /* WalletConnectAuth */ = { + 8487A9452A836C3F0003D5AF /* Sentry */ = { isa = XCSwiftPackageProductDependency; - productName = WalletConnectAuth; + package = 8487A9422A836C2A0003D5AF /* XCRemoteSwiftPackageReference "sentry-cocoa" */; + productName = Sentry; }; - 84E6B85329787AAE00428BAF /* WalletConnectPush */ = { + 84943C7A2A9BA206007EBAC2 /* Mixpanel */ = { isa = XCSwiftPackageProductDependency; - productName = WalletConnectPush; + package = 84943C792A9BA206007EBAC2 /* XCRemoteSwiftPackageReference "mixpanel-swift" */; + productName = Mixpanel; }; - 84E6B86229816A7900428BAF /* WalletConnectPush */ = { + 84943C7C2A9BA328007EBAC2 /* Mixpanel */ = { isa = XCSwiftPackageProductDependency; - productName = WalletConnectPush; + package = 84943C792A9BA206007EBAC2 /* XCRemoteSwiftPackageReference "mixpanel-swift" */; + productName = Mixpanel; }; - 84E6B8642981720400428BAF /* WalletConnectPush */ = { + 84DDB4EC28ABB663003D66ED /* WalletConnectAuth */ = { isa = XCSwiftPackageProductDependency; - productName = WalletConnectPush; + productName = WalletConnectAuth; }; A50DF19C2A25084A0036EA6C /* WalletConnectHistory */ = { isa = XCSwiftPackageProductDependency; @@ -3203,6 +3241,22 @@ package = A5AE354528A1A2AC0059AE8A /* XCRemoteSwiftPackageReference "Web3" */; productName = Web3; }; + A5B6C0F02A6EAB0800927332 /* WalletConnectNotify */ = { + isa = XCSwiftPackageProductDependency; + productName = WalletConnectNotify; + }; + A5B6C0F22A6EAB1700927332 /* WalletConnectNotify */ = { + isa = XCSwiftPackageProductDependency; + productName = WalletConnectNotify; + }; + A5B6C0F42A6EAB2800927332 /* WalletConnectNotify */ = { + isa = XCSwiftPackageProductDependency; + productName = WalletConnectNotify; + }; + A5B6C0F62A6EAB3200927332 /* WalletConnectNotify */ = { + isa = XCSwiftPackageProductDependency; + productName = WalletConnectNotify; + }; A5BB7FA228B6A50400707FC6 /* WalletConnectAuth */ = { isa = XCSwiftPackageProductDependency; productName = WalletConnectAuth; diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 53171f58a..d4ad48aad 100644 --- a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -28,6 +28,15 @@ "version": null } }, + { + "package": "Mixpanel", + "repositoryURL": "https://github.com/mixpanel/mixpanel-swift", + "state": { + "branch": "master", + "revision": "1ce27d937009d5ecce74dad97d69898ffea49c75", + "version": null + } + }, { "package": "PromiseKit", "repositoryURL": "https://github.com/mxcl/PromiseKit.git", @@ -55,6 +64,15 @@ "version": "0.1.7" } }, + { + "package": "Sentry", + "repositoryURL": "https://github.com/getsentry/sentry-cocoa.git", + "state": { + "branch": null, + "revision": "12998398eb51e2e8ff7098163fa97d305eee6d87", + "version": "8.11.0" + } + }, { "package": "SolanaSwift", "repositoryURL": "https://github.com/flypaper0/solana-swift", diff --git a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/BuildAll.xcscheme b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/BuildAll.xcscheme index 9bdb81b02..6b4a6df22 100644 --- a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/BuildAll.xcscheme +++ b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/BuildAll.xcscheme @@ -83,6 +83,9 @@ + + + + + + + + diff --git a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnect.xcscheme b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnect.xcscheme index aa58da974..4f51ebe1b 100644 --- a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnect.xcscheme +++ b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnect.xcscheme @@ -288,6 +288,16 @@ ReferencedContainer = "container:.."> + + + + (PairingClient, AuthClient) { - let logger = ConsoleLogger(suffix: prefix, loggingLevel: .debug) + let logger = ConsoleLogger(prefix: prefix, loggingLevel: .debug) let keyValueStorage = RuntimeKeyValueStorage() let keychain = KeychainStorageMock() let relayClient = RelayClientFactory.create(relayHost: InputConfig.relayHost, projectId: InputConfig.projectId, keyValueStorage: RuntimeKeyValueStorage(), keychainStorage: keychain, logger: logger) @@ -70,7 +70,7 @@ final class AuthTests: XCTestCase { let uri = try! await appPairingClient.create() try! await appAuthClient.request(RequestParams.stub(), topic: uri.topic) - try! await walletPairingClient.pair(uri: uri) + try? await walletPairingClient.pair(uri: uri) walletAuthClient.authRequestPublisher.sink { _ in requestExpectation.fulfill() }.store(in: &publishers) @@ -82,7 +82,7 @@ final class AuthTests: XCTestCase { let uri = try! await appPairingClient.create() try! await appAuthClient.request(RequestParams.stub(), topic: uri.topic) - try! await walletPairingClient.pair(uri: uri) + try? await walletPairingClient.pair(uri: uri) walletAuthClient.authRequestPublisher.sink { [unowned self] request in Task(priority: .high) { let signerFactory = DefaultSignerFactory() @@ -120,7 +120,7 @@ final class AuthTests: XCTestCase { resources: nil ), topic: uri.topic) - try! await walletPairingClient.pair(uri: uri) + try? await walletPairingClient.pair(uri: uri) walletAuthClient.authRequestPublisher.sink { [unowned self] request in Task(priority: .high) { let signature = CacaoSignature(t: .eip1271, s: eip1271Signature) @@ -141,7 +141,7 @@ final class AuthTests: XCTestCase { let uri = try! await appPairingClient.create() try! await appAuthClient.request(RequestParams.stub(), topic: uri.topic) - try! await walletPairingClient.pair(uri: uri) + try? await walletPairingClient.pair(uri: uri) walletAuthClient.authRequestPublisher.sink { [unowned self] request in Task(priority: .high) { let signature = CacaoSignature(t: .eip1271, s: eip1271Signature) @@ -162,7 +162,7 @@ final class AuthTests: XCTestCase { let uri = try! await appPairingClient.create() try! await appAuthClient.request(RequestParams.stub(), topic: uri.topic) - try! await walletPairingClient.pair(uri: uri) + try? await walletPairingClient.pair(uri: uri) walletAuthClient.authRequestPublisher.sink { [unowned self] request in Task(priority: .high) { try! await walletAuthClient.reject(requestId: request.0.id) @@ -183,7 +183,7 @@ final class AuthTests: XCTestCase { let uri = try! await appPairingClient.create() try! await appAuthClient.request(RequestParams.stub(), topic: uri.topic) - try! await walletPairingClient.pair(uri: uri) + try? await walletPairingClient.pair(uri: uri) walletAuthClient.authRequestPublisher.sink { [unowned self] request in Task(priority: .high) { let invalidSignature = "438effc459956b57fcd9f3dac6c675f9cee88abf21acab7305e8e32aa0303a883b06dcbd956279a7a2ca21ffa882ff55cc22e8ab8ec0f3fe90ab45f306938cfa1b" @@ -201,9 +201,3 @@ final class AuthTests: XCTestCase { wait(for: [responseExpectation], timeout: InputConfig.defaultTimeout) } } - -private struct IATProviderMock: IATProvider { - var iat: String { - return "2022-10-10T23:03:35.700Z" - } -} diff --git a/Example/IntegrationTests/Auth/ENS/ENSResolverTests.swift b/Example/IntegrationTests/Auth/ENS/ENSResolverTests.swift index 8ea3661e8..6cced3613 100644 --- a/Example/IntegrationTests/Auth/ENS/ENSResolverTests.swift +++ b/Example/IntegrationTests/Auth/ENS/ENSResolverTests.swift @@ -7,16 +7,16 @@ class ENSResolverTests: XCTestCase { private let account = Account("eip155:1:0xD02D090F8f99B61D65d8e8876Ea86c2720aB27BC")! private let ens = "web3.eth" -// Note: - removed until RPC server fix -// func testResolveEns() async throws { -// let resolver = ENSResolverFactory(crypto: DefaultCryptoProvider()).create(projectId: InputConfig.projectId) -// let resolved = try await resolver.resolveEns(account: account) -// XCTAssertEqual(resolved, ens) -// } -// -// func testResolveAddress() async throws { -// let resolver = ENSResolverFactory(crypto: DefaultCryptoProvider()).create(projectId: InputConfig.projectId) -// let resolved = try await resolver.resolveAddress(ens: ens, blockchain: account.blockchain) -// XCTAssertEqual(resolved, account) -// } + // Note: - removed until RPC server fix + // func testResolveEns() async throws { + // let resolver = ENSResolverFactory(crypto: DefaultCryptoProvider()).create(projectId: InputConfig.projectId) + // let resolved = try await resolver.resolveEns(account: account) + // XCTAssertEqual(resolved, ens) + // } + // + // func testResolveAddress() async throws { + // let resolver = ENSResolverFactory(crypto: DefaultCryptoProvider()).create(projectId: InputConfig.projectId) + // let resolved = try await resolver.resolveAddress(ens: ens, blockchain: account.blockchain) + // XCTAssertEqual(resolved, account) + // } } diff --git a/Example/IntegrationTests/Chat/ChatTests.swift b/Example/IntegrationTests/Chat/ChatTests.swift index 7c69c508e..d4817e619 100644 --- a/Example/IntegrationTests/Chat/ChatTests.swift +++ b/Example/IntegrationTests/Chat/ChatTests.swift @@ -48,7 +48,7 @@ final class ChatTests: XCTestCase { func makeClient(prefix: String, account: Account) -> ChatClient { let keyserverURL = URL(string: "https://keys.walletconnect.com")! - let logger = ConsoleLogger(suffix: prefix, loggingLevel: .debug) + let logger = ConsoleLogger(prefix: prefix, loggingLevel: .debug) let keyValueStorage = RuntimeKeyValueStorage() let keychain = KeychainStorageMock() let relayClient = RelayClientFactory.create(relayHost: InputConfig.relayHost, projectId: InputConfig.projectId, keyValueStorage: RuntimeKeyValueStorage(), keychainStorage: keychain, logger: logger) diff --git a/Example/IntegrationTests/Chat/RegistryTests.swift b/Example/IntegrationTests/Chat/RegistryTests.swift index 074251e63..a6de83f9e 100644 --- a/Example/IntegrationTests/Chat/RegistryTests.swift +++ b/Example/IntegrationTests/Chat/RegistryTests.swift @@ -32,27 +32,27 @@ final class RegistryTests: XCTestCase { signer = MessageSignerFactory(signerFactory: DefaultSignerFactory()).create(projectId: InputConfig.projectId) } - func testRegisterIdentityAndInviteKey() async throws { - let publicKey = try await sut.registerIdentity(account: account, onSign: onSign) - - let iss = DIDKey(rawData: Data(hex: publicKey)).did(variant: .ED25519) - let resolvedAccount = try await sut.resolveIdentity(iss: iss) - XCTAssertEqual(resolvedAccount, account) - - let recovered = try storage.getIdentityKey(for: account).publicKey.hexRepresentation - XCTAssertEqual(publicKey, recovered) - - let inviteKey = try await sut.registerInvite(account: account) - - let recoveredKey = try storage.getInviteKey(for: account) - XCTAssertEqual(inviteKey, recoveredKey) - - let resolvedKey = try await sut.resolveInvite(account: account) - XCTAssertEqual(inviteKey.did, resolvedKey) - - _ = try await sut.goPrivate(account: account) - try await sut.unregister(account: account, onSign: onSign) - } +// func testRegisterIdentityAndInviteKey() async throws { +// let publicKey = try await sut.registerIdentity(account: account, onSign: onSign) +// +// let iss = DIDKey(rawData: Data(hex: publicKey)).did(variant: .ED25519) +// let resolvedAccount = try await sut.resolveIdentity(iss: iss) +// XCTAssertEqual(resolvedAccount, account) +// +// let recovered = try storage.getIdentityKey(for: account).publicKey.hexRepresentation +// XCTAssertEqual(publicKey, recovered) +// +// let inviteKey = try await sut.registerInvite(account: account) +// +// let recoveredKey = try storage.getInviteKey(for: account) +// XCTAssertEqual(inviteKey, recoveredKey) +// +// let resolvedKey = try await sut.resolveInvite(account: account) +// XCTAssertEqual(inviteKey.did, resolvedKey) +// +// _ = try await sut.goPrivate(account: account) +// try await sut.unregister(account: account, onSign: onSign) +// } } private extension RegistryTests { diff --git a/Example/IntegrationTests/History/HistoryTests.swift b/Example/IntegrationTests/History/HistoryTests.swift index 00c0a01f6..40d52f5c0 100644 --- a/Example/IntegrationTests/History/HistoryTests.swift +++ b/Example/IntegrationTests/History/HistoryTests.swift @@ -29,7 +29,8 @@ final class HistoryTests: XCTestCase { projectId: InputConfig.projectId, keyValueStorage: RuntimeKeyValueStorage(), keychainStorage: keychain, - logger: ConsoleLogger(suffix: prefix + " [Relay]", loggingLevel: .debug)) + socketFactory: DefaultSocketFactory(), + logger: ConsoleLogger(prefix: prefix + " [Relay]", loggingLevel: .debug)) } private func makeHistoryClient(keychain: KeychainStorageProtocol) -> HistoryNetworkService { diff --git a/Example/IntegrationTests/Pairing/PairingTests.swift b/Example/IntegrationTests/Pairing/PairingTests.swift index 502cb5a44..0c9e8f523 100644 --- a/Example/IntegrationTests/Pairing/PairingTests.swift +++ b/Example/IntegrationTests/Pairing/PairingTests.swift @@ -1,12 +1,12 @@ import Foundation import XCTest -import WalletConnectUtils +@testable import WalletConnectUtils @testable import WalletConnectKMS import WalletConnectRelay import Combine import WalletConnectNetworking -import WalletConnectEcho -@testable import WalletConnectPush +import WalletConnectPush +@testable import Auth @testable import WalletConnectPairing @testable import WalletConnectSync @testable import WalletConnectHistory @@ -16,20 +16,20 @@ final class PairingTests: XCTestCase { var appPairingClient: PairingClient! var walletPairingClient: PairingClient! - var appPushClient: DappPushClient! - var walletPushClient: WalletPushClient! + var appAuthClient: AuthClient! + var walletAuthClient: AuthClient! var pairingStorage: PairingStorage! private var publishers = [AnyCancellable]() - func makeClientDependencies(prefix: String) -> (PairingClient, NetworkInteracting, SyncClient, KeychainStorageProtocol, KeyValueStorage) { + func makeClientDependencies(prefix: String) -> (PairingClient, NetworkingInteractor, KeychainStorageProtocol, KeyValueStorage) { let keychain = KeychainStorageMock() let keyValueStorage = RuntimeKeyValueStorage() - let relayLogger = ConsoleLogger(suffix: prefix + " [Relay]", loggingLevel: .debug) - let pairingLogger = ConsoleLogger(suffix: prefix + " [Pairing]", loggingLevel: .debug) - let networkingLogger = ConsoleLogger(suffix: prefix + " [Networking]", loggingLevel: .debug) + let relayLogger = ConsoleLogger(prefix: prefix + " [Relay]", loggingLevel: .debug) + let pairingLogger = ConsoleLogger(prefix: prefix + " [Pairing]", loggingLevel: .debug) + let networkingLogger = ConsoleLogger(prefix: prefix + " [Networking]", loggingLevel: .debug) let relayClient = RelayClientFactory.create( relayHost: InputConfig.relayHost, @@ -49,59 +49,55 @@ final class PairingTests: XCTestCase { keyValueStorage: keyValueStorage, keychainStorage: keychain, networkingClient: networkingClient) - - let syncClient = SyncClientFactory.create(networkInteractor: networkingClient, bip44: DefaultBIP44Provider(), keychain: keychain) - let clientId = try! networkingClient.getClientId() networkingLogger.debug("My client id is: \(clientId)") - return (pairingClient, networkingClient, syncClient, keychain, keyValueStorage) + return (pairingClient, networkingClient, keychain, keyValueStorage) } func makeDappClients() { let prefix = "🤖 Dapp: " - let (pairingClient, networkingInteractor, syncClient, keychain, keyValueStorage) = makeClientDependencies(prefix: prefix) - let pushLogger = ConsoleLogger(suffix: prefix + " [Push]", loggingLevel: .debug) + let (pairingClient, networkingInteractor, keychain, keyValueStorage) = makeClientDependencies(prefix: prefix) + let notifyLogger = ConsoleLogger(prefix: prefix + " [Notify]", loggingLevel: .debug) appPairingClient = pairingClient - appPushClient = DappPushClientFactory.create(metadata: AppMetadata(name: name, description: "", url: "", icons: [""]), - logger: pushLogger, - keyValueStorage: keyValueStorage, - keychainStorage: keychain, - groupKeychainStorage: KeychainStorageMock(), - networkInteractor: networkingInteractor, - syncClient: syncClient) + + appAuthClient = AuthClientFactory.create( + metadata: AppMetadata(name: name, description: "", url: "", icons: [""]), + projectId: InputConfig.projectId, + crypto: DefaultCryptoProvider(), + logger: notifyLogger, + keyValueStorage: keyValueStorage, + keychainStorage: keychain, + networkingClient: networkingInteractor, + pairingRegisterer: pairingClient, + iatProvider: IATProviderMock()) } func makeWalletClients() { let prefix = "🐶 Wallet: " - let (pairingClient, networkingInteractor, syncClient, keychain, keyValueStorage) = makeClientDependencies(prefix: prefix) - let pushLogger = ConsoleLogger(suffix: prefix + " [Push]", loggingLevel: .debug) + let (pairingClient, networkingInteractor, keychain, keyValueStorage) = makeClientDependencies(prefix: prefix) + let notifyLogger = ConsoleLogger(prefix: prefix + " [Notify]", loggingLevel: .debug) walletPairingClient = pairingClient - let echoClient = EchoClientFactory.create(projectId: "", - echoHost: "echo.walletconnect.com", - keychainStorage: keychain, - environment: .sandbox) - let keyserverURL = URL(string: "https://keys.walletconnect.com")! let historyClient = HistoryClientFactory.create( historyUrl: "https://history.walletconnect.com", relayUrl: "wss://relay.walletconnect.com", keychain: keychain ) - walletPushClient = WalletPushClientFactory.create(keyserverURL: keyserverURL, - logger: pushLogger, - keyValueStorage: keyValueStorage, - keychainStorage: keychain, - groupKeychainStorage: KeychainStorageMock(), - networkInteractor: networkingInteractor, - pairingRegisterer: pairingClient, - echoClient: echoClient, - syncClient: syncClient, - historyClient: historyClient) + appAuthClient = AuthClientFactory.create( + metadata: AppMetadata(name: name, description: "", url: "", icons: [""]), + projectId: InputConfig.projectId, + crypto: DefaultCryptoProvider(), + logger: notifyLogger, + keyValueStorage: keyValueStorage, + keychainStorage: keychain, + networkingClient: networkingInteractor, + pairingRegisterer: pairingClient, + iatProvider: IATProviderMock()) } func makeWalletPairingClient() { let prefix = "🐶 Wallet: " - let (pairingClient, _, _, _, _) = makeClientDependencies(prefix: prefix) + let (pairingClient, _, _, _) = makeClientDependencies(prefix: prefix) walletPairingClient = pairingClient } @@ -109,28 +105,11 @@ final class PairingTests: XCTestCase { makeDappClients() } - func testProposePushOnPairing() async { - makeWalletClients() - let expectation = expectation(description: "propose push on pairing") - - walletPushClient.requestPublisher.sink { _ in - expectation.fulfill() - }.store(in: &publishers) - - let uri = try! await appPairingClient.create() - - try! await walletPairingClient.pair(uri: uri) - - try! await appPushClient.propose(account: Account.stub(), topic: uri.topic) - - wait(for: [expectation], timeout: InputConfig.defaultTimeout) - } - func testPing() async { let expectation = expectation(description: "expects ping response") makeWalletClients() let uri = try! await appPairingClient.create() - try! await walletPairingClient.pair(uri: uri) + try? await walletPairingClient.pair(uri: uri) try! await walletPairingClient.ping(topic: uri.topic) walletPairingClient.pingResponsePublisher .sink { topic in @@ -144,16 +123,16 @@ final class PairingTests: XCTestCase { makeWalletPairingClient() let expectation = expectation(description: "wallet responds unsupported method for unregistered method") - appPushClient.proposalResponsePublisher.sink { (response) in - XCTAssertEqual(response, .failure(PushError(code: 10001)!)) + appAuthClient.authResponsePublisher.sink { (_, response) in + XCTAssertEqual(response, .failure(AuthError(code: 10001)!)) expectation.fulfill() }.store(in: &publishers) let uri = try! await appPairingClient.create() - try! await walletPairingClient.pair(uri: uri) + try? await walletPairingClient.pair(uri: uri) - try! await appPushClient.propose(account: Account.stub(), topic: uri.topic) + try! await appAuthClient.request(RequestParams.stub(), topic: uri.topic) wait(for: [expectation], timeout: InputConfig.defaultTimeout) } diff --git a/Example/IntegrationTests/Push/NotifyTests.swift b/Example/IntegrationTests/Push/NotifyTests.swift new file mode 100644 index 000000000..965c56f5a --- /dev/null +++ b/Example/IntegrationTests/Push/NotifyTests.swift @@ -0,0 +1,176 @@ +import Foundation +import XCTest +import WalletConnectUtils +import Web3 +@testable import WalletConnectKMS +import WalletConnectRelay +import Combine +import WalletConnectNetworking +import WalletConnectPush +@testable import WalletConnectNotify +@testable import WalletConnectPairing +import WalletConnectIdentity +import WalletConnectSigner + +final class NotifyTests: XCTestCase { + + var walletPairingClient: PairingClient! + + var walletNotifyClient: NotifyClient! + + let gmDappUrl = "https://notify.gm.walletconnect.com/" + + let pk = try! EthereumPrivateKey() + + var privateKey: Data { + return Data(pk.rawPrivateKey) + } + + var account: Account { + return Account("eip155:1:" + pk.address.hex(eip55: true))! + } + + private var publishers = Set() + + func makeClientDependencies(prefix: String) -> (PairingClient, NetworkInteracting, KeychainStorageProtocol, KeyValueStorage) { + let keychain = KeychainStorageMock() + let keyValueStorage = RuntimeKeyValueStorage() + + let relayLogger = ConsoleLogger(prefix: prefix + " [Relay]", loggingLevel: .debug) + let pairingLogger = ConsoleLogger(prefix: prefix + " [Pairing]", loggingLevel: .debug) + let networkingLogger = ConsoleLogger(prefix: prefix + " [Networking]", loggingLevel: .debug) + + let relayClient = RelayClientFactory.create( + relayHost: InputConfig.relayHost, + projectId: InputConfig.projectId, + keyValueStorage: keyValueStorage, + keychainStorage: keychain, + socketFactory: DefaultSocketFactory(), + logger: relayLogger) + + let networkingClient = NetworkingClientFactory.create( + relayClient: relayClient, + logger: networkingLogger, + keychainStorage: keychain, + keyValueStorage: keyValueStorage) + + let pairingClient = PairingClientFactory.create( + logger: pairingLogger, + keyValueStorage: keyValueStorage, + keychainStorage: keychain, + networkingClient: networkingClient) + + let clientId = try! networkingClient.getClientId() + networkingLogger.debug("My client id is: \(clientId)") + return (pairingClient, networkingClient, keychain, keyValueStorage) + } + + func makeWalletClients() { + let prefix = "🦋 Wallet: " + let (pairingClient, networkingInteractor, keychain, keyValueStorage) = makeClientDependencies(prefix: prefix) + let notifyLogger = ConsoleLogger(prefix: prefix + " [Notify]", loggingLevel: .debug) + walletPairingClient = pairingClient + let pushClient = PushClientFactory.create(projectId: "", + pushHost: "echo.walletconnect.com", + keychainStorage: keychain, + environment: .sandbox) + let keyserverURL = URL(string: "https://keys.walletconnect.com")! + walletNotifyClient = NotifyClientFactory.create(keyserverURL: keyserverURL, + logger: notifyLogger, + keyValueStorage: keyValueStorage, + keychainStorage: keychain, + groupKeychainStorage: KeychainStorageMock(), + networkInteractor: networkingInteractor, + pairingRegisterer: pairingClient, + pushClient: pushClient, + crypto: DefaultCryptoProvider()) + } + + override func setUp() { + makeWalletClients() + } + + func testWalletCreatesSubscription() async { + let expectation = expectation(description: "expects to create notify subscription") + let metadata = AppMetadata(name: "GM Dapp", description: "", url: gmDappUrl, icons: []) + + walletNotifyClient.newSubscriptionPublisher + .sink { [unowned self] subscription in + Task(priority: .high) { + try! await walletNotifyClient.deleteSubscription(topic: subscription.topic) + expectation.fulfill() + } + }.store(in: &publishers) + + try! await walletNotifyClient.register(account: account, onSign: sign) + try! await walletNotifyClient.subscribe(metadata: metadata, account: account, onSign: sign) + + wait(for: [expectation], timeout: InputConfig.defaultTimeout) + } + + func testWalletCreatesAndUpdatesSubscription() async { + let expectation = expectation(description: "expects to create and update notify subscription") + let metadata = AppMetadata(name: "GM Dapp", description: "", url: gmDappUrl, icons: []) + let updateScope: Set = ["alerts"] + try! await walletNotifyClient.register(account: account, onSign: sign) + try! await walletNotifyClient.subscribe(metadata: metadata, account: account, onSign: sign) + walletNotifyClient.newSubscriptionPublisher + .sink { [unowned self] subscription in + Task(priority: .high) { + try! await walletNotifyClient.update(topic: subscription.topic, scope: updateScope) + } + } + .store(in: &publishers) + + walletNotifyClient.updateSubscriptionPublisher + .sink { [unowned self] subscription in + let updatedScope = Set(subscription.scope.filter{ $0.value.enabled == true }.keys) + XCTAssertEqual(updatedScope, updateScope) + Task(priority: .high) { + try! await walletNotifyClient.deleteSubscription(topic: subscription.topic) + expectation.fulfill() + } + }.store(in: &publishers) + + wait(for: [expectation], timeout: InputConfig.defaultTimeout) + } + + func testNotifyServerSubscribeAndNotifies() async throws { + let subscribeExpectation = expectation(description: "creates notify subscription") + let messageExpectation = expectation(description: "receives a notify message") + let notifyMessage = NotifyMessage.stub() + + let metadata = AppMetadata(name: "GM Dapp", description: "", url: gmDappUrl, icons: []) + try! await walletNotifyClient.register(account: account, onSign: sign) + try! await walletNotifyClient.subscribe(metadata: metadata, account: account, onSign: sign) + + walletNotifyClient.newSubscriptionPublisher + .sink { subscription in + let notifier = Publisher() + Task(priority: .high) { + try await notifier.notify(topic: subscription.topic, account: subscription.account, message: notifyMessage) + subscribeExpectation.fulfill() + } + }.store(in: &publishers) + + walletNotifyClient.notifyMessagePublisher + .sink { [unowned self] notifyMessageRecord in + XCTAssertEqual(notifyMessage, notifyMessageRecord.message) + + Task(priority: .high) { + try await walletNotifyClient.deleteSubscription(topic: notifyMessageRecord.topic) + messageExpectation.fulfill() + } + }.store(in: &publishers) + + wait(for: [subscribeExpectation, messageExpectation], timeout: InputConfig.defaultTimeout) + } +} + + +private extension NotifyTests { + func sign(_ message: String) -> SigningResult { + let signer = MessageSignerFactory(signerFactory: DefaultSignerFactory()).create(projectId: InputConfig.projectId) + return .signed(try! signer.sign(message: message, privateKey: privateKey, type: .eip191)) + } +} diff --git a/Example/IntegrationTests/Push/Publisher.swift b/Example/IntegrationTests/Push/Publisher.swift index 50667f1ce..5ee993759 100644 --- a/Example/IntegrationTests/Push/Publisher.swift +++ b/Example/IntegrationTests/Push/Publisher.swift @@ -1,8 +1,8 @@ -@testable import WalletConnectPush +@testable import WalletConnectNotify import Foundation class Publisher { - func notify(topic: String, account: Account, message: PushMessage) async throws { + func notify(topic: String, account: Account, message: NotifyMessage) async throws { let url = URL(string: "https://\(InputConfig.castHost)/\(InputConfig.gmDappProjectId)/notify")! var request = URLRequest(url: url) let notifyRequestPayload = NotifyRequest(notification: message, accounts: [account]) @@ -19,6 +19,6 @@ class Publisher { } struct NotifyRequest: Codable { - let notification: PushMessage + let notification: NotifyMessage let accounts: [Account] } diff --git a/Example/IntegrationTests/Push/PushTests.swift b/Example/IntegrationTests/Push/PushTests.swift deleted file mode 100644 index d9a43acf7..000000000 --- a/Example/IntegrationTests/Push/PushTests.swift +++ /dev/null @@ -1,271 +0,0 @@ -import Foundation -import XCTest -import WalletConnectUtils -import Web3 -@testable import WalletConnectKMS -import WalletConnectRelay -import Combine -import WalletConnectNetworking -import WalletConnectEcho -@testable import WalletConnectPush -@testable import WalletConnectPairing -@testable import WalletConnectSync -@testable import WalletConnectHistory -import WalletConnectIdentity -import WalletConnectSigner - -final class PushTests: XCTestCase { - - var dappPairingClient: PairingClient! - var walletPairingClient: PairingClient! - - var dappPushClient: DappPushClient! - var walletPushClient: WalletPushClient! - - var pairingStorage: PairingStorage! - - let pk = try! EthereumPrivateKey() - - var privateKey: Data { - return Data(pk.rawPrivateKey) - } - - var account: Account { - return Account("eip155:1:" + pk.address.hex(eip55: true))! - } - - private var publishers = [AnyCancellable]() - - func makeClientDependencies(prefix: String) -> (PairingClient, NetworkInteracting, SyncClient, KeychainStorageProtocol, KeyValueStorage) { - let keychain = KeychainStorageMock() - let keyValueStorage = RuntimeKeyValueStorage() - - let relayLogger = ConsoleLogger(suffix: prefix + " [Relay]", loggingLevel: .debug) - let pairingLogger = ConsoleLogger(suffix: prefix + " [Pairing]", loggingLevel: .debug) - let networkingLogger = ConsoleLogger(suffix: prefix + " [Networking]", loggingLevel: .debug) - - let relayClient = RelayClientFactory.create( - relayHost: InputConfig.relayHost, - projectId: InputConfig.projectId, - keyValueStorage: keyValueStorage, - keychainStorage: keychain, - logger: relayLogger) - - let networkingClient = NetworkingClientFactory.create( - relayClient: relayClient, - logger: networkingLogger, - keychainStorage: keychain, - keyValueStorage: keyValueStorage) - - let pairingClient = PairingClientFactory.create( - logger: pairingLogger, - keyValueStorage: keyValueStorage, - keychainStorage: keychain, - networkingClient: networkingClient) - - let syncClient = SyncClientFactory.create(networkInteractor: networkingClient, bip44: DefaultBIP44Provider(), keychain: keychain) - - let clientId = try! networkingClient.getClientId() - networkingLogger.debug("My client id is: \(clientId)") - return (pairingClient, networkingClient, syncClient, keychain, keyValueStorage) - } - - func makeDappClients() { - let prefix = "🦄 Dapp: " - let (pairingClient, networkingInteractor, syncClient, keychain, keyValueStorage) = makeClientDependencies(prefix: prefix) - let pushLogger = ConsoleLogger(suffix: prefix + " [Push]", loggingLevel: .debug) - dappPairingClient = pairingClient - dappPushClient = DappPushClientFactory.create(metadata: AppMetadata(name: "GM Dapp", description: "", url: "https://gm-dapp-xi.vercel.app/", icons: []), - logger: pushLogger, - keyValueStorage: keyValueStorage, - keychainStorage: keychain, - groupKeychainStorage: KeychainStorageMock(), - networkInteractor: networkingInteractor, - syncClient: syncClient) - } - - func makeWalletClients() { - let prefix = "🦋 Wallet: " - let (pairingClient, networkingInteractor, syncClient, keychain, keyValueStorage) = makeClientDependencies(prefix: prefix) - let pushLogger = ConsoleLogger(suffix: prefix + " [Push]", loggingLevel: .debug) - walletPairingClient = pairingClient - let echoClient = EchoClientFactory.create(projectId: "", - echoHost: "echo.walletconnect.com", - keychainStorage: keychain, - environment: .sandbox) - let keyserverURL = URL(string: "https://keys.walletconnect.com")! - let historyClient = HistoryClientFactory.create( - historyUrl: "https://history.walletconnect.com", - relayUrl: "wss://relay.walletconnect.com", - keychain: keychain - ) - walletPushClient = WalletPushClientFactory.create(keyserverURL: keyserverURL, - logger: pushLogger, - keyValueStorage: keyValueStorage, - keychainStorage: keychain, - groupKeychainStorage: KeychainStorageMock(), - networkInteractor: networkingInteractor, - pairingRegisterer: pairingClient, - echoClient: echoClient, - syncClient: syncClient, - historyClient: historyClient) - } - - override func setUp() { - makeDappClients() - makeWalletClients() - } - - func testPushPropose() async { - let expectation = expectation(description: "expects dapp to receive error response") - - let uri = try! await dappPairingClient.create() - try! await walletPairingClient.pair(uri: uri) - try! await walletPushClient.enableSync(account: account, onSign: sign) - try! await dappPushClient.propose(account: account, topic: uri.topic) - - walletPushClient.requestPublisher.sink { [unowned self] (id, _, _) in - Task(priority: .high) { try! await walletPushClient.approve(id: id, onSign: sign) } - }.store(in: &publishers) - - dappPushClient.proposalResponsePublisher.sink { (result) in - guard case .success = result else { - XCTFail() - return - } - expectation.fulfill() - }.store(in: &publishers) - wait(for: [expectation], timeout: InputConfig.defaultTimeout) - - } - - func testWalletRejectsPushPropose() async { - let expectation = expectation(description: "expects dapp to receive error response") - - let uri = try! await dappPairingClient.create() - try! await walletPairingClient.pair(uri: uri) - try! await dappPushClient.propose(account: account, topic: uri.topic) - - walletPushClient.requestPublisher.sink { [unowned self] (id, _, _) in - Task(priority: .high) { try! await walletPushClient.reject(id: id) } - }.store(in: &publishers) - - dappPushClient.proposalResponsePublisher.sink { (result) in - guard case .failure = result else { - XCTFail() - return - } - expectation.fulfill() - }.store(in: &publishers) - - wait(for: [expectation], timeout: InputConfig.defaultTimeout) - } - - func testWalletCreatesSubscription() async { - let expectation = expectation(description: "expects to create push subscription") - let metadata = AppMetadata(name: "GM Dapp", description: "", url: "https://gm-dapp-xi.vercel.app/", icons: []) - try! await walletPushClient.enableSync(account: account, onSign: sign) - try! await walletPushClient.subscribe(metadata: metadata, account: account, onSign: sign) - walletPushClient.subscriptionsPublisher - .first() - .sink { [unowned self] subscriptions in - XCTAssertNotNil(subscriptions.first) - Task { try! await walletPushClient.deleteSubscription(topic: subscriptions.first!.topic) } - expectation.fulfill() - }.store(in: &publishers) - wait(for: [expectation], timeout: InputConfig.defaultTimeout) - } - - func testDeletePushSubscription() async { - let expectation = expectation(description: "expects to delete push subscription") - let uri = try! await dappPairingClient.create() - try! await walletPairingClient.pair(uri: uri) - try! await walletPushClient.enableSync(account: account, onSign: sign) - try! await dappPushClient.propose(account: account, topic: uri.topic) - var subscriptionTopic: String! - - walletPushClient.requestPublisher.sink { [unowned self] (id, _, _) in - Task(priority: .high) { try! await walletPushClient.approve(id: id, onSign: sign) } - }.store(in: &publishers) - - dappPushClient.proposalResponsePublisher.sink { [unowned self] (result) in - guard case .success(let pushSubscription) = result else { - XCTFail() - return - } - subscriptionTopic = pushSubscription.topic - sleep(1) - Task(priority: .userInitiated) { try! await walletPushClient.deleteSubscription(topic: pushSubscription.topic)} - }.store(in: &publishers) - - dappPushClient.deleteSubscriptionPublisher.sink { topic in - XCTAssertEqual(subscriptionTopic, topic) - expectation.fulfill() - }.store(in: &publishers) - wait(for: [expectation], timeout: InputConfig.defaultTimeout) - } - - func testWalletCreatesAndUpdatesSubscription() async { - let expectation = expectation(description: "expects to create and update push subscription") - let metadata = AppMetadata(name: "GM Dapp", description: "", url: "https://gm-dapp-xi.vercel.app/", icons: []) - let updateScope: Set = ["alerts"] - try! await walletPushClient.enableSync(account: account, onSign: sign) - try! await walletPushClient.subscribe(metadata: metadata, account: account, onSign: sign) - walletPushClient.subscriptionsPublisher - .first() - .sink { [unowned self] subscriptions in - sleep(1) - Task { try! await walletPushClient.update(topic: subscriptions.first!.topic, scope: updateScope) } - } - .store(in: &publishers) - - walletPushClient.updateSubscriptionPublisher - .sink { [unowned self] result in - guard case .success(let subscription) = result else { XCTFail(); return } - let updatedScope = Set(subscription.scope.filter{ $0.value.enabled == true }.keys) - XCTAssertEqual(updatedScope, updateScope) - Task { try! await walletPushClient.deleteSubscription(topic: subscription.topic) } - expectation.fulfill() - }.store(in: &publishers) - - wait(for: [expectation], timeout: InputConfig.defaultTimeout) - } - - func testNotifyServerSubscribeAndNotifies() async throws { - let subscribeExpectation = expectation(description: "creates push subscription") - let messageExpectation = expectation(description: "receives a push message") - let pushMessage = PushMessage.stub() - - let metadata = AppMetadata(name: "GM Dapp", description: "", url: "https://gm-dapp-xi.vercel.app/", icons: []) - try! await walletPushClient.enableSync(account: account, onSign: sign) - try! await walletPushClient.subscribe(metadata: metadata, account: account, onSign: sign) - var subscription: PushSubscription! - walletPushClient.subscriptionsPublisher - .first() - .sink { subscriptions in - XCTAssertNotNil(subscriptions.first) - subscribeExpectation.fulfill() - subscription = subscriptions.first! - let notifier = Publisher() - sleep(1) - Task(priority: .high) { try await notifier.notify(topic: subscriptions.first!.topic, account: subscriptions.first!.account, message: pushMessage) } - }.store(in: &publishers) - walletPushClient.pushMessagePublisher - .sink { pushMessageRecord in - XCTAssertEqual(pushMessage, pushMessageRecord.message) - messageExpectation.fulfill() - }.store(in: &publishers) - - wait(for: [subscribeExpectation, messageExpectation], timeout: InputConfig.defaultTimeout) - try await walletPushClient.deleteSubscription(topic: subscription.topic) - } - -} - - -private extension PushTests { - func sign(_ message: String) -> SigningResult { - let signer = MessageSignerFactory(signerFactory: DefaultSignerFactory()).create(projectId: InputConfig.projectId) - return .signed(try! signer.sign(message: message, privateKey: privateKey, type: .eip191)) - } -} diff --git a/Example/IntegrationTests/Sign/SignClientTests.swift b/Example/IntegrationTests/Sign/SignClientTests.swift index da4e82373..256c9e92b 100644 --- a/Example/IntegrationTests/Sign/SignClientTests.swift +++ b/Example/IntegrationTests/Sign/SignClientTests.swift @@ -12,7 +12,7 @@ final class SignClientTests: XCTestCase { var wallet: ClientDelegate! static private func makeClientDelegate(name: String) -> ClientDelegate { - let logger = ConsoleLogger(suffix: name, loggingLevel: .debug) + let logger = ConsoleLogger(prefix: name, loggingLevel: .debug) let keychain = KeychainStorageMock() let keyValueStorage = RuntimeKeyValueStorage() let relayClient = RelayClientFactory.create( @@ -433,7 +433,7 @@ final class SignClientTests: XCTestCase { let sessionProposal = Session.Proposal( id: "", pairingTopic: "", - proposer: AppMetadata(name: "", description: "", url: "", icons: []), + proposer: AppMetadata.stub(), requiredNamespaces: requiredNamespaces, optionalNamespaces: optionalNamespaces, sessionProperties: nil, diff --git a/Example/IntegrationTests/Stubs/PushMessage.swift b/Example/IntegrationTests/Stubs/PushMessage.swift index 5355a3aeb..1ad6880ee 100644 --- a/Example/IntegrationTests/Stubs/PushMessage.swift +++ b/Example/IntegrationTests/Stubs/PushMessage.swift @@ -1,9 +1,9 @@ import Foundation -import WalletConnectPush +import WalletConnectNotify -extension PushMessage { - static func stub() -> PushMessage { - return PushMessage( +extension NotifyMessage { + static func stub() -> NotifyMessage { + return NotifyMessage( title: "swift_test", body: "gm_hourly", icon: "https://images.unsplash.com/photo-1581224463294-908316338239?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=250&q=80", diff --git a/Example/IntegrationTests/Stubs/Stubs.swift b/Example/IntegrationTests/Stubs/Stubs.swift index b2aa32f53..8b815a474 100644 --- a/Example/IntegrationTests/Stubs/Stubs.swift +++ b/Example/IntegrationTests/Stubs/Stubs.swift @@ -22,3 +22,14 @@ extension SessionNamespace { } } } + +extension AppMetadata { + static func stub() -> AppMetadata { + return AppMetadata( + name: "WalletConnectSwift", + description: "WalletConnectSwift", + url: "https://walletconnect.com", + icons: [] + ) + } +} diff --git a/Example/IntegrationTests/Sync/SyncTests.swift b/Example/IntegrationTests/Sync/SyncTests.swift index f5e4a4088..adcfdc532 100644 --- a/Example/IntegrationTests/Sync/SyncTests.swift +++ b/Example/IntegrationTests/Sync/SyncTests.swift @@ -56,9 +56,14 @@ final class SyncTests: XCTestCase { let keychain = KeychainStorageMock() let kms = KeyManagementService(keychain: keychain) let derivationService = SyncDerivationService(syncStorage: syncSignatureStore, bip44: DefaultBIP44Provider(), kms: kms) - let logger = ConsoleLogger(suffix: suffix, loggingLevel: .debug) - let relayClient = RelayClientFactory.create(relayHost: InputConfig.relayHost, projectId: InputConfig.projectId, keyValueStorage: RuntimeKeyValueStorage(), keychainStorage: keychain, logger: logger) - + let logger = ConsoleLogger(prefix: suffix, loggingLevel: .debug) + let relayClient = RelayClientFactory.create( + relayHost: InputConfig.relayHost, + projectId: InputConfig.projectId, + keyValueStorage: RuntimeKeyValueStorage(), + keychainStorage: keychain, + socketFactory: DefaultSocketFactory(), + logger: logger) let networkingInteractor = NetworkingClientFactory.create( relayClient: relayClient, logger: logger, @@ -83,8 +88,10 @@ final class SyncTests: XCTestCase { func testSync() async throws { let setExpectation = expectation(description: "syncSetTest") let delExpectation = expectation(description: "syncDelTest") + let uptExpectation = expectation(description: "syncUptTest") let object = TestObject(id: "id-1", value: "value-1") + let updated = TestObject(id: "id-1", value: "value-2") syncStore1.syncUpdatePublisher.sink { (_, _, update) in switch update { @@ -92,6 +99,8 @@ final class SyncTests: XCTestCase { XCTFail() case .delete: delExpectation.fulfill() + case .update: + XCTFail() } }.store(in: &publishers) @@ -101,6 +110,8 @@ final class SyncTests: XCTestCase { setExpectation.fulfill() case .delete: XCTFail() + case .update: + uptExpectation.fulfill() } }.store(in: &publishers) @@ -118,6 +129,15 @@ final class SyncTests: XCTestCase { XCTAssertEqual(try syncStore1.getAll(for: account), [object]) XCTAssertEqual(try syncStore2.getAll(for: account), [object]) + // Testing SyncStore `update` + + try await syncStore1.set(object: updated, for: account) + + wait(for: [uptExpectation], timeout: InputConfig.defaultTimeout) + + XCTAssertEqual(try syncStore1.getAll(for: account), [updated]) + XCTAssertEqual(try syncStore2.getAll(for: account), [updated]) + // Testing SyncStore `delete` try await syncStore2.delete(id: object.id, for: account) @@ -135,5 +155,6 @@ final class SyncTests: XCTestCase { try await client.register(account: account, signature: signature) try await client.create(account: account, store: storeName) + try await client.subscribe(account: account, store: storeName) } } diff --git a/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift b/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift new file mode 100644 index 000000000..e08391021 --- /dev/null +++ b/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift @@ -0,0 +1,144 @@ +import Foundation +import XCTest +import Combine +@testable import Web3Wallet +@testable import Auth +@testable import WalletConnectSign +@testable import WalletConnectPush + +final class XPlatformW3WTests: XCTestCase { + var w3wClient: Web3WalletClient! + var javaScriptAutoTestsAPI: JavaScriptAutoTestsAPI! + private var publishers = [AnyCancellable]() + + override func setUp() { + makeClient() + javaScriptAutoTestsAPI = JavaScriptAutoTestsAPI() + } + + func makeClient() { + let keychain = KeychainStorageMock() + let keyValueStorage = RuntimeKeyValueStorage() + + let relayLogger = ConsoleLogger(prefix: "🚄" + " [Relay]", loggingLevel: .debug) + let pairingLogger = ConsoleLogger(prefix: "👩‍❤️‍💋‍👩" + " [Pairing]", loggingLevel: .debug) + let networkingLogger = ConsoleLogger(prefix: "🕸️" + " [Networking]", loggingLevel: .debug) + let authLogger = ConsoleLogger(prefix: "🪪", loggingLevel: .debug) + + let signLogger = ConsoleLogger(prefix: "✍🏿", loggingLevel: .debug) + + let relayClient = RelayClientFactory.create( + relayHost: InputConfig.relayHost, + projectId: InputConfig.projectId, + keyValueStorage: keyValueStorage, + keychainStorage: keychain, + socketFactory: DefaultSocketFactory(), + logger: relayLogger + ) + + let networkingClient = NetworkingClientFactory.create( + relayClient: relayClient, + logger: networkingLogger, + keychainStorage: keychain, + keyValueStorage: keyValueStorage) + + let pairingClient = PairingClientFactory.create( + logger: pairingLogger, + keyValueStorage: keyValueStorage, + keychainStorage: keychain, + networkingClient: networkingClient) + + let signClient = SignClientFactory.create( + metadata: AppMetadata(name: name, description: "", url: "", icons: [""]), + logger: signLogger, + keyValueStorage: keyValueStorage, + keychainStorage: keychain, + pairingClient: pairingClient, + networkingClient: networkingClient + ) + + let authClient = AuthClientFactory.create( + metadata: AppMetadata(name: name, description: "", url: "", icons: [""]), + projectId: InputConfig.projectId, + crypto: DefaultCryptoProvider(), + logger: authLogger, + keyValueStorage: keyValueStorage, + keychainStorage: keychain, + networkingClient: networkingClient, + pairingRegisterer: pairingClient, + iatProvider: DefaultIATProvider()) + + w3wClient = Web3WalletClientFactory.create( + authClient: authClient, + signClient: signClient, + pairingClient: pairingClient, + pushClient: PushClientMock()) + } + + func testSessionSettle() async throws { + + let expectation = expectation(description: "session settled") + + w3wClient.sessionProposalPublisher + .sink { [unowned self] (proposal, _) in + Task(priority: .high) { + let sessionNamespaces = SessionNamespace.make(toRespond: proposal.requiredNamespaces) + try await w3wClient.approve(proposalId: proposal.id, namespaces: sessionNamespaces) + } + } + .store(in: &publishers) + + w3wClient.sessionSettlePublisher.sink { [unowned self] session in + Task { + var jsSession: JavaScriptAutoTestsAPI.Session? + + while jsSession == nil { + print("🎃 geting session") + do { + jsSession = try await javaScriptAutoTestsAPI.getSession(topic: session.topic) + } catch { + print("No session on JS client yet") + } + + if jsSession == nil { + sleep(1) + } + } + + XCTAssertEqual(jsSession?.topic, session.topic) + expectation.fulfill() + } + } + .store(in: &publishers) + + let pairingUri = try await javaScriptAutoTestsAPI.quickConnect() + try await w3wClient.pair(uri: pairingUri) + + wait(for: [expectation], timeout: InputConfig.defaultTimeout) + } + +} + + +class JavaScriptAutoTestsAPI { + private let host = "https://\(InputConfig.jsClientApiHost)" + + func quickConnect() async throws -> WalletConnectURI { + let url = URL(string: "\(host)/quick_connect")! + let (data, _) = try await URLSession.shared.data(from: url) + let uriString = String(decoding: data, as: UTF8.self) + return WalletConnectURI(string: uriString)! + } + + func getSession(topic: String) async throws -> Session { + let url = URL(string: "\(host)/session/\(topic)")! + let (data, _) = try await URLSession.shared.data(from: url) + return try JSONDecoder().decode(Session.self, from: data) + } + + // Testing Data Structures to match JS responses + + struct Session: Decodable { + let topic: String + } +} diff --git a/Example/PNDecryptionService/NotificationService.swift b/Example/PNDecryptionService/NotificationService.swift index 1be5f2335..abf6473c2 100644 --- a/Example/PNDecryptionService/NotificationService.swift +++ b/Example/PNDecryptionService/NotificationService.swift @@ -1,5 +1,5 @@ import UserNotifications -import WalletConnectPush +import WalletConnectNotify import os class NotificationService: UNNotificationServiceExtension { @@ -13,9 +13,9 @@ class NotificationService: UNNotificationServiceExtension { if let bestAttemptContent = bestAttemptContent { let topic = bestAttemptContent.userInfo["topic"] as! String let ciphertext = bestAttemptContent.userInfo["blob"] as! String - NSLog("echo decryption, topic=%@", topic) + NSLog("Push decryption, topic=%@", topic) do { - let service = PushDecryptionService() + let service = NotifyDecryptionService() let pushMessage = try service.decryptMessage(topic: topic, ciphertext: ciphertext) bestAttemptContent.title = pushMessage.title bestAttemptContent.body = pushMessage.body @@ -23,7 +23,7 @@ class NotificationService: UNNotificationServiceExtension { return } catch { - NSLog("echo decryption, error=%@", error.localizedDescription) + NSLog("Push decryption, error=%@", error.localizedDescription) bestAttemptContent.title = "" bestAttemptContent.body = "content not set" } diff --git a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift index 8bc77ffec..2f584b3b3 100644 --- a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift +++ b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift @@ -40,10 +40,9 @@ final class RelayClientEndToEndTests: XCTestCase { projectId: InputConfig.projectId, socketAuthenticator: socketAuthenticator ) - let logger = ConsoleLogger(suffix: prefix, loggingLevel: .debug) - let socket = WebSocketClient(url: urlFactory.create(fallback: false), logger: ConsoleLogger(suffix: prefix, loggingLevel: .debug)) + let socket = WebSocket(url: urlFactory.create(fallback: false)) let webSocketFactory = WebSocketFactoryMock(webSocket: socket) - + let logger = ConsoleLogger(prefix: prefix, loggingLevel: .debug) let dispatcher = Dispatcher( socketFactory: webSocketFactory, relayUrlFactory: urlFactory, diff --git a/Example/Shared/DefaultCryptoProvider.swift b/Example/Shared/DefaultCryptoProvider.swift index 4905855f7..3baf2b7e9 100644 --- a/Example/Shared/DefaultCryptoProvider.swift +++ b/Example/Shared/DefaultCryptoProvider.swift @@ -22,20 +22,4 @@ struct DefaultCryptoProvider: CryptoProvider { return Data(hash) } - public func derive(entropy: Data, path: [WalletConnectSigner.DerivationPath]) -> Data { - let mnemonic = Mnemonic.create(entropy: entropy) - let seed = Mnemonic.createSeed(mnemonic: mnemonic) - let privateKey = PrivateKey(seed: seed, coin: .bitcoin) - - let derived = path.reduce(privateKey) { result, path in - switch path { - case .hardened(let index): - return result.derived(at: .hardened(index)) - case .notHardened(let index): - return result.derived(at: .notHardened(index)) - } - } - - return derived.raw - } } diff --git a/Example/Shared/Tests/InputConfig.swift b/Example/Shared/Tests/InputConfig.swift index 8ffbb8770..e32975741 100644 --- a/Example/Shared/Tests/InputConfig.swift +++ b/Example/Shared/Tests/InputConfig.swift @@ -18,6 +18,10 @@ struct InputConfig { return config(for: "GM_DAPP_PROJECT_SECRET")! } + static var jsClientApiHost: String { + return config(for: "JS_CLIENT_API_HOST")! + } + static var relayUrl: String { return "wss://\(relayHost)" } diff --git a/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift b/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift index 4a7a87c8f..c6a2d32d6 100644 --- a/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift +++ b/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift @@ -18,7 +18,7 @@ final class Web3InboxViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - Web3Inbox.configure(account: importAccount.account, bip44: DefaultBIP44Provider(), config: [.pushEnabled: false], environment: .sandbox, onSign: onSing) + Web3Inbox.configure(account: importAccount.account, bip44: DefaultBIP44Provider(), config: [.notifyEnabled: false], environment: .sandbox, crypto: DefaultCryptoProvider(), onSign: onSing) edgesForExtendedLayout = [] navigationItem.title = "Web3Inbox SDK" diff --git a/Example/WalletApp/ApplicationLayer/Application.swift b/Example/WalletApp/ApplicationLayer/Application.swift index c1b3499dc..4015be2e3 100644 --- a/Example/WalletApp/ApplicationLayer/Application.swift +++ b/Example/WalletApp/ApplicationLayer/Application.swift @@ -11,3 +11,4 @@ final class Application { lazy var messageSigner = MessageSignerFactory(signerFactory: DefaultSignerFactory()).create() lazy var configurationService = ConfigurationService() } + diff --git a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift index 2686adab6..919d3292b 100644 --- a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift +++ b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift @@ -5,12 +5,32 @@ import Web3Inbox final class ConfigurationService { func configure(importAccount: ImportAccount) { + Networking.configure(projectId: InputConfig.projectId, socketFactory: DefaultSocketFactory()) + Networking.instance.setLogging(level: .debug) + + let metadata = AppMetadata( + name: "Example Wallet", + description: "wallet description", + url: "example.wallet", + icons: ["https://avatars.githubusercontent.com/u/37784886"] + ) + + Web3Wallet.configure(metadata: metadata, crypto: DefaultCryptoProvider(), environment: BuildConfiguration.shared.apnsEnvironment) + Web3Inbox.configure( account: importAccount.account, bip44: DefaultBIP44Provider(), config: [.chatEnabled: false, .settingsEnabled: false], environment: BuildConfiguration.shared.apnsEnvironment, + crypto: DefaultCryptoProvider(), onSign: importAccount.onSign ) + Web3Inbox.instance.setLogging(level: .debug) + + if let clientId = try? Networking.interactor.getClientId() { + LoggingService.instance.setUpUser(account: importAccount.account.absoluteString, clientId: clientId) + ProfilingService.instance.setUpProfiling(account: importAccount.account.absoluteString, clientId: clientId) + } + LoggingService.instance.startLogging() } } diff --git a/Example/WalletApp/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift b/Example/WalletApp/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift index 6770fd604..6669e9637 100644 --- a/Example/WalletApp/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift +++ b/Example/WalletApp/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift @@ -6,15 +6,10 @@ import Web3Wallet struct ThirdPartyConfigurator: Configurator { func configure() { - Networking.configure(projectId: InputConfig.projectId, socketFactory: DefaultSocketFactory()) - let metadata = AppMetadata( - name: "Example Wallet", - description: "wallet description", - url: "example.wallet", - icons: ["https://avatars.githubusercontent.com/u/37784886"] - ) + } - Web3Wallet.configure(metadata: metadata, crypto: DefaultCryptoProvider(), environment: BuildConfiguration.shared.apnsEnvironment) + private func configureLogging() { + LoggingService.instance.configure() } } diff --git a/Example/WalletApp/ApplicationLayer/LoggingService.swift b/Example/WalletApp/ApplicationLayer/LoggingService.swift new file mode 100644 index 000000000..e7a74cb9b --- /dev/null +++ b/Example/WalletApp/ApplicationLayer/LoggingService.swift @@ -0,0 +1,62 @@ +import Foundation +import Combine +import Sentry +import WalletConnectNetworking + +final class LoggingService { + enum LoggingError: Error { + case networking(String) + } + + public static var instance = LoggingService() + private var publishers = [AnyCancellable]() + private var isLogging: Bool { + get { + return queue.sync { _isLogging } + } + set { + queue.sync { _isLogging = newValue } + } + } + private var _isLogging = false + + private let queue = DispatchQueue(label: "com.walletApp.loggingService") + + func setUpUser(account: String, clientId: String) { + let user = User() + user.userId = clientId + user.data = ["account": account] + SentrySDK.setUser(user) + } + + func configure() { + guard let sentryDsn = InputConfig.sentryDsn, !sentryDsn.isEmpty else { return } + SentrySDK.start { options in + options.dsn = "https://\(sentryDsn)" + options.tracesSampleRate = 1.0 + } + } + + func startLogging() { + guard !isLogging else { return } + isLogging = true + + Networking.instance.logsPublisher + .sink { [weak self] log in + self?.queue.sync { + switch log { + case .error(let log): + SentrySDK.capture(error: LoggingError.networking(log.aggregated)) + case .warn(let log): + // Example of setting level to warning + var event = Event(level: .warning) + event.message = SentryMessage(formatted: log.aggregated) + SentrySDK.capture(event: event) + default: + return + } + } + } + .store(in: &publishers) + } +} diff --git a/Example/WalletApp/ApplicationLayer/ProfilingService.swift b/Example/WalletApp/ApplicationLayer/ProfilingService.swift new file mode 100644 index 000000000..14a778061 --- /dev/null +++ b/Example/WalletApp/ApplicationLayer/ProfilingService.swift @@ -0,0 +1,58 @@ +import Foundation +import Mixpanel +import WalletConnectNetworking +import Combine +import Web3Inbox + +final class ProfilingService { + public static var instance = ProfilingService() + + private let queue = DispatchQueue(label: "com.walletApp.profilingService") + private var publishers = [AnyCancellable]() + private var isProfiling: Bool { + get { + return queue.sync { _isProfiling } + } + set { + queue.sync { _isProfiling = newValue } + } + } + private var _isProfiling = false + + func setUpProfiling(account: String, clientId: String) { + guard !isProfiling else { return } + isProfiling = true + + guard let token = InputConfig.mixpanelToken, !token.isEmpty else { return } + + Mixpanel.initialize(token: token, trackAutomaticEvents: true) + let mixpanel = Mixpanel.mainInstance() + mixpanel.alias = account + mixpanel.identify(distinctId: clientId) + mixpanel.people.set(properties: ["$name": account, "account": account]) + + handleLogs(from: Networking.instance.logsPublisher) + handleLogs(from: Web3Inbox.instance.logsPublisher) + } + + private func handleLogs(from publisher: AnyPublisher) { + publisher + .sink { [unowned self] log in + self.queue.sync { + switch log { + case .error(let logMessage), + .warn(let logMessage), + .debug(let logMessage): + self.send(logMessage: logMessage) + default: + return + } + } + } + .store(in: &publishers) + } + + func send(logMessage: LogMessage) { + Mixpanel.mainInstance().track(event: logMessage.message, properties: logMessage.properties) + } +} diff --git a/Example/WalletApp/ApplicationLayer/PushRegisterer.swift b/Example/WalletApp/ApplicationLayer/PushRegisterer.swift index 91a7d63d5..b3f02f00d 100644 --- a/Example/WalletApp/ApplicationLayer/PushRegisterer.swift +++ b/Example/WalletApp/ApplicationLayer/PushRegisterer.swift @@ -1,12 +1,9 @@ - -import WalletConnectPush +import WalletConnectNotify import Combine import UIKit class PushRegisterer { - private var publishers = [AnyCancellable]() - func getNotificationSettings() { UNUserNotificationCenter.current().getNotificationSettings { settings in print("Notification settings: \(settings)") diff --git a/Example/WalletApp/Common/BuildConfiguration.swift b/Example/WalletApp/Common/BuildConfiguration.swift index f17d08e73..4e70cc13f 100644 --- a/Example/WalletApp/Common/BuildConfiguration.swift +++ b/Example/WalletApp/Common/BuildConfiguration.swift @@ -1,5 +1,5 @@ import Foundation -import WalletConnectPush +import WalletConnectNotify class BuildConfiguration { enum Environment: String { diff --git a/Example/WalletApp/Common/InputConfig.swift b/Example/WalletApp/Common/InputConfig.swift index 944f6a840..b8ce51193 100644 --- a/Example/WalletApp/Common/InputConfig.swift +++ b/Example/WalletApp/Common/InputConfig.swift @@ -8,6 +8,14 @@ struct InputConfig { return projectId } + + static var sentryDsn: String? { + return config(for: "WALLETAPP_SENTRY_DSN") + } + + static var mixpanelToken: String? { + return config(for: "MIXPANEL_TOKEN") + } private static func config(for key: String) -> String? { return Bundle.main.object(forInfoDictionaryKey: key) as? String diff --git a/Example/WalletApp/Other/Info.plist b/Example/WalletApp/Other/Info.plist index f8d34de93..2f0f15d26 100644 --- a/Example/WalletApp/Other/Info.plist +++ b/Example/WalletApp/Other/Info.plist @@ -26,8 +26,12 @@ PROJECT_ID $(PROJECT_ID) + WALLETAPP_SENTRY_DSN + $(WALLETAPP_SENTRY_DSN) SIMULATOR_IDENTIFIER $(SIMULATOR_IDENTIFIER) + MIXPANEL_TOKEN + $(MIXPANEL_TOKEN) UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainInteractor.swift index 150fe3ad6..ecae96621 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainInteractor.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainInteractor.swift @@ -2,10 +2,19 @@ import Foundation import Combine import Web3Wallet -import WalletConnectPush +import WalletConnectNotify final class MainInteractor { - var pushRequestPublisher: AnyPublisher<(id: RPCID, account: Account, metadata: AppMetadata), Never> { - return Push.wallet.requestPublisher + + var sessionProposalPublisher: AnyPublisher<(proposal: Session.Proposal, context: VerifyContext?), Never> { + return Web3Wallet.instance.sessionProposalPublisher + } + + var sessionRequestPublisher: AnyPublisher<(request: Request, context: VerifyContext?), Never> { + return Web3Wallet.instance.sessionRequestPublisher + } + + var requestPublisher: AnyPublisher<(request: AuthRequest, context: VerifyContext?), Never> { + return Web3Wallet.instance.authRequestPublisher } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift index dc8a74069..4be0dc58a 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainPresenter.swift @@ -16,6 +16,7 @@ final class MainPresenter { var viewControllers: [UIViewController] { return [ router.walletViewController(importAccount: importAccount), + router.notificationsViewController(), router.web3InboxViewController(), router.settingsViewController() ] @@ -35,15 +36,28 @@ final class MainPresenter { // MARK: - Private functions extension MainPresenter { - private func setupInitialState() { configurationService.configure(importAccount: importAccount) pushRegisterer.registerForPushNotifications() - interactor.pushRequestPublisher + interactor.sessionProposalPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] session in + router.present(proposal: session.proposal, importAccount: importAccount, context: session.context) + } + .store(in: &disposeBag) + + interactor.sessionRequestPublisher .receive(on: DispatchQueue.main) - .sink { [unowned self] request in - router.present(pushRequest: request) + .sink { [unowned self] request, context in + router.present(sessionRequest: request, importAccount: importAccount, sessionContext: context) }.store(in: &disposeBag) + + interactor.requestPublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] result in + router.present(request: result.request, importAccount: importAccount, context: result.context) + } + .store(in: &disposeBag) } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift index e78a40f6d..7d176926f 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainRouter.swift @@ -1,7 +1,7 @@ import UIKit import Web3Wallet -import WalletConnectPush +import WalletConnectNotify final class MainRouter { weak var viewController: UIViewController! @@ -31,9 +31,19 @@ final class MainRouter { return SettingsModule.create(app: app) .wrapToNavigationController() } + + func present(proposal: Session.Proposal, importAccount: ImportAccount, context: VerifyContext?) { + SessionProposalModule.create(app: app, importAccount: importAccount, proposal: proposal, context: context) + .presentFullScreen(from: viewController, transparentBackground: true) + } + + func present(sessionRequest: Request, importAccount: ImportAccount, sessionContext: VerifyContext?) { + SessionRequestModule.create(app: app, sessionRequest: sessionRequest, importAccount: importAccount, sessionContext: sessionContext) + .presentFullScreen(from: viewController, transparentBackground: true) + } - func present(pushRequest: PushRequest) { -// PushRequestModule.create(app: app, pushRequest: pushRequest) -// .presentFullScreen(from: viewController, transparentBackground: true) + func present(request: AuthRequest, importAccount: ImportAccount, context: VerifyContext?) { + AuthRequestModule.create(app: app, request: request, importAccount: importAccount, context: context) + .presentFullScreen(from: viewController, transparentBackground: true) } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/MainViewController.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/MainViewController.swift index 5fd88ac40..f36c7be7a 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/MainViewController.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/MainViewController.swift @@ -1,5 +1,9 @@ import UIKit - +import Sentry +enum LoginError: Error { + case wrongUser(id: String) + case wrongPassword +} final class MainViewController: UITabBarController { private let presenter: MainPresenter @@ -11,7 +15,6 @@ final class MainViewController: UITabBarController { override func viewDidLoad() { super.viewDidLoad() - setupTabs() } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Main/Model/TabPage.swift b/Example/WalletApp/PresentationLayer/Wallet/Main/Model/TabPage.swift index 1c93883ad..3f2051baf 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Main/Model/TabPage.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Main/Model/TabPage.swift @@ -2,6 +2,7 @@ import UIKit enum TabPage: CaseIterable { case wallet + case notifications case web3Inbox case settings @@ -9,6 +10,8 @@ enum TabPage: CaseIterable { switch self { case .wallet: return "Apps" + case .notifications: + return "Notifications" case .web3Inbox: return "Web3Inbox" case .settings: @@ -20,6 +23,8 @@ enum TabPage: CaseIterable { switch self { case .wallet: return UIImage(systemName: "house.fill")! + case .notifications: + return UIImage(systemName: "bell.fill")! case .web3Inbox: return UIImage(systemName: "bell.fill")! case .settings: @@ -32,6 +37,6 @@ enum TabPage: CaseIterable { } static var enabledTabs: [TabPage] { - return [.wallet, .web3Inbox, .settings] + return [.wallet, .notifications, .web3Inbox, .settings] } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Notifications/Models/SubscriptionsViewModel.swift b/Example/WalletApp/PresentationLayer/Wallet/Notifications/Models/SubscriptionsViewModel.swift index caf9826ee..e727c94e3 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Notifications/Models/SubscriptionsViewModel.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Notifications/Models/SubscriptionsViewModel.swift @@ -1,8 +1,8 @@ import Foundation -import WalletConnectPush +import WalletConnectNotify struct SubscriptionsViewModel: Identifiable { - let subscription: WalletConnectPush.PushSubscription + let subscription: NotifySubscription var id: String { return subscription.topic diff --git a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsInteractor.swift index f07d922f7..84797d283 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsInteractor.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsInteractor.swift @@ -1,20 +1,20 @@ -import WalletConnectPush +import WalletConnectNotify import Combine final class NotificationsInteractor { - var subscriptionsPublisher: AnyPublisher<[PushSubscription], Never> { - return Push.wallet.subscriptionsPublisher + var subscriptionsPublisher: AnyPublisher<[NotifySubscription], Never> { + return Notify.instance.subscriptionsPublisher } - func getSubscriptions() -> [PushSubscription] { - let subs = Push.wallet.getActiveSubscriptions() + func getSubscriptions() -> [NotifySubscription] { + let subs = Notify.instance.getActiveSubscriptions() return subs } - func removeSubscription(_ subscription: PushSubscription) async { + func removeSubscription(_ subscription: NotifySubscription) async { do { - try await Push.wallet.deleteSubscription(topic: subscription.topic) + try await Notify.instance.deleteSubscription(topic: subscription.topic) } catch { print(error) } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsPresenter.swift index ba6761a50..0aa9d878d 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsPresenter.swift @@ -54,8 +54,8 @@ private extension NotificationsPresenter { } interactor.subscriptionsPublisher .receive(on: DispatchQueue.main) - .sink { [weak self] pushSubscriptions in - self?.subscriptions = pushSubscriptions + .sink { [weak self] notifySubscriptions in + self?.subscriptions = notifySubscriptions .map { SubscriptionsViewModel(subscription: $0) } } .store(in: &disposeBag) diff --git a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsRouter.swift index 1441d32ba..eebcfd7b3 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsRouter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Notifications/NotificationsRouter.swift @@ -1,5 +1,5 @@ import UIKit -import WalletConnectPush +import WalletConnectNotify final class NotificationsRouter { @@ -11,7 +11,7 @@ final class NotificationsRouter { self.app = app } - func presentNotifications(subscription: WalletConnectPush.PushSubscription) { + func presentNotifications(subscription: NotifySubscription) { PushMessagesModule.create(app: app, subscription: subscription) .push(from: viewController) } diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/Models/PushMessageViewModel.swift b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/Models/PushMessageViewModel.swift index 35a61e1d6..cfb200823 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/Models/PushMessageViewModel.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/Models/PushMessageViewModel.swift @@ -1,10 +1,9 @@ - import Foundation -import WalletConnectPush +import WalletConnectNotify struct PushMessageViewModel: Identifiable { - let pushMessageRecord: WalletConnectPush.PushMessageRecord + let pushMessageRecord: NotifyMessageRecord var id: String { return pushMessageRecord.id diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesInteractor.swift index b2fa4a51f..b1953934e 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesInteractor.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesInteractor.swift @@ -1,23 +1,23 @@ -import WalletConnectPush +import WalletConnectNotify import Combine final class PushMessagesInteractor { - let subscription: PushSubscription + let subscription: NotifySubscription - init(subscription: PushSubscription) { + init(subscription: NotifySubscription) { self.subscription = subscription } - var pushMessagePublisher: AnyPublisher { - return Push.wallet.pushMessagePublisher + var messagesPublisher: AnyPublisher<[NotifyMessageRecord], Never> { + return Notify.instance.messagesPublisher(topic: subscription.topic) } - func getPushMessages() -> [PushMessageRecord] { - return Push.wallet.getMessageHistory(topic: subscription.topic) + func getPushMessages() -> [NotifyMessageRecord] { + return Notify.instance.getMessageHistory(topic: subscription.topic) } func deletePushMessage(id: String) { - Push.wallet.deletePushMessage(id: id) + Notify.instance.deleteNotifyMessage(id: id) } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesModule.swift b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesModule.swift index e7b6cc382..447c08ce3 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesModule.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesModule.swift @@ -1,10 +1,10 @@ import SwiftUI -import WalletConnectPush +import WalletConnectNotify final class PushMessagesModule { @discardableResult - static func create(app: Application, subscription: PushSubscription) -> UIViewController { + static func create(app: Application, subscription: NotifySubscription) -> UIViewController { let router = PushMessagesRouter(app: app) let interactor = PushMessagesInteractor(subscription: subscription) let presenter = PushMessagesPresenter(interactor: interactor, router: router) diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesPresenter.swift index 1abf8a408..ec8b91c8f 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesPresenter.swift @@ -1,6 +1,6 @@ import UIKit import Combine -import WalletConnectPush +import WalletConnectNotify final class PushMessagesPresenter: ObservableObject { @@ -8,19 +8,35 @@ final class PushMessagesPresenter: ObservableObject { private let router: PushMessagesRouter private var disposeBag = Set() - @Published var pushMessages: [PushMessageViewModel] = [] + @Published private var pushMessages: [NotifyMessageRecord] = [] + + var messages: [PushMessageViewModel] { + return pushMessages + .sorted { $0.publishedAt > $1.publishedAt } + .map { PushMessageViewModel(pushMessageRecord: $0) } + } init(interactor: PushMessagesInteractor, router: PushMessagesRouter) { - defer { reloadPushMessages() } + defer { setupInitialState() } self.interactor = interactor self.router = router + setUpMessagesRefresh() + } + + private func setUpMessagesRefresh() { + Timer.publish(every: 10.0, on: .main, in: .default) + .autoconnect() + .sink(receiveValue: { [weak self] _ in + guard let self = self else { return } + self.pushMessages = self.interactor.getPushMessages() + }).store(in: &disposeBag) } + func deletePushMessage(at indexSet: IndexSet) { if let index = indexSet.first { interactor.deletePushMessage(id: pushMessages[index].id) } - reloadPushMessages() } } @@ -40,27 +56,14 @@ extension PushMessagesPresenter: SceneViewModel { private extension PushMessagesPresenter { - func reloadPushMessages() { - self.pushMessages = interactor.getPushMessages() - .sorted { - // Most recent first - $0.publishedAt > $1.publishedAt - } - .map { pushMessageRecord in - PushMessageViewModel(pushMessageRecord: pushMessageRecord) - } - - interactor.pushMessagePublisher + func setupInitialState() { + pushMessages = interactor.getPushMessages() + + interactor.messagesPublisher .receive(on: DispatchQueue.main) - .sink { [weak self] newPushMessage in - let newMessageViewModel = PushMessageViewModel(pushMessageRecord: newPushMessage) - guard let index = self?.pushMessages.firstIndex( - where: { $0.pushMessageRecord.publishedAt > newPushMessage.publishedAt } - ) else { - self?.pushMessages.append(newMessageViewModel) - return - } - self?.pushMessages.insert(newMessageViewModel, at: index) + .sink { [weak self] messages in + guard let self = self else { return } + self.pushMessages = self.interactor.getPushMessages() } .store(in: &disposeBag) } diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesView.swift b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesView.swift index c29766ed3..7f22c53f6 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/PushMessages/PushMessagesView.swift @@ -11,7 +11,7 @@ struct PushMessagesView: View { VStack(alignment: .leading, spacing: 16) { ZStack { - if presenter.pushMessages.isEmpty { + if presenter.messages.isEmpty { VStack(spacing: 10) { Image(systemName: "bell.badge.fill") .resizable() @@ -29,9 +29,9 @@ struct PushMessagesView: View { } VStack { - if !presenter.pushMessages.isEmpty { + if !presenter.messages.isEmpty { List { - ForEach(presenter.pushMessages, id: \.id) { pm in + ForEach(presenter.messages, id: \.id) { pm in notificationView(pushMessage: pm) .listRowSeparator(.hidden) .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 16, trailing: 0)) diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushRequest/PushRequestInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/PushRequest/PushRequestInteractor.swift deleted file mode 100644 index 21610770c..000000000 --- a/Example/WalletApp/PresentationLayer/Wallet/PushRequest/PushRequestInteractor.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Foundation -import WalletConnectPush - -final class PushRequestInteractor { - func approve(pushRequest: PushRequest, importAccount: ImportAccount) async throws { - try await Push.wallet.approve(id: pushRequest.id, onSign: importAccount.onSign) - } - - func reject(pushRequest: PushRequest) async throws { - try await Push.wallet.reject(id: pushRequest.id) - } -} diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushRequest/PushRequestModule.swift b/Example/WalletApp/PresentationLayer/Wallet/PushRequest/PushRequestModule.swift deleted file mode 100644 index ffb464f0f..000000000 --- a/Example/WalletApp/PresentationLayer/Wallet/PushRequest/PushRequestModule.swift +++ /dev/null @@ -1,17 +0,0 @@ -import SwiftUI -import WalletConnectPush - -final class PushRequestModule { - @discardableResult - static func create(app: Application, pushRequest: PushRequest, importAccount: ImportAccount) -> UIViewController { - let router = PushRequestRouter(app: app) - let interactor = PushRequestInteractor() - let presenter = PushRequestPresenter(interactor: interactor, router: router, pushRequest: pushRequest, importAccount: importAccount) - let view = PushRequestView().environmentObject(presenter) - let viewController = SceneViewController(viewModel: presenter, content: view) - - router.viewController = viewController - - return viewController - } -} diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushRequest/PushRequestPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/PushRequest/PushRequestPresenter.swift deleted file mode 100644 index 19e2dadd3..000000000 --- a/Example/WalletApp/PresentationLayer/Wallet/PushRequest/PushRequestPresenter.swift +++ /dev/null @@ -1,54 +0,0 @@ -import UIKit -import Combine -import WalletConnectPush - -final class PushRequestPresenter: ObservableObject { - private let interactor: PushRequestInteractor - private let router: PushRequestRouter - private let importAccount: ImportAccount - - let pushRequest: PushRequest - - var message: String { - return String(describing: pushRequest.account) - } - - private var disposeBag = Set() - - init( - interactor: PushRequestInteractor, - router: PushRequestRouter, - pushRequest: PushRequest, - importAccount: ImportAccount - ) { - defer { setupInitialState() } - self.interactor = interactor - self.router = router - self.pushRequest = pushRequest - self.importAccount = importAccount - } - - @MainActor - func onApprove() async throws { - try await interactor.approve(pushRequest: pushRequest, importAccount: importAccount) - router.dismiss() - } - - @MainActor - func onReject() async throws { - try await interactor.reject(pushRequest: pushRequest) - router.dismiss() - } -} - -// MARK: - Private functions -private extension PushRequestPresenter { - func setupInitialState() { - - } -} - -// MARK: - SceneViewModel -extension PushRequestPresenter: SceneViewModel { - -} diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushRequest/PushRequestRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/PushRequest/PushRequestRouter.swift deleted file mode 100644 index 6ac5f730c..000000000 --- a/Example/WalletApp/PresentationLayer/Wallet/PushRequest/PushRequestRouter.swift +++ /dev/null @@ -1,15 +0,0 @@ -import UIKit - -final class PushRequestRouter { - weak var viewController: UIViewController! - - private let app: Application - - init(app: Application) { - self.app = app - } - - func dismiss() { - viewController.dismiss() - } -} diff --git a/Example/WalletApp/PresentationLayer/Wallet/PushRequest/PushRequestView.swift b/Example/WalletApp/PresentationLayer/Wallet/PushRequest/PushRequestView.swift deleted file mode 100644 index 62a21e17f..000000000 --- a/Example/WalletApp/PresentationLayer/Wallet/PushRequest/PushRequestView.swift +++ /dev/null @@ -1,127 +0,0 @@ -import SwiftUI - -struct PushRequestView: View { - @EnvironmentObject var presenter: PushRequestPresenter - - @State var text = "" - - var body: some View { - ZStack { - Color.black.opacity(0.6) - - VStack { - Spacer() - - VStack(spacing: 0) { - Image("header") - .resizable() - .scaledToFit() - - Text("would you like to send notifications") - .foregroundColor(.grey8) - .font(.system(size: 22, weight: .bold, design: .rounded)) - .padding(.top, 10) - - pushRequestView() - - HStack(spacing: 20) { - Button { - Task(priority: .userInitiated) { try await - presenter.onReject() - } - } label: { - Text("Decline") - .frame(maxWidth: .infinity) - .foregroundColor(.white) - .font(.system(size: 20, weight: .semibold, design: .rounded)) - .padding(.vertical, 11) - .background( - LinearGradient( - gradient: Gradient(colors: [ - .foregroundNegative, - .lightForegroundNegative - ]), - startPoint: .top, endPoint: .bottom) - ) - .cornerRadius(20) - } - .shadow(color: .white.opacity(0.25), radius: 8, y: 2) - - Button { - Task(priority: .userInitiated) { try await - presenter.onApprove() - } - } label: { - Text("Allow") - .frame(maxWidth: .infinity) - .foregroundColor(.white) - .font(.system(size: 20, weight: .semibold, design: .rounded)) - .padding(.vertical, 11) - .background( - LinearGradient( - gradient: Gradient(colors: [ - .foregroundPositive, - .lightForegroundPositive - ]), - startPoint: .top, endPoint: .bottom) - ) - .cornerRadius(20) - } - .shadow(color: .white.opacity(0.25), radius: 8, y: 2) - } - .padding(.top, 25) - } - .padding(20) - .background(.ultraThinMaterial) - .cornerRadius(34) - .padding(.horizontal, 10) - - Spacer() - } - } - .edgesIgnoringSafeArea(.all) - } - - private func pushRequestView() -> some View { - VStack { - VStack(alignment: .leading) { - Text("Notifications") - .font(.system(size: 15, weight: .semibold, design: .rounded)) - .foregroundColor(.whiteBackground) - .padding(.horizontal, 8) - .padding(.vertical, 5) - .background(Color.grey70) - .cornerRadius(28, corners: .allCorners) - .padding(.leading, 15) - .padding(.top, 9) - - VStack(spacing: 0) { - ScrollView { - Text(presenter.message) - .foregroundColor(.grey50) - .font(.system(size: 13, weight: .semibold, design: .rounded)) - } - .padding(.horizontal, 18) - .padding(.vertical, 10) - .frame(height: 250) - } - .background(Color.whiteBackground) - .cornerRadius(20, corners: .allCorners) - .padding(.horizontal, 5) - .padding(.bottom, 5) - - } - .background(.thinMaterial) - .cornerRadius(25, corners: .allCorners) - } - .padding(.top, 30) - } -} - -#if DEBUG -struct PushRequestView_Previews: PreviewProvider { - static var previews: some View { - PushRequestView() - } -} -#endif diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalInteractor.swift index 226f5b80d..0216a3db6 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalInteractor.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalInteractor.swift @@ -20,18 +20,14 @@ final class SessionProposalInteractor { let supportedChains = [Blockchain("eip155:1")!, Blockchain("eip155:137")!] let supportedAccounts = [Account(blockchain: Blockchain("eip155:1")!, address: ETHSigner.address)!, Account(blockchain: Blockchain("eip155:137")!, address: ETHSigner.address)!] */ - do { - let sessionNamespaces = try AutoNamespaces.build( - sessionProposal: proposal, - chains: Array(supportedChains), - methods: Array(supportedMethods), - events: Array(supportedEvents), - accounts: supportedAccounts - ) - try await Web3Wallet.instance.approve(proposalId: proposal.id, namespaces: sessionNamespaces, sessionProperties: proposal.sessionProperties) - } catch { - print(error) - } + let sessionNamespaces = try AutoNamespaces.build( + sessionProposal: proposal, + chains: Array(supportedChains), + methods: Array(supportedMethods), + events: Array(supportedEvents), + accounts: supportedAccounts + ) + try await Web3Wallet.instance.approve(proposalId: proposal.id, namespaces: sessionNamespaces, sessionProperties: proposal.sessionProperties) } func reject(proposal: Session.Proposal) async throws { diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalPresenter.swift index 117db225c..ad02dd7a4 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalPresenter.swift @@ -11,6 +11,9 @@ final class SessionProposalPresenter: ObservableObject { let sessionProposal: Session.Proposal let verified: Bool? + @Published var showError = false + @Published var errorMessage = "Error" + private var disposeBag = Set() init( @@ -30,14 +33,24 @@ final class SessionProposalPresenter: ObservableObject { @MainActor func onApprove() async throws { - try await interactor.approve(proposal: sessionProposal, account: importAccount.account) - router.dismiss() + do { + try await interactor.approve(proposal: sessionProposal, account: importAccount.account) + router.dismiss() + } catch { + errorMessage = error.localizedDescription + showError.toggle() + } } @MainActor func onReject() async throws { - try await interactor.reject(proposal: sessionProposal) - router.dismiss() + do { + try await interactor.reject(proposal: sessionProposal) + router.dismiss() + } catch { + errorMessage = error.localizedDescription + showError.toggle() + } } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalView.swift b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalView.swift index 97717d9b5..8082ce1ee 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalView.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/SessionProposal/SessionProposalView.swift @@ -146,6 +146,9 @@ struct SessionProposalView: View { Spacer() } } + .alert(presenter.errorMessage, isPresented: $presenter.showError) { + Button("OK", role: .cancel) {} + } .edgesIgnoringSafeArea(.all) } //private func sessionProposalView(chain: String) -> some View { diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletInteractor.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletInteractor.swift index 564c31f64..3a32be3f8 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletInteractor.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletInteractor.swift @@ -1,21 +1,9 @@ import Combine import Web3Wallet -import WalletConnectPush +import WalletConnectNotify final class WalletInteractor { - var sessionProposalPublisher: AnyPublisher<(proposal: Session.Proposal, context: VerifyContext?), Never> { - return Web3Wallet.instance.sessionProposalPublisher - } - - var sessionRequestPublisher: AnyPublisher<(request: Request, context: VerifyContext?), Never> { - return Web3Wallet.instance.sessionRequestPublisher - } - - var requestPublisher: AnyPublisher<(request: AuthRequest, context: VerifyContext?), Never> { - return Web3Wallet.instance.authRequestPublisher - } - var sessionsPublisher: AnyPublisher<[Session], Never> { return Web3Wallet.instance.sessionsPublisher } @@ -31,4 +19,12 @@ final class WalletInteractor { func disconnectSession(session: Session) async throws { try await Web3Wallet.instance.disconnect(topic: session.topic) } + + func getPendingProposals() -> [(proposal: Session.Proposal, context: VerifyContext?)] { + Web3Wallet.instance.getPendingProposals() + } + + func getPendingRequests() -> [(request: Request, context: VerifyContext?)] { + Web3Wallet.instance.getPendingRequests() + } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift index cb8bc32fe..c8a5baee1 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletPresenter.swift @@ -40,6 +40,11 @@ final class WalletPresenter: ObservableObject { func onAppear() { showPairingLoading = app.requestSent removePairingIndicator() + + let pendingRequests = interactor.getPendingRequests() + if let request = pendingRequests.first(where: { $0.context != nil }) { + router.present(sessionRequest: request.request, importAccount: importAccount, sessionContext: request.context) + } } func onConnection(session: Session) { @@ -88,29 +93,6 @@ final class WalletPresenter: ObservableObject { // MARK: - Private functions extension WalletPresenter { private func setupInitialState() { - interactor.sessionProposalPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] session in - showPairingLoading = false - router.present(proposal: session.proposal, importAccount: importAccount, context: session.context) - } - .store(in: &disposeBag) - - interactor.sessionRequestPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] request, context in - showPairingLoading = false - router.present(sessionRequest: request, importAccount: importAccount, sessionContext: context) - }.store(in: &disposeBag) - - interactor.requestPublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] result in - showPairingLoading = false - router.present(request: result.request, importAccount: importAccount, context: result.context) - } - .store(in: &disposeBag) - interactor.sessionsPublisher .receive(on: DispatchQueue.main) .sink { [weak self] sessions in @@ -148,10 +130,6 @@ extension WalletPresenter { private func removePairingIndicator() { DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - if self.showPairingLoading { - self.errorMessage = "WalletConnect - Pairing timeout error" - self.showError.toggle() - } self.showPairingLoading = false } } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift index 12604e895..c9907a0b5 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Wallet/WalletRouter.swift @@ -11,18 +11,13 @@ final class WalletRouter { self.app = app } - func present(proposal: Session.Proposal, importAccount: ImportAccount, context: VerifyContext?) { - SessionProposalModule.create(app: app, importAccount: importAccount, proposal: proposal, context: context) - .presentFullScreen(from: viewController, transparentBackground: true) - } - func present(sessionRequest: Request, importAccount: ImportAccount, sessionContext: VerifyContext?) { SessionRequestModule.create(app: app, sessionRequest: sessionRequest, importAccount: importAccount, sessionContext: sessionContext) .presentFullScreen(from: viewController, transparentBackground: true) } - - func present(request: AuthRequest, importAccount: ImportAccount, context: VerifyContext?) { - AuthRequestModule.create(app: app, request: request, importAccount: importAccount, context: context) + + func present(sessionProposal: Session.Proposal, importAccount: ImportAccount, sessionContext: VerifyContext?) { + SessionProposalModule.create(app: app, importAccount: importAccount, proposal: sessionProposal, context: sessionContext) .presentFullScreen(from: viewController, transparentBackground: true) } diff --git a/Example/WalletApp/PresentationLayer/Wallet/Web3Inbox/Web3InboxViewController.swift b/Example/WalletApp/PresentationLayer/Wallet/Web3Inbox/Web3InboxViewController.swift index 223c16a85..d79c8b4ea 100644 --- a/Example/WalletApp/PresentationLayer/Wallet/Web3Inbox/Web3InboxViewController.swift +++ b/Example/WalletApp/PresentationLayer/Wallet/Web3Inbox/Web3InboxViewController.swift @@ -25,11 +25,22 @@ final class Web3InboxViewController: UIViewController { view = Web3Inbox.instance.getWebView() let refresh = UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(refreshTapped)) - navigationItem.rightBarButtonItem = refresh + let getUrl = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(getUrlPressed)) + + navigationItem.rightBarButtonItems = [refresh, getUrl] } @objc func refreshTapped() { - webView?.reload() + Web3Inbox.instance.reload() + } + + @objc func getUrlPressed(_ sender: UIBarItem) { + UIPasteboard.general.string = webView?.url?.absoluteString + + let alert = UIAlertController(title: "URL copied to clipboard", message: nil, preferredStyle: .alert) + let action = UIAlertAction(title: "OK", style: .cancel) + alert.addAction(action) + present(alert, animated: true) } } diff --git a/Example/WalletConnect-Package.xctestplan b/Example/WalletConnect-Package.xctestplan new file mode 100644 index 000000000..d2c5cb9e5 --- /dev/null +++ b/Example/WalletConnect-Package.xctestplan @@ -0,0 +1,108 @@ +{ + "configurations" : [ + { + "id" : "BDCC881D-E5D4-477B-A899-4984FA326FED", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:", + "identifier" : "AuthTests", + "name" : "AuthTests" + } + }, + { + "target" : { + "containerPath" : "container:", + "identifier" : "ChatTests", + "name" : "ChatTests" + } + }, + { + "target" : { + "containerPath" : "container:", + "identifier" : "CommonsTests", + "name" : "CommonsTests" + } + }, + { + "target" : { + "containerPath" : "container:", + "identifier" : "JSONRPCTests", + "name" : "JSONRPCTests" + } + }, + { + "target" : { + "containerPath" : "container:", + "identifier" : "RelayerTests", + "name" : "RelayerTests" + } + }, + { + "target" : { + "containerPath" : "container:", + "identifier" : "VerifyTests", + "name" : "VerifyTests" + } + }, + { + "target" : { + "containerPath" : "container:", + "identifier" : "WalletConnectKMSTests", + "name" : "WalletConnectKMSTests" + } + }, + { + "target" : { + "containerPath" : "container:", + "identifier" : "WalletConnectPairingTests", + "name" : "WalletConnectPairingTests" + } + }, + { + "target" : { + "containerPath" : "container:", + "identifier" : "WalletConnectSignTests", + "name" : "WalletConnectSignTests" + } + }, + { + "target" : { + "containerPath" : "container:", + "identifier" : "WalletConnectUtilsTests", + "name" : "WalletConnectUtilsTests" + } + }, + { + "target" : { + "containerPath" : "container:", + "identifier" : "Web3WalletTests", + "name" : "Web3WalletTests" + } + }, + { + "target" : { + "containerPath" : "container:", + "identifier" : "WalletConnectModalTests", + "name" : "WalletConnectModalTests" + } + }, + { + "target" : { + "containerPath" : "container:", + "identifier" : "NotifyTests", + "name" : "NotifyTests" + } + } + ], + "version" : 1 +} diff --git a/Makefile b/Makefile index adf185529..32cdfdd66 100755 --- a/Makefile +++ b/Makefile @@ -16,21 +16,31 @@ ifeq "${EXISTS_FASTLANE}" "" endif @echo "All dependencies was installed" -test_setup: - defaults write com.apple.dt.XCBuild IgnoreFileSystemDeviceInodeChanges -bool YES - rm -rf test_results - mkdir test_results - build_all: - rm -rf test_results - set -o pipefail && xcodebuild -scheme "WalletConnect-Package" -destination "platform=iOS Simulator,name=iPhone 14" -derivedDataPath DerivedDataCache -clonedSourcePackagesDirPath ../SourcePackagesCache RELAY_HOST='$(RELAY_HOST)' PROJECT_ID='$(PROJECT_ID)' build-for-testing | xcpretty - set -o pipefail && xcodebuild -project "Example/ExampleApp.xcodeproj" -scheme "BuildAll" -destination "platform=iOS Simulator,name=iPhone 14" -derivedDataPath DerivedDataCache -clonedSourcePackagesDirPath ../SourcePackagesCache RELAY_HOST='$(RELAY_HOST)' PROJECT_ID='$(PROJECT_ID)' CAST_HOST='$(CAST_HOST)' build-for-testing | xcpretty - -build_dapp: - fastlane build scheme:DApp - -build_wallet: - fastlane build scheme:WalletApp + set -o pipefail && env NSUnbufferedIO=YES \ + xcodebuild \ + -scheme "WalletConnect-Package" \ + -destination "platform=iOS Simulator,name=iPhone 14" \ + -derivedDataPath DerivedDataCache \ + -clonedSourcePackagesDirPath ../SourcePackagesCache \ + RELAY_HOST='$(RELAY_HOST)' \ + PROJECT_ID='$(PROJECT_ID)' \ + build-for-testing \ + | xcbeautify + + set -o pipefail && env NSUnbufferedIO=YES \ + xcodebuild \ + -project "Example/ExampleApp.xcodeproj" \ + -scheme "BuildAll" \ + -destination "platform=iOS Simulator,name=iPhone 14" \ + -derivedDataPath DerivedDataCache \ + -clonedSourcePackagesDirPath ../SourcePackagesCache \ + RELAY_HOST='$(RELAY_HOST)' \ + PROJECT_ID='$(PROJECT_ID)' \ + CAST_HOST='$(CAST_HOST)' \ + JS_CLIENT_API_HOST='$(JS_CLIENT_API_HOST)' \ + build-for-testing \ + | xcbeautify echo_ui_tests: echo "EchoUITests disabled" @@ -38,79 +48,30 @@ echo_ui_tests: ui_tests: echo "UI Tests disabled" -unitxctestrun = $(shell find . -name '*WalletConnect-Package*.xctestrun') - -unit_tests: test_setup -ifneq ($(unitxctestrun),) - set -o pipefail && env NSUnbufferedIO=YES xcodebuild -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath DerivedDataCache -clonedSourcePackagesDirPath ../SourcePackagesCache -resultBundlePath 'test_results/UnitTests.xcresult' -xctestrun '$(unitxctestrun)' test-without-building | tee ./test_results/xcodebuild.log | xcpretty --report junit --output ./test_results/report.junit -else - set -o pipefail && env NSUnbufferedIO=YES xcodebuild -scheme WalletConnect-Package -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath DerivedDataCache -clonedSourcePackagesDirPath ../SourcePackagesCache -resultBundlePath 'test_results/UnitTests.xcresult' test | tee ./test_results/xcodebuild.log | xcpretty --report junit --output ./test_results/report.junit -endif - -integrationxctestrun = $(shell find . -name '*_IntegrationTests*.xctestrun') - -integration_tests: test_setup -ifneq ($(integrationxctestrun),) -# override ENV variables - plutil -replace TestConfigurations.0.TestTargets.0.EnvironmentVariables.RELAY_HOST -string $(RELAY_HOST) $(integrationxctestrun) - plutil -replace TestConfigurations.0.TestTargets.0.EnvironmentVariables.PROJECT_ID -string $(PROJECT_ID) $(integrationxctestrun) - plutil -replace TestConfigurations.0.TestTargets.0.EnvironmentVariables.GM_DAPP_PROJECT_ID -string $(GM_DAPP_PROJECT_ID) $(integrationxctestrun) - plutil -replace TestConfigurations.0.TestTargets.0.EnvironmentVariables.GM_DAPP_PROJECT_SECRET -string $(GM_DAPP_PROJECT_SECRET) $(integrationxctestrun) - plutil -replace TestConfigurations.0.TestTargets.0.EnvironmentVariables.CAST_HOST -string $(CAST_HOST) $(integrationxctestrun) -# test-without-building - set -o pipefail && env NSUnbufferedIO=YES xcodebuild -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath DerivedDataCache -clonedSourcePackagesDirPath ../SourcePackagesCache -resultBundlePath 'test_results/IntegrationTests.xcresult' -xctestrun '$(integrationxctestrun)' test-without-building | tee ./test_results/xcodebuild.log | xcpretty --report junit --output ./test_results/report.junit -else - set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project Example/ExampleApp.xcodeproj -scheme IntegrationTests -testPlan IntegrationTests -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath DerivedDataCache -clonedSourcePackagesDirPath ../SourcePackagesCache -resultBundlePath 'test_results/IntegrationTests.xcresult' RELAY_HOST='$(RELAY_HOST)' PROJECT_ID='$(PROJECT_ID)' GM_DAPP_PROJECT_ID='$(GM_DAPP_PROJECT_ID)' GM_DAPP_PROJECT_SECRET='$(GM_DAPP_PROJECT_SECRET)' CAST_HOST='$(CAST_HOST)' test | tee ./test_results/xcodebuild.log | xcpretty --report junit --output ./test_results/report.junit -endif - -relayxctestrun = $(shell find . -name '*_RelayIntegrationTests*.xctestrun') - -relay_tests: test_setup -ifneq ($(relayxctestrun),) -# override ENV variables - plutil -replace TestConfigurations.0.TestTargets.0.EnvironmentVariables.RELAY_HOST -string $(RELAY_HOST) $(relayxctestrun) - plutil -replace TestConfigurations.0.TestTargets.0.EnvironmentVariables.PROJECT_ID -string $(PROJECT_ID) $(relayxctestrun) -# test-without-building - set -o pipefail && env NSUnbufferedIO=YES xcodebuild -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath DerivedDataCache -resultBundlePath 'test_results/RelayIntegrationTests.xcresult' -xctestrun '$(relayxctestrun)' test-without-building | tee ./test_results/xcodebuild.log | xcpretty --report junit --output ./test_results/report.junit -else - set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project Example/ExampleApp.xcodeproj -scheme RelayIntegrationTests -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath DerivedDataCache -resultBundlePath 'test_results/RelayIntegrationTests.xcresult' RELAY_HOST='$(RELAY_HOST)' PROJECT_ID='$(PROJECT_ID)' test | tee ./test_results/xcodebuild.log | xcpretty --report junit --output ./test_results/report.junit -endif - -notifyxctestrun = $(shell find . -name '*_NotifyTests*.xctestrun') - -notify_tests: test_setup -ifneq ($(notifyxctestrun),) -# override ENV variables - plutil -replace TestConfigurations.0.TestTargets.0.EnvironmentVariables.RELAY_HOST -string $(RELAY_HOST) $(notifyxctestrun) - plutil -replace TestConfigurations.0.TestTargets.0.EnvironmentVariables.PROJECT_ID -string $(PROJECT_ID) $(notifyxctestrun) - plutil -replace TestConfigurations.0.TestTargets.0.EnvironmentVariables.GM_DAPP_PROJECT_ID -string $(GM_DAPP_PROJECT_ID) $(notifyxctestrun) - plutil -replace TestConfigurations.0.TestTargets.0.EnvironmentVariables.GM_DAPP_PROJECT_SECRET -string $(GM_DAPP_PROJECT_SECRET) $(notifyxctestrun) - plutil -replace TestConfigurations.0.TestTargets.0.EnvironmentVariables.CAST_HOST -string $(CAST_HOST) $(notifyxctestrun) -# test-without-building - set -o pipefail && env NSUnbufferedIO=YES xcodebuild -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath DerivedDataCache -clonedSourcePackagesDirPath ../SourcePackagesCache -resultBundlePath 'test_results/NotifyTests.xcresult' -xctestrun '$(notifyxctestrun)' test-without-building | tee ./test_results/xcodebuild.log | xcpretty --report junit --output ./test_results/report.junit -else - set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project Example/ExampleApp.xcodeproj -scheme NotifyTests -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath DerivedDataCache -resultBundlePath 'test_results/NotifyTests.xcresult' RELAY_HOST='$(RELAY_HOST)' PROJECT_ID='$(PROJECT_ID)' GM_DAPP_PROJECT_ID='$(GM_DAPP_PROJECT_ID)' GM_DAPP_PROJECT_SECRET='$(GM_DAPP_PROJECT_SECRET)' CAST_HOST='$(CAST_HOST)' test | tee ./test_results/xcodebuild.log | xcpretty --report junit --output ./test_results/report.junit -endif - -smokexctestrun = $(shell find . -name '*_SmokeTests*.xctestrun') - -smoke_tests: test_setup -ifneq ($(smokexctestrun),) -# override ENV variables - plutil -replace TestConfigurations.0.TestTargets.0.EnvironmentVariables.RELAY_HOST -string $(RELAY_HOST) $(smokexctestrun) - plutil -replace TestConfigurations.0.TestTargets.0.EnvironmentVariables.PROJECT_ID -string $(PROJECT_ID) $(smokexctestrun) -# test-without-building - set -o pipefail && env NSUnbufferedIO=YES xcodebuild -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath DerivedDataCache -clonedSourcePackagesDirPath ../SourcePackagesCache -resultBundlePath 'test_results/SmokeTests.xcresult' -xctestrun '$(smokexctestrun)' test-without-building | tee ./test_results/xcodebuild.log | xcpretty --report junit --output ./test_results/report.junit -else - set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project Example/ExampleApp.xcodeproj -scheme IntegrationTests -testPlan SmokeTests -destination 'platform=iOS Simulator,name=iPhone 14' -derivedDataPath DerivedDataCache -clonedSourcePackagesDirPath ../SourcePackagesCache -resultBundlePath 'test_results/SmokeTests.xcresult' RELAY_HOST='$(RELAY_HOST)' PROJECT_ID='$(PROJECT_ID)' test | tee ./test_results/xcodebuild.log | xcpretty --report junit --output ./test_results/report.junit -endif +unit_tests: + ./run_tests.sh --scheme WalletConnect-Package + +integration_tests: + ./run_tests.sh --scheme IntegrationTests --testplan IntegrationTests --project Example/ExampleApp.xcodeproj + +relay_tests: + ./run_tests.sh --scheme RelayIntegrationTests --project Example/ExampleApp.xcodeproj + +notify_tests: + ./run_tests.sh --scheme NotifyTests --project Example/ExampleApp.xcodeproj + +smoke_tests: + ./run_tests.sh --scheme IntegrationTests --testplan SmokeTests --project Example/ExampleApp.xcodeproj + +x_platform_protocol_tests: + ./run_tests.sh --scheme IntegrationTests --testplan XPlatformProtocolTests --project Example/ExampleApp.xcodeproj release_wallet: - fastlane release_testflight username:$(APPLE_ID) token:$(TOKEN) relay_host:$(RELAY_HOST) project_id:$(PROJECT_ID) --env WalletApp + fastlane release_testflight username:$(APPLE_ID) token:$(TOKEN) relay_host:$(RELAY_HOST) project_id:$(PROJECT_ID) sentry_dsn:$(WALLETAPP_SENTRY_DSN) mixpanel_token:$(MIXPANEL_TOKEN) --env WalletApp release_showcase: fastlane release_testflight username:$(APPLE_ID) token:$(TOKEN) relay_host:$(RELAY_HOST) project_id:$(PROJECT_ID) --env Showcase -release_all: - fastlane release_testflight username:$(APPLE_ID) token:$(TOKEN) relay_host:$(RELAY_HOST) project_id:$(PROJECT_ID) --env WalletApp +release_all: + fastlane release_testflight username:$(APPLE_ID) token:$(TOKEN) relay_host:$(RELAY_HOST) project_id:$(PROJECT_ID) sentry_dsn:$(WALLETAPP_SENTRY_DSN) mixpanel_token:$(MIXPANEL_TOKEN) --env WalletApp fastlane release_testflight username:$(APPLE_ID) token:$(TOKEN) relay_host:$(RELAY_HOST) project_id:$(PROJECT_ID) --env Showcase diff --git a/NotifyTests.xctestplan b/NotifyTests.xctestplan index 68ef8125e..fc50c4a84 100644 --- a/NotifyTests.xctestplan +++ b/NotifyTests.xctestplan @@ -9,6 +9,7 @@ } ], "defaultOptions" : { + "codeCoverage" : false, "environmentVariableEntries" : [ { "key" : "RELAY_HOST", diff --git a/Package.swift b/Package.swift index cbbb29165..8815d2a74 100644 --- a/Package.swift +++ b/Package.swift @@ -25,12 +25,12 @@ let package = Package( .library( name: "WalletConnectPairing", targets: ["WalletConnectPairing"]), + .library( + name: "WalletConnectNotify", + targets: ["WalletConnectNotify"]), .library( name: "WalletConnectPush", targets: ["WalletConnectPush"]), - .library( - name: "WalletConnectEcho", - targets: ["WalletConnectEcho"]), .library( name: "WalletConnectRouter", targets: ["WalletConnectRouter"]), @@ -73,16 +73,16 @@ let package = Package( path: "Sources/Auth"), .target( name: "Web3Wallet", - dependencies: ["Auth", "WalletConnectSign", "WalletConnectEcho", "WalletConnectVerify"], + dependencies: ["Auth", "WalletConnectSign", "WalletConnectPush", "WalletConnectVerify"], path: "Sources/Web3Wallet"), .target( - name: "WalletConnectPush", - dependencies: ["WalletConnectPairing", "WalletConnectEcho", "WalletConnectIdentity", "WalletConnectSync", "WalletConnectHistory"], - path: "Sources/WalletConnectPush"), + name: "WalletConnectNotify", + dependencies: ["WalletConnectPairing", "WalletConnectPush", "WalletConnectIdentity", "WalletConnectSigner"], + path: "Sources/WalletConnectNotify"), .target( - name: "WalletConnectEcho", + name: "WalletConnectPush", dependencies: ["WalletConnectNetworking", "WalletConnectJWT"], - path: "Sources/WalletConnectEcho"), + path: "Sources/WalletConnectPush"), .target( name: "WalletConnectRelay", dependencies: ["WalletConnectJWT"], @@ -100,7 +100,7 @@ let package = Package( dependencies: ["HTTPClient", "WalletConnectRelay"]), .target( name: "Web3Inbox", - dependencies: ["WalletConnectChat", "WalletConnectPush"]), + dependencies: ["WalletConnectChat", "WalletConnectNotify"]), .target( name: "WalletConnectSigner", dependencies: ["WalletConnectNetworking"]), @@ -157,7 +157,7 @@ let package = Package( dependencies: ["WalletConnectChat", "WalletConnectUtils", "TestingUtils"]), .testTarget( name: "NotifyTests", - dependencies: ["WalletConnectPush", "TestingUtils"]), + dependencies: ["WalletConnectNotify", "TestingUtils"]), .testTarget( name: "AuthTests", dependencies: ["Auth", "WalletConnectUtils", "TestingUtils", "WalletConnectVerify"]), diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index 26f3797be..8189bdea0 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -89,7 +89,7 @@ public class AuthClient: AuthClientProtocol { /// Query pending authentication requests /// - Returns: Pending authentication requests - public func getPendingRequests() throws -> [AuthRequest] { + public func getPendingRequests() throws -> [(AuthRequest, VerifyContext?)] { return try pendingRequestsProvider.getPendingRequests() } diff --git a/Sources/Auth/AuthClientFactory.swift b/Sources/Auth/AuthClientFactory.swift index 5bdc07a41..dbddf2a53 100644 --- a/Sources/Auth/AuthClientFactory.swift +++ b/Sources/Auth/AuthClientFactory.swift @@ -1,7 +1,6 @@ import Foundation public struct AuthClientFactory { - public static func create( metadata: AppMetadata, projectId: String, @@ -9,7 +8,6 @@ public struct AuthClientFactory { networkingClient: NetworkingInteractor, pairingRegisterer: PairingRegisterer ) -> AuthClient { - let logger = ConsoleLogger(loggingLevel: .off) let keyValueStorage = UserDefaults.standard let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") @@ -39,19 +37,19 @@ public struct AuthClientFactory { pairingRegisterer: PairingRegisterer, iatProvider: IATProvider ) -> AuthClient { - let kms = KeyManagementService(keychain: keychainStorage) let history = RPCHistoryFactory.createForNetwork(keyValueStorage: keyValueStorage) let messageFormatter = SIWECacaoFormatter() let appRequestService = AppRequestService(networkingInteractor: networkingClient, kms: kms, appMetadata: metadata, logger: logger, iatProvader: iatProvider) let verifyClient = VerifyClientFactory.create() + let verifyContextStore = CodableStore(defaults: keyValueStorage, identifier: VerifyStorageIdentifiers.context.rawValue) let messageVerifierFactory = MessageVerifierFactory(crypto: crypto) let signatureVerifier = messageVerifierFactory.create(projectId: projectId) let appRespondSubscriber = AppRespondSubscriber(networkingInteractor: networkingClient, logger: logger, rpcHistory: history, signatureVerifier: signatureVerifier, pairingRegisterer: pairingRegisterer, messageFormatter: messageFormatter) let walletErrorResponder = WalletErrorResponder(networkingInteractor: networkingClient, logger: logger, kms: kms, rpcHistory: history) - let walletRequestSubscriber = WalletRequestSubscriber(networkingInteractor: networkingClient, logger: logger, kms: kms, walletErrorResponder: walletErrorResponder, pairingRegisterer: pairingRegisterer, verifyClient: verifyClient) - let walletRespondService = WalletRespondService(networkingInteractor: networkingClient, logger: logger, kms: kms, rpcHistory: history, walletErrorResponder: walletErrorResponder) - let pendingRequestsProvider = PendingRequestsProvider(rpcHistory: history) + let walletRequestSubscriber = WalletRequestSubscriber(networkingInteractor: networkingClient, logger: logger, kms: kms, walletErrorResponder: walletErrorResponder, pairingRegisterer: pairingRegisterer, verifyClient: verifyClient, verifyContextStore: verifyContextStore) + let walletRespondService = WalletRespondService(networkingInteractor: networkingClient, logger: logger, kms: kms, rpcHistory: history, verifyContextStore: verifyContextStore, walletErrorResponder: walletErrorResponder, pairingRegisterer: pairingRegisterer) + let pendingRequestsProvider = PendingRequestsProvider(rpcHistory: history, verifyContextStore: verifyContextStore) return AuthClient( appRequestService: appRequestService, diff --git a/Sources/Auth/AuthClientProtocol.swift b/Sources/Auth/AuthClientProtocol.swift index d79048ca8..23d5cd55c 100644 --- a/Sources/Auth/AuthClientProtocol.swift +++ b/Sources/Auth/AuthClientProtocol.swift @@ -7,5 +7,5 @@ public protocol AuthClientProtocol { func formatMessage(payload: AuthPayload, address: String) throws -> String func respond(requestId: RPCID, signature: CacaoSignature, from account: Account) async throws func reject(requestId: RPCID) async throws - func getPendingRequests() throws -> [AuthRequest] + func getPendingRequests() throws -> [(AuthRequest, VerifyContext?)] } diff --git a/Sources/Auth/Services/Wallet/PendingRequestsProvider.swift b/Sources/Auth/Services/Wallet/PendingRequestsProvider.swift index 5050ef778..b351be66a 100644 --- a/Sources/Auth/Services/Wallet/PendingRequestsProvider.swift +++ b/Sources/Auth/Services/Wallet/PendingRequestsProvider.swift @@ -2,18 +2,24 @@ import Foundation class PendingRequestsProvider { private let rpcHistory: RPCHistory + private let verifyContextStore: CodableStore - init(rpcHistory: RPCHistory) { + init( + rpcHistory: RPCHistory, + verifyContextStore: CodableStore + ) { self.rpcHistory = rpcHistory + self.verifyContextStore = verifyContextStore } - public func getPendingRequests() throws -> [AuthRequest] { + public func getPendingRequests() throws -> [(AuthRequest, VerifyContext?)] { let pendingRequests: [AuthRequest] = rpcHistory.getPending() .filter {$0.request.method == "wc_authRequest"} .compactMap { guard let params = try? $0.request.params?.get(AuthRequestParams.self) else { return nil } return AuthRequest(id: $0.request.id!, topic: $0.topic, payload: params.payloadParams) } - return pendingRequests + + return pendingRequests.map { ($0, try? verifyContextStore.get(key: $0.id.string)) } } } diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift index d1b523f4b..c2a7030c0 100644 --- a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift +++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift @@ -9,6 +9,8 @@ class WalletRequestSubscriber { private let walletErrorResponder: WalletErrorResponder private let pairingRegisterer: PairingRegisterer private let verifyClient: VerifyClientProtocol + private let verifyContextStore: CodableStore + var onRequest: (((request: AuthRequest, context: VerifyContext?)) -> Void)? init( @@ -17,7 +19,8 @@ class WalletRequestSubscriber { kms: KeyManagementServiceProtocol, walletErrorResponder: WalletErrorResponder, pairingRegisterer: PairingRegisterer, - verifyClient: VerifyClientProtocol + verifyClient: VerifyClientProtocol, + verifyContextStore: CodableStore ) { self.networkingInteractor = networkingInteractor self.logger = logger @@ -25,6 +28,7 @@ class WalletRequestSubscriber { self.walletErrorResponder = walletErrorResponder self.pairingRegisterer = pairingRegisterer self.verifyClient = verifyClient + self.verifyContextStore = verifyContextStore subscribeForRequest() } @@ -33,10 +37,7 @@ class WalletRequestSubscriber { .sink { [unowned self] (payload: RequestSubscriptionPayload) in logger.debug("WalletRequestSubscriber: Received request") - pairingRegisterer.activate( - pairingTopic: payload.topic, - peerMetadata: payload.request.requester.metadata - ) + pairingRegisterer.setReceived(pairingTopic: payload.topic) let request = AuthRequest(id: payload.id, topic: payload.topic, payload: payload.request.payloadParams) @@ -45,9 +46,11 @@ class WalletRequestSubscriber { do { let origin = try await verifyClient.verifyOrigin(assertionId: assertionId) let verifyContext = verifyClient.createVerifyContext(origin: origin, domain: payload.request.payloadParams.domain) + verifyContextStore.set(verifyContext, forKey: request.id.string) onRequest?((request, verifyContext)) } catch { let verifyContext = verifyClient.createVerifyContext(origin: nil, domain: payload.request.payloadParams.domain) + verifyContextStore.set(verifyContext, forKey: request.id.string) onRequest?((request, verifyContext)) return } diff --git a/Sources/Auth/Services/Wallet/WalletRespondService.swift b/Sources/Auth/Services/Wallet/WalletRespondService.swift index 62bb3427a..a06a16027 100644 --- a/Sources/Auth/Services/Wallet/WalletRespondService.swift +++ b/Sources/Auth/Services/Wallet/WalletRespondService.swift @@ -8,19 +8,27 @@ actor WalletRespondService { private let networkingInteractor: NetworkInteracting private let kms: KeyManagementService private let rpcHistory: RPCHistory + private let verifyContextStore: CodableStore private let logger: ConsoleLogging private let walletErrorResponder: WalletErrorResponder + private let pairingRegisterer: PairingRegisterer - init(networkingInteractor: NetworkInteracting, - logger: ConsoleLogging, - kms: KeyManagementService, - rpcHistory: RPCHistory, - walletErrorResponder: WalletErrorResponder) { + init( + networkingInteractor: NetworkInteracting, + logger: ConsoleLogging, + kms: KeyManagementService, + rpcHistory: RPCHistory, + verifyContextStore: CodableStore, + walletErrorResponder: WalletErrorResponder, + pairingRegisterer: PairingRegisterer + ) { self.networkingInteractor = networkingInteractor self.logger = logger self.kms = kms self.rpcHistory = rpcHistory + self.verifyContextStore = verifyContextStore self.walletErrorResponder = walletErrorResponder + self.pairingRegisterer = pairingRegisterer } func respond(requestId: RPCID, signature: CacaoSignature, account: Account) async throws { @@ -31,14 +39,22 @@ actor WalletRespondService { let header = CacaoHeader(t: "eip4361") let payload = try authRequestParams.payloadParams.cacaoPayload(address: account.address) - let responseParams = AuthResponseParams(h: header, p: payload, s: signature) + let responseParams = AuthResponseParams(h: header, p: payload, s: signature) let response = RPCResponse(id: requestId, result: responseParams) try await networkingInteractor.respond(topic: topic, response: response, protocolMethod: AuthRequestProtocolMethod(), envelopeType: .type1(pubKey: keys.publicKey.rawRepresentation)) + + pairingRegisterer.activate( + pairingTopic: topic, + peerMetadata: authRequestParams.requester.metadata + ) + + verifyContextStore.delete(forKey: requestId.string) } func respondError(requestId: RPCID) async throws { try await walletErrorResponder.respondError(AuthError.userRejeted, requestId: requestId) + verifyContextStore.delete(forKey: requestId.string) } private func getAuthRequestParams(requestId: RPCID) throws -> AuthRequestParams { diff --git a/Sources/Auth/Types/Errors/AuthError.swift b/Sources/Auth/Types/Errors/AuthError.swift index 7f9256ebd..cf4accb16 100644 --- a/Sources/Auth/Types/Errors/AuthError.swift +++ b/Sources/Auth/Types/Errors/AuthError.swift @@ -2,6 +2,7 @@ import Foundation /// Authentication error public enum AuthError: Codable, Equatable, Error { + case methodUnsupported case userDisconnected case userRejeted case malformedResponseParams @@ -14,6 +15,8 @@ extension AuthError: Reason { init?(code: Int) { switch code { + case Self.methodUnsupported.code: + self = .methodUnsupported case Self.userRejeted.code: self = .userRejeted case Self.malformedResponseParams.code: @@ -31,6 +34,8 @@ extension AuthError: Reason { public var code: Int { switch self { + case .methodUnsupported: + return 10001 case .userDisconnected: return 6000 case .userRejeted: @@ -48,6 +53,8 @@ extension AuthError: Reason { public var message: String { switch self { + case .methodUnsupported: + return "Method Unsupported" case .userRejeted: return "Auth request rejected by user" case .malformedResponseParams: diff --git a/Sources/Chat/ChatClientFactory.swift b/Sources/Chat/ChatClientFactory.swift index 558b3f7b2..d8f336381 100644 --- a/Sources/Chat/ChatClientFactory.swift +++ b/Sources/Chat/ChatClientFactory.swift @@ -28,7 +28,7 @@ public struct ChatClientFactory { historyClient: HistoryClient ) -> ChatClient { let kms = KeyManagementService(keychain: keychain) - let serializer = Serializer(kms: kms) + let serializer = Serializer(kms: kms, logger: logger) let historyService = HistoryService(historyClient: historyClient, seiralizer: serializer) let messageStore = KeyedDatabase(storage: storage, identifier: ChatStorageIdentifiers.messages.rawValue) let receivedInviteStore = KeyedDatabase(storage: storage, identifier: ChatStorageIdentifiers.receivedInvites.rawValue) diff --git a/Sources/Chat/Storage/ChatStorage.swift b/Sources/Chat/Storage/ChatStorage.swift index 400536e9f..67ff84402 100644 --- a/Sources/Chat/Storage/ChatStorage.swift +++ b/Sources/Chat/Storage/ChatStorage.swift @@ -102,10 +102,15 @@ final class ChatStorage { // MARK: - Configuration func initializeStores(for account: Account) async throws { - try await sentInviteStore.initialize(for: account) - try await threadStore.initialize(for: account) - try await inviteKeyStore.initialize(for: account) - try await receivedInviteStatusStore.initialize(for: account) + try await sentInviteStore.create(for: account) + try await threadStore.create(for: account) + try await inviteKeyStore.create(for: account) + try await receivedInviteStatusStore.create(for: account) + + try await sentInviteStore.subscribe(for: account) + try await threadStore.subscribe(for: account) + try await inviteKeyStore.subscribe(for: account) + try await receivedInviteStatusStore.subscribe(for: account) } func initializeDelegates() async throws { @@ -132,9 +137,9 @@ final class ChatStorage { receivedInvitesPublisherSubject.send(getReceivedInvites(account: account)) } - try sentInviteStore.setupSubscriptions(account: account) - try threadStore.setupSubscriptions(account: account) - try inviteKeyStore.setupSubscriptions(account: account) + try sentInviteStore.setupDatabaseSubscriptions(account: account) + try threadStore.setupDatabaseSubscriptions(account: account) + try inviteKeyStore.setupDatabaseSubscriptions(account: account) } // MARK: - Invites @@ -280,7 +285,7 @@ private extension ChatStorage { func setupSyncSubscriptions() { sentInviteStore.syncUpdatePublisher.sink { [unowned self] topic, account, update in switch update { - case .set(let object): + case .set(let object), .update(let object): self.sentInviteStoreDelegate.onUpdate(object) case .delete(let object): self.sentInviteStoreDelegate.onDelete(object) @@ -289,7 +294,7 @@ private extension ChatStorage { threadStore.syncUpdatePublisher.sink { [unowned self] topic, account, update in switch update { - case .set(let object): + case .set(let object), .update(let object): self.threadStoreDelegate.onUpdate(object, storage: self) case .delete(let object): self.threadStoreDelegate.onDelete(object) @@ -298,7 +303,7 @@ private extension ChatStorage { inviteKeyStore.syncUpdatePublisher.sink { [unowned self] topic, account, update in switch update { - case .set(let object): + case .set(let object), .update(let object): self.inviteKeyDelegate.onUpdate(object, account: account) case .delete(let object): self.inviteKeyDelegate.onDelete(object) @@ -307,7 +312,7 @@ private extension ChatStorage { receivedInviteStatusStore.syncUpdatePublisher.sink { [unowned self] topic, account, update in switch update { - case .set(let object): + case .set(let object), .update(let object): self.receiviedInviteStatusDelegate.onUpdate(object, storage: self, account: account) case .delete(let object): self.receiviedInviteStatusDelegate.onDelete(object) diff --git a/Sources/Chat/Types/Payloads/AcceptPayload.swift b/Sources/Chat/Types/Payloads/AcceptPayload.swift index 52c403c31..be06d1aa6 100644 --- a/Sources/Chat/Types/Payloads/AcceptPayload.swift +++ b/Sources/Chat/Types/Payloads/AcceptPayload.swift @@ -10,7 +10,11 @@ struct AcceptPayload: JWTClaimsCodable { let aud: String // proposer/inviter blockchain account (did:pkh) let sub: String // public key sent by the responder/invitee - let act: String // description of action intent + let act: String? // description of action intent + + static var action: String? { + return "invite_approval" + } } struct Wrapper: JWTWrapper { @@ -49,7 +53,7 @@ struct AcceptPayload: JWTClaimsCodable { ksu: keyserver.absoluteString, aud: inviterAccount.did, sub: inviteePublicKey.did(variant: .X25519), - act: "invite_approval" + act: Claims.action ) } } diff --git a/Sources/Chat/Types/Payloads/InvitePayload.swift b/Sources/Chat/Types/Payloads/InvitePayload.swift index f33bea807..bd6fdef67 100644 --- a/Sources/Chat/Types/Payloads/InvitePayload.swift +++ b/Sources/Chat/Types/Payloads/InvitePayload.swift @@ -23,7 +23,11 @@ struct InvitePayload: JWTClaimsCodable { let aud: String // responder/invitee blockchain account (did:pkh) let sub: String // opening message included in the invite let pke: String // proposer/inviter public key (did:key) - let act: String // description of action intent + let act: String? // description of action intent + + static var action: String? { + return "invite_proposal" + } } let keyserver: URL @@ -54,7 +58,7 @@ struct InvitePayload: JWTClaimsCodable { aud: inviteeAccount.did, sub: message, pke: inviterPublicKey.did(variant: .X25519), - act: "invite_proposal" + act: Claims.action ) } } diff --git a/Sources/Chat/Types/Payloads/MessagePayload.swift b/Sources/Chat/Types/Payloads/MessagePayload.swift index bb0632d5d..445612dc9 100644 --- a/Sources/Chat/Types/Payloads/MessagePayload.swift +++ b/Sources/Chat/Types/Payloads/MessagePayload.swift @@ -10,10 +10,14 @@ struct MessagePayload: JWTClaimsCodable { let aud: String // recipient blockchain account (did:pkh) let sub: String // message sent by the author account - let act: String // description of action intent + let act: String? // description of action intent // TODO: Media not implemented // public let xma: Media? + + static var action: String? { + return "chat_message" + } } struct Wrapper: JWTWrapper { @@ -52,7 +56,7 @@ struct MessagePayload: JWTClaimsCodable { ksu: keyserver.absoluteString, aud: DIDPKH(account: recipientAccount).string, sub: message, - act: "chat_message" + act: Claims.action ) } } diff --git a/Sources/Chat/Types/Payloads/ReceiptPayload.swift b/Sources/Chat/Types/Payloads/ReceiptPayload.swift index c5206922e..90362b305 100644 --- a/Sources/Chat/Types/Payloads/ReceiptPayload.swift +++ b/Sources/Chat/Types/Payloads/ReceiptPayload.swift @@ -10,7 +10,11 @@ struct ReceiptPayload: JWTClaimsCodable { let sub: String // hash of the message received let aud: String // sender blockchain account (did:pkh) - let act: String // description of action intent + let act: String? // description of action intent + + static var action: String? { + return "chat_receipt" + } } struct Wrapper: JWTWrapper { @@ -49,7 +53,7 @@ struct ReceiptPayload: JWTClaimsCodable { ksu: keyserver.absoluteString, sub: messageHash, aud: DIDPKH(account: senderAccount).string, - act: "chat_receipt" + act: Claims.action ) } } diff --git a/Sources/HTTPClient/HTTPClient.swift b/Sources/HTTPClient/HTTPClient.swift index 0fb4c8a68..99eeca29a 100644 --- a/Sources/HTTPClient/HTTPClient.swift +++ b/Sources/HTTPClient/HTTPClient.swift @@ -3,4 +3,5 @@ import Foundation public protocol HTTPClient { func request(_ type: T.Type, at service: HTTPService) async throws -> T func request(service: HTTPService) async throws + func updateHost(host: String) async } diff --git a/Sources/HTTPClient/HTTPError.swift b/Sources/HTTPClient/HTTPError.swift index 446c8cbf2..959663bcc 100644 --- a/Sources/HTTPClient/HTTPError.swift +++ b/Sources/HTTPClient/HTTPError.swift @@ -1,10 +1,27 @@ import Foundation -enum HTTPError: Error { +public enum HTTPError: Error, Equatable { case malformedURL(HTTPService) + case couldNotConnect case dataTaskError(Error) case noResponse case badStatusCode(Int) case responseDataNil case jsonDecodeFailed(Error, Data) + + public static func ==(lhs: HTTPError, rhs: HTTPError) -> Bool { + switch (lhs, rhs) { + case (.malformedURL, .malformedURL), + (.couldNotConnect, .couldNotConnect), + (.noResponse, .noResponse), + (.responseDataNil, .responseDataNil), + (.dataTaskError, .dataTaskError), + (.badStatusCode, .badStatusCode), + (.jsonDecodeFailed, .jsonDecodeFailed): + return true + + default: + return false + } + } } diff --git a/Sources/HTTPClient/HTTPNetworkClient.swift b/Sources/HTTPClient/HTTPNetworkClient.swift index 1cb5da106..a00ca2119 100644 --- a/Sources/HTTPClient/HTTPNetworkClient.swift +++ b/Sources/HTTPClient/HTTPNetworkClient.swift @@ -2,7 +2,7 @@ import Foundation public actor HTTPNetworkClient: HTTPClient { - let host: String + private var host: String private let session: URLSession @@ -31,6 +31,10 @@ public actor HTTPNetworkClient: HTTPClient { } } } + + public func updateHost(host: String) async { + self.host = host + } private func request(_ type: T.Type, at service: HTTPService, completion: @escaping (Result) -> Void) { guard let request = service.resolve(for: host) else { @@ -67,6 +71,9 @@ public actor HTTPNetworkClient: HTTPClient { } private static func validate(_ urlResponse: URLResponse?, _ error: Error?) throws { + if let error = (error as? NSError), error.code == -1004 { + throw HTTPError.couldNotConnect + } if let error = error { throw HTTPError.dataTaskError(error) } diff --git a/Sources/WalletConnectEcho/Echo.swift b/Sources/WalletConnectEcho/Echo.swift deleted file mode 100644 index 702033cb5..000000000 --- a/Sources/WalletConnectEcho/Echo.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation - -public class Echo { - static public let echoHost = "echo.walletconnect.com" - public static var instance: EchoClient = { - guard let config = Echo.config else { - fatalError("Error - you must call Echo.configure(_:) before accessing the shared instance.") - } - - return EchoClientFactory.create( - projectId: Networking.projectId, - echoHost: config.echoHost, - environment: config.environment) - }() - - private static var config: Config? - - private init() { } - - /// Echo instance config method - /// - Parameter clientId: https://github.com/WalletConnect/walletconnect-docs/blob/main/docs/specs/clients/core/relay/relay-client-auth.md#overview - static public func configure( - echoHost: String = echoHost, - environment: APNSEnvironment - ) { - Echo.config = Echo.Config(echoHost: echoHost, environment: environment) - } -} diff --git a/Sources/WalletConnectEcho/EchoClient.swift b/Sources/WalletConnectEcho/EchoClient.swift deleted file mode 100644 index d7724ec5a..000000000 --- a/Sources/WalletConnectEcho/EchoClient.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation - -public class EchoClient: EchoClientProtocol { - private let registerService: EchoRegisterService - - init(registerService: EchoRegisterService) { - self.registerService = registerService - } - - public func register(deviceToken: Data) async throws { - try await registerService.register(deviceToken: deviceToken) - } - -#if DEBUG - public func register(deviceToken: String) async throws { - try await registerService.register(deviceToken: deviceToken) - } -#endif -} diff --git a/Sources/WalletConnectEcho/EchoClientFactory.swift b/Sources/WalletConnectEcho/EchoClientFactory.swift deleted file mode 100644 index 563aeef9d..000000000 --- a/Sources/WalletConnectEcho/EchoClientFactory.swift +++ /dev/null @@ -1,35 +0,0 @@ -import Foundation - -public struct EchoClientFactory { - public static func create(projectId: String, - echoHost: String, - environment: APNSEnvironment) -> EchoClient { - - let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") - - return EchoClientFactory.create( - projectId: projectId, - echoHost: echoHost, - keychainStorage: keychainStorage, - environment: environment) - } - - public static func create(projectId: String, - echoHost: String, - keychainStorage: KeychainStorageProtocol, - environment: APNSEnvironment) -> EchoClient { - - let httpClient = HTTPNetworkClient(host: echoHost) - - let clientIdStorage = ClientIdStorage(keychain: keychainStorage) - - let echoAuthenticator = EchoAuthenticator(clientIdStorage: clientIdStorage, echoHost: echoHost) - - let logger = ConsoleLogger(loggingLevel: .debug) - - let registerService = EchoRegisterService(httpClient: httpClient, projectId: projectId, clientIdStorage: clientIdStorage, echoAuthenticator: echoAuthenticator, logger: logger, environment: environment) - - return EchoClient( - registerService: registerService) - } -} diff --git a/Sources/WalletConnectEcho/EchoImports.swift b/Sources/WalletConnectEcho/EchoImports.swift deleted file mode 100644 index 463cb7c23..000000000 --- a/Sources/WalletConnectEcho/EchoImports.swift +++ /dev/null @@ -1,4 +0,0 @@ -#if !CocoaPods -@_exported import WalletConnectNetworking -@_exported import WalletConnectJWT -#endif diff --git a/Sources/WalletConnectEcho/Register/EchoRegisterService.swift b/Sources/WalletConnectEcho/Register/EchoRegisterService.swift deleted file mode 100644 index b3df306ce..000000000 --- a/Sources/WalletConnectEcho/Register/EchoRegisterService.swift +++ /dev/null @@ -1,62 +0,0 @@ -import Foundation - -actor EchoRegisterService { - private let httpClient: HTTPClient - private let projectId: String - private let logger: ConsoleLogging - private let environment: APNSEnvironment - private let echoAuthenticator: EchoAuthenticating - private let clientIdStorage: ClientIdStoring - - enum Errors: Error { - case registrationFailed - } - - init(httpClient: HTTPClient, - projectId: String, - clientIdStorage: ClientIdStoring, - echoAuthenticator: EchoAuthenticating, - logger: ConsoleLogging, - environment: APNSEnvironment) { - self.httpClient = httpClient - self.clientIdStorage = clientIdStorage - self.echoAuthenticator = echoAuthenticator - self.projectId = projectId - self.logger = logger - self.environment = environment - } - - func register(deviceToken: Data) async throws { - let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) } - let token = tokenParts.joined() - let echoAuthToken = try echoAuthenticator.createAuthToken() - let clientId = try clientIdStorage.getClientId() - let clientIdMutlibase = try DIDKey(did: clientId).multibase(variant: .ED25519) - logger.debug("APNS device token: \(token)") - let response = try await httpClient.request( - EchoResponse.self, - at: EchoAPI.register(clientId: clientIdMutlibase, token: token, projectId: projectId, environment: environment, auth: echoAuthToken) - ) - guard response.status == .success else { - throw Errors.registrationFailed - } - logger.debug("Successfully registered at Echo Server") - } - -#if DEBUG - public func register(deviceToken: String) async throws { - let echoAuthToken = try echoAuthenticator.createAuthToken() - let clientId = try clientIdStorage.getClientId() - let clientIdMutlibase = try DIDKey(did: clientId).multibase(variant: .ED25519) - let response = try await httpClient.request( - EchoResponse.self, - at: EchoAPI.register(clientId: clientIdMutlibase, token: deviceToken, projectId: projectId, environment: environment, auth: echoAuthToken) - ) - guard response.status == .success else { - throw Errors.registrationFailed - } - logger.debug("Successfully registered at Echo Server") - } -#endif -} - diff --git a/Sources/WalletConnectHistory/HistoryClientFactory.swift b/Sources/WalletConnectHistory/HistoryClientFactory.swift index c328ff4eb..5168430a3 100644 --- a/Sources/WalletConnectHistory/HistoryClientFactory.swift +++ b/Sources/WalletConnectHistory/HistoryClientFactory.swift @@ -14,7 +14,7 @@ class HistoryClientFactory { static func create(historyUrl: String, relayUrl: String, keychain: KeychainStorageProtocol) -> HistoryClient { let clientIdStorage = ClientIdStorage(keychain: keychain) let kms = KeyManagementService(keychain: keychain) - let serializer = Serializer(kms: kms) + let serializer = Serializer(kms: kms, logger: ConsoleLogger(prefix: "🔐", loggingLevel: .off)) let historyNetworkService = HistoryNetworkService(clientIdStorage: clientIdStorage) return HistoryClient( historyUrl: historyUrl, diff --git a/Sources/WalletConnectIdentity/IdentityClient.swift b/Sources/WalletConnectIdentity/IdentityClient.swift index 8ec6d2943..57335fb79 100644 --- a/Sources/WalletConnectIdentity/IdentityClient.swift +++ b/Sources/WalletConnectIdentity/IdentityClient.swift @@ -72,4 +72,9 @@ public final class IdentityClient { public func getInviteKey(for account: Account) throws -> AgreementPublicKey { return try identityStorage.getInviteKey(for: account) } + + public func isIdentityRegistered(account: Account) -> Bool { + let key = try? identityStorage.getIdentityKey(for: account) + return key != nil + } } diff --git a/Sources/WalletConnectIdentity/IdentityService.swift b/Sources/WalletConnectIdentity/IdentityService.swift index 8a1a984a8..5f7fc0431 100644 --- a/Sources/WalletConnectIdentity/IdentityService.swift +++ b/Sources/WalletConnectIdentity/IdentityService.swift @@ -48,7 +48,7 @@ actor IdentityService { let inviteKey = try kms.createX25519KeyPair() let invitePublicKey = DIDKey(rawData: inviteKey.rawRepresentation) - let idAuth = try makeIDAuth(account: account, issuer: invitePublicKey, kind: .registerInvite) + let idAuth = try makeIDAuth(account: account, issuer: invitePublicKey, claims: RegisterInviteClaims.self) try await networkService.registerInvite(idAuth: idAuth) return try storage.saveInviteKey(inviteKey, for: account) @@ -57,7 +57,7 @@ actor IdentityService { func unregister(account: Account, onSign: SigningCallback) async throws { let identityKey = try storage.getIdentityKey(for: account) let identityPublicKey = DIDKey(rawData: identityKey.publicKey.rawRepresentation) - let idAuth = try makeIDAuth(account: account, issuer: identityPublicKey, kind: .unregisterIdentity) + let idAuth = try makeIDAuth(account: account, issuer: identityPublicKey, claims: UnregisterIdentityClaims.self) try await networkService.removeIdentity(idAuth: idAuth) try storage.removeIdentityKey(for: account) } @@ -65,7 +65,7 @@ actor IdentityService { func goPrivate(account: Account) async throws -> AgreementPublicKey { let inviteKey = try storage.getInviteKey(for: account) let invitePublicKey = DIDKey(rawData: inviteKey.rawRepresentation) - let idAuth = try makeIDAuth(account: account, issuer: invitePublicKey, kind: .unregisterInvite) + let idAuth = try makeIDAuth(account: account, issuer: invitePublicKey, claims: UnregisterInviteClaims.self) try await networkService.removeInvite(idAuth: idAuth) try storage.removeInviteKey(for: account) @@ -113,11 +113,10 @@ private extension IdentityService { } } - func makeIDAuth(account: Account, issuer: DIDKey, kind: IDAuthPayload.Kind) throws -> String { + func makeIDAuth(account: Account, issuer: DIDKey, claims: Claims.Type) throws -> String { let identityKey = try storage.getIdentityKey(for: account) - let payload = IDAuthPayload( - kind: kind, + let payload = IDAuthPayload( keyserver: keyserverURL, account: account, invitePublicKey: issuer diff --git a/Sources/WalletConnectIdentity/Types/IDAuthClaims.swift b/Sources/WalletConnectIdentity/Types/IDAuthClaims.swift new file mode 100644 index 000000000..55c47a7df --- /dev/null +++ b/Sources/WalletConnectIdentity/Types/IDAuthClaims.swift @@ -0,0 +1,13 @@ +import Foundation + +protocol IDAuthClaims: JWTClaims { + var iss: String { get } + var sub: String { get } + var aud: String { get } + var iat: UInt64 { get } + var exp: UInt64 { get } + var pkh: String { get } + var act: String? { get } + + init(iss: String, sub: String, aud: String, iat: UInt64, exp: UInt64, pkh: String, act: String?) +} diff --git a/Sources/WalletConnectIdentity/Types/IDAuthPayload.swift b/Sources/WalletConnectIdentity/Types/IDAuthPayload.swift index f8dcc7226..6b418538f 100644 --- a/Sources/WalletConnectIdentity/Types/IDAuthPayload.swift +++ b/Sources/WalletConnectIdentity/Types/IDAuthPayload.swift @@ -1,52 +1,26 @@ import Foundation -struct IDAuthPayload: JWTClaimsCodable { +struct IDAuthPayload: JWTClaimsCodable { enum Errors: Error { case undefinedKind } - enum Kind: String { - case registerInvite = "register_invite" - case unregisterInvite = "unregister_invite" - case unregisterIdentity = "unregister_identity" - - init(rawValue: String) throws { - guard let kind = Kind(rawValue: rawValue) else { - throw Errors.undefinedKind - } - self = kind - } - } - struct Wrapper: JWTWrapper { let jwtString: String } - struct Claims: JWTClaims { - let iss: String - let sub: String - let aud: String - let iat: UInt64 - let exp: UInt64 - let pkh: String - let act: String - } - - let kind: Kind let keyserver: URL let account: Account let invitePublicKey: DIDKey - init(kind: Kind, keyserver: URL, account: Account, invitePublicKey: DIDKey) { - self.kind = kind + init(keyserver: URL, account: Account, invitePublicKey: DIDKey) { self.keyserver = keyserver self.account = account self.invitePublicKey = invitePublicKey } init(claims: Claims) throws { - self.kind = try Kind(rawValue: claims.act) self.keyserver = try claims.aud.asURL() self.account = try Account(DIDPKHString: claims.pkh) self.invitePublicKey = try DIDKey(did: claims.sub) @@ -60,7 +34,7 @@ struct IDAuthPayload: JWTClaimsCodable { iat: defaultIatMilliseconds(), exp: expiry(days: 30), pkh: account.did, - act: kind.rawValue + act: Claims.action ) } } diff --git a/Sources/WalletConnectIdentity/Types/RegisterInviteClaims.swift b/Sources/WalletConnectIdentity/Types/RegisterInviteClaims.swift new file mode 100644 index 000000000..2a32ea8c3 --- /dev/null +++ b/Sources/WalletConnectIdentity/Types/RegisterInviteClaims.swift @@ -0,0 +1,15 @@ +import Foundation + +struct RegisterInviteClaims: IDAuthClaims { + let iss: String + let sub: String + let aud: String + let iat: UInt64 + let exp: UInt64 + let pkh: String + let act: String? + + static var action: String? { + return "register_invite" + } +} diff --git a/Sources/WalletConnectIdentity/Types/UnregisterIdentityClaims.swift b/Sources/WalletConnectIdentity/Types/UnregisterIdentityClaims.swift new file mode 100644 index 000000000..881c988ba --- /dev/null +++ b/Sources/WalletConnectIdentity/Types/UnregisterIdentityClaims.swift @@ -0,0 +1,15 @@ +import Foundation + +struct UnregisterIdentityClaims: IDAuthClaims { + let iss: String + let sub: String + let aud: String + let iat: UInt64 + let exp: UInt64 + let pkh: String + let act: String? + + static var action: String? { + return "unregister_identity" + } +} diff --git a/Sources/WalletConnectIdentity/Types/UnregisterInviteClaims.swift b/Sources/WalletConnectIdentity/Types/UnregisterInviteClaims.swift new file mode 100644 index 000000000..9b3452051 --- /dev/null +++ b/Sources/WalletConnectIdentity/Types/UnregisterInviteClaims.swift @@ -0,0 +1,15 @@ +import Foundation + +struct UnregisterInviteClaims: IDAuthClaims { + let iss: String + let sub: String + let aud: String + let iat: UInt64 + let exp: UInt64 + let pkh: String + let act: String? + + static var action: String? { + return "unregister_invite" + } +} diff --git a/Sources/WalletConnectJWT/JWTDecodable.swift b/Sources/WalletConnectJWT/JWTDecodable.swift index 59b1871b1..2f96ae072 100644 --- a/Sources/WalletConnectJWT/JWTDecodable.swift +++ b/Sources/WalletConnectJWT/JWTDecodable.swift @@ -10,6 +10,9 @@ public protocol JWTClaims: JWTEncodable { var iss: String { get } var iat: UInt64 { get } var exp: UInt64 { get } + var act: String? { get } + + static var action: String? { get } } public protocol JWTClaimsCodable { @@ -32,6 +35,9 @@ extension JWTClaimsCodable { guard try JWTValidator(jwtString: wrapper.jwtString).isValid(publicKey: signingPublicKey) else { throw JWTError.signatureVerificationFailed } + guard Claims.action == jwt.claims.act + else { throw JWTError.actMismatch } + return (try Self.init(claims: jwt.claims), jwt.claims) } diff --git a/Sources/WalletConnectJWT/JWTError.swift b/Sources/WalletConnectJWT/JWTError.swift index 0d84e2b5f..90cc40e14 100644 --- a/Sources/WalletConnectJWT/JWTError.swift +++ b/Sources/WalletConnectJWT/JWTError.swift @@ -7,4 +7,5 @@ enum JWTError: Error { case noSignature case invalidJWTString case signatureVerificationFailed + case actMismatch } diff --git a/Sources/WalletConnectKMS/Serialiser/Serializer.swift b/Sources/WalletConnectKMS/Serialiser/Serializer.swift index dcd8f984e..7a3dcf739 100644 --- a/Sources/WalletConnectKMS/Serialiser/Serializer.swift +++ b/Sources/WalletConnectKMS/Serialiser/Serializer.swift @@ -1,23 +1,43 @@ import Foundation +import Combine public class Serializer: Serializing { - enum Errors: String, Error { - case symmetricKeyForTopicNotFound + enum Errors: Error, CustomStringConvertible { + case symmetricKeyForTopicNotFound(String) case publicKeyForTopicNotFound + + var description: String { + switch self { + case .symmetricKeyForTopicNotFound(let topic): + return "Error: Symmetric key for topic '\(topic)' was not found." + case .publicKeyForTopicNotFound: + return "Error: Public key for topic was not found." + } + } } private let kms: KeyManagementServiceProtocol private let codec: Codec + private let logger: ConsoleLogging + public var logsPublisher: AnyPublisher { + logger.logsPublisher.eraseToAnyPublisher() + } - init(kms: KeyManagementServiceProtocol, codec: Codec = ChaChaPolyCodec()) { + init(kms: KeyManagementServiceProtocol, codec: Codec = ChaChaPolyCodec(), logger: ConsoleLogging) { self.kms = kms self.codec = codec + self.logger = logger } - public init(kms: KeyManagementServiceProtocol) { + public init(kms: KeyManagementServiceProtocol, logger: ConsoleLogging) { self.kms = kms self.codec = ChaChaPolyCodec() + self.logger = logger + } + + public func setLogging(level: LoggingLevel) { + logger.setLogging(level: level) } /// Encrypts and serializes an object @@ -29,7 +49,9 @@ public class Serializer: Serializing { public func serialize(topic: String, encodable: Encodable, envelopeType: Envelope.EnvelopeType) throws -> String { let messageJson = try encodable.json() guard let symmetricKey = kms.getSymmetricKeyRepresentable(for: topic) else { - throw Errors.symmetricKeyForTopicNotFound + let error = Errors.symmetricKeyForTopicNotFound(topic) + logger.error("\(error)") + throw error } let sealbox = try codec.encode(plaintext: messageJson, symmetricKey: symmetricKey) return Envelope(type: envelopeType, sealbox: sealbox).serialised() @@ -53,15 +75,29 @@ public class Serializer: Serializing { private func handleType0Envelope(_ topic: String, _ envelope: Envelope) throws -> (T, Data) { if let symmetricKey = kms.getSymmetricKeyRepresentable(for: topic) { - return try decode(sealbox: envelope.sealbox, symmetricKey: symmetricKey) + do { + let decoded: (T, Data) = try decode(sealbox: envelope.sealbox, symmetricKey: symmetricKey) + logger.debug("Decoded: \(decoded.0)") + return decoded + } + catch { + logger.error("\(error)") + throw error + } } else { - throw Errors.symmetricKeyForTopicNotFound + let error = Errors.symmetricKeyForTopicNotFound(topic) + logger.error("\(error)") + throw error } } private func handleType1Envelope(_ topic: String, peerPubKey: Data, sealbox: Data) throws -> (T, String, Data) { guard let selfPubKey = kms.getPublicKey(for: topic) - else { throw Errors.publicKeyForTopicNotFound } + else { + let error = Errors.publicKeyForTopicNotFound + logger.error("\(error)") + throw error + } let agreementKeys = try kms.performKeyAgreement(selfPublicKey: selfPubKey, peerPublicKey: peerPubKey.toHexString()) let decodedType: (object: T, data: Data) = try decode(sealbox: sealbox, symmetricKey: agreementKeys.sharedKey.rawRepresentation) @@ -71,7 +107,13 @@ public class Serializer: Serializing { } private func decode(sealbox: Data, symmetricKey: Data) throws -> (T, Data) { - let decryptedData = try codec.decode(sealbox: sealbox, symmetricKey: symmetricKey) - return (try JSONDecoder().decode(T.self, from: decryptedData), decryptedData) + do { + let decryptedData = try codec.decode(sealbox: sealbox, symmetricKey: symmetricKey) + let decodedType = try JSONDecoder().decode(T.self, from: decryptedData) + return (decodedType, decryptedData) + } catch { + logger.error("Failed to decode with error: \(error)") + throw error + } } } diff --git a/Sources/WalletConnectKMS/Serialiser/Serializing.swift b/Sources/WalletConnectKMS/Serialiser/Serializing.swift index 5982ce660..c9fc25cd2 100644 --- a/Sources/WalletConnectKMS/Serialiser/Serializing.swift +++ b/Sources/WalletConnectKMS/Serialiser/Serializing.swift @@ -1,6 +1,9 @@ import Foundation +import Combine public protocol Serializing { + var logsPublisher: AnyPublisher {get} + func setLogging(level: LoggingLevel) func serialize(topic: String, encodable: Encodable, envelopeType: Envelope.EnvelopeType) throws -> String /// - derivedTopic: topic derived from symmetric key as a result of key exchange if peers has sent envelope(type1) prefixed with it's public key func deserialize(topic: String, encodedEnvelope: String) throws -> (T, derivedTopic: String?, decryptedPayload: Data) diff --git a/Sources/WalletConnectModal/Mocks/Listing+Mocks.swift b/Sources/WalletConnectModal/Mocks/Listing+Mocks.swift index 138b3c84a..b6f33a2a1 100644 --- a/Sources/WalletConnectModal/Mocks/Listing+Mocks.swift +++ b/Sources/WalletConnectModal/Mocks/Listing+Mocks.swift @@ -2,8 +2,62 @@ import Foundation extension Listing { static let stubList: [Listing] = [ - Listing(id: UUID().uuidString, name: "Sample Wallet", homepage: "https://example.com", order: 1, imageId: UUID().uuidString, app: Listing.App(ios: "https://example.com/download-ios", mac: "https://example.com/download-mac", safari: "https://example.com/download-safari"), mobile: Listing.Mobile(native: "sampleapp://deeplink", universal: "https://example.com/universal")), - Listing(id: UUID().uuidString, name: "Awesome Wallet", homepage: "https://example.com/awesome", order: 2, imageId: UUID().uuidString, app: Listing.App(ios: "https://example.com/download-ios", mac: "https://example.com/download-mac", safari: "https://example.com/download-safari"), mobile: Listing.Mobile(native: "awesomeapp://deeplink", universal: "https://example.com/awesome/universal")), - Listing(id: UUID().uuidString, name: "Cool Wallet", homepage: "https://example.com/cool", order: 3, imageId: UUID().uuidString, app: Listing.App(ios: "https://example.com/download-ios", mac: "https://example.com/download-mac", safari: "https://example.com/download-safari"), mobile: Listing.Mobile(native: "coolapp://deeplink", universal: "https://example.com/cool/universal")) + Listing( + id: UUID().uuidString, + name: "Sample Wallet", + homepage: "https://example.com", + order: 1, + imageId: UUID().uuidString, + app: Listing.App( + ios: "https://example.com/download-ios", + browser: "https://example.com/download-safari" + ), + mobile: .init( + native: "sampleapp://deeplink", + universal: "https://example.com/universal" + ), + desktop: .init( + native: nil, + universal: "https://example.com/universal" + ) + ), + Listing( + id: UUID().uuidString, + name: "Awesome Wallet", + homepage: "https://example.com/awesome", + order: 2, + imageId: UUID().uuidString, + app: Listing.App( + ios: "https://example.com/download-ios", + browser: "https://example.com/download-safari" + ), + mobile: .init( + native: "awesomeapp://deeplink", + universal: "https://example.com/awesome/universal" + ), + desktop: .init( + native: nil, + universal: "https://example.com/awesome/universal" + ) + ), + Listing( + id: UUID().uuidString, + name: "Cool Wallet", + homepage: "https://example.com/cool", + order: 3, + imageId: UUID().uuidString, + app: Listing.App( + ios: "https://example.com/download-ios", + browser: "https://example.com/download-safari" + ), + mobile: .init( + native: "coolapp://deeplink", + universal: "https://example.com/cool/universal" + ), + desktop: .init( + native: nil, + universal: "https://example.com/cool/universal" + ) + ) ] } diff --git a/Sources/WalletConnectModal/Modal/ModalSheet.swift b/Sources/WalletConnectModal/Modal/ModalSheet.swift index 3abe3146d..c51e69d2b 100644 --- a/Sources/WalletConnectModal/Modal/ModalSheet.swift +++ b/Sources/WalletConnectModal/Modal/ModalSheet.swift @@ -14,7 +14,6 @@ public struct ModalSheet: View { VStack(spacing: 0) { contentHeader() content() - } .frame(maxWidth: .infinity) .background(Color.background1) @@ -75,13 +74,12 @@ public struct ModalSheet: View { EmptyView() } } - .animation(.default) + .animation(.default, value: viewModel.destination) .foregroundColor(.accent) .frame(height: 60) .overlay( VStack { if viewModel.destination.hasSearch { - HStack { Image(systemName: "magnifyingglass") TextField("Search", text: $viewModel.searchTerm, onEditingChanged: { editing in @@ -128,7 +126,7 @@ public struct ModalSheet: View { viewModel.destination }, set: { _ in }), navigateTo: viewModel.navigateTo(_:), - onListingTap: { viewModel.onListingTap($0, preferUniversal: false) } + onListingTap: { viewModel.onListingTap($0) } ) } @@ -144,7 +142,6 @@ public struct ModalSheet: View { @ViewBuilder private func content() -> some View { - switch viewModel.destination { case .welcome, .viewAll: @@ -155,7 +152,7 @@ public struct ModalSheet: View { case .getWallet: GetAWalletView( wallets: Array(viewModel.wallets.prefix(6)), - onWalletTap: viewModel.onGetWalletTap(_:), + onWalletTap: viewModel.openAppstore(wallet:), navigateToExternalLink: viewModel.navigateToExternalLink(_:) ) .frame(minHeight: verticalSizeClass == .compact ? 200 : 550) @@ -163,10 +160,10 @@ public struct ModalSheet: View { case let .walletDetail(wallet): WalletDetail( - wallet: wallet, - deeplink: { viewModel.onListingTap($0, preferUniversal: false) }, - deeplinkUniversal: { viewModel.onListingTap($0, preferUniversal: true) }, - openAppStore: viewModel.onGetWalletTap(_:) + viewModel: .init( + wallet: wallet, + deeplinkHandler: viewModel + ) ) } } @@ -177,7 +174,7 @@ extension ModalSheet { Button { viewModel.onCloseButton() } label: { - Image(.close) + Image(Asset.close) .padding(8) } .buttonStyle(CircuralIconButtonStyle()) diff --git a/Sources/WalletConnectModal/Modal/ModalViewModel.swift b/Sources/WalletConnectModal/Modal/ModalViewModel.swift index bdd5e9a9f..5c274468a 100644 --- a/Sources/WalletConnectModal/Modal/ModalViewModel.swift +++ b/Sources/WalletConnectModal/Modal/ModalViewModel.swift @@ -38,6 +38,7 @@ final class ModalViewModel: ObservableObject { var isShown: Binding let interactor: ModalSheetInteractor let uiApplicationWrapper: UIApplicationWrapper + let recentWalletStorage: RecentWalletsStorage @Published private(set) var destinationStack: [Destination] = [.welcome] @Published private(set) var uri: String? @@ -52,11 +53,9 @@ final class ModalViewModel: ObservableObject { } var filteredWallets: [Listing] { - if searchTerm.isEmpty { return sortByRecent(wallets) } - - return sortByRecent( - wallets.filter { $0.name.lowercased().contains(searchTerm.lowercased()) } - ) + wallets + .sortByRecent() + .filter(searchTerm: searchTerm) } private var disposeBag = Set() @@ -65,11 +64,13 @@ final class ModalViewModel: ObservableObject { init( isShown: Binding, interactor: ModalSheetInteractor, - uiApplicationWrapper: UIApplicationWrapper = .live + uiApplicationWrapper: UIApplicationWrapper = .live, + recentWalletStorage: RecentWalletsStorage = RecentWalletsStorage() ) { self.isShown = isShown self.interactor = interactor self.uiApplicationWrapper = uiApplicationWrapper + self.recentWalletStorage = recentWalletStorage interactor.sessionSettlePublisher .receive(on: DispatchQueue.main) @@ -82,7 +83,7 @@ final class ModalViewModel: ObservableObject { interactor.sessionRejectionPublisher .receive(on: DispatchQueue.main) - .sink { (proposal, reason) in + .sink { _, reason in print(reason) self.toast = Toast(style: .error, message: reason.message) @@ -118,28 +119,16 @@ final class ModalViewModel: ObservableObject { uiApplicationWrapper.openURL(url, nil) } - func onListingTap(_ listing: Listing, preferUniversal: Bool) { + func onListingTap(_ listing: Listing) { setLastTimeUsed(listing.id) - - navigateToDeepLink( - universalLink: listing.mobile.universal ?? "", - nativeLink: listing.mobile.native ?? "", - preferUniversal: preferUniversal - ) - } - - func onGetWalletTap(_ listing: Listing) { - guard - let storeLinkString = listing.app.ios, - let storeLink = URL(string: storeLinkString) - else { return } - - uiApplicationWrapper.openURL(storeLink, nil) } func onBackButton() { guard destinationStack.count != 1 else { return } - _ = destinationStack.popLast() + + withAnimation { + _ = destinationStack.popLast() + } if destinationStack.last?.hasSearch == false { searchTerm = "" @@ -147,7 +136,6 @@ final class ModalViewModel: ObservableObject { } func onCopyButton() { - guard let uri else { toast = Toast(style: .error, message: "No uri found") return @@ -179,106 +167,130 @@ final class ModalViewModel: ObservableObject { // Small deliberate delay to ensure animations execute properly try await Task.sleep(nanoseconds: 500_000_000) - withAnimation { - self.wallets = wallets.sorted { - guard let lhs = $0.order else { - return false - } - - guard let rhs = $1.order else { - return true - } - - return lhs < rhs - } - - loadRecentWallets() - } + loadRecentWallets() + checkWhetherInstalled(wallets: wallets) + + self.wallets = wallets + .sortByOrder() + .sortByInstalled() } catch { toast = Toast(style: .error, message: error.localizedDescription) } } } -// MARK: - Recent Wallets +// MARK: - Sorting and filtering -private extension ModalViewModel { - - func sortByRecent(_ input: [Listing]) -> [Listing] { - input.sorted { lhs, rhs in - guard let lhsLastTimeUsed = lhs.lastTimeUsed else { +private extension Array where Element: Listing { + func sortByOrder() -> [Listing] { + sorted { + guard let lhs = $0.order else { return false } - guard let rhsLastTimeUsed = rhs.lastTimeUsed else { + guard let rhs = $1.order else { return true } - return lhsLastTimeUsed > rhsLastTimeUsed + return lhs < rhs } } - func loadRecentWallets() { - RecentWalletsStorage().recentWallets.forEach { wallet in - - guard let lastTimeUsed = wallet.lastTimeUsed else { - return + func sortByInstalled() -> [Listing] { + sorted { lhs, rhs in + if lhs.installed, !rhs.installed { + return true } - // Consider Recent only for 3 days - if abs(lastTimeUsed.timeIntervalSinceNow) > (24 * 60 * 60 * 3) { - return + if !lhs.installed, rhs.installed { + return false } - setLastTimeUsed(wallet.id, date: lastTimeUsed) + return false } } - func saveRecentWallets() { - RecentWalletsStorage().recentWallets = Array(wallets.filter { - $0.lastTimeUsed != nil - }.prefix(5)) + func sortByRecent() -> [Listing] { + sorted { lhs, rhs in + guard let lhsLastTimeUsed = lhs.lastTimeUsed else { + return false + } + + guard let rhsLastTimeUsed = rhs.lastTimeUsed else { + return true + } + + return lhsLastTimeUsed > rhsLastTimeUsed + } } - func setLastTimeUsed(_ walletId: String, date: Date = Date()) { - guard let index = wallets.firstIndex(where: { - $0.id == walletId - }) else { - return - } - - var copy = wallets[index] - copy.lastTimeUsed = date - wallets[index] = copy + func filter(searchTerm: String) -> [Listing] { + if searchTerm.isEmpty { return self } - saveRecentWallets() + return filter { + $0.name.lowercased().contains(searchTerm.lowercased()) + } } } -// MARK: - Deeplinking +// MARK: - Recent & Installed Wallets private extension ModalViewModel { - enum DeeplinkErrors: LocalizedError { - case noWalletLinkFound - case uriNotCreated - case failedToOpen + func checkWhetherInstalled(wallets: [Listing]) { + guard let schemes = Bundle.main.object(forInfoDictionaryKey: "LSApplicationQueriesSchemes") as? [String] else { + return + } - var errorDescription: String? { - switch self { - case .noWalletLinkFound: - return NSLocalizedString("No valid link for opening given wallet found", comment: "") - case .uriNotCreated: - return NSLocalizedString("Couldn't generate link due to missing connection URI", comment: "") - case .failedToOpen: - return NSLocalizedString("Given link couldn't be opened", comment: "") + wallets.forEach { + if + let walletScheme = $0.mobile.native, + !walletScheme.isEmpty, + schemes.contains(walletScheme.replacingOccurrences(of: "://", with: "")) + { + $0.installed = uiApplicationWrapper.canOpenURL(URL(string: walletScheme)!) } } } + + func loadRecentWallets() { + recentWalletStorage.recentWallets.forEach { wallet in + guard let lastTimeUsed = wallet.lastTimeUsed else { return } + setLastTimeUsed(wallet.id, date: lastTimeUsed) + } + } + + func setLastTimeUsed(_ id: String, date: Date = Date()) { + wallets.first { + $0.id == id + }?.lastTimeUsed = date + recentWalletStorage.recentWallets = wallets + } +} + +// MARK: - Deeplinking + +protocol WalletDeeplinkHandler { + func openAppstore(wallet: Listing) + func navigateToDeepLink(wallet: Listing, preferUniversal: Bool, preferBrowser: Bool) +} - func navigateToDeepLink(universalLink: String, nativeLink: String, preferUniversal: Bool) { +extension ModalViewModel: WalletDeeplinkHandler { + func openAppstore(wallet: Listing) { + guard + let storeLinkString = wallet.app.ios, + let storeLink = URL(string: storeLinkString) + else { return } + + uiApplicationWrapper.openURL(storeLink, nil) + } + + func navigateToDeepLink(wallet: Listing, preferUniversal: Bool, preferBrowser: Bool) { do { - let nativeUrlString = try formatNativeUrlString(nativeLink) - let universalUrlString = try formatUniversalUrlString(universalLink) + let nativeScheme = preferBrowser ? nil : wallet.mobile.native + let universalScheme = preferBrowser ? wallet.desktop.universal : wallet.mobile.universal + + let nativeUrlString = try formatNativeUrlString(nativeScheme) + let universalUrlString = try formatUniversalUrlString(universalScheme) if let nativeUrl = nativeUrlString?.toURL(), !preferUniversal { uiApplicationWrapper.openURL(nativeUrl) { success in @@ -291,7 +303,7 @@ private extension ModalViewModel { if !success { self.toast = Toast(style: .error, message: DeeplinkErrors.failedToOpen.localizedDescription) } - } + } } else { throw DeeplinkErrors.noWalletLinkFound } @@ -299,13 +311,32 @@ private extension ModalViewModel { toast = Toast(style: .error, message: error.localizedDescription) } } +} + +private extension ModalViewModel { + enum DeeplinkErrors: LocalizedError { + case noWalletLinkFound + case uriNotCreated + case failedToOpen + + var errorDescription: String? { + switch self { + case .noWalletLinkFound: + return NSLocalizedString("No valid link for opening given wallet found", comment: "") + case .uriNotCreated: + return NSLocalizedString("Couldn't generate link due to missing connection URI", comment: "") + case .failedToOpen: + return NSLocalizedString("Given link couldn't be opened", comment: "") + } + } + } func isHttpUrl(url: String) -> Bool { return url.hasPrefix("http://") || url.hasPrefix("https://") } - func formatNativeUrlString(_ string: String) throws -> String? { - if string.isEmpty { return nil } + func formatNativeUrlString(_ string: String?) throws -> String? { + guard let string = string, !string.isEmpty else { return nil } if isHttpUrl(url: string) { return try formatUniversalUrlString(string) @@ -324,8 +355,8 @@ private extension ModalViewModel { return "\(safeAppUrl)wc?uri=\(deeplinkUri)" } - func formatUniversalUrlString(_ string: String) throws -> String? { - if string.isEmpty { return nil } + func formatUniversalUrlString(_ string: String?) throws -> String? { + guard let string = string, !string.isEmpty else { return nil } if !isHttpUrl(url: string) { return try formatNativeUrlString(string) diff --git a/Sources/WalletConnectModal/Modal/RecentWalletStorage.swift b/Sources/WalletConnectModal/Modal/RecentWalletStorage.swift index 04487edce..00ccd5929 100644 --- a/Sources/WalletConnectModal/Modal/RecentWalletStorage.swift +++ b/Sources/WalletConnectModal/Modal/RecentWalletStorage.swift @@ -9,23 +9,44 @@ final class RecentWalletsStorage { var recentWallets: [Listing] { get { - guard - let data = defaults.data(forKey: "recentWallets"), - let wallets = try? JSONDecoder().decode([Listing].self, from: data) - else { - return [] - } - - return wallets + loadRecentWallets() } set { - guard - let walletsData = try? JSONEncoder().encode(newValue) - else { - return + saveRecentWallets(newValue) + } + } + + func loadRecentWallets() -> [Listing] { + guard + let data = defaults.data(forKey: "recentWallets"), + let wallets = try? JSONDecoder().decode([Listing].self, from: data) + else { + return [] + } + + return wallets.filter { listing in + guard let lastTimeUsed = listing.lastTimeUsed else { + assertionFailure("Shouldn't happen we stored wallet without `lastTimeUsed`") + return false } - defaults.set(walletsData, forKey: "recentWallets") + // Consider Recent only for 3 days + return abs(lastTimeUsed.timeIntervalSinceNow) > (24 * 60 * 60 * 3) + } + } + + func saveRecentWallets(_ listings: [Listing]) { + + let subset = Array(listings.filter { + $0.lastTimeUsed != nil + }.prefix(5)) + + guard + let walletsData = try? JSONEncoder().encode(subset) + else { + return } + + defaults.set(walletsData, forKey: "recentWallets") } } diff --git a/Sources/WalletConnectModal/Modal/Screens/WalletDetail.swift b/Sources/WalletConnectModal/Modal/Screens/WalletDetail/WalletDetail.swift similarity index 50% rename from Sources/WalletConnectModal/Modal/Screens/WalletDetail.swift rename to Sources/WalletConnectModal/Modal/Screens/WalletDetail/WalletDetail.swift index c8f56798c..3bae82d17 100644 --- a/Sources/WalletConnectModal/Modal/Screens/WalletDetail.swift +++ b/Sources/WalletConnectModal/Modal/Screens/WalletDetail/WalletDetail.swift @@ -3,29 +3,71 @@ import SwiftUI struct WalletDetail: View { @Environment(\.verticalSizeClass) var verticalSizeClass - @State var wallet: Listing - @State var retryShown: Bool = false + @ObservedObject var viewModel: WalletDetailViewModel - let deeplink: (Listing) -> Void - var deeplinkUniversal: (Listing) -> Void - var openAppStore: (Listing) -> Void + @State var retryShown: Bool = false var body: some View { - content() - .onAppear { - if verticalSizeClass == .compact { - retryShown = true - } else { - DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { - withAnimation { - retryShown = true + VStack { + if viewModel.showToggle { + Web3ModalPicker( + WalletDetailViewModel.Platform.allCases, + selection: viewModel.preferredPlatform + ) { item in + + HStack { + switch item { + case .native: + Image(systemName: "iphone") + case .browser: + Image(systemName: "safari") + } + Text(item.rawValue.capitalized) + } + .font(.system(size: 14).weight(.semibold)) + .multilineTextAlignment(.center) + .foregroundColor(viewModel.preferredPlatform == item ? .foreground1 : .foreground2) + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) + .padding(.horizontal, 8) + .padding(.vertical, 8) + .onTapGesture { + withAnimation(.easeInOut(duration: 0.15)) { + viewModel.preferredPlatform = item } } } + .pickerBackgroundColor(.background2) + .cornerRadius(20) + .borderWidth(1) + .borderColor(.thinOverlay) + .accentColor(.thinOverlay) + .frame(maxWidth: 250) + .padding() } - .onDisappear { - retryShown = false - } + + content() + .onAppear { + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + viewModel.handle(.onAppear) + } + + if verticalSizeClass == .compact { + retryShown = true + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + withAnimation { + retryShown = true + } + } + } + } + .onDisappear { + retryShown = false + } + .animation(.easeInOut, value: viewModel.preferredPlatform) + } } @ViewBuilder @@ -41,9 +83,11 @@ struct WalletDetail: View { retrySection() } - Divider() - - appStoreRow() + VStack { + Divider() + appStoreRow() + } + .opacity(viewModel.preferredPlatform != .native ? 0 : 1) } .padding(.horizontal, 20) } @@ -56,12 +100,15 @@ struct WalletDetail: View { VStack(spacing: 15) { if retryShown { retrySection() + .frame(maxWidth: .infinity) .padding(.top, 15) } - Divider() - - appStoreRow() + VStack { + Divider() + appStoreRow() + } + .opacity(viewModel.preferredPlatform != .native ? 0 : 1) } .padding(.horizontal, 20) .padding(.bottom, 40) @@ -72,7 +119,7 @@ struct WalletDetail: View { func walletImage() -> some View { VStack(spacing: 20) { - WalletImage(wallet: wallet, size: .large) + WalletImage(wallet: viewModel.wallet, size: .large) .frame(width: 96, height: 96) .cornerRadius(24) .overlay( @@ -80,7 +127,7 @@ struct WalletDetail: View { .stroke(.gray.opacity(0.4), lineWidth: 1) ) - Text("Continue in \(wallet.name)...") + Text("Continue in \(viewModel.wallet.name)...") .font(.system(size: 16, weight: .medium)) .foregroundColor(.foreground1) } @@ -88,10 +135,7 @@ struct WalletDetail: View { func retrySection() -> some View { VStack(spacing: 15) { - let hasUniversalLink = wallet.mobile.universal?.isEmpty == false - let hasNativeLink = wallet.mobile.native?.isEmpty == false - - Text("You can try opening \(wallet.name) again \((hasNativeLink && hasUniversalLink) ? "or try using a Universal Link instead" : "")") + Text("You can try opening \(viewModel.wallet.name) again \((viewModel.hasNativeLink && viewModel.showUniversalLink) ? "or try using a Universal Link instead" : "")") .font(.system(size: 14, weight: .medium)) .multilineTextAlignment(.center) .foregroundColor(.foreground2) @@ -99,15 +143,15 @@ struct WalletDetail: View { HStack { Button { - deeplink(wallet) + viewModel.handle(.didTapTryAgain) } label: { Text("Try Again") } .buttonStyle(WCMAccentButtonStyle()) - if hasUniversalLink { + if viewModel.showUniversalLink { Button { - deeplinkUniversal(wallet) + viewModel.handle(.didTapUniversalLink) } label: { Text("Universal link") } @@ -115,16 +159,17 @@ struct WalletDetail: View { } } } + .frame(height: 100) } func appStoreRow() -> some View { HStack(spacing: 0) { HStack(spacing: 10) { - WalletImage(wallet: wallet, size: .small) + WalletImage(wallet: viewModel.wallet, size: .small) .frame(width: 28, height: 28) .cornerRadius(8) - Text("Get \(wallet.name)") + Text("Get \(viewModel.wallet.name)") .font(.system(size: 16).weight(.semibold)) .foregroundColor(.foreground1) } @@ -141,7 +186,7 @@ struct WalletDetail: View { } } .onTapGesture { - openAppStore(wallet) + viewModel.handle(.didTapAppStore) } } } diff --git a/Sources/WalletConnectModal/Modal/Screens/WalletDetail/WalletDetailViewModel.swift b/Sources/WalletConnectModal/Modal/Screens/WalletDetail/WalletDetailViewModel.swift new file mode 100644 index 000000000..4b146927c --- /dev/null +++ b/Sources/WalletConnectModal/Modal/Screens/WalletDetail/WalletDetailViewModel.swift @@ -0,0 +1,63 @@ +import Foundation + +final class WalletDetailViewModel: ObservableObject { + enum Platform: String, CaseIterable, Identifiable { + case native + case browser + + var id: Self { self } + } + + enum Event { + case onAppear + case didTapUniversalLink + case didTapTryAgain + case didTapAppStore + } + + let wallet: Listing + let deeplinkHandler: WalletDeeplinkHandler + + @Published var preferredPlatform: Platform = .native + + var showToggle: Bool { wallet.app.browser != nil && wallet.app.ios != nil } + var showUniversalLink: Bool { preferredPlatform == .native && wallet.mobile.universal?.isEmpty == false } + var hasNativeLink: Bool { wallet.mobile.native?.isEmpty == false } + + init( + wallet: Listing, + deeplinkHandler: WalletDeeplinkHandler + ) { + self.wallet = wallet + self.deeplinkHandler = deeplinkHandler + preferredPlatform = wallet.app.ios != nil ? .native : .browser + } + + func handle(_ event: Event) { + switch event { + case .onAppear: + deeplinkHandler.navigateToDeepLink( + wallet: wallet, + preferUniversal: true, + preferBrowser: preferredPlatform == .browser + ) + + case .didTapUniversalLink: + deeplinkHandler.navigateToDeepLink( + wallet: wallet, + preferUniversal: true, + preferBrowser: preferredPlatform == .browser + ) + + case .didTapTryAgain: + deeplinkHandler.navigateToDeepLink( + wallet: wallet, + preferUniversal: false, + preferBrowser: preferredPlatform == .browser + ) + + case .didTapAppStore: + deeplinkHandler.openAppstore(wallet: wallet) + } + } +} diff --git a/Sources/WalletConnectModal/Modal/Screens/WalletList.swift b/Sources/WalletConnectModal/Modal/Screens/WalletList.swift index 8a76bcd83..7ea02d286 100644 --- a/Sources/WalletConnectModal/Modal/Screens/WalletList.swift +++ b/Sources/WalletConnectModal/Modal/Screens/WalletList.swift @@ -35,6 +35,7 @@ struct WalletList: View { case .viewAll: viewAll() .frame(minHeight: 250) + .animation(nil) default: EmptyView() } @@ -173,8 +174,8 @@ struct WalletList: View { .foregroundColor(.foreground1) .multilineTextAlignment(.center) - Text("RECENT") - .opacity(wallet.lastTimeUsed != nil ? 1 : 0) + Text(wallet.lastTimeUsed != nil ? "RECENT" : "INSTALLED") + .opacity(wallet.lastTimeUsed != nil || wallet.installed ? 1 : 0) .font(.system(size: 10)) .foregroundColor(.foreground3) .padding(.horizontal, 12) diff --git a/Sources/WalletConnectModal/Networking/Explorer/ListingsResponse.swift b/Sources/WalletConnectModal/Networking/Explorer/ListingsResponse.swift index bc7c91619..0ddd4446c 100644 --- a/Sources/WalletConnectModal/Networking/Explorer/ListingsResponse.swift +++ b/Sources/WalletConnectModal/Networking/Explorer/ListingsResponse.swift @@ -4,15 +4,51 @@ struct ListingsResponse: Codable { let listings: [String: Listing] } -struct Listing: Codable, Hashable, Identifiable { +class Listing: Codable, Hashable, Identifiable { + init( + id: String, + name: String, + homepage: String, + order: Int? = nil, + imageId: String, + app: Listing.App, + mobile: Listing.Links, + desktop: Listing.Links, + lastTimeUsed: Date? = nil, + installed: Bool = false + ) { + self.id = id + self.name = name + self.homepage = homepage + self.order = order + self.imageId = imageId + self.app = app + self.mobile = mobile + self.desktop = desktop + self.lastTimeUsed = lastTimeUsed + self.installed = installed + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(name) + } + + static func == (lhs: Listing, rhs: Listing) -> Bool { + lhs.id == rhs.id && lhs.name == rhs.name + } + let id: String let name: String let homepage: String let order: Int? let imageId: String let app: App - let mobile: Mobile - var lastTimeUsed: Date? + let mobile: Links + let desktop: Links + + var lastTimeUsed: Date? + var installed: Bool = false private enum CodingKeys: String, CodingKey { case id @@ -22,16 +58,16 @@ struct Listing: Codable, Hashable, Identifiable { case imageId = "image_id" case app case mobile + case desktop case lastTimeUsed } struct App: Codable, Hashable { let ios: String? - let mac: String? - let safari: String? + let browser: String? } - struct Mobile: Codable, Hashable { + struct Links: Codable, Hashable { let native: String? let universal: String? } diff --git a/Sources/WalletConnectModal/Resources/Color.swift b/Sources/WalletConnectModal/Resources/Color.swift index 00a4fe434..3a01b85f8 100644 --- a/Sources/WalletConnectModal/Resources/Color.swift +++ b/Sources/WalletConnectModal/Resources/Color.swift @@ -20,17 +20,17 @@ extension Color { self.init(asset.rawValue, bundle: .module) } - static let foreground1 = Color(.foreground1) - static let foreground2 = Color(.foreground2) - static let foreground3 = Color(.foreground3) - static let foregroundInverse = Color(.foregroundInverse) - static let background1 = Color(.background1) - static let background2 = Color(.background2) - static let background3 = Color(.background3) - static let negative = Color(.negative) - static let thickOverlay = Color(.thickOverlay) - static let thinOverlay = Color(.thinOverlay) - static let accent = Color(.accent) + static let foreground1 = Color(AssetColor.foreground1) + static let foreground2 = Color(AssetColor.foreground2) + static let foreground3 = Color(AssetColor.foreground3) + static let foregroundInverse = Color(AssetColor.foregroundInverse) + static let background1 = Color(AssetColor.background1) + static let background2 = Color(AssetColor.background2) + static let background3 = Color(AssetColor.background3) + static let negative = Color(AssetColor.negative) + static let thickOverlay = Color(AssetColor.thickOverlay) + static let thinOverlay = Color(AssetColor.thinOverlay) + static var accent = Color(AssetColor.accent) } #if canImport(UIKit) diff --git a/Sources/WalletConnectModal/UI/Common/Web3ModalPicker.swift b/Sources/WalletConnectModal/UI/Common/Web3ModalPicker.swift new file mode 100644 index 000000000..0822b7fc9 --- /dev/null +++ b/Sources/WalletConnectModal/UI/Common/Web3ModalPicker.swift @@ -0,0 +1,127 @@ +import SwiftUI + +struct Web3ModalPicker: View where Data: Hashable, Content: View { + let sources: [Data] + let selection: Data? + let itemBuilder: (Data) -> Content + + @State private var backgroundColor: Color = Color.black.opacity(0.05) + + func pickerBackgroundColor(_ color: Color) -> Web3ModalPicker { + var view = self + view._backgroundColor = State(initialValue: color) + return view + } + + @State private var cornerRadius: CGFloat? + + func cornerRadius(_ cornerRadius: CGFloat) -> Web3ModalPicker { + var view = self + view._cornerRadius = State(initialValue: cornerRadius) + return view + } + + @State private var borderColor: Color? + + func borderColor(_ borderColor: Color) -> Web3ModalPicker { + var view = self + view._borderColor = State(initialValue: borderColor) + return view + } + + @State private var borderWidth: CGFloat? + + func borderWidth(_ borderWidth: CGFloat) -> Web3ModalPicker { + var view = self + view._borderWidth = State(initialValue: borderWidth) + return view + } + + private var customIndicator: AnyView? + + init( + _ sources: [Data], + selection: Data?, + @ViewBuilder itemBuilder: @escaping (Data) -> Content + ) { + self.sources = sources + self.selection = selection + self.itemBuilder = itemBuilder + } + + public var body: some View { + ZStack(alignment: .center) { + if let selection = selection, let selectedIdx = sources.firstIndex(of: selection) { + + GeometryReader { geo in + RoundedRectangle(cornerRadius: cornerRadius ?? 6.0) + .stroke(borderColor ?? .clear, lineWidth: borderWidth ?? 0) + .foregroundColor(.accentColor) + .padding(EdgeInsets(top: borderWidth ?? 2, leading: borderWidth ?? 2, bottom: borderWidth ?? 2, trailing: borderWidth ?? 2)) + .frame(width: geo.size.width / CGFloat(sources.count)) + .animation(.spring().speed(1.5), value: selection) + .offset(x: geo.size.width / CGFloat(sources.count) * CGFloat(selectedIdx), y: 0) + }.frame(height: 32) + } + + HStack(spacing: 0) { + ForEach(sources, id: \.self) { item in + itemBuilder(item) + } + } + } + .background( + RoundedRectangle(cornerRadius: cornerRadius ?? 6.0) + .fill(backgroundColor) + .padding(-5) + ) + } +} + +struct PreviewWeb3ModalPicker: View { + + enum Platform: String, CaseIterable { + case native + case browser + } + + @State private var selectedItem: Platform? = .native + + var body: some View { + Web3ModalPicker( + Platform.allCases, + selection: selectedItem + ) { item in + + HStack { + Image(systemName: "iphone") + Text(item.rawValue.capitalized) + } + .font(.system(size: 14).weight(.semibold)) + .multilineTextAlignment(.center) + .foregroundColor(selectedItem == item ? .foreground1 : .foreground2) + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) + .padding(.horizontal, 8) + .padding(.vertical, 8) + .onTapGesture { + withAnimation(.easeInOut(duration: 0.15)) { + selectedItem = item + } + } + } + .pickerBackgroundColor(.background2) + .cornerRadius(20) + .borderWidth(1) + .borderColor(.thinOverlay) + .accentColor(.thinOverlay) + .frame(maxWidth: 250) + .padding() + } +} + +struct Web3ModalPicker_Previews: PreviewProvider { + static var previews: some View { + PreviewWeb3ModalPicker() + } +} diff --git a/Sources/WalletConnectModal/WalletConnectModal.swift b/Sources/WalletConnectModal/WalletConnectModal.swift index d405d9178..7fe3c3674 100644 --- a/Sources/WalletConnectModal/WalletConnectModal.swift +++ b/Sources/WalletConnectModal/WalletConnectModal.swift @@ -1,4 +1,5 @@ import Foundation +import SwiftUI #if canImport(UIKit) import UIKit @@ -53,7 +54,8 @@ public class WalletConnectModal { metadata: AppMetadata, sessionParams: SessionParams = .default, recommendedWalletIds: [String] = [], - excludedWalletIds: [String] = [] + excludedWalletIds: [String] = [], + accentColor: Color? = nil ) { Pair.configure(metadata: metadata) WalletConnectModal.config = WalletConnectModal.Config( @@ -63,6 +65,10 @@ public class WalletConnectModal { recommendedWalletIds: recommendedWalletIds, excludedWalletIds: excludedWalletIds ) + + if let accentColor { + Color.accent = accentColor + } } public static func set(sessionParams: SessionParams) { diff --git a/Sources/WalletConnectNetworking/NetworkInteracting.swift b/Sources/WalletConnectNetworking/NetworkInteracting.swift index ca15160a8..54987272c 100644 --- a/Sources/WalletConnectNetworking/NetworkInteracting.swift +++ b/Sources/WalletConnectNetworking/NetworkInteracting.swift @@ -3,6 +3,7 @@ import Combine public protocol NetworkInteracting { var socketConnectionStatusPublisher: AnyPublisher { get } + var networkConnectionStatusPublisher: AnyPublisher { get } var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest, decryptedPayload: Data, publishedAt: Date, derivedTopic: String?), Never> { get } func subscribe(topic: String) async throws func unsubscribe(topic: String) @@ -12,7 +13,8 @@ public protocol NetworkInteracting { func respond(topic: String, response: RPCResponse, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws func respondSuccess(topic: String, requestId: RPCID, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws func respondError(topic: String, requestId: RPCID, protocolMethod: ProtocolMethod, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws - + func handleHistoryRequest(topic: String, request: RPCRequest) + func requestSubscription( on request: ProtocolMethod ) -> AnyPublisher, Never> diff --git a/Sources/WalletConnectNetworking/NetworkingClient.swift b/Sources/WalletConnectNetworking/NetworkingClient.swift index 150208766..e7c7fcd76 100644 --- a/Sources/WalletConnectNetworking/NetworkingClient.swift +++ b/Sources/WalletConnectNetworking/NetworkingClient.swift @@ -3,6 +3,8 @@ import Combine public protocol NetworkingClient { var socketConnectionStatusPublisher: AnyPublisher { get } + var logsPublisher: AnyPublisher {get} + func setLogging(level: LoggingLevel) func connect() throws func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws } diff --git a/Sources/WalletConnectNetworking/NetworkingClientFactory.swift b/Sources/WalletConnectNetworking/NetworkingClientFactory.swift index 6b5e39a2c..4470087d1 100644 --- a/Sources/WalletConnectNetworking/NetworkingClientFactory.swift +++ b/Sources/WalletConnectNetworking/NetworkingClientFactory.swift @@ -3,7 +3,7 @@ import Foundation public struct NetworkingClientFactory { public static func create(relayClient: RelayClient) -> NetworkingInteractor { - let logger = ConsoleLogger(loggingLevel: .debug) + let logger = ConsoleLogger(prefix: "🕸️", loggingLevel: .off) let keyValueStorage = UserDefaults.standard let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") return NetworkingClientFactory.create(relayClient: relayClient, logger: logger, keychainStorage: keychainStorage, keyValueStorage: keyValueStorage) @@ -12,7 +12,7 @@ public struct NetworkingClientFactory { public static func create(relayClient: RelayClient, logger: ConsoleLogging, keychainStorage: KeychainStorageProtocol, keyValueStorage: KeyValueStorage) -> NetworkingInteractor { let kms = KeyManagementService(keychain: keychainStorage) - let serializer = Serializer(kms: kms) + let serializer = Serializer(kms: kms, logger: ConsoleLogger(prefix: "🔐", loggingLevel: .off)) let rpcHistory = RPCHistoryFactory.createForNetwork(keyValueStorage: keyValueStorage) diff --git a/Sources/WalletConnectNetworking/NetworkingInteractor.swift b/Sources/WalletConnectNetworking/NetworkingInteractor.swift index 931876d79..e92cfa3ea 100644 --- a/Sources/WalletConnectNetworking/NetworkingInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkingInteractor.swift @@ -1,7 +1,6 @@ import Foundation import Combine - public class NetworkingInteractor: NetworkInteracting { private var publishers = Set() private let relayClient: RelayClient @@ -20,7 +19,17 @@ public class NetworkingInteractor: NetworkInteracting { responsePublisherSubject.eraseToAnyPublisher() } + public var logsPublisher: AnyPublisher { + logger.logsPublisher + .merge(with: serializer.logsPublisher) + .merge(with: relayClient.logsPublisher) + .eraseToAnyPublisher() + } + + public var networkConnectionStatusPublisher: AnyPublisher public var socketConnectionStatusPublisher: AnyPublisher + + private let networkMonitor: NetworkMonitoring public init( relayClient: RelayClient, @@ -33,6 +42,8 @@ public class NetworkingInteractor: NetworkInteracting { self.rpcHistory = rpcHistory self.logger = logger self.socketConnectionStatusPublisher = relayClient.socketConnectionStatusPublisher + self.networkMonitor = NetworkMonitor() + self.networkConnectionStatusPublisher = networkMonitor.networkConnectionStatusPublisher setupRelaySubscribtion() } @@ -43,6 +54,13 @@ public class NetworkingInteractor: NetworkInteracting { }.store(in: &publishers) } + public func setLogging(level: LoggingLevel) { + logger.setLogging(level: level) + serializer.setLogging(level: level) + relayClient.setLogging(level: level) + } + + public func subscribe(topic: String) async throws { try await relayClient.subscribe(topic: topic) } @@ -144,6 +162,10 @@ public class NetworkingInteractor: NetworkInteracting { logger.debug("Networking Interactor - Received unknown object type from networking relay") } } + + public func handleHistoryRequest(topic: String, request: RPCRequest) { + requestPublisherSubject.send((topic, request, Data(), Date(), nil)) + } private func handleRequest(topic: String, request: RPCRequest, decryptedPayload: Data, publishedAt: Date, derivedTopic: String?) { do { diff --git a/Sources/WalletConnectNotify/Client/Common/DeleteNotifySubscriptionService.swift b/Sources/WalletConnectNotify/Client/Common/DeleteNotifySubscriptionService.swift new file mode 100644 index 000000000..fc4a361fd --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Common/DeleteNotifySubscriptionService.swift @@ -0,0 +1,72 @@ +import Foundation + +class DeleteNotifySubscriptionService { + enum Errors: Error { + case notifySubscriptionNotFound + } + private let keyserver: URL + private let networkingInteractor: NetworkInteracting + private let identityClient: IdentityClient + private let webDidResolver: WebDidResolver + private let kms: KeyManagementServiceProtocol + private let logger: ConsoleLogging + private let notifyStorage: NotifyStorage + + init( + keyserver: URL, + networkingInteractor: NetworkInteracting, + identityClient: IdentityClient, + webDidResolver: WebDidResolver, + kms: KeyManagementServiceProtocol, + logger: ConsoleLogging, + notifyStorage: NotifyStorage + ) { + self.keyserver = keyserver + self.networkingInteractor = networkingInteractor + self.identityClient = identityClient + self.webDidResolver = webDidResolver + self.kms = kms + self.logger = logger + self.notifyStorage = notifyStorage + } + + func delete(topic: String) async throws { + logger.debug("Will delete notify subscription") + + guard let subscription = notifyStorage.getSubscription(topic: topic) + else { throw Errors.notifySubscriptionNotFound} + + let protocolMethod = NotifyDeleteProtocolMethod() + let dappPubKey = try await webDidResolver.resolvePublicKey(dappUrl: subscription.metadata.url) + + let wrapper = try createJWTWrapper( + dappPubKey: DIDKey(rawData: dappPubKey.rawRepresentation), + reason: NotifyDeleteParams.userDisconnected.message, + app: subscription.metadata.url, + account: subscription.account + ) + + let request = RPCRequest(method: protocolMethod.method, params: wrapper) + try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) + + try notifyStorage.deleteSubscription(topic: topic) + notifyStorage.deleteMessages(topic: topic) + + networkingInteractor.unsubscribe(topic: topic) + + logger.debug("Subscription removed, topic: \(topic)") + + kms.deleteSymmetricKey(for: topic) + } +} + +private extension DeleteNotifySubscriptionService { + + func createJWTWrapper(dappPubKey: DIDKey, reason: String, app: String, account: Account) throws -> NotifyDeletePayload.Wrapper { + let jwtPayload = NotifyDeletePayload(keyserver: keyserver, dappPubKey: dappPubKey, reason: reason, app: app) + return try identityClient.signAndCreateWrapper( + payload: jwtPayload, + account: account + ) + } +} diff --git a/Sources/WalletConnectPush/Client/Common/DeletePushSubscriptionSubscriber.swift b/Sources/WalletConnectNotify/Client/Common/DeleteNotifySubscriptionSubscriber.swift similarity index 60% rename from Sources/WalletConnectPush/Client/Common/DeletePushSubscriptionSubscriber.swift rename to Sources/WalletConnectNotify/Client/Common/DeleteNotifySubscriptionSubscriber.swift index 7ec0f6f3a..435ece85f 100644 --- a/Sources/WalletConnectPush/Client/Common/DeletePushSubscriptionSubscriber.swift +++ b/Sources/WalletConnectNotify/Client/Common/DeleteNotifySubscriptionSubscriber.swift @@ -1,36 +1,34 @@ import Foundation import Combine -class DeletePushSubscriptionSubscriber { +class DeleteNotifySubscriptionSubscriber { private let networkingInteractor: NetworkInteracting private let kms: KeyManagementServiceProtocol private let logger: ConsoleLogging private var publishers = [AnyCancellable]() - private let pushStorage: PushStorage + private let notifyStorage: NotifyStorage init(networkingInteractor: NetworkInteracting, kms: KeyManagementServiceProtocol, logger: ConsoleLogging, - pushStorage: PushStorage + notifyStorage: NotifyStorage ) { self.networkingInteractor = networkingInteractor self.kms = kms self.logger = logger - self.pushStorage = pushStorage + self.notifyStorage = notifyStorage subscribeForDeleteSubscription() } private func subscribeForDeleteSubscription() { - let protocolMethod = PushDeleteProtocolMethod() + let protocolMethod = NotifyDeleteProtocolMethod() networkingInteractor.requestSubscription(on: protocolMethod) - .sink { [unowned self] (payload: RequestSubscriptionPayload) in + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + + guard let (_, _) = try? NotifyDeleteResponsePayload.decodeAndVerify(from: payload.request) + else { fatalError() /* TODO: Handle error */ } + logger.debug("Peer deleted subscription") - let topic = payload.topic - networkingInteractor.unsubscribe(topic: topic) - Task(priority: .high) { - try await pushStorage.deleteSubscription(topic: topic) - } - kms.deleteSymmetricKey(for: topic) }.store(in: &publishers) } } diff --git a/Sources/WalletConnectPush/Client/Common/PushDecryptionService.swift b/Sources/WalletConnectNotify/Client/Common/NotifyDecryptionService.swift similarity index 59% rename from Sources/WalletConnectPush/Client/Common/PushDecryptionService.swift rename to Sources/WalletConnectNotify/Client/Common/NotifyDecryptionService.swift index 995ff4e4a..5fe116af5 100644 --- a/Sources/WalletConnectPush/Client/Common/PushDecryptionService.swift +++ b/Sources/WalletConnectNotify/Client/Common/NotifyDecryptionService.swift @@ -1,8 +1,8 @@ import Foundation -public class PushDecryptionService { +public class NotifyDecryptionService { enum Errors: Error { - case malformedPushMessage + case malformedNotifyMessage } private let serializer: Serializing @@ -13,12 +13,14 @@ public class PushDecryptionService { public init() { let keychainStorage = GroupKeychainStorage(serviceIdentifier: "group.com.walletconnect.sdk") let kms = KeyManagementService(keychain: keychainStorage) - self.serializer = Serializer(kms: kms) + self.serializer = Serializer(kms: kms, logger: ConsoleLogger(prefix: "🔐", loggingLevel: .off)) } - public func decryptMessage(topic: String, ciphertext: String) throws -> PushMessage { + public func decryptMessage(topic: String, ciphertext: String) throws -> NotifyMessage { let (rpcRequest, _, _): (RPCRequest, String?, Data) = try serializer.deserialize(topic: topic, encodedEnvelope: ciphertext) - guard let params = rpcRequest.params else { throw Errors.malformedPushMessage } - return try params.get(PushMessage.self) + guard let params = rpcRequest.params else { throw Errors.malformedNotifyMessage } + let wrapper = try params.get(NotifyMessagePayload.Wrapper.self) + let (messagePayload, _) = try NotifyMessagePayload.decodeAndVerify(from: wrapper) + return messagePayload.message } } diff --git a/Sources/WalletConnectPush/Client/Common/PushResubscribeService.swift b/Sources/WalletConnectNotify/Client/Common/NotifyResubscribeService.swift similarity index 54% rename from Sources/WalletConnectPush/Client/Common/PushResubscribeService.swift rename to Sources/WalletConnectNotify/Client/Common/NotifyResubscribeService.swift index c19769f4d..80fbd12ec 100644 --- a/Sources/WalletConnectPush/Client/Common/PushResubscribeService.swift +++ b/Sources/WalletConnectNotify/Client/Common/NotifyResubscribeService.swift @@ -1,16 +1,18 @@ import Foundation import Combine -final class PushResubscribeService { +final class NotifyResubscribeService { private var publishers = Set() + private let logger: ConsoleLogging private let networkInteractor: NetworkInteracting - private let pushStorage: PushStorage + private let notifyStorage: NotifyStorage - init(networkInteractor: NetworkInteracting, pushStorage: PushStorage) { + init(networkInteractor: NetworkInteracting, notifyStorage: NotifyStorage, logger: ConsoleLogging) { self.networkInteractor = networkInteractor - self.pushStorage = pushStorage + self.notifyStorage = notifyStorage + self.logger = logger setUpResubscription() } @@ -18,7 +20,8 @@ final class PushResubscribeService { networkInteractor.socketConnectionStatusPublisher .sink { [unowned self] status in guard status == .connected else { return } - let topics = pushStorage.getSubscriptions().map{$0.topic} + let topics = notifyStorage.getSubscriptions().map{$0.topic} + logger.debug("Resubscribing to notify subscription topics: \(topics)", properties: ["topics": topics.joined(separator: ", ")]) Task(priority: .high) { try await networkInteractor.batchSubscribe(topics: topics) } diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift new file mode 100644 index 000000000..8065dfa78 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift @@ -0,0 +1,136 @@ +import Foundation +import Combine + +public class NotifyClient { + + private var publishers = Set() + + /// publishes new subscriptions + public var newSubscriptionPublisher: AnyPublisher { + return notifyStorage.newSubscriptionPublisher + } + + public var subscriptionErrorPublisher: AnyPublisher { + return notifySubscribeResponseSubscriber.subscriptionErrorPublisher + } + + public var deleteSubscriptionPublisher: AnyPublisher { + return notifyStorage.deleteSubscriptionPublisher + } + + public var subscriptionsPublisher: AnyPublisher<[NotifySubscription], Never> { + return notifyStorage.subscriptionsPublisher + } + + public var notifyMessagePublisher: AnyPublisher { + return notifyMessageSubscriber.notifyMessagePublisher + } + + public var updateSubscriptionPublisher: AnyPublisher { + return notifyStorage.updateSubscriptionPublisher + } + + public var logsPublisher: AnyPublisher { + logger.logsPublisher + .eraseToAnyPublisher() + } + + private let deleteNotifySubscriptionService: DeleteNotifySubscriptionService + private let notifySubscribeRequester: NotifySubscribeRequester + + public let logger: ConsoleLogging + + private let pushClient: PushClient + private let identityClient: IdentityClient + private let notifyStorage: NotifyStorage + private let notifyMessageSubscriber: NotifyMessageSubscriber + private let resubscribeService: NotifyResubscribeService + private let notifySubscribeResponseSubscriber: NotifySubscribeResponseSubscriber + private let deleteNotifySubscriptionSubscriber: DeleteNotifySubscriptionSubscriber + private let notifyUpdateRequester: NotifyUpdateRequester + private let notifyUpdateResponseSubscriber: NotifyUpdateResponseSubscriber + private let subscriptionsAutoUpdater: SubscriptionsAutoUpdater + + init(logger: ConsoleLogging, + kms: KeyManagementServiceProtocol, + identityClient: IdentityClient, + pushClient: PushClient, + notifyMessageSubscriber: NotifyMessageSubscriber, + notifyStorage: NotifyStorage, + deleteNotifySubscriptionService: DeleteNotifySubscriptionService, + resubscribeService: NotifyResubscribeService, + notifySubscribeRequester: NotifySubscribeRequester, + notifySubscribeResponseSubscriber: NotifySubscribeResponseSubscriber, + deleteNotifySubscriptionSubscriber: DeleteNotifySubscriptionSubscriber, + notifyUpdateRequester: NotifyUpdateRequester, + notifyUpdateResponseSubscriber: NotifyUpdateResponseSubscriber, + subscriptionsAutoUpdater: SubscriptionsAutoUpdater + ) { + self.logger = logger + self.pushClient = pushClient + self.identityClient = identityClient + self.notifyMessageSubscriber = notifyMessageSubscriber + self.notifyStorage = notifyStorage + self.deleteNotifySubscriptionService = deleteNotifySubscriptionService + self.resubscribeService = resubscribeService + self.notifySubscribeRequester = notifySubscribeRequester + self.notifySubscribeResponseSubscriber = notifySubscribeResponseSubscriber + self.deleteNotifySubscriptionSubscriber = deleteNotifySubscriptionSubscriber + self.notifyUpdateRequester = notifyUpdateRequester + self.notifyUpdateResponseSubscriber = notifyUpdateResponseSubscriber + self.subscriptionsAutoUpdater = subscriptionsAutoUpdater + } + + public func register(account: Account, onSign: @escaping SigningCallback) async throws { + _ = try await identityClient.register(account: account, onSign: onSign) + } + + public func setLogging(level: LoggingLevel) { + logger.setLogging(level: level) + } + + public func subscribe(metadata: AppMetadata, account: Account, onSign: @escaping SigningCallback) async throws { + try await notifySubscribeRequester.subscribe(metadata: metadata, account: account, onSign: onSign) + } + + public func update(topic: String, scope: Set) async throws { + try await notifyUpdateRequester.update(topic: topic, scope: scope) + } + + public func getActiveSubscriptions() -> [NotifySubscription] { + return notifyStorage.getSubscriptions() + } + + public func getMessageHistory(topic: String) -> [NotifyMessageRecord] { + notifyStorage.getMessages(topic: topic) + } + + public func deleteSubscription(topic: String) async throws { + try await deleteNotifySubscriptionService.delete(topic: topic) + } + + public func deleteNotifyMessage(id: String) { + notifyStorage.deleteMessage(id: id) + } + + public func register(deviceToken: Data) async throws { + try await pushClient.register(deviceToken: deviceToken) + } + + public func isIdentityRegistered(account: Account) -> Bool { + return identityClient.isIdentityRegistered(account: account) + } + + public func messagesPublisher(topic: String) -> AnyPublisher<[NotifyMessageRecord], Never> { + return notifyStorage.messagesPublisher(topic: topic) + } +} + +#if targetEnvironment(simulator) +extension NotifyClient { + public func register(deviceToken: String) async throws { + try await pushClient.register(deviceToken: deviceToken) + } +} +#endif + diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift new file mode 100644 index 000000000..cb77bce9a --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyClientFactory.swift @@ -0,0 +1,78 @@ +import Foundation + +public struct NotifyClientFactory { + + public static func create(networkInteractor: NetworkInteracting, pairingRegisterer: PairingRegisterer, pushClient: PushClient, crypto: CryptoProvider) -> NotifyClient { + let logger = ConsoleLogger(prefix: "🔔",loggingLevel: .debug) + let keyValueStorage = UserDefaults.standard + let keyserverURL = URL(string: "https://keys.walletconnect.com")! + let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") + let groupKeychainService = GroupKeychainStorage(serviceIdentifier: "group.com.walletconnect.sdk") + + return NotifyClientFactory.create( + keyserverURL: keyserverURL, + logger: logger, + keyValueStorage: keyValueStorage, + keychainStorage: keychainStorage, + groupKeychainStorage: groupKeychainService, + networkInteractor: networkInteractor, + pairingRegisterer: pairingRegisterer, + pushClient: pushClient, + crypto: crypto + ) + } + + static func create( + keyserverURL: URL, + logger: ConsoleLogging, + keyValueStorage: KeyValueStorage, + keychainStorage: KeychainStorageProtocol, + groupKeychainStorage: KeychainStorageProtocol, + networkInteractor: NetworkInteracting, + pairingRegisterer: PairingRegisterer, + pushClient: PushClient, + crypto: CryptoProvider + ) -> NotifyClient { + let kms = KeyManagementService(keychain: keychainStorage) + let subscriptionStore = KeyedDatabase(storage: keyValueStorage, identifier: NotifyStorageIdntifiers.notifySubscription) + let messagesStore = KeyedDatabase(storage: keyValueStorage, identifier: NotifyStorageIdntifiers.notifyMessagesRecords) + let notifyStorage = NotifyStorage(subscriptionStore: subscriptionStore, messagesStore: messagesStore) + let identityClient = IdentityClientFactory.create(keyserver: keyserverURL, keychain: keychainStorage, logger: logger) + let notifyMessageSubscriber = NotifyMessageSubscriber(keyserver: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, notifyStorage: notifyStorage, crypto: crypto, logger: logger) + let webDidResolver = WebDidResolver() + let deleteNotifySubscriptionService = DeleteNotifySubscriptionService(keyserver: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, webDidResolver: webDidResolver, kms: kms, logger: logger, notifyStorage: notifyStorage) + let resubscribeService = NotifyResubscribeService(networkInteractor: networkInteractor, notifyStorage: notifyStorage, logger: logger) + + let dappsMetadataStore = CodableStore(defaults: keyValueStorage, identifier: NotifyStorageIdntifiers.dappsMetadataStore) + let subscriptionScopeProvider = SubscriptionScopeProvider() + + let notifySubscribeRequester = NotifySubscribeRequester(keyserverURL: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, logger: logger, kms: kms, webDidResolver: webDidResolver, subscriptionScopeProvider: subscriptionScopeProvider, dappsMetadataStore: dappsMetadataStore) + + let notifySubscribeResponseSubscriber = NotifySubscribeResponseSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, groupKeychainStorage: groupKeychainStorage, notifyStorage: notifyStorage, dappsMetadataStore: dappsMetadataStore, subscriptionScopeProvider: subscriptionScopeProvider) + + let notifyUpdateRequester = NotifyUpdateRequester(keyserverURL: keyserverURL, webDidResolver: webDidResolver, identityClient: identityClient, networkingInteractor: networkInteractor, subscriptionScopeProvider: subscriptionScopeProvider, logger: logger, notifyStorage: notifyStorage) + + let notifyUpdateResponseSubscriber = NotifyUpdateResponseSubscriber(networkingInteractor: networkInteractor, logger: logger, subscriptionScopeProvider: subscriptionScopeProvider, notifyStorage: notifyStorage) + + let deleteNotifySubscriptionSubscriber = DeleteNotifySubscriptionSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, notifyStorage: notifyStorage) + + let subscriptionsAutoUpdater = SubscriptionsAutoUpdater(notifyUpdateRequester: notifyUpdateRequester, logger: logger, notifyStorage: notifyStorage) + + return NotifyClient( + logger: logger, + kms: kms, + identityClient: identityClient, + pushClient: pushClient, + notifyMessageSubscriber: notifyMessageSubscriber, + notifyStorage: notifyStorage, + deleteNotifySubscriptionService: deleteNotifySubscriptionService, + resubscribeService: resubscribeService, + notifySubscribeRequester: notifySubscribeRequester, + notifySubscribeResponseSubscriber: notifySubscribeResponseSubscriber, + deleteNotifySubscriptionSubscriber: deleteNotifySubscriptionSubscriber, + notifyUpdateRequester: notifyUpdateRequester, + notifyUpdateResponseSubscriber: notifyUpdateResponseSubscriber, + subscriptionsAutoUpdater: subscriptionsAutoUpdater + ) + } +} diff --git a/Sources/WalletConnectPush/Client/Wallet/PushMessageRecord.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyMessageRecord.swift similarity index 60% rename from Sources/WalletConnectPush/Client/Wallet/PushMessageRecord.swift rename to Sources/WalletConnectNotify/Client/Wallet/NotifyMessageRecord.swift index 214d9234e..a9431587a 100644 --- a/Sources/WalletConnectPush/Client/Wallet/PushMessageRecord.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyMessageRecord.swift @@ -1,9 +1,9 @@ import Foundation -public struct PushMessageRecord: Codable, Equatable, DatabaseObject { +public struct NotifyMessageRecord: Codable, Equatable, DatabaseObject { public let id: String public let topic: String - public let message: PushMessage + public let message: NotifyMessage public let publishedAt: Date public var databaseId: String { diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyStorage.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyStorage.swift new file mode 100644 index 000000000..ebc55c784 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyStorage.swift @@ -0,0 +1,123 @@ +import Foundation +import Combine + +protocol NotifyStoring { + func getSubscriptions() -> [NotifySubscription] + func getSubscription(topic: String) -> NotifySubscription? + func setSubscription(_ subscription: NotifySubscription) async throws + func deleteSubscription(topic: String) async throws +} + +final class NotifyStorage: NotifyStoring { + + private var publishers = Set() + + private let subscriptionStore: KeyedDatabase + private let messagesStore: KeyedDatabase + + private let newSubscriptionSubject = PassthroughSubject() + private let updateSubscriptionSubject = PassthroughSubject() + private let deleteSubscriptionSubject = PassthroughSubject() + private let subscriptionsSubject = PassthroughSubject<[NotifySubscription], Never>() + private let messagesSubject = PassthroughSubject<[NotifyMessageRecord], Never>() + + var newSubscriptionPublisher: AnyPublisher { + return newSubscriptionSubject.eraseToAnyPublisher() + } + + var updateSubscriptionPublisher: AnyPublisher { + return updateSubscriptionSubject.eraseToAnyPublisher() + } + + var deleteSubscriptionPublisher: AnyPublisher { + return deleteSubscriptionSubject.eraseToAnyPublisher() + } + + var subscriptionsPublisher: AnyPublisher<[NotifySubscription], Never> { + return subscriptionsSubject.eraseToAnyPublisher() + } + + var messagesPublisher: AnyPublisher<[NotifyMessageRecord], Never> { + return messagesSubject.eraseToAnyPublisher() + } + + init(subscriptionStore: KeyedDatabase, messagesStore: KeyedDatabase) { + self.subscriptionStore = subscriptionStore + self.messagesStore = messagesStore + + setupSubscriptions() + } + + // MARK: Subscriptions + + func getSubscriptions() -> [NotifySubscription] { + return subscriptionStore.getAll() + } + + func getSubscription(topic: String) -> NotifySubscription? { + return subscriptionStore.getAll().first(where: { $0.topic == topic }) + } + + func setSubscription(_ subscription: NotifySubscription) { + subscriptionStore.set(element: subscription, for: subscription.account.absoluteString) + newSubscriptionSubject.send(subscription) + } + + func deleteSubscription(topic: String) throws { + guard let subscription = getSubscription(topic: topic) else { + throw Errors.subscriptionNotFound + } + subscriptionStore.delete(id: topic, for: subscription.account.absoluteString) + deleteSubscriptionSubject.send(topic) + } + + func updateSubscription(_ subscription: NotifySubscription, scope: [String: ScopeValue], expiry: UInt64) { + let expiry = Date(timeIntervalSince1970: TimeInterval(expiry)) + let updated = NotifySubscription(topic: subscription.topic, account: subscription.account, relay: subscription.relay, metadata: subscription.metadata, scope: scope, expiry: expiry, symKey: subscription.symKey) + subscriptionStore.set(element: updated, for: updated.account.absoluteString) + updateSubscriptionSubject.send(updated) + } + + // MARK: Messages + + func messagesPublisher(topic: String) -> AnyPublisher<[NotifyMessageRecord], Never> { + return messagesPublisher + .map { $0.filter { $0.topic == topic } } + .eraseToAnyPublisher() + } + + func getMessages(topic: String) -> [NotifyMessageRecord] { + return messagesStore.getAll(for: topic) + .sorted{$0.publishedAt > $1.publishedAt} + } + + func deleteMessages(topic: String) { + messagesStore.deleteAll(for: topic) + } + + func deleteMessage(id: String) { + guard let result = messagesStore.find(id: id) else { return } + messagesStore.delete(id: id, for: result.key) + } + + func setMessage(_ record: NotifyMessageRecord) { + messagesStore.set(element: record, for: record.topic) + } +} + +private extension NotifyStorage { + + enum Errors: Error { + case subscriptionNotFound + } + + func setupSubscriptions() { + messagesStore.onUpdate = { [unowned self] in + messagesSubject.send(messagesStore.getAll()) + } + + subscriptionStore.onUpdate = { [unowned self] in + subscriptionsSubject.send(subscriptionStore.getAll()) + } + } +} diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyMessage/NotifyMessageSubscriber.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyMessage/NotifyMessageSubscriber.swift new file mode 100644 index 000000000..957c4df60 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyMessage/NotifyMessageSubscriber.swift @@ -0,0 +1,70 @@ +import Foundation +import Combine + +class NotifyMessageSubscriber { + private let keyserver: URL + private let networkingInteractor: NetworkInteracting + private let identityClient: IdentityClient + private let notifyStorage: NotifyStorage + private let crypto: CryptoProvider + private let logger: ConsoleLogging + private var publishers = [AnyCancellable]() + private let notifyMessagePublisherSubject = PassthroughSubject() + + public var notifyMessagePublisher: AnyPublisher { + notifyMessagePublisherSubject.eraseToAnyPublisher() + } + + init(keyserver: URL, networkingInteractor: NetworkInteracting, identityClient: IdentityClient, notifyStorage: NotifyStorage, crypto: CryptoProvider, logger: ConsoleLogging) { + self.keyserver = keyserver + self.networkingInteractor = networkingInteractor + self.identityClient = identityClient + self.notifyStorage = notifyStorage + self.crypto = crypto + self.logger = logger + subscribeForNotifyMessages() + } + + private func subscribeForNotifyMessages() { + let protocolMethod = NotifyMessageProtocolMethod() + networkingInteractor.requestSubscription(on: protocolMethod) + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + + logger.debug("Received Notify Message on topic: \(payload.topic)", properties: ["topic": payload.topic]) + + Task(priority: .high) { + let (messagePayload, claims) = try NotifyMessagePayload.decodeAndVerify(from: payload.request) + logger.debug("Decoded Notify Message: \(payload.topic)", properties: ["topic": payload.topic, "messageBody": messagePayload.message.body, "messageTitle": messagePayload.message.title, "publishedAt": payload.publishedAt.description, "id": payload.id.string]) + let dappPubKey = try DIDKey(did: claims.iss) + let messageData = try JSONEncoder().encode(messagePayload.message) + + let record = NotifyMessageRecord(id: payload.id.string, topic: payload.topic, message: messagePayload.message, publishedAt: payload.publishedAt) + notifyStorage.setMessage(record) + notifyMessagePublisherSubject.send(record) + + let receiptPayload = NotifyMessageReceiptPayload( + keyserver: keyserver, dappPubKey: dappPubKey, + messageHash: crypto.keccak256(messageData).toHexString(), + app: messagePayload.app + ) + + let wrapper = try identityClient.signAndCreateWrapper( + payload: receiptPayload, + account: messagePayload.account + ) + + let response = RPCResponse(id: payload.id, result: wrapper) + + try await networkingInteractor.respond( + topic: payload.topic, + response: response, + protocolMethod: NotifyMessageProtocolMethod() + ) + + logger.debug("Sent Notify Message Response on topic: \(payload.topic)", properties: ["topic" : payload.topic, "messageBody": messagePayload.message.body, "messageTitle": messagePayload.message.title, "id": payload.id.string, "result": wrapper.jwtString]) + } + + }.store(in: &publishers) + + } +} diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateRequester.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateRequester.swift new file mode 100644 index 000000000..8f4c8a103 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateRequester.swift @@ -0,0 +1,66 @@ +import Foundation + +protocol NotifyUpdateRequesting { + func update(topic: String, scope: Set) async throws +} + +class NotifyUpdateRequester: NotifyUpdateRequesting { + enum Errors: Error { + case noSubscriptionForGivenTopic + } + + private let keyserverURL: URL + private let webDidResolver: WebDidResolver + private let identityClient: IdentityClient + private let networkingInteractor: NetworkInteracting + private let subscriptionScopeProvider: SubscriptionScopeProvider + private let logger: ConsoleLogging + private let notifyStorage: NotifyStorage + + init( + keyserverURL: URL, + webDidResolver: WebDidResolver, + identityClient: IdentityClient, + networkingInteractor: NetworkInteracting, + subscriptionScopeProvider: SubscriptionScopeProvider, + logger: ConsoleLogging, + notifyStorage: NotifyStorage + ) { + self.keyserverURL = keyserverURL + self.webDidResolver = webDidResolver + self.identityClient = identityClient + self.networkingInteractor = networkingInteractor + self.subscriptionScopeProvider = subscriptionScopeProvider + self.logger = logger + self.notifyStorage = notifyStorage + } + + func update(topic: String, scope: Set) async throws { + logger.debug("NotifyUpdateRequester: updating subscription for topic: \(topic)") + + guard let subscription = notifyStorage.getSubscription(topic: topic) else { throw Errors.noSubscriptionForGivenTopic } + + let dappPubKey = try await webDidResolver.resolvePublicKey(dappUrl: subscription.metadata.url) + + let request = try createJWTRequest( + dappPubKey: DIDKey(rawData: dappPubKey.rawRepresentation), + subscriptionAccount: subscription.account, + dappUrl: subscription.metadata.url, scope: scope + ) + + let protocolMethod = NotifyUpdateProtocolMethod() + + try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) + } + + private func createJWTRequest(dappPubKey: DIDKey, subscriptionAccount: Account, dappUrl: String, scope: Set) throws -> RPCRequest { + let protocolMethod = NotifyUpdateProtocolMethod().method + let scopeClaim = scope.joined(separator: " ") + let jwtPayload = NotifyUpdatePayload(dappPubKey: dappPubKey, keyserver: keyserverURL, subscriptionAccount: subscriptionAccount, dappUrl: dappUrl, scope: scopeClaim) + let wrapper = try identityClient.signAndCreateWrapper( + payload: jwtPayload, + account: subscriptionAccount + ) + return RPCRequest(method: protocolMethod, params: wrapper) + } +} diff --git a/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateResponseSubscriber.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateResponseSubscriber.swift new file mode 100644 index 000000000..ce74dea16 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateResponseSubscriber.swift @@ -0,0 +1,65 @@ +import Foundation +import Combine + +class NotifyUpdateResponseSubscriber { + private let networkingInteractor: NetworkInteracting + private var publishers = [AnyCancellable]() + private let logger: ConsoleLogging + private let notifyStorage: NotifyStorage + private let subscriptionScopeProvider: SubscriptionScopeProvider + + init(networkingInteractor: NetworkInteracting, + logger: ConsoleLogging, + subscriptionScopeProvider: SubscriptionScopeProvider, + notifyStorage: NotifyStorage + ) { + self.networkingInteractor = networkingInteractor + self.logger = logger + self.notifyStorage = notifyStorage + self.subscriptionScopeProvider = subscriptionScopeProvider + subscribeForUpdateResponse() + } + + // TODO: handle error response +} + +private extension NotifyUpdateResponseSubscriber { + enum Errors: Error { + case subscriptionDoesNotExist + case selectedScopeNotFound + } + + func subscribeForUpdateResponse() { + let protocolMethod = NotifyUpdateProtocolMethod() + networkingInteractor.responseSubscription(on: protocolMethod) + .sink {[unowned self] (payload: ResponseSubscriptionPayload) in + Task(priority: .high) { + logger.debug("Received Notify Update response") + + let subscriptionTopic = payload.topic + + let (requestPayload, requestClaims) = try NotifyUpdatePayload.decodeAndVerify(from: payload.request) + let (_, _) = try NotifyUpdateResponsePayload.decodeAndVerify(from: payload.response) + + let scope = try await buildScope(selected: requestPayload.scope, dappUrl: requestPayload.dappUrl) + + guard let oldSubscription = notifyStorage.getSubscription(topic: subscriptionTopic) else { + logger.debug("NotifyUpdateResponseSubscriber Subscription does not exist") + return + } + + notifyStorage.updateSubscription(oldSubscription, scope: scope, expiry: requestClaims.exp) + + logger.debug("Updated Subscription") + } + }.store(in: &publishers) + } + + func buildScope(selected: String, dappUrl: String) async throws -> [String: ScopeValue] { + let selectedScope = selected.components(separatedBy: " ") + let availableScope = try await subscriptionScopeProvider.getSubscriptionScope(dappUrl: dappUrl) + return availableScope.reduce(into: [:]) { + $0[$1.name] = ScopeValue(description: $1.description, enabled: selectedScope.contains($1.name)) + } + } +} diff --git a/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_pushSubscribe/PushSubscribeRequester.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeRequester.swift similarity index 61% rename from Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_pushSubscribe/PushSubscribeRequester.swift rename to Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeRequester.swift index cfd061084..7a77693ed 100644 --- a/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_pushSubscribe/PushSubscribeRequester.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeRequester.swift @@ -1,12 +1,9 @@ import Foundation -class PushSubscribeRequester { +class NotifySubscribeRequester { enum Errors: Error { - case didDocDoesNotContainKeyAgreement - case noVerificationMethodForKey - case unsupportedCurve case signatureRejected } @@ -24,7 +21,7 @@ class PushSubscribeRequester { identityClient: IdentityClient, logger: ConsoleLogging, kms: KeyManagementService, - webDidResolver: WebDidResolver = WebDidResolver(), + webDidResolver: WebDidResolver, subscriptionScopeProvider: SubscriptionScopeProvider, dappsMetadataStore: CodableStore ) { @@ -38,35 +35,36 @@ class PushSubscribeRequester { self.dappsMetadataStore = dappsMetadataStore } - @discardableResult func subscribe(metadata: AppMetadata, account: Account, onSign: @escaping SigningCallback) async throws -> SubscriptionJWTPayload.Wrapper { + @discardableResult func subscribe(metadata: AppMetadata, account: Account, onSign: @escaping SigningCallback) async throws -> NotifySubscriptionPayload.Wrapper { let dappUrl = metadata.url - logger.debug("Subscribing for Push") + logger.debug("Subscribing for Notify") - let peerPublicKey = try await resolvePublicKey(dappUrl: metadata.url) + let peerPublicKey = try await webDidResolver.resolvePublicKey(dappUrl: metadata.url) let subscribeTopic = peerPublicKey.rawRepresentation.sha256().toHexString() let keysY = try generateAgreementKeys(peerPublicKey: peerPublicKey) - let responseTopic = keysY.derivedTopic() + let responseTopic = keysY.derivedTopic() dappsMetadataStore.set(metadata, forKey: responseTopic) try kms.setSymmetricKey(keysY.sharedKey, for: subscribeTopic) - - _ = try await identityClient.register(account: account, onSign: onSign) - try kms.setAgreementSecret(keysY, topic: responseTopic) logger.debug("setting symm key for response topic \(responseTopic)") - let protocolMethod = PushSubscribeProtocolMethod() + let protocolMethod = NotifySubscribeProtocolMethod() - let subscriptionAuthWrapper = try await createJWTWrapper(subscriptionAccount: account, dappUrl: dappUrl) + let subscriptionAuthWrapper = try await createJWTWrapper( + dappPubKey: DIDKey(did: peerPublicKey.did), + subscriptionAccount: account, + dappUrl: dappUrl + ) let request = RPCRequest(method: protocolMethod.method, params: subscriptionAuthWrapper) - logger.debug("PushSubscribeRequester: subscribing to response topic: \(responseTopic)") + logger.debug("NotifySubscribeRequester: subscribing to response topic: \(responseTopic)") try await networkingInteractor.subscribe(topic: responseTopic) @@ -74,17 +72,6 @@ class PushSubscribeRequester { return subscriptionAuthWrapper } - private func resolvePublicKey(dappUrl: String) async throws -> AgreementPublicKey { - logger.debug("PushSubscribeRequester: Resolving DIDDoc for: \(dappUrl)") - let didDoc = try await webDidResolver.resolveDidDoc(domainUrl: dappUrl) - guard let keyAgreement = didDoc.keyAgreement.first else { throw Errors.didDocDoesNotContainKeyAgreement } - guard let verificationMethod = didDoc.verificationMethod.first(where: { verificationMethod in verificationMethod.id == keyAgreement }) else { throw Errors.noVerificationMethodForKey } - guard verificationMethod.publicKeyJwk.crv == .X25519 else { throw Errors.unsupportedCurve} - let pubKeyBase64Url = verificationMethod.publicKeyJwk.x - return try AgreementPublicKey(base64url: pubKeyBase64Url) - } - - private func generateAgreementKeys(peerPublicKey: AgreementPublicKey) throws -> AgreementKeys { let selfPubKey = try kms.createX25519KeyPair() @@ -92,10 +79,10 @@ class PushSubscribeRequester { return keys } - private func createJWTWrapper(subscriptionAccount: Account, dappUrl: String) async throws -> SubscriptionJWTPayload.Wrapper { + private func createJWTWrapper(dappPubKey: DIDKey, subscriptionAccount: Account, dappUrl: String) async throws -> NotifySubscriptionPayload.Wrapper { let types = try await subscriptionScopeProvider.getSubscriptionScope(dappUrl: dappUrl) let scope = types.map{$0.name}.joined(separator: " ") - let jwtPayload = SubscriptionJWTPayload(keyserver: keyserverURL, subscriptionAccount: subscriptionAccount, dappUrl: dappUrl, scope: scope) + let jwtPayload = NotifySubscriptionPayload(dappPubKey: dappPubKey, keyserver: keyserverURL, subscriptionAccount: subscriptionAccount, dappUrl: dappUrl, scope: scope) return try identityClient.signAndCreateWrapper( payload: jwtPayload, account: subscriptionAccount diff --git a/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_pushSubscribe/PushSubscribeResponseSubscriber.swift b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeResponseSubscriber.swift similarity index 66% rename from Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_pushSubscribe/PushSubscribeResponseSubscriber.swift rename to Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeResponseSubscriber.swift index d8c5fe66c..b663aaa14 100644 --- a/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_pushSubscribe/PushSubscribeResponseSubscriber.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/ProtocolEngine/wc_pushSubscribe/NotifySubscribeResponseSubscriber.swift @@ -1,7 +1,7 @@ import Foundation import Combine -class PushSubscribeResponseSubscriber { +class NotifySubscribeResponseSubscriber { enum Errors: Error { case couldNotCreateSubscription } @@ -16,7 +16,7 @@ class PushSubscribeResponseSubscriber { private let kms: KeyManagementServiceProtocol private var publishers = [AnyCancellable]() private let logger: ConsoleLogging - private let pushStorage: PushStorage + private let notifyStorage: NotifyStorage private let groupKeychainStorage: KeychainStorageProtocol private let dappsMetadataStore: CodableStore private let subscriptionScopeProvider: SubscriptionScopeProvider @@ -25,7 +25,7 @@ class PushSubscribeResponseSubscriber { kms: KeyManagementServiceProtocol, logger: ConsoleLogging, groupKeychainStorage: KeychainStorageProtocol, - pushStorage: PushStorage, + notifyStorage: NotifyStorage, dappsMetadataStore: CodableStore, subscriptionScopeProvider: SubscriptionScopeProvider ) { @@ -33,65 +33,69 @@ class PushSubscribeResponseSubscriber { self.kms = kms self.logger = logger self.groupKeychainStorage = groupKeychainStorage - self.pushStorage = pushStorage + self.notifyStorage = notifyStorage self.dappsMetadataStore = dappsMetadataStore self.subscriptionScopeProvider = subscriptionScopeProvider subscribeForSubscriptionResponse() } private func subscribeForSubscriptionResponse() { - let protocolMethod = PushSubscribeProtocolMethod() + let protocolMethod = NotifySubscribeProtocolMethod() networkingInteractor.responseSubscription(on: protocolMethod) - .sink {[unowned self] (payload: ResponseSubscriptionPayload) in + .sink { [unowned self] (payload: ResponseSubscriptionPayload) in Task(priority: .high) { - logger.debug("PushSubscribeResponseSubscriber: Received Push Subscribe response") + logger.debug("Received Notify Subscribe response") + + guard + let (responsePayload, _) = try? NotifySubscriptionResponsePayload.decodeAndVerify(from: payload.response) + else { fatalError() /* TODO: Handle error */ } guard let responseKeys = kms.getAgreementSecret(for: payload.topic) else { - logger.debug("PushSubscribeResponseSubscriber: no symmetric key for topic \(payload.topic)") + logger.debug("No symmetric key for topic \(payload.topic)") return subscriptionErrorSubject.send(Errors.couldNotCreateSubscription) } // get keypair Y let pubKeyY = responseKeys.publicKey - let peerPubKeyZ = payload.response.publicKey + let peerPubKeyZ = responsePayload.publicKey.hexString var account: Account! var metadata: AppMetadata! - var pushSubscriptionTopic: String! + var notifySubscriptionTopic: String! var subscribedTypes: Set! var agreementKeysP: AgreementKeys! - let (subscriptionPayload, claims) = try SubscriptionJWTPayload.decodeAndVerify(from: payload.request) + let (subscriptionPayload, claims) = try NotifySubscriptionPayload.decodeAndVerify(from: payload.request) let subscribedScope = subscriptionPayload.scope .components(separatedBy: " ") do { // generate symm key P agreementKeysP = try kms.performKeyAgreement(selfPublicKey: pubKeyY, peerPublicKey: peerPubKeyZ) - pushSubscriptionTopic = agreementKeysP.derivedTopic() - try kms.setAgreementSecret(agreementKeysP, topic: pushSubscriptionTopic) - try groupKeychainStorage.add(agreementKeysP, forKey: pushSubscriptionTopic) + notifySubscriptionTopic = agreementKeysP.derivedTopic() + try kms.setAgreementSecret(agreementKeysP, topic: notifySubscriptionTopic) + try groupKeychainStorage.add(agreementKeysP, forKey: notifySubscriptionTopic) account = try Account(DIDPKHString: claims.sub) metadata = try dappsMetadataStore.get(key: payload.topic) let availableTypes = try await subscriptionScopeProvider.getSubscriptionScope(dappUrl: metadata!.url) subscribedTypes = availableTypes.filter{subscribedScope.contains($0.name)} - logger.debug("PushSubscribeResponseSubscriber: subscribing push subscription topic: \(pushSubscriptionTopic!)") - try await networkingInteractor.subscribe(topic: pushSubscriptionTopic) + logger.debug("subscribing notify subscription topic: \(notifySubscriptionTopic!)") + try await networkingInteractor.subscribe(topic: notifySubscriptionTopic) } catch { - logger.debug("PushSubscribeResponseSubscriber: error: \(error)") + logger.debug("error: \(error)") return subscriptionErrorSubject.send(Errors.couldNotCreateSubscription) } guard let metadata = metadata else { - logger.debug("PushSubscribeResponseSubscriber: no metadata for topic: \(pushSubscriptionTopic!)") + logger.debug("No metadata for topic: \(notifySubscriptionTopic!)") return subscriptionErrorSubject.send(Errors.couldNotCreateSubscription) } dappsMetadataStore.delete(forKey: payload.topic) let expiry = Date(timeIntervalSince1970: TimeInterval(claims.exp)) let scope: [String: ScopeValue] = subscribedTypes.reduce(into: [:]) { $0[$1.name] = ScopeValue(description: $1.description, enabled: true) } - let pushSubscription = PushSubscription(topic: pushSubscriptionTopic, account: account, relay: RelayProtocolOptions(protocol: "irn", data: nil), metadata: metadata, scope: scope, expiry: expiry, symKey: agreementKeysP.sharedKey.hexRepresentation) + let notifySubscription = NotifySubscription(topic: notifySubscriptionTopic, account: account, relay: RelayProtocolOptions(protocol: "irn", data: nil), metadata: metadata, scope: scope, expiry: expiry, symKey: agreementKeysP.sharedKey.hexRepresentation) - try await pushStorage.setSubscription(pushSubscription) + notifyStorage.setSubscription(notifySubscription) - logger.debug("PushSubscribeResponseSubscriber: unsubscribing response topic: \(payload.topic)") + logger.debug("Unsubscribing response topic: \(payload.topic)") networkingInteractor.unsubscribe(topic: payload.topic) } }.store(in: &publishers) diff --git a/Sources/WalletConnectPush/Client/Wallet/SubscriptionScopeProvider.swift b/Sources/WalletConnectNotify/Client/Wallet/SubscriptionScopeProvider.swift similarity index 92% rename from Sources/WalletConnectPush/Client/Wallet/SubscriptionScopeProvider.swift rename to Sources/WalletConnectNotify/Client/Wallet/SubscriptionScopeProvider.swift index a2140addd..a362dd4e6 100644 --- a/Sources/WalletConnectPush/Client/Wallet/SubscriptionScopeProvider.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/SubscriptionScopeProvider.swift @@ -12,7 +12,7 @@ class SubscriptionScopeProvider { if let availableScope = cache[dappUrl] { return availableScope } - guard let scopeUrl = URL(string: "\(dappUrl)/.well-known/wc-push-config.json") else { throw Errors.invalidUrl } + guard let scopeUrl = URL(string: "\(dappUrl)/.well-known/wc-notify-config.json") else { throw Errors.invalidUrl } let (data, _) = try await URLSession.shared.data(from: scopeUrl) let config = try JSONDecoder().decode(NotificationConfig.self, from: data) let availableScope = Set(config.types) diff --git a/Sources/WalletConnectPush/Client/Wallet/SubscriptionsAutoUpdater.swift b/Sources/WalletConnectNotify/Client/Wallet/SubscriptionsAutoUpdater.swift similarity index 83% rename from Sources/WalletConnectPush/Client/Wallet/SubscriptionsAutoUpdater.swift rename to Sources/WalletConnectNotify/Client/Wallet/SubscriptionsAutoUpdater.swift index d6e939a5b..4eff66e89 100644 --- a/Sources/WalletConnectPush/Client/Wallet/SubscriptionsAutoUpdater.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/SubscriptionsAutoUpdater.swift @@ -4,19 +4,19 @@ import Foundation class SubscriptionsAutoUpdater { private let notifyUpdateRequester: NotifyUpdateRequesting private let logger: ConsoleLogging - private let pushStorage: PushStoring + private let notifyStorage: NotifyStoring init(notifyUpdateRequester: NotifyUpdateRequesting, logger: ConsoleLogging, - pushStorage: PushStoring) { + notifyStorage: NotifyStoring) { self.notifyUpdateRequester = notifyUpdateRequester self.logger = logger - self.pushStorage = pushStorage + self.notifyStorage = notifyStorage updateSubscriptionsIfNeeded() } private func updateSubscriptionsIfNeeded() { - for subscription in pushStorage.getSubscriptions() { + for subscription in notifyStorage.getSubscriptions() { if shouldUpdate(subscription: subscription) { let scope = Set(subscription.scope.filter{ $0.value.enabled == true }.keys) let topic = subscription.topic @@ -31,7 +31,7 @@ class SubscriptionsAutoUpdater { } } - private func shouldUpdate(subscription: PushSubscription) -> Bool { + private func shouldUpdate(subscription: NotifySubscription) -> Bool { let currentDate = Date() let calendar = Calendar.current let expiryDate = subscription.expiry diff --git a/Sources/WalletConnectNotify/Client/Wallet/WebDidResolver.swift b/Sources/WalletConnectNotify/Client/Wallet/WebDidResolver.swift new file mode 100644 index 000000000..2f27c2693 --- /dev/null +++ b/Sources/WalletConnectNotify/Client/Wallet/WebDidResolver.swift @@ -0,0 +1,29 @@ +import Foundation + +final class WebDidResolver { + + func resolvePublicKey(dappUrl: String) async throws -> AgreementPublicKey { + let didDoc = try await resolveDidDoc(domainUrl: dappUrl) + guard let keyAgreement = didDoc.keyAgreement.first else { throw Errors.didDocDoesNotContainKeyAgreement } + guard let verificationMethod = didDoc.verificationMethod.first(where: { verificationMethod in verificationMethod.id == keyAgreement }) else { throw Errors.noVerificationMethodForKey } + guard verificationMethod.publicKeyJwk.crv == .X25519 else { throw Errors.unsupportedCurve} + let pubKeyBase64Url = verificationMethod.publicKeyJwk.x + return try AgreementPublicKey(base64url: pubKeyBase64Url) + } +} + +private extension WebDidResolver { + + enum Errors: Error { + case invalidUrl + case didDocDoesNotContainKeyAgreement + case noVerificationMethodForKey + case unsupportedCurve + } + + func resolveDidDoc(domainUrl: String) async throws -> WebDidDoc { + guard let didDocUrl = URL(string: "\(domainUrl)/.well-known/did.json") else { throw Errors.invalidUrl } + let (data, _) = try await URLSession.shared.data(from: didDocUrl) + return try JSONDecoder().decode(WebDidDoc.self, from: data) + } +} diff --git a/Sources/WalletConnectNotify/Notify.swift b/Sources/WalletConnectNotify/Notify.swift new file mode 100644 index 000000000..3a8a0bb14 --- /dev/null +++ b/Sources/WalletConnectNotify/Notify.swift @@ -0,0 +1,26 @@ +import Foundation + +public class Notify { + public static var instance: NotifyClient = { + guard let config = Notify.config else { + fatalError("Error - you must call Notify.configure(_:) before accessing the shared wallet instance.") + } + Push.configure(pushHost: config.pushHost, environment: config.environment) + return NotifyClientFactory.create( + networkInteractor: Networking.interactor, + pairingRegisterer: Pair.registerer, + pushClient: Push.instance, + crypto: config.crypto + ) + }() + + private static var config: Config? + + private init() { } + + /// Wallet's configuration method + static public func configure(pushHost: String = "echo.walletconnect.com", environment: APNSEnvironment, crypto: CryptoProvider) { + Notify.config = Notify.Config(pushHost: pushHost, environment: environment, crypto: crypto) + } + +} diff --git a/Sources/WalletConnectEcho/EchoConfig.swift b/Sources/WalletConnectNotify/NotifyConfig.swift similarity index 51% rename from Sources/WalletConnectEcho/EchoConfig.swift rename to Sources/WalletConnectNotify/NotifyConfig.swift index 595ea2b73..479c830ab 100644 --- a/Sources/WalletConnectEcho/EchoConfig.swift +++ b/Sources/WalletConnectNotify/NotifyConfig.swift @@ -1,8 +1,9 @@ import Foundation -extension Echo { +extension Notify { struct Config { - let echoHost: String + let pushHost: String let environment: APNSEnvironment + let crypto: CryptoProvider } } diff --git a/Sources/WalletConnectNotify/NotifyImports.swift b/Sources/WalletConnectNotify/NotifyImports.swift new file mode 100644 index 000000000..74fcfa250 --- /dev/null +++ b/Sources/WalletConnectNotify/NotifyImports.swift @@ -0,0 +1,6 @@ +#if !CocoaPods +@_exported import WalletConnectPairing +@_exported import WalletConnectPush +@_exported import WalletConnectIdentity +@_exported import WalletConnectSigner +#endif diff --git a/Sources/WalletConnectNotify/NotifyStorageIdntifiers.swift b/Sources/WalletConnectNotify/NotifyStorageIdntifiers.swift new file mode 100644 index 000000000..fb1b21c53 --- /dev/null +++ b/Sources/WalletConnectNotify/NotifyStorageIdntifiers.swift @@ -0,0 +1,9 @@ +import Foundation + +enum NotifyStorageIdntifiers { + static let notifySubscription = "com.walletconnect.notify.notifySubscription" + + static let notifyMessagesRecords = "com.walletconnect.sdk.notifyMessagesRecords" + static let dappsMetadataStore = "com.walletconnect.sdk.dappsMetadataStore" + static let coldStartStore = "com.walletconnect.sdk.coldStartStore" +} diff --git a/Sources/WalletConnectPush/ProtocolMethods/PushDeleteProtocolMethod.swift b/Sources/WalletConnectNotify/ProtocolMethods/NotifyDeleteProtocolMethod.swift similarity index 64% rename from Sources/WalletConnectPush/ProtocolMethods/PushDeleteProtocolMethod.swift rename to Sources/WalletConnectNotify/ProtocolMethods/NotifyDeleteProtocolMethod.swift index dd90fe7e2..79e123797 100644 --- a/Sources/WalletConnectPush/ProtocolMethods/PushDeleteProtocolMethod.swift +++ b/Sources/WalletConnectNotify/ProtocolMethods/NotifyDeleteProtocolMethod.swift @@ -1,7 +1,7 @@ import Foundation -struct PushDeleteProtocolMethod: ProtocolMethod { - let method: String = "wc_pushDelete" +struct NotifyDeleteProtocolMethod: ProtocolMethod { + let method: String = "wc_notifyDelete" let requestConfig = RelayConfig(tag: 4004, prompt: false, ttl: 86400) diff --git a/Sources/WalletConnectPush/ProtocolMethods/PushMessageProtocolMethod.swift b/Sources/WalletConnectNotify/ProtocolMethods/NotifyMessageProtocolMethod.swift similarity index 58% rename from Sources/WalletConnectPush/ProtocolMethods/PushMessageProtocolMethod.swift rename to Sources/WalletConnectNotify/ProtocolMethods/NotifyMessageProtocolMethod.swift index 6345d1dc8..361aec841 100644 --- a/Sources/WalletConnectPush/ProtocolMethods/PushMessageProtocolMethod.swift +++ b/Sources/WalletConnectNotify/ProtocolMethods/NotifyMessageProtocolMethod.swift @@ -1,9 +1,9 @@ import Foundation -struct PushMessageProtocolMethod: ProtocolMethod { - let method: String = "wc_pushMessage" +struct NotifyMessageProtocolMethod: ProtocolMethod { + let method: String = "wc_notifyMessage" let requestConfig: RelayConfig = RelayConfig(tag: 4002, prompt: true, ttl: 2592000) - let responseConfig: RelayConfig = RelayConfig(tag: 4003, prompt: true, ttl: 2592000) + let responseConfig: RelayConfig = RelayConfig(tag: 4003, prompt: false, ttl: 2592000) } diff --git a/Sources/WalletConnectNotify/ProtocolMethods/NotifySubscribeProtocolMethod.swift b/Sources/WalletConnectNotify/ProtocolMethods/NotifySubscribeProtocolMethod.swift new file mode 100644 index 000000000..efb256dd7 --- /dev/null +++ b/Sources/WalletConnectNotify/ProtocolMethods/NotifySubscribeProtocolMethod.swift @@ -0,0 +1,10 @@ + +import Foundation + +struct NotifySubscribeProtocolMethod: ProtocolMethod { + let method: String = "wc_notifySubscribe" + + let requestConfig: RelayConfig = RelayConfig(tag: 4000, prompt: false, ttl: 86400) + + let responseConfig: RelayConfig = RelayConfig(tag: 4001, prompt: false, ttl: 86400) +} diff --git a/Sources/WalletConnectPush/ProtocolMethods/NotifyUpdateProtocolMethod.swift b/Sources/WalletConnectNotify/ProtocolMethods/NotifyUpdateProtocolMethod.swift similarity index 69% rename from Sources/WalletConnectPush/ProtocolMethods/NotifyUpdateProtocolMethod.swift rename to Sources/WalletConnectNotify/ProtocolMethods/NotifyUpdateProtocolMethod.swift index 198ec859f..01ed5a6d8 100644 --- a/Sources/WalletConnectPush/ProtocolMethods/NotifyUpdateProtocolMethod.swift +++ b/Sources/WalletConnectNotify/ProtocolMethods/NotifyUpdateProtocolMethod.swift @@ -2,10 +2,10 @@ import Foundation struct NotifyUpdateProtocolMethod: ProtocolMethod { - let method: String = "wc_pushUpdate" + let method: String = "wc_notifyUpdate" - let requestConfig: RelayConfig = RelayConfig(tag: 4008, prompt: true, ttl: 86400) + let requestConfig: RelayConfig = RelayConfig(tag: 4008, prompt: false, ttl: 86400) - let responseConfig: RelayConfig = RelayConfig(tag: 4009, prompt: true, ttl: 86400) + let responseConfig: RelayConfig = RelayConfig(tag: 4009, prompt: false, ttl: 86400) } diff --git a/Sources/WalletConnectNotify/RPCRequests/NotifyDeleteParams.swift b/Sources/WalletConnectNotify/RPCRequests/NotifyDeleteParams.swift new file mode 100644 index 000000000..7b998c753 --- /dev/null +++ b/Sources/WalletConnectNotify/RPCRequests/NotifyDeleteParams.swift @@ -0,0 +1,10 @@ +import Foundation + +public struct NotifyDeleteParams: Codable { + let code: Int + let message: String + + static var userDisconnected: NotifyDeleteParams { + return NotifyDeleteParams(code: 6000, message: "User Disconnected") + } +} diff --git a/Sources/WalletConnectPush/RPCRequests/NotifyProposeParams.swift b/Sources/WalletConnectNotify/RPCRequests/NotifyProposeParams.swift similarity index 100% rename from Sources/WalletConnectPush/RPCRequests/NotifyProposeParams.swift rename to Sources/WalletConnectNotify/RPCRequests/NotifyProposeParams.swift diff --git a/Sources/WalletConnectPush/RPCRequests/NotifyProposeResponseParams.swift b/Sources/WalletConnectNotify/RPCRequests/NotifyProposeResponseParams.swift similarity index 100% rename from Sources/WalletConnectPush/RPCRequests/NotifyProposeResponseParams.swift rename to Sources/WalletConnectNotify/RPCRequests/NotifyProposeResponseParams.swift diff --git a/Sources/WalletConnectPush/RPCRequests/SubscribeResponseParams.swift b/Sources/WalletConnectNotify/RPCRequests/SubscribeResponseParams.swift similarity index 100% rename from Sources/WalletConnectPush/RPCRequests/SubscribeResponseParams.swift rename to Sources/WalletConnectNotify/RPCRequests/SubscribeResponseParams.swift diff --git a/Sources/WalletConnectPush/Types/NotificationConfig.swift b/Sources/WalletConnectNotify/Types/NotificationConfig.swift similarity index 99% rename from Sources/WalletConnectPush/Types/NotificationConfig.swift rename to Sources/WalletConnectNotify/Types/NotificationConfig.swift index c53e9b674..f17133c93 100644 --- a/Sources/WalletConnectPush/Types/NotificationConfig.swift +++ b/Sources/WalletConnectNotify/Types/NotificationConfig.swift @@ -5,5 +5,4 @@ struct NotificationConfig: Codable { let version: Int let lastModified: TimeInterval let types: [NotificationType] - } diff --git a/Sources/WalletConnectPush/Types/NotificationType.swift b/Sources/WalletConnectNotify/Types/NotificationType.swift similarity index 100% rename from Sources/WalletConnectPush/Types/NotificationType.swift rename to Sources/WalletConnectNotify/Types/NotificationType.swift diff --git a/Sources/WalletConnectPush/Types/PushError.swift b/Sources/WalletConnectNotify/Types/NotifyError.swift similarity index 90% rename from Sources/WalletConnectPush/Types/PushError.swift rename to Sources/WalletConnectNotify/Types/NotifyError.swift index 82c16c00e..31d567893 100644 --- a/Sources/WalletConnectPush/Types/PushError.swift +++ b/Sources/WalletConnectNotify/Types/NotifyError.swift @@ -1,13 +1,13 @@ import Foundation -public enum PushError: Codable, Equatable, Error { +public enum NotifyError: Codable, Equatable, Error { case userRejeted case userHasExistingSubscription case methodUnsupported case registerSignatureRejected } -extension PushError: Reason { +extension NotifyError: Reason { init?(code: Int) { switch code { @@ -41,7 +41,7 @@ extension PushError: Reason { case .methodUnsupported: return "Method Unsupported" case .userRejeted: - return "Push request rejected" + return "Notify request rejected" case .userHasExistingSubscription: return "User Has Existing Subscription" case .registerSignatureRejected: diff --git a/Sources/WalletConnectPush/Types/PushMessage.swift b/Sources/WalletConnectNotify/Types/NotifyMessage.swift similarity index 88% rename from Sources/WalletConnectPush/Types/PushMessage.swift rename to Sources/WalletConnectNotify/Types/NotifyMessage.swift index d48604937..783721bbd 100644 --- a/Sources/WalletConnectPush/Types/PushMessage.swift +++ b/Sources/WalletConnectNotify/Types/NotifyMessage.swift @@ -1,6 +1,6 @@ import Foundation -public struct PushMessage: Codable, Equatable { +public struct NotifyMessage: Codable, Equatable { public let title: String public let body: String public let icon: String diff --git a/Sources/WalletConnectNotify/Types/NotifyRequest.swift b/Sources/WalletConnectNotify/Types/NotifyRequest.swift new file mode 100644 index 000000000..be2a15c42 --- /dev/null +++ b/Sources/WalletConnectNotify/Types/NotifyRequest.swift @@ -0,0 +1,3 @@ +import Foundation + +public typealias NotifyRequest = (id: RPCID, account: Account, metadata: AppMetadata) diff --git a/Sources/WalletConnectPush/Types/PushSubscription.swift b/Sources/WalletConnectNotify/Types/NotifySubscription.swift similarity index 89% rename from Sources/WalletConnectPush/Types/PushSubscription.swift rename to Sources/WalletConnectNotify/Types/NotifySubscription.swift index 756d33781..b44189c80 100644 --- a/Sources/WalletConnectPush/Types/PushSubscription.swift +++ b/Sources/WalletConnectNotify/Types/NotifySubscription.swift @@ -1,6 +1,6 @@ import Foundation -public struct PushSubscription: DatabaseObject { +public struct NotifySubscription: DatabaseObject { public let topic: String public let account: Account public let relay: RelayProtocolOptions diff --git a/Sources/WalletConnectNotify/Types/NotifySubscriptionResult.swift b/Sources/WalletConnectNotify/Types/NotifySubscriptionResult.swift new file mode 100644 index 000000000..c9df6a3de --- /dev/null +++ b/Sources/WalletConnectNotify/Types/NotifySubscriptionResult.swift @@ -0,0 +1,7 @@ + +import Foundation + +public struct NotifySubscriptionResult: Equatable, Codable { + public let notifySubscription: NotifySubscription + public let subscriptionAuth: String +} diff --git a/Sources/WalletConnectNotify/Types/Payload/NotifyDeletePayload.swift b/Sources/WalletConnectNotify/Types/Payload/NotifyDeletePayload.swift new file mode 100644 index 000000000..62aa74204 --- /dev/null +++ b/Sources/WalletConnectNotify/Types/Payload/NotifyDeletePayload.swift @@ -0,0 +1,77 @@ +import Foundation + +struct NotifyDeletePayload: JWTClaimsCodable { + + struct Claims: JWTClaims { + /// Timestamp when JWT was issued + let iat: UInt64 + /// Timestamp when JWT must expire + let exp: UInt64 + /// Key server URL + let ksu: String + /// Description of action intent. Must be equal to `notify_delete` + let act: String? + + /// `did:key` of an identity key. Enables to resolve attached blockchain account. + let iss: String + /// `did:key` of an identity key. Enables to resolve associated Dapp domain used. + let aud: String + /// Reason for deleting the subscription + let sub: String + /// Dapp's domain url + let app: String + + static var action: String? { + return "notify_delete" + } + } + + struct Wrapper: JWTWrapper { + let deleteAuth: String + + init(jwtString: String) { + self.deleteAuth = jwtString + } + + var jwtString: String { + return deleteAuth + } + } + + let keyserver: URL + let dappPubKey: DIDKey + let reason: String + let app: String + + init( + keyserver: URL, + dappPubKey: DIDKey, + reason: String, + app: String + ) { + self.keyserver = keyserver + self.dappPubKey = dappPubKey + self.reason = reason + self.app = app + } + + init(claims: Claims) throws { + self.keyserver = try claims.ksu.asURL() + self.dappPubKey = try DIDKey(did: claims.aud) + self.reason = claims.sub + self.app = claims.app + } + + func encode(iss: String) throws -> Claims { + return Claims( + iat: defaultIat(), + exp: expiry(days: 1), + ksu: keyserver.absoluteString, + act: Claims.action, + iss: iss, + aud: dappPubKey.did(variant: .ED25519), + sub: reason, + app: app + ) + } +} diff --git a/Sources/WalletConnectNotify/Types/Payload/NotifyDeleteResponsePayload.swift b/Sources/WalletConnectNotify/Types/Payload/NotifyDeleteResponsePayload.swift new file mode 100644 index 000000000..338ff414e --- /dev/null +++ b/Sources/WalletConnectNotify/Types/Payload/NotifyDeleteResponsePayload.swift @@ -0,0 +1,77 @@ +import Foundation + +struct NotifyDeleteResponsePayload: JWTClaimsCodable { + + struct Claims: JWTClaims { + /// Timestamp when JWT was issued + let iat: UInt64 + /// Timestamp when JWT must expire + let exp: UInt64 + /// Key server URL + let ksu: String + /// Description of action intent. Must be equal to `notify_delete_response` + let act: String? + + /// `did:key` of an identity key. Enables to resolve associated Dapp domain used + let iss: String + /// `did:key` of an identity key. Enables to resolve attached blockchain account. + let aud: String + /// Hash of the existing subscription payload + let sub: String + /// Dapp's domain url + let app: String + + static var action: String? { + return "notify_delete_response" + } + } + + struct Wrapper: JWTWrapper { + let responseAuth: String + + init(jwtString: String) { + self.responseAuth = jwtString + } + + var jwtString: String { + return responseAuth + } + } + + let keyserver: URL + let selfPubKey: DIDKey + let subscriptionHash: String + let app: String + + init( + keyserver: URL, + selfPubKey: DIDKey, + subscriptionHash: String, + app: String + ) { + self.keyserver = keyserver + self.selfPubKey = selfPubKey + self.subscriptionHash = subscriptionHash + self.app = app + } + + init(claims: Claims) throws { + self.keyserver = try claims.ksu.asURL() + self.selfPubKey = try DIDKey(did: claims.aud) + self.subscriptionHash = claims.sub + self.app = claims.app + } + + func encode(iss: String) throws -> Claims { + return Claims( + iat: defaultIat(), + exp: expiry(days: 1), + ksu: keyserver.absoluteString, + act: Claims.action, + iss: iss, + aud: selfPubKey.did(variant: .ED25519), + sub: subscriptionHash, + app: app + ) + } +} diff --git a/Sources/WalletConnectNotify/Types/Payload/NotifyMessagePayload.swift b/Sources/WalletConnectNotify/Types/Payload/NotifyMessagePayload.swift new file mode 100644 index 000000000..e932f80fe --- /dev/null +++ b/Sources/WalletConnectNotify/Types/Payload/NotifyMessagePayload.swift @@ -0,0 +1,89 @@ +import Foundation + +struct NotifyMessagePayload: JWTClaimsCodable { + + struct Claims: JWTClaims { + /// Timestamp when JWT was issued + let iat: UInt64 + /// Timestamp when JWT must expire + let exp: UInt64 + /// Key server URL + let ksu: String + /// Action intent (must be `notify_message`) + let act: String? + + /// `did:key` of an identity key. Enables to resolve associated Dapp domain used. diddoc authentication key + let iss: String + /// Blockchain account `did:pkh` + let aud: String + /// Subscription ID (sha256 hash of subscriptionAuth) + let sub: String + /// Dapp domain url + let app: String + /// Message object + let msg: NotifyMessage + + static var action: String? { + return "notify_message" + } + } + + struct Wrapper: JWTWrapper { + let messageAuth: String + + init(jwtString: String) { + self.messageAuth = jwtString + } + + var jwtString: String { + return messageAuth + } + } + + let castServerPubKey: DIDKey + let keyserver: URL + let account: Account + let subscriptionId: String + let app: String + let message: NotifyMessage + + init( + castServerPubKey: DIDKey, + keyserver: URL, + account: Account, + subscriptionId: String, + app: String, + message: NotifyMessage + ) { + self.castServerPubKey = castServerPubKey + self.keyserver = keyserver + self.account = account + self.subscriptionId = subscriptionId + self.app = app + self.message = message + } + + init(claims: Claims) throws { + self.castServerPubKey = try DIDKey(did: claims.iss) + self.keyserver = try claims.ksu.asURL() + self.account = try DIDPKH(did: claims.aud).account + self.subscriptionId = claims.sub + self.app = claims.app + self.message = claims.msg + } + + func encode(iss: String) throws -> Claims { + return Claims( + iat: defaultIat(), + exp: expiry(days: 1), + ksu: keyserver.absoluteString, + act: Claims.action, + iss: castServerPubKey.multibase(variant: .ED25519), + aud: account.did, + sub: subscriptionId, + app: app, + msg: message + ) + } + +} diff --git a/Sources/WalletConnectNotify/Types/Payload/NotifyMessageReceiptPayload.swift b/Sources/WalletConnectNotify/Types/Payload/NotifyMessageReceiptPayload.swift new file mode 100644 index 000000000..57934d03c --- /dev/null +++ b/Sources/WalletConnectNotify/Types/Payload/NotifyMessageReceiptPayload.swift @@ -0,0 +1,77 @@ +import Foundation + +struct NotifyMessageReceiptPayload: JWTClaimsCodable { + + struct Claims: JWTClaims { + /// Timestamp when JWT was issued + let iat: UInt64 + /// Timestamp when JWT must expire + let exp: UInt64 + /// Key server URL + let ksu: String + /// Action intent (must be `notify_receipt`) + let act: String? + + /// `did:key` of an identity key. Enables to resolve attached blockchain account. + let iss: String + /// `did:key` of an identity key. Enables to resolve associated Dapp domain used. + let aud: String + /// Hash of the stringified notify message object received + let sub: String + /// Dapp's domain url + let app: String + + static var action: String? { + return "notify_receipt" + } + } + + struct Wrapper: JWTWrapper { + let receiptAuth: String + + init(jwtString: String) { + self.receiptAuth = jwtString + } + + var jwtString: String { + return receiptAuth + } + } + + let keyserver: URL + let dappPubKey: DIDKey + let messageHash: String + let app: String + + init( + keyserver: URL, + dappPubKey: DIDKey, + messageHash: String, + app: String + ) { + self.keyserver = keyserver + self.dappPubKey = dappPubKey + self.messageHash = messageHash + self.app = app + } + + init(claims: Claims) throws { + self.keyserver = try claims.ksu.asURL() + self.dappPubKey = try DIDKey(did: claims.aud) + self.messageHash = claims.sub + self.app = claims.app + } + + func encode(iss: String) throws -> Claims { + return Claims( + iat: defaultIat(), + exp: expiry(days: 1), + ksu: keyserver.absoluteString, + act: Claims.action, + iss: iss, + aud: dappPubKey.did(variant: .ED25519), + sub: messageHash, + app: app + ) + } +} diff --git a/Sources/WalletConnectNotify/Types/Payload/NotifySubscriptionPayload.swift b/Sources/WalletConnectNotify/Types/Payload/NotifySubscriptionPayload.swift new file mode 100644 index 000000000..847635da0 --- /dev/null +++ b/Sources/WalletConnectNotify/Types/Payload/NotifySubscriptionPayload.swift @@ -0,0 +1,79 @@ +import Foundation + +struct NotifySubscriptionPayload: JWTClaimsCodable { + + struct Claims: JWTClaims { + /// Timestamp when JWT was issued + let iat: UInt64 + /// Timestamp when JWT must expire + let exp: UInt64 + /// Key server URL + let ksu: String + /// Description of action intent. Must be equal to `notify_subscription` + let act: String? + + /// `did:key` of an identity key. Enables to resolve attached blockchain account. + let iss: String + /// `did:key` of an identity key. Enables to resolve associated Dapp domain used. + let aud: String + /// Blockchain account that notify subscription has been proposed for -`did:pkh` + let sub: String + /// Scope of notification types authorized by the user + let scp: String + /// Dapp's domain url + let app: String + + static var action: String? { + return "notify_subscription" + } + } + + struct Wrapper: JWTWrapper { + let subscriptionAuth: String + + init(jwtString: String) { + self.subscriptionAuth = jwtString + } + + var jwtString: String { + return subscriptionAuth + } + } + + let dappPubKey: DIDKey + let keyserver: URL + let subscriptionAccount: Account + let dappUrl: String + let scope: String + + init(dappPubKey: DIDKey, keyserver: URL, subscriptionAccount: Account, dappUrl: String, scope: String) { + self.dappPubKey = dappPubKey + self.keyserver = keyserver + self.subscriptionAccount = subscriptionAccount + self.dappUrl = dappUrl + self.scope = scope + } + + init(claims: Claims) throws { + self.dappPubKey = try DIDKey(did: claims.aud) + self.keyserver = try claims.ksu.asURL() + self.subscriptionAccount = try Account(DIDPKHString: claims.sub) + self.dappUrl = claims.app + self.scope = claims.scp + } + + func encode(iss: String) throws -> Claims { + return Claims( + iat: defaultIat(), + exp: expiry(days: 30), + ksu: keyserver.absoluteString, + act: Claims.action, + iss: iss, + aud: dappPubKey.did(variant: .ED25519), + sub: subscriptionAccount.did, + scp: scope, + app: dappUrl + ) + } +} + diff --git a/Sources/WalletConnectNotify/Types/Payload/NotifySubscriptionResponsePayload.swift b/Sources/WalletConnectNotify/Types/Payload/NotifySubscriptionResponsePayload.swift new file mode 100644 index 000000000..b330882c1 --- /dev/null +++ b/Sources/WalletConnectNotify/Types/Payload/NotifySubscriptionResponsePayload.swift @@ -0,0 +1,65 @@ +import Foundation + +struct NotifySubscriptionResponsePayload: JWTClaimsCodable { + + struct Claims: JWTClaims { + /// timestamp when jwt was issued + let iat: UInt64 + /// timestamp when jwt must expire + let exp: UInt64 + /// Key server URL + let ksu: String + /// Description of action intent. Must be equal to "notify_subscription_response" + let act: String? + + /// `did:key` of an identity key. Allows for the resolution of which Notify server was used. + let iss: String + /// `did:key` of an identity key. Allows for the resolution of the attached blockchain account. + let aud: String + /// `did:key` of the public key used for key agreement on the Notify topic + let sub: String + /// Dapp's domain url + let app: String + + static var action: String? { + return "notify_subscription_response" + } + } + + struct Wrapper: JWTWrapper { + let responseAuth: String + + init(jwtString: String) { + self.responseAuth = jwtString + } + + var jwtString: String { + return responseAuth + } + } + + let keyserver: URL + let selfPubKey: DIDKey + let publicKey: DIDKey + let app: String + + init(claims: Claims) throws { + self.keyserver = try claims.ksu.asURL() + self.selfPubKey = try DIDKey(did: claims.aud) + self.publicKey = try DIDKey(did: claims.sub) + self.app = claims.app + } + + func encode(iss: String) throws -> Claims { + return Claims( + iat: defaultIat(), + exp: expiry(days: 1), + ksu: keyserver.absoluteString, + act: Claims.action, + iss: iss, + aud: selfPubKey.did(variant: .ED25519), + sub: publicKey.did(variant: .X25519), + app: app + ) + } +} diff --git a/Sources/WalletConnectNotify/Types/Payload/NotifyUpdatePayload.swift b/Sources/WalletConnectNotify/Types/Payload/NotifyUpdatePayload.swift new file mode 100644 index 000000000..ef88ebc87 --- /dev/null +++ b/Sources/WalletConnectNotify/Types/Payload/NotifyUpdatePayload.swift @@ -0,0 +1,78 @@ +import Foundation + +struct NotifyUpdatePayload: JWTClaimsCodable { + + struct Claims: JWTClaims { + /// Timestamp when JWT was issued + let iat: UInt64 + /// Timestamp when JWT must expire + let exp: UInt64 + /// Key server URL + let ksu: String + /// Description of action intent. Must be equal to `notify_update` + let act: String? + + /// `did:key` of an identity key. Enables to resolve attached blockchain account. + let iss: String + /// `did:key` of an identity key. Enables to resolve associated Dapp domain used. + let aud: String + /// Blockchain account that notify subscription has been proposed for -`did:pkh` + let sub: String + /// Scope of notification types authorized by the user + let scp: String + /// Dapp's domain url + let app: String + + static var action: String? { + return "notify_update" + } + } + + struct Wrapper: JWTWrapper { + let updateAuth: String + + init(jwtString: String) { + self.updateAuth = jwtString + } + + var jwtString: String { + return updateAuth + } + } + + let dappPubKey: DIDKey + let keyserver: URL + let subscriptionAccount: Account + let dappUrl: String + let scope: String + + init(dappPubKey: DIDKey, keyserver: URL, subscriptionAccount: Account, dappUrl: String, scope: String) { + self.dappPubKey = dappPubKey + self.keyserver = keyserver + self.subscriptionAccount = subscriptionAccount + self.dappUrl = dappUrl + self.scope = scope + } + + init(claims: Claims) throws { + self.dappPubKey = try DIDKey(did: claims.aud) + self.keyserver = try claims.ksu.asURL() + self.subscriptionAccount = try Account(DIDPKHString: claims.sub) + self.dappUrl = claims.app + self.scope = claims.scp + } + + func encode(iss: String) throws -> Claims { + return Claims( + iat: defaultIat(), + exp: expiry(days: 30), + ksu: keyserver.absoluteString, + act: Claims.action, + iss: iss, + aud: dappPubKey.did(variant: .ED25519), + sub: subscriptionAccount.did, + scp: scope, + app: dappUrl + ) + } +} diff --git a/Sources/WalletConnectNotify/Types/Payload/NotifyUpdateResponsePayload.swift b/Sources/WalletConnectNotify/Types/Payload/NotifyUpdateResponsePayload.swift new file mode 100644 index 000000000..db607ffea --- /dev/null +++ b/Sources/WalletConnectNotify/Types/Payload/NotifyUpdateResponsePayload.swift @@ -0,0 +1,65 @@ +import Foundation + +struct NotifyUpdateResponsePayload: JWTClaimsCodable { + + struct Claims: JWTClaims { + /// timestamp when jwt was issued + let iat: UInt64 + /// timestamp when jwt must expire + let exp: UInt64 + /// Key server URL + let ksu: String + /// Description of action intent. Must be equal to "notify_update_response" + let act: String? + + /// `did:key` of an identity key. Enables to resolve associated Dapp domain used. + let iss: String + /// `did:key` of an identity key. Enables to resolve attached blockchain account. + let aud: String + /// Hash of the new subscription payload + let sub: String + /// Dapp's domain url + let app: String + + static var action: String? { + return "notify_update_response" + } + } + + struct Wrapper: JWTWrapper { + let responseAuth: String + + init(jwtString: String) { + self.responseAuth = jwtString + } + + var jwtString: String { + return responseAuth + } + } + + let keyserver: URL + let selfPubKey: DIDKey + let subscriptionHash: String + let app: String + + init(claims: Claims) throws { + self.keyserver = try claims.ksu.asURL() + self.selfPubKey = try DIDKey(did: claims.aud) + self.subscriptionHash = claims.sub + self.app = claims.app + } + + func encode(iss: String) throws -> Claims { + return Claims( + iat: defaultIat(), + exp: expiry(days: 1), + ksu: keyserver.absoluteString, + act: Claims.action, + iss: iss, + aud: selfPubKey.did(variant: .ED25519), + sub: subscriptionHash, + app: app + ) + } +} diff --git a/Sources/WalletConnectPush/Types/WebDidDoc.swift b/Sources/WalletConnectNotify/Types/WebDidDoc.swift similarity index 94% rename from Sources/WalletConnectPush/Types/WebDidDoc.swift rename to Sources/WalletConnectNotify/Types/WebDidDoc.swift index aca4975f0..adf0c17f9 100644 --- a/Sources/WalletConnectPush/Types/WebDidDoc.swift +++ b/Sources/WalletConnectNotify/Types/WebDidDoc.swift @@ -24,7 +24,8 @@ extension WebDidDoc { struct PublicKeyJwk: Codable { enum Curve: String, Codable { - case X25519 = "X25519" + case X25519 + case Ed25519 } let kty: String diff --git a/Sources/WalletConnectPairing/PairingClient.swift b/Sources/WalletConnectPairing/PairingClient.swift index 389c11354..3eafd2838 100644 --- a/Sources/WalletConnectPairing/PairingClient.swift +++ b/Sources/WalletConnectPairing/PairingClient.swift @@ -7,6 +7,7 @@ public class PairingClient: PairingRegisterer, PairingInteracting, PairingClient } public let socketConnectionStatusPublisher: AnyPublisher + private let pairingStorage: WCPairingStorage private let walletPairService: WalletPairService private let appPairService: AppPairService private let appPairActivateService: AppPairActivationService @@ -22,20 +23,23 @@ public class PairingClient: PairingRegisterer, PairingInteracting, PairingClient private let cleanupService: PairingCleanupService - init(appPairService: AppPairService, - networkingInteractor: NetworkInteracting, - logger: ConsoleLogging, - walletPairService: WalletPairService, - deletePairingService: DeletePairingService, - resubscribeService: PairingResubscribeService, - expirationService: ExpirationService, - pairingRequestsSubscriber: PairingRequestsSubscriber, - appPairActivateService: AppPairActivationService, - cleanupService: PairingCleanupService, - pingService: PairingPingService, - socketConnectionStatusPublisher: AnyPublisher, - pairingsProvider: PairingsProvider + init( + pairingStorage: WCPairingStorage, + appPairService: AppPairService, + networkingInteractor: NetworkInteracting, + logger: ConsoleLogging, + walletPairService: WalletPairService, + deletePairingService: DeletePairingService, + resubscribeService: PairingResubscribeService, + expirationService: ExpirationService, + pairingRequestsSubscriber: PairingRequestsSubscriber, + appPairActivateService: AppPairActivationService, + cleanupService: PairingCleanupService, + pingService: PairingPingService, + socketConnectionStatusPublisher: AnyPublisher, + pairingsProvider: PairingsProvider ) { + self.pairingStorage = pairingStorage self.appPairService = appPairService self.walletPairService = walletPairService self.networkingInteractor = networkingInteractor @@ -81,6 +85,15 @@ public class PairingClient: PairingRegisterer, PairingInteracting, PairingClient public func activate(pairingTopic: String, peerMetadata: AppMetadata?) { appPairActivateService.activate(for: pairingTopic, peerMetadata: peerMetadata) } + + public func setReceived(pairingTopic: String) { + guard var pairing = pairingStorage.getPairing(forTopic: pairingTopic) else { + return logger.error("Pairing not found for topic: \(pairingTopic)") + } + + pairing.receivedRequest() + pairingStorage.setPairing(pairing) + } public func getPairings() -> [Pairing] { pairingsProvider.getPairings() diff --git a/Sources/WalletConnectPairing/PairingClientFactory.swift b/Sources/WalletConnectPairing/PairingClientFactory.swift index 8666747ac..ba49bb6af 100644 --- a/Sources/WalletConnectPairing/PairingClientFactory.swift +++ b/Sources/WalletConnectPairing/PairingClientFactory.swift @@ -12,8 +12,9 @@ public struct PairingClientFactory { public static func create(logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, networkingClient: NetworkingInteractor) -> 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) + let walletPairService = WalletPairService(networkingInteractor: networkingClient, kms: kms, pairingStorage: pairingStore, history: history) let pairingRequestsSubscriber = PairingRequestsSubscriber(networkingInteractor: networkingClient, pairingStorage: pairingStore, logger: logger) let pairingsProvider = PairingsProvider(pairingStorage: pairingStore) let cleanupService = PairingCleanupService(pairingStore: pairingStore, kms: kms) @@ -24,6 +25,7 @@ public struct PairingClientFactory { let resubscribeService = PairingResubscribeService(networkInteractor: networkingClient, pairingStorage: pairingStore) return PairingClient( + pairingStorage: pairingStore, appPairService: appPairService, networkingInteractor: networkingClient, logger: logger, diff --git a/Sources/WalletConnectPairing/PairingRegisterer.swift b/Sources/WalletConnectPairing/PairingRegisterer.swift index 6aa017038..e09cf9a42 100644 --- a/Sources/WalletConnectPairing/PairingRegisterer.swift +++ b/Sources/WalletConnectPairing/PairingRegisterer.swift @@ -7,5 +7,6 @@ public protocol PairingRegisterer { ) -> AnyPublisher, Never> func activate(pairingTopic: String, peerMetadata: AppMetadata?) + func setReceived(pairingTopic: String) func validatePairingExistance(_ topic: String) throws } diff --git a/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift b/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift index cfbd6c930..c962b8a41 100644 --- a/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift +++ b/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift @@ -3,34 +3,80 @@ import Foundation actor WalletPairService { enum Errors: Error { case pairingAlreadyExist(topic: String) + case networkNotConnected } let networkingInteractor: NetworkInteracting let kms: KeyManagementServiceProtocol private let pairingStorage: WCPairingStorage + private let history: RPCHistory - init(networkingInteractor: NetworkInteracting, - kms: KeyManagementServiceProtocol, - pairingStorage: WCPairingStorage) { + init( + networkingInteractor: NetworkInteracting, + kms: KeyManagementServiceProtocol, + pairingStorage: WCPairingStorage, + history: RPCHistory + ) { self.networkingInteractor = networkingInteractor self.kms = kms self.pairingStorage = pairingStorage + self.history = history } func pair(_ uri: WalletConnectURI) async throws { - guard !hasPairing(for: uri.topic) else { - throw Errors.pairingAlreadyExist(topic: uri.topic) + guard try !pairingHasPendingRequest(for: uri.topic) else { + return } - var pairing = WCPairing(uri: uri) + + let pairing = WCPairing(uri: uri) let symKey = try SymmetricKey(hex: uri.symKey) try kms.setSymmetricKey(symKey, for: pairing.topic) - pairing.activate() pairingStorage.setPairing(pairing) + + let networkConnectionStatus = await resolveNetworkConnectionStatus() + guard networkConnectionStatus == .connected else { + throw Errors.networkNotConnected + } + try await networkingInteractor.subscribe(topic: pairing.topic) } +} + +// MARK: - Private functions +extension WalletPairService { + func pairingHasPendingRequest(for topic: String) throws -> Bool { + guard let pairing = pairingStorage.getPairing(forTopic: topic), pairing.requestReceived else { + return false + } + + if pairing.active { + throw Errors.pairingAlreadyExist(topic: topic) + } + + let pendingRequests = history.getPending() + .compactMap { record -> RPCRequest? in + (record.topic == pairing.topic) ? record.request : nil + } - func hasPairing(for topic: String) -> Bool { - return pairingStorage.hasPairing(forTopic: topic) + if let pendingRequest = pendingRequests.first { + networkingInteractor.handleHistoryRequest(topic: topic, request: pendingRequest) + return true + } + return false + } + + private func resolveNetworkConnectionStatus() async -> NetworkConnectionStatus { + return await withCheckedContinuation { continuation in + let cancellable = networkingInteractor.networkConnectionStatusPublisher.sink { value in + continuation.resume(returning: value) + } + + Task(priority: .high) { + await withTaskCancellationHandler { + cancellable.cancel() + } onCancel: { } + } + } } } @@ -38,7 +84,8 @@ actor WalletPairService { extension WalletPairService.Errors: LocalizedError { var errorDescription: String? { switch self { - case .pairingAlreadyExist(let topic): return "Pairing with topic (\(topic)) already exist" + case .pairingAlreadyExist(let topic): return "Pairing with topic (\(topic)) is already active" + case .networkNotConnected: return "Pairing failed. You seem to be offline" } } } diff --git a/Sources/WalletConnectPairing/Types/WCPairing.swift b/Sources/WalletConnectPairing/Types/WCPairing.swift index 6ceb323cd..d87bd8946 100644 --- a/Sources/WalletConnectPairing/Types/WCPairing.swift +++ b/Sources/WalletConnectPairing/Types/WCPairing.swift @@ -11,6 +11,7 @@ public struct WCPairing: SequenceObject { public private (set) var peerMetadata: AppMetadata? public private (set) var expiryDate: Date public private (set) var active: Bool + public private (set) var requestReceived: Bool #if DEBUG public static var dateInitializer: () -> Date = Date.init @@ -26,11 +27,12 @@ public struct WCPairing: SequenceObject { 30 * .day } - public init(topic: String, relay: RelayProtocolOptions, peerMetadata: AppMetadata, isActive: Bool = false, expiryDate: Date) { + public init(topic: String, relay: RelayProtocolOptions, peerMetadata: AppMetadata, isActive: Bool = false, requestReceived: Bool = false, expiryDate: Date) { self.topic = topic self.relay = relay self.peerMetadata = peerMetadata self.active = isActive + self.requestReceived = requestReceived self.expiryDate = expiryDate } @@ -38,6 +40,7 @@ public struct WCPairing: SequenceObject { self.topic = topic self.relay = RelayProtocolOptions(protocol: "irn", data: nil) self.active = false + self.requestReceived = false self.expiryDate = Self.dateInitializer().advanced(by: Self.timeToLiveInactive) } @@ -45,6 +48,7 @@ public struct WCPairing: SequenceObject { self.topic = uri.topic self.relay = uri.relay self.active = false + self.requestReceived = false self.expiryDate = Self.dateInitializer().advanced(by: Self.timeToLiveInactive) } @@ -52,6 +56,10 @@ public struct WCPairing: SequenceObject { active = true try? updateExpiry() } + + public mutating func receivedRequest() { + requestReceived = true + } public mutating func updatePeerMetadata(_ metadata: AppMetadata?) { peerMetadata = metadata diff --git a/Sources/WalletConnectEcho/APNSEnvironment.swift b/Sources/WalletConnectPush/APNSEnvironment.swift similarity index 100% rename from Sources/WalletConnectEcho/APNSEnvironment.swift rename to Sources/WalletConnectPush/APNSEnvironment.swift diff --git a/Sources/WalletConnectPush/Client/Common/DeletePushSubscriptionService.swift b/Sources/WalletConnectPush/Client/Common/DeletePushSubscriptionService.swift deleted file mode 100644 index 2751fca46..000000000 --- a/Sources/WalletConnectPush/Client/Common/DeletePushSubscriptionService.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation - -class DeletePushSubscriptionService { - enum Errors: Error { - case pushSubscriptionNotFound - } - private let networkingInteractor: NetworkInteracting - private let kms: KeyManagementServiceProtocol - private let logger: ConsoleLogging - private let pushStorage: PushStorage - - init(networkingInteractor: NetworkInteracting, - kms: KeyManagementServiceProtocol, - logger: ConsoleLogging, - pushStorage: PushStorage) { - self.networkingInteractor = networkingInteractor - self.kms = kms - self.logger = logger - self.pushStorage = pushStorage - } - - func delete(topic: String) async throws { - let params = PushDeleteParams.userDisconnected - logger.debug("Will delete push subscription for reason: message: \(params.message) code: \(params.code), topic: \(topic)") - guard let _ = pushStorage.getSubscription(topic: topic) - else { throw Errors.pushSubscriptionNotFound} - let protocolMethod = PushDeleteProtocolMethod() - try await pushStorage.deleteSubscription(topic: topic) - pushStorage.deleteMessages(topic: topic) - let request = RPCRequest(method: protocolMethod.method, params: params) - try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) - - networkingInteractor.unsubscribe(topic: topic) - logger.debug("Subscription removed, topic: \(topic)") - - kms.deleteSymmetricKey(for: topic) - } -} diff --git a/Sources/WalletConnectPush/Client/Dapp/DappPushClient.swift b/Sources/WalletConnectPush/Client/Dapp/DappPushClient.swift deleted file mode 100644 index 9c7d820ea..000000000 --- a/Sources/WalletConnectPush/Client/Dapp/DappPushClient.swift +++ /dev/null @@ -1,43 +0,0 @@ -import Foundation -import Combine - -public class DappPushClient { - public var proposalResponsePublisher: AnyPublisher, Never> { - return notifyProposeResponseSubscriber.proposalResponsePublisher - } - - public var deleteSubscriptionPublisher: AnyPublisher { - return pushStorage.deleteSubscriptionPublisher - } - - public let logger: ConsoleLogging - - private let notifyProposer: NotifyProposer - private let pushStorage: PushStorage - private let deletePushSubscriptionSubscriber: DeletePushSubscriptionSubscriber - private let resubscribeService: PushResubscribeService - private let notifyProposeResponseSubscriber: NotifyProposeResponseSubscriber - - init(logger: ConsoleLogging, - kms: KeyManagementServiceProtocol, - pushStorage: PushStorage, - deletePushSubscriptionSubscriber: DeletePushSubscriptionSubscriber, - resubscribeService: PushResubscribeService, - notifyProposer: NotifyProposer, - notifyProposeResponseSubscriber: NotifyProposeResponseSubscriber) { - self.logger = logger - self.pushStorage = pushStorage - self.deletePushSubscriptionSubscriber = deletePushSubscriptionSubscriber - self.resubscribeService = resubscribeService - self.notifyProposer = notifyProposer - self.notifyProposeResponseSubscriber = notifyProposeResponseSubscriber - } - - public func propose(account: Account, topic: String) async throws { - try await notifyProposer.propose(topic: topic, account: account) - } - - public func getActiveSubscriptions() -> [PushSubscription] { - pushStorage.getSubscriptions() - } -} diff --git a/Sources/WalletConnectPush/Client/Dapp/DappPushClientFactory.swift b/Sources/WalletConnectPush/Client/Dapp/DappPushClientFactory.swift deleted file mode 100644 index 610edff97..000000000 --- a/Sources/WalletConnectPush/Client/Dapp/DappPushClientFactory.swift +++ /dev/null @@ -1,41 +0,0 @@ -import Foundation - -public struct DappPushClientFactory { - - public static func create(metadata: AppMetadata, networkInteractor: NetworkInteracting, syncClient: SyncClient) -> DappPushClient { - let logger = ConsoleLogger(loggingLevel: .off) - let keyValueStorage = UserDefaults.standard - let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") - let groupKeychainStorage = GroupKeychainStorage(serviceIdentifier: "group.com.walletconnect.sdk") - return DappPushClientFactory.create( - metadata: metadata, - logger: logger, - keyValueStorage: keyValueStorage, - keychainStorage: keychainStorage, - groupKeychainStorage: groupKeychainStorage, - networkInteractor: networkInteractor, - syncClient: syncClient - ) - } - - static func create(metadata: AppMetadata, logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, groupKeychainStorage: KeychainStorageProtocol, networkInteractor: NetworkInteracting, syncClient: SyncClient) -> DappPushClient { - let kms = KeyManagementService(keychain: keychainStorage) - let subscriptionStore: SyncStore = SyncStoreFactory.create(name: PushStorageIdntifiers.pushSubscription, syncClient: syncClient, storage: keyValueStorage) - let messagesStore = KeyedDatabase(storage: keyValueStorage, identifier: PushStorageIdntifiers.pushMessagesRecords) - let subscriptionStoreDelegate = PushSubscriptionStoreDelegate(networkingInteractor: networkInteractor, kms: kms, groupKeychainStorage: groupKeychainStorage) - let pushStorage = PushStorage(subscriptionStore: subscriptionStore, messagesStore: messagesStore, subscriptionStoreDelegate: subscriptionStoreDelegate) - let deletePushSubscriptionSubscriber = DeletePushSubscriptionSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, pushStorage: pushStorage) - let resubscribeService = PushResubscribeService(networkInteractor: networkInteractor, pushStorage: pushStorage) - let notifyProposer = NotifyProposer(networkingInteractor: networkInteractor, kms: kms, appMetadata: metadata, logger: logger) - let notifyProposeResponseSubscriber = NotifyProposeResponseSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, metadata: metadata) - return DappPushClient( - logger: logger, - kms: kms, - pushStorage: pushStorage, - deletePushSubscriptionSubscriber: deletePushSubscriptionSubscriber, - resubscribeService: resubscribeService, - notifyProposer: notifyProposer, - notifyProposeResponseSubscriber: notifyProposeResponseSubscriber - ) - } -} diff --git a/Sources/WalletConnectPush/Client/Dapp/wc_notifyPropose/NotifyProposeResponseSubscriber.swift b/Sources/WalletConnectPush/Client/Dapp/wc_notifyPropose/NotifyProposeResponseSubscriber.swift deleted file mode 100644 index ee294a384..000000000 --- a/Sources/WalletConnectPush/Client/Dapp/wc_notifyPropose/NotifyProposeResponseSubscriber.swift +++ /dev/null @@ -1,71 +0,0 @@ - -import Foundation -import Combine - -class NotifyProposeResponseSubscriber { - private let networkingInteractor: NetworkInteracting - private let metadata: AppMetadata - private let kms: KeyManagementServiceProtocol - private let logger: ConsoleLogging - var proposalResponsePublisher: AnyPublisher, Never> { - proposalResponsePublisherSubject.eraseToAnyPublisher() - } - private let proposalResponsePublisherSubject = PassthroughSubject, Never>() - - private var publishers = [AnyCancellable]() - - init(networkingInteractor: NetworkInteracting, - kms: KeyManagementServiceProtocol, - logger: ConsoleLogging, - metadata: AppMetadata) { - self.networkingInteractor = networkingInteractor - self.kms = kms - self.logger = logger - self.metadata = metadata - subscribeForProposalResponse() - subscribeForProposalErrors() - } - - - private func subscribeForProposalResponse() { - let protocolMethod = NotifyProposeProtocolMethod() - networkingInteractor.responseSubscription(on: protocolMethod) - .sink { [unowned self] (payload: ResponseSubscriptionPayload) in - logger.debug("Received Notify Proposal response") - Task(priority: .userInitiated) { - do { - let pushSubscription = try await handleResponse(payload: payload) - proposalResponsePublisherSubject.send(.success(pushSubscription)) - } catch { - logger.error(error) - } - } - }.store(in: &publishers) - } - - func handleResponse(payload: ResponseSubscriptionPayload) async throws -> PushSubscription { - let jwtWrapper = SubscriptionJWTPayload.Wrapper(jwtString: payload.response.subscriptionAuth) - let (_, claims) = try SubscriptionJWTPayload.decodeAndVerify(from: jwtWrapper) - logger.debug("subscriptionAuth JWT validated") - let expiry = Date(timeIntervalSince1970: TimeInterval(claims.exp)) - let subscriptionKey = try SymmetricKey(hex: payload.response.subscriptionSymKey) - let subscriptionTopic = subscriptionKey.rawRepresentation.sha256().toHexString() - let relay = RelayProtocolOptions(protocol: "irn", data: nil) - let subscription = PushSubscription(topic: subscriptionTopic, account: payload.request.account, relay: relay, metadata: metadata, scope: [:], expiry: expiry, symKey: subscriptionKey.hexRepresentation) - try kms.setSymmetricKey(subscriptionKey, for: subscriptionTopic) - try await networkingInteractor.subscribe(topic: subscriptionTopic) - return subscription - } - - private func subscribeForProposalErrors() { - let protocolMethod = NotifyProposeProtocolMethod() - networkingInteractor.responseErrorSubscription(on: protocolMethod) - .sink { [unowned self] (payload: ResponseSubscriptionErrorPayload) in - kms.deletePrivateKey(for: payload.request.publicKey) - networkingInteractor.unsubscribe(topic: payload.topic) - guard let error = PushError(code: payload.error.code) else { return } - proposalResponsePublisherSubject.send(.failure(error)) - }.store(in: &publishers) - } - -} diff --git a/Sources/WalletConnectPush/Client/Dapp/wc_notifyPropose/NotifyProposer.swift b/Sources/WalletConnectPush/Client/Dapp/wc_notifyPropose/NotifyProposer.swift deleted file mode 100644 index 02c3720f2..000000000 --- a/Sources/WalletConnectPush/Client/Dapp/wc_notifyPropose/NotifyProposer.swift +++ /dev/null @@ -1,33 +0,0 @@ - -import Foundation - -class NotifyProposer { - private let networkingInteractor: NetworkInteracting - private let kms: KeyManagementService - private let logger: ConsoleLogging - private let metadata: AppMetadata - - init(networkingInteractor: NetworkInteracting, - kms: KeyManagementService, - appMetadata: AppMetadata, - logger: ConsoleLogging) { - self.networkingInteractor = networkingInteractor - self.kms = kms - self.metadata = appMetadata - self.logger = logger - } - - func propose(topic: String, account: Account) async throws { - logger.debug("NotifyProposer: Sending Notify Proposal") - let protocolMethod = NotifyProposeProtocolMethod() - let publicKey = try kms.createX25519KeyPair() - let responseTopic = publicKey.rawRepresentation.sha256().toHexString() - try kms.setPublicKey(publicKey: publicKey, for: responseTopic) - - let params = NotifyProposeParams(publicKey: publicKey.hexRepresentation, metadata: metadata, account: account, scope: []) - let request = RPCRequest(method: protocolMethod.method, params: params) - try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) - try await networkingInteractor.subscribe(topic: responseTopic) - } - -} diff --git a/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_notifyPropose/NotifyProposeResponder.swift b/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_notifyPropose/NotifyProposeResponder.swift deleted file mode 100644 index 01b4fbcf2..000000000 --- a/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_notifyPropose/NotifyProposeResponder.swift +++ /dev/null @@ -1,93 +0,0 @@ - -import Foundation -import Combine - -class NotifyProposeResponder { - enum Errors: Error { - case recordForIdNotFound - case malformedRequestParams - case subscriptionNotFound - } - private let networkingInteractor: NetworkInteracting - private let kms: KeyManagementServiceProtocol - private let logger: ConsoleLogging - private let pushStorage: PushStorage - private let pushSubscribeRequester: PushSubscribeRequester - private let rpcHistory: RPCHistory - - private var publishers = [AnyCancellable]() - - init(networkingInteractor: NetworkInteracting, - kms: KeyManagementServiceProtocol, - logger: ConsoleLogging, - pushStorage: PushStorage, - pushSubscribeRequester: PushSubscribeRequester, - rpcHistory: RPCHistory, - pushSubscribeResponseSubscriber: PushSubscribeResponseSubscriber - ) { - self.networkingInteractor = networkingInteractor - self.kms = kms - self.logger = logger - self.pushStorage = pushStorage - self.pushSubscribeRequester = pushSubscribeRequester - self.rpcHistory = rpcHistory - } - - func approve(requestId: RPCID, onSign: @escaping SigningCallback) async throws { - - logger.debug("NotifyProposeResponder: approving proposal") - - guard let requestRecord = rpcHistory.get(recordId: requestId) else { throw Errors.recordForIdNotFound } - let proposal = try requestRecord.request.params!.get(NotifyProposeParams.self) - - let subscriptionAuthWrapper = try await pushSubscribeRequester.subscribe(metadata: proposal.metadata, account: proposal.account, onSign: onSign) - - var pushSubscription: PushSubscription! - try await withCheckedThrowingContinuation { [unowned self] continuation in - pushStorage.newSubscriptionPublisher - .first() - .sink { value in - pushSubscription = value - continuation.resume() - }.store(in: &publishers) - } - - guard let peerPublicKey = try? AgreementPublicKey(hex: proposal.publicKey) else { - throw Errors.malformedRequestParams - } - - let responseTopic = peerPublicKey.rawRepresentation.sha256().toHexString() - - let keys = try generateAgreementKeys(peerPublicKey: peerPublicKey) - - try kms.setSymmetricKey(keys.sharedKey, for: responseTopic) - - guard let subscriptionKey = kms.getSymmetricKeyRepresentable(for: pushSubscription.topic)?.toHexString() else { throw Errors.subscriptionNotFound } - - let responseParams = NotifyProposeResponseParams(subscriptionAuth: subscriptionAuthWrapper.subscriptionAuth, subscriptionSymKey: subscriptionKey) - - let response = RPCResponse(id: requestId, result: responseParams) - - let protocolMethod = NotifyProposeProtocolMethod() - - logger.debug("NotifyProposeResponder: sending response") - - try await networkingInteractor.respond(topic: responseTopic, response: response, protocolMethod: protocolMethod, envelopeType: .type1(pubKey: keys.publicKey.rawRepresentation)) - kms.deleteSymmetricKey(for: responseTopic) - } - - func reject(requestId: RPCID) async throws { - logger.debug("NotifyProposeResponder - rejecting notify request") - guard let requestRecord = rpcHistory.get(recordId: requestId) else { throw Errors.recordForIdNotFound } - let pairingTopic = requestRecord.topic - - try await networkingInteractor.respondError(topic: pairingTopic, requestId: requestId, protocolMethod: NotifyProposeProtocolMethod(), reason: PushError.userRejeted) - } - - private func generateAgreementKeys(peerPublicKey: AgreementPublicKey) throws -> AgreementKeys { - let selfPubKey = try kms.createX25519KeyPair() - let keys = try kms.performKeyAgreement(selfPublicKey: selfPubKey, peerPublicKey: peerPublicKey.hexRepresentation) - return keys - } -} - diff --git a/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_notifyPropose/NotifyProposeSubscriber.swift b/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_notifyPropose/NotifyProposeSubscriber.swift deleted file mode 100644 index 3ea41d805..000000000 --- a/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_notifyPropose/NotifyProposeSubscriber.swift +++ /dev/null @@ -1,53 +0,0 @@ - -import Foundation -import Combine - -class NotifyProposeSubscriber { - - private let requestPublisherSubject = PassthroughSubject() - private let networkingInteractor: NetworkInteracting - private let pushStorage: PushStorage - private var publishers = Set() - public var requestPublisher: AnyPublisher { - requestPublisherSubject.eraseToAnyPublisher() - } - public let logger: ConsoleLogging - private let pairingRegisterer: PairingRegisterer - - init(networkingInteractor: NetworkInteracting, - pushStorage: PushStorage, - publishers: Set = Set(), - logger: ConsoleLogging, - pairingRegisterer: PairingRegisterer) { - self.networkingInteractor = networkingInteractor - self.pushStorage = pushStorage - self.publishers = publishers - self.logger = logger - self.pairingRegisterer = pairingRegisterer - setupSubscription() - } - - func setupSubscription() { - pairingRegisterer.register(method: NotifyProposeProtocolMethod()) - .sink { [unowned self] (payload: RequestSubscriptionPayload) in - logger.debug("NotifyProposeSubscriber - new notify propose request") - guard hasNoSubscription(for: payload.request.metadata.url) else { - Task(priority: .high) { try await respondError(requestId: payload.id, pairingTopic: payload.topic) } - return - } - requestPublisherSubject.send((id: payload.id, account: payload.request.account, metadata: payload.request.metadata)) - }.store(in: &publishers) - } - - func hasNoSubscription(for domain: String) -> Bool { - pushStorage.getSubscriptions().first { $0.metadata.url == domain } == nil - } - - func respondError(requestId: RPCID, pairingTopic: String) async throws { - logger.debug("NotifyProposeSubscriber - responding error for notify propose") - - let pairingTopic = pairingTopic - - try await networkingInteractor.respondError(topic: pairingTopic, requestId: requestId, protocolMethod: NotifyProposeProtocolMethod(), reason: PushError.userHasExistingSubscription) - } -} diff --git a/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateRequester.swift b/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateRequester.swift deleted file mode 100644 index aa1c4fffe..000000000 --- a/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateRequester.swift +++ /dev/null @@ -1,54 +0,0 @@ -import Foundation - -protocol NotifyUpdateRequesting { - func update(topic: String, scope: Set) async throws -} - -class NotifyUpdateRequester: NotifyUpdateRequesting { - enum Errors: Error { - case noSubscriptionForGivenTopic - } - - private let keyserverURL: URL - private let identityClient: IdentityClient - private let networkingInteractor: NetworkInteracting - private let logger: ConsoleLogging - private let pushStorage: PushStorage - - init(keyserverURL: URL, - identityClient: IdentityClient, - networkingInteractor: NetworkInteracting, - logger: ConsoleLogging, - pushStorage: PushStorage - ) { - self.keyserverURL = keyserverURL - self.identityClient = identityClient - self.networkingInteractor = networkingInteractor - self.logger = logger - self.pushStorage = pushStorage - } - - func update(topic: String, scope: Set) async throws { - logger.debug("NotifyUpdateRequester: updating subscription for topic: \(topic)") - - guard let subscription = pushStorage.getSubscription(topic: topic) else { throw Errors.noSubscriptionForGivenTopic } - - let request = try createJWTRequest(subscriptionAccount: subscription.account, dappUrl: subscription.metadata.url, scope: scope) - - let protocolMethod = NotifyUpdateProtocolMethod() - - try await networkingInteractor.request(request, topic: topic, protocolMethod: protocolMethod) - } - - private func createJWTRequest(subscriptionAccount: Account, dappUrl: String, scope: Set) throws -> RPCRequest { - let protocolMethod = NotifyUpdateProtocolMethod().method - let scopeClaim = scope.joined(separator: " ") - let jwtPayload = SubscriptionJWTPayload(keyserver: keyserverURL, subscriptionAccount: subscriptionAccount, dappUrl: dappUrl, scope: scopeClaim) - let wrapper = try identityClient.signAndCreateWrapper( - payload: jwtPayload, - account: subscriptionAccount - ) - print(wrapper.subscriptionAuth) - return RPCRequest(method: protocolMethod, params: wrapper) - } -} diff --git a/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateResponseSubscriber.swift b/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateResponseSubscriber.swift deleted file mode 100644 index e3b77d8ca..000000000 --- a/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_notifyUpdate/NotifyUpdateResponseSubscriber.swift +++ /dev/null @@ -1,71 +0,0 @@ - -import Foundation -import Combine - -class NotifyUpdateResponseSubscriber { - enum Errors: Error { - case subscriptionDoesNotExist - } - - private let networkingInteractor: NetworkInteracting - private var publishers = [AnyCancellable]() - private let logger: ConsoleLogging - private let pushStorage: PushStorage - private let subscriptionScopeProvider: SubscriptionScopeProvider - private var subscriptionPublisherSubject = PassthroughSubject, Never>() - var updateSubscriptionPublisher: AnyPublisher, Never> { - return subscriptionPublisherSubject.eraseToAnyPublisher() - } - - init(networkingInteractor: NetworkInteracting, - logger: ConsoleLogging, - subscriptionScopeProvider: SubscriptionScopeProvider, - pushStorage: PushStorage - ) { - self.networkingInteractor = networkingInteractor - self.logger = logger - self.pushStorage = pushStorage - self.subscriptionScopeProvider = subscriptionScopeProvider - subscribeForUpdateResponse() - } - - private func subscribeForUpdateResponse() { - let protocolMethod = NotifyUpdateProtocolMethod() - networkingInteractor.responseSubscription(on: protocolMethod) - .sink {[unowned self] (payload: ResponseSubscriptionPayload) in - Task(priority: .high) { - logger.debug("Received Push Update response") - - let subscriptionTopic = payload.topic - - let (_, claims) = try SubscriptionJWTPayload.decodeAndVerify(from: payload.request) - let scope = try await buildScope(selected: claims.scp, dappUrl: claims.aud) - - guard let oldSubscription = pushStorage.getSubscription(topic: subscriptionTopic) else { - logger.debug("NotifyUpdateResponseSubscriber Subscription does not exist") - subscriptionPublisherSubject.send(.failure(Errors.subscriptionDoesNotExist)) - return - } - let expiry = Date(timeIntervalSince1970: TimeInterval(claims.exp)) - - let updatedSubscription = PushSubscription(topic: subscriptionTopic, account: oldSubscription.account, relay: oldSubscription.relay, metadata: oldSubscription.metadata, scope: scope, expiry: expiry, symKey: oldSubscription.symKey) - - try await pushStorage.setSubscription(updatedSubscription) - - subscriptionPublisherSubject.send(.success(updatedSubscription)) - - logger.debug("Updated Subscription") - } - }.store(in: &publishers) - } - - private func buildScope(selected: String, dappUrl: String) async throws -> [String: ScopeValue] { - let selectedScope = selected - .components(separatedBy: " ") - - let availableScope = try await subscriptionScopeProvider.getSubscriptionScope(dappUrl: dappUrl) - return availableScope.reduce(into: [:]) { $0[$1.name] = ScopeValue(description: $1.description, enabled: selectedScope.contains($1.name)) } - } - - // TODO: handle error response -} diff --git a/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_pushMessage/PushMessageSubscriber.swift b/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_pushMessage/PushMessageSubscriber.swift deleted file mode 100644 index 897d86f65..000000000 --- a/Sources/WalletConnectPush/Client/Wallet/ProtocolEngine/wc_pushMessage/PushMessageSubscriber.swift +++ /dev/null @@ -1,35 +0,0 @@ -import Foundation -import Combine - -class PushMessageSubscriber { - private let networkingInteractor: NetworkInteracting - private let pushStorage: PushStorage - private let logger: ConsoleLogging - private var publishers = [AnyCancellable]() - private let pushMessagePublisherSubject = PassthroughSubject() - - public var pushMessagePublisher: AnyPublisher { - pushMessagePublisherSubject.eraseToAnyPublisher() - } - - init(networkingInteractor: NetworkInteracting, pushStorage: PushStorage, logger: ConsoleLogging) { - self.networkingInteractor = networkingInteractor - self.pushStorage = pushStorage - self.logger = logger - subscribeForPushMessages() - } - - private func subscribeForPushMessages() { - let protocolMethod = PushMessageProtocolMethod() - networkingInteractor.requestSubscription(on: protocolMethod) - .sink { [unowned self] (payload: RequestSubscriptionPayload) in - logger.debug("Received Push Message") - - let record = PushMessageRecord(id: payload.id.string, topic: payload.topic, message: payload.request, publishedAt: payload.publishedAt) - pushStorage.setMessage(record) - pushMessagePublisherSubject.send(record) - - }.store(in: &publishers) - - } -} diff --git a/Sources/WalletConnectPush/Client/Wallet/PushStorage.swift b/Sources/WalletConnectPush/Client/Wallet/PushStorage.swift deleted file mode 100644 index fa04a9297..000000000 --- a/Sources/WalletConnectPush/Client/Wallet/PushStorage.swift +++ /dev/null @@ -1,111 +0,0 @@ -import Foundation -import Combine - -protocol PushStoring { - func getSubscriptions() -> [PushSubscription] - func getSubscription(topic: String) -> PushSubscription? - func setSubscription(_ subscription: PushSubscription) async throws - func deleteSubscription(topic: String) async throws -} - -final class PushStorage: PushStoring { - - private var publishers = Set() - - private let subscriptionStore: SyncStore - private let messagesStore: KeyedDatabase - - private let newSubscriptionSubject = PassthroughSubject() - private let deleteSubscriptionSubject = PassthroughSubject() - - private let subscriptionStoreDelegate: PushSubscriptionStoreDelegate - - var newSubscriptionPublisher: AnyPublisher { - return newSubscriptionSubject.eraseToAnyPublisher() - } - - var deleteSubscriptionPublisher: AnyPublisher { - return deleteSubscriptionSubject.eraseToAnyPublisher() - } - - var subscriptionsPublisher: AnyPublisher<[PushSubscription], Never> { - return subscriptionStore.dataUpdatePublisher - } - - init( - subscriptionStore: SyncStore, - messagesStore: KeyedDatabase, - subscriptionStoreDelegate: PushSubscriptionStoreDelegate - ) { - self.subscriptionStore = subscriptionStore - self.messagesStore = messagesStore - self.subscriptionStoreDelegate = subscriptionStoreDelegate - setupSubscriptions() - } - - // MARK: Configuration - - func initialize(account: Account) async throws { - try await subscriptionStore.initialize(for: account) - } - - func setupSubscriptions(account: Account) async throws { - try subscriptionStore.setupSubscriptions(account: account) - } - - // MARK: Subscriptions - - func getSubscriptions() -> [PushSubscription] { - return subscriptionStore.getAll() - } - - func getSubscription(topic: String) -> PushSubscription? { - return subscriptionStore.get(for: topic) - } - - func setSubscription(_ subscription: PushSubscription) async throws { - try await subscriptionStore.set(object: subscription, for: subscription.account) - newSubscriptionSubject.send(subscription) - } - - func deleteSubscription(topic: String) async throws { - try await subscriptionStore.delete(id: topic) - deleteSubscriptionSubject.send(topic) - } - - // MARK: Messages - - func getMessages(topic: String) -> [PushMessageRecord] { - return messagesStore.getAll(for: topic) - .sorted{$0.publishedAt > $1.publishedAt} - } - - func deleteMessages(topic: String) { - messagesStore.deleteAll(for: topic) - } - - func deleteMessage(id: String) { - guard let result = messagesStore.find(id: id) else { return } - messagesStore.delete(id: id, for: result.key) - } - - func setMessage(_ record: PushMessageRecord) { - messagesStore.set(element: record, for: record.topic) - } -} - -private extension PushStorage { - - func setupSubscriptions() { - subscriptionStore.syncUpdatePublisher.sink { [unowned self] (_, _, update) in - switch update { - case .set(let subscription): - subscriptionStoreDelegate.onUpdate(subscription) - newSubscriptionSubject.send(subscription) - case .delete(let object): - subscriptionStoreDelegate.onDelete(object, pushStorage: self) - deleteSubscriptionSubject.send(object.topic) - } - }.store(in: &publishers) - } -} diff --git a/Sources/WalletConnectPush/Client/Wallet/PushSubscriptionStoreDelegate.swift b/Sources/WalletConnectPush/Client/Wallet/PushSubscriptionStoreDelegate.swift deleted file mode 100644 index 5831e2453..000000000 --- a/Sources/WalletConnectPush/Client/Wallet/PushSubscriptionStoreDelegate.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation - -final class PushSubscriptionStoreDelegate { - - private let networkingInteractor: NetworkInteracting - private let kms: KeyManagementServiceProtocol - private let groupKeychainStorage: KeychainStorageProtocol - - init(networkingInteractor: NetworkInteracting, kms: KeyManagementServiceProtocol, groupKeychainStorage: KeychainStorageProtocol) { - self.networkingInteractor = networkingInteractor - self.kms = kms - self.groupKeychainStorage = groupKeychainStorage - } - - func onUpdate(_ subscription: PushSubscription) { - Task(priority: .high) { - let symmetricKey = try SymmetricKey(hex: subscription.symKey) - try kms.setSymmetricKey(symmetricKey, for: subscription.topic) - try groupKeychainStorage.add(symmetricKey, forKey: subscription.topic) - try await networkingInteractor.subscribe(topic: subscription.topic) - } - } - - func onDelete(_ subscription: PushSubscription, pushStorage: PushStorage) { - Task(priority: .high) { - kms.deleteSymmetricKey(for: subscription.topic) - try? groupKeychainStorage.delete(key: subscription.topic) - networkingInteractor.unsubscribe(topic: subscription.topic) - pushStorage.deleteMessages(topic: subscription.topic) - } - } -} diff --git a/Sources/WalletConnectPush/Client/Wallet/PushSyncService.swift b/Sources/WalletConnectPush/Client/Wallet/PushSyncService.swift deleted file mode 100644 index 7b8ab9979..000000000 --- a/Sources/WalletConnectPush/Client/Wallet/PushSyncService.swift +++ /dev/null @@ -1,127 +0,0 @@ -import Foundation - -final class PushSyncService { - - private let syncClient: SyncClient - private let historyClient: HistoryClient - private let logger: ConsoleLogging - private let subscriptionsStore: SyncStore - private let messagesStore: KeyedDatabase - private let networkingInteractor: NetworkInteracting - private let kms: KeyManagementServiceProtocol - private let coldStartStore: CodableStore - private let groupKeychainStorage: KeychainStorageProtocol - - init( - syncClient: SyncClient, - logger: ConsoleLogging, - historyClient: HistoryClient, - subscriptionsStore: SyncStore, - messagesStore: KeyedDatabase, - networkingInteractor: NetworkInteracting, - kms: KeyManagementServiceProtocol, - coldStartStore: CodableStore, - groupKeychainStorage: KeychainStorageProtocol - ) { - self.syncClient = syncClient - self.logger = logger - self.historyClient = historyClient - self.subscriptionsStore = subscriptionsStore - self.messagesStore = messagesStore - self.networkingInteractor = networkingInteractor - self.kms = kms - self.coldStartStore = coldStartStore - self.groupKeychainStorage = groupKeychainStorage - } - - func registerSyncIfNeeded(account: Account, onSign: @escaping SigningCallback) async throws { - guard !syncClient.isRegistered(account: account) else { return } - - let result = await onSign(syncClient.getMessage(account: account)) - - switch result { - case .signed(let signature): - try await syncClient.register(account: account, signature: signature) - logger.debug("Sync pushSubscriptions store registered and initialized") - case .rejected: - throw PushError.registerSignatureRejected - } - } - - func fetchHistoryIfNeeded(account: Account) async throws { - guard try isColdStart(account: account) else { return } - - try await historyClient.register(tags: [ - "5000", // sync_set - "5002", // sync_delete - "4002" // push_message - ]) - - let syncTopic = try subscriptionsStore.getStoreTopic(account: account) - - let updates: [StoreSetDelete] = try await historyClient.getMessages( - topic: syncTopic, - count: 200, - direction: .backward - ) - - let inserts: [PushSubscription] = updates.compactMap { update in - guard let value = update.value else { return nil } - return try? JSONDecoder().decode(PushSubscription.self, from: Data(value.utf8)) - } - - let deletions: [String] = updates.compactMap { update in - guard update.value == nil else { return nil } - return update.key - } - - let subscriptions = inserts.filter { !deletions.contains( $0.databaseId ) } - - try subscriptionsStore.setInStore(objects: subscriptions, for: account) - - for subscription in subscriptions { - let symmetricKey = try SymmetricKey(hex: subscription.symKey) - try kms.setSymmetricKey(symmetricKey, for: subscription.topic) - try groupKeychainStorage.add(symmetricKey, forKey: subscription.topic) - try await networkingInteractor.subscribe(topic: subscription.topic) - - let historyRecords: [HistoryRecord] = try await historyClient.getRecords( - topic: subscription.topic, - count: 200, - direction: .backward - ) - - let messageRecords = historyRecords.map { record in - return PushMessageRecord( - id: record.id.string, - topic: subscription.topic, - message: record.object, - publishedAt: Date() - ) - } - - messagesStore.set(elements: messageRecords, for: subscription.topic) - } - - coldStartStore.set(Date(), forKey: account.absoluteString) - } -} - -private extension PushSyncService { - - struct StoreSetDelete: Codable, Equatable { - let key: String - let value: String? - } - - func isColdStart(account: Account) throws -> Bool { - guard let lastFetch = try coldStartStore.get(key: account.absoluteString) else { - return true - } - guard let days = Calendar.current.dateComponents([.day], from: lastFetch, to: Date()).day else { - return true - } - - return days >= 30 - } -} diff --git a/Sources/WalletConnectPush/Client/Wallet/WalletPushClient.swift b/Sources/WalletConnectPush/Client/Wallet/WalletPushClient.swift deleted file mode 100644 index cb90ef867..000000000 --- a/Sources/WalletConnectPush/Client/Wallet/WalletPushClient.swift +++ /dev/null @@ -1,141 +0,0 @@ -import Foundation -import Combine - -public class WalletPushClient { - - private var publishers = Set() - - /// publishes new subscriptions - public var newSubscriptionPublisher: AnyPublisher { - return pushStorage.newSubscriptionPublisher - } - - public var subscriptionErrorPublisher: AnyPublisher { - return pushSubscribeResponseSubscriber.subscriptionErrorPublisher - } - - public var deleteSubscriptionPublisher: AnyPublisher { - return pushStorage.deleteSubscriptionPublisher - } - - public var subscriptionsPublisher: AnyPublisher<[PushSubscription], Never> { - return pushStorage.subscriptionsPublisher - } - - public var requestPublisher: AnyPublisher { - notifyProposeSubscriber.requestPublisher - } - - public var pushMessagePublisher: AnyPublisher { - pushMessageSubscriber.pushMessagePublisher - } - - public var updateSubscriptionPublisher: AnyPublisher, Never> { - return notifyUpdateResponseSubscriber.updateSubscriptionPublisher - } - - private let deletePushSubscriptionService: DeletePushSubscriptionService - private let pushSubscribeRequester: PushSubscribeRequester - - public let logger: ConsoleLogging - - private let echoClient: EchoClient - private let pushStorage: PushStorage - private let pushSyncService: PushSyncService - private let pushMessageSubscriber: PushMessageSubscriber - private let resubscribeService: PushResubscribeService - private let pushSubscribeResponseSubscriber: PushSubscribeResponseSubscriber - private let deletePushSubscriptionSubscriber: DeletePushSubscriptionSubscriber - private let notifyUpdateRequester: NotifyUpdateRequester - private let notifyUpdateResponseSubscriber: NotifyUpdateResponseSubscriber - private let notifyProposeResponder: NotifyProposeResponder - private let notifyProposeSubscriber: NotifyProposeSubscriber - private let subscriptionsAutoUpdater: SubscriptionsAutoUpdater - - init(logger: ConsoleLogging, - kms: KeyManagementServiceProtocol, - echoClient: EchoClient, - pushMessageSubscriber: PushMessageSubscriber, - pushStorage: PushStorage, - pushSyncService: PushSyncService, - deletePushSubscriptionService: DeletePushSubscriptionService, - resubscribeService: PushResubscribeService, - pushSubscribeRequester: PushSubscribeRequester, - pushSubscribeResponseSubscriber: PushSubscribeResponseSubscriber, - deletePushSubscriptionSubscriber: DeletePushSubscriptionSubscriber, - notifyUpdateRequester: NotifyUpdateRequester, - notifyUpdateResponseSubscriber: NotifyUpdateResponseSubscriber, - notifyProposeResponder: NotifyProposeResponder, - notifyProposeSubscriber: NotifyProposeSubscriber, - subscriptionsAutoUpdater: SubscriptionsAutoUpdater - ) { - self.logger = logger - self.echoClient = echoClient - self.pushMessageSubscriber = pushMessageSubscriber - self.pushStorage = pushStorage - self.pushSyncService = pushSyncService - self.deletePushSubscriptionService = deletePushSubscriptionService - self.resubscribeService = resubscribeService - self.pushSubscribeRequester = pushSubscribeRequester - self.pushSubscribeResponseSubscriber = pushSubscribeResponseSubscriber - self.deletePushSubscriptionSubscriber = deletePushSubscriptionSubscriber - self.notifyUpdateRequester = notifyUpdateRequester - self.notifyUpdateResponseSubscriber = notifyUpdateResponseSubscriber - self.notifyProposeResponder = notifyProposeResponder - self.notifyProposeSubscriber = notifyProposeSubscriber - self.subscriptionsAutoUpdater = subscriptionsAutoUpdater - } - - // TODO: Add docs - public func enableSync(account: Account, onSign: @escaping SigningCallback) async throws { - try await pushSyncService.registerSyncIfNeeded(account: account, onSign: onSign) - try await pushStorage.initialize(account: account) - try await pushStorage.setupSubscriptions(account: account) - try await pushSyncService.fetchHistoryIfNeeded(account: account) - } - - public func subscribe(metadata: AppMetadata, account: Account, onSign: @escaping SigningCallback) async throws { - try await pushSubscribeRequester.subscribe(metadata: metadata, account: account, onSign: onSign) - } - - public func approve(id: RPCID, onSign: @escaping SigningCallback) async throws { - try await notifyProposeResponder.approve(requestId: id, onSign: onSign) - } - - public func reject(id: RPCID) async throws { - try await notifyProposeResponder.reject(requestId: id) - } - - public func update(topic: String, scope: Set) async throws { - try await notifyUpdateRequester.update(topic: topic, scope: scope) - } - - public func getActiveSubscriptions() -> [PushSubscription] { - return pushStorage.getSubscriptions() - } - - public func getMessageHistory(topic: String) -> [PushMessageRecord] { - pushStorage.getMessages(topic: topic) - } - - public func deleteSubscription(topic: String) async throws { - try await deletePushSubscriptionService.delete(topic: topic) - } - - public func deletePushMessage(id: String) { - pushStorage.deleteMessage(id: id) - } - - public func register(deviceToken: Data) async throws { - try await echoClient.register(deviceToken: deviceToken) - } -} - -#if targetEnvironment(simulator) -extension WalletPushClient { - public func register(deviceToken: String) async throws { - try await echoClient.register(deviceToken: deviceToken) - } -} -#endif - diff --git a/Sources/WalletConnectPush/Client/Wallet/WalletPushClientFactory.swift b/Sources/WalletConnectPush/Client/Wallet/WalletPushClientFactory.swift deleted file mode 100644 index d95d7cb6d..000000000 --- a/Sources/WalletConnectPush/Client/Wallet/WalletPushClientFactory.swift +++ /dev/null @@ -1,88 +0,0 @@ -import Foundation - -public struct WalletPushClientFactory { - - public static func create(networkInteractor: NetworkInteracting, pairingRegisterer: PairingRegisterer, echoClient: EchoClient, syncClient: SyncClient, historyClient: HistoryClient) -> WalletPushClient { - let logger = ConsoleLogger(suffix: "🔔",loggingLevel: .debug) - let keyValueStorage = UserDefaults.standard - let keyserverURL = URL(string: "https://keys.walletconnect.com")! - let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") - let groupKeychainService = GroupKeychainStorage(serviceIdentifier: "group.com.walletconnect.sdk") - - return WalletPushClientFactory.create( - keyserverURL: keyserverURL, - logger: logger, - keyValueStorage: keyValueStorage, - keychainStorage: keychainStorage, - groupKeychainStorage: groupKeychainService, - networkInteractor: networkInteractor, - pairingRegisterer: pairingRegisterer, - echoClient: echoClient, - syncClient: syncClient, - historyClient: historyClient - ) - } - - static func create( - keyserverURL: URL, - logger: ConsoleLogging, - keyValueStorage: KeyValueStorage, - keychainStorage: KeychainStorageProtocol, - groupKeychainStorage: KeychainStorageProtocol, - networkInteractor: NetworkInteracting, - pairingRegisterer: PairingRegisterer, - echoClient: EchoClient, - syncClient: SyncClient, - historyClient: HistoryClient - ) -> WalletPushClient { - let kms = KeyManagementService(keychain: keychainStorage) - let history = RPCHistoryFactory.createForNetwork(keyValueStorage: keyValueStorage) - let subscriptionStore: SyncStore = SyncStoreFactory.create(name: PushStorageIdntifiers.pushSubscription, syncClient: syncClient, storage: keyValueStorage) - let subscriptionStoreDelegate = PushSubscriptionStoreDelegate(networkingInteractor: networkInteractor, kms: kms, groupKeychainStorage: groupKeychainStorage) - let messagesStore = KeyedDatabase(storage: keyValueStorage, identifier: PushStorageIdntifiers.pushMessagesRecords) - let pushStorage = PushStorage(subscriptionStore: subscriptionStore, messagesStore: messagesStore, subscriptionStoreDelegate: subscriptionStoreDelegate) - let coldStartStore = CodableStore(defaults: keyValueStorage, identifier: PushStorageIdntifiers.coldStartStore) - let pushSyncService = PushSyncService(syncClient: syncClient, logger: logger, historyClient: historyClient, subscriptionsStore: subscriptionStore, messagesStore: messagesStore, networkingInteractor: networkInteractor, kms: kms, coldStartStore: coldStartStore, groupKeychainStorage: groupKeychainStorage) - let identityClient = IdentityClientFactory.create(keyserver: keyserverURL, keychain: keychainStorage, logger: logger) - let pushMessageSubscriber = PushMessageSubscriber(networkingInteractor: networkInteractor, pushStorage: pushStorage, logger: logger) - let deletePushSubscriptionService = DeletePushSubscriptionService(networkingInteractor: networkInteractor, kms: kms, logger: logger, pushStorage: pushStorage) - let resubscribeService = PushResubscribeService(networkInteractor: networkInteractor, pushStorage: pushStorage) - - let dappsMetadataStore = CodableStore(defaults: keyValueStorage, identifier: PushStorageIdntifiers.dappsMetadataStore) - let subscriptionScopeProvider = SubscriptionScopeProvider() - - let pushSubscribeRequester = PushSubscribeRequester(keyserverURL: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, logger: logger, kms: kms, subscriptionScopeProvider: subscriptionScopeProvider, dappsMetadataStore: dappsMetadataStore) - - let pushSubscribeResponseSubscriber = PushSubscribeResponseSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, groupKeychainStorage: groupKeychainStorage, pushStorage: pushStorage, dappsMetadataStore: dappsMetadataStore, subscriptionScopeProvider: subscriptionScopeProvider) - - let notifyUpdateRequester = NotifyUpdateRequester(keyserverURL: keyserverURL, identityClient: identityClient, networkingInteractor: networkInteractor, logger: logger, pushStorage: pushStorage) - - let notifyUpdateResponseSubscriber = NotifyUpdateResponseSubscriber(networkingInteractor: networkInteractor, logger: logger, subscriptionScopeProvider: subscriptionScopeProvider, pushStorage: pushStorage) - let notifyProposeResponder = NotifyProposeResponder(networkingInteractor: networkInteractor, kms: kms, logger: logger, pushStorage: pushStorage, pushSubscribeRequester: pushSubscribeRequester, rpcHistory: history, pushSubscribeResponseSubscriber: pushSubscribeResponseSubscriber) - - let notifyProposeSubscriber = NotifyProposeSubscriber(networkingInteractor: networkInteractor, pushStorage: pushStorage, logger: logger, pairingRegisterer: pairingRegisterer) - - let deletePushSubscriptionSubscriber = DeletePushSubscriptionSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, pushStorage: pushStorage) - - let subscriptionsAutoUpdater = SubscriptionsAutoUpdater(notifyUpdateRequester: notifyUpdateRequester, logger: logger, pushStorage: pushStorage) - - return WalletPushClient( - logger: logger, - kms: kms, - echoClient: echoClient, - pushMessageSubscriber: pushMessageSubscriber, - pushStorage: pushStorage, - pushSyncService: pushSyncService, - deletePushSubscriptionService: deletePushSubscriptionService, - resubscribeService: resubscribeService, - pushSubscribeRequester: pushSubscribeRequester, - pushSubscribeResponseSubscriber: pushSubscribeResponseSubscriber, - deletePushSubscriptionSubscriber: deletePushSubscriptionSubscriber, - notifyUpdateRequester: notifyUpdateRequester, - notifyUpdateResponseSubscriber: notifyUpdateResponseSubscriber, - notifyProposeResponder: notifyProposeResponder, - notifyProposeSubscriber: notifyProposeSubscriber, - subscriptionsAutoUpdater: subscriptionsAutoUpdater - ) - } -} diff --git a/Sources/WalletConnectPush/Client/Wallet/WebDidResolver.swift b/Sources/WalletConnectPush/Client/Wallet/WebDidResolver.swift deleted file mode 100644 index f7e6617ad..000000000 --- a/Sources/WalletConnectPush/Client/Wallet/WebDidResolver.swift +++ /dev/null @@ -1,15 +0,0 @@ - -import Foundation - -class WebDidResolver { - - enum Errors: Error { - case invalidUrl - } - - func resolveDidDoc(domainUrl: String) async throws -> WebDidDoc { - guard let didDocUrl = URL(string: "\(domainUrl)/.well-known/did.json") else { throw Errors.invalidUrl } - let (data, _) = try await URLSession.shared.data(from: didDocUrl) - return try JSONDecoder().decode(WebDidDoc.self, from: data) - } -} diff --git a/Sources/WalletConnectPush/ProtocolMethods/NotifyProposeProtocolMethod.swift b/Sources/WalletConnectPush/ProtocolMethods/NotifyProposeProtocolMethod.swift deleted file mode 100644 index 027293d19..000000000 --- a/Sources/WalletConnectPush/ProtocolMethods/NotifyProposeProtocolMethod.swift +++ /dev/null @@ -1,12 +0,0 @@ -// - -import Foundation - -struct NotifyProposeProtocolMethod: ProtocolMethod { - let method: String = "wc_pushPropose" - - let requestConfig: RelayConfig = RelayConfig(tag: 4010, prompt: true, ttl: 86400) - - let responseConfig: RelayConfig = RelayConfig(tag: 4011, prompt: true, ttl: 86400) -} - diff --git a/Sources/WalletConnectPush/ProtocolMethods/PushSubscribeProtocolMethod.swift b/Sources/WalletConnectPush/ProtocolMethods/PushSubscribeProtocolMethod.swift deleted file mode 100644 index a101dc0e5..000000000 --- a/Sources/WalletConnectPush/ProtocolMethods/PushSubscribeProtocolMethod.swift +++ /dev/null @@ -1,10 +0,0 @@ - -import Foundation - -struct PushSubscribeProtocolMethod: ProtocolMethod { - let method: String = "wc_pushSubscribe" - - let requestConfig: RelayConfig = RelayConfig(tag: 4006, prompt: true, ttl: 86400) - - let responseConfig: RelayConfig = RelayConfig(tag: 4007, prompt: true, ttl: 86400) -} diff --git a/Sources/WalletConnectPush/Push.swift b/Sources/WalletConnectPush/Push.swift index 019b64aa6..fe955d08e 100644 --- a/Sources/WalletConnectPush/Push.swift +++ b/Sources/WalletConnectPush/Push.swift @@ -1,36 +1,28 @@ import Foundation public class Push { - - public static var dapp: DappPushClient = { - return DappPushClientFactory.create( - metadata: Pair.metadata, - networkInteractor: Networking.interactor, - syncClient: Sync.instance - ) - }() - - public static var wallet: WalletPushClient = { + static public let pushHost = "echo.walletconnect.com" + public static var instance: PushClient = { guard let config = Push.config else { - fatalError("Error - you must call Push.configure(_:) before accessing the shared wallet instance.") + fatalError("Error - you must call Push.configure(_:) before accessing the shared instance.") } - Echo.configure(echoHost: config.echoHost, environment: config.environment) - return WalletPushClientFactory.create( - networkInteractor: Networking.interactor, - pairingRegisterer: Pair.registerer, - echoClient: Echo.instance, - syncClient: Sync.instance, - historyClient: History.instance - ) + + return PushClientFactory.create( + projectId: Networking.projectId, + pushHost: config.pushHost, + environment: config.environment) }() private static var config: Config? private init() { } - /// Wallet's configuration method - static public func configure(echoHost: String = "echo.walletconnect.com", environment: APNSEnvironment) { - Push.config = Push.Config(echoHost: echoHost, environment: environment) + /// Push instance config method + /// - Parameter clientId: https://github.com/WalletConnect/walletconnect-docs/blob/main/docs/specs/clients/core/relay/relay-client-auth.md#overview + static public func configure( + pushHost: String = pushHost, + environment: APNSEnvironment + ) { + Push.config = Push.Config(pushHost: pushHost, environment: environment) } - } diff --git a/Sources/WalletConnectPush/PushClient.swift b/Sources/WalletConnectPush/PushClient.swift new file mode 100644 index 000000000..2eb23aacd --- /dev/null +++ b/Sources/WalletConnectPush/PushClient.swift @@ -0,0 +1,34 @@ +import Foundation + +public class PushClient: PushClientProtocol { + private let registerService: PushRegisterService + + init(registerService: PushRegisterService) { + self.registerService = registerService + } + + public func register(deviceToken: Data) async throws { + try await registerService.register(deviceToken: deviceToken) + } + +#if DEBUG + public func register(deviceToken: String) async throws { + try await registerService.register(deviceToken: deviceToken) + } +#endif +} + + +#if DEBUG +final class PushClientMock: PushClientProtocol { + var registedCalled = false + + func register(deviceToken: Data) async throws { + registedCalled = true + } + + func register(deviceToken: String) async throws { + registedCalled = true + } +} +#endif diff --git a/Sources/WalletConnectPush/PushClientFactory.swift b/Sources/WalletConnectPush/PushClientFactory.swift new file mode 100644 index 000000000..50a0145f9 --- /dev/null +++ b/Sources/WalletConnectPush/PushClientFactory.swift @@ -0,0 +1,39 @@ +import Foundation + +public struct PushClientFactory { + public static func create(projectId: String, + pushHost: String, + environment: APNSEnvironment) -> PushClient { + + let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") + + return PushClientFactory.create( + projectId: projectId, + pushHost: pushHost, + keychainStorage: keychainStorage, + environment: environment) + } + + public static func create( + projectId: String, + pushHost: String, + keychainStorage: KeychainStorageProtocol, + environment: APNSEnvironment + ) -> PushClient { + let sessionConfiguration = URLSessionConfiguration.default + sessionConfiguration.timeoutIntervalForRequest = 5.0 + sessionConfiguration.timeoutIntervalForResource = 5.0 + let session = URLSession(configuration: sessionConfiguration) + + let logger = ConsoleLogger(prefix: "👂🏻", loggingLevel: .off) + let httpClient = HTTPNetworkClient(host: pushHost, session: session) + + let clientIdStorage = ClientIdStorage(keychain: keychainStorage) + + let pushAuthenticator = PushAuthenticator(clientIdStorage: clientIdStorage, pushHost: pushHost) + + let registerService = PushRegisterService(httpClient: httpClient, projectId: projectId, clientIdStorage: clientIdStorage, pushAuthenticator: pushAuthenticator, logger: logger, environment: environment) + + return PushClient(registerService: registerService) + } +} diff --git a/Sources/WalletConnectEcho/EchoClientProtocol.swift b/Sources/WalletConnectPush/PushClientProtocol.swift similarity index 79% rename from Sources/WalletConnectEcho/EchoClientProtocol.swift rename to Sources/WalletConnectPush/PushClientProtocol.swift index c3b783df9..35eb8f3fa 100644 --- a/Sources/WalletConnectEcho/EchoClientProtocol.swift +++ b/Sources/WalletConnectPush/PushClientProtocol.swift @@ -1,6 +1,6 @@ import Foundation -public protocol EchoClientProtocol { +public protocol PushClientProtocol { func register(deviceToken: Data) async throws #if DEBUG func register(deviceToken: String) async throws diff --git a/Sources/WalletConnectPush/PushConfig.swift b/Sources/WalletConnectPush/PushConfig.swift index 9c955803a..e8d2eab40 100644 --- a/Sources/WalletConnectPush/PushConfig.swift +++ b/Sources/WalletConnectPush/PushConfig.swift @@ -2,7 +2,7 @@ import Foundation extension Push { struct Config { - let echoHost: String + let pushHost: String let environment: APNSEnvironment } } diff --git a/Sources/WalletConnectPush/PushImports.swift b/Sources/WalletConnectPush/PushImports.swift index 5d03d0533..463cb7c23 100644 --- a/Sources/WalletConnectPush/PushImports.swift +++ b/Sources/WalletConnectPush/PushImports.swift @@ -1,7 +1,4 @@ #if !CocoaPods -@_exported import WalletConnectPairing -@_exported import WalletConnectEcho -@_exported import WalletConnectIdentity -@_exported import WalletConnectSync -@_exported import WalletConnectHistory +@_exported import WalletConnectNetworking +@_exported import WalletConnectJWT #endif diff --git a/Sources/WalletConnectPush/PushStorageIdntifiers.swift b/Sources/WalletConnectPush/PushStorageIdntifiers.swift deleted file mode 100644 index 7be76bf7e..000000000 --- a/Sources/WalletConnectPush/PushStorageIdntifiers.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation - -enum PushStorageIdntifiers { - static let pushSubscription = "com.walletconnect.notify.pushSubscription" - - static let pushMessagesRecords = "com.walletconnect.sdk.pushMessagesRecords" - static let dappsMetadataStore = "com.walletconnect.sdk.dappsMetadataStore" - static let coldStartStore = "com.walletconnect.sdk.coldStartStore" -} diff --git a/Sources/WalletConnectPush/RPCRequests/PushDeleteParams.swift b/Sources/WalletConnectPush/RPCRequests/PushDeleteParams.swift deleted file mode 100644 index cc8829319..000000000 --- a/Sources/WalletConnectPush/RPCRequests/PushDeleteParams.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation - -public struct PushDeleteParams: Codable { - let code: Int - let message: String - - static var userDisconnected: PushDeleteParams { - return PushDeleteParams(code: 6000, message: "User Disconnected") - } -} diff --git a/Sources/WalletConnectPush/RPCRequests/SubscriptionJWTPayload.swift b/Sources/WalletConnectPush/RPCRequests/SubscriptionJWTPayload.swift deleted file mode 100644 index d45ac558b..000000000 --- a/Sources/WalletConnectPush/RPCRequests/SubscriptionJWTPayload.swift +++ /dev/null @@ -1,68 +0,0 @@ -import Foundation - -struct SubscriptionJWTPayload: JWTClaimsCodable { - - struct Claims: JWTClaims { - /// timestamp when jwt was issued - let iat: UInt64 - /// timestamp when jwt must expire - let exp: UInt64 - /// did:key of an identity key. Enables to resolve attached blockchain account. - let iss: String - /// key server for identity key verification - let ksu: String - /// dapp's url - let aud: String - /// blockchain account that push subscription has been proposed for (did:pkh) - let sub: String - /// description of action intent. Must be equal to "push_subscription" - let act: String - - let scp: String - } - - struct Wrapper: JWTWrapper { - let subscriptionAuth: String - - init(jwtString: String) { - self.subscriptionAuth = jwtString - } - - var jwtString: String { - return subscriptionAuth - } - } - - let keyserver: URL - let subscriptionAccount: Account - let dappUrl: String - let scope: String - - init(keyserver: URL, subscriptionAccount: Account, dappUrl: String, scope: String) { - self.keyserver = keyserver - self.subscriptionAccount = subscriptionAccount - self.dappUrl = dappUrl - self.scope = scope - } - - init(claims: Claims) throws { - self.keyserver = try claims.ksu.asURL() - self.subscriptionAccount = try Account(DIDPKHString: claims.sub) - self.dappUrl = claims.aud - self.scope = claims.scp - } - - func encode(iss: String) throws -> Claims { - return Claims( - iat: defaultIatMilliseconds(), - exp: expiry(days: 30), - iss: iss, - ksu: keyserver.absoluteString, - aud: dappUrl, - sub: subscriptionAccount.did, - act: "push_subscription", - scp: scope - ) - } -} - diff --git a/Sources/WalletConnectEcho/Register/EchoAuthPayload.swift b/Sources/WalletConnectPush/Register/PushAuthPayload.swift similarity index 80% rename from Sources/WalletConnectEcho/Register/EchoAuthPayload.swift rename to Sources/WalletConnectPush/Register/PushAuthPayload.swift index 12b0e26e9..ef4ecb8a3 100644 --- a/Sources/WalletConnectEcho/Register/EchoAuthPayload.swift +++ b/Sources/WalletConnectPush/Register/PushAuthPayload.swift @@ -1,6 +1,6 @@ import Foundation -struct EchoAuthPayload: JWTClaimsCodable { +struct PushAuthPayload: JWTClaimsCodable { struct Claims: JWTClaims { let iss: String @@ -8,6 +8,10 @@ struct EchoAuthPayload: JWTClaimsCodable { let aud: String let iat: UInt64 let exp: UInt64 + + let act: String? + + static var action: String? { nil } } struct Wrapper: JWTWrapper { @@ -33,7 +37,8 @@ struct EchoAuthPayload: JWTClaimsCodable { sub: subject, aud: audience, iat: defaultIat(), - exp: expiry(days: 1) + exp: expiry(days: 1), + act: Claims.action ) } } diff --git a/Sources/WalletConnectEcho/Register/EchoAuthenticator.swift b/Sources/WalletConnectPush/Register/PushAuthenticator.swift similarity index 63% rename from Sources/WalletConnectEcho/Register/EchoAuthenticator.swift rename to Sources/WalletConnectPush/Register/PushAuthenticator.swift index c47247fa4..0091c6bf0 100644 --- a/Sources/WalletConnectEcho/Register/EchoAuthenticator.swift +++ b/Sources/WalletConnectPush/Register/PushAuthenticator.swift @@ -1,26 +1,26 @@ import Foundation -protocol EchoAuthenticating { +protocol PushAuthenticating { func createAuthToken() throws -> String } -class EchoAuthenticator: EchoAuthenticating { +class PushAuthenticator: PushAuthenticating { private let clientIdStorage: ClientIdStoring - private let echoHost: String + private let pushHost: String - init(clientIdStorage: ClientIdStoring, echoHost: String) { + init(clientIdStorage: ClientIdStoring, pushHost: String) { self.clientIdStorage = clientIdStorage - self.echoHost = echoHost + self.pushHost = pushHost } func createAuthToken() throws -> String { let keyPair = try clientIdStorage.getOrCreateKeyPair() - let payload = EchoAuthPayload(subject: getSubject(), audience: getAudience()) + let payload = PushAuthPayload(subject: getSubject(), audience: getAudience()) return try payload.signAndCreateWrapper(keyPair: keyPair).jwtString } private func getAudience() -> String { - return "https://\(echoHost)" + return "https://\(pushHost)" } private func getSubject() -> String { diff --git a/Sources/WalletConnectPush/Register/PushRegisterService.swift b/Sources/WalletConnectPush/Register/PushRegisterService.swift new file mode 100644 index 000000000..2e142ae26 --- /dev/null +++ b/Sources/WalletConnectPush/Register/PushRegisterService.swift @@ -0,0 +1,89 @@ +import Foundation + +actor PushRegisterService { + private let httpClient: HTTPClient + private let projectId: String + private let logger: ConsoleLogging + private let environment: APNSEnvironment + private let pushAuthenticator: PushAuthenticating + private let clientIdStorage: ClientIdStoring + /// The property is used to determine whether echo.walletconnect.org will be used + /// in case echo.walletconnect.com doesn't respond for some reason (most likely due to being blocked in the user's location). + private var fallback = false + + enum Errors: Error { + case registrationFailed + } + + init(httpClient: HTTPClient, + projectId: String, + clientIdStorage: ClientIdStoring, + pushAuthenticator: PushAuthenticating, + logger: ConsoleLogging, + environment: APNSEnvironment) { + self.httpClient = httpClient + self.clientIdStorage = clientIdStorage + self.pushAuthenticator = pushAuthenticator + self.projectId = projectId + self.logger = logger + self.environment = environment + } + + func register(deviceToken: Data) async throws { + let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) } + let token = tokenParts.joined() + let pushAuthToken = try pushAuthenticator.createAuthToken() + let clientId = try clientIdStorage.getClientId() + let clientIdMutlibase = try DIDKey(did: clientId).multibase(variant: .ED25519) + logger.debug("APNS device token: \(token)") + + do { + let response = try await httpClient.request( + PushResponse.self, + at: PushAPI.register(clientId: clientIdMutlibase, token: token, projectId: projectId, environment: environment, auth: pushAuthToken) + ) + guard response.status == .success else { + throw Errors.registrationFailed + } + logger.debug("Successfully registered at Echo Server") + } catch { + if (error as? HTTPError) == .couldNotConnect && !fallback { + fallback = true + await echoHostFallback() + try await register(deviceToken: deviceToken) + } + throw error + } + } + + func echoHostFallback() async { + await httpClient.updateHost(host: "echo.walletconnect.org") + } + +#if DEBUG + public func register(deviceToken: String) async throws { + let pushAuthToken = try pushAuthenticator.createAuthToken() + let clientId = try clientIdStorage.getClientId() + let clientIdMutlibase = try DIDKey(did: clientId).multibase(variant: .ED25519) + + do { + let response = try await httpClient.request( + PushResponse.self, + at: PushAPI.register(clientId: clientIdMutlibase, token: deviceToken, projectId: projectId, environment: environment, auth: pushAuthToken) + ) + guard response.status == .success else { + throw Errors.registrationFailed + } + logger.debug("Successfully registered at Echo Server") + } catch { + if (error as? HTTPError) == .couldNotConnect && !fallback { + fallback = true + await echoHostFallback() + try await register(deviceToken: deviceToken) + } + throw error + } + } +#endif +} + diff --git a/Sources/WalletConnectEcho/Register/EchoResponse.swift b/Sources/WalletConnectPush/Register/PushResponse.swift similarity index 82% rename from Sources/WalletConnectEcho/Register/EchoResponse.swift rename to Sources/WalletConnectPush/Register/PushResponse.swift index f480c510c..3dd803c7b 100644 --- a/Sources/WalletConnectEcho/Register/EchoResponse.swift +++ b/Sources/WalletConnectPush/Register/PushResponse.swift @@ -1,6 +1,6 @@ import Foundation -struct EchoResponse: Codable { +struct PushResponse: Codable { enum Status: String, Codable { case success = "SUCCESS" case failed = "FAILED" diff --git a/Sources/WalletConnectEcho/Register/EchoService.swift b/Sources/WalletConnectPush/Register/PushService.swift similarity index 98% rename from Sources/WalletConnectEcho/Register/EchoService.swift rename to Sources/WalletConnectPush/Register/PushService.swift index 44c328101..d6ddbc6f3 100644 --- a/Sources/WalletConnectEcho/Register/EchoService.swift +++ b/Sources/WalletConnectPush/Register/PushService.swift @@ -1,6 +1,6 @@ import Foundation -enum EchoAPI: HTTPService { +enum PushAPI: HTTPService { case register(clientId: String, token: String, projectId: String, environment: APNSEnvironment, auth: String) case unregister(clientId: String, projectId: String, auth: String) diff --git a/Sources/WalletConnectPush/Types/PushRequest.swift b/Sources/WalletConnectPush/Types/PushRequest.swift deleted file mode 100644 index d1c4c8a92..000000000 --- a/Sources/WalletConnectPush/Types/PushRequest.swift +++ /dev/null @@ -1,3 +0,0 @@ -import Foundation - -public typealias PushRequest = (id: RPCID, account: Account, metadata: AppMetadata) diff --git a/Sources/WalletConnectPush/Types/PushSubscriptionResult.swift b/Sources/WalletConnectPush/Types/PushSubscriptionResult.swift deleted file mode 100644 index 3e7044917..000000000 --- a/Sources/WalletConnectPush/Types/PushSubscriptionResult.swift +++ /dev/null @@ -1,7 +0,0 @@ - -import Foundation - -public struct PushSubscriptionResult: Equatable, Codable { - public let pushSubscription: PushSubscription - public let subscriptionAuth: String -} diff --git a/Sources/WalletConnectRelay/ClientAuth/RelayAuthPayload.swift b/Sources/WalletConnectRelay/ClientAuth/RelayAuthPayload.swift index 737f515c0..3c44c4e5c 100644 --- a/Sources/WalletConnectRelay/ClientAuth/RelayAuthPayload.swift +++ b/Sources/WalletConnectRelay/ClientAuth/RelayAuthPayload.swift @@ -12,6 +12,10 @@ struct RelayAuthPayload: JWTClaimsCodable { let aud: String let iat: UInt64 let exp: UInt64 + + let act: String? + + static var action: String? { nil } } let subject: String @@ -33,7 +37,8 @@ struct RelayAuthPayload: JWTClaimsCodable { sub: subject, aud: audience, iat: defaultIat(), - exp: expiry(days: 1) + exp: expiry(days: 1), + act: Claims.action ) } } diff --git a/Sources/WalletConnectRelay/Dispatching.swift b/Sources/WalletConnectRelay/Dispatching.swift index 0b3bb8b56..bfca5d4cf 100644 --- a/Sources/WalletConnectRelay/Dispatching.swift +++ b/Sources/WalletConnectRelay/Dispatching.swift @@ -20,6 +20,9 @@ final class Dispatcher: NSObject, Dispatching { private let logger: ConsoleLogging private let defaultTimeout: Int = 5 + + /// The property is used to determine whether relay.walletconnect.org will be used + /// in case relay.walletconnect.com doesn't respond for some reason (most likely due to being blocked in the user's location). private var fallback = false private let socketConnectionStatusPublisherSubject = CurrentValueSubject(.disconnected) @@ -73,10 +76,7 @@ final class Dispatcher: NSObject, Dispatching { .filter { $0 == .connected } .setFailureType(to: NetworkError.self) .timeout(.seconds(defaultTimeout), scheduler: concurrentQueue, customError: { .webSocketNotConnected }) - .sink(receiveCompletion: { [weak self] result in - guard let self else { - return - } + .sink(receiveCompletion: { [unowned self] result in switch result { case .failure(let error): cancellable?.cancel() @@ -141,7 +141,9 @@ extension Dispatcher { logger.debug("[WebSocket] - Fallback to \(NetworkConstants.fallbackUrl)") fallback = true socket.request.url = relayUrlFactory.create(fallback: fallback) - socket.connect() + Task(priority: .high) { + await self.socketConnectionHandler.handleDisconnection() + } } } } diff --git a/Sources/WalletConnectRelay/NetworkMonitoring.swift b/Sources/WalletConnectRelay/NetworkMonitoring.swift index c4200171f..1d3932db5 100644 --- a/Sources/WalletConnectRelay/NetworkMonitoring.swift +++ b/Sources/WalletConnectRelay/NetworkMonitoring.swift @@ -1,27 +1,32 @@ import Foundation +import Combine import Network -protocol NetworkMonitoring: AnyObject { - var onSatisfied: (() -> Void)? {get set} - var onUnsatisfied: (() -> Void)? {get set} - func startMonitoring() +public enum NetworkConnectionStatus { + case connected + case notConnected } -class NetworkMonitor: NetworkMonitoring { - var onSatisfied: (() -> Void)? - var onUnsatisfied: (() -> Void)? - - private let monitor = NWPathMonitor() - private let monitorQueue = DispatchQueue(label: "com.walletconnect.sdk.network.monitor") +public protocol NetworkMonitoring: AnyObject { + var networkConnectionStatusPublisher: AnyPublisher { get } +} - func startMonitoring() { - monitor.pathUpdateHandler = { [weak self] path in - if path.status == .satisfied { - self?.onSatisfied?() - } else { - self?.onUnsatisfied?() - } +public final class NetworkMonitor: NetworkMonitoring { + private let networkMonitor = NWPathMonitor() + private let workerQueue = DispatchQueue(label: "com.walletconnect.sdk.network.monitor") + + private let networkConnectionStatusPublisherSubject = CurrentValueSubject(.connected) + + public var networkConnectionStatusPublisher: AnyPublisher { + networkConnectionStatusPublisherSubject + .share() + .eraseToAnyPublisher() + } + + public init() { + networkMonitor.pathUpdateHandler = { [weak self] path in + self?.networkConnectionStatusPublisherSubject.send((path.status == .satisfied) ? .connected : .notConnected) } - monitor.start(queue: monitorQueue) + networkMonitor.start(queue: workerQueue) } } diff --git a/Sources/WalletConnectRelay/PackageConfig.json b/Sources/WalletConnectRelay/PackageConfig.json index e0c9c2edf..b0a94210e 100644 --- a/Sources/WalletConnectRelay/PackageConfig.json +++ b/Sources/WalletConnectRelay/PackageConfig.json @@ -1 +1 @@ -{"version": "1.6.14"} +{"version": "1.7.1"} diff --git a/Sources/WalletConnectRelay/RelayClient.swift b/Sources/WalletConnectRelay/RelayClient.swift index ddb9cf91b..441f40314 100644 --- a/Sources/WalletConnectRelay/RelayClient.swift +++ b/Sources/WalletConnectRelay/RelayClient.swift @@ -42,6 +42,11 @@ public final class RelayClient { private let concurrentQueue = DispatchQueue(label: "com.walletconnect.sdk.relay_client", attributes: .concurrent) + public var logsPublisher: AnyPublisher { + logger.logsPublisher + .eraseToAnyPublisher() + } + // MARK: - Initialization init( @@ -63,6 +68,10 @@ public final class RelayClient { } } + public func setLogging(level: LoggingLevel) { + logger.setLogging(level: level) + } + /// Connects web socket /// /// Use this method for manual socket connection only @@ -82,12 +91,12 @@ public final class RelayClient { let request = Publish(params: .init(topic: topic, message: payload, ttl: ttl, prompt: prompt, tag: tag)) .asRPCRequest() let message = try request.asJSONEncodedString() - logger.debug("[RelayClient]: Publishing payload on topic: \(topic)") + logger.debug("Publishing payload on topic: \(topic)") try await dispatcher.protectedSend(message) } public func subscribe(topic: String) async throws { - logger.debug("[RelayClient]: Subscribing to topic: \(topic)") + logger.debug("Subscribing to topic: \(topic)") let rpc = Subscribe(params: .init(topic: topic)) let request = rpc .asRPCRequest() @@ -98,7 +107,7 @@ public final class RelayClient { public func batchSubscribe(topics: [String]) async throws { guard !topics.isEmpty else { return } - logger.debug("[RelayClient]: Subscribing to topics: \(topics)") + logger.debug("Subscribing to topics: \(topics)") let rpc = BatchSubscribe(params: .init(topics: topics)) let request = rpc .asRPCRequest() @@ -134,7 +143,7 @@ public final class RelayClient { completion(Errors.subscriptionIdNotFound) return } - logger.debug("[RelayClient]: Unsubscribing from topic: \(topic)") + logger.debug("Unsubscribing from topic: \(topic)") let rpc = Unsubscribe(params: .init(id: subscriptionId, topic: topic)) let request = rpc .asRPCRequest() @@ -142,7 +151,7 @@ public final class RelayClient { rpcHistory.deleteAll(forTopic: topic) dispatcher.protectedSend(message) { [weak self] error in if let error = error { - self?.logger.debug("[RelayClient]:Failed to unsubscribe from topic") + self?.logger.debug("Failed to unsubscribe from topic") completion(error) } else { self?.concurrentQueue.async(flags: .barrier) { @@ -161,9 +170,9 @@ public final class RelayClient { .sink { [unowned self] (_, subscriptionIds) in cancellable?.cancel() concurrentQueue.async(flags: .barrier) { [unowned self] in - logger.debug("[RelayClient]: Subscribed to topics: \(topics)") + logger.debug("Subscribed to topics: \(topics)") guard topics.count == subscriptionIds.count else { - logger.warn("RelayClient: Number of topics in (batch)subscribe does not match number of subscriptions") + logger.warn("Number of topics in (batch)subscribe does not match number of subscriptions") return } for i in 0.. Void)? @@ -20,6 +21,7 @@ final class ApproveEngine { private let sessionStore: WCSessionStorage private let verifyClient: VerifyClientProtocol private let proposalPayloadsStore: CodableStore> + private let verifyContextStore: CodableStore private let sessionTopicToProposal: CodableStore private let pairingRegisterer: PairingRegisterer private let metadata: AppMetadata @@ -31,6 +33,7 @@ final class ApproveEngine { init( networkingInteractor: NetworkInteracting, proposalPayloadsStore: CodableStore>, + verifyContextStore: CodableStore, sessionTopicToProposal: CodableStore, pairingRegisterer: PairingRegisterer, metadata: AppMetadata, @@ -42,6 +45,7 @@ final class ApproveEngine { ) { self.networkingInteractor = networkingInteractor self.proposalPayloadsStore = proposalPayloadsStore + self.verifyContextStore = verifyContextStore self.sessionTopicToProposal = sessionTopicToProposal self.pairingRegisterer = pairingRegisterer self.metadata = metadata @@ -60,11 +64,17 @@ final class ApproveEngine { guard let payload = try proposalPayloadsStore.get(key: proposerPubKey) else { throw Errors.wrongRequestParams } + + let networkConnectionStatus = await resolveNetworkConnectionStatus() + guard networkConnectionStatus == .connected else { + throw Errors.networkNotConnected + } let proposal = payload.request let pairingTopic = payload.topic proposalPayloadsStore.delete(forKey: proposerPubKey) + verifyContextStore.delete(forKey: proposerPubKey) try Namespace.validate(sessionNamespaces) try Namespace.validateApproved(sessionNamespaces, against: proposal.requiredNamespaces) @@ -113,6 +123,7 @@ final class ApproveEngine { throw Errors.proposalPayloadsNotFound } proposalPayloadsStore.delete(forKey: proposerPubKey) + verifyContextStore.delete(forKey: proposerPubKey) try await networkingInteractor.respondError(topic: payload.topic, requestId: payload.id, protocolMethod: SessionProposeProtocolMethod(), reason: reason) // TODO: Delete pairing if inactive } @@ -293,6 +304,13 @@ private extension ApproveEngine { } proposalPayloadsStore.set(payload, forKey: proposal.proposer.publicKey) + pairingRegisterer.setReceived(pairingTopic: payload.topic) + + if let verifyContext = try? verifyContextStore.get(key: proposal.proposer.publicKey) { + onSessionProposal?(proposal.publicRepresentation(pairingTopic: payload.topic), verifyContext) + return + } + Task(priority: .high) { let assertionId = payload.decryptedPayload.sha256().toHexString() do { @@ -301,6 +319,7 @@ private extension ApproveEngine { origin: origin, domain: payload.request.proposer.metadata.url ) + verifyContextStore.set(verifyContext, forKey: proposal.proposer.publicKey) onSessionProposal?(proposal.publicRepresentation(pairingTopic: payload.topic), verifyContext) } catch { let verifyContext = verifyClient.createVerifyContext(origin: nil, domain: payload.request.proposer.metadata.url) @@ -365,4 +384,28 @@ private extension ApproveEngine { } onSessionSettle?(session.publicRepresentation()) } + + func resolveNetworkConnectionStatus() async -> NetworkConnectionStatus { + return await withCheckedContinuation { continuation in + let cancellable = networkingInteractor.networkConnectionStatusPublisher.sink { value in + continuation.resume(returning: value) + } + + Task(priority: .high) { + await withTaskCancellationHandler { + cancellable.cancel() + } onCancel: { } + } + } + } +} + +// MARK: - LocalizedError +extension ApproveEngine.Errors: LocalizedError { + var errorDescription: String? { + switch self { + case .networkNotConnected: return "Action failed. You seem to be offline" + default: return "" + } + } } diff --git a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift index d8f265dab..b90a06fb5 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -17,6 +17,7 @@ final class SessionEngine { private let sessionStore: WCSessionStorage private let networkingInteractor: NetworkInteracting private let historyService: HistoryService + private let verifyContextStore: CodableStore private let verifyClient: VerifyClientProtocol private let kms: KeyManagementServiceProtocol private var publishers = [AnyCancellable]() @@ -25,6 +26,7 @@ final class SessionEngine { init( networkingInteractor: NetworkInteracting, historyService: HistoryService, + verifyContextStore: CodableStore, verifyClient: VerifyClientProtocol, kms: KeyManagementServiceProtocol, sessionStore: WCSessionStorage, @@ -32,6 +34,7 @@ final class SessionEngine { ) { self.networkingInteractor = networkingInteractor self.historyService = historyService + self.verifyContextStore = verifyContextStore self.verifyClient = verifyClient self.kms = kms self.sessionStore = sessionStore @@ -82,6 +85,7 @@ final class SessionEngine { protocolMethod: protocolMethod, reason: SignReasonCode.sessionRequestExpired ) + verifyContextStore.delete(forKey: requestId.string) throw Errors.sessionRequestExpired } @@ -90,6 +94,7 @@ final class SessionEngine { response: RPCResponse(id: requestId, outcome: response), protocolMethod: protocolMethod ) + verifyContextStore.delete(forKey: requestId.string) } func emit(topic: String, event: SessionType.EventParams.Event, chainId: Blockchain) async throws { @@ -185,7 +190,7 @@ private extension SessionEngine { } func sessionRequestNotExpired(requestId: RPCID) -> Bool { - guard let request = historyService.getSessionRequest(id: requestId) + guard let request = historyService.getSessionRequest(id: requestId)?.request else { return false } return !request.isExpired() @@ -243,9 +248,11 @@ private extension SessionEngine { do { let origin = try await verifyClient.verifyOrigin(assertionId: assertionId) let verifyContext = verifyClient.createVerifyContext(origin: origin, domain: session.peerParticipant.metadata.url) + verifyContextStore.set(verifyContext, forKey: request.id.string) onSessionRequest?(request, verifyContext) } catch { let verifyContext = verifyClient.createVerifyContext(origin: nil, domain: session.peerParticipant.metadata.url) + verifyContextStore.set(verifyContext, forKey: request.id.string) onSessionRequest?(request, verifyContext) return } diff --git a/Sources/WalletConnectSign/Services/HistoryService.swift b/Sources/WalletConnectSign/Services/HistoryService.swift index 394ff0c61..5a47a81d1 100644 --- a/Sources/WalletConnectSign/Services/HistoryService.swift +++ b/Sources/WalletConnectSign/Services/HistoryService.swift @@ -3,29 +3,66 @@ import Foundation final class HistoryService { private let history: RPCHistory + private let proposalPayloadsStore: CodableStore> + private let verifyContextStore: CodableStore - init(history: RPCHistory) { + init( + history: RPCHistory, + proposalPayloadsStore: CodableStore>, + verifyContextStore: CodableStore + ) { self.history = history + self.proposalPayloadsStore = proposalPayloadsStore + self.verifyContextStore = verifyContextStore } - func getPendingRequests() -> [Request] { - return history.getPending() + public func getSessionRequest(id: RPCID) -> (request: Request, context: VerifyContext?)? { + guard let record = history.get(recordId: id) else { return nil } + guard let request = mapRequestRecord(record) else { + return nil + } + return (request, try? verifyContextStore.get(key: request.id.string)) + } + + func getPendingRequests() -> [(request: Request, context: VerifyContext?)] { + let requests = history.getPending() .compactMap { mapRequestRecord($0) } .filter { !$0.isExpired() } + return requests.map { ($0, try? verifyContextStore.get(key: $0.id.string)) } } - func getPendingRequests(topic: String) -> [Request] { - return getPendingRequests().filter { $0.topic == topic } + func getPendingRequests(topic: String) -> [(request: Request, context: VerifyContext?)] { + return getPendingRequests().filter { $0.request.topic == topic } } - - public func getSessionRequest(id: RPCID) -> Request? { - guard let record = history.get(recordId: id) else { return nil } - return mapRequestRecord(record) + + func getPendingProposals() -> [(proposal: Session.Proposal, context: VerifyContext?)] { + let pendingHistory = history.getPending() + + let requestSubscriptionPayloads = pendingHistory + .compactMap { record -> RequestSubscriptionPayload? in + guard let proposalParams = mapProposeParams(record) else { + return nil + } + return RequestSubscriptionPayload(id: record.id, topic: record.topic, request: proposalParams, decryptedPayload: Data(), publishedAt: Date(), derivedTopic: nil) + } + + requestSubscriptionPayloads.forEach { + let proposal = $0.request + proposalPayloadsStore.set($0, forKey: proposal.proposer.publicKey) + } + + let proposals = pendingHistory + .compactMap { mapProposalRecord($0) } + + return proposals.map { ($0, try? verifyContextStore.get(key: $0.proposal.proposer.publicKey)) } + } + + func getPendingProposals(topic: String) -> [(proposal: Session.Proposal, context: VerifyContext?)] { + return getPendingProposals().filter { $0.proposal.pairingTopic == topic } } } private extension HistoryService { - func mapRequestRecord(_ record: RPCHistory.Record) -> Request? { guard let request = try? record.request.params?.get(SessionType.RequestParams.self) else { return nil } @@ -39,4 +76,25 @@ private extension HistoryService { expiry: request.request.expiry ) } + + func mapProposeParams(_ record: RPCHistory.Record) -> SessionType.ProposeParams? { + guard let proposal = try? record.request.params?.get(SessionType.ProposeParams.self) + else { return nil } + return proposal + } + + func mapProposalRecord(_ record: RPCHistory.Record) -> Session.Proposal? { + guard let proposal = try? record.request.params?.get(SessionType.ProposeParams.self) + else { return nil } + + return Session.Proposal( + id: proposal.proposer.publicKey, + pairingTopic: record.topic, + proposer: proposal.proposer.metadata, + requiredNamespaces: proposal.requiredNamespaces, + optionalNamespaces: proposal.optionalNamespaces, + sessionProperties: proposal.sessionProperties, + proposal: proposal + ) + } } diff --git a/Sources/WalletConnectSign/Session.swift b/Sources/WalletConnectSign/Session.swift index 0d6ca96a8..7e0d24fb4 100644 --- a/Sources/WalletConnectSign/Session.swift +++ b/Sources/WalletConnectSign/Session.swift @@ -3,7 +3,7 @@ import Foundation /** A representation of an active session connection. */ -public struct Session { +public struct Session: Codable { public let topic: String public let pairingTopic: String public let peer: AppMetadata @@ -28,6 +28,24 @@ extension Session { // TODO: Refactor internal objects to manage only needed data internal let proposal: SessionProposal + + init( + id: String, + pairingTopic: String, + proposer: AppMetadata, + requiredNamespaces: [String: ProposalNamespace], + optionalNamespaces: [String: ProposalNamespace]?, + sessionProperties: [String: String]?, + proposal: SessionProposal + ) { + self.id = id + self.pairingTopic = pairingTopic + self.proposer = proposer + self.requiredNamespaces = requiredNamespaces + self.optionalNamespaces = optionalNamespaces + self.sessionProperties = sessionProperties + self.proposal = proposal + } } public struct Event: Equatable, Hashable { diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index 11830442c..57dd897a2 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -338,17 +338,27 @@ public final class SignClient: SignClientProtocol { /// Query pending requests /// - 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] { + public func getPendingRequests(topic: String? = nil) -> [(request: Request, context: VerifyContext?)] { if let topic = topic { return historyService.getPendingRequests(topic: topic) } else { return historyService.getPendingRequests() } } + + /// Query pending proposals + /// - Returns: Pending proposals received from peer with `wc_sessionPropose` protocol method + public func getPendingProposals(topic: String? = nil) -> [(proposal: Session.Proposal, context: VerifyContext?)] { + if let topic = topic { + return historyService.getPendingProposals(topic: topic) + } else { + return historyService.getPendingProposals() + } + } /// - 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? { + public func getSessionRequestRecord(id: RPCID) -> (request: Request, context: VerifyContext?)? { return historyService.getSessionRequest(id: id) } diff --git a/Sources/WalletConnectSign/Sign/SignClientFactory.swift b/Sources/WalletConnectSign/Sign/SignClientFactory.swift index 53a7d56af..a7903a01b 100644 --- a/Sources/WalletConnectSign/Sign/SignClientFactory.swift +++ b/Sources/WalletConnectSign/Sign/SignClientFactory.swift @@ -24,13 +24,26 @@ public struct SignClientFactory { let pairingStore = PairingStorage(storage: SequenceStore(store: .init(defaults: keyValueStorage, identifier: SignStorageIdentifiers.pairings.rawValue))) let sessionStore = SessionStorage(storage: SequenceStore(store: .init(defaults: keyValueStorage, identifier: SignStorageIdentifiers.sessions.rawValue))) let proposalPayloadsStore = CodableStore>(defaults: RuntimeKeyValueStorage(), identifier: SignStorageIdentifiers.proposals.rawValue) - let historyService = HistoryService(history: rpcHistory) + let verifyContextStore = CodableStore(defaults: keyValueStorage, identifier: VerifyStorageIdentifiers.context.rawValue) + let historyService = HistoryService(history: rpcHistory, proposalPayloadsStore: proposalPayloadsStore, verifyContextStore: verifyContextStore) let verifyClient = VerifyClientFactory.create() - let sessionEngine = SessionEngine(networkingInteractor: networkingClient, historyService: historyService, verifyClient: verifyClient, kms: kms, sessionStore: sessionStore, logger: logger) + let sessionEngine = SessionEngine(networkingInteractor: networkingClient, historyService: historyService, verifyContextStore: verifyContextStore, verifyClient: verifyClient, 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 sessionTopicToProposal = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: SignStorageIdentifiers.sessionTopicToProposal.rawValue) - let approveEngine = ApproveEngine(networkingInteractor: networkingClient, proposalPayloadsStore: proposalPayloadsStore, sessionTopicToProposal: sessionTopicToProposal, pairingRegisterer: pairingClient, metadata: metadata, kms: kms, logger: logger, pairingStore: pairingStore, sessionStore: sessionStore, verifyClient: verifyClient) + let approveEngine = ApproveEngine( + networkingInteractor: networkingClient, + proposalPayloadsStore: proposalPayloadsStore, + verifyContextStore: verifyContextStore, + sessionTopicToProposal: sessionTopicToProposal, + pairingRegisterer: pairingClient, + metadata: metadata, + kms: kms, + logger: logger, + pairingStore: pairingStore, + sessionStore: sessionStore, + verifyClient: verifyClient + ) let cleanupService = SignCleanupService(pairingStore: pairingStore, sessionStore: sessionStore, kms: kms, sessionTopicToProposal: sessionTopicToProposal, networkInteractor: networkingClient) let deleteSessionService = DeleteSessionService(networkingInteractor: networkingClient, kms: kms, sessionStore: sessionStore, logger: logger) let disconnectService = DisconnectService(deleteSessionService: deleteSessionService, sessionStorage: sessionStore) diff --git a/Sources/WalletConnectSign/Sign/SignClientProtocol.swift b/Sources/WalletConnectSign/Sign/SignClientProtocol.swift index f23857f04..437c115c6 100644 --- a/Sources/WalletConnectSign/Sign/SignClientProtocol.swift +++ b/Sources/WalletConnectSign/Sign/SignClientProtocol.swift @@ -24,6 +24,7 @@ public protocol SignClientProtocol { func getSessions() -> [Session] func cleanup() async throws - func getPendingRequests(topic: String?) -> [Request] - func getSessionRequestRecord(id: RPCID) -> Request? + func getPendingRequests(topic: String?) -> [(request: Request, context: VerifyContext?)] + func getPendingProposals(topic: String?) -> [(proposal: Session.Proposal, context: VerifyContext?)] + func getSessionRequestRecord(id: RPCID) -> (request: Request, context: VerifyContext?)? } diff --git a/Sources/WalletConnectSync/Services/SyncService.swift b/Sources/WalletConnectSync/Services/SyncService.swift index d18a5580a..9902578de 100644 --- a/Sources/WalletConnectSync/Services/SyncService.swift +++ b/Sources/WalletConnectSync/Services/SyncService.swift @@ -31,6 +31,22 @@ final class SyncService { setupSubscriptions() } + func create(account: Account, store: String) async throws { + if let _ = try? indexStore.getRecord(account: account, name: store) { + return + } + + let topic = try derivationService.deriveTopic(account: account, store: store) + indexStore.set(topic: topic, name: store, account: account) + } + + func subscribe(account: Account, store: String) async throws { + guard let record = try? indexStore.getRecord(account: account, name: store) else { + throw Errors.recordNotFoundForAccount + } + try await networkInteractor.subscribe(topic: record.topic) + } + func set(account: Account, store: String, object: Object) async throws { let protocolMethod = SyncSetMethod() let params = StoreSet(key: object.databaseId, value: try object.json()) @@ -57,11 +73,6 @@ final class SyncService { logger.debug("Did delete value for \(store). Sent on: \(record.topic). Key: \n\(key)\n") } - - func create(account: Account, store: String) async throws { - let topic = try getTopic(for: account, store: store) - try await networkInteractor.subscribe(topic: topic) - } } private extension SyncService { @@ -87,14 +98,4 @@ private extension SyncService { } .store(in: &publishers) } - - func getTopic(for account: Account, store: String) throws -> String { - if let record = try? indexStore.getRecord(account: account, name: store) { - return record.topic - } - - let topic = try derivationService.deriveTopic(account: account, store: store) - indexStore.set(topic: topic, name: store, account: account) - return topic - } } diff --git a/Sources/WalletConnectSync/Stores/SyncStore.swift b/Sources/WalletConnectSync/Stores/SyncStore.swift index fae99c935..e6d93f32c 100644 --- a/Sources/WalletConnectSync/Stores/SyncStore.swift +++ b/Sources/WalletConnectSync/Stores/SyncStore.swift @@ -4,6 +4,7 @@ import Combine public enum SyncUpdate { case set(object: Object) case delete(object: Object) + case update(object: Object) } public final class SyncStore { @@ -39,11 +40,15 @@ public final class SyncStore { setupSubscriptions() } - public func initialize(for account: Account) async throws { + public func create(for account: Account) async throws { try await syncClient.create(account: account, store: name) } - public func setupSubscriptions(account: Account) throws { + public func subscribe(for account: Account) async throws { + try await syncClient.subscribe(account: account, store: name) + } + + public func setupDatabaseSubscriptions(account: Account) throws { let record = try indexStore.getRecord(account: account, name: name) objectStore.onUpdate = { [unowned self] in @@ -93,8 +98,9 @@ public final class SyncStore { return record.topic } - public func setInStore(objects: [Object], for account: Account) throws { + public func replaceInStore(objects: [Object], for account: Account) throws { let record = try indexStore.getRecord(account: account, name: name) + objectStore.deleteAll(for: record.topic) objectStore.set(elements: objects, for: record.topic) } } @@ -111,8 +117,10 @@ private extension SyncStore { switch update { case .set(let set): let object = try! JSONDecoder().decode(Object.self, from: Data(set.value.utf8)) + let exists = objectStore.exists(for: record.topic, id: object.databaseId) if try! setInStore(object: object, for: record.account) { - syncUpdateSubject.send((topic, record.account, .set(object: object))) + let update: SyncUpdate = exists ? .update(object: object) : .set(object: object) + syncUpdateSubject.send((topic, record.account, update)) } case .delete(let delete): if let object = get(for: delete.key), try! deleteInStore(id: delete.key, for: record.account) { diff --git a/Sources/WalletConnectSync/SyncClient.swift b/Sources/WalletConnectSync/SyncClient.swift index fbe09bf6f..e7e336d8e 100644 --- a/Sources/WalletConnectSync/SyncClient.swift +++ b/Sources/WalletConnectSync/SyncClient.swift @@ -40,6 +40,11 @@ public final class SyncClient { try await syncService.create(account: account, store: store) } + /// Subscribe for sync topic + public func subscribe(account: Account, store: String) async throws { + try await syncService.subscribe(account: account, store: store) + } + // Set value to store public func set( account: Account, diff --git a/Sources/WalletConnectUtils/Cacao/IATProvider.swift b/Sources/WalletConnectUtils/Cacao/IATProvider.swift index 22aefa45d..17637c22c 100644 --- a/Sources/WalletConnectUtils/Cacao/IATProvider.swift +++ b/Sources/WalletConnectUtils/Cacao/IATProvider.swift @@ -12,3 +12,11 @@ public struct DefaultIATProvider: IATProvider { return ISO8601DateFormatter().string(from: Date()) } } + +#if DEBUG +struct IATProviderMock: IATProvider { + var iat: String { + return "2022-10-10T23:03:35.700Z" + } +} +#endif diff --git a/Sources/WalletConnectUtils/KeyedDatabase.swift b/Sources/WalletConnectUtils/KeyedDatabase.swift index a7807aecd..c619d62d5 100644 --- a/Sources/WalletConnectUtils/KeyedDatabase.swift +++ b/Sources/WalletConnectUtils/KeyedDatabase.swift @@ -51,6 +51,11 @@ public class KeyedDatabase where Element: DatabaseObject { return (value.key, element) } + public func exists(for key: String, id: String) -> Bool { + let element = getElement(for: key, id: id) + return element != nil + } + @discardableResult public func set(elements: [Element], for key: String) -> Bool { var map = index[key] ?? [:] @@ -71,7 +76,7 @@ public class KeyedDatabase where Element: DatabaseObject { var map = index[key] ?? [:] guard - map[element.databaseId] == nil else { return false } + map[element.databaseId] == nil || map[element.databaseId] != element else { return false } map[element.databaseId] = element index[key] = map @@ -94,8 +99,6 @@ public class KeyedDatabase where Element: DatabaseObject { @discardableResult public func deleteAll(for key: String) -> Bool { - var map = index[key] - guard index[key] != nil else { return false } index[key] = nil diff --git a/Sources/WalletConnectUtils/Logger.swift b/Sources/WalletConnectUtils/Logger.swift deleted file mode 100644 index ee6b0aad2..000000000 --- a/Sources/WalletConnectUtils/Logger.swift +++ /dev/null @@ -1,80 +0,0 @@ -import Foundation - -/// Logging Protocol -public protocol ConsoleLogging { - /// Writes a debug message to the log. - func debug(_ items: Any...) - - /// Writes an informative message to the log. - func info(_ items: Any...) - - /// Writes information about a warning to the log. - func warn(_ items: Any...) - - /// Writes information about an error to the log. - func error(_ items: Any...) - - func setLogging(level: LoggingLevel) -} - -public class ConsoleLogger: ConsoleLogging { - private var loggingLevel: LoggingLevel - private var suffix: String - - public func setLogging(level: LoggingLevel) { - self.loggingLevel = level - } - - public init(suffix: String? = nil, loggingLevel: LoggingLevel = .warn) { - self.suffix = suffix ?? "" - self.loggingLevel = loggingLevel - } - - public func debug(_ items: Any...) { - if loggingLevel >= .debug { - items.forEach { - Swift.print("\(suffix) \($0) - \(logFormattedDate(Date()))") - } - } - } - - public func info(_ items: Any...) { - if loggingLevel >= .info { - items.forEach { - Swift.print("\(suffix) \($0)") - } - } - } - - public func warn(_ items: Any...) { - if loggingLevel >= .warn { - items.forEach { - Swift.print("\(suffix) ⚠️ \($0)") - } - } - } - - public func error(_ items: Any...) { - if loggingLevel >= .error { - items.forEach { - Swift.print("\(suffix) ‼️ \($0)") - } - } - } -} - -public enum LoggingLevel: Comparable { - case off - case error - case warn - case info - case debug -} - - -fileprivate func logFormattedDate(_ date: Date) -> String { - let dateFormatter = DateFormatter() - dateFormatter.locale = NSLocale.current - dateFormatter.dateFormat = "HH:mm:ss.SSSS" - return dateFormatter.string(from: date) -} diff --git a/Sources/WalletConnectUtils/Logger/ConsoleLogger.swift b/Sources/WalletConnectUtils/Logger/ConsoleLogger.swift new file mode 100644 index 000000000..f87f2dfef --- /dev/null +++ b/Sources/WalletConnectUtils/Logger/ConsoleLogger.swift @@ -0,0 +1,126 @@ +import Foundation +import Combine + +public protocol ConsoleLogging { + var logsPublisher: AnyPublisher { get } + func debug(_ items: Any..., file: String, function: String, line: Int, properties: [String: String]?) + func info(_ items: Any..., file: String, function: String, line: Int) + func warn(_ items: Any..., file: String, function: String, line: Int) + func error(_ items: Any..., file: String, function: String, line: Int) + func setLogging(level: LoggingLevel) +} + +public extension ConsoleLogging { + func debug(_ items: Any..., file: String = #file, function: String = #function, line: Int = #line, properties: [String: String]? = nil) { + debug(items, file: file, function: function, line: line, properties: properties) + } + func info(_ items: Any..., file: String = #file, function: String = #function, line: Int = #line) { + info(items, file: file, function: function, line: line) + } + func warn(_ items: Any..., file: String = #file, function: String = #function, line: Int = #line) { + warn(items, file: file, function: function, line: line) + } + func error(_ items: Any..., file: String = #file, function: String = #function, line: Int = #line) { + error(items, file: file, function: function, line: line) + } +} + +public class ConsoleLogger { + private var loggingLevel: LoggingLevel + private var prefix: String + private var logsPublisherSubject = PassthroughSubject() + public var logsPublisher: AnyPublisher { + return logsPublisherSubject.eraseToAnyPublisher() + } + + public func setLogging(level: LoggingLevel) { + self.loggingLevel = level + } + + public init(prefix: String? = nil, loggingLevel: LoggingLevel = .warn) { + self.prefix = prefix ?? "" + self.loggingLevel = loggingLevel + } + + private func logMessage(_ items: Any..., logType: LoggingLevel, file: String = #file, function: String = #function, line: Int = #line, properties: [String: String]? = nil) { + let fileName = (file as NSString).lastPathComponent + items.forEach { + var logMessage = "\($0)" + var properties = properties ?? [String: String]() + properties["fileName"] = fileName + properties["line"] = "\(line)" + properties["function"] = function + switch logType { + case .debug: + logMessage = "\(prefix) \(logMessage)" + logsPublisherSubject.send(.debug(LogMessage(message: logMessage, properties: properties))) + case .info: + logMessage = "\(prefix) ℹ️ \(logMessage)" + logsPublisherSubject.send(.info(LogMessage(message: logMessage, properties: properties))) + case .warn: + logMessage = "\(prefix) ⚠️ \(logMessage)" + logsPublisherSubject.send(.warn(LogMessage(message: logMessage, properties: properties))) + case .error: + logMessage = "\(prefix) ‼️ \(logMessage)" + logsPublisherSubject.send(.error(LogMessage(message: logMessage, properties: properties))) + case .off: + return + } + logMessage = "\(prefix) [\(fileName)]: \($0) - \(function) - line: \(line) - \(logFormattedDate(Date()))" + Swift.print(logMessage) + } + } + + private func logFormattedDate(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + return formatter.string(from: date) + } +} + + +extension ConsoleLogger: ConsoleLogging { + public func debug(_ items: Any..., file: String, function: String, line: Int, properties: [String : String]?) { + if loggingLevel >= .debug { + logMessage(items, logType: .debug, file: file, function: function, line: line, properties: properties) + } + } + + public func info(_ items: Any..., file: String, function: String, line: Int) { + if loggingLevel >= .info { + logMessage(items, logType: .info, file: file, function: function, line: line) + } + } + + public func warn(_ items: Any..., file: String, function: String, line: Int) { + if loggingLevel >= .warn { + logMessage(items, logType: .warn, file: file, function: function, line: line) + } + } + + public func error(_ items: Any..., file: String, function: String, line: Int) { + if loggingLevel >= .error { + logMessage(items, logType: .error, file: file, function: function, line: line) + } + } + +} + + +#if DEBUG +public struct ConsoleLoggerMock: ConsoleLogging { + public var logsPublisher: AnyPublisher { + return PassthroughSubject().eraseToAnyPublisher() + } + + public init() {} + + public func debug(_ items: Any..., file: String, function: String, line: Int, properties: [String: String]?) { } + public func info(_ items: Any..., file: String, function: String, line: Int) { } + public func warn(_ items: Any..., file: String, function: String, line: Int) { } + public func error(_ items: Any..., file: String, function: String, line: Int) { } + + public func setLogging(level: LoggingLevel) { } +} +#endif + diff --git a/Sources/WalletConnectUtils/Logger/Log.swift b/Sources/WalletConnectUtils/Logger/Log.swift new file mode 100644 index 000000000..5e3c7f785 --- /dev/null +++ b/Sources/WalletConnectUtils/Logger/Log.swift @@ -0,0 +1,26 @@ +import Foundation + +public struct LogMessage { + public let message: String + public let properties: [String: String]? + public var aggregated: String { + var aggregatedProperties = "" + + properties?.forEach { key, value in + aggregatedProperties += "\(key): \(value), " + } + + if !aggregatedProperties.isEmpty { + aggregatedProperties = String(aggregatedProperties.dropLast(2)) + } + + return "\(message), properties: [\(aggregatedProperties)]" + } +} + +public enum Log { + case error(LogMessage) + case warn(LogMessage) + case info(LogMessage) + case debug(LogMessage) +} diff --git a/Sources/WalletConnectUtils/Logger/LoggingLevel.swift b/Sources/WalletConnectUtils/Logger/LoggingLevel.swift new file mode 100644 index 000000000..fbb1f7ec0 --- /dev/null +++ b/Sources/WalletConnectUtils/Logger/LoggingLevel.swift @@ -0,0 +1,9 @@ +import Foundation + +public enum LoggingLevel: Comparable { + case off + case error + case warn + case info + case debug +} diff --git a/Sources/WalletConnectVerify/OriginVerifier.swift b/Sources/WalletConnectVerify/OriginVerifier.swift index 039cccecf..2ab3267ae 100644 --- a/Sources/WalletConnectVerify/OriginVerifier.swift +++ b/Sources/WalletConnectVerify/OriginVerifier.swift @@ -5,22 +5,44 @@ public final class OriginVerifier { case registrationFailed } - private let verifyHost: String + private var verifyHost: String + /// The property is used to determine whether verify.walletconnect.org will be used + /// in case verify.walletconnect.com doesn't respond for some reason (most likely due to being blocked in the user's location). + private var fallback = false init(verifyHost: String) { self.verifyHost = verifyHost } func verifyOrigin(assertionId: String) async throws -> String { - let httpClient = HTTPNetworkClient(host: verifyHost) - let response = try await httpClient.request( - VerifyResponse.self, - at: VerifyAPI.resolve(assertionId: assertionId) - ) - guard let origin = response.origin else { - throw Errors.registrationFailed + let sessionConfiguration = URLSessionConfiguration.default + sessionConfiguration.timeoutIntervalForRequest = 5.0 + sessionConfiguration.timeoutIntervalForResource = 5.0 + let session = URLSession(configuration: sessionConfiguration) + + let httpClient = HTTPNetworkClient(host: verifyHost, session: session) + + do { + let response = try await httpClient.request( + VerifyResponse.self, + at: VerifyAPI.resolve(assertionId: assertionId) + ) + guard let origin = response.origin else { + throw Errors.registrationFailed + } + return origin + } catch { + if (error as? HTTPError) == .couldNotConnect && !fallback { + fallback = true + verifyHostFallback() + return try await verifyOrigin(assertionId: assertionId) + } + throw error } - return origin + } + + func verifyHostFallback() { + verifyHost = "verify.walletconnect.org" } } diff --git a/Sources/WalletConnectVerify/Storage/VerifyStorageIdentifiers.swift b/Sources/WalletConnectVerify/Storage/VerifyStorageIdentifiers.swift new file mode 100644 index 000000000..32d29d5a5 --- /dev/null +++ b/Sources/WalletConnectVerify/Storage/VerifyStorageIdentifiers.swift @@ -0,0 +1,5 @@ +import Foundation + +public enum VerifyStorageIdentifiers: String { + case context = "com.walletconnect.sdk.verifyContext" +} diff --git a/Sources/WalletConnectVerify/VerifyClient.swift b/Sources/WalletConnectVerify/VerifyClient.swift index 0b67e0f70..b1364f347 100644 --- a/Sources/WalletConnectVerify/VerifyClient.swift +++ b/Sources/WalletConnectVerify/VerifyClient.swift @@ -38,11 +38,19 @@ public actor VerifyClient: VerifyClientProtocol { } nonisolated public func createVerifyContext(origin: String?, domain: String) -> VerifyContext { - return VerifyContext( - origin: origin, - validation: (origin == domain) ? .valid : (origin == nil ? .unknown : .invalid), - verifyUrl: verifyHost - ) + if let origin, let originUrl = URL(string: origin), let domainUrl = URL(string: domain) { + return VerifyContext( + origin: origin, + validation: (originUrl.host == domainUrl.host) ? .valid : .invalid, + verifyUrl: verifyHost + ) + } else { + return VerifyContext( + origin: origin, + validation: .unknown, + verifyUrl: verifyHost + ) + } } public func registerAssertion() async throws { diff --git a/Sources/WalletConnectVerify/VerifyContext.swift b/Sources/WalletConnectVerify/VerifyContext.swift index 094ea95ab..62b0be4a2 100644 --- a/Sources/WalletConnectVerify/VerifyContext.swift +++ b/Sources/WalletConnectVerify/VerifyContext.swift @@ -1,5 +1,5 @@ -public struct VerifyContext: Equatable, Hashable { - public enum ValidationStatus { +public struct VerifyContext: Equatable, Hashable, Codable { + public enum ValidationStatus: Codable { case unknown case valid case invalid diff --git a/Sources/Web3Inbox/ChatClient/ChatClientProxy.swift b/Sources/Web3Inbox/ChatClient/ChatClientProxy.swift index ef51bd3b4..b6fd91d00 100644 --- a/Sources/Web3Inbox/ChatClient/ChatClientProxy.swift +++ b/Sources/Web3Inbox/ChatClient/ChatClientProxy.swift @@ -5,7 +5,7 @@ final class ChatClientProxy { private let client: ChatClient var onSign: SigningCallback - var onResponse: ((RPCResponse) async throws -> Void)? + var onResponse: ((RPCResponse, RPCRequest) async throws -> Void)? init(client: ChatClient, onSign: @escaping SigningCallback) { self.client = client @@ -119,6 +119,6 @@ private extension ChatClientProxy { func respond(with object: Object = Blob(), request: RPCRequest) async throws { let response = RPCResponse(matchingRequest: request, result: object) - try await onResponse?(response) + try await onResponse?(response, request) } } diff --git a/Sources/Web3Inbox/ConfigParam.swift b/Sources/Web3Inbox/ConfigParam.swift index 429be268d..ad2ff6db7 100644 --- a/Sources/Web3Inbox/ConfigParam.swift +++ b/Sources/Web3Inbox/ConfigParam.swift @@ -3,6 +3,6 @@ import Foundation public enum ConfigParam { case chatEnabled - case pushEnabled + case notifyEnabled case settingsEnabled } diff --git a/Sources/Web3Inbox/PushClientProxy/PushClientProxy.swift b/Sources/Web3Inbox/NotifyClientProxy/NotifyClientProxy.swift similarity index 68% rename from Sources/Web3Inbox/PushClientProxy/PushClientProxy.swift rename to Sources/Web3Inbox/NotifyClientProxy/NotifyClientProxy.swift index 8ad36f8ba..4a2a39138 100644 --- a/Sources/Web3Inbox/PushClientProxy/PushClientProxy.swift +++ b/Sources/Web3Inbox/NotifyClientProxy/NotifyClientProxy.swift @@ -1,36 +1,28 @@ import Foundation -final class PushClientProxy { +final class NotifyClientProxy { - private let client: WalletPushClient + private let client: NotifyClient var onSign: SigningCallback - var onResponse: ((RPCResponse) async throws -> Void)? + var onResponse: ((RPCResponse, RPCRequest) async throws -> Void)? - init(client: WalletPushClient, onSign: @escaping SigningCallback) { + init(client: NotifyClient, onSign: @escaping SigningCallback) { self.client = client self.onSign = onSign } func request(_ request: RPCRequest) async throws { - guard let event = PushWebViewEvent(rawValue: request.method) + guard let event = NotifyWebViewEvent(rawValue: request.method) else { throw Errors.unregisteredMethod } // TODO: Handle register event switch event { - case .approve: - let params = try parse(ApproveRequest.self, params: request.params) - try await client.approve(id: params.id, onSign: onSign) - try await respond(request: request) case .update: let params = try parse(UpdateRequest.self, params: request.params) try await client.update(topic: params.topic, scope: params.scope) try await respond(request: request) - case .reject: - let params = try parse(RejectRequest.self, params: request.params) - try await client.reject(id: params.id) - try await respond(request: request) case .subscribe: let params = try parse(SubscribeRequest.self, params: request.params) try await client.subscribe(metadata: params.metadata, account: params.account, onSign: onSign) @@ -46,19 +38,19 @@ final class PushClientProxy { let params = try parse(DeleteSubscriptionRequest.self, params: request.params) try await client.deleteSubscription(topic: params.topic) try await respond(request: request) - case .deletePushMessage: - let params = try parse(DeletePushMessageRequest.self, params: request.params) - client.deletePushMessage(id: params.id.string) + case .deleteNotifyMessage: + let params = try parse(DeleteNotifyMessageRequest.self, params: request.params) + client.deleteNotifyMessage(id: params.id.string) try await respond(request: request) - case .enableSync: - let params = try parse(EnableSyncRequest.self, params: request.params) - try await client.enableSync(account: params.account, onSign: onSign) + case .register: + let params = try parse(RegisterRequest.self, params: request.params) + try await client.register(account: params.account, onSign: onSign) try await respond(request: request) } } } -private extension PushClientProxy { +private extension NotifyClientProxy { private typealias Blob = Dictionary @@ -93,11 +85,11 @@ private extension PushClientProxy { let topic: String } - struct DeletePushMessageRequest: Codable { + struct DeleteNotifyMessageRequest: Codable { let id: RPCID } - struct EnableSyncRequest: Codable { + struct RegisterRequest: Codable { let account: Account } @@ -109,6 +101,6 @@ private extension PushClientProxy { func respond(with object: Object = Blob(), request: RPCRequest) async throws { let response = RPCResponse(matchingRequest: request, result: object) - try await onResponse?(response) + try await onResponse?(response, request) } } diff --git a/Sources/Web3Inbox/NotifyClientProxy/NotifyClientRequest.swift b/Sources/Web3Inbox/NotifyClientProxy/NotifyClientRequest.swift new file mode 100644 index 000000000..b152330ce --- /dev/null +++ b/Sources/Web3Inbox/NotifyClientProxy/NotifyClientRequest.swift @@ -0,0 +1,12 @@ +import Foundation + +enum NotifyClientRequest: String { + case notifyMessage = "notify_message" + case notifyUpdate = "notify_update" + case notifyDelete = "notify_delete" + case notifySubscription = "notify_subscription" + + var method: String { + return rawValue + } +} diff --git a/Sources/Web3Inbox/NotifyClientProxy/NotifyClientRequestSubscriber.swift b/Sources/Web3Inbox/NotifyClientProxy/NotifyClientRequestSubscriber.swift new file mode 100644 index 000000000..8b255bd81 --- /dev/null +++ b/Sources/Web3Inbox/NotifyClientProxy/NotifyClientRequestSubscriber.swift @@ -0,0 +1,60 @@ +import Foundation +import Combine + +final class NotifyClientRequestSubscriber { + + private var publishers: Set = [] + + private let client: NotifyClient + private let logger: ConsoleLogging + + var onRequest: ((RPCRequest) async throws -> Void)? + + init(client: NotifyClient, logger: ConsoleLogging) { + self.client = client + self.logger = logger + + setupSubscriptions() + } + + func setupSubscriptions() { + client.notifyMessagePublisher.sink { [unowned self] record in + handle(event: .notifyMessage, params: record.message) + }.store(in: &publishers) + + client.newSubscriptionPublisher.sink { [unowned self] subscription in + handle(event: .notifySubscription, params: subscription) + }.store(in: &publishers) + + client.deleteSubscriptionPublisher.sink { [unowned self] topic in + handle(event: .notifyDelete, params: topic) + }.store(in: &publishers) + + client.updateSubscriptionPublisher.sink { [unowned self] subscription in + handle(event: .notifyUpdate, params: subscription) + }.store(in: &publishers) + } +} + +private extension NotifyClientRequestSubscriber { + + struct RequestPayload: Codable { + let id: RPCID + let account: Account + let metadata: AppMetadata + } + + func handle(event: NotifyClientRequest, params: Codable) { + Task { + do { + let request = RPCRequest( + method: event.method, + params: params + ) + try await onRequest?(request) + } catch { + logger.error("Client Request error: \(error.localizedDescription)") + } + } + } +} diff --git a/Sources/Web3Inbox/PushClientProxy/PushClientRequest.swift b/Sources/Web3Inbox/PushClientProxy/PushClientRequest.swift deleted file mode 100644 index e03e3378c..000000000 --- a/Sources/Web3Inbox/PushClientProxy/PushClientRequest.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -enum PushClientRequest: String { - case pushRequest = "push_request" - case pushMessage = "push_message" - case pushUpdate = "push_update" - case pushDelete = "push_delete" - case pushSubscription = "push_subscription" - - var method: String { - return rawValue - } -} diff --git a/Sources/Web3Inbox/PushClientProxy/PushClientRequestSubscriber.swift b/Sources/Web3Inbox/PushClientProxy/PushClientRequestSubscriber.swift deleted file mode 100644 index ad77322d5..000000000 --- a/Sources/Web3Inbox/PushClientProxy/PushClientRequestSubscriber.swift +++ /dev/null @@ -1,75 +0,0 @@ -import Foundation -import Combine - -final class PushClientRequestSubscriber { - - private var publishers: Set = [] - - private let client: WalletPushClient - private let logger: ConsoleLogging - - var onRequest: ((RPCRequest) async throws -> Void)? - - init(client: WalletPushClient, logger: ConsoleLogging) { - self.client = client - self.logger = logger - - setupSubscriptions() - } - - func setupSubscriptions() { - client.requestPublisher.sink { [unowned self] id, account, metadata in - let params = RequestPayload(id: id, account: account, metadata: metadata) - handle(event: .pushRequest, params: params) - }.store(in: &publishers) - - client.pushMessagePublisher.sink { [unowned self] record in - handle(event: .pushMessage, params: record.message) - }.store(in: &publishers) - - client.deleteSubscriptionPublisher.sink { [unowned self] record in - handle(event: .pushDelete, params: record) - }.store(in: &publishers) - - client.newSubscriptionPublisher.sink { [unowned self] subscription in - handle(event: .pushSubscription, params: subscription) - }.store(in: &publishers) - - client.deleteSubscriptionPublisher.sink { [unowned self] topic in - handle(event: .pushDelete, params: topic) - }.store(in: &publishers) - - client.updateSubscriptionPublisher.sink { [unowned self] record in - switch record { - case .success(let subscription): - handle(event: .pushUpdate, params: subscription) - case .failure: - //TODO - handle error - break - } - }.store(in: &publishers) - } -} - -private extension PushClientRequestSubscriber { - - struct RequestPayload: Codable { - let id: RPCID - let account: Account - let metadata: AppMetadata - } - - func handle(event: PushClientRequest, params: Codable) { - Task { - do { - let request = RPCRequest( - method: event.method, - params: params - ) - try await onRequest?(request) - } catch { - logger.error("Client Request error: \(error.localizedDescription)") - } - } - } -} diff --git a/Sources/Web3Inbox/Web3Inbox.swift b/Sources/Web3Inbox/Web3Inbox.swift index e2555049e..17bbc7f55 100644 --- a/Sources/Web3Inbox/Web3Inbox.swift +++ b/Sources/Web3Inbox/Web3Inbox.swift @@ -7,7 +7,7 @@ public final class Web3Inbox { guard let account, let config = config, let onSign else { fatalError("Error - you must call Web3Inbox.configure(_:) before accessing the shared instance.") } - return Web3InboxClientFactory.create(chatClient: Chat.instance, pushClient: Push.wallet, account: account, config: config, onSign: onSign) + return Web3InboxClientFactory.create(chatClient: Chat.instance, notifyClient: Notify.instance, account: account, config: config, onSign: onSign) }() private static var account: Account? @@ -22,12 +22,13 @@ public final class Web3Inbox { bip44: BIP44Provider, config: [ConfigParam: Bool] = [:], environment: APNSEnvironment, + crypto: CryptoProvider, onSign: @escaping SigningCallback ) { Web3Inbox.account = account Web3Inbox.config = config Web3Inbox.onSign = onSign Chat.configure(bip44: bip44) - Push.configure(environment: environment) + Notify.configure(environment: environment, crypto: crypto) } } diff --git a/Sources/Web3Inbox/Web3InboxClient.swift b/Sources/Web3Inbox/Web3InboxClient.swift index 3a17e4d0e..7475cd74e 100644 --- a/Sources/Web3Inbox/Web3InboxClient.swift +++ b/Sources/Web3Inbox/Web3InboxClient.swift @@ -1,24 +1,31 @@ import Foundation import WebKit +import Combine public final class Web3InboxClient { private let webView: WKWebView private var account: Account private let logger: ConsoleLogging - private let pushClient: WalletPushClient + private let notifyClient: NotifyClient private let chatClientProxy: ChatClientProxy private let chatClientSubscriber: ChatClientRequestSubscriber - private let pushClientProxy: PushClientProxy - private let pushClientSubscriber: PushClientRequestSubscriber + private let notifyClientProxy: NotifyClientProxy + private let notifyClientSubscriber: NotifyClientRequestSubscriber private let chatWebviewProxy: WebViewProxy - private let pushWebviewProxy: WebViewProxy + private let notifyWebviewProxy: WebViewProxy private let webviewSubscriber: WebViewRequestSubscriber + public var logsPublisher: AnyPublisher { + logger.logsPublisher + .merge(with: notifyClient.logsPublisher) + .eraseToAnyPublisher() + } + init( webView: WKWebView, account: Account, @@ -26,11 +33,11 @@ public final class Web3InboxClient { chatClientProxy: ChatClientProxy, clientSubscriber: ChatClientRequestSubscriber, chatWebviewProxy: WebViewProxy, - pushWebviewProxy: WebViewProxy, + notifyWebviewProxy: WebViewProxy, webviewSubscriber: WebViewRequestSubscriber, - pushClientProxy: PushClientProxy, - pushClientSubscriber: PushClientRequestSubscriber, - pushClient: WalletPushClient + notifyClientProxy: NotifyClientProxy, + notifyClientSubscriber: NotifyClientRequestSubscriber, + notifyClient: NotifyClient ) { self.webView = webView self.account = account @@ -38,14 +45,20 @@ public final class Web3InboxClient { self.chatClientProxy = chatClientProxy self.chatClientSubscriber = clientSubscriber self.chatWebviewProxy = chatWebviewProxy - self.pushWebviewProxy = pushWebviewProxy + self.notifyWebviewProxy = notifyWebviewProxy self.webviewSubscriber = webviewSubscriber - self.pushClientProxy = pushClientProxy - self.pushClientSubscriber = pushClientSubscriber - self.pushClient = pushClient + self.notifyClientProxy = notifyClientProxy + self.notifyClientSubscriber = notifyClientSubscriber + self.notifyClient = notifyClient setupSubscriptions() } + + public func setLogging(level: LoggingLevel) { + logger.setLogging(level: level) + notifyClient.setLogging(level: .debug) + } + public func getWebView() -> WKWebView { return webView } @@ -59,7 +72,11 @@ public final class Web3InboxClient { } public func register(deviceToken: Data) async throws { - try await pushClient.register(deviceToken: deviceToken) + try await notifyClient.register(deviceToken: deviceToken) + } + + public func reload() { + webviewSubscriber.reload(webView) } } @@ -71,8 +88,8 @@ private extension Web3InboxClient { // Chat - chatClientProxy.onResponse = { [unowned self] response in - try await self.chatWebviewProxy.respond(response) + chatClientProxy.onResponse = { [unowned self] response, request in + try await self.chatWebviewProxy.respond(response, request) } chatClientSubscriber.onRequest = { [unowned self] request in @@ -84,19 +101,19 @@ private extension Web3InboxClient { try await self.chatClientProxy.request(request) } - // Push + // Notify - pushClientProxy.onResponse = { [unowned self] response in - try await self.pushWebviewProxy.respond(response) + notifyClientProxy.onResponse = { [unowned self] response, request in + try await self.notifyWebviewProxy.respond(response, request) } - pushClientSubscriber.onRequest = { [unowned self] request in - try await self.pushWebviewProxy.request(request) + notifyClientSubscriber.onRequest = { [unowned self] request in + try await self.notifyWebviewProxy.request(request) } - webviewSubscriber.onPushRequest = { [unowned self] request in - logger.debug("w3i: push method \(request.method) requested") - try await self.pushClientProxy.request(request) + webviewSubscriber.onNotifyRequest = { [unowned self] request in + logger.debug("w3i: notify method \(request.method) requested") + try await self.notifyClientProxy.request(request) } } diff --git a/Sources/Web3Inbox/Web3InboxClientFactory.swift b/Sources/Web3Inbox/Web3InboxClientFactory.swift index 70d59071a..83ce998fe 100644 --- a/Sources/Web3Inbox/Web3InboxClientFactory.swift +++ b/Sources/Web3Inbox/Web3InboxClientFactory.swift @@ -5,23 +5,24 @@ final class Web3InboxClientFactory { static func create( chatClient: ChatClient, - pushClient: WalletPushClient, + notifyClient: NotifyClient, account: Account, config: [ConfigParam: Bool], onSign: @escaping SigningCallback ) -> Web3InboxClient { let url = buildUrl(account: account, config: config) - let logger = ConsoleLogger(suffix: "📬", loggingLevel: .debug) + + let logger = ConsoleLogger(prefix: "📬", loggingLevel: .off) let webviewSubscriber = WebViewRequestSubscriber(url: url, logger: logger) let webView = WebViewFactory(url: url, webviewSubscriber: webviewSubscriber).create() let chatWebViewProxy = WebViewProxy(webView: webView, scriptFormatter: ChatWebViewScriptFormatter(), logger: logger) - let pushWebViewProxy = WebViewProxy(webView: webView, scriptFormatter: PushWebViewScriptFormatter(), logger: logger) + let notifyWebViewProxy = WebViewProxy(webView: webView, scriptFormatter: NotifyWebViewScriptFormatter(), logger: logger) let clientProxy = ChatClientProxy(client: chatClient, onSign: onSign) let clientSubscriber = ChatClientRequestSubscriber(chatClient: chatClient, logger: logger) - let pushClientProxy = PushClientProxy(client: pushClient, onSign: onSign) - let pushClientSubscriber = PushClientRequestSubscriber(client: pushClient, logger: logger) + let notifyClientProxy = NotifyClientProxy(client: notifyClient, onSign: onSign) + let notifyClientSubscriber = NotifyClientRequestSubscriber(client: notifyClient, logger: logger) return Web3InboxClient( webView: webView, @@ -30,17 +31,17 @@ final class Web3InboxClientFactory { chatClientProxy: clientProxy, clientSubscriber: clientSubscriber, chatWebviewProxy: chatWebViewProxy, - pushWebviewProxy: pushWebViewProxy, + notifyWebviewProxy: notifyWebViewProxy, webviewSubscriber: webviewSubscriber, - pushClientProxy: pushClientProxy, - pushClientSubscriber: pushClientSubscriber, - pushClient: pushClient + notifyClientProxy: notifyClientProxy, + notifyClientSubscriber: notifyClientSubscriber, + notifyClient: notifyClient ) } private static func buildUrl(account: Account, config: [ConfigParam: Bool]) -> URL { var urlComponents = URLComponents(string: "https://web3inbox-dev-hidden.vercel.app/")! - var queryItems = [URLQueryItem(name: "chatProvider", value: "ios"), URLQueryItem(name: "pushProvider", value: "ios"), URLQueryItem(name: "account", value: account.address), URLQueryItem(name: "authProvider", value: "ios")] + var queryItems = [URLQueryItem(name: "chatProvider", value: "ios"), URLQueryItem(name: "notifyProvider", value: "ios"), URLQueryItem(name: "account", value: account.address), URLQueryItem(name: "authProvider", value: "ios")] for param in config.filter({ $0.value == false}) { queryItems.append(URLQueryItem(name: "\(param.key)", value: "false")) diff --git a/Sources/Web3Inbox/Web3InboxImports.swift b/Sources/Web3Inbox/Web3InboxImports.swift index fd78f4977..b03055b3f 100644 --- a/Sources/Web3Inbox/Web3InboxImports.swift +++ b/Sources/Web3Inbox/Web3InboxImports.swift @@ -1,4 +1,4 @@ #if !CocoaPods @_exported import WalletConnectChat -@_exported import WalletConnectPush +@_exported import WalletConnectNotify #endif diff --git a/Sources/Web3Inbox/WebView/PushWebViewEvent.swift b/Sources/Web3Inbox/WebView/NotifyWebViewEvent.swift similarity index 56% rename from Sources/Web3Inbox/WebView/PushWebViewEvent.swift rename to Sources/Web3Inbox/WebView/NotifyWebViewEvent.swift index d320a88d7..e9a8e954b 100644 --- a/Sources/Web3Inbox/WebView/PushWebViewEvent.swift +++ b/Sources/Web3Inbox/WebView/NotifyWebViewEvent.swift @@ -1,13 +1,11 @@ import Foundation -enum PushWebViewEvent: String { - case approve +enum NotifyWebViewEvent: String { case update - case reject case subscribe case getActiveSubscriptions case getMessageHistory case deleteSubscription - case deletePushMessage - case enableSync + case deleteNotifyMessage + case register } diff --git a/Sources/Web3Inbox/WebView/WebViewFactory.swift b/Sources/Web3Inbox/WebView/WebViewFactory.swift index 5a6fc90ad..1d8cf45c1 100644 --- a/Sources/Web3Inbox/WebView/WebViewFactory.swift +++ b/Sources/Web3Inbox/WebView/WebViewFactory.swift @@ -22,7 +22,7 @@ final class WebViewFactory { ) configuration.userContentController.add( webviewSubscriber, - name: WebViewRequestSubscriber.push + name: WebViewRequestSubscriber.notify ) let webview = WKWebView(frame: .zero, configuration: configuration) diff --git a/Sources/Web3Inbox/WebView/WebViewProxy.swift b/Sources/Web3Inbox/WebView/WebViewProxy.swift index af3d0d8d7..5f4612701 100644 --- a/Sources/Web3Inbox/WebView/WebViewProxy.swift +++ b/Sources/Web3Inbox/WebView/WebViewProxy.swift @@ -17,9 +17,10 @@ actor WebViewProxy { } @MainActor - func respond(_ response: RPCResponse) async throws { + func respond(_ response: RPCResponse, _ request: RPCRequest) async throws { let body = try response.json(dateEncodingStrategy: .millisecondsSince1970) - logger.debug("resonding to w3i with \(body)") + let logProperties: [String: String] = ["method": request.method, "requestId": "\(request.id!)", "response": body] + logger.debug("resonding to w3i request \(request.method) with \(body)", properties: logProperties) let script = scriptFormatter.formatScript(body: body) webView.evaluateJavaScript(script, completionHandler: nil) } @@ -27,7 +28,8 @@ actor WebViewProxy { @MainActor func request(_ request: RPCRequest) async throws { let body = try request.json(dateEncodingStrategy: .millisecondsSince1970) - logger.debug("requesting w3i with \(body)") + let logProperties = ["method": request.method, "requestId": "\(request.id!)"] + logger.debug("requesting w3i with \(body)", properties: logProperties) let script = scriptFormatter.formatScript(body: body) webView.evaluateJavaScript(script, completionHandler: nil) } @@ -44,8 +46,8 @@ class ChatWebViewScriptFormatter: WebViewScriptFormatter { } } -class PushWebViewScriptFormatter: WebViewScriptFormatter { +class NotifyWebViewScriptFormatter: WebViewScriptFormatter { func formatScript(body: String) -> String { - return "window.web3inbox.push.postMessage(\(body))" + return "window.web3inbox.notify.postMessage(\(body))" } } diff --git a/Sources/Web3Inbox/WebView/WebViewRequestSubscriber.swift b/Sources/Web3Inbox/WebView/WebViewRequestSubscriber.swift index 7c5307221..49fc5a151 100644 --- a/Sources/Web3Inbox/WebView/WebViewRequestSubscriber.swift +++ b/Sources/Web3Inbox/WebView/WebViewRequestSubscriber.swift @@ -4,10 +4,10 @@ import WebKit final class WebViewRequestSubscriber: NSObject, WKScriptMessageHandler { static let chat = "web3inboxChat" - static let push = "web3inboxPush" + static let notify = "web3inboxNotify" var onChatRequest: ((RPCRequest) async throws -> Void)? - var onPushRequest: ((RPCRequest) async throws -> Void)? + var onNotifyRequest: ((RPCRequest) async throws -> Void)? private let url: URL private let logger: ConsoleLogging @@ -35,8 +35,8 @@ final class WebViewRequestSubscriber: NSObject, WKScriptMessageHandler { switch name { case Self.chat: try await onChatRequest?(request) - case Self.push: - try await onPushRequest?(request) + case Self.notify: + try await onNotifyRequest?(request) default: break } @@ -45,31 +45,32 @@ final class WebViewRequestSubscriber: NSObject, WKScriptMessageHandler { } } } + + func reload(_ webView: WKWebView) { + webView.load(URLRequest(url: url)) + } } extension WebViewRequestSubscriber: WKUIDelegate { - + + #if os(iOS) + @available(iOS 15.0, *) func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) { decisionHandler(.grant) } + + #endif } extension WebViewRequestSubscriber: WKNavigationDelegate { func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - guard - let from = webView.url, - let to = navigationAction.request.url - else { return decisionHandler(.cancel) } - - if from.absoluteString.contains("/login") || to.absoluteString.contains("/login") { - decisionHandler(.cancel) - webView.load(URLRequest(url: url)) - } else { + if navigationAction.request.url == url { decisionHandler(.allow) + } else { + decisionHandler(.cancel) } } } - diff --git a/Sources/Web3Wallet/Web3Wallet.swift b/Sources/Web3Wallet/Web3Wallet.swift index 5a7c2e0a5..a00f42b4b 100644 --- a/Sources/Web3Wallet/Web3Wallet.swift +++ b/Sources/Web3Wallet/Web3Wallet.swift @@ -27,7 +27,7 @@ public class Web3Wallet { authClient: Auth.instance, signClient: Sign.instance, pairingClient: Pair.instance as! PairingClient, - echoClient: Echo.instance + pushClient: Push.instance ) }() @@ -42,12 +42,12 @@ public class Web3Wallet { static public func configure( metadata: AppMetadata, crypto: CryptoProvider, - echoHost: String = "echo.walletconnect.com", + pushHost: String = "echo.walletconnect.com", environment: APNSEnvironment = .production ) { Pair.configure(metadata: metadata) Auth.configure(crypto: crypto) - Echo.configure(echoHost: echoHost, environment: environment) + Push.configure(pushHost: pushHost, environment: environment) Web3Wallet.config = Web3Wallet.Config(crypto: crypto) } } diff --git a/Sources/Web3Wallet/Web3WalletClient.swift b/Sources/Web3Wallet/Web3WalletClient.swift index 757b15e7f..a90093a10 100644 --- a/Sources/Web3Wallet/Web3WalletClient.swift +++ b/Sources/Web3Wallet/Web3WalletClient.swift @@ -67,7 +67,7 @@ public class Web3WalletClient { private let authClient: AuthClientProtocol private let signClient: SignClientProtocol private let pairingClient: PairingClientProtocol - private let echoClient: EchoClientProtocol + private let pushClient: PushClientProtocol private var account: Account? @@ -75,12 +75,12 @@ public class Web3WalletClient { authClient: AuthClientProtocol, signClient: SignClientProtocol, pairingClient: PairingClientProtocol, - echoClient: EchoClientProtocol + pushClient: PushClientProtocol ) { self.authClient = authClient self.signClient = signClient self.pairingClient = pairingClient - self.echoClient = echoClient + self.pushClient = pushClient } /// For a wallet to approve a session proposal. @@ -190,24 +190,30 @@ public class Web3WalletClient { /// Query pending requests /// - 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] { + public func getPendingRequests(topic: String? = nil) -> [(request: Request, context: VerifyContext?)] { signClient.getPendingRequests(topic: topic) } + /// Query pending proposals + /// - Returns: Pending proposals received from peer with `wc_sessionPropose` protocol method + public func getPendingProposals(topic: String? = nil) -> [(proposal: Session.Proposal, context: VerifyContext?)] { + signClient.getPendingProposals(topic: topic) + } + /// - 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? { + public func getSessionRequestRecord(id: RPCID) -> (request: Request, context: VerifyContext?)? { signClient.getSessionRequestRecord(id: id) } /// Query pending authentication requests /// - Returns: Pending authentication requests - public func getPendingRequests() throws -> [AuthRequest] { + public func getPendingRequests() throws -> [(AuthRequest, VerifyContext?)] { try authClient.getPendingRequests() } - public func registerEchoClient(deviceToken: Data) async throws { - try await echoClient.register(deviceToken: deviceToken) + public func registerPushClient(deviceToken: Data) async throws { + try await pushClient.register(deviceToken: deviceToken) } /// Delete all stored data such as: pairings, sessions, keys @@ -224,8 +230,8 @@ public class Web3WalletClient { #if DEBUG extension Web3WalletClient { - public func registerEchoClient(deviceToken: String) async throws { - try await echoClient.register(deviceToken: deviceToken) + public func registerPushClient(deviceToken: String) async throws { + try await pushClient.register(deviceToken: deviceToken) } } #endif diff --git a/Sources/Web3Wallet/Web3WalletClientFactory.swift b/Sources/Web3Wallet/Web3WalletClientFactory.swift index b27654757..99b5cc969 100644 --- a/Sources/Web3Wallet/Web3WalletClientFactory.swift +++ b/Sources/Web3Wallet/Web3WalletClientFactory.swift @@ -5,13 +5,13 @@ public struct Web3WalletClientFactory { authClient: AuthClientProtocol, signClient: SignClientProtocol, pairingClient: PairingClientProtocol, - echoClient: EchoClientProtocol + pushClient: PushClientProtocol ) -> Web3WalletClient { return Web3WalletClient( authClient: authClient, signClient: signClient, pairingClient: pairingClient, - echoClient: echoClient + pushClient: pushClient ) } } diff --git a/Sources/Web3Wallet/Web3WalletImports.swift b/Sources/Web3Wallet/Web3WalletImports.swift index c27041056..f2626dea0 100644 --- a/Sources/Web3Wallet/Web3WalletImports.swift +++ b/Sources/Web3Wallet/Web3WalletImports.swift @@ -1,6 +1,6 @@ #if !CocoaPods @_exported import Auth @_exported import WalletConnectSign -@_exported import WalletConnectEcho +@_exported import WalletConnectPush @_exported import WalletConnectVerify #endif diff --git a/Tests/AuthTests/WalletRequestSubscriberTests.swift b/Tests/AuthTests/WalletRequestSubscriberTests.swift index abf3b76da..1fba88977 100644 --- a/Tests/AuthTests/WalletRequestSubscriberTests.swift +++ b/Tests/AuthTests/WalletRequestSubscriberTests.swift @@ -11,21 +11,31 @@ class WalletRequestSubscriberTests: XCTestCase { var pairingRegisterer: PairingRegistererMock! var sut: WalletRequestSubscriber! var messageFormatter: SIWEMessageFormatterMock! - + var verifyContextStore: CodableStore! + let defaultTimeout: TimeInterval = 0.01 override func setUp() { let networkingInteractor = NetworkingInteractorMock() pairingRegisterer = PairingRegistererMock() messageFormatter = SIWEMessageFormatterMock() - - let walletErrorResponder = WalletErrorResponder(networkingInteractor: networkingInteractor, logger: ConsoleLoggerMock(), kms: KeyManagementServiceMock(), rpcHistory: RPCHistory(keyValueStore: CodableStore(defaults: RuntimeKeyValueStorage(), identifier: ""))) - sut = WalletRequestSubscriber(networkingInteractor: networkingInteractor, - logger: ConsoleLoggerMock(), - kms: KeyManagementServiceMock(), - walletErrorResponder: walletErrorResponder, - pairingRegisterer: pairingRegisterer, - verifyClient: VerifyClientMock()) + verifyContextStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: "") + + let walletErrorResponder = WalletErrorResponder( + networkingInteractor: networkingInteractor, + logger: ConsoleLoggerMock(), + kms: KeyManagementServiceMock(), + rpcHistory: RPCHistory(keyValueStore: CodableStore(defaults: RuntimeKeyValueStorage(), identifier: "")) + ) + sut = WalletRequestSubscriber( + networkingInteractor: networkingInteractor, + logger: ConsoleLoggerMock(), + kms: KeyManagementServiceMock(), + walletErrorResponder: walletErrorResponder, + pairingRegisterer: pairingRegisterer, + verifyClient: VerifyClientMock(), + verifyContextStore: verifyContextStore + ) } func testSubscribeRequest() { @@ -47,7 +57,7 @@ class WalletRequestSubscriberTests: XCTestCase { pairingRegisterer.subject.send(payload) wait(for: [messageExpectation], timeout: defaultTimeout) - XCTAssertTrue(pairingRegisterer.isActivateCalled) + XCTAssertTrue(pairingRegisterer.isReceivedCalled) XCTAssertEqual(requestPayload, expectedPayload) XCTAssertEqual(requestId, expectedRequestId) } diff --git a/Tests/NotifyTests/Mocks/MockPushStoring.swift b/Tests/NotifyTests/Mocks/MockNotifyStoring.swift similarity index 58% rename from Tests/NotifyTests/Mocks/MockPushStoring.swift rename to Tests/NotifyTests/Mocks/MockNotifyStoring.swift index 06dc07960..4f1be0991 100644 --- a/Tests/NotifyTests/Mocks/MockPushStoring.swift +++ b/Tests/NotifyTests/Mocks/MockNotifyStoring.swift @@ -1,23 +1,22 @@ - import Foundation -@testable import WalletConnectPush +@testable import WalletConnectNotify -class MockPushStoring: PushStoring { - var subscriptions: [PushSubscription] +class MockNotifyStoring: NotifyStoring { + var subscriptions: [NotifySubscription] - init(subscriptions: [PushSubscription]) { + init(subscriptions: [NotifySubscription]) { self.subscriptions = subscriptions } - func getSubscriptions() -> [PushSubscription] { + func getSubscriptions() -> [NotifySubscription] { return subscriptions } - func getSubscription(topic: String) -> PushSubscription? { + func getSubscription(topic: String) -> NotifySubscription? { return subscriptions.first { $0.topic == topic } } - func setSubscription(_ subscription: PushSubscription) async throws { + func setSubscription(_ subscription: NotifySubscription) async throws { if let index = subscriptions.firstIndex(where: { $0.topic == subscription.topic }) { subscriptions[index] = subscription } else { diff --git a/Tests/NotifyTests/Mocks/MockNotifyUpdateRequester.swift b/Tests/NotifyTests/Mocks/MockNotifyUpdateRequester.swift index 8f4d698e1..927676953 100644 --- a/Tests/NotifyTests/Mocks/MockNotifyUpdateRequester.swift +++ b/Tests/NotifyTests/Mocks/MockNotifyUpdateRequester.swift @@ -1,6 +1,6 @@ import Foundation -@testable import WalletConnectPush +@testable import WalletConnectNotify class MockNotifyUpdateRequester: NotifyUpdateRequesting { diff --git a/Tests/NotifyTests/Stubs/PushSubscription.swift b/Tests/NotifyTests/Stubs/NotifySubscription.swift similarity index 74% rename from Tests/NotifyTests/Stubs/PushSubscription.swift rename to Tests/NotifyTests/Stubs/NotifySubscription.swift index de40c5772..7251c8477 100644 --- a/Tests/NotifyTests/Stubs/PushSubscription.swift +++ b/Tests/NotifyTests/Stubs/NotifySubscription.swift @@ -1,15 +1,14 @@ - import Foundation -@testable import WalletConnectPush +@testable import WalletConnectNotify -extension PushSubscription { - static func stub(topic: String, expiry: Date) -> PushSubscription { +extension NotifySubscription { + static func stub(topic: String, expiry: Date) -> NotifySubscription { let account = Account(chainIdentifier: "eip155:1", address: "0x15bca56b6e2728aec2532df9d436bd1600e86688")! let relay = RelayProtocolOptions.stub() let metadata = AppMetadata.stub() let symKey = "key1" - return PushSubscription( + return NotifySubscription( topic: topic, account: account, relay: relay, diff --git a/Tests/NotifyTests/SubscriptionsAutoUpdaterTests.swift b/Tests/NotifyTests/SubscriptionsAutoUpdaterTests.swift index 60f0c09c3..357281daf 100644 --- a/Tests/NotifyTests/SubscriptionsAutoUpdaterTests.swift +++ b/Tests/NotifyTests/SubscriptionsAutoUpdaterTests.swift @@ -1,33 +1,34 @@ import Foundation import XCTest import TestingUtils -@testable import WalletConnectPush +@testable import WalletConnectNotify class SubscriptionsAutoUpdaterTests: XCTestCase { var sut: SubscriptionsAutoUpdater! func testUpdateSubscriptionsIfNeeded() async { - let subscriptions: [PushSubscription] = [ - PushSubscription.stub(topic: "topic1", expiry: Date().addingTimeInterval(60 * 60 * 24 * 20)), - PushSubscription.stub(topic: "topic2", expiry: Date().addingTimeInterval(60 * 60 * 24 * 10)), - PushSubscription.stub(topic: "topic3", expiry: Date().addingTimeInterval(60 * 60 * 24 * 30)) + let subscriptions: [NotifySubscription] = [ + NotifySubscription.stub(topic: "topic1", expiry: Date().addingTimeInterval(60 * 60 * 24 * 20)), + NotifySubscription.stub(topic: "topic2", expiry: Date().addingTimeInterval(60 * 60 * 24 * 10)), + NotifySubscription.stub(topic: "topic3", expiry: Date().addingTimeInterval(60 * 60 * 24 * 30)) ] let expectation = expectation(description: "update") let notifyUpdateRequester = MockNotifyUpdateRequester() let logger = ConsoleLoggerMock() - let pushStorage = MockPushStoring(subscriptions: subscriptions) + let pushStorage = MockNotifyStoring(subscriptions: subscriptions) notifyUpdateRequester.completionHandler = { if notifyUpdateRequester.updatedTopics.contains("topic2") { expectation.fulfill() } } - - sut = SubscriptionsAutoUpdater(notifyUpdateRequester: notifyUpdateRequester, - logger: logger, - pushStorage: pushStorage) + + sut = SubscriptionsAutoUpdater( + notifyUpdateRequester: notifyUpdateRequester, + logger: logger, + notifyStorage: pushStorage) await waitForExpectations(timeout: 1, handler: nil) diff --git a/Tests/RelayerTests/AuthTests/JWTTests.swift b/Tests/RelayerTests/AuthTests/JWTTests.swift index 83f8ebf4c..40ad5be31 100644 --- a/Tests/RelayerTests/AuthTests/JWTTests.swift +++ b/Tests/RelayerTests/AuthTests/JWTTests.swift @@ -36,6 +36,6 @@ extension RelayAuthPayload.Claims { let aud = "wss://relay.walletconnect.com" let expDate = Calendar.current.date(byAdding: components, to: iatDate)! let exp = UInt64(expDate.timeIntervalSince1970) - return RelayAuthPayload.Claims(iss: iss, sub: sub, aud: aud, iat: iat, exp: exp) + return RelayAuthPayload.Claims(iss: iss, sub: sub, aud: aud, iat: iat, exp: exp, act: nil) } } diff --git a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift index 92c0895af..12f7c1d94 100644 --- a/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift +++ b/Tests/RelayerTests/AutomaticSocketConnectionHandlerTests.swift @@ -24,7 +24,7 @@ final class AutomaticSocketConnectionHandlerTests: XCTestCase { func testConnectsOnConnectionSatisfied() { webSocketSession.disconnect() XCTAssertFalse(webSocketSession.isConnected) - networkMonitor.onSatisfied?() + networkMonitor.networkConnectionStatusPublisherSubject.send(.connected) XCTAssertTrue(webSocketSession.isConnected) } diff --git a/Tests/RelayerTests/Mocks/NetworkMonitoringMock.swift b/Tests/RelayerTests/Mocks/NetworkMonitoringMock.swift index 7f6a5245e..1095d1677 100644 --- a/Tests/RelayerTests/Mocks/NetworkMonitoringMock.swift +++ b/Tests/RelayerTests/Mocks/NetworkMonitoringMock.swift @@ -1,9 +1,14 @@ import Foundation +import Combine + @testable import WalletConnectRelay class NetworkMonitoringMock: NetworkMonitoring { - var onSatisfied: (() -> Void)? - var onUnsatisfied: (() -> Void)? - - func startMonitoring() { } + var networkConnectionStatusPublisher: AnyPublisher { + networkConnectionStatusPublisherSubject.eraseToAnyPublisher() + } + + let networkConnectionStatusPublisherSubject = CurrentValueSubject(.connected) + + public init() { } } diff --git a/Tests/TestingUtils/ConsoleLoggerMock.swift b/Tests/TestingUtils/ConsoleLoggerMock.swift deleted file mode 100644 index 9be922bf1..000000000 --- a/Tests/TestingUtils/ConsoleLoggerMock.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Foundation -import WalletConnectUtils - -public struct ConsoleLoggerMock: ConsoleLogging { - public init() {} - public func error(_ items: Any...) { } - public func debug(_ items: Any...) { } - public func info(_ items: Any...) { } - public func warn(_ items: Any...) { } - public func setLogging(level: LoggingLevel) { } -} diff --git a/Tests/TestingUtils/Mocks/HTTPClientMock.swift b/Tests/TestingUtils/Mocks/HTTPClientMock.swift index 4746bed92..5593b4096 100644 --- a/Tests/TestingUtils/Mocks/HTTPClientMock.swift +++ b/Tests/TestingUtils/Mocks/HTTPClientMock.swift @@ -2,7 +2,6 @@ import Foundation @testable import HTTPClient public final class HTTPClientMock: HTTPClient { - private let object: T public init(object: T) { @@ -16,4 +15,8 @@ public final class HTTPClientMock: HTTPClient { public func request(service: HTTPService) async throws { } + + public func updateHost(host: String) async { + + } } diff --git a/Tests/TestingUtils/Mocks/PairingRegistererMock.swift b/Tests/TestingUtils/Mocks/PairingRegistererMock.swift index ee0cfec9b..3c21567c6 100644 --- a/Tests/TestingUtils/Mocks/PairingRegistererMock.swift +++ b/Tests/TestingUtils/Mocks/PairingRegistererMock.swift @@ -4,10 +4,10 @@ import Combine import WalletConnectNetworking public class PairingRegistererMock: PairingRegisterer where RequestParams: Codable { - public let subject = PassthroughSubject, Never>() public var isActivateCalled: Bool = false + public var isReceivedCalled: Bool = false public func register(method: ProtocolMethod) -> AnyPublisher, Never> where RequestParams: Decodable, RequestParams: Encodable { subject.eraseToAnyPublisher() as! AnyPublisher, Never> @@ -20,4 +20,8 @@ public class PairingRegistererMock: PairingRegisterer where Reque public func validatePairingExistance(_ topic: String) throws { } + + public func setReceived(pairingTopic: String) { + isReceivedCalled = true + } } diff --git a/Tests/TestingUtils/NetworkingInteractorMock.swift b/Tests/TestingUtils/NetworkingInteractorMock.swift index 14f7f9d24..8764e7675 100644 --- a/Tests/TestingUtils/NetworkingInteractorMock.swift +++ b/Tests/TestingUtils/NetworkingInteractorMock.swift @@ -15,6 +15,7 @@ public class NetworkingInteractorMock: NetworkInteracting { private(set) var didRespondError = false private(set) var didCallSubscribe = false private(set) var didCallUnsubscribe = false + private(set) var didCallHandleHistoryRequest = false private(set) var didRespondOnTopic: String? private(set) var lastErrorCode = -1 @@ -25,9 +26,14 @@ public class NetworkingInteractorMock: NetworkInteracting { var onRespondError: ((Int) -> Void)? public let socketConnectionStatusPublisherSubject = PassthroughSubject() + public let networkConnectionStatusPublisherSubject = CurrentValueSubject(.connected) + public var socketConnectionStatusPublisher: AnyPublisher { socketConnectionStatusPublisherSubject.eraseToAnyPublisher() } + public var networkConnectionStatusPublisher: AnyPublisher { + networkConnectionStatusPublisherSubject.eraseToAnyPublisher() + } public let requestPublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest, decryptedPayload: Data, publishedAt: Date, derivedTopic: String?), Never>() public let responsePublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest, response: RPCResponse, publishedAt: Date, derivedTopic: String?), Never>() @@ -85,6 +91,10 @@ public class NetworkingInteractorMock: NetworkInteracting { subscriptions.append(topic) didCallSubscribe = true } + + public func handleHistoryRequest(topic: String, request: JSONRPC.RPCRequest) { + didCallHandleHistoryRequest = true + } func didSubscribe(to topic: String) -> Bool { subscriptions.contains { $0 == topic } diff --git a/Tests/WalletConnectKMSTests/SerialiserTests.swift b/Tests/WalletConnectKMSTests/SerialiserTests.swift index 627171b5a..bf36da69d 100644 --- a/Tests/WalletConnectKMSTests/SerialiserTests.swift +++ b/Tests/WalletConnectKMSTests/SerialiserTests.swift @@ -12,10 +12,10 @@ final class SerializerTests: XCTestCase { override func setUp() { self.myKms = KeyManagementServiceMock() - self.mySerializer = Serializer(kms: myKms) + self.mySerializer = Serializer(kms: myKms, logger: ConsoleLoggerMock()) self.peerKms = KeyManagementServiceMock() - self.peerSerializer = Serializer(kms: peerKms) + self.peerSerializer = Serializer(kms: peerKms, logger: ConsoleLoggerMock()) } func testSerializeDeserializeType0Envelope() { diff --git a/Tests/WalletConnectModalTests/ModalViewModelTests.swift b/Tests/WalletConnectModalTests/ModalViewModelTests.swift index ae020953e..55de25cc9 100644 --- a/Tests/WalletConnectModalTests/ModalViewModelTests.swift +++ b/Tests/WalletConnectModalTests/ModalViewModelTests.swift @@ -18,11 +18,47 @@ final class ModalViewModelTests: XCTestCase { sut = .init( isShown: .constant(true), interactor: ModalSheetInteractorMock(listings: [ - Listing(id: "1", name: "Sample App", homepage: "https://example.com", order: 1, imageId: "1", app: Listing.App(ios: "https://example.com/download-ios", mac: "https://example.com/download-mac", safari: "https://example.com/download-safari"), mobile: Listing.Mobile(native: nil, universal: "https://example.com/universal")), - Listing(id: "2", name: "Awesome App", homepage: "https://example.com/awesome", order: 2, imageId: "2", app: Listing.App(ios: "https://example.com/download-ios", mac: "https://example.com/download-mac", safari: "https://example.com/download-safari"), mobile: Listing.Mobile(native: "awesomeapp://deeplink", universal: "https://awesome.com/awesome/universal")), + Listing( + id: "1", + name: "Sample App", + homepage: "https://example.com", + order: 1, + imageId: "1", + app: Listing.App( + ios: "https://example.com/download-ios", + browser: "https://example.com/wallet" + ), + mobile: Listing.Links( + native: nil, + universal: "https://example.com/universal" + ), + desktop: Listing.Links( + native: nil, + universal: "https://example.com/universal" + ) + ), + Listing( + id: "2", + name: "Awesome App", + homepage: "https://example.com/awesome", + order: 2, + imageId: "2", + app: Listing.App( + ios: "https://example.com/download-ios", + browser: "https://example.com/wallet" + ), + mobile: Listing.Links( + native: "awesomeapp://deeplink", + universal: "https://awesome.com/awesome/universal" + ), + desktop: Listing.Links( + native: "awesomeapp://deeplink", + universal: "https://awesome.com/awesome/desktop/universal" + ) + ), ]), uiApplicationWrapper: .init( - openURL: { url, _ in + openURL: { url, _ in self.openURLFuncTest.call(url) self.expectation.fulfill() }, @@ -47,15 +83,13 @@ final class ModalViewModelTests: XCTestCase { await sut.createURI() XCTAssertEqual(sut.uri, "wc:foo@2?symKey=bar&relay-protocol=irn") - XCTAssertEqual(sut.wallets.count, 2) XCTAssertEqual(sut.wallets.map(\.id), ["1", "2"]) XCTAssertEqual(sut.wallets.map(\.name), ["Sample App", "Awesome App"]) expectation = XCTestExpectation(description: "Wait for openUrl to be called") - sut.onListingTap(sut.wallets[0], preferUniversal: true) - + sut.navigateToDeepLink(wallet: sut.wallets[0], preferUniversal: true, preferBrowser: false) XCTWaiter.wait(for: [expectation], timeout: 3) XCTAssertEqual( @@ -65,8 +99,7 @@ final class ModalViewModelTests: XCTestCase { expectation = XCTestExpectation(description: "Wait for openUrl to be called using universal link") - sut.onListingTap(sut.wallets[1], preferUniversal: false) - + sut.navigateToDeepLink(wallet: sut.wallets[1], preferUniversal: false, preferBrowser: false) XCTWaiter.wait(for: [expectation], timeout: 3) XCTAssertEqual( @@ -74,16 +107,24 @@ final class ModalViewModelTests: XCTestCase { URL(string: "awesomeapp://deeplinkwc?uri=wc%3Afoo%402%3FsymKey%3Dbar%26relay-protocol%3Dirn")! ) - expectation = XCTestExpectation(description: "Wait for openUrl to be called using native link") - sut.onListingTap(sut.wallets[1], preferUniversal: true) - + sut.navigateToDeepLink(wallet: sut.wallets[1], preferUniversal: true, preferBrowser: false) XCTWaiter.wait(for: [expectation], timeout: 3) XCTAssertEqual( openURLFuncTest.currentValue, URL(string: "https://awesome.com/awesome/universal/wc?uri=wc%3Afoo%402%3FsymKey%3Dbar%26relay-protocol%3Dirn")! ) + + expectation = XCTestExpectation(description: "Wait for openUrl to be called using native link") + + sut.navigateToDeepLink(wallet: sut.wallets[1], preferUniversal: false, preferBrowser: true) + XCTWaiter.wait(for: [expectation], timeout: 3) + + XCTAssertEqual( + openURLFuncTest.currentValue, + URL(string: "https://awesome.com/awesome/desktop/universal/wc?uri=wc%3Afoo%402%3FsymKey%3Dbar%26relay-protocol%3Dirn")! + ) } } diff --git a/Tests/WalletConnectPairingTests/WalletPairServiceTests.swift b/Tests/WalletConnectPairingTests/WalletPairServiceTests.swift index 7617aee57..4f88e9c94 100644 --- a/Tests/WalletConnectPairingTests/WalletPairServiceTests.swift +++ b/Tests/WalletConnectPairingTests/WalletPairServiceTests.swift @@ -11,23 +11,34 @@ final class WalletPairServiceTestsTests: XCTestCase { var networkingInteractor: NetworkingInteractorMock! var storageMock: WCPairingStorageMock! var cryptoMock: KeyManagementServiceMock! + var rpcHistory: RPCHistory! override func setUp() { networkingInteractor = NetworkingInteractorMock() storageMock = WCPairingStorageMock() cryptoMock = KeyManagementServiceMock() - service = WalletPairService(networkingInteractor: networkingInteractor, kms: cryptoMock, pairingStorage: storageMock) + rpcHistory = RPCHistoryFactory.createForNetwork(keyValueStorage: RuntimeKeyValueStorage()) + service = WalletPairService(networkingInteractor: networkingInteractor, kms: cryptoMock, pairingStorage: storageMock, history: rpcHistory) + } + + func testPairWhenNetworkNotConnectedThrows() async { + let uri = WalletConnectURI.stub() + networkingInteractor.networkConnectionStatusPublisherSubject.send(.notConnected) + await XCTAssertThrowsErrorAsync(try await service.pair(uri)) } - func testPairMultipleTimesOnSameURIThrows() async { + func testPairOnSameUriPresentsRequest() async { + let rpcRequest = RPCRequest(method: "session_propose", id: 1234) + let uri = WalletConnectURI.stub() - for i in 1...10 { - if i == 1 { - await XCTAssertNoThrowAsync(try await service.pair(uri)) - } else { - await XCTAssertThrowsErrorAsync(try await service.pair(uri)) - } - } + try! await service.pair(uri) + var pairing = storageMock.getPairing(forTopic: uri.topic) + pairing?.receivedRequest() + storageMock.setPairing(pairing!) + try! rpcHistory.set(rpcRequest, forTopic: uri.topic, emmitedBy: .local) + + try! await service.pair(uri) + XCTAssertTrue(networkingInteractor.didCallHandleHistoryRequest) } func testPair() async { diff --git a/Tests/WalletConnectSignTests/AppProposalServiceTests.swift b/Tests/WalletConnectSignTests/AppProposalServiceTests.swift index 066473ae9..bdc8c7180 100644 --- a/Tests/WalletConnectSignTests/AppProposalServiceTests.swift +++ b/Tests/WalletConnectSignTests/AppProposalServiceTests.swift @@ -62,6 +62,7 @@ final class AppProposalServiceTests: XCTestCase { approveEngine = ApproveEngine( networkingInteractor: networkingInteractor, proposalPayloadsStore: .init(defaults: RuntimeKeyValueStorage(), identifier: ""), + verifyContextStore: CodableStore(defaults: RuntimeKeyValueStorage(), identifier: ""), sessionTopicToProposal: CodableStore(defaults: RuntimeKeyValueStorage(), identifier: ""), pairingRegisterer: pairingRegisterer, metadata: meta, diff --git a/Tests/WalletConnectSignTests/ApproveEngineTests.swift b/Tests/WalletConnectSignTests/ApproveEngineTests.swift index 4a4108dba..de84c86d2 100644 --- a/Tests/WalletConnectSignTests/ApproveEngineTests.swift +++ b/Tests/WalletConnectSignTests/ApproveEngineTests.swift @@ -18,6 +18,7 @@ final class ApproveEngineTests: XCTestCase { var sessionStorageMock: WCSessionStorageMock! var pairingRegisterer: PairingRegistererMock! var proposalPayloadsStore: CodableStore>! + var verifyContextStore: CodableStore! var sessionTopicToProposal: CodableStore! var publishers = Set() @@ -30,10 +31,12 @@ final class ApproveEngineTests: XCTestCase { sessionStorageMock = WCSessionStorageMock() pairingRegisterer = PairingRegistererMock() proposalPayloadsStore = CodableStore>(defaults: RuntimeKeyValueStorage(), identifier: "") + verifyContextStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: "") sessionTopicToProposal = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: "") engine = ApproveEngine( networkingInteractor: networkingInteractor, proposalPayloadsStore: proposalPayloadsStore, + verifyContextStore: verifyContextStore, sessionTopicToProposal: sessionTopicToProposal, pairingRegisterer: pairingRegisterer, metadata: metadata, @@ -147,4 +150,69 @@ final class ApproveEngineTests: XCTestCase { XCTAssertFalse(cryptoMock.hasAgreementSecret(for: session.topic), "Responder must remove agreement secret") XCTAssertFalse(cryptoMock.hasPrivateKey(for: session.self.publicKey!), "Responder must remove private key") } + + func testVerifyContextStorageAdd() { + let proposalReceivedExpectation = expectation(description: "Wallet expects to receive a proposal") + + let pairing = WCPairing.stub() + let topicA = pairing.topic + pairingStorageMock.setPairing(pairing) + let proposerPubKey = AgreementPrivateKey().publicKey.hexRepresentation + let proposal = SessionProposal.stub(proposerPubKey: proposerPubKey) + + engine.onSessionProposal = { _, _ in + proposalReceivedExpectation.fulfill() + } + pairingRegisterer.subject.send(RequestSubscriptionPayload(id: RPCID("id"), topic: topicA, request: proposal, decryptedPayload: Data(), publishedAt: Date(), derivedTopic: nil)) + + wait(for: [proposalReceivedExpectation], timeout: 0.1) + + XCTAssertTrue(verifyContextStore.getAll().count == 1) + } + + func testVerifyContextStorageRemoveOnApprove() async throws { + let proposalReceivedExpectation = expectation(description: "Wallet expects to receive a proposal") + + let pairing = WCPairing.stub() + let topicA = pairing.topic + pairingStorageMock.setPairing(pairing) + let proposerPubKey = AgreementPrivateKey().publicKey.hexRepresentation + let proposal = SessionProposal.stub(proposerPubKey: proposerPubKey) + + engine.onSessionProposal = { _, _ in + proposalReceivedExpectation.fulfill() + } + pairingRegisterer.subject.send(RequestSubscriptionPayload(id: RPCID("id"), topic: topicA, request: proposal, decryptedPayload: Data(), publishedAt: Date(), derivedTopic: nil)) + + wait(for: [proposalReceivedExpectation], timeout: 0.1) + + XCTAssertTrue(verifyContextStore.getAll().count == 1) + + try await engine.approveProposal(proposerPubKey: proposal.proposer.publicKey, validating: SessionNamespace.stubDictionary()) + + XCTAssertTrue(verifyContextStore.getAll().isEmpty) + } + + func testVerifyContextStorageRemoveOnReject() async throws { + let proposalReceivedExpectation = expectation(description: "Wallet expects to receive a proposal") + + let pairing = WCPairing.stub() + let topicA = pairing.topic + pairingStorageMock.setPairing(pairing) + let proposerPubKey = AgreementPrivateKey().publicKey.hexRepresentation + let proposal = SessionProposal.stub(proposerPubKey: proposerPubKey) + + engine.onSessionProposal = { _, _ in + proposalReceivedExpectation.fulfill() + } + pairingRegisterer.subject.send(RequestSubscriptionPayload(id: RPCID("id"), topic: topicA, request: proposal, decryptedPayload: Data(), publishedAt: Date(), derivedTopic: nil)) + + wait(for: [proposalReceivedExpectation], timeout: 0.1) + + XCTAssertTrue(verifyContextStore.getAll().count == 1) + + try await engine.reject(proposerPubKey: proposal.proposer.publicKey, reason: .userRejected) + + XCTAssertTrue(verifyContextStore.getAll().isEmpty) + } } diff --git a/Tests/WalletConnectSignTests/SessionEngineTests.swift b/Tests/WalletConnectSignTests/SessionEngineTests.swift index c37a9b98b..23f1e420b 100644 --- a/Tests/WalletConnectSignTests/SessionEngineTests.swift +++ b/Tests/WalletConnectSignTests/SessionEngineTests.swift @@ -7,11 +7,15 @@ final class SessionEngineTests: XCTestCase { var networkingInteractor: NetworkingInteractorMock! var sessionStorage: WCSessionStorageMock! + var proposalPayloadsStore: CodableStore>! + var verifyContextStore: CodableStore! var engine: SessionEngine! override func setUp() { networkingInteractor = NetworkingInteractorMock() sessionStorage = WCSessionStorageMock() + proposalPayloadsStore = CodableStore>(defaults: RuntimeKeyValueStorage(), identifier: "") + verifyContextStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: "") engine = SessionEngine( networkingInteractor: networkingInteractor, historyService: HistoryService( @@ -20,8 +24,11 @@ final class SessionEngineTests: XCTestCase { defaults: RuntimeKeyValueStorage(), identifier: "" ) - ) + ), + proposalPayloadsStore: proposalPayloadsStore, + verifyContextStore: verifyContextStore ), + verifyContextStore: verifyContextStore, verifyClient: VerifyClientMock(), kms: KeyManagementServiceMock(), sessionStore: sessionStorage, diff --git a/Tests/WalletConnectUtilsTests/KeyedDatabaseTests.swift b/Tests/WalletConnectUtilsTests/KeyedDatabaseTests.swift new file mode 100644 index 000000000..1cd08b53f --- /dev/null +++ b/Tests/WalletConnectUtilsTests/KeyedDatabaseTests.swift @@ -0,0 +1,50 @@ +import XCTest +import JSONRPC +import TestingUtils +@testable import WalletConnectUtils + +final class KeyedDatabaseTests: XCTestCase { + + struct Object: DatabaseObject { + let key: String + let value: String + + var databaseId: String { + return key + } + } + + let storageKey: String = "storageKey" + + var sut: KeyedDatabase! + + override func setUp() { + sut = KeyedDatabase(storage: RuntimeKeyValueStorage(), identifier: "identifier") + } + + override func tearDown() { + sut = nil + } + + func testIsChanged() throws { + let new = Object(key: "key1", value: "value1") + let updated = Object(key: "key1", value: "value2") + + sut.set(element: new, for: storageKey) + sut.set(element: updated, for: storageKey) + + let value = sut.getElement(for: storageKey, id: updated.databaseId) + + XCTAssertEqual(value, updated) + } + + func testOnUpdate() { + let new = Object(key: "key1", value: "value1") + + var onUpdateCalled = false + sut.onUpdate = { onUpdateCalled = true } + sut.set(element: new, for: storageKey) + + XCTAssertTrue(onUpdateCalled) + } +} diff --git a/Tests/Web3WalletTests/Mocks/AuthClientMock.swift b/Tests/Web3WalletTests/Mocks/AuthClientMock.swift index 302d40a53..a2cde013d 100644 --- a/Tests/Web3WalletTests/Mocks/AuthClientMock.swift +++ b/Tests/Web3WalletTests/Mocks/AuthClientMock.swift @@ -43,7 +43,7 @@ final class AuthClientMock: AuthClientProtocol { rejectCalled = true } - func getPendingRequests() throws -> [AuthRequest] { - return [authRequest] + func getPendingRequests() throws -> [(AuthRequest, VerifyContext?)] { + return [(authRequest, nil)] } } diff --git a/Tests/Web3WalletTests/Mocks/EchoClientMock.swift b/Tests/Web3WalletTests/Mocks/EchoClientMock.swift deleted file mode 100644 index 2a43ee0e6..000000000 --- a/Tests/Web3WalletTests/Mocks/EchoClientMock.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation -import Combine - -@testable import WalletConnectEcho - -final class EchoClientMock: EchoClientProtocol { - var registedCalled = false - - func register(deviceToken: Data) async throws { - registedCalled = true - } - - func register(deviceToken: String) async throws { - registedCalled = true - } -} diff --git a/Tests/Web3WalletTests/Mocks/SignClientMock.swift b/Tests/Web3WalletTests/Mocks/SignClientMock.swift index 721af0e46..3d5cca9fe 100644 --- a/Tests/Web3WalletTests/Mocks/SignClientMock.swift +++ b/Tests/Web3WalletTests/Mocks/SignClientMock.swift @@ -115,12 +115,16 @@ final class SignClientMock: SignClientProtocol { return [WalletConnectSign.Session(topic: "", pairingTopic: "", peer: metadata, requiredNamespaces: [:], namespaces: [:], sessionProperties: nil, expiryDate: Date())] } - func getPendingRequests(topic: String?) -> [WalletConnectSign.Request] { - return [request] + func getPendingProposals(topic: String?) -> [(proposal: WalletConnectSign.Session.Proposal, context: VerifyContext?)] { + return [] } - func getSessionRequestRecord(id: JSONRPC.RPCID) -> WalletConnectSign.Request? { - return request + func getPendingRequests(topic: String?) -> [(request: WalletConnectSign.Request, context: WalletConnectSign.VerifyContext?)] { + return [(request, nil)] + } + + func getSessionRequestRecord(id: JSONRPC.RPCID) -> (request: WalletConnectSign.Request, context: WalletConnectSign.VerifyContext?)? { + return (request, nil) } func cleanup() async throws { diff --git a/Tests/Web3WalletTests/Web3WalletTests.swift b/Tests/Web3WalletTests/Web3WalletTests.swift index dc8489d73..39165751d 100644 --- a/Tests/Web3WalletTests/Web3WalletTests.swift +++ b/Tests/Web3WalletTests/Web3WalletTests.swift @@ -3,13 +3,14 @@ import Combine @testable import Auth @testable import Web3Wallet +@testable import WalletConnectPush final class Web3WalletTests: XCTestCase { var web3WalletClient: Web3WalletClient! var authClient: AuthClientMock! var signClient: SignClientMock! var pairingClient: PairingClientMock! - var echoClient: EchoClientMock! + var pushClient: PushClientMock! private var disposeBag = Set() @@ -17,13 +18,13 @@ final class Web3WalletTests: XCTestCase { authClient = AuthClientMock() signClient = SignClientMock() pairingClient = PairingClientMock() - echoClient = EchoClientMock() + pushClient = PushClientMock() web3WalletClient = Web3WalletClientFactory.create( authClient: authClient, signClient: signClient, pairingClient: pairingClient, - echoClient: echoClient + pushClient: pushClient ) } @@ -267,11 +268,11 @@ final class Web3WalletTests: XCTestCase { XCTAssertEqual(1, web3WalletClient.getPairings().count) } - func testEchoClientRegisterCalled() async { - try! await echoClient.register(deviceToken: Data()) - XCTAssertTrue(echoClient.registedCalled) - echoClient.registedCalled = false - try! await echoClient.register(deviceToken: "") - XCTAssertTrue(echoClient.registedCalled) + func testPushClientRegisterCalled() async { + try! await pushClient.register(deviceToken: Data()) + XCTAssertTrue(pushClient.registedCalled) + pushClient.registedCalled = false + try! await pushClient.register(deviceToken: "") + XCTAssertTrue(pushClient.registedCalled) } } diff --git a/XPlatformProtocolTests.xctestplan b/XPlatformProtocolTests.xctestplan new file mode 100644 index 000000000..a58bda3b5 --- /dev/null +++ b/XPlatformProtocolTests.xctestplan @@ -0,0 +1,40 @@ +{ + "configurations" : [ + { + "id" : "39E198AA-2027-4FAD-8628-1C954F89CC54", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + "environmentVariableEntries" : [ + { + "key" : "JS_CLIENT_API_HOST", + "value" : "$(JS_CLIENT_API_HOST)" + }, + { + "key" : "PROJECT_ID", + "value" : "$(PROJECT_ID)" + }, + { + "key" : "RELAY_HOST", + "value" : "$(RELAY_HOST)" + } + ] + }, + "testTargets" : [ + { + "selectedTests" : [ + "XPlatformW3WTests\/testSessionSettle()" + ], + "target" : { + "containerPath" : "container:ExampleApp.xcodeproj", + "identifier" : "A5E03DEC286464DB00888481", + "name" : "IntegrationTests" + } + } + ], + "version" : 1 +} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 1be80dbf8..ef829c945 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -103,7 +103,7 @@ platform :ios do project: "Example/ExampleApp.xcodeproj", scheme: ENV["SCHEME"], export_method: "app-store", - xcargs: "RELAY_HOST='#{options[:relay_host]}' PROJECT_ID='#{options[:project_id]}'" + xcargs: "RELAY_HOST='#{options[:relay_host]}' PROJECT_ID='#{options[:project_id]}' WALLETAPP_SENTRY_DSN='#{options[:sentry_dsn]}' MIXPANEL_TOKEN='#{options[:mixpanel_token]}'" ) upload_to_testflight( apple_id: ENV["APPLE_ID"], @@ -121,4 +121,4 @@ platform :ios do end -end \ No newline at end of file +end diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 000000000..eb906df0e --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,109 @@ +#!/bin/bash +set -eE +trap 'xcrun simctl delete "$DEVICE_ID"' ERR + +# Parse named arguments +while [[ $# -gt 0 ]]; do + case $1 in + -s|--scheme) SCHEME="$2"; shift;; + -p|--project) PROJECT="$2"; shift;; + -t|--testplan) TESTPLAN="$2"; shift;; + esac + shift +done + +if [ -z "$SCHEME" ]; then + echo "No scheme provided" + exit 1 +fi + +# Function to update xctestrun file +update_xctestrun() { + # Parse named arguments + while [[ $# -gt 0 ]]; do + case $1 in + -k|--key) KEY="$2"; shift;; + -v|--value) VALUE="$2"; shift;; + -t|--target) TARGET="$2"; shift;; + esac + shift + done + + if [ -n "$VALUE" ]; then + echo "Updating $KEY with $VALUE" + plutil -replace TestConfigurations.0.TestTargets.0.EnvironmentVariables.$KEY -string "$VALUE" "$TARGET" + else + echo "No value provided for $KEY" + fi +} + +# Set XCBuild defaults +defaults write com.apple.dt.XCBuild IgnoreFileSystemDeviceInodeChanges -bool YES + +# Remove and recreate test_results directory +echo "Removing and recreating test_results directory" +rm -rf test_results +mkdir test_results + +# Create ephemeral simulator +DEVICE_ID=$(xcrun simctl create "EphemeralSim$SCHEME" "iPhone 14") +echo "Created ephemeral simulator with id: $DEVICE_ID" + +if [ -z "$TESTPLAN" ]; then + XCTESTRUN=$(find . -name "*_$SCHEME*.xctestrun") +else + XCTESTRUN=$(find . -name "*_$TESTPLAN*.xctestrun") +fi + +# If xctestrun file exists, update it and run test-without-building otherwise run regular test +if [ -z "$XCTESTRUN" ]; then + echo "XCTESTRUN file not found" + + ( + set -x + + #If xctestrun file does not exist, run regular test + set -o pipefail && env NSUnbufferedIO=YES \ + xcodebuild \ + ${PROJECT:+-project "$PROJECT"} \ + ${TESTPLAN:+-testPlan "$TESTPLAN"} \ + -scheme "$SCHEME" \ + -destination "platform=iOS Simulator,id=$DEVICE_ID" \ + -derivedDataPath DerivedDataCache \ + -clonedSourcePackagesDirPath ../SourcePackagesCache \ + -resultBundlePath "test_results/$SCHEME.xcresult" \ + test \ + | tee ./test_results/xcodebuild.log \ + | xcbeautify --report junit --junit-report-filename report.junit --report-path ./test_results + ) +else + + echo "XCTESTRUN file found: $XCTESTRUN" + + update_xctestrun --key "RELAY_HOST" --value "$RELAY_HOST" --target "$XCTESTRUN" + update_xctestrun --key "PROJECT_ID" --value "$PROJECT_ID" --target "$XCTESTRUN" + update_xctestrun --key "GM_DAPP_PROJECT_ID" --value "$GM_DAPP_PROJECT_ID" --target "$XCTESTRUN" + update_xctestrun --key "GM_DAPP_PROJECT_SECRET" --value "$GM_DAPP_PROJECT_SECRET" --target "$XCTESTRUN" + update_xctestrun --key "CAST_HOST" --value "$CAST_HOST" --target "$XCTESTRUN" + update_xctestrun --key "JS_CLIENT_API_HOST" --value "$JS_CLIENT_API_HOST" --target "$XCTESTRUN" + + ( + set -x + + set -o pipefail && env NSUnbufferedIO=YES \ + xcodebuild \ + -xctestrun "$XCTESTRUN" \ + -destination "platform=iOS Simulator,id=$DEVICE_ID" \ + -derivedDataPath DerivedDataCache \ + -clonedSourcePackagesDirPath ../SourcePackagesCache \ + -resultBundlePath "test_results/$SCHEME.xcresult" \ + test-without-building \ + | tee ./test_results/xcodebuild.log \ + | xcbeautify --report junit --junit-report-filename report.junit --report-path ./test_results + ) +fi + +echo "Removing ephemeral simulator" +xcrun simctl delete "$DEVICE_ID" + +echo "Done"