diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml
index 0e3715428..caba3649b 100644
--- a/.github/actions/build/action.yml
+++ b/.github/actions/build/action.yml
@@ -13,15 +13,14 @@ runs:
using: "composite"
steps:
- uses: actions/checkout@v3
+ with:
+ ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/cache@v3
with:
path: |
**/SourcePackagesCache
- DerivedDataCache
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
- restore-keys: |
- ${{ runner.os }}-spm-
- name: Build for testing
shell: bash
diff --git a/.github/actions/run_tests_without_building/action.yml b/.github/actions/run_tests_without_building/action.yml
index 4a9b2a44c..f9567cf73 100644
--- a/.github/actions/run_tests_without_building/action.yml
+++ b/.github/actions/run_tests_without_building/action.yml
@@ -11,6 +11,9 @@ inputs:
project-id:
description: 'WalletConnect project id'
required: true
+ slack-webhook-url:
+ description: 'Smoke tests slack webhoook url'
+ required: true
runs:
using: "composite"
@@ -53,13 +56,13 @@ runs:
run: make smoke_tests RELAY_HOST=${{ inputs.relay-endpoint }} PROJECT_ID=${{ inputs.project-id }}
- name: Slack Notification for Failure
- if: failure() && matrix.type == 'smoke-tests'
+ if: failure() && (inputs.type == 'smoke-tests' || inputs.type == 'relay-tests')
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: The smoke tests failed in the CI pipeline. Check the logs for more details.
env:
- SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
+ SLACK_WEBHOOK_URL: ${{ inputs.slack-webhook-url }}
- name: Publish Test Report
uses: mikepenz/action-junit-report@v3
diff --git a/.github/workflows/build_artifacts.yml b/.github/workflows/build_artifacts.yml
index b51a09e9f..cc8adb7a5 100644
--- a/.github/workflows/build_artifacts.yml
+++ b/.github/workflows/build_artifacts.yml
@@ -25,10 +25,7 @@ jobs:
with:
path: |
**/SourcePackagesCache
- DerivedDataCache
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
- restore-keys: |
- ${{ runner.os }}-spm-
- name: Build for testing on workflow_dispatch
if: ${{ github.event_name == 'workflow_dispatch' }}
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index bb5ce36ae..500646255 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -28,4 +28,4 @@ jobs:
- name: Lint CocoaPods
run: |
- pod lib lint WalletConnectSwiftV2.podspec --verbose --allow-warnings
\ No newline at end of file
+ pod lib lint --verbose --no-clean --quick --allow-warnings --platforms=ios WalletConnectSwiftV2.podspec
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c6ced26fd..7acd8274b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -25,6 +25,8 @@ jobs:
runs-on: macos-12
steps:
- uses: actions/checkout@v3
+ with:
+ ref: ${{ github.event.pull_request.head.sha }}
- uses: ./.github/actions/build
with:
@@ -41,6 +43,8 @@ jobs:
steps:
- uses: actions/checkout@v3
+ with:
+ ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/cache/restore@v3
with:
diff --git a/.gitignore b/.gitignore
index 172334855..9ad6aa3e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,7 +10,6 @@ xcuserdata/
# Swift Package Manager
Packages/
Package.pins
-Package.resolved
/*.xcodeproj
# Fastlane
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectHistory.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectHistory.xcscheme
new file mode 100644
index 000000000..4311c49f7
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectHistory.xcscheme
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Example/DApp/SceneDelegate.swift b/Example/DApp/SceneDelegate.swift
index f6b051abc..fc931226d 100644
--- a/Example/DApp/SceneDelegate.swift
+++ b/Example/DApp/SceneDelegate.swift
@@ -2,6 +2,7 @@ import UIKit
import Auth
import WalletConnectRelay
import WalletConnectNetworking
+import Web3Modal
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
@@ -13,6 +14,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
Networking.configure(projectId: InputConfig.projectId, socketFactory: DefaultSocketFactory())
Auth.configure(crypto: DefaultCryptoProvider())
+
+ let metadata = AppMetadata(
+ name: "Swift Dapp",
+ description: "WalletConnect DApp sample",
+ url: "wallet.connect",
+ icons: ["https://avatars.githubusercontent.com/u/37784886"]
+ )
+
+ Web3Modal.configure(projectId: InputConfig.projectId, metadata: metadata)
setupWindow(scene: scene)
}
diff --git a/Example/DApp/Sign/Connect/ConnectViewController.swift b/Example/DApp/Sign/Connect/ConnectViewController.swift
index ccb0d6fa2..094b82b6e 100644
--- a/Example/DApp/Sign/Connect/ConnectViewController.swift
+++ b/Example/DApp/Sign/Connect/ConnectViewController.swift
@@ -1,7 +1,6 @@
import Foundation
import UIKit
-import WalletConnectSign
-import WalletConnectPairing
+import Web3Modal
class ConnectViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let uri: WalletConnectURI
diff --git a/Example/DApp/Sign/SelectChain/SelectChainViewController.swift b/Example/DApp/Sign/SelectChain/SelectChainViewController.swift
index 5c456a453..4bbf7a67e 100644
--- a/Example/DApp/Sign/SelectChain/SelectChainViewController.swift
+++ b/Example/DApp/Sign/SelectChain/SelectChainViewController.swift
@@ -1,6 +1,5 @@
import Foundation
-import WalletConnectSign
-import WalletConnectPairing
+import Web3Modal
import UIKit
import Combine
@@ -70,16 +69,18 @@ class SelectChainViewController: UIViewController, UITableViewDataSource {
let sessionProperties: [String: String] = [
"caip154-mandatory": "true"
]
+
Task {
- let uri = try await Pair.instance.create()
- try await Sign.instance.connect(
+ Web3Modal.set(sessionParams: .init(
requiredNamespaces: namespaces,
optionalNamespaces: optionalNamespaces,
- sessionProperties: sessionProperties,
- topic: uri.topic
- )
- showConnectScreen(uri: uri)
+ sessionProperties: sessionProperties
+ ))
+
+ let uri = try await Web3Modal.instance.connect(topic: nil)
}
+
+ Web3Modal.present(from: self)
}
@objc
diff --git a/Example/ExampleApp.xcodeproj/IntegrationTests.xctestplan b/Example/ExampleApp.xcodeproj/IntegrationTests.xctestplan
index 597f8cc1f..47c6a2314 100644
--- a/Example/ExampleApp.xcodeproj/IntegrationTests.xctestplan
+++ b/Example/ExampleApp.xcodeproj/IntegrationTests.xctestplan
@@ -30,7 +30,8 @@
{
"skippedTests" : [
"AuthTests\/testEIP1271RespondSuccess()",
- "ChatTests"
+ "ChatTests",
+ "ENSResolverTests"
],
"target" : {
"containerPath" : "container:ExampleApp.xcodeproj",
diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj
index 35ab930db..7fcb4ca7e 100644
--- a/Example/ExampleApp.xcodeproj/project.pbxproj
+++ b/Example/ExampleApp.xcodeproj/project.pbxproj
@@ -75,19 +75,28 @@
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 */; };
+ A50DF19D2A25084A0036EA6C /* WalletConnectHistory in Frameworks */ = {isa = PBXBuildFile; productRef = A50DF19C2A25084A0036EA6C /* WalletConnectHistory */; };
A50F3946288005B200064555 /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50F3945288005B200064555 /* Types.swift */; };
+ A51606F82A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51606F72A2F47BD00CACB92 /* DefaultBIP44Provider.swift */; };
+ A51606F92A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51606F72A2F47BD00CACB92 /* DefaultBIP44Provider.swift */; };
+ A51606FA2A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51606F72A2F47BD00CACB92 /* DefaultBIP44Provider.swift */; };
+ A51606FB2A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51606F72A2F47BD00CACB92 /* DefaultBIP44Provider.swift */; };
A518A98729683FB60035247E /* Web3InboxViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A518A98429683FB60035247E /* Web3InboxViewController.swift */; };
A518A98829683FB60035247E /* Web3InboxModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A518A98529683FB60035247E /* Web3InboxModule.swift */; };
A518A98929683FB60035247E /* Web3InboxRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A518A98629683FB60035247E /* Web3InboxRouter.swift */; };
A518B31428E33A6500A2CE93 /* InputConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A518B31328E33A6500A2CE93 /* InputConfig.swift */; };
A51AC0D928E436A3001BACF9 /* InputConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51AC0D828E436A3001BACF9 /* InputConfig.swift */; };
A51AC0DF28E4379F001BACF9 /* InputConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51AC0DE28E4379F001BACF9 /* InputConfig.swift */; };
+ A5321C2B2A250367006CADC3 /* HistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5321C2A2A250367006CADC3 /* HistoryTests.swift */; };
A5417BBE299BFC3E00B469F3 /* ImportAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5417BBD299BFC3E00B469F3 /* ImportAccount.swift */; };
A541959E2934BFEF0035AD19 /* CacaoSignerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A541959A2934BFEF0035AD19 /* CacaoSignerTests.swift */; };
A541959F2934BFEF0035AD19 /* SignerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A541959B2934BFEF0035AD19 /* SignerTests.swift */; };
A54195A02934BFEF0035AD19 /* EIP1271VerifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A541959C2934BFEF0035AD19 /* EIP1271VerifierTests.swift */; };
A54195A12934BFEF0035AD19 /* EIP191VerifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A541959D2934BFEF0035AD19 /* EIP191VerifierTests.swift */; };
A54195A52934E83F0035AD19 /* Web3 in Frameworks */ = {isa = PBXBuildFile; productRef = A54195A42934E83F0035AD19 /* Web3 */; };
+ A561C80029DF32CE00DF540D /* HDWalletKit in Frameworks */ = {isa = PBXBuildFile; productRef = A561C7FF29DF32CE00DF540D /* HDWalletKit */; };
+ A561C80329DFCCDC00DF540D /* SyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A561C80229DFCCDC00DF540D /* SyncTests.swift */; };
+ A561C80529DFCD4500DF540D /* WalletConnectSync in Frameworks */ = {isa = PBXBuildFile; productRef = A561C80429DFCD4500DF540D /* WalletConnectSync */; };
A5629AA92876A23100094373 /* ChatService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5629AA82876A23100094373 /* ChatService.swift */; };
A5629ABD2876CBC000094373 /* ChatListModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5629AB82876CBC000094373 /* ChatListModule.swift */; };
A5629ABE2876CBC000094373 /* ChatListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5629AB92876CBC000094373 /* ChatListPresenter.swift */; };
@@ -108,6 +117,10 @@
A5629AE828772A0100094373 /* InviteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5629AE728772A0100094373 /* InviteViewModel.swift */; };
A5629AEA2877F2D600094373 /* WalletConnectChat in Frameworks */ = {isa = PBXBuildFile; productRef = A5629AE92877F2D600094373 /* WalletConnectChat */; };
A5629AF22877F75100094373 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = A5629AF12877F75100094373 /* Starscream */; };
+ A573C53729EC34A600E3CBFD /* SyncDerivationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A573C53629EC34A600E3CBFD /* SyncDerivationServiceTests.swift */; };
+ A573C53929EC365000E3CBFD /* HDWalletKit in Frameworks */ = {isa = PBXBuildFile; productRef = A573C53829EC365000E3CBFD /* HDWalletKit */; };
+ A573C53B29EC365800E3CBFD /* HDWalletKit in Frameworks */ = {isa = PBXBuildFile; productRef = A573C53A29EC365800E3CBFD /* HDWalletKit */; };
+ A573C53D29EC366500E3CBFD /* HDWalletKit in Frameworks */ = {isa = PBXBuildFile; productRef = A573C53C29EC366500E3CBFD /* HDWalletKit */; };
A578FA322873036400AA7720 /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A578FA312873036400AA7720 /* InputView.swift */; };
A578FA35287304A300AA7720 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = A578FA34287304A300AA7720 /* Color.swift */; };
A578FA372873D8EE00AA7720 /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A578FA362873D8EE00AA7720 /* UIColor.swift */; };
@@ -192,6 +205,7 @@
A5E22D222840C8D300E36487 /* WalletEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E22D212840C8D300E36487 /* WalletEngine.swift */; };
A5E22D242840C8DB00E36487 /* SafariEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E22D232840C8DB00E36487 /* SafariEngine.swift */; };
A5E22D2C2840EAC300E36487 /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E22D2B2840EAC300E36487 /* XCUIElement.swift */; };
+ A5E776BA29F4362D00172091 /* AlertError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E776B929F4362D00172091 /* AlertError.swift */; };
A74D32BA2A1E25AD00CB8536 /* QueryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74D32B92A1E25AD00CB8536 /* QueryParameters.swift */; };
C5133A78294125CC00A8314C /* Web3 in Frameworks */ = {isa = PBXBuildFile; productRef = C5133A77294125CC00A8314C /* Web3 */; };
C53AA4362941251C008EA57C /* DefaultSignerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59CF4F5292F83D50031A42F /* DefaultSignerFactory.swift */; };
@@ -268,6 +282,7 @@
C5F32A322954816C00A6476E /* ConnectionDetailsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F32A312954816C00A6476E /* ConnectionDetailsPresenter.swift */; };
C5F32A342954817600A6476E /* ConnectionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F32A332954817600A6476E /* ConnectionDetailsView.swift */; };
C5F32A362954FE3C00A6476E /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C5F32A352954FE3C00A6476E /* Colors.xcassets */; };
+ CF140F2D2A2A288D00BEB791 /* Web3Modal in Frameworks */ = {isa = PBXBuildFile; productRef = CF140F2C2A2A288D00BEB791 /* Web3Modal */; };
CF1A594529E5876600AAC16B /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF1A593A29E5876600AAC16B /* XCUIElement.swift */; };
CF1A594629E5876600AAC16B /* PushNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF1A593C29E5876600AAC16B /* PushNotificationTests.swift */; };
CF1A594829E5876600AAC16B /* Engine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF1A593F29E5876600AAC16B /* Engine.swift */; };
@@ -406,17 +421,20 @@
A507BE1929E8032E0038EF70 /* EIP55Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EIP55Tests.swift; sourceTree = ""; };
A50C036428AAD32200FE72D3 /* ClientDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClientDelegate.swift; sourceTree = ""; };
A50F3945288005B200064555 /* Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Types.swift; sourceTree = ""; };
+ A51606F72A2F47BD00CACB92 /* DefaultBIP44Provider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultBIP44Provider.swift; sourceTree = ""; };
A518A98429683FB60035247E /* Web3InboxViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Web3InboxViewController.swift; sourceTree = ""; };
A518A98529683FB60035247E /* Web3InboxModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Web3InboxModule.swift; sourceTree = ""; };
A518A98629683FB60035247E /* Web3InboxRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Web3InboxRouter.swift; sourceTree = ""; };
A518B31328E33A6500A2CE93 /* InputConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputConfig.swift; sourceTree = ""; };
A51AC0D828E436A3001BACF9 /* InputConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputConfig.swift; sourceTree = ""; };
A51AC0DE28E4379F001BACF9 /* InputConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputConfig.swift; sourceTree = ""; };
+ A5321C2A2A250367006CADC3 /* HistoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryTests.swift; sourceTree = ""; };
A5417BBD299BFC3E00B469F3 /* ImportAccount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportAccount.swift; sourceTree = ""; };
A541959A2934BFEF0035AD19 /* CacaoSignerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacaoSignerTests.swift; sourceTree = ""; };
A541959B2934BFEF0035AD19 /* SignerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignerTests.swift; sourceTree = ""; };
A541959C2934BFEF0035AD19 /* EIP1271VerifierTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EIP1271VerifierTests.swift; sourceTree = ""; };
A541959D2934BFEF0035AD19 /* EIP191VerifierTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EIP191VerifierTests.swift; sourceTree = ""; };
+ A561C80229DFCCDC00DF540D /* SyncTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncTests.swift; sourceTree = ""; };
A5629AA82876A23100094373 /* ChatService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatService.swift; sourceTree = ""; };
A5629AB82876CBC000094373 /* ChatListModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListModule.swift; sourceTree = ""; };
A5629AB92876CBC000094373 /* ChatListPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListPresenter.swift; sourceTree = ""; };
@@ -436,6 +454,7 @@
A5629AE32876E6D200094373 /* ThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadViewModel.swift; sourceTree = ""; };
A5629AE728772A0100094373 /* InviteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteViewModel.swift; sourceTree = ""; };
A5629AEF2877F73000094373 /* DefaultSocketFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultSocketFactory.swift; sourceTree = ""; };
+ A573C53629EC34A600E3CBFD /* SyncDerivationServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncDerivationServiceTests.swift; sourceTree = ""; };
A578FA312873036400AA7720 /* InputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputView.swift; sourceTree = ""; };
A578FA34287304A300AA7720 /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; };
A578FA362873D8EE00AA7720 /* UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; };
@@ -508,6 +527,7 @@
A5E22D212840C8D300E36487 /* WalletEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletEngine.swift; sourceTree = ""; };
A5E22D232840C8DB00E36487 /* SafariEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariEngine.swift; sourceTree = ""; };
A5E22D2B2840EAC300E36487 /* XCUIElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCUIElement.swift; sourceTree = ""; };
+ A5E776B929F4362D00172091 /* AlertError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertError.swift; sourceTree = ""; };
A5F48A0528E43D3F0034CBFB /* Configuration.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Configuration.xcconfig; path = ../Configuration.xcconfig; sourceTree = ""; };
A74D32B92A1E25AD00CB8536 /* QueryParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryParameters.swift; sourceTree = ""; };
C55D347A295DD7140004314A /* AuthRequestModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthRequestModule.swift; sourceTree = ""; };
@@ -604,7 +624,9 @@
8448F1D427E4726F0000B866 /* WalletConnect in Frameworks */,
A54195A52934E83F0035AD19 /* Web3 in Frameworks */,
84E6B8652981720400428BAF /* WalletConnectPush in Frameworks */,
+ CF140F2D2A2A288D00BEB791 /* Web3Modal in Frameworks */,
A5D85228286333E300DAF5C3 /* Starscream in Frameworks */,
+ A573C53929EC365000E3CBFD /* HDWalletKit in Frameworks */,
A5BB7FA328B6A50400707FC6 /* WalletConnectAuth in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -624,6 +646,7 @@
CF9C7E4A2A01802F0037C006 /* Web3Modal in Frameworks */,
A58EC618299D665A00F3452A /* Web3Inbox in Frameworks */,
A5629AEA2877F2D600094373 /* WalletConnectChat in Frameworks */,
+ A561C80029DF32CE00DF540D /* HDWalletKit in Frameworks */,
A59FAEC928B7B93A002BB66F /* Web3 in Frameworks */,
A5629AF22877F75100094373 /* Starscream in Frameworks */,
A59F877628B5462900A9CD80 /* WalletConnectAuth in Frameworks */,
@@ -643,11 +666,14 @@
buildActionMask = 2147483647;
files = (
A5E03DFF2864662500888481 /* WalletConnect in Frameworks */,
+ A561C80529DFCD4500DF540D /* WalletConnectSync in Frameworks */,
A5E03DF52864651200888481 /* Starscream in Frameworks */,
+ A50DF19D2A25084A0036EA6C /* WalletConnectHistory in Frameworks */,
847CF3AF28E3141700F1D760 /* WalletConnectPush in Frameworks */,
A5C8BE85292FE20B006CC85C /* Web3 in Frameworks */,
84DDB4ED28ABB663003D66ED /* WalletConnectAuth in Frameworks */,
C5DD5BE1294E09E3008FD3A4 /* Web3Wallet in Frameworks */,
+ A573C53B29EC365800E3CBFD /* HDWalletKit in Frameworks */,
A5E03E01286466EA00888481 /* WalletConnectChat in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -656,6 +682,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ A573C53D29EC366500E3CBFD /* HDWalletKit in Frameworks */,
C56EE27D293F56F8004840D1 /* WalletConnectChat in Frameworks */,
C5133A78294125CC00A8314C /* Web3 in Frameworks */,
84536D7429EEBCF0008EA8DB /* Web3Inbox in Frameworks */,
@@ -932,6 +959,14 @@
path = Types;
sourceTree = "";
};
+ A5321C292A25035A006CADC3 /* History */ = {
+ isa = PBXGroup;
+ children = (
+ A5321C2A2A250367006CADC3 /* HistoryTests.swift */,
+ );
+ path = History;
+ sourceTree = "";
+ };
A54195992934BFDD0035AD19 /* Signer */ = {
isa = PBXGroup;
children = (
@@ -944,6 +979,15 @@
path = Signer;
sourceTree = "";
};
+ A561C80129DFCCD300DF540D /* Sync */ = {
+ isa = PBXGroup;
+ children = (
+ A561C80229DFCCDC00DF540D /* SyncTests.swift */,
+ A573C53629EC34A600E3CBFD /* SyncDerivationServiceTests.swift */,
+ );
+ path = Sync;
+ sourceTree = "";
+ };
A5629AA42876A19D00094373 /* DomainLayer */ = {
isa = PBXGroup;
children = (
@@ -995,6 +1039,7 @@
isa = PBXGroup;
children = (
A5629AE32876E6D200094373 /* ThreadViewModel.swift */,
+ A5E776B929F4362D00172091 /* AlertError.swift */,
);
path = Models;
sourceTree = "";
@@ -1260,6 +1305,7 @@
A5629AEF2877F73000094373 /* DefaultSocketFactory.swift */,
A59CF4F5292F83D50031A42F /* DefaultSignerFactory.swift */,
A5A0843B29D2F60A000B9B17 /* DefaultCryptoProvider.swift */,
+ A51606F72A2F47BD00CACB92 /* DefaultBIP44Provider.swift */,
);
path = Shared;
sourceTree = "";
@@ -1342,6 +1388,8 @@
A5E03DEE286464DB00888481 /* IntegrationTests */ = {
isa = PBXGroup;
children = (
+ A5321C292A25035A006CADC3 /* History */,
+ A561C80129DFCCD300DF540D /* Sync */,
849D7A91292E2115006A2BD4 /* Push */,
84CEC64728D8A98900D081A8 /* Pairing */,
A5E03E0A28646A8A00888481 /* Stubs */,
@@ -1730,6 +1778,8 @@
A5BB7FA228B6A50400707FC6 /* WalletConnectAuth */,
A54195A42934E83F0035AD19 /* Web3 */,
84E6B8642981720400428BAF /* WalletConnectPush */,
+ CF140F2C2A2A288D00BEB791 /* Web3Modal */,
+ A573C53829EC365000E3CBFD /* HDWalletKit */,
);
productName = DApp;
productReference = 84CE641C27981DED00142511 /* DApp.app */;
@@ -1776,6 +1826,7 @@
A58EC610299D57B800F3452A /* AsyncButton */,
A58EC617299D665A00F3452A /* Web3Inbox */,
CF9C7E492A01802F0037C006 /* Web3Modal */,
+ A561C7FF29DF32CE00DF540D /* HDWalletKit */,
);
productName = Showcase;
productReference = A58E7CE828729F550082D443 /* Showcase.app */;
@@ -1820,6 +1871,9 @@
847CF3AE28E3141700F1D760 /* WalletConnectPush */,
A5C8BE84292FE20B006CC85C /* Web3 */,
C5DD5BE0294E09E3008FD3A4 /* Web3Wallet */,
+ A561C80429DFCD4500DF540D /* WalletConnectSync */,
+ A573C53A29EC365800E3CBFD /* HDWalletKit */,
+ A50DF19C2A25084A0036EA6C /* WalletConnectHistory */,
);
productName = IntegrationTests;
productReference = A5E03DED286464DB00888481 /* IntegrationTests.xctest */;
@@ -1849,6 +1903,7 @@
C5B2F7042970573D000DBA0E /* SolanaSwift */,
84E6B85329787AAE00428BAF /* WalletConnectPush */,
84536D7329EEBCF0008EA8DB /* Web3Inbox */,
+ A573C53C29EC366500E3CBFD /* HDWalletKit */,
);
productName = ChatWallet;
productReference = C56EE21B293F55ED004840D1 /* WalletApp.app */;
@@ -1923,6 +1978,7 @@
A5AE354528A1A2AC0059AE8A /* XCRemoteSwiftPackageReference "Web3" */,
A5434021291E6A270068F706 /* XCRemoteSwiftPackageReference "solana-swift" */,
A58EC60F299D57B800F3452A /* XCRemoteSwiftPackageReference "swiftui-async-button" */,
+ A561C7FE29DF32CE00DF540D /* XCRemoteSwiftPackageReference "HDWallet" */,
);
productRefGroup = 764E1D3D26F8D3FC00A1FB15 /* Products */;
projectDirPath = "";
@@ -2039,6 +2095,7 @@
84CE644E279ED2FF00142511 /* SelectChainView.swift in Sources */,
84CE644B279EA1FA00142511 /* AccountRequestView.swift in Sources */,
84CE6431279820F600142511 /* AccountsView.swift in Sources */,
+ A51606F82A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */,
84CE643D2798322600142511 /* ConnectViewController.swift in Sources */,
84CE6444279AB5AD00142511 /* SelectChainViewController.swift in Sources */,
);
@@ -2083,8 +2140,10 @@
A58E7D3D2872D55F0082D443 /* ChatView.swift in Sources */,
A5629ABD2876CBC000094373 /* ChatListModule.swift in Sources */,
A58E7CEB28729F550082D443 /* AppDelegate.swift in Sources */,
+ A51606FA2A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */,
A578FA35287304A300AA7720 /* Color.swift in Sources */,
A5629ADE2876CC6E00094373 /* InviteListModule.swift in Sources */,
+ A5E776BA29F4362D00172091 /* AlertError.swift in Sources */,
A578FA322873036400AA7720 /* InputView.swift in Sources */,
A5A0843F29D2F625000B9B17 /* DefaultCryptoProvider.swift in Sources */,
A5C2021B287E1FD8007E3188 /* ImportRouter.swift in Sources */,
@@ -2149,8 +2208,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ A51606F92A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */,
+ A573C53729EC34A600E3CBFD /* SyncDerivationServiceTests.swift in Sources */,
A5A0843E29D2F624000B9B17 /* DefaultCryptoProvider.swift in Sources */,
84CEC64628D89D6B00D081A8 /* PairingTests.swift in Sources */,
+ A561C80329DFCCDC00DF540D /* SyncTests.swift in Sources */,
767DC83528997F8E00080FA9 /* EthSendTransaction.swift in Sources */,
8439CB89293F658E00F2F2E2 /* PushMessage.swift in Sources */,
A518B31428E33A6500A2CE93 /* InputConfig.swift in Sources */,
@@ -2164,6 +2226,7 @@
7694A5262874296A0001257E /* RegistryTests.swift in Sources */,
A541959F2934BFEF0035AD19 /* SignerTests.swift in Sources */,
A50C036528AAD32200FE72D3 /* ClientDelegate.swift in Sources */,
+ A5321C2B2A250367006CADC3 /* HistoryTests.swift in Sources */,
A58A1ECC29BF458600A82A20 /* ENSResolverTests.swift in Sources */,
A5E03DFA286465C700888481 /* SignClientTests.swift in Sources */,
A54195A02934BFEF0035AD19 /* EIP1271VerifierTests.swift in Sources */,
@@ -2198,6 +2261,7 @@
C56EE241293F566D004840D1 /* WalletModule.swift in Sources */,
C56EE245293F566D004840D1 /* WalletPresenter.swift in Sources */,
C56EE240293F566D004840D1 /* ScanQRView.swift in Sources */,
+ A51606FB2A2F47BD00CACB92 /* DefaultBIP44Provider.swift in Sources */,
C56EE250293F566D004840D1 /* ScanTargetView.swift in Sources */,
C56EE28F293F5757004840D1 /* MigrationConfigurator.swift in Sources */,
84B815552991217900FAD54E /* PushMessagesPresenter.swift in Sources */,
@@ -2989,6 +3053,14 @@
kind = branch;
};
};
+ A561C7FE29DF32CE00DF540D /* XCRemoteSwiftPackageReference "HDWallet" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/WalletConnect/HDWallet";
+ requirement = {
+ branch = develop;
+ kind = branch;
+ };
+ };
A58EC60F299D57B800F3452A /* XCRemoteSwiftPackageReference "swiftui-async-button" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/lorenzofiamingo/swiftui-async-button";
@@ -3053,11 +3125,24 @@
isa = XCSwiftPackageProductDependency;
productName = WalletConnectPush;
};
+ A50DF19C2A25084A0036EA6C /* WalletConnectHistory */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = WalletConnectHistory;
+ };
A54195A42934E83F0035AD19 /* Web3 */ = {
isa = XCSwiftPackageProductDependency;
package = A5AE354528A1A2AC0059AE8A /* XCRemoteSwiftPackageReference "Web3" */;
productName = Web3;
};
+ A561C7FF29DF32CE00DF540D /* HDWalletKit */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = A561C7FE29DF32CE00DF540D /* XCRemoteSwiftPackageReference "HDWallet" */;
+ productName = HDWalletKit;
+ };
+ A561C80429DFCD4500DF540D /* WalletConnectSync */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = WalletConnectSync;
+ };
A5629AE92877F2D600094373 /* WalletConnectChat */ = {
isa = XCSwiftPackageProductDependency;
productName = WalletConnectChat;
@@ -3067,6 +3152,21 @@
package = A5D85224286333D500DAF5C3 /* XCRemoteSwiftPackageReference "Starscream" */;
productName = Starscream;
};
+ A573C53829EC365000E3CBFD /* HDWalletKit */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = A561C7FE29DF32CE00DF540D /* XCRemoteSwiftPackageReference "HDWallet" */;
+ productName = HDWalletKit;
+ };
+ A573C53A29EC365800E3CBFD /* HDWalletKit */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = A561C7FE29DF32CE00DF540D /* XCRemoteSwiftPackageReference "HDWallet" */;
+ productName = HDWalletKit;
+ };
+ A573C53C29EC366500E3CBFD /* HDWalletKit */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = A561C7FE29DF32CE00DF540D /* XCRemoteSwiftPackageReference "HDWallet" */;
+ productName = HDWalletKit;
+ };
A58EC610299D57B800F3452A /* AsyncButton */ = {
isa = XCSwiftPackageProductDependency;
package = A58EC60F299D57B800F3452A /* XCRemoteSwiftPackageReference "swiftui-async-button" */;
@@ -3143,6 +3243,10 @@
isa = XCSwiftPackageProductDependency;
productName = Web3Wallet;
};
+ CF140F2C2A2A288D00BEB791 /* Web3Modal */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = Web3Modal;
+ };
CF9C7E492A01802F0037C006 /* Web3Modal */ = {
isa = XCSwiftPackageProductDependency;
productName = Web3Modal;
diff --git a/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 000000000..b9a5ff3f7
--- /dev/null
+++ b/Example/ExampleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,133 @@
+{
+ "object": {
+ "pins": [
+ {
+ "package": "BigInt",
+ "repositoryURL": "https://github.com/attaswift/BigInt.git",
+ "state": {
+ "branch": null,
+ "revision": "0ed110f7555c34ff468e72e1686e59721f2b0da6",
+ "version": "5.3.0"
+ }
+ },
+ {
+ "package": "CryptoSwift",
+ "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git",
+ "state": {
+ "branch": null,
+ "revision": "32f641cf24fc7abc1c591a2025e9f2f572648b0f",
+ "version": "1.7.2"
+ }
+ },
+ {
+ "package": "HDWalletKit",
+ "repositoryURL": "https://github.com/WalletConnect/HDWallet",
+ "state": {
+ "branch": "develop",
+ "revision": "748a85b1dfe9a2fa592bd9266c5a926e4e1d3f44",
+ "version": null
+ }
+ },
+ {
+ "package": "PromiseKit",
+ "repositoryURL": "https://github.com/mxcl/PromiseKit.git",
+ "state": {
+ "branch": null,
+ "revision": "8a98e31a47854d3180882c8068cc4d9381bf382d",
+ "version": "6.22.1"
+ }
+ },
+ {
+ "package": "QRCode",
+ "repositoryURL": "https://github.com/WalletConnect/QRCode",
+ "state": {
+ "branch": null,
+ "revision": "263f280d2c8144adfb0b6676109846cfc8dd552b",
+ "version": "14.3.1"
+ }
+ },
+ {
+ "package": "secp256k1",
+ "repositoryURL": "https://github.com/Boilertalk/secp256k1.swift.git",
+ "state": {
+ "branch": null,
+ "revision": "cd187c632fb812fd93711a9f7e644adb7e5f97f0",
+ "version": "0.1.7"
+ }
+ },
+ {
+ "package": "SolanaSwift",
+ "repositoryURL": "https://github.com/flypaper0/solana-swift",
+ "state": {
+ "branch": "feature/available-13",
+ "revision": "a98811518e0a90c2dfc60c30cfd3ec85c33b6790",
+ "version": null
+ }
+ },
+ {
+ "package": "Starscream",
+ "repositoryURL": "https://github.com/daltoniam/Starscream",
+ "state": {
+ "branch": null,
+ "revision": "a063fda2b8145a231953c20e7a646be254365396",
+ "version": "3.1.2"
+ }
+ },
+ {
+ "package": "swift-qrcode-generator",
+ "repositoryURL": "https://github.com/dagronf/swift-qrcode-generator",
+ "state": {
+ "branch": null,
+ "revision": "5ca09b6a2ad190f94aa3d6ddef45b187f8c0343b",
+ "version": "1.0.3"
+ }
+ },
+ {
+ "package": "SwiftImageReadWrite",
+ "repositoryURL": "https://github.com/dagronf/SwiftImageReadWrite",
+ "state": {
+ "branch": null,
+ "revision": "5596407d1cf61b953b8e658fa8636a471df3c509",
+ "version": "1.1.6"
+ }
+ },
+ {
+ "package": "swiftui-async-button",
+ "repositoryURL": "https://github.com/lorenzofiamingo/swiftui-async-button",
+ "state": {
+ "branch": null,
+ "revision": "9fe9ccddf59c7e4185aa978547fbb9d95236455e",
+ "version": "1.1.0"
+ }
+ },
+ {
+ "package": "Task_retrying",
+ "repositoryURL": "https://github.com/bigearsenal/task-retrying-swift.git",
+ "state": {
+ "branch": null,
+ "revision": "1249b3524378423c848cef39fb220041e00a08ec",
+ "version": "1.0.4"
+ }
+ },
+ {
+ "package": "TweetNacl",
+ "repositoryURL": "https://github.com/bitmark-inc/tweetnacl-swiftwrap.git",
+ "state": {
+ "branch": null,
+ "revision": "f8fd111642bf2336b11ef9ea828510693106e954",
+ "version": "1.1.0"
+ }
+ },
+ {
+ "package": "Web3",
+ "repositoryURL": "https://github.com/WalletConnect/Web3.swift",
+ "state": {
+ "branch": null,
+ "revision": "569255adcfff0b37e4cb8004aea29d0e2d6266df",
+ "version": "1.0.2"
+ }
+ }
+ ]
+ },
+ "version": 1
+}
diff --git a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectSync.xcscheme b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectSync.xcscheme
new file mode 100644
index 000000000..4b58c89d1
--- /dev/null
+++ b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/WalletConnectSync.xcscheme
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Example/IntegrationTests/Auth/ENS/ENSResolverTests.swift b/Example/IntegrationTests/Auth/ENS/ENSResolverTests.swift
index df4a26f3e..8ea3661e8 100644
--- a/Example/IntegrationTests/Auth/ENS/ENSResolverTests.swift
+++ b/Example/IntegrationTests/Auth/ENS/ENSResolverTests.swift
@@ -7,15 +7,16 @@ class ENSResolverTests: XCTestCase {
private let account = Account("eip155:1:0xD02D090F8f99B61D65d8e8876Ea86c2720aB27BC")!
private let ens = "web3.eth"
- 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 2c917aa35..c2aaab6c5 100644
--- a/Example/IntegrationTests/Chat/ChatTests.swift
+++ b/Example/IntegrationTests/Chat/ChatTests.swift
@@ -3,25 +3,46 @@ import XCTest
@testable import WalletConnectChat
import WalletConnectUtils
@testable import WalletConnectKMS
+@testable import WalletConnectSync
import WalletConnectRelay
import Combine
+import Web3
final class ChatTests: XCTestCase {
- var invitee: ChatClient!
- var inviter: ChatClient!
+ var invitee1: ChatClient!
+ var inviter1: ChatClient!
+ var invitee2: ChatClient!
+ var inviter2: ChatClient!
private var publishers = [AnyCancellable]()
- let inviteeAccount = Account("eip155:1:0x15bca56b6e2728aec2532df9d436bd1600e86688")!
- let inviterAccount = Account("eip155:2:0x15bca56b6e2728aec2532df9d436bd1600e86688")!
+ var inviteeAccount: Account {
+ return Account("eip155:1:" + pk1.address.hex(eip55: true))!
+ }
+
+ var inviterAccount: Account {
+ return Account("eip155:1:" + pk2.address.hex(eip55: true))!
+ }
- let privateKey = Data(hex: "305c6cde3846927892cd32762f6120539f3ec74c9e3a16b9b798b1e85351ae2a")
+ let pk1 = try! EthereumPrivateKey()
+ let pk2 = try! EthereumPrivateKey()
- override func setUp() async throws {
- invitee = makeClient(prefix: "🦖 Invitee", account: inviteeAccount)
- inviter = makeClient(prefix: "🍄 Inviter", account: inviterAccount)
+ var privateKey1: Data {
+ return Data(pk1.rawPrivateKey)
+ }
+ var privateKey2: Data {
+ return Data(pk2.rawPrivateKey)
+ }
- try await invitee.register(account: inviteeAccount, onSign: sign)
- try await inviter.register(account: inviterAccount, onSign: sign)
+ override func setUp() async throws {
+ invitee1 = makeClient(prefix: "🦖 Invitee", account: inviteeAccount)
+ inviter1 = makeClient(prefix: "🍄 Inviter", account: inviterAccount)
+
+ try await invitee1.register(account: inviteeAccount) { message in
+ return self.sign(message, privateKey: self.privateKey1)
+ }
+ try await inviter1.register(account: inviterAccount) { message in
+ return self.sign(message, privateKey: self.privateKey2)
+ }
}
func makeClient(prefix: String, account: Account) -> ChatClient {
@@ -36,27 +57,33 @@ final class ChatTests: XCTestCase {
keychainStorage: keychain,
keyValueStorage: keyValueStorage)
+ let syncClient = SyncClientFactory.create(
+ networkInteractor: networkingInteractor,
+ bip44: DefaultBIP44Provider(),
+ keychain: keychain
+ )
+
let clientId = try! networkingInteractor.getClientId()
logger.debug("My client id is: \(clientId)")
- return ChatClientFactory.create(keyserverURL: keyserverURL, relayClient: relayClient, networkingInteractor: networkingInteractor, keychain: keychain, logger: logger, keyValueStorage: keyValueStorage)
+ return ChatClientFactory.create(keyserverURL: keyserverURL, relayClient: relayClient, networkingInteractor: networkingInteractor, keychain: keychain, logger: logger, storage: keyValueStorage, syncClient: syncClient)
}
func testInvite() async throws {
let inviteExpectation = expectation(description: "invitation expectation")
inviteExpectation.expectedFulfillmentCount = 2
- invitee.newReceivedInvitePublisher.sink { _ in
+ invitee1.newReceivedInvitePublisher.sink { _ in
inviteExpectation.fulfill()
}.store(in: &publishers)
- inviter.newSentInvitePublisher.sink { _ in
+ inviter1.newSentInvitePublisher.sink { _ in
inviteExpectation.fulfill()
}.store(in: &publishers)
- let inviteePublicKey = try await inviter.resolve(account: inviteeAccount)
+ let inviteePublicKey = try await inviter1.resolve(account: inviteeAccount)
let invite = Invite(message: "", inviterAccount: inviterAccount, inviteeAccount: inviteeAccount, inviteePublicKey: inviteePublicKey)
- _ = try await inviter.invite(invite: invite)
+ _ = try await inviter1.invite(invite: invite)
wait(for: [inviteExpectation], timeout: InputConfig.defaultTimeout)
}
@@ -65,21 +92,21 @@ final class ChatTests: XCTestCase {
let newThreadInviterExpectation = expectation(description: "new thread on inviting client expectation")
let newThreadinviteeExpectation = expectation(description: "new thread on invitee client expectation")
- invitee.newReceivedInvitePublisher.sink { [unowned self] invite in
- Task { try! await invitee.accept(inviteId: invite.id) }
+ invitee1.newReceivedInvitePublisher.sink { [unowned self] invite in
+ Task { try! await invitee1.accept(inviteId: invite.id) }
}.store(in: &publishers)
- invitee.newThreadPublisher.sink { _ in
+ invitee1.newThreadPublisher.sink { _ in
newThreadinviteeExpectation.fulfill()
}.store(in: &publishers)
- inviter.newThreadPublisher.sink { _ in
+ inviter1.newThreadPublisher.sink { _ in
newThreadInviterExpectation.fulfill()
}.store(in: &publishers)
- let inviteePublicKey = try await inviter.resolve(account: inviteeAccount)
+ let inviteePublicKey = try await inviter1.resolve(account: inviteeAccount)
let invite = Invite(message: "", inviterAccount: inviterAccount, inviteeAccount: inviteeAccount, inviteePublicKey: inviteePublicKey)
- try await inviter.invite(invite: invite)
+ try await inviter1.invite(invite: invite)
wait(for: [newThreadinviteeExpectation, newThreadInviterExpectation], timeout: InputConfig.defaultTimeout)
}
@@ -88,19 +115,19 @@ final class ChatTests: XCTestCase {
let messageExpectation = expectation(description: "message received")
messageExpectation.expectedFulfillmentCount = 4
- invitee.newReceivedInvitePublisher.sink { [unowned self] invite in
- Task { try! await invitee.accept(inviteId: invite.id) }
+ invitee1.newReceivedInvitePublisher.sink { [unowned self] invite in
+ Task { try! await invitee1.accept(inviteId: invite.id) }
}.store(in: &publishers)
- invitee.newThreadPublisher.sink { [unowned self] thread in
- Task { try! await invitee.message(topic: thread.topic, message: "message1") }
+ invitee1.newThreadPublisher.sink { [unowned self] thread in
+ Task { try! await invitee1.message(topic: thread.topic, message: "message1") }
}.store(in: &publishers)
- inviter.newThreadPublisher.sink { [unowned self] thread in
- Task { try! await inviter.message(topic: thread.topic, message: "message2") }
+ inviter1.newThreadPublisher.sink { [unowned self] thread in
+ Task { try! await inviter1.message(topic: thread.topic, message: "message2") }
}.store(in: &publishers)
- inviter.newMessagePublisher.sink { message in
+ inviter1.newMessagePublisher.sink { message in
if message.authorAccount == self.inviterAccount {
XCTAssertEqual(message.message, "message2")
} else {
@@ -109,7 +136,7 @@ final class ChatTests: XCTestCase {
messageExpectation.fulfill()
}.store(in: &publishers)
- invitee.newMessagePublisher.sink { message in
+ invitee1.newMessagePublisher.sink { message in
if message.authorAccount == self.inviteeAccount {
XCTAssertEqual(message.message, "message1")
} else {
@@ -118,15 +145,14 @@ final class ChatTests: XCTestCase {
messageExpectation.fulfill()
}.store(in: &publishers)
- let inviteePublicKey = try await inviter.resolve(account: inviteeAccount)
+ let inviteePublicKey = try await inviter1.resolve(account: inviteeAccount)
let invite = Invite(message: "", inviterAccount: inviterAccount, inviteeAccount: inviteeAccount, inviteePublicKey: inviteePublicKey)
- try await inviter.invite(invite: invite)
+ try await inviter1.invite(invite: invite)
wait(for: [messageExpectation], timeout: InputConfig.defaultTimeout)
}
- private func sign(_ message: String) -> SigningResult {
- let privateKey = Data(hex: "305c6cde3846927892cd32762f6120539f3ec74c9e3a16b9b798b1e85351ae2a")
+ private func sign(_ message: String, privateKey: Data) -> 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/History/HistoryTests.swift b/Example/IntegrationTests/History/HistoryTests.swift
new file mode 100644
index 000000000..ebd8f202e
--- /dev/null
+++ b/Example/IntegrationTests/History/HistoryTests.swift
@@ -0,0 +1,79 @@
+import Foundation
+import Combine
+import XCTest
+@testable import WalletConnectHistory
+
+final class HistoryTests: XCTestCase {
+
+ var publishers = Set()
+
+ let relayUrl = "wss://relay.walletconnect.com"
+ let historyUrl = "https://history.walletconnect.com"
+
+ var relayClient1: RelayClient!
+ var relayClient2: RelayClient!
+
+ var historyClient: HistoryNetworkService!
+
+ override func setUp() {
+ let keychain1 = KeychainStorageMock()
+ let keychain2 = KeychainStorageMock()
+ relayClient1 = makeRelayClient(prefix: "🐄", keychain: keychain1)
+ relayClient2 = makeRelayClient(prefix: "🐫", keychain: keychain2)
+ historyClient = makeHistoryClient(keychain: keychain1)
+ }
+
+ private func makeRelayClient(prefix: String, keychain: KeychainStorageProtocol) -> RelayClient {
+ return RelayClient(
+ relayHost: InputConfig.relayHost,
+ projectId: InputConfig.projectId,
+ keyValueStorage: RuntimeKeyValueStorage(),
+ keychainStorage: keychain,
+ socketFactory: DefaultSocketFactory(),
+ logger: ConsoleLogger(suffix: prefix + " [Relay]", loggingLevel: .debug))
+ }
+
+ private func makeHistoryClient(keychain: KeychainStorageProtocol) -> HistoryNetworkService {
+ let clientIdStorage = ClientIdStorage(keychain: keychain)
+ return HistoryNetworkService(clientIdStorage: clientIdStorage)
+ }
+
+ func testRegister() async throws {
+ let payload = RegisterPayload(tags: ["7000"], relayUrl: relayUrl)
+
+ try await historyClient.registerTags(payload: payload, historyUrl: historyUrl)
+ }
+
+ func testGetMessages() async throws {
+ let exp = expectation(description: "Test Get Messages")
+ let tag = 7000
+ let payload = "{}"
+ let agreement = AgreementPrivateKey()
+ let topic = agreement.publicKey.rawRepresentation.sha256().hex
+
+ relayClient2.messagePublisher.sink { (topic, message, publishedAt) in
+ exp.fulfill()
+ }.store(in: &publishers)
+
+ try await historyClient.registerTags(
+ payload: RegisterPayload(tags: [String(tag)], relayUrl: relayUrl),
+ historyUrl: historyUrl)
+
+ try await relayClient2.subscribe(topic: topic)
+ try await relayClient1.publish(topic: topic, payload: payload, tag: tag, prompt: false, ttl: 3000)
+
+ wait(for: [exp], timeout: InputConfig.defaultTimeout)
+
+ sleep(5) // History server has a queue
+
+ let messages = try await historyClient.getMessages(
+ payload: GetMessagesPayload(
+ topic: topic,
+ originId: nil,
+ messageCount: 200,
+ direction: .forward),
+ historyUrl: historyUrl)
+
+ XCTAssertEqual(messages.messages, [payload])
+ }
+}
diff --git a/Example/IntegrationTests/Sync/SyncDerivationServiceTests.swift b/Example/IntegrationTests/Sync/SyncDerivationServiceTests.swift
new file mode 100644
index 000000000..549ed6929
--- /dev/null
+++ b/Example/IntegrationTests/Sync/SyncDerivationServiceTests.swift
@@ -0,0 +1,26 @@
+import Foundation
+import XCTest
+@testable import WalletConnectSync
+@testable import WalletConnectSigner
+
+class SyncDerivationServiceTests: XCTestCase {
+
+ func testDerivation() throws {
+ let account = Account("eip155:1:0x1FF34C90a0850Fe7227fcFA642688b9712477482")!
+ let signature = "0xc91265eadb1473d90f8d49d31b7016feb7f7761a2a986ca2146a4b8964f3357569869680154927596a5829ceea925f4196b8a853a29c2c1d5915832fc9f1c6a01c"
+ let keychain = KeychainStorageMock()
+ let syncStorage = SyncSignatureStore(keychain: keychain)
+ let kms = KeyManagementService(keychain: keychain)
+ let derivationService = SyncDerivationService(
+ syncStorage: syncStorage,
+ bip44: DefaultBIP44Provider(),
+ kms: kms
+ )
+
+ try syncStorage.saveSignature(signature, for: account)
+
+ let topic = try derivationService.deriveTopic(account: account, store: "my-user-profile")
+
+ XCTAssertEqual(topic, "741f8902d339c4c16f33fa598a6598b63e5ed125d761374511b2e06562b033eb")
+ }
+}
diff --git a/Example/IntegrationTests/Sync/SyncTests.swift b/Example/IntegrationTests/Sync/SyncTests.swift
new file mode 100644
index 000000000..595052dd0
--- /dev/null
+++ b/Example/IntegrationTests/Sync/SyncTests.swift
@@ -0,0 +1,138 @@
+import Foundation
+import Combine
+import XCTest
+import Web3
+@testable import WalletConnectSync
+@testable import WalletConnectSigner
+
+final class SyncTests: XCTestCase {
+
+ struct TestObject: DatabaseObject {
+ let id: String
+ let value: String
+
+ var databaseId: String {
+ return id
+ }
+ }
+
+ var publishers = Set()
+
+ var client1: SyncClient!
+ var client2: SyncClient!
+
+ var indexStore1: SyncIndexStore!
+ var indexStore2: SyncIndexStore!
+
+ var syncStore1: SyncStore!
+ var syncStore2: SyncStore!
+
+ var signer: MessageSigner!
+
+ let storeName = "SyncTests_store"
+
+ var account: Account {
+ return Account("eip155:1:" + pk.address.hex(eip55: true))!
+ }
+
+ let pk = try! EthereumPrivateKey()
+
+ var privateKey: Data {
+ return Data(pk.rawPrivateKey)
+ }
+
+ override func setUp() async throws {
+ indexStore1 = makeIndexStore()
+ indexStore2 = makeIndexStore()
+ client1 = makeClient(indexStore: indexStore1, suffix: "❤️")
+ client2 = makeClient(indexStore: indexStore2, suffix: "💜")
+ syncStore1 = makeSyncStore(client: client1, indexStore: indexStore1)
+ syncStore2 = makeSyncStore(client: client2, indexStore: indexStore2)
+ signer = MessageSignerFactory(signerFactory: DefaultSignerFactory()).create(projectId: InputConfig.projectId)
+ }
+
+ func makeClient(indexStore: SyncIndexStore, suffix: String) -> SyncClient {
+ let syncSignatureStore = SyncSignatureStore(keychain: KeychainStorageMock())
+ 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 = RelayClient(relayHost: InputConfig.relayHost, projectId: InputConfig.projectId, keychainStorage: keychain, socketFactory: DefaultSocketFactory(), logger: logger)
+ let networkingInteractor = NetworkingClientFactory.create(
+ relayClient: relayClient,
+ logger: logger,
+ keychainStorage: keychain,
+ keyValueStorage: RuntimeKeyValueStorage())
+ let historyStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: "historyStore")
+ let syncHistoryStore = SyncHistoryStore(store: historyStore)
+ let syncService = SyncService(networkInteractor: networkingInteractor, derivationService: derivationService, signatureStore: syncSignatureStore, indexStore: indexStore, historyStore: syncHistoryStore, logger: logger)
+ return SyncClient(syncService: syncService, syncSignatureStore: syncSignatureStore)
+ }
+
+ func makeIndexStore() -> SyncIndexStore {
+ let store = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: "indexStore")
+ return SyncIndexStore(store: store)
+ }
+
+ func makeSyncStore(client: SyncClient, indexStore: SyncIndexStore) -> SyncStore {
+ let objectStore = KeyedDatabase(storage: RuntimeKeyValueStorage(), identifier: "objectStore")
+ return SyncStore(name: storeName, syncClient: client, indexStore: indexStore, objectStore: objectStore)
+ }
+
+ func testSync() async throws {
+ let setExpectation = expectation(description: "syncSetTest")
+ let delExpectation = expectation(description: "syncDelTest")
+
+ let object = TestObject(id: "id-1", value: "value-1")
+
+ syncStore1.syncUpdatePublisher.sink { (_, _, update) in
+ switch update {
+ case .set:
+ XCTFail()
+ case .delete:
+ delExpectation.fulfill()
+ }
+ }.store(in: &publishers)
+
+ syncStore2.syncUpdatePublisher.sink { (_, _, update) in
+ switch update {
+ case .set:
+ setExpectation.fulfill()
+ case .delete:
+ XCTFail()
+ }
+ }.store(in: &publishers)
+
+ // Configure clients
+
+ try await registerClient(client: client1)
+ try await registerClient(client: client2)
+
+ // Testing SyncStore `set`
+
+ try await syncStore1.set(object: object, for: account)
+
+ wait(for: [setExpectation], timeout: InputConfig.defaultTimeout)
+
+ XCTAssertEqual(try syncStore1.getAll(for: account), [object])
+ XCTAssertEqual(try syncStore2.getAll(for: account), [object])
+
+ // Testing SyncStore `delete`
+
+ try await syncStore2.delete(id: object.id, for: account)
+
+ wait(for: [delExpectation], timeout: InputConfig.defaultTimeout)
+
+ XCTAssertEqual(try syncStore1.getAll(for: account), [])
+ XCTAssertEqual(try syncStore2.getAll(for: account), [])
+ }
+
+ private func registerClient(client: SyncClient) async throws {
+ let message = client.getMessage(account: account)
+
+ let signature = try signer.sign(message: message, privateKey: privateKey, type: .eip191)
+
+ try await client.register(account: account, signature: signature)
+ try await client.create(account: account, store: storeName)
+ }
+}
diff --git a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift
index 129ebb8a0..fd273e35a 100644
--- a/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift
+++ b/Example/RelayIntegrationTests/RelayClientEndToEndTests.swift
@@ -32,9 +32,9 @@ final class RelayClientEndToEndTests: XCTestCase {
func makeRelayClient(prefix: String) -> RelayClient {
let clientIdStorage = ClientIdStorage(keychain: KeychainStorageMock())
- let socketAuthenticator = SocketAuthenticator(
+ let socketAuthenticator = ClientIdAuthenticator(
clientIdStorage: clientIdStorage,
- relayHost: InputConfig.relayHost
+ url: InputConfig.relayUrl
)
let urlFactory = RelayUrlFactory(
relayHost: InputConfig.relayHost,
@@ -101,18 +101,23 @@ final class RelayClientEndToEndTests: XCTestCase {
relayB.messagePublisher.sink { topic, payload, _ in
(subscriptionBTopic, subscriptionBPayload) = (topic, payload)
+ Task(priority: .high) {
+ sleep(1)
+ try await relayB.publish(topic: randomTopic, payload: payloadB, tag: 0, prompt: false, ttl: 60)
+ }
expectationB.fulfill()
}.store(in: &publishers)
- relayA.socketConnectionStatusPublisher.sink { _ in
+ relayA.socketConnectionStatusPublisher.sink { status in
+ guard status == .connected else {return}
Task(priority: .high) {
- try await relayA.publish(topic: randomTopic, payload: payloadA, tag: 0, prompt: false, ttl: 60)
try await relayA.subscribe(topic: randomTopic)
+ try await relayA.publish(topic: randomTopic, payload: payloadA, tag: 0, prompt: false, ttl: 60)
}
}.store(in: &publishers)
- relayB.socketConnectionStatusPublisher.sink { _ in
+ relayB.socketConnectionStatusPublisher.sink { status in
+ guard status == .connected else {return}
Task(priority: .high) {
- try await relayB.publish(topic: randomTopic, payload: payloadB, tag: 0, prompt: false, ttl: 60)
try await relayB.subscribe(topic: randomTopic)
}
}.store(in: &publishers)
diff --git a/Example/Shared/DefaultBIP44Provider.swift b/Example/Shared/DefaultBIP44Provider.swift
new file mode 100644
index 000000000..5aed02650
--- /dev/null
+++ b/Example/Shared/DefaultBIP44Provider.swift
@@ -0,0 +1,25 @@
+import Foundation
+import Auth
+import Web3
+import CryptoSwift
+import HDWalletKit
+
+struct DefaultBIP44Provider: BIP44Provider {
+
+ 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/DefaultCryptoProvider.swift b/Example/Shared/DefaultCryptoProvider.swift
index 79a11fe2c..4905855f7 100644
--- a/Example/Shared/DefaultCryptoProvider.swift
+++ b/Example/Shared/DefaultCryptoProvider.swift
@@ -2,6 +2,7 @@ import Foundation
import Auth
import Web3
import CryptoSwift
+import HDWalletKit
struct DefaultCryptoProvider: CryptoProvider {
@@ -20,4 +21,21 @@ struct DefaultCryptoProvider: CryptoProvider {
let hash = digest.calculate(for: [UInt8](data))
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 0a71a0bed..8a19855af 100644
--- a/Example/Shared/Tests/InputConfig.swift
+++ b/Example/Shared/Tests/InputConfig.swift
@@ -6,6 +6,10 @@ struct InputConfig {
return config(for: "RELAY_HOST")!
}
+ static var relayUrl: String {
+ return "wss://\(relayHost)"
+ }
+
static var projectId: String {
return config(for: "PROJECT_ID")!
}
diff --git a/Example/Showcase/Classes/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift b/Example/Showcase/Classes/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift
index fbd9ee1a7..c7734a2f1 100644
--- a/Example/Showcase/Classes/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift
+++ b/Example/Showcase/Classes/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift
@@ -1,19 +1,21 @@
import WalletConnectNetworking
import WalletConnectPairing
import Auth
+import Web3Modal
struct ThirdPartyConfigurator: Configurator {
func configure() {
+
+ let metadata = AppMetadata(
+ name: "Showcase App",
+ description: "Showcase description",
+ url: "example.wallet",
+ icons: ["https://avatars.githubusercontent.com/u/37784886"]
+ )
+
Networking.configure(projectId: InputConfig.projectId, socketFactory: DefaultSocketFactory())
- Pair.configure(
- metadata: AppMetadata(
- name: "Showcase App",
- description: "Showcase description",
- url: "example.wallet",
- icons: ["https://avatars.githubusercontent.com/u/37784886"]
- ))
-
Auth.configure(crypto: DefaultCryptoProvider())
+ Web3Modal.configure(projectId: InputConfig.projectId, metadata: metadata)
}
}
diff --git a/Example/Showcase/Classes/DomainLayer/AccountStorage/AccountStorage.swift b/Example/Showcase/Classes/DomainLayer/AccountStorage/AccountStorage.swift
index c8bfc44db..fafa8bc8b 100644
--- a/Example/Showcase/Classes/DomainLayer/AccountStorage/AccountStorage.swift
+++ b/Example/Showcase/Classes/DomainLayer/AccountStorage/AccountStorage.swift
@@ -24,15 +24,3 @@ final class AccountStorage {
}
}
}
-
-private extension ImportAccount {
-
- var storageId: String {
- switch self {
- case .swift, .kotlin, .js:
- return name
- case .custom(let privateKey):
- return privateKey
- }
- }
-}
diff --git a/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift b/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift
index a55dc13b7..799726841 100644
--- a/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift
+++ b/Example/Showcase/Classes/DomainLayer/Chat/ChatService.swift
@@ -2,13 +2,14 @@ import Foundation
import Combine
import WalletConnectChat
import WalletConnectRelay
+import WalletConnectSign
typealias Stream = AnyPublisher
final class ChatService {
- private lazy var client: ChatClient = {
- Chat.configure()
+ private var client: ChatClient = {
+ Chat.configure(bip44: DefaultBIP44Provider())
return Chat.instance
}()
@@ -66,7 +67,7 @@ final class ChatService {
}
func setupSubscriptions(account: Account) {
- client.setupSubscriptions(account: account)
+ try! client.setupSubscriptions(account: account)
}
func sendMessage(topic: String, message: String) async throws {
@@ -81,7 +82,7 @@ final class ChatService {
try await client.reject(inviteId: invite.id)
}
- func goPublic(account: Account, privateKey: String) async throws {
+ func goPublic(account: Account) async throws {
try await client.goPublic(account: account)
}
@@ -91,17 +92,15 @@ final class ChatService {
try await client.invite(invite: invite)
}
- func register(account: Account, privateKey: String) async throws {
+ func register(account: Account, importAccount: ImportAccount) async throws {
_ = try await client.register(account: account) { message in
- let signature = self.onSign(message: message, privateKey: privateKey)
- return SigningResult.signed(signature)
+ return await self.onSign(message: message, importAccount: importAccount)
}
}
- func unregister(account: Account, privateKey: String) async throws {
+ func unregister(account: Account, importAccount: ImportAccount) async throws {
try await client.unregister(account: account) { message in
- let signature = self.onSign(message: message, privateKey: privateKey)
- return SigningResult.signed(signature)
+ return await self.onSign(message: message, importAccount: importAccount)
}
}
@@ -116,9 +115,71 @@ final class ChatService {
private extension ChatService {
+ func onSign(message: String, importAccount: ImportAccount) async -> SigningResult {
+ switch importAccount {
+ case .swift, .kotlin, .js, .custom:
+ return .signed(onSign(message: message, privateKey: importAccount.privateKey))
+ case .web3Modal(let account, let topic):
+ return await onWeb3ModalSign(message: message, account: account, topic: topic)
+ }
+ }
+
func onSign(message: String, privateKey: String) -> CacaoSignature {
let privateKey = Data(hex: privateKey)
let signer = MessageSignerFactory(signerFactory: DefaultSignerFactory()).create()
return try! signer.sign(message: message, privateKey: privateKey, type: .eip191)
}
+
+ func onWeb3ModalSign(message: String, account: Account, topic: String) async -> SigningResult {
+ guard let session = Sign.instance.getSessions().first(where: { $0.topic == topic }) else { return .rejected }
+
+ do {
+ let request = makeRequest(session: session, message: message, account: account)
+ try await Sign.instance.request(params: request)
+
+ let signature: CacaoSignature = try await withCheckedThrowingContinuation { continuation in
+ var cancellable: AnyCancellable?
+ cancellable = Sign.instance.sessionResponsePublisher
+ .sink { response in
+ defer { cancellable?.cancel() }
+ switch response.result {
+ case .response(let value):
+ do {
+ let string = try value.get(String.self)
+ let signature = CacaoSignature(t: .eip191, s: string.deleting0x())
+ continuation.resume(returning: signature)
+ } catch {
+ continuation.resume(throwing: error)
+ }
+ case .error(let error):
+ continuation.resume(throwing: error)
+ }
+ }
+ }
+
+ return .signed(signature)
+ } catch {
+ return .rejected
+ }
+ }
+
+ func makeRequest(session: WalletConnectSign.Session, message: String, account: Account) -> Request {
+ return Request(
+ topic: session.topic,
+ method: "personal_sign",
+ params: AnyCodable(["0x" + message.data(using: .utf8)!.toHexString(), account.address]),
+ chainId: Blockchain("eip155:1")!
+ )
+ }
+}
+
+fileprivate extension String {
+
+ func deleting0x() -> String {
+ var string = self
+ if starts(with: "0x") {
+ string.removeFirst(2)
+ }
+ return string
+ }
}
diff --git a/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift b/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift
index 2c8ccffa0..508b70a69 100644
--- a/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift
+++ b/Example/Showcase/Classes/DomainLayer/Chat/ImportAccount.swift
@@ -1,68 +1,98 @@
import Foundation
import Web3
+import WalletConnectSign
-enum ImportAccount {
+enum ImportAccount: Codable {
case swift
case kotlin
case js
case custom(privateKey: String)
+ case web3Modal(account: Account, topic: String)
+
+ static let swiftId = "swift.eth"
+ static let kotlinId = "kotlin.eth"
+ static let jsId = "js.eth"
+ static let privateKeyId = "privateKey"
+ static let web3ModalId = "web3Modal"
init?(input: String) {
switch input.lowercased() {
- case ImportAccount.swift.name:
+ case ImportAccount.swiftId:
self = .swift
- case ImportAccount.kotlin.name:
+ case ImportAccount.kotlinId:
self = .kotlin
- case ImportAccount.js.name:
+ case ImportAccount.jsId:
self = .js
default:
- if let _ = try? EthereumPrivateKey(hexPrivateKey: "0x" + input, ctx: nil) {
- self = .custom(privateKey: input)
- } else if let _ = try? EthereumPrivateKey(hexPrivateKey: input, ctx: nil) {
- self = .custom(privateKey: input.replacingOccurrences(of: "0x", with: ""))
- } else {
+ switch true {
+ case input.starts(with: ImportAccount.privateKeyId):
+ if let _ = try? EthereumPrivateKey(hexPrivateKey: "0x" + input, ctx: nil) {
+ self = .custom(privateKey: input)
+ } else if let _ = try? EthereumPrivateKey(hexPrivateKey: input, ctx: nil) {
+ self = .custom(privateKey: input.replacingOccurrences(of: "0x", with: ""))
+ } else {
+ return nil
+ }
+ case input.starts(with: ImportAccount.web3ModalId):
+ let components = input.components(separatedBy: "-")
+ guard components.count == 3, let account = Account(components[1]) else {
+ return nil
+ }
+ self = .web3Modal(account: account, topic: components[2])
+ default:
return nil
}
}
}
- var name: String {
+ var storageId: String {
switch self {
case .swift:
- return "swift.eth"
+ return ImportAccount.swiftId
case .kotlin:
- return "kotlin.eth"
+ return ImportAccount.kotlinId
case .js:
- return "js.eth"
- case .custom:
- return account.address
+ return ImportAccount.jsId
+ case .custom(let privateKey):
+ return "\(ImportAccount.privateKeyId)-\(privateKey)"
+ case .web3Modal(let account, let topic):
+ return "\(ImportAccount.web3ModalId)-\(account.absoluteString)-\(topic)"
}
}
var account: Account {
switch self {
case .swift:
- return Account("eip155:1:0x1AAe9864337E821f2F86b5D27468C59AA333C877")!
+ return Account("eip155:1:0x5F847B18b4a2Dd0F428796E89CaEe71480a2a98e")!
case .kotlin:
- return Account("eip155:1:0x4c0fb06CD854ab7D5909E830a5f49D184EB41BF5")!
+ return Account("eip155:1:0xC313B6F74FcB89147e751220184F0C56D37a210e")!
case .js:
- return Account("eip155:1:0x7ABa5B1F436e42f6d4A579FB3Ad6D204F6A91863")!
+ return Account("eip155:1:0x265F4Eb49ab95ED142C4995EF8B5FC9e57538836")!
case .custom(let privateKey):
- let address = try! EthereumPrivateKey(hexPrivateKey: "0x" + privateKey, ctx: nil).address.hex(eip55: false)
+ let address = try! EthereumPrivateKey(hexPrivateKey: "0x" + privateKey, ctx: nil).address.hex(eip55: true)
return Account("eip155:1:\(address)")!
+ case .web3Modal(let account, _):
+ return account
}
}
var privateKey: String {
switch self {
case .swift:
- return "4dc0055d1831f7df8d855fc8cd9118f4a85ddc05395104c4cb0831a6752621a8"
+ return "85f52ec43821c1e2e24a248ee464e8d3f883e460acb0506e1eb6b520eb67ae15"
case .kotlin:
- return "ebe738a76b9a3b7457c3d5eca8d3d9ea6909bc563e05b6e0c5c35448f93100a0"
+ return "646a0ebac6bd34ba5f498b809148b2aca3793374cafe9dc417cf63bea80450bf"
case .js:
- return "de15cb11963e9bde0a5cce06a5ee2bda1cf3a67be6fbcd7a4fc8c0e4c4db0298"
+ return "8df6b8206eebcd3da89b750f1cf9bba887630c3c5eade83f44c06fa4f7cc5f65"
case .custom(let privateKey):
return privateKey
+ case .web3Modal:
+ fatalError("Private key not available")
}
}
+
+ static func new() -> ImportAccount {
+ let key = try! EthereumPrivateKey()
+ return ImportAccount.custom(privateKey: key.rawPrivateKey.toHexString())
+ }
}
diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListInteractor.swift
index 758b03fb7..0ddb01a9c 100644
--- a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListInteractor.swift
+++ b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListInteractor.swift
@@ -5,6 +5,10 @@ final class ChatListInteractor {
private let chatService: ChatService
private let accountStorage: AccountStorage
+ var account: Account? {
+ return accountStorage.importAccount?.account
+ }
+
init(chatService: ChatService, accountStorage: AccountStorage) {
self.chatService = chatService
self.accountStorage = accountStorage
@@ -41,7 +45,7 @@ final class ChatListInteractor {
func logout() async throws {
guard let importAccount = accountStorage.importAccount else { return }
try await chatService.goPrivate(account: importAccount.account)
- try await chatService.unregister(account: importAccount.account, privateKey: importAccount.privateKey)
+ try await chatService.unregister(account: importAccount.account, importAccount: importAccount)
accountStorage.importAccount = nil
}
}
diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListPresenter.swift
index e44e516b8..56a3a0f9c 100644
--- a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListPresenter.swift
+++ b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListPresenter.swift
@@ -21,14 +21,12 @@ final class ChatListPresenter: ObservableObject {
var receivedInviteViewModels: [InviteViewModel] {
return receivedInvites
- .filter { $0.status == .pending }
.sorted(by: { $0.timestamp < $1.timestamp })
.map { InviteViewModel(invite: $0) }
}
var sentInviteViewModels: [InviteViewModel] {
return sentInvites
- .filter { $0.status == .pending }
.sorted(by: { $0.timestamp < $1.timestamp })
.map { InviteViewModel(invite: $0) }
}
@@ -66,6 +64,14 @@ final class ChatListPresenter: ObservableObject {
router.presentWelcome()
}
+ @MainActor
+ func didCopyPress() async throws {
+ guard let account = interactor.account else { return }
+ UIPasteboard.general.string = account.absoluteString
+
+ throw AlertError(message: "Account copied to clipboard")
+ }
+
func didPressNewChat() {
presentInvite()
}
diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListView.swift b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListView.swift
index 3dc52a15f..979d1fe6d 100644
--- a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListView.swift
+++ b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListView.swift
@@ -36,6 +36,14 @@ struct ChatListView: View {
}
}
+ PlainButton {
+ try await presenter.didCopyPress()
+ } label: {
+ Text("Copy account")
+ .foregroundColor(.white)
+ }
+ .padding(.bottom, 16)
+
PlainButton {
try await presenter.didLogoutPress()
} label: {
diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/Models/AlertError.swift b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/Models/AlertError.swift
new file mode 100644
index 000000000..c7cc4f270
--- /dev/null
+++ b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/Models/AlertError.swift
@@ -0,0 +1,9 @@
+import Foundation
+
+struct AlertError: Error, LocalizedError {
+ let message: String
+
+ var errorDescription: String? {
+ return message
+ }
+}
diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportInteractor.swift
index 95c1cf133..c412afe30 100644
--- a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportInteractor.swift
+++ b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportInteractor.swift
@@ -13,6 +13,6 @@ final class ImportInteractor {
}
func register(importAccount: ImportAccount) async throws {
- try await chatService.register(account: importAccount.account, privateKey: importAccount.privateKey)
+ try await chatService.register(account: importAccount.account, importAccount: importAccount)
}
}
diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift
index 9987116c2..6146979f3 100644
--- a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift
+++ b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift
@@ -1,5 +1,6 @@
import UIKit
import Combine
+import WalletConnectSign
final class ImportPresenter: ObservableObject {
@@ -18,16 +19,33 @@ final class ImportPresenter: ObservableObject {
@MainActor
func didPressWeb3Modal() async throws {
router.presentWeb3Modal()
+
+ let session: Session = try await withCheckedThrowingContinuation { continuation in
+ var cancellable: AnyCancellable?
+ cancellable = Sign.instance.sessionSettlePublisher.sink { session in
+ defer { cancellable?.cancel() }
+ return continuation.resume(returning: session)
+ }
+ }
+
+ guard let account = session.accounts.first(where: { $0.blockchain.absoluteString == "eip155:1" }) else {
+ throw AlertError(message: "Тo matching accounts found in namespaces")
+ }
+
+ try await importAccount(.web3Modal(account: account, topic: session.topic))
}
@MainActor
func didPressImport() async throws {
- guard let importAccount = ImportAccount(input: input)
+ guard let account = ImportAccount(input: input)
else { return input = .empty }
+ try await importAccount(account)
+ }
- interactor.save(importAccount: importAccount)
- try await interactor.register(importAccount: importAccount)
- router.presentChat(importAccount: importAccount)
+
+ func didPressRandom() async throws {
+ let account = ImportAccount.new()
+ try await importAccount(account)
}
}
@@ -49,6 +67,18 @@ extension ImportPresenter: SceneViewModel {
private extension ImportPresenter {
func setupInitialState() {
+ Sign.instance.sessionSettlePublisher.sink { session in
+ Task(priority: .userInitiated) {
+ try await self.importAccount(.web3Modal(account: session.accounts.first!, topic: session.topic))
+ }
+
+ }.store(in: &disposeBag)
+ }
+ @MainActor
+ func importAccount(_ importAccount: ImportAccount) async throws {
+ try! await interactor.register(importAccount: importAccount)
+ interactor.save(importAccount: importAccount)
+ router.presentChat(importAccount: importAccount)
}
}
diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportRouter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportRouter.swift
index 512dc95da..57b880cfc 100644
--- a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportRouter.swift
+++ b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportRouter.swift
@@ -13,16 +13,7 @@ final class ImportRouter {
}
func presentWeb3Modal() {
- Web3ModalSheetController(
- projectId: InputConfig.projectId,
- metadata: AppMetadata(
- name: "Showcase App",
- description: "Showcase description",
- url: "example.wallet",
- icons: ["https://avatars.githubusercontent.com/u/37784886"]
- ),
- webSocketFactory: DefaultSocketFactory()
- ).present(from: viewController)
+ Web3ModalSheetController().present(from: viewController)
}
func presentChat(importAccount: ImportAccount) {
diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift
index 9de69d9f3..93954d6b5 100644
--- a/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift
+++ b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift
@@ -26,6 +26,14 @@ struct ImportView: View {
}
}
.padding(16.0)
+
+ PlainButton {
+ try await presenter.didPressRandom()
+ } label: {
+ Text("Create new account")
+ .foregroundColor(.white)
+ }
+ .padding(.bottom, 16)
}
}
}
diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeInteractor.swift
index 318dce292..cb76537c5 100644
--- a/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeInteractor.swift
+++ b/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeInteractor.swift
@@ -34,7 +34,7 @@ final class WelcomeInteractor {
func goPublic() async throws {
guard let importAccount = importAccount else { return }
- try await chatService.goPublic(account: importAccount.account, privateKey: importAccount.privateKey)
+ try await chatService.goPublic(account: importAccount.account)
}
}
diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomePresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomePresenter.swift
index db4e676f4..2cf58ec21 100644
--- a/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomePresenter.swift
+++ b/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomePresenter.swift
@@ -33,6 +33,8 @@ final class WelcomePresenter: ObservableObject {
private extension WelcomePresenter {
func setupInitialState() {
-
+ interactor.trackConnection().sink { status in
+ print("Socket connection status: \(status)")
+ }.store(in: &disposeBag)
}
}
diff --git a/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift b/Example/Showcase/Classes/PresentationLayer/Web3Inbox/Web3InboxViewController.swift
index 05aef8a73..4a7a87c8f 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, config: [.pushEnabled: false], onSign: onSing, environment: .sandbox)
+ Web3Inbox.configure(account: importAccount.account, bip44: DefaultBIP44Provider(), config: [.pushEnabled: false], environment: .sandbox, onSign: onSing)
edgesForExtendedLayout = []
navigationItem.title = "Web3Inbox SDK"
diff --git a/Example/SmokeTests.xctestplan b/Example/SmokeTests.xctestplan
index 894a5d133..06e6e3467 100644
--- a/Example/SmokeTests.xctestplan
+++ b/Example/SmokeTests.xctestplan
@@ -44,6 +44,7 @@
"EIP191VerifierTests",
"EIP55Tests",
"ENSResolverTests",
+ "HistoryTests",
"PairingTests",
"PushTests",
"PushTests\/testDappDeletePushSubscription()",
@@ -70,7 +71,9 @@
"SignClientTests\/testSessionRequestFailureResponse()",
"SignClientTests\/testSuccessfulSessionExtend()",
"SignClientTests\/testSuccessfulSessionUpdateNamespaces()",
- "SignerTest"
+ "SignerTest",
+ "SyncDerivationServiceTests",
+ "SyncTests"
],
"target" : {
"containerPath" : "container:ExampleApp.xcodeproj",
diff --git a/Example/WalletApp/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift b/Example/WalletApp/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift
index 58064d376..d90554a5d 100644
--- a/Example/WalletApp/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift
+++ b/Example/WalletApp/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift
@@ -19,7 +19,13 @@ struct ThirdPartyConfigurator: Configurator {
let account = Account(blockchain: Blockchain("eip155:1")!, address: EthKeyStore.shared.address)!
- Web3Inbox.configure(account: account, config: [.chatEnabled: false, .settingsEnabled: false], onSign: Web3InboxSigner.onSing, environment: BuildConfiguration.shared.apnsEnvironment)
+ Web3Inbox.configure(
+ account: account,
+ bip44: DefaultBIP44Provider(),
+ config: [.chatEnabled: false, .settingsEnabled: false],
+ environment: BuildConfiguration.shared.apnsEnvironment,
+ onSign: Web3InboxSigner.onSing
+ )
}
}
diff --git a/Example/WalletApp/ApplicationLayer/SceneDelegate.swift b/Example/WalletApp/ApplicationLayer/SceneDelegate.swift
index d6831aeb1..9744d8fa3 100644
--- a/Example/WalletApp/ApplicationLayer/SceneDelegate.swift
+++ b/Example/WalletApp/ApplicationLayer/SceneDelegate.swift
@@ -1,12 +1,10 @@
-import UIKit
import Auth
+import UIKit
import WalletConnectPairing
-
final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
-
private let app = Application()
private var configurators: [Configurator] {
@@ -17,19 +15,19 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
AppearanceConfigurator()
]
}
-
+
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
- let sceneConfig: UISceneConfiguration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
- sceneConfig.delegateClass = SceneDelegate.self
- return sceneConfig
- }
+ let sceneConfig = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
+ sceneConfig.delegateClass = SceneDelegate.self
+ return sceneConfig
+ }
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
window?.makeKeyAndVisible()
-
+
app.uri = connectionOptions.urlContexts.first?.url.absoluteString.replacingOccurrences(of: "walletapp://wc?uri=", with: "")
configurators.configure()
diff --git a/Example/WalletApp/Common/Helpers/QueryParameters.swift b/Example/WalletApp/Common/Helpers/QueryParameters.swift
index 8e6c0c493..50527700a 100644
--- a/Example/WalletApp/Common/Helpers/QueryParameters.swift
+++ b/Example/WalletApp/Common/Helpers/QueryParameters.swift
@@ -1,20 +1,14 @@
-//
-// QueryParameters.swift
-// WalletApp
-//
-// Created by Aleksandr Maltsev on 24.05.2023.
-//
import Foundation
extension URL {
- var queryParameters: [AnyHashable: Any] {
- let urlComponents = URLComponents(url: self, resolvingAgainstBaseURL: false)
- guard let queryItems = urlComponents?.queryItems else { return [:] }
- var queryParams: [AnyHashable: Any] = [:]
- queryItems.forEach {
- queryParams[$0.name] = $0.value
- }
- return queryParams
- }
+ var queryParameters: [AnyHashable: Any] {
+ let urlComponents = URLComponents(url: self, resolvingAgainstBaseURL: false)
+ guard let queryItems = urlComponents?.queryItems else { return [:] }
+ var queryParams: [AnyHashable: Any] = [:]
+ queryItems.forEach {
+ queryParams[$0.name] = $0.value
+ }
+ return queryParams
+ }
}
diff --git a/Package.swift b/Package.swift
index e1db6a6fd..9a61e0040 100644
--- a/Package.swift
+++ b/Package.swift
@@ -37,9 +37,15 @@ let package = Package(
.library(
name: "WalletConnectNetworking",
targets: ["WalletConnectNetworking"]),
+ .library(
+ name: "WalletConnectSync",
+ targets: ["WalletConnectSync"]),
.library(
name: "WalletConnectVerify",
targets: ["WalletConnectVerify"]),
+ .library(
+ name: "WalletConnectHistory",
+ targets: ["WalletConnectHistory"]),
.library(
name: "Web3Inbox",
targets: ["Web3Inbox"]),
@@ -58,7 +64,7 @@ let package = Package(
path: "Sources/WalletConnectSign"),
.target(
name: "WalletConnectChat",
- dependencies: ["WalletConnectIdentity", "WalletConnectSigner"],
+ dependencies: ["WalletConnectIdentity", "WalletConnectSync", "WalletConnectHistory"],
path: "Sources/Chat"),
.target(
name: "Auth",
@@ -88,6 +94,9 @@ let package = Package(
.target(
name: "WalletConnectPairing",
dependencies: ["WalletConnectNetworking"]),
+ .target(
+ name: "WalletConnectHistory",
+ dependencies: ["HTTPClient", "WalletConnectRelay"]),
.target(
name: "Web3Inbox",
dependencies: ["WalletConnectChat", "WalletConnectPush"]),
@@ -124,6 +133,9 @@ let package = Package(
.target(
name: "Web3Modal",
dependencies: ["QRCode", "WalletConnectSign"]),
+ .target(
+ name: "WalletConnectSync",
+ dependencies: ["WalletConnectSigner"]),
.testTarget(
name: "WalletConnectSignTests",
dependencies: ["WalletConnectSign", "WalletConnectUtils", "TestingUtils", "WalletConnectVerify"]),
diff --git a/Sources/Chat/Chat.swift b/Sources/Chat/Chat.swift
index 9937d51ba..003b64335 100644
--- a/Sources/Chat/Chat.swift
+++ b/Sources/Chat/Chat.swift
@@ -11,7 +11,8 @@ public class Chat {
return ChatClientFactory.create(
keyserverUrl: keyserverUrl,
relayClient: Relay.instance,
- networkingInteractor: Networking.interactor
+ networkingInteractor: Networking.interactor,
+ syncClient: Sync.instance
)
}()
@@ -22,7 +23,12 @@ public class Chat {
/// Chat instance config method
/// - Parameters:
/// - account: Chat initial account
- static public func configure(keyserverUrl: String = "https://keys.walletconnect.com") {
+ /// - crypto: Crypto utils implementation
+ static public func configure(
+ keyserverUrl: String = "https://keys.walletconnect.com",
+ bip44: BIP44Provider
+ ) {
Chat.keyserverUrl = keyserverUrl
+ Sync.configure(bip44: bip44)
}
}
diff --git a/Sources/Chat/ChatClient.swift b/Sources/Chat/ChatClient.swift
index 543bfbe1d..7d0f8ed5a 100644
--- a/Sources/Chat/ChatClient.swift
+++ b/Sources/Chat/ChatClient.swift
@@ -11,6 +11,7 @@ public class ChatClient {
private let leaveService: LeaveService
private let kms: KeyManagementService
private let chatStorage: ChatStorage
+ private let syncRegisterService: SyncRegisterService
public let socketConnectionStatusPublisher: AnyPublisher
@@ -64,6 +65,7 @@ public class ChatClient {
leaveService: LeaveService,
kms: KeyManagementService,
chatStorage: ChatStorage,
+ syncRegisterService: SyncRegisterService,
socketConnectionStatusPublisher: AnyPublisher
) {
self.identityClient = identityClient
@@ -74,6 +76,7 @@ public class ChatClient {
self.leaveService = leaveService
self.kms = kms
self.chatStorage = chatStorage
+ self.syncRegisterService = syncRegisterService
self.socketConnectionStatusPublisher = socketConnectionStatusPublisher
}
@@ -90,6 +93,11 @@ public class ChatClient {
) async throws -> String {
let publicKey = try await identityClient.register(account: account, onSign: onSign)
+ if !syncRegisterService.isRegistered(account: account) {
+ try await chatStorage.initializeHistory(account: account)
+ try await syncRegisterService.register(account: account, onSign: onSign)
+ }
+
guard !isPrivate else {
return publicKey
}
@@ -128,6 +136,7 @@ public class ChatClient {
public func goPrivate(account: Account) async throws {
let inviteKey = try await identityClient.goPrivate(account: account)
resubscriptionService.unsubscribeFromInvites(inviteKey: inviteKey)
+ try await chatStorage.removeInviteKey(inviteKey, account: account)
}
/// Registers an invite key if not yet registered on this client from keyserver
@@ -137,6 +146,9 @@ public class ChatClient {
public func goPublic(account: Account) async throws {
let inviteKey = try await identityClient.goPublic(account: account)
try await resubscriptionService.subscribeForInvites(inviteKey: inviteKey)
+ try await chatStorage.initializeStores(for: account)
+ try await chatStorage.initializeDelegates()
+ try await chatStorage.setInviteKey(inviteKey, account: account)
}
/// Accepts a chat invite by id from account specified as inviteeAccount in Invite
@@ -189,7 +201,7 @@ public class ChatClient {
return chatStorage.getMessages(topic: topic)
}
- public func setupSubscriptions(account: Account) {
- chatStorage.setupSubscriptions(account: account)
+ public func setupSubscriptions(account: Account) throws {
+ try chatStorage.setupSubscriptions(account: account)
}
}
diff --git a/Sources/Chat/ChatClientFactory.swift b/Sources/Chat/ChatClientFactory.swift
index a36b2cac5..c304954c0 100644
--- a/Sources/Chat/ChatClientFactory.swift
+++ b/Sources/Chat/ChatClientFactory.swift
@@ -2,7 +2,7 @@ import Foundation
public struct ChatClientFactory {
- static func create(keyserverUrl: String, relayClient: RelayClient, networkingInteractor: NetworkingInteractor) -> ChatClient {
+ static func create(keyserverUrl: String, relayClient: RelayClient, networkingInteractor: NetworkingInteractor, syncClient: SyncClient) -> ChatClient {
let keychain = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk")
let keyserverURL = URL(string: keyserverUrl)!
return ChatClientFactory.create(
@@ -11,7 +11,8 @@ public struct ChatClientFactory {
networkingInteractor: networkingInteractor,
keychain: keychain,
logger: ConsoleLogger(loggingLevel: .debug),
- keyValueStorage: UserDefaults.standard
+ storage: UserDefaults.standard,
+ syncClient: syncClient
)
}
@@ -21,20 +22,31 @@ public struct ChatClientFactory {
networkingInteractor: NetworkingInteractor,
keychain: KeychainStorageProtocol,
logger: ConsoleLogging,
- keyValueStorage: KeyValueStorage
+ storage: KeyValueStorage,
+ syncClient: SyncClient
) -> ChatClient {
+ let historyClient = HistoryClientFactory.create(keychain: keychain)
let kms = KeyManagementService(keychain: keychain)
- let messageStore = KeyedDatabase(storage: keyValueStorage, identifier: ChatStorageIdentifiers.messages.rawValue)
- let receivedInviteStore = KeyedDatabase(storage: keyValueStorage, identifier: ChatStorageIdentifiers.receivedInvites.rawValue)
- let sentInviteStore = KeyedDatabase(storage: keyValueStorage, identifier: ChatStorageIdentifiers.sentInvites.rawValue)
- let threadStore = KeyedDatabase(storage: keyValueStorage, identifier: ChatStorageIdentifiers.threads.rawValue)
- let chatStorage = ChatStorage(messageStore: messageStore, receivedInviteStore: receivedInviteStore, sentInviteStore: sentInviteStore, threadStore: threadStore)
- let resubscriptionService = ResubscriptionService(networkingInteractor: networkingInteractor, kms: kms, chatStorage: chatStorage, logger: logger)
+ let serializer = Serializer(kms: kms)
+ 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)
+ let threadStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.thread.rawValue, syncClient: syncClient, storage: storage)
let identityClient = IdentityClientFactory.create(keyserver: keyserverURL, keychain: keychain, logger: logger)
+ let inviteKeyDelegate = InviteKeyDelegate(networkingInteractor: networkingInteractor, kms: kms, identityClient: identityClient)
+ let sentInviteDelegate = SentInviteStoreDelegate(networkingInteractor: networkingInteractor, kms: kms)
+ let threadDelegate = ThreadStoreDelegate(networkingInteractor: networkingInteractor, kms: kms, historyService: historyService)
+ let sentInviteStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.sentInvite.rawValue, syncClient: syncClient, storage: storage)
+ let inviteKeyStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.inviteKey.rawValue, syncClient: syncClient, storage: storage)
+ let receivedInviteStatusStore: SyncStore = SyncStoreFactory.create(name: ChatStorageIdentifiers.receivedInviteStatus.rawValue, syncClient: syncClient, storage: storage)
+ let receivedInviteStatusDelegate = ReceiviedInviteStatusDelegate()
+ let chatStorage = ChatStorage(kms: kms, messageStore: messageStore, receivedInviteStore: receivedInviteStore, sentInviteStore: sentInviteStore, threadStore: threadStore, inviteKeyStore: inviteKeyStore, receivedInviteStatusStore: receivedInviteStatusStore, historyService: historyService, sentInviteStoreDelegate: sentInviteDelegate, threadStoreDelegate: threadDelegate, inviteKeyDelegate: inviteKeyDelegate, receiviedInviteStatusDelegate: receivedInviteStatusDelegate)
+ let resubscriptionService = ResubscriptionService(networkingInteractor: networkingInteractor, kms: kms, logger: logger)
let invitationHandlingService = InvitationHandlingService(keyserverURL: keyserverURL, networkingInteractor: networkingInteractor, identityClient: identityClient, kms: kms, logger: logger, chatStorage: chatStorage)
let inviteService = InviteService(keyserverURL: keyserverURL, networkingInteractor: networkingInteractor, identityClient: identityClient, kms: kms, chatStorage: chatStorage, logger: logger)
let leaveService = LeaveService()
let messagingService = MessagingService(keyserverURL: keyserverURL, networkingInteractor: networkingInteractor, identityClient: identityClient, chatStorage: chatStorage, logger: logger)
+ let syncRegisterService = SyncRegisterService(syncClient: syncClient)
let client = ChatClient(
identityClient: identityClient,
@@ -45,6 +57,7 @@ public struct ChatClientFactory {
leaveService: leaveService,
kms: kms,
chatStorage: chatStorage,
+ syncRegisterService: syncRegisterService,
socketConnectionStatusPublisher: relayClient.socketConnectionStatusPublisher
)
diff --git a/Sources/Chat/ChatImports.swift b/Sources/Chat/ChatImports.swift
index 9af0db974..447ee4a25 100644
--- a/Sources/Chat/ChatImports.swift
+++ b/Sources/Chat/ChatImports.swift
@@ -1,4 +1,6 @@
#if !CocoaPods
@_exported import WalletConnectSigner
@_exported import WalletConnectIdentity
+@_exported import WalletConnectSync
+@_exported import WalletConnectHistory
#endif
diff --git a/Sources/Chat/ChatStorage.swift b/Sources/Chat/ChatStorage.swift
deleted file mode 100644
index c26ed3e1f..000000000
--- a/Sources/Chat/ChatStorage.swift
+++ /dev/null
@@ -1,193 +0,0 @@
-import Foundation
-import Combine
-
-final class ChatStorage {
-
- private let messageStore: KeyedDatabase
- private let receivedInviteStore: KeyedDatabase
- private let sentInviteStore: KeyedDatabase
- private let threadStore: KeyedDatabase
-
- private var messagesPublisherSubject = PassthroughSubject<[Message], Never>()
- private var receivedInvitesPublisherSubject = PassthroughSubject<[ReceivedInvite], Never>()
- private var sentInvitesPublisherSubject = PassthroughSubject<[SentInvite], Never>()
- private var threadsPublisherSubject = PassthroughSubject<[Thread], Never>()
-
- private var newMessagePublisherSubject = PassthroughSubject()
- private var newReceivedInvitePublisherSubject = PassthroughSubject()
- private var newSentInvitePublisherSubject = PassthroughSubject()
- private var newThreadPublisherSubject = PassthroughSubject()
-
- private var acceptPublisherSubject = PassthroughSubject<(String, SentInvite), Never>()
- private var rejectPublisherSubject = PassthroughSubject<(SentInvite), Never>()
-
- var messagesPublisher: AnyPublisher<[Message], Never> {
- messagesPublisherSubject.eraseToAnyPublisher()
- }
-
- var receivedInvitesPublisher: AnyPublisher<[ReceivedInvite], Never> {
- receivedInvitesPublisherSubject.eraseToAnyPublisher()
- }
-
- var sentInvitesPublisher: AnyPublisher<[SentInvite], Never> {
- sentInvitesPublisherSubject.eraseToAnyPublisher()
- }
-
- var threadsPublisher: AnyPublisher<[Thread], Never> {
- threadsPublisherSubject.eraseToAnyPublisher()
- }
-
- var newMessagePublisher: AnyPublisher {
- newMessagePublisherSubject.eraseToAnyPublisher()
- }
-
- var newReceivedInvitePublisher: AnyPublisher {
- newReceivedInvitePublisherSubject.eraseToAnyPublisher()
- }
-
- var newSentInvitePublisher: AnyPublisher {
- newSentInvitePublisherSubject.eraseToAnyPublisher()
- }
-
- var newThreadPublisher: AnyPublisher {
- newThreadPublisherSubject.eraseToAnyPublisher()
- }
-
- var acceptPublisher: AnyPublisher<(String, SentInvite), Never> {
- acceptPublisherSubject.eraseToAnyPublisher()
- }
-
- var rejectPublisher: AnyPublisher {
- rejectPublisherSubject.eraseToAnyPublisher()
- }
-
- init(
- messageStore: KeyedDatabase,
- receivedInviteStore: KeyedDatabase,
- sentInviteStore: KeyedDatabase,
- threadStore: KeyedDatabase
- ) {
- self.messageStore = messageStore
- self.receivedInviteStore = receivedInviteStore
- self.sentInviteStore = sentInviteStore
- self.threadStore = threadStore
- }
-
- func setupSubscriptions(account: Account) {
- messageStore.onUpdate = { [unowned self] in
- messagesPublisherSubject.send(getMessages(account: account))
- }
- receivedInviteStore.onUpdate = { [unowned self] in
- receivedInvitesPublisherSubject.send(getReceivedInvites(account: account))
- }
- sentInviteStore.onUpdate = { [unowned self] in
- sentInvitesPublisherSubject.send(getSentInvites(account: account))
- }
- threadStore.onUpdate = { [unowned self] in
- threadsPublisherSubject.send(getThreads(account: account))
- }
- }
-
- // MARK: - Invites
-
- func getReceivedInvite(id: Int64) -> ReceivedInvite? {
- return receivedInviteStore.getAll()
- .first(where: { $0.id == id })
- }
-
- func getSentInvite(id: Int64) -> SentInvite? {
- return sentInviteStore.getAll()
- .first(where: { $0.id == id })
- }
-
- func set(receivedInvite: ReceivedInvite, account: Account) {
- receivedInviteStore.set(receivedInvite, for: account.absoluteString)
- newReceivedInvitePublisherSubject.send(receivedInvite)
- }
-
- func set(sentInvite: SentInvite, account: Account) {
- sentInviteStore.set(sentInvite, for: account.absoluteString)
- newSentInvitePublisherSubject.send(sentInvite)
- }
-
- func getReceivedInvites(account: Account) -> [ReceivedInvite] {
- return receivedInviteStore.getElements(for: account.absoluteString)
- }
-
- func getSentInvites(account: Account) -> [SentInvite] {
- return sentInviteStore.getElements(for: account.absoluteString)
- }
-
- func accept(receivedInvite: ReceivedInvite, account: Account) {
- receivedInviteStore.delete(receivedInvite, for: account.absoluteString)
-
- let accepted = ReceivedInvite(invite: receivedInvite, status: .approved)
- receivedInviteStore.set(accepted, for: account.absoluteString)
- }
-
- func reject(receivedInvite: ReceivedInvite, account: Account) {
- receivedInviteStore.delete(receivedInvite, for: account.absoluteString)
-
- let rejected = ReceivedInvite(invite: receivedInvite, status: .rejected)
- receivedInviteStore.set(rejected, for: account.absoluteString)
- }
-
- func accept(sentInviteId: Int64, account: Account, topic: String) {
- guard let invite = getSentInvite(id: sentInviteId)
- else { return }
-
- sentInviteStore.delete(invite, for: account.absoluteString)
-
- let approved = SentInvite(invite: invite, status: .approved)
- sentInviteStore.set(approved, for: account.absoluteString)
-
- acceptPublisherSubject.send((topic, approved))
- }
-
- func reject(sentInviteId: Int64, account: Account) {
- guard let invite = getSentInvite(id: sentInviteId)
- else { return }
-
- sentInviteStore.delete(invite, for: account.absoluteString)
-
- let rejected = SentInvite(invite: invite, status: .rejected)
- // TODO: Update also for peer invites
- sentInviteStore.set(rejected, for: account.absoluteString)
-
- rejectPublisherSubject.send(rejected)
- }
-
- // MARK: - Threads
-
- func getAllThreads() -> [Thread] {
- return threadStore.getAll()
- }
-
- func getThreads(account: Account) -> [Thread] {
- return threadStore.getElements(for: account.absoluteString)
- }
-
- func getThread(topic: String) -> Thread? {
- return getAllThreads().first(where: { $0.topic == topic })
- }
-
- func set(thread: Thread, account: Account) {
- threadStore.set(thread, for: account.absoluteString)
- newThreadPublisherSubject.send(thread)
- }
-
- // MARK: - Messages
-
- func set(message: Message, account: Account) {
- messageStore.set(message, for: account.absoluteString)
- newMessagePublisherSubject.send(message)
- }
-
- func getMessages(topic: String) -> [Message] {
- return messageStore.getAll().filter { $0.topic == topic }
- }
-
- func getMessages(account: Account) -> [Message] {
- return messageStore.getElements(for: account.absoluteString)
- }
-}
diff --git a/Sources/Chat/ChatStorageIdentifiers.swift b/Sources/Chat/ChatStorageIdentifiers.swift
index 2194a3986..f20d9f2e1 100644
--- a/Sources/Chat/ChatStorageIdentifiers.swift
+++ b/Sources/Chat/ChatStorageIdentifiers.swift
@@ -3,7 +3,10 @@ import Foundation
enum ChatStorageIdentifiers: String {
case topicToInvitationPubKey = "com.walletconnect.chat.topicToInvitationPubKey"
case messages = "com.walletconnect.chat.messages"
- case threads = "com.walletconnect.chat.threads"
case receivedInvites = "com.walletconnect.chat.receivedInvites"
- case sentInvites = "com.walletconnect.chat.sentInvites"
+
+ case thread = "com.walletconnect.chat.threads"
+ case sentInvite = "com.walletconnect.chat.sentInvites"
+ case inviteKey = "com.walletconnect.chat.inviteKeys"
+ case receivedInviteStatus = "com.walletconnect.chat.receivedInviteStatuses"
}
diff --git a/Sources/Chat/KeyedDatabase.swift b/Sources/Chat/KeyedDatabase.swift
deleted file mode 100644
index 9755542fc..000000000
--- a/Sources/Chat/KeyedDatabase.swift
+++ /dev/null
@@ -1,57 +0,0 @@
-import Foundation
-
-class KeyedDatabase where Element: Codable & Equatable {
-
- private var index: [String: [Element]] = [:] {
- didSet {
- guard oldValue != index else { return }
- set(index, for: identifier)
- onUpdate?()
- }
- }
-
- private let storage: KeyValueStorage
- private let identifier: String
-
- var onUpdate: (() -> Void)?
-
- init(storage: KeyValueStorage, identifier: String) {
- self.storage = storage
- self.identifier = identifier
-
- initializeIndex()
- }
-
- func getAll() -> [Element] {
- return index.values.reduce([], +)
- }
-
- func getElements(for key: String) -> [Element] {
- return index[key] ?? []
- }
-
- func set(_ element: Element, for key: String) {
- index.append(element, for: key)
- }
-
- func delete(_ element: Element, for key: String) {
- index.delete(element, for: key)
- }
-}
-
-private extension KeyedDatabase {
-
- func initializeIndex() {
- guard
- let data = storage.object(forKey: identifier) as? Data,
- let decoded = try? JSONDecoder().decode([String: [Element]].self, from: data)
- else { return }
-
- index = decoded
- }
-
- func set(_ value: [String: [Element]], for key: String) {
- let data = try! JSONEncoder().encode(value)
- storage.set(data, forKey: key)
- }
-}
diff --git a/Sources/Chat/ProtocolServices/Common/MessagingService.swift b/Sources/Chat/ProtocolServices/Common/MessagingService.swift
index 2e330ec05..f032b3b97 100644
--- a/Sources/Chat/ProtocolServices/Common/MessagingService.swift
+++ b/Sources/Chat/ProtocolServices/Common/MessagingService.swift
@@ -82,6 +82,8 @@ private extension MessagingService {
guard let (message, messageClaims) = try? MessagePayload.decodeAndVerify(from: payload.request)
else { fatalError() /* TODO: Handle error */ }
+ // TODO: Compare message hash
+
Task(priority: .high) {
let authorAccount = try await identityClient.resolveIdentity(iss: messageClaims.iss)
diff --git a/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift b/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift
index b2f87f7b6..215a2c8bd 100644
--- a/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift
+++ b/Sources/Chat/ProtocolServices/Common/ResubscriptionService.swift
@@ -5,33 +5,16 @@ class ResubscriptionService {
private let networkingInteractor: NetworkInteracting
private let kms: KeyManagementServiceProtocol
private let logger: ConsoleLogging
- private var chatStorage: ChatStorage
private var publishers = [AnyCancellable]()
init(
networkingInteractor: NetworkInteracting,
kms: KeyManagementServiceProtocol,
- chatStorage: ChatStorage,
logger: ConsoleLogging
) {
self.networkingInteractor = networkingInteractor
self.kms = kms
self.logger = logger
- self.chatStorage = chatStorage
-
- setUpResubscription()
- }
-
- func setUpResubscription() {
- networkingInteractor.socketConnectionStatusPublisher
- .sink { [unowned self] status in
- guard status == .connected else { return }
-
- Task(priority: .high) {
- let topics = chatStorage.getAllThreads().map { $0.topic }
- try await networkingInteractor.batchSubscribe(topics: topics)
- }
- }.store(in: &publishers)
}
func subscribeForInvites(inviteKey: AgreementPublicKey) async throws {
diff --git a/Sources/Chat/ProtocolServices/History/HistoryService.swift b/Sources/Chat/ProtocolServices/History/HistoryService.swift
new file mode 100644
index 000000000..ebad3a50d
--- /dev/null
+++ b/Sources/Chat/ProtocolServices/History/HistoryService.swift
@@ -0,0 +1,37 @@
+import Foundation
+
+final class HistoryService {
+
+ private let historyClient: HistoryClient
+ private let seiralizer: Serializing
+
+ init(historyClient: HistoryClient, seiralizer: Serializing) {
+ self.historyClient = historyClient
+ self.seiralizer = seiralizer
+ }
+
+ func register() async throws {
+ try await historyClient.register(tags: ["2002"])
+ }
+
+ func fetchMessageHistory(thread: Thread) async throws -> [Message] {
+ let wrappers: [MessagePayload.Wrapper] = try await historyClient.getMessages(
+ topic: thread.topic,
+ count: 200, direction: .backward
+ )
+
+ return wrappers.map { wrapper in
+ let (messagePayload, messageClaims) = try! MessagePayload.decodeAndVerify(from: wrapper)
+
+ let authorAccount = messagePayload.recipientAccount == thread.selfAccount
+ ? thread.peerAccount
+ : thread.selfAccount
+
+ return Message(
+ topic: thread.topic,
+ message: messagePayload.message,
+ authorAccount: authorAccount,
+ timestamp: messageClaims.iat)
+ }
+ }
+}
diff --git a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift
index f35fbd3bd..6226f8704 100644
--- a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift
+++ b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift
@@ -32,7 +32,8 @@ class InvitationHandlingService {
guard let invite = chatStorage.getReceivedInvite(id: inviteId)
else { throw Errors.inviteForIdNotFound }
- let inviteePublicKey = try identityClient.getInviteKey(for: invite.inviteeAccount)
+ let inviteePublicKeyHex = try DIDKey(did: invite.inviteePublicKey).hexString
+ let inviteePublicKey = try AgreementPublicKey(hex: inviteePublicKeyHex)
let inviterPublicKey = try DIDKey(did: invite.inviterPublicKey).hexString
let symmetricKey = try kms.performKeyAgreement(selfPublicKey: inviteePublicKey, peerPublicKey: inviterPublicKey)
@@ -66,10 +67,12 @@ class InvitationHandlingService {
let thread = Thread(
topic: threadTopic,
selfAccount: invite.inviteeAccount,
- peerAccount: invite.inviterAccount
+ peerAccount: invite.inviterAccount,
+ symKey: threadSymmetricKey.sharedKey.hexRepresentation
)
- chatStorage.set(thread: thread, account: invite.inviteeAccount)
+ try await chatStorage.set(thread: thread, account: invite.inviteeAccount)
+
chatStorage.accept(receivedInvite: invite, account: invite.inviteeAccount)
return thread.topic
@@ -94,6 +97,8 @@ class InvitationHandlingService {
)
chatStorage.reject(receivedInvite: invite, account: invite.inviteeAccount)
+
+ try await chatStorage.syncRejectedReceivedInviteStatus(id: inviteId, account: invite.inviteeAccount)
}
}
@@ -114,7 +119,7 @@ private extension InvitationHandlingService {
Task(priority: .high) {
let inviterAccount = try await identityClient.resolveIdentity(iss: claims.iss)
// TODO: Should we cache it?
- let inviteePublicKey = try await identityClient.resolveInvite(account: inviterAccount)
+ let inviteePublicKey = try await identityClient.resolveInvite(account: invite.inviteeAccount)
let inviterPublicKey = invite.inviterPublicKey.did(variant: .X25519)
let invite = ReceivedInvite(
diff --git a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift
index 4a5797d7e..f1f09d31f 100644
--- a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift
+++ b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift
@@ -30,35 +30,38 @@ class InviteService {
@discardableResult
func invite(invite: Invite) async throws -> Int64 {
- // TODO ad storage
let protocolMethod = ChatInviteProtocolMethod()
+
let selfPubKeyY = try kms.createX25519KeyPair()
+ let selfPrivKeyY = try kms.getPrivateKey(for: selfPubKeyY)!
+
let inviteePublicKey = try DIDKey(did: invite.inviteePublicKey)
- let symKeyI = try kms.performKeyAgreement(selfPublicKey: selfPubKeyY, peerPublicKey: inviteePublicKey.hexString)
- let inviteTopic = try AgreementPublicKey(hex: inviteePublicKey.hexString).rawRepresentation.sha256().toHexString()
- // overrides on invite toipic
+ let symKeyI = try kms.performKeyAgreement(
+ selfPublicKey: selfPubKeyY,
+ peerPublicKey: inviteePublicKey.hexString
+ )
+
+ let pubKeyX = try AgreementPublicKey(hex: inviteePublicKey.hexString)
+ let inviteTopic = pubKeyX.rawRepresentation.sha256().toHexString()
+ let responseTopic = symKeyI.derivedTopic()
+
try kms.setSymmetricKey(symKeyI.sharedKey, for: inviteTopic)
+ try kms.setSymmetricKey(symKeyI.sharedKey, for: responseTopic)
- let payload = InvitePayload(
- keyserver: keyserverURL,
- message: invite.message,
- inviteeAccount: invite.inviteeAccount,
- inviterPublicKey: DIDKey(rawData: selfPubKeyY.rawRepresentation)
- )
let wrapper = try identityClient.signAndCreateWrapper(
- payload: payload,
+ payload: InvitePayload(
+ keyserver: keyserverURL,
+ message: invite.message,
+ inviteeAccount: invite.inviteeAccount,
+ inviterPublicKey: DIDKey(rawData: selfPubKeyY.rawRepresentation)
+ ),
account: invite.inviterAccount
)
let inviteId = RPCID()
let request = RPCRequest(method: protocolMethod.method, params: wrapper, rpcid: inviteId)
- // 2. Proposer subscribes to topic R which is the hash of the derived symKey
- let responseTopic = symKeyI.derivedTopic()
-
- try kms.setSymmetricKey(symKeyI.sharedKey, for: responseTopic)
-
try await networkingInteractor.subscribe(topic: responseTopic)
try await networkingInteractor.request(request, topic: inviteTopic, protocolMethod: protocolMethod, envelopeType: .type1(pubKey: selfPubKeyY.rawRepresentation))
@@ -67,10 +70,14 @@ class InviteService {
message: invite.message,
inviterAccount: invite.inviterAccount,
inviteeAccount: invite.inviteeAccount,
+ inviterPubKeyY: selfPubKeyY.hexRepresentation,
+ inviterPrivKeyY: selfPrivKeyY.rawRepresentation.toHexString(),
+ responseTopic: responseTopic,
+ symKey: symKeyI.sharedKey.hexRepresentation,
timestamp: Date().millisecondsSince1970
)
- chatStorage.set(sentInvite: sentInvite, account: invite.inviterAccount)
+ try await chatStorage.set(sentInvite: sentInvite, account: invite.inviterAccount)
logger.debug("invite sent on topic: \(inviteTopic)")
@@ -100,6 +107,14 @@ private extension InviteService {
)
}
}.store(in: &publishers)
+
+ networkingInteractor.responseErrorSubscription(on: ChatInviteProtocolMethod())
+ .sink { [unowned self] (payload: ResponseSubscriptionErrorPayload) in
+
+ Task(priority: .high) {
+ try await chatStorage.reject(sentInviteId: payload.id.integer)
+ }
+ }.store(in: &publishers)
}
func createThread(sentInviteId: Int64, selfPubKeyHex: String, peerPubKey: DIDKey, account: Account, peerAccount: Account) async throws {
@@ -113,13 +128,12 @@ private extension InviteService {
let thread = Thread(
topic: threadTopic,
selfAccount: account,
- peerAccount: peerAccount
+ peerAccount: peerAccount,
+ symKey: agreementKeys.sharedKey.hexRepresentation
)
- chatStorage.set(thread: thread, account: account)
-
- // TODO: Implement reject for sentInvite
- chatStorage.accept(sentInviteId: sentInviteId, account: account, topic: threadTopic)
+ try await chatStorage.set(thread: thread, account: account)
+ try await chatStorage.accept(sentInviteId: sentInviteId, topic: threadTopic)
// TODO - remove symKeyI
}
diff --git a/Sources/Chat/ProtocolServices/Sync/SyncRegisterService.swift b/Sources/Chat/ProtocolServices/Sync/SyncRegisterService.swift
new file mode 100644
index 000000000..8bffdfbbe
--- /dev/null
+++ b/Sources/Chat/ProtocolServices/Sync/SyncRegisterService.swift
@@ -0,0 +1,32 @@
+import Foundation
+
+final class SyncRegisterService {
+
+ private let syncClient: SyncClient
+
+ init(syncClient: SyncClient) {
+ self.syncClient = syncClient
+ }
+
+ func register(account: Account, onSign: @escaping SigningCallback) async throws {
+ let message = syncClient.getMessage(account: account)
+
+ switch await onSign(message) {
+ case .signed(let signature):
+ try await syncClient.register(account: account, signature: signature)
+ case .rejected:
+ throw Errors.signatureRejected
+ }
+ }
+
+ func isRegistered(account: Account) -> Bool {
+ return syncClient.isRegistered(account: account)
+ }
+}
+
+private extension SyncRegisterService {
+
+ enum Errors: Error {
+ case signatureRejected
+ }
+}
diff --git a/Sources/Chat/Storage/ChatStorage.swift b/Sources/Chat/Storage/ChatStorage.swift
new file mode 100644
index 000000000..da0afe8a6
--- /dev/null
+++ b/Sources/Chat/Storage/ChatStorage.swift
@@ -0,0 +1,317 @@
+import Foundation
+import Combine
+
+final class ChatStorage {
+
+ private var publishers = Set()
+
+ private let kms: KeyManagementServiceProtocol
+ private let messageStore: KeyedDatabase
+ private let receivedInviteStore: KeyedDatabase
+ private let sentInviteStore: SyncStore
+ private let threadStore: SyncStore
+ private let inviteKeyStore: SyncStore
+ private let receivedInviteStatusStore: SyncStore
+ private let historyService: HistoryService
+
+ private let sentInviteStoreDelegate: SentInviteStoreDelegate
+ private let threadStoreDelegate: ThreadStoreDelegate
+ private let inviteKeyDelegate: InviteKeyDelegate
+ private let receiviedInviteStatusDelegate: ReceiviedInviteStatusDelegate
+
+ private var messagesPublisherSubject = PassthroughSubject<[Message], Never>()
+ private var receivedInvitesPublisherSubject = PassthroughSubject<[ReceivedInvite], Never>()
+ private var newMessagePublisherSubject = PassthroughSubject()
+ private var newReceivedInvitePublisherSubject = PassthroughSubject()
+ private var newSentInvitePublisherSubject = PassthroughSubject()
+ private var newThreadPublisherSubject = PassthroughSubject()
+
+ private var acceptPublisherSubject = PassthroughSubject<(String, SentInvite), Never>()
+ private var rejectPublisherSubject = PassthroughSubject<(SentInvite), Never>()
+
+ var messagesPublisher: AnyPublisher<[Message], Never> {
+ messagesPublisherSubject.eraseToAnyPublisher()
+ }
+
+ var receivedInvitesPublisher: AnyPublisher<[ReceivedInvite], Never> {
+ receivedInvitesPublisherSubject.eraseToAnyPublisher()
+ }
+
+ var sentInvitesPublisher: AnyPublisher<[SentInvite], Never> {
+ sentInviteStore.dataUpdatePublisher
+ }
+
+ var threadsPublisher: AnyPublisher<[Thread], Never> {
+ threadStore.dataUpdatePublisher
+ }
+
+ var newMessagePublisher: AnyPublisher {
+ newMessagePublisherSubject.eraseToAnyPublisher()
+ }
+
+ var newReceivedInvitePublisher: AnyPublisher {
+ newReceivedInvitePublisherSubject.eraseToAnyPublisher()
+ }
+
+ var newSentInvitePublisher: AnyPublisher {
+ newSentInvitePublisherSubject.eraseToAnyPublisher()
+ }
+
+ var newThreadPublisher: AnyPublisher {
+ newThreadPublisherSubject.eraseToAnyPublisher()
+ }
+
+ var acceptPublisher: AnyPublisher<(String, SentInvite), Never> {
+ acceptPublisherSubject.eraseToAnyPublisher()
+ }
+
+ var rejectPublisher: AnyPublisher {
+ rejectPublisherSubject.eraseToAnyPublisher()
+ }
+
+ init(
+ kms: KeyManagementServiceProtocol,
+ messageStore: KeyedDatabase,
+ receivedInviteStore: KeyedDatabase,
+ sentInviteStore: SyncStore,
+ threadStore: SyncStore,
+ inviteKeyStore: SyncStore,
+ receivedInviteStatusStore: SyncStore,
+ historyService: HistoryService,
+ sentInviteStoreDelegate: SentInviteStoreDelegate,
+ threadStoreDelegate: ThreadStoreDelegate,
+ inviteKeyDelegate: InviteKeyDelegate,
+ receiviedInviteStatusDelegate: ReceiviedInviteStatusDelegate
+ ) {
+ self.kms = kms
+ self.messageStore = messageStore
+ self.receivedInviteStore = receivedInviteStore
+ self.sentInviteStore = sentInviteStore
+ self.threadStore = threadStore
+ self.inviteKeyStore = inviteKeyStore
+ self.receivedInviteStatusStore = receivedInviteStatusStore
+ self.historyService = historyService
+ self.sentInviteStoreDelegate = sentInviteStoreDelegate
+ self.threadStoreDelegate = threadStoreDelegate
+ self.inviteKeyDelegate = inviteKeyDelegate
+ self.receiviedInviteStatusDelegate = receiviedInviteStatusDelegate
+
+ setupSyncSubscriptions()
+ }
+
+ // 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)
+ }
+
+ func initializeDelegates() async throws {
+ try await sentInviteStoreDelegate.onInitialization(sentInviteStore.getAll())
+ try await threadStoreDelegate.onInitialization(storage: self)
+ try await inviteKeyDelegate.onInitialization(inviteKeyStore.getAll())
+ try await receiviedInviteStatusDelegate.onInitialization()
+ }
+
+ func initializeHistory(account: Account) async throws {
+ try await historyService.register()
+
+ for thread in getAllThreads() {
+ let messages = try await historyService.fetchMessageHistory(thread: thread)
+ set(messages: messages, account: account)
+ }
+ }
+
+ func setupSubscriptions(account: Account) throws {
+ messageStore.onUpdate = { [unowned self] in
+ messagesPublisherSubject.send(getMessages(account: account))
+ }
+ receivedInviteStore.onUpdate = { [unowned self] in
+ receivedInvitesPublisherSubject.send(getReceivedInvites(account: account))
+ }
+
+ try sentInviteStore.setupSubscriptions(account: account)
+ try threadStore.setupSubscriptions(account: account)
+ try inviteKeyStore.setupSubscriptions(account: account)
+ }
+
+ // MARK: - Invites
+
+ func getReceivedInvite(id: Int64) -> ReceivedInvite? {
+ return receivedInviteStore.getAll()
+ .first(where: { $0.id == id })
+ }
+
+ func getSentInvite(id: Int64) -> SentInvite? {
+ return sentInviteStore.getAll()
+ .first(where: { $0.id == id })
+ }
+
+ func set(receivedInvite: ReceivedInvite, account: Account) {
+ receivedInviteStore.set(element: receivedInvite, for: account.absoluteString)
+ newReceivedInvitePublisherSubject.send(receivedInvite)
+ }
+
+ func set(sentInvite: SentInvite, account: Account) async throws {
+ try await sentInviteStore.set(object: sentInvite, for: account)
+ newSentInvitePublisherSubject.send(sentInvite)
+ }
+
+ func getReceivedInvites(account: Account) -> [ReceivedInvite] {
+ return receivedInviteStore.getAll(for: account.absoluteString)
+ }
+
+ func syncRejectedReceivedInviteStatus(id: Int64, account: Account) async throws {
+ let status = ReceivedInviteStatus(id: id, status: .rejected)
+ try await receivedInviteStatusStore.set(object: status, for: account)
+ }
+
+ func getReceivedInvites(thread: Thread) -> [ReceivedInvite] {
+ return getReceivedInvites(account: thread.selfAccount)
+ .filter { $0.inviterAccount == thread.peerAccount }
+ }
+
+ func getSentInvites(account: Account) -> [SentInvite] {
+ do {
+ return try sentInviteStore.getAll(for: account)
+ } catch {
+ // TODO: remove fatalError
+ fatalError(error.localizedDescription)
+ }
+ }
+
+ func accept(receivedInvite: ReceivedInvite, account: Account) {
+ receivedInviteStore.delete(id: receivedInvite.databaseId, for: account.absoluteString)
+
+ let accepted = ReceivedInvite(invite: receivedInvite, status: .approved)
+ receivedInviteStore.set(element: accepted, for: account.absoluteString)
+ }
+
+ func reject(receivedInvite: ReceivedInvite, account: Account) {
+ receivedInviteStore.delete(id: receivedInvite.databaseId, for: account.absoluteString)
+
+ let rejected = ReceivedInvite(invite: receivedInvite, status: .rejected)
+ receivedInviteStore.set(element: rejected, for: account.absoluteString)
+ }
+
+ func accept(sentInviteId: Int64, topic: String) async throws {
+ guard let invite = getSentInvite(id: sentInviteId)
+ else { return }
+
+ let approved = SentInvite(invite: invite, status: .approved)
+ try await sentInviteStore.set(object: approved, for: invite.inviterAccount)
+
+ acceptPublisherSubject.send((topic, approved))
+ }
+
+ func reject(sentInviteId: Int64) async throws {
+ guard let invite = getSentInvite(id: sentInviteId)
+ else { return }
+
+ let rejected = SentInvite(invite: invite, status: .rejected)
+ try await sentInviteStore.set(object: rejected, for: invite.inviterAccount)
+
+ rejectPublisherSubject.send(rejected)
+ }
+
+ // MARK: InviteKeys
+
+ func setInviteKey(_ inviteKey: AgreementPublicKey, account: Account) async throws {
+ if let privateKey = try kms.getPrivateKey(for: inviteKey) {
+ let pubKeyHex = inviteKey.hexRepresentation
+ let privKeyHex = privateKey.rawRepresentation.toHexString()
+ let key = InviteKey(publicKey: pubKeyHex, privateKey: privKeyHex, account: account)
+ try await inviteKeyStore.set(object: key, for: account)
+ }
+ }
+
+ func removeInviteKey(_ inviteKey: AgreementPublicKey, account: Account) async throws {
+ try await inviteKeyStore.delete(id: inviteKey.hexRepresentation, for: account)
+ }
+
+ // MARK: - Threads
+
+ func getAllThreads() -> [Thread] {
+ return threadStore.getAll()
+ }
+
+ func getThreads(account: Account) -> [Thread] {
+ do {
+ return try threadStore.getAll(for: account)
+ } catch {
+ // TODO: remove fatalError
+ fatalError(error.localizedDescription)
+ }
+ }
+
+ func getThread(topic: String) -> Thread? {
+ return getAllThreads().first(where: { $0.topic == topic })
+ }
+
+ func set(thread: Thread, account: Account) async throws {
+ try await threadStore.set(object: thread, for: account)
+ newThreadPublisherSubject.send(thread)
+ }
+
+ // MARK: - Messages
+
+ func set(message: Message, account: Account) {
+ messageStore.set(element: message, for: account.absoluteString)
+ newMessagePublisherSubject.send(message)
+ }
+
+ func set(messages: [Message], account: Account) {
+ messageStore.set(elements: messages, for: account.absoluteString)
+ }
+
+ func getMessages(topic: String) -> [Message] {
+ return messageStore.getAll().filter { $0.topic == topic }
+ }
+
+ func getMessages(account: Account) -> [Message] {
+ return messageStore.getAll(for: account.absoluteString)
+ }
+}
+
+private extension ChatStorage {
+
+ func setupSyncSubscriptions() {
+ sentInviteStore.syncUpdatePublisher.sink { [unowned self] topic, account, update in
+ switch update {
+ case .set(let object):
+ self.sentInviteStoreDelegate.onUpdate(object)
+ case .delete(let id):
+ self.sentInviteStoreDelegate.onDelete(id)
+ }
+ }.store(in: &publishers)
+
+ threadStore.syncUpdatePublisher.sink { [unowned self] topic, account, update in
+ switch update {
+ case .set(let object):
+ self.threadStoreDelegate.onUpdate(object, storage: self)
+ case .delete(let id):
+ self.threadStoreDelegate.onDelete(id)
+ }
+ }.store(in: &publishers)
+
+ inviteKeyStore.syncUpdatePublisher.sink { [unowned self] topic, account, update in
+ switch update {
+ case .set(let object):
+ self.inviteKeyDelegate.onUpdate(object, account: account)
+ case .delete(let id):
+ self.inviteKeyDelegate.onDelete(id)
+ }
+ }.store(in: &publishers)
+
+ receivedInviteStatusStore.syncUpdatePublisher.sink { [unowned self] topic, account, update in
+ switch update {
+ case .set(let object):
+ self.receiviedInviteStatusDelegate.onUpdate(object, storage: self, account: account)
+ case .delete(let id):
+ self.receiviedInviteStatusDelegate.onDelete(id)
+ }
+ }.store(in: &publishers)
+ }
+}
diff --git a/Sources/Chat/Storage/InviteKeyDelegate.swift b/Sources/Chat/Storage/InviteKeyDelegate.swift
new file mode 100644
index 000000000..a2337df6f
--- /dev/null
+++ b/Sources/Chat/Storage/InviteKeyDelegate.swift
@@ -0,0 +1,56 @@
+import Foundation
+
+final class InviteKeyDelegate {
+
+ private let networkingInteractor: NetworkInteracting
+ private let kms: KeyManagementServiceProtocol
+ private let identityClient: IdentityClient
+
+ init(networkingInteractor: NetworkInteracting, kms: KeyManagementServiceProtocol, identityClient: IdentityClient) {
+ self.networkingInteractor = networkingInteractor
+ self.kms = kms
+ self.identityClient = identityClient
+ }
+
+ func onInitialization(_ keys: [InviteKey]) async throws {
+ for key in keys {
+ try syncKms(key: key)
+ }
+
+ let topics = keys.map { $0.topic }
+ try await networkingInteractor.batchSubscribe(topics: topics)
+ }
+
+ func onUpdate(_ key: InviteKey, account: Account) {
+ Task(priority: .high) {
+ try syncKms(key: key)
+ try syncIdentityStorage(key: key, account: account)
+ try await networkingInteractor.subscribe(topic: key.topic)
+ }
+ }
+
+ func onDelete(_ id: String) {
+ Task(priority: .high) {
+ let inviteKey = try AgreementPublicKey(hex: id) // InviteKey id is pubKey hex
+ let topic = inviteKey.rawRepresentation.sha256().toHexString()
+ kms.deletePublicKey(for: topic)
+ networkingInteractor.unsubscribe(topic: topic)
+ }
+ }
+}
+
+private extension InviteKeyDelegate {
+
+ func syncKms(key: InviteKey) throws {
+ let inviteKey = try AgreementPublicKey(hex: key.publicKey)
+ let privateKey = try AgreementPrivateKey(hex: key.privateKey)
+ try kms.setPublicKey(publicKey: inviteKey, for: key.topic)
+ try kms.setPrivateKey(privateKey)
+ }
+
+ func syncIdentityStorage(key: InviteKey, account: Account) throws {
+ let inviteKey = try AgreementPublicKey(hex: key.publicKey)
+ try identityClient.setInviteKey(inviteKey, account: account)
+ }
+}
+
diff --git a/Sources/Chat/Storage/ReceiviedInviteStatusDelegate.swift b/Sources/Chat/Storage/ReceiviedInviteStatusDelegate.swift
new file mode 100644
index 000000000..1d5fe63b9
--- /dev/null
+++ b/Sources/Chat/Storage/ReceiviedInviteStatusDelegate.swift
@@ -0,0 +1,20 @@
+import Foundation
+
+final class ReceiviedInviteStatusDelegate {
+
+ func onInitialization() async throws {
+
+ }
+
+ func onUpdate(_ status: ReceivedInviteStatus, storage: ChatStorage, account: Account) {
+ guard status.status == .rejected else { return }
+
+ if let receivedInvite = storage.getReceivedInvite(id: status.id) {
+ storage.reject(receivedInvite: receivedInvite, account: account)
+ }
+ }
+
+ func onDelete(_ id: String) {
+
+ }
+}
diff --git a/Sources/Chat/Storage/SentInviteStoreDelegate.swift b/Sources/Chat/Storage/SentInviteStoreDelegate.swift
new file mode 100644
index 000000000..c48aadf76
--- /dev/null
+++ b/Sources/Chat/Storage/SentInviteStoreDelegate.swift
@@ -0,0 +1,44 @@
+import Foundation
+
+final class SentInviteStoreDelegate {
+
+ private let networkingInteractor: NetworkInteracting
+ private let kms: KeyManagementServiceProtocol
+
+ init(networkingInteractor: NetworkInteracting, kms: KeyManagementServiceProtocol) {
+ self.networkingInteractor = networkingInteractor
+ self.kms = kms
+ }
+
+ func onInitialization(_ objects: [SentInvite]) async throws {
+ for invite in objects {
+ try syncKeychain(invite: invite)
+ }
+
+ let topics = objects.map { $0.responseTopic }
+ try await networkingInteractor.batchSubscribe(topics: topics)
+ }
+
+ func onUpdate(_ object: SentInvite) {
+ Task(priority: .high) {
+ try syncKeychain(invite: object)
+ try await networkingInteractor.subscribe(topic: object.responseTopic)
+ }
+ }
+
+ func onDelete(_ id: String) {
+ // TODO: Implement unsubscribe
+ }
+}
+
+private extension SentInviteStoreDelegate {
+
+ func syncKeychain(invite: SentInvite) throws {
+ let symmetricKey = try SymmetricKey(hex: invite.symKey)
+ let agreementPrivateKey = try AgreementPrivateKey(hex: invite.inviterPrivKeyY)
+
+ // TODO: Should we set symKey for inviteTopic???
+ try kms.setSymmetricKey(symmetricKey, for: invite.responseTopic)
+ try kms.setPrivateKey(agreementPrivateKey)
+ }
+}
diff --git a/Sources/Chat/Storage/ThreadStoreDelegate.swift b/Sources/Chat/Storage/ThreadStoreDelegate.swift
new file mode 100644
index 000000000..91e8a4afc
--- /dev/null
+++ b/Sources/Chat/Storage/ThreadStoreDelegate.swift
@@ -0,0 +1,38 @@
+import Foundation
+
+final class ThreadStoreDelegate {
+
+ private let networkingInteractor: NetworkInteracting
+ private let kms: KeyManagementServiceProtocol
+ private let historyService: HistoryService
+
+ init(networkingInteractor: NetworkInteracting, kms: KeyManagementServiceProtocol, historyService: HistoryService) {
+ self.networkingInteractor = networkingInteractor
+ self.kms = kms
+ self.historyService = historyService
+ }
+
+ func onInitialization(storage: ChatStorage) async throws {
+ let threads = storage.getAllThreads()
+ try await networkingInteractor.batchSubscribe(topics: threads.map { $0.topic })
+ }
+
+ func onUpdate(_ thread: Thread, storage: ChatStorage) {
+ Task(priority: .high) {
+ for receivedInvite in storage.getReceivedInvites(thread: thread) {
+ storage.accept(receivedInvite: receivedInvite, account: thread.selfAccount)
+ }
+
+ let symmetricKey = try SymmetricKey(hex: thread.symKey)
+ try kms.setSymmetricKey(symmetricKey, for: thread.topic)
+ try await networkingInteractor.subscribe(topic: thread.topic)
+
+ let messages = try await historyService.fetchMessageHistory(thread: thread)
+ storage.set(messages: messages, account: thread.selfAccount)
+ }
+ }
+
+ func onDelete(_ id: String) {
+
+ }
+}
diff --git a/Sources/Chat/Types/Plain/InviteKey.swift b/Sources/Chat/Types/Plain/InviteKey.swift
new file mode 100644
index 000000000..bf5cb4796
--- /dev/null
+++ b/Sources/Chat/Types/Plain/InviteKey.swift
@@ -0,0 +1,15 @@
+import Foundation
+
+struct InviteKey: DatabaseObject {
+ let publicKey: String
+ let privateKey: String
+ let account: Account
+
+ var topic: String {
+ return Data(hex: publicKey).sha256().toHexString()
+ }
+
+ var databaseId: String {
+ return account.absoluteString
+ }
+}
diff --git a/Sources/Chat/Types/Plain/Message.swift b/Sources/Chat/Types/Plain/Message.swift
index fe6d07c2c..f6bd8e70c 100644
--- a/Sources/Chat/Types/Plain/Message.swift
+++ b/Sources/Chat/Types/Plain/Message.swift
@@ -1,12 +1,16 @@
import Foundation
-public struct Message: Codable, Equatable {
+public struct Message: DatabaseObject {
public let topic: String
public let message: String
public let authorAccount: Account
public let timestamp: UInt64
public let media: Media?
+ public var databaseId: String {
+ return String(timestamp)
+ }
+
init(
topic: String,
message: String,
diff --git a/Sources/Chat/Types/Plain/ReceivedInvite.swift b/Sources/Chat/Types/Plain/ReceivedInvite.swift
index dde40c77b..64d20f357 100644
--- a/Sources/Chat/Types/Plain/ReceivedInvite.swift
+++ b/Sources/Chat/Types/Plain/ReceivedInvite.swift
@@ -1,6 +1,6 @@
import Foundation
-public struct ReceivedInvite: Codable, Equatable {
+public struct ReceivedInvite: DatabaseObject {
public let id: Int64
public let message: String
public let inviterAccount: Account
@@ -10,6 +10,10 @@ public struct ReceivedInvite: Codable, Equatable {
public let timestamp: UInt64
public var status: Status
+ public var databaseId: String {
+ return String(id)
+ }
+
public init(
id: Int64,
message: String,
diff --git a/Sources/Chat/Types/Plain/ReceivedInviteStatus.swift b/Sources/Chat/Types/Plain/ReceivedInviteStatus.swift
new file mode 100644
index 000000000..3263669b7
--- /dev/null
+++ b/Sources/Chat/Types/Plain/ReceivedInviteStatus.swift
@@ -0,0 +1,10 @@
+import Foundation
+
+struct ReceivedInviteStatus: DatabaseObject {
+ let id: Int64
+ let status: ReceivedInvite.Status
+
+ var databaseId: String {
+ return String(id)
+ }
+}
diff --git a/Sources/Chat/Types/Plain/SentInvite.swift b/Sources/Chat/Types/Plain/SentInvite.swift
index a1d8f1dcc..011cf96dc 100644
--- a/Sources/Chat/Types/Plain/SentInvite.swift
+++ b/Sources/Chat/Types/Plain/SentInvite.swift
@@ -5,21 +5,33 @@ public struct SentInvite: Codable, Equatable {
public let message: String
public let inviterAccount: Account
public let inviteeAccount: Account
+ public let inviterPubKeyY: String
+ public let inviterPrivKeyY: String
+ public let responseTopic: String
+ public let symKey: String
public let timestamp: UInt64
public var status: Status
- public init(
+ init(
id: Int64,
message: String,
inviterAccount: Account,
inviteeAccount: Account,
+ inviterPubKeyY: String,
+ inviterPrivKeyY: String,
+ responseTopic: String,
+ symKey: String,
timestamp: UInt64,
- status: SentInvite.Status = .pending // TODO: Implement statuses
+ status: SentInvite.Status = .pending
) {
self.id = id
self.message = message
self.inviterAccount = inviterAccount
self.inviteeAccount = inviteeAccount
+ self.inviterPubKeyY = inviterPubKeyY
+ self.inviterPrivKeyY = inviterPrivKeyY
+ self.responseTopic = responseTopic
+ self.symKey = symKey
self.timestamp = timestamp
self.status = status
}
@@ -30,12 +42,23 @@ public struct SentInvite: Codable, Equatable {
message: invite.message,
inviterAccount: invite.inviterAccount,
inviteeAccount: invite.inviteeAccount,
+ inviterPubKeyY: invite.inviterPubKeyY,
+ inviterPrivKeyY: invite.inviterPrivKeyY,
+ responseTopic: invite.responseTopic,
+ symKey: invite.symKey,
timestamp: invite.timestamp,
status: status
)
}
}
+extension SentInvite: DatabaseObject {
+
+ public var databaseId: String {
+ return responseTopic
+ }
+}
+
extension SentInvite {
public enum Status: String, Codable, Equatable {
diff --git a/Sources/Chat/Types/Plain/Thread.swift b/Sources/Chat/Types/Plain/Thread.swift
index ed5ef5ca0..1c1015377 100644
--- a/Sources/Chat/Types/Plain/Thread.swift
+++ b/Sources/Chat/Types/Plain/Thread.swift
@@ -4,4 +4,12 @@ public struct Thread: Codable, Equatable {
public let topic: String
public let selfAccount: Account
public let peerAccount: Account
+ public let symKey: String
+}
+
+extension Thread: DatabaseObject {
+
+ public var databaseId: String {
+ return topic
+ }
}
diff --git a/Sources/WalletConnectHistory/HistoryAPI.swift b/Sources/WalletConnectHistory/HistoryAPI.swift
new file mode 100644
index 000000000..58f6b0cbd
--- /dev/null
+++ b/Sources/WalletConnectHistory/HistoryAPI.swift
@@ -0,0 +1,56 @@
+import Foundation
+
+enum HistoryAPI: HTTPService {
+ case register(payload: RegisterPayload, jwt: String)
+ case messages(payload: GetMessagesPayload)
+
+ var path: String {
+ switch self {
+ case .register:
+ return "/register"
+ case .messages:
+ return "/messages"
+ }
+ }
+
+ var method: HTTPMethod {
+ switch self {
+ case .register:
+ return .post
+ case .messages:
+ return .get
+ }
+ }
+
+ var body: Data? {
+ switch self {
+ case .register(let payload, _):
+ return try? JSONEncoder().encode(payload)
+ case .messages:
+ return nil
+ }
+ }
+
+ var additionalHeaderFields: [String : String]? {
+ switch self {
+ case .register(_, let jwt):
+ return ["Authorization": "Bearer \(jwt)"]
+ case .messages:
+ return nil
+ }
+ }
+
+ var queryParameters: [String: String]? {
+ switch self {
+ case .messages(let payload):
+ return [
+ "topic": payload.topic,
+ "originId": payload.originId.map { String($0) },
+ "messageCount": payload.messageCount.map { String($0) },
+ "direction": payload.direction.rawValue
+ ].compactMapValues { $0 }
+ case .register:
+ return nil
+ }
+ }
+}
diff --git a/Sources/WalletConnectHistory/HistoryClient.swift b/Sources/WalletConnectHistory/HistoryClient.swift
new file mode 100644
index 000000000..be8673b4e
--- /dev/null
+++ b/Sources/WalletConnectHistory/HistoryClient.swift
@@ -0,0 +1,40 @@
+import Foundation
+
+public final class HistoryClient {
+
+ private let historyUrl: String
+ private let relayUrl: String
+ private let serializer: Serializer
+ private let historyNetworkService: HistoryNetworkService
+
+ init(historyUrl: String, relayUrl: String, serializer: Serializer, historyNetworkService: HistoryNetworkService) {
+ self.historyUrl = historyUrl
+ self.relayUrl = relayUrl
+ self.serializer = serializer
+ self.historyNetworkService = historyNetworkService
+ }
+
+ public func register(tags: [String]) async throws {
+ let payload = RegisterPayload(tags: tags, relayUrl: relayUrl)
+ try await historyNetworkService.registerTags(payload: payload, historyUrl: historyUrl)
+ }
+
+ public func getMessages(topic: String, count: Int, direction: GetMessagesPayload.Direction) async throws -> [T] {
+ let payload = GetMessagesPayload(topic: topic, originId: nil, messageCount: count, direction: direction)
+ let response = try await historyNetworkService.getMessages(payload: payload, historyUrl: historyUrl)
+
+ let objects = response.messages.compactMap { payload in
+ do {
+ let (request, _, _): (RPCRequest, _, _) = try serializer.deserialize(
+ topic: topic,
+ encodedEnvelope: payload
+ )
+ return try request.params?.get(T.self)
+ } catch {
+ fatalError(error.localizedDescription)
+ }
+ }
+
+ return objects
+ }
+}
diff --git a/Sources/WalletConnectHistory/HistoryClientFactory.swift b/Sources/WalletConnectHistory/HistoryClientFactory.swift
new file mode 100644
index 000000000..d8c69b49a
--- /dev/null
+++ b/Sources/WalletConnectHistory/HistoryClientFactory.swift
@@ -0,0 +1,25 @@
+import Foundation
+
+public class HistoryClientFactory {
+
+ public static func create(keychain: KeychainStorageProtocol) -> HistoryClient {
+ return HistoryClientFactory.create(
+ historyUrl: "https://history.walletconnect.com",
+ relayUrl: "wss://relay.walletconnect.com",
+ keychain: keychain
+ )
+ }
+
+ 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 historyNetworkService = HistoryNetworkService(clientIdStorage: clientIdStorage)
+ return HistoryClient(
+ historyUrl: historyUrl,
+ relayUrl: relayUrl,
+ serializer: serializer,
+ historyNetworkService: historyNetworkService
+ )
+ }
+}
diff --git a/Sources/WalletConnectHistory/HistoryImports.swift b/Sources/WalletConnectHistory/HistoryImports.swift
new file mode 100644
index 000000000..e6d4b859c
--- /dev/null
+++ b/Sources/WalletConnectHistory/HistoryImports.swift
@@ -0,0 +1,4 @@
+#if !CocoaPods
+@_exported import HTTPClient
+@_exported import WalletConnectRelay
+#endif
diff --git a/Sources/WalletConnectHistory/HistoryNetworkService.swift b/Sources/WalletConnectHistory/HistoryNetworkService.swift
new file mode 100644
index 000000000..f4d01ddb8
--- /dev/null
+++ b/Sources/WalletConnectHistory/HistoryNetworkService.swift
@@ -0,0 +1,41 @@
+import Foundation
+
+final class HistoryNetworkService {
+
+ private let clientIdStorage: ClientIdStorage
+
+ init(clientIdStorage: ClientIdStorage) {
+ self.clientIdStorage = clientIdStorage
+ }
+
+ func registerTags(payload: RegisterPayload, historyUrl: String) async throws {
+ let service = HTTPNetworkClient(host: try host(from: historyUrl))
+ let api = HistoryAPI.register(payload: payload, jwt: try getJwt(historyUrl: historyUrl))
+ try await service.request(service: api)
+ }
+
+ func getMessages(payload: GetMessagesPayload, historyUrl: String) async throws -> GetMessagesResponse {
+ let service = HTTPNetworkClient(host: try host(from: historyUrl))
+ let api = HistoryAPI.messages(payload: payload)
+ return try await service.request(GetMessagesResponse.self, at: api)
+ }
+}
+
+private extension HistoryNetworkService {
+
+ enum Errors: Error {
+ case couldNotResolveHost
+ }
+
+ func getJwt(historyUrl: String) throws -> String {
+ let authenticator = ClientIdAuthenticator(clientIdStorage: clientIdStorage, url: historyUrl)
+ return try authenticator.createAuthToken()
+ }
+
+ func host(from url: String) throws -> String {
+ guard let host = URL(string: url)?.host else {
+ throw Errors.couldNotResolveHost
+ }
+ return host
+ }
+}
diff --git a/Sources/WalletConnectHistory/Types/GetMessagesPayload.swift b/Sources/WalletConnectHistory/Types/GetMessagesPayload.swift
new file mode 100644
index 000000000..7dcc9a08d
--- /dev/null
+++ b/Sources/WalletConnectHistory/Types/GetMessagesPayload.swift
@@ -0,0 +1,19 @@
+import Foundation
+
+public struct GetMessagesPayload: Codable {
+ public enum Direction: String, Codable {
+ case forward
+ case backward
+ }
+ public let topic: String
+ public let originId: Int64?
+ public let messageCount: Int?
+ public let direction: Direction
+
+ public init(topic: String, originId: Int64?, messageCount: Int?, direction: GetMessagesPayload.Direction) {
+ self.topic = topic
+ self.originId = originId
+ self.messageCount = messageCount
+ self.direction = direction
+ }
+}
diff --git a/Sources/WalletConnectHistory/Types/GetMessagesResponse.swift b/Sources/WalletConnectHistory/Types/GetMessagesResponse.swift
new file mode 100644
index 000000000..032bad07e
--- /dev/null
+++ b/Sources/WalletConnectHistory/Types/GetMessagesResponse.swift
@@ -0,0 +1,28 @@
+import Foundation
+
+public struct GetMessagesResponse: Decodable {
+ public struct Message: Codable {
+ public let message: String
+ }
+ public let topic: String
+ public let direction: GetMessagesPayload.Direction
+ public let nextId: Int64?
+ public let messages: [String]
+
+ enum CodingKeys: String, CodingKey {
+ case topic
+ case direction
+ case nextId
+ case messages
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ self.topic = try container.decode(String.self, forKey: .topic)
+ self.direction = try container.decode(GetMessagesPayload.Direction.self, forKey: .direction)
+ self.nextId = try container.decodeIfPresent(Int64.self, forKey: .nextId)
+
+ let messages = try container.decode([Message].self, forKey: .messages)
+ self.messages = messages.map { $0.message }
+ }
+}
diff --git a/Sources/WalletConnectHistory/Types/RegisterPayload.swift b/Sources/WalletConnectHistory/Types/RegisterPayload.swift
new file mode 100644
index 000000000..b759c5ce5
--- /dev/null
+++ b/Sources/WalletConnectHistory/Types/RegisterPayload.swift
@@ -0,0 +1,11 @@
+import Foundation
+
+public struct RegisterPayload: Codable {
+ public let tags: [String]
+ public let relayUrl: String
+
+ public init(tags: [String], relayUrl: String) {
+ self.tags = tags
+ self.relayUrl = relayUrl
+ }
+}
diff --git a/Sources/WalletConnectIdentity/IdentityClient.swift b/Sources/WalletConnectIdentity/IdentityClient.swift
index bbccb14b5..8ec6d2943 100644
--- a/Sources/WalletConnectIdentity/IdentityClient.swift
+++ b/Sources/WalletConnectIdentity/IdentityClient.swift
@@ -45,6 +45,14 @@ public final class IdentityClient {
return inviteKey
}
+ public func setInviteKey(_ inviteKey: AgreementPublicKey, account: Account) throws {
+ try identityStorage.saveInviteKey(inviteKey, for: account)
+ }
+
+ public func deleteInviteKey(account: Account) throws {
+ try identityStorage.removeInviteKey(for: account)
+ }
+
public func resolveInvite(account: Account) async throws -> String {
return try await identityService.resolveInvite(account: account)
}
diff --git a/Sources/WalletConnectIdentity/IdentityService.swift b/Sources/WalletConnectIdentity/IdentityService.swift
index 7d9a7e2c7..8a1a984a8 100644
--- a/Sources/WalletConnectIdentity/IdentityService.swift
+++ b/Sources/WalletConnectIdentity/IdentityService.swift
@@ -99,7 +99,7 @@ private extension IdentityService {
version: getVersion(),
nonce: getNonce(),
iat: iatProvader.iat,
- nbf: nil, exp: nil, statement: nil, requestId: nil,
+ nbf: nil, exp: nil, statement: "statement", requestId: nil,
resources: [DIDKey]
)
diff --git a/Sources/WalletConnectIdentity/IdentityStorage.swift b/Sources/WalletConnectIdentity/IdentityStorage.swift
index 0e591f097..0988cd045 100644
--- a/Sources/WalletConnectIdentity/IdentityStorage.swift
+++ b/Sources/WalletConnectIdentity/IdentityStorage.swift
@@ -18,7 +18,7 @@ public final class IdentityStorage {
}
@discardableResult
- func saveInviteKey(
+ public func saveInviteKey(
_ key: AgreementPublicKey,
for account: Account
) throws -> AgreementPublicKey {
@@ -30,7 +30,7 @@ public final class IdentityStorage {
try keychain.delete(key: identityKeyIdentifier(for: account))
}
- func removeInviteKey(for account: Account) throws {
+ public func removeInviteKey(for account: Account) throws {
try keychain.delete(key: inviteKeyIdentifier(for: account))
}
diff --git a/Sources/WalletConnectKMS/Crypto/CryptoKitWrapper/AgreementCryptoKit.swift b/Sources/WalletConnectKMS/Crypto/CryptoKitWrapper/AgreementCryptoKit.swift
index f5e0f906e..b61fdc66c 100644
--- a/Sources/WalletConnectKMS/Crypto/CryptoKitWrapper/AgreementCryptoKit.swift
+++ b/Sources/WalletConnectKMS/Crypto/CryptoKitWrapper/AgreementCryptoKit.swift
@@ -47,7 +47,7 @@ public struct AgreementPublicKey: GenericPasswordConvertible, Equatable {
}
public var hexRepresentation: String {
- key.rawRepresentation.toHexString()
+ rawRepresentation.toHexString()
}
public var did: String {
@@ -80,6 +80,11 @@ public struct AgreementPrivateKey: GenericPasswordConvertible, Equatable {
self.key = Curve25519.KeyAgreement.PrivateKey()
}
+ public init(hex: String) throws {
+ let data = Data(hex: hex)
+ try self.init(rawRepresentation: data)
+ }
+
public init(rawRepresentation: D) throws where D: ContiguousBytes {
self.key = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: rawRepresentation)
}
diff --git a/Sources/WalletConnectKMS/Keychain/String+GenericPasswordConvertible.swift b/Sources/WalletConnectKMS/Keychain/String+GenericPasswordConvertible.swift
new file mode 100644
index 000000000..4c8717a4e
--- /dev/null
+++ b/Sources/WalletConnectKMS/Keychain/String+GenericPasswordConvertible.swift
@@ -0,0 +1,20 @@
+import Foundation
+
+extension String: GenericPasswordConvertible {
+
+ public init(rawRepresentation data: D) throws where D: ContiguousBytes {
+ let buffer = data.withUnsafeBytes { Data($0) }
+ guard let string = String(data: buffer, encoding: .utf8) else {
+ throw Errors.notUTF8
+ }
+ self = string
+ }
+
+ public var rawRepresentation: Data {
+ return data(using: .utf8) ?? Data()
+ }
+}
+
+fileprivate enum Errors: Error {
+ case notUTF8
+}
diff --git a/Sources/WalletConnectRelay/ClientAuth/SocketAuthenticator.swift b/Sources/WalletConnectRelay/ClientAuth/ClientIdAuthenticator.swift
similarity index 56%
rename from Sources/WalletConnectRelay/ClientAuth/SocketAuthenticator.swift
rename to Sources/WalletConnectRelay/ClientAuth/ClientIdAuthenticator.swift
index bb7a8b540..2055e1813 100644
--- a/Sources/WalletConnectRelay/ClientAuth/SocketAuthenticator.swift
+++ b/Sources/WalletConnectRelay/ClientAuth/ClientIdAuthenticator.swift
@@ -1,28 +1,24 @@
import Foundation
-protocol SocketAuthenticating {
+public protocol ClientIdAuthenticating {
func createAuthToken() throws -> String
}
-struct SocketAuthenticator: SocketAuthenticating {
+public struct ClientIdAuthenticator: ClientIdAuthenticating {
private let clientIdStorage: ClientIdStoring
- private let relayHost: String
+ private let url: String
- init(clientIdStorage: ClientIdStoring, relayHost: String) {
+ public init(clientIdStorage: ClientIdStoring, url: String) {
self.clientIdStorage = clientIdStorage
- self.relayHost = relayHost
+ self.url = url
}
- func createAuthToken() throws -> String {
+ public func createAuthToken() throws -> String {
let keyPair = try clientIdStorage.getOrCreateKeyPair()
- let payload = RelayAuthPayload(subject: getSubject(), audience: getAudience())
+ let payload = RelayAuthPayload(subject: getSubject(), audience: url)
return try payload.signAndCreateWrapper(keyPair: keyPair).jwtString
}
- private func getAudience() -> String {
- return "wss://\(relayHost)"
- }
-
private func getSubject() -> String {
return Data.randomBytes(count: 32).toHexString()
}
diff --git a/Sources/WalletConnectRelay/PackageConfig.json b/Sources/WalletConnectRelay/PackageConfig.json
index 18a6f779c..0a1b0e189 100644
--- a/Sources/WalletConnectRelay/PackageConfig.json
+++ b/Sources/WalletConnectRelay/PackageConfig.json
@@ -1 +1 @@
-{"version": "1.6.7"}
+{"version": "1.6.8"}
diff --git a/Sources/WalletConnectRelay/RelayClient.swift b/Sources/WalletConnectRelay/RelayClient.swift
index 880e4b79c..a84c4c264 100644
--- a/Sources/WalletConnectRelay/RelayClient.swift
+++ b/Sources/WalletConnectRelay/RelayClient.swift
@@ -80,9 +80,9 @@ public final class RelayClient {
logger: ConsoleLogging = ConsoleLogger(loggingLevel: .debug)
) {
let clientIdStorage = ClientIdStorage(keychain: keychainStorage)
- let socketAuthenticator = SocketAuthenticator(
+ let socketAuthenticator = ClientIdAuthenticator(
clientIdStorage: clientIdStorage,
- relayHost: relayHost
+ url: "wss://\(relayHost)"
)
let relayUrlFactory = RelayUrlFactory(
relayHost: relayHost,
diff --git a/Sources/WalletConnectRelay/RelayURLFactory.swift b/Sources/WalletConnectRelay/RelayURLFactory.swift
index d3b9d155c..ec35ce87c 100644
--- a/Sources/WalletConnectRelay/RelayURLFactory.swift
+++ b/Sources/WalletConnectRelay/RelayURLFactory.swift
@@ -3,12 +3,12 @@ import Foundation
struct RelayUrlFactory {
private let relayHost: String
private let projectId: String
- private let socketAuthenticator: SocketAuthenticating
+ private let socketAuthenticator: ClientIdAuthenticating
init(
relayHost: String,
projectId: String,
- socketAuthenticator: SocketAuthenticating
+ socketAuthenticator: ClientIdAuthenticating
) {
self.relayHost = relayHost
self.projectId = projectId
diff --git a/Sources/WalletConnectSign/Session.swift b/Sources/WalletConnectSign/Session.swift
index c880f6cd0..ef3a2205f 100644
--- a/Sources/WalletConnectSign/Session.swift
+++ b/Sources/WalletConnectSign/Session.swift
@@ -41,4 +41,10 @@ extension Session {
SessionType.EventParams.Event(name: name, data: data)
}
}
+
+ public var accounts: [Account] {
+ return namespaces.values.reduce(into: []) { result, namespace in
+ result = result + Array(namespace.accounts)
+ }
+ }
}
diff --git a/Sources/WalletConnectSign/Sign/SignClientProtocol.swift b/Sources/WalletConnectSign/Sign/SignClientProtocol.swift
index a1dcfc3f3..397a59bf4 100644
--- a/Sources/WalletConnectSign/Sign/SignClientProtocol.swift
+++ b/Sources/WalletConnectSign/Sign/SignClientProtocol.swift
@@ -9,7 +9,10 @@ public protocol SignClientProtocol {
var sessionSettlePublisher: AnyPublisher { get }
var sessionDeletePublisher: AnyPublisher<(String, Reason), Never> { get }
var sessionResponsePublisher: AnyPublisher { get }
+ var sessionRejectionPublisher: AnyPublisher<(Session.Proposal, Reason), Never> { get }
+ func connect(requiredNamespaces: [String: ProposalNamespace], optionalNamespaces: [String: ProposalNamespace]?, sessionProperties: [String: String]?, topic: String) async throws
+ func request(params: Request) async throws
func approve(proposalId: String, namespaces: [String: SessionNamespace]) async throws
func reject(proposalId: String, reason: RejectionReason) async throws
func update(topic: String, namespaces: [String: SessionNamespace]) async throws
diff --git a/Sources/WalletConnectSigner/BIP44Provider.swift b/Sources/WalletConnectSigner/BIP44Provider.swift
new file mode 100644
index 000000000..806fe0c5e
--- /dev/null
+++ b/Sources/WalletConnectSigner/BIP44Provider.swift
@@ -0,0 +1,10 @@
+import Foundation
+
+public enum DerivationPath {
+ case hardened(UInt32)
+ case notHardened(UInt32)
+}
+
+public protocol BIP44Provider {
+ func derive(entropy: Data, path: [DerivationPath]) -> Data
+}
diff --git a/Sources/WalletConnectSync/Services/SyncDerivationService.swift b/Sources/WalletConnectSync/Services/SyncDerivationService.swift
new file mode 100644
index 000000000..d859f00fc
--- /dev/null
+++ b/Sources/WalletConnectSync/Services/SyncDerivationService.swift
@@ -0,0 +1,63 @@
+import Foundation
+
+final class SyncDerivationService {
+
+ private let syncStorage: SyncSignatureStore
+ private let bip44: BIP44Provider
+ private let kms: KeyManagementServiceProtocol
+
+ init(
+ syncStorage: SyncSignatureStore,
+ bip44: BIP44Provider,
+ kms: KeyManagementServiceProtocol
+ ) {
+ self.syncStorage = syncStorage
+ self.bip44 = bip44
+ self.kms = kms
+ }
+
+ func deriveTopic(account: Account, store: String) throws -> String {
+ let signature = try syncStorage.getSignature(for: account)
+
+ guard let signatureData = signature.data(using: .utf8) else {
+ throw Errors.signatureIsNotUTF8
+ }
+
+ let slice = store.components(withMaxLength: 4)
+ .compactMap { $0.data(using: .utf8) }
+ .compactMap { UInt32($0.toHexString(), radix: 16) }
+
+ let path: [DerivationPath] = [
+ .hardened(77),
+ .hardened(0),
+ .notHardened(0)
+ ] + slice.map { .notHardened($0) }
+
+ let entropy = signatureData.sha256()
+ let storeKey = bip44.derive(entropy: entropy, path: path)
+ let topic = storeKey.sha256().toHexString()
+
+ let symmetricKey = try SymmetricKey(rawRepresentation: storeKey)
+ try kms.setSymmetricKey(symmetricKey, for: topic)
+
+ return topic
+ }
+}
+
+private extension SyncDerivationService {
+
+ enum Errors: Error {
+ case signatureIsNotUTF8
+ }
+}
+
+fileprivate extension String {
+
+ func components(withMaxLength length: Int) -> [String] {
+ return stride(from: 0, to: count, by: length).map {
+ let start = index(startIndex, offsetBy: $0)
+ let end = index(start, offsetBy: length, limitedBy: endIndex) ?? endIndex
+ return String(self[start..()
+
+ var updatePublisher: AnyPublisher<(String, StoreUpdate), Never> {
+ return updateSubject.eraseToAnyPublisher()
+ }
+
+ private var publishers: Set = []
+
+ private let networkInteractor: NetworkInteracting
+ private let derivationService: SyncDerivationService
+ private let signatureStore: SyncSignatureStore
+ private let historyStore: SyncHistoryStore
+ private let logger: ConsoleLogging
+
+ /// `account` to `Record` keyValue store
+ private let indexStore: SyncIndexStore
+
+ init(networkInteractor: NetworkInteracting, derivationService: SyncDerivationService, signatureStore: SyncSignatureStore, indexStore: SyncIndexStore, historyStore: SyncHistoryStore, logger: ConsoleLogging) {
+ self.networkInteractor = networkInteractor
+ self.derivationService = derivationService
+ self.signatureStore = signatureStore
+ self.indexStore = indexStore
+ self.historyStore = historyStore
+ self.logger = logger
+
+ setupSubscriptions()
+ }
+
+ func set(account: Account, store: String, object: Object) async throws {
+ let protocolMethod = SyncSetMethod()
+ let params = StoreSet(key: object.databaseId, value: try object.json())
+ let rpcid = RPCID()
+ let request = RPCRequest(method: protocolMethod.method, params: params, rpcid: rpcid)
+ let record = try indexStore.getRecord(account: account, name: store)
+
+ try await networkInteractor.request(request, topic: record.topic, protocolMethod: protocolMethod)
+
+ historyStore.set(rpcid: rpcid.integer, topic: record.topic)
+
+ logger.debug("Did set value for \(store). Sent on \(record.topic). Object: \n\(object)\n")
+ }
+
+ func delete(account: Account, store: String, key: String) async throws {
+ let protocolMethod = SyncDeleteMethod()
+ let rpcid = RPCID()
+ let request = RPCRequest(method: protocolMethod.method, params: ["key": key], rpcid: rpcid)
+ let record = try indexStore.getRecord(account: account, name: store)
+
+ try await networkInteractor.request(request, topic: record.topic, protocolMethod: protocolMethod)
+
+ historyStore.set(rpcid: rpcid.integer, topic: record.topic)
+
+ 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 {
+
+ enum Errors: Error {
+ case recordNotFoundForAccount
+ }
+
+ func setupSubscriptions() {
+ networkInteractor.requestSubscription(on: SyncSetMethod())
+ .sink { [unowned self] (payload: RequestSubscriptionPayload) in
+ if historyStore.update(topic: payload.topic, rpcid: payload.id) {
+ self.updateSubject.send((payload.topic, .set(payload.request)))
+ }
+ }
+ .store(in: &publishers)
+
+ networkInteractor.requestSubscription(on: SyncDeleteMethod())
+ .sink { [unowned self] (payload: RequestSubscriptionPayload) in
+ if historyStore.update(topic: payload.topic, rpcid: payload.id) {
+ self.updateSubject.send((payload.topic, .delete(payload.request)))
+ }
+ }
+ .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/SyncHistoryStore.swift b/Sources/WalletConnectSync/Stores/SyncHistoryStore.swift
new file mode 100644
index 000000000..a2f290074
--- /dev/null
+++ b/Sources/WalletConnectSync/Stores/SyncHistoryStore.swift
@@ -0,0 +1,29 @@
+import Foundation
+
+final class SyncHistoryStore {
+
+ /// `topic` to `rpcid` keyValue store
+ private let store: CodableStore
+
+ init(store: CodableStore) {
+ self.store = store
+ }
+
+ func set(rpcid: Int64, topic: String) {
+ store.set(rpcid, forKey: topic)
+ }
+
+ func update(topic: String, rpcid: RPCID) -> Bool {
+ guard isNew(topic: topic, rpcid: rpcid) else { return false }
+ store.set(rpcid.integer, forKey: topic)
+ return true
+ }
+}
+
+private extension SyncHistoryStore {
+
+ func isNew(topic: String, rpcid: RPCID) -> Bool {
+ guard let old = try? store.get(key: topic) else { return true }
+ return old < rpcid.integer
+ }
+}
diff --git a/Sources/WalletConnectSync/Stores/SyncIndexStore.swift b/Sources/WalletConnectSync/Stores/SyncIndexStore.swift
new file mode 100644
index 000000000..e92def273
--- /dev/null
+++ b/Sources/WalletConnectSync/Stores/SyncIndexStore.swift
@@ -0,0 +1,44 @@
+import Foundation
+
+final class SyncIndexStore {
+
+ /// `account-store` to SyncRecord map keyValue store
+ private let store: CodableStore
+
+ init(store: CodableStore) {
+ self.store = store
+ }
+
+ func getRecord(account: Account, name: String) throws -> SyncRecord {
+ let identifier = identifier(account: account, name: name)
+ guard let record = try store.get(key: identifier) else {
+ throw Errors.recordNotFoundForAccount
+ }
+ return record
+ }
+
+ func getRecord(topic: String) throws -> SyncRecord {
+ guard let record = store.getAll().first(where: { $0.topic == topic }) else {
+ throw Errors.accountNotFoundForTopic
+ }
+ return record
+ }
+
+ func set(topic: String, name: String, account: Account) {
+ let identifier = identifier(account: account, name: name)
+ let record = SyncRecord(topic: topic, store: name, account: account)
+ store.set(record, forKey: identifier)
+ }
+}
+
+private extension SyncIndexStore {
+
+ enum Errors: Error {
+ case recordNotFoundForAccount
+ case accountNotFoundForTopic
+ }
+
+ func identifier(account: Account, name: String) -> String {
+ return "\(account.absoluteString)-\(name)"
+ }
+}
diff --git a/Sources/WalletConnectSync/Stores/SyncSignatureStore.swift b/Sources/WalletConnectSync/Stores/SyncSignatureStore.swift
new file mode 100644
index 000000000..e8c5b69c0
--- /dev/null
+++ b/Sources/WalletConnectSync/Stores/SyncSignatureStore.swift
@@ -0,0 +1,39 @@
+import Foundation
+import Combine
+
+final class SyncSignatureStore {
+
+ private let keychain: KeychainStorageProtocol
+
+ init(keychain: KeychainStorageProtocol) {
+ self.keychain = keychain
+ }
+
+ func saveSignature(_ key: String, for account: Account) throws {
+ try keychain.add(key, forKey: signatureIdentifier(for: account))
+ }
+
+ func getSignature(for account: Account) throws -> String {
+ let identifier = signatureIdentifier(for: account)
+
+ guard let key: String = try? keychain.read(key: identifier)
+ else { throw Errors.signatureNotFound }
+
+ return key
+ }
+
+ func isSignatureExists(account: Account) -> Bool {
+ return (try? getSignature(for: account)) != nil
+ }
+}
+
+private extension SyncSignatureStore {
+
+ enum Errors: Error {
+ case signatureNotFound
+ }
+
+ func signatureIdentifier(for account: Account) -> String {
+ return "com.walletconnect.sync.signature.\(account.absoluteString)"
+ }
+}
diff --git a/Sources/WalletConnectSync/Stores/SyncStore.swift b/Sources/WalletConnectSync/Stores/SyncStore.swift
new file mode 100644
index 000000000..57b5320b4
--- /dev/null
+++ b/Sources/WalletConnectSync/Stores/SyncStore.swift
@@ -0,0 +1,112 @@
+import Foundation
+import Combine
+
+public enum SyncUpdate {
+ case set(object: Object)
+ case delete(id: String)
+}
+
+public final class SyncStore {
+
+ private var publishers = Set()
+
+ private let name: String
+ private let syncClient: SyncClient
+
+ /// `account` to `Record` keyValue store
+ private let indexStore: SyncIndexStore
+
+ /// `storeTopic` to [`id`: `Object`] map keyValue store
+ private let objectStore: KeyedDatabase