From 55e057c42a108d6877de9cc6ffab758c7811f6f6 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 23 Aug 2022 21:00:27 +0300 Subject: [PATCH 01/92] Wallet: Pairing --- Example/ExampleApp.xcodeproj/project.pbxproj | 93 +++++++++++++++++-- Example/IntegrationTests/Auth/AuthTests.swift | 6 +- .../ApplicationLayer/Application.swift | 2 +- .../ApplicationConfigurator.swift | 2 +- .../Configurator/ThirdPartyConfigurator.swift | 14 +++ .../ApplicationLayer/SceneDelegate.swift | 2 +- .../DomainLayer/Chat/ChatFactory.swift | 5 +- .../Chat/{ => Chat}/ChatInteractor.swift | 0 .../Chat/{ => Chat}/ChatModule.swift | 0 .../Chat/{ => Chat}/ChatPresenter.swift | 0 .../Chat/{ => Chat}/ChatRouter.swift | 0 .../Chat/{ => Chat}/ChatView.swift | 0 .../{ => Chat}/Models/MessageViewModel.swift | 0 .../{ => Chat}/Views/ChatScrollView.swift | 0 .../{ => Chat}/Views/ContentMessageView.swift | 0 .../Chat/{ => Chat}/Views/MessageView.swift | 0 .../ChatList/ChatListInteractor.swift | 0 .../{ => Chat}/ChatList/ChatListModule.swift | 0 .../ChatList/ChatListPresenter.swift | 2 +- .../{ => Chat}/ChatList/ChatListRouter.swift | 4 +- .../{ => Chat}/ChatList/ChatListView.swift | 0 .../ChatList/Models/ThreadViewModel.swift | 0 .../{ => Chat}/Import/ImportInteractor.swift | 0 .../{ => Chat}/Import/ImportModule.swift | 0 .../{ => Chat}/Import/ImportPresenter.swift | 0 .../{ => Chat}/Import/ImportRouter.swift | 0 .../{ => Chat}/Import/ImportView.swift | 0 .../{ => Chat}/Invite/InviteInteractor.swift | 0 .../{ => Chat}/Invite/InviteModule.swift | 0 .../{ => Chat}/Invite/InvitePresenter.swift | 0 .../{ => Chat}/Invite/InviteRouter.swift | 0 .../{ => Chat}/Invite/InviteView.swift | 0 .../InviteList/InviteListInteractor.swift | 0 .../InviteList/InviteListModule.swift | 0 .../InviteList/InviteListPresenter.swift | 0 .../InviteList/InviteListRouter.swift | 0 .../InviteList/InviteListView.swift | 0 .../InviteList/Models/InviteViewModel.swift | 0 .../{ => Chat}/Main/MainModule.swift | 0 .../{ => Chat}/Main/MainPresenter.swift | 7 +- .../{ => Chat}/Main/MainRouter.swift | 4 + .../{ => Chat}/Main/MainViewController.swift | 0 .../Chat/Main/Model/TabPage.swift | 32 +++++++ .../Welcome/WelcomeInteractor.swift | 0 .../{ => Chat}/Welcome/WelcomeModule.swift | 0 .../{ => Chat}/Welcome/WelcomePresenter.swift | 0 .../{ => Chat}/Welcome/WelcomeRouter.swift | 0 .../{ => Chat}/Welcome/WelcomeView.swift | 0 .../Main/Model/TabPage.swift | 47 ---------- .../AuthRequest/AuthRequestInteractor.swift | 3 + .../AuthRequest/AuthRequestModule.swift | 19 ++++ .../AuthRequest/AuthRequestPresenter.swift | 37 ++++++++ .../AuthRequest/AuthRequestRouter.swift | 12 +++ .../Wallet/AuthRequest/AuthRequestView.swift | 18 ++++ .../Wallet/Wallet/WalletInteractor.swift | 13 +++ .../Wallet/Wallet/WalletModule.swift | 18 ++++ .../Wallet/Wallet/WalletPresenter.swift | 48 ++++++++++ .../Wallet/Wallet/WalletRouter.swift | 17 ++++ .../Wallet/Wallet/WalletView.swift | 32 +++++++ Sources/Auth/AuthClient.swift | 8 +- .../Wallet/WalletRequestSubscriber.swift | 4 +- 61 files changed, 371 insertions(+), 78 deletions(-) rename Example/Showcase/Classes/PresentationLayer/Chat/{ => Chat}/ChatInteractor.swift (100%) rename Example/Showcase/Classes/PresentationLayer/Chat/{ => Chat}/ChatModule.swift (100%) rename Example/Showcase/Classes/PresentationLayer/Chat/{ => Chat}/ChatPresenter.swift (100%) rename Example/Showcase/Classes/PresentationLayer/Chat/{ => Chat}/ChatRouter.swift (100%) rename Example/Showcase/Classes/PresentationLayer/Chat/{ => Chat}/ChatView.swift (100%) rename Example/Showcase/Classes/PresentationLayer/Chat/{ => Chat}/Models/MessageViewModel.swift (100%) rename Example/Showcase/Classes/PresentationLayer/Chat/{ => Chat}/Views/ChatScrollView.swift (100%) rename Example/Showcase/Classes/PresentationLayer/Chat/{ => Chat}/Views/ContentMessageView.swift (100%) rename Example/Showcase/Classes/PresentationLayer/Chat/{ => Chat}/Views/MessageView.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/ChatList/ChatListInteractor.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/ChatList/ChatListModule.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/ChatList/ChatListPresenter.swift (98%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/ChatList/ChatListRouter.swift (89%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/ChatList/ChatListView.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/ChatList/Models/ThreadViewModel.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/Import/ImportInteractor.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/Import/ImportModule.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/Import/ImportPresenter.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/Import/ImportRouter.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/Import/ImportView.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/Invite/InviteInteractor.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/Invite/InviteModule.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/Invite/InvitePresenter.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/Invite/InviteRouter.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/Invite/InviteView.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/InviteList/InviteListInteractor.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/InviteList/InviteListModule.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/InviteList/InviteListPresenter.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/InviteList/InviteListRouter.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/InviteList/InviteListView.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/InviteList/Models/InviteViewModel.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/Main/MainModule.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/Main/MainPresenter.swift (64%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/Main/MainRouter.swift (68%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/Main/MainViewController.swift (100%) create mode 100644 Example/Showcase/Classes/PresentationLayer/Chat/Main/Model/TabPage.swift rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/Welcome/WelcomeInteractor.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/Welcome/WelcomeModule.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/Welcome/WelcomePresenter.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/Welcome/WelcomeRouter.swift (100%) rename Example/Showcase/Classes/PresentationLayer/{ => Chat}/Welcome/WelcomeView.swift (100%) delete mode 100644 Example/Showcase/Classes/PresentationLayer/Main/Model/TabPage.swift create mode 100644 Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestInteractor.swift create mode 100644 Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestModule.swift create mode 100644 Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift create mode 100644 Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestRouter.swift create mode 100644 Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestView.swift create mode 100644 Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletInteractor.swift create mode 100644 Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletModule.swift create mode 100644 Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift create mode 100644 Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletRouter.swift create mode 100644 Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletView.swift diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 3bc7a26a5..24c1ebdfd 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -103,6 +103,17 @@ A58E7D432872EE320082D443 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58E7D422872EE320082D443 /* MessageView.swift */; }; A58E7D452872EE570082D443 /* ContentMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58E7D442872EE570082D443 /* ContentMessageView.swift */; }; A58E7D482872EF610082D443 /* MessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58E7D472872EF610082D443 /* MessageViewModel.swift */; }; + A59EBEF828B54A2A003EDAAF /* AuthRequestModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59EBEF328B54A2A003EDAAF /* AuthRequestModule.swift */; }; + A59EBEF928B54A2A003EDAAF /* AuthRequestPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59EBEF428B54A2A003EDAAF /* AuthRequestPresenter.swift */; }; + A59EBEFA28B54A2A003EDAAF /* AuthRequestRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59EBEF528B54A2A003EDAAF /* AuthRequestRouter.swift */; }; + A59EBEFB28B54A2A003EDAAF /* AuthRequestInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59EBEF628B54A2A003EDAAF /* AuthRequestInteractor.swift */; }; + A59EBEFC28B54A2A003EDAAF /* AuthRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59EBEF728B54A2A003EDAAF /* AuthRequestView.swift */; }; + A59F876F28B53EA000A9CD80 /* WalletModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59F876A28B53EA000A9CD80 /* WalletModule.swift */; }; + A59F877028B53EA000A9CD80 /* WalletPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59F876B28B53EA000A9CD80 /* WalletPresenter.swift */; }; + A59F877128B53EA000A9CD80 /* WalletRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59F876C28B53EA000A9CD80 /* WalletRouter.swift */; }; + A59F877228B53EA000A9CD80 /* WalletInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59F876D28B53EA000A9CD80 /* WalletInteractor.swift */; }; + A59F877328B53EA000A9CD80 /* WalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59F876E28B53EA000A9CD80 /* WalletView.swift */; }; + A59F877628B5462900A9CD80 /* WalletConnectAuth in Frameworks */ = {isa = PBXBuildFile; productRef = A59F877528B5462900A9CD80 /* WalletConnectAuth */; }; A5A4FC56283CBB7800BBEC1E /* SessionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A4FC55283CBB7800BBEC1E /* SessionDetailView.swift */; }; A5A4FC58283CBB9F00BBEC1E /* SessionDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A4FC57283CBB9F00BBEC1E /* SessionDetailViewModel.swift */; }; A5A4FC5A283CC08600BBEC1E /* SessionNamespaceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A4FC59283CC08600BBEC1E /* SessionNamespaceViewModel.swift */; }; @@ -265,6 +276,16 @@ A58E7D422872EE320082D443 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; A58E7D442872EE570082D443 /* ContentMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentMessageView.swift; sourceTree = ""; }; A58E7D472872EF610082D443 /* MessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageViewModel.swift; sourceTree = ""; }; + A59EBEF328B54A2A003EDAAF /* AuthRequestModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthRequestModule.swift; sourceTree = ""; }; + A59EBEF428B54A2A003EDAAF /* AuthRequestPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthRequestPresenter.swift; sourceTree = ""; }; + A59EBEF528B54A2A003EDAAF /* AuthRequestRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthRequestRouter.swift; sourceTree = ""; }; + A59EBEF628B54A2A003EDAAF /* AuthRequestInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthRequestInteractor.swift; sourceTree = ""; }; + A59EBEF728B54A2A003EDAAF /* AuthRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthRequestView.swift; sourceTree = ""; }; + A59F876A28B53EA000A9CD80 /* WalletModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletModule.swift; sourceTree = ""; }; + A59F876B28B53EA000A9CD80 /* WalletPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletPresenter.swift; sourceTree = ""; }; + A59F876C28B53EA000A9CD80 /* WalletRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletRouter.swift; sourceTree = ""; }; + A59F876D28B53EA000A9CD80 /* WalletInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletInteractor.swift; sourceTree = ""; }; + A59F876E28B53EA000A9CD80 /* WalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletView.swift; sourceTree = ""; }; A5A4FC55283CBB7800BBEC1E /* SessionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDetailView.swift; sourceTree = ""; }; A5A4FC57283CBB9F00BBEC1E /* SessionDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDetailViewModel.swift; sourceTree = ""; }; A5A4FC59283CC08600BBEC1E /* SessionNamespaceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionNamespaceViewModel.swift; sourceTree = ""; }; @@ -330,6 +351,7 @@ files = ( A5629AEA2877F2D600094373 /* WalletConnectChat in Frameworks */, A5629AF22877F75100094373 /* Starscream in Frameworks */, + A59F877628B5462900A9CD80 /* WalletConnectAuth in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -713,13 +735,8 @@ A58E7D062872A4390082D443 /* PresentationLayer */ = { isa = PBXGroup; children = ( - A5C20213287E1FC8007E3188 /* Import */, - A5C20205287D9DB9007E3188 /* Welcome */, - A5629AD82876CC5B00094373 /* InviteList */, - A5629ACD2876CC4A00094373 /* Invite */, - A5629AB72876CBA700094373 /* ChatList */, - A58E7D332872D55F0082D443 /* Chat */, - A58E7D282872D52A0082D443 /* Main */, + A59F876928B53E7800A9CD80 /* Wallet */, + A59F876828B53E6400A9CD80 /* Chat */, ); path = PresentationLayer; sourceTree = ""; @@ -799,6 +816,53 @@ path = Models; sourceTree = ""; }; + A59EBEFD28B54A2E003EDAAF /* AuthRequest */ = { + isa = PBXGroup; + children = ( + A59EBEF328B54A2A003EDAAF /* AuthRequestModule.swift */, + A59EBEF428B54A2A003EDAAF /* AuthRequestPresenter.swift */, + A59EBEF528B54A2A003EDAAF /* AuthRequestRouter.swift */, + A59EBEF628B54A2A003EDAAF /* AuthRequestInteractor.swift */, + A59EBEF728B54A2A003EDAAF /* AuthRequestView.swift */, + ); + path = AuthRequest; + sourceTree = ""; + }; + A59F876828B53E6400A9CD80 /* Chat */ = { + isa = PBXGroup; + children = ( + A5C20213287E1FC8007E3188 /* Import */, + A5C20205287D9DB9007E3188 /* Welcome */, + A5629AD82876CC5B00094373 /* InviteList */, + A5629ACD2876CC4A00094373 /* Invite */, + A5629AB72876CBA700094373 /* ChatList */, + A58E7D332872D55F0082D443 /* Chat */, + A58E7D282872D52A0082D443 /* Main */, + ); + path = Chat; + sourceTree = ""; + }; + A59F876928B53E7800A9CD80 /* Wallet */ = { + isa = PBXGroup; + children = ( + A59EBEFD28B54A2E003EDAAF /* AuthRequest */, + A59F877428B53EA500A9CD80 /* Wallet */, + ); + path = Wallet; + sourceTree = ""; + }; + A59F877428B53EA500A9CD80 /* Wallet */ = { + isa = PBXGroup; + children = ( + A59F876A28B53EA000A9CD80 /* WalletModule.swift */, + A59F876B28B53EA000A9CD80 /* WalletPresenter.swift */, + A59F876C28B53EA000A9CD80 /* WalletRouter.swift */, + A59F876D28B53EA000A9CD80 /* WalletInteractor.swift */, + A59F876E28B53EA000A9CD80 /* WalletView.swift */, + ); + path = Wallet; + sourceTree = ""; + }; A5A4FC732840C12C00BBEC1E /* UITests */ = { isa = PBXGroup; children = ( @@ -992,6 +1056,7 @@ packageProductDependencies = ( A5629AE92877F2D600094373 /* WalletConnectChat */, A5629AF12877F75100094373 /* Starscream */, + A59F877528B5462900A9CD80 /* WalletConnectAuth */, ); productName = Showcase; productReference = A58E7CE828729F550082D443 /* Showcase.app */; @@ -1203,12 +1268,18 @@ A58E7D0E2872A45B0082D443 /* MainRouter.swift in Sources */, A58E7D432872EE320082D443 /* MessageView.swift in Sources */, A58E7D3F2872E99A0082D443 /* TabPage.swift in Sources */, + A59EBEFB28B54A2A003EDAAF /* AuthRequestInteractor.swift in Sources */, + A59F877228B53EA000A9CD80 /* WalletInteractor.swift in Sources */, A58E7D3C2872D55F0082D443 /* ChatPresenter.swift in Sources */, + A59EBEF928B54A2A003EDAAF /* AuthRequestPresenter.swift in Sources */, A5C2020B287D9DEE007E3188 /* WelcomeModule.swift in Sources */, + A59F876F28B53EA000A9CD80 /* WalletModule.swift in Sources */, A58E7D152872A5410082D443 /* UIViewController.swift in Sources */, + A59EBEF828B54A2A003EDAAF /* AuthRequestModule.swift in Sources */, A58E7D132872A4A80082D443 /* Application.swift in Sources */, A58E7D002872A1050082D443 /* SceneViewController.swift in Sources */, A58E7D242872AB130082D443 /* MainViewController.swift in Sources */, + A59EBEFC28B54A2A003EDAAF /* AuthRequestView.swift in Sources */, A5629AE02876CC6E00094373 /* InviteListRouter.swift in Sources */, A58E7D032872A1630082D443 /* String.swift in Sources */, A58E7D3D2872D55F0082D443 /* ChatView.swift in Sources */, @@ -1218,6 +1289,7 @@ A5629ADE2876CC6E00094373 /* InviteListModule.swift in Sources */, A578FA322873036400AA7720 /* InputView.swift in Sources */, A5C2021B287E1FD8007E3188 /* ImportRouter.swift in Sources */, + A59F877328B53EA000A9CD80 /* WalletView.swift in Sources */, A5629AE42876E6D200094373 /* ThreadViewModel.swift in Sources */, A58E7D0C2872A45B0082D443 /* MainModule.swift in Sources */, A5C2021C287E1FD8007E3188 /* ImportInteractor.swift in Sources */, @@ -1227,6 +1299,7 @@ A5629AE12876CC6E00094373 /* InviteListInteractor.swift in Sources */, A58E7CED28729F550082D443 /* SceneDelegate.swift in Sources */, A5C2020F287D9DEE007E3188 /* WelcomeView.swift in Sources */, + A59F877128B53EA000A9CD80 /* WalletRouter.swift in Sources */, A5C20226287EB099007E3188 /* AccountNameResolver.swift in Sources */, A5C2020D287D9DEE007E3188 /* WelcomeRouter.swift in Sources */, A578FA372873D8EE00AA7720 /* UIColor.swift in Sources */, @@ -1239,6 +1312,7 @@ A5629AE22876CC6E00094373 /* InviteListView.swift in Sources */, A578FA3D2874002400AA7720 /* View.swift in Sources */, A5629AD72876CC5700094373 /* InviteView.swift in Sources */, + A59EBEFA28B54A2A003EDAAF /* AuthRequestRouter.swift in Sources */, A5629AF02877F73000094373 /* SocketFactory.swift in Sources */, A5629AED2877F6A600094373 /* ChatFactory.swift in Sources */, A5C20221287EA5B8007E3188 /* TextFieldView.swift in Sources */, @@ -1246,6 +1320,7 @@ A5C2020C287D9DEE007E3188 /* WelcomePresenter.swift in Sources */, A58E7D1D2872A57B0082D443 /* Configurator.swift in Sources */, A58E7D482872EF610082D443 /* MessageViewModel.swift in Sources */, + A59F877028B53EA000A9CD80 /* WalletPresenter.swift in Sources */, A5629AD32876CC5700094373 /* InviteModule.swift in Sources */, A5629AD52876CC5700094373 /* InviteRouter.swift in Sources */, A5629ABF2876CBC000094373 /* ChatListRouter.swift in Sources */, @@ -1800,6 +1875,10 @@ package = A5D85224286333D500DAF5C3 /* XCRemoteSwiftPackageReference "Starscream" */; productName = Starscream; }; + A59F877528B5462900A9CD80 /* WalletConnectAuth */ = { + isa = XCSwiftPackageProductDependency; + productName = WalletConnectAuth; + }; A5AE354628A1A2AC0059AE8A /* Web3 */ = { isa = XCSwiftPackageProductDependency; package = A5AE354528A1A2AC0059AE8A /* XCRemoteSwiftPackageReference "Web3" */; diff --git a/Example/IntegrationTests/Auth/AuthTests.swift b/Example/IntegrationTests/Auth/AuthTests.swift index 731d74b14..eb18be0e9 100644 --- a/Example/IntegrationTests/Auth/AuthTests.swift +++ b/Example/IntegrationTests/Auth/AuthTests.swift @@ -65,11 +65,11 @@ final class AuthTests: XCTestCase { let responseExpectation = expectation(description: "successful response delivered") let uri = try! await app.request(RequestParams.stub()) try! await wallet.pair(uri: uri) - wallet.authRequestPublisher.sink { [unowned self] (id, message) in + wallet.authRequestPublisher.sink { [unowned self] request in Task(priority: .high) { - let signature = try! MessageSigner().sign(message: message, privateKey: prvKey) + let signature = try! MessageSigner().sign(message: request.message, privateKey: prvKey) let cacaoSignature = CacaoSignature(t: "eip191", s: signature) - try! await wallet.respond(requestId: id, signature: cacaoSignature) + try! await wallet.respond(requestId: request.id, signature: cacaoSignature) } } .store(in: &publishers) diff --git a/Example/Showcase/Classes/ApplicationLayer/Application.swift b/Example/Showcase/Classes/ApplicationLayer/Application.swift index 03e514790..bd59d3dd3 100644 --- a/Example/Showcase/Classes/ApplicationLayer/Application.swift +++ b/Example/Showcase/Classes/ApplicationLayer/Application.swift @@ -3,7 +3,7 @@ import Chat final class Application { - let chatService: ChatService = { + lazy var chatService: ChatService = { return ChatService(client: ChatFactory.create()) }() diff --git a/Example/Showcase/Classes/ApplicationLayer/Configurator/ApplicationConfigurator.swift b/Example/Showcase/Classes/ApplicationLayer/Configurator/ApplicationConfigurator.swift index 810260eef..a50e9c6e6 100644 --- a/Example/Showcase/Classes/ApplicationLayer/Configurator/ApplicationConfigurator.swift +++ b/Example/Showcase/Classes/ApplicationLayer/Configurator/ApplicationConfigurator.swift @@ -11,6 +11,6 @@ struct ApplicationConfigurator: Configurator { } func configure() { - WelcomeModule.create(app: app).present() + MainModule.create(app: app).present() } } diff --git a/Example/Showcase/Classes/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift b/Example/Showcase/Classes/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift index f07e0c5bc..b6596d99e 100644 --- a/Example/Showcase/Classes/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift +++ b/Example/Showcase/Classes/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift @@ -1,6 +1,20 @@ +import WalletConnectRelay +import WalletConnectPairing +import Auth + struct ThirdPartyConfigurator: Configurator { func configure() { + Relay.configure(projectId: "relay.walletconnect.com", socketFactory: SocketFactory()) + Auth.configure( + metadata: AppMetadata( + name: "Showcase App", + description: "Showcase description", + url: "example.wallet", + icons: ["https://avatars.githubusercontent.com/u/37784886"] + ), + account: Account("eip155:56:0xe5EeF1368781911d265fDB6946613dA61915a501")! + ) } } diff --git a/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift b/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift index 18f786cbe..08d86bdbc 100644 --- a/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift +++ b/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift @@ -9,9 +9,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { private var configurators: [Configurator] { return [ MigrationConfigurator(app: app), + ThirdPartyConfigurator(), ApplicationConfigurator(app: app), AppearanceConfigurator(), - ThirdPartyConfigurator() ] } diff --git a/Example/Showcase/Classes/DomainLayer/Chat/ChatFactory.swift b/Example/Showcase/Classes/DomainLayer/Chat/ChatFactory.swift index fe4d61fa7..b47b23b17 100644 --- a/Example/Showcase/Classes/DomainLayer/Chat/ChatFactory.swift +++ b/Example/Showcase/Classes/DomainLayer/Chat/ChatFactory.swift @@ -7,15 +7,12 @@ import WalletConnectUtils class ChatFactory { static func create() -> ChatClient { - let relayHost = "relay.walletconnect.com" - let projectId = "8ba9ee138960775e5231b70cc5ef1c3a" let keychain = KeychainStorage(serviceIdentifier: "com.walletconnect.showcase") let client = HTTPClient(host: "keys.walletconnect.com") let registry = KeyserverRegistryProvider(client: client) - let relayClient = RelayClient(relayHost: relayHost, projectId: projectId, keychainStorage: keychain, socketFactory: SocketFactory()) return ChatClientFactory.create( registry: registry, - relayClient: relayClient, + relayClient: Relay.instance, kms: KeyManagementService(keychain: keychain), logger: ConsoleLogger(), keyValueStorage: UserDefaults.standard diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/ChatInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatInteractor.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Chat/ChatInteractor.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatInteractor.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/ChatModule.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatModule.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Chat/ChatModule.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatModule.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/ChatPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatPresenter.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Chat/ChatPresenter.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatPresenter.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/ChatRouter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatRouter.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Chat/ChatRouter.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatRouter.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/ChatView.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatView.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Chat/ChatView.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Chat/ChatView.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Models/MessageViewModel.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Chat/Models/MessageViewModel.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Chat/Models/MessageViewModel.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Chat/Models/MessageViewModel.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Views/ChatScrollView.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Chat/Views/ChatScrollView.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Chat/Views/ChatScrollView.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Chat/Views/ChatScrollView.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Views/ContentMessageView.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Chat/Views/ContentMessageView.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Chat/Views/ContentMessageView.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Chat/Views/ContentMessageView.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Views/MessageView.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Chat/Views/MessageView.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Chat/Views/MessageView.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Chat/Views/MessageView.swift diff --git a/Example/Showcase/Classes/PresentationLayer/ChatList/ChatListInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListInteractor.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/ChatList/ChatListInteractor.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListInteractor.swift diff --git a/Example/Showcase/Classes/PresentationLayer/ChatList/ChatListModule.swift b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListModule.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/ChatList/ChatListModule.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListModule.swift diff --git a/Example/Showcase/Classes/PresentationLayer/ChatList/ChatListPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListPresenter.swift similarity index 98% rename from Example/Showcase/Classes/PresentationLayer/ChatList/ChatListPresenter.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListPresenter.swift index e648ab2eb..a839d8dd7 100644 --- a/Example/Showcase/Classes/PresentationLayer/ChatList/ChatListPresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListPresenter.swift @@ -44,7 +44,7 @@ final class ChatListPresenter: ObservableObject { func didLogoutPress() { interactor.logout() - router.presentWelcome() + router.presentMain() } func didPressNewChat() { diff --git a/Example/Showcase/Classes/PresentationLayer/ChatList/ChatListRouter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListRouter.swift similarity index 89% rename from Example/Showcase/Classes/PresentationLayer/ChatList/ChatListRouter.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListRouter.swift index e41c699d8..21f8332aa 100644 --- a/Example/Showcase/Classes/PresentationLayer/ChatList/ChatListRouter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListRouter.swift @@ -25,7 +25,7 @@ final class ChatListRouter { ChatModule.create(thread: thread, app: app).push(from: viewController) } - func presentWelcome() { - WelcomeModule.create(app: app).present() + func presentMain() { + MainModule.create(app: app).present() } } diff --git a/Example/Showcase/Classes/PresentationLayer/ChatList/ChatListView.swift b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListView.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/ChatList/ChatListView.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/ChatList/ChatListView.swift diff --git a/Example/Showcase/Classes/PresentationLayer/ChatList/Models/ThreadViewModel.swift b/Example/Showcase/Classes/PresentationLayer/Chat/ChatList/Models/ThreadViewModel.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/ChatList/Models/ThreadViewModel.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/ChatList/Models/ThreadViewModel.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Import/ImportInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportInteractor.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Import/ImportInteractor.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportInteractor.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Import/ImportModule.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportModule.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Import/ImportModule.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportModule.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Import/ImportPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Import/ImportPresenter.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportPresenter.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Import/ImportRouter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportRouter.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Import/ImportRouter.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportRouter.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Import/ImportView.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Import/ImportView.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Import/ImportView.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Invite/InviteInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Invite/InviteInteractor.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Invite/InviteInteractor.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Invite/InviteInteractor.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Invite/InviteModule.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Invite/InviteModule.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Invite/InviteModule.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Invite/InviteModule.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Invite/InvitePresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Invite/InvitePresenter.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Invite/InvitePresenter.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Invite/InvitePresenter.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Invite/InviteRouter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Invite/InviteRouter.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Invite/InviteRouter.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Invite/InviteRouter.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Invite/InviteView.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Invite/InviteView.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Invite/InviteView.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Invite/InviteView.swift diff --git a/Example/Showcase/Classes/PresentationLayer/InviteList/InviteListInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListInteractor.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/InviteList/InviteListInteractor.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListInteractor.swift diff --git a/Example/Showcase/Classes/PresentationLayer/InviteList/InviteListModule.swift b/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListModule.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/InviteList/InviteListModule.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListModule.swift diff --git a/Example/Showcase/Classes/PresentationLayer/InviteList/InviteListPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListPresenter.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/InviteList/InviteListPresenter.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListPresenter.swift diff --git a/Example/Showcase/Classes/PresentationLayer/InviteList/InviteListRouter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListRouter.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/InviteList/InviteListRouter.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListRouter.swift diff --git a/Example/Showcase/Classes/PresentationLayer/InviteList/InviteListView.swift b/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListView.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/InviteList/InviteListView.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/InviteList/InviteListView.swift diff --git a/Example/Showcase/Classes/PresentationLayer/InviteList/Models/InviteViewModel.swift b/Example/Showcase/Classes/PresentationLayer/Chat/InviteList/Models/InviteViewModel.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/InviteList/Models/InviteViewModel.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/InviteList/Models/InviteViewModel.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Main/MainModule.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Main/MainModule.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Main/MainModule.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Main/MainModule.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Main/MainPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Main/MainPresenter.swift similarity index 64% rename from Example/Showcase/Classes/PresentationLayer/Main/MainPresenter.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Main/MainPresenter.swift index f0d57eaa1..b61e7dd27 100644 --- a/Example/Showcase/Classes/PresentationLayer/Main/MainPresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Main/MainPresenter.swift @@ -11,11 +11,8 @@ final class MainPresenter { var viewControllers: [UIViewController] { return [ - UIViewController(), - UIViewController(), - UIViewController(), - UIViewController(), - router.chatViewController + router.chatViewController, + router.walletViewController ] } diff --git a/Example/Showcase/Classes/PresentationLayer/Main/MainRouter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Main/MainRouter.swift similarity index 68% rename from Example/Showcase/Classes/PresentationLayer/Main/MainRouter.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Main/MainRouter.swift index fdd32686d..d80001361 100644 --- a/Example/Showcase/Classes/PresentationLayer/Main/MainRouter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Main/MainRouter.swift @@ -6,6 +6,10 @@ final class MainRouter { private let app: Application + var walletViewController: UIViewController { + return WalletModule.create(app: app).wrapToNavigationController() + } + var chatViewController: UIViewController { return WelcomeModule.create(app: app) } diff --git a/Example/Showcase/Classes/PresentationLayer/Main/MainViewController.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Main/MainViewController.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Main/MainViewController.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Main/MainViewController.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Chat/Main/Model/TabPage.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Main/Model/TabPage.swift new file mode 100644 index 000000000..2613f6568 --- /dev/null +++ b/Example/Showcase/Classes/PresentationLayer/Chat/Main/Model/TabPage.swift @@ -0,0 +1,32 @@ +import UIKit + +enum TabPage: CaseIterable { + case chat + case wallet + + var title: String { + switch self { + case .chat: + return "Chat" + case .wallet: + return "Wallet" + } + } + + var icon: UIImage { + switch self { + case .chat: + return UIImage(systemName: "message.fill")! + case .wallet: + return UIImage(systemName: "signature")! + } + } + + static var selectedIndex: Int { + return 0 + } + + static var enabledTabs: [TabPage] { + return [.chat, .wallet] + } +} diff --git a/Example/Showcase/Classes/PresentationLayer/Welcome/WelcomeInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeInteractor.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Welcome/WelcomeInteractor.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeInteractor.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Welcome/WelcomeModule.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeModule.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Welcome/WelcomeModule.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeModule.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Welcome/WelcomePresenter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomePresenter.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Welcome/WelcomePresenter.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomePresenter.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Welcome/WelcomeRouter.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeRouter.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Welcome/WelcomeRouter.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeRouter.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Welcome/WelcomeView.swift b/Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeView.swift similarity index 100% rename from Example/Showcase/Classes/PresentationLayer/Welcome/WelcomeView.swift rename to Example/Showcase/Classes/PresentationLayer/Chat/Welcome/WelcomeView.swift diff --git a/Example/Showcase/Classes/PresentationLayer/Main/Model/TabPage.swift b/Example/Showcase/Classes/PresentationLayer/Main/Model/TabPage.swift deleted file mode 100644 index 94186b979..000000000 --- a/Example/Showcase/Classes/PresentationLayer/Main/Model/TabPage.swift +++ /dev/null @@ -1,47 +0,0 @@ -import UIKit - -enum TabPage: CaseIterable { - case tokens - case transactions - case connect - case notifications - case chat - - var title: String { - switch self { - case .tokens: - return "Tokens" - case .transactions: - return "Transactions" - case .connect: - return "Connect & Sign" - case .notifications: - return "Notifications" - case .chat: - return "Chat" - } - } - - var icon: UIImage { - switch self { - case .tokens: - return UIImage(systemName: "star.fill")! - case .transactions: - return UIImage(systemName: "list.bullet.rectangle.fill")! - case .connect: - return UIImage(systemName: "signature")! - case .notifications: - return UIImage(systemName: "note")! - case .chat: - return UIImage(systemName: "message.fill")! - } - } - - static var selectedIndex: Int { - return 4 - } - - static var enabledTabs: [TabPage] { - return [.chat] - } -} diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestInteractor.swift new file mode 100644 index 000000000..1eab65b72 --- /dev/null +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestInteractor.swift @@ -0,0 +1,3 @@ +final class AuthRequestInteractor { + +} diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestModule.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestModule.swift new file mode 100644 index 000000000..230e05bbc --- /dev/null +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestModule.swift @@ -0,0 +1,19 @@ +import SwiftUI +import Auth + +final class AuthRequestModule { + + @discardableResult + static func create(app: Application, request: AuthRequest) -> UIViewController { + let router = AuthRequestRouter(app: app) + let interactor = AuthRequestInteractor() + let presenter = AuthRequestPresenter(request: request, interactor: interactor, router: router) + let view = AuthRequestView().environmentObject(presenter) + let viewController = SceneViewController(viewModel: presenter, content: view) + + router.viewController = viewController + + return viewController + } + +} diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift new file mode 100644 index 000000000..0e94e11fe --- /dev/null +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift @@ -0,0 +1,37 @@ +import UIKit +import Combine +import Auth + +final class AuthRequestPresenter: ObservableObject { + + private let request: AuthRequest + private let interactor: AuthRequestInteractor + private let router: AuthRequestRouter + private var disposeBag = Set() + + init(request: AuthRequest, interactor: AuthRequestInteractor, router: AuthRequestRouter) { + defer { setupInitialState() } + self.request = request + self.interactor = interactor + self.router = router + } + + var message: String { + return request.message + } +} + +// MARK: SceneViewModel + +extension AuthRequestPresenter: SceneViewModel { + +} + +// MARK: Privates + +private extension AuthRequestPresenter { + + func setupInitialState() { + + } +} diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestRouter.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestRouter.swift new file mode 100644 index 000000000..3ff215e54 --- /dev/null +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestRouter.swift @@ -0,0 +1,12 @@ +import UIKit + +final class AuthRequestRouter { + + weak var viewController: UIViewController! + + private let app: Application + + init(app: Application) { + self.app = app + } +} diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestView.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestView.swift new file mode 100644 index 000000000..5d10ea046 --- /dev/null +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestView.swift @@ -0,0 +1,18 @@ +import SwiftUI + +struct AuthRequestView: View { + + @EnvironmentObject var presenter: AuthRequestPresenter + + var body: some View { + Text(presenter.message) + } +} + +#if DEBUG +struct AuthRequestView_Previews: PreviewProvider { + static var previews: some View { + AuthRequestView() + } +} +#endif diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletInteractor.swift new file mode 100644 index 000000000..ac53b05f3 --- /dev/null +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletInteractor.swift @@ -0,0 +1,13 @@ +import Combine +import Auth + +final class WalletInteractor { + + func pair(uri: String) async throws { + try await Auth.instance.pair(uri: uri) + } + + var requestPublisher: AnyPublisher { + return Auth.instance.authRequestPublisher + } +} diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletModule.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletModule.swift new file mode 100644 index 000000000..fed9b1307 --- /dev/null +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletModule.swift @@ -0,0 +1,18 @@ +import SwiftUI + +final class WalletModule { + + @discardableResult + static func create(app: Application) -> UIViewController { + let router = WalletRouter(app: app) + let interactor = WalletInteractor() + let presenter = WalletPresenter(interactor: interactor, router: router) + let view = WalletView().environmentObject(presenter) + let viewController = SceneViewController(viewModel: presenter, content: view) + + router.viewController = viewController + + return viewController + } + +} diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift new file mode 100644 index 000000000..f0b10b2f5 --- /dev/null +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift @@ -0,0 +1,48 @@ +import UIKit +import Combine +import Auth + +final class WalletPresenter: ObservableObject { + + private let interactor: WalletInteractor + private let router: WalletRouter + private var disposeBag = Set() + + init(interactor: WalletInteractor, router: WalletRouter) { + defer { setupInitialState() } + self.interactor = interactor + self.router = router + } + + func didPastePairingURI() { + guard let uri = UIPasteboard.general.string else { return } + + Task(priority: .userInitiated) { [unowned self] in + try await self.interactor.pair(uri: uri) + } + } +} + +// MARK: SceneViewModel + +extension WalletPresenter: SceneViewModel { + + var sceneTitle: String? { + return "Wallet" + } + + var largeTitleDisplayMode: UINavigationItem.LargeTitleDisplayMode { + return .always + } +} + +// MARK: Privates + +private extension WalletPresenter { + + func setupInitialState() { + interactor.requestPublisher.sink { [unowned self] request in + self.router.present(request: request) + }.store(in: &disposeBag) + } +} diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletRouter.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletRouter.swift new file mode 100644 index 000000000..039210a24 --- /dev/null +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletRouter.swift @@ -0,0 +1,17 @@ +import UIKit +import Auth + +final class WalletRouter { + + weak var viewController: UIViewController! + + private let app: Application + + init(app: Application) { + self.app = app + } + + func present(request: AuthRequest) { + AuthRequestModule.create(app: app, request: request).present(from: viewController) + } +} diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletView.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletView.swift new file mode 100644 index 000000000..079746886 --- /dev/null +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletView.swift @@ -0,0 +1,32 @@ +import SwiftUI + +struct WalletView: View { + + @EnvironmentObject var presenter: WalletPresenter + + var body: some View { + VStack { + Button(action: { presenter.didPastePairingURI() }, label: { + HStack(spacing: 8.0) { + Text("Paste pairing URI") + .foregroundColor(.w_foreground) + .font(.system(size: 18, weight: .semibold)) + } + .padding(.trailing, 8.0) + }) + .frame(width: 200, height: 44) + .background( + Capsule() + .foregroundColor(.w_greenForground) + ) + } + } +} + +#if DEBUG +struct WalletView_Previews: PreviewProvider { + static var previews: some View { + WalletView() + } +} +#endif diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index 158e54a2f..5fcfcc112 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -11,8 +11,8 @@ public class AuthClient { case unknownWalletAddress case noPairingMatchingTopic } - private var authRequestPublisherSubject = PassthroughSubject<(id: RPCID, message: String), Never>() - public var authRequestPublisher: AnyPublisher<(id: RPCID, message: String), Never> { + private var authRequestPublisherSubject = PassthroughSubject() + public var authRequestPublisher: AnyPublisher { authRequestPublisherSubject.eraseToAnyPublisher() } @@ -116,8 +116,8 @@ public class AuthClient { authResponsePublisherSubject.send((id, result)) } - walletRequestSubscriber.onRequest = { [unowned self] (id, message) in - authRequestPublisherSubject.send((id, message)) + walletRequestSubscriber.onRequest = { [unowned self] request in + authRequestPublisherSubject.send(request) } } } diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift index 6d04629ca..522820187 100644 --- a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift +++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift @@ -11,7 +11,7 @@ class WalletRequestSubscriber { private let address: String? private var publishers = [AnyCancellable]() private let messageFormatter: SIWEMessageFormatting - var onRequest: ((_ id: RPCID, _ message: String) -> Void)? + var onRequest: ((AuthRequest) -> Void)? init(networkingInteractor: NetworkInteracting, logger: ConsoleLogging, @@ -41,7 +41,7 @@ class WalletRequestSubscriber { let message = messageFormatter.formatMessage(from: authRequestParams.payloadParams, address: address) - onRequest?(requestId, message) + onRequest?(.init(id: requestId, message: message)) }.store(in: &publishers) } From 9838c6ac0ea227b5d80448b581072a23b0b5377e Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 25 Aug 2022 16:44:55 +0300 Subject: [PATCH 02/92] DApp pairing --- Example/DApp/Auth/AuthCoordinator.swift | 37 +++++++++ Example/DApp/Auth/AuthView.swift | 44 ++++++++++ Example/DApp/Auth/AuthViewModel.swift | 51 ++++++++++++ Example/DApp/Common/QRCodeGenerator.swift | 21 +++++ Example/DApp/SceneDelegate.swift | 80 +++++-------------- .../AccountRequest/AccountRequestView.swift | 0 .../AccountRequestViewController.swift | 0 .../Accounts}/AccountsView.swift | 0 .../Accounts}/AccountsViewController.swift | 0 .../DApp/{ => Sign}/Connect/ConnectView.swift | 0 .../Connect/ConnectViewController.swift | 21 +---- .../{ => Sign}/ResponseViewController.swift | 0 .../SelectChain/SelectChainView.swift | 0 .../SelectChainViewController.swift | 0 Example/DApp/Sign/SignCoordinator.swift | 73 +++++++++++++++++ Example/ExampleApp.xcodeproj/project.pbxproj | 65 +++++++++++++-- .../xcshareddata/xcschemes/Showcase.xcscheme | 78 ++++++++++++++++++ .../Stubs/RequestParams.swift | 1 - .../AuthRequest/AuthRequestInteractor.swift | 9 +++ .../AuthRequest/AuthRequestPresenter.swift | 7 ++ .../Wallet/AuthRequest/AuthRequestView.swift | 52 ++++++++++-- .../Wallet/Wallet/WalletRouter.swift | 4 +- Sources/Auth/Types/RequestParams.swift | 40 +++++++--- 23 files changed, 479 insertions(+), 104 deletions(-) create mode 100644 Example/DApp/Auth/AuthCoordinator.swift create mode 100644 Example/DApp/Auth/AuthView.swift create mode 100644 Example/DApp/Auth/AuthViewModel.swift create mode 100644 Example/DApp/Common/QRCodeGenerator.swift rename Example/DApp/{ => Sign}/AccountRequest/AccountRequestView.swift (100%) rename Example/DApp/{ => Sign}/AccountRequest/AccountRequestViewController.swift (100%) rename Example/DApp/{ Accounts => Sign/Accounts}/AccountsView.swift (100%) rename Example/DApp/{ Accounts => Sign/Accounts}/AccountsViewController.swift (100%) rename Example/DApp/{ => Sign}/Connect/ConnectView.swift (100%) rename Example/DApp/{ => Sign}/Connect/ConnectViewController.swift (81%) rename Example/DApp/{ => Sign}/ResponseViewController.swift (100%) rename Example/DApp/{ => Sign}/SelectChain/SelectChainView.swift (100%) rename Example/DApp/{ => Sign}/SelectChain/SelectChainViewController.swift (100%) create mode 100644 Example/DApp/Sign/SignCoordinator.swift create mode 100644 Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/Showcase.xcscheme diff --git a/Example/DApp/Auth/AuthCoordinator.swift b/Example/DApp/Auth/AuthCoordinator.swift new file mode 100644 index 000000000..231f0be4b --- /dev/null +++ b/Example/DApp/Auth/AuthCoordinator.swift @@ -0,0 +1,37 @@ +import SwiftUI +import Auth + +final class AuthCoordinator { + + let navigationController = UINavigationController() + + private lazy var tabBarItem: UITabBarItem = { + let item = UITabBarItem() + item.title = "Auth" + item.image = UIImage(systemName: "person") + return item + }() + + private lazy var authViewController: UIViewController = { + let viewModel = AuthViewModel() + let view = AuthView(viewModel: viewModel) + let controller = UIHostingController(rootView: view) + controller.title = "DApp" + return controller + }() + + func start() { + navigationController.tabBarItem = tabBarItem + navigationController.viewControllers = [UIViewController()] + + let metadata = AppMetadata( + name: "Swift Dapp", + description: "WalletConnect DApp sample", + url: "wallet.connect", + icons: ["https://avatars.githubusercontent.com/u/37784886"]) + + Auth.configure(metadata: metadata, account: nil) + + navigationController.viewControllers = [authViewController] + } +} diff --git a/Example/DApp/Auth/AuthView.swift b/Example/DApp/Auth/AuthView.swift new file mode 100644 index 000000000..0e39b3de8 --- /dev/null +++ b/Example/DApp/Auth/AuthView.swift @@ -0,0 +1,44 @@ +import SwiftUI + +struct AuthView: View { + + @ObservedObject var viewModel: AuthViewModel + + var body: some View { + VStack(spacing: 16.0) { + + Spacer() + + Image(uiImage: viewModel.qrImage ?? UIImage()) + .interpolation(.none) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 300, height: 300) + + Spacer() + + Button("Connect Wallet", action: { }) + .buttonStyle(CircleButtonStyle()) + + Button("Copy URI", action: { viewModel.copyDidPressed() }) + .buttonStyle(CircleButtonStyle()) + } + .padding(16.0) + .onAppear { Task(priority: .userInitiated) { + try await viewModel.setupInitialState() + }} + } +} + +struct CircleButtonStyle: ButtonStyle { + + func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + .frame(width: 200, height: 44) + .foregroundColor(.white) + .background(configuration.isPressed ? Color.blue.opacity(0.5) : Color.blue) + .font(.system(size: 17, weight: .semibold)) + .cornerRadius(8.0) + } +} + diff --git a/Example/DApp/Auth/AuthViewModel.swift b/Example/DApp/Auth/AuthViewModel.swift new file mode 100644 index 000000000..84fbf41b0 --- /dev/null +++ b/Example/DApp/Auth/AuthViewModel.swift @@ -0,0 +1,51 @@ +import UIKit +import Combine +import Auth + +final class AuthViewModel: ObservableObject { + + @Published var uri: String? + + var qrImage: UIImage? { + return uri.map { QRCodeGenerator.generateQRCode(from: $0) } + } + + @MainActor + func setupInitialState() async throws { + uri = try await Auth.instance.request(.stub()) + } + + func copyDidPressed() { + UIPasteboard.general.string = uri + } + + func walletDidPressed() { + + } +} + +private extension RequestParams { + static func stub( + domain: String = "service.invalid", + chainId: String = "1", + nonce: String = "32891756", + aud: String = "https://service.invalid/login", + nbf: String? = nil, + exp: String? = nil, + statement: String? = "I accept the ServiceOrg Terms of Service: https://service.invalid/tos", + requestId: String? = nil, + resources: [String]? = ["ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/", "https://example.com/my-web2-claim.json"] + ) -> RequestParams { + return RequestParams( + domain: domain, + chainId: chainId, + nonce: nonce, + aud: aud, + nbf: nbf, + exp: exp, + statement: statement, + requestId: requestId, + resources: resources + ) + } +} diff --git a/Example/DApp/Common/QRCodeGenerator.swift b/Example/DApp/Common/QRCodeGenerator.swift new file mode 100644 index 000000000..380005581 --- /dev/null +++ b/Example/DApp/Common/QRCodeGenerator.swift @@ -0,0 +1,21 @@ +import Foundation +import UIKit +import CoreImage.CIFilterBuiltins + +struct QRCodeGenerator { + + static func generateQRCode(from string: String) -> UIImage { + let context = CIContext() + let filter = CIFilter.qrCodeGenerator() + let data = Data(string.utf8) + filter.setValue(data, forKey: "inputMessage") + let transform = CGAffineTransform(scaleX: 4, y: 4) + if let qrCodeImage = filter.outputImage?.transformed(by: transform) { + if let qrCodeCGImage = context.createCGImage(qrCodeImage, from: qrCodeImage.extent) { + return UIImage(cgImage: qrCodeCGImage) + } + } + + return UIImage() + } +} diff --git a/Example/DApp/SceneDelegate.swift b/Example/DApp/SceneDelegate.swift index 4cb1f895e..163421bcc 100644 --- a/Example/DApp/SceneDelegate.swift +++ b/Example/DApp/SceneDelegate.swift @@ -1,8 +1,6 @@ import UIKit -import WalletConnectSign -import WalletConnectRelay -import Combine import Starscream +import WalletConnectRelay extension WebSocket: WebSocketConnecting { } @@ -15,70 +13,30 @@ struct SocketFactory: WebSocketFactory { class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - private var publishers = [AnyCancellable]() - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let windowScene = (scene as? UIWindowScene) else { return } - window = UIWindow(windowScene: windowScene) - let metadata = AppMetadata( - name: "Swift Dapp", - description: "a description", - url: "wallet.connect", - icons: ["https://avatars.githubusercontent.com/u/37784886"]) - - Relay.configure(projectId: "8ba9ee138960775e5231b70cc5ef1c3a", socketFactory: SocketFactory()) - Sign.configure(metadata: metadata) - if CommandLine.arguments.contains("-cleanInstall") { - try? Sign.instance.cleanup() - } + private let signCoordinator = SignCoordinator() + private let authCoordinator = AuthCoordinator() - Sign.instance.sessionDeletePublisher - .receive(on: DispatchQueue.main) - .sink { [unowned self] _ in - showSelectChainScreen() - }.store(in: &publishers) - - Sign.instance.sessionResponsePublisher.sink { [unowned self] response in - presentResponse(for: response) - }.store(in: &publishers) + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + Relay.configure(projectId: "8ba9ee138960775e5231b70cc5ef1c3a",socketFactory: SocketFactory()) - if let session = Sign.instance.getSessions().first { - showAccountsScreen(session) - } else { - showSelectChainScreen() - } + setupWindow(scene: scene) } - func showSelectChainScreen() { - DispatchQueue.main.async { [unowned self] in - let vc = SelectChainViewController() - vc.onSessionSettled = { [unowned self] session in - showAccountsScreen(session) - } - window?.rootViewController = UINavigationController(rootViewController: vc) - window?.makeKeyAndVisible() - } - } + private func setupWindow(scene: UIScene) { + guard let windowScene = (scene as? UIWindowScene) else { return } + window = UIWindow(windowScene: windowScene) - func showAccountsScreen(_ session: Session) { - DispatchQueue.main.async { [unowned self] in - let vc = AccountsViewController(session: session) - vc.onDisconnect = { [unowned self] in - showSelectChainScreen() - } - window?.rootViewController = UINavigationController(rootViewController: vc) - window?.makeKeyAndVisible() - } - } + let tabController = UITabBarController() + tabController.viewControllers = [ + signCoordinator.navigationController, + authCoordinator.navigationController + ] + + signCoordinator.start() + authCoordinator.start() - func presentResponse(for response: Response) { - DispatchQueue.main.async { [unowned self] in - let vc = UINavigationController(rootViewController: ResponseViewController(response: response)) - window?.rootViewController?.present(vc, animated: true, completion: nil) - } + window?.rootViewController = tabController + window?.makeKeyAndVisible() } } diff --git a/Example/DApp/AccountRequest/AccountRequestView.swift b/Example/DApp/Sign/AccountRequest/AccountRequestView.swift similarity index 100% rename from Example/DApp/AccountRequest/AccountRequestView.swift rename to Example/DApp/Sign/AccountRequest/AccountRequestView.swift diff --git a/Example/DApp/AccountRequest/AccountRequestViewController.swift b/Example/DApp/Sign/AccountRequest/AccountRequestViewController.swift similarity index 100% rename from Example/DApp/AccountRequest/AccountRequestViewController.swift rename to Example/DApp/Sign/AccountRequest/AccountRequestViewController.swift diff --git a/Example/DApp/ Accounts/AccountsView.swift b/Example/DApp/Sign/Accounts/AccountsView.swift similarity index 100% rename from Example/DApp/ Accounts/AccountsView.swift rename to Example/DApp/Sign/Accounts/AccountsView.swift diff --git a/Example/DApp/ Accounts/AccountsViewController.swift b/Example/DApp/Sign/Accounts/AccountsViewController.swift similarity index 100% rename from Example/DApp/ Accounts/AccountsViewController.swift rename to Example/DApp/Sign/Accounts/AccountsViewController.swift diff --git a/Example/DApp/Connect/ConnectView.swift b/Example/DApp/Sign/Connect/ConnectView.swift similarity index 100% rename from Example/DApp/Connect/ConnectView.swift rename to Example/DApp/Sign/Connect/ConnectView.swift diff --git a/Example/DApp/Connect/ConnectViewController.swift b/Example/DApp/Sign/Connect/ConnectViewController.swift similarity index 81% rename from Example/DApp/Connect/ConnectViewController.swift rename to Example/DApp/Sign/Connect/ConnectViewController.swift index c23eef52d..6f253ecbe 100644 --- a/Example/DApp/Connect/ConnectViewController.swift +++ b/Example/DApp/Sign/Connect/ConnectViewController.swift @@ -27,11 +27,10 @@ class ConnectViewController: UIViewController, UITableViewDataSource, UITableVie override func viewDidLoad() { super.viewDidLoad() DispatchQueue.global().async { [unowned self] in - if let qrImage = generateQRCode(from: uriString) { - DispatchQueue.main.async { [self] in - self.connectView.qrCodeView.image = qrImage - self.connectView.copyButton.isHidden = false - } + let qrImage = QRCodeGenerator.generateQRCode(from: uriString) + DispatchQueue.main.async { [self] in + self.connectView.qrCodeView.image = qrImage + self.connectView.copyButton.isHidden = false } } connectView.copyButton.addTarget(self, action: #selector(copyURI), for: .touchUpInside) @@ -60,18 +59,6 @@ class ConnectViewController: UIViewController, UITableViewDataSource, UITableVie UIPasteboard.general.string = uriString } - private func generateQRCode(from string: String) -> UIImage? { - let data = string.data(using: .ascii) - if let filter = CIFilter(name: "CIQRCodeGenerator") { - filter.setValue(data, forKey: "inputMessage") - let transform = CGAffineTransform(scaleX: 4, y: 4) - if let output = filter.outputImage?.transformed(by: transform) { - return UIImage(ciImage: output) - } - } - return nil - } - @objc func connectWithExampleWallet() { let url = URL(string: "https://walletconnect.com/wc?uri=\(uriString)")! DispatchQueue.main.async { diff --git a/Example/DApp/ResponseViewController.swift b/Example/DApp/Sign/ResponseViewController.swift similarity index 100% rename from Example/DApp/ResponseViewController.swift rename to Example/DApp/Sign/ResponseViewController.swift diff --git a/Example/DApp/SelectChain/SelectChainView.swift b/Example/DApp/Sign/SelectChain/SelectChainView.swift similarity index 100% rename from Example/DApp/SelectChain/SelectChainView.swift rename to Example/DApp/Sign/SelectChain/SelectChainView.swift diff --git a/Example/DApp/SelectChain/SelectChainViewController.swift b/Example/DApp/Sign/SelectChain/SelectChainViewController.swift similarity index 100% rename from Example/DApp/SelectChain/SelectChainViewController.swift rename to Example/DApp/Sign/SelectChain/SelectChainViewController.swift diff --git a/Example/DApp/Sign/SignCoordinator.swift b/Example/DApp/Sign/SignCoordinator.swift new file mode 100644 index 000000000..75d25df1e --- /dev/null +++ b/Example/DApp/Sign/SignCoordinator.swift @@ -0,0 +1,73 @@ +import UIKit +import Combine +import WalletConnectSign +import WalletConnectRelay + +final class SignCoordinator { + + private var publishers = Set() + + let navigationController = UINavigationController() + + lazy var tabBarItem: UITabBarItem = { + let item = UITabBarItem() + item.title = "Sign" + item.image = UIImage(systemName: "signature") + return item + }() + + func start() { + navigationController.tabBarItem = tabBarItem + + let metadata = AppMetadata( + name: "Swift Dapp", + description: "WalletConnect DApp sample", + url: "wallet.connect", + icons: ["https://avatars.githubusercontent.com/u/37784886"]) + + Sign.configure(metadata: metadata) + + if CommandLine.arguments.contains("-cleanInstall") { + try? Sign.instance.cleanup() + } + + Sign.instance.sessionDeletePublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] _ in + showSelectChainScreen() + }.store(in: &publishers) + + Sign.instance.sessionResponsePublisher + .receive(on: DispatchQueue.main) + .sink { [unowned self] response in + presentResponse(for: response) + }.store(in: &publishers) + + if let session = Sign.instance.getSessions().first { + showAccountsScreen(session) + } else { + showSelectChainScreen() + } + } + + private func showSelectChainScreen() { + let controller = SelectChainViewController() + controller.onSessionSettled = { [unowned self] session in + showAccountsScreen(session) + } + navigationController.viewControllers = [controller] + } + + private func showAccountsScreen(_ session: Session) { + let controller = AccountsViewController(session: session) + controller.onDisconnect = { [unowned self] in + showSelectChainScreen() + } + navigationController.viewControllers = [controller] + } + + private func presentResponse(for response: Response) { + let controller = UINavigationController(rootViewController: ResponseViewController(response: response)) + navigationController.present(controller, animated: true, completion: nil) + } +} diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 24c1ebdfd..8bcb63641 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -121,6 +121,12 @@ A5A4FC5E283D23CA00BBEC1E /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A4FC5D283D23CA00BBEC1E /* Array.swift */; }; A5A4FC772840C12C00BBEC1E /* RegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A4FC762840C12C00BBEC1E /* RegressionTests.swift */; }; A5AE354728A1A2AC0059AE8A /* Web3 in Frameworks */ = {isa = PBXBuildFile; productRef = A5AE354628A1A2AC0059AE8A /* Web3 */; }; + A5BB7F9F28B69B7100707FC6 /* SignCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BB7F9E28B69B7100707FC6 /* SignCoordinator.swift */; }; + A5BB7FA128B69F3400707FC6 /* AuthCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BB7FA028B69F3400707FC6 /* AuthCoordinator.swift */; }; + A5BB7FA328B6A50400707FC6 /* WalletConnectAuth in Frameworks */ = {isa = PBXBuildFile; productRef = A5BB7FA228B6A50400707FC6 /* WalletConnectAuth */; }; + A5BB7FA728B6A5F600707FC6 /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BB7FA628B6A5F600707FC6 /* AuthView.swift */; }; + A5BB7FA928B6A5FD00707FC6 /* AuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BB7FA828B6A5FD00707FC6 /* AuthViewModel.swift */; }; + A5BB7FAD28B6AA7D00707FC6 /* QRCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BB7FAC28B6AA7D00707FC6 /* QRCodeGenerator.swift */; }; A5C2020B287D9DEE007E3188 /* WelcomeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5C20206287D9DEE007E3188 /* WelcomeModule.swift */; }; A5C2020C287D9DEE007E3188 /* WelcomePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5C20207287D9DEE007E3188 /* WelcomePresenter.swift */; }; A5C2020D287D9DEE007E3188 /* WelcomeRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5C20208287D9DEE007E3188 /* WelcomeRouter.swift */; }; @@ -293,6 +299,11 @@ A5A4FC5D283D23CA00BBEC1E /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = ""; }; A5A4FC722840C12C00BBEC1E /* UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; A5A4FC762840C12C00BBEC1E /* RegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegressionTests.swift; sourceTree = ""; }; + A5BB7F9E28B69B7100707FC6 /* SignCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignCoordinator.swift; sourceTree = ""; }; + A5BB7FA028B69F3400707FC6 /* AuthCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthCoordinator.swift; sourceTree = ""; }; + A5BB7FA628B6A5F600707FC6 /* AuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthView.swift; sourceTree = ""; }; + A5BB7FA828B6A5FD00707FC6 /* AuthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewModel.swift; sourceTree = ""; }; + A5BB7FAC28B6AA7D00707FC6 /* QRCodeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeGenerator.swift; sourceTree = ""; }; A5C20206287D9DEE007E3188 /* WelcomeModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeModule.swift; sourceTree = ""; }; A5C20207287D9DEE007E3188 /* WelcomePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomePresenter.swift; sourceTree = ""; }; A5C20208287D9DEE007E3188 /* WelcomeRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeRouter.swift; sourceTree = ""; }; @@ -342,6 +353,7 @@ files = ( 8448F1D427E4726F0000B866 /* WalletConnect in Frameworks */, A5D85228286333E300DAF5C3 /* Starscream in Frameworks */, + A5BB7FA328B6A50400707FC6 /* WalletConnectAuth in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -498,13 +510,11 @@ 84CE641D27981DED00142511 /* DApp */ = { isa = PBXGroup; children = ( + A5BB7FAB28B6AA7100707FC6 /* Common */, 84CE641E27981DED00142511 /* AppDelegate.swift */, 84CE642027981DED00142511 /* SceneDelegate.swift */, - 84CE645427A29D4C00142511 /* ResponseViewController.swift */, - 84CE6449279EA1E600142511 /* AccountRequest */, - 84CE644C279ED2EC00142511 /* SelectChain */, - 84CE6450279ED41D00142511 /* Connect */, - 84CE644F279ED3FB00142511 /* Accounts */, + A5BB7FAA28B6A64A00707FC6 /* Sign */, + A5BB7FA528B6A5DC00707FC6 /* Auth */, 84CE642727981DF000142511 /* Assets.xcassets */, 84CE642927981DF000142511 /* LaunchScreen.storyboard */, 84CE642C27981DF000142511 /* Info.plist */, @@ -530,13 +540,13 @@ path = SelectChain; sourceTree = ""; }; - 84CE644F279ED3FB00142511 /* Accounts */ = { + 84CE644F279ED3FB00142511 /* Accounts */ = { isa = PBXGroup; children = ( 761C649926FB7ABB004239D1 /* AccountsViewController.swift */, 7603D74C2703429A00DD27A2 /* AccountsView.swift */, ); - path = " Accounts"; + path = Accounts; sourceTree = ""; }; 84CE6450279ED41D00142511 /* Connect */ = { @@ -886,6 +896,37 @@ path = Engine; sourceTree = ""; }; + A5BB7FA528B6A5DC00707FC6 /* Auth */ = { + isa = PBXGroup; + children = ( + A5BB7FA028B69F3400707FC6 /* AuthCoordinator.swift */, + A5BB7FA628B6A5F600707FC6 /* AuthView.swift */, + A5BB7FA828B6A5FD00707FC6 /* AuthViewModel.swift */, + ); + path = Auth; + sourceTree = ""; + }; + A5BB7FAA28B6A64A00707FC6 /* Sign */ = { + isa = PBXGroup; + children = ( + 84CE645427A29D4C00142511 /* ResponseViewController.swift */, + 84CE6449279EA1E600142511 /* AccountRequest */, + 84CE644C279ED2EC00142511 /* SelectChain */, + 84CE6450279ED41D00142511 /* Connect */, + 84CE644F279ED3FB00142511 /* Accounts */, + A5BB7F9E28B69B7100707FC6 /* SignCoordinator.swift */, + ); + path = Sign; + sourceTree = ""; + }; + A5BB7FAB28B6AA7100707FC6 /* Common */ = { + isa = PBXGroup; + children = ( + A5BB7FAC28B6AA7D00707FC6 /* QRCodeGenerator.swift */, + ); + path = Common; + sourceTree = ""; + }; A5C20205287D9DB9007E3188 /* Welcome */ = { isa = PBXGroup; children = ( @@ -1035,6 +1076,7 @@ packageProductDependencies = ( 8448F1D327E4726F0000B866 /* WalletConnect */, A5D85227286333E300DAF5C3 /* Starscream */, + A5BB7FA228B6A50400707FC6 /* WalletConnectAuth */, ); productName = DApp; productReference = 84CE641C27981DED00142511 /* DApp.app */; @@ -1236,11 +1278,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A5BB7FA728B6A5F600707FC6 /* AuthView.swift in Sources */, 84CE6430279820F600142511 /* AccountsViewController.swift in Sources */, + A5BB7FA128B69F3400707FC6 /* AuthCoordinator.swift in Sources */, 84CE645527A29D4D00142511 /* ResponseViewController.swift in Sources */, 84CE641F27981DED00142511 /* AppDelegate.swift in Sources */, + A5BB7FAD28B6AA7D00707FC6 /* QRCodeGenerator.swift in Sources */, + A5BB7FA928B6A5FD00707FC6 /* AuthViewModel.swift in Sources */, 84CE6452279ED42B00142511 /* ConnectView.swift in Sources */, 84CE6448279AE68600142511 /* AccountRequestViewController.swift in Sources */, + A5BB7F9F28B69B7100707FC6 /* SignCoordinator.swift in Sources */, 84CE642127981DED00142511 /* SceneDelegate.swift in Sources */, 84CE644E279ED2FF00142511 /* SelectChainView.swift in Sources */, 84CE644B279EA1FA00142511 /* AccountRequestView.swift in Sources */, @@ -1884,6 +1931,10 @@ package = A5AE354528A1A2AC0059AE8A /* XCRemoteSwiftPackageReference "Web3" */; productName = Web3; }; + A5BB7FA228B6A50400707FC6 /* WalletConnectAuth */ = { + isa = XCSwiftPackageProductDependency; + productName = WalletConnectAuth; + }; A5C4DD8628A2DE88006A626D /* WalletConnectRouter */ = { isa = XCSwiftPackageProductDependency; productName = WalletConnectRouter; diff --git a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/Showcase.xcscheme b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/Showcase.xcscheme new file mode 100644 index 000000000..f9350d24d --- /dev/null +++ b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/Showcase.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/IntegrationTests/Stubs/RequestParams.swift b/Example/IntegrationTests/Stubs/RequestParams.swift index 89cb11ae9..e594798ee 100644 --- a/Example/IntegrationTests/Stubs/RequestParams.swift +++ b/Example/IntegrationTests/Stubs/RequestParams.swift @@ -1,4 +1,3 @@ - import Foundation @testable import Auth diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestInteractor.swift index 1eab65b72..d68804e77 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestInteractor.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestInteractor.swift @@ -1,3 +1,12 @@ +import Auth + final class AuthRequestInteractor { + func approve(request: AuthRequest) { + + } + + func reject(request: AuthRequest) { + + } } diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift index 0e94e11fe..aa498f7f9 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift @@ -25,6 +25,13 @@ final class AuthRequestPresenter: ObservableObject { extension AuthRequestPresenter: SceneViewModel { + var sceneTitle: String? { + return "Auth Request" + } + + var largeTitleDisplayMode: UINavigationItem.LargeTitleDisplayMode { + return .always + } } // MARK: Privates diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestView.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestView.swift index 5d10ea046..2559b6de4 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestView.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestView.swift @@ -5,14 +5,50 @@ struct AuthRequestView: View { @EnvironmentObject var presenter: AuthRequestPresenter var body: some View { - Text(presenter.message) - } -} + VStack(spacing: 16.0) { + HStack { + Text("Message to sign:") + Spacer() + } + + VStack { + Text(presenter.message) + .font(Font.system(size: 13)) + .padding(16.0) + } + .background(Color.white.opacity(0.1)) + .cornerRadius(10) + + HStack(spacing: 16.0) { + Button(action: { }, label: { + HStack(spacing: 8.0) { + Text("Reject") + .foregroundColor(.w_foreground) + .font(.system(size: 18, weight: .semibold)) + } + }) + .frame(width: 120, height: 44) + .background( + Capsule() + .foregroundColor(.w_purpleForeground) + ) + + Button(action: { }, label: { + HStack(spacing: 8.0) { + Text("Approve") + .foregroundColor(.w_foreground) + .font(.system(size: 18, weight: .semibold)) + } + }) + .frame(width: 120, height: 44) + .background( + Capsule() + .foregroundColor(.w_greenForground) + ) + } -#if DEBUG -struct AuthRequestView_Previews: PreviewProvider { - static var previews: some View { - AuthRequestView() + Spacer() + } + .padding(16.0) } } -#endif diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletRouter.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletRouter.swift index 039210a24..50b4ed24b 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletRouter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletRouter.swift @@ -12,6 +12,8 @@ final class WalletRouter { } func present(request: AuthRequest) { - AuthRequestModule.create(app: app, request: request).present(from: viewController) + AuthRequestModule.create(app: app, request: request) + .wrapToNavigationController() + .present(from: viewController) } } diff --git a/Sources/Auth/Types/RequestParams.swift b/Sources/Auth/Types/RequestParams.swift index a3f456add..e5a7295de 100644 --- a/Sources/Auth/Types/RequestParams.swift +++ b/Sources/Auth/Types/RequestParams.swift @@ -1,13 +1,35 @@ import Foundation public struct RequestParams { - let domain: String - let chainId: String - let nonce: String - let aud: String - let nbf: String? - let exp: String? - let statement: String? - let requestId: String? - let resources: [String]? + public let domain: String + public let chainId: String + public let nonce: String + public let aud: String + public let nbf: String? + public let exp: String? + public let statement: String? + public let requestId: String? + public let resources: [String]? + + public init( + domain: String, + chainId: String, + nonce: String, + aud: String, + nbf: String?, + exp: String?, + statement: String?, + requestId: String?, + resources: [String]? + ) { + self.domain = domain + self.chainId = chainId + self.nonce = nonce + self.aud = aud + self.nbf = nbf + self.exp = exp + self.statement = statement + self.requestId = requestId + self.resources = resources + } } From c237850448ac5ed14f5fdbf917b8ae78f1d633b2 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 25 Aug 2022 16:48:19 +0300 Subject: [PATCH 03/92] Auth tests use AuthRequest object --- Tests/AuthTests/WalletRequestSubscriberTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/AuthTests/WalletRequestSubscriberTests.swift b/Tests/AuthTests/WalletRequestSubscriberTests.swift index 3ca0f2741..bce269680 100644 --- a/Tests/AuthTests/WalletRequestSubscriberTests.swift +++ b/Tests/AuthTests/WalletRequestSubscriberTests.swift @@ -28,9 +28,9 @@ class WalletRequestSubscriberTests: XCTestCase { messageFormatter.formattedMessage = expectedMessage var messageId: RPCID! var message: String! - sut.onRequest = { id, formattedMessage in - messageId = id - message = formattedMessage + sut.onRequest = { request in + messageId = request.id + message = request.message messageExpectation.fulfill() } From 02e8dbc47b3ee1f4f53ed8d4ac1487491df55fa3 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 25 Aug 2022 19:24:57 +0300 Subject: [PATCH 04/92] Auth Demo interactions --- Example/DApp/Auth/AuthView.swift | 56 ++++++++++++++++++- Example/DApp/Auth/AuthViewModel.swift | 29 ++++++++++ Example/ExampleApp.xcodeproj/project.pbxproj | 8 +++ Example/IntegrationTests/Auth/AuthTests.swift | 11 ++-- .../AuthRequest/AuthRequestInteractor.swift | 13 +++-- .../AuthRequest/AuthRequestPresenter.swift | 12 ++++ .../AuthRequest/AuthRequestRouter.swift | 4 ++ .../Wallet/AuthRequest/AuthRequestView.swift | 4 +- .../Wallet/Wallet/WalletPresenter.swift | 4 ++ .../Wallet/Wallet/WalletView.swift | 16 +++++- .../Services/App/AppRespondSubscriber.swift | 2 +- .../Auth/Services/Signer/MessageSigner.swift | 12 ++-- .../AuthTests/AppRespondSubscriberTests.swift | 3 +- Tests/AuthTests/CacaoSignerTests.swift | 2 +- 14 files changed, 152 insertions(+), 24 deletions(-) diff --git a/Example/DApp/Auth/AuthView.swift b/Example/DApp/Auth/AuthView.swift index 0e39b3de8..24d029c1b 100644 --- a/Example/DApp/Auth/AuthView.swift +++ b/Example/DApp/Auth/AuthView.swift @@ -12,9 +12,11 @@ struct AuthView: View { Image(uiImage: viewModel.qrImage ?? UIImage()) .interpolation(.none) .resizable() - .aspectRatio(contentMode: .fill) .frame(width: 300, height: 300) + signingLabel() + .frame(maxWidth: .infinity) + Spacer() Button("Connect Wallet", action: { }) @@ -28,6 +30,58 @@ struct AuthView: View { try await viewModel.setupInitialState() }} } + + @ViewBuilder + private func signingLabel() -> some View { + switch viewModel.state { + case .error(let error): + SigningLabel(state: .error(error.localizedDescription)) + .frame(height: 50) + case .signed: + SigningLabel(state: .signed) + .frame(height: 50) + case .none: + Spacer().frame(height: 50) + } + } +} + +struct SigningLabel: View { + enum State { + case signed + case error(String) + + var color: Color { + switch self { + case .signed: return .green.opacity(0.6) + case .error: return .red.opacity(0.6) + } + } + + var text: String { + switch self { + case .signed: + return "Authenticated" + case .error: + return "Authenticion error" + } + } + } + + let state: State + + var body: some View { + VStack { + Text(state.text) + .multilineTextAlignment(.center) + .foregroundColor(.white) + .font(.system(size: 14, weight: .semibold)) + .padding(16.0) + } + .fixedSize(horizontal: true, vertical: false) + .background(state.color) + .cornerRadius(4.0) + } } struct CircleButtonStyle: ButtonStyle { diff --git a/Example/DApp/Auth/AuthViewModel.swift b/Example/DApp/Auth/AuthViewModel.swift index 84fbf41b0..fe823ee05 100644 --- a/Example/DApp/Auth/AuthViewModel.swift +++ b/Example/DApp/Auth/AuthViewModel.swift @@ -4,14 +4,29 @@ import Auth final class AuthViewModel: ObservableObject { + enum SigningState { + case none + case signed(Cacao) + case error(Error) + } + + private var disposeBag = Set() + + @Published var state: SigningState = .none @Published var uri: String? var qrImage: UIImage? { return uri.map { QRCodeGenerator.generateQRCode(from: $0) } } + init() { + setupSubscriptions() + } + @MainActor func setupInitialState() async throws { + state = .none + uri = nil uri = try await Auth.instance.request(.stub()) } @@ -24,6 +39,20 @@ final class AuthViewModel: ObservableObject { } } +private extension AuthViewModel { + + func setupSubscriptions() { + Auth.instance.authResponsePublisher.sink { [weak self] (id, result) in + switch result { + case .success(let cacao): + self?.state = .signed(cacao) + case .failure(let error): + self?.state = .error(error) + } + }.store(in: &disposeBag) + } +} + private extension RequestParams { static func stub( domain: String = "service.invalid", diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 8bcb63641..05e0fbfe7 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -114,6 +114,7 @@ A59F877228B53EA000A9CD80 /* WalletInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59F876D28B53EA000A9CD80 /* WalletInteractor.swift */; }; A59F877328B53EA000A9CD80 /* WalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59F876E28B53EA000A9CD80 /* WalletView.swift */; }; A59F877628B5462900A9CD80 /* WalletConnectAuth in Frameworks */ = {isa = PBXBuildFile; productRef = A59F877528B5462900A9CD80 /* WalletConnectAuth */; }; + A59FAEC928B7B93A002BB66F /* Web3 in Frameworks */ = {isa = PBXBuildFile; productRef = A59FAEC828B7B93A002BB66F /* Web3 */; }; A5A4FC56283CBB7800BBEC1E /* SessionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A4FC55283CBB7800BBEC1E /* SessionDetailView.swift */; }; A5A4FC58283CBB9F00BBEC1E /* SessionDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A4FC57283CBB9F00BBEC1E /* SessionDetailViewModel.swift */; }; A5A4FC5A283CC08600BBEC1E /* SessionNamespaceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A4FC59283CC08600BBEC1E /* SessionNamespaceViewModel.swift */; }; @@ -362,6 +363,7 @@ buildActionMask = 2147483647; files = ( A5629AEA2877F2D600094373 /* WalletConnectChat in Frameworks */, + A59FAEC928B7B93A002BB66F /* Web3 in Frameworks */, A5629AF22877F75100094373 /* Starscream in Frameworks */, A59F877628B5462900A9CD80 /* WalletConnectAuth in Frameworks */, ); @@ -1099,6 +1101,7 @@ A5629AE92877F2D600094373 /* WalletConnectChat */, A5629AF12877F75100094373 /* Starscream */, A59F877528B5462900A9CD80 /* WalletConnectAuth */, + A59FAEC828B7B93A002BB66F /* Web3 */, ); productName = Showcase; productReference = A58E7CE828729F550082D443 /* Showcase.app */; @@ -1926,6 +1929,11 @@ isa = XCSwiftPackageProductDependency; productName = WalletConnectAuth; }; + A59FAEC828B7B93A002BB66F /* Web3 */ = { + isa = XCSwiftPackageProductDependency; + package = A5AE354528A1A2AC0059AE8A /* XCRemoteSwiftPackageReference "Web3" */; + productName = Web3; + }; A5AE354628A1A2AC0059AE8A /* Web3 */ = { isa = XCSwiftPackageProductDependency; package = A5AE354528A1A2AC0059AE8A /* XCRemoteSwiftPackageReference "Web3" */; diff --git a/Example/IntegrationTests/Auth/AuthTests.swift b/Example/IntegrationTests/Auth/AuthTests.swift index eb18be0e9..1c2deff7d 100644 --- a/Example/IntegrationTests/Auth/AuthTests.swift +++ b/Example/IntegrationTests/Auth/AuthTests.swift @@ -68,8 +68,7 @@ final class AuthTests: XCTestCase { wallet.authRequestPublisher.sink { [unowned self] request in Task(priority: .high) { let signature = try! MessageSigner().sign(message: request.message, privateKey: prvKey) - let cacaoSignature = CacaoSignature(t: "eip191", s: signature) - try! await wallet.respond(requestId: request.id, signature: cacaoSignature) + try! await wallet.respond(requestId: request.id, signature: signature) } } .store(in: &publishers) @@ -85,9 +84,9 @@ final class AuthTests: XCTestCase { let responseExpectation = expectation(description: "error response delivered") let uri = try! await app.request(RequestParams.stub()) try! await wallet.pair(uri: uri) - wallet.authRequestPublisher.sink { [unowned self] (id, message) in + wallet.authRequestPublisher.sink { [unowned self] request in Task(priority: .high) { - try! await wallet.reject(requestId: id) + try! await wallet.reject(requestId: request.id) } } .store(in: &publishers) @@ -104,11 +103,11 @@ final class AuthTests: XCTestCase { let responseExpectation = expectation(description: "invalid signature response delivered") let uri = try! await app.request(RequestParams.stub()) try! await wallet.pair(uri: uri) - wallet.authRequestPublisher.sink { [unowned self] (id, message) in + wallet.authRequestPublisher.sink { [unowned self] request in Task(priority: .high) { let invalidSignature = "438effc459956b57fcd9f3dac6c675f9cee88abf21acab7305e8e32aa0303a883b06dcbd956279a7a2ca21ffa882ff55cc22e8ab8ec0f3fe90ab45f306938cfa1b" let cacaoSignature = CacaoSignature(t: "eip191", s: invalidSignature) - try! await wallet.respond(requestId: id, signature: cacaoSignature) + try! await wallet.respond(requestId: request.id, signature: cacaoSignature) } } .store(in: &publishers) diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestInteractor.swift index d68804e77..ed3702e8f 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestInteractor.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestInteractor.swift @@ -1,12 +1,17 @@ +import Foundation import Auth +import WalletConnectUtils final class AuthRequestInteractor { - func approve(request: AuthRequest) { - + func approve(request: AuthRequest) async throws { + let privateKey = Data(hex: "e56da0e170b5e09a8bb8f1b693392c7d56c3739a9c75740fbc558a2877868540") + let signer = MessageSigner() + let signature = try signer.sign(message: request.message, privateKey: privateKey) + try await Auth.instance.respond(requestId: request.id, signature: signature) } - func reject(request: AuthRequest) { - + func reject(request: AuthRequest) async throws { + try await Auth.instance.reject(requestId: request.id) } } diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift index aa498f7f9..f70700e86 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestPresenter.swift @@ -19,6 +19,18 @@ final class AuthRequestPresenter: ObservableObject { var message: String { return request.message } + + @MainActor + func approvePressed() async throws { + try await interactor.approve(request: request) + router.dismiss() + } + + @MainActor + func rejectPressed() async throws { + try await interactor.reject(request: request) + router.dismiss() + } } // MARK: SceneViewModel diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestRouter.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestRouter.swift index 3ff215e54..53d3d684e 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestRouter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestRouter.swift @@ -9,4 +9,8 @@ final class AuthRequestRouter { init(app: Application) { self.app = app } + + func dismiss() { + viewController.navigationController?.dismiss() + } } diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestView.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestView.swift index 2559b6de4..c8443c337 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestView.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/AuthRequest/AuthRequestView.swift @@ -20,7 +20,7 @@ struct AuthRequestView: View { .cornerRadius(10) HStack(spacing: 16.0) { - Button(action: { }, label: { + Button(action: { Task(priority: .userInitiated) { try await presenter.rejectPressed() }}, label: { HStack(spacing: 8.0) { Text("Reject") .foregroundColor(.w_foreground) @@ -33,7 +33,7 @@ struct AuthRequestView: View { .foregroundColor(.w_purpleForeground) ) - Button(action: { }, label: { + Button(action: { Task(priority: .userInitiated) { try await presenter.approvePressed() }}, label: { HStack(spacing: 8.0) { Text("Approve") .foregroundColor(.w_foreground) diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift index f0b10b2f5..287c0cc91 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift @@ -21,6 +21,10 @@ final class WalletPresenter: ObservableObject { try await self.interactor.pair(uri: uri) } } + + func didScanPairingURI() { + + } } // MARK: SceneViewModel diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletView.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletView.swift index 079746886..e9469e4b6 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletView.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletView.swift @@ -5,7 +5,7 @@ struct WalletView: View { @EnvironmentObject var presenter: WalletPresenter var body: some View { - VStack { + VStack(spacing: 16) { Button(action: { presenter.didPastePairingURI() }, label: { HStack(spacing: 8.0) { Text("Paste pairing URI") @@ -19,6 +19,20 @@ struct WalletView: View { Capsule() .foregroundColor(.w_greenForground) ) + + Button(action: { presenter.didScanPairingURI() }, label: { + HStack(spacing: 8.0) { + Text("Scan pairing URI") + .foregroundColor(.w_foreground) + .font(.system(size: 18, weight: .semibold)) + } + .padding(.trailing, 8.0) + }) + .frame(width: 200, height: 44) + .background( + Capsule() + .foregroundColor(.w_purpleForeground) + ) } } } diff --git a/Sources/Auth/Services/App/AppRespondSubscriber.swift b/Sources/Auth/Services/App/AppRespondSubscriber.swift index a73015b4c..21708bd73 100644 --- a/Sources/Auth/Services/App/AppRespondSubscriber.swift +++ b/Sources/Auth/Services/App/AppRespondSubscriber.swift @@ -55,7 +55,7 @@ class AppRespondSubscriber { guard messageFormatter.formatMessage(from: requestPayload.payloadParams, address: address) == message else { self.onResponse?(requestId, .failure(.messageCompromised)); return } - guard let _ = try? signatureVerifier.verify(signature: cacao.signature.s, message: message, address: address) + guard let _ = try? signatureVerifier.verify(signature: cacao.signature, message: message, address: address) else { self.onResponse?(requestId, .failure(.signatureVerificationFailed)); return } onResponse?(requestId, .success(cacao)) diff --git a/Sources/Auth/Services/Signer/MessageSigner.swift b/Sources/Auth/Services/Signer/MessageSigner.swift index d07972a5c..94896343f 100644 --- a/Sources/Auth/Services/Signer/MessageSigner.swift +++ b/Sources/Auth/Services/Signer/MessageSigner.swift @@ -1,11 +1,11 @@ import Foundation protocol MessageSignatureVerifying { - func verify(signature: String, message: String, address: String) throws + func verify(signature: CacaoSignature, message: String, address: String) throws } protocol MessageSigning { - func sign(message: String, privateKey: Data) throws -> String + func sign(message: String, privateKey: Data) throws -> CacaoSignature } public struct MessageSigner: MessageSignatureVerifying, MessageSigning { @@ -21,15 +21,15 @@ public struct MessageSigner: MessageSignatureVerifying, MessageSigning { self.signer = signer } - public func sign(message: String, privateKey: Data) throws -> String { + public func sign(message: String, privateKey: Data) throws -> CacaoSignature { guard let messageData = message.data(using: .utf8) else { throw Errors.utf8EncodingFailed } let signature = try signer.sign(message: messageData, with: privateKey) - return signature.toHexString() + return CacaoSignature(t: "eip191", s: signature.toHexString()) } - public func verify(signature: String, message: String, address: String) throws { + public func verify(signature: CacaoSignature, message: String, address: String) throws { guard let messageData = message.data(using: .utf8) else { throw Errors.utf8EncodingFailed } - let signatureData = Data(hex: signature) + let signatureData = Data(hex: signature.s) guard try signer.isValid(signature: signatureData, message: messageData, address: address) else { throw Errors.signatureValidationFailed } } diff --git a/Tests/AuthTests/AppRespondSubscriberTests.swift b/Tests/AuthTests/AppRespondSubscriberTests.swift index 6bd0b4e43..d7baec010 100644 --- a/Tests/AuthTests/AppRespondSubscriberTests.swift +++ b/Tests/AuthTests/AppRespondSubscriberTests.swift @@ -52,8 +52,7 @@ class AppRespondSubscriberTests: XCTestCase { let payload = CacaoPayload(params: AuthPayload.stub(nonce: "compromised nonce"), didpkh: DIDPKH(account: walletAccount)) let message = try! messageFormatter.formatMessage(from: payload) - let signature = try! messageSigner.sign(message: message, privateKey: prvKey) - let cacaoSignature = CacaoSignature(t: "eip191", s: signature) + let cacaoSignature = try! messageSigner.sign(message: message, privateKey: prvKey) let cacao = Cacao(header: header, payload: payload, signature: cacaoSignature) diff --git a/Tests/AuthTests/CacaoSignerTests.swift b/Tests/AuthTests/CacaoSignerTests.swift index a1615a361..5b74f7333 100644 --- a/Tests/AuthTests/CacaoSignerTests.swift +++ b/Tests/AuthTests/CacaoSignerTests.swift @@ -24,7 +24,7 @@ class CacaoSignerTest: XCTestCase { - https://example.com/my-web2-claim.json """ - let signature = "438effc459956b57fcd9f3dac6c675f9cee88abf21acab7305e8e32aa0303a883b06dcbd956279a7a2ca21ffa882ff55cc22e8ab8ec0f3fe90ab45f306938cfa1b" + let signature = CacaoSignature(t: "eip191", s: "438effc459956b57fcd9f3dac6c675f9cee88abf21acab7305e8e32aa0303a883b06dcbd956279a7a2ca21ffa882ff55cc22e8ab8ec0f3fe90ab45f306938cfa1b") func testCacaoSign() throws { let signer = MessageSigner(signer: Signer()) From e7c1bab146b3ba1da43b4bdf7951cedd49b99ced Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 26 Aug 2022 19:50:04 +0300 Subject: [PATCH 05/92] Scan QR --- Example/ExampleApp.xcodeproj/project.pbxproj | 48 +++++ .../Wallet/Scan/ScanInteractor.swift | 3 + .../Wallet/Scan/ScanModule.swift | 22 +++ .../Wallet/Scan/ScanPresenter.swift | 48 +++++ .../Wallet/Scan/ScanRouter.swift | 12 ++ .../Wallet/Scan/ScanView.swift | 18 ++ .../Wallet/Scan/Views/ScanQR.swift | 39 ++++ .../Wallet/Scan/Views/ScanQRView.swift | 187 ++++++++++++++++++ .../Wallet/Scan/Views/ScanTargetView.swift | 87 ++++++++ .../Wallet/Wallet/WalletPresenter.swift | 19 +- .../Wallet/Wallet/WalletRouter.swift | 10 + Example/Showcase/Other/Info.plist | 2 + 12 files changed, 490 insertions(+), 5 deletions(-) create mode 100644 Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanInteractor.swift create mode 100644 Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanModule.swift create mode 100644 Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanPresenter.swift create mode 100644 Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanRouter.swift create mode 100644 Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanView.swift create mode 100644 Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQR.swift create mode 100644 Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQRView.swift create mode 100644 Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanTargetView.swift diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 05e0fbfe7..c6a979651 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -50,6 +50,14 @@ 84FE684628ACDB4700C893FF /* RequestParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FE684528ACDB4700C893FF /* RequestParams.swift */; }; A50C036528AAD32200FE72D3 /* ClientDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50C036428AAD32200FE72D3 /* ClientDelegate.swift */; }; A50F3946288005B200064555 /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50F3945288005B200064555 /* Types.swift */; }; + A55CAAB028B92AFF00844382 /* ScanModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55CAAAB28B92AFF00844382 /* ScanModule.swift */; }; + A55CAAB128B92AFF00844382 /* ScanPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55CAAAC28B92AFF00844382 /* ScanPresenter.swift */; }; + A55CAAB228B92AFF00844382 /* ScanRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55CAAAD28B92AFF00844382 /* ScanRouter.swift */; }; + A55CAAB328B92AFF00844382 /* ScanInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55CAAAE28B92AFF00844382 /* ScanInteractor.swift */; }; + A55CAAB428B92AFF00844382 /* ScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55CAAAF28B92AFF00844382 /* ScanView.swift */; }; + A55CAAB928B92B4600844382 /* ScanQRView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55CAAB628B92B4600844382 /* ScanQRView.swift */; }; + A55CAABA28B92B4600844382 /* ScanTargetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55CAAB728B92B4600844382 /* ScanTargetView.swift */; }; + A55CAABB28B92B4600844382 /* ScanQR.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55CAAB828B92B4600844382 /* ScanQR.swift */; }; 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 */; }; @@ -230,6 +238,14 @@ 84FE684528ACDB4700C893FF /* RequestParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestParams.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 = ""; }; + A55CAAAB28B92AFF00844382 /* ScanModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanModule.swift; sourceTree = ""; }; + A55CAAAC28B92AFF00844382 /* ScanPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanPresenter.swift; sourceTree = ""; }; + A55CAAAD28B92AFF00844382 /* ScanRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanRouter.swift; sourceTree = ""; }; + A55CAAAE28B92AFF00844382 /* ScanInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanInteractor.swift; sourceTree = ""; }; + A55CAAAF28B92AFF00844382 /* ScanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanView.swift; sourceTree = ""; }; + A55CAAB628B92B4600844382 /* ScanQRView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScanQRView.swift; sourceTree = ""; }; + A55CAAB728B92B4600844382 /* ScanTargetView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScanTargetView.swift; sourceTree = ""; }; + A55CAAB828B92B4600844382 /* ScanQR.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScanQR.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 = ""; }; @@ -576,6 +592,29 @@ path = Types; sourceTree = ""; }; + A55CAAAA28B92AF200844382 /* Scan */ = { + isa = PBXGroup; + children = ( + A55CAAB528B92B3200844382 /* Views */, + A55CAAAB28B92AFF00844382 /* ScanModule.swift */, + A55CAAAC28B92AFF00844382 /* ScanPresenter.swift */, + A55CAAAD28B92AFF00844382 /* ScanRouter.swift */, + A55CAAAE28B92AFF00844382 /* ScanInteractor.swift */, + A55CAAAF28B92AFF00844382 /* ScanView.swift */, + ); + path = Scan; + sourceTree = ""; + }; + A55CAAB528B92B3200844382 /* Views */ = { + isa = PBXGroup; + children = ( + A55CAAB828B92B4600844382 /* ScanQR.swift */, + A55CAAB628B92B4600844382 /* ScanQRView.swift */, + A55CAAB728B92B4600844382 /* ScanTargetView.swift */, + ); + path = Views; + sourceTree = ""; + }; A5629AA42876A19D00094373 /* DomainLayer */ = { isa = PBXGroup; children = ( @@ -857,6 +896,7 @@ A59F876928B53E7800A9CD80 /* Wallet */ = { isa = PBXGroup; children = ( + A55CAAAA28B92AF200844382 /* Scan */, A59EBEFD28B54A2E003EDAAF /* AuthRequest */, A59F877428B53EA500A9CD80 /* Wallet */, ); @@ -1349,21 +1389,27 @@ A5629AE12876CC6E00094373 /* InviteListInteractor.swift in Sources */, A58E7CED28729F550082D443 /* SceneDelegate.swift in Sources */, A5C2020F287D9DEE007E3188 /* WelcomeView.swift in Sources */, + A55CAAB128B92AFF00844382 /* ScanPresenter.swift in Sources */, A59F877128B53EA000A9CD80 /* WalletRouter.swift in Sources */, A5C20226287EB099007E3188 /* AccountNameResolver.swift in Sources */, A5C2020D287D9DEE007E3188 /* WelcomeRouter.swift in Sources */, + A55CAAB328B92AFF00844382 /* ScanInteractor.swift in Sources */, + A55CAABA28B92B4600844382 /* ScanTargetView.swift in Sources */, A578FA372873D8EE00AA7720 /* UIColor.swift in Sources */, A5C2021D287E1FD8007E3188 /* ImportView.swift in Sources */, A5C2021A287E1FD8007E3188 /* ImportPresenter.swift in Sources */, A5629AE828772A0100094373 /* InviteViewModel.swift in Sources */, + A55CAAB928B92B4600844382 /* ScanQRView.swift in Sources */, A5629AA92876A23100094373 /* ChatService.swift in Sources */, A5C20229287EB34C007E3188 /* AccountStorage.swift in Sources */, A5629AC02876CBC000094373 /* ChatListInteractor.swift in Sources */, + A55CAAB428B92AFF00844382 /* ScanView.swift in Sources */, A5629AE22876CC6E00094373 /* InviteListView.swift in Sources */, A578FA3D2874002400AA7720 /* View.swift in Sources */, A5629AD72876CC5700094373 /* InviteView.swift in Sources */, A59EBEFA28B54A2A003EDAAF /* AuthRequestRouter.swift in Sources */, A5629AF02877F73000094373 /* SocketFactory.swift in Sources */, + A55CAAB228B92AFF00844382 /* ScanRouter.swift in Sources */, A5629AED2877F6A600094373 /* ChatFactory.swift in Sources */, A5C20221287EA5B8007E3188 /* TextFieldView.swift in Sources */, A5C2022B287EB89A007E3188 /* WelcomeInteractor.swift in Sources */, @@ -1373,7 +1419,9 @@ A59F877028B53EA000A9CD80 /* WalletPresenter.swift in Sources */, A5629AD32876CC5700094373 /* InviteModule.swift in Sources */, A5629AD52876CC5700094373 /* InviteRouter.swift in Sources */, + A55CAABB28B92B4600844382 /* ScanQR.swift in Sources */, A5629ABF2876CBC000094373 /* ChatListRouter.swift in Sources */, + A55CAAB028B92AFF00844382 /* ScanModule.swift in Sources */, A5629AC12876CBC000094373 /* ChatListView.swift in Sources */, A578FA392873FCE000AA7720 /* ChatScrollView.swift in Sources */, A58E7D1E2872A57B0082D443 /* ThirdPartyConfigurator.swift in Sources */, diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanInteractor.swift new file mode 100644 index 000000000..8585d6043 --- /dev/null +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanInteractor.swift @@ -0,0 +1,3 @@ +final class ScanInteractor { + +} diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanModule.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanModule.swift new file mode 100644 index 000000000..eac6302cd --- /dev/null +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanModule.swift @@ -0,0 +1,22 @@ +import SwiftUI + +final class ScanModule { + + @discardableResult + static func create( + app: Application, + onValue: @escaping (String) -> Void, + onError: @escaping (Error) -> Void + ) -> UIViewController { + let router = ScanRouter(app: app) + let interactor = ScanInteractor() + let presenter = ScanPresenter(interactor: interactor, router: router, onValue: onValue, onError: onError) + let view = ScanView().environmentObject(presenter) + let viewController = SceneViewController(viewModel: presenter, content: view) + + router.viewController = viewController + + return viewController + } + +} diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanPresenter.swift new file mode 100644 index 000000000..f36c77456 --- /dev/null +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanPresenter.swift @@ -0,0 +1,48 @@ +import UIKit +import Combine + +final class ScanPresenter: ObservableObject { + + private let interactor: ScanInteractor + private let router: ScanRouter + + private var disposeBag = Set() + + let onValue: (String) -> Void + let onError: (Error) -> Void + + init( + interactor: ScanInteractor, + router: ScanRouter, + onValue: @escaping (String) -> Void, + onError: @escaping (Error) -> Void + ) { + defer { setupInitialState() } + self.interactor = interactor + self.router = router + self.onValue = onValue + self.onError = onError + } +} + +// MARK: SceneViewModel + +extension ScanPresenter: SceneViewModel { + + var sceneTitle: String? { + return "Scan URI" + } + + var largeTitleDisplayMode: UINavigationItem.LargeTitleDisplayMode { + return .always + } +} + +// MARK: Privates + +private extension ScanPresenter { + + func setupInitialState() { + + } +} diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanRouter.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanRouter.swift new file mode 100644 index 000000000..eef0df5c6 --- /dev/null +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanRouter.swift @@ -0,0 +1,12 @@ +import UIKit + +final class ScanRouter { + + weak var viewController: UIViewController! + + private let app: Application + + init(app: Application) { + self.app = app + } +} diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanView.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanView.swift new file mode 100644 index 000000000..aec4003c3 --- /dev/null +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanView.swift @@ -0,0 +1,18 @@ +import SwiftUI + +struct ScanView: View { + + @EnvironmentObject var presenter: ScanPresenter + + var body: some View { + ScanQR(onValue: presenter.onValue, onError: presenter.onError) + } +} + +#if DEBUG +struct ScanView_Previews: PreviewProvider { + static var previews: some View { + ScanView() + } +} +#endif diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQR.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQR.swift new file mode 100644 index 000000000..0bb3369d6 --- /dev/null +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQR.swift @@ -0,0 +1,39 @@ +import SwiftUI + +struct ScanQR: UIViewRepresentable { + + class Coordinator: ScanQRViewDelegate { + private let onValue: (String) -> Void + private let onError: (Error) -> Void + + init(onValue: @escaping (String) -> Void, onError: @escaping (Error) -> Void) { + self.onValue = onValue + self.onError = onError + } + + func scanDidDetect(value: String) { + onValue(value) + } + + func scanDidFail(with error: Error) { + onError(error) + } + } + + let onValue: (String) -> Void + let onError: (Error) -> Void + + func makeUIView(context: Context) -> ScanQRView { + let view = ScanQRView() + view.delegate = context.coordinator + return view + } + + func updateUIView(_ uiView: ScanQRView, context: Context) { + + } + + func makeCoordinator() -> Coordinator { + return Coordinator(onValue: onValue, onError: onError) + } +} diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQRView.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQRView.swift new file mode 100644 index 000000000..c3c8327b4 --- /dev/null +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQRView.swift @@ -0,0 +1,187 @@ +import UIKit +import AVFoundation + +protocol ScanQRViewDelegate: AnyObject { + func scanDidDetect(value: String) + func scanDidFail(with error: Error) +} + +final class ScanQRView: UIView { + enum Errors: Error { + case deviceNotFound + } + + weak var delegate: ScanQRViewDelegate? + + private let targetSize = CGSize( + width: UIScreen.main.bounds.width - 32.0, + height: UIScreen.main.bounds.width - 32.0 + ) + + private var videoPreviewLayer: AVCaptureVideoPreviewLayer? + private var captureSession: AVCaptureSession? + + private lazy var borderView: UIView = { + let borderView = ScanTargetView(radius: 24.0, color: .white, strokeWidth: 2.0, length: 36.0) + borderView.alpha = 0.85 + return borderView + }() + + private lazy var bluredView: UIView = { + let blurEffect = UIBlurEffect(style: .dark) + let bluredView = UIVisualEffectView(effect: blurEffect) + bluredView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + bluredView.layer.mask = createMaskLayer() + return bluredView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + setupView() + startCaptureSession() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + updateFrames() + updateOrientation() + } + + deinit { + stopCaptureSession() + } +} + +// MARK: AVCaptureMetadataOutputObjectsDelegate + +extension ScanQRView: AVCaptureMetadataOutputObjectsDelegate { + + func metadataOutput(_ metadataOutput: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { + guard + let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject, + let value = metadataObject.stringValue else { + return + } + + delegate?.scanDidDetect(value: value) + } +} + +// MARK: Privates + +private extension ScanQRView { + + private func setupView() { + backgroundColor = .black + + addSubview(bluredView) + addSubview(borderView) + } + + private func createMaskLayer() -> CAShapeLayer { + let maskPath = UIBezierPath(rect: bounds) + let rect = UIBezierPath( + roundedRect: CGRect( + x: center.x - targetSize.height * 0.5, + y: center.y - targetSize.width * 0.5, + width: targetSize.width, + height: targetSize.height + ), + byRoundingCorners: .allCorners, + cornerRadii: CGSize(width: 24.0, height: 24.0) + ) + maskPath.append(rect) + maskPath.usesEvenOddFillRule = true + + let maskLayer = CAShapeLayer() + maskLayer.path = maskPath.cgPath + maskLayer.fillRule = .evenOdd + return maskLayer + } + + private func startCaptureSession() { + DispatchQueue.global().async { [weak self] in + guard let self = self else { return } + + do { + let session = try self.createCaptureSession() + session.startRunning() + self.captureSession = session + + DispatchQueue.main.async { self.setupVideoPreviewLayer(with: session) } + } catch { + DispatchQueue.main.async { self.delegate?.scanDidFail(with: error) } + } + } + } + + private func createCaptureSession() throws -> AVCaptureSession { + guard let captureDevice = AVCaptureDevice.default(for: .video) else { + throw Errors.deviceNotFound + } + + let input = try AVCaptureDeviceInput(device: captureDevice) + + let session = AVCaptureSession() + session.addInput(input) + + let captureMetadataOutput = AVCaptureMetadataOutput() + captureMetadataOutput.setMetadataObjectsDelegate(self, queue: .main) + session.addOutput(captureMetadataOutput) + + captureMetadataOutput.metadataObjectTypes = [.qr] + + return session + } + + private func stopCaptureSession() { + captureSession?.stopRunning() + captureSession = nil + } + + private func setupVideoPreviewLayer(with session: AVCaptureSession) { + let previewLayer = AVCaptureVideoPreviewLayer(session: session) + previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill + previewLayer.frame = layer.bounds + videoPreviewLayer = previewLayer + + layer.insertSublayer(previewLayer, at: 0) + } + + private func updateFrames() { + borderView.frame.size = targetSize + borderView.center = center + bluredView.frame = bounds + bluredView.layer.mask = createMaskLayer() + videoPreviewLayer?.frame = layer.bounds + } + + private func updateOrientation() { + guard let connection = videoPreviewLayer?.connection else { + return + } + let previewLayerConnection: AVCaptureConnection = connection + + guard previewLayerConnection.isVideoOrientationSupported else { + return + } + switch UIDevice.current.orientation { + case .portrait: return + previewLayerConnection.videoOrientation = .portrait + case .landscapeRight: + previewLayerConnection.videoOrientation = .landscapeLeft + case .landscapeLeft: return + previewLayerConnection.videoOrientation = .landscapeRight + case .portraitUpsideDown: + previewLayerConnection.videoOrientation = .portraitUpsideDown + default: + previewLayerConnection.videoOrientation = .portrait + } + } +} diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanTargetView.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanTargetView.swift new file mode 100644 index 000000000..99bbaaa3e --- /dev/null +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanTargetView.swift @@ -0,0 +1,87 @@ +import UIKit + +final class ScanTargetView: UIView { + + private let radius: CGFloat + private let color: UIColor + private let strokeWidth: CGFloat + private let length: CGFloat + + private lazy var shapeLayer: CAShapeLayer = { + let shapeLayer = CAShapeLayer() + shapeLayer.strokeColor = color.cgColor + shapeLayer.fillColor = UIColor.clear.cgColor + shapeLayer.lineWidth = strokeWidth + return shapeLayer + }() + + init(radius: CGFloat, color: UIColor, strokeWidth: CGFloat, length: CGFloat) { + self.radius = radius + self.color = color + self.strokeWidth = strokeWidth + self.length = length + + super.init(frame: .zero) + + setupView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + shapeLayer.path = targetPath() + } + + private func setupView() { + backgroundColor = .clear + layer.addSublayer(shapeLayer) + } + + private func targetPath() -> CGPath { + let path = UIBezierPath() + path.append(createTopLeft()) + path.append(createTopRight()) + path.append(createBottomLeft()) + path.append(createBottomRight()) + return path.cgPath + } + + private func createTopLeft() -> UIBezierPath { + let topLeft = UIBezierPath() + topLeft.move(to: CGPoint(x: strokeWidth/2, y: radius+length)) + topLeft.addLine(to: CGPoint(x: strokeWidth/2, y: radius)) + topLeft.addQuadCurve(to: CGPoint(x: radius, y: strokeWidth/2), controlPoint: CGPoint(x: strokeWidth/2, y: strokeWidth/2)) + topLeft.addLine(to: CGPoint(x: radius+length, y: strokeWidth/2)) + return topLeft + } + + private func createTopRight() -> UIBezierPath { + let topRight = UIBezierPath() + topRight.move(to: CGPoint(x: frame.width-radius-length, y: strokeWidth/2)) + topRight.addLine(to: CGPoint(x: frame.width-radius, y: strokeWidth/2)) + topRight.addQuadCurve(to: CGPoint(x: frame.width-strokeWidth/2, y: radius), controlPoint: CGPoint(x: frame.width-strokeWidth/2, y: strokeWidth/2)) + topRight.addLine(to: CGPoint(x: frame.width-strokeWidth/2, y: radius+length)) + return topRight + } + + private func createBottomRight() -> UIBezierPath { + let bottomRight = UIBezierPath() + bottomRight.move(to: CGPoint(x: frame.width-strokeWidth/2, y: frame.height-radius-length)) + bottomRight.addLine(to: CGPoint(x: frame.width-strokeWidth/2, y: frame.height-radius)) + bottomRight.addQuadCurve(to: CGPoint(x: frame.width-radius, y: frame.height-strokeWidth/2), controlPoint: CGPoint(x: frame.width-strokeWidth/2, y: frame.height-strokeWidth/2)) + bottomRight.addLine(to: CGPoint(x: frame.width-radius-length, y: frame.height-strokeWidth/2)) + return bottomRight + } + + private func createBottomLeft() -> UIBezierPath { + let bottomLeft = UIBezierPath() + bottomLeft.move(to: CGPoint(x: radius+length, y: frame.height-strokeWidth/2)) + bottomLeft.addLine(to: CGPoint(x: radius, y: frame.height-strokeWidth/2)) + bottomLeft.addQuadCurve(to: CGPoint(x: strokeWidth/2, y: frame.height-radius), controlPoint: CGPoint(x: strokeWidth/2, y: frame.height-strokeWidth/2)) + bottomLeft.addLine(to: CGPoint(x: strokeWidth/2, y: frame.height-radius-length)) + return bottomLeft + } +} diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift index 287c0cc91..2f631ad65 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift @@ -16,14 +16,17 @@ final class WalletPresenter: ObservableObject { func didPastePairingURI() { guard let uri = UIPasteboard.general.string else { return } - - Task(priority: .userInitiated) { [unowned self] in - try await self.interactor.pair(uri: uri) - } + pair(uri: uri) } func didScanPairingURI() { - + router.presentScan { [unowned self] value in + self.pair(uri: value) + self.router.dismiss() + } onError: { error in + print(error.localizedDescription) + self.router.dismiss() + } } } @@ -49,4 +52,10 @@ private extension WalletPresenter { self.router.present(request: request) }.store(in: &disposeBag) } + + func pair(uri: String) { + Task(priority: .high) { [unowned self] in + try await self.interactor.pair(uri: uri) + } + } } diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletRouter.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletRouter.swift index 50b4ed24b..288f481f8 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletRouter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletRouter.swift @@ -16,4 +16,14 @@ final class WalletRouter { .wrapToNavigationController() .present(from: viewController) } + + func presentScan(onValue: @escaping (String) -> Void, onError: @escaping (Error) -> Void) { + ScanModule.create(app: app, onValue: onValue, onError: onError) + .wrapToNavigationController() + .present(from: viewController) + } + + func dismiss() { + viewController.navigationController?.dismiss() + } } diff --git a/Example/Showcase/Other/Info.plist b/Example/Showcase/Other/Info.plist index bc240fdb1..852f59f09 100644 --- a/Example/Showcase/Other/Info.plist +++ b/Example/Showcase/Other/Info.plist @@ -2,6 +2,8 @@ + NSCameraUsageDescription + Used to scan pairing URI NSAppTransportSecurity NSAllowsArbitraryLoads From 4d7d10238633476df01e231f1641d9f40c001062 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 26 Aug 2022 20:10:42 +0300 Subject: [PATCH 06/92] Scan Pairing URI --- .../Classes/PresentationLayer/Wallet/Scan/ScanView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanView.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanView.swift index aec4003c3..f8f36e7bb 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanView.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/ScanView.swift @@ -6,6 +6,7 @@ struct ScanView: View { var body: some View { ScanQR(onValue: presenter.onValue, onError: presenter.onError) + .ignoresSafeArea() } } From 4c272ee2c9bc0e84a9e7b917a0d7fa60e89086d1 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Mon, 29 Aug 2022 14:10:47 +0300 Subject: [PATCH 07/92] Deeplink + pairing bug fix --- Example/DApp/Auth/AuthView.swift | 3 +++ Example/DApp/Auth/AuthViewModel.swift | 5 +++++ .../Classes/ApplicationLayer/SceneDelegate.swift | 10 ++++++++++ Example/Showcase/Other/Info.plist | 13 +++++++++++-- Sources/Auth/AuthClient.swift | 2 +- Sources/Auth/AuthClientFactory.swift | 2 +- Sources/WalletConnectRelay/AppStateObserving.swift | 1 - Sources/WalletConnectRelay/RelayClient.swift | 2 +- .../AutomaticSocketConnectionHandler.swift | 4 +++- 9 files changed, 35 insertions(+), 7 deletions(-) diff --git a/Example/DApp/Auth/AuthView.swift b/Example/DApp/Auth/AuthView.swift index 24d029c1b..f120f1871 100644 --- a/Example/DApp/Auth/AuthView.swift +++ b/Example/DApp/Auth/AuthView.swift @@ -24,6 +24,9 @@ struct AuthView: View { Button("Copy URI", action: { viewModel.copyDidPressed() }) .buttonStyle(CircleButtonStyle()) + + Button("Deeplink", action: { viewModel.deeplinkPressed() }) + .buttonStyle(CircleButtonStyle()) } .padding(16.0) .onAppear { Task(priority: .userInitiated) { diff --git a/Example/DApp/Auth/AuthViewModel.swift b/Example/DApp/Auth/AuthViewModel.swift index fe823ee05..037e5131a 100644 --- a/Example/DApp/Auth/AuthViewModel.swift +++ b/Example/DApp/Auth/AuthViewModel.swift @@ -37,6 +37,11 @@ final class AuthViewModel: ObservableObject { func walletDidPressed() { } + + func deeplinkPressed() { + guard let uri = uri else { return } + UIApplication.shared.open(URL(string: "showcase://wc?uri=\(uri)")!) + } } private extension AuthViewModel { diff --git a/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift b/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift index 08d86bdbc..8660ed4c5 100644 --- a/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift +++ b/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift @@ -1,4 +1,5 @@ import UIKit +import Auth class SceneDelegate: UIResponder, UIWindowSceneDelegate { @@ -23,4 +24,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { configurators.configure() } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + guard let context = URLContexts.first else { return } + + let uri = context.url.absoluteString.replacingOccurrences(of: "showcase://wc?uri=", with: "") + Task { + try await Auth.instance.pair(uri: uri) + } + } } diff --git a/Example/Showcase/Other/Info.plist b/Example/Showcase/Other/Info.plist index 852f59f09..15a9080c9 100644 --- a/Example/Showcase/Other/Info.plist +++ b/Example/Showcase/Other/Info.plist @@ -2,8 +2,17 @@ - NSCameraUsageDescription - Used to scan pairing URI + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + showcase + + + NSAppTransportSecurity NSAllowsArbitraryLoads diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index 5fcfcc112..6ea6d040c 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -49,7 +49,7 @@ public class AuthClient { logger: ConsoleLogging, pairingStorage: WCPairingStorage, socketConnectionStatusPublisher: AnyPublisher -) { + ) { self.appPairService = appPairService self.appRequestService = appRequestService self.walletPairService = walletPairService diff --git a/Sources/Auth/AuthClientFactory.swift b/Sources/Auth/AuthClientFactory.swift index 6be615528..46f103383 100644 --- a/Sources/Auth/AuthClientFactory.swift +++ b/Sources/Auth/AuthClientFactory.swift @@ -7,7 +7,7 @@ import WalletConnectPairing public struct AuthClientFactory { public static func create(metadata: AppMetadata, account: Account?, relayClient: RelayClient) -> AuthClient { - let logger = ConsoleLogger(loggingLevel: .off) + let logger = ConsoleLogger(loggingLevel: .debug) let keyValueStorage = UserDefaults.standard let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") return AuthClientFactory.create(metadata: metadata, account: account, logger: logger, keyValueStorage: keyValueStorage, keychainStorage: keychainStorage, relayClient: relayClient) diff --git a/Sources/WalletConnectRelay/AppStateObserving.swift b/Sources/WalletConnectRelay/AppStateObserving.swift index fec2c7a06..bc708c74b 100644 --- a/Sources/WalletConnectRelay/AppStateObserving.swift +++ b/Sources/WalletConnectRelay/AppStateObserving.swift @@ -41,5 +41,4 @@ class AppStateObserver: AppStateObserving { private func appWillEnterForeground() { onWillEnterForeground?() } - } diff --git a/Sources/WalletConnectRelay/RelayClient.swift b/Sources/WalletConnectRelay/RelayClient.swift index 0c18c01d7..2c475b654 100644 --- a/Sources/WalletConnectRelay/RelayClient.swift +++ b/Sources/WalletConnectRelay/RelayClient.swift @@ -82,7 +82,7 @@ public final class RelayClient { keychainStorage: KeychainStorageProtocol = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk"), socketFactory: WebSocketFactory, socketConnectionType: SocketConnectionType = .automatic, - logger: ConsoleLogging = ConsoleLogger(loggingLevel: .off) + logger: ConsoleLogging = ConsoleLogger(loggingLevel: .debug) ) { let socketAuthenticator = SocketAuthenticator( clientIdStorage: ClientIdStorage(keychain: keychainStorage), diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift index 20eab6208..273b3a175 100644 --- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift +++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift @@ -36,7 +36,9 @@ class AutomaticSocketConnectionHandler: SocketConnectionHandler { } appStateObserver.onWillEnterForeground = { [unowned self] in - socket.connect() + if !socket.isConnected { + socket.connect() + } } } From 1d0aa52e4414aea2e6835a7f57d7b84237c857fe Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 30 Aug 2022 04:44:42 +0300 Subject: [PATCH 08/92] Disable logger --- Sources/Auth/AuthClientFactory.swift | 2 +- Sources/WalletConnectRelay/RelayClient.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Auth/AuthClientFactory.swift b/Sources/Auth/AuthClientFactory.swift index 46f103383..6be615528 100644 --- a/Sources/Auth/AuthClientFactory.swift +++ b/Sources/Auth/AuthClientFactory.swift @@ -7,7 +7,7 @@ import WalletConnectPairing public struct AuthClientFactory { public static func create(metadata: AppMetadata, account: Account?, relayClient: RelayClient) -> AuthClient { - let logger = ConsoleLogger(loggingLevel: .debug) + let logger = ConsoleLogger(loggingLevel: .off) let keyValueStorage = UserDefaults.standard let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") return AuthClientFactory.create(metadata: metadata, account: account, logger: logger, keyValueStorage: keyValueStorage, keychainStorage: keychainStorage, relayClient: relayClient) diff --git a/Sources/WalletConnectRelay/RelayClient.swift b/Sources/WalletConnectRelay/RelayClient.swift index 2c475b654..0c18c01d7 100644 --- a/Sources/WalletConnectRelay/RelayClient.swift +++ b/Sources/WalletConnectRelay/RelayClient.swift @@ -82,7 +82,7 @@ public final class RelayClient { keychainStorage: KeychainStorageProtocol = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk"), socketFactory: WebSocketFactory, socketConnectionType: SocketConnectionType = .automatic, - logger: ConsoleLogging = ConsoleLogger(loggingLevel: .debug) + logger: ConsoleLogging = ConsoleLogger(loggingLevel: .off) ) { let socketAuthenticator = SocketAuthenticator( clientIdStorage: ClientIdStorage(keychain: keychainStorage), From c5e8c0978b216150d724581f793a8927245cbf14 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 24 Aug 2022 14:31:06 +0200 Subject: [PATCH 09/92] auth public api docs --- Sources/Auth/AuthClient.swift | 14 ++++++++++++++ Sources/Auth/Types/RequestParams.swift | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index 5fcfcc112..2fbaabeb8 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -66,6 +66,13 @@ public class AuthClient { setUpPublishers() } + /// For wallet to establish a pairing and receive an authentication request + /// Wallet should call this function in order to accept peer's pairing proposal and be able to subscribe for future authentication request. + /// - Parameter uri: Pairing URI that is commonly presented as a QR code by a dapp or delivered with universal linking. + /// + /// Throws Error: + /// - When URI is invalid format or missing params + /// - When topic is already in use public func pair(uri: String) async throws { guard let pairingURI = WalletConnectURI(string: uri) else { throw Errors.malformedPairingURI @@ -73,6 +80,10 @@ public class AuthClient { try await walletPairService.pair(pairingURI) } + /// For a dapp to send an authentication request to a wallet + /// - Parameter params: Set of parameters required to request authentication + /// + /// - Returns: Pairing URI that should be shared with wallet out of bound. Common way is to present it as a QR code. public func request(_ params: RequestParams) async throws -> String { logger.debug("Requesting Authentication") let uri = try await appPairService.create() @@ -80,6 +91,9 @@ public class AuthClient { return uri.absoluteString } + /// For a dapp to send an authentication request to a wallet + /// - Parameter params: Set of parameters required to request authentication + /// - Parameter topic: Pairing topic that wallet already subscribes for public func request(_ params: RequestParams, topic: String) async throws { logger.debug("Requesting Authentication on existing pairing") guard pairingStorage.hasPairing(forTopic: topic) else { diff --git a/Sources/Auth/Types/RequestParams.swift b/Sources/Auth/Types/RequestParams.swift index e5a7295de..1f57884a9 100644 --- a/Sources/Auth/Types/RequestParams.swift +++ b/Sources/Auth/Types/RequestParams.swift @@ -1,5 +1,9 @@ import Foundation +/// Parameters required to construct authentication request +/// for details read CAIP-74 and EIP-4361 specs +/// https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-74.md +/// https://eips.ethereum.org/EIPS/eip-4361 public struct RequestParams { public let domain: String public let chainId: String From 2c31ac12bdfe23ab7cfd08f213ed6595a1fb16cf Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 30 Aug 2022 10:26:58 +0200 Subject: [PATCH 10/92] update auth client public api docs --- Sources/Auth/AuthClient.swift | 23 +++++++++++++++++++ Sources/Auth/Types/Errors/AuthError.swift | 1 + .../WalletConnectSign/Sign/SignClient.swift | 9 +++----- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index 2fbaabeb8..a5dc38a05 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -5,6 +5,11 @@ import WalletConnectPairing import WalletConnectRelay +/// WalletConnect Auth Client +/// +/// Cannot be instantiated outside of the SDK +/// +/// Access via `Auth.instance` public class AuthClient { enum Errors: Error { case malformedPairingURI @@ -12,11 +17,21 @@ public class AuthClient { case noPairingMatchingTopic } private var authRequestPublisherSubject = PassthroughSubject() + + /// Publisher that sends authentication requests + /// + /// Wallet should subscribe on events in order to receive auth requests. public var authRequestPublisher: AnyPublisher { authRequestPublisherSubject.eraseToAnyPublisher() } private var authResponsePublisherSubject = PassthroughSubject<(id: RPCID, result: Result), Never>() + + /// Publisher that sends authentication responses + /// + /// App should subscribe for events in order to receive CACAO object with a signature matching authentication request. + /// + /// Emited result may be an error. public var authResponsePublisher: AnyPublisher<(id: RPCID, result: Result), Never> { authResponsePublisherSubject.eraseToAnyPublisher() } @@ -102,15 +117,23 @@ public class AuthClient { try await appRequestService.request(params: params, topic: topic) } + /// For a wallet to respond on authentication request + /// - Parameters: + /// - requestId: authentication request id + /// - signature: CACAO signature of requested message public func respond(requestId: RPCID, signature: CacaoSignature) async throws { guard let account = account else { throw Errors.unknownWalletAddress } try await walletRespondService.respond(requestId: requestId, signature: signature, account: account) } + /// For wallet to reject authentication request + /// - Parameter requestId: authentication request id public func reject(requestId: RPCID) async throws { try await walletRespondService.respondError(requestId: requestId) } + /// Query pending authentication requests + /// - Returns: Pending authentication requests public func getPendingRequests() throws -> [AuthRequest] { guard let account = account else { throw Errors.unknownWalletAddress } return try pendingRequestsProvider.getPendingRequests(account: account) diff --git a/Sources/Auth/Types/Errors/AuthError.swift b/Sources/Auth/Types/Errors/AuthError.swift index f5b52b21a..84c43a7d0 100644 --- a/Sources/Auth/Types/Errors/AuthError.swift +++ b/Sources/Auth/Types/Errors/AuthError.swift @@ -1,5 +1,6 @@ import Foundation +/// Authentication error public enum AuthError: Codable, Equatable, Error { case userRejeted case malformedResponseParams diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index d8fc4d288..a87e1950c 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -4,14 +4,11 @@ import WalletConnectUtils import WalletConnectKMS import Combine -/// An Object that expose public API to provide interactions with WalletConnect SDK +/// WalletConnect Sign Client /// -/// WalletConnect Client is not a singleton but once you create an instance, you should not deinitialize it. Usually only one instance of a client is required in the application. +/// Cannot be instantiated outside of the SDK /// -/// ```swift -/// let metadata = AppMetadata(name: String?, description: String?, url: String?, icons: [String]?) -/// let client = SignClient(metadata: AppMetadata, projectId: String, relayHost: String) -/// ``` +/// Access via `Sign.instance` public final class SignClient { /// Tells the delegate that session proposal has been received. From 4d052c3f7821ef712514ccb4571887c9ec9eb264 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 30 Aug 2022 11:17:14 +0200 Subject: [PATCH 11/92] Update Sign client properties documentation --- Sources/Auth/AuthClient.swift | 17 ++++--- .../WalletConnectSign/Sign/SignClient.swift | 46 +++++++++---------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index a5dc38a05..cd7519c41 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -16,7 +16,8 @@ public class AuthClient { case unknownWalletAddress case noPairingMatchingTopic } - private var authRequestPublisherSubject = PassthroughSubject() + + // MARK: - Public Properties /// Publisher that sends authentication requests /// @@ -25,8 +26,6 @@ public class AuthClient { authRequestPublisherSubject.eraseToAnyPublisher() } - private var authResponsePublisherSubject = PassthroughSubject<(id: RPCID, result: Result), Never>() - /// Publisher that sends authentication responses /// /// App should subscribe for events in order to receive CACAO object with a signature matching authentication request. @@ -36,20 +35,26 @@ public class AuthClient { authResponsePublisherSubject.eraseToAnyPublisher() } + /// Publisher that sends web socket connection status public let socketConnectionStatusPublisher: AnyPublisher + /// An object that loggs SDK's errors and info messages + public let logger: ConsoleLogging + + + // MARK: - Private Properties + + private var authResponsePublisherSubject = PassthroughSubject<(id: RPCID, result: Result), Never>() + private var authRequestPublisherSubject = PassthroughSubject() private let appPairService: AppPairService private let appRequestService: AppRequestService private let appRespondSubscriber: AppRespondSubscriber - private let walletPairService: WalletPairService private let walletRequestSubscriber: WalletRequestSubscriber private let walletRespondService: WalletRespondService private let cleanupService: CleanupService private let pairingStorage: WCPairingStorage private let pendingRequestsProvider: PendingRequestsProvider - public let logger: ConsoleLogging - private var account: Account? init(appPairService: AppPairService, diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index a87e1950c..8d8d8e345 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -11,77 +11,77 @@ import Combine /// Access via `Sign.instance` public final class SignClient { - /// Tells the delegate that session proposal has been received. + // MARK: - Public Properties + + /// Publisher that sends session proposal /// - /// Function is executed on responder client only + /// event is emited on responder client only public var sessionProposalPublisher: AnyPublisher { sessionProposalPublisherSubject.eraseToAnyPublisher() } - /// Tells the delegate that session payload request has been received + /// Publisher that sends session request /// - /// In most cases that function is supposed to be called on wallet client. - /// - Parameters: - /// - sessionRequest: Object containing request received from peer client. + /// In most cases event will be emited on wallet public var sessionRequestPublisher: AnyPublisher { sessionRequestPublisherSubject.eraseToAnyPublisher() } - /// Tells the delegate that client has connected WebSocket + /// Publisher that sends web socket connection status public var socketConnectionStatusPublisher: AnyPublisher { socketConnectionStatusPublisherSubject.eraseToAnyPublisher() } - /// Tells the delegate that the client has settled a session. + /// Publisher that sends session when one is settled /// - /// Function is executed on proposer and responder client when both communicating peers have successfully established a session. + /// Event is emited on proposer and responder client when both communicating peers have successfully established a session. public var sessionSettlePublisher: AnyPublisher { sessionSettlePublisherSubject.eraseToAnyPublisher() } - /// Tells the delegate that the peer client has terminated the session. + /// Publisher that sends deleted session topic /// - /// Function can be executed on any type of the client. + /// Event can be emited on any type of the client. public var sessionDeletePublisher: AnyPublisher<(String, Reason), Never> { sessionDeletePublisherSubject.eraseToAnyPublisher() } - /// Tells the delegate that session payload response has been received + /// Publisher that sends response for session request /// - /// In most cases that function is supposed to be called on dApp client. - /// - Parameters: - /// - sessionResponse: Object containing response received from peer client. + /// In most cases that event will be emited on dApp client. public var sessionResponsePublisher: AnyPublisher { sessionResponsePublisherSubject.eraseToAnyPublisher() } - /// Tells the delegate that peer client has rejected a session proposal. + /// Publisher that sends session proposal that has been rejected /// - /// Function will be executed on proposer client only. + /// Event will be emited on dApp client only. public var sessionRejectionPublisher: AnyPublisher<(Session.Proposal, Reason), Never> { sessionRejectionPublisherSubject.eraseToAnyPublisher() } - /// Tells the delegate that methods has been updated in session + /// Publisher that sends session topic and namespaces on session update /// - /// Function is executed on controller and non-controller client when both communicating peers have successfully updated methods requested by the controller client. + /// Event will be emited controller and non-controller client when both communicating peers have successfully updated methods requested by the controller client. public var sessionUpdatePublisher: AnyPublisher<(sessionTopic: String, namespaces: [String: SessionNamespace]), Never> { sessionUpdatePublisherSubject.eraseToAnyPublisher() } - /// Tells the delegate that event has been received. + /// Publisher that sends session event + /// + /// Event will be emited on dApp client only public var sessionEventPublisher: AnyPublisher<(event: Session.Event, sessionTopic: String, chainId: Blockchain?), Never> { sessionEventPublisherSubject.eraseToAnyPublisher() } - /// Tells the delegate that session expiry has been updated + /// Publisher that sends session topic when session is extended /// - /// Function will be executed on controller and non-controller clients. + /// Event will be emited on controller and non-controller clients. public var sessionExtendPublisher: AnyPublisher<(sessionTopic: String, date: Date), Never> { sessionExtendPublisherSubject.eraseToAnyPublisher() } - /// An object for logging messages + /// An object that loggs SDK's errors and info messages public let logger: ConsoleLogging // MARK: - Private properties From 608f637601fecd76d0f7a03e36ea4c6a978c6ac8 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 30 Aug 2022 12:04:40 +0200 Subject: [PATCH 12/92] update sign client api --- .../Sign/Connect/ConnectViewController.swift | 2 +- .../Sign/SignClientTests.swift | 2 +- .../Engine/Common/PairingEngine.swift | 2 +- .../WalletConnectSign/Sign/SignClient.swift | 91 +++++++++---------- 4 files changed, 47 insertions(+), 50 deletions(-) diff --git a/Example/DApp/Sign/Connect/ConnectViewController.swift b/Example/DApp/Sign/Connect/ConnectViewController.swift index 6f253ecbe..8a9c6aec9 100644 --- a/Example/DApp/Sign/Connect/ConnectViewController.swift +++ b/Example/DApp/Sign/Connect/ConnectViewController.swift @@ -4,7 +4,7 @@ import WalletConnectSign class ConnectViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { let uriString: String - let activePairings: [Pairing] = Sign.instance.getSettledPairings() + let activePairings: [Pairing] = Sign.instance.getPairings() let segmentedControl = UISegmentedControl(items: ["Pairings", "New Pairing"]) init(uri: String) { diff --git a/Example/IntegrationTests/Sign/SignClientTests.swift b/Example/IntegrationTests/Sign/SignClientTests.swift index cd79047f4..d6fcc75d1 100644 --- a/Example/IntegrationTests/Sign/SignClientTests.swift +++ b/Example/IntegrationTests/Sign/SignClientTests.swift @@ -143,7 +143,7 @@ final class SignClientTests: XCTestCase { let uri = try await dapp.client.connect(requiredNamespaces: ProposalNamespace.stubRequired())! try await wallet.client.pair(uri: uri) - let pairing = wallet.client.getSettledPairings().first! + let pairing = wallet.client.getPairings().first! wallet.client.ping(topic: pairing.topic) { result in if case .failure = result { XCTFail() } pongResponseExpectation.fulfill() diff --git a/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift b/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift index 106652d6e..d9ec3ac3c 100644 --- a/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift @@ -43,7 +43,7 @@ final class PairingEngine { return pairing } - func getSettledPairings() -> [Pairing] { + func getPairings() -> [Pairing] { pairingStore.getAll() .map {Pairing(topic: $0.topic, peer: $0.peerMetadata, expiryDate: $0.expiryDate)} } diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index 8d8d8e345..d00e54242 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -139,10 +139,11 @@ public final class SignClient { // MARK: - Public interface - /// For the Proposer to propose a session to a responder. - /// Function will create pending pairing sequence or propose a session on existing pairing. When responder client approves pairing, session is be proposed automatically by your client. - /// - Parameter sessionPermissions: The session permissions the responder will be requested for. - /// - Parameter topic: Optional parameter - use it if you already have an established pairing with peer client. + /// For a dApp to propose a session to a wallet. + /// Function will create pairing and propose session or propose a session on existing pairing. + /// - Parameters: + /// - requiredNamespaces: required namespaces for a session + /// - topic: Optional parameter - use it if you already have an established pairing with peer client. /// - Returns: Pairing URI that should be shared with responder out of bound. Common way is to present it as a QR code. Pairing URI will be nil if you are going to establish a session on existing Pairing and `topic` function parameter was provided. public func connect(requiredNamespaces: [String: ProposalNamespace], topic: String? = nil) async throws -> String? { logger.debug("Connecting Application") @@ -160,12 +161,12 @@ public final class SignClient { } } - /// For responder to receive a session proposal from a proposer - /// Responder should call this function in order to accept peer's pairing proposal and be able to subscribe for future session proposals. + /// For wallet to receive a session proposal from a dApp + /// Responder should call this function in order to accept peer's pairing and be able to subscribe for future session proposals. /// - Parameter uri: Pairing URI that is commonly presented as a QR code by a dapp. /// /// Should Error: - /// - When URI is invalid format or missing params + /// - When URI has invalid format or missing params /// - When topic is already in use public func pair(uri: String) async throws { guard let pairingURI = WalletConnectURI(string: uri) else { @@ -174,26 +175,23 @@ public final class SignClient { try await pairEngine.pair(pairingURI) } - /// For the responder to approve a session proposal. + /// For a wallet to approve a session proposal. /// - Parameters: - /// - proposalId: Session Proposal Public key received from peer client in a WalletConnect delegate function: `didReceive(sessionProposal: Session.Proposal)` - /// - accounts: A Set of accounts that the dapp will be allowed to request methods executions on. - /// - methods: A Set of methods that the dapp will be allowed to request. - /// - events: A Set of events + /// - proposalId: Session Proposal id + /// - namespaces: namespaces for given session, needs to contain at least required namespaces proposed by dApp. public func approve(proposalId: String, namespaces: [String: SessionNamespace]) async throws { - // TODO - accounts should be validated for matching namespaces BEFORE responding proposal try await approveEngine.approveProposal(proposerPubKey: proposalId, validating: namespaces) } - /// For the responder to reject a session proposal. + /// For the wallet to reject a session proposal. /// - Parameters: - /// - proposalId: Session Proposal Public key received from peer client in a WalletConnect delegate. - /// - reason: Reason why the session proposal was rejected. Conforms to CAIP25. + /// - proposalId: Session Proposal id + /// - reason: Reason why the session proposal has been rejected. Conforms to CAIP25. public func reject(proposalId: String, reason: RejectionReason) async throws { try await approveEngine.reject(proposerPubKey: proposalId, reason: reason.internalRepresentation()) } - /// For the responder to update session namespaces + /// For the wallet to update session namespaces /// - Parameters: /// - topic: Topic of the session that is intended to be updated. /// - namespaces: Dictionary of namespaces that will replace existing ones. @@ -201,10 +199,9 @@ public final class SignClient { try await controllerSessionStateMachine.update(topic: topic, namespaces: namespaces) } - /// For controller to update expiry of a session + /// For wallet to extend a session to 7 days /// - Parameters: - /// - topic: Topic of the Session, it can be a pairing or a session topic. - /// - ttl: Time in seconds that a target session is expected to be extended for. Must be greater than current time to expire and than 7 days + /// - topic: Topic of the session that is intended to be extended. public func extend(topic: String) async throws { let ttl: Int64 = Session.defaultTimeToLive if sessionEngine.hasSession(for: topic) { @@ -212,14 +209,14 @@ public final class SignClient { } } - /// For the proposer to send JSON-RPC requests to responding peer. + /// For a dApp to send JSON-RPC requests to wallet. /// - Parameters: /// - params: Parameters defining request and related session public func request(params: Request) async throws { try await sessionEngine.request(params) } - /// For the responder to respond on pending peer's session JSON-RPC Request + /// For the wallet to respond on pending dApp's JSON-RPC request /// - Parameters: /// - topic: Topic of the session for which the request was received. /// - response: Your JSON RPC response or an error. @@ -227,16 +224,15 @@ public final class SignClient { try await sessionEngine.respondSessionRequest(topic: topic, response: response) } - /// Ping method allows to check if client's peer is online and is subscribing for your sequence topic + /// Ping method allows to check if peer client is online and is subscribing for given topic /// /// Should Error: /// - When the session topic is not found /// - When the response is neither result or error - /// - When the peer fails to respond within timeout /// /// - Parameters: - /// - topic: Topic of the sequence, it can be a pairing or a session topic. - /// - completion: Result will be success on response or error on timeout. -- TODO: timeout + /// - topic: Topic of a session or a pairing + /// - completion: Result will be success on response or an error public func ping(topic: String, completion: @escaping ((Result) -> Void)) { if pairingEngine.hasPairing(for: topic) { pairingEngine.ping(topic: topic) { result in @@ -249,45 +245,45 @@ public final class SignClient { } } - /// For the proposer and responder to emits an event on the peer for an existing session + /// For the wallet to emit an event to a dApp /// - /// When: a client wants to emit an event to its peer client (eg. chain changed or tx replaced) + /// When a client wants to emit an event to its peer client (eg. chain changed or tx replaced) /// /// Should Error: /// - When the session topic is not found /// - When the event params are invalid - /// - Parameters: /// - topic: Session topic - /// - params: Event Parameters - /// - completion: calls a handler upon completion + /// - event: session event + /// - chainId: CAIP-2 chain public func emit(topic: String, event: Session.Event, chainId: Blockchain) async throws { try await sessionEngine.emit(topic: topic, event: event.internalRepresentation(), chainId: chainId) } - /// For the proposer and responder to terminate a session + /// For a wallet and a dApp to terminate a session /// /// Should Error: /// - When the session topic is not found - /// - Parameters: /// - topic: Session topic that you want to delete - /// - reason: Reason of session deletion public func disconnect(topic: String) async throws { try await sessionEngine.delete(topic: topic) } + /// Query sessions /// - Returns: All sessions public func getSessions() -> [Session] { sessionEngine.getSessions() } - /// - Returns: All settled pairings that are active - public func getSettledPairings() -> [Pairing] { - pairingEngine.getSettledPairings() + /// Query pairings + /// - Returns: All pairings + public func getPairings() -> [Pairing] { + pairingEngine.getPairings() } - /// - Returns: Pending requests received with wc_sessionRequest + /// Query pending requests + /// - Returns: Pending requests received from peer with `wc_sessionRequest` protocol method /// - Parameter topic: topic representing session for which you want to get pending requests. If nil, you will receive pending requests for all active sessions. public func getPendingRequests(topic: String? = nil) -> [Request] { let pendingRequests: [Request] = history.getPending() @@ -312,6 +308,16 @@ public final class SignClient { return WalletConnectUtils.JsonRpcRecord(id: record.id, topic: record.topic, request: request, response: record.response, chainId: record.chainId) } + +#if DEBUG + /// Delete all stored data such as: pairings, sessions, keys + /// + /// - Note: Doesn't unsubscribe from topics + public func cleanup() throws { + try cleanupService.cleanup() + } +#endif + // MARK: - Private private func setUpEnginesCallbacks() { @@ -355,13 +361,4 @@ public final class SignClient { self?.socketConnectionStatusPublisherSubject.send(status) }.store(in: &publishers) } - -#if DEBUG - /// Delete all stored data such as: pairings, sessions, keys - /// - /// - Note: Doesn't unsubscribe from topics - public func cleanup() throws { - try cleanupService.cleanup() - } -#endif } From 87204814efce9f3925c0c114b920fd4d09c7234f Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 30 Aug 2022 12:18:47 +0200 Subject: [PATCH 13/92] update api docs --- Sources/Auth/Auth.swift | 16 ++++++++++++++++ Sources/Auth/Types/Cacao/Cacao.swift | 3 +++ Sources/WalletConnectSign/Sign/Sign.swift | 15 +++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/Sources/Auth/Auth.swift b/Sources/Auth/Auth.swift index ab10c1f39..237694ad1 100644 --- a/Sources/Auth/Auth.swift +++ b/Sources/Auth/Auth.swift @@ -2,8 +2,20 @@ import Foundation import WalletConnectRelay import Combine +/// Auth instatnce wrapper +/// +/// ```swift +/// let metadata = AppMetadata( +/// name: "Swift wallet", +/// description: "wallet", +/// url: "wallet.connect", +/// icons: ["https://my_icon.com/1"]) +/// Auth.configure(metadata: metadata, account: account) +/// try await Auth.instance.pair(uri: uri) +/// ``` public class Auth { + /// Auth client instance public static var instance: AuthClient = { guard let config = Auth.config else { fatalError("Error - you must call Auth.configure(_:) before accessing the shared instance.") @@ -18,6 +30,10 @@ public class Auth { private init() { } + /// Auth instance config method + /// - Parameters: + /// - metadata: App metadata + /// - account: account that wallet will be authenticating with. Should be nil for non wallet clients. static public func configure(metadata: AppMetadata, account: Account?) { Auth.config = Auth.Config( metadata: metadata, diff --git a/Sources/Auth/Types/Cacao/Cacao.swift b/Sources/Auth/Types/Cacao/Cacao.swift index 80e7ce8b0..003316e42 100644 --- a/Sources/Auth/Types/Cacao/Cacao.swift +++ b/Sources/Auth/Types/Cacao/Cacao.swift @@ -1,5 +1,8 @@ import Foundation +/// CAIP-74 Cacao object +/// +/// specs at: https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-74.md public struct Cacao: Codable, Equatable { let header: CacaoHeader let payload: CacaoPayload diff --git a/Sources/WalletConnectSign/Sign/Sign.swift b/Sources/WalletConnectSign/Sign/Sign.swift index 75a384af7..42685d660 100644 --- a/Sources/WalletConnectSign/Sign/Sign.swift +++ b/Sources/WalletConnectSign/Sign/Sign.swift @@ -6,8 +6,20 @@ import Combine public typealias Account = WalletConnectUtils.Account public typealias Blockchain = WalletConnectUtils.Blockchain +/// Sign instatnce wrapper +/// +/// ```swift +/// let metadata = AppMetadata( +/// name: "Swift wallet", +/// description: "wallet", +/// url: "wallet.connect", +/// icons: ["https://my_icon.com/1"]) +/// Sign.configure(metadata: metadata) +/// try await Sign.instance.pair(uri: uri) +/// ``` public class Sign { + /// Sign client instance public static var instance: SignClient = { guard let metadata = Sign.metadata else { fatalError("Error - you must call Sign.configure(_:) before accessing the shared instance.") @@ -22,6 +34,9 @@ public class Sign { private init() { } + /// Sign instance config method + /// - Parameters: + /// - metadata: App metadata static public func configure(metadata: AppMetadata) { Sign.metadata = metadata } From f808348949169500c81b45fc00e622ca94b440e8 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 30 Aug 2022 12:25:55 +0200 Subject: [PATCH 14/92] update relay api docs --- Sources/Auth/Auth.swift | 2 +- Sources/WalletConnectRelay/Relay.swift | 12 ++++++++++++ Sources/WalletConnectRelay/RelayClient.swift | 11 +++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Sources/Auth/Auth.swift b/Sources/Auth/Auth.swift index 237694ad1..002671891 100644 --- a/Sources/Auth/Auth.swift +++ b/Sources/Auth/Auth.swift @@ -4,7 +4,7 @@ import Combine /// Auth instatnce wrapper /// -/// ```swift +/// ```Swift /// let metadata = AppMetadata( /// name: "Swift wallet", /// description: "wallet", diff --git a/Sources/WalletConnectRelay/Relay.swift b/Sources/WalletConnectRelay/Relay.swift index f6f823cf5..9e118558e 100644 --- a/Sources/WalletConnectRelay/Relay.swift +++ b/Sources/WalletConnectRelay/Relay.swift @@ -1,7 +1,13 @@ import Foundation +/// Relay instatnce wrapper +/// +/// ```swift +/// Relay.configure(projectId: projectId,socketFactory: SocketFactory()) +/// ``` public class Relay { + /// Relay client instance public static var instance: RelayClient = { guard let config = Relay.config else { fatalError("Error - you must call Relay.configure(_:) before accessing the shared instance.") @@ -18,6 +24,12 @@ public class Relay { private init() { } + /// Relay instance config method + /// - Parameters: + /// - relayHost: relay host + /// - projectId: project id + /// - socketFactory: web socket factory + /// - socketConnectionType: socket connection type static public func configure( relayHost: String = "relay.walletconnect.com", projectId: String, diff --git a/Sources/WalletConnectRelay/RelayClient.swift b/Sources/WalletConnectRelay/RelayClient.swift index 0c18c01d7..b6aef67c0 100644 --- a/Sources/WalletConnectRelay/RelayClient.swift +++ b/Sources/WalletConnectRelay/RelayClient.swift @@ -9,6 +9,11 @@ public enum SocketConnectionStatus { case disconnected } +/// WalletConnect Relay Client +/// +/// Should not be instantiated outside of the SDK +/// +/// Access via `Relay.instance` public final class RelayClient { enum Errors: Error { @@ -106,10 +111,16 @@ public final class RelayClient { self.init(dispatcher: dispatcher, logger: logger, keyValueStorage: keyValueStorage) } + /// Connects web socket + /// + /// Use this method for manual socket connection only public func connect() throws { try dispatcher.connect() } + /// Disconnects web socket + /// + /// Use this method for manual socket connection only public func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws { try dispatcher.disconnect(closeCode: closeCode) } From 92bed29bd3fda220c1f1c74c246b3530bdc96732 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 30 Aug 2022 19:16:02 +0300 Subject: [PATCH 15/92] Serializer refactor + logging improvements --- Sources/Chat/NetworkingInteractor.swift | 2 +- .../Serialiser/Serializer.swift | 48 ++++++++----------- .../Serialiser/Serializing.swift | 6 ++- Sources/WalletConnectRelay/Dispatching.swift | 4 +- .../Engine/Common/SessionEngine.swift | 2 +- .../ControllerSessionStateMachine.swift | 2 +- 6 files changed, 29 insertions(+), 35 deletions(-) diff --git a/Sources/Chat/NetworkingInteractor.swift b/Sources/Chat/NetworkingInteractor.swift index c8bf6176c..32bbcfd49 100644 --- a/Sources/Chat/NetworkingInteractor.swift +++ b/Sources/Chat/NetworkingInteractor.swift @@ -86,7 +86,7 @@ class NetworkingInteractor: NetworkInteracting { } else if let deserializedJsonRpcError: JSONRPCErrorResponse = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { handleJsonRpcErrorResponse(response: deserializedJsonRpcError) } else { - print("Warning: Networking Interactor - Received unknown object type from networking relay") + logger.error("Warning: Networking Interactor - Received unknown object type from networking relay") } } diff --git a/Sources/WalletConnectKMS/Serialiser/Serializer.swift b/Sources/WalletConnectKMS/Serialiser/Serializer.swift index 87c87198c..d58050137 100644 --- a/Sources/WalletConnectKMS/Serialiser/Serializer.swift +++ b/Sources/WalletConnectKMS/Serialiser/Serializer.swift @@ -2,8 +2,10 @@ import Foundation import WalletConnectUtils public class Serializer: Serializing { + enum Errors: String, Error { case symmetricKeyForTopicNotFound + case publicKeyForTopicNotFound } private let kms: KeyManagementServiceProtocol @@ -39,24 +41,17 @@ public class Serializer: Serializing { /// - topic: Topic that is associated with a symetric key for decrypting particular codable object /// - encodedEnvelope: Envelope to deserialize and decrypt /// - Returns: Deserialized object - public func tryDeserialize(topic: String, encodedEnvelope: String) -> T? { - do { - let envelope = try Envelope(encodedEnvelope) - switch envelope.type { - case .type0: - let decodedType: T? = try handleType0Envelope(topic, envelope) - return decodedType - case .type1: - let decodedType: T? = try handleType1Envelope(topic, envelope) - return decodedType - } - } catch { - print(error) - return nil + public func deserialize(topic: String, encodedEnvelope: String) throws -> T { + let envelope = try Envelope(encodedEnvelope) + switch envelope.type { + case .type0: + return try handleType0Envelope(topic, envelope) + case .type1(let peerPubKey): + return try handleType1Envelope(topic, peerPubKey: peerPubKey, sealbox: envelope.sealbox) } } - private func handleType0Envelope(_ topic: String, _ envelope: Envelope) throws -> T? { + private func handleType0Envelope(_ topic: String, _ envelope: Envelope) throws -> T { if let symmetricKey = kms.getSymmetricKeyRepresentable(for: topic) { return try decode(sealbox: envelope.sealbox, symmetricKey: symmetricKey) } else { @@ -64,20 +59,15 @@ public class Serializer: Serializing { } } - private func handleType1Envelope(_ topic: String, _ envelope: Envelope) throws -> T? { - guard let selfPubKey = kms.getPublicKey(for: topic), - case let .type1(peerPubKey) = envelope.type else { return nil } - do { - // self pub key is good - let agreementKeys = try kms.performKeyAgreement(selfPublicKey: selfPubKey, peerPublicKey: peerPubKey.toHexString()) - let decodedType: T? = try decode(sealbox: envelope.sealbox, symmetricKey: agreementKeys.sharedKey.rawRepresentation) - let newTopic = agreementKeys.derivedTopic() - try kms.setAgreementSecret(agreementKeys, topic: newTopic) - return decodedType - } catch { - print(error) - } - return nil + private func handleType1Envelope(_ topic: String, peerPubKey: Data, sealbox: Data) throws -> T { + guard let selfPubKey = kms.getPublicKey(for: topic) + else { throw Errors.publicKeyForTopicNotFound } + + let agreementKeys = try kms.performKeyAgreement(selfPublicKey: selfPubKey, peerPublicKey: peerPubKey.toHexString()) + let decodedType: T = try decode(sealbox: sealbox, symmetricKey: agreementKeys.sharedKey.rawRepresentation) + let newTopic = agreementKeys.derivedTopic() + try kms.setAgreementSecret(agreementKeys, topic: newTopic) + return decodedType } private func decode(sealbox: Data, symmetricKey: Data) throws -> T { diff --git a/Sources/WalletConnectKMS/Serialiser/Serializing.swift b/Sources/WalletConnectKMS/Serialiser/Serializing.swift index b33e51a77..353e846b3 100644 --- a/Sources/WalletConnectKMS/Serialiser/Serializing.swift +++ b/Sources/WalletConnectKMS/Serialiser/Serializing.swift @@ -2,10 +2,14 @@ import Foundation public protocol Serializing { func serialize(topic: String, encodable: Encodable, envelopeType: Envelope.EnvelopeType) throws -> String - func tryDeserialize(topic: String, encodedEnvelope: String) -> T? + func deserialize(topic: String, encodedEnvelope: String) throws -> T } public extension Serializing { + func tryDeserialize(topic: String, encodedEnvelope: String) -> T? { + return try? deserialize(topic: topic, encodedEnvelope: encodedEnvelope) + } + func serialize(topic: String, encodable: Encodable, envelopeType: Envelope.EnvelopeType = .type0) throws -> String { try serialize(topic: topic, encodable: encodable, envelopeType: envelopeType) } diff --git a/Sources/WalletConnectRelay/Dispatching.swift b/Sources/WalletConnectRelay/Dispatching.swift index e053051ee..c7ab47f30 100644 --- a/Sources/WalletConnectRelay/Dispatching.swift +++ b/Sources/WalletConnectRelay/Dispatching.swift @@ -82,9 +82,9 @@ final class Dispatcher: NSObject, Dispatching { private func dequeuePendingTextFrames() { while let frame = textFramesQueue.dequeue() { - send(frame) { error in + send(frame) { [weak self] error in if let error = error { - print(error) + self?.logger.error(error.localizedDescription) } } } diff --git a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift index c535d7ce3..80f255b0a 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -70,7 +70,7 @@ final class SessionEngine { } func request(_ request: Request) async throws { - print("will request on session topic: \(request.topic)") + logger.debug("will request on session topic: \(request.topic)") guard let session = sessionStore.getSession(forTopic: request.topic), session.acknowledged else { logger.debug("Could not find session for topic \(request.topic)") return // TODO: Marked to review on developer facing error cases diff --git a/Sources/WalletConnectSign/Engine/Controller/ControllerSessionStateMachine.swift b/Sources/WalletConnectSign/Engine/Controller/ControllerSessionStateMachine.swift index b6c25568a..146f750af 100644 --- a/Sources/WalletConnectSign/Engine/Controller/ControllerSessionStateMachine.swift +++ b/Sources/WalletConnectSign/Engine/Controller/ControllerSessionStateMachine.swift @@ -28,7 +28,7 @@ final class ControllerSessionStateMachine { } func update(topic: String, namespaces: [String: SessionNamespace]) async throws { - var session = try getSession(for: topic) + let session = try getSession(for: topic) try validateControlledAcknowledged(session) try Namespace.validate(namespaces) logger.debug("Controller will update methods") From 412d80679123052aaf1f4135cf2902a886310b0d Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 30 Aug 2022 22:15:29 +0300 Subject: [PATCH 16/92] SerializerMock fixed --- Tests/WalletConnectSignTests/Mocks/SerializerMock.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/WalletConnectSignTests/Mocks/SerializerMock.swift b/Tests/WalletConnectSignTests/Mocks/SerializerMock.swift index a5f4ba7cb..339da3ff8 100644 --- a/Tests/WalletConnectSignTests/Mocks/SerializerMock.swift +++ b/Tests/WalletConnectSignTests/Mocks/SerializerMock.swift @@ -12,8 +12,8 @@ class SerializerMock: Serializing { func serialize(topic: String, encodable: Encodable, envelopeType: Envelope.EnvelopeType) throws -> String { try serialize(json: try encodable.json(), agreementKeys: AgreementKeys.stub()) } - func tryDeserialize(topic: String, encodedEnvelope: String) -> T? { - try? deserialize(message: encodedEnvelope, symmetricKey: Data()) + func deserialize(topic: String, encodedEnvelope: String) throws -> T { + return try deserialize(message: encodedEnvelope, symmetricKey: Data()) } func deserializeJsonRpc(topic: String, message: String) throws -> Result, JSONRPCErrorResponse> { .success(try deserialize(message: message, symmetricKey: Data())) From c94e926b809b0f8ef93dac23000b415736cdb728 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Wed, 31 Aug 2022 00:36:14 +0300 Subject: [PATCH 17/92] String replaced with WalletConnectURI --- Example/DApp/Auth/AuthViewModel.swift | 2 +- .../DApp/Sign/Connect/ConnectViewController.swift | 12 ++++++------ .../Sign/SelectChain/SelectChainViewController.swift | 6 +++--- Example/ExampleApp/SceneDelegate.swift | 2 +- Example/ExampleApp/Wallet/WalletViewController.swift | 8 +++++--- Sources/Auth/AuthClient.swift | 11 ++++------- Sources/Auth/Types/Aliases/WalletConnectURI.swift | 2 +- Sources/WalletConnectSign/Sign/SignClient.swift | 11 ++++------- .../WalletConnectSign/Types/WalletConnectURI.swift | 2 +- Sources/WalletConnectUtils/WalletConnectURI.swift | 2 +- 10 files changed, 27 insertions(+), 31 deletions(-) diff --git a/Example/DApp/Auth/AuthViewModel.swift b/Example/DApp/Auth/AuthViewModel.swift index 037e5131a..b67611948 100644 --- a/Example/DApp/Auth/AuthViewModel.swift +++ b/Example/DApp/Auth/AuthViewModel.swift @@ -27,7 +27,7 @@ final class AuthViewModel: ObservableObject { func setupInitialState() async throws { state = .none uri = nil - uri = try await Auth.instance.request(.stub()) + uri = try await Auth.instance.request(.stub()).absoluteString } func copyDidPressed() { diff --git a/Example/DApp/Sign/Connect/ConnectViewController.swift b/Example/DApp/Sign/Connect/ConnectViewController.swift index 8a9c6aec9..adebac5c3 100644 --- a/Example/DApp/Sign/Connect/ConnectViewController.swift +++ b/Example/DApp/Sign/Connect/ConnectViewController.swift @@ -3,12 +3,12 @@ import UIKit import WalletConnectSign class ConnectViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { - let uriString: String + let uri: WalletConnectURI let activePairings: [Pairing] = Sign.instance.getPairings() let segmentedControl = UISegmentedControl(items: ["Pairings", "New Pairing"]) - init(uri: String) { - self.uriString = uri + init(uri: WalletConnectURI) { + self.uri = uri super.init(nibName: nil, bundle: nil) } @@ -27,7 +27,7 @@ class ConnectViewController: UIViewController, UITableViewDataSource, UITableVie override func viewDidLoad() { super.viewDidLoad() DispatchQueue.global().async { [unowned self] in - let qrImage = QRCodeGenerator.generateQRCode(from: uriString) + let qrImage = QRCodeGenerator.generateQRCode(from: uri.absoluteString) DispatchQueue.main.async { [self] in self.connectView.qrCodeView.image = qrImage self.connectView.copyButton.isHidden = false @@ -56,11 +56,11 @@ class ConnectViewController: UIViewController, UITableViewDataSource, UITableVie } @objc func copyURI() { - UIPasteboard.general.string = uriString + UIPasteboard.general.string = uri.absoluteString } @objc func connectWithExampleWallet() { - let url = URL(string: "https://walletconnect.com/wc?uri=\(uriString)")! + let url = URL(string: "https://walletconnect.com/wc?uri=\(uri.absoluteString)")! DispatchQueue.main.async { UIApplication.shared.open(url, options: [:]) { [weak self] _ in self?.dismiss(animated: true, completion: nil) diff --git a/Example/DApp/Sign/SelectChain/SelectChainViewController.swift b/Example/DApp/Sign/SelectChain/SelectChainViewController.swift index 4ca4b6c65..74210f740 100644 --- a/Example/DApp/Sign/SelectChain/SelectChainViewController.swift +++ b/Example/DApp/Sign/SelectChain/SelectChainViewController.swift @@ -39,7 +39,7 @@ class SelectChainViewController: UIViewController, UITableViewDataSource { let namespaces: [String: ProposalNamespace] = ["eip155": ProposalNamespace(chains: blockchains, methods: methods, events: [], extensions: nil)] Task { let uri = try await Sign.instance.connect(requiredNamespaces: namespaces) - showConnectScreen(uriString: uri!) + showConnectScreen(uri: uri!) } } @@ -48,9 +48,9 @@ class SelectChainViewController: UIViewController, UITableViewDataSource { UIApplication.shared.open(URL(string: "walletconnectwallet://")!) } - private func showConnectScreen(uriString: String) { + private func showConnectScreen(uri: WalletConnectURI) { DispatchQueue.main.async { [unowned self] in - let vc = UINavigationController(rootViewController: ConnectViewController(uri: uriString)) + let vc = UINavigationController(rootViewController: ConnectViewController(uri: uri)) present(vc, animated: true, completion: nil) } } diff --git a/Example/ExampleApp/SceneDelegate.swift b/Example/ExampleApp/SceneDelegate.swift index 660e6f195..83b087ae3 100644 --- a/Example/ExampleApp/SceneDelegate.swift +++ b/Example/ExampleApp/SceneDelegate.swift @@ -46,7 +46,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { vc.onClientConnected = { Task { do { - try await Sign.instance.pair(uri: wcUri) + try await Sign.instance.pair(uri: WalletConnectURI(string: wcUri)!) } catch { print(error) } diff --git a/Example/ExampleApp/Wallet/WalletViewController.swift b/Example/ExampleApp/Wallet/WalletViewController.swift index 5553c823e..9d574ea34 100644 --- a/Example/ExampleApp/Wallet/WalletViewController.swift +++ b/Example/ExampleApp/Wallet/WalletViewController.swift @@ -45,7 +45,8 @@ final class WalletViewController: UIViewController { @objc private func showTextInput() { let alert = UIAlertController.createInputAlert { [weak self] inputText in - self?.pairClient(uri: inputText) + guard let self = self, let uri = WalletConnectURI(string: inputText) else { return } + self.pairClient(uri: uri) } present(alert, animated: true) } @@ -119,7 +120,7 @@ final class WalletViewController: UIViewController { } @MainActor - private func pairClient(uri: String) { + private func pairClient(uri: WalletConnectURI) { print("[WALLET] Pairing to: \(uri)") Task { do { @@ -201,7 +202,8 @@ extension WalletViewController: UITableViewDataSource, UITableViewDelegate { extension WalletViewController: ScannerViewControllerDelegate { func didScan(_ code: String) { - pairClient(uri: code) + guard let uri = WalletConnectURI(string: code) else { return } + pairClient(uri: uri) } } diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index f4da23fa5..e86c97a35 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -93,22 +93,19 @@ public class AuthClient { /// Throws Error: /// - When URI is invalid format or missing params /// - When topic is already in use - public func pair(uri: String) async throws { - guard let pairingURI = WalletConnectURI(string: uri) else { - throw Errors.malformedPairingURI - } - try await walletPairService.pair(pairingURI) + public func pair(uri: WalletConnectURI) async throws { + try await walletPairService.pair(uri) } /// For a dapp to send an authentication request to a wallet /// - Parameter params: Set of parameters required to request authentication /// /// - Returns: Pairing URI that should be shared with wallet out of bound. Common way is to present it as a QR code. - public func request(_ params: RequestParams) async throws -> String { + public func request(_ params: RequestParams) async throws -> WalletConnectURI { logger.debug("Requesting Authentication") let uri = try await appPairService.create() try await appRequestService.request(params: params, topic: uri.topic) - return uri.absoluteString + return uri } /// For a dapp to send an authentication request to a wallet diff --git a/Sources/Auth/Types/Aliases/WalletConnectURI.swift b/Sources/Auth/Types/Aliases/WalletConnectURI.swift index a2cfe2de9..f1ff170b5 100644 --- a/Sources/Auth/Types/Aliases/WalletConnectURI.swift +++ b/Sources/Auth/Types/Aliases/WalletConnectURI.swift @@ -1,4 +1,4 @@ import Foundation import WalletConnectUtils -typealias WalletConnectURI = WalletConnectUtils.WalletConnectURI +public typealias WalletConnectURI = WalletConnectUtils.WalletConnectURI diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index d00e54242..f2a32426a 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -145,7 +145,7 @@ public final class SignClient { /// - requiredNamespaces: required namespaces for a session /// - topic: Optional parameter - use it if you already have an established pairing with peer client. /// - Returns: Pairing URI that should be shared with responder out of bound. Common way is to present it as a QR code. Pairing URI will be nil if you are going to establish a session on existing Pairing and `topic` function parameter was provided. - public func connect(requiredNamespaces: [String: ProposalNamespace], topic: String? = nil) async throws -> String? { + public func connect(requiredNamespaces: [String: ProposalNamespace], topic: String? = nil) async throws -> WalletConnectURI? { logger.debug("Connecting Application") if let topic = topic { guard let pairing = pairingEngine.getSettledPairing(for: topic) else { @@ -157,7 +157,7 @@ public final class SignClient { } else { let pairingURI = try await pairingEngine.create() try await pairingEngine.propose(pairingTopic: pairingURI.topic, namespaces: requiredNamespaces, relay: pairingURI.relay) - return pairingURI.absoluteString + return pairingURI } } @@ -168,11 +168,8 @@ public final class SignClient { /// Should Error: /// - When URI has invalid format or missing params /// - When topic is already in use - public func pair(uri: String) async throws { - guard let pairingURI = WalletConnectURI(string: uri) else { - throw WalletConnectError.malformedPairingURI - } - try await pairEngine.pair(pairingURI) + public func pair(uri: WalletConnectURI) async throws { + try await pairEngine.pair(uri) } /// For a wallet to approve a session proposal. diff --git a/Sources/WalletConnectSign/Types/WalletConnectURI.swift b/Sources/WalletConnectSign/Types/WalletConnectURI.swift index a2cfe2de9..f1ff170b5 100644 --- a/Sources/WalletConnectSign/Types/WalletConnectURI.swift +++ b/Sources/WalletConnectSign/Types/WalletConnectURI.swift @@ -1,4 +1,4 @@ import Foundation import WalletConnectUtils -typealias WalletConnectURI = WalletConnectUtils.WalletConnectURI +public typealias WalletConnectURI = WalletConnectUtils.WalletConnectURI diff --git a/Sources/WalletConnectUtils/WalletConnectURI.swift b/Sources/WalletConnectUtils/WalletConnectURI.swift index 18a0a6a66..cd4fa03f7 100644 --- a/Sources/WalletConnectUtils/WalletConnectURI.swift +++ b/Sources/WalletConnectUtils/WalletConnectURI.swift @@ -14,7 +14,7 @@ public struct WalletConnectURI: Equatable { public let relay: RelayProtocolOptions public var api: TargetAPI { - apiType ?? .sign + return apiType ?? .sign } public var absoluteString: String { From 927c3883d2360b1e99267e09bcefc577a01eef4a Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Wed, 31 Aug 2022 00:36:39 +0300 Subject: [PATCH 18/92] Lint --- Example/DApp/Auth/AuthView.swift | 1 - Example/DApp/Auth/AuthViewModel.swift | 4 +- Example/DApp/SceneDelegate.swift | 2 +- Example/IntegrationTests/Auth/AuthTests.swift | 6 +- .../ApplicationLayer/SceneDelegate.swift | 2 +- .../Wallet/Scan/Views/ScanQR.swift | 16 +++--- .../Wallet/Scan/Views/ScanQRView.swift | 56 +++++++++---------- Sources/Auth/Auth.swift | 2 +- Sources/Auth/AuthClient.swift | 2 - .../Common/NetworkingInteractor.swift | 2 +- Sources/Auth/Types/Errors/Reason.swift | 1 - .../WalletConnectSign/Sign/SignClient.swift | 1 - Tests/AuthTests/Mocks/AppMetadata.swift | 1 - .../Mocks/NetworkingInteractorMock.swift | 2 +- .../Mocks/BackgroundTaskRegistrarMock.swift | 4 +- .../Mocks/AppMetadata.swift | 1 - Tests/WalletConnectSignTests/Stub/Stubs.swift | 1 - .../WCPairingTests.swift | 10 ++-- 18 files changed, 53 insertions(+), 61 deletions(-) diff --git a/Example/DApp/Auth/AuthView.swift b/Example/DApp/Auth/AuthView.swift index f120f1871..5acdaf08c 100644 --- a/Example/DApp/Auth/AuthView.swift +++ b/Example/DApp/Auth/AuthView.swift @@ -98,4 +98,3 @@ struct CircleButtonStyle: ButtonStyle { .cornerRadius(8.0) } } - diff --git a/Example/DApp/Auth/AuthViewModel.swift b/Example/DApp/Auth/AuthViewModel.swift index b67611948..7031c0eef 100644 --- a/Example/DApp/Auth/AuthViewModel.swift +++ b/Example/DApp/Auth/AuthViewModel.swift @@ -35,7 +35,7 @@ final class AuthViewModel: ObservableObject { } func walletDidPressed() { - + } func deeplinkPressed() { @@ -47,7 +47,7 @@ final class AuthViewModel: ObservableObject { private extension AuthViewModel { func setupSubscriptions() { - Auth.instance.authResponsePublisher.sink { [weak self] (id, result) in + Auth.instance.authResponsePublisher.sink { [weak self] (_, result) in switch result { case .success(let cacao): self?.state = .signed(cacao) diff --git a/Example/DApp/SceneDelegate.swift b/Example/DApp/SceneDelegate.swift index 163421bcc..eeaaf2369 100644 --- a/Example/DApp/SceneDelegate.swift +++ b/Example/DApp/SceneDelegate.swift @@ -18,7 +18,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { private let authCoordinator = AuthCoordinator() func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - Relay.configure(projectId: "8ba9ee138960775e5231b70cc5ef1c3a",socketFactory: SocketFactory()) + Relay.configure(projectId: "8ba9ee138960775e5231b70cc5ef1c3a", socketFactory: SocketFactory()) setupWindow(scene: scene) } diff --git a/Example/IntegrationTests/Auth/AuthTests.swift b/Example/IntegrationTests/Auth/AuthTests.swift index 1c2deff7d..43d56d372 100644 --- a/Example/IntegrationTests/Auth/AuthTests.swift +++ b/Example/IntegrationTests/Auth/AuthTests.swift @@ -72,7 +72,7 @@ final class AuthTests: XCTestCase { } } .store(in: &publishers) - app.authResponsePublisher.sink { (id, result) in + app.authResponsePublisher.sink { (_, result) in guard case .success = result else { XCTFail(); return } responseExpectation.fulfill() } @@ -90,7 +90,7 @@ final class AuthTests: XCTestCase { } } .store(in: &publishers) - app.authResponsePublisher.sink { (id, result) in + app.authResponsePublisher.sink { (_, result) in guard case .failure(let error) = result else { XCTFail(); return } XCTAssertEqual(error, .userRejeted) responseExpectation.fulfill() @@ -111,7 +111,7 @@ final class AuthTests: XCTestCase { } } .store(in: &publishers) - app.authResponsePublisher.sink { (id, result) in + app.authResponsePublisher.sink { (_, result) in guard case .failure(let error) = result else { XCTFail(); return } XCTAssertEqual(error, .signatureVerificationFailed) responseExpectation.fulfill() diff --git a/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift b/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift index 8660ed4c5..43335bd7d 100644 --- a/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift +++ b/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift @@ -12,7 +12,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { MigrationConfigurator(app: app), ThirdPartyConfigurator(), ApplicationConfigurator(app: app), - AppearanceConfigurator(), + AppearanceConfigurator() ] } diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQR.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQR.swift index 0bb3369d6..8b78b3098 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQR.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQR.swift @@ -1,28 +1,28 @@ import SwiftUI struct ScanQR: UIViewRepresentable { - + class Coordinator: ScanQRViewDelegate { private let onValue: (String) -> Void private let onError: (Error) -> Void - + init(onValue: @escaping (String) -> Void, onError: @escaping (Error) -> Void) { self.onValue = onValue self.onError = onError } - + func scanDidDetect(value: String) { onValue(value) } - + func scanDidFail(with error: Error) { onError(error) } } - + let onValue: (String) -> Void let onError: (Error) -> Void - + func makeUIView(context: Context) -> ScanQRView { let view = ScanQRView() view.delegate = context.coordinator @@ -30,9 +30,9 @@ struct ScanQR: UIViewRepresentable { } func updateUIView(_ uiView: ScanQRView, context: Context) { - + } - + func makeCoordinator() -> Coordinator { return Coordinator(onValue: onValue, onError: onError) } diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQRView.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQRView.swift index c3c8327b4..2d9db1056 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQRView.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQRView.swift @@ -10,17 +10,17 @@ final class ScanQRView: UIView { enum Errors: Error { case deviceNotFound } - + weak var delegate: ScanQRViewDelegate? - + private let targetSize = CGSize( width: UIScreen.main.bounds.width - 32.0, height: UIScreen.main.bounds.width - 32.0 ) - + private var videoPreviewLayer: AVCaptureVideoPreviewLayer? private var captureSession: AVCaptureSession? - + private lazy var borderView: UIView = { let borderView = ScanTargetView(radius: 24.0, color: .white, strokeWidth: 2.0, length: 36.0) borderView.alpha = 0.85 @@ -34,21 +34,21 @@ final class ScanQRView: UIView { bluredView.layer.mask = createMaskLayer() return bluredView }() - + override init(frame: CGRect) { super.init(frame: frame) - + setupView() startCaptureSession() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func layoutSubviews() { super.layoutSubviews() - + updateFrames() updateOrientation() } @@ -61,7 +61,7 @@ final class ScanQRView: UIView { // MARK: AVCaptureMetadataOutputObjectsDelegate extension ScanQRView: AVCaptureMetadataOutputObjectsDelegate { - + func metadataOutput(_ metadataOutput: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { guard let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject, @@ -76,14 +76,14 @@ extension ScanQRView: AVCaptureMetadataOutputObjectsDelegate { // MARK: Privates private extension ScanQRView { - + private func setupView() { backgroundColor = .black addSubview(bluredView) addSubview(borderView) } - + private func createMaskLayer() -> CAShapeLayer { let maskPath = UIBezierPath(rect: bounds) let rect = UIBezierPath( @@ -98,62 +98,62 @@ private extension ScanQRView { ) maskPath.append(rect) maskPath.usesEvenOddFillRule = true - + let maskLayer = CAShapeLayer() maskLayer.path = maskPath.cgPath maskLayer.fillRule = .evenOdd return maskLayer } - + private func startCaptureSession() { DispatchQueue.global().async { [weak self] in guard let self = self else { return } - + do { let session = try self.createCaptureSession() session.startRunning() self.captureSession = session - + DispatchQueue.main.async { self.setupVideoPreviewLayer(with: session) } } catch { DispatchQueue.main.async { self.delegate?.scanDidFail(with: error) } } } } - + private func createCaptureSession() throws -> AVCaptureSession { guard let captureDevice = AVCaptureDevice.default(for: .video) else { throw Errors.deviceNotFound } - + let input = try AVCaptureDeviceInput(device: captureDevice) - + let session = AVCaptureSession() session.addInput(input) - + let captureMetadataOutput = AVCaptureMetadataOutput() captureMetadataOutput.setMetadataObjectsDelegate(self, queue: .main) session.addOutput(captureMetadataOutput) - + captureMetadataOutput.metadataObjectTypes = [.qr] - + return session } - + private func stopCaptureSession() { captureSession?.stopRunning() captureSession = nil } - + private func setupVideoPreviewLayer(with session: AVCaptureSession) { let previewLayer = AVCaptureVideoPreviewLayer(session: session) previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill previewLayer.frame = layer.bounds videoPreviewLayer = previewLayer - + layer.insertSublayer(previewLayer, at: 0) } - + private func updateFrames() { borderView.frame.size = targetSize borderView.center = center @@ -161,13 +161,13 @@ private extension ScanQRView { bluredView.layer.mask = createMaskLayer() videoPreviewLayer?.frame = layer.bounds } - + private func updateOrientation() { guard let connection = videoPreviewLayer?.connection else { return } let previewLayerConnection: AVCaptureConnection = connection - + guard previewLayerConnection.isVideoOrientationSupported else { return } diff --git a/Sources/Auth/Auth.swift b/Sources/Auth/Auth.swift index 002671891..b209104bd 100644 --- a/Sources/Auth/Auth.swift +++ b/Sources/Auth/Auth.swift @@ -25,7 +25,7 @@ public class Auth { account: config.account, relayClient: Relay.instance) }() - + private static var config: Config? private init() { } diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index e86c97a35..9e33a3167 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -4,7 +4,6 @@ import WalletConnectUtils import WalletConnectPairing import WalletConnectRelay - /// WalletConnect Auth Client /// /// Cannot be instantiated outside of the SDK @@ -41,7 +40,6 @@ public class AuthClient { /// An object that loggs SDK's errors and info messages public let logger: ConsoleLogging - // MARK: - Private Properties private var authResponsePublisherSubject = PassthroughSubject<(id: RPCID, result: Result), Never>() diff --git a/Sources/Auth/Services/Common/NetworkingInteractor.swift b/Sources/Auth/Services/Common/NetworkingInteractor.swift index 24419b295..e66e97a77 100644 --- a/Sources/Auth/Services/Common/NetworkingInteractor.swift +++ b/Sources/Auth/Services/Common/NetworkingInteractor.swift @@ -11,7 +11,7 @@ protocol NetworkInteracting { func subscribe(topic: String) async throws func unsubscribe(topic: String) func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws - func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws + func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws } diff --git a/Sources/Auth/Types/Errors/Reason.swift b/Sources/Auth/Types/Errors/Reason.swift index bbf491fdd..4822fe64c 100644 --- a/Sources/Auth/Types/Errors/Reason.swift +++ b/Sources/Auth/Types/Errors/Reason.swift @@ -4,4 +4,3 @@ protocol Reason { var code: Int { get } var message: String { get } } - diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index f2a32426a..d4ced6b64 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -305,7 +305,6 @@ public final class SignClient { return WalletConnectUtils.JsonRpcRecord(id: record.id, topic: record.topic, request: request, response: record.response, chainId: record.chainId) } - #if DEBUG /// Delete all stored data such as: pairings, sessions, keys /// diff --git a/Tests/AuthTests/Mocks/AppMetadata.swift b/Tests/AuthTests/Mocks/AppMetadata.swift index 2f8d6a558..75d48acb1 100644 --- a/Tests/AuthTests/Mocks/AppMetadata.swift +++ b/Tests/AuthTests/Mocks/AppMetadata.swift @@ -1,4 +1,3 @@ - import Foundation import WalletConnectPairing diff --git a/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift b/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift index a3364de40..1a9c03790 100644 --- a/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift +++ b/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift @@ -33,7 +33,7 @@ struct NetworkingInteractorMock: NetworkInteracting { } func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { - + } func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws { diff --git a/Tests/RelayerTests/Mocks/BackgroundTaskRegistrarMock.swift b/Tests/RelayerTests/Mocks/BackgroundTaskRegistrarMock.swift index d8f895242..52d49d774 100644 --- a/Tests/RelayerTests/Mocks/BackgroundTaskRegistrarMock.swift +++ b/Tests/RelayerTests/Mocks/BackgroundTaskRegistrarMock.swift @@ -3,12 +3,12 @@ import Foundation class BackgroundTaskRegistrarMock: BackgroundTaskRegistering { var completion: (() -> Void)? - + func register(name: String, completion: @escaping () -> Void) { self.completion = completion } func invalidate() { - + } } diff --git a/Tests/WalletConnectSignTests/Mocks/AppMetadata.swift b/Tests/WalletConnectSignTests/Mocks/AppMetadata.swift index 2f8d6a558..75d48acb1 100644 --- a/Tests/WalletConnectSignTests/Mocks/AppMetadata.swift +++ b/Tests/WalletConnectSignTests/Mocks/AppMetadata.swift @@ -1,4 +1,3 @@ - import Foundation import WalletConnectPairing diff --git a/Tests/WalletConnectSignTests/Stub/Stubs.swift b/Tests/WalletConnectSignTests/Stub/Stubs.swift index 7f80b8da9..a17ad98a8 100644 --- a/Tests/WalletConnectSignTests/Stub/Stubs.swift +++ b/Tests/WalletConnectSignTests/Stub/Stubs.swift @@ -5,7 +5,6 @@ import WalletConnectUtils import TestingUtils import WalletConnectPairing - extension Pairing { static func stub(expiryDate: Date = Date(timeIntervalSinceNow: 10000), topic: String = String.generateTopic()) -> Pairing { Pairing(topic: topic, peer: nil, expiryDate: expiryDate) diff --git a/Tests/WalletConnectSignTests/WCPairingTests.swift b/Tests/WalletConnectSignTests/WCPairingTests.swift index ca6872e24..6dcad44a0 100644 --- a/Tests/WalletConnectSignTests/WCPairingTests.swift +++ b/Tests/WalletConnectSignTests/WCPairingTests.swift @@ -41,7 +41,7 @@ final class WCPairingTests: XCTestCase { try? pairing.updateExpiry() XCTAssertEqual(pairing.expiryDate, activeExpiry) } - + func testUpdateExpiryForUri() { var pairing = WCPairing(uri: WalletConnectURI.stub()) let activeExpiry = referenceDate.advanced(by: WCPairing.timeToLiveActive) @@ -57,7 +57,7 @@ final class WCPairingTests: XCTestCase { XCTAssertTrue(pairing.active) XCTAssertEqual(pairing.expiryDate, activeExpiry) } - + func testActivateURI() { var pairing = WCPairing(uri: WalletConnectURI.stub()) let activeExpiry = referenceDate.advanced(by: WCPairing.timeToLiveActive) @@ -66,14 +66,14 @@ final class WCPairingTests: XCTestCase { XCTAssertTrue(pairing.active) XCTAssertEqual(pairing.expiryDate, activeExpiry) } - + func testUpdateExpiryWhenValueIsGreaterThanMax() { var pairing = WCPairing(topic: "", relay: .stub(), peerMetadata: .stub(), expiryDate: referenceDate) XCTAssertThrowsError(try pairing.updateExpiry(40 * .day)) { error in XCTAssertEqual(error as! WCPairing.Errors, WCPairing.Errors.invalidUpdateExpiryValue) } } - + func testUpdateExpiryWhenNewExpiryDateIsLessThanExpiryDate() { let expiryDate = referenceDate.advanced(by: 40 * .day) var pairing = WCPairing(topic: "", relay: .stub(), peerMetadata: .stub(), expiryDate: expiryDate) @@ -89,7 +89,7 @@ final class WCPairingTests: XCTestCase { XCTAssertTrue(pairing.active) XCTAssertEqual(referenceDate.advanced(by: 30 * .day), pairing.expiryDate) } - + func testActivateWhenUpdateExpiryIsInvalid() { let expiryDate = referenceDate.advanced(by: 40 * .day) var pairing = WCPairing(topic: "", relay: .stub(), peerMetadata: .stub(), expiryDate: expiryDate) From eed97f307df483603206f677a9fabc4f66005bef Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 31 Aug 2022 09:54:51 +0200 Subject: [PATCH 19/92] extend pairing on request response - savepoint --- Sources/Auth/AuthClientFactory.swift | 2 +- .../Services/App/AppRespondSubscriber.swift | 35 +++++++++++++++---- .../AuthTests/AppRespondSubscriberTests.swift | 5 ++- .../Mocks/WCPairingStorageMock.swift | 1 - 4 files changed, 34 insertions(+), 9 deletions(-) rename Tests/{WalletConnectSignTests => TestingUtils}/Mocks/WCPairingStorageMock.swift (95%) diff --git a/Sources/Auth/AuthClientFactory.swift b/Sources/Auth/AuthClientFactory.swift index 6be615528..3dc48fbea 100644 --- a/Sources/Auth/AuthClientFactory.swift +++ b/Sources/Auth/AuthClientFactory.swift @@ -24,7 +24,7 @@ public struct AuthClientFactory { let appPairService = AppPairService(networkingInteractor: networkingInteractor, kms: kms, pairingStorage: pairingStore) let appRequestService = AppRequestService(networkingInteractor: networkingInteractor, kms: kms, appMetadata: metadata, logger: logger) let messageSigner = MessageSigner(signer: Signer()) - let appRespondSubscriber = AppRespondSubscriber(networkingInteractor: networkingInteractor, logger: logger, rpcHistory: history, signatureVerifier: messageSigner, messageFormatter: messageFormatter) + let appRespondSubscriber = AppRespondSubscriber(networkingInteractor: networkingInteractor, logger: logger, rpcHistory: history, signatureVerifier: messageSigner, messageFormatter: messageFormatter, pairingStorage: pairingStore) let walletPairService = WalletPairService(networkingInteractor: networkingInteractor, kms: kms, pairingStorage: pairingStore) let walletRequestSubscriber = WalletRequestSubscriber(networkingInteractor: networkingInteractor, logger: logger, kms: kms, messageFormatter: messageFormatter, address: account?.address) let walletRespondService = WalletRespondService(networkingInteractor: networkingInteractor, logger: logger, kms: kms, rpcHistory: history) diff --git a/Sources/Auth/Services/App/AppRespondSubscriber.swift b/Sources/Auth/Services/App/AppRespondSubscriber.swift index 21708bd73..84a0d241f 100644 --- a/Sources/Auth/Services/App/AppRespondSubscriber.swift +++ b/Sources/Auth/Services/App/AppRespondSubscriber.swift @@ -2,9 +2,11 @@ import Combine import Foundation import WalletConnectUtils import JSONRPC +import WalletConnectPairing class AppRespondSubscriber { private let networkingInteractor: NetworkInteracting + private let pairingStorage: WCPairingStorage private let logger: ConsoleLogging private let rpcHistory: RPCHistory private let signatureVerifier: MessageSignatureVerifying @@ -17,12 +19,14 @@ class AppRespondSubscriber { logger: ConsoleLogging, rpcHistory: RPCHistory, signatureVerifier: MessageSignatureVerifying, - messageFormatter: SIWEMessageFormatting) { + messageFormatter: SIWEMessageFormatting, + pairingStorage: WCPairingStorage) { self.networkingInteractor = networkingInteractor self.logger = logger self.rpcHistory = rpcHistory self.signatureVerifier = signatureVerifier self.messageFormatter = messageFormatter + self.pairingStorage = pairingStorage subscribeForResponse() } @@ -35,13 +39,10 @@ class AppRespondSubscriber { let requestParams = request.params, request.method == "wc_authRequest" else { return } + activatePairingIfNeeded(id: requestId) networkingInteractor.unsubscribe(topic: subscriptionPayload.topic) - if let errorResponse = response.error, - let error = AuthError(code: errorResponse.code) { - onResponse?(requestId, .failure(error)) - return - } + handleIfError(response: response) guard let cacao = try? response.result?.get(Cacao.self), @@ -62,4 +63,26 @@ class AppRespondSubscriber { }.store(in: &publishers) } + + + private func activatePairingIfNeeded(id: RPCID) { + guard let record = rpcHistory.get(recordId: id) else { return } + let pairingTopic = record.topic + print(pairingTopic) + guard var pairing = pairingStorage.getPairing(forTopic: pairingTopic) else { return } + if !pairing.active { + pairing.activate() + } else { + try? pairing.updateExpiry() + } + } + + private func handleIfError(response: RPCResponse) { + if let errorResponse = response.error, + let id = response.id, + let error = AuthError(code: errorResponse.code) { + onResponse?(id, .failure(error)) + return + } + } } diff --git a/Tests/AuthTests/AppRespondSubscriberTests.swift b/Tests/AuthTests/AppRespondSubscriberTests.swift index d7baec010..a5eb20c2f 100644 --- a/Tests/AuthTests/AppRespondSubscriberTests.swift +++ b/Tests/AuthTests/AppRespondSubscriberTests.swift @@ -15,6 +15,7 @@ class AppRespondSubscriberTests: XCTestCase { let walletAccount = Account(chainIdentifier: "eip155:1", address: "0x724d0D2DaD3fbB0C168f947B87Fa5DBe36F1A8bf")! let prvKey = Data(hex: "462c1dad6832d7d96ccf87bd6a686a4110e114aaaebd5512e552c0e3a87b480f") var messageSigner: MessageSigner! + var pairingStorage: WCPairingStorageMock! override func setUp() { networkingInteractor = NetworkingInteractorMock() @@ -22,12 +23,14 @@ class AppRespondSubscriberTests: XCTestCase { messageSigner = MessageSigner() let historyStorage = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: StorageDomainIdentifiers.jsonRpcHistory.rawValue) rpcHistory = RPCHistory(keyValueStore: historyStorage) + pairingStorage = WCPairingStorageMock() sut = AppRespondSubscriber( networkingInteractor: networkingInteractor, logger: ConsoleLoggerMock(), rpcHistory: rpcHistory, signatureVerifier: messageSigner, - messageFormatter: messageFormatter) + messageFormatter: messageFormatter, + pairingStorage: pairingStorage) } func testMessageCompromisedFailure() { diff --git a/Tests/WalletConnectSignTests/Mocks/WCPairingStorageMock.swift b/Tests/TestingUtils/Mocks/WCPairingStorageMock.swift similarity index 95% rename from Tests/WalletConnectSignTests/Mocks/WCPairingStorageMock.swift rename to Tests/TestingUtils/Mocks/WCPairingStorageMock.swift index f3c11550e..664d206e2 100644 --- a/Tests/WalletConnectSignTests/Mocks/WCPairingStorageMock.swift +++ b/Tests/TestingUtils/Mocks/WCPairingStorageMock.swift @@ -1,5 +1,4 @@ import WalletConnectPairing -@testable import WalletConnectSign final class WCPairingStorageMock: WCPairingStorage { From 65c8fa31b6b864d1f133f451f2454a85e084d293 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 31 Aug 2022 10:12:20 +0200 Subject: [PATCH 20/92] update testing utils package dependencies --- Package.swift | 2 +- .../Mocks/WCPairingStorageMock.swift | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Package.swift b/Package.swift index 22c22aac8..9c3cd5b68 100644 --- a/Package.swift +++ b/Package.swift @@ -85,7 +85,7 @@ let package = Package( dependencies: ["WalletConnectKMS", "WalletConnectUtils", "TestingUtils"]), .target( name: "TestingUtils", - dependencies: ["WalletConnectUtils", "WalletConnectKMS", "JSONRPC"], + dependencies: ["WalletConnectUtils", "WalletConnectKMS", "JSONRPC", "WalletConnectPairing"], path: "Tests/TestingUtils"), .testTarget( name: "WalletConnectUtilsTests", diff --git a/Tests/TestingUtils/Mocks/WCPairingStorageMock.swift b/Tests/TestingUtils/Mocks/WCPairingStorageMock.swift index 664d206e2..35e737082 100644 --- a/Tests/TestingUtils/Mocks/WCPairingStorageMock.swift +++ b/Tests/TestingUtils/Mocks/WCPairingStorageMock.swift @@ -1,32 +1,32 @@ import WalletConnectPairing -final class WCPairingStorageMock: WCPairingStorage { +public final class WCPairingStorageMock: WCPairingStorage { - var onPairingExpiration: ((WCPairing) -> Void)? + public var onPairingExpiration: ((WCPairing) -> Void)? private(set) var pairings: [String: WCPairing] = [:] - func hasPairing(forTopic topic: String) -> Bool { + public func hasPairing(forTopic topic: String) -> Bool { pairings[topic] != nil } - func setPairing(_ pairing: WCPairing) { + public func setPairing(_ pairing: WCPairing) { pairings[pairing.topic] = pairing } - func getPairing(forTopic topic: String) -> WCPairing? { + public func getPairing(forTopic topic: String) -> WCPairing? { pairings[topic] } - func getAll() -> [WCPairing] { + public func getAll() -> [WCPairing] { Array(pairings.values) } - func delete(topic: String) { + public func delete(topic: String) { pairings[topic] = nil } - func deleteAll() { + public func deleteAll() { pairings = [:] } } From 97754f5e12b5229f22839b9c64d9aa0d4f461314 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 31 Aug 2022 10:22:24 +0200 Subject: [PATCH 21/92] run lint --- Example/DApp/Auth/AuthView.swift | 1 - Example/DApp/Auth/AuthViewModel.swift | 4 +- Example/DApp/SceneDelegate.swift | 2 +- Example/IntegrationTests/Auth/AuthTests.swift | 6 +- .../ApplicationLayer/SceneDelegate.swift | 2 +- .../Wallet/Scan/Views/ScanQR.swift | 16 +++--- .../Wallet/Scan/Views/ScanQRView.swift | 56 +++++++++---------- Sources/Auth/Auth.swift | 2 +- Sources/Auth/AuthClient.swift | 2 - .../Services/App/AppRespondSubscriber.swift | 2 - .../Common/NetworkingInteractor.swift | 2 +- Sources/Auth/Types/Errors/Reason.swift | 1 - .../WalletConnectSign/Sign/SignClient.swift | 1 - Tests/AuthTests/Mocks/AppMetadata.swift | 1 - .../Mocks/NetworkingInteractorMock.swift | 2 +- .../Mocks/BackgroundTaskRegistrarMock.swift | 4 +- .../Mocks/AppMetadata.swift | 1 - Tests/WalletConnectSignTests/Stub/Stubs.swift | 1 - .../WCPairingTests.swift | 10 ++-- 19 files changed, 53 insertions(+), 63 deletions(-) diff --git a/Example/DApp/Auth/AuthView.swift b/Example/DApp/Auth/AuthView.swift index f120f1871..5acdaf08c 100644 --- a/Example/DApp/Auth/AuthView.swift +++ b/Example/DApp/Auth/AuthView.swift @@ -98,4 +98,3 @@ struct CircleButtonStyle: ButtonStyle { .cornerRadius(8.0) } } - diff --git a/Example/DApp/Auth/AuthViewModel.swift b/Example/DApp/Auth/AuthViewModel.swift index 037e5131a..8c59d2a19 100644 --- a/Example/DApp/Auth/AuthViewModel.swift +++ b/Example/DApp/Auth/AuthViewModel.swift @@ -35,7 +35,7 @@ final class AuthViewModel: ObservableObject { } func walletDidPressed() { - + } func deeplinkPressed() { @@ -47,7 +47,7 @@ final class AuthViewModel: ObservableObject { private extension AuthViewModel { func setupSubscriptions() { - Auth.instance.authResponsePublisher.sink { [weak self] (id, result) in + Auth.instance.authResponsePublisher.sink { [weak self] (_, result) in switch result { case .success(let cacao): self?.state = .signed(cacao) diff --git a/Example/DApp/SceneDelegate.swift b/Example/DApp/SceneDelegate.swift index 163421bcc..eeaaf2369 100644 --- a/Example/DApp/SceneDelegate.swift +++ b/Example/DApp/SceneDelegate.swift @@ -18,7 +18,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { private let authCoordinator = AuthCoordinator() func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - Relay.configure(projectId: "8ba9ee138960775e5231b70cc5ef1c3a",socketFactory: SocketFactory()) + Relay.configure(projectId: "8ba9ee138960775e5231b70cc5ef1c3a", socketFactory: SocketFactory()) setupWindow(scene: scene) } diff --git a/Example/IntegrationTests/Auth/AuthTests.swift b/Example/IntegrationTests/Auth/AuthTests.swift index 1c2deff7d..43d56d372 100644 --- a/Example/IntegrationTests/Auth/AuthTests.swift +++ b/Example/IntegrationTests/Auth/AuthTests.swift @@ -72,7 +72,7 @@ final class AuthTests: XCTestCase { } } .store(in: &publishers) - app.authResponsePublisher.sink { (id, result) in + app.authResponsePublisher.sink { (_, result) in guard case .success = result else { XCTFail(); return } responseExpectation.fulfill() } @@ -90,7 +90,7 @@ final class AuthTests: XCTestCase { } } .store(in: &publishers) - app.authResponsePublisher.sink { (id, result) in + app.authResponsePublisher.sink { (_, result) in guard case .failure(let error) = result else { XCTFail(); return } XCTAssertEqual(error, .userRejeted) responseExpectation.fulfill() @@ -111,7 +111,7 @@ final class AuthTests: XCTestCase { } } .store(in: &publishers) - app.authResponsePublisher.sink { (id, result) in + app.authResponsePublisher.sink { (_, result) in guard case .failure(let error) = result else { XCTFail(); return } XCTAssertEqual(error, .signatureVerificationFailed) responseExpectation.fulfill() diff --git a/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift b/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift index 8660ed4c5..43335bd7d 100644 --- a/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift +++ b/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift @@ -12,7 +12,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { MigrationConfigurator(app: app), ThirdPartyConfigurator(), ApplicationConfigurator(app: app), - AppearanceConfigurator(), + AppearanceConfigurator() ] } diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQR.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQR.swift index 0bb3369d6..8b78b3098 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQR.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQR.swift @@ -1,28 +1,28 @@ import SwiftUI struct ScanQR: UIViewRepresentable { - + class Coordinator: ScanQRViewDelegate { private let onValue: (String) -> Void private let onError: (Error) -> Void - + init(onValue: @escaping (String) -> Void, onError: @escaping (Error) -> Void) { self.onValue = onValue self.onError = onError } - + func scanDidDetect(value: String) { onValue(value) } - + func scanDidFail(with error: Error) { onError(error) } } - + let onValue: (String) -> Void let onError: (Error) -> Void - + func makeUIView(context: Context) -> ScanQRView { let view = ScanQRView() view.delegate = context.coordinator @@ -30,9 +30,9 @@ struct ScanQR: UIViewRepresentable { } func updateUIView(_ uiView: ScanQRView, context: Context) { - + } - + func makeCoordinator() -> Coordinator { return Coordinator(onValue: onValue, onError: onError) } diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQRView.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQRView.swift index c3c8327b4..2d9db1056 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQRView.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Scan/Views/ScanQRView.swift @@ -10,17 +10,17 @@ final class ScanQRView: UIView { enum Errors: Error { case deviceNotFound } - + weak var delegate: ScanQRViewDelegate? - + private let targetSize = CGSize( width: UIScreen.main.bounds.width - 32.0, height: UIScreen.main.bounds.width - 32.0 ) - + private var videoPreviewLayer: AVCaptureVideoPreviewLayer? private var captureSession: AVCaptureSession? - + private lazy var borderView: UIView = { let borderView = ScanTargetView(radius: 24.0, color: .white, strokeWidth: 2.0, length: 36.0) borderView.alpha = 0.85 @@ -34,21 +34,21 @@ final class ScanQRView: UIView { bluredView.layer.mask = createMaskLayer() return bluredView }() - + override init(frame: CGRect) { super.init(frame: frame) - + setupView() startCaptureSession() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func layoutSubviews() { super.layoutSubviews() - + updateFrames() updateOrientation() } @@ -61,7 +61,7 @@ final class ScanQRView: UIView { // MARK: AVCaptureMetadataOutputObjectsDelegate extension ScanQRView: AVCaptureMetadataOutputObjectsDelegate { - + func metadataOutput(_ metadataOutput: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { guard let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject, @@ -76,14 +76,14 @@ extension ScanQRView: AVCaptureMetadataOutputObjectsDelegate { // MARK: Privates private extension ScanQRView { - + private func setupView() { backgroundColor = .black addSubview(bluredView) addSubview(borderView) } - + private func createMaskLayer() -> CAShapeLayer { let maskPath = UIBezierPath(rect: bounds) let rect = UIBezierPath( @@ -98,62 +98,62 @@ private extension ScanQRView { ) maskPath.append(rect) maskPath.usesEvenOddFillRule = true - + let maskLayer = CAShapeLayer() maskLayer.path = maskPath.cgPath maskLayer.fillRule = .evenOdd return maskLayer } - + private func startCaptureSession() { DispatchQueue.global().async { [weak self] in guard let self = self else { return } - + do { let session = try self.createCaptureSession() session.startRunning() self.captureSession = session - + DispatchQueue.main.async { self.setupVideoPreviewLayer(with: session) } } catch { DispatchQueue.main.async { self.delegate?.scanDidFail(with: error) } } } } - + private func createCaptureSession() throws -> AVCaptureSession { guard let captureDevice = AVCaptureDevice.default(for: .video) else { throw Errors.deviceNotFound } - + let input = try AVCaptureDeviceInput(device: captureDevice) - + let session = AVCaptureSession() session.addInput(input) - + let captureMetadataOutput = AVCaptureMetadataOutput() captureMetadataOutput.setMetadataObjectsDelegate(self, queue: .main) session.addOutput(captureMetadataOutput) - + captureMetadataOutput.metadataObjectTypes = [.qr] - + return session } - + private func stopCaptureSession() { captureSession?.stopRunning() captureSession = nil } - + private func setupVideoPreviewLayer(with session: AVCaptureSession) { let previewLayer = AVCaptureVideoPreviewLayer(session: session) previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill previewLayer.frame = layer.bounds videoPreviewLayer = previewLayer - + layer.insertSublayer(previewLayer, at: 0) } - + private func updateFrames() { borderView.frame.size = targetSize borderView.center = center @@ -161,13 +161,13 @@ private extension ScanQRView { bluredView.layer.mask = createMaskLayer() videoPreviewLayer?.frame = layer.bounds } - + private func updateOrientation() { guard let connection = videoPreviewLayer?.connection else { return } let previewLayerConnection: AVCaptureConnection = connection - + guard previewLayerConnection.isVideoOrientationSupported else { return } diff --git a/Sources/Auth/Auth.swift b/Sources/Auth/Auth.swift index 002671891..b209104bd 100644 --- a/Sources/Auth/Auth.swift +++ b/Sources/Auth/Auth.swift @@ -25,7 +25,7 @@ public class Auth { account: config.account, relayClient: Relay.instance) }() - + private static var config: Config? private init() { } diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index f4da23fa5..6b45e3183 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -4,7 +4,6 @@ import WalletConnectUtils import WalletConnectPairing import WalletConnectRelay - /// WalletConnect Auth Client /// /// Cannot be instantiated outside of the SDK @@ -41,7 +40,6 @@ public class AuthClient { /// An object that loggs SDK's errors and info messages public let logger: ConsoleLogging - // MARK: - Private Properties private var authResponsePublisherSubject = PassthroughSubject<(id: RPCID, result: Result), Never>() diff --git a/Sources/Auth/Services/App/AppRespondSubscriber.swift b/Sources/Auth/Services/App/AppRespondSubscriber.swift index 84a0d241f..a0daf9e1d 100644 --- a/Sources/Auth/Services/App/AppRespondSubscriber.swift +++ b/Sources/Auth/Services/App/AppRespondSubscriber.swift @@ -64,11 +64,9 @@ class AppRespondSubscriber { }.store(in: &publishers) } - private func activatePairingIfNeeded(id: RPCID) { guard let record = rpcHistory.get(recordId: id) else { return } let pairingTopic = record.topic - print(pairingTopic) guard var pairing = pairingStorage.getPairing(forTopic: pairingTopic) else { return } if !pairing.active { pairing.activate() diff --git a/Sources/Auth/Services/Common/NetworkingInteractor.swift b/Sources/Auth/Services/Common/NetworkingInteractor.swift index 24419b295..e66e97a77 100644 --- a/Sources/Auth/Services/Common/NetworkingInteractor.swift +++ b/Sources/Auth/Services/Common/NetworkingInteractor.swift @@ -11,7 +11,7 @@ protocol NetworkInteracting { func subscribe(topic: String) async throws func unsubscribe(topic: String) func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws - func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws + func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws } diff --git a/Sources/Auth/Types/Errors/Reason.swift b/Sources/Auth/Types/Errors/Reason.swift index bbf491fdd..4822fe64c 100644 --- a/Sources/Auth/Types/Errors/Reason.swift +++ b/Sources/Auth/Types/Errors/Reason.swift @@ -4,4 +4,3 @@ protocol Reason { var code: Int { get } var message: String { get } } - diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index d00e54242..72fa56e24 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -308,7 +308,6 @@ public final class SignClient { return WalletConnectUtils.JsonRpcRecord(id: record.id, topic: record.topic, request: request, response: record.response, chainId: record.chainId) } - #if DEBUG /// Delete all stored data such as: pairings, sessions, keys /// diff --git a/Tests/AuthTests/Mocks/AppMetadata.swift b/Tests/AuthTests/Mocks/AppMetadata.swift index 2f8d6a558..75d48acb1 100644 --- a/Tests/AuthTests/Mocks/AppMetadata.swift +++ b/Tests/AuthTests/Mocks/AppMetadata.swift @@ -1,4 +1,3 @@ - import Foundation import WalletConnectPairing diff --git a/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift b/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift index a3364de40..1a9c03790 100644 --- a/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift +++ b/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift @@ -33,7 +33,7 @@ struct NetworkingInteractorMock: NetworkInteracting { } func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { - + } func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws { diff --git a/Tests/RelayerTests/Mocks/BackgroundTaskRegistrarMock.swift b/Tests/RelayerTests/Mocks/BackgroundTaskRegistrarMock.swift index d8f895242..52d49d774 100644 --- a/Tests/RelayerTests/Mocks/BackgroundTaskRegistrarMock.swift +++ b/Tests/RelayerTests/Mocks/BackgroundTaskRegistrarMock.swift @@ -3,12 +3,12 @@ import Foundation class BackgroundTaskRegistrarMock: BackgroundTaskRegistering { var completion: (() -> Void)? - + func register(name: String, completion: @escaping () -> Void) { self.completion = completion } func invalidate() { - + } } diff --git a/Tests/WalletConnectSignTests/Mocks/AppMetadata.swift b/Tests/WalletConnectSignTests/Mocks/AppMetadata.swift index 2f8d6a558..75d48acb1 100644 --- a/Tests/WalletConnectSignTests/Mocks/AppMetadata.swift +++ b/Tests/WalletConnectSignTests/Mocks/AppMetadata.swift @@ -1,4 +1,3 @@ - import Foundation import WalletConnectPairing diff --git a/Tests/WalletConnectSignTests/Stub/Stubs.swift b/Tests/WalletConnectSignTests/Stub/Stubs.swift index 7f80b8da9..a17ad98a8 100644 --- a/Tests/WalletConnectSignTests/Stub/Stubs.swift +++ b/Tests/WalletConnectSignTests/Stub/Stubs.swift @@ -5,7 +5,6 @@ import WalletConnectUtils import TestingUtils import WalletConnectPairing - extension Pairing { static func stub(expiryDate: Date = Date(timeIntervalSinceNow: 10000), topic: String = String.generateTopic()) -> Pairing { Pairing(topic: topic, peer: nil, expiryDate: expiryDate) diff --git a/Tests/WalletConnectSignTests/WCPairingTests.swift b/Tests/WalletConnectSignTests/WCPairingTests.swift index ca6872e24..6dcad44a0 100644 --- a/Tests/WalletConnectSignTests/WCPairingTests.swift +++ b/Tests/WalletConnectSignTests/WCPairingTests.swift @@ -41,7 +41,7 @@ final class WCPairingTests: XCTestCase { try? pairing.updateExpiry() XCTAssertEqual(pairing.expiryDate, activeExpiry) } - + func testUpdateExpiryForUri() { var pairing = WCPairing(uri: WalletConnectURI.stub()) let activeExpiry = referenceDate.advanced(by: WCPairing.timeToLiveActive) @@ -57,7 +57,7 @@ final class WCPairingTests: XCTestCase { XCTAssertTrue(pairing.active) XCTAssertEqual(pairing.expiryDate, activeExpiry) } - + func testActivateURI() { var pairing = WCPairing(uri: WalletConnectURI.stub()) let activeExpiry = referenceDate.advanced(by: WCPairing.timeToLiveActive) @@ -66,14 +66,14 @@ final class WCPairingTests: XCTestCase { XCTAssertTrue(pairing.active) XCTAssertEqual(pairing.expiryDate, activeExpiry) } - + func testUpdateExpiryWhenValueIsGreaterThanMax() { var pairing = WCPairing(topic: "", relay: .stub(), peerMetadata: .stub(), expiryDate: referenceDate) XCTAssertThrowsError(try pairing.updateExpiry(40 * .day)) { error in XCTAssertEqual(error as! WCPairing.Errors, WCPairing.Errors.invalidUpdateExpiryValue) } } - + func testUpdateExpiryWhenNewExpiryDateIsLessThanExpiryDate() { let expiryDate = referenceDate.advanced(by: 40 * .day) var pairing = WCPairing(topic: "", relay: .stub(), peerMetadata: .stub(), expiryDate: expiryDate) @@ -89,7 +89,7 @@ final class WCPairingTests: XCTestCase { XCTAssertTrue(pairing.active) XCTAssertEqual(referenceDate.advanced(by: 30 * .day), pairing.expiryDate) } - + func testActivateWhenUpdateExpiryIsInvalid() { let expiryDate = referenceDate.advanced(by: 40 * .day) var pairing = WCPairing(topic: "", relay: .stub(), peerMetadata: .stub(), expiryDate: expiryDate) From 50ed654506db0977f1817319248ae9ad552b7e70 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 31 Aug 2022 10:48:48 +0200 Subject: [PATCH 22/92] fix test --- .../Auth/Services/App/AppRespondSubscriber.swift | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Sources/Auth/Services/App/AppRespondSubscriber.swift b/Sources/Auth/Services/App/AppRespondSubscriber.swift index a0daf9e1d..3a501b718 100644 --- a/Sources/Auth/Services/App/AppRespondSubscriber.swift +++ b/Sources/Auth/Services/App/AppRespondSubscriber.swift @@ -42,7 +42,11 @@ class AppRespondSubscriber { activatePairingIfNeeded(id: requestId) networkingInteractor.unsubscribe(topic: subscriptionPayload.topic) - handleIfError(response: response) + if let errorResponse = response.error, + let error = AuthError(code: errorResponse.code) { + onResponse?(requestId, .failure(error)) + return + } guard let cacao = try? response.result?.get(Cacao.self), @@ -74,13 +78,4 @@ class AppRespondSubscriber { try? pairing.updateExpiry() } } - - private func handleIfError(response: RPCResponse) { - if let errorResponse = response.error, - let id = response.id, - let error = AuthError(code: errorResponse.code) { - onResponse?(id, .failure(error)) - return - } - } } From 3a689831ec624f7c927536b8860b083f7d725e1e Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 1 Sep 2022 13:38:26 +0300 Subject: [PATCH 23/92] PR suggestions --- Sources/Chat/NetworkingInteractor.swift | 2 +- Sources/WalletConnectRelay/Dispatching.swift | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/Chat/NetworkingInteractor.swift b/Sources/Chat/NetworkingInteractor.swift index 32bbcfd49..ee31ec033 100644 --- a/Sources/Chat/NetworkingInteractor.swift +++ b/Sources/Chat/NetworkingInteractor.swift @@ -86,7 +86,7 @@ class NetworkingInteractor: NetworkInteracting { } else if let deserializedJsonRpcError: JSONRPCErrorResponse = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { handleJsonRpcErrorResponse(response: deserializedJsonRpcError) } else { - logger.error("Warning: Networking Interactor - Received unknown object type from networking relay") + logger.warn("Networking Interactor - Received unknown object type from networking relay") } } diff --git a/Sources/WalletConnectRelay/Dispatching.swift b/Sources/WalletConnectRelay/Dispatching.swift index c7ab47f30..2d8be9ba5 100644 --- a/Sources/WalletConnectRelay/Dispatching.swift +++ b/Sources/WalletConnectRelay/Dispatching.swift @@ -65,26 +65,26 @@ final class Dispatcher: NSObject, Dispatching { } private func setUpWebSocketSession() { - socket.onText = { [weak self] in - self?.onMessage?($0) + socket.onText = { [unowned self] in + self.onMessage?($0) } } private func setUpSocketConnectionObserving() { - socket.onConnect = { [weak self] in - self?.dequeuePendingTextFrames() - self?.onConnect?() + socket.onConnect = { [unowned self] in + self.dequeuePendingTextFrames() + self.onConnect?() } - socket.onDisconnect = { [weak self] _ in - self?.onDisconnect?() + socket.onDisconnect = { [unowned self] _ in + self.onDisconnect?() } } private func dequeuePendingTextFrames() { while let frame = textFramesQueue.dequeue() { - send(frame) { [weak self] error in + send(frame) { [unowned self] error in if let error = error { - self?.logger.error(error.localizedDescription) + self.logger.error(error.localizedDescription) } } } From d23134832b4137c00cd34e98945bbffc081bda72 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 1 Sep 2022 13:32:13 +0300 Subject: [PATCH 24/92] Throw error on wrong API param --- Sources/Auth/AuthClient.swift | 5 ++++- Sources/WalletConnectSign/Sign/SignClient.swift | 3 +++ Sources/WalletConnectSign/WalletConnectError.swift | 6 +++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index 9e33a3167..ba84fe74c 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -11,7 +11,7 @@ import WalletConnectRelay /// Access via `Auth.instance` public class AuthClient { enum Errors: Error { - case malformedPairingURI + case pairingUriWrongApiParam case unknownWalletAddress case noPairingMatchingTopic } @@ -92,6 +92,9 @@ public class AuthClient { /// - When URI is invalid format or missing params /// - When topic is already in use public func pair(uri: WalletConnectURI) async throws { + guard uri.api == .auth else { + throw Errors.pairingUriWrongApiParam + } try await walletPairService.pair(uri) } diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index d4ced6b64..d43546582 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -169,6 +169,9 @@ public final class SignClient { /// - When URI has invalid format or missing params /// - When topic is already in use public func pair(uri: WalletConnectURI) async throws { + guard uri.api == .sign else { + throw WalletConnectError.pairingUriWrongApiParam + } try await pairEngine.pair(uri) } diff --git a/Sources/WalletConnectSign/WalletConnectError.swift b/Sources/WalletConnectSign/WalletConnectError.swift index dd6d21361..afd7f73b8 100644 --- a/Sources/WalletConnectSign/WalletConnectError.swift +++ b/Sources/WalletConnectSign/WalletConnectError.swift @@ -1,7 +1,7 @@ enum WalletConnectError: Error { case pairingProposalFailed - case malformedPairingURI + case pairingUriWrongApiParam case noPairingMatchingTopic(String) case noSessionMatchingTopic(String) case sessionNotAcknowledged(String) @@ -29,8 +29,8 @@ extension WalletConnectError { switch self { case .pairingProposalFailed: return "Pairing proposal failed." - case .malformedPairingURI: - return "Pairing URI string is invalid." + case .pairingUriWrongApiParam: + return "Pairing URI containt wrong API param" case .noPairingMatchingTopic(let topic): return "There is no existing pairing matching the topic: \(topic)." case .noSessionMatchingTopic(let topic): From 482f3b21dac2790f29650e8dc07a04a2c6e5d4cd Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 1 Sep 2022 14:46:13 +0300 Subject: [PATCH 25/92] Auth prefix --- Sources/Auth/Services/App/AppPairService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Auth/Services/App/AppPairService.swift b/Sources/Auth/Services/App/AppPairService.swift index 05a309653..1dc9c0cbc 100644 --- a/Sources/Auth/Services/App/AppPairService.swift +++ b/Sources/Auth/Services/App/AppPairService.swift @@ -18,7 +18,7 @@ actor AppPairService { try await networkingInteractor.subscribe(topic: topic) let symKey = try! kms.createSymmetricKey(topic) let pairing = WCPairing(topic: topic) - let uri = WalletConnectURI(topic: topic, symKey: symKey.hexRepresentation, relay: pairing.relay) + let uri = WalletConnectURI(topic: topic, symKey: symKey.hexRepresentation, relay: pairing.relay, api: .auth) pairingStorage.setPairing(pairing) return uri } From b5f4f137834233efd5c190f797511cae6ada5514 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 1 Sep 2022 18:28:04 +0300 Subject: [PATCH 26/92] NetworkInteracting moved to package --- Package.swift | 9 +++++++- .../Auth/Services/App/AppPairService.swift | 1 + .../Auth/Services/App/AppRequestService.swift | 3 ++- .../Services/App/AppRespondSubscriber.swift | 5 +++-- .../Common/NetworkingInteractor.swift | 22 +++---------------- .../Services/Wallet/WalletPairService.swift | 1 + .../Wallet/WalletRequestSubscriber.swift | 5 +++-- .../Wallet/WalletRespondService.swift | 3 ++- Sources/Auth/Types/Errors/AuthError.swift | 1 + .../Types/RequestSubscriptionPayload.swift | 7 ------ .../Types/ResponseSubscriptionPayload.swift | 7 ------ .../NetworkInteractor.swift | 21 ++++++++++++++++++ .../Reason.swift | 2 +- .../RequestSubscriptionPayload.swift | 12 ++++++++++ .../ResponseSubscriptionPayload.swift | 12 ++++++++++ .../AuthTests/AppRespondSubscriberTests.swift | 1 + .../Mocks/NetworkingInteractorMock.swift | 1 + .../Stubs/RequestSubscriptionPayload.swift | 3 ++- .../WalletRequestSubscriberTests.swift | 5 +++-- 19 files changed, 77 insertions(+), 44 deletions(-) delete mode 100644 Sources/Auth/Types/RequestSubscriptionPayload.swift delete mode 100644 Sources/Auth/Types/ResponseSubscriptionPayload.swift create mode 100644 Sources/WalletConnectNetworking/NetworkInteractor.swift rename Sources/{Auth/Types/Errors => WalletConnectNetworking}/Reason.swift (75%) create mode 100644 Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift create mode 100644 Sources/WalletConnectNetworking/ResponseSubscriptionPayload.swift diff --git a/Package.swift b/Package.swift index 9c3cd5b68..6e0da39f7 100644 --- a/Package.swift +++ b/Package.swift @@ -21,7 +21,10 @@ let package = Package( targets: ["Auth"]), .library( name: "WalletConnectRouter", - targets: ["WalletConnectRouter"]) + targets: ["WalletConnectRouter"]), + .library( + name: "WalletConnectNetworking", + targets: ["WalletConnectNetworking"]), ], dependencies: [ .package(url: "https://github.com/flypaper0/Web3.swift", .branch("feature/eip-155")) @@ -42,6 +45,7 @@ let package = Package( "WalletConnectUtils", "WalletConnectKMS", "WalletConnectPairing", + "WalletConnectNetworking", .product(name: "Web3", package: "Web3.swift") ], path: "Sources/Auth"), @@ -65,6 +69,9 @@ let package = Package( .target( name: "Commons", dependencies: []), + .target( + name: "WalletConnectNetworking", + dependencies: ["JSONRPC", "WalletConnectKMS"]), .target( name: "WalletConnectRouter", dependencies: []), diff --git a/Sources/Auth/Services/App/AppPairService.swift b/Sources/Auth/Services/App/AppPairService.swift index 1dc9c0cbc..dc05600b0 100644 --- a/Sources/Auth/Services/App/AppPairService.swift +++ b/Sources/Auth/Services/App/AppPairService.swift @@ -1,6 +1,7 @@ import Foundation import WalletConnectKMS import WalletConnectPairing +import WalletConnectNetworking actor AppPairService { private let networkingInteractor: NetworkInteracting diff --git a/Sources/Auth/Services/App/AppRequestService.swift b/Sources/Auth/Services/App/AppRequestService.swift index b3186e77d..9303d7bb8 100644 --- a/Sources/Auth/Services/App/AppRequestService.swift +++ b/Sources/Auth/Services/App/AppRequestService.swift @@ -1,7 +1,8 @@ import Foundation +import JSONRPC +import WalletConnectNetworking import WalletConnectUtils import WalletConnectKMS -import JSONRPC actor AppRequestService { private let networkingInteractor: NetworkInteracting diff --git a/Sources/Auth/Services/App/AppRespondSubscriber.swift b/Sources/Auth/Services/App/AppRespondSubscriber.swift index 3a501b718..16e0ff807 100644 --- a/Sources/Auth/Services/App/AppRespondSubscriber.swift +++ b/Sources/Auth/Services/App/AppRespondSubscriber.swift @@ -1,7 +1,8 @@ -import Combine import Foundation -import WalletConnectUtils +import Combine import JSONRPC +import WalletConnectNetworking +import WalletConnectUtils import WalletConnectPairing class AppRespondSubscriber { diff --git a/Sources/Auth/Services/Common/NetworkingInteractor.swift b/Sources/Auth/Services/Common/NetworkingInteractor.swift index e66e97a77..b1cc7684a 100644 --- a/Sources/Auth/Services/Common/NetworkingInteractor.swift +++ b/Sources/Auth/Services/Common/NetworkingInteractor.swift @@ -1,26 +1,10 @@ import Foundation +import Combine +import JSONRPC +import WalletConnectNetworking import WalletConnectRelay import WalletConnectUtils -import Combine import WalletConnectKMS -import JSONRPC - -protocol NetworkInteracting { - var requestPublisher: AnyPublisher {get} - var responsePublisher: AnyPublisher {get} - func subscribe(topic: String) async throws - func unsubscribe(topic: String) - func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws - func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws - func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws - func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws -} - -extension NetworkInteracting { - func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType = .type0) async throws { - try await self.request(request, topic: topic, tag: tag, envelopeType: envelopeType) - } -} class NetworkingInteractor: NetworkInteracting { private var publishers = Set() diff --git a/Sources/Auth/Services/Wallet/WalletPairService.swift b/Sources/Auth/Services/Wallet/WalletPairService.swift index ef4fc2e28..355de0a29 100644 --- a/Sources/Auth/Services/Wallet/WalletPairService.swift +++ b/Sources/Auth/Services/Wallet/WalletPairService.swift @@ -1,6 +1,7 @@ import Foundation import WalletConnectKMS import WalletConnectPairing +import WalletConnectNetworking actor WalletPairService { enum Errors: Error { diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift index 522820187..c54bd3935 100644 --- a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift +++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift @@ -1,7 +1,8 @@ -import Combine import Foundation -import WalletConnectUtils +import Combine import JSONRPC +import WalletConnectNetworking +import WalletConnectUtils import WalletConnectKMS class WalletRequestSubscriber { diff --git a/Sources/Auth/Services/Wallet/WalletRespondService.swift b/Sources/Auth/Services/Wallet/WalletRespondService.swift index 07e01871c..9107f4df3 100644 --- a/Sources/Auth/Services/Wallet/WalletRespondService.swift +++ b/Sources/Auth/Services/Wallet/WalletRespondService.swift @@ -1,7 +1,8 @@ import Foundation -import WalletConnectKMS import JSONRPC +import WalletConnectKMS import WalletConnectUtils +import WalletConnectNetworking actor WalletRespondService { enum Errors: Error { diff --git a/Sources/Auth/Types/Errors/AuthError.swift b/Sources/Auth/Types/Errors/AuthError.swift index 84c43a7d0..f28cab817 100644 --- a/Sources/Auth/Types/Errors/AuthError.swift +++ b/Sources/Auth/Types/Errors/AuthError.swift @@ -1,4 +1,5 @@ import Foundation +import WalletConnectNetworking /// Authentication error public enum AuthError: Codable, Equatable, Error { diff --git a/Sources/Auth/Types/RequestSubscriptionPayload.swift b/Sources/Auth/Types/RequestSubscriptionPayload.swift deleted file mode 100644 index 00a5cdcb4..000000000 --- a/Sources/Auth/Types/RequestSubscriptionPayload.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation -import JSONRPC - -struct RequestSubscriptionPayload: Codable, Equatable { - let topic: String - let request: RPCRequest -} diff --git a/Sources/Auth/Types/ResponseSubscriptionPayload.swift b/Sources/Auth/Types/ResponseSubscriptionPayload.swift deleted file mode 100644 index d6425622f..000000000 --- a/Sources/Auth/Types/ResponseSubscriptionPayload.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation -import JSONRPC - -struct ResponseSubscriptionPayload: Codable, Equatable { - let topic: String - let response: RPCResponse -} diff --git a/Sources/WalletConnectNetworking/NetworkInteractor.swift b/Sources/WalletConnectNetworking/NetworkInteractor.swift new file mode 100644 index 000000000..668448236 --- /dev/null +++ b/Sources/WalletConnectNetworking/NetworkInteractor.swift @@ -0,0 +1,21 @@ +import Foundation +import Combine +import JSONRPC +import WalletConnectKMS + +public protocol NetworkInteracting { + var requestPublisher: AnyPublisher {get} + var responsePublisher: AnyPublisher {get} + func subscribe(topic: String) async throws + func unsubscribe(topic: String) + func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws + func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws + func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws + func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws +} + +extension NetworkInteracting { + public func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType = .type0) async throws { + try await self.request(request, topic: topic, tag: tag, envelopeType: envelopeType) + } +} diff --git a/Sources/Auth/Types/Errors/Reason.swift b/Sources/WalletConnectNetworking/Reason.swift similarity index 75% rename from Sources/Auth/Types/Errors/Reason.swift rename to Sources/WalletConnectNetworking/Reason.swift index 4822fe64c..b9b26694f 100644 --- a/Sources/Auth/Types/Errors/Reason.swift +++ b/Sources/WalletConnectNetworking/Reason.swift @@ -1,6 +1,6 @@ import Foundation -protocol Reason { +public protocol Reason { var code: Int { get } var message: String { get } } diff --git a/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift b/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift new file mode 100644 index 000000000..6132f19a2 --- /dev/null +++ b/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift @@ -0,0 +1,12 @@ +import Foundation +import JSONRPC + +public struct RequestSubscriptionPayload: Codable, Equatable { + public let topic: String + public let request: RPCRequest + + public init(topic: String, request: RPCRequest) { + self.topic = topic + self.request = request + } +} diff --git a/Sources/WalletConnectNetworking/ResponseSubscriptionPayload.swift b/Sources/WalletConnectNetworking/ResponseSubscriptionPayload.swift new file mode 100644 index 000000000..cfe2e6ab2 --- /dev/null +++ b/Sources/WalletConnectNetworking/ResponseSubscriptionPayload.swift @@ -0,0 +1,12 @@ +import Foundation +import JSONRPC + +public struct ResponseSubscriptionPayload: Codable, Equatable { + public let topic: String + public let response: RPCResponse + + public init(topic: String, response: RPCResponse) { + self.topic = topic + self.response = response + } +} diff --git a/Tests/AuthTests/AppRespondSubscriberTests.swift b/Tests/AuthTests/AppRespondSubscriberTests.swift index a5eb20c2f..95f5807db 100644 --- a/Tests/AuthTests/AppRespondSubscriberTests.swift +++ b/Tests/AuthTests/AppRespondSubscriberTests.swift @@ -2,6 +2,7 @@ import Foundation import XCTest @testable import Auth import WalletConnectUtils +import WalletConnectNetworking @testable import WalletConnectKMS @testable import TestingUtils import JSONRPC diff --git a/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift b/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift index 1a9c03790..5c9664d03 100644 --- a/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift +++ b/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift @@ -3,6 +3,7 @@ import Combine @testable import Auth import JSONRPC import WalletConnectKMS +import WalletConnectNetworking struct NetworkingInteractorMock: NetworkInteracting { diff --git a/Tests/AuthTests/Stubs/RequestSubscriptionPayload.swift b/Tests/AuthTests/Stubs/RequestSubscriptionPayload.swift index 48ded537d..156780d42 100644 --- a/Tests/AuthTests/Stubs/RequestSubscriptionPayload.swift +++ b/Tests/AuthTests/Stubs/RequestSubscriptionPayload.swift @@ -1,6 +1,7 @@ import Foundation -@testable import Auth import JSONRPC +import WalletConnectNetworking +@testable import Auth extension RequestSubscriptionPayload { static func stub(id: RPCID) -> RequestSubscriptionPayload { diff --git a/Tests/AuthTests/WalletRequestSubscriberTests.swift b/Tests/AuthTests/WalletRequestSubscriberTests.swift index bce269680..00e7f62ee 100644 --- a/Tests/AuthTests/WalletRequestSubscriberTests.swift +++ b/Tests/AuthTests/WalletRequestSubscriberTests.swift @@ -1,10 +1,11 @@ import Foundation import XCTest -@testable import Auth +import JSONRPC import WalletConnectUtils +import WalletConnectNetworking +@testable import Auth @testable import WalletConnectKMS @testable import TestingUtils -import JSONRPC class WalletRequestSubscriberTests: XCTestCase { var networkingInteractor: NetworkingInteractorMock! From fca1b495d002b5f22a2402c30f4c24e0afdc416a Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 1 Sep 2022 18:41:10 +0300 Subject: [PATCH 27/92] Auth NetworkInteractor moved to package --- Package.swift | 10 ++++--- Sources/Auth/AuthClientFactory.swift | 1 + .../NetworkingInteractor.swift | 30 ++++++++++--------- 3 files changed, 23 insertions(+), 18 deletions(-) rename Sources/{Auth/Services/Common => WalletConnectNetworking}/NetworkingInteractor.swift (82%) diff --git a/Package.swift b/Package.swift index 6e0da39f7..1578731af 100644 --- a/Package.swift +++ b/Package.swift @@ -41,9 +41,6 @@ let package = Package( .target( name: "Auth", dependencies: [ - "WalletConnectRelay", - "WalletConnectUtils", - "WalletConnectKMS", "WalletConnectPairing", "WalletConnectNetworking", .product(name: "Web3", package: "Web3.swift") @@ -71,7 +68,12 @@ let package = Package( dependencies: []), .target( name: "WalletConnectNetworking", - dependencies: ["JSONRPC", "WalletConnectKMS"]), + dependencies: [ + "JSONRPC", + "WalletConnectKMS", + "WalletConnectRelay", + "WalletConnectUtils", + ]), .target( name: "WalletConnectRouter", dependencies: []), diff --git a/Sources/Auth/AuthClientFactory.swift b/Sources/Auth/AuthClientFactory.swift index 3dc48fbea..b084a1846 100644 --- a/Sources/Auth/AuthClientFactory.swift +++ b/Sources/Auth/AuthClientFactory.swift @@ -3,6 +3,7 @@ import WalletConnectRelay import WalletConnectUtils import WalletConnectKMS import WalletConnectPairing +import WalletConnectNetworking public struct AuthClientFactory { diff --git a/Sources/Auth/Services/Common/NetworkingInteractor.swift b/Sources/WalletConnectNetworking/NetworkingInteractor.swift similarity index 82% rename from Sources/Auth/Services/Common/NetworkingInteractor.swift rename to Sources/WalletConnectNetworking/NetworkingInteractor.swift index b1cc7684a..91a664578 100644 --- a/Sources/Auth/Services/Common/NetworkingInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkingInteractor.swift @@ -1,12 +1,11 @@ import Foundation import Combine import JSONRPC -import WalletConnectNetworking import WalletConnectRelay import WalletConnectUtils import WalletConnectKMS -class NetworkingInteractor: NetworkInteracting { +public class NetworkingInteractor: NetworkInteracting { private var publishers = Set() private let relayClient: RelayClient private let serializer: Serializing @@ -14,21 +13,24 @@ class NetworkingInteractor: NetworkInteracting { private let logger: ConsoleLogging private let requestPublisherSubject = PassthroughSubject() - var requestPublisher: AnyPublisher { + private let responsePublisherSubject = PassthroughSubject() + + public var requestPublisher: AnyPublisher { requestPublisherSubject.eraseToAnyPublisher() } - private let responsePublisherSubject = PassthroughSubject() - var responsePublisher: AnyPublisher { + public var responsePublisher: AnyPublisher { responsePublisherSubject.eraseToAnyPublisher() } - var socketConnectionStatusPublisher: AnyPublisher + public var socketConnectionStatusPublisher: AnyPublisher - init(relayClient: RelayClient, + public init( + relayClient: RelayClient, serializer: Serializing, logger: ConsoleLogging, - rpcHistory: RPCHistory) { + rpcHistory: RPCHistory + ) { self.relayClient = relayClient self.serializer = serializer self.rpcHistory = rpcHistory @@ -40,11 +42,11 @@ class NetworkingInteractor: NetworkInteracting { .store(in: &publishers) } - func subscribe(topic: String) async throws { + public func subscribe(topic: String) async throws { try await relayClient.subscribe(topic: topic) } - func unsubscribe(topic: String) { + public func unsubscribe(topic: String) { relayClient.unsubscribe(topic: topic) { [unowned self] error in if let error = error { logger.error(error) @@ -54,7 +56,7 @@ class NetworkingInteractor: NetworkInteracting { } } - func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + public func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) let message = try! serializer.serialize(topic: topic, encodable: request, envelopeType: envelopeType) try await relayClient.publish(topic: topic, payload: message, tag: tag) @@ -63,7 +65,7 @@ class NetworkingInteractor: NetworkInteracting { /// Completes with an acknowledgement from the relay network. /// completes with error if networking client was not able to send a message /// TODO - relay client should provide async function - continualion should be removed from here - func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws { + public func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws { do { try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) let message = try serializer.serialize(topic: topic, encodable: request) @@ -81,13 +83,13 @@ class NetworkingInteractor: NetworkInteracting { } } - func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + public func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { try rpcHistory.resolve(response) let message = try! serializer.serialize(topic: topic, encodable: response, envelopeType: envelopeType) try await relayClient.publish(topic: topic, payload: message, tag: tag) } - func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { + public func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { let error = JSONRPCError(code: reason.code, message: reason.message) let response = RPCResponse(id: requestId, error: error) let message = try! serializer.serialize(topic: topic, encodable: response, envelopeType: envelopeType) From 8cef57d7d81cf3c938c01cf5259c3e6d149ec2e9 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 1 Sep 2022 22:24:12 +0300 Subject: [PATCH 28/92] Chat Genaric interactor connected --- .../xcschemes/WalletConnectChat.xcscheme | 77 +++++++++++ Package.swift | 17 +-- Sources/Auth/AuthClientFactory.swift | 3 +- Sources/Chat/ChatClient.swift | 7 +- Sources/Chat/ChatClientFactory.swift | 10 +- Sources/Chat/NetworkingInteractor.swift | 122 ------------------ .../Chat/ProtocolServices/Common/File.swift | 4 +- .../Common/MessagingService.swift | 44 +++---- .../Invitee/InvitationHandlingService.swift | 64 ++++----- .../Invitee/RegistryService.swift | 1 + .../Inviter/InviteService.swift | 56 ++++---- Sources/Chat/Types/ChatError.swift | 15 +++ Sources/Chat/Types/ChatRequestParams.swift | 63 --------- Sources/Chat/Types/ChatResponse.swift | 9 -- .../{InviteParams.swift => Invite.swift} | 8 ++ Sources/Chat/Types/Message.swift | 22 +++- .../Types/RequestSubscriptionPayload.swift | 7 - .../NetworkInteractor.swift | 23 +++- .../NetworkingInteractor.swift | 14 +- .../Mocks/NetworkingInteractorMock.swift | 44 ------- .../Mocks/NetworkingInteractorMock.swift | 50 ------- Tests/ChatTests/RegistryManagerTests.swift | 5 +- .../NetworkingInteractorMock.swift | 58 +++++++++ 23 files changed, 298 insertions(+), 425 deletions(-) create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/WalletConnectChat.xcscheme delete mode 100644 Sources/Chat/NetworkingInteractor.swift delete mode 100644 Sources/Chat/Types/ChatRequestParams.swift delete mode 100644 Sources/Chat/Types/ChatResponse.swift rename Sources/Chat/Types/{InviteParams.swift => Invite.swift} (71%) delete mode 100644 Sources/Chat/Types/RequestSubscriptionPayload.swift delete mode 100644 Tests/AuthTests/Mocks/NetworkingInteractorMock.swift delete mode 100644 Tests/ChatTests/Mocks/NetworkingInteractorMock.swift create mode 100644 Tests/TestingUtils/NetworkingInteractorMock.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectChat.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectChat.xcscheme new file mode 100644 index 000000000..c04003b78 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectChat.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Package.swift b/Package.swift index 1578731af..a562eca58 100644 --- a/Package.swift +++ b/Package.swift @@ -36,15 +36,11 @@ let package = Package( path: "Sources/WalletConnectSign"), .target( name: "Chat", - dependencies: ["WalletConnectRelay", "WalletConnectUtils", "WalletConnectKMS"], + dependencies: ["WalletConnectNetworking"], path: "Sources/Chat"), .target( name: "Auth", - dependencies: [ - "WalletConnectPairing", - "WalletConnectNetworking", - .product(name: "Web3", package: "Web3.swift") - ], + dependencies: ["WalletConnectPairing", "WalletConnectNetworking", .product(name: "Web3", package: "Web3.swift")], path: "Sources/Auth"), .target( name: "WalletConnectRelay", @@ -68,12 +64,7 @@ let package = Package( dependencies: []), .target( name: "WalletConnectNetworking", - dependencies: [ - "JSONRPC", - "WalletConnectKMS", - "WalletConnectRelay", - "WalletConnectUtils", - ]), + dependencies: ["JSONRPC", "WalletConnectKMS", "WalletConnectRelay", "WalletConnectUtils"]), .target( name: "WalletConnectRouter", dependencies: []), @@ -94,7 +85,7 @@ let package = Package( dependencies: ["WalletConnectKMS", "WalletConnectUtils", "TestingUtils"]), .target( name: "TestingUtils", - dependencies: ["WalletConnectUtils", "WalletConnectKMS", "JSONRPC", "WalletConnectPairing"], + dependencies: ["WalletConnectPairing", "WalletConnectNetworking"], path: "Tests/TestingUtils"), .testTarget( name: "WalletConnectUtilsTests", diff --git a/Sources/Auth/AuthClientFactory.swift b/Sources/Auth/AuthClientFactory.swift index b084a1846..469fa37a6 100644 --- a/Sources/Auth/AuthClientFactory.swift +++ b/Sources/Auth/AuthClientFactory.swift @@ -42,6 +42,7 @@ public struct AuthClientFactory { pendingRequestsProvider: pendingRequestsProvider, cleanupService: cleanupService, logger: logger, - pairingStorage: pairingStore, socketConnectionStatusPublisher: relayClient.socketConnectionStatusPublisher) + pairingStorage: pairingStore, + socketConnectionStatusPublisher: relayClient.socketConnectionStatusPublisher) } } diff --git a/Sources/Chat/ChatClient.swift b/Sources/Chat/ChatClient.swift index 1c44bbeb6..678a3436e 100644 --- a/Sources/Chat/ChatClient.swift +++ b/Sources/Chat/ChatClient.swift @@ -2,6 +2,7 @@ import Foundation import WalletConnectUtils import WalletConnectKMS import WalletConnectRelay +import WalletConnectNetworking import Combine public class ChatClient { @@ -16,7 +17,7 @@ public class ChatClient { private let kms: KeyManagementService private let threadStore: Database private let messagesStore: Database - private let invitePayloadStore: CodableStore<(RequestSubscriptionPayload)> + private let invitePayloadStore: CodableStore public let socketConnectionStatusPublisher: AnyPublisher @@ -47,7 +48,7 @@ public class ChatClient { kms: KeyManagementService, threadStore: Database, messagesStore: Database, - invitePayloadStore: CodableStore<(RequestSubscriptionPayload)>, + invitePayloadStore: CodableStore, socketConnectionStatusPublisher: AnyPublisher ) { self.registry = registry @@ -121,7 +122,7 @@ public class ChatClient { public func getInvites(account: Account) -> [Invite] { var invites = [Invite]() invitePayloadStore.getAll().forEach { - guard case .invite(let invite) = $0.request.params else {return} + guard let invite = try? $0.request.params?.get(Invite.self) else {return} invites.append(invite) } return invites diff --git a/Sources/Chat/ChatClientFactory.swift b/Sources/Chat/ChatClientFactory.swift index 89f4907b1..47ae2dcdd 100644 --- a/Sources/Chat/ChatClientFactory.swift +++ b/Sources/Chat/ChatClientFactory.swift @@ -2,6 +2,7 @@ import Foundation import WalletConnectRelay import WalletConnectUtils import WalletConnectKMS +import WalletConnectNetworking public struct ChatClientFactory { @@ -10,17 +11,18 @@ public struct ChatClientFactory { relayClient: RelayClient, kms: KeyManagementService, logger: ConsoleLogging, - keyValueStorage: KeyValueStorage) -> ChatClient { + keyValueStorage: KeyValueStorage + ) -> ChatClient { let topicToRegistryRecordStore = CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.topicToInvitationPubKey.rawValue) let serialiser = Serializer(kms: kms) - let jsonRpcHistory = JsonRpcHistory(logger: logger, keyValueStore: CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.jsonRpcHistory.rawValue)) - let networkingInteractor = NetworkingInteractor(relayClient: relayClient, serializer: serialiser, logger: logger, jsonRpcHistory: jsonRpcHistory) + let rpcHistory = RPCHistory(keyValueStore: CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.jsonRpcHistory.rawValue)) + let networkingInteractor = NetworkingInteractor(relayClient: relayClient, serializer: serialiser, logger: logger, rpcHistory: rpcHistory) let invitePayloadStore = CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.invite.rawValue) let registryService = RegistryService(registry: registry, networkingInteractor: networkingInteractor, kms: kms, logger: logger, topicToRegistryRecordStore: topicToRegistryRecordStore) let threadStore = Database(keyValueStorage: keyValueStorage, identifier: StorageDomainIdentifiers.threads.rawValue) let resubscriptionService = ResubscriptionService(networkingInteractor: networkingInteractor, threadStore: threadStore, logger: logger) let invitationHandlingService = InvitationHandlingService(registry: registry, networkingInteractor: networkingInteractor, kms: kms, logger: logger, topicToRegistryRecordStore: topicToRegistryRecordStore, invitePayloadStore: invitePayloadStore, threadsStore: threadStore) - let inviteService = InviteService(networkingInteractor: networkingInteractor, kms: kms, threadStore: threadStore, logger: logger) + let inviteService = InviteService(networkingInteractor: networkingInteractor, kms: kms, threadStore: threadStore, rpcHistory: rpcHistory, logger: logger) let leaveService = LeaveService() let messagesStore = Database(keyValueStorage: keyValueStorage, identifier: StorageDomainIdentifiers.messages.rawValue) let messagingService = MessagingService(networkingInteractor: networkingInteractor, messagesStore: messagesStore, threadStore: threadStore, logger: logger) diff --git a/Sources/Chat/NetworkingInteractor.swift b/Sources/Chat/NetworkingInteractor.swift deleted file mode 100644 index ee31ec033..000000000 --- a/Sources/Chat/NetworkingInteractor.swift +++ /dev/null @@ -1,122 +0,0 @@ -import Foundation -import Combine -import WalletConnectRelay -import WalletConnectUtils -import WalletConnectKMS - -protocol NetworkInteracting { - var socketConnectionStatusPublisher: AnyPublisher { get } - var requestPublisher: AnyPublisher {get} - var responsePublisher: AnyPublisher {get} - func respondSuccess(payload: RequestSubscriptionPayload) async throws - func subscribe(topic: String) async throws - func request(_ request: JSONRPCRequest, topic: String, envelopeType: Envelope.EnvelopeType) async throws - func respond(topic: String, response: JsonRpcResult, tag: Int) async throws -} - -extension NetworkInteracting { - func request(_ request: JSONRPCRequest, topic: String, envelopeType: Envelope.EnvelopeType = .type0) async throws { - try await self.request(request, topic: topic, envelopeType: envelopeType) - } -} - -class NetworkingInteractor: NetworkInteracting { - enum Error: Swift.Error { - case failedToInitialiseMethodFromRecord - } - private let jsonRpcHistory: JsonRpcHistory - private let serializer: Serializing - private let relayClient: RelayClient - private let logger: ConsoleLogging - var requestPublisher: AnyPublisher { - requestPublisherSubject.eraseToAnyPublisher() - } - private let requestPublisherSubject = PassthroughSubject() - - var responsePublisher: AnyPublisher { - responsePublisherSubject.eraseToAnyPublisher() - } - private let responsePublisherSubject = PassthroughSubject() - var socketConnectionStatusPublisher: AnyPublisher - - private var publishers = Set() - - init(relayClient: RelayClient, - serializer: Serializing, - logger: ConsoleLogging, - jsonRpcHistory: JsonRpcHistory - ) { - self.relayClient = relayClient - self.serializer = serializer - self.jsonRpcHistory = jsonRpcHistory - self.logger = logger - self.socketConnectionStatusPublisher = relayClient.socketConnectionStatusPublisher - - relayClient.messagePublisher.sink { [unowned self] (topic, message) in - manageSubscription(topic, message) - }.store(in: &publishers) - } - - func request(_ request: JSONRPCRequest, topic: String, envelopeType: Envelope.EnvelopeType) async throws { - try jsonRpcHistory.set(topic: topic, request: request) - let message = try! serializer.serialize(topic: topic, encodable: request, envelopeType: envelopeType) - try await relayClient.publish(topic: topic, payload: message, tag: request.params.tag) - } - - func respondSuccess(payload: RequestSubscriptionPayload) async throws { - let response = JSONRPCResponse(id: payload.request.id, result: AnyCodable(true)) - try await respond(topic: payload.topic, response: JsonRpcResult.response(response), tag: payload.request.params.responseTag) - } - - func respond(topic: String, response: JsonRpcResult, tag: Int) async throws { - _ = try jsonRpcHistory.resolve(response: response) - let message = try serializer.serialize(topic: topic, encodable: response.value) - try await relayClient.publish(topic: topic, payload: message, tag: tag, prompt: false) - } - - func subscribe(topic: String) async throws { - try await relayClient.subscribe(topic: topic) - } - - private func manageSubscription(_ topic: String, _ encodedEnvelope: String) { - if let deserializedJsonRpcRequest: JSONRPCRequest = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { - handleChatRequest(topic: topic, request: deserializedJsonRpcRequest) - } else if let deserializedJsonRpcResponse: JSONRPCResponse = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { - handleJsonRpcResponse(response: deserializedJsonRpcResponse) - } else if let deserializedJsonRpcError: JSONRPCErrorResponse = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { - handleJsonRpcErrorResponse(response: deserializedJsonRpcError) - } else { - logger.warn("Networking Interactor - Received unknown object type from networking relay") - } - } - - private func handleChatRequest(topic: String, request: JSONRPCRequest) { - do { - try jsonRpcHistory.set(topic: topic, request: request) - let payload = RequestSubscriptionPayload(topic: topic, request: request) - requestPublisherSubject.send(payload) - } catch { - logger.debug(error) - } - } - - private func handleJsonRpcResponse(response: JSONRPCResponse) { - do { - let record = try jsonRpcHistory.resolve(response: JsonRpcResult.response(response)) - let params = try record.request.params.get(ChatRequestParams.self) - let chatResponse = ChatResponse( - topic: record.topic, - requestMethod: record.request.method, - requestParams: params, - result: JsonRpcResult.response(response)) - responsePublisherSubject.send(chatResponse) - } catch { - logger.debug("Handle json rpc response error: \(error)") - } - } - - private func handleJsonRpcErrorResponse(response: JSONRPCErrorResponse) { - // todo - } - -} diff --git a/Sources/Chat/ProtocolServices/Common/File.swift b/Sources/Chat/ProtocolServices/Common/File.swift index 8bd04f569..e821f03d7 100644 --- a/Sources/Chat/ProtocolServices/Common/File.swift +++ b/Sources/Chat/ProtocolServices/Common/File.swift @@ -1,6 +1,8 @@ import Foundation -import WalletConnectUtils import Combine +import WalletConnectRelay +import WalletConnectUtils +import WalletConnectNetworking class ResubscriptionService { private let networkingInteractor: NetworkInteracting diff --git a/Sources/Chat/ProtocolServices/Common/MessagingService.swift b/Sources/Chat/ProtocolServices/Common/MessagingService.swift index ca774136d..eaeaf061e 100644 --- a/Sources/Chat/ProtocolServices/Common/MessagingService.swift +++ b/Sources/Chat/ProtocolServices/Common/MessagingService.swift @@ -1,6 +1,8 @@ import Foundation -import WalletConnectUtils import Combine +import JSONRPC +import WalletConnectUtils +import WalletConnectNetworking class MessagingService { enum Errors: Error { @@ -31,8 +33,8 @@ class MessagingService { guard let authorAccount = thread?.selfAccount else { throw Errors.threadDoNotExist} let timestamp = Int64(Date().timeIntervalSince1970 * 1000) let message = Message(topic: topic, message: messageString, authorAccount: authorAccount, timestamp: timestamp) - let request = JSONRPCRequest(params: .message(message)) - try await networkingInteractor.request(request, topic: topic, envelopeType: .type0) + let request = RPCRequest(method: Message.method, params: message) + try await networkingInteractor.request(request, topic: topic, tag: Message.tag) Task(priority: .background) { await messagesStore.add(message) onMessage?(message) @@ -41,38 +43,34 @@ class MessagingService { private func setUpResponseHandling() { networkingInteractor.responsePublisher - .sink { [unowned self] response in - switch response.requestParams { - case .message: - handleMessageResponse(response) - default: - return - } + .sink { [unowned self] payload in + logger.debug("Received Message response") }.store(in: &publishers) } private func setUpRequestHandling() { - networkingInteractor.requestPublisher.sink { [unowned self] subscriptionPayload in - switch subscriptionPayload.request.params { - case .message(var message): - message.topic = subscriptionPayload.topic - handleMessage(message, subscriptionPayload) - default: - return + networkingInteractor.requestPublisher.sink { [unowned self] payload in + do { + guard + let requestId = payload.request.id, payload.request.method == Message.method, + var message = try payload.request.params?.get(Message.self) + else { return } + + message.topic = payload.topic + + handleMessage(message, topic: payload.topic, requestId: requestId) + } catch { + logger.debug("Handling message response has failed") } }.store(in: &publishers) } - private func handleMessage(_ message: Message, _ payload: RequestSubscriptionPayload) { + private func handleMessage(_ message: Message, topic: String, requestId: RPCID) { Task(priority: .background) { - try await networkingInteractor.respondSuccess(payload: payload) + try await networkingInteractor.respondSuccess(topic: topic, requestId: requestId, tag: Message.tag) await messagesStore.add(message) logger.debug("Received message") onMessage?(message) } } - - private func handleMessageResponse(_ response: ChatResponse) { - logger.debug("Received Message response") - } } diff --git a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift index 38021014b..102e9f00a 100644 --- a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift +++ b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift @@ -1,8 +1,10 @@ import Foundation +import Combine +import JSONRPC import WalletConnectKMS import WalletConnectUtils import WalletConnectRelay -import Combine +import WalletConnectNetworking class InvitationHandlingService { enum Error: Swift.Error { @@ -11,7 +13,7 @@ class InvitationHandlingService { var onInvite: ((Invite) -> Void)? var onNewThread: ((Thread) -> Void)? private let networkingInteractor: NetworkInteracting - private let invitePayloadStore: CodableStore<(RequestSubscriptionPayload)> + private let invitePayloadStore: CodableStore private let topicToRegistryRecordStore: CodableStore private let registry: Registry private let logger: ConsoleLogging @@ -37,27 +39,22 @@ class InvitationHandlingService { } func accept(inviteId: String) async throws { - guard let payload = try invitePayloadStore.get(key: inviteId) else { throw Error.inviteForIdNotFound } let selfThreadPubKey = try kms.createX25519KeyPair() let inviteResponse = InviteResponse(publicKey: selfThreadPubKey.hexRepresentation) - let response = JsonRpcResult.response(JSONRPCResponse(id: payload.request.id, result: AnyCodable(inviteResponse))) - - guard case .invite(let invite) = payload.request.params else {return} + guard let requestId = payload.request.id, let invite = try? payload.request.params?.get(Invite.self) + else { return } - let responseTopic = try getInviteResponseTopic(payload, invite) - - try await networkingInteractor.respond(topic: responseTopic, response: response, tag: payload.request.params.responseTag) + let response = RPCResponse(id: requestId, result: inviteResponse) + let responseTopic = try getInviteResponseTopic(requestTopic: payload.topic, invite: invite) + try await networkingInteractor.respond(topic: responseTopic, response: response, tag: Invite.tag) let threadAgreementKeys = try kms.performKeyAgreement(selfPublicKey: selfThreadPubKey, peerPublicKey: invite.publicKey) - let threadTopic = threadAgreementKeys.derivedTopic() - try kms.setSymmetricKey(threadAgreementKeys.sharedKey, for: threadTopic) - try await networkingInteractor.subscribe(topic: threadTopic) logger.debug("Accepting an invite on topic: \(threadTopic)") @@ -73,47 +70,36 @@ class InvitationHandlingService { } func reject(inviteId: String) async throws { - guard let payload = try invitePayloadStore.get(key: inviteId) else { throw Error.inviteForIdNotFound } - guard case .invite(let invite) = payload.request.params else {return} + guard let requestId = payload.request.id, let invite = try? payload.request.params?.get(Invite.self) + else { return } - let responseTopic = try getInviteResponseTopic(payload, invite) + let responseTopic = try getInviteResponseTopic(requestTopic: payload.topic, invite: invite) - // TODO - error not in specs yet - let error = JSONRPCErrorResponse.Error(code: 0, message: "user rejected") - let response = JsonRpcResult.error(JSONRPCErrorResponse(id: payload.request.id, error: error)) - - try await networkingInteractor.respond(topic: responseTopic, response: response, tag: payload.request.params.responseTag) + try await networkingInteractor.respondError(topic: responseTopic, requestId: requestId, tag: Invite.tag, reason: ChatError.userRejected) invitePayloadStore.delete(forKey: inviteId) } private func setUpRequestHandling() { - networkingInteractor.requestPublisher.sink { [unowned self] subscriptionPayload in - switch subscriptionPayload.request.params { - case .invite(let invite): - do { - try handleInvite(invite, subscriptionPayload) - } catch { - logger.debug("Did not handle invite, error: \(error)") - } - default: - return - } - }.store(in: &publishers) - } + networkingInteractor.requestPublisher.sink { [unowned self] payload in + guard payload.request.method == "wc_chatInvite" + else { return } + + guard let invite = try? payload.request.params?.get(Invite.self) + else { return } - private func handleInvite(_ invite: Invite, _ payload: RequestSubscriptionPayload) throws { - logger.debug("did receive an invite") - invitePayloadStore.set(payload, forKey: invite.publicKey) - onInvite?(invite) + logger.debug("did receive an invite") + invitePayloadStore.set(payload, forKey: invite.publicKey) + onInvite?(invite) + }.store(in: &publishers) } - private func getInviteResponseTopic(_ payload: RequestSubscriptionPayload, _ invite: Invite) throws -> String { + private func getInviteResponseTopic(requestTopic: String, invite: Invite) throws -> String { // todo - remove topicToInvitationPubKeyStore ? - guard let record = try? topicToRegistryRecordStore.get(key: payload.topic) else { + guard let record = try? topicToRegistryRecordStore.get(key: requestTopic) else { logger.debug("PubKey for invitation topic not found") fatalError("todo") } diff --git a/Sources/Chat/ProtocolServices/Invitee/RegistryService.swift b/Sources/Chat/ProtocolServices/Invitee/RegistryService.swift index 33a5241a8..4476f306e 100644 --- a/Sources/Chat/ProtocolServices/Invitee/RegistryService.swift +++ b/Sources/Chat/ProtocolServices/Invitee/RegistryService.swift @@ -1,6 +1,7 @@ import Foundation import WalletConnectUtils import WalletConnectKMS +import WalletConnectNetworking actor RegistryService { let networkingInteractor: NetworkInteracting diff --git a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift index f3a5d32b9..d3051cf2d 100644 --- a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift +++ b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift @@ -1,7 +1,9 @@ import Foundation +import Combine +import JSONRPC import WalletConnectKMS import WalletConnectUtils -import Combine +import WalletConnectNetworking class InviteService { private var publishers = [AnyCancellable]() @@ -9,6 +11,7 @@ class InviteService { private let logger: ConsoleLogging private let kms: KeyManagementService private let threadStore: Database + private let rpcHistory: RPCHistory var onNewThread: ((Thread) -> Void)? var onInvite: ((Invite) -> Void)? @@ -16,11 +19,13 @@ class InviteService { init(networkingInteractor: NetworkInteracting, kms: KeyManagementService, threadStore: Database, + rpcHistory: RPCHistory, logger: ConsoleLogging) { self.kms = kms self.networkingInteractor = networkingInteractor self.logger = logger self.threadStore = threadStore + self.rpcHistory = rpcHistory setUpResponseHandling() } @@ -37,7 +42,7 @@ class InviteService { // overrides on invite toipic try kms.setSymmetricKey(symKeyI.sharedKey, for: inviteTopic) - let request = JSONRPCRequest(params: .invite(invite)) + let request = RPCRequest(method: Invite.method, params: invite) // 2. Proposer subscribes to topic R which is the hash of the derived symKey let responseTopic = symKeyI.derivedTopic() @@ -45,40 +50,35 @@ class InviteService { try kms.setSymmetricKey(symKeyI.sharedKey, for: responseTopic) try await networkingInteractor.subscribe(topic: responseTopic) - try await networkingInteractor.request(request, topic: inviteTopic, envelopeType: .type1(pubKey: selfPubKeyY.rawRepresentation)) + try await networkingInteractor.request(request, topic: inviteTopic, tag: Invite.tag, envelopeType: .type1(pubKey: selfPubKeyY.rawRepresentation)) logger.debug("invite sent on topic: \(inviteTopic)") } private func setUpResponseHandling() { networkingInteractor.responsePublisher - .sink { [unowned self] response in - switch response.requestParams { - case .invite: - handleInviteResponse(response) - default: - return - } - }.store(in: &publishers) - } + .sink { [unowned self] payload in + do { + guard + let requestId = payload.response.id, + let request = rpcHistory.get(recordId: requestId)?.request, + let requestParams = request.params, request.method == Invite.method + else { return } - private func handleInviteResponse(_ response: ChatResponse) { - switch response.result { - case .response(let jsonrpc): - do { - let inviteResponse = try jsonrpc.result.get(InviteResponse.self) - logger.debug("Invite has been accepted") - guard case .invite(let inviteParams) = response.requestParams else { return } - Task(priority: .background) { - try await createThread(selfPubKeyHex: inviteParams.publicKey, peerPubKey: inviteResponse.publicKey, account: inviteParams.account, peerAccount: peerAccount) + guard let inviteResponse = try payload.response.result?.get(InviteResponse.self) + else { return } + + let inviteParams = try requestParams.get(Invite.self) + + logger.debug("Invite has been accepted") + + Task(priority: .background) { + try await createThread(selfPubKeyHex: inviteParams.publicKey, peerPubKey: inviteResponse.publicKey, account: inviteParams.account, peerAccount: peerAccount) + } + } catch { + logger.debug("Handling invite response has failed") } - } catch { - logger.debug("Handling invite response has failed") - } - case .error: - logger.debug("Invite has been rejected") - // TODO - remove keys, clean storage - } + }.store(in: &publishers) } private func createThread(selfPubKeyHex: String, peerPubKey: String, account: Account, peerAccount: Account) async throws { diff --git a/Sources/Chat/Types/ChatError.swift b/Sources/Chat/Types/ChatError.swift index 0672ba16f..edac89e77 100644 --- a/Sources/Chat/Types/ChatError.swift +++ b/Sources/Chat/Types/ChatError.swift @@ -1,6 +1,21 @@ import Foundation +import WalletConnectNetworking enum ChatError: Error { case noInviteForId case recordNotFound + case userRejected +} + +extension ChatError: Reason { + + var code: Int { + // Errors not in specs yet + return 0 + } + + var message: String { + // Errors not in specs yet + return localizedDescription + } } diff --git a/Sources/Chat/Types/ChatRequestParams.swift b/Sources/Chat/Types/ChatRequestParams.swift deleted file mode 100644 index 51b9e512d..000000000 --- a/Sources/Chat/Types/ChatRequestParams.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Foundation -import WalletConnectUtils - -enum ChatRequestParams: Codable, Equatable { - enum Errors: Error { - case decoding - } - case invite(Invite) - case message(Message) - - private enum CodingKeys: String, CodingKey { - case invite - case message - } - - func encode(to encoder: Encoder) throws { - switch self { - case .invite(let invite): - try invite.encode(to: encoder) - case .message(let message): - try message.encode(to: encoder) - } - } - - init(from decoder: Decoder) throws { - if let invite = try? Invite(from: decoder) { - self = .invite(invite) - } else if let massage = try? Message(from: decoder) { - self = .message(massage) - } else { - throw Errors.decoding - } - } -} - -extension ChatRequestParams { - - var tag: Int { - switch self { - case .invite: - return 2000 - case .message: - return 2002 - } - } - - var responseTag: Int { - return tag + 1 - } -} - -extension JSONRPCRequest { - init(id: Int64 = JsonRpcID.generate(), params: T) where T == ChatRequestParams { - var method: String! - switch params { - case .invite: - method = "wc_chatInvite" - case .message: - method = "wc_chatMessage" - } - self.init(id: id, method: method, params: params) - } -} diff --git a/Sources/Chat/Types/ChatResponse.swift b/Sources/Chat/Types/ChatResponse.swift deleted file mode 100644 index 448abb7e8..000000000 --- a/Sources/Chat/Types/ChatResponse.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation -import WalletConnectUtils - -struct ChatResponse: Codable { - let topic: String - let requestMethod: String - let requestParams: ChatRequestParams - let result: JsonRpcResult -} diff --git a/Sources/Chat/Types/InviteParams.swift b/Sources/Chat/Types/Invite.swift similarity index 71% rename from Sources/Chat/Types/InviteParams.swift rename to Sources/Chat/Types/Invite.swift index f5b0f7180..5f69e72e8 100644 --- a/Sources/Chat/Types/InviteParams.swift +++ b/Sources/Chat/Types/Invite.swift @@ -12,4 +12,12 @@ public struct Invite: Codable, Equatable { public let message: String public let account: Account public let publicKey: String + + static var tag: Int { + return 2000 + } + + static var method: String { + return "wc_chatInvite" + } } diff --git a/Sources/Chat/Types/Message.swift b/Sources/Chat/Types/Message.swift index 7325d8a21..09113ed2a 100644 --- a/Sources/Chat/Types/Message.swift +++ b/Sources/Chat/Types/Message.swift @@ -2,13 +2,6 @@ import Foundation import WalletConnectUtils public struct Message: Codable, Equatable { - internal init(topic: String? = nil, message: String, authorAccount: Account, timestamp: Int64) { - self.topic = topic - self.message = message - self.authorAccount = authorAccount - self.timestamp = timestamp - } - public var topic: String? public let message: String public let authorAccount: Account @@ -20,4 +13,19 @@ public struct Message: Codable, Equatable { case authorAccount case timestamp } + + static var tag: Int { + return 2002 + } + + static var method: String { + return "wc_chatMessage" + } + + init(topic: String? = nil, message: String, authorAccount: Account, timestamp: Int64) { + self.topic = topic + self.message = message + self.authorAccount = authorAccount + self.timestamp = timestamp + } } diff --git a/Sources/Chat/Types/RequestSubscriptionPayload.swift b/Sources/Chat/Types/RequestSubscriptionPayload.swift deleted file mode 100644 index 575af574a..000000000 --- a/Sources/Chat/Types/RequestSubscriptionPayload.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation -import WalletConnectUtils - -struct RequestSubscriptionPayload: Codable { - let topic: String - let request: JSONRPCRequest -} diff --git a/Sources/WalletConnectNetworking/NetworkInteractor.swift b/Sources/WalletConnectNetworking/NetworkInteractor.swift index 668448236..fca33ed16 100644 --- a/Sources/WalletConnectNetworking/NetworkInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkInteractor.swift @@ -2,20 +2,35 @@ import Foundation import Combine import JSONRPC import WalletConnectKMS +import WalletConnectRelay public protocol NetworkInteracting { - var requestPublisher: AnyPublisher {get} - var responsePublisher: AnyPublisher {get} + var socketConnectionStatusPublisher: AnyPublisher { get } + var requestPublisher: AnyPublisher { get } + var responsePublisher: AnyPublisher { get } func subscribe(topic: String) async throws func unsubscribe(topic: String) func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws + func respondSuccess(topic: String, requestId: RPCID, tag: Int, envelopeType: Envelope.EnvelopeType) async throws func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws } extension NetworkInteracting { - public func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType = .type0) async throws { - try await self.request(request, topic: topic, tag: tag, envelopeType: envelopeType) + public func request(_ request: RPCRequest, topic: String, tag: Int) async throws { + try await self.request(request, topic: topic, tag: tag, envelopeType: .type0) + } + + public func respond(topic: String, response: RPCResponse, tag: Int) async throws { + try await self.respond(topic: topic, response: response, tag: tag, envelopeType: .type0) + } + + public func respondSuccess(topic: String, requestId: RPCID, tag: Int) async throws { + try await self.respondSuccess(topic: topic, requestId: requestId, tag: tag, envelopeType: .type0) + } + + public func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason) async throws { + try await self.respondError(topic: topic, requestId: requestId, tag: tag, reason: reason, envelopeType: .type0) } } diff --git a/Sources/WalletConnectNetworking/NetworkingInteractor.swift b/Sources/WalletConnectNetworking/NetworkingInteractor.swift index 91a664578..cf4f99e46 100644 --- a/Sources/WalletConnectNetworking/NetworkingInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkingInteractor.swift @@ -27,9 +27,9 @@ public class NetworkingInteractor: NetworkInteracting { public init( relayClient: RelayClient, - serializer: Serializing, - logger: ConsoleLogging, - rpcHistory: RPCHistory + serializer: Serializing, + logger: ConsoleLogging, + rpcHistory: RPCHistory ) { self.relayClient = relayClient self.serializer = serializer @@ -89,11 +89,15 @@ public class NetworkingInteractor: NetworkInteracting { try await relayClient.publish(topic: topic, payload: message, tag: tag) } + public func respondSuccess(topic: String, requestId: RPCID, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + let response = RPCResponse(id: requestId, result: true) + try await respond(topic: topic, response: response, tag: tag, envelopeType: envelopeType) + } + public func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { let error = JSONRPCError(code: reason.code, message: reason.message) let response = RPCResponse(id: requestId, error: error) - let message = try! serializer.serialize(topic: topic, encodable: response, envelopeType: envelopeType) - try await relayClient.publish(topic: topic, payload: message, tag: tag) + try await respond(topic: topic, response: response, tag: tag, envelopeType: envelopeType) } private func manageSubscription(_ topic: String, _ encodedEnvelope: String) { diff --git a/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift b/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift deleted file mode 100644 index 5c9664d03..000000000 --- a/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift +++ /dev/null @@ -1,44 +0,0 @@ -import Foundation -import Combine -@testable import Auth -import JSONRPC -import WalletConnectKMS -import WalletConnectNetworking - -struct NetworkingInteractorMock: NetworkInteracting { - - var responsePublisher: AnyPublisher { - responsePublisherSubject.eraseToAnyPublisher() - } - let responsePublisherSubject = PassthroughSubject() - - let requestPublisherSubject = PassthroughSubject() - var requestPublisher: AnyPublisher { - requestPublisherSubject.eraseToAnyPublisher() - } - - func subscribe(topic: String) async throws { - - } - - func unsubscribe(topic: String) { - - } - - func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { - - } - - func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { - - } - - func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { - - } - - func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws { - - } - -} diff --git a/Tests/ChatTests/Mocks/NetworkingInteractorMock.swift b/Tests/ChatTests/Mocks/NetworkingInteractorMock.swift deleted file mode 100644 index 030c9d1d9..000000000 --- a/Tests/ChatTests/Mocks/NetworkingInteractorMock.swift +++ /dev/null @@ -1,50 +0,0 @@ -import Foundation -@testable import Chat -import Combine -import WalletConnectUtils -import WalletConnectRelay - -class NetworkingInteractorMock: NetworkInteracting { - - var socketConnectionStatusPublisher: AnyPublisher { - socketConnectionStatusPublisherSubject.eraseToAnyPublisher() - } - let socketConnectionStatusPublisherSubject = PassthroughSubject() - - let responsePublisherSubject = PassthroughSubject() - let requestPublisherSubject = PassthroughSubject() - - var requestPublisher: AnyPublisher { - requestPublisherSubject.eraseToAnyPublisher() - } - - var responsePublisher: AnyPublisher { - responsePublisherSubject.eraseToAnyPublisher() - } - - func requestUnencrypted(_ request: JSONRPCRequest, topic: String) async throws { - - } - - func request(_ request: JSONRPCRequest, topic: String) async throws { - - } - - func respond(topic: String, response: JsonRpcResult, tag: Int) async throws { - - } - - func respondSuccess(payload: RequestSubscriptionPayload) async throws { - - } - - private(set) var subscriptions: [String] = [] - - func subscribe(topic: String) async throws { - subscriptions.append(topic) - } - - func didSubscribe(to topic: String) -> Bool { - subscriptions.contains { $0 == topic } - } -} diff --git a/Tests/ChatTests/RegistryManagerTests.swift b/Tests/ChatTests/RegistryManagerTests.swift index b0ea58338..c9ff87848 100644 --- a/Tests/ChatTests/RegistryManagerTests.swift +++ b/Tests/ChatTests/RegistryManagerTests.swift @@ -2,7 +2,8 @@ import Foundation import XCTest @testable import Chat import WalletConnectUtils -@testable import WalletConnectKMS +import WalletConnectNetworking +import WalletConnectKMS @testable import TestingUtils final class RegistryManagerTests: XCTestCase { @@ -27,7 +28,7 @@ final class RegistryManagerTests: XCTestCase { func testRegister() async { let account = Account("eip155:1:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb")! - try! await registryManager.register(account: account) + _ = try! await registryManager.register(account: account) XCTAssert(!networkingInteractor.subscriptions.isEmpty, "networkingInteractors subscribes to new topic") let resolved = try! await registry.resolve(account: account) XCTAssertNotNil(resolved, "register account is resolvable") diff --git a/Tests/TestingUtils/NetworkingInteractorMock.swift b/Tests/TestingUtils/NetworkingInteractorMock.swift new file mode 100644 index 000000000..70abbc425 --- /dev/null +++ b/Tests/TestingUtils/NetworkingInteractorMock.swift @@ -0,0 +1,58 @@ +import Foundation +import Combine +import JSONRPC +import WalletConnectRelay +import WalletConnectKMS +import WalletConnectNetworking + +public class NetworkingInteractorMock: NetworkInteracting { + + private(set) var subscriptions: [String] = [] + + public let socketConnectionStatusPublisherSubject = PassthroughSubject() + public var socketConnectionStatusPublisher: AnyPublisher { + socketConnectionStatusPublisherSubject.eraseToAnyPublisher() + } + + public var responsePublisher: AnyPublisher { + responsePublisherSubject.eraseToAnyPublisher() + } + public let responsePublisherSubject = PassthroughSubject() + + public let requestPublisherSubject = PassthroughSubject() + public var requestPublisher: AnyPublisher { + requestPublisherSubject.eraseToAnyPublisher() + } + + public func subscribe(topic: String) async throws { + subscriptions.append(topic) + } + + func didSubscribe(to topic: String) -> Bool { + subscriptions.contains { $0 == topic } + } + + public func unsubscribe(topic: String) { + + } + + public func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + + } + + public func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + + } + + public func respondSuccess(topic: String, requestId: RPCID, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + + } + + public func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { + + } + + public func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws { + + } +} From 429b32e478a570518738df422c6a6d3914248c0d Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 1 Sep 2022 22:26:55 +0300 Subject: [PATCH 29/92] Showcase build errors --- .../Showcase/Classes/ApplicationLayer/SceneDelegate.swift | 2 +- .../PresentationLayer/Wallet/Wallet/WalletInteractor.swift | 2 +- .../PresentationLayer/Wallet/Wallet/WalletPresenter.swift | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift b/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift index 43335bd7d..5956c494e 100644 --- a/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift +++ b/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift @@ -30,7 +30,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let uri = context.url.absoluteString.replacingOccurrences(of: "showcase://wc?uri=", with: "") Task { - try await Auth.instance.pair(uri: uri) + try await Auth.instance.pair(uri: WalletConnectURI(string: uri)!) } } } diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletInteractor.swift index ac53b05f3..051ba266e 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletInteractor.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletInteractor.swift @@ -3,7 +3,7 @@ import Auth final class WalletInteractor { - func pair(uri: String) async throws { + func pair(uri: WalletConnectURI) async throws { try await Auth.instance.pair(uri: uri) } diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift index 2f631ad65..47a773591 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift @@ -15,13 +15,14 @@ final class WalletPresenter: ObservableObject { } func didPastePairingURI() { - guard let uri = UIPasteboard.general.string else { return } + guard let string = UIPasteboard.general.string, let uri = WalletConnectURI(string: string) else { return } pair(uri: uri) } func didScanPairingURI() { router.presentScan { [unowned self] value in - self.pair(uri: value) + guard let uri = WalletConnectURI(string: value) else { return } + self.pair(uri: uri) self.router.dismiss() } onError: { error in print(error.localizedDescription) @@ -53,7 +54,7 @@ private extension WalletPresenter { }.store(in: &disposeBag) } - func pair(uri: String) { + func pair(uri: WalletConnectURI) { Task(priority: .high) { [unowned self] in try await self.interactor.pair(uri: uri) } From eaccdad46775f9aa2ee16d2eb600c06461070232 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Fri, 2 Sep 2022 10:25:42 +0200 Subject: [PATCH 30/92] savepoint --- Sources/Auth/Services/Common/DisconnectPairService.swift | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 Sources/Auth/Services/Common/DisconnectPairService.swift diff --git a/Sources/Auth/Services/Common/DisconnectPairService.swift b/Sources/Auth/Services/Common/DisconnectPairService.swift new file mode 100644 index 000000000..881fa1e3b --- /dev/null +++ b/Sources/Auth/Services/Common/DisconnectPairService.swift @@ -0,0 +1,8 @@ +// + +import Foundation + +class DisconnectPairService { + + +} From a54aafd863c2fc95da03f1874a93db89b6e066cb Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Fri, 2 Sep 2022 10:55:52 +0200 Subject: [PATCH 31/92] Add disconnect method --- .../Common/DisconnectPairService.swift | 25 ++++++++++++++++++- Sources/Auth/Types/Errors/AuthError.swift | 5 ++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Sources/Auth/Services/Common/DisconnectPairService.swift b/Sources/Auth/Services/Common/DisconnectPairService.swift index 881fa1e3b..b808caaef 100644 --- a/Sources/Auth/Services/Common/DisconnectPairService.swift +++ b/Sources/Auth/Services/Common/DisconnectPairService.swift @@ -1,8 +1,31 @@ -// import Foundation +import WalletConnectNetworking +import WalletConnectKMS +import WalletConnectUtils +import WalletConnectPairing class DisconnectPairService { + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + private let pairingStorage: WCPairingStorage + private let logger: ConsoleLogging + init(networkingInteractor: NetworkInteracting, + kms: KeyManagementServiceProtocol, + pairingStorage: WCPairingStorage, + logger: ConsoleLogging) { + self.networkingInteractor = networkingInteractor + self.kms = kms + self.pairingStorage = pairingStorage + } + func disconnect(topic: String) async throws { + let reason = AuthError.userDisconnected + logger.debug("Will delete pairing for reason: message: \(reason.message) code: \(reason.code)") + try await networkingInteractor.request(<#T##RPCRequest#>, topic: <#T##String#>, tag: <#T##Int#>, envelopeType: <#T##Envelope.EnvelopeType#>) + pairingStorage.delete(topic: topic) + kms.deleteSymmetricKey(for: topic) + networkingInteractor.unsubscribe(topic: topic) + } } diff --git a/Sources/Auth/Types/Errors/AuthError.swift b/Sources/Auth/Types/Errors/AuthError.swift index f28cab817..a8191beff 100644 --- a/Sources/Auth/Types/Errors/AuthError.swift +++ b/Sources/Auth/Types/Errors/AuthError.swift @@ -3,6 +3,7 @@ import WalletConnectNetworking /// Authentication error public enum AuthError: Codable, Equatable, Error { + case userDisconnected case userRejeted case malformedResponseParams case malformedRequestParams @@ -31,6 +32,8 @@ extension AuthError: Reason { public var code: Int { switch self { + case .userDisconnected: + return 6000 case .userRejeted: return 14001 case .malformedResponseParams: @@ -56,6 +59,8 @@ extension AuthError: Reason { return "Original message compromised" case .signatureVerificationFailed: return "Message verification failed" + case .userDisconnected: + return "User Disconnected" } } } From 320ce8523dff70b742daf268d249b72c70efcdd6 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Fri, 2 Sep 2022 11:53:51 +0200 Subject: [PATCH 32/92] app app icon to showcase --- .../AppIcon-1.appiconset/Contents.json | 116 ++++++++++++++++++ .../Icon-App-20x20@1x.png | Bin 0 -> 909 bytes .../Icon-App-20x20@2x-1.png | Bin 0 -> 1706 bytes .../Icon-App-20x20@2x.png | Bin 0 -> 1706 bytes .../Icon-App-20x20@3x.png | Bin 0 -> 2303 bytes .../Icon-App-29x29@1x.png | Bin 0 -> 1247 bytes .../Icon-App-29x29@2x-1.png | Bin 0 -> 2260 bytes .../Icon-App-29x29@2x.png | Bin 0 -> 2260 bytes .../Icon-App-29x29@3x.png | Bin 0 -> 3370 bytes .../Icon-App-40x40@1x.png | Bin 0 -> 1706 bytes .../Icon-App-40x40@2x-1.png | Bin 0 -> 3211 bytes .../Icon-App-40x40@2x.png | Bin 0 -> 3211 bytes .../Icon-App-40x40@3x.png | Bin 0 -> 4313 bytes .../Icon-App-60x60@2x.png | Bin 0 -> 4313 bytes .../Icon-App-60x60@3x.png | Bin 0 -> 6262 bytes .../Icon-App-76x76@1x.png | Bin 0 -> 2990 bytes .../Icon-App-76x76@2x.png | Bin 0 -> 5312 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 6074 bytes .../AppIcon-1.appiconset/ItunesArtwork@2x.png | Bin 0 -> 39006 bytes .../AppIcon-2.appiconset/Contents.json | 116 ++++++++++++++++++ .../Icon-App-20x20@1x.png | Bin 0 -> 909 bytes .../Icon-App-20x20@2x-1.png | Bin 0 -> 1706 bytes .../Icon-App-20x20@2x.png | Bin 0 -> 1706 bytes .../Icon-App-20x20@3x.png | Bin 0 -> 2303 bytes .../Icon-App-29x29@1x.png | Bin 0 -> 1247 bytes .../Icon-App-29x29@2x-1.png | Bin 0 -> 2260 bytes .../Icon-App-29x29@2x.png | Bin 0 -> 2260 bytes .../Icon-App-29x29@3x.png | Bin 0 -> 3370 bytes .../Icon-App-40x40@1x.png | Bin 0 -> 1706 bytes .../Icon-App-40x40@2x-1.png | Bin 0 -> 3211 bytes .../Icon-App-40x40@2x.png | Bin 0 -> 3211 bytes .../Icon-App-40x40@3x.png | Bin 0 -> 4313 bytes .../Icon-App-60x60@2x.png | Bin 0 -> 4313 bytes .../Icon-App-60x60@3x.png | Bin 0 -> 6262 bytes .../Icon-App-76x76@1x.png | Bin 0 -> 2990 bytes .../Icon-App-76x76@2x.png | Bin 0 -> 5312 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 6074 bytes .../AppIcon-2.appiconset/ItunesArtwork@2x.png | Bin 0 -> 39006 bytes Example/DApp/Sign/SignCoordinator.swift | 3 +- Example/ExampleApp.xcodeproj/project.pbxproj | 4 +- .../ApplicationLayer/SceneDelegate.swift | 2 +- .../Wallet/Wallet/WalletInteractor.swift | 2 +- .../AppIcon.appiconset/Contents.json | 18 +++ .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 909 bytes .../Icon-App-20x20@2x-1.png | Bin 0 -> 1706 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 1706 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 2303 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 1247 bytes .../Icon-App-29x29@2x-1.png | Bin 0 -> 2260 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 2260 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 3370 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 1706 bytes .../Icon-App-40x40@2x-1.png | Bin 0 -> 3211 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 3211 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 4313 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 4313 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 6262 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 2990 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 5312 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 6074 bytes .../AppIcon.appiconset/ItunesArtwork@2x.png | Bin 0 -> 39006 bytes Example/Showcase/Other/Info.plist | 2 + 62 files changed, 258 insertions(+), 5 deletions(-) create mode 100644 Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Contents.json create mode 100644 Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-20x20@1x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-20x20@2x-1.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-20x20@2x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-20x20@3x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-29x29@1x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-29x29@2x-1.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-29x29@2x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-29x29@3x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-40x40@1x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-40x40@2x-1.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-40x40@2x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-40x40@3x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-60x60@2x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-60x60@3x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-76x76@1x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-76x76@2x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-1.appiconset/ItunesArtwork@2x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Contents.json create mode 100644 Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-20x20@1x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-20x20@2x-1.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-20x20@2x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-20x20@3x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-29x29@1x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-29x29@2x-1.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-29x29@2x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-29x29@3x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-40x40@1x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-40x40@2x-1.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-40x40@2x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-40x40@3x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-60x60@2x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-60x60@3x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-76x76@1x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-76x76@2x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 Example/DApp/Assets.xcassets/AppIcon-2.appiconset/ItunesArtwork@2x.png create mode 100644 Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png create mode 100644 Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png create mode 100644 Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png create mode 100644 Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png diff --git a/Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Contents.json b/Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Contents.json new file mode 100644 index 000000000..78d34c2c3 --- /dev/null +++ b/Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Contents.json @@ -0,0 +1,116 @@ +{ + "images" : [ + { + "filename" : "Icon-App-20x20@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-20x20@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-29x29@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-29x29@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-40x40@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-40x40@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-60x60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "Icon-App-60x60@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "Icon-App-20x20@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-20x20@2x-1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-29x29@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-29x29@2x-1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-40x40@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-40x40@2x-1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-76x76@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "Icon-App-76x76@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "Icon-App-83.5x83.5@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "ItunesArtwork@2x.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-20x20@1x.png b/Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..e8b3b928c6cbe31cb3bd7f8e321c0430cfab8603 GIT binary patch literal 909 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 A0ssI2 literal 0 HcmV?d00001 diff --git a/Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-20x20@2x.png b/Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..7f3a702c374a8efe35d793c31a205cffb1b42ecf GIT binary patch literal 1706 zcmV;b237fqP)4Tx0C=2ZU|>>7EGWofVPIg$%_}Jia(7aQh>TKTf5^ZNguD!53<`H2BL+u3tZkNpBf}F%kg#cp$t|bGMq*j!GXy^Qb%A(Blj1mP$U?`<3c;+SR z=_nW(7@9LMfWjQ2`g0&SEE&blAjF#QGcefh|NsC0ZiJZEeg=k(K%HDW5n>t^3=D$Y z3=AiB79!vFuj7ybYLcQH`&9R`L2c>on5W$tWJ|6>3E1rJF?K~#90?U-ALl~ojgzrD|# zxtrrSHboD4874&vi%_EnNmDVEf*^`OdWaycL;~%GgGLu261x~a1Yt-KM0v~W7(!l) z90VO3#u+uKM(3vEoS8Z2?7eze|3Bx9GpD&6p@LZtKj+_$4CDi7n+2>HNhNUtku;0l zNobBiXIxbkNgAwMVcoT19FJSYMfHPji09~*?j7lgWIp{)zzPRHd&U|a!ImKo+u zG(0ldP?Vbr1v3oLA$!nEu$92KXN0Y%JQv!uEk6gMkyO7INI3PZ7nnE>mfdf8a=PiK z>y`jYu ze{{g=y^hmOP*$MZb*%?-EL4=jJrfM&MLM>r6MnlY)VD&?!`M7MBrRQVZ#isO7~!tl z0)Rmv2aQ255V7#(DbMlWVM?ib>_!~sOfoE+VVH5JA#zhjbslQ|6t>m~hZ}|BTqrMw zqnBWNt!Kq-i}QmYoQxh63DQWAB3fdwW}oN8WuYhvs~)yIeP2qu6NLg@P`cr-PYWB5 zcskkc`&-mULRH!ySvDk8G2#>=)E-=}RLm{8aCZne?ani(>u zuQ%1z4Xvkd`K#*Dd|xSK-5-bdTfC^4tTax6hhX)$u_H zw0FbGU7n+tgr6@vHZ6`YxwQZ3?v24P5EBlb`pR*!9mA_(9E zfX{ysHXZYfD^MM6UGU07mggTdy^AuW=C*dh%R4=1t_Tx~pt%$7E`e{Ji4cthU;$6> z*)xF$qZXc>VX+3f5?T??UJ;h>a5Tr%fb%*wVqwck&!%IZlKfzm0wwwI(J{}Klb(px zvEhkrj=}OBj#-^W_ey2Ee@%OdD`a_<=s8)&gE~pp}pDc|~UaVs` zpYVKkLJb)MH+r^|rD^tdp-P?a{vG=%{CP$!fXDr8FxR?Idmo@!aO z+tJXfaB&CS30ODJvSey1UT>C+c`%%?w@!HLpeHY)p6pD(ZF!ogqgE?POAO{$7(Q5J zF~F;PJO}C(KItLhp*R<=_h@-cIOuWk_M?_Xm6_rG5fD^)j)epD!W;WN(Eu{xG@c>> z+@eaudkbuz%fjpXJ$vdD(s?}~;oO9ScNbXZSD0QKnPkvf94a`}AiTEElVjC$#wgs9 zsfKsvTaBHxE42u`dBC&loI;AICzBrDUuc;-d2qNP0j5J9ZWPw+^H`%>{P;A(`gth} zVSuM=TwCqgUaPg(3v5_unKNlt=H{dv zx#;`o8j1|-${$`d|LTD9F{#{{snCag0yK5N9b<-fS%ndNlML5DwsVL5U2IL?7#pg{ z2p2Y@5BK4G^kg#!{s;doi~?pWqk!4UC}6hoH~csBNPfMn>Hq)$07*qoM6N<$f}02$ A0ssI2 literal 0 HcmV?d00001 diff --git a/Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-20x20@3x.png b/Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..d38db763f8541059b36d84eb9c4844a1a9ca89da GIT binary patch literal 2303 zcmV4Tx0C=2ZU|>>7EGWofVPIg$%_}Jia(7aQh>TKTf5^ZNguD!53<`H2BL+u3tZkNpBf}F%kg#cp$t|bGMq*j!GXy^Qb%A(Blj1mP$U?`<3c;+SR z=_nW(7@9LMfWjQ2`g0&SEE&blAjF#QGcefh|NsC0ZiJZEeg=k(K%HDW5n>t^3=D$Y z3=AiB79!vFuj7ybYLcQH`&9R`L2c>on5W$tWJ|6>3E2V_Y^K~#90?V5jZRMi#7KlkqD zCkc%Ntr!cn6hY|^3$ZPwPQik^6?z^XdocDI$ZgxqsyI5!4Gm~WZy?ytd z&pqdU&v|cSL`3)inf-q|@LpR5xZJG*T<%r@E_bT{m%CMf%iSu#=C+ckA@bEYsJ0l!>2L^_O!5EAt^;}&P zn(JY1lVQYoI7r>V!GHWm*x4r>Iwib40&!2%)%vN6|9K~RZFK~$ zXn@wM3~Lq|KK~K@kdxA;dP#u8k?0u^b{rOtoDw{N+Ni>WAu}8Z9gll3mV}6bC07~N zE;THhYbMN5j1>*+i8?qu4qLk&&%UO^&8XI>1Q7&Zu(F(%tn=3B`wMYBcE@#wdzV`p zYtnNSV?_Z64)hNS_wRD_4?67uK{NdH zR?C8BQ*@*Y1`Z}ZdRn-r(=i-}hH3=@KU!8(bTpx9-CPfs*F!^%dOR{NoQc6;Ow+lp zO1FS>>8M97jKra_8amo6H_Ryxx**^{NRFNo?(THNJ*bI-=fg$}B%G$@ye9bae8cj2 zhWWD$m(^vCb#4@npB1`K2!A;)oEXyWP-QiP03sH~lbQ*?xZU!pD+`A%s4N9)+&>6! zjlk72H9~=NBjfO)TKN8FEnmMrv(yCFgD%=-bvubL^$WF8cyOg=OEzqma?i9dNw)uBvIo=C7OK z;TvwUYse&~$M3Bf+H-|^cQ zg)19X)PB6%;fZCv1&IW|pO)F}Qn~<$J9PZF7iRFt?z7ZT_p{_b;h}2ciX94m;W`E3U~8J?$qRo`YHS z@YGAf!~0yS_sw25@fR*);qhLV0UO--i)bslt;g|ruS^Pj6dV!h;ZD??x9C#4u!`h`<2|%yi(B=5kv5Bxrr#<`T zTP&;Qo0P8?vne$m!9e@o5bl3o_gXgeJ27ax*6_ee%M-nh-yBf~vtftFNoB*vE=Oa{ zMCid7e0Paq-Hnz_`y8FGYZ_<6dQKail^>fL?397S(EI-_-1of07`>wj-`mC#&@vNF z4{Ng@R$~#kkbn&jTD8>6k|oJKf#9_p(y=SevI>?2>X-1O0@ z!%hnr()IMffbhdVJ4E!BE$mF((_`7Nu>`c=Y`Nn)lZsT-!n3cb@YUylPI#L38L*z7 z``K45Uzj_!?y6v5NJskMo5H<29iGc{D(rO75051@joTJxMk9FL*(Yr5a@1ClQt4b{ z#XW`Fy2kRktEU&)f`K6&=|d;AF-bV6iKLY{1)Hs)Iie99%U20N7h&AfE!5FwX>FN) zWQzh0JNn{j;Rnw-#yqHvK+-P@(^1fTmB15-8d78b{5H#H<`fQF6mS^&pKl3wJ?9ur z=;N8O$9%yx9OvuVql>Zk7<1eUU^%g|2v`&+_n^;py5jJ zqg{^Sarkh9!Y;m|cvt|W1eWylZ{Haw>z?tXXTW6~FJ!Eq0UfLFOyOhYvXXt(lHI=+ z^W3SFV`W=OzddwWD*b*LD{PnS>(ckDec=8BpaNX(Rsk+|s{og~Re;OgD!}D#72tBW Z{{c7J0-wbL?PUM}002ovPDHLkV1gH9Sxx`| literal 0 HcmV?d00001 diff --git a/Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-29x29@1x.png b/Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..941819fce212f20c523916380fea9450f0576883 GIT binary patch literal 1247 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 A0ssI2 literal 0 HcmV?d00001 diff --git a/Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-40x40@2x-1.png b/Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-40x40@2x-1.png new file mode 100644 index 0000000000000000000000000000000000000000..5c06655825fc02dee4eeb2e3e3a262f5c5f6d772 GIT binary patch literal 3211 zcma)i#709gO$*N)swTN1~wlRm|&7TQXdO5N` z-vTPrN9LS_PZ;BnY0UA4yu7@~NRL0;sdUgrP#3opohqa&7v%6xcbrdaO_b?~>4C99 zihN2KsLRTr-+-R@bsy9si)VI5;** z>$Qyi8lmk>ZM={p+A&BU06&1P0L3aSRu#PgM$G*%Kc2(3WumvG!m4q$9qhXBDAOtSepkw7jwmJ%phD+U0Prsp*iX^Z?XC8tkn zDKOS?y6EIH5EJz8kHWk&A$S?!hHQnlA?U8?@Rmpsut+`YEsI6s-TMev(AQ<9?K-c0 zZ#Dk4flmRtf#lF!o~TrH%vl}=%yG}aexzA z^ZLoZ@~ko|%0Z|C3J!qN6&;vaUR4g`Ck2tvJvi+aB}fsHJO^8i>(fF>U~F{LFdZ($ zFRzJJ*7+#yLo-_u@OV#MHJjOTd*Z|| zmWbcw4<^yCu;J}6QxVP+KAv#bet&*urc2IlykHa_{lf zjY-B%2LHcDx39>3u*sWJ8={VJMIYTvIsO+;;L6LoA6XY2FE|X$a>m!i9yX3U9QYnGYl02!X2+`+RToE69&fVuyS_Gt!#1 ze0P_#pJ&NM);S7=Rx~^*Er74IzW*)uYPF3ajb8Ki9iO{J!ea=E)l24_J?v?%?M^?N z@yi?^juYp=^-(X}?v?IteHU(rPSA3wchJMUv}2sdKZ8RTmQ0}3&{ybfxfg`bR|ZO7 zx@`GX%g#_LIZ3u+<8`Q-1yEnCEpe{t8S8baH_eP?hHupEXA}r>&dwKFYmhq=84&&8 z#!6O~bfDUvM>p%R-mxI>RJNkZGTLP*;SoGxT3*plJ%@vRYWym$xbv|?LxW73L`J#i zRrl5rvVAFZilh83&o=)9pHinzdJaY&e<`v0n()j;nq|Bf55#ZAjK|^?FYZUCv&08M zxwfxtn{v*%r?FKm?cXQO2<)bIBH5~T?X5zF-=jPx%DqRZf4?}`^O2On++)i8sCaMQ z`OI;HN5S;w2+c<6T?M(Z3Q=ru{0WI^;mQ3#Dn8-%R@Dnv(@9!-1VKmCqxa; zMgG_7E&dq+9$o_V0Pad8`Mo9CuB>FD^oiaI^jqg+#k5p504jUc?!>@v_*RE;@1?5n z!uh315ypjIpWbDpZ>C4>(M2?tdk+3DE-Cif@>ux_%yZ{3lwOO#_M zzGh|48!;ph{ro23_@DM!?N7U``sW+$mis#;GIK2r*8Yf+LC*CO{;g`LS{-J6Ak-J; zEwz0Wb6G+Nc#V_Izl{~i5J-}lpXW1*W*aV^4zYQ#8&fP9i00M{>Q|Pb+iNK;IWc~Icj_8plSEKgAQrpRUuHHKK^{Tl|dRUPCN9G~eH%iW}c)iEt}xIsG`)*FLU$GuNhSfBrc!o-WTQ zUMbQ-!(SBsw5_U`*(*m5T2SkXbnj?r6W+98eR*nj%WrN|vGg&+P~Gg)M)=OOnK1dQ zhM1mF&r1J}*tv#VwiAaxe~e1oQRmnxhjy~RGA#~nGjGMZ;t!U3_fu=acQY!8`eS7{ z0Tr$$I|eLn?N)ct#n{&G>_;#URu5`L!^zW_QE@+YK*64=I}i0q?!uw#3+Jsy)sEgr zC$lO;qS`NSD7HonV@QqDwZ;NIlb62Q$~#j#?d!0D_U|NLaLOcz--BEXzW`*G$s{2a zsyDHE-SqDNL{}-a2HA2TOI;AY`%flVx_6Nb%qy27CSJvf9rb>cD$&8wB*ec5&zonS ztQU9A22#GVea2MJ(6UoAR}@UkO`G59It1v8sy}I1D4)M8_$XPOZ~ij1Nq=W`?2<+$ z0<}BMq@HPDr2RBzW(P#XlSb~{nsY9@eh=N9!Dd;sxHI{DX7SI>5NqQ_mBpF`wcy!8 zIee`6MZ<>^-M;kxjf+v5K`~Cj&L8l|{Lp*_o9U8~#lT2j=_F{g6IXp|*{ zVzcbyfhZ8rZ&Rv}b!q6O=|=0WC2EgcXD!5^9|M3;`>}T~k^p{6}S2z^ur7TGOW`hfU<+S~ooUSUL zN~jY!@NdP~RTNZA(EnT}-Gw!+KtSFe_gCsKthpSq^Tn|mpF)p%2?M{}F|>k*smLFg zN)yt@^p^F3B}TX(O49mEKAI_x!*z%zk;DP3BqU1lA6v2qggnL#RaIjJe_!Ga zhlW@dkb;Z|zXm1Jof!q4mT!dRyvQ~KM||#iQ%%nnm;-&+aIqLgbXGIRrHp&t+fMY( zv6I5m;EHS^9~U;NdTy#K>ufO6Tw{W2Z%+LEcIPKPCKCbilSjU*_58Hgw(c!C3df2I zff6v;jC&Pfngcy-9~y>9c`~KkTaNa{@zlBsL7pkM0TNP7($o(LDZ93v?0>eBh4Shu zPYUi#709gO$*N)swTN1~wlRm|&7TQXdO5N` z-vTPrN9LS_PZ;BnY0UA4yu7@~NRL0;sdUgrP#3opohqa&7v%6xcbrdaO_b?~>4C99 zihN2KsLRTr-+-R@bsy9si)VI5;** z>$Qyi8lmk>ZM={p+A&BU06&1P0L3aSRu#PgM$G*%Kc2(3WumvG!m4q$9qhXBDAOtSepkw7jwmJ%phD+U0Prsp*iX^Z?XC8tkn zDKOS?y6EIH5EJz8kHWk&A$S?!hHQnlA?U8?@Rmpsut+`YEsI6s-TMev(AQ<9?K-c0 zZ#Dk4flmRtf#lF!o~TrH%vl}=%yG}aexzA z^ZLoZ@~ko|%0Z|C3J!qN6&;vaUR4g`Ck2tvJvi+aB}fsHJO^8i>(fF>U~F{LFdZ($ zFRzJJ*7+#yLo-_u@OV#MHJjOTd*Z|| zmWbcw4<^yCu;J}6QxVP+KAv#bet&*urc2IlykHa_{lf zjY-B%2LHcDx39>3u*sWJ8={VJMIYTvIsO+;;L6LoA6XY2FE|X$a>m!i9yX3U9QYnGYl02!X2+`+RToE69&fVuyS_Gt!#1 ze0P_#pJ&NM);S7=Rx~^*Er74IzW*)uYPF3ajb8Ki9iO{J!ea=E)l24_J?v?%?M^?N z@yi?^juYp=^-(X}?v?IteHU(rPSA3wchJMUv}2sdKZ8RTmQ0}3&{ybfxfg`bR|ZO7 zx@`GX%g#_LIZ3u+<8`Q-1yEnCEpe{t8S8baH_eP?hHupEXA}r>&dwKFYmhq=84&&8 z#!6O~bfDUvM>p%R-mxI>RJNkZGTLP*;SoGxT3*plJ%@vRYWym$xbv|?LxW73L`J#i zRrl5rvVAFZilh83&o=)9pHinzdJaY&e<`v0n()j;nq|Bf55#ZAjK|^?FYZUCv&08M zxwfxtn{v*%r?FKm?cXQO2<)bIBH5~T?X5zF-=jPx%DqRZf4?}`^O2On++)i8sCaMQ z`OI;HN5S;w2+c<6T?M(Z3Q=ru{0WI^;mQ3#Dn8-%R@Dnv(@9!-1VKmCqxa; zMgG_7E&dq+9$o_V0Pad8`Mo9CuB>FD^oiaI^jqg+#k5p504jUc?!>@v_*RE;@1?5n z!uh315ypjIpWbDpZ>C4>(M2?tdk+3DE-Cif@>ux_%yZ{3lwOO#_M zzGh|48!;ph{ro23_@DM!?N7U``sW+$mis#;GIK2r*8Yf+LC*CO{;g`LS{-J6Ak-J; zEwz0Wb6G+Nc#V_Izl{~i5J-}lpXW1*W*aV^4zYQ#8&fP9i00M{>Q|Pb+iNK;IWc~Icj_8plSEKgAQrpRUuHHKK^{Tl|dRUPCN9G~eH%iW}c)iEt}xIsG`)*FLU$GuNhSfBrc!o-WTQ zUMbQ-!(SBsw5_U`*(*m5T2SkXbnj?r6W+98eR*nj%WrN|vGg&+P~Gg)M)=OOnK1dQ zhM1mF&r1J}*tv#VwiAaxe~e1oQRmnxhjy~RGA#~nGjGMZ;t!U3_fu=acQY!8`eS7{ z0Tr$$I|eLn?N)ct#n{&G>_;#URu5`L!^zW_QE@+YK*64=I}i0q?!uw#3+Jsy)sEgr zC$lO;qS`NSD7HonV@QqDwZ;NIlb62Q$~#j#?d!0D_U|NLaLOcz--BEXzW`*G$s{2a zsyDHE-SqDNL{}-a2HA2TOI;AY`%flVx_6Nb%qy27CSJvf9rb>cD$&8wB*ec5&zonS ztQU9A22#GVea2MJ(6UoAR}@UkO`G59It1v8sy}I1D4)M8_$XPOZ~ij1Nq=W`?2<+$ z0<}BMq@HPDr2RBzW(P#XlSb~{nsY9@eh=N9!Dd;sxHI{DX7SI>5NqQ_mBpF`wcy!8 zIee`6MZ<>^-M;kxjf+v5K`~Cj&L8l|{Lp*_o9U8~#lT2j=_F{g6IXp|*{ zVzcbyfhZ8rZ&Rv}b!q6O=|=0WC2EgcXD!5^9|M3;`>}T~k^p{6}S2z^ur7TGOW`hfU<+S~ooUSUL zN~jY!@NdP~RTNZA(EnT}-Gw!+KtSFe_gCsKthpSq^Tn|mpF)p%2?M{}F|>k*smLFg zN)yt@^p^F3B}TX(O49mEKAI_x!*z%zk;DP3BqU1lA6v2qggnL#RaIjJe_!Ga zhlW@dkb;Z|zXm1Jof!q4mT!dRyvQ~KM||#iQ%%nnm;-&+aIqLgbXGIRrHp&t+fMY( zv6I5m;EHS^9~U;NdTy#K>ufO6Tw{W2Z%+LEcIPKPCKCbilSjU*_58Hgw(c!C3df2I zff6v;jC&Pfngcy-9~y>9c`~KkTaNa{@zlBsL7pkM0TNP7($o(LDZ93v?0>eBh4Shu zPYU{zOU!UimbH_RN%RTqp^}8>O4Ik6du+abj06HD*Ma@DgvhQ4lsHCJjnzRG0;u_tR3h8 zq+wM7s(xaKgnnyVgW{LsTY17}F zc&@0PL*LLQ=(4b|KqJwc#Mh*^+sFo(z@&;a1NmeQWstAzYWw`u7u4nk+Nn~hVPpd^ z?J;fg?4I9b1G73Bj7EeQXY5}se5?ETK%l2jT(7TaeNsJVa z5yIGefm;H5JHjliEZI^IB6~UB{qfn`T2>+1js<6}Aa}V-DQcY*_ z`c=1m$3s}+>;oyNZ&Um$BCKAFEp&#H2jS6J|EiM|Q!LR)_BW5j7bhENtDuH2A8A~L z=}-ka-O5?SEGIo)xGz=>NdAxd9QgxMn)QdkW5Qd4&uNNX0U^H#^<&W!v+OwoRP@=o zXxsJ~Kg>UV*yZzN)>4mWRX0;HQZ3dD@O7I8aA8e?C=dbc`%pxNRRe0rL4ea1(ry*_ zf6E!G$En8aBxb&mC~}ZzTKGJoT;nk;6s(HomVp(%<4NNyF~1x$rfFb^wEGyzIc=OB zKZ3`|PuVLh-r81LH(~HSvG|j8*{K2Xh;;~*k)G@Mp~wQQVR_3=3&|OrJq4zg; zeg%qf$Ria37rZw}bKRO6Gp8zEpFf6^0*a=3)%tbW*+#JI1u>p zHqg5T?rh2#-mrNxS=o8_q*%_!VqNENkgGa7*mU0a^g_^&WLqcitYKbqk4Dst?gl$f zwrwZDmjd#>je#rdls8w3hnZ?M)mkV+uWC0{8O&AW`4n2KH3}_7=xnHW@=xENObSoCFrasqn4>QqgS0%Uy;7<1H(JY&LdX5 zT-W>NAPq)eh10EiG7hOpDe;r;h2^-_P?tKU-AbB8E2;Fo8P0#sk2_ftsBQ|Q#_+SP z;XU^5a&XXDSq3VA<)!Vj&u-^oCoIi zGy(fNSBn*T`f0Rggo6LTwat`(=ivl8SZ1Ai~b zPp-Q!hL+60EHJR_Mpm$<4=1pOu|MJW)69Pz9P@W-a}S@L+P)}P6zvCB??+>y=80yC zsCI`F2Lp(i>y)1kppCZkD_sg&l0)9Z6?aN6S3<6VstH{w(Q0;W|5o}lVQ19gAi4_#ui` zr}EYxdp*@>jc45*-_}PyFD7p6W82 z-6(~EC(6D#4yVl}i5o8mKXO7t+G6Uj(~zT!8DSA0fhlE+MV1ErAt7*XoMqwLh^l)Q z4EGHZUPefDL%U=y&*C9vu5!r~aX=6O=*j{qB9_2@IFE^BFfa^S-DrIbNIA{Do>{5BHmxG|fRbq-Oj>{2FZDA>a3r zZkCU}s%}RorByG3z>!YfLZK#!&C&Ox~g})6Uh6npvliF6- z{f>=p&#@9RTi&1La&bAEwfI;`eprvepQIz=GjT!-VwIxLYuUQa?R+o%cax&$co+KH zpA)JKRAbJwr2C{_115qg^5uFKB7D zRY05%8bToHm!SwI*Tn9#EjO;_x}(%(?Rm>Uhf4cT1>$o48Z`e(JRO9IPmZt_#P@4A zMlK`PQiMcTSJ2P1H+bv|IW{z>K&tsQU$Uds{ppCqR~ZEaNomtJEMz4^VrmU1-A&tW zow99Ssmlma=QS!Tx*P^N%xJ5+oB1arX4g)6Ub#vsk#54X5JPhqTGCvpS#EEGXI09q z&Mit1cdX>l5cz^EIH&-5vKaSywR)-#9<(tu zTs#&6r?EB1wFpoRFXjj=~{G2xY#Og3$C!`33k2e){T3Kp@!O@ z#?P{o)>^wzRr)!w>y@M9j<{6A`;r?K)2`o(YuFpZ)8-eN>LyFWe#-iLq#Nj42$Z13 zRon{8DOdh{CFYIBX-fwT+-jx?Z|tx2ZyKe>?5;L3vAH7-Fy%jhsBmUQ>g)51xou(CQuRaT`Z8ET^;h)Q$GgRcyWTMJR0N3nxaR*R-E%0}slEn=h$p3?0h zLz2A^tc$j~5pE=jzU-M<&;oHn5CRuBg*&bMs2=g%V!Agn7|W+{hFe} zz}dLT<*-c*#L#9W&?{#BN@`^&5~6F3ThKNCU@ql@X0=E+EKj6kz(_YdF46cRvcXym z+RNDOLky~O{aB}meUr*E{l@N^oKp0yb%MgKL#3mP;DFv?YQ7Gb<+B!xsk1k$_1AfO zUsk@Q`){ZrPt&QjZIg6b?@y27smI`1u`kkdCw3MZJq%4YE!v)iX^eOHPXt6QQJtE2 z^}j7GR`O>nAcv>W^A$p#cW;& zx2s_iyJZYaMkQKR6ePJvK5^<+57m`1gomayEHSJ|o5EAG4xQQH6=ejK3p7=zJ0z*A zVW5&rx#u9Z)Y81kRLalE9yb}Cw%@F#ig}f&ItRwP+VUD0MNOEQ%WD&9qGOL(^NXr9w)(UJH2ZR>eJ+8fAcGGHM$StTi zm7H_QZ44s4_~P*9#!T_}=X(Cm#&2sm98&0AW==65>34Q=Lf?99&0Vp;d7Ox$(!Ai# zpD7rtL@+B)i)6ljNjk6;7rV{mwq;;=jrLJp3>qOWi&0{ojy5-y$Gip~*^6mX-eDII zOYsyQyJ$=>VLrIO;oGLB68vRRN(73%|KVXo%~s<$y-n|K_}QsYeQ)|;JxbQwF#*g+ zPWWn%4|!|(g=Te`BJUtA`X`@QJy^*u75Z=~!j9kl{o84wHplvt>6((-jL|J*A+egx#9QrN6%`L&ZF<^z7ct1Ue^c>T*%yZ4v*4LHCQpUF! z!-)la@l<-XWDrMp9m?-{kfpIi=tDdp8Fqh(W1bvOyj~5A6jm|m* zE{vto!`v$o4(DVuoP}g_lPxgaDJ0Em%@fDOj5?La<1wBWK^m5_p)>Fvyk^>Uqcs)r_RmcyTCu|6hyC!pj~!g)vFO@ zKdQ5>TSQ{I{#+An`D;&ZKe`E!xaa)U;6!TT=6dkSH0P3)stD-bM-Voi7I{=t-Rx^) z#ml$0C}u9C9M?f*-#t8BL3vh{-dl zyv@D*Zw*{m%v7W?es5#!Db8%`knG`20At{UZRXu)eirmzMM(h2wc@v%S&zOLn6wt^ za^C}2$LCb|<_AMAJ(O+$5y2gs)pdJO)yK?&B<`;g30K6Y+Vfh?n*zp*3TMPC8JFk( zA<{|E7ch$le~}LQV$m7kwxNflVtE}BloV^n+0fu?MJT|sT39V;`EGYkP}FcqOV0@Z zeVU=$9#q3&%dKpTyu~uv1uEoVldu8qIEXQ)AEcN=yet4Znud?6H4xGN0VRw}u>b%7 literal 0 HcmV?d00001 diff --git a/Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-60x60@2x.png b/Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0fdab5f9586ace7626a8e31c96c5a47240c2912c GIT binary patch literal 4313 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 literal 0 HcmV?d00001 diff --git a/Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-60x60@3x.png b/Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..3b88e179c7f015f579ade8b2e29ec59cf5f9f287 GIT binary patch literal 6262 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 z zI?d@jKyE!`s;*^x-9xGzwsi6g_qvv<_QC3KY9yCOI!#G*hICepQ&>gaG0p|V1K6%=)aRzI;H>M&6U>)p&@z2&29 z_t@=^M0?bP(>E>6!e)pw!FgYKDnK=Uw`)4Crh4h_{?R2K0T4_g8Ls*!JS{v&(5~Mu zUU+&a^c=Y#l(w!(9@HB?y{I^+dquxN#-^()t|joyp!I>Nao z>H3#+s-j~mU0SVQ9`ef{QoC=?$cp9fyb!##pBED`?l&Q7>bbAp*8aN4`aulGsDA9j ztJH4+(7A>Z=~!HTKa&+?;k)}cf%|}KnGKtd!E^l|e(K0-ePjd7gM5XXbQ|}Nd^iAR LYHd<)gu?#^lZ&uu literal 0 HcmV?d00001 diff --git a/Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-76x76@2x.png b/Example/DApp/Assets.xcassets/AppIcon-1.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..03bb3d0210da26a6704db58fcae0cce08b92ba02 GIT binary patch literal 5312 zcmc&&_ct40+onxoD` z=ar{lScHa#*49g3-wLL$FKiWfAMNGqK|>Q>% zsMz5?R%{QmD>GvtI}ORKFT$_%{%j_Mxlqc4~Lg&EZlRcVE_I`vJuR&P$EHX0D1 z6BRVXC!d$xg-_`oBXXHiOnG>C5RZIUwsS79bzdCjlDwe7I9zhk^_l(@pUygj{+Rv_ z+$2jWEBfNFt;u^60G{;s;_w%k-epT;SN#njt$)Gj5~k>_X#$Pg1)}K1CzZBO9(jI0 z!MrOU-A%g;G)75ONrkV9{#ujZ;pM?AK_B(=mmdWQtI6;Vycd3p|Dyz*`h9kO{IR&n z7*JxmPO-xccGoLh%*>cMW@hnFE(n(>CIJ_Rlax)5`#K&70?nI%Kwp`3v_1`Z4H}^$ zj0D9&pf8bUXR)^DgDM^E+YIYJJL}~>Jv$3iu-Xv-C0>l60gHJ2_O(89qoLstg&FAD zgcofTM`YO;mOSbmt<`?&#oB4WwsnCWoTEvRcun;Opm(|iHaZATg4(zUzXx8QRqq|G-AhW*yAypY3HZk(k&M$^N zozAgwe&k?cWYqaz3{uahVJn}Db{RMsH`tdExHc z$&k{h23=autnE#`mfAEEJ)IM1JQIrqkY0(E-5K%iwJpYr#gsshouzJ`QeH&&qU~jU z8xx8{@UK{`W|`9&JiR6$2w5Ub>Ou3m`pax4ST@Q2l%E3lkZzY>+Hm+a<<@G5`RVIZhS111ADcL#;-k|sHD-zU zTc<#Ambm0MK=DiL%rbvYq8>`WvW2F1X20x%&)uf`<2zA5RetDRuv!Ni-vh%WG79}I zK_CZG{+e%={DoCXzl`Y|ELL$yvz*^TP)hB7%VNwA#b~yYsKNd&X?tqu?F!V;$@xtb-Mgi#3(SMmfUztwHWoZYDtLGryC}%RCUm{5i=ilG&M zQsvLJW~L+@Hs4l>_wN@NM#v&>S5j(JuDJ|gCN=(-Q1|t>M@mSy%Efn#bvM?f+Ggv$ zl$$9@&tQYhqb<5mt)Q6c#OQl z*)k?oPr-0FJNNIdx2RD@8mip*)xS6PZwBz$H#?&15|8Dx9JJd4AgMj5Hj%Rqqn#go zDcS?ozvzR6@4o@wCFppXDduTqA9drf;ixzVub}2!DURlpXZeH_*Fv|1=nPZ+Xr{SC zIC+ADQNPKA2~F49dupyw^_ma-b;|w#dY?4U(%NIf*QD>TfB$UQ52rocfz2!j zgbDo6g0)S4;y4ib5|3zZ(KZZNLXoDgeH~@Td;>8Y6`;e`qv4x}PL#-5mDm{&-+|{( z4s>!YL@>j%Et2Smv43Xr`i8LJZqRq@fxf1>mCLxd{e#$SADdWQXc21W z0p>yc?7RRWIZmv&YKDDn-R{!^qqj#|c#l#-X~S`vyN96|yTYUodq4f@95_eP*x0Vg zq}qJeM)PNr~PG+tD}tE=J2NX>;UDiY&9$bd=Fu|`?9!m~33q%6C%;uaXHt%e|J ze7?)CR)d>2|2g|`mi|sSpFR^MG^ZRf2?6w+a#NmEBt$4aBy}bF)wx7d{ys0q1*>|6 zvDE~xM5jY5?j0UWbuSvlafmTth|6wimoiIXAkf!`WR zZ3y>CKW~->n3`WWpn;K%EH_5%Jq@`zN;KW}klmGCn-|v>vyR^ajB4D1hBdk0k@_UR z0E^kk6$<-8Qj!KUUB};ke0BQOB`fp(dC=c^UJT(wUiOBdNI53%5X(qAd+?^H7BcHi z$t{zPo4_fLAEsRHh)!8<;y_F@)cODgi63!W1<`lqW}X+Gx#)21*y_s9Mz zmF!sKFZ6%*{4$axtdu(!L9;4jvTc~2>=`D8bkKuyqzTD{dv-13Xlul|K6SFUl}m~( zre5i|SL3=NNO?4-?@ZWuS4UVpY6b+o&j)6Ev3s54RLnVE3aaHSweyj>B!YCnTOI(1 zcsh@`j3YPRm)JX|X0Nx(D_C`GTR9{X7nM-xVc9wg`qpc!VXE1?OHRSfGk|Ajr2)xSU?r#us zZSF;%yY(G%y@Oo__sK8ACb*)j2;2J4)mP==F%4)`1Uj^{k-=ZvE|9WTJ~Wg~twI6f z)7+2oi-QR%nPK<`BPF1YhnU6JOzx!BX1ZUDF}wJsOdsV_(^2=8Hp!gWqB}f3nH%If z6xk+Nv*0IMdio!_!y|jCxB(xFm*{Y-oz-nDrp0uNKX&A0yRh15Wz#S@TFn2xKlI_2 zg3wx;f`69=V&G-UNpU}c;Jk(}$ zyfEjfdW;FHF;R$rwoT~I=ee#sFLxfPuZj*a2^PvpW^|T%-QbX!N~i8OE$w}NW{hG~ zeYdnZj?77ZQ=uN-y0orF6T}Ufc7SW7-*e_jsS}LpMOCqtv*U0TB{(6{q!e5O7Ym^~HGx#O8JZpDT8}Lmq+L%XpmcZi~~GMGRE& z3Z>U$?=?PFD0gmwH2WRShf*l1oXL|mp&?7UA|qitVnd@3skvLkW&7Vj}B-M@vwSsl3fS$5oQjH$ETXImPIl=)-*zYj7{)#!LF?f=ll|*&;7s7Uv)Z+VWCyN7)4ESrR-L8P?+1S{K@UvX_zgNl{_M{GiW23U8%iT* zY&@MYoJC8wdHTm9MX<1o8V+BR^VWB-0WB#xyV49{p}Z+5RYIWiwYlA#{=;7-RWt2dG$Y4pKyW)@QlaA8IJ*NV%B`LjV+C%$bCql%1HOXMeU*@HYx15Wopw zr_2K{F}Y{V9GWVKvEH=czUy{+UKb(-GJBqe$3}jqx*ALP$tj9`%@9plAt*m6EG@q) z-Jht1pS}&Nw4;jQ;k#WQF6~^{8RY735t91R70uyyf07OPsMJanTcCel`Rf4(eYrn*Hmf{)ve5Q-%wUJov+{I&cY$QeMHSq%6@-%H zHiyzhY9g$Pq@M<7U)>v`em5IrO^l{YXO{Z@eu(7%4PP-%h|b064)bwIUAYU-_Ek*~ zr4z6xxT&<@TgoSH%oWE@fGE);V2=mm=;?Rh0(AHO%Xo^`dc93!H!4sk!{!p+m4 zi3;3d>v74NW2?4+Dz> zu#1HwqVdND0bE`%aiD77TtdoTw|-9jKvdJe{pC6T904{VIeK0$KMc75CMJ+KyFTS~ z92;96@i(N|(wHYOo$u=3X&G|aHvJGuW*^{l1#Z2Fccd~Q`GYYvsahYI!_4zesFKsL zFH~XoJ6N!J+vG`7rb_1PnlZySox`n-WSO~}T4vP;$kbrSf~#dq8~N>k_FQ&_d(F-L z>jcG?Sha@891lJ$bE-o;dH?R&jy_`bOCxr-&3YQOqT$z(9C=X+_(d&oN7m3G|cW>lr7OY zwydyM?-nn{K_xw4FuU45Sdx2LQmFnl(BG-@PHV%l?vyZzkw6eLdmV;jB%s)~e^;f1 zvp%>w96UMQhI4%Bd3$p+m^?5fcm%5^k3K&;)uJGz!KXoUF2nCsyeb#Zmryi+y;j>$ zgpm(4jV^m>i0_eIOMnV1~YVc+etYbcyl&d6)FlMR^T9<28 zCC7fKx6y1jt#Z?8+nNQ46s!tjTWZveZE>Yt`Rn(Z&h<3A#41L&^3XqTsgP|2uW@&C zdGv$~jHs7lthQYE*R<3Muilq05HD=V?b(;v8369r?JZrd{o=Fws#Ze&o!KzmeAZ0DgV4hrE|Zu6v02)j>_+@TjEZw!A}IXO_pHI_vz zUTqGbA_8?U{einIAn9=jwM&PVfkI)+dK&cV{lg(0wcf{Jba*&ReMCGGHRFwJ?}xGJ*W0TZAI?x4JX8 zrIAG4;TQ2_>cG3bVgxWOTIuc$AtODgr}Xp*IAx*!eWp7}c-c(}QiZhDn4MH^?e|Ls z-}fhHy4w=}Sa8n8IJGQe=ovtMqD<0UpLt#AL}ux7ovB5UMU6@`Lo^j)+y>_7U`Qxl zz(^yAjSpKCTE93{FdXcE=bU(-wCq8c(%<&X6KVX{c5ZLJYbCVhIbb~2<8 z(m|vNC1lj2*kUYfUdLyxp_&qpoyoGeIjTd)b2cxM45|LT_=4@Obk!`fcujaYoYa%L z&5iZtFpn|Zy6K~09E@lRei!cDz80&kb>#+oNCJk9d1j@m@kfdKMts&`MgZMHV>fBJ z%%Qz>^7J9Lr27B-UGLeQop0B{;DO2^^`xs(y2HL0a%fa|nP_`TZAoPrAU@vb;&JFz z*fBqekNXwxQUZY4`(cFo46Z0ILm8g%Q zv566lIDkIk_f>56uNfua^%4$&P8k7EF2Y&BZ9E-Q^Ep9^oAU)KD|@{v9e-Rvz|7o{ zYX0i{K7X{3lUfrsi)vb3uq_mSV9obQu;Y&6&fETcCPs(0Ve;LOPdDmE2vrLqc3akXmFZmyqrf7x~g6 zT@T-H?>~5R&di)MXYS10dp~pUJ)cB99aT~w0}uxXhg4ln$>4sj{MU&H?nggbtz8_P z2PP0jMLl&zMHW3DFEGU22?r-I&%xH#K%M{Dpq-tq?cfL>56~yTAR!^ez&2vA=TS@t zi9HE0aR`l{_TYhx94Cn&15N1H{vM(dfgLDDl2fc%z1g~F*1qC!I=Mk9S8%!%r#^FP z!;$5NFu^c`I7#F2mQ<~MkF~XyvN!~W61ju8 zgA%#91pwbFSmddVh$!V*l`^$WqTmm}?amcn|GUw*S`LV$2e&O7wpJf7(*54v4)F@J z+Bq=gY?@(sHz4mAU=uko$zZMX4jBk~HM;TZuS9ar?JQ`axZVBEK9p3Rs`fdb3$h@PKhd_jidY+v>cV1*nko8+ zpRu@^I{^Kep-&0Lq#pIuD4K;yL@1YFz2QI!tT+i<)XctY?nVTM77smk4KzW`C>~+y*>rC&?qG*HH zbnV^XRLTg@o>GIe&jGuIq$DhJOSUDg6)O`fn;0)+sVh=KAm09!L!r5eUode=wCV2O zK=QBH7`+%xeQGfI3($J)9==>L0gtKL-PA9IvD%5$mm;#hLzJ2OniQAMjz5JaRsv<-#@RYX_#SC6%i>m*WKW**cpP_nzQeY9xn;XgFuKMJQ$9=Gx{R;4 zy=v6vadC-bDi>TgLT!yvL7_~byD$?Hx! zuaC!4Uad?fCLhFDPtSAy2S1%6Y-9}Y06F(e66A5G2NY?)owh*G&G0_rD0Vih|5O@x zBmC4o#`@PGTqs>2U!aP2L_3Nmw^W(a`4g>KyV>zyA9R11%5b1kxpW>(^kyPDRq(Sc zrbEdG&C_=4A~ap1%E3ePpF=i3oREIKqA{CBk%yi$2kp4Xd!la;Dcz zEcreoNKzQ}1CJk%dX5Cpxry&i|K)kv&?Y(#$lc9+O{qdk9=gSgGK*F|4N}Ti`wnTa zBs!mz9C2viMNEQexQy|%149y>%~R4JFTHKsv>7FTUZ8E1SeqyA@^G!%Daod@d|Myv znqpu7+(HpJ3B5Pwy+&X53?^F$3k|0d%JN!%u4cv&((+(E$Nx6Cw58>HK}&zGQF4yk zLqv9`%f%Z2$Cjs@YjOI}1FPpesmcLG)F9AabiG%c)D~a1$IRc47%!3Va~}tD_?YI- zk}cmk_1@S(S@qD*iR*~2Y(+Be7j_Z2mcdDDM?0LxoIj{V4SW^|LqAb~M~$yNzE*fC zhxauV`o@_!gFCArL2Tm>RemysmH_9{u4(Eg^j{sp`#4pA}n$t7_ZvWTJ{> zF<}Z5hlJaE+bKDT>Q-C1aO$C8X;&6GLva2wEz zVlKc|K1(2$K>gs4%ZWTYYh`Ee0>_fDX`_D*oEK<_fe8v_o@ir_b@p*{9?@BPC&3H0 z^@IPOHUHisP*`6C`bteXR&-{2&hRgm%$vK#RP%~8mT!}5a=aMvm2ys4YpYH#D0ei} zvgD@pS-}2|Qck)HF%_wbPn0IZHSYakVRvxy1lw;w8w-Wt8#B?_8Q1s%dTF4<6$}ZBhEh)d1`laGC8%n&lKVe72WJM}Ig5 z&$DL8(vZ4Fn~kNiXNtuezBg$#?xqKr>O_vWRW#g6U5uBg-_zuKN%oN_Kmh3S4S3#iWKJGqo8QCv@FH#3u2I$WI3 z>rNrMbM;>SQUdk&=m#Yg8wdC1L+KV~L8#|Px56R?9EL}tz4!5DDIAW88*h^q{4|?m zaJcD~wAxj-ZA_5RvZdnDS}YEDX~S{RFF`Rhc%e1~0Lce1#XzQ$M#L(5%&ClkhJ&dP zD(WM+(2t}r66-BuH|;l?X8cs4p_%E#Ng#)g&5sWF5GR5NiZ9EwN;*1&fc7hLtp<~h z?z`X6VN8!IB=FpIXqvs$OpifiBTfBvlfLgATP%h@nE=1peuh(<{7-}x`N&nXlEFXO z))k_5JFo8$E*^|!75U-Z+jPx%VuN977o*16ZueyDC_Xr?nqx+o9r*$fV99kcb{uoO zob5ni24bDv+|zw5KYQ?Fp0mB8DQGWoDE;VLi9iK@KLR#kH*l^6R;LqXis^+gki-GyLQ1CHIWx_KwA-O3b7Ehd_k z8v@@8sn3(vFlx)u($w^Ck3oc`9`2KLSrkFepzGV-_SMiVeN~>SyibWt42ySD!uspF+S!(Y_M(!X%<{qqYQH*YrV$P*6|(U<+u$V zStd@)lWs|L6@s{qCo?izS^O{n?gyzit`(|a)*i}PGRSl1zX#N!KYrI$Q`}Sy%7QLpd9%Hy=T97GKRuuy zfqZd`vPXreCIPTxH+<{msTITybhwsUW;#xNmR-iaiIZoZudky< zv6{a!I;QhXFtJa>&R!U04$sU+v(o9+HgB(5S{Hdahcx2W_GeP~4smIBJ8;aR%gtC| zf(rkT`T2{52+`?9SNnWuVvHal)FVsZ9Bu{gj!4}^e7JaC+Ht+$hlSEz8~ToE4$x9z z+&H7Ww$im@FZ^2w`Ha}`TgTiAVYb!>rVlj5;aK4h!)RCkmnJowxjuE~7WN9@hDnV0 zW{YOV80myU&g4o@9x39AmUnn#fL7$(I`?PT*+X>6*;gAqKCmKET{YZ8^Qy1uwY<$@ z2^?hlV|?ibYA$3GzM6t-|7URD#BJ`+^W<+~#3Qz!I)5$dD|pO!$S*J;YeZW=$f**= zwY~C5b)48<7E$|r=36zC(Dvus=+@Ap^q)pU^5x0C#jW#=5X2Q7uf`%#w;QoV z{fYKrv>WGam{v@R~M}^1`oOs)q}ydKWbwidE70T2~nJ?a6)gmk6Mc)lZDJyOgid%hKD zIo^a^iFQiC(-y5aDF0(v|9cBHGvth}=>Y4hXqU zV$087fZfH}-cqcMf5fJKLK!yf87b{_2~w zr&0Ao`9~KSFKJ}0?ArD065C`Sm3srYYAQP!J&-jsX^TkwPVYjn zd9!^4u2CFIMUNj}@7!lWuHs_ArE%}ON)44$uTGn~p}HCAtM&~#z7bzi2i%4Iv>M(y z*XH~IzT|IIvzlB5Tyr@@+)KQ5+VZ#d<=AQ`-mmV#ffsWRS3a5gPP&UcV2XpCuDDXI zQ(K$Eckc0(3(h5FPZMu_LVaBAL@&hmjmp=ich}C(tWsI->JLC#GP_Z!NW2H|rJwi%p*JXetdVfe|6@LRr(pIzBBbFJ08@ zuNJ=F8I{8_%#a7Jq*PQ~J~bR*^Js5wfJtaon?qM(xIl93F@0j@HkGWDvM~75V8Jje zzI40M(PDQze+4^8Z@J~Cs$j@b)V=HHpy60|)D4wZb4PGI7%QzTjQ@*OJp(fZ)72H4 zFV%)nl_P^I!C1zuuveL58qP9qpD~`Kw}+HyVz+w~^#8y{*6oin?al1Dd@kf)Lumzj zyoUCdo3Y>RK1Q9}LF03tQI4z+rK`ys1EfWieNNvmMRI(hAt;l>blr;yUl#onZO zBt{hJ{7BM=!{hFHpJ_5xV$P)uSQ?RQpke?A!~xh}_qB-LiK`s$>3(75l3?L(+Fgo> zNZbfu6*q37V(!ng(NZX(56=1KYskUOJa{R+)rjD3T?UYdu7 za5udoHFe2*Ph}3g{8pZ{8Z$RU7QMFgv$k4D=OAKt$ouLt&JKOu+XnTr zUeAWDz0Vsr7$2}PyuN(?60iBAIIhS?@yce0>3&MfRD?-BmR;4Pv=(L~ZS!W!X+IH= zHUxN%9~Rt(&51B9t_$=d;KQylak2c&?0UL7K74zPWiX8PKa=dT(!UyxO^4qj8I4Ey zD<&H{8mxGPq*;GH^cF7!{3Ef`b|zIwXMg!3e`fcpWg0G*u}3Hw(V}Pi$)4a{gtxxd z8zUVTdd%S@O;TZ3wPNyT2$>1Su2VZ^Jh8LA3%@Ar?o?~{cgsRAfFWh09g??J;ZHE+ z$2~fH|GXQt2HMw)T4mhU@A9Aezv-Zf3sidRp_}ST(?sySQMBqc{#lzNsBVtMbh?V>IUG0qY zk$E6UNPc4i&pd_U8B9!BxQI960U1hb>^^IeGRFq6eddJu(x89Zu(5M*;@zyaipd5W zJB@LY{d`mlb0(pberY_>z;^dR0#)h?Hp(3=9Z%aGwLC+qe7XAL!gNQ;^Y0>JH!;~j z&OEuY!pI?Oelo zF25tw^b^)(`GT6cDSRJ#iE&8+P-Id0KyQQcvBll~p4`Ny{^8MY{F{(x ztK%fAw=tWB&Pt|Q37_uawpoD92N<+;9hA`=4^uz~o-S2BH3!$Mw8GV-dCt@#~r#MtUN$a@vot$tHJsdIjRJ710o+JO!K+U^CY51b~1h559!A;%)Jc^qv%O5TY8Wv+XlhIu4o>o2f)$)92HiZ<`2 zwX0b*`-wfpk5ESu{l^fXO@3lEz%{(fz%V0{ zPbaG3UB-Oq>9Yh#7{8L#!KQx~2GQzT_c?SKQi`si+J5fg2!Z*0&-T!-f^ZyXBcVu7)1FLYe S63sm)kE5=vqg1b83;iDjP5l%A literal 0 HcmV?d00001 diff --git a/Example/DApp/Assets.xcassets/AppIcon-1.appiconset/ItunesArtwork@2x.png b/Example/DApp/Assets.xcassets/AppIcon-1.appiconset/ItunesArtwork@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f527fe6244244e39e804a088c941e39ce3327f07 GIT binary patch literal 39006 zcmeFZWmHt(`v-bxL`tPwF$e{vLl_hV1nHI%B&54z0I`q|1(8PS9Ho1ZkdRUshDK^= z2BaBg=5D_K`|7^DuYYT~Jh(V#pJzY&$ET_v?#!lzaFcwBCai8=3LVy3}mZ!h@vm0^T zM3*k9ht2$N=QBILUy_eou)KJq<{_2jNH8n)2y|I)vLIs@x3EF4M*6m*(c58b8L!7> z62az&nZkv(6qqkGvx2vV+kVRnBLP1^C-nba)ELi%f!_0-i3aiAqdvPRMKYdU!pSe6 zU7&OK|GQj_`tNd;`hO1sMf^`W|FfL`{NaDz@xRbD!kBCB$ zhtjDCaS*#YyM?*oZsUh*N5eZe2(zS6@0%S$nL9NUesTB1>A>2wvxA^}j7t`JVZeF0 ztbL=846_fTfI2RL^sMfPo}a;$lTK_rW6uy4(AT++NY`)4ZqgqR#D>fWo-TwMqqeSM z8^eTpIN150*)~>2-BBN(hh0DA;}&!^%+T}A^{H})%VQe}arrXAN@`H}hby2im=*kl z>l6vAS}rpgDEXV{@6_WIT;S((#Le!C9g2&2BIfj~SYcT_(C(iF3Z9>Bey{T@%xb&d zf;1e>DIUI;zh)&*rZgeMoMOYKrdk}=Ag1K2juJ%WYlbFq6r+$&$eh4mc<26b4oL$+ zJLd7))KmePN7Ig~4@jXEeGq3>0ujP{C_`&3!8phjI})c=Z5yItG$~h|r@ z$5PATSNNToeTjUon$M~!(T*N4x}xFta^iJal%s`T)ag+9@L0-`CX|1UJO?8!lwAu7 zq-=+chN;epX@l~>f`fER*Wa&LkY5bf&8mtFC*|>RTCNNvd^t_)<;AXOhR6PCP0r^u z{WV>3ZJ3ekzEj9mRa{V#wEIayU^_AtpT$X-7s{vbU5dK>;zHQ_3a3IN6aa`sVK73{H&D%2}Ihn@I;eMf(qeliM zD)lm6;_HtXjzWH_6n-j4rPx1+tZRIOI` zHP?wLOeMujcuZnk!^GszjVXp!W`FHI5zfeFZ01$LZVf|SG)(y<}adH5^B5n?--T1%+QBg~D@% z-DW~}O%)K;PMh>d`?So7m8_Y<*jtJVi9Npp^V(OB5~_a5vFsJkuL(Y?kMyEgN-xy~ zrf;Oo+2^V3d#WSuluf_t{tAWMI()VJa_liJ1l&!n)TRKA{&BVl)57*7v>ujzYku!6N zlR+0_&#L?AYdB7pH^?drQS7#MOTH=^w_IDyzbwD&Wr<2CzGA9TZ$%lM&?;+pzL&{1 z)=95eGcAX5b@2iw%37`zDI3zd9%dYHMJ)IY$yFK>NNH)(wGt_t(U6On){PZIu<1lXB{SmKD+>q zB*rZLcWYw9{g>lD3vt6

7n`a<89#HYNG6_>mxYj}KP*;mdRKmCcF5h2o;m3_j{T zt7exrFW|O`WB>a5g(h-uz-m_VmJ%DJ^93X!Ax#se7UxR1^Xo0Eb>1fhhGwU!WnpHo zo%GZnMmNagNATx#ACrOrmLyszj)u>S2(`nITuvtuuSl~k=Nx_Gut6d_&lr&lyfz0W}`c9>zK;><7n6Vdq zzhBrjK3hzKJ%w~yfJD7%!T5QC`8{p-J^z;68&i>iiC4rUwyY$IpV|*x}JLJX%-YN_5gx( zv}V4wz&@fEI8R+gSKiWvEwRid7`qm)vtW1_!6!&~;4`D1wE zkABP@IKBVXs^KTB!jkhzh_Cvp+68=Ahsw$xYm*m+-9L_*r$6I|a&<9N{EuDLgN9W( zp!slMuzNF8|KRhIaJ0sgre1{e&Uhs7mFNboQ*s-Dbh^}Az^sbZ_&NI)?_j3dedoWG z4@z7jh33y}y<!`dS}(5a^C2;T0Uf^NJR4&FSGMw?4u0U>CCk z0-k?nWP5@*_j=-xt3aP#Djz3t`Im?_)Eo~~!@4Z3l~uSu^O$(;5E`eK&y7U;!3s-zuqJ)=ZDvX%;ecn=Dq0*1uk-I(rm)FySyWQlMZ zBUKgTY|!}Yrz&)hO_W>#h&hLxzc%}6 zGw1{Bd3*$(@uxvRoC#7W!@KUd`z*VzfE|QgNHS&}r zxjqAPm}d(nOyo~tKusG-7FihF#&!Js9i&Ta&N{PfAk3R`Nh@DE3{9o zKmpSfYZ1?9UTKI7{nKw%^eG0RDg1SvAu&=?D8P4dM8`Eh4)t35TMp$T1T2~rI*5Xx zzhz9)9BwWs1kWKnaKPPV7vFXc=7W#;bM9r>kCIy@U+{hM@lw2;4zETvHW(8HS6pblwLUPfw^$4=DppUnz01Oo@DjN@K_>M9K}&7bd(RfevE8dt#$i z-vuC$rF$^=2&{G-ooisXB!lLC!~4uOuTmep~tR&!Q=w{htD+72T)R6)!lJ^=3*ZVq3N^4{ib%w}1~aS43CLaoLIq^9m9 zBZvp$w;mBIRj2Z$Bn|vEsUfw*p?~f@E7@TuBLTQCmmd2@cTxNYX9`(;uRYujBDUl3 z`1KH1b-`a=aK7KAx-&)m2yvBEB&urFA{-ss)7I3x|_8Mp6!a z9pjG3`FweqCo=Uldqa*Mga$Qj4q~42kx)ZQokD*9wgNkeyUm0mbO=)D;j8`@$nu6c z%S44-*yrOvrkD-o6dFi~iv%iE>SQKP%I|-Ceaf5ZHQu!LCy@Xh2#5i<6Ut-Ya)$*M zv+J~%L)kVLpm25ljmc?z1#ua7I*d3DGpu;UOoV1s>U65?e5Y>j=85-5wS!|30u5jx zSEo3^-)F6hp?(%tWd39Li0Yemn}L!Xh)egfoi?z7X? z^I1P(;dq^N8W|wx1iAN`T8@Q?7}U!RRwSoDbPMPY!4)>(2ZhdqMZB?ai0V3M_=-fz zv@={_vNHw+4I+~N-7MhVNrp3W-I=YQoW)-RH!9@IFyEfLy&J#$`OXWv*HNU*Ad4n` zw~^jr&_0GY&$@4J?>ov498nE<$JoX`@#ZjaZt-|^+`6M;BLoS!lSA3&FFCg{QlZOk zFnCf)MX`F(Uv+#1Wbn0HD9w1(ag7ZZf2r?%m@SkY12pw-p{ew04%O(1tpCY!?a?bI zo~`rQ{x2O!t89Hno{BHr^**_*VyVS-ktUIs6w_B?}fxX)f?8@xCpU z^D2(p8z0`7O>gt!%f1O#t?QWVAX&kI|Nj1fBdEy0U zfDTv4rV&(e_B#f3Iw6u6YV>p-Dp2M_o?il+A;2! zijj5BYb0T~!VCJIq>C#Tx6vmo{+m&om%LXRu;i z8&{CC(JZoyNDwsU7PK=xAmbYat|F*EL=|6ar8qfa{GE&XjUXw+C{Iz)y|%=iT0J$5 z9F;ck=Z0vYJFN6%u01O;%6-hNO_JO7WUu<$fmfiQj z$kjB~MZ(z1R-3zix0jOdI#8zh(H7?-QE>rT-R6%8(Nw3P6bKh%V)w-uW{Azopqma& zfR%be>TwX%5xHBTQXffJQas#M4NZMWgnG@#F1{CDxpX59E;F2;Q+}R1`~z_4JXW{p zNjZsizgm}SZ1ShMTsX8e$PtLBNxvQ^Wo-VGYV-q1xCzkuH5RQNlS)lvQ9o_hy;#&u z9EpzZl^je=9IsP(mWwLg;&Z<2Js_muOU^mGGv4>tsq?KCD@n635`*mS;#)b^bePn) zm>6YRa6Gqwt#QoRYCG>OFP}EaTc~PD-d#Sgv=m(`=UuGnJP~+cp~|`3I6zA=bO@F_ z$E4kU1Oh8^3VWsflNFRC>T+Hr0N>7jFkIjv!Kfn!+ye1Z!6PNIrLql_2*$~u?wqZ6 zCF++&uokcEZ^8n(euji@r}j&;={T+FfHbqyCil`$hc=9)YV`cXb&9pkSdK<0&1;{ z5ZxJuvbP$HfI%N_RXFuO?_(5qu3%T_j(T@BTu})ucK^AX!hWiV*#>A3svTvE;m})$ zGC41~x*t;#=AP2F0u>UXa|E3RO-tS?tLV2TS4 zvvb$7FyG+N7`Jnx_lLf|-F{r0WGMw{5Hkt1!aU~z&SJd_Uu$$!wLj`L27@*d?s#d* zI!9n3X zMjLqII-W4g<5MljzEda)jS0_1jwhgJGwhKbl)I1;0&GB`r^I<-!XJA83+_PWU2E^< zg~q6Us#$KzhB`Ij*T{h>mq9_~;FWj2=o@aIjM5ygcZDK_dWLaS;i2|d*{kjLe_!fH z4^}6vrkYX=@dHB{q^1+X`VMo0V0sc}_+bT~V;f_M7MsSk^&km1LZWnqtjD#@KxHsv z9u^__3aGs6OkPHq+PD*;n$xe#FH>ij;8PZ*xUIrflwrkdL;!M^l}Y0` z1L=h(y7+(Je+6s%T)r@7b-1t4MEQp_=!K+H|5|8QipF5=YT%;=t*Q(|`BvJpl8HP{xaE9VVBboX+Y=5%*Id6>UmP}D79P}HJcq~=)C zEx;|vEa;jTofwxWrvC6lqNo5iNm#&2XF%9Wr#w5-ctFrf$JywX>C*X`Kv_g&vNtFY z@gqHK4^aial7R7#dpj#$=(*Q_ai!aKRG0g(tbKgjy!)FyK0s5N(ht@_+b-Q08W~e zP52nI2^{Pn5*V40zs&C)<;Z(E4HRAj9^~FrxSmz&=i_8n;J#|$tpe(xsiIG%xm#Qn z8hm$Drhh+O4x#vV2;xTRjO?g+U_#lWw_Q;rRBu3yj2wE?%i`0oSNk;2y;S!{(uw33 zu8oZ?&f}qQ|G7Jb#b>&x;V^kd?=~_N#_1DYp=$cfau4)1Iv_A(VDZyw-#!8|! zD-18}LaijA5WopmWlwlF_!XHorOv=+)W3IOM^Mv)tcm`;@~B)N9T;7?xblf(K(nk( zHvKw+pEC7_ull5d-&O{8D<>_(?)`60wSkzk3zh@{5T6yDa!bGy(-)imcD;|&r(7y<~-IDUyNIj;t< zw`MI9(gynYh1lq{=gRRgGo2qLLVAYk`;OitPh;6jffO$|C33X@?DunRn&^MOU-I0LkCgB1EUcz?1;vP77*anfo#)_}NTuZi}4-0n8rq+gR=h5A7U zFhTB#*%@Sl%o;4Hj1?#W^J&UEnnHa~~q{>e++jPM5g9bBDuZOHMwd4%okp78)n zRg3Am+3Bv?-o|xe-*o_SrZjnJfwo6q%s#nVTkYFz29JRY7>tHXC^JyJ;+f+4s!~(M z8(h9L;_QNmJ%**|MwUJKvsC-%0$CsU@X z9{;Q>6SvaP-e!A$<%oOEL7t6p&&I(;!;gB!xNZHncK3UjF(lWy?Ae^tSMOW`21A^TU7dQ_Sc`s^*6#f zFeKrjAR!&7{}2|N82!S2eoA>ggzQGx^+z&4XC4jsO5!++CG6kGx)sXWMQJIH(H2kF zr(QESb+??2#4{Q_+DxTtI>TX4is1pxnKLWKt_Qcqw;V|(_oY0|G~&98^YjbiB)n5M zS|`fSONQ5i4ra#kv-!+Oe#-gj90b_gQW4?f8D=4_9vU6UK>8q?X@q@$juVFi1 zzMieU7TYpXt@k`xfs6vdTDv;Hif>=vpRzx6dRKhK@L_aYI)oz^viMBJ=Kn1PZTouf z84TJ!poUt_&h_Wg-AZj~RjB?g{cQ{R_2nPrU(_eC8F)22?EW&hTO4O`4kh-cfXU*e zOYW9@S67d35msXxcCQh;g8=FQ;HoDHmnjYEfZ?^>-p%lzxL3VF1?uXA_d}Y2nO6A_~?B&@Te2SKLTQtB_Ux zd@siED*P)r|2Th*vAAPz-c59h31M}6r03_gV1n}uupoTy2WLBS6I zTESlx7q6rg;aKs$JJddQZxQ z^y;xEHg9u@V3T~;`c~f)KzG_795-D*Qt(&yN9Dhlp{>?hd^hP%kPbeB!eN{ARv(R$ zHc#H)p855JZG&4jbe;R4iJw-R67mz?KR}YJeg5Y1*Es%|uB_!t^Z|VQUE2>u)4Z}c zuIDqpF|t@{@1yW?IOh{WBK0FiQcKT|tpGEh@@TG_o;vb&8>xUP!+vypO!BOlxp*Ae>(_Et1 zISC%YA9_)gi;JnkswR8M`83iH1c#PuISo~MMm9Mffns_y&L*(klK8QSy-8vaU&Ln; z$g=BELX}e`+%pZK;f)j3u>Jyf(2^~9aguV#i_rM_*|WVNI`j#FI!1Q{A6OVIVHcAk z_VX?w)vBsjG|Vz>B4ncYdjA3FJg+%d6-@&;I-b{hjE1%efYkvR+s}VfjS6DND4&G2 zxhfj#s2aWAm)3mNN2RqQ_Y3AiS^Y{6Lg=5~l@Wa=;r5wbx-aCPEq?A1@@W!0@HIe5 zxP9@YiRby5&xVXgddQ1>nZO&@qb$M)-FH^i(oaoQViF|Rx{LlwR3HK5;w0A__dIc= zx@=hIWB~*zhbVSOlV-x)&E1xTzR2Zi-mK9EZpDgX&0m27w6_?33Sg_slrLVAX>~iZ z4KK?{`Nn)wM);S~!NsIoprqf8`;Gxc9*N`!1CNTkj7-7(KJMptK6rEdE*#9XnT4kl zj;fHpc~;fq#t)0N9`#XnSOyGr#l_@qeVm3iA_2aqJLBlxSel1o^`HAYaM09*Ub%-M z>q>i+)BjhlsBS2W?KR#ex7#l?nsQ`Cyx_Ax@3T+kJMe%%h?%umU5ny%9o>$P$)x~L zx>^i>Dh)R8X2KuH-cF~OACr(&LE($>-O$-3ESzC}b9M1U`wrC%^y$-fQcpcH6hk5d z4}W7s2aiFY3#2ZBez4wu&h^l!zG>ng z$M|fBZTd`T)ut7{FJdNtJwB6J(ZKq~BiAPY+FhehbPWjG4 zZui<>6@E}V$b+aHTZ+n_D0ljB0#j@|aal%96-v9z)oFpwylLV2Zb{d4WRlablZL8N zYq-ZMd(J@VfXoBQ)p8BFaB0wyAQqag5l>a?_Zp$@*=LnSgCB`Y*GtHcxdRU_R>(y%Jt<2hyb%@+oK2z{ z-5Nr|7Fo*w9Y;D1yU=gRj~2@245r%8dxs{VXpzSk^L;j}f9`x}E1l(M_}9nU&; zW<8uO!H!t}tpnoz8|TuwbgZRA%pxjtxK(a<$Ma<9ZB0w`5|D%}?H1jLDbC`TVzW{|~`oEh1|5Bh)$o&)} zVzy}1w8&Xf?|sXcyi-_X_Z>Qvg_Uajo0umR zV7!!~&`DpOb3%0hu%7^7OEEoXqhwJp{}wgM73%~;2g{jtg{}$FOlwMXkA}?_Pf{nY zyj`HSYMts3M-2YSo|DS-F6i^`E(mYbX0H4)FpOGul$a2D6Uf^cB+uu*oK;r*?Z)I7I2^;sSMWw02T zGiZp{edYvkNtJ!pWDP8^Z}Ys5^iLO&O%}6Aw5ZiWajD*gc)A(k1XP@sCWl>lW8M0< zr{$MhTfiUy$G4Xv7C2oug%7;83;qS5Tfn?HK3&DUxWVQe(8T&dgqCD^A9n)}3}I3} z6e*FMSbHIen9OBYo?*NGXO9qp2X)DT=4UDandxs+cx+`$#43aKW2HqhVL2Dp9>ZKatQM+t+)SL_UJc`>VOxNdJfiIPA%DHTOP#SLs;HCXupHJ=So`q9V26gQFn@yd|?sJyG!oO!YXY88RS z_dk%Y;&t$WsDx<0#=17RrI_t}aDV*gQ}_^$xEFv!o%`u;Wex2y0jTg6wYmMy2n0xxKbD>J{=ah@Y5$-cf#O>U5<7@h>te@sDkY*|LhaKMx zBK{v}mM_Dwz3}>!Vapfft(^P9X_CFni@AeYZZQAi1P}@`4Y+h*Vco;=3H0k_W5JU(f33Ur zbWR9{(?X}}x;Bv3lCwQh*)$C$(BcjYIIT{vgl7VDG|0r0=kId-F}BFSNH8dV^|3zU!}<2@WT5n!%`0a1-Z7WIFJ|Ot zIKTp$tgl@jN(M)Z5FQ}AX`Z+KNnV!sq1|Z7c@**xj784M*6n%IW3oge&#QzTM;MOY z{qgu7>wPTpuLS-sE@A=vZVzy4hE(svomZXUn|fJ?iWb)o9UqMfnCTeWmS;W};nB>Y zf0KOsYI5lTT6A|j5)qAm))tfC{qK(*>zF@p`!xyy60m~*{bc834vXp$W}23pJj;%; zVifxtX`I(5z3DH#^NFYv9Jo^+U}}8J&sElnd%;=bvsR+dF~xyJ*AR|M{RKG6qM`MM z^@s4Ki&;Hg3SE;GumimE^kmdCwd8Ao^8^8gn!Oejx#F3Q#Eb4AAnVD1uacU#HUeOB+IQ+H5BY<7_INaiqrjHG9+ z8F=|vR!Qxy(vBt9taH{j08Frft&c3~1-X+RX5ta1jo(oK%2S1?)Z00j)3tbuWHkZd zvS)EY&sJ4sQx@M5OKHrQ)sVU64b$M1$q_AVM)xw_Sj zJ(h`?fibf7fzc2XJ9Cp3Pr0n7y-wNc&DFE``Dyr$a^F+oq?0{hel0Zokb}dIf3C@s&AejFnLd?0dLy44LDK{SKgOReP!RBZcx12 zE-&WPvzB?2g#-%eU5;1C`}DF;zIGB`` zBuvLMEPo*FU=fFunoM#c(Q{n*r=Pu7cZU}1xI0~JDbqg5A?e^Vmw%7LF;>`B>iMjGsMrA?JYklNCGQN+>T1(h>)n%FQq!OyZpnmM1P26TefX)N39v^6=6pGJx@` zzk=^8{}ntE#}?fA^7>tu&t{VEZRf5GrJzeEPqQ+8adHfdrqw=cN7nW;2J)qrch^}3 z_<`rrGKse?y_+Ii4{2{X%Zq@KVB9q>EOLAG%?u1G3g+Q69x9#01Lh3n#{ONUUT`Fr zUZ==v`mx0`Q1t>7SAc=a>%TV>Wr9~J@}t*yTSeL^E}ISA@;@SmIUz74pj=wK05;8S zpo--cfPt5~fR`q{n~qKnS(w>hyOP7r9(wd3c=h-vN8np|$#9F8zan=VmgtF-H_VAn zvIqrcsxOD|Uh1>G-T{MDt!Re;tNM;>X%|uv0|*YVc)2+DpShA|MMFY5SYw6xrE{!{q@nbQ*9Eq_C~ z5}Bu;#pRClEKdN_z5wA)jSeJi1%8?h*f}3RCB!UV69qFB*1foA8-2T)%1K;<7fE|r zfEr-dJlVGj(%ih^VWa4LX082mFJvVM@?Kt+Sv>AYA0y-8tQlB%tx?mfyHzv9HBaKU zoZYS>)-Lu>f(nBe7}=V8CEj_J1XBtG&AwgHSP2fWcF^&->V8Gn#;q<2L?n9b$63?c z7;lY&g@@|j+>)+wLa`wEb0!kwxNPayXZL+Ygp}&R<4eT>p9d$n8{ipmn-{YqC6)Y# z(K{^E0DYccTG*ir`yr%I+XFzS+Nxh{ro*V{+bZoRDY`EJL3?)~r+?)h0v8LZm&mYX zjW4=e6gfzrpvO|!|0VkPUea;Vb4pZxC}(&@S}wf6vku%RchvK-$4rcJJ26tAu^;7kWtd)?P8qtEiwoes*Ww$aZOFe z&tk8~p3&dBXlLpbAf-o4T`TV<_tI`mwHbz)6~q7r^+|uuSdsXRWfVf4mCI4lvqIs3 z?=4%n0^b?uZ88LY@{M>%6&gH%&TBM23TQrF&`VFg8Bp!us{`9jI)?#Fg-4mRP~v^h z75u>?;tSmAftFSBmeVmdjL9D+Ef+ISQjOsqkl>Yz(4|d$4DLFXB{nzW^QEkeQ3}z@ zZSdxK0Y5PI1~n2=p+Dq=iNASkkDd>_-uHe03iNjbJLw6r%oK<&+Q7TN6r%V&H#Y6_|f2Bm2o$?Bs5}~XOU0b{9Rww+5_(0 zhu;8AL_e-Fyuke+8yht@`e``n-PAwU5P(9~hw^rQ0CTc!HSzU}lcN0slqXtjZl(q6 zpBj*NmpO~=4nmnY`Gq&TSNfFu`;*7+Rma#gfe&siXe<+ckJ4*e{pGP-=5#*zGQMq} zpzE#jF$;?lz{H6m`-I{C{%yyq`Z*gj z4XO((c3KsG;NEJx1bizEM)b;$Jqq{$1Av^{434>$E`q&3pLIRN)UG zrMmf2_urr_N{Lk%b{{h=uczcGPuJ4?KOFHbdCD;n=kS_LGJ>=s`+zYmg&~97*cQC7Esh6h@z|W*TG`hvkEkDxdTEWl3*2xE5HG575)~-6<8rM{?IurV_ z%+yrKIfo07LTO#F!$ip8%1LJWk4eK7zRbIAea;mx)>imP6^a4~GaxZWCkM5hFf@p@ z-m~8_kpCK?o9;s%zDQ%7o?PpEa{utu-pUFkY@E(#?mr;YF(uYXUuew(t+01a%ZpQ3zQ@Cn;JR? zJ`X`@OiH*$m{p?Chgw*;v9g&&7><0~8f#^-x9&k=dloj}6Hy)||>tc|cO?P@d z3XGnNnez7meZR>f&&uz!mx{HnoXH#vhAuYHxd)KiSC3B9X4Ig`U#p2NTwX9f`cf4A zd=D5XYWh(Nzl|Vcd^Ol$-*56ab9#i8T@he+m0-{;UeFWgV#`E^6Q#}S-%PDm#82ykGqI7J4spc?&V|6--x!Cqt1f#hj7fJ{;w~=m({5R3&vd zUayo;(C;rSeQ2@I3tl{S85AO7(6_9%O?QpP{6YWtmI+qzfX3+32{AdB@RVuWE?UrP zyrqVEE|$v-y8RPpIWbNPwXoVJ_#2u(yj|NvzkHMQDeue6yYqfPY9sg6>9cKPNI*IT zv)Bt{?47MG=alW0OYDKk9<&_?2lE4VrQ?GffJbt9GwwofmFG1WmzmkYOjFzJFIpPc zpf^JIfkulCHD^~xS5}cPAxb8oI?%(>T&FpVX+)WYgklkFGi8y+q18t2`@<}CtTjCx z^@^fa;bMj><%EKy@)HKpoO1Q`RQ%pcF#S{%^O}11F^WArmwJw{Lr181>GrH~P4AjW zqcWA}#yxwtj|x$S_59TN$&k=K6;B6CGG|HW3I*TXb9Q7xoLG;S!T}GB z4x8siEf#D23+1#>$TszPq2_NW?KG^ByWO=r-#D!g2R?G*V<^IuSN(v9CXbrFbOKAx z3W!bW0Qmc~ymO5c@Z`49KZw)77m>z*pCae8dIdiFgm*lk`R*R2JtSmGy>r;KfiT~g z@G6eB33^5^{FcF0qB5;->@v`DMw>^#yd=y!Vv^~_nU2`2mwk0I+)~=qZT|YUd6?9X z51Oo?w*>H884BN3#v|8Cd=@$nO4~nE6?!z)G7Ik!upz&#TE@ESPm&F`*NpRwQ}I<4 zgz^5bFR01CxCI0>XSKAt$9e7}0J!@Hb7r!Dtk17?NP5+G@UKFn=`hO?_|5|P8QAv{ zm{x#E$gvH4S)-54Kn4h}U&!eO|7}&-;mcxR_bWRW2>)90vswP2)IWEGUWU0%-k>Fxc4c)sTi9- zpS|-*_E=pR@TljacaxI{`Ab@Q2ASYlqYDzFD?fV<`jl7rt_ZfN;%it>t2He0b;{Wl zsZq+lxq!uVKHipz|Lk{!YQT>yuYU}T^{u4EuTLV#?iTNxk6OMmLP8JTV2HahS>BD} zMn(T%YF!+5G{p!pw6@qX*MKio62cijvr9OcG;irEA!gS_|6O~xu?W;y7&52aGJg%e zuQ!{zk6My_Q7i16c=%6CT=W}O{!?Ih?>)fOwm=bw!w$Gb>}*Sg7A!WOJbXV&$2tj` z=ofF)S=L@XEIFn7^~>vUAk)VV$`KZ=(Es_KM5m`sbFDo2)KJ4*l^w*NOV1WFXe`AC zYT>?I?AiAj!6$70!F-#S2x-FRy+#w4PcD;<$H{`(OLw^lfpC*sq)1w?|$!@UdvH}h1NINfMrnA=_W zIF4<`p);uioOGJW!*CisbENcg9L+co&HCDR?^Ms0P*@4I&b_uQQP~{0j zslj$H^Ip=qgG)EW8FXf-p(UXyMKEO~5){g>Uwf2tmA?Y>sWCCnoVp*fPP;nKAQh%yH)MX^m`Sov1A5emglyuE&~lOK z{iG<6^)r0Nj}H{O{@i;|Ps2NHrLcvxs0WYc)_pi~ULK2&4B{R5LS0?;%W;xL`jv7FGGnRZP43bRtFDm-7onN5?M^-^I<%%H9SzwyLgi{F>PLe z09x7L!>iAME;{Mw!GI=) zT;Tc!yk(Gh^driMcyEOd(=f>Yj0dv2D$zvT{SV7`B|XaqY!%#rp~o(`)tx@F?DVL# zf&Wl(uT}C5kc(_6k0ysVVCjxk8ebxh_@P89?9#Pod-z5?{O$%vabwE|6KidcfmP>A z8(mso55vWDHaG~l7X+%q#s+d@;*SACa)$||Lac6#m=0GbGN=!Zb4#vCLarZ{vG=f5 z+wMshj3FN19`FyP*ii*M{7yCeCNr4e=-~YYGaqip11)Afpt|76O8GCIXAkyx21GbP z`*0+zJ&bS`oVVIyF{}XJF7-gYH3!IbC++sC{sv6zDNnWwBQ!741hYKBSP)p_eGQ4! z(ZX+uv*QQirB_wVUv{{~5|+Y<;A5SjVxmP9$z>YtN{%x8CZp*OKWL@*EM zof$l7t_9WVYpp8rLGgs7=RKWlBz*bz_p|&PR;7ru+KmL1&^8#y?A(kT%(6y$$Td@S z+}}|?>Srb_Z_EVB0~rTaJ}zuZ4_GttvqbZ@@hbj>2f)7$&Q5A1{EmxJ^hj?$y?@E{ z&MjUu@^Bd%{n(Tyw}xpVR(y|e*bAim_JJ*rxx7^COwY#g-^1L&ryZYS6#YJcG01h7 zbqA&#gXl(o6J)J6KxTvEtCOvs7xQ~QP`!HrvboW*^!T3iO@R6g93uYX@Fu;@SWd`r zLV8I7m+UKV^bf4m06qw=t^5W>n z!X^NsQrAdA%T3nd%Wg!g_`0Bh*-3wECT(tKL`yedAyU2JCRK`wN;5apLf`UzmsxO# zB)0i3FWeYkO@A2PVmQ?-zAiove0}kzWvDv1%X!Npy=9uLO(0XE*WCX*U7A20YZo~QA4^+onQTtUr#gAd?btm+ z@O_g4ZW_Z>K}sf0AR?M00vboW?R1buxWv>*tc9+4~Fc zWPY2q4c`Ft=b$3wzO0I8yr7Jv(fa7}sicN^u-4C^xQFiNw6nTdd=|XU@JN0XoZ(zf zz%d7GT`Jk%_Na~zO0=w7IsK~$C|kTb?aT!?WY0)FSq0Ojljb#i&$_{XTrqd~o~x`y zbH^?7r4zJH?m_PDXjVlm!gBseWkrn5UgZ1?-n8}@cDG+(%bEf~BKhQ7FY&LUwzvrR z2Id-zh#dF?H%sv`@{4iQOgm8#YCI3%*U;YpE`u{yrsT2|+ zS+ax_A=!-*LP%xHmOYex-$qFo+o2?~B#ErqvJPcQc9MMy+4p4(#?0KWsq?wN-}^7P zfBF34oX0sc@8x=5bG=`$=XT8#aOlfusNwX4Jn{^bMSS#`8F;_;~y^#w>OsoGB z?wPh9o|?F!1WL<`EC@NCwqNeQ-Iqmi@iuVQ{Cn?FsIHJ{KfgEX;bi*dIDzmYpk0?E zuwG?p{uO`BmxAxua=&<6|FNnJQ#V-s8p>VLojtp?0~-l8*Zw7^ntiD6mKo-CR1l3;Nq7;S1Hqi%6Gg`z4ZNw&}dWf+#?wd ziMys%%`%REUrCnNUbD;XmMhpvpdNYUA&Q{6IY%h7cJHMx+wrzu+Y1%HU(zCBGZ3d| zelRfBIW|W3@iuVaiC6013Ns|_>DdpSK~0-8<;xqb6mne0)|M*93%SweVo&I}oxp2; z{g|dko4W~m))$`pIEP^a7H-dh>-RprtH}IUeX=XpaRGvB&C`xUi0rkM#?llq0%o2Ltc8Xc`U= zQkx!_kSziUHhlfV)oiMzxZ4uB_SLNlx}vXDf2vA3S)Am^O1Zt{#>4bEgcee zccu3xj5 zaD9+>=iXc1#xi&!Iexf6^v(x9!ID)F4U6y#@VaIzL{GQF9qkazS844$O9;v)*$QtOx5-H!{Y$9 zY#LUc!MM?#%qH1L;0s0<`bu{yuh{4|B)ev-+xv%;_VQK|G*XTSUJhdV@I1u^rzz_t zz%~!&@}-0PeXG;WYe^^_wggFxWKyyQqCsg0^nzW|01`3hWr5^ zQqEO;!T5s7(8S^#p-Fjc{wa4mNb!3O_!;4uv2d%R{9{=LAG$>GpQS!jF!Zu-d98K`6q>}RA-!#aq#WAEJ zXzA|`^-b8cd4l#k#%$5%&a(y5d?azPtP*=^i)hinTxtUcYI}R%TdSwI0e(x?zcKP& zPrW;xKTD;DO7O5l?eR?)`S=OS4o+~x$%Kr^T=9Q0-mQu?-M)T!pnKlt;u>wQL4^_5L?qn2q$HQ`s7uTB6drB1`0_$&ygNxb!qE!aBu96WLH=$slrJ@?0+0 zz>k?)=N#Whl$!{~{9l%l5TIgn(Jjv)n{;fVI{S`YJ1h_*{`MFv_P6hX7f7(W4COhv zvQ~MT^8m>_SE2|OyIYn#iUd=igMmN9CXTmUFh(}BOZ+UEc}*1>K6&9OOn~$PQQF&u zzzC+qi(5Q#Y~9NatxR)~gvgA?Qh>eG;;m*VF6hET@z)_gl-%*~7vHnRGa_(Go#DtHbtDYSBh$4Ofs6q_174I#GrJVo;l)CpsG!^|BeS&a^jd z@=Jh&k+<#FXsxc;=^glONQUKt4AQwgK`eLy>G7HZ1SPVcOYCQ^gA0B9i>$&U{q#rI z+-+TZcxQ4I6R5Hdrk$wvi>&nt>-0@MKcrI$cu~G9j``K9FmOz5<*^I>%DTk-@ zySFBJ%A*@fFCUH2PUAEnOS-1{rG)DP8j2W&>O3*;p{VGP?L|dA6GLfo*jj6RR@`&0 zir_w=a5gxt2y++SinszRc24E%hi_)K<%YL=t8`tvsAm=mQ5uf=D;S)B4@uQLazj&e zW`SCxM_=W!iiqgEhwb31u5O;vNKllnfxtI zY$BXbiYhQ9EbuhC*;ab0y-`Jk_O>Qj z1~qO&xKt*?-Hq4Ey-fQn1AL8;H}Z`3aLe`b-$Dk<1;z{1uzI~9>-Lz=nmHtaUh^Z- zA`*?1)GRL^=&oPcUJVC-1D=-Jt0s>eDHclc_^GzHAvP<9>(mY9P| zHnPk+Y*a?XJTg%%(eGu2N9=O^2b}5QBM2+bhPi)r1(xtM0qUG>w_o}Uu~2P_X=^OT7U%{aj!;Yoauc?8h;cEkI`c& zu&lofq5B}Sndtt8)KeVg9k_FZt>I~Z+vc(AoMx)u=wa|LAOsG5GAOiLaiZ?EFDuit-TGm4jNG|d>AZUTg|qKaJA zOf^3&tjU$Loa{4RNItssJDjN|AUizA^B!G;eu9J-n4vJ|aieoRyJc+pX2c40=)4(4U2^UUs-u^*-i?J6~ z0XRv5la|ZhPUYF6X}eh6 zahuvk6s?{siAE8#NGqdlzZ%sg*Ej3=hSWPH`a-`LLA=Q{U$w;_y+eO=)nT}1ZZ|ry zKY+-`^;{mI3@3@bcs%r)E~Nrt;*ApP`*(1sk zksKcGwqg0_wu56Vf2QKs5Pg++5Jf$-63Wmgdw3o!E`MIYspL2}+nFOcp;AyZM@i3p6-c<0TF#un3O*Q-dlXH3#KC@eUl|&*veI zeB9>HR~^x-!wq`-+Xv%&|A>7T0fwwJXX|aGOY)mM2Yx3}8-i zPYCtJcH3x`n##YqX9QNb={&EzeaPX6=5WGV{ylWtPQE>_kIuTTTkGMZKCD6R;CV>L%nz<=Z@B zkO@td9Sm^4zQdx|W^yv+Ym)H8j%2dG+efz{ZB{?MFJcV!G{HMXL&>-^T`5}7qoBq& znpB{2R?KT%yE>jgVA)>P%EXzTJA#OVbhs^m<-nW1UW)6jQOw6Dp!q4cT{skO-n^27X3@b;I!6VLn$S)Ho9 zTAzIRlpNF)uxr?0sa&_!gTR+W>XKo;L5>3wNnT7ggJ2I1{2v_EFQNT zpZ|}W-Bvo3>Z0FI8J9|j3d8ar5*zm(ZPk1S*M|Tsa7=?kB@aW{-%hS110ag>L&LgC zj}Ccs%{2J|G61IZu^WI_!^4Mwq*_c}2A5LO+q1OBYz_Z<(sOy_IVTMJ6Z&=3M}G9y zq}@NHx6uF0GBo$TXd-x!oeJ|*MbJ1?XIuxibu=}_3a1$XdN{p&0C_EtSEI&IRzTN$ zc&cZ)h0i4CNVJI3(s6@~KzH@5J;WgNy`SS;iC)vNZ(Yddb=W=QLjth`gQ?ql5CIGq zptY-!brN}E1+4K^wYipX>}OBU&0bkHBIo1q^WFA7f?yOhG9j(KO!dtSzqpBj%2C|Y zBabH^4TpWRwhWGcbmK}C(?{0ZXU{E-_#$R98?g^FL0WFgZn z8WwW|z5^7TC+i-VY*p7fSF}oHG$RVZ%FrP-G9nus*Wk;r@ca_+>|JuzV-mWa?%9Eo zHw~azYB?h2i)J%Yi5T*`bf@IAkzl5SbvdjCb& z!2>K%pHVs2nL(A6%QF_~Br!CUZ%aLrzG$!;BzesKi6*-lRJ&i@+bFBG%X8rzgV#NA zt@of`$R=gq1o;a@+uXjuc@#tm^ed`mm|su=$!ykK;G%3$`qy3!3pjdBStx_{`3 zMUC&IxDP257*40j%VJf{GzEOks|-R9jTdo!d&((8Ct1nTYK;~nv1TJT9t_Fy9D+yv z2u{#s1H3oOQZW>h@aob6Rm&AnwYbq`tbE_QpYx&JQ|s|>Thq|0%jjq*&^1cy7g!qa z;P}V-(!gIGWS{UgJmxl`Kf|5SxII!oPvE1W;B>B=-dTw!=&1YI_kpK}{7Tf49%xH2 zr(KBOqA}lDEU@nbxk-gaO-k_rZ5pnezKCz2G`>2hc!s61Ncqjs71uQ1EgkDM^M?OL z&uOkAL64xy@RSI7FVo>h>dL(Wq6$XPV3!BASD%-8Z|Wiwn&zNCoIAfc+R|Q=GKOqX z`Hq0_9dC{8e~8WPh>4BT3JnkrG?`}LIJo=xUw~W-%4?gm*H@8{UwM>Ks2%VoJ)w1I z8y};$?TkQs%ff~v%USAKWA){p)oz3jL)T|m+}@wL>-4UT?D35}^AJXio9etLgX2R` z(LmP}zp^k6Wq*g{voVy*NkpvRGop$qHvW=};gHhd(CB|CqS)+a#0P^|W5sEIf#M`F zFR`!-^C=E4Sm!mBr1?JyGL2DZeqz6BhB`I-lq$g`+hh@^ctjVz+XE|J(-4JxJ%)OB z5MQpK^RPCGyAL8ItM2xE9}AnOsOrZ$ED+Ec)!jnOh?1hjli1-*BSU7>^Z9oS1o~ zCR|;YXW6ccv`9leffD!5@pr{Z6~lF}^Z6zHH-=Ka#;T@uS2i=Yz15uw!Nn1GkyL&^bbT63->2`e81_Tk1E$ z768AhTuGGDn3+RY`4zLUsnT6FrOEFB?br)JhfS+;ez~&2PNQD{mIX0!tByrPl_?{> zXr8TQHUu>`s9F5Oq&yx@yU{6*u60SHcFm*Gm4c~R9z4h+H@jDhTw?vo&&m}ycN)$0 z7l&TaM2Fv;J&%>VUkwHjlgvemTQX#^Q6uH+NghyH=k;H@yA)P`AB%BBq$L> zVgBALbh!O)o2pe!tmMxVWOCnR(ohtX)JLJS^Dl)LlGwzj4b6=S`mKS(sKM*CA0x@{ z(*FpX|546HEkQUy)yWlAh}H#d$4`6NIyvP^_NhBLpeE0Z;P0m~_Zs}q+M%O#k93pH z{^g}Cp*+C;XV039rSD8&$+)#W71Ujpvs1q_hbFMiJ}-H;O;}9ayYO7xv*NiXn;(Wi ziuNkvj^CZhv|lL*hKp$~Wx^$Y z{C9nR`r{Q0hE=4CCYULb9}1t*2}s|m(c6y;H+I$SwcHtiqCnbx<11NrRvrZ%0k|g} zc`u$F`H>IH9)QOdr5l!kUPoBon#NTG#7g1THuP1`n*5%eC7ftA?$9Q1&qI4|XzU$> z`JLr*rqL7gyTD+IMatfp-{)GwmtKlEcOoXZTp_PwAtOKNKfV24nso=S zhr%Tcsu~o=V<|6kOVafO4<@mgp&!#xkEHm@Fi65#$4^_eftf&r#m2 z+&_TNx0v*;)GM#P_=njS^VY(0aF|VZvV7ib^W2EE+V1}*RSuq7KRg_QPxL=i6Z0Te+uim!x_JBe!T@0t zfTEJ>$vVyFcDlp|jwcbH%C6r{BkKvPWK6aqbu0tRkFaoBxiu<%w!Kv?9XiptKVgf+ z(b(kOd0t5|aKavPjU+XYodLhRrm7>t)Hx&~CJ*K8=S~Z)0y+Tn#4LxIwXykP&K$vl zgAVbDw(jxUOF6)EP1^%1cGCWylPFnrk{K1LP9;(ufs~z|9h<+S`>hKf9kWI;aUv}G zvcd*%>BStf@+RqCL^a@@W-(mZq=6&7BqiXv&JPFZY!M3~93FyWzJlKWZ)FMg%qC@L z-gUBa;H=@KH$vE3_;28qUirPBSf`^6*9r$O3KUg~i_niH`du>x$iU5LewL!n>NhE6 zy=>&`O0w6{S%TKt#}2P8?R(%9o#uWIkC->#?8rHv0l*9-Bg}N@Z)I^dLn5+rM~(L$sn%}iMHWZaZailUuN9i?LU3 zKXQLrMG|o=s7f?EW1*DjQHc?K%5bO`o z##m`NMF53kkO^Ubw5eVFf?04Vm#?bzu;b-(o~t&j<=u@W0mJ*!=KhDy&cpe*;#^_6!<6L zZe=<{dnnVOtO6-NwV3n!Bm3l6wL=1WU3z>VC-|wt)&P==agH@)`FL+eRZVCt^=g2< zE=sOLQ>OE#437FV>hjN4)4dA7Y9pTtCH z+v@_-W#pX}IY*fKLI=ALz-^m$CCQ9(q-3qv}RCl?FN#b&y|3l`_eM zn$0wwA#C~+n`q1T>}H-$a^>P3I~RMVmi0OU@De0EZYNcKa$$g$jh+=tA6L;lFFGA)xzuoLt zn~WgN+`~^7`##`x_A*TFuddoJ;K+mmc%slgp_)9{Z35r${sHOrj=P;I`^eF$?7Ol} zmz06>Q291EKb!c8<~Ksc`MWPz-APSBWKi&vZzgv^tyFKo{H^SOE!hL$&Rvh%D8TKa zeE%tZ4=)1M{e-*AdY$rbVZH{O-vT$%xctPP-X=q6IseK&5n#9G7dWwbMwRtW4(vGQ zP0xc#UeTeL?hvh#_!P%F^cv`gW68+MydwF0lXp4gJ+ldu!^>k7+q4u2Or2-yk==AE zqZ4cml7(iBU%l|9M~x+366|SbkCDAgA(=+p7+9q{pL8&8VO^J0n%L$B3nt1)|00Bw z2A@RemDj=e9&hrXuIKHx@RaJ__9(HIoeWYAV0sdIW;;t#k$HNW4hWbs!U7(2Qrbe# zuVdHx`36~Lmp5e<3inb$X))E4kBU4d%gfIm5wqFC*jBRH7NRwmpko5ExxYv!y4p&~ z^wIw|cloy=8qXndnxyb1K+GF%YPS5w(%KAlrl_rQU_!yA{1baCL51MC>Z}7lLmtb( z=%k04sWx1K%f?u`v96A4>(Lp~)dD=|XmsBW z09aF8|I0&XC#+m(`Tl8tdl`Nl*-Tyn#JtErHW-;-Kwx73NkrG;b<+jUKMQgT|GOY& zR#3szOf5|_z0RJ?gLl4Z>8#uGow7M^t^UjMWQaf%ZfMNAZgqp;dLXp2@Jd()y!e<)1H))mr9Pe%EBnNm(YWW=qn3`ok9d z_G1u%TaB1hO>c8&8s5t;JJUG$)SE{2jvL3w*j(orXSh|P$NgEh|e3OM0|3Qf&)5}+{ zV@=~kA%&v@`A*n}RCNqBk}SL@7{Bv~jQfy{IyP+9`Kgw)3uT*&!!qvmqGgXFXTwI< zwMQ_ny-EUhSxbsKmBtqwA%909ZAx4Uh_n~ePQ-Z;YXWh zuIDB>m!@(x6W$0Ho}yW-0?lh3hYzl`(l2JQ4SzvY;nB19Jr$ZO9LVM*=PZ#Qii?B- z4qWG>Ekd@}o5?z6#$%#=Mq=G2F8732U`2WcTLS^d0*P0>>VD)qHIJ0uKu6g1#Qscq zfjg!W!A%1<7=Kkl<0{<8vC&r9J(HdERsTlQn(t+^-vQ}+g=dA7EFSmY_tvtl7@Cw{ ze^obd9pw=#)bdzGI4UC)YyswgJtMMtg*t)4IofG=B*&kt$6Z={@(URv(R%ad!+;pA zfCe~LCJv3%YQ|r3du$-85OEo8k)ux-cPhj<-J4etp$~@*g^oUsW)$9SWh#zk_F_%j za|Gg6OC&S$q80heR$!3)!^`@KKem3*3D=!0DxrCGmR%FTL0Aa|f@hlKgcn71=_!4! zBSpQj1tQRx?*vWtiXT^MOD_OOt_%}&9I<4gU#s^*R3gp+M{}DYh=!j2qAYQ3ss2i4 z^Hz@N-~px^zap<=k)Qh79*a)V>YlT9SG2shCcXQh-BvWnPQymtM3^=-2++1c4eL6y z3zE-|sztDX+G~zezC;sLK&N$2BC3wCN{9ReKkW81GxT!eleM~(p)BCZKV0qn=KPl5 z5;9&~HyM2Qz?~g>$Qp6mCr110{Lh#~ncl4e5f#6=i&fCj7!kp)z7$P(rd>InATr(5TxqlE-d2_HC zq!WOZRG+^q)LUDrvOe%NvEPzrzEc)Zo!oKOd-jkNdXE&!YK@>1o}k}tI}hIZtDsB? zv&@q&B71beU?TNtU-x(j)BfaDe}MnIWj4WQbeq8Ov$O4uU9?>20!JZaI8E}p(M8MM z+#EA|F7aAZI`ro2?^$v=kl^f^!gHo`jGU^=eb{d7o@)bVPl^Qtw*jOTRX^_s zlh}z~=mDXc4zMS-Xv`-5>_u%@07Ox zF|#!GgI?0|lQNqW#7aY#pMLl*f&0U23gEE41G|H+;MiumC_c!w9%6_SE`mUur3df~ z1aJ$cvEnW!Ay?obNcn`?wl+?7H!2hytMY_Rdj5`w7>tK0gag}F0W>N6D4i^=CkI!J z+!pi9W-~F}?nPXjwbC^qvI)ofw2HKo{K(QvBdSb2E5pegln`qH!d zTk6lXBQ{QhJx<)AJV65+?E=%NMYX)zVY#dA&`KH>6wnn#;QLMnf8@IPaP4^mcg3C7 z&z5eG2Zax>L2e@WwagB$zC6iT6F?h>-llF*o#Uj*?geyE16o9|5xq-{nell^tGu&(|m1|TLs;KH9+=fyBnSMdB`FG z!ir4ITyVyajA32|=u{HThp)WSjwm|mu?jRuf_TCLhZT9<$;pTIlPr=nRV2&FeMqa! z_5~?WSN%KzVE9Vg;~X*-V(+TTArJBwe>aO{J<(zj(i@zCn_>V^9bA+%R#)a=v&l@huS27h;fGFvV9XlG-i2VQO_e#$chS#E<-uVwI=>xm(}hQJ zHad>hf>hqE7P6N_(B1g!=42r~-!OzT@hdk%K`t7M9%uG<3?>P%ATYu&k35$0pCGq| zk(;8=D<=ZIntct@q31+-)#j*s`k$PWOT&@o*8vHLu))z>fK5Lba|S+x>ea_^=Y8qy z7FmL!C0=se_x6DI?hA6d{i5AndN_1?x$kUo=Adw@(kcgI6l`~)3+z;fYLhfyVdQGwTL+zT}`yc-Y`XZHI8WltTDtSOWVYZmlA^u@-h}Suo%k%AtnDUt5 z>{g`fQi#@-Lj|GPY4O318-GnHw!_%d2>hkvgV17+8*GbZUJeR@y_Puz&};y4uUDQOO2f4s z0x|ZQe=BehXqT>?Al2IJw|hzo8np6%lc#1P0a*=T-=nh_6c6~)oA1CqLr=O4o+9{F z`&)o)D^^Hu99ofu=Rb~&ZzJWV9Bb-T)e1Qfc&@fz)6{!IQrqcMa|CYS!fw9ny0-t< z>(r=bzEt{Yv7g!4IW1ZG)fh@3*pUeNtKX(Oe76r;I0jP(jM?saZ&Nmjp<`|zMDX03 z<0RsG0#?{M^fO*leHkXl61^mQs6L2n5!HoXI#BEFvO2nYXI^1JI5wS`#w7J@Z7x$q zL2*z4XU@MZxBu4_2Ftmlz&(;}4rFMBrO%)Fru0>y=>F$#rd4gz$1KVISKlV}c;g4- z{>Rgq!(btM7rfmGfsH^LSKIK4cyykzqRx(L6Mxaxp#Ag2I{jD_)Y0y(el}xzN)hHu z$}ry35ck9exgn}52!$WP3ufIHQ1veHI5)a=WymbnSlm;EB-~pMrMPwL-nc-}V>WL- zH=ihC45E@^y5pDz1uVX=0WdPx0lfjPe|%;mm+OlV?Dg?>?*bIrt}7F8UdFLRD2j99 zA(N!wJmp(MXTq2cIipDgO#z#x9R%)qn3_10A*6UX@cqg{ZK0Zk=4TsrwN>Ow9jhDZ zK6oViM?Nkvq%*??oI39p&ks?d7fRVW|3>K@(63A9cag|JG_F{vKMQJnd zIgBtKI;vL@HjLK}DCsU6u>I^Dk!?)6lGJm(Ma!#|ef*cO@tj2IZsi3=qmQPi_Fh3o zQ}6A^`L*PXCU?>oR>)72I2QTtU$1K}k0!a0%uK^3$~D4<5|wF2!4fOYk;EUt#M0BI z@n9dnb{#ujt6Y43xQ8|kM@uG@1|o0E$(D40Q`d2EEtl(NC^>-WWF#acL9+T5vDMHE zpE8;YCH$WCc(iw*WoU%-#uaGtgX)({lmI16_8zYE|q186s z%#Dn|3%Aa95y^=GVz0-LpY$L()dceYg&lf;D+|tZqZ7Pl*&Ewks0ba2uTxMK>je|9 zaETH!_qU}y{)Jx%1qI#rYt|hMG9bJ8Vgf-ozQCQDj^xABTDas!*J_wWjOMTdCU)Y) z7V!mFbvNsMFdq1#kKdkw5!9w6`_&s1ltn(#!nx&-NW6CcQ$vrm$tqN!fo&6Cc_3EM zOf#>tiZrMj##9(Xh6B*%C&VKrix_-Je|dbmzEui*=tt299Qr^Y7HLCk0g*3xEHnv1 zNP(PJ5r1VB=Lo;Eh^Nkrx|k>~T(QY!L)Mj8zBFfue{-7^a**r ze!lc-d(tX9s$D`?Q0s6gh8&v(dyGK|Pa(cRZ>1O;O#1~gw<^0vNyk)cWZ&l|ib*@a z2Bk_!FYGL`4SPlnXr(Qelqy2TflB{fxdvl$@%pU3t^MvJ3vqYK_bWW(N!@?4Fo3eE zg37`B88n?#z5faIM5Xx*4CN<~ca8Bw%}`8U0M?7$3}$?v9?14IC+fQrdtpx*HsM%` zNs!8Dv6w-ESkU74wJ#KAi>E%3>Qa7Pb59($g!N}>?mZi$O4?rSukZN_;}ER1WXUtD zW10bsFB7|MBEVXb=m-zDg{>G!kH|*7rGmt>v;2Gpwkpte{9f?}Eax=8CKo{CLT4U3 zR1frh04fJpN0rWLQ_j(?gU4F7ER1{oh&mbH{0GfxSOeYE-1Vop0 z0s`|nDPJGFJKYi+Nx$xd=R%`@r4N=-U8ODJ`gUCik71X%`eiY z6&0j}C}?rrq?7}l3O?gc6N08O4-0lDtwNI_)=mC;ph20lMs!}WRaOa}N} zfB}4SzSx&)&J4om5zuRmHoXR;Tug?2sQ)SX%ZD;qUsaL( zVSlD(-x-R%`{dsnBoM<*YQQqTbr~jP3WId0xPY-|A;$7JoT{R@hp2V05v7cpil_X#Sfe%cCH_x0m`7(sBzL^-^hFhL>>U+!=FHFl}_aL(M3Gs0zEr#g?0GUnTF`67eBQ9D>tqt=9s_z8;5U z7jS`Fx%b|{uUOooW@{K@s`&~^&EGd*7cW7remW|i2y^dt*-qH=&bHOV|0Jdbh_tPI z3oxcXMzhGi?T5Lsb$Z-bam4EleyXPiU@^?M*ZCn8M69aEP=)dqv0D+%@t#~_`~B<{ zq3yB<^l;2Gw_@@-Kwg+|$YtaLi~0t5TAd(}9Joi3Up|Q-Z++EE?gCQS0=~VZ_PHfe zhFWDOyDlp)gz=11lSwVuh_6ku^8=T63YL27JOo5mp*3;`%c{m@r|}uQ(xGxq%*{D+ z1)UMFh1MG%3d~~a$~w}&0f@wU{uPmP`DHfPv{P*vO!^{0R9o`!{lz*_-C68%)u=GX zCIO_e|J>#(ncb@R<7=+NC5$0`6kk()Qbv|{lJJW8LcC$xBa~<{FGzu^35Y8Fu!ukY zCwsK!_M?aXcn9bPKnY8k8?y;{pqrV)K$JdYc}M#!dazb*j$F*Ey89m~90x)v8aDY# z{j$RLb`_%PDr5xcSY1f!97yP4>MTl?0qxKpnJj9e-Q|G7{)aJ# z(ozU`zmI-N_N zdq!3x&rocuL*?~{ZhHZb6TsC#c^xQL*CrGVo)y?K-xVi zLP}=Lzrd>=JI-WQ4V-IP=8*6wUn+i3yWBpIHj0HT+1fD`Drb&=b(N`Lo`2_jmLc@V zQI6Vi*@;^Bcy$A%SK8HjFdJ&To)boFA?@@Br1SFLhLeNfwG&rKzPP8Lhu?jg;E}T1 zb=2*Iwt1uWJ_IO_fXd&;K7q=M)ws%YWABFn^eko!l)Q=%vUxt2huCOAl<};}z6ri5 z053N*JN{B9VjayxcWTco9OGyN#uxUkig;9gg$|6 z)5p|hasv)KD*8V>h<)8=+{d$YR3^rqxrF~@vWn1tk}*3hYzwQF_FNk`0`o&IrveCD8Oyx zf@*y=bAL(3&vKq~#WLd9q2@$bV>B#4ytxiN61CoZ<+Nsp%E|O{@C;~rJ6b2%$UDhm zP@E9Arf?Bhbh0qR?Ty59ns+<~e~lRAq!5j-g6|$GL-|F)yqr5u z=dtSLNG1dpACj*gB5Tcg?$J_12`-G4{|X%bAJE_AQHRV^P`;_|l#gNGLCi_#?-y?% zws4#oko^y~#}|oDydbA`o!;TaUu54^KCU{h;jfkv_U=jXm4w-sx|W8)TS2s?`;5QN zhN!{-#)6!Ejrq+Fr!Iu&Z_ejN7hkZhCyd>FmdACZ7iAm=(Oy!Ex9>Wk^+h5?8RRR; zAvlIh!^O=l2vRM^H@q;XnntpwMvlo>w#LlD;*&)i<~HYhp3`*nLKLz=_dn((X6f|G zZ)FH;>y&=JPANdw(wLH#oxPn#>u<}R{Xr)?JMYnv+qYb8Szo>j7 zOEN-^MeG_C6aJ=cEiZ^TxOxFZ!LWqh59%WN|cb7JQ@8sBCN2; z&Njhtjuh=SyjCAHoxX&%{(%+jKba=tjMeU z{d27i;h#>pjNHC6Qj!_4`2aaiSu#xee!F6EZ(*aogP4|6K@qspo5E>QBjw>Bm|ky{ zueC~%Oo+j ztQsO)2=dpn>BBYr+$@&P2hWtdWsd1GP!zDxR{!- zsqI)s(ot`1@TzdLxY$S7Og-%1b^@{W+(Rm0e|n&>Dq!jXGKo`{m&RkVB+kmbTG}&j zIhEKS@VyrA^Jv_7Ri-QAIJTaF!zaM8x^jo%?s2(#^4GTyuD`o$?oyr0y1aX;VS5SH zlPxL*=jZ9xTpzTzFwgaSs3SEnb=N5F^>RaVYfFPyAwkj6nG%6{PeA)G^bXWy23Te5 ztx^mXliG7yF;l&-UhJ39x!5At#!+GcC{Mz^VSCY7HPypTh_8GVw!X0M{uwnV;(&M? zU&0rPEEZZ}6VA+^Yd+QRgkCIppy1pOxsRu5 z*dE1-72tlzDd;qQ-5n??x;3VsT>5RBR<0QaAq9pySDZZ51wvw*nDs-lw;SE~I%Fzt zMKKN|n{{{>&R@-s2wQgMCwu~shC(pDAA99#jH8w+kVM#*7M2}`twSQLh5mSeukOuZMCGD)r( zVo;=A45p;|)nuzhCs7aur`jIMuc2bjnq1y3f2cSpZjBG|EV)}9;JtJXDB}cXVR)vr@Ju;MOq&bZ$-_M*?B4pK zfs&V|t!`%NocJNuUzifgjlM^IMB?5X;VXjiZmkSj?M=a~Ee)UTFtfO~rfxmw zDAxi?`9k@G!;Oct)ioEU>quc) zedLBwv5W1+zNV>hncI!TL1vk<5ueB6qRbkn8sR|dGV;aL&Ui)aO6D7#CTBEl+qXEw z77k+@d`Jy1;w`e&boj38dI)VFt^cZ566a?d5)x(N-YRZ2{}w|bpXCv1%{18&@&@IX zE$?vGuRUQpykAFf zyXM}b^BE=CGV~=~T;)6egI8`VVV!7SErr=>s%$fl&VzI!jD6}?RxQFTLzI}WZ(=}bUf8pnB$LrFpnQ+SG)1@bz=DV&P{BUUJ1$4G0-AE zd60gcZ~P%~0lCP?@s?sJVHC-lTJvddgs=6VGEYwE!^28)6ir1D}ma5C@0Ds)T`y6)=xup;|o}xv}@v16KVV> z0ug9LqqQH;Zcm&IbI-%@u84}9-?t;i@Sog%vl2vb}@J07i8Swikx8q7YpG}X# z-`KPv>1uU0&E5XFnz(|y&2=~%memK0wV2Yqc6FM_f@0K7C4_>s#*Hi5YO))Co0;Pe zQ~c8nq?v;#wFwUjy>KLkq1l6KH`uxjA}WF<9gTM}(eZh2(D8>R4%$(z846!T?PF&? z>b>C&61k^@e^EYWfo3{w;c>3R=x)F)+NJl^k1Hq7uy341{By5LuKFq)Y2S_DsnyVW zSHni+`C;=cbhdQKV88oMjW%KB%&3VASMx8VR%At_-qnrX(LU!iS!%J<_h`wxa(82@ zJ)A0Ww~;y9+>0}tUdMf||EIg#!b7V4m($aFLJ_Q;d3}+I=HJy33ML`7#BoVxT4cYU zuc_>&&*p-gSWnoz=F3nkMG%4WH^UI~X;)fWboBvmhU z314mugdK0ZAA0>F4IVxH#*>}nI}d#zFYY!v%jhPg1~NfbrpjH{g!~*8%uT|KE@P ocLe@B0{|Bk@__Yv4dVq!jXJ(oxzUkO50N%Kbj^#@P?53!~wZvX%Q literal 0 HcmV?d00001 diff --git a/Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Contents.json b/Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Contents.json new file mode 100644 index 000000000..78d34c2c3 --- /dev/null +++ b/Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Contents.json @@ -0,0 +1,116 @@ +{ + "images" : [ + { + "filename" : "Icon-App-20x20@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-20x20@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-29x29@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-29x29@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-40x40@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-40x40@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-60x60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "Icon-App-60x60@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "Icon-App-20x20@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-20x20@2x-1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "Icon-App-29x29@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-29x29@2x-1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-40x40@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-40x40@2x-1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "Icon-App-76x76@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "Icon-App-76x76@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "Icon-App-83.5x83.5@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "ItunesArtwork@2x.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-20x20@1x.png b/Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..e8b3b928c6cbe31cb3bd7f8e321c0430cfab8603 GIT binary patch literal 909 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 A0ssI2 literal 0 HcmV?d00001 diff --git a/Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-20x20@2x.png b/Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..7f3a702c374a8efe35d793c31a205cffb1b42ecf GIT binary patch literal 1706 zcmV;b237fqP)4Tx0C=2ZU|>>7EGWofVPIg$%_}Jia(7aQh>TKTf5^ZNguD!53<`H2BL+u3tZkNpBf}F%kg#cp$t|bGMq*j!GXy^Qb%A(Blj1mP$U?`<3c;+SR z=_nW(7@9LMfWjQ2`g0&SEE&blAjF#QGcefh|NsC0ZiJZEeg=k(K%HDW5n>t^3=D$Y z3=AiB79!vFuj7ybYLcQH`&9R`L2c>on5W$tWJ|6>3E1rJF?K~#90?U-ALl~ojgzrD|# zxtrrSHboD4874&vi%_EnNmDVEf*^`OdWaycL;~%GgGLu261x~a1Yt-KM0v~W7(!l) z90VO3#u+uKM(3vEoS8Z2?7eze|3Bx9GpD&6p@LZtKj+_$4CDi7n+2>HNhNUtku;0l zNobBiXIxbkNgAwMVcoT19FJSYMfHPji09~*?j7lgWIp{)zzPRHd&U|a!ImKo+u zG(0ldP?Vbr1v3oLA$!nEu$92KXN0Y%JQv!uEk6gMkyO7INI3PZ7nnE>mfdf8a=PiK z>y`jYu ze{{g=y^hmOP*$MZb*%?-EL4=jJrfM&MLM>r6MnlY)VD&?!`M7MBrRQVZ#isO7~!tl z0)Rmv2aQ255V7#(DbMlWVM?ib>_!~sOfoE+VVH5JA#zhjbslQ|6t>m~hZ}|BTqrMw zqnBWNt!Kq-i}QmYoQxh63DQWAB3fdwW}oN8WuYhvs~)yIeP2qu6NLg@P`cr-PYWB5 zcskkc`&-mULRH!ySvDk8G2#>=)E-=}RLm{8aCZne?ani(>u zuQ%1z4Xvkd`K#*Dd|xSK-5-bdTfC^4tTax6hhX)$u_H zw0FbGU7n+tgr6@vHZ6`YxwQZ3?v24P5EBlb`pR*!9mA_(9E zfX{ysHXZYfD^MM6UGU07mggTdy^AuW=C*dh%R4=1t_Tx~pt%$7E`e{Ji4cthU;$6> z*)xF$qZXc>VX+3f5?T??UJ;h>a5Tr%fb%*wVqwck&!%IZlKfzm0wwwI(J{}Klb(px zvEhkrj=}OBj#-^W_ey2Ee@%OdD`a_<=s8)&gE~pp}pDc|~UaVs` zpYVKkLJb)MH+r^|rD^tdp-P?a{vG=%{CP$!fXDr8FxR?Idmo@!aO z+tJXfaB&CS30ODJvSey1UT>C+c`%%?w@!HLpeHY)p6pD(ZF!ogqgE?POAO{$7(Q5J zF~F;PJO}C(KItLhp*R<=_h@-cIOuWk_M?_Xm6_rG5fD^)j)epD!W;WN(Eu{xG@c>> z+@eaudkbuz%fjpXJ$vdD(s?}~;oO9ScNbXZSD0QKnPkvf94a`}AiTEElVjC$#wgs9 zsfKsvTaBHxE42u`dBC&loI;AICzBrDUuc;-d2qNP0j5J9ZWPw+^H`%>{P;A(`gth} zVSuM=TwCqgUaPg(3v5_unKNlt=H{dv zx#;`o8j1|-${$`d|LTD9F{#{{snCag0yK5N9b<-fS%ndNlML5DwsVL5U2IL?7#pg{ z2p2Y@5BK4G^kg#!{s;doi~?pWqk!4UC}6hoH~csBNPfMn>Hq)$07*qoM6N<$f}02$ A0ssI2 literal 0 HcmV?d00001 diff --git a/Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-20x20@3x.png b/Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..d38db763f8541059b36d84eb9c4844a1a9ca89da GIT binary patch literal 2303 zcmV4Tx0C=2ZU|>>7EGWofVPIg$%_}Jia(7aQh>TKTf5^ZNguD!53<`H2BL+u3tZkNpBf}F%kg#cp$t|bGMq*j!GXy^Qb%A(Blj1mP$U?`<3c;+SR z=_nW(7@9LMfWjQ2`g0&SEE&blAjF#QGcefh|NsC0ZiJZEeg=k(K%HDW5n>t^3=D$Y z3=AiB79!vFuj7ybYLcQH`&9R`L2c>on5W$tWJ|6>3E2V_Y^K~#90?V5jZRMi#7KlkqD zCkc%Ntr!cn6hY|^3$ZPwPQik^6?z^XdocDI$ZgxqsyI5!4Gm~WZy?ytd z&pqdU&v|cSL`3)inf-q|@LpR5xZJG*T<%r@E_bT{m%CMf%iSu#=C+ckA@bEYsJ0l!>2L^_O!5EAt^;}&P zn(JY1lVQYoI7r>V!GHWm*x4r>Iwib40&!2%)%vN6|9K~RZFK~$ zXn@wM3~Lq|KK~K@kdxA;dP#u8k?0u^b{rOtoDw{N+Ni>WAu}8Z9gll3mV}6bC07~N zE;THhYbMN5j1>*+i8?qu4qLk&&%UO^&8XI>1Q7&Zu(F(%tn=3B`wMYBcE@#wdzV`p zYtnNSV?_Z64)hNS_wRD_4?67uK{NdH zR?C8BQ*@*Y1`Z}ZdRn-r(=i-}hH3=@KU!8(bTpx9-CPfs*F!^%dOR{NoQc6;Ow+lp zO1FS>>8M97jKra_8amo6H_Ryxx**^{NRFNo?(THNJ*bI-=fg$}B%G$@ye9bae8cj2 zhWWD$m(^vCb#4@npB1`K2!A;)oEXyWP-QiP03sH~lbQ*?xZU!pD+`A%s4N9)+&>6! zjlk72H9~=NBjfO)TKN8FEnmMrv(yCFgD%=-bvubL^$WF8cyOg=OEzqma?i9dNw)uBvIo=C7OK z;TvwUYse&~$M3Bf+H-|^cQ zg)19X)PB6%;fZCv1&IW|pO)F}Qn~<$J9PZF7iRFt?z7ZT_p{_b;h}2ciX94m;W`E3U~8J?$qRo`YHS z@YGAf!~0yS_sw25@fR*);qhLV0UO--i)bslt;g|ruS^Pj6dV!h;ZD??x9C#4u!`h`<2|%yi(B=5kv5Bxrr#<`T zTP&;Qo0P8?vne$m!9e@o5bl3o_gXgeJ27ax*6_ee%M-nh-yBf~vtftFNoB*vE=Oa{ zMCid7e0Paq-Hnz_`y8FGYZ_<6dQKail^>fL?397S(EI-_-1of07`>wj-`mC#&@vNF z4{Ng@R$~#kkbn&jTD8>6k|oJKf#9_p(y=SevI>?2>X-1O0@ z!%hnr()IMffbhdVJ4E!BE$mF((_`7Nu>`c=Y`Nn)lZsT-!n3cb@YUylPI#L38L*z7 z``K45Uzj_!?y6v5NJskMo5H<29iGc{D(rO75051@joTJxMk9FL*(Yr5a@1ClQt4b{ z#XW`Fy2kRktEU&)f`K6&=|d;AF-bV6iKLY{1)Hs)Iie99%U20N7h&AfE!5FwX>FN) zWQzh0JNn{j;Rnw-#yqHvK+-P@(^1fTmB15-8d78b{5H#H<`fQF6mS^&pKl3wJ?9ur z=;N8O$9%yx9OvuVql>Zk7<1eUU^%g|2v`&+_n^;py5jJ zqg{^Sarkh9!Y;m|cvt|W1eWylZ{Haw>z?tXXTW6~FJ!Eq0UfLFOyOhYvXXt(lHI=+ z^W3SFV`W=OzddwWD*b*LD{PnS>(ckDec=8BpaNX(Rsk+|s{og~Re;OgD!}D#72tBW Z{{c7J0-wbL?PUM}002ovPDHLkV1gH9Sxx`| literal 0 HcmV?d00001 diff --git a/Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-29x29@1x.png b/Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..941819fce212f20c523916380fea9450f0576883 GIT binary patch literal 1247 zcmV<51R(o~P)4Tx0C=2ZU|>>7EGWofVPIg$%_}Jia(7aQh>TKTf5^ZNguD!53<`H2BL+u3tZkNpBf}F%kg#cp$t|bGMq*j!GXy^Qb%A(Blj1mP$U?`<3c;+SR z=_nW(7@9LMfWjQ2`g0&SEE&blAjF#QGcefh|NsC0ZiJZEeg=k(K%HDW5n>t^3=D$Y z3=AiB79!vFuj7ybYLcQH`&9R`L2c>on5W$tWJ|6>3E14KzgK~#90?UvtbTtyVeKQpsQ zHfxt8YYerGDTNA_f*=&7MSK&ilEnYPhmt6^R3G{(zKGbOqAC6hK9r^*R*@n|(W(W( zDs9pji}_)@-A$XUySdrDGslP7+uY5~rU}@-wCCYo=FFV$XU@!-xmSsZ@PJJ}Xy7C0 zeYezB1El^CK;a+ienDNm#$vsZ*a^1__}LJKJaooU$DqZrt$c)nLP#x*h3>~e?#7X$d5|o0K5T1S< zK77TntFsPR2b@@55eS`Mx~Zb@`%S-ReVuUhpyt3+szFEuew-0L`N1(+goz@|EeX52 zl-~lq4p?I5oFwea%TC@nZDlF&ZM$Yn)Lsfi4xh2|^<+|HtuAXjrWZus|ISKKOHQB~ z`gfr$^4@tXg>s`mZb9gErWcan6!^kW3Z@re_^jox+c3Z6IB`hR-s1a3pv6F;0&kBw zewh=l-g11}uj$T!b%64P71ZFJbB^J`Z{5vAPkLK@@3y|6jZ8kWZZFaM#yDWHRGJS)f<)gdHjcWLg!!&I?9EZwHL@8@6Xx zb(@}3D?W?-zwqXm-`|w>LCZDR->W!zNRw@Y(h3|q=eRU0clSPYu)7OB?>F>pTh)u( zx|c&VvqA;l8na9lz3+uy#mQHFafQxLX1NB(zHkA@@oSoNYPBzV2O%6CcidhUP7NAz?ahH3LgU~{&1cnWl6Pv4Tx0C=2ZU|>>7EGWofVPIg$%_}Jia(7aQh>TKTf5^ZNguD!53<`H2BL+u3tZkNpBf}F%kg#cp$t|bGMq*j!GXy^Qb%A(Blj1mP$U?`<3c;+SR z=_nW(7@9LMfWjQ2`g0&SEE&blAjF#QGcefh|NsC0ZiJZEeg=k(K%HDW5n>t^3=D$Y z3=AiB79!vFuj7ybYLcQH`&9R`L2c>on5W$tWJ|6>3E2RTVZK~#90?U`+GTvZvzf9Gu8 zo5m8`lxb@P9I>?)3RYVBrihL}8Nc0M~u;yhSF9-8!Hi{U}=CR7)qMA-RwP&AD(-6@9t(d$!1fX?LJ@j-n-{M=lR|9 z-2Zvbxkf~U^T6!?I)HO!CD3BC5@@km3AEU(1X^rX0xdQ>Z$Qh=MJECTONU?uVS-8)~!%U%$uCff?Q3V&xGAy5OSa&hJ_nijA z*bi(fR*(^Ok_0^Xs<7**aHLxph(Wo9iU>q(<|sJ<&q2(CQ3tgZuzbGZ#>)*W-(@m< zPsOGT>Ip;)yml70A8@q3BnXSj#Yu{t|)@sYVnaMt;Vp9V3 zoWlI&W#NZyj^05{MS}(hNihlpC!D(J|A7Jt@b(DInE~Iw#`2Mi&6HvD1{Gf=e|=2& z>3&CLMBll%OM(Clk3!UgG7Ht^P#MwdQBVJiJH1w2uGa(LGOO=&)PvjBTE1|ZN%VQn zK!ad@c~sc4-%(eo0R#eKEeu4p&Ef{bs)ew0u3>&1)K+Sw15u&7A39G9?I+>+9(7t< z3C<;jO$7R5uyKv$OUq1R%9?giL16}BaKl3$Q4gvj5SL_NXQOb{0>f9XG+e#Vj2oB} zc>1L9;IqP^Q^L&9gDbb{Z(bez_KOiBel0gy*1Sj58hG+w!uOtZ31y9XIvPz^TfV$3 z8H(pLGDE5uiGL6Y6{3$kC*1qA!&;~+gTa`p@s9PDr5BpKRd~Xno=dGQrNhs zO8b*fSXRH=WS#iKVw8S8P~Zdq7QXXGN53yfI~pxZ8d4#H$}2L_YF#E^E_CdLPU4>a zww-Wt-GgyC(yf1vdSm+u-y1#-TX65;Q%*kjD^I$5T}of+_hxaEVL3-MJlE~%5>`4F87xboC0iv^ZWl z124X5*s#Xx>L>?k))5Ul*u2m2$a6x&3{~3N3fTEceP_Z!X6q+t^ZVz8yZ`K%QK8od zV{qq3EuUUIHf%OO$9sh4y)F?;&mi2i%y8Q}n^UO9bRXQXz1{I}hcLTF>(Odo1)KIb zj=dp7tgoo!K<+*++|}x+DNp`aQx12vI(8pV^^*(o*c(FA9>-uzV~Ke%y9OTW5Vp6w zLW{Ufk(tri-dUjmj{c;=Mw z?Z+LS)1o9i-@Y{r-&kSz`ik6;8YRk=6s-O0g!p0{YB2Yd?+vb&quYSnl1zR@RIVV;b1u68v z--K=LnE<0s&$nK0Inf83_dDvVGarZmI~z4Gn_C=)On|;&xP7hVl6u(igu@tpMKT`9 zD$C6uoS0%y7&Hv~aEGwIg0>IVt7Rd` zAjLPVvD|dU#33gI8iw6XIS@zDT91Dk$Xb>oP^F z;GkjH-*gH;{;OkVMJili5d$N>O72*1xnkbfirVp-@Xg0uV!7v~`3a!U#^AdjwtRl+ znv^a)GQ|HLQ=U_~?R6e^TKghg6 z!$JP;AHomXwCXWNE0E#1Un(|Qmdwo^T%xo)Ei|`iX;)na4uu>Vg*&dXeCGZ6Lrw`a z9ArzEaA&Kdrb735>MFG!UOZ=Vb!=C!(6ralAJzS*p%~nKt)+3%)F7b{+ns&@e|SL` z;B#i^NM1U7a=`^{UwBhTws#PI^l{5|mrflL3cP{cd0M!j+OVK5zrEPto>Tqs*08X2 zZvI;*=~l3BFX`JH`36@gKj~J`k4%O7UNRjYnhO3k?f=!zn=jra&|rIP3Clc8B9s{b00004Tx0C=2ZU|>>7EGWofVPIg$%_}Jia(7aQh>TKTf5^ZNguD!53<`H2BL+u3tZkNpBf}F%kg#cp$t|bGMq*j!GXy^Qb%A(Blj1mP$U?`<3c;+SR z=_nW(7@9LMfWjQ2`g0&SEE&blAjF#QGcefh|NsC0ZiJZEeg=k(K%HDW5n>t^3=D$Y z3=AiB79!vFuj7ybYLcQH`&9R`L2c>on5W$tWJ|6>3E2RTVZK~#90?U`+GTvZvzf9Gu8 zo5m8`lxb@P9I>?)3RYVBrihL}8Nc0M~u;yhSF9-8!Hi{U}=CR7)qMA-RwP&AD(-6@9t(d$!1fX?LJ@j-n-{M=lR|9 z-2Zvbxkf~U^T6!?I)HO!CD3BC5@@km3AEU(1X^rX0xdQ>Z$Qh=MJECTONU?uVS-8)~!%U%$uCff?Q3V&xGAy5OSa&hJ_nijA z*bi(fR*(^Ok_0^Xs<7**aHLxph(Wo9iU>q(<|sJ<&q2(CQ3tgZuzbGZ#>)*W-(@m< zPsOGT>Ip;)yml70A8@q3BnXSj#Yu{t|)@sYVnaMt;Vp9V3 zoWlI&W#NZyj^05{MS}(hNihlpC!D(J|A7Jt@b(DInE~Iw#`2Mi&6HvD1{Gf=e|=2& z>3&CLMBll%OM(Clk3!UgG7Ht^P#MwdQBVJiJH1w2uGa(LGOO=&)PvjBTE1|ZN%VQn zK!ad@c~sc4-%(eo0R#eKEeu4p&Ef{bs)ew0u3>&1)K+Sw15u&7A39G9?I+>+9(7t< z3C<;jO$7R5uyKv$OUq1R%9?giL16}BaKl3$Q4gvj5SL_NXQOb{0>f9XG+e#Vj2oB} zc>1L9;IqP^Q^L&9gDbb{Z(bez_KOiBel0gy*1Sj58hG+w!uOtZ31y9XIvPz^TfV$3 z8H(pLGDE5uiGL6Y6{3$kC*1qA!&;~+gTa`p@s9PDr5BpKRd~Xno=dGQrNhs zO8b*fSXRH=WS#iKVw8S8P~Zdq7QXXGN53yfI~pxZ8d4#H$}2L_YF#E^E_CdLPU4>a zww-Wt-GgyC(yf1vdSm+u-y1#-TX65;Q%*kjD^I$5T}of+_hxaEVL3-MJlE~%5>`4F87xboC0iv^ZWl z124X5*s#Xx>L>?k))5Ul*u2m2$a6x&3{~3N3fTEceP_Z!X6q+t^ZVz8yZ`K%QK8od zV{qq3EuUUIHf%OO$9sh4y)F?;&mi2i%y8Q}n^UO9bRXQXz1{I}hcLTF>(Odo1)KIb zj=dp7tgoo!K<+*++|}x+DNp`aQx12vI(8pV^^*(o*c(FA9>-uzV~Ke%y9OTW5Vp6w zLW{Ufk(tri-dUjmj{c;=Mw z?Z+LS)1o9i-@Y{r-&kSz`ik6;8YRk=6s-O0g!p0{YB2Yd?+vb&quYSnl1zR@RIVV;b1u68v z--K=LnE<0s&$nK0Inf83_dDvVGarZmI~z4Gn_C=)On|;&xP7hVl6u(igu@tpMKT`9 zD$C6uoS0%y7&Hv~aEGwIg0>IVt7Rd` zAjLPVvD|dU#33gI8iw6XIS@zDT91Dk$Xb>oP^F z;GkjH-*gH;{;OkVMJili5d$N>O72*1xnkbfirVp-@Xg0uV!7v~`3a!U#^AdjwtRl+ znv^a)GQ|HLQ=U_~?R6e^TKghg6 z!$JP;AHomXwCXWNE0E#1Un(|Qmdwo^T%xo)Ei|`iX;)na4uu>Vg*&dXeCGZ6Lrw`a z9ArzEaA&Kdrb735>MFG!UOZ=Vb!=C!(6ralAJzS*p%~nKt)+3%)F7b{+ns&@e|SL` z;B#i^NM1U7a=`^{UwBhTws#PI^l{5|mrflL3cP{cd0M!j+OVK5zrEPto>Tqs*08X2 zZvI;*=~l3BFX`JH`36@gKj~J`k4%O7UNRjYnhO3k?f=!zn=jra&|rIP3Clc8B9s{b0000go%oody!-abH87gkmTCfW-NDNMDY&Mo0e+C6#l)XqJo zm&W^u%HzQUO6uzdrn0c;=t}dbhzdou^w7A9lo@e!P3db5rVVbiA0A~JBmdMIXI!uiDh`I%%h5fKslSj@to4AyfkY`qXTt0t&7pUtBjIVz^Ra+&Lh%gNH@ znd-A>wqBIUpb2|+$39!{cT;_SYhw@ODv$;Df<70Q+hvvj@MNu#X8X4U^>6I`rQ^8% zBKr1IexR|tY^hw>qBLV!R^+@$wyH{Or+CpJK~h8EeD|PaSN6K9%IF&N@8BMhemvwz zOwrAAMeuouLU?%MCV6;ZA&|=u>9_A;~K5J`J88UwAUG5hh z%l>WrJIc5eY3vJP$vrLPNtRlugvh7b8KNOAWKSPtTtqm(@f{WbY_OCE?Ngn-D; z+~}yawSbtkR<7y>83)OXrAPpC2sF!4aSj*zzks@>9cwTwsf`eQ5eHU^Cb-dkUD0sE&mi9I_=Wmz&=?^*??asZqAqX619 zXo45q0;))g%w#z`&1t%ZC0NbA*Th*fz-YIr>am9g0>J|)WjtUu8t|S;sH#;^lLBCM zLw8AyxC+Rms;vt4sYW@|RThRnnb@+C|AupzSM)#XYQ)G)R&c4i}jzmH>=jSD$sS~7s!m* z={W7T8Zx_SncSrFg-20EeZ&#X!#g9l75{m2>e=8=C>j%FEQk|R-tV^PI|tKgdnG91 zirC0fayo1*nkARjYwYa4$Ukuy2P_AsRKJx}#2b^q@R=*?_w%BJod{nhx$TY4Q`0{6GVb4=RjYXOtBT)eQ zd9>_Cc+bjHsc@V58(DplOQyee?_AR-*Bf1>Ue*vu4{mO3C{ra@3@%XYRTQ|Lq@K2n z@LiK9OyWs_N$&mkJA{Ovkg#jk*r|;J;q1f@*#n$vk=8GTG~d#i(Mdj8u-OOfJt?u{ zq956%Sj2;FKW4~z{z83|M`1@C!0{#lOt#RH+okDlBs;}shI7YuU7XfkO}LLsbn)Ao z7wrRXKo?%W>3y58cmsQqFNzS;c*hfUKtzQdL;9$-Nu$k_%Bl|y>X=)_K6-ija8KbQ zoTIV63%inB0`Ti)O6}9)AV((Ps2@BH_W9tOow<|jxoU$7Q~IfeMxA(PlKpCsj!6ec zn4CmiObw8_LtZRAD+dYg=2smSe=tL}*kHU2Y*|X1P@}9<@R+N7%RnP^MB#Sre~vymMQ2A_U?7Q?QbQ5y6VYs1?{04%;@F%(1X8qj z^X?TKaUug1YS7|dN2i45S z&CRcRU%GI`z4??VEmi_N-sGmlwX3?R#Jr}<@-&=ci*CXqhZ3jco8&^o6TB(X3-IHK z2TNIIY;pbw%v#(oPQ)dD+Px*>mFx`S;46?pc^qkt?m0W86}5VVsSjOs0ym1zM5qvr zda?!`%Zjxvvs->N(_L2oEJ~yZCLx`d4P6ZnreB*2RNa}f70$vrMD3mJ9!^QF5KEgI zPCplTOX<85qUrkH{l)5#cm64#^2_E4=Hme}@;`8!X{r8@5x8cg#pMx+1Y=*5wouEU zm@8AOJZ)41Wv$~zkFO60fZZF0Fgn#?V_Tu`8Fd9}5;7TcVzSlcV~8du9Vc(yGfbJ! zPJemSiT;psQ2SBsABG^GFsM3G`svW&*7=Np+dMvA->-yrmY}k>Uv) zqGt#$fF;P#b;bq1hfTL_QCwkIqliKkaMll#nidnUZ?~NB*my*$A0)tLyV<9Eyy%s- zqqXtR4l(IHx9fOXW9^`XQ@vZ=4mq&+c$SdA1ha-JI;k7mX>rj)a%)$HwT{|6oMy+y z)IWauy?`J`HF^zKY_pU<^2a(5geU99OSR1u?u4R{q$TG|BboCwJ0BcKvxpql z>M@Z{=y1MGd;SLaY02cdbuWP2~6O&oW%Mwt_@w66;Br> z11Cg@2mV4Wsiju7aR+;U^hcvDRAZ_u@g=O{Z1O)3I1I6^R-o_cK5asVKBwO?CBHa) zqF$}~Cm#6Uu?p5V)$B2V03U~Q??OWI^dq@~2%a59}BdEWSbV?45&1Mk^l zY6W9o$waNSx@~Nv1kytM)q_bPIsu9Iko@rtI<-64Bm4YEU#IX1meRb}HZbQ+Kw*c` z-SP;_KKK|3QG^eyMEhDC&@#-=Bg|9PpegXj!noiHrTA3bpc(#XL9Rj}@m7fFk}GKa zUkIKGefa*n_gxUVb1U5VtZF-~KD4#}Ev0tsZ6tMSyO`R`NE)c@STt2F_Q8;rqaOBiU5V4#f~SD1YT#sAM{i?X>+uAP z%lSROb>pppY>U11bn4E*F{Zg!Pk=3PWno(tZg#fu5L@OBGK*A2Gb9luE4mg(Dr@oIrF zGm+S)#mc=o?f~XIrYQPQq=Q1CJil*Umg_2&vfcSXOehE3#WIoebJaG?D{+2MP`c=W z*Ua6M^hMKJ2hV`lE@V}uE7C=s3RzG1;TxZZLRf8oxJpO+w)+^=y%QamNVq#lU%R6? zF*tKtdax=Z)B=vG7r?dAU7?D-9aFBoUmBtj=@P;cCyzZ&+$;lij{VB!8@b2Y*?lJ> z{nHk&=U7Mu4{CG>H1&=)ympQH0Q0@8QOE65dT-!kcHP3?AjP#}F4;JBh|G?!#(x~S zm<9bb(i9PNh?U3@BZDG4Tx0C=2ZU|>>7EGWofVPIg$%_}Jia(7aQh>TKTf5^ZNguD!53<`H2BL+u3tZkNpBf}F%kg#cp$t|bGMq*j!GXy^Qb%A(Blj1mP$U?`<3c;+SR z=_nW(7@9LMfWjQ2`g0&SEE&blAjF#QGcefh|NsC0ZiJZEeg=k(K%HDW5n>t^3=D$Y z3=AiB79!vFuj7ybYLcQH`&9R`L2c>on5W$tWJ|6>3E1rJF?K~#90?U-ALl~ojgzrD|# zxtrrSHboD4874&vi%_EnNmDVEf*^`OdWaycL;~%GgGLu261x~a1Yt-KM0v~W7(!l) z90VO3#u+uKM(3vEoS8Z2?7eze|3Bx9GpD&6p@LZtKj+_$4CDi7n+2>HNhNUtku;0l zNobBiXIxbkNgAwMVcoT19FJSYMfHPji09~*?j7lgWIp{)zzPRHd&U|a!ImKo+u zG(0ldP?Vbr1v3oLA$!nEu$92KXN0Y%JQv!uEk6gMkyO7INI3PZ7nnE>mfdf8a=PiK z>y`jYu ze{{g=y^hmOP*$MZb*%?-EL4=jJrfM&MLM>r6MnlY)VD&?!`M7MBrRQVZ#isO7~!tl z0)Rmv2aQ255V7#(DbMlWVM?ib>_!~sOfoE+VVH5JA#zhjbslQ|6t>m~hZ}|BTqrMw zqnBWNt!Kq-i}QmYoQxh63DQWAB3fdwW}oN8WuYhvs~)yIeP2qu6NLg@P`cr-PYWB5 zcskkc`&-mULRH!ySvDk8G2#>=)E-=}RLm{8aCZne?ani(>u zuQ%1z4Xvkd`K#*Dd|xSK-5-bdTfC^4tTax6hhX)$u_H zw0FbGU7n+tgr6@vHZ6`YxwQZ3?v24P5EBlb`pR*!9mA_(9E zfX{ysHXZYfD^MM6UGU07mggTdy^AuW=C*dh%R4=1t_Tx~pt%$7E`e{Ji4cthU;$6> z*)xF$qZXc>VX+3f5?T??UJ;h>a5Tr%fb%*wVqwck&!%IZlKfzm0wwwI(J{}Klb(px zvEhkrj=}OBj#-^W_ey2Ee@%OdD`a_<=s8)&gE~pp}pDc|~UaVs` zpYVKkLJb)MH+r^|rD^tdp-P?a{vG=%{CP$!fXDr8FxR?Idmo@!aO z+tJXfaB&CS30ODJvSey1UT>C+c`%%?w@!HLpeHY)p6pD(ZF!ogqgE?POAO{$7(Q5J zF~F;PJO}C(KItLhp*R<=_h@-cIOuWk_M?_Xm6_rG5fD^)j)epD!W;WN(Eu{xG@c>> z+@eaudkbuz%fjpXJ$vdD(s?}~;oO9ScNbXZSD0QKnPkvf94a`}AiTEElVjC$#wgs9 zsfKsvTaBHxE42u`dBC&loI;AICzBrDUuc;-d2qNP0j5J9ZWPw+^H`%>{P;A(`gth} zVSuM=TwCqgUaPg(3v5_unKNlt=H{dv zx#;`o8j1|-${$`d|LTD9F{#{{snCag0yK5N9b<-fS%ndNlML5DwsVL5U2IL?7#pg{ z2p2Y@5BK4G^kg#!{s;doi~?pWqk!4UC}6hoH~csBNPfMn>Hq)$07*qoM6N<$f}02$ A0ssI2 literal 0 HcmV?d00001 diff --git a/Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-40x40@2x-1.png b/Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-40x40@2x-1.png new file mode 100644 index 0000000000000000000000000000000000000000..5c06655825fc02dee4eeb2e3e3a262f5c5f6d772 GIT binary patch literal 3211 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 literal 0 HcmV?d00001 diff --git a/Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-60x60@2x.png b/Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0fdab5f9586ace7626a8e31c96c5a47240c2912c GIT binary patch literal 4313 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 literal 0 HcmV?d00001 diff --git a/Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-60x60@3x.png b/Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..3b88e179c7f015f579ade8b2e29ec59cf5f9f287 GIT binary patch literal 6262 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 z zI?d@jKyE!`s;*^x-9xGzwsi6g_qvv<_QC3KY9yCOI!#G*hICepQ&>gaG0p|V1K6%=)aRzI;H>M&6U>)p&@z2&29 z_t@=^M0?bP(>E>6!e)pw!FgYKDnK=Uw`)4Crh4h_{?R2K0T4_g8Ls*!JS{v&(5~Mu zUU+&a^c=Y#l(w!(9@HB?y{I^+dquxN#-^()t|joyp!I>Nao z>H3#+s-j~mU0SVQ9`ef{QoC=?$cp9fyb!##pBED`?l&Q7>bbAp*8aN4`aulGsDA9j ztJH4+(7A>Z=~!HTKa&+?;k)}cf%|}KnGKtd!E^l|e(K0-ePjd7gM5XXbQ|}Nd^iAR LYHd<)gu?#^lZ&uu literal 0 HcmV?d00001 diff --git a/Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-76x76@2x.png b/Example/DApp/Assets.xcassets/AppIcon-2.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..03bb3d0210da26a6704db58fcae0cce08b92ba02 GIT binary patch literal 5312 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 S63sm)kE5=vqg1b83;iDjP5l%A literal 0 HcmV?d00001 diff --git a/Example/DApp/Assets.xcassets/AppIcon-2.appiconset/ItunesArtwork@2x.png b/Example/DApp/Assets.xcassets/AppIcon-2.appiconset/ItunesArtwork@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f527fe6244244e39e804a088c941e39ce3327f07 GIT binary patch literal 39006 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 literal 0 HcmV?d00001 diff --git a/Example/DApp/Sign/SignCoordinator.swift b/Example/DApp/Sign/SignCoordinator.swift index 75d25df1e..1373e4c30 100644 --- a/Example/DApp/Sign/SignCoordinator.swift +++ b/Example/DApp/Sign/SignCoordinator.swift @@ -26,10 +26,11 @@ final class SignCoordinator { icons: ["https://avatars.githubusercontent.com/u/37784886"]) Sign.configure(metadata: metadata) - +#if DEBUG if CommandLine.arguments.contains("-cleanInstall") { try? Sign.instance.cleanup() } +#endif Sign.instance.sessionDeletePublisher .receive(on: DispatchQueue.main) diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index c6a979651..e49f1265a 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -1762,7 +1762,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.showcase; + PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.chat; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -1791,7 +1791,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.showcase; + PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.chat; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; diff --git a/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift b/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift index 43335bd7d..5956c494e 100644 --- a/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift +++ b/Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift @@ -30,7 +30,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let uri = context.url.absoluteString.replacingOccurrences(of: "showcase://wc?uri=", with: "") Task { - try await Auth.instance.pair(uri: uri) + try await Auth.instance.pair(uri: WalletConnectURI(string: uri)!) } } } diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletInteractor.swift index ac53b05f3..bd06b4567 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletInteractor.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletInteractor.swift @@ -4,7 +4,7 @@ import Auth final class WalletInteractor { func pair(uri: String) async throws { - try await Auth.instance.pair(uri: uri) + try await Auth.instance.pair(uri: WalletConnectURI(string: uri)!) } var requestPublisher: AnyPublisher { diff --git a/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Contents.json index 9221b9bb1..78d34c2c3 100644 --- a/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,91 +1,109 @@ { "images" : [ { + "filename" : "Icon-App-20x20@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "20x20" }, { + "filename" : "Icon-App-20x20@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "20x20" }, { + "filename" : "Icon-App-29x29@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "29x29" }, { + "filename" : "Icon-App-29x29@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "29x29" }, { + "filename" : "Icon-App-40x40@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "40x40" }, { + "filename" : "Icon-App-40x40@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "40x40" }, { + "filename" : "Icon-App-60x60@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "60x60" }, { + "filename" : "Icon-App-60x60@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "60x60" }, { + "filename" : "Icon-App-20x20@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "20x20" }, { + "filename" : "Icon-App-20x20@2x-1.png", "idiom" : "ipad", "scale" : "2x", "size" : "20x20" }, { + "filename" : "Icon-App-29x29@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "29x29" }, { + "filename" : "Icon-App-29x29@2x-1.png", "idiom" : "ipad", "scale" : "2x", "size" : "29x29" }, { + "filename" : "Icon-App-40x40@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "40x40" }, { + "filename" : "Icon-App-40x40@2x-1.png", "idiom" : "ipad", "scale" : "2x", "size" : "40x40" }, { + "filename" : "Icon-App-76x76@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "76x76" }, { + "filename" : "Icon-App-76x76@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "76x76" }, { + "filename" : "Icon-App-83.5x83.5@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "83.5x83.5" }, { + "filename" : "ItunesArtwork@2x.png", "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" diff --git a/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..e8b3b928c6cbe31cb3bd7f8e321c0430cfab8603 GIT binary patch literal 909 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 A0ssI2 literal 0 HcmV?d00001 diff --git a/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..7f3a702c374a8efe35d793c31a205cffb1b42ecf GIT binary patch literal 1706 zcmV;b237fqP)4Tx0C=2ZU|>>7EGWofVPIg$%_}Jia(7aQh>TKTf5^ZNguD!53<`H2BL+u3tZkNpBf}F%kg#cp$t|bGMq*j!GXy^Qb%A(Blj1mP$U?`<3c;+SR z=_nW(7@9LMfWjQ2`g0&SEE&blAjF#QGcefh|NsC0ZiJZEeg=k(K%HDW5n>t^3=D$Y z3=AiB79!vFuj7ybYLcQH`&9R`L2c>on5W$tWJ|6>3E1rJF?K~#90?U-ALl~ojgzrD|# zxtrrSHboD4874&vi%_EnNmDVEf*^`OdWaycL;~%GgGLu261x~a1Yt-KM0v~W7(!l) z90VO3#u+uKM(3vEoS8Z2?7eze|3Bx9GpD&6p@LZtKj+_$4CDi7n+2>HNhNUtku;0l zNobBiXIxbkNgAwMVcoT19FJSYMfHPji09~*?j7lgWIp{)zzPRHd&U|a!ImKo+u zG(0ldP?Vbr1v3oLA$!nEu$92KXN0Y%JQv!uEk6gMkyO7INI3PZ7nnE>mfdf8a=PiK z>y`jYu ze{{g=y^hmOP*$MZb*%?-EL4=jJrfM&MLM>r6MnlY)VD&?!`M7MBrRQVZ#isO7~!tl z0)Rmv2aQ255V7#(DbMlWVM?ib>_!~sOfoE+VVH5JA#zhjbslQ|6t>m~hZ}|BTqrMw zqnBWNt!Kq-i}QmYoQxh63DQWAB3fdwW}oN8WuYhvs~)yIeP2qu6NLg@P`cr-PYWB5 zcskkc`&-mULRH!ySvDk8G2#>=)E-=}RLm{8aCZne?ani(>u zuQ%1z4Xvkd`K#*Dd|xSK-5-bdTfC^4tTax6hhX)$u_H zw0FbGU7n+tgr6@vHZ6`YxwQZ3?v24P5EBlb`pR*!9mA_(9E zfX{ysHXZYfD^MM6UGU07mggTdy^AuW=C*dh%R4=1t_Tx~pt%$7E`e{Ji4cthU;$6> z*)xF$qZXc>VX+3f5?T??UJ;h>a5Tr%fb%*wVqwck&!%IZlKfzm0wwwI(J{}Klb(px zvEhkrj=}OBj#-^W_ey2Ee@%OdD`a_<=s8)&gE~pp}pDc|~UaVs` zpYVKkLJb)MH+r^|rD^tdp-P?a{vG=%{CP$!fXDr8FxR?Idmo@!aO z+tJXfaB&CS30ODJvSey1UT>C+c`%%?w@!HLpeHY)p6pD(ZF!ogqgE?POAO{$7(Q5J zF~F;PJO}C(KItLhp*R<=_h@-cIOuWk_M?_Xm6_rG5fD^)j)epD!W;WN(Eu{xG@c>> z+@eaudkbuz%fjpXJ$vdD(s?}~;oO9ScNbXZSD0QKnPkvf94a`}AiTEElVjC$#wgs9 zsfKsvTaBHxE42u`dBC&loI;AICzBrDUuc;-d2qNP0j5J9ZWPw+^H`%>{P;A(`gth} zVSuM=TwCqgUaPg(3v5_unKNlt=H{dv zx#;`o8j1|-${$`d|LTD9F{#{{snCag0yK5N9b<-fS%ndNlML5DwsVL5U2IL?7#pg{ z2p2Y@5BK4G^kg#!{s;doi~?pWqk!4UC}6hoH~csBNPfMn>Hq)$07*qoM6N<$f}02$ A0ssI2 literal 0 HcmV?d00001 diff --git a/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..d38db763f8541059b36d84eb9c4844a1a9ca89da GIT binary patch literal 2303 zcmV4Tx0C=2ZU|>>7EGWofVPIg$%_}Jia(7aQh>TKTf5^ZNguD!53<`H2BL+u3tZkNpBf}F%kg#cp$t|bGMq*j!GXy^Qb%A(Blj1mP$U?`<3c;+SR z=_nW(7@9LMfWjQ2`g0&SEE&blAjF#QGcefh|NsC0ZiJZEeg=k(K%HDW5n>t^3=D$Y z3=AiB79!vFuj7ybYLcQH`&9R`L2c>on5W$tWJ|6>3E2V_Y^K~#90?V5jZRMi#7KlkqD zCkc%Ntr!cn6hY|^3$ZPwPQik^6?z^XdocDI$ZgxqsyI5!4Gm~WZy?ytd z&pqdU&v|cSL`3)inf-q|@LpR5xZJG*T<%r@E_bT{m%CMf%iSu#=C+ckA@bEYsJ0l!>2L^_O!5EAt^;}&P zn(JY1lVQYoI7r>V!GHWm*x4r>Iwib40&!2%)%vN6|9K~RZFK~$ zXn@wM3~Lq|KK~K@kdxA;dP#u8k?0u^b{rOtoDw{N+Ni>WAu}8Z9gll3mV}6bC07~N zE;THhYbMN5j1>*+i8?qu4qLk&&%UO^&8XI>1Q7&Zu(F(%tn=3B`wMYBcE@#wdzV`p zYtnNSV?_Z64)hNS_wRD_4?67uK{NdH zR?C8BQ*@*Y1`Z}ZdRn-r(=i-}hH3=@KU!8(bTpx9-CPfs*F!^%dOR{NoQc6;Ow+lp zO1FS>>8M97jKra_8amo6H_Ryxx**^{NRFNo?(THNJ*bI-=fg$}B%G$@ye9bae8cj2 zhWWD$m(^vCb#4@npB1`K2!A;)oEXyWP-QiP03sH~lbQ*?xZU!pD+`A%s4N9)+&>6! zjlk72H9~=NBjfO)TKN8FEnmMrv(yCFgD%=-bvubL^$WF8cyOg=OEzqma?i9dNw)uBvIo=C7OK z;TvwUYse&~$M3Bf+H-|^cQ zg)19X)PB6%;fZCv1&IW|pO)F}Qn~<$J9PZF7iRFt?z7ZT_p{_b;h}2ciX94m;W`E3U~8J?$qRo`YHS z@YGAf!~0yS_sw25@fR*);qhLV0UO--i)bslt;g|ruS^Pj6dV!h;ZD??x9C#4u!`h`<2|%yi(B=5kv5Bxrr#<`T zTP&;Qo0P8?vne$m!9e@o5bl3o_gXgeJ27ax*6_ee%M-nh-yBf~vtftFNoB*vE=Oa{ zMCid7e0Paq-Hnz_`y8FGYZ_<6dQKail^>fL?397S(EI-_-1of07`>wj-`mC#&@vNF z4{Ng@R$~#kkbn&jTD8>6k|oJKf#9_p(y=SevI>?2>X-1O0@ z!%hnr()IMffbhdVJ4E!BE$mF((_`7Nu>`c=Y`Nn)lZsT-!n3cb@YUylPI#L38L*z7 z``K45Uzj_!?y6v5NJskMo5H<29iGc{D(rO75051@joTJxMk9FL*(Yr5a@1ClQt4b{ z#XW`Fy2kRktEU&)f`K6&=|d;AF-bV6iKLY{1)Hs)Iie99%U20N7h&AfE!5FwX>FN) zWQzh0JNn{j;Rnw-#yqHvK+-P@(^1fTmB15-8d78b{5H#H<`fQF6mS^&pKl3wJ?9ur z=;N8O$9%yx9OvuVql>Zk7<1eUU^%g|2v`&+_n^;py5jJ zqg{^Sarkh9!Y;m|cvt|W1eWylZ{Haw>z?tXXTW6~FJ!Eq0UfLFOyOhYvXXt(lHI=+ z^W3SFV`W=OzddwWD*b*LD{PnS>(ckDec=8BpaNX(Rsk+|s{og~Re;OgD!}D#72tBW Z{{c7J0-wbL?PUM}002ovPDHLkV1gH9Sxx`| literal 0 HcmV?d00001 diff --git a/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..941819fce212f20c523916380fea9450f0576883 GIT binary patch literal 1247 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 A0ssI2 literal 0 HcmV?d00001 diff --git a/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png b/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png new file mode 100644 index 0000000000000000000000000000000000000000..5c06655825fc02dee4eeb2e3e3a262f5c5f6d772 GIT binary patch literal 3211 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 literal 0 HcmV?d00001 diff --git a/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0fdab5f9586ace7626a8e31c96c5a47240c2912c GIT binary patch literal 4313 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 literal 0 HcmV?d00001 diff --git a/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..3b88e179c7f015f579ade8b2e29ec59cf5f9f287 GIT binary patch literal 6262 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 z zI?d@jKyE!`s;*^x-9xGzwsi6g_qvv<_QC3KY9yCOI!#G*hICepQ&>gaG0p|V1K6%=)aRzI;H>M&6U>)p&@z2&29 z_t@=^M0?bP(>E>6!e)pw!FgYKDnK=Uw`)4Crh4h_{?R2K0T4_g8Ls*!JS{v&(5~Mu zUU+&a^c=Y#l(w!(9@HB?y{I^+dquxN#-^()t|joyp!I>Nao z>H3#+s-j~mU0SVQ9`ef{QoC=?$cp9fyb!##pBED`?l&Q7>bbAp*8aN4`aulGsDA9j ztJH4+(7A>Z=~!HTKa&+?;k)}cf%|}KnGKtd!E^l|e(K0-ePjd7gM5XXbQ|}Nd^iAR LYHd<)gu?#^lZ&uu literal 0 HcmV?d00001 diff --git a/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..03bb3d0210da26a6704db58fcae0cce08b92ba02 GIT binary patch literal 5312 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 S63sm)kE5=vqg1b83;iDjP5l%A literal 0 HcmV?d00001 diff --git a/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png b/Example/Showcase/Other/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f527fe6244244e39e804a088c941e39ce3327f07 GIT binary patch literal 39006 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 literal 0 HcmV?d00001 diff --git a/Example/Showcase/Other/Info.plist b/Example/Showcase/Other/Info.plist index 15a9080c9..8767aa73a 100644 --- a/Example/Showcase/Other/Info.plist +++ b/Example/Showcase/Other/Info.plist @@ -2,6 +2,8 @@ + CFBundleIconName + AppIcon CFBundleURLTypes From 1e35971241f1e8fad408343a5931aba3ab3534cd Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Fri, 2 Sep 2022 13:25:27 +0200 Subject: [PATCH 33/92] savepoint --- Example/ExampleApp.xcodeproj/project.pbxproj | 2 + .../Common/DisconnectPairService.swift | 40 ++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index c6a979651..fd35fb2eb 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -1752,6 +1752,7 @@ DEVELOPMENT_TEAM = W5R8AG9K22; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Showcase/Other/Info.plist; + INFOPLIST_KEY_NSCameraUsageDescription = "Allow the app to scan for QR codes"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; @@ -1781,6 +1782,7 @@ DEVELOPMENT_TEAM = W5R8AG9K22; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Showcase/Other/Info.plist; + INFOPLIST_KEY_NSCameraUsageDescription = "Allow the app to scan for QR codes"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; diff --git a/Sources/Auth/Services/Common/DisconnectPairService.swift b/Sources/Auth/Services/Common/DisconnectPairService.swift index b808caaef..cd21c09ff 100644 --- a/Sources/Auth/Services/Common/DisconnectPairService.swift +++ b/Sources/Auth/Services/Common/DisconnectPairService.swift @@ -1,11 +1,15 @@ import Foundation import WalletConnectNetworking +import JSONRPC import WalletConnectKMS import WalletConnectUtils import WalletConnectPairing class DisconnectPairService { + enum Errors: Error { + case pairingNotFound + } private let networkingInteractor: NetworkInteracting private let kms: KeyManagementServiceProtocol private let pairingStorage: WCPairingStorage @@ -18,14 +22,46 @@ class DisconnectPairService { self.networkingInteractor = networkingInteractor self.kms = kms self.pairingStorage = pairingStorage + self.logger = logger } - func disconnect(topic: String) async throws { + func delete(topic: String) async throws { + guard pairingStorage.hasPairing(forTopic: topic) else { throw Errors.pairingNotFound} let reason = AuthError.userDisconnected logger.debug("Will delete pairing for reason: message: \(reason.message) code: \(reason.code)") - try await networkingInteractor.request(<#T##RPCRequest#>, topic: <#T##String#>, tag: <#T##Int#>, envelopeType: <#T##Envelope.EnvelopeType#>) + let request = RPCRequest(method: AuthProtocolMethods.pairingDelete.rawValue, params: reason) + try await networkingInteractor.request(request, topic: topic, tag: AuthProtocolMethods.pairingDelete.requestTag) pairingStorage.delete(topic: topic) kms.deleteSymmetricKey(for: topic) networkingInteractor.unsubscribe(topic: topic) } } + + +enum AuthProtocolMethods: String { + case authRequest = "wc_authRequest" + case pairingDelete = "wc_pairingDelete" + case pairingPing = "wc_pairingPing" + + var requestTag: Int { + switch self { + case .authRequest: + return 3000 + case .pairingDelete: + return 1000 + case .pairingPing: + return 1002 + } + } + + var responseTag: Int { + switch self { + case .authRequest: + return 3001 + case .pairingDelete: + return 1001 + case .pairingPing: + return 1003 + } + } +} From fbd546a794245a36776d6297070d5cc3af3c379d Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 1 Sep 2022 18:28:04 +0300 Subject: [PATCH 34/92] NetworkInteracting moved to package --- Package.swift | 9 +++++++- .../Auth/Services/App/AppPairService.swift | 1 + .../Auth/Services/App/AppRequestService.swift | 3 ++- .../Services/App/AppRespondSubscriber.swift | 5 +++-- .../Common/NetworkingInteractor.swift | 22 +++---------------- .../Services/Wallet/WalletPairService.swift | 1 + .../Wallet/WalletRequestSubscriber.swift | 5 +++-- .../Wallet/WalletRespondService.swift | 3 ++- Sources/Auth/Types/Errors/AuthError.swift | 1 + .../Types/RequestSubscriptionPayload.swift | 7 ------ .../Types/ResponseSubscriptionPayload.swift | 7 ------ .../NetworkInteractor.swift | 21 ++++++++++++++++++ .../Reason.swift | 2 +- .../RequestSubscriptionPayload.swift | 12 ++++++++++ .../ResponseSubscriptionPayload.swift | 12 ++++++++++ .../AuthTests/AppRespondSubscriberTests.swift | 1 + .../Mocks/NetworkingInteractorMock.swift | 1 + .../Stubs/RequestSubscriptionPayload.swift | 3 ++- .../WalletRequestSubscriberTests.swift | 5 +++-- 19 files changed, 77 insertions(+), 44 deletions(-) delete mode 100644 Sources/Auth/Types/RequestSubscriptionPayload.swift delete mode 100644 Sources/Auth/Types/ResponseSubscriptionPayload.swift create mode 100644 Sources/WalletConnectNetworking/NetworkInteractor.swift rename Sources/{Auth/Types/Errors => WalletConnectNetworking}/Reason.swift (75%) create mode 100644 Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift create mode 100644 Sources/WalletConnectNetworking/ResponseSubscriptionPayload.swift diff --git a/Package.swift b/Package.swift index 9c3cd5b68..6e0da39f7 100644 --- a/Package.swift +++ b/Package.swift @@ -21,7 +21,10 @@ let package = Package( targets: ["Auth"]), .library( name: "WalletConnectRouter", - targets: ["WalletConnectRouter"]) + targets: ["WalletConnectRouter"]), + .library( + name: "WalletConnectNetworking", + targets: ["WalletConnectNetworking"]), ], dependencies: [ .package(url: "https://github.com/flypaper0/Web3.swift", .branch("feature/eip-155")) @@ -42,6 +45,7 @@ let package = Package( "WalletConnectUtils", "WalletConnectKMS", "WalletConnectPairing", + "WalletConnectNetworking", .product(name: "Web3", package: "Web3.swift") ], path: "Sources/Auth"), @@ -65,6 +69,9 @@ let package = Package( .target( name: "Commons", dependencies: []), + .target( + name: "WalletConnectNetworking", + dependencies: ["JSONRPC", "WalletConnectKMS"]), .target( name: "WalletConnectRouter", dependencies: []), diff --git a/Sources/Auth/Services/App/AppPairService.swift b/Sources/Auth/Services/App/AppPairService.swift index 1dc9c0cbc..dc05600b0 100644 --- a/Sources/Auth/Services/App/AppPairService.swift +++ b/Sources/Auth/Services/App/AppPairService.swift @@ -1,6 +1,7 @@ import Foundation import WalletConnectKMS import WalletConnectPairing +import WalletConnectNetworking actor AppPairService { private let networkingInteractor: NetworkInteracting diff --git a/Sources/Auth/Services/App/AppRequestService.swift b/Sources/Auth/Services/App/AppRequestService.swift index b3186e77d..9303d7bb8 100644 --- a/Sources/Auth/Services/App/AppRequestService.swift +++ b/Sources/Auth/Services/App/AppRequestService.swift @@ -1,7 +1,8 @@ import Foundation +import JSONRPC +import WalletConnectNetworking import WalletConnectUtils import WalletConnectKMS -import JSONRPC actor AppRequestService { private let networkingInteractor: NetworkInteracting diff --git a/Sources/Auth/Services/App/AppRespondSubscriber.swift b/Sources/Auth/Services/App/AppRespondSubscriber.swift index 3a501b718..16e0ff807 100644 --- a/Sources/Auth/Services/App/AppRespondSubscriber.swift +++ b/Sources/Auth/Services/App/AppRespondSubscriber.swift @@ -1,7 +1,8 @@ -import Combine import Foundation -import WalletConnectUtils +import Combine import JSONRPC +import WalletConnectNetworking +import WalletConnectUtils import WalletConnectPairing class AppRespondSubscriber { diff --git a/Sources/Auth/Services/Common/NetworkingInteractor.swift b/Sources/Auth/Services/Common/NetworkingInteractor.swift index e66e97a77..b1cc7684a 100644 --- a/Sources/Auth/Services/Common/NetworkingInteractor.swift +++ b/Sources/Auth/Services/Common/NetworkingInteractor.swift @@ -1,26 +1,10 @@ import Foundation +import Combine +import JSONRPC +import WalletConnectNetworking import WalletConnectRelay import WalletConnectUtils -import Combine import WalletConnectKMS -import JSONRPC - -protocol NetworkInteracting { - var requestPublisher: AnyPublisher {get} - var responsePublisher: AnyPublisher {get} - func subscribe(topic: String) async throws - func unsubscribe(topic: String) - func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws - func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws - func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws - func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws -} - -extension NetworkInteracting { - func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType = .type0) async throws { - try await self.request(request, topic: topic, tag: tag, envelopeType: envelopeType) - } -} class NetworkingInteractor: NetworkInteracting { private var publishers = Set() diff --git a/Sources/Auth/Services/Wallet/WalletPairService.swift b/Sources/Auth/Services/Wallet/WalletPairService.swift index ef4fc2e28..355de0a29 100644 --- a/Sources/Auth/Services/Wallet/WalletPairService.swift +++ b/Sources/Auth/Services/Wallet/WalletPairService.swift @@ -1,6 +1,7 @@ import Foundation import WalletConnectKMS import WalletConnectPairing +import WalletConnectNetworking actor WalletPairService { enum Errors: Error { diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift index 522820187..c54bd3935 100644 --- a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift +++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift @@ -1,7 +1,8 @@ -import Combine import Foundation -import WalletConnectUtils +import Combine import JSONRPC +import WalletConnectNetworking +import WalletConnectUtils import WalletConnectKMS class WalletRequestSubscriber { diff --git a/Sources/Auth/Services/Wallet/WalletRespondService.swift b/Sources/Auth/Services/Wallet/WalletRespondService.swift index 07e01871c..9107f4df3 100644 --- a/Sources/Auth/Services/Wallet/WalletRespondService.swift +++ b/Sources/Auth/Services/Wallet/WalletRespondService.swift @@ -1,7 +1,8 @@ import Foundation -import WalletConnectKMS import JSONRPC +import WalletConnectKMS import WalletConnectUtils +import WalletConnectNetworking actor WalletRespondService { enum Errors: Error { diff --git a/Sources/Auth/Types/Errors/AuthError.swift b/Sources/Auth/Types/Errors/AuthError.swift index 84c43a7d0..f28cab817 100644 --- a/Sources/Auth/Types/Errors/AuthError.swift +++ b/Sources/Auth/Types/Errors/AuthError.swift @@ -1,4 +1,5 @@ import Foundation +import WalletConnectNetworking /// Authentication error public enum AuthError: Codable, Equatable, Error { diff --git a/Sources/Auth/Types/RequestSubscriptionPayload.swift b/Sources/Auth/Types/RequestSubscriptionPayload.swift deleted file mode 100644 index 00a5cdcb4..000000000 --- a/Sources/Auth/Types/RequestSubscriptionPayload.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation -import JSONRPC - -struct RequestSubscriptionPayload: Codable, Equatable { - let topic: String - let request: RPCRequest -} diff --git a/Sources/Auth/Types/ResponseSubscriptionPayload.swift b/Sources/Auth/Types/ResponseSubscriptionPayload.swift deleted file mode 100644 index d6425622f..000000000 --- a/Sources/Auth/Types/ResponseSubscriptionPayload.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation -import JSONRPC - -struct ResponseSubscriptionPayload: Codable, Equatable { - let topic: String - let response: RPCResponse -} diff --git a/Sources/WalletConnectNetworking/NetworkInteractor.swift b/Sources/WalletConnectNetworking/NetworkInteractor.swift new file mode 100644 index 000000000..668448236 --- /dev/null +++ b/Sources/WalletConnectNetworking/NetworkInteractor.swift @@ -0,0 +1,21 @@ +import Foundation +import Combine +import JSONRPC +import WalletConnectKMS + +public protocol NetworkInteracting { + var requestPublisher: AnyPublisher {get} + var responsePublisher: AnyPublisher {get} + func subscribe(topic: String) async throws + func unsubscribe(topic: String) + func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws + func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws + func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws + func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws +} + +extension NetworkInteracting { + public func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType = .type0) async throws { + try await self.request(request, topic: topic, tag: tag, envelopeType: envelopeType) + } +} diff --git a/Sources/Auth/Types/Errors/Reason.swift b/Sources/WalletConnectNetworking/Reason.swift similarity index 75% rename from Sources/Auth/Types/Errors/Reason.swift rename to Sources/WalletConnectNetworking/Reason.swift index 4822fe64c..b9b26694f 100644 --- a/Sources/Auth/Types/Errors/Reason.swift +++ b/Sources/WalletConnectNetworking/Reason.swift @@ -1,6 +1,6 @@ import Foundation -protocol Reason { +public protocol Reason { var code: Int { get } var message: String { get } } diff --git a/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift b/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift new file mode 100644 index 000000000..6132f19a2 --- /dev/null +++ b/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift @@ -0,0 +1,12 @@ +import Foundation +import JSONRPC + +public struct RequestSubscriptionPayload: Codable, Equatable { + public let topic: String + public let request: RPCRequest + + public init(topic: String, request: RPCRequest) { + self.topic = topic + self.request = request + } +} diff --git a/Sources/WalletConnectNetworking/ResponseSubscriptionPayload.swift b/Sources/WalletConnectNetworking/ResponseSubscriptionPayload.swift new file mode 100644 index 000000000..cfe2e6ab2 --- /dev/null +++ b/Sources/WalletConnectNetworking/ResponseSubscriptionPayload.swift @@ -0,0 +1,12 @@ +import Foundation +import JSONRPC + +public struct ResponseSubscriptionPayload: Codable, Equatable { + public let topic: String + public let response: RPCResponse + + public init(topic: String, response: RPCResponse) { + self.topic = topic + self.response = response + } +} diff --git a/Tests/AuthTests/AppRespondSubscriberTests.swift b/Tests/AuthTests/AppRespondSubscriberTests.swift index a5eb20c2f..95f5807db 100644 --- a/Tests/AuthTests/AppRespondSubscriberTests.swift +++ b/Tests/AuthTests/AppRespondSubscriberTests.swift @@ -2,6 +2,7 @@ import Foundation import XCTest @testable import Auth import WalletConnectUtils +import WalletConnectNetworking @testable import WalletConnectKMS @testable import TestingUtils import JSONRPC diff --git a/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift b/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift index 1a9c03790..5c9664d03 100644 --- a/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift +++ b/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift @@ -3,6 +3,7 @@ import Combine @testable import Auth import JSONRPC import WalletConnectKMS +import WalletConnectNetworking struct NetworkingInteractorMock: NetworkInteracting { diff --git a/Tests/AuthTests/Stubs/RequestSubscriptionPayload.swift b/Tests/AuthTests/Stubs/RequestSubscriptionPayload.swift index 48ded537d..156780d42 100644 --- a/Tests/AuthTests/Stubs/RequestSubscriptionPayload.swift +++ b/Tests/AuthTests/Stubs/RequestSubscriptionPayload.swift @@ -1,6 +1,7 @@ import Foundation -@testable import Auth import JSONRPC +import WalletConnectNetworking +@testable import Auth extension RequestSubscriptionPayload { static func stub(id: RPCID) -> RequestSubscriptionPayload { diff --git a/Tests/AuthTests/WalletRequestSubscriberTests.swift b/Tests/AuthTests/WalletRequestSubscriberTests.swift index bce269680..00e7f62ee 100644 --- a/Tests/AuthTests/WalletRequestSubscriberTests.swift +++ b/Tests/AuthTests/WalletRequestSubscriberTests.swift @@ -1,10 +1,11 @@ import Foundation import XCTest -@testable import Auth +import JSONRPC import WalletConnectUtils +import WalletConnectNetworking +@testable import Auth @testable import WalletConnectKMS @testable import TestingUtils -import JSONRPC class WalletRequestSubscriberTests: XCTestCase { var networkingInteractor: NetworkingInteractorMock! From 6c8dcb70334397e3d27a9c8fd6e4d409a1a7ac15 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 1 Sep 2022 18:41:10 +0300 Subject: [PATCH 35/92] Auth NetworkInteractor moved to package --- Package.swift | 10 ++++--- Sources/Auth/AuthClientFactory.swift | 1 + .../NetworkingInteractor.swift | 30 ++++++++++--------- 3 files changed, 23 insertions(+), 18 deletions(-) rename Sources/{Auth/Services/Common => WalletConnectNetworking}/NetworkingInteractor.swift (82%) diff --git a/Package.swift b/Package.swift index 6e0da39f7..1578731af 100644 --- a/Package.swift +++ b/Package.swift @@ -41,9 +41,6 @@ let package = Package( .target( name: "Auth", dependencies: [ - "WalletConnectRelay", - "WalletConnectUtils", - "WalletConnectKMS", "WalletConnectPairing", "WalletConnectNetworking", .product(name: "Web3", package: "Web3.swift") @@ -71,7 +68,12 @@ let package = Package( dependencies: []), .target( name: "WalletConnectNetworking", - dependencies: ["JSONRPC", "WalletConnectKMS"]), + dependencies: [ + "JSONRPC", + "WalletConnectKMS", + "WalletConnectRelay", + "WalletConnectUtils", + ]), .target( name: "WalletConnectRouter", dependencies: []), diff --git a/Sources/Auth/AuthClientFactory.swift b/Sources/Auth/AuthClientFactory.swift index 3dc48fbea..b084a1846 100644 --- a/Sources/Auth/AuthClientFactory.swift +++ b/Sources/Auth/AuthClientFactory.swift @@ -3,6 +3,7 @@ import WalletConnectRelay import WalletConnectUtils import WalletConnectKMS import WalletConnectPairing +import WalletConnectNetworking public struct AuthClientFactory { diff --git a/Sources/Auth/Services/Common/NetworkingInteractor.swift b/Sources/WalletConnectNetworking/NetworkingInteractor.swift similarity index 82% rename from Sources/Auth/Services/Common/NetworkingInteractor.swift rename to Sources/WalletConnectNetworking/NetworkingInteractor.swift index b1cc7684a..91a664578 100644 --- a/Sources/Auth/Services/Common/NetworkingInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkingInteractor.swift @@ -1,12 +1,11 @@ import Foundation import Combine import JSONRPC -import WalletConnectNetworking import WalletConnectRelay import WalletConnectUtils import WalletConnectKMS -class NetworkingInteractor: NetworkInteracting { +public class NetworkingInteractor: NetworkInteracting { private var publishers = Set() private let relayClient: RelayClient private let serializer: Serializing @@ -14,21 +13,24 @@ class NetworkingInteractor: NetworkInteracting { private let logger: ConsoleLogging private let requestPublisherSubject = PassthroughSubject() - var requestPublisher: AnyPublisher { + private let responsePublisherSubject = PassthroughSubject() + + public var requestPublisher: AnyPublisher { requestPublisherSubject.eraseToAnyPublisher() } - private let responsePublisherSubject = PassthroughSubject() - var responsePublisher: AnyPublisher { + public var responsePublisher: AnyPublisher { responsePublisherSubject.eraseToAnyPublisher() } - var socketConnectionStatusPublisher: AnyPublisher + public var socketConnectionStatusPublisher: AnyPublisher - init(relayClient: RelayClient, + public init( + relayClient: RelayClient, serializer: Serializing, logger: ConsoleLogging, - rpcHistory: RPCHistory) { + rpcHistory: RPCHistory + ) { self.relayClient = relayClient self.serializer = serializer self.rpcHistory = rpcHistory @@ -40,11 +42,11 @@ class NetworkingInteractor: NetworkInteracting { .store(in: &publishers) } - func subscribe(topic: String) async throws { + public func subscribe(topic: String) async throws { try await relayClient.subscribe(topic: topic) } - func unsubscribe(topic: String) { + public func unsubscribe(topic: String) { relayClient.unsubscribe(topic: topic) { [unowned self] error in if let error = error { logger.error(error) @@ -54,7 +56,7 @@ class NetworkingInteractor: NetworkInteracting { } } - func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + public func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) let message = try! serializer.serialize(topic: topic, encodable: request, envelopeType: envelopeType) try await relayClient.publish(topic: topic, payload: message, tag: tag) @@ -63,7 +65,7 @@ class NetworkingInteractor: NetworkInteracting { /// Completes with an acknowledgement from the relay network. /// completes with error if networking client was not able to send a message /// TODO - relay client should provide async function - continualion should be removed from here - func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws { + public func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws { do { try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) let message = try serializer.serialize(topic: topic, encodable: request) @@ -81,13 +83,13 @@ class NetworkingInteractor: NetworkInteracting { } } - func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + public func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { try rpcHistory.resolve(response) let message = try! serializer.serialize(topic: topic, encodable: response, envelopeType: envelopeType) try await relayClient.publish(topic: topic, payload: message, tag: tag) } - func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { + public func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { let error = JSONRPCError(code: reason.code, message: reason.message) let response = RPCResponse(id: requestId, error: error) let message = try! serializer.serialize(topic: topic, encodable: response, envelopeType: envelopeType) From 8cf76ad1772fad3a1b0fc3dc0e15c225b2a2508d Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 1 Sep 2022 22:24:12 +0300 Subject: [PATCH 36/92] Chat Genaric interactor connected --- .../xcschemes/WalletConnectChat.xcscheme | 77 +++++++++++ Package.swift | 17 +-- Sources/Auth/AuthClientFactory.swift | 3 +- Sources/Chat/ChatClient.swift | 7 +- Sources/Chat/ChatClientFactory.swift | 10 +- Sources/Chat/NetworkingInteractor.swift | 122 ------------------ .../Chat/ProtocolServices/Common/File.swift | 4 +- .../Common/MessagingService.swift | 44 +++---- .../Invitee/InvitationHandlingService.swift | 64 ++++----- .../Invitee/RegistryService.swift | 1 + .../Inviter/InviteService.swift | 56 ++++---- Sources/Chat/Types/ChatError.swift | 15 +++ Sources/Chat/Types/ChatRequestParams.swift | 63 --------- Sources/Chat/Types/ChatResponse.swift | 9 -- .../{InviteParams.swift => Invite.swift} | 8 ++ Sources/Chat/Types/Message.swift | 22 +++- .../Types/RequestSubscriptionPayload.swift | 7 - .../NetworkInteractor.swift | 23 +++- .../NetworkingInteractor.swift | 14 +- .../Mocks/NetworkingInteractorMock.swift | 44 ------- .../Mocks/NetworkingInteractorMock.swift | 50 ------- Tests/ChatTests/RegistryManagerTests.swift | 5 +- .../NetworkingInteractorMock.swift | 58 +++++++++ 23 files changed, 298 insertions(+), 425 deletions(-) create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/WalletConnectChat.xcscheme delete mode 100644 Sources/Chat/NetworkingInteractor.swift delete mode 100644 Sources/Chat/Types/ChatRequestParams.swift delete mode 100644 Sources/Chat/Types/ChatResponse.swift rename Sources/Chat/Types/{InviteParams.swift => Invite.swift} (71%) delete mode 100644 Sources/Chat/Types/RequestSubscriptionPayload.swift delete mode 100644 Tests/AuthTests/Mocks/NetworkingInteractorMock.swift delete mode 100644 Tests/ChatTests/Mocks/NetworkingInteractorMock.swift create mode 100644 Tests/TestingUtils/NetworkingInteractorMock.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectChat.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectChat.xcscheme new file mode 100644 index 000000000..c04003b78 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectChat.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Package.swift b/Package.swift index 1578731af..a562eca58 100644 --- a/Package.swift +++ b/Package.swift @@ -36,15 +36,11 @@ let package = Package( path: "Sources/WalletConnectSign"), .target( name: "Chat", - dependencies: ["WalletConnectRelay", "WalletConnectUtils", "WalletConnectKMS"], + dependencies: ["WalletConnectNetworking"], path: "Sources/Chat"), .target( name: "Auth", - dependencies: [ - "WalletConnectPairing", - "WalletConnectNetworking", - .product(name: "Web3", package: "Web3.swift") - ], + dependencies: ["WalletConnectPairing", "WalletConnectNetworking", .product(name: "Web3", package: "Web3.swift")], path: "Sources/Auth"), .target( name: "WalletConnectRelay", @@ -68,12 +64,7 @@ let package = Package( dependencies: []), .target( name: "WalletConnectNetworking", - dependencies: [ - "JSONRPC", - "WalletConnectKMS", - "WalletConnectRelay", - "WalletConnectUtils", - ]), + dependencies: ["JSONRPC", "WalletConnectKMS", "WalletConnectRelay", "WalletConnectUtils"]), .target( name: "WalletConnectRouter", dependencies: []), @@ -94,7 +85,7 @@ let package = Package( dependencies: ["WalletConnectKMS", "WalletConnectUtils", "TestingUtils"]), .target( name: "TestingUtils", - dependencies: ["WalletConnectUtils", "WalletConnectKMS", "JSONRPC", "WalletConnectPairing"], + dependencies: ["WalletConnectPairing", "WalletConnectNetworking"], path: "Tests/TestingUtils"), .testTarget( name: "WalletConnectUtilsTests", diff --git a/Sources/Auth/AuthClientFactory.swift b/Sources/Auth/AuthClientFactory.swift index b084a1846..469fa37a6 100644 --- a/Sources/Auth/AuthClientFactory.swift +++ b/Sources/Auth/AuthClientFactory.swift @@ -42,6 +42,7 @@ public struct AuthClientFactory { pendingRequestsProvider: pendingRequestsProvider, cleanupService: cleanupService, logger: logger, - pairingStorage: pairingStore, socketConnectionStatusPublisher: relayClient.socketConnectionStatusPublisher) + pairingStorage: pairingStore, + socketConnectionStatusPublisher: relayClient.socketConnectionStatusPublisher) } } diff --git a/Sources/Chat/ChatClient.swift b/Sources/Chat/ChatClient.swift index 1c44bbeb6..678a3436e 100644 --- a/Sources/Chat/ChatClient.swift +++ b/Sources/Chat/ChatClient.swift @@ -2,6 +2,7 @@ import Foundation import WalletConnectUtils import WalletConnectKMS import WalletConnectRelay +import WalletConnectNetworking import Combine public class ChatClient { @@ -16,7 +17,7 @@ public class ChatClient { private let kms: KeyManagementService private let threadStore: Database private let messagesStore: Database - private let invitePayloadStore: CodableStore<(RequestSubscriptionPayload)> + private let invitePayloadStore: CodableStore public let socketConnectionStatusPublisher: AnyPublisher @@ -47,7 +48,7 @@ public class ChatClient { kms: KeyManagementService, threadStore: Database, messagesStore: Database, - invitePayloadStore: CodableStore<(RequestSubscriptionPayload)>, + invitePayloadStore: CodableStore, socketConnectionStatusPublisher: AnyPublisher ) { self.registry = registry @@ -121,7 +122,7 @@ public class ChatClient { public func getInvites(account: Account) -> [Invite] { var invites = [Invite]() invitePayloadStore.getAll().forEach { - guard case .invite(let invite) = $0.request.params else {return} + guard let invite = try? $0.request.params?.get(Invite.self) else {return} invites.append(invite) } return invites diff --git a/Sources/Chat/ChatClientFactory.swift b/Sources/Chat/ChatClientFactory.swift index 89f4907b1..47ae2dcdd 100644 --- a/Sources/Chat/ChatClientFactory.swift +++ b/Sources/Chat/ChatClientFactory.swift @@ -2,6 +2,7 @@ import Foundation import WalletConnectRelay import WalletConnectUtils import WalletConnectKMS +import WalletConnectNetworking public struct ChatClientFactory { @@ -10,17 +11,18 @@ public struct ChatClientFactory { relayClient: RelayClient, kms: KeyManagementService, logger: ConsoleLogging, - keyValueStorage: KeyValueStorage) -> ChatClient { + keyValueStorage: KeyValueStorage + ) -> ChatClient { let topicToRegistryRecordStore = CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.topicToInvitationPubKey.rawValue) let serialiser = Serializer(kms: kms) - let jsonRpcHistory = JsonRpcHistory(logger: logger, keyValueStore: CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.jsonRpcHistory.rawValue)) - let networkingInteractor = NetworkingInteractor(relayClient: relayClient, serializer: serialiser, logger: logger, jsonRpcHistory: jsonRpcHistory) + let rpcHistory = RPCHistory(keyValueStore: CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.jsonRpcHistory.rawValue)) + let networkingInteractor = NetworkingInteractor(relayClient: relayClient, serializer: serialiser, logger: logger, rpcHistory: rpcHistory) let invitePayloadStore = CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.invite.rawValue) let registryService = RegistryService(registry: registry, networkingInteractor: networkingInteractor, kms: kms, logger: logger, topicToRegistryRecordStore: topicToRegistryRecordStore) let threadStore = Database(keyValueStorage: keyValueStorage, identifier: StorageDomainIdentifiers.threads.rawValue) let resubscriptionService = ResubscriptionService(networkingInteractor: networkingInteractor, threadStore: threadStore, logger: logger) let invitationHandlingService = InvitationHandlingService(registry: registry, networkingInteractor: networkingInteractor, kms: kms, logger: logger, topicToRegistryRecordStore: topicToRegistryRecordStore, invitePayloadStore: invitePayloadStore, threadsStore: threadStore) - let inviteService = InviteService(networkingInteractor: networkingInteractor, kms: kms, threadStore: threadStore, logger: logger) + let inviteService = InviteService(networkingInteractor: networkingInteractor, kms: kms, threadStore: threadStore, rpcHistory: rpcHistory, logger: logger) let leaveService = LeaveService() let messagesStore = Database(keyValueStorage: keyValueStorage, identifier: StorageDomainIdentifiers.messages.rawValue) let messagingService = MessagingService(networkingInteractor: networkingInteractor, messagesStore: messagesStore, threadStore: threadStore, logger: logger) diff --git a/Sources/Chat/NetworkingInteractor.swift b/Sources/Chat/NetworkingInteractor.swift deleted file mode 100644 index ee31ec033..000000000 --- a/Sources/Chat/NetworkingInteractor.swift +++ /dev/null @@ -1,122 +0,0 @@ -import Foundation -import Combine -import WalletConnectRelay -import WalletConnectUtils -import WalletConnectKMS - -protocol NetworkInteracting { - var socketConnectionStatusPublisher: AnyPublisher { get } - var requestPublisher: AnyPublisher {get} - var responsePublisher: AnyPublisher {get} - func respondSuccess(payload: RequestSubscriptionPayload) async throws - func subscribe(topic: String) async throws - func request(_ request: JSONRPCRequest, topic: String, envelopeType: Envelope.EnvelopeType) async throws - func respond(topic: String, response: JsonRpcResult, tag: Int) async throws -} - -extension NetworkInteracting { - func request(_ request: JSONRPCRequest, topic: String, envelopeType: Envelope.EnvelopeType = .type0) async throws { - try await self.request(request, topic: topic, envelopeType: envelopeType) - } -} - -class NetworkingInteractor: NetworkInteracting { - enum Error: Swift.Error { - case failedToInitialiseMethodFromRecord - } - private let jsonRpcHistory: JsonRpcHistory - private let serializer: Serializing - private let relayClient: RelayClient - private let logger: ConsoleLogging - var requestPublisher: AnyPublisher { - requestPublisherSubject.eraseToAnyPublisher() - } - private let requestPublisherSubject = PassthroughSubject() - - var responsePublisher: AnyPublisher { - responsePublisherSubject.eraseToAnyPublisher() - } - private let responsePublisherSubject = PassthroughSubject() - var socketConnectionStatusPublisher: AnyPublisher - - private var publishers = Set() - - init(relayClient: RelayClient, - serializer: Serializing, - logger: ConsoleLogging, - jsonRpcHistory: JsonRpcHistory - ) { - self.relayClient = relayClient - self.serializer = serializer - self.jsonRpcHistory = jsonRpcHistory - self.logger = logger - self.socketConnectionStatusPublisher = relayClient.socketConnectionStatusPublisher - - relayClient.messagePublisher.sink { [unowned self] (topic, message) in - manageSubscription(topic, message) - }.store(in: &publishers) - } - - func request(_ request: JSONRPCRequest, topic: String, envelopeType: Envelope.EnvelopeType) async throws { - try jsonRpcHistory.set(topic: topic, request: request) - let message = try! serializer.serialize(topic: topic, encodable: request, envelopeType: envelopeType) - try await relayClient.publish(topic: topic, payload: message, tag: request.params.tag) - } - - func respondSuccess(payload: RequestSubscriptionPayload) async throws { - let response = JSONRPCResponse(id: payload.request.id, result: AnyCodable(true)) - try await respond(topic: payload.topic, response: JsonRpcResult.response(response), tag: payload.request.params.responseTag) - } - - func respond(topic: String, response: JsonRpcResult, tag: Int) async throws { - _ = try jsonRpcHistory.resolve(response: response) - let message = try serializer.serialize(topic: topic, encodable: response.value) - try await relayClient.publish(topic: topic, payload: message, tag: tag, prompt: false) - } - - func subscribe(topic: String) async throws { - try await relayClient.subscribe(topic: topic) - } - - private func manageSubscription(_ topic: String, _ encodedEnvelope: String) { - if let deserializedJsonRpcRequest: JSONRPCRequest = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { - handleChatRequest(topic: topic, request: deserializedJsonRpcRequest) - } else if let deserializedJsonRpcResponse: JSONRPCResponse = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { - handleJsonRpcResponse(response: deserializedJsonRpcResponse) - } else if let deserializedJsonRpcError: JSONRPCErrorResponse = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { - handleJsonRpcErrorResponse(response: deserializedJsonRpcError) - } else { - logger.warn("Networking Interactor - Received unknown object type from networking relay") - } - } - - private func handleChatRequest(topic: String, request: JSONRPCRequest) { - do { - try jsonRpcHistory.set(topic: topic, request: request) - let payload = RequestSubscriptionPayload(topic: topic, request: request) - requestPublisherSubject.send(payload) - } catch { - logger.debug(error) - } - } - - private func handleJsonRpcResponse(response: JSONRPCResponse) { - do { - let record = try jsonRpcHistory.resolve(response: JsonRpcResult.response(response)) - let params = try record.request.params.get(ChatRequestParams.self) - let chatResponse = ChatResponse( - topic: record.topic, - requestMethod: record.request.method, - requestParams: params, - result: JsonRpcResult.response(response)) - responsePublisherSubject.send(chatResponse) - } catch { - logger.debug("Handle json rpc response error: \(error)") - } - } - - private func handleJsonRpcErrorResponse(response: JSONRPCErrorResponse) { - // todo - } - -} diff --git a/Sources/Chat/ProtocolServices/Common/File.swift b/Sources/Chat/ProtocolServices/Common/File.swift index 8bd04f569..e821f03d7 100644 --- a/Sources/Chat/ProtocolServices/Common/File.swift +++ b/Sources/Chat/ProtocolServices/Common/File.swift @@ -1,6 +1,8 @@ import Foundation -import WalletConnectUtils import Combine +import WalletConnectRelay +import WalletConnectUtils +import WalletConnectNetworking class ResubscriptionService { private let networkingInteractor: NetworkInteracting diff --git a/Sources/Chat/ProtocolServices/Common/MessagingService.swift b/Sources/Chat/ProtocolServices/Common/MessagingService.swift index ca774136d..eaeaf061e 100644 --- a/Sources/Chat/ProtocolServices/Common/MessagingService.swift +++ b/Sources/Chat/ProtocolServices/Common/MessagingService.swift @@ -1,6 +1,8 @@ import Foundation -import WalletConnectUtils import Combine +import JSONRPC +import WalletConnectUtils +import WalletConnectNetworking class MessagingService { enum Errors: Error { @@ -31,8 +33,8 @@ class MessagingService { guard let authorAccount = thread?.selfAccount else { throw Errors.threadDoNotExist} let timestamp = Int64(Date().timeIntervalSince1970 * 1000) let message = Message(topic: topic, message: messageString, authorAccount: authorAccount, timestamp: timestamp) - let request = JSONRPCRequest(params: .message(message)) - try await networkingInteractor.request(request, topic: topic, envelopeType: .type0) + let request = RPCRequest(method: Message.method, params: message) + try await networkingInteractor.request(request, topic: topic, tag: Message.tag) Task(priority: .background) { await messagesStore.add(message) onMessage?(message) @@ -41,38 +43,34 @@ class MessagingService { private func setUpResponseHandling() { networkingInteractor.responsePublisher - .sink { [unowned self] response in - switch response.requestParams { - case .message: - handleMessageResponse(response) - default: - return - } + .sink { [unowned self] payload in + logger.debug("Received Message response") }.store(in: &publishers) } private func setUpRequestHandling() { - networkingInteractor.requestPublisher.sink { [unowned self] subscriptionPayload in - switch subscriptionPayload.request.params { - case .message(var message): - message.topic = subscriptionPayload.topic - handleMessage(message, subscriptionPayload) - default: - return + networkingInteractor.requestPublisher.sink { [unowned self] payload in + do { + guard + let requestId = payload.request.id, payload.request.method == Message.method, + var message = try payload.request.params?.get(Message.self) + else { return } + + message.topic = payload.topic + + handleMessage(message, topic: payload.topic, requestId: requestId) + } catch { + logger.debug("Handling message response has failed") } }.store(in: &publishers) } - private func handleMessage(_ message: Message, _ payload: RequestSubscriptionPayload) { + private func handleMessage(_ message: Message, topic: String, requestId: RPCID) { Task(priority: .background) { - try await networkingInteractor.respondSuccess(payload: payload) + try await networkingInteractor.respondSuccess(topic: topic, requestId: requestId, tag: Message.tag) await messagesStore.add(message) logger.debug("Received message") onMessage?(message) } } - - private func handleMessageResponse(_ response: ChatResponse) { - logger.debug("Received Message response") - } } diff --git a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift index 38021014b..102e9f00a 100644 --- a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift +++ b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift @@ -1,8 +1,10 @@ import Foundation +import Combine +import JSONRPC import WalletConnectKMS import WalletConnectUtils import WalletConnectRelay -import Combine +import WalletConnectNetworking class InvitationHandlingService { enum Error: Swift.Error { @@ -11,7 +13,7 @@ class InvitationHandlingService { var onInvite: ((Invite) -> Void)? var onNewThread: ((Thread) -> Void)? private let networkingInteractor: NetworkInteracting - private let invitePayloadStore: CodableStore<(RequestSubscriptionPayload)> + private let invitePayloadStore: CodableStore private let topicToRegistryRecordStore: CodableStore private let registry: Registry private let logger: ConsoleLogging @@ -37,27 +39,22 @@ class InvitationHandlingService { } func accept(inviteId: String) async throws { - guard let payload = try invitePayloadStore.get(key: inviteId) else { throw Error.inviteForIdNotFound } let selfThreadPubKey = try kms.createX25519KeyPair() let inviteResponse = InviteResponse(publicKey: selfThreadPubKey.hexRepresentation) - let response = JsonRpcResult.response(JSONRPCResponse(id: payload.request.id, result: AnyCodable(inviteResponse))) - - guard case .invite(let invite) = payload.request.params else {return} + guard let requestId = payload.request.id, let invite = try? payload.request.params?.get(Invite.self) + else { return } - let responseTopic = try getInviteResponseTopic(payload, invite) - - try await networkingInteractor.respond(topic: responseTopic, response: response, tag: payload.request.params.responseTag) + let response = RPCResponse(id: requestId, result: inviteResponse) + let responseTopic = try getInviteResponseTopic(requestTopic: payload.topic, invite: invite) + try await networkingInteractor.respond(topic: responseTopic, response: response, tag: Invite.tag) let threadAgreementKeys = try kms.performKeyAgreement(selfPublicKey: selfThreadPubKey, peerPublicKey: invite.publicKey) - let threadTopic = threadAgreementKeys.derivedTopic() - try kms.setSymmetricKey(threadAgreementKeys.sharedKey, for: threadTopic) - try await networkingInteractor.subscribe(topic: threadTopic) logger.debug("Accepting an invite on topic: \(threadTopic)") @@ -73,47 +70,36 @@ class InvitationHandlingService { } func reject(inviteId: String) async throws { - guard let payload = try invitePayloadStore.get(key: inviteId) else { throw Error.inviteForIdNotFound } - guard case .invite(let invite) = payload.request.params else {return} + guard let requestId = payload.request.id, let invite = try? payload.request.params?.get(Invite.self) + else { return } - let responseTopic = try getInviteResponseTopic(payload, invite) + let responseTopic = try getInviteResponseTopic(requestTopic: payload.topic, invite: invite) - // TODO - error not in specs yet - let error = JSONRPCErrorResponse.Error(code: 0, message: "user rejected") - let response = JsonRpcResult.error(JSONRPCErrorResponse(id: payload.request.id, error: error)) - - try await networkingInteractor.respond(topic: responseTopic, response: response, tag: payload.request.params.responseTag) + try await networkingInteractor.respondError(topic: responseTopic, requestId: requestId, tag: Invite.tag, reason: ChatError.userRejected) invitePayloadStore.delete(forKey: inviteId) } private func setUpRequestHandling() { - networkingInteractor.requestPublisher.sink { [unowned self] subscriptionPayload in - switch subscriptionPayload.request.params { - case .invite(let invite): - do { - try handleInvite(invite, subscriptionPayload) - } catch { - logger.debug("Did not handle invite, error: \(error)") - } - default: - return - } - }.store(in: &publishers) - } + networkingInteractor.requestPublisher.sink { [unowned self] payload in + guard payload.request.method == "wc_chatInvite" + else { return } + + guard let invite = try? payload.request.params?.get(Invite.self) + else { return } - private func handleInvite(_ invite: Invite, _ payload: RequestSubscriptionPayload) throws { - logger.debug("did receive an invite") - invitePayloadStore.set(payload, forKey: invite.publicKey) - onInvite?(invite) + logger.debug("did receive an invite") + invitePayloadStore.set(payload, forKey: invite.publicKey) + onInvite?(invite) + }.store(in: &publishers) } - private func getInviteResponseTopic(_ payload: RequestSubscriptionPayload, _ invite: Invite) throws -> String { + private func getInviteResponseTopic(requestTopic: String, invite: Invite) throws -> String { // todo - remove topicToInvitationPubKeyStore ? - guard let record = try? topicToRegistryRecordStore.get(key: payload.topic) else { + guard let record = try? topicToRegistryRecordStore.get(key: requestTopic) else { logger.debug("PubKey for invitation topic not found") fatalError("todo") } diff --git a/Sources/Chat/ProtocolServices/Invitee/RegistryService.swift b/Sources/Chat/ProtocolServices/Invitee/RegistryService.swift index 33a5241a8..4476f306e 100644 --- a/Sources/Chat/ProtocolServices/Invitee/RegistryService.swift +++ b/Sources/Chat/ProtocolServices/Invitee/RegistryService.swift @@ -1,6 +1,7 @@ import Foundation import WalletConnectUtils import WalletConnectKMS +import WalletConnectNetworking actor RegistryService { let networkingInteractor: NetworkInteracting diff --git a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift index f3a5d32b9..d3051cf2d 100644 --- a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift +++ b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift @@ -1,7 +1,9 @@ import Foundation +import Combine +import JSONRPC import WalletConnectKMS import WalletConnectUtils -import Combine +import WalletConnectNetworking class InviteService { private var publishers = [AnyCancellable]() @@ -9,6 +11,7 @@ class InviteService { private let logger: ConsoleLogging private let kms: KeyManagementService private let threadStore: Database + private let rpcHistory: RPCHistory var onNewThread: ((Thread) -> Void)? var onInvite: ((Invite) -> Void)? @@ -16,11 +19,13 @@ class InviteService { init(networkingInteractor: NetworkInteracting, kms: KeyManagementService, threadStore: Database, + rpcHistory: RPCHistory, logger: ConsoleLogging) { self.kms = kms self.networkingInteractor = networkingInteractor self.logger = logger self.threadStore = threadStore + self.rpcHistory = rpcHistory setUpResponseHandling() } @@ -37,7 +42,7 @@ class InviteService { // overrides on invite toipic try kms.setSymmetricKey(symKeyI.sharedKey, for: inviteTopic) - let request = JSONRPCRequest(params: .invite(invite)) + let request = RPCRequest(method: Invite.method, params: invite) // 2. Proposer subscribes to topic R which is the hash of the derived symKey let responseTopic = symKeyI.derivedTopic() @@ -45,40 +50,35 @@ class InviteService { try kms.setSymmetricKey(symKeyI.sharedKey, for: responseTopic) try await networkingInteractor.subscribe(topic: responseTopic) - try await networkingInteractor.request(request, topic: inviteTopic, envelopeType: .type1(pubKey: selfPubKeyY.rawRepresentation)) + try await networkingInteractor.request(request, topic: inviteTopic, tag: Invite.tag, envelopeType: .type1(pubKey: selfPubKeyY.rawRepresentation)) logger.debug("invite sent on topic: \(inviteTopic)") } private func setUpResponseHandling() { networkingInteractor.responsePublisher - .sink { [unowned self] response in - switch response.requestParams { - case .invite: - handleInviteResponse(response) - default: - return - } - }.store(in: &publishers) - } + .sink { [unowned self] payload in + do { + guard + let requestId = payload.response.id, + let request = rpcHistory.get(recordId: requestId)?.request, + let requestParams = request.params, request.method == Invite.method + else { return } - private func handleInviteResponse(_ response: ChatResponse) { - switch response.result { - case .response(let jsonrpc): - do { - let inviteResponse = try jsonrpc.result.get(InviteResponse.self) - logger.debug("Invite has been accepted") - guard case .invite(let inviteParams) = response.requestParams else { return } - Task(priority: .background) { - try await createThread(selfPubKeyHex: inviteParams.publicKey, peerPubKey: inviteResponse.publicKey, account: inviteParams.account, peerAccount: peerAccount) + guard let inviteResponse = try payload.response.result?.get(InviteResponse.self) + else { return } + + let inviteParams = try requestParams.get(Invite.self) + + logger.debug("Invite has been accepted") + + Task(priority: .background) { + try await createThread(selfPubKeyHex: inviteParams.publicKey, peerPubKey: inviteResponse.publicKey, account: inviteParams.account, peerAccount: peerAccount) + } + } catch { + logger.debug("Handling invite response has failed") } - } catch { - logger.debug("Handling invite response has failed") - } - case .error: - logger.debug("Invite has been rejected") - // TODO - remove keys, clean storage - } + }.store(in: &publishers) } private func createThread(selfPubKeyHex: String, peerPubKey: String, account: Account, peerAccount: Account) async throws { diff --git a/Sources/Chat/Types/ChatError.swift b/Sources/Chat/Types/ChatError.swift index 0672ba16f..edac89e77 100644 --- a/Sources/Chat/Types/ChatError.swift +++ b/Sources/Chat/Types/ChatError.swift @@ -1,6 +1,21 @@ import Foundation +import WalletConnectNetworking enum ChatError: Error { case noInviteForId case recordNotFound + case userRejected +} + +extension ChatError: Reason { + + var code: Int { + // Errors not in specs yet + return 0 + } + + var message: String { + // Errors not in specs yet + return localizedDescription + } } diff --git a/Sources/Chat/Types/ChatRequestParams.swift b/Sources/Chat/Types/ChatRequestParams.swift deleted file mode 100644 index 51b9e512d..000000000 --- a/Sources/Chat/Types/ChatRequestParams.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Foundation -import WalletConnectUtils - -enum ChatRequestParams: Codable, Equatable { - enum Errors: Error { - case decoding - } - case invite(Invite) - case message(Message) - - private enum CodingKeys: String, CodingKey { - case invite - case message - } - - func encode(to encoder: Encoder) throws { - switch self { - case .invite(let invite): - try invite.encode(to: encoder) - case .message(let message): - try message.encode(to: encoder) - } - } - - init(from decoder: Decoder) throws { - if let invite = try? Invite(from: decoder) { - self = .invite(invite) - } else if let massage = try? Message(from: decoder) { - self = .message(massage) - } else { - throw Errors.decoding - } - } -} - -extension ChatRequestParams { - - var tag: Int { - switch self { - case .invite: - return 2000 - case .message: - return 2002 - } - } - - var responseTag: Int { - return tag + 1 - } -} - -extension JSONRPCRequest { - init(id: Int64 = JsonRpcID.generate(), params: T) where T == ChatRequestParams { - var method: String! - switch params { - case .invite: - method = "wc_chatInvite" - case .message: - method = "wc_chatMessage" - } - self.init(id: id, method: method, params: params) - } -} diff --git a/Sources/Chat/Types/ChatResponse.swift b/Sources/Chat/Types/ChatResponse.swift deleted file mode 100644 index 448abb7e8..000000000 --- a/Sources/Chat/Types/ChatResponse.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation -import WalletConnectUtils - -struct ChatResponse: Codable { - let topic: String - let requestMethod: String - let requestParams: ChatRequestParams - let result: JsonRpcResult -} diff --git a/Sources/Chat/Types/InviteParams.swift b/Sources/Chat/Types/Invite.swift similarity index 71% rename from Sources/Chat/Types/InviteParams.swift rename to Sources/Chat/Types/Invite.swift index f5b0f7180..5f69e72e8 100644 --- a/Sources/Chat/Types/InviteParams.swift +++ b/Sources/Chat/Types/Invite.swift @@ -12,4 +12,12 @@ public struct Invite: Codable, Equatable { public let message: String public let account: Account public let publicKey: String + + static var tag: Int { + return 2000 + } + + static var method: String { + return "wc_chatInvite" + } } diff --git a/Sources/Chat/Types/Message.swift b/Sources/Chat/Types/Message.swift index 7325d8a21..09113ed2a 100644 --- a/Sources/Chat/Types/Message.swift +++ b/Sources/Chat/Types/Message.swift @@ -2,13 +2,6 @@ import Foundation import WalletConnectUtils public struct Message: Codable, Equatable { - internal init(topic: String? = nil, message: String, authorAccount: Account, timestamp: Int64) { - self.topic = topic - self.message = message - self.authorAccount = authorAccount - self.timestamp = timestamp - } - public var topic: String? public let message: String public let authorAccount: Account @@ -20,4 +13,19 @@ public struct Message: Codable, Equatable { case authorAccount case timestamp } + + static var tag: Int { + return 2002 + } + + static var method: String { + return "wc_chatMessage" + } + + init(topic: String? = nil, message: String, authorAccount: Account, timestamp: Int64) { + self.topic = topic + self.message = message + self.authorAccount = authorAccount + self.timestamp = timestamp + } } diff --git a/Sources/Chat/Types/RequestSubscriptionPayload.swift b/Sources/Chat/Types/RequestSubscriptionPayload.swift deleted file mode 100644 index 575af574a..000000000 --- a/Sources/Chat/Types/RequestSubscriptionPayload.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation -import WalletConnectUtils - -struct RequestSubscriptionPayload: Codable { - let topic: String - let request: JSONRPCRequest -} diff --git a/Sources/WalletConnectNetworking/NetworkInteractor.swift b/Sources/WalletConnectNetworking/NetworkInteractor.swift index 668448236..fca33ed16 100644 --- a/Sources/WalletConnectNetworking/NetworkInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkInteractor.swift @@ -2,20 +2,35 @@ import Foundation import Combine import JSONRPC import WalletConnectKMS +import WalletConnectRelay public protocol NetworkInteracting { - var requestPublisher: AnyPublisher {get} - var responsePublisher: AnyPublisher {get} + var socketConnectionStatusPublisher: AnyPublisher { get } + var requestPublisher: AnyPublisher { get } + var responsePublisher: AnyPublisher { get } func subscribe(topic: String) async throws func unsubscribe(topic: String) func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws + func respondSuccess(topic: String, requestId: RPCID, tag: Int, envelopeType: Envelope.EnvelopeType) async throws func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws } extension NetworkInteracting { - public func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType = .type0) async throws { - try await self.request(request, topic: topic, tag: tag, envelopeType: envelopeType) + public func request(_ request: RPCRequest, topic: String, tag: Int) async throws { + try await self.request(request, topic: topic, tag: tag, envelopeType: .type0) + } + + public func respond(topic: String, response: RPCResponse, tag: Int) async throws { + try await self.respond(topic: topic, response: response, tag: tag, envelopeType: .type0) + } + + public func respondSuccess(topic: String, requestId: RPCID, tag: Int) async throws { + try await self.respondSuccess(topic: topic, requestId: requestId, tag: tag, envelopeType: .type0) + } + + public func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason) async throws { + try await self.respondError(topic: topic, requestId: requestId, tag: tag, reason: reason, envelopeType: .type0) } } diff --git a/Sources/WalletConnectNetworking/NetworkingInteractor.swift b/Sources/WalletConnectNetworking/NetworkingInteractor.swift index 91a664578..cf4f99e46 100644 --- a/Sources/WalletConnectNetworking/NetworkingInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkingInteractor.swift @@ -27,9 +27,9 @@ public class NetworkingInteractor: NetworkInteracting { public init( relayClient: RelayClient, - serializer: Serializing, - logger: ConsoleLogging, - rpcHistory: RPCHistory + serializer: Serializing, + logger: ConsoleLogging, + rpcHistory: RPCHistory ) { self.relayClient = relayClient self.serializer = serializer @@ -89,11 +89,15 @@ public class NetworkingInteractor: NetworkInteracting { try await relayClient.publish(topic: topic, payload: message, tag: tag) } + public func respondSuccess(topic: String, requestId: RPCID, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + let response = RPCResponse(id: requestId, result: true) + try await respond(topic: topic, response: response, tag: tag, envelopeType: envelopeType) + } + public func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { let error = JSONRPCError(code: reason.code, message: reason.message) let response = RPCResponse(id: requestId, error: error) - let message = try! serializer.serialize(topic: topic, encodable: response, envelopeType: envelopeType) - try await relayClient.publish(topic: topic, payload: message, tag: tag) + try await respond(topic: topic, response: response, tag: tag, envelopeType: envelopeType) } private func manageSubscription(_ topic: String, _ encodedEnvelope: String) { diff --git a/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift b/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift deleted file mode 100644 index 5c9664d03..000000000 --- a/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift +++ /dev/null @@ -1,44 +0,0 @@ -import Foundation -import Combine -@testable import Auth -import JSONRPC -import WalletConnectKMS -import WalletConnectNetworking - -struct NetworkingInteractorMock: NetworkInteracting { - - var responsePublisher: AnyPublisher { - responsePublisherSubject.eraseToAnyPublisher() - } - let responsePublisherSubject = PassthroughSubject() - - let requestPublisherSubject = PassthroughSubject() - var requestPublisher: AnyPublisher { - requestPublisherSubject.eraseToAnyPublisher() - } - - func subscribe(topic: String) async throws { - - } - - func unsubscribe(topic: String) { - - } - - func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { - - } - - func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { - - } - - func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { - - } - - func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws { - - } - -} diff --git a/Tests/ChatTests/Mocks/NetworkingInteractorMock.swift b/Tests/ChatTests/Mocks/NetworkingInteractorMock.swift deleted file mode 100644 index 030c9d1d9..000000000 --- a/Tests/ChatTests/Mocks/NetworkingInteractorMock.swift +++ /dev/null @@ -1,50 +0,0 @@ -import Foundation -@testable import Chat -import Combine -import WalletConnectUtils -import WalletConnectRelay - -class NetworkingInteractorMock: NetworkInteracting { - - var socketConnectionStatusPublisher: AnyPublisher { - socketConnectionStatusPublisherSubject.eraseToAnyPublisher() - } - let socketConnectionStatusPublisherSubject = PassthroughSubject() - - let responsePublisherSubject = PassthroughSubject() - let requestPublisherSubject = PassthroughSubject() - - var requestPublisher: AnyPublisher { - requestPublisherSubject.eraseToAnyPublisher() - } - - var responsePublisher: AnyPublisher { - responsePublisherSubject.eraseToAnyPublisher() - } - - func requestUnencrypted(_ request: JSONRPCRequest, topic: String) async throws { - - } - - func request(_ request: JSONRPCRequest, topic: String) async throws { - - } - - func respond(topic: String, response: JsonRpcResult, tag: Int) async throws { - - } - - func respondSuccess(payload: RequestSubscriptionPayload) async throws { - - } - - private(set) var subscriptions: [String] = [] - - func subscribe(topic: String) async throws { - subscriptions.append(topic) - } - - func didSubscribe(to topic: String) -> Bool { - subscriptions.contains { $0 == topic } - } -} diff --git a/Tests/ChatTests/RegistryManagerTests.swift b/Tests/ChatTests/RegistryManagerTests.swift index b0ea58338..c9ff87848 100644 --- a/Tests/ChatTests/RegistryManagerTests.swift +++ b/Tests/ChatTests/RegistryManagerTests.swift @@ -2,7 +2,8 @@ import Foundation import XCTest @testable import Chat import WalletConnectUtils -@testable import WalletConnectKMS +import WalletConnectNetworking +import WalletConnectKMS @testable import TestingUtils final class RegistryManagerTests: XCTestCase { @@ -27,7 +28,7 @@ final class RegistryManagerTests: XCTestCase { func testRegister() async { let account = Account("eip155:1:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb")! - try! await registryManager.register(account: account) + _ = try! await registryManager.register(account: account) XCTAssert(!networkingInteractor.subscriptions.isEmpty, "networkingInteractors subscribes to new topic") let resolved = try! await registry.resolve(account: account) XCTAssertNotNil(resolved, "register account is resolvable") diff --git a/Tests/TestingUtils/NetworkingInteractorMock.swift b/Tests/TestingUtils/NetworkingInteractorMock.swift new file mode 100644 index 000000000..70abbc425 --- /dev/null +++ b/Tests/TestingUtils/NetworkingInteractorMock.swift @@ -0,0 +1,58 @@ +import Foundation +import Combine +import JSONRPC +import WalletConnectRelay +import WalletConnectKMS +import WalletConnectNetworking + +public class NetworkingInteractorMock: NetworkInteracting { + + private(set) var subscriptions: [String] = [] + + public let socketConnectionStatusPublisherSubject = PassthroughSubject() + public var socketConnectionStatusPublisher: AnyPublisher { + socketConnectionStatusPublisherSubject.eraseToAnyPublisher() + } + + public var responsePublisher: AnyPublisher { + responsePublisherSubject.eraseToAnyPublisher() + } + public let responsePublisherSubject = PassthroughSubject() + + public let requestPublisherSubject = PassthroughSubject() + public var requestPublisher: AnyPublisher { + requestPublisherSubject.eraseToAnyPublisher() + } + + public func subscribe(topic: String) async throws { + subscriptions.append(topic) + } + + func didSubscribe(to topic: String) -> Bool { + subscriptions.contains { $0 == topic } + } + + public func unsubscribe(topic: String) { + + } + + public func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + + } + + public func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + + } + + public func respondSuccess(topic: String, requestId: RPCID, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + + } + + public func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { + + } + + public func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws { + + } +} From a9f921b5259188f9c6f84f71c47659d0bbc69a16 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Thu, 1 Sep 2022 22:26:55 +0300 Subject: [PATCH 37/92] Showcase build errors --- .../PresentationLayer/Wallet/Wallet/WalletInteractor.swift | 4 ++-- .../PresentationLayer/Wallet/Wallet/WalletPresenter.swift | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletInteractor.swift index bd06b4567..051ba266e 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletInteractor.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletInteractor.swift @@ -3,8 +3,8 @@ import Auth final class WalletInteractor { - func pair(uri: String) async throws { - try await Auth.instance.pair(uri: WalletConnectURI(string: uri)!) + func pair(uri: WalletConnectURI) async throws { + try await Auth.instance.pair(uri: uri) } var requestPublisher: AnyPublisher { diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift index 2f631ad65..47a773591 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift @@ -15,13 +15,14 @@ final class WalletPresenter: ObservableObject { } func didPastePairingURI() { - guard let uri = UIPasteboard.general.string else { return } + guard let string = UIPasteboard.general.string, let uri = WalletConnectURI(string: string) else { return } pair(uri: uri) } func didScanPairingURI() { router.presentScan { [unowned self] value in - self.pair(uri: value) + guard let uri = WalletConnectURI(string: value) else { return } + self.pair(uri: uri) self.router.dismiss() } onError: { error in print(error.localizedDescription) @@ -53,7 +54,7 @@ private extension WalletPresenter { }.store(in: &disposeBag) } - func pair(uri: String) { + func pair(uri: WalletConnectURI) { Task(priority: .high) { [unowned self] in try await self.interactor.pair(uri: uri) } From cdc60845cab0098ecdfb8dfbef97345f99d687a2 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 2 Sep 2022 22:11:13 +0300 Subject: [PATCH 38/92] Generic NetworkInteractorPackage --- Sources/JSONRPC/RPCRequestConvertible.swift | 3 - .../NetworkInteracting.swift | 42 +++++ .../NetworkInteractor.swift | 163 +++++++++++++++--- .../NetworkRequest.swift | 6 + .../NetworkingInteractor.swift | 132 -------------- .../RequestSubscriptionPayload.swift | 6 +- .../ResponseSubscriptionPayload.swift | 8 +- Sources/WalletConnectUtils/RPCHistory.swift | 4 +- 8 files changed, 200 insertions(+), 164 deletions(-) delete mode 100644 Sources/JSONRPC/RPCRequestConvertible.swift create mode 100644 Sources/WalletConnectNetworking/NetworkInteracting.swift create mode 100644 Sources/WalletConnectNetworking/NetworkRequest.swift delete mode 100644 Sources/WalletConnectNetworking/NetworkingInteractor.swift diff --git a/Sources/JSONRPC/RPCRequestConvertible.swift b/Sources/JSONRPC/RPCRequestConvertible.swift deleted file mode 100644 index eec89d36f..000000000 --- a/Sources/JSONRPC/RPCRequestConvertible.swift +++ /dev/null @@ -1,3 +0,0 @@ -public protocol RPCRequestConvertible { - func asRPCRequest() -> RPCRequest -} diff --git a/Sources/WalletConnectNetworking/NetworkInteracting.swift b/Sources/WalletConnectNetworking/NetworkInteracting.swift new file mode 100644 index 000000000..ba6f1cfd7 --- /dev/null +++ b/Sources/WalletConnectNetworking/NetworkInteracting.swift @@ -0,0 +1,42 @@ +import Foundation +import Combine +import JSONRPC +import WalletConnectKMS +import WalletConnectRelay + +public protocol NetworkInteracting { + var socketConnectionStatusPublisher: AnyPublisher { get } + func subscribe(topic: String) async throws + func unsubscribe(topic: String) + func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws + func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws + func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws + func respondSuccess(topic: String, requestId: RPCID, tag: Int, envelopeType: Envelope.EnvelopeType) async throws + func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws + + func requestSubscription( + on request: NetworkRequest + ) -> AnyPublisher, Never> + + func responseSubscription( + on request: NetworkRequest + ) -> AnyPublisher, Never> +} + +extension NetworkInteracting { + public func request(_ request: RPCRequest, topic: String, tag: Int) async throws { + try await self.request(request, topic: topic, tag: tag, envelopeType: .type0) + } + + public func respond(topic: String, response: RPCResponse, tag: Int) async throws { + try await self.respond(topic: topic, response: response, tag: tag, envelopeType: .type0) + } + + public func respondSuccess(topic: String, requestId: RPCID, tag: Int) async throws { + try await self.respondSuccess(topic: topic, requestId: requestId, tag: tag, envelopeType: .type0) + } + + public func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason) async throws { + try await self.respondError(topic: topic, requestId: requestId, tag: tag, reason: reason, envelopeType: .type0) + } +} diff --git a/Sources/WalletConnectNetworking/NetworkInteractor.swift b/Sources/WalletConnectNetworking/NetworkInteractor.swift index fca33ed16..09c5a698d 100644 --- a/Sources/WalletConnectNetworking/NetworkInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkInteractor.swift @@ -1,36 +1,155 @@ import Foundation import Combine import JSONRPC -import WalletConnectKMS import WalletConnectRelay +import WalletConnectUtils +import WalletConnectKMS -public protocol NetworkInteracting { - var socketConnectionStatusPublisher: AnyPublisher { get } - var requestPublisher: AnyPublisher { get } - var responsePublisher: AnyPublisher { get } - func subscribe(topic: String) async throws - func unsubscribe(topic: String) - func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws - func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws - func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws - func respondSuccess(topic: String, requestId: RPCID, tag: Int, envelopeType: Envelope.EnvelopeType) async throws - func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws -} +public class NetworkingInteractor: NetworkInteracting { + private var publishers = Set() + private let relayClient: RelayClient + private let serializer: Serializing + private let rpcHistory: RPCHistory + private let logger: ConsoleLogging + + private let requestPublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest), Never>() + private let responsePublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest, response: RPCResponse), Never>() + + private var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest), Never> { + requestPublisherSubject.eraseToAnyPublisher() + } + + private var responsePublisher: AnyPublisher<(topic: String, request: RPCRequest, response: RPCResponse), Never> { + responsePublisherSubject.eraseToAnyPublisher() + } + + public var socketConnectionStatusPublisher: AnyPublisher + + public init( + relayClient: RelayClient, + serializer: Serializing, + logger: ConsoleLogging, + rpcHistory: RPCHistory + ) { + self.relayClient = relayClient + self.serializer = serializer + self.rpcHistory = rpcHistory + self.logger = logger + self.socketConnectionStatusPublisher = relayClient.socketConnectionStatusPublisher + relayClient.messagePublisher.sink { [unowned self] (topic, message) in + manageSubscription(topic, message) + } + .store(in: &publishers) + } + + public func subscribe(topic: String) async throws { + try await relayClient.subscribe(topic: topic) + } + + public func unsubscribe(topic: String) { + relayClient.unsubscribe(topic: topic) { [unowned self] error in + if let error = error { + logger.error(error) + } else { + rpcHistory.deleteAll(forTopic: topic) + } + } + } + + public func requestSubscription(on request: NetworkRequest) -> AnyPublisher, Never> { + return requestPublisher + .filter { $0.request.method == request.method } + .compactMap { topic, rpcRequest in + guard let request = try? rpcRequest.params?.get(Request.self) else { return nil } + + return RequestSubscriptionPayload(topic: topic, request: request) + } + .eraseToAnyPublisher() + } + + public func responseSubscription(on request: NetworkRequest) -> AnyPublisher, Never> { + return responsePublisher + .filter { $0.request.method == request.method } + .compactMap { topic, rpcRequest, rpcResponce in + guard + let request = try? rpcRequest.params?.get(Request.self), + let response = try? rpcResponce.result?.get(Response.self) else { return nil } + + return ResponseSubscriptionPayload(topic: topic, request: request, response: response) + } + .eraseToAnyPublisher() + } + + public func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) + let message = try! serializer.serialize(topic: topic, encodable: request, envelopeType: envelopeType) + try await relayClient.publish(topic: topic, payload: message, tag: tag) + } + + /// Completes with an acknowledgement from the relay network. + /// completes with error if networking client was not able to send a message + /// TODO - relay client should provide async function - continualion should be removed from here + public func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws { + do { + try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) + let message = try serializer.serialize(topic: topic, encodable: request) + return try await withCheckedThrowingContinuation { continuation in + relayClient.publish(topic: topic, payload: message, tag: tag) { error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + } + } catch { + logger.error(error) + } + } + + public func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + try rpcHistory.resolve(response) + let message = try! serializer.serialize(topic: topic, encodable: response, envelopeType: envelopeType) + try await relayClient.publish(topic: topic, payload: message, tag: tag) + } + + public func respondSuccess(topic: String, requestId: RPCID, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + let response = RPCResponse(id: requestId, result: true) + try await respond(topic: topic, response: response, tag: tag, envelopeType: envelopeType) + } -extension NetworkInteracting { - public func request(_ request: RPCRequest, topic: String, tag: Int) async throws { - try await self.request(request, topic: topic, tag: tag, envelopeType: .type0) + public func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { + let error = JSONRPCError(code: reason.code, message: reason.message) + let response = RPCResponse(id: requestId, error: error) + try await respond(topic: topic, response: response, tag: tag, envelopeType: envelopeType) } - public func respond(topic: String, response: RPCResponse, tag: Int) async throws { - try await self.respond(topic: topic, response: response, tag: tag, envelopeType: .type0) + private func manageSubscription(_ topic: String, _ encodedEnvelope: String) { + if let deserializedJsonRpcRequest: RPCRequest = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { + handleRequest(topic: topic, request: deserializedJsonRpcRequest) + } else if let response: RPCResponse = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { + handleResponse(response: response) + } else { + logger.debug("Networking Interactor - Received unknown object type from networking relay") + } } - public func respondSuccess(topic: String, requestId: RPCID, tag: Int) async throws { - try await self.respondSuccess(topic: topic, requestId: requestId, tag: tag, envelopeType: .type0) + private func handleRequest(topic: String, request: RPCRequest) { + do { + try rpcHistory.set(request, forTopic: topic, emmitedBy: .remote) + requestPublisherSubject.send((topic, request)) + } catch { + logger.debug(error) + } } - public func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason) async throws { - try await self.respondError(topic: topic, requestId: requestId, tag: tag, reason: reason, envelopeType: .type0) + private func handleResponse(response: RPCResponse) { + do { + try rpcHistory.resolve(response) + let record = rpcHistory.get(recordId: response.id!)! + responsePublisherSubject.send((record.topic, record.request, response)) + } catch { + logger.debug("Handle json rpc response error: \(error)") + } } } diff --git a/Sources/WalletConnectNetworking/NetworkRequest.swift b/Sources/WalletConnectNetworking/NetworkRequest.swift new file mode 100644 index 000000000..295de4bac --- /dev/null +++ b/Sources/WalletConnectNetworking/NetworkRequest.swift @@ -0,0 +1,6 @@ +import Foundation + +public protocol NetworkRequest { + var method: String { get } + var tag: String { get } +} diff --git a/Sources/WalletConnectNetworking/NetworkingInteractor.swift b/Sources/WalletConnectNetworking/NetworkingInteractor.swift deleted file mode 100644 index cf4f99e46..000000000 --- a/Sources/WalletConnectNetworking/NetworkingInteractor.swift +++ /dev/null @@ -1,132 +0,0 @@ -import Foundation -import Combine -import JSONRPC -import WalletConnectRelay -import WalletConnectUtils -import WalletConnectKMS - -public class NetworkingInteractor: NetworkInteracting { - private var publishers = Set() - private let relayClient: RelayClient - private let serializer: Serializing - private let rpcHistory: RPCHistory - private let logger: ConsoleLogging - - private let requestPublisherSubject = PassthroughSubject() - private let responsePublisherSubject = PassthroughSubject() - - public var requestPublisher: AnyPublisher { - requestPublisherSubject.eraseToAnyPublisher() - } - - public var responsePublisher: AnyPublisher { - responsePublisherSubject.eraseToAnyPublisher() - } - - public var socketConnectionStatusPublisher: AnyPublisher - - public init( - relayClient: RelayClient, - serializer: Serializing, - logger: ConsoleLogging, - rpcHistory: RPCHistory - ) { - self.relayClient = relayClient - self.serializer = serializer - self.rpcHistory = rpcHistory - self.logger = logger - self.socketConnectionStatusPublisher = relayClient.socketConnectionStatusPublisher - relayClient.messagePublisher.sink { [unowned self] (topic, message) in - manageSubscription(topic, message) - } - .store(in: &publishers) - } - - public func subscribe(topic: String) async throws { - try await relayClient.subscribe(topic: topic) - } - - public func unsubscribe(topic: String) { - relayClient.unsubscribe(topic: topic) { [unowned self] error in - if let error = error { - logger.error(error) - } else { - rpcHistory.deleteAll(forTopic: topic) - } - } - } - - public func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { - try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) - let message = try! serializer.serialize(topic: topic, encodable: request, envelopeType: envelopeType) - try await relayClient.publish(topic: topic, payload: message, tag: tag) - } - - /// Completes with an acknowledgement from the relay network. - /// completes with error if networking client was not able to send a message - /// TODO - relay client should provide async function - continualion should be removed from here - public func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws { - do { - try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) - let message = try serializer.serialize(topic: topic, encodable: request) - return try await withCheckedThrowingContinuation { continuation in - relayClient.publish(topic: topic, payload: message, tag: tag) { error in - if let error = error { - continuation.resume(throwing: error) - } else { - continuation.resume() - } - } - } - } catch { - logger.error(error) - } - } - - public func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { - try rpcHistory.resolve(response) - let message = try! serializer.serialize(topic: topic, encodable: response, envelopeType: envelopeType) - try await relayClient.publish(topic: topic, payload: message, tag: tag) - } - - public func respondSuccess(topic: String, requestId: RPCID, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { - let response = RPCResponse(id: requestId, result: true) - try await respond(topic: topic, response: response, tag: tag, envelopeType: envelopeType) - } - - public func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { - let error = JSONRPCError(code: reason.code, message: reason.message) - let response = RPCResponse(id: requestId, error: error) - try await respond(topic: topic, response: response, tag: tag, envelopeType: envelopeType) - } - - private func manageSubscription(_ topic: String, _ encodedEnvelope: String) { - if let deserializedJsonRpcRequest: RPCRequest = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { - handleRequest(topic: topic, request: deserializedJsonRpcRequest) - } else if let response: RPCResponse = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { - handleResponse(response: response) - } else { - logger.debug("Networking Interactor - Received unknown object type from networking relay") - } - } - - private func handleRequest(topic: String, request: RPCRequest) { - do { - try rpcHistory.set(request, forTopic: topic, emmitedBy: .remote) - let payload = RequestSubscriptionPayload(topic: topic, request: request) - requestPublisherSubject.send(payload) - } catch { - logger.debug(error) - } - } - - private func handleResponse(response: RPCResponse) { - do { - try rpcHistory.resolve(response) - let record = rpcHistory.get(recordId: response.id!)! - responsePublisherSubject.send(ResponseSubscriptionPayload(topic: record.topic, response: response)) - } catch { - logger.debug("Handle json rpc response error: \(error)") - } - } -} diff --git a/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift b/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift index 6132f19a2..9acba5245 100644 --- a/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift +++ b/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift @@ -1,11 +1,11 @@ import Foundation import JSONRPC -public struct RequestSubscriptionPayload: Codable, Equatable { +public struct RequestSubscriptionPayload { public let topic: String - public let request: RPCRequest + public let request: Request - public init(topic: String, request: RPCRequest) { + public init(topic: String, request: Request) { self.topic = topic self.request = request } diff --git a/Sources/WalletConnectNetworking/ResponseSubscriptionPayload.swift b/Sources/WalletConnectNetworking/ResponseSubscriptionPayload.swift index cfe2e6ab2..34e16303a 100644 --- a/Sources/WalletConnectNetworking/ResponseSubscriptionPayload.swift +++ b/Sources/WalletConnectNetworking/ResponseSubscriptionPayload.swift @@ -1,12 +1,14 @@ import Foundation import JSONRPC -public struct ResponseSubscriptionPayload: Codable, Equatable { +public struct ResponseSubscriptionPayload { public let topic: String - public let response: RPCResponse + public let request: Request + public let response: Response - public init(topic: String, response: RPCResponse) { + public init(topic: String, request: Request, response: Response) { self.topic = topic + self.request = request self.response = response } } diff --git a/Sources/WalletConnectUtils/RPCHistory.swift b/Sources/WalletConnectUtils/RPCHistory.swift index ae0b0df71..255801b24 100644 --- a/Sources/WalletConnectUtils/RPCHistory.swift +++ b/Sources/WalletConnectUtils/RPCHistory.swift @@ -43,7 +43,8 @@ public final class RPCHistory { storage.set(record, forKey: "\(record.id)") } - public func resolve(_ response: RPCResponse) throws { + @discardableResult + public func resolve(_ response: RPCResponse) throws -> Record { guard let id = response.id else { throw HistoryError.unidentifiedResponse } @@ -55,6 +56,7 @@ public final class RPCHistory { } record.response = response storage.set(record, forKey: "\(record.id)") + return record } public func deleteAll(forTopic topic: String) { From d187574448986d111376f23b5d953a61007a3258 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 2 Sep 2022 22:46:53 +0300 Subject: [PATCH 39/92] Chat SDK refactored --- Sources/Chat/ChatClient.swift | 11 ++--- Sources/Chat/ChatClientFactory.swift | 2 +- .../Common/MessagingService.swift | 28 +++++-------- .../Invitee/InvitationHandlingService.swift | 41 +++++++------------ .../Inviter/InviteService.swift | 36 ++++++---------- Sources/Chat/Types/ChatRequest.swift | 25 +++++++++++ Sources/Chat/Types/Invite.swift | 8 ---- Sources/Chat/Types/Message.swift | 8 ---- .../NetworkInteractor.swift | 9 ++-- .../NetworkRequest.swift | 2 +- .../RequestSubscriptionPayload.swift | 6 ++- .../ResponseSubscriptionPayload.swift | 4 +- .../NetworkingInteractorMock.swift | 34 ++++++++++++--- 13 files changed, 108 insertions(+), 106 deletions(-) create mode 100644 Sources/Chat/Types/ChatRequest.swift diff --git a/Sources/Chat/ChatClient.swift b/Sources/Chat/ChatClient.swift index 678a3436e..19b3fc550 100644 --- a/Sources/Chat/ChatClient.swift +++ b/Sources/Chat/ChatClient.swift @@ -17,7 +17,7 @@ public class ChatClient { private let kms: KeyManagementService private let threadStore: Database private let messagesStore: Database - private let invitePayloadStore: CodableStore + private let invitePayloadStore: CodableStore> public let socketConnectionStatusPublisher: AnyPublisher @@ -48,7 +48,7 @@ public class ChatClient { kms: KeyManagementService, threadStore: Database, messagesStore: Database, - invitePayloadStore: CodableStore, + invitePayloadStore: CodableStore>, socketConnectionStatusPublisher: AnyPublisher ) { self.registry = registry @@ -120,12 +120,7 @@ public class ChatClient { } public func getInvites(account: Account) -> [Invite] { - var invites = [Invite]() - invitePayloadStore.getAll().forEach { - guard let invite = try? $0.request.params?.get(Invite.self) else {return} - invites.append(invite) - } - return invites + return invitePayloadStore.getAll().map { $0.request } } public func getThreads() async -> [Thread] { diff --git a/Sources/Chat/ChatClientFactory.swift b/Sources/Chat/ChatClientFactory.swift index 47ae2dcdd..9918dbc70 100644 --- a/Sources/Chat/ChatClientFactory.swift +++ b/Sources/Chat/ChatClientFactory.swift @@ -17,7 +17,7 @@ public struct ChatClientFactory { let serialiser = Serializer(kms: kms) let rpcHistory = RPCHistory(keyValueStore: CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.jsonRpcHistory.rawValue)) let networkingInteractor = NetworkingInteractor(relayClient: relayClient, serializer: serialiser, logger: logger, rpcHistory: rpcHistory) - let invitePayloadStore = CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.invite.rawValue) + let invitePayloadStore = CodableStore>(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.invite.rawValue) let registryService = RegistryService(registry: registry, networkingInteractor: networkingInteractor, kms: kms, logger: logger, topicToRegistryRecordStore: topicToRegistryRecordStore) let threadStore = Database(keyValueStorage: keyValueStorage, identifier: StorageDomainIdentifiers.threads.rawValue) let resubscriptionService = ResubscriptionService(networkingInteractor: networkingInteractor, threadStore: threadStore, logger: logger) diff --git a/Sources/Chat/ProtocolServices/Common/MessagingService.swift b/Sources/Chat/ProtocolServices/Common/MessagingService.swift index eaeaf061e..3d7638b70 100644 --- a/Sources/Chat/ProtocolServices/Common/MessagingService.swift +++ b/Sources/Chat/ProtocolServices/Common/MessagingService.swift @@ -33,8 +33,8 @@ class MessagingService { guard let authorAccount = thread?.selfAccount else { throw Errors.threadDoNotExist} let timestamp = Int64(Date().timeIntervalSince1970 * 1000) let message = Message(topic: topic, message: messageString, authorAccount: authorAccount, timestamp: timestamp) - let request = RPCRequest(method: Message.method, params: message) - try await networkingInteractor.request(request, topic: topic, tag: Message.tag) + let request = RPCRequest(method: ChatRequest.message.method, params: message) + try await networkingInteractor.request(request, topic: topic, tag: ChatRequest.message.tag) Task(priority: .background) { await messagesStore.add(message) onMessage?(message) @@ -42,32 +42,24 @@ class MessagingService { } private func setUpResponseHandling() { - networkingInteractor.responsePublisher - .sink { [unowned self] payload in + networkingInteractor.responseSubscription(on: ChatRequest.message) + .sink { [unowned self] (payload: ResponseSubscriptionPayload) in logger.debug("Received Message response") }.store(in: &publishers) } private func setUpRequestHandling() { - networkingInteractor.requestPublisher.sink { [unowned self] payload in - do { - guard - let requestId = payload.request.id, payload.request.method == Message.method, - var message = try payload.request.params?.get(Message.self) - else { return } - + networkingInteractor.requestSubscription(on: ChatRequest.message) + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + var message = payload.request message.topic = payload.topic - - handleMessage(message, topic: payload.topic, requestId: requestId) - } catch { - logger.debug("Handling message response has failed") - } - }.store(in: &publishers) + handleMessage(message, topic: payload.topic, requestId: payload.id) + }.store(in: &publishers) } private func handleMessage(_ message: Message, topic: String, requestId: RPCID) { Task(priority: .background) { - try await networkingInteractor.respondSuccess(topic: topic, requestId: requestId, tag: Message.tag) + try await networkingInteractor.respondSuccess(topic: topic, requestId: requestId, tag: ChatRequest.message.tag) await messagesStore.add(message) logger.debug("Received message") onMessage?(message) diff --git a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift index 102e9f00a..624736fe3 100644 --- a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift +++ b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift @@ -13,7 +13,7 @@ class InvitationHandlingService { var onInvite: ((Invite) -> Void)? var onNewThread: ((Thread) -> Void)? private let networkingInteractor: NetworkInteracting - private let invitePayloadStore: CodableStore + private let invitePayloadStore: CodableStore> private let topicToRegistryRecordStore: CodableStore private let registry: Registry private let logger: ConsoleLogging @@ -26,7 +26,7 @@ class InvitationHandlingService { kms: KeyManagementService, logger: ConsoleLogging, topicToRegistryRecordStore: CodableStore, - invitePayloadStore: CodableStore, + invitePayloadStore: CodableStore>, threadsStore: Database) { self.registry = registry self.kms = kms @@ -45,14 +45,11 @@ class InvitationHandlingService { let inviteResponse = InviteResponse(publicKey: selfThreadPubKey.hexRepresentation) - guard let requestId = payload.request.id, let invite = try? payload.request.params?.get(Invite.self) - else { return } + let response = RPCResponse(id: payload.id, result: inviteResponse) + let responseTopic = try getInviteResponseTopic(requestTopic: payload.topic, invite: payload.request) + try await networkingInteractor.respond(topic: responseTopic, response: response, tag: ChatRequest.invite.tag) - let response = RPCResponse(id: requestId, result: inviteResponse) - let responseTopic = try getInviteResponseTopic(requestTopic: payload.topic, invite: invite) - try await networkingInteractor.respond(topic: responseTopic, response: response, tag: Invite.tag) - - let threadAgreementKeys = try kms.performKeyAgreement(selfPublicKey: selfThreadPubKey, peerPublicKey: invite.publicKey) + let threadAgreementKeys = try kms.performKeyAgreement(selfPublicKey: selfThreadPubKey, peerPublicKey: payload.request.publicKey) let threadTopic = threadAgreementKeys.derivedTopic() try kms.setSymmetricKey(threadAgreementKeys.sharedKey, for: threadTopic) try await networkingInteractor.subscribe(topic: threadTopic) @@ -61,7 +58,7 @@ class InvitationHandlingService { // TODO - derive account let selfAccount = try! topicToRegistryRecordStore.get(key: payload.topic)!.account - let thread = Thread(topic: threadTopic, selfAccount: selfAccount, peerAccount: invite.account) + let thread = Thread(topic: threadTopic, selfAccount: selfAccount, peerAccount: payload.request.account) await threadsStore.add(thread) invitePayloadStore.delete(forKey: inviteId) @@ -72,28 +69,20 @@ class InvitationHandlingService { func reject(inviteId: String) async throws { guard let payload = try invitePayloadStore.get(key: inviteId) else { throw Error.inviteForIdNotFound } - guard let requestId = payload.request.id, let invite = try? payload.request.params?.get(Invite.self) - else { return } - - let responseTopic = try getInviteResponseTopic(requestTopic: payload.topic, invite: invite) + let responseTopic = try getInviteResponseTopic(requestTopic: payload.topic, invite: payload.request) - try await networkingInteractor.respondError(topic: responseTopic, requestId: requestId, tag: Invite.tag, reason: ChatError.userRejected) + try await networkingInteractor.respondError(topic: responseTopic, requestId: payload.id, tag: ChatRequest.invite.tag, reason: ChatError.userRejected) invitePayloadStore.delete(forKey: inviteId) } private func setUpRequestHandling() { - networkingInteractor.requestPublisher.sink { [unowned self] payload in - guard payload.request.method == "wc_chatInvite" - else { return } - - guard let invite = try? payload.request.params?.get(Invite.self) - else { return } - - logger.debug("did receive an invite") - invitePayloadStore.set(payload, forKey: invite.publicKey) - onInvite?(invite) - }.store(in: &publishers) + networkingInteractor.requestSubscription(on: ChatRequest.invite) + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + logger.debug("did receive an invite") + invitePayloadStore.set(payload, forKey: payload.request.publicKey) + onInvite?(payload.request) + }.store(in: &publishers) } private func getInviteResponseTopic(requestTopic: String, invite: Invite) throws -> String { diff --git a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift index d3051cf2d..41fec4d67 100644 --- a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift +++ b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift @@ -42,7 +42,7 @@ class InviteService { // overrides on invite toipic try kms.setSymmetricKey(symKeyI.sharedKey, for: inviteTopic) - let request = RPCRequest(method: Invite.method, params: invite) + let request = RPCRequest(method: ChatRequest.invite.method, params: invite) // 2. Proposer subscribes to topic R which is the hash of the derived symKey let responseTopic = symKeyI.derivedTopic() @@ -50,33 +50,23 @@ class InviteService { try kms.setSymmetricKey(symKeyI.sharedKey, for: responseTopic) try await networkingInteractor.subscribe(topic: responseTopic) - try await networkingInteractor.request(request, topic: inviteTopic, tag: Invite.tag, envelopeType: .type1(pubKey: selfPubKeyY.rawRepresentation)) + try await networkingInteractor.request(request, topic: inviteTopic, tag: ChatRequest.invite.tag, envelopeType: .type1(pubKey: selfPubKeyY.rawRepresentation)) logger.debug("invite sent on topic: \(inviteTopic)") } private func setUpResponseHandling() { - networkingInteractor.responsePublisher - .sink { [unowned self] payload in - do { - guard - let requestId = payload.response.id, - let request = rpcHistory.get(recordId: requestId)?.request, - let requestParams = request.params, request.method == Invite.method - else { return } - - guard let inviteResponse = try payload.response.result?.get(InviteResponse.self) - else { return } - - let inviteParams = try requestParams.get(Invite.self) - - logger.debug("Invite has been accepted") - - Task(priority: .background) { - try await createThread(selfPubKeyHex: inviteParams.publicKey, peerPubKey: inviteResponse.publicKey, account: inviteParams.account, peerAccount: peerAccount) - } - } catch { - logger.debug("Handling invite response has failed") + networkingInteractor.responseSubscription(on: ChatRequest.invite) + .sink { [unowned self] (payload: ResponseSubscriptionPayload) in + logger.debug("Invite has been accepted") + + Task(priority: .background) { + try await createThread( + selfPubKeyHex: payload.request.publicKey, + peerPubKey: payload.response.publicKey, + account: payload.request.account, + peerAccount: peerAccount + ) } }.store(in: &publishers) } diff --git a/Sources/Chat/Types/ChatRequest.swift b/Sources/Chat/Types/ChatRequest.swift new file mode 100644 index 000000000..7a0ec4f7e --- /dev/null +++ b/Sources/Chat/Types/ChatRequest.swift @@ -0,0 +1,25 @@ +import Foundation +import WalletConnectNetworking + +enum ChatRequest: NetworkRequest { + case invite + case message + + var tag: Int { + switch self { + case .invite: + return 2002 + case .message: + return 2002 + } + } + + var method: String { + switch self { + case .invite: + return "wc_chatInvite" + case .message: + return "wc_chatMessage" + } + } +} diff --git a/Sources/Chat/Types/Invite.swift b/Sources/Chat/Types/Invite.swift index 5f69e72e8..f5b0f7180 100644 --- a/Sources/Chat/Types/Invite.swift +++ b/Sources/Chat/Types/Invite.swift @@ -12,12 +12,4 @@ public struct Invite: Codable, Equatable { public let message: String public let account: Account public let publicKey: String - - static var tag: Int { - return 2000 - } - - static var method: String { - return "wc_chatInvite" - } } diff --git a/Sources/Chat/Types/Message.swift b/Sources/Chat/Types/Message.swift index 09113ed2a..1a2694748 100644 --- a/Sources/Chat/Types/Message.swift +++ b/Sources/Chat/Types/Message.swift @@ -14,14 +14,6 @@ public struct Message: Codable, Equatable { case timestamp } - static var tag: Int { - return 2002 - } - - static var method: String { - return "wc_chatMessage" - } - init(topic: String? = nil, message: String, authorAccount: Account, timestamp: Int64) { self.topic = topic self.message = message diff --git a/Sources/WalletConnectNetworking/NetworkInteractor.swift b/Sources/WalletConnectNetworking/NetworkInteractor.swift index 09c5a698d..e502046d4 100644 --- a/Sources/WalletConnectNetworking/NetworkInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkInteractor.swift @@ -60,9 +60,8 @@ public class NetworkingInteractor: NetworkInteracting { return requestPublisher .filter { $0.request.method == request.method } .compactMap { topic, rpcRequest in - guard let request = try? rpcRequest.params?.get(Request.self) else { return nil } - - return RequestSubscriptionPayload(topic: topic, request: request) + guard let id = rpcRequest.id, let request = try? rpcRequest.params?.get(Request.self) else { return nil } + return RequestSubscriptionPayload(id: id, topic: topic, request: request) } .eraseToAnyPublisher() } @@ -72,10 +71,10 @@ public class NetworkingInteractor: NetworkInteracting { .filter { $0.request.method == request.method } .compactMap { topic, rpcRequest, rpcResponce in guard + let id = rpcRequest.id, let request = try? rpcRequest.params?.get(Request.self), let response = try? rpcResponce.result?.get(Response.self) else { return nil } - - return ResponseSubscriptionPayload(topic: topic, request: request, response: response) + return ResponseSubscriptionPayload(id: id, topic: topic, request: request, response: response) } .eraseToAnyPublisher() } diff --git a/Sources/WalletConnectNetworking/NetworkRequest.swift b/Sources/WalletConnectNetworking/NetworkRequest.swift index 295de4bac..3922f4721 100644 --- a/Sources/WalletConnectNetworking/NetworkRequest.swift +++ b/Sources/WalletConnectNetworking/NetworkRequest.swift @@ -2,5 +2,5 @@ import Foundation public protocol NetworkRequest { var method: String { get } - var tag: String { get } + var tag: Int { get } } diff --git a/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift b/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift index 9acba5245..fa6b0e8db 100644 --- a/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift +++ b/Sources/WalletConnectNetworking/RequestSubscriptionPayload.swift @@ -1,11 +1,13 @@ import Foundation import JSONRPC -public struct RequestSubscriptionPayload { +public struct RequestSubscriptionPayload: Codable { + public let id: RPCID public let topic: String public let request: Request - public init(topic: String, request: Request) { + public init(id: RPCID, topic: String, request: Request) { + self.id = id self.topic = topic self.request = request } diff --git a/Sources/WalletConnectNetworking/ResponseSubscriptionPayload.swift b/Sources/WalletConnectNetworking/ResponseSubscriptionPayload.swift index 34e16303a..21043eb9d 100644 --- a/Sources/WalletConnectNetworking/ResponseSubscriptionPayload.swift +++ b/Sources/WalletConnectNetworking/ResponseSubscriptionPayload.swift @@ -2,11 +2,13 @@ import Foundation import JSONRPC public struct ResponseSubscriptionPayload { + public let id: RPCID public let topic: String public let request: Request public let response: Response - public init(topic: String, request: Request, response: Response) { + public init(id: RPCID, topic: String, request: Request, response: Response) { + self.id = id self.topic = topic self.request = request self.response = response diff --git a/Tests/TestingUtils/NetworkingInteractorMock.swift b/Tests/TestingUtils/NetworkingInteractorMock.swift index 70abbc425..021e85f41 100644 --- a/Tests/TestingUtils/NetworkingInteractorMock.swift +++ b/Tests/TestingUtils/NetworkingInteractorMock.swift @@ -14,14 +14,38 @@ public class NetworkingInteractorMock: NetworkInteracting { socketConnectionStatusPublisherSubject.eraseToAnyPublisher() } - public var responsePublisher: AnyPublisher { + private let requestPublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest), Never>() + private let responsePublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest, response: RPCResponse), Never>() + + private var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest), Never> { + requestPublisherSubject.eraseToAnyPublisher() + } + + private var responsePublisher: AnyPublisher<(topic: String, request: RPCRequest, response: RPCResponse), Never> { responsePublisherSubject.eraseToAnyPublisher() } - public let responsePublisherSubject = PassthroughSubject() - public let requestPublisherSubject = PassthroughSubject() - public var requestPublisher: AnyPublisher { - requestPublisherSubject.eraseToAnyPublisher() + public func requestSubscription(on request: NetworkRequest) -> AnyPublisher, Never> { + return requestPublisher + .filter { $0.request.method == request.method } + .compactMap { topic, rpcRequest in + guard let id = rpcRequest.id, let request = try? rpcRequest.params?.get(Request.self) else { return nil } + return RequestSubscriptionPayload(id: id, topic: topic, request: request) + } + .eraseToAnyPublisher() + } + + public func responseSubscription(on request: NetworkRequest) -> AnyPublisher, Never> { + return responsePublisher + .filter { $0.request.method == request.method } + .compactMap { topic, rpcRequest, rpcResponce in + guard + let id = rpcRequest.id, + let request = try? rpcRequest.params?.get(Request.self), + let response = try? rpcResponce.result?.get(Response.self) else { return nil } + return ResponseSubscriptionPayload(id: id, topic: topic, request: request, response: response) + } + .eraseToAnyPublisher() } public func subscribe(topic: String) async throws { From 7d8d0026f2f119dfa46a2875d28662171fe03568 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 2 Sep 2022 23:38:03 +0300 Subject: [PATCH 40/92] New publishers connected to Auth --- .../Auth/Services/App/AppRequestService.swift | 2 +- .../Services/App/AppRespondSubscriber.swift | 49 +++++++++---------- .../Wallet/WalletRequestSubscriber.swift | 22 +++------ .../Wallet/WalletRespondService.swift | 4 +- Sources/Auth/Types/AuthNetworkRequest.swift | 14 ++++++ .../ProtocolRPCParams/AuthRequestParams.swift | 7 +-- .../AuthResponseParams.swift | 4 -- .../NetworkInteracting.swift | 2 + .../NetworkInteractor.swift | 10 ++++ .../ResponseSubscriptionErrorPayload.swift | 12 +++++ .../AuthTests/AppRespondSubscriberTests.swift | 2 +- .../Stubs/RequestSubscriptionPayload.swift | 8 ++- .../WalletRequestSubscriberTests.swift | 3 +- .../NetworkingInteractorMock.swift | 14 +++++- 14 files changed, 89 insertions(+), 64 deletions(-) create mode 100644 Sources/Auth/Types/AuthNetworkRequest.swift create mode 100644 Sources/WalletConnectNetworking/ResponseSubscriptionErrorPayload.swift diff --git a/Sources/Auth/Services/App/AppRequestService.swift b/Sources/Auth/Services/App/AppRequestService.swift index 9303d7bb8..7d629d7af 100644 --- a/Sources/Auth/Services/App/AppRequestService.swift +++ b/Sources/Auth/Services/App/AppRequestService.swift @@ -30,7 +30,7 @@ actor AppRequestService { let request = RPCRequest(method: "wc_authRequest", params: params) try kms.setPublicKey(publicKey: pubKey, for: responseTopic) logger.debug("AppRequestService: Subscribibg for response topic: \(responseTopic)") - try await networkingInteractor.requestNetworkAck(request, topic: topic, tag: AuthRequestParams.tag) + try await networkingInteractor.requestNetworkAck(request, topic: topic, tag: AuthNetworkRequest.request.tag) try await networkingInteractor.subscribe(topic: responseTopic) } } diff --git a/Sources/Auth/Services/App/AppRespondSubscriber.swift b/Sources/Auth/Services/App/AppRespondSubscriber.swift index 16e0ff807..5dfd4fc10 100644 --- a/Sources/Auth/Services/App/AppRespondSubscriber.swift +++ b/Sources/Auth/Services/App/AppRespondSubscriber.swift @@ -32,41 +32,36 @@ class AppRespondSubscriber { } private func subscribeForResponse() { - networkingInteractor.responsePublisher.sink { [unowned self] subscriptionPayload in - let response = subscriptionPayload.response - guard - let requestId = response.id, - let request = rpcHistory.get(recordId: requestId)?.request, - let requestParams = request.params, request.method == "wc_authRequest" - else { return } + networkingInteractor.responceErrorSubscription(on: AuthNetworkRequest.request) + .sink { [unowned self] payload in + guard let error = AuthError(code: payload.error.code) else { return } + onResponse?(payload.id, .failure(error)) + }.store(in: &publishers) - activatePairingIfNeeded(id: requestId) - networkingInteractor.unsubscribe(topic: subscriptionPayload.topic) + networkingInteractor.responseSubscription(on: AuthNetworkRequest.request) + .sink { [unowned self] (payload: ResponseSubscriptionPayload) in - if let errorResponse = response.error, - let error = AuthError(code: errorResponse.code) { - onResponse?(requestId, .failure(error)) - return - } + activatePairingIfNeeded(id: payload.id) + networkingInteractor.unsubscribe(topic: payload.topic) - guard - let cacao = try? response.result?.get(Cacao.self), - let address = try? DIDPKH(iss: cacao.payload.iss).account.address, - let message = try? messageFormatter.formatMessage(from: cacao.payload) - else { self.onResponse?(requestId, .failure(.malformedResponseParams)); return } + let requestId = payload.id + let cacao = payload.response + let requestPayload = payload.request - guard let requestPayload = try? requestParams.get(AuthRequestParams.self) - else { self.onResponse?(requestId, .failure(.malformedRequestParams)); return } + guard + let address = try? DIDPKH(iss: cacao.payload.iss).account.address, + let message = try? messageFormatter.formatMessage(from: cacao.payload) + else { self.onResponse?(requestId, .failure(.malformedResponseParams)); return } - guard messageFormatter.formatMessage(from: requestPayload.payloadParams, address: address) == message - else { self.onResponse?(requestId, .failure(.messageCompromised)); return } + guard messageFormatter.formatMessage(from: requestPayload.payloadParams, address: address) == message + else { self.onResponse?(requestId, .failure(.messageCompromised)); return } - guard let _ = try? signatureVerifier.verify(signature: cacao.signature, message: message, address: address) - else { self.onResponse?(requestId, .failure(.signatureVerificationFailed)); return } + guard let _ = try? signatureVerifier.verify(signature: cacao.signature, message: message, address: address) + else { self.onResponse?(requestId, .failure(.signatureVerificationFailed)); return } - onResponse?(requestId, .success(cacao)) + onResponse?(requestId, .success(cacao)) - }.store(in: &publishers) + }.store(in: &publishers) } private func activatePairingIfNeeded(id: RPCID) { diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift index c54bd3935..5688b926f 100644 --- a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift +++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift @@ -30,27 +30,19 @@ class WalletRequestSubscriber { private func subscribeForRequest() { guard let address = address else { return } - networkingInteractor.requestPublisher.sink { [unowned self] payload in - - logger.debug("WalletRequestSubscriber: Received request") - - guard let requestId = payload.request.id, payload.request.method == "wc_authRequest" - else { return } - - guard let authRequestParams = try? payload.request.params?.get(AuthRequestParams.self) - else { return respondError(.malformedRequestParams, topic: payload.topic, requestId: requestId) } - - let message = messageFormatter.formatMessage(from: authRequestParams.payloadParams, address: address) - - onRequest?(.init(id: requestId, message: message)) - }.store(in: &publishers) + networkingInteractor.requestSubscription(on: AuthNetworkRequest.request) + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + logger.debug("WalletRequestSubscriber: Received request") + let message = messageFormatter.formatMessage(from: payload.request.payloadParams, address: address) + onRequest?(.init(id: payload.id, message: message)) + }.store(in: &publishers) } private func respondError(_ error: AuthError, topic: String, requestId: RPCID) { guard let pubKey = kms.getAgreementSecret(for: topic)?.publicKey else { return logger.error("Agreement key for topic \(topic) not found") } - let tag = AuthResponseParams.tag + let tag = AuthNetworkRequest.request.tag let envelopeType = Envelope.EnvelopeType.type1(pubKey: pubKey.rawRepresentation) Task(priority: .high) { diff --git a/Sources/Auth/Services/Wallet/WalletRespondService.swift b/Sources/Auth/Services/Wallet/WalletRespondService.swift index 9107f4df3..4e44e8f97 100644 --- a/Sources/Auth/Services/Wallet/WalletRespondService.swift +++ b/Sources/Auth/Services/Wallet/WalletRespondService.swift @@ -33,7 +33,7 @@ actor WalletRespondService { let didpkh = DIDPKH(account: account) let cacao = CacaoFormatter().format(authRequestParams, signature, didpkh) let response = RPCResponse(id: requestId, result: cacao) - try await networkingInteractor.respond(topic: topic, response: response, tag: AuthResponseParams.tag, envelopeType: .type1(pubKey: keys.publicKey.rawRepresentation)) + try await networkingInteractor.respond(topic: topic, response: response, tag: AuthNetworkRequest.request.tag, envelopeType: .type1(pubKey: keys.publicKey.rawRepresentation)) } func respondError(requestId: RPCID) async throws { @@ -42,7 +42,7 @@ actor WalletRespondService { try kms.setAgreementSecret(keys, topic: topic) - let tag = AuthResponseParams.tag + let tag = AuthNetworkRequest.request.tag let error = AuthError.userRejeted let envelopeType = Envelope.EnvelopeType.type1(pubKey: keys.publicKey.rawRepresentation) try await networkingInteractor.respondError(topic: topic, requestId: requestId, tag: tag, reason: error, envelopeType: envelopeType) diff --git a/Sources/Auth/Types/AuthNetworkRequest.swift b/Sources/Auth/Types/AuthNetworkRequest.swift new file mode 100644 index 000000000..0a0f39417 --- /dev/null +++ b/Sources/Auth/Types/AuthNetworkRequest.swift @@ -0,0 +1,14 @@ +import Foundation +import WalletConnectNetworking + +enum AuthNetworkRequest: NetworkRequest { + case request + + var method: String { + return "wc_authRequest" + } + + var tag: Int { + return 3001 + } +} diff --git a/Sources/Auth/Types/ProtocolRPCParams/AuthRequestParams.swift b/Sources/Auth/Types/ProtocolRPCParams/AuthRequestParams.swift index 46feee75f..18d3f9328 100644 --- a/Sources/Auth/Types/ProtocolRPCParams/AuthRequestParams.swift +++ b/Sources/Auth/Types/ProtocolRPCParams/AuthRequestParams.swift @@ -4,12 +4,7 @@ import WalletConnectUtils /// wc_authRequest RPC method request param struct AuthRequestParams: Codable, Equatable { let requester: Requester - let payloadParams: AuthPayload - - static var tag: Int { - return 3000 - } -} + let payloadParams: AuthPayload} extension AuthRequestParams { struct Requester: Codable, Equatable { diff --git a/Sources/Auth/Types/ProtocolRPCParams/AuthResponseParams.swift b/Sources/Auth/Types/ProtocolRPCParams/AuthResponseParams.swift index 2dab1477b..d8e6d9fcf 100644 --- a/Sources/Auth/Types/ProtocolRPCParams/AuthResponseParams.swift +++ b/Sources/Auth/Types/ProtocolRPCParams/AuthResponseParams.swift @@ -6,8 +6,4 @@ struct AuthResponseParams: Codable, Equatable { let header: CacaoHeader let payload: CacaoPayload let signature: CacaoSignature - - static var tag: Int { - return 3001 - } } diff --git a/Sources/WalletConnectNetworking/NetworkInteracting.swift b/Sources/WalletConnectNetworking/NetworkInteracting.swift index ba6f1cfd7..99db4ac69 100644 --- a/Sources/WalletConnectNetworking/NetworkInteracting.swift +++ b/Sources/WalletConnectNetworking/NetworkInteracting.swift @@ -21,6 +21,8 @@ public protocol NetworkInteracting { func responseSubscription( on request: NetworkRequest ) -> AnyPublisher, Never> + + func responceErrorSubscription(on request: NetworkRequest) -> AnyPublisher } extension NetworkInteracting { diff --git a/Sources/WalletConnectNetworking/NetworkInteractor.swift b/Sources/WalletConnectNetworking/NetworkInteractor.swift index e502046d4..3d8e81a22 100644 --- a/Sources/WalletConnectNetworking/NetworkInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkInteractor.swift @@ -79,6 +79,16 @@ public class NetworkingInteractor: NetworkInteracting { .eraseToAnyPublisher() } + public func responceErrorSubscription(on request: NetworkRequest) -> AnyPublisher { + return responsePublisher + .filter { $0.request.method == request.method } + .compactMap { (_, _, rpcResponce) in + guard let id = rpcResponce.id, let error = rpcResponce.error else { return nil } + return ResponseSubscriptionErrorPayload(id: id, error: error) + } + .eraseToAnyPublisher() + } + public func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) let message = try! serializer.serialize(topic: topic, encodable: request, envelopeType: envelopeType) diff --git a/Sources/WalletConnectNetworking/ResponseSubscriptionErrorPayload.swift b/Sources/WalletConnectNetworking/ResponseSubscriptionErrorPayload.swift new file mode 100644 index 000000000..8b38df244 --- /dev/null +++ b/Sources/WalletConnectNetworking/ResponseSubscriptionErrorPayload.swift @@ -0,0 +1,12 @@ +import Foundation +import JSONRPC + +public struct ResponseSubscriptionErrorPayload { + public let id: RPCID + public let error: JSONRPCError + + public init(id: RPCID, error: JSONRPCError) { + self.id = id + self.error = error + } +} diff --git a/Tests/AuthTests/AppRespondSubscriberTests.swift b/Tests/AuthTests/AppRespondSubscriberTests.swift index 95f5807db..686c0c830 100644 --- a/Tests/AuthTests/AppRespondSubscriberTests.swift +++ b/Tests/AuthTests/AppRespondSubscriberTests.swift @@ -61,7 +61,7 @@ class AppRespondSubscriberTests: XCTestCase { let cacao = Cacao(header: header, payload: payload, signature: cacaoSignature) let response = RPCResponse(id: requestId, result: cacao) - networkingInteractor.responsePublisherSubject.send(ResponseSubscriptionPayload(topic: topic, response: response)) + networkingInteractor.responsePublisherSubject.send((topic, request, response)) wait(for: [messageExpectation], timeout: defaultTimeout) XCTAssertEqual(result, .failure(AuthError.messageCompromised)) diff --git a/Tests/AuthTests/Stubs/RequestSubscriptionPayload.swift b/Tests/AuthTests/Stubs/RequestSubscriptionPayload.swift index 156780d42..7944b241b 100644 --- a/Tests/AuthTests/Stubs/RequestSubscriptionPayload.swift +++ b/Tests/AuthTests/Stubs/RequestSubscriptionPayload.swift @@ -3,14 +3,12 @@ import JSONRPC import WalletConnectNetworking @testable import Auth -extension RequestSubscriptionPayload { - static func stub(id: RPCID) -> RequestSubscriptionPayload { +extension AuthRequestParams { + static func stub(id: RPCID) -> AuthRequestParams { let appMetadata = AppMetadata(name: "", description: "", url: "", icons: []) let requester = AuthRequestParams.Requester(publicKey: "", metadata: appMetadata) let issueAt = ISO8601DateFormatter().string(from: Date()) let payload = AuthPayload(requestParams: RequestParams.stub(), iat: issueAt) - let params = AuthRequestParams(requester: requester, payloadParams: payload) - let request = RPCRequest(method: "wc_authRequest", params: params, rpcid: id) - return RequestSubscriptionPayload(topic: "123", request: request) + return AuthRequestParams(requester: requester, payloadParams: payload) } } diff --git a/Tests/AuthTests/WalletRequestSubscriberTests.swift b/Tests/AuthTests/WalletRequestSubscriberTests.swift index 00e7f62ee..6d39d42b5 100644 --- a/Tests/AuthTests/WalletRequestSubscriberTests.swift +++ b/Tests/AuthTests/WalletRequestSubscriberTests.swift @@ -35,7 +35,8 @@ class WalletRequestSubscriberTests: XCTestCase { messageExpectation.fulfill() } - networkingInteractor.requestPublisherSubject.send(RequestSubscriptionPayload.stub(id: expectedRequestId)) + let request = RPCRequest(method: AuthNetworkRequest.request.method, params: AuthRequestParams.stub(id: expectedRequestId), id: expectedRequestId.right!) + networkingInteractor.requestPublisherSubject.send(("123", request)) wait(for: [messageExpectation], timeout: defaultTimeout) XCTAssertEqual(message, expectedMessage) diff --git a/Tests/TestingUtils/NetworkingInteractorMock.swift b/Tests/TestingUtils/NetworkingInteractorMock.swift index 021e85f41..0f1fbe6a6 100644 --- a/Tests/TestingUtils/NetworkingInteractorMock.swift +++ b/Tests/TestingUtils/NetworkingInteractorMock.swift @@ -14,8 +14,8 @@ public class NetworkingInteractorMock: NetworkInteracting { socketConnectionStatusPublisherSubject.eraseToAnyPublisher() } - private let requestPublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest), Never>() - private let responsePublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest, response: RPCResponse), Never>() + public let requestPublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest), Never>() + public let responsePublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest, response: RPCResponse), Never>() private var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest), Never> { requestPublisherSubject.eraseToAnyPublisher() @@ -48,6 +48,16 @@ public class NetworkingInteractorMock: NetworkInteracting { .eraseToAnyPublisher() } + public func responceErrorSubscription(on request: NetworkRequest) -> AnyPublisher { + return responsePublisher + .filter { $0.request.method == request.method } + .compactMap { (_, _, rpcResponce) in + guard let id = rpcResponce.id, let error = rpcResponce.error else { return nil } + return ResponseSubscriptionErrorPayload(id: id, error: error) + } + .eraseToAnyPublisher() + } + public func subscribe(topic: String) async throws { subscriptions.append(topic) } From 20b0b74f12440c1da6d7b9faeee5b949c6ad1fe3 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Mon, 5 Sep 2022 08:21:38 +0200 Subject: [PATCH 41/92] add info plist values to showcase app --- Example/ExampleApp.xcodeproj/project.pbxproj | 2 ++ Example/Showcase/Other/Info.plist | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index e49f1265a..b07675f09 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -1752,6 +1752,7 @@ DEVELOPMENT_TEAM = W5R8AG9K22; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Showcase/Other/Info.plist; + INFOPLIST_KEY_NSCameraUsageDescription = "Allow the app to scan for QR codes"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; @@ -1781,6 +1782,7 @@ DEVELOPMENT_TEAM = W5R8AG9K22; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Showcase/Other/Info.plist; + INFOPLIST_KEY_NSCameraUsageDescription = "Allow the app to scan for QR codes"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; diff --git a/Example/Showcase/Other/Info.plist b/Example/Showcase/Other/Info.plist index 8767aa73a..95d2824b0 100644 --- a/Example/Showcase/Other/Info.plist +++ b/Example/Showcase/Other/Info.plist @@ -15,6 +15,8 @@ + ITSAppUsesNonExemptEncryption + NSAppTransportSecurity NSAllowsArbitraryLoads From 26bc699a6c91f880d1f89d2ba14cf32021c6d63f Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Mon, 5 Sep 2022 08:39:33 +0200 Subject: [PATCH 42/92] Add disconnect to auth client api --- Sources/Auth/AuthClient.swift | 8 ++++- Sources/Auth/AuthClientFactory.swift | 3 +- Sources/Auth/AuthProtocolMethods.swift | 29 +++++++++++++++++ ...rvice.swift => DeletePairingService.swift} | 31 +------------------ 4 files changed, 39 insertions(+), 32 deletions(-) create mode 100644 Sources/Auth/AuthProtocolMethods.swift rename Sources/Auth/Services/Common/{DisconnectPairService.swift => DeletePairingService.swift} (68%) diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index ba84fe74c..41ab8d3ad 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -46,6 +46,7 @@ public class AuthClient { private var authRequestPublisherSubject = PassthroughSubject() private let appPairService: AppPairService private let appRequestService: AppRequestService + private let deletePairingService: DeletePairingService private let appRespondSubscriber: AppRespondSubscriber private let walletPairService: WalletPairService private let walletRequestSubscriber: WalletRequestSubscriber @@ -61,6 +62,7 @@ public class AuthClient { walletPairService: WalletPairService, walletRequestSubscriber: WalletRequestSubscriber, walletRespondService: WalletRespondService, + deletePairingService: DeletePairingService, account: Account?, pendingRequestsProvider: PendingRequestsProvider, cleanupService: CleanupService, @@ -80,7 +82,7 @@ public class AuthClient { self.logger = logger self.pairingStorage = pairingStorage self.socketConnectionStatusPublisher = socketConnectionStatusPublisher - + self.deletePairingService = deletePairingService setUpPublishers() } @@ -135,6 +137,10 @@ public class AuthClient { try await walletRespondService.respondError(requestId: requestId) } + public func disconnect(topic: String) async throws { + try await deletePairingService.delete(topic: topic) + } + /// Query pending authentication requests /// - Returns: Pending authentication requests public func getPendingRequests() throws -> [AuthRequest] { diff --git a/Sources/Auth/AuthClientFactory.swift b/Sources/Auth/AuthClientFactory.swift index 469fa37a6..160489b69 100644 --- a/Sources/Auth/AuthClientFactory.swift +++ b/Sources/Auth/AuthClientFactory.swift @@ -31,13 +31,14 @@ public struct AuthClientFactory { let walletRespondService = WalletRespondService(networkingInteractor: networkingInteractor, logger: logger, kms: kms, rpcHistory: history) let pendingRequestsProvider = PendingRequestsProvider(rpcHistory: history) let cleanupService = CleanupService(pairingStore: pairingStore, kms: kms) + let deletePairingService = DeletePairingService(networkingInteractor: networkingInteractor, kms: kms, pairingStorage: pairingStore, logger: logger) return AuthClient(appPairService: appPairService, appRequestService: appRequestService, appRespondSubscriber: appRespondSubscriber, walletPairService: walletPairService, walletRequestSubscriber: walletRequestSubscriber, - walletRespondService: walletRespondService, + walletRespondService: walletRespondService, deletePairingService: deletePairingService, account: account, pendingRequestsProvider: pendingRequestsProvider, cleanupService: cleanupService, diff --git a/Sources/Auth/AuthProtocolMethods.swift b/Sources/Auth/AuthProtocolMethods.swift new file mode 100644 index 000000000..b4ce15058 --- /dev/null +++ b/Sources/Auth/AuthProtocolMethods.swift @@ -0,0 +1,29 @@ +import Foundation + +enum AuthProtocolMethods: String { + case authRequest = "wc_authRequest" + case pairingDelete = "wc_pairingDelete" + case pairingPing = "wc_pairingPing" + + var requestTag: Int { + switch self { + case .authRequest: + return 3000 + case .pairingDelete: + return 1000 + case .pairingPing: + return 1002 + } + } + + var responseTag: Int { + switch self { + case .authRequest: + return 3001 + case .pairingDelete: + return 1001 + case .pairingPing: + return 1003 + } + } +} diff --git a/Sources/Auth/Services/Common/DisconnectPairService.swift b/Sources/Auth/Services/Common/DeletePairingService.swift similarity index 68% rename from Sources/Auth/Services/Common/DisconnectPairService.swift rename to Sources/Auth/Services/Common/DeletePairingService.swift index cd21c09ff..d28546534 100644 --- a/Sources/Auth/Services/Common/DisconnectPairService.swift +++ b/Sources/Auth/Services/Common/DeletePairingService.swift @@ -6,7 +6,7 @@ import WalletConnectKMS import WalletConnectUtils import WalletConnectPairing -class DisconnectPairService { +class DeletePairingService { enum Errors: Error { case pairingNotFound } @@ -36,32 +36,3 @@ class DisconnectPairService { networkingInteractor.unsubscribe(topic: topic) } } - - -enum AuthProtocolMethods: String { - case authRequest = "wc_authRequest" - case pairingDelete = "wc_pairingDelete" - case pairingPing = "wc_pairingPing" - - var requestTag: Int { - switch self { - case .authRequest: - return 3000 - case .pairingDelete: - return 1000 - case .pairingPing: - return 1002 - } - } - - var responseTag: Int { - switch self { - case .authRequest: - return 3001 - case .pairingDelete: - return 1001 - case .pairingPing: - return 1003 - } - } -} From 9d6cfb0fa9b8609432e8b0eff9dc29aa4edc0265 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Mon, 5 Sep 2022 09:38:51 +0200 Subject: [PATCH 43/92] add disconnect service for sign --- .../Engine/Common/DeletePairingService.swift | 32 ++++++++++++++++++ .../Engine/Common/DeleteSessionService.swift | 30 +++++++++++++++++ .../Engine/Common/DisconnectService.swift | 33 +++++++++++++++++++ .../Engine/Common/SessionEngine.swift | 9 ----- .../WalletConnectSign/Sign/SignClient.swift | 6 ++-- .../Sign/SignClientFactory.swift | 6 ++-- 6 files changed, 103 insertions(+), 13 deletions(-) create mode 100644 Sources/WalletConnectSign/Engine/Common/DeletePairingService.swift create mode 100644 Sources/WalletConnectSign/Engine/Common/DeleteSessionService.swift create mode 100644 Sources/WalletConnectSign/Engine/Common/DisconnectService.swift diff --git a/Sources/WalletConnectSign/Engine/Common/DeletePairingService.swift b/Sources/WalletConnectSign/Engine/Common/DeletePairingService.swift new file mode 100644 index 000000000..b2e260b0a --- /dev/null +++ b/Sources/WalletConnectSign/Engine/Common/DeletePairingService.swift @@ -0,0 +1,32 @@ +import Foundation +import WalletConnectKMS +import WalletConnectUtils +import WalletConnectPairing + +class DeletePairingService { + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + private let pairingStorage: WCPairingStorage + private let logger: ConsoleLogging + + init(networkingInteractor: NetworkInteracting, + kms: KeyManagementServiceProtocol, + pairingStorage: WCPairingStorage, + logger: ConsoleLogging) { + self.networkingInteractor = networkingInteractor + self.kms = kms + self.pairingStorage = pairingStorage + self.logger = logger + } + + func delete(topic: String) async throws { + let reasonCode = ReasonCode.userDisconnected + let reason = SessionType.Reason(code: reasonCode.code, message: reasonCode.message) + logger.debug("Will delete pairing for reason: message: \(reason.message) code: \(reason.code)") + try await networkingInteractor.request(.wcSessionDelete(reason), onTopic: topic) + pairingStorage.delete(topic: topic) + kms.deleteSymmetricKey(for: topic) + networkingInteractor.unsubscribe(topic: topic) + } +} + diff --git a/Sources/WalletConnectSign/Engine/Common/DeleteSessionService.swift b/Sources/WalletConnectSign/Engine/Common/DeleteSessionService.swift new file mode 100644 index 000000000..561af3507 --- /dev/null +++ b/Sources/WalletConnectSign/Engine/Common/DeleteSessionService.swift @@ -0,0 +1,30 @@ +import Foundation +import WalletConnectKMS +import WalletConnectUtils + +class DeleteSessionService { + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + private let sessionStore: WCSessionStorage + private let logger: ConsoleLogging + + init(networkingInteractor: NetworkInteracting, + kms: KeyManagementServiceProtocol, + sessionStore: WCSessionStorage, + logger: ConsoleLogging) { + self.networkingInteractor = networkingInteractor + self.kms = kms + self.sessionStore = sessionStore + self.logger = logger + } + + func delete(topic: String) async throws { + let reasonCode = ReasonCode.userDisconnected + let reason = SessionType.Reason(code: reasonCode.code, message: reasonCode.message) + logger.debug("Will delete session for reason: message: \(reason.message) code: \(reason.code)") + try await networkingInteractor.request(.wcSessionDelete(reason), onTopic: topic) + sessionStore.delete(topic: topic) + kms.deleteSymmetricKey(for: topic) + networkingInteractor.unsubscribe(topic: topic) + } +} diff --git a/Sources/WalletConnectSign/Engine/Common/DisconnectService.swift b/Sources/WalletConnectSign/Engine/Common/DisconnectService.swift new file mode 100644 index 000000000..d0b417a7f --- /dev/null +++ b/Sources/WalletConnectSign/Engine/Common/DisconnectService.swift @@ -0,0 +1,33 @@ +import Foundation +import WalletConnectPairing + +class DisconnectService { + enum Errors: Error { + case objectForTopicNotFound + } + + private let deletePairingService: DeletePairingService + private let deleteSessionService: DeleteSessionService + private let pairingStorage: WCPairingStorage + private let sessionStorage: WCSessionStorage + + init(deletePairingService: DeletePairingService, + deleteSessionService: DeleteSessionService, + pairingStorage: WCPairingStorage, + sessionStorage: WCSessionStorage) { + self.deletePairingService = deletePairingService + self.deleteSessionService = deleteSessionService + self.pairingStorage = pairingStorage + self.sessionStorage = sessionStorage + } + + func disconnect(topic: String) async throws { + if pairingStorage.hasPairing(forTopic: topic) { + try await deletePairingService.delete(topic: topic) + } else if sessionStorage.hasSession(forTopic: topic) { + try await deleteSessionService.delete(topic: topic) + } else { + throw Errors.objectForTopicNotFound + } + } +} diff --git a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift index 80f255b0a..a1197c8e1 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -44,15 +44,6 @@ final class SessionEngine { sessionStore.getAll().map {$0.publicRepresentation()} } - func delete(topic: String) async throws { - let reasonCode = ReasonCode.userDisconnected - let reason = SessionType.Reason(code: reasonCode.code, message: reasonCode.message) - logger.debug("Will delete session for reason: message: \(reason.message) code: \(reason.code)") - try await networkingInteractor.request(.wcSessionDelete(reason), onTopic: topic) - sessionStore.delete(topic: topic) - networkingInteractor.unsubscribe(topic: topic) - } - func ping(topic: String, completion: @escaping (Result) -> Void) { guard sessionStore.hasSession(forTopic: topic) else { logger.debug("Could not find session to ping for topic \(topic)") diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index d43546582..befbca947 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -91,6 +91,7 @@ public final class SignClient { private let pairEngine: PairEngine private let sessionEngine: SessionEngine private let approveEngine: ApproveEngine + private let disconnectService: DisconnectService private let nonControllerSessionStateMachine: NonControllerSessionStateMachine private let controllerSessionStateMachine: ControllerSessionStateMachine private let history: JsonRpcHistory @@ -119,6 +120,7 @@ public final class SignClient { approveEngine: ApproveEngine, nonControllerSessionStateMachine: NonControllerSessionStateMachine, controllerSessionStateMachine: ControllerSessionStateMachine, + disconnectService: DisconnectService, history: JsonRpcHistory, cleanupService: CleanupService ) { @@ -132,7 +134,7 @@ public final class SignClient { self.controllerSessionStateMachine = controllerSessionStateMachine self.history = history self.cleanupService = cleanupService - + self.disconnectService = disconnectService setUpConnectionObserving() setUpEnginesCallbacks() } @@ -267,7 +269,7 @@ public final class SignClient { /// - Parameters: /// - topic: Session topic that you want to delete public func disconnect(topic: String) async throws { - try await sessionEngine.delete(topic: topic) + try await disconnectService.disconnect(topic: topic) } /// Query sessions diff --git a/Sources/WalletConnectSign/Sign/SignClientFactory.swift b/Sources/WalletConnectSign/Sign/SignClientFactory.swift index 8ab60ee52..9a0412ad4 100644 --- a/Sources/WalletConnectSign/Sign/SignClientFactory.swift +++ b/Sources/WalletConnectSign/Sign/SignClientFactory.swift @@ -38,6 +38,9 @@ public struct SignClientFactory { let pairEngine = PairEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore) let approveEngine = ApproveEngine(networkingInteractor: networkingInteractor, proposalPayloadsStore: proposalPayloadsStore, sessionToPairingTopic: sessionToPairingTopic, metadata: metadata, kms: kms, logger: logger, pairingStore: pairingStore, sessionStore: sessionStore) let cleanupService = CleanupService(pairingStore: pairingStore, sessionStore: sessionStore, kms: kms, sessionToPairingTopic: sessionToPairingTopic) + let deletePairingService = DeletePairingService(networkingInteractor: networkingInteractor, kms: kms, pairingStorage: pairingStore, logger: logger) + let deleteSessionService = DeleteSessionService(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) + let disconnectService = DisconnectService(deletePairingService: deletePairingService, deleteSessionService: deleteSessionService, pairingStorage: pairingStore, sessionStorage: sessionStore) let client = SignClient( logger: logger, @@ -47,11 +50,10 @@ public struct SignClientFactory { sessionEngine: sessionEngine, approveEngine: approveEngine, nonControllerSessionStateMachine: nonControllerSessionStateMachine, - controllerSessionStateMachine: controllerSessionStateMachine, + controllerSessionStateMachine: controllerSessionStateMachine, disconnectService: disconnectService, history: history, cleanupService: cleanupService ) - return client } } From 1895acb57e67131662842e82acd1182f3d65ab9c Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Mon, 5 Sep 2022 09:39:10 +0200 Subject: [PATCH 44/92] run lint --- Package.swift | 2 +- Sources/Auth/Services/Common/DeletePairingService.swift | 1 - Sources/Chat/ProtocolServices/Common/MessagingService.swift | 2 +- Sources/WalletConnectKMS/Serialiser/Serializer.swift | 2 +- .../WalletConnectSign/Engine/Common/DeletePairingService.swift | 1 - 5 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Package.swift b/Package.swift index a562eca58..893eb9338 100644 --- a/Package.swift +++ b/Package.swift @@ -24,7 +24,7 @@ let package = Package( targets: ["WalletConnectRouter"]), .library( name: "WalletConnectNetworking", - targets: ["WalletConnectNetworking"]), + targets: ["WalletConnectNetworking"]) ], dependencies: [ .package(url: "https://github.com/flypaper0/Web3.swift", .branch("feature/eip-155")) diff --git a/Sources/Auth/Services/Common/DeletePairingService.swift b/Sources/Auth/Services/Common/DeletePairingService.swift index d28546534..0b0cc4919 100644 --- a/Sources/Auth/Services/Common/DeletePairingService.swift +++ b/Sources/Auth/Services/Common/DeletePairingService.swift @@ -1,4 +1,3 @@ - import Foundation import WalletConnectNetworking import JSONRPC diff --git a/Sources/Chat/ProtocolServices/Common/MessagingService.swift b/Sources/Chat/ProtocolServices/Common/MessagingService.swift index eaeaf061e..a672fe386 100644 --- a/Sources/Chat/ProtocolServices/Common/MessagingService.swift +++ b/Sources/Chat/ProtocolServices/Common/MessagingService.swift @@ -43,7 +43,7 @@ class MessagingService { private func setUpResponseHandling() { networkingInteractor.responsePublisher - .sink { [unowned self] payload in + .sink { [unowned self] _ in logger.debug("Received Message response") }.store(in: &publishers) } diff --git a/Sources/WalletConnectKMS/Serialiser/Serializer.swift b/Sources/WalletConnectKMS/Serialiser/Serializer.swift index d58050137..742ddf2de 100644 --- a/Sources/WalletConnectKMS/Serialiser/Serializer.swift +++ b/Sources/WalletConnectKMS/Serialiser/Serializer.swift @@ -62,7 +62,7 @@ public class Serializer: Serializing { private func handleType1Envelope(_ topic: String, peerPubKey: Data, sealbox: Data) throws -> T { guard let selfPubKey = kms.getPublicKey(for: topic) else { throw Errors.publicKeyForTopicNotFound } - + let agreementKeys = try kms.performKeyAgreement(selfPublicKey: selfPubKey, peerPublicKey: peerPubKey.toHexString()) let decodedType: T = try decode(sealbox: sealbox, symmetricKey: agreementKeys.sharedKey.rawRepresentation) let newTopic = agreementKeys.derivedTopic() diff --git a/Sources/WalletConnectSign/Engine/Common/DeletePairingService.swift b/Sources/WalletConnectSign/Engine/Common/DeletePairingService.swift index b2e260b0a..a6324481e 100644 --- a/Sources/WalletConnectSign/Engine/Common/DeletePairingService.swift +++ b/Sources/WalletConnectSign/Engine/Common/DeletePairingService.swift @@ -29,4 +29,3 @@ class DeletePairingService { networkingInteractor.unsubscribe(topic: topic) } } - From 5cb017b54e4f38ed642eb68cc4d09fa25eb779cc Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Mon, 5 Sep 2022 13:18:20 +0300 Subject: [PATCH 45/92] Rename to ProtocolMethod --- Sources/Auth/Services/App/AppRequestService.swift | 2 +- Sources/Auth/Services/App/AppRespondSubscriber.swift | 4 ++-- .../Auth/Services/Wallet/WalletRequestSubscriber.swift | 4 ++-- .../Auth/Services/Wallet/WalletRespondService.swift | 4 ++-- ...thNetworkRequest.swift => AuthProtocolMethod.swift} | 2 +- .../ProtocolServices/Common/MessagingService.swift | 10 +++++----- .../Invitee/InvitationHandlingService.swift | 6 +++--- .../Chat/ProtocolServices/Inviter/InviteService.swift | 6 +++--- .../{ChatRequest.swift => ChatProtocolMethod.swift} | 2 +- .../WalletConnectNetworking/NetworkInteracting.swift | 6 +++--- .../WalletConnectNetworking/NetworkInteractor.swift | 6 +++--- .../{NetworkRequest.swift => ProtocolMethod.swift} | 2 +- Tests/AuthTests/WalletRequestSubscriberTests.swift | 2 +- Tests/TestingUtils/NetworkingInteractorMock.swift | 6 +++--- 14 files changed, 31 insertions(+), 31 deletions(-) rename Sources/Auth/Types/{AuthNetworkRequest.swift => AuthProtocolMethod.swift} (80%) rename Sources/Chat/Types/{ChatRequest.swift => ChatProtocolMethod.swift} (90%) rename Sources/WalletConnectNetworking/{NetworkRequest.swift => ProtocolMethod.swift} (70%) diff --git a/Sources/Auth/Services/App/AppRequestService.swift b/Sources/Auth/Services/App/AppRequestService.swift index 7d629d7af..e1c988eac 100644 --- a/Sources/Auth/Services/App/AppRequestService.swift +++ b/Sources/Auth/Services/App/AppRequestService.swift @@ -30,7 +30,7 @@ actor AppRequestService { let request = RPCRequest(method: "wc_authRequest", params: params) try kms.setPublicKey(publicKey: pubKey, for: responseTopic) logger.debug("AppRequestService: Subscribibg for response topic: \(responseTopic)") - try await networkingInteractor.requestNetworkAck(request, topic: topic, tag: AuthNetworkRequest.request.tag) + try await networkingInteractor.requestNetworkAck(request, topic: topic, tag: AuthProtocolMethod.request.tag) try await networkingInteractor.subscribe(topic: responseTopic) } } diff --git a/Sources/Auth/Services/App/AppRespondSubscriber.swift b/Sources/Auth/Services/App/AppRespondSubscriber.swift index 5dfd4fc10..442a45372 100644 --- a/Sources/Auth/Services/App/AppRespondSubscriber.swift +++ b/Sources/Auth/Services/App/AppRespondSubscriber.swift @@ -32,13 +32,13 @@ class AppRespondSubscriber { } private func subscribeForResponse() { - networkingInteractor.responceErrorSubscription(on: AuthNetworkRequest.request) + networkingInteractor.responceErrorSubscription(on: AuthProtocolMethod.request) .sink { [unowned self] payload in guard let error = AuthError(code: payload.error.code) else { return } onResponse?(payload.id, .failure(error)) }.store(in: &publishers) - networkingInteractor.responseSubscription(on: AuthNetworkRequest.request) + networkingInteractor.responseSubscription(on: AuthProtocolMethod.request) .sink { [unowned self] (payload: ResponseSubscriptionPayload) in activatePairingIfNeeded(id: payload.id) diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift index 5688b926f..41d74d8a7 100644 --- a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift +++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift @@ -30,7 +30,7 @@ class WalletRequestSubscriber { private func subscribeForRequest() { guard let address = address else { return } - networkingInteractor.requestSubscription(on: AuthNetworkRequest.request) + networkingInteractor.requestSubscription(on: AuthProtocolMethod.request) .sink { [unowned self] (payload: RequestSubscriptionPayload) in logger.debug("WalletRequestSubscriber: Received request") let message = messageFormatter.formatMessage(from: payload.request.payloadParams, address: address) @@ -42,7 +42,7 @@ class WalletRequestSubscriber { guard let pubKey = kms.getAgreementSecret(for: topic)?.publicKey else { return logger.error("Agreement key for topic \(topic) not found") } - let tag = AuthNetworkRequest.request.tag + let tag = AuthProtocolMethod.request.tag let envelopeType = Envelope.EnvelopeType.type1(pubKey: pubKey.rawRepresentation) Task(priority: .high) { diff --git a/Sources/Auth/Services/Wallet/WalletRespondService.swift b/Sources/Auth/Services/Wallet/WalletRespondService.swift index 4e44e8f97..ae87580f4 100644 --- a/Sources/Auth/Services/Wallet/WalletRespondService.swift +++ b/Sources/Auth/Services/Wallet/WalletRespondService.swift @@ -33,7 +33,7 @@ actor WalletRespondService { let didpkh = DIDPKH(account: account) let cacao = CacaoFormatter().format(authRequestParams, signature, didpkh) let response = RPCResponse(id: requestId, result: cacao) - try await networkingInteractor.respond(topic: topic, response: response, tag: AuthNetworkRequest.request.tag, envelopeType: .type1(pubKey: keys.publicKey.rawRepresentation)) + try await networkingInteractor.respond(topic: topic, response: response, tag: AuthProtocolMethod.request.tag, envelopeType: .type1(pubKey: keys.publicKey.rawRepresentation)) } func respondError(requestId: RPCID) async throws { @@ -42,7 +42,7 @@ actor WalletRespondService { try kms.setAgreementSecret(keys, topic: topic) - let tag = AuthNetworkRequest.request.tag + let tag = AuthProtocolMethod.request.tag let error = AuthError.userRejeted let envelopeType = Envelope.EnvelopeType.type1(pubKey: keys.publicKey.rawRepresentation) try await networkingInteractor.respondError(topic: topic, requestId: requestId, tag: tag, reason: error, envelopeType: envelopeType) diff --git a/Sources/Auth/Types/AuthNetworkRequest.swift b/Sources/Auth/Types/AuthProtocolMethod.swift similarity index 80% rename from Sources/Auth/Types/AuthNetworkRequest.swift rename to Sources/Auth/Types/AuthProtocolMethod.swift index 0a0f39417..37ff013d3 100644 --- a/Sources/Auth/Types/AuthNetworkRequest.swift +++ b/Sources/Auth/Types/AuthProtocolMethod.swift @@ -1,7 +1,7 @@ import Foundation import WalletConnectNetworking -enum AuthNetworkRequest: NetworkRequest { +enum AuthProtocolMethod: ProtocolMethod { case request var method: String { diff --git a/Sources/Chat/ProtocolServices/Common/MessagingService.swift b/Sources/Chat/ProtocolServices/Common/MessagingService.swift index 3d7638b70..d9fc9e772 100644 --- a/Sources/Chat/ProtocolServices/Common/MessagingService.swift +++ b/Sources/Chat/ProtocolServices/Common/MessagingService.swift @@ -33,8 +33,8 @@ class MessagingService { guard let authorAccount = thread?.selfAccount else { throw Errors.threadDoNotExist} let timestamp = Int64(Date().timeIntervalSince1970 * 1000) let message = Message(topic: topic, message: messageString, authorAccount: authorAccount, timestamp: timestamp) - let request = RPCRequest(method: ChatRequest.message.method, params: message) - try await networkingInteractor.request(request, topic: topic, tag: ChatRequest.message.tag) + let request = RPCRequest(method: ChatProtocolMethod.message.method, params: message) + try await networkingInteractor.request(request, topic: topic, tag: ChatProtocolMethod.message.tag) Task(priority: .background) { await messagesStore.add(message) onMessage?(message) @@ -42,14 +42,14 @@ class MessagingService { } private func setUpResponseHandling() { - networkingInteractor.responseSubscription(on: ChatRequest.message) + networkingInteractor.responseSubscription(on: ChatProtocolMethod.message) .sink { [unowned self] (payload: ResponseSubscriptionPayload) in logger.debug("Received Message response") }.store(in: &publishers) } private func setUpRequestHandling() { - networkingInteractor.requestSubscription(on: ChatRequest.message) + networkingInteractor.requestSubscription(on: ChatProtocolMethod.message) .sink { [unowned self] (payload: RequestSubscriptionPayload) in var message = payload.request message.topic = payload.topic @@ -59,7 +59,7 @@ class MessagingService { private func handleMessage(_ message: Message, topic: String, requestId: RPCID) { Task(priority: .background) { - try await networkingInteractor.respondSuccess(topic: topic, requestId: requestId, tag: ChatRequest.message.tag) + try await networkingInteractor.respondSuccess(topic: topic, requestId: requestId, tag: ChatProtocolMethod.message.tag) await messagesStore.add(message) logger.debug("Received message") onMessage?(message) diff --git a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift index 624736fe3..85509e248 100644 --- a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift +++ b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift @@ -47,7 +47,7 @@ class InvitationHandlingService { let response = RPCResponse(id: payload.id, result: inviteResponse) let responseTopic = try getInviteResponseTopic(requestTopic: payload.topic, invite: payload.request) - try await networkingInteractor.respond(topic: responseTopic, response: response, tag: ChatRequest.invite.tag) + try await networkingInteractor.respond(topic: responseTopic, response: response, tag: ChatProtocolMethod.invite.tag) let threadAgreementKeys = try kms.performKeyAgreement(selfPublicKey: selfThreadPubKey, peerPublicKey: payload.request.publicKey) let threadTopic = threadAgreementKeys.derivedTopic() @@ -71,13 +71,13 @@ class InvitationHandlingService { let responseTopic = try getInviteResponseTopic(requestTopic: payload.topic, invite: payload.request) - try await networkingInteractor.respondError(topic: responseTopic, requestId: payload.id, tag: ChatRequest.invite.tag, reason: ChatError.userRejected) + try await networkingInteractor.respondError(topic: responseTopic, requestId: payload.id, tag: ChatProtocolMethod.invite.tag, reason: ChatError.userRejected) invitePayloadStore.delete(forKey: inviteId) } private func setUpRequestHandling() { - networkingInteractor.requestSubscription(on: ChatRequest.invite) + networkingInteractor.requestSubscription(on: ChatProtocolMethod.invite) .sink { [unowned self] (payload: RequestSubscriptionPayload) in logger.debug("did receive an invite") invitePayloadStore.set(payload, forKey: payload.request.publicKey) diff --git a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift index 41fec4d67..d04308f5e 100644 --- a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift +++ b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift @@ -42,7 +42,7 @@ class InviteService { // overrides on invite toipic try kms.setSymmetricKey(symKeyI.sharedKey, for: inviteTopic) - let request = RPCRequest(method: ChatRequest.invite.method, params: invite) + let request = RPCRequest(method: ChatProtocolMethod.invite.method, params: invite) // 2. Proposer subscribes to topic R which is the hash of the derived symKey let responseTopic = symKeyI.derivedTopic() @@ -50,13 +50,13 @@ class InviteService { try kms.setSymmetricKey(symKeyI.sharedKey, for: responseTopic) try await networkingInteractor.subscribe(topic: responseTopic) - try await networkingInteractor.request(request, topic: inviteTopic, tag: ChatRequest.invite.tag, envelopeType: .type1(pubKey: selfPubKeyY.rawRepresentation)) + try await networkingInteractor.request(request, topic: inviteTopic, tag: ChatProtocolMethod.invite.tag, envelopeType: .type1(pubKey: selfPubKeyY.rawRepresentation)) logger.debug("invite sent on topic: \(inviteTopic)") } private func setUpResponseHandling() { - networkingInteractor.responseSubscription(on: ChatRequest.invite) + networkingInteractor.responseSubscription(on: ChatProtocolMethod.invite) .sink { [unowned self] (payload: ResponseSubscriptionPayload) in logger.debug("Invite has been accepted") diff --git a/Sources/Chat/Types/ChatRequest.swift b/Sources/Chat/Types/ChatProtocolMethod.swift similarity index 90% rename from Sources/Chat/Types/ChatRequest.swift rename to Sources/Chat/Types/ChatProtocolMethod.swift index 7a0ec4f7e..1566f375c 100644 --- a/Sources/Chat/Types/ChatRequest.swift +++ b/Sources/Chat/Types/ChatProtocolMethod.swift @@ -1,7 +1,7 @@ import Foundation import WalletConnectNetworking -enum ChatRequest: NetworkRequest { +enum ChatProtocolMethod: ProtocolMethod { case invite case message diff --git a/Sources/WalletConnectNetworking/NetworkInteracting.swift b/Sources/WalletConnectNetworking/NetworkInteracting.swift index 99db4ac69..9708b7290 100644 --- a/Sources/WalletConnectNetworking/NetworkInteracting.swift +++ b/Sources/WalletConnectNetworking/NetworkInteracting.swift @@ -15,14 +15,14 @@ public protocol NetworkInteracting { func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws func requestSubscription( - on request: NetworkRequest + on request: ProtocolMethod ) -> AnyPublisher, Never> func responseSubscription( - on request: NetworkRequest + on request: ProtocolMethod ) -> AnyPublisher, Never> - func responceErrorSubscription(on request: NetworkRequest) -> AnyPublisher + func responceErrorSubscription(on request: ProtocolMethod) -> AnyPublisher } extension NetworkInteracting { diff --git a/Sources/WalletConnectNetworking/NetworkInteractor.swift b/Sources/WalletConnectNetworking/NetworkInteractor.swift index 3d8e81a22..a0e6b21ec 100644 --- a/Sources/WalletConnectNetworking/NetworkInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkInteractor.swift @@ -56,7 +56,7 @@ public class NetworkingInteractor: NetworkInteracting { } } - public func requestSubscription(on request: NetworkRequest) -> AnyPublisher, Never> { + public func requestSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { return requestPublisher .filter { $0.request.method == request.method } .compactMap { topic, rpcRequest in @@ -66,7 +66,7 @@ public class NetworkingInteractor: NetworkInteracting { .eraseToAnyPublisher() } - public func responseSubscription(on request: NetworkRequest) -> AnyPublisher, Never> { + public func responseSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { return responsePublisher .filter { $0.request.method == request.method } .compactMap { topic, rpcRequest, rpcResponce in @@ -79,7 +79,7 @@ public class NetworkingInteractor: NetworkInteracting { .eraseToAnyPublisher() } - public func responceErrorSubscription(on request: NetworkRequest) -> AnyPublisher { + public func responceErrorSubscription(on request: ProtocolMethod) -> AnyPublisher { return responsePublisher .filter { $0.request.method == request.method } .compactMap { (_, _, rpcResponce) in diff --git a/Sources/WalletConnectNetworking/NetworkRequest.swift b/Sources/WalletConnectNetworking/ProtocolMethod.swift similarity index 70% rename from Sources/WalletConnectNetworking/NetworkRequest.swift rename to Sources/WalletConnectNetworking/ProtocolMethod.swift index 3922f4721..1809ad6ba 100644 --- a/Sources/WalletConnectNetworking/NetworkRequest.swift +++ b/Sources/WalletConnectNetworking/ProtocolMethod.swift @@ -1,6 +1,6 @@ import Foundation -public protocol NetworkRequest { +public protocol ProtocolMethod { var method: String { get } var tag: Int { get } } diff --git a/Tests/AuthTests/WalletRequestSubscriberTests.swift b/Tests/AuthTests/WalletRequestSubscriberTests.swift index 6d39d42b5..5a7f94649 100644 --- a/Tests/AuthTests/WalletRequestSubscriberTests.swift +++ b/Tests/AuthTests/WalletRequestSubscriberTests.swift @@ -35,7 +35,7 @@ class WalletRequestSubscriberTests: XCTestCase { messageExpectation.fulfill() } - let request = RPCRequest(method: AuthNetworkRequest.request.method, params: AuthRequestParams.stub(id: expectedRequestId), id: expectedRequestId.right!) + let request = RPCRequest(method: AuthProtocolMethod.request.method, params: AuthRequestParams.stub(id: expectedRequestId), id: expectedRequestId.right!) networkingInteractor.requestPublisherSubject.send(("123", request)) wait(for: [messageExpectation], timeout: defaultTimeout) diff --git a/Tests/TestingUtils/NetworkingInteractorMock.swift b/Tests/TestingUtils/NetworkingInteractorMock.swift index 0f1fbe6a6..7e8ff61cf 100644 --- a/Tests/TestingUtils/NetworkingInteractorMock.swift +++ b/Tests/TestingUtils/NetworkingInteractorMock.swift @@ -25,7 +25,7 @@ public class NetworkingInteractorMock: NetworkInteracting { responsePublisherSubject.eraseToAnyPublisher() } - public func requestSubscription(on request: NetworkRequest) -> AnyPublisher, Never> { + public func requestSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { return requestPublisher .filter { $0.request.method == request.method } .compactMap { topic, rpcRequest in @@ -35,7 +35,7 @@ public class NetworkingInteractorMock: NetworkInteracting { .eraseToAnyPublisher() } - public func responseSubscription(on request: NetworkRequest) -> AnyPublisher, Never> { + public func responseSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { return responsePublisher .filter { $0.request.method == request.method } .compactMap { topic, rpcRequest, rpcResponce in @@ -48,7 +48,7 @@ public class NetworkingInteractorMock: NetworkInteracting { .eraseToAnyPublisher() } - public func responceErrorSubscription(on request: NetworkRequest) -> AnyPublisher { + public func responceErrorSubscription(on request: ProtocolMethod) -> AnyPublisher { return responsePublisher .filter { $0.request.method == request.method } .compactMap { (_, _, rpcResponce) in From eff673e5ee88399d354692edc0e07bf0f503e4f9 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Mon, 5 Sep 2022 13:20:00 +0300 Subject: [PATCH 46/92] Responce typo --- Sources/Auth/Services/App/AppRespondSubscriber.swift | 2 +- .../WalletConnectNetworking/NetworkInteracting.swift | 2 +- .../WalletConnectNetworking/NetworkInteractor.swift | 10 +++++----- Tests/TestingUtils/NetworkingInteractorMock.swift | 10 +++++----- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/Auth/Services/App/AppRespondSubscriber.swift b/Sources/Auth/Services/App/AppRespondSubscriber.swift index 442a45372..61477b712 100644 --- a/Sources/Auth/Services/App/AppRespondSubscriber.swift +++ b/Sources/Auth/Services/App/AppRespondSubscriber.swift @@ -32,7 +32,7 @@ class AppRespondSubscriber { } private func subscribeForResponse() { - networkingInteractor.responceErrorSubscription(on: AuthProtocolMethod.request) + networkingInteractor.responseErrorSubscription(on: AuthProtocolMethod.request) .sink { [unowned self] payload in guard let error = AuthError(code: payload.error.code) else { return } onResponse?(payload.id, .failure(error)) diff --git a/Sources/WalletConnectNetworking/NetworkInteracting.swift b/Sources/WalletConnectNetworking/NetworkInteracting.swift index 9708b7290..8dd02d382 100644 --- a/Sources/WalletConnectNetworking/NetworkInteracting.swift +++ b/Sources/WalletConnectNetworking/NetworkInteracting.swift @@ -22,7 +22,7 @@ public protocol NetworkInteracting { on request: ProtocolMethod ) -> AnyPublisher, Never> - func responceErrorSubscription(on request: ProtocolMethod) -> AnyPublisher + func responseErrorSubscription(on request: ProtocolMethod) -> AnyPublisher } extension NetworkInteracting { diff --git a/Sources/WalletConnectNetworking/NetworkInteractor.swift b/Sources/WalletConnectNetworking/NetworkInteractor.swift index a0e6b21ec..13859ead6 100644 --- a/Sources/WalletConnectNetworking/NetworkInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkInteractor.swift @@ -69,21 +69,21 @@ public class NetworkingInteractor: NetworkInteracting { public func responseSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { return responsePublisher .filter { $0.request.method == request.method } - .compactMap { topic, rpcRequest, rpcResponce in + .compactMap { topic, rpcRequest, rpcResponse in guard let id = rpcRequest.id, let request = try? rpcRequest.params?.get(Request.self), - let response = try? rpcResponce.result?.get(Response.self) else { return nil } + let response = try? rpcResponse.result?.get(Response.self) else { return nil } return ResponseSubscriptionPayload(id: id, topic: topic, request: request, response: response) } .eraseToAnyPublisher() } - public func responceErrorSubscription(on request: ProtocolMethod) -> AnyPublisher { + public func responseErrorSubscription(on request: ProtocolMethod) -> AnyPublisher { return responsePublisher .filter { $0.request.method == request.method } - .compactMap { (_, _, rpcResponce) in - guard let id = rpcResponce.id, let error = rpcResponce.error else { return nil } + .compactMap { (_, _, rpcResponse) in + guard let id = rpcResponse.id, let error = rpcResponse.error else { return nil } return ResponseSubscriptionErrorPayload(id: id, error: error) } .eraseToAnyPublisher() diff --git a/Tests/TestingUtils/NetworkingInteractorMock.swift b/Tests/TestingUtils/NetworkingInteractorMock.swift index 7e8ff61cf..10f42fc08 100644 --- a/Tests/TestingUtils/NetworkingInteractorMock.swift +++ b/Tests/TestingUtils/NetworkingInteractorMock.swift @@ -38,21 +38,21 @@ public class NetworkingInteractorMock: NetworkInteracting { public func responseSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { return responsePublisher .filter { $0.request.method == request.method } - .compactMap { topic, rpcRequest, rpcResponce in + .compactMap { topic, rpcRequest, rpcResponse in guard let id = rpcRequest.id, let request = try? rpcRequest.params?.get(Request.self), - let response = try? rpcResponce.result?.get(Response.self) else { return nil } + let response = try? rpcResponse.result?.get(Response.self) else { return nil } return ResponseSubscriptionPayload(id: id, topic: topic, request: request, response: response) } .eraseToAnyPublisher() } - public func responceErrorSubscription(on request: ProtocolMethod) -> AnyPublisher { + public func responseErrorSubscription(on request: ProtocolMethod) -> AnyPublisher { return responsePublisher .filter { $0.request.method == request.method } - .compactMap { (_, _, rpcResponce) in - guard let id = rpcResponce.id, let error = rpcResponce.error else { return nil } + .compactMap { (_, _, rpcResponse) in + guard let id = rpcResponse.id, let error = rpcResponse.error else { return nil } return ResponseSubscriptionErrorPayload(id: id, error: error) } .eraseToAnyPublisher() From afec45aee61c4e5d6a444a5225163d7b1a02843d Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Mon, 5 Sep 2022 12:45:54 +0200 Subject: [PATCH 47/92] after merge fix --- .../NetworkInteractor.swift | 14 -- .../NetworkingInteractor.swift | 178 ------------------ 2 files changed, 192 deletions(-) delete mode 100644 Sources/WalletConnectNetworking/NetworkingInteractor.swift diff --git a/Sources/WalletConnectNetworking/NetworkInteractor.swift b/Sources/WalletConnectNetworking/NetworkInteractor.swift index 7ae5f3c5f..13859ead6 100644 --- a/Sources/WalletConnectNetworking/NetworkInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkInteractor.swift @@ -12,16 +12,6 @@ public class NetworkingInteractor: NetworkInteracting { private let rpcHistory: RPCHistory private let logger: ConsoleLogging -<<<<<<< HEAD:Sources/WalletConnectNetworking/NetworkingInteractor.swift - private let requestPublisherSubject = PassthroughSubject() - private let responsePublisherSubject = PassthroughSubject() - - public var requestPublisher: AnyPublisher { - requestPublisherSubject.eraseToAnyPublisher() - } - - public var responsePublisher: AnyPublisher { -======= private let requestPublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest), Never>() private let responsePublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest, response: RPCResponse), Never>() @@ -30,7 +20,6 @@ public class NetworkingInteractor: NetworkInteracting { } private var responsePublisher: AnyPublisher<(topic: String, request: RPCRequest, response: RPCResponse), Never> { ->>>>>>> 7cb497caa4e4587bb940bd35ece55b709f14aac5:Sources/WalletConnectNetworking/NetworkInteractor.swift responsePublisherSubject.eraseToAnyPublisher() } @@ -67,8 +56,6 @@ public class NetworkingInteractor: NetworkInteracting { } } -<<<<<<< HEAD:Sources/WalletConnectNetworking/NetworkingInteractor.swift -======= public func requestSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { return requestPublisher .filter { $0.request.method == request.method } @@ -102,7 +89,6 @@ public class NetworkingInteractor: NetworkInteracting { .eraseToAnyPublisher() } ->>>>>>> 7cb497caa4e4587bb940bd35ece55b709f14aac5:Sources/WalletConnectNetworking/NetworkInteractor.swift public func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) let message = try! serializer.serialize(topic: topic, encodable: request, envelopeType: envelopeType) diff --git a/Sources/WalletConnectNetworking/NetworkingInteractor.swift b/Sources/WalletConnectNetworking/NetworkingInteractor.swift deleted file mode 100644 index 7ae5f3c5f..000000000 --- a/Sources/WalletConnectNetworking/NetworkingInteractor.swift +++ /dev/null @@ -1,178 +0,0 @@ -import Foundation -import Combine -import JSONRPC -import WalletConnectRelay -import WalletConnectUtils -import WalletConnectKMS - -public class NetworkingInteractor: NetworkInteracting { - private var publishers = Set() - private let relayClient: RelayClient - private let serializer: Serializing - private let rpcHistory: RPCHistory - private let logger: ConsoleLogging - -<<<<<<< HEAD:Sources/WalletConnectNetworking/NetworkingInteractor.swift - private let requestPublisherSubject = PassthroughSubject() - private let responsePublisherSubject = PassthroughSubject() - - public var requestPublisher: AnyPublisher { - requestPublisherSubject.eraseToAnyPublisher() - } - - public var responsePublisher: AnyPublisher { -======= - private let requestPublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest), Never>() - private let responsePublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest, response: RPCResponse), Never>() - - private var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest), Never> { - requestPublisherSubject.eraseToAnyPublisher() - } - - private var responsePublisher: AnyPublisher<(topic: String, request: RPCRequest, response: RPCResponse), Never> { ->>>>>>> 7cb497caa4e4587bb940bd35ece55b709f14aac5:Sources/WalletConnectNetworking/NetworkInteractor.swift - responsePublisherSubject.eraseToAnyPublisher() - } - - public var socketConnectionStatusPublisher: AnyPublisher - - public init( - relayClient: RelayClient, - serializer: Serializing, - logger: ConsoleLogging, - rpcHistory: RPCHistory - ) { - self.relayClient = relayClient - self.serializer = serializer - self.rpcHistory = rpcHistory - self.logger = logger - self.socketConnectionStatusPublisher = relayClient.socketConnectionStatusPublisher - relayClient.messagePublisher.sink { [unowned self] (topic, message) in - manageSubscription(topic, message) - } - .store(in: &publishers) - } - - public func subscribe(topic: String) async throws { - try await relayClient.subscribe(topic: topic) - } - - public func unsubscribe(topic: String) { - relayClient.unsubscribe(topic: topic) { [unowned self] error in - if let error = error { - logger.error(error) - } else { - rpcHistory.deleteAll(forTopic: topic) - } - } - } - -<<<<<<< HEAD:Sources/WalletConnectNetworking/NetworkingInteractor.swift -======= - public func requestSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { - return requestPublisher - .filter { $0.request.method == request.method } - .compactMap { topic, rpcRequest in - guard let id = rpcRequest.id, let request = try? rpcRequest.params?.get(Request.self) else { return nil } - return RequestSubscriptionPayload(id: id, topic: topic, request: request) - } - .eraseToAnyPublisher() - } - - public func responseSubscription(on request: ProtocolMethod) -> AnyPublisher, Never> { - return responsePublisher - .filter { $0.request.method == request.method } - .compactMap { topic, rpcRequest, rpcResponse in - guard - let id = rpcRequest.id, - let request = try? rpcRequest.params?.get(Request.self), - let response = try? rpcResponse.result?.get(Response.self) else { return nil } - return ResponseSubscriptionPayload(id: id, topic: topic, request: request, response: response) - } - .eraseToAnyPublisher() - } - - public func responseErrorSubscription(on request: ProtocolMethod) -> AnyPublisher { - return responsePublisher - .filter { $0.request.method == request.method } - .compactMap { (_, _, rpcResponse) in - guard let id = rpcResponse.id, let error = rpcResponse.error else { return nil } - return ResponseSubscriptionErrorPayload(id: id, error: error) - } - .eraseToAnyPublisher() - } - ->>>>>>> 7cb497caa4e4587bb940bd35ece55b709f14aac5:Sources/WalletConnectNetworking/NetworkInteractor.swift - public func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { - try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) - let message = try! serializer.serialize(topic: topic, encodable: request, envelopeType: envelopeType) - try await relayClient.publish(topic: topic, payload: message, tag: tag) - } - - /// Completes with an acknowledgement from the relay network. - /// completes with error if networking client was not able to send a message - /// TODO - relay client should provide async function - continualion should be removed from here - public func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws { - do { - try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) - let message = try serializer.serialize(topic: topic, encodable: request) - return try await withCheckedThrowingContinuation { continuation in - relayClient.publish(topic: topic, payload: message, tag: tag) { error in - if let error = error { - continuation.resume(throwing: error) - } else { - continuation.resume() - } - } - } - } catch { - logger.error(error) - } - } - - public func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { - try rpcHistory.resolve(response) - let message = try! serializer.serialize(topic: topic, encodable: response, envelopeType: envelopeType) - try await relayClient.publish(topic: topic, payload: message, tag: tag) - } - - public func respondSuccess(topic: String, requestId: RPCID, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { - let response = RPCResponse(id: requestId, result: true) - try await respond(topic: topic, response: response, tag: tag, envelopeType: envelopeType) - } - - public func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { - let error = JSONRPCError(code: reason.code, message: reason.message) - let response = RPCResponse(id: requestId, error: error) - try await respond(topic: topic, response: response, tag: tag, envelopeType: envelopeType) - } - - private func manageSubscription(_ topic: String, _ encodedEnvelope: String) { - if let deserializedJsonRpcRequest: RPCRequest = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { - handleRequest(topic: topic, request: deserializedJsonRpcRequest) - } else if let response: RPCResponse = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) { - handleResponse(response: response) - } else { - logger.debug("Networking Interactor - Received unknown object type from networking relay") - } - } - - private func handleRequest(topic: String, request: RPCRequest) { - do { - try rpcHistory.set(request, forTopic: topic, emmitedBy: .remote) - requestPublisherSubject.send((topic, request)) - } catch { - logger.debug(error) - } - } - - private func handleResponse(response: RPCResponse) { - do { - try rpcHistory.resolve(response) - let record = rpcHistory.get(recordId: response.id!)! - responsePublisherSubject.send((record.topic, record.request, response)) - } catch { - logger.debug("Handle json rpc response error: \(error)") - } - } -} From 3f9b42b3a0099ccf6edb9f9c61e5bd700e969449 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Mon, 5 Sep 2022 13:34:52 +0200 Subject: [PATCH 48/92] fix tags --- ...tocolMethods.swift => AuthProtocolMethod.swift} | 7 ++++++- Sources/Auth/Services/App/AppRequestService.swift | 2 +- .../Services/Wallet/WalletRequestSubscriber.swift | 4 ++-- .../Services/Wallet/WalletRespondService.swift | 4 ++-- Sources/Auth/Types/AuthProtocolMethod.swift | 14 -------------- .../ProtocolServices/Common/MessagingService.swift | 4 ++-- .../Invitee/InvitationHandlingService.swift | 4 ++-- .../ProtocolServices/Inviter/InviteService.swift | 2 +- Sources/Chat/Types/ChatProtocolMethod.swift | 13 +++++++++++-- .../WalletConnectNetworking/ProtocolMethod.swift | 3 ++- 10 files changed, 29 insertions(+), 28 deletions(-) rename Sources/Auth/{AuthProtocolMethods.swift => AuthProtocolMethod.swift} (80%) delete mode 100644 Sources/Auth/Types/AuthProtocolMethod.swift diff --git a/Sources/Auth/AuthProtocolMethods.swift b/Sources/Auth/AuthProtocolMethod.swift similarity index 80% rename from Sources/Auth/AuthProtocolMethods.swift rename to Sources/Auth/AuthProtocolMethod.swift index b4ce15058..c3df72dae 100644 --- a/Sources/Auth/AuthProtocolMethods.swift +++ b/Sources/Auth/AuthProtocolMethod.swift @@ -1,10 +1,15 @@ import Foundation +import WalletConnectNetworking -enum AuthProtocolMethods: String { +enum AuthProtocolMethod: String, ProtocolMethod { case authRequest = "wc_authRequest" case pairingDelete = "wc_pairingDelete" case pairingPing = "wc_pairingPing" + var method: String { + return self.rawValue + } + var requestTag: Int { switch self { case .authRequest: diff --git a/Sources/Auth/Services/App/AppRequestService.swift b/Sources/Auth/Services/App/AppRequestService.swift index e1c988eac..da88c993a 100644 --- a/Sources/Auth/Services/App/AppRequestService.swift +++ b/Sources/Auth/Services/App/AppRequestService.swift @@ -30,7 +30,7 @@ actor AppRequestService { let request = RPCRequest(method: "wc_authRequest", params: params) try kms.setPublicKey(publicKey: pubKey, for: responseTopic) logger.debug("AppRequestService: Subscribibg for response topic: \(responseTopic)") - try await networkingInteractor.requestNetworkAck(request, topic: topic, tag: AuthProtocolMethod.request.tag) + try await networkingInteractor.requestNetworkAck(request, topic: topic, tag: AuthProtocolMethod.authRequest.responseTag) try await networkingInteractor.subscribe(topic: responseTopic) } } diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift index 41d74d8a7..660b88a72 100644 --- a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift +++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift @@ -30,7 +30,7 @@ class WalletRequestSubscriber { private func subscribeForRequest() { guard let address = address else { return } - networkingInteractor.requestSubscription(on: AuthProtocolMethod.request) + networkingInteractor.requestSubscription(on: AuthProtocolMethod.authRequest) .sink { [unowned self] (payload: RequestSubscriptionPayload) in logger.debug("WalletRequestSubscriber: Received request") let message = messageFormatter.formatMessage(from: payload.request.payloadParams, address: address) @@ -42,7 +42,7 @@ class WalletRequestSubscriber { guard let pubKey = kms.getAgreementSecret(for: topic)?.publicKey else { return logger.error("Agreement key for topic \(topic) not found") } - let tag = AuthProtocolMethod.request.tag + let tag = AuthProtocolMethod.authRequest.responseTag let envelopeType = Envelope.EnvelopeType.type1(pubKey: pubKey.rawRepresentation) Task(priority: .high) { diff --git a/Sources/Auth/Services/Wallet/WalletRespondService.swift b/Sources/Auth/Services/Wallet/WalletRespondService.swift index ae87580f4..2f3ddad63 100644 --- a/Sources/Auth/Services/Wallet/WalletRespondService.swift +++ b/Sources/Auth/Services/Wallet/WalletRespondService.swift @@ -33,7 +33,7 @@ actor WalletRespondService { let didpkh = DIDPKH(account: account) let cacao = CacaoFormatter().format(authRequestParams, signature, didpkh) let response = RPCResponse(id: requestId, result: cacao) - try await networkingInteractor.respond(topic: topic, response: response, tag: AuthProtocolMethod.request.tag, envelopeType: .type1(pubKey: keys.publicKey.rawRepresentation)) + try await networkingInteractor.respond(topic: topic, response: response, tag: AuthProtocolMethod.authRequest.responseTag, envelopeType: .type1(pubKey: keys.publicKey.rawRepresentation)) } func respondError(requestId: RPCID) async throws { @@ -42,7 +42,7 @@ actor WalletRespondService { try kms.setAgreementSecret(keys, topic: topic) - let tag = AuthProtocolMethod.request.tag + let tag = AuthProtocolMethod.authRequest.responseTag let error = AuthError.userRejeted let envelopeType = Envelope.EnvelopeType.type1(pubKey: keys.publicKey.rawRepresentation) try await networkingInteractor.respondError(topic: topic, requestId: requestId, tag: tag, reason: error, envelopeType: envelopeType) diff --git a/Sources/Auth/Types/AuthProtocolMethod.swift b/Sources/Auth/Types/AuthProtocolMethod.swift deleted file mode 100644 index 37ff013d3..000000000 --- a/Sources/Auth/Types/AuthProtocolMethod.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation -import WalletConnectNetworking - -enum AuthProtocolMethod: ProtocolMethod { - case request - - var method: String { - return "wc_authRequest" - } - - var tag: Int { - return 3001 - } -} diff --git a/Sources/Chat/ProtocolServices/Common/MessagingService.swift b/Sources/Chat/ProtocolServices/Common/MessagingService.swift index d9fc9e772..20edfb6b5 100644 --- a/Sources/Chat/ProtocolServices/Common/MessagingService.swift +++ b/Sources/Chat/ProtocolServices/Common/MessagingService.swift @@ -34,7 +34,7 @@ class MessagingService { let timestamp = Int64(Date().timeIntervalSince1970 * 1000) let message = Message(topic: topic, message: messageString, authorAccount: authorAccount, timestamp: timestamp) let request = RPCRequest(method: ChatProtocolMethod.message.method, params: message) - try await networkingInteractor.request(request, topic: topic, tag: ChatProtocolMethod.message.tag) + try await networkingInteractor.request(request, topic: topic, tag: ChatProtocolMethod.message.requestTag) Task(priority: .background) { await messagesStore.add(message) onMessage?(message) @@ -59,7 +59,7 @@ class MessagingService { private func handleMessage(_ message: Message, topic: String, requestId: RPCID) { Task(priority: .background) { - try await networkingInteractor.respondSuccess(topic: topic, requestId: requestId, tag: ChatProtocolMethod.message.tag) + try await networkingInteractor.respondSuccess(topic: topic, requestId: requestId, tag: ChatProtocolMethod.message.responseTag) await messagesStore.add(message) logger.debug("Received message") onMessage?(message) diff --git a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift index 85509e248..c6dd9aede 100644 --- a/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift +++ b/Sources/Chat/ProtocolServices/Invitee/InvitationHandlingService.swift @@ -47,7 +47,7 @@ class InvitationHandlingService { let response = RPCResponse(id: payload.id, result: inviteResponse) let responseTopic = try getInviteResponseTopic(requestTopic: payload.topic, invite: payload.request) - try await networkingInteractor.respond(topic: responseTopic, response: response, tag: ChatProtocolMethod.invite.tag) + try await networkingInteractor.respond(topic: responseTopic, response: response, tag: ChatProtocolMethod.invite.responseTag) let threadAgreementKeys = try kms.performKeyAgreement(selfPublicKey: selfThreadPubKey, peerPublicKey: payload.request.publicKey) let threadTopic = threadAgreementKeys.derivedTopic() @@ -71,7 +71,7 @@ class InvitationHandlingService { let responseTopic = try getInviteResponseTopic(requestTopic: payload.topic, invite: payload.request) - try await networkingInteractor.respondError(topic: responseTopic, requestId: payload.id, tag: ChatProtocolMethod.invite.tag, reason: ChatError.userRejected) + try await networkingInteractor.respondError(topic: responseTopic, requestId: payload.id, tag: ChatProtocolMethod.invite.responseTag, reason: ChatError.userRejected) invitePayloadStore.delete(forKey: inviteId) } diff --git a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift index d04308f5e..514138042 100644 --- a/Sources/Chat/ProtocolServices/Inviter/InviteService.swift +++ b/Sources/Chat/ProtocolServices/Inviter/InviteService.swift @@ -50,7 +50,7 @@ class InviteService { try kms.setSymmetricKey(symKeyI.sharedKey, for: responseTopic) try await networkingInteractor.subscribe(topic: responseTopic) - try await networkingInteractor.request(request, topic: inviteTopic, tag: ChatProtocolMethod.invite.tag, envelopeType: .type1(pubKey: selfPubKeyY.rawRepresentation)) + try await networkingInteractor.request(request, topic: inviteTopic, tag: ChatProtocolMethod.invite.requestTag, envelopeType: .type1(pubKey: selfPubKeyY.rawRepresentation)) logger.debug("invite sent on topic: \(inviteTopic)") } diff --git a/Sources/Chat/Types/ChatProtocolMethod.swift b/Sources/Chat/Types/ChatProtocolMethod.swift index 1566f375c..a32e5d4bf 100644 --- a/Sources/Chat/Types/ChatProtocolMethod.swift +++ b/Sources/Chat/Types/ChatProtocolMethod.swift @@ -5,14 +5,23 @@ enum ChatProtocolMethod: ProtocolMethod { case invite case message - var tag: Int { + var requestTag: Int { switch self { case .invite: - return 2002 + return 2000 case .message: return 2002 } } + + var responseTag: Int { + switch self { + case .invite: + return 2001 + case .message: + return 2003 + } + } var method: String { switch self { diff --git a/Sources/WalletConnectNetworking/ProtocolMethod.swift b/Sources/WalletConnectNetworking/ProtocolMethod.swift index 1809ad6ba..dea8bd255 100644 --- a/Sources/WalletConnectNetworking/ProtocolMethod.swift +++ b/Sources/WalletConnectNetworking/ProtocolMethod.swift @@ -2,5 +2,6 @@ import Foundation public protocol ProtocolMethod { var method: String { get } - var tag: Int { get } + var requestTag: Int { get } + var responseTag: Int { get } } From 8a7e004a2e024e2a2667aa0e53fe9dd733f53fbe Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Mon, 5 Sep 2022 13:35:31 +0200 Subject: [PATCH 49/92] update --- Sources/Auth/Services/App/AppRespondSubscriber.swift | 4 ++-- Sources/Auth/Services/Common/DeletePairingService.swift | 4 ++-- Tests/AuthTests/WalletRequestSubscriberTests.swift | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Auth/Services/App/AppRespondSubscriber.swift b/Sources/Auth/Services/App/AppRespondSubscriber.swift index 61477b712..5c59d1128 100644 --- a/Sources/Auth/Services/App/AppRespondSubscriber.swift +++ b/Sources/Auth/Services/App/AppRespondSubscriber.swift @@ -32,13 +32,13 @@ class AppRespondSubscriber { } private func subscribeForResponse() { - networkingInteractor.responseErrorSubscription(on: AuthProtocolMethod.request) + networkingInteractor.responseErrorSubscription(on: AuthProtocolMethod.authRequest) .sink { [unowned self] payload in guard let error = AuthError(code: payload.error.code) else { return } onResponse?(payload.id, .failure(error)) }.store(in: &publishers) - networkingInteractor.responseSubscription(on: AuthProtocolMethod.request) + networkingInteractor.responseSubscription(on: AuthProtocolMethod.authRequest) .sink { [unowned self] (payload: ResponseSubscriptionPayload) in activatePairingIfNeeded(id: payload.id) diff --git a/Sources/Auth/Services/Common/DeletePairingService.swift b/Sources/Auth/Services/Common/DeletePairingService.swift index 0b0cc4919..721bdc81f 100644 --- a/Sources/Auth/Services/Common/DeletePairingService.swift +++ b/Sources/Auth/Services/Common/DeletePairingService.swift @@ -28,8 +28,8 @@ class DeletePairingService { guard pairingStorage.hasPairing(forTopic: topic) else { throw Errors.pairingNotFound} let reason = AuthError.userDisconnected logger.debug("Will delete pairing for reason: message: \(reason.message) code: \(reason.code)") - let request = RPCRequest(method: AuthProtocolMethods.pairingDelete.rawValue, params: reason) - try await networkingInteractor.request(request, topic: topic, tag: AuthProtocolMethods.pairingDelete.requestTag) + let request = RPCRequest(method: AuthProtocolMethod.pairingDelete.rawValue, params: reason) + try await networkingInteractor.request(request, topic: topic, tag: AuthProtocolMethod.pairingDelete.requestTag) pairingStorage.delete(topic: topic) kms.deleteSymmetricKey(for: topic) networkingInteractor.unsubscribe(topic: topic) diff --git a/Tests/AuthTests/WalletRequestSubscriberTests.swift b/Tests/AuthTests/WalletRequestSubscriberTests.swift index 5a7f94649..4a2062f61 100644 --- a/Tests/AuthTests/WalletRequestSubscriberTests.swift +++ b/Tests/AuthTests/WalletRequestSubscriberTests.swift @@ -35,7 +35,7 @@ class WalletRequestSubscriberTests: XCTestCase { messageExpectation.fulfill() } - let request = RPCRequest(method: AuthProtocolMethod.request.method, params: AuthRequestParams.stub(id: expectedRequestId), id: expectedRequestId.right!) + let request = RPCRequest(method: AuthProtocolMethod.authRequest.method, params: AuthRequestParams.stub(id: expectedRequestId), id: expectedRequestId.right!) networkingInteractor.requestPublisherSubject.send(("123", request)) wait(for: [messageExpectation], timeout: defaultTimeout) From fd82eec7b757edf3d45b1ff3b8f203a00e5125d2 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Mon, 5 Sep 2022 10:58:42 +0200 Subject: [PATCH 50/92] savepoint --- Sources/Auth/Services/Common/PingService.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 Sources/Auth/Services/Common/PingService.swift diff --git a/Sources/Auth/Services/Common/PingService.swift b/Sources/Auth/Services/Common/PingService.swift new file mode 100644 index 000000000..a18e707d4 --- /dev/null +++ b/Sources/Auth/Services/Common/PingService.swift @@ -0,0 +1,13 @@ +import Foundation +import WalletConnectPairing +import JSONRPC + +class PingService { + private let pairingStorage: WCPairingStorage + private let networkingInteractor: NetworkInteracting + + func ping(topic: String, completion: @escaping (Result) -> Void) { + guard pairingStorage.hasSession(forTopic: topic) else { return } + let request = RPCRequest( + } +} From fe5222d678e22adb6bb0f17ad82dc6d345683c23 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Mon, 5 Sep 2022 11:28:48 +0200 Subject: [PATCH 51/92] update ping service --- Sources/Auth/Services/Common/PingService.swift | 14 +++++++++++--- .../ProtocolRPCParams/PairingPingParams.swift | 5 +++++ 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 Sources/Auth/Types/ProtocolRPCParams/PairingPingParams.swift diff --git a/Sources/Auth/Services/Common/PingService.swift b/Sources/Auth/Services/Common/PingService.swift index a18e707d4..5c48b648f 100644 --- a/Sources/Auth/Services/Common/PingService.swift +++ b/Sources/Auth/Services/Common/PingService.swift @@ -1,13 +1,21 @@ import Foundation import WalletConnectPairing import JSONRPC +import WalletConnectNetworking class PingService { private let pairingStorage: WCPairingStorage private let networkingInteractor: NetworkInteracting - func ping(topic: String, completion: @escaping (Result) -> Void) { - guard pairingStorage.hasSession(forTopic: topic) else { return } - let request = RPCRequest( + init(pairingStorage: WCPairingStorage, networkingInteractor: NetworkInteracting) { + self.pairingStorage = pairingStorage + self.networkingInteractor = networkingInteractor + } + + func ping(topic: String) async throws { + guard pairingStorage.hasPairing(forTopic: topic) else { return } + let request = RPCRequest(method: AuthProtocolMethods.pairingPing.rawValue, params: PairingPingParams()) + try await networkingInteractor.request(request, topic: topic, tag: AuthProtocolMethods.pairingDelete.requestTag) + //todo return a publisher? } } diff --git a/Sources/Auth/Types/ProtocolRPCParams/PairingPingParams.swift b/Sources/Auth/Types/ProtocolRPCParams/PairingPingParams.swift new file mode 100644 index 000000000..9ed7828d7 --- /dev/null +++ b/Sources/Auth/Types/ProtocolRPCParams/PairingPingParams.swift @@ -0,0 +1,5 @@ +import Foundation + +struct PairingPingParams: Codable { + +} From e5028517d47e68f50f02d0a6516147f00df8c06b Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 6 Sep 2022 09:03:29 +0200 Subject: [PATCH 52/92] Add PingResponder --- Package.swift | 2 +- .../Auth/Services/Common/PingService.swift | 21 -------- .../PairingProtocolMethod.swift | 18 +++++++ .../WalletConnectPairing/PingService.swift | 51 +++++++++++++++++++ .../RPCParams}/PairingPingParams.swift | 0 5 files changed, 70 insertions(+), 22 deletions(-) delete mode 100644 Sources/Auth/Services/Common/PingService.swift create mode 100644 Sources/WalletConnectPairing/PairingProtocolMethod.swift create mode 100644 Sources/WalletConnectPairing/PingService.swift rename Sources/{Auth/Types/ProtocolRPCParams => WalletConnectPairing/RPCParams}/PairingPingParams.swift (100%) diff --git a/Package.swift b/Package.swift index 893eb9338..7640db6ad 100644 --- a/Package.swift +++ b/Package.swift @@ -52,7 +52,7 @@ let package = Package( path: "Sources/WalletConnectKMS"), .target( name: "WalletConnectPairing", - dependencies: ["WalletConnectUtils"]), + dependencies: ["WalletConnectUtils", "WalletConnectNetworking", "JSONRPC"]), .target( name: "WalletConnectUtils", dependencies: ["Commons", "JSONRPC"]), diff --git a/Sources/Auth/Services/Common/PingService.swift b/Sources/Auth/Services/Common/PingService.swift deleted file mode 100644 index 5c48b648f..000000000 --- a/Sources/Auth/Services/Common/PingService.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation -import WalletConnectPairing -import JSONRPC -import WalletConnectNetworking - -class PingService { - private let pairingStorage: WCPairingStorage - private let networkingInteractor: NetworkInteracting - - init(pairingStorage: WCPairingStorage, networkingInteractor: NetworkInteracting) { - self.pairingStorage = pairingStorage - self.networkingInteractor = networkingInteractor - } - - func ping(topic: String) async throws { - guard pairingStorage.hasPairing(forTopic: topic) else { return } - let request = RPCRequest(method: AuthProtocolMethods.pairingPing.rawValue, params: PairingPingParams()) - try await networkingInteractor.request(request, topic: topic, tag: AuthProtocolMethods.pairingDelete.requestTag) - //todo return a publisher? - } -} diff --git a/Sources/WalletConnectPairing/PairingProtocolMethod.swift b/Sources/WalletConnectPairing/PairingProtocolMethod.swift new file mode 100644 index 000000000..a6f9d14cf --- /dev/null +++ b/Sources/WalletConnectPairing/PairingProtocolMethod.swift @@ -0,0 +1,18 @@ +import Foundation +import WalletConnectNetworking + +enum PairingProtocolMethod: String, ProtocolMethod { + case ping = "wc_pairingPing" + + var method: String { + return self.rawValue + } + + var requestTag: Int { + return 1002 + } + + var responseTag: Int { + return 1003 + } +} diff --git a/Sources/WalletConnectPairing/PingService.swift b/Sources/WalletConnectPairing/PingService.swift new file mode 100644 index 000000000..6784e1665 --- /dev/null +++ b/Sources/WalletConnectPairing/PingService.swift @@ -0,0 +1,51 @@ +import Foundation +import WalletConnectNetworking +import JSONRPC + +class PingService { + private let pairingStorage: WCPairingStorage + private let networkingInteractor: NetworkInteracting + + init(pairingStorage: WCPairingStorage, networkingInteractor: NetworkInteracting) { + self.pairingStorage = pairingStorage + self.networkingInteractor = networkingInteractor + } + + func ping(topic: String) async throws { + guard pairingStorage.hasPairing(forTopic: topic) else { return } + let request = RPCRequest(method: PairingProtocolMethod.ping.rawValue, params: PairingPingParams()) + try await networkingInteractor.request(request, topic: topic, tag: PairingProtocolMethod.ping.requestTag) + } +} + + +import Combine +import WalletConnectUtils + +class PingResponder { + private let networkingInteractor: NetworkInteracting + private let logger: ConsoleLogging + private var publishers = [AnyCancellable]() + + init(networkingInteractor: NetworkInteracting, + logger: ConsoleLogging) { + self.networkingInteractor = networkingInteractor + self.logger = logger + subscribePingRequests() + } + + func subscribePingRequests() { + networkingInteractor.requestSubscription(on: PairingProtocolMethod.ping) + .sink { [unowned self] (payload: RequestSubscriptionPayload) in + logger.debug("Responding for pairing ping") + Task { + try? await networkingInteractor.respondSuccess(topic: payload.topic, requestId: payload.id, tag: PairingProtocolMethod.ping.responseTag) + } + } + .store(in: &publishers) + } +} + +class PingResponseSubscriber { + +} diff --git a/Sources/Auth/Types/ProtocolRPCParams/PairingPingParams.swift b/Sources/WalletConnectPairing/RPCParams/PairingPingParams.swift similarity index 100% rename from Sources/Auth/Types/ProtocolRPCParams/PairingPingParams.swift rename to Sources/WalletConnectPairing/RPCParams/PairingPingParams.swift From 02a8d65b4968938161ee74bbc736719f88dd3647 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 6 Sep 2022 09:33:47 +0200 Subject: [PATCH 53/92] Add ping response subscriber --- .../PingResponder.swift} | 28 ++----------------- .../Services/PingResponseSubscriber.swift | 26 +++++++++++++++++ .../Services/PingService.swift | 19 +++++++++++++ 3 files changed, 47 insertions(+), 26 deletions(-) rename Sources/WalletConnectPairing/{PingService.swift => Services/PingResponder.swift} (54%) create mode 100644 Sources/WalletConnectPairing/Services/PingResponseSubscriber.swift create mode 100644 Sources/WalletConnectPairing/Services/PingService.swift diff --git a/Sources/WalletConnectPairing/PingService.swift b/Sources/WalletConnectPairing/Services/PingResponder.swift similarity index 54% rename from Sources/WalletConnectPairing/PingService.swift rename to Sources/WalletConnectPairing/Services/PingResponder.swift index 6784e1665..412a4d805 100644 --- a/Sources/WalletConnectPairing/PingService.swift +++ b/Sources/WalletConnectPairing/Services/PingResponder.swift @@ -1,26 +1,6 @@ -import Foundation -import WalletConnectNetworking -import JSONRPC - -class PingService { - private let pairingStorage: WCPairingStorage - private let networkingInteractor: NetworkInteracting - - init(pairingStorage: WCPairingStorage, networkingInteractor: NetworkInteracting) { - self.pairingStorage = pairingStorage - self.networkingInteractor = networkingInteractor - } - - func ping(topic: String) async throws { - guard pairingStorage.hasPairing(forTopic: topic) else { return } - let request = RPCRequest(method: PairingProtocolMethod.ping.rawValue, params: PairingPingParams()) - try await networkingInteractor.request(request, topic: topic, tag: PairingProtocolMethod.ping.requestTag) - } -} - - import Combine import WalletConnectUtils +import WalletConnectNetworking class PingResponder { private let networkingInteractor: NetworkInteracting @@ -34,7 +14,7 @@ class PingResponder { subscribePingRequests() } - func subscribePingRequests() { + private func subscribePingRequests() { networkingInteractor.requestSubscription(on: PairingProtocolMethod.ping) .sink { [unowned self] (payload: RequestSubscriptionPayload) in logger.debug("Responding for pairing ping") @@ -45,7 +25,3 @@ class PingResponder { .store(in: &publishers) } } - -class PingResponseSubscriber { - -} diff --git a/Sources/WalletConnectPairing/Services/PingResponseSubscriber.swift b/Sources/WalletConnectPairing/Services/PingResponseSubscriber.swift new file mode 100644 index 000000000..d2ce74a89 --- /dev/null +++ b/Sources/WalletConnectPairing/Services/PingResponseSubscriber.swift @@ -0,0 +1,26 @@ +import Combine +import WalletConnectUtils +import WalletConnectNetworking + +class PingResponseSubscriber { + private let networkingInteractor: NetworkInteracting + private let logger: ConsoleLogging + private var publishers = [AnyCancellable]() + + var onResponse: ((String)->())? + + init(networkingInteractor: NetworkInteracting, + logger: ConsoleLogging) { + self.networkingInteractor = networkingInteractor + self.logger = logger + subscribePingResponses() + } + + private func subscribePingResponses() { + networkingInteractor.responseSubscription(on: PairingProtocolMethod.ping) + .sink { [unowned self] (payload: ResponseSubscriptionPayload) in + onResponse?(payload.topic) + } + .store(in: &publishers) + } +} diff --git a/Sources/WalletConnectPairing/Services/PingService.swift b/Sources/WalletConnectPairing/Services/PingService.swift new file mode 100644 index 000000000..ad64eecfa --- /dev/null +++ b/Sources/WalletConnectPairing/Services/PingService.swift @@ -0,0 +1,19 @@ +import Foundation +import WalletConnectNetworking +import JSONRPC + +class PingService { + private let pairingStorage: WCPairingStorage + private let networkingInteractor: NetworkInteracting + + init(pairingStorage: WCPairingStorage, networkingInteractor: NetworkInteracting) { + self.pairingStorage = pairingStorage + self.networkingInteractor = networkingInteractor + } + + func ping(topic: String) async throws { + guard pairingStorage.hasPairing(forTopic: topic) else { return } + let request = RPCRequest(method: PairingProtocolMethod.ping.rawValue, params: PairingPingParams()) + try await networkingInteractor.request(request, topic: topic, tag: PairingProtocolMethod.ping.requestTag) + } +} From 4e2d546c3b356d0d965e4ba497e9c1c398d3738a Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 6 Sep 2022 10:13:21 +0200 Subject: [PATCH 54/92] rename ping service --- .../Services/{PingService.swift => PingRequester.swift} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename Sources/WalletConnectPairing/Services/{PingService.swift => PingRequester.swift} (97%) diff --git a/Sources/WalletConnectPairing/Services/PingService.swift b/Sources/WalletConnectPairing/Services/PingRequester.swift similarity index 97% rename from Sources/WalletConnectPairing/Services/PingService.swift rename to Sources/WalletConnectPairing/Services/PingRequester.swift index ad64eecfa..f936a8c9c 100644 --- a/Sources/WalletConnectPairing/Services/PingService.swift +++ b/Sources/WalletConnectPairing/Services/PingRequester.swift @@ -2,7 +2,7 @@ import Foundation import WalletConnectNetworking import JSONRPC -class PingService { +class PingRequester { private let pairingStorage: WCPairingStorage private let networkingInteractor: NetworkInteracting From 3aeae186b26377326b5af3d22f0eb737f7eee75d Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 6 Sep 2022 10:27:15 +0200 Subject: [PATCH 55/92] Add Pairing Ping Service consume pairing ping service by Auth client --- Sources/Auth/AuthClient.swift | 14 +++++++++- Sources/Auth/AuthClientFactory.swift | 4 ++- .../Services/PairingPingService.swift | 27 +++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 Sources/WalletConnectPairing/Services/PairingPingService.swift diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index 41ab8d3ad..aeb4fc40b 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -34,6 +34,10 @@ public class AuthClient { authResponsePublisherSubject.eraseToAnyPublisher() } + public var pingResponsePublisher: AnyPublisher<(String), Never> { + pingResponsePublisherSubject.eraseToAnyPublisher() + } + /// Publisher that sends web socket connection status public let socketConnectionStatusPublisher: AnyPublisher @@ -44,6 +48,7 @@ public class AuthClient { private var authResponsePublisherSubject = PassthroughSubject<(id: RPCID, result: Result), Never>() private var authRequestPublisherSubject = PassthroughSubject() + private var pingResponsePublisherSubject = PassthroughSubject() private let appPairService: AppPairService private let appRequestService: AppRequestService private let deletePairingService: DeletePairingService @@ -54,6 +59,7 @@ public class AuthClient { private let cleanupService: CleanupService private let pairingStorage: WCPairingStorage private let pendingRequestsProvider: PendingRequestsProvider + private let pingService: PairingPingService private var account: Account? init(appPairService: AppPairService, @@ -68,7 +74,8 @@ public class AuthClient { cleanupService: CleanupService, logger: ConsoleLogging, pairingStorage: WCPairingStorage, - socketConnectionStatusPublisher: AnyPublisher + socketConnectionStatusPublisher: AnyPublisher, + pingService: PairingPingService ) { self.appPairService = appPairService self.appRequestService = appRequestService @@ -83,6 +90,7 @@ public class AuthClient { self.pairingStorage = pairingStorage self.socketConnectionStatusPublisher = socketConnectionStatusPublisher self.deletePairingService = deletePairingService + self.pingService = pingService setUpPublishers() } @@ -141,6 +149,10 @@ public class AuthClient { try await deletePairingService.delete(topic: topic) } + public func ping(topic: String) async throws { + try await pingService.ping(topic: topic) + } + /// Query pending authentication requests /// - Returns: Pending authentication requests public func getPendingRequests() throws -> [AuthRequest] { diff --git a/Sources/Auth/AuthClientFactory.swift b/Sources/Auth/AuthClientFactory.swift index 160489b69..202f10895 100644 --- a/Sources/Auth/AuthClientFactory.swift +++ b/Sources/Auth/AuthClientFactory.swift @@ -32,6 +32,7 @@ public struct AuthClientFactory { let pendingRequestsProvider = PendingRequestsProvider(rpcHistory: history) let cleanupService = CleanupService(pairingStore: pairingStore, kms: kms) let deletePairingService = DeletePairingService(networkingInteractor: networkingInteractor, kms: kms, pairingStorage: pairingStore, logger: logger) + let pingService = PairingPingService(pairingStorage: pairingStore, networkingInteractor: networkingInteractor, logger: logger) return AuthClient(appPairService: appPairService, appRequestService: appRequestService, @@ -44,6 +45,7 @@ public struct AuthClientFactory { cleanupService: cleanupService, logger: logger, pairingStorage: pairingStore, - socketConnectionStatusPublisher: relayClient.socketConnectionStatusPublisher) + socketConnectionStatusPublisher: relayClient.socketConnectionStatusPublisher, + pingService: pingService) } } diff --git a/Sources/WalletConnectPairing/Services/PairingPingService.swift b/Sources/WalletConnectPairing/Services/PairingPingService.swift new file mode 100644 index 000000000..a59e1a8f0 --- /dev/null +++ b/Sources/WalletConnectPairing/Services/PairingPingService.swift @@ -0,0 +1,27 @@ +import WalletConnectUtils +import Foundation +import WalletConnectNetworking + +public class PairingPingService { + private let pingRequester: PingRequester + private let pingResponder: PingResponder + private let pingResponseSubscriber: PingResponseSubscriber + + var onResponse: ((String)->())? { + return pingResponseSubscriber.onResponse + } + + public init( + pairingStorage: WCPairingStorage, + networkingInteractor: NetworkInteracting, + logger: ConsoleLogging) { + pingRequester = PingRequester(pairingStorage: pairingStorage, networkingInteractor: networkingInteractor) + pingResponder = PingResponder(networkingInteractor: networkingInteractor, logger: logger) + pingResponseSubscriber = PingResponseSubscriber(networkingInteractor: networkingInteractor, logger: logger) + } + + public func ping(topic: String) async throws { + try await pingRequester.ping(topic: topic) + } + +} From 6b4d03e6913aee15a6cae46e192cfdcad84a7fb9 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 6 Sep 2022 11:05:38 +0200 Subject: [PATCH 56/92] Auth - Add ping integration test --- Example/IntegrationTests/Auth/AuthTests.swift | 14 ++++++++++++++ Sources/Auth/AuthClient.swift | 4 ++++ .../Services/PairingPingService.swift | 9 +++++++-- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Example/IntegrationTests/Auth/AuthTests.swift b/Example/IntegrationTests/Auth/AuthTests.swift index 43d56d372..92f16f2e6 100644 --- a/Example/IntegrationTests/Auth/AuthTests.swift +++ b/Example/IntegrationTests/Auth/AuthTests.swift @@ -119,4 +119,18 @@ final class AuthTests: XCTestCase { .store(in: &publishers) wait(for: [responseExpectation], timeout: 2) } + + func testPing() async { + let pingExpectation = expectation(description: "expects ping response") + let uri = try! await app.request(RequestParams.stub()) + try! await wallet.pair(uri: uri) + try! await wallet.ping(topic: uri.topic) + wallet.pingResponsePublisher + .sink { topic in + XCTAssertEqual(topic, uri.topic) + pingExpectation.fulfill() + } + .store(in: &publishers) + wait(for: [pingExpectation], timeout: 3) + } } diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index aeb4fc40b..5e7e6ad08 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -177,5 +177,9 @@ public class AuthClient { walletRequestSubscriber.onRequest = { [unowned self] request in authRequestPublisherSubject.send(request) } + + pingService.onResponse = { [unowned self] topic in + pingResponsePublisherSubject.send(topic) + } } } diff --git a/Sources/WalletConnectPairing/Services/PairingPingService.swift b/Sources/WalletConnectPairing/Services/PairingPingService.swift index a59e1a8f0..6baef98e0 100644 --- a/Sources/WalletConnectPairing/Services/PairingPingService.swift +++ b/Sources/WalletConnectPairing/Services/PairingPingService.swift @@ -7,8 +7,13 @@ public class PairingPingService { private let pingResponder: PingResponder private let pingResponseSubscriber: PingResponseSubscriber - var onResponse: ((String)->())? { - return pingResponseSubscriber.onResponse + public var onResponse: ((String)->())? { + get { + return pingResponseSubscriber.onResponse + } + set { + pingResponseSubscriber.onResponse = newValue + } } public init( From aa5e41fb29a6a9d3aca70135be1dbdc08c6f6501 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 6 Sep 2022 13:53:20 +0200 Subject: [PATCH 57/92] extend timeout --- Example/IntegrationTests/Auth/AuthTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Example/IntegrationTests/Auth/AuthTests.swift b/Example/IntegrationTests/Auth/AuthTests.swift index 92f16f2e6..e5da74c07 100644 --- a/Example/IntegrationTests/Auth/AuthTests.swift +++ b/Example/IntegrationTests/Auth/AuthTests.swift @@ -131,6 +131,6 @@ final class AuthTests: XCTestCase { pingExpectation.fulfill() } .store(in: &publishers) - wait(for: [pingExpectation], timeout: 3) + wait(for: [pingExpectation], timeout: 5) } } From 6ddb47eb3add8b63bf5b9baa303e774b336a6c89 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 6 Sep 2022 14:08:57 +0200 Subject: [PATCH 58/92] test --- Sources/WalletConnectPairing/Services/PingResponder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WalletConnectPairing/Services/PingResponder.swift b/Sources/WalletConnectPairing/Services/PingResponder.swift index 412a4d805..9ed8c0806 100644 --- a/Sources/WalletConnectPairing/Services/PingResponder.swift +++ b/Sources/WalletConnectPairing/Services/PingResponder.swift @@ -18,7 +18,7 @@ class PingResponder { networkingInteractor.requestSubscription(on: PairingProtocolMethod.ping) .sink { [unowned self] (payload: RequestSubscriptionPayload) in logger.debug("Responding for pairing ping") - Task { + Task(priority: .high) { try? await networkingInteractor.respondSuccess(topic: payload.topic, requestId: payload.id, tag: PairingProtocolMethod.ping.responseTag) } } From 0c12ebdc144a6c869ee8712020e0025dc8d55d88 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Tue, 6 Sep 2022 15:19:55 +0300 Subject: [PATCH 59/92] Auth testing session --- .../Configurator/ThirdPartyConfigurator.swift | 2 +- Sources/Auth/AuthClientFactory.swift | 2 +- Sources/Auth/Services/Common/SIWEMessageFormatter.swift | 2 +- Sources/Auth/Types/AuthPayload.swift | 4 ++-- Sources/Auth/Types/Cacao/CacaoPayload.swift | 4 ++-- Sources/Auth/Types/ProtocolRPCParams/AuthRequestParams.swift | 3 ++- Sources/WalletConnectRelay/RelayClient.swift | 2 +- Sources/WalletConnectUtils/Encodable.swift | 4 +++- 8 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Example/Showcase/Classes/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift b/Example/Showcase/Classes/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift index b6596d99e..8aa2f8457 100644 --- a/Example/Showcase/Classes/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift +++ b/Example/Showcase/Classes/ApplicationLayer/Configurator/ThirdPartyConfigurator.swift @@ -14,7 +14,7 @@ struct ThirdPartyConfigurator: Configurator { url: "example.wallet", icons: ["https://avatars.githubusercontent.com/u/37784886"] ), - account: Account("eip155:56:0xe5EeF1368781911d265fDB6946613dA61915a501")! + account: Account("eip155:1:0xe5EeF1368781911d265fDB6946613dA61915a501")! ) } } diff --git a/Sources/Auth/AuthClientFactory.swift b/Sources/Auth/AuthClientFactory.swift index 160489b69..7b8c0a8fb 100644 --- a/Sources/Auth/AuthClientFactory.swift +++ b/Sources/Auth/AuthClientFactory.swift @@ -8,7 +8,7 @@ import WalletConnectNetworking public struct AuthClientFactory { public static func create(metadata: AppMetadata, account: Account?, relayClient: RelayClient) -> AuthClient { - let logger = ConsoleLogger(loggingLevel: .off) + let logger = ConsoleLogger(loggingLevel: .debug) let keyValueStorage = UserDefaults.standard let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") return AuthClientFactory.create(metadata: metadata, account: account, logger: logger, keyValueStorage: keyValueStorage, keychainStorage: keychainStorage, relayClient: relayClient) diff --git a/Sources/Auth/Services/Common/SIWEMessageFormatter.swift b/Sources/Auth/Services/Common/SIWEMessageFormatter.swift index 64f6a6ced..3dd2309de 100644 --- a/Sources/Auth/Services/Common/SIWEMessageFormatter.swift +++ b/Sources/Auth/Services/Common/SIWEMessageFormatter.swift @@ -48,7 +48,7 @@ private struct SIWEMessage: Equatable { let domain: String let uri: String // aud let address: String - let version: Int + let version: String let nonce: String let chainId: String let iat: String diff --git a/Sources/Auth/Types/AuthPayload.swift b/Sources/Auth/Types/AuthPayload.swift index 35a4b2338..1329f5265 100644 --- a/Sources/Auth/Types/AuthPayload.swift +++ b/Sources/Auth/Types/AuthPayload.swift @@ -3,7 +3,7 @@ import Foundation struct AuthPayload: Codable, Equatable { let domain: String let aud: String - let version: Int + let version: String let nonce: String let chainId: String let type: String @@ -19,7 +19,7 @@ struct AuthPayload: Codable, Equatable { self.chainId = requestParams.chainId self.domain = requestParams.domain self.aud = requestParams.aud - self.version = 1 + self.version = "1" self.nonce = requestParams.nonce self.iat = iat self.nbf = requestParams.nbf diff --git a/Sources/Auth/Types/Cacao/CacaoPayload.swift b/Sources/Auth/Types/Cacao/CacaoPayload.swift index 1a71050c7..41f66e736 100644 --- a/Sources/Auth/Types/Cacao/CacaoPayload.swift +++ b/Sources/Auth/Types/Cacao/CacaoPayload.swift @@ -4,7 +4,7 @@ struct CacaoPayload: Codable, Equatable { let iss: String let domain: String let aud: String - let version: Int + let version: String let nonce: String let iat: String let nbf: String? @@ -17,7 +17,7 @@ struct CacaoPayload: Codable, Equatable { self.iss = didpkh.iss self.domain = params.domain self.aud = params.aud - self.version = 1 + self.version = "1" self.nonce = params.nonce self.iat = params.iat self.nbf = params.nbf diff --git a/Sources/Auth/Types/ProtocolRPCParams/AuthRequestParams.swift b/Sources/Auth/Types/ProtocolRPCParams/AuthRequestParams.swift index 18d3f9328..d161e4522 100644 --- a/Sources/Auth/Types/ProtocolRPCParams/AuthRequestParams.swift +++ b/Sources/Auth/Types/ProtocolRPCParams/AuthRequestParams.swift @@ -4,7 +4,8 @@ import WalletConnectUtils /// wc_authRequest RPC method request param struct AuthRequestParams: Codable, Equatable { let requester: Requester - let payloadParams: AuthPayload} + let payloadParams: AuthPayload +} extension AuthRequestParams { struct Requester: Codable, Equatable { diff --git a/Sources/WalletConnectRelay/RelayClient.swift b/Sources/WalletConnectRelay/RelayClient.swift index b6aef67c0..90066deb6 100644 --- a/Sources/WalletConnectRelay/RelayClient.swift +++ b/Sources/WalletConnectRelay/RelayClient.swift @@ -87,7 +87,7 @@ public final class RelayClient { keychainStorage: KeychainStorageProtocol = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk"), socketFactory: WebSocketFactory, socketConnectionType: SocketConnectionType = .automatic, - logger: ConsoleLogging = ConsoleLogger(loggingLevel: .off) + logger: ConsoleLogging = ConsoleLogger(loggingLevel: .debug) ) { let socketAuthenticator = SocketAuthenticator( clientIdStorage: ClientIdStorage(keychain: keychainStorage), diff --git a/Sources/WalletConnectUtils/Encodable.swift b/Sources/WalletConnectUtils/Encodable.swift index 4d18c65b9..dbd436880 100644 --- a/Sources/WalletConnectUtils/Encodable.swift +++ b/Sources/WalletConnectUtils/Encodable.swift @@ -11,7 +11,9 @@ public extension Encodable { // TODO: Migrate func json() throws -> String { - let data = try JSONEncoder().encode(self) + let encoder = JSONEncoder() + encoder.outputFormatting = .withoutEscapingSlashes + let data = try encoder.encode(self) guard let string = String(data: data, encoding: .utf8) else { throw DataConversionError.dataToStringFailed } From 22b84e413f74c184e04ce1eda1ae290444845bf7 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Tue, 6 Sep 2022 15:28:17 +0200 Subject: [PATCH 60/92] savepoint --- Sources/Auth/Services/App/AppRespondSubscriber.swift | 6 +++--- Sources/Auth/Services/Common/CacaoFormatter.swift | 6 +++--- Sources/Auth/Services/Signer/MessageSigner.swift | 2 +- Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift | 1 + Sources/Auth/Services/Wallet/WalletRespondService.swift | 4 ++-- Sources/Auth/Types/Cacao/Cacao.swift | 6 +++--- Sources/WalletConnectNetworking/NetworkInteractor.swift | 2 +- 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Sources/Auth/Services/App/AppRespondSubscriber.swift b/Sources/Auth/Services/App/AppRespondSubscriber.swift index 5c59d1128..2b3b353fa 100644 --- a/Sources/Auth/Services/App/AppRespondSubscriber.swift +++ b/Sources/Auth/Services/App/AppRespondSubscriber.swift @@ -49,14 +49,14 @@ class AppRespondSubscriber { let requestPayload = payload.request guard - let address = try? DIDPKH(iss: cacao.payload.iss).account.address, - let message = try? messageFormatter.formatMessage(from: cacao.payload) + let address = try? DIDPKH(iss: cacao.p.iss).account.address, + let message = try? messageFormatter.formatMessage(from: cacao.p) else { self.onResponse?(requestId, .failure(.malformedResponseParams)); return } guard messageFormatter.formatMessage(from: requestPayload.payloadParams, address: address) == message else { self.onResponse?(requestId, .failure(.messageCompromised)); return } - guard let _ = try? signatureVerifier.verify(signature: cacao.signature, message: message, address: address) + guard let _ = try? signatureVerifier.verify(signature: cacao.s, message: message, address: address) else { self.onResponse?(requestId, .failure(.signatureVerificationFailed)); return } onResponse?(requestId, .success(cacao)) diff --git a/Sources/Auth/Services/Common/CacaoFormatter.swift b/Sources/Auth/Services/Common/CacaoFormatter.swift index b8331b577..471fb2ba9 100644 --- a/Sources/Auth/Services/Common/CacaoFormatter.swift +++ b/Sources/Auth/Services/Common/CacaoFormatter.swift @@ -2,13 +2,13 @@ import Foundation import WalletConnectUtils protocol CacaoFormatting { - func format(_ request: AuthRequestParams, _ signature: CacaoSignature, _ didpkh: DIDPKH) -> Cacao + func format(_ request: AuthRequestParams, _ signature: CacaoSignature, _ didpkh: DIDPKH) -> AuthResponseParams } class CacaoFormatter: CacaoFormatting { - func format(_ request: AuthRequestParams, _ signature: CacaoSignature, _ didpkh: DIDPKH) -> Cacao { + func format(_ request: AuthRequestParams, _ signature: CacaoSignature, _ didpkh: DIDPKH) -> AuthResponseParams { let header = CacaoHeader(t: "eip4361") let payload = CacaoPayload(params: request.payloadParams, didpkh: didpkh) - return Cacao(header: header, payload: payload, signature: signature) + return AuthResponseParams(header: header, payload: payload, signature: signature) } } diff --git a/Sources/Auth/Services/Signer/MessageSigner.swift b/Sources/Auth/Services/Signer/MessageSigner.swift index 94896343f..4facc7099 100644 --- a/Sources/Auth/Services/Signer/MessageSigner.swift +++ b/Sources/Auth/Services/Signer/MessageSigner.swift @@ -24,7 +24,7 @@ public struct MessageSigner: MessageSignatureVerifying, MessageSigning { public func sign(message: String, privateKey: Data) throws -> CacaoSignature { guard let messageData = message.data(using: .utf8) else { throw Errors.utf8EncodingFailed } let signature = try signer.sign(message: messageData, with: privateKey) - return CacaoSignature(t: "eip191", s: signature.toHexString()) + return CacaoSignature(t: "eip191", s: "0x"+signature.toHexString()) } public func verify(signature: CacaoSignature, message: String, address: String) throws { diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift index 660b88a72..5f43b1e6a 100644 --- a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift +++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift @@ -34,6 +34,7 @@ class WalletRequestSubscriber { .sink { [unowned self] (payload: RequestSubscriptionPayload) in logger.debug("WalletRequestSubscriber: Received request") let message = messageFormatter.formatMessage(from: payload.request.payloadParams, address: address) + print(message) onRequest?(.init(id: payload.id, message: message)) }.store(in: &publishers) } diff --git a/Sources/Auth/Services/Wallet/WalletRespondService.swift b/Sources/Auth/Services/Wallet/WalletRespondService.swift index 2f3ddad63..051fbbaa1 100644 --- a/Sources/Auth/Services/Wallet/WalletRespondService.swift +++ b/Sources/Auth/Services/Wallet/WalletRespondService.swift @@ -31,8 +31,8 @@ actor WalletRespondService { try kms.setAgreementSecret(keys, topic: topic) let didpkh = DIDPKH(account: account) - let cacao = CacaoFormatter().format(authRequestParams, signature, didpkh) - let response = RPCResponse(id: requestId, result: cacao) + let responseParams = CacaoFormatter().format(authRequestParams, signature, didpkh) + let response = RPCResponse(id: requestId, result: responseParams) try await networkingInteractor.respond(topic: topic, response: response, tag: AuthProtocolMethod.authRequest.responseTag, envelopeType: .type1(pubKey: keys.publicKey.rawRepresentation)) } diff --git a/Sources/Auth/Types/Cacao/Cacao.swift b/Sources/Auth/Types/Cacao/Cacao.swift index 003316e42..a82da2ab4 100644 --- a/Sources/Auth/Types/Cacao/Cacao.swift +++ b/Sources/Auth/Types/Cacao/Cacao.swift @@ -4,7 +4,7 @@ import Foundation /// /// specs at: https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-74.md public struct Cacao: Codable, Equatable { - let header: CacaoHeader - let payload: CacaoPayload - let signature: CacaoSignature + let h: CacaoHeader + let p: CacaoPayload + let s: CacaoSignature } diff --git a/Sources/WalletConnectNetworking/NetworkInteractor.swift b/Sources/WalletConnectNetworking/NetworkInteractor.swift index 13859ead6..96b68aea5 100644 --- a/Sources/WalletConnectNetworking/NetworkInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkInteractor.swift @@ -60,7 +60,7 @@ public class NetworkingInteractor: NetworkInteracting { return requestPublisher .filter { $0.request.method == request.method } .compactMap { topic, rpcRequest in - guard let id = rpcRequest.id, let request = try? rpcRequest.params?.get(Request.self) else { return nil } + guard let id = rpcRequest.id, let request = try! rpcRequest.params?.get(Request.self) else { return nil } return RequestSubscriptionPayload(id: id, topic: topic, request: request) } .eraseToAnyPublisher() From f08f6a84bb85ecf935ac8e0e962919f37dc1fb5a Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 7 Sep 2022 07:57:42 +0200 Subject: [PATCH 61/92] remove cacao formatter --- Sources/Auth/Services/Common/CacaoFormatter.swift | 14 -------------- .../Services/Wallet/WalletRespondService.swift | 5 ++++- Tests/AuthTests/AppRespondSubscriberTests.swift | 2 +- 3 files changed, 5 insertions(+), 16 deletions(-) delete mode 100644 Sources/Auth/Services/Common/CacaoFormatter.swift diff --git a/Sources/Auth/Services/Common/CacaoFormatter.swift b/Sources/Auth/Services/Common/CacaoFormatter.swift deleted file mode 100644 index 471fb2ba9..000000000 --- a/Sources/Auth/Services/Common/CacaoFormatter.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation -import WalletConnectUtils - -protocol CacaoFormatting { - func format(_ request: AuthRequestParams, _ signature: CacaoSignature, _ didpkh: DIDPKH) -> AuthResponseParams -} - -class CacaoFormatter: CacaoFormatting { - func format(_ request: AuthRequestParams, _ signature: CacaoSignature, _ didpkh: DIDPKH) -> AuthResponseParams { - let header = CacaoHeader(t: "eip4361") - let payload = CacaoPayload(params: request.payloadParams, didpkh: didpkh) - return AuthResponseParams(header: header, payload: payload, signature: signature) - } -} diff --git a/Sources/Auth/Services/Wallet/WalletRespondService.swift b/Sources/Auth/Services/Wallet/WalletRespondService.swift index 051fbbaa1..2d8cf31f7 100644 --- a/Sources/Auth/Services/Wallet/WalletRespondService.swift +++ b/Sources/Auth/Services/Wallet/WalletRespondService.swift @@ -31,7 +31,10 @@ actor WalletRespondService { try kms.setAgreementSecret(keys, topic: topic) let didpkh = DIDPKH(account: account) - let responseParams = CacaoFormatter().format(authRequestParams, signature, didpkh) + let header = CacaoHeader(t: "eip4361") + let payload = CacaoPayload(params: authRequestParams.payloadParams, didpkh: didpkh) + let responseParams = AuthResponseParams(header: header, payload: payload, signature: signature) + let response = RPCResponse(id: requestId, result: responseParams) try await networkingInteractor.respond(topic: topic, response: response, tag: AuthProtocolMethod.authRequest.responseTag, envelopeType: .type1(pubKey: keys.publicKey.rawRepresentation)) } diff --git a/Tests/AuthTests/AppRespondSubscriberTests.swift b/Tests/AuthTests/AppRespondSubscriberTests.swift index 686c0c830..98b8f47dd 100644 --- a/Tests/AuthTests/AppRespondSubscriberTests.swift +++ b/Tests/AuthTests/AppRespondSubscriberTests.swift @@ -58,7 +58,7 @@ class AppRespondSubscriberTests: XCTestCase { let message = try! messageFormatter.formatMessage(from: payload) let cacaoSignature = try! messageSigner.sign(message: message, privateKey: prvKey) - let cacao = Cacao(header: header, payload: payload, signature: cacaoSignature) + let cacao = Cacao(h: header, p: payload, s: cacaoSignature) let response = RPCResponse(id: requestId, result: cacao) networkingInteractor.responsePublisherSubject.send((topic, request, response)) From 509fbd794bcf224a812844db17854d75cac48c1a Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 7 Sep 2022 09:25:20 +0200 Subject: [PATCH 62/92] savepoint --- .../PresentationLayer/Wallet/Wallet/WalletPresenter.swift | 1 + Sources/Auth/Services/Common/SIWEMessageFormatter.swift | 8 +++++--- .../Auth/Services/Wallet/PendingRequestsProvider.swift | 2 +- Sources/Auth/Services/Wallet/WalletPairService.swift | 1 + .../Auth/Services/Wallet/WalletRequestSubscriber.swift | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift index 47a773591..ccf630ca8 100644 --- a/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift +++ b/Example/Showcase/Classes/PresentationLayer/Wallet/Wallet/WalletPresenter.swift @@ -16,6 +16,7 @@ final class WalletPresenter: ObservableObject { func didPastePairingURI() { guard let string = UIPasteboard.general.string, let uri = WalletConnectURI(string: string) else { return } + print(uri) pair(uri: uri) } diff --git a/Sources/Auth/Services/Common/SIWEMessageFormatter.swift b/Sources/Auth/Services/Common/SIWEMessageFormatter.swift index 3dd2309de..f38a74bd8 100644 --- a/Sources/Auth/Services/Common/SIWEMessageFormatter.swift +++ b/Sources/Auth/Services/Common/SIWEMessageFormatter.swift @@ -2,18 +2,19 @@ import Foundation import WalletConnectUtils protocol SIWEMessageFormatting { - func formatMessage(from authPayload: AuthPayload, address: String) -> String + func formatMessage(from authPayload: AuthPayload, address: String) -> String? func formatMessage(from payload: CacaoPayload) throws -> String } struct SIWEMessageFormatter: SIWEMessageFormatting { - func formatMessage(from payload: AuthPayload, address: String) -> String { + func formatMessage(from payload: AuthPayload, address: String) -> String? { + guard let chain = Blockchain(payload.chainId) else {return nil} let message = SIWEMessage(domain: payload.domain, uri: payload.aud, address: address, version: payload.version, nonce: payload.nonce, - chainId: payload.chainId, + chainId: chain.reference, iat: payload.iat, nbf: payload.nbf, exp: payload.exp, @@ -26,6 +27,7 @@ struct SIWEMessageFormatter: SIWEMessageFormatting { func formatMessage(from payload: CacaoPayload) throws -> String { let address = try DIDPKH(iss: payload.iss).account.address + let message = SIWEMessage( domain: payload.domain, uri: payload.aud, diff --git a/Sources/Auth/Services/Wallet/PendingRequestsProvider.swift b/Sources/Auth/Services/Wallet/PendingRequestsProvider.swift index 16fe22128..4287aa78b 100644 --- a/Sources/Auth/Services/Wallet/PendingRequestsProvider.swift +++ b/Sources/Auth/Services/Wallet/PendingRequestsProvider.swift @@ -14,7 +14,7 @@ class PendingRequestsProvider { .filter {$0.request.method == "wc_authRequest"} .compactMap { guard let params = try? $0.request.params?.get(AuthRequestParams.self) else {return nil} - let message = SIWEMessageFormatter().formatMessage(from: params.payloadParams, address: account.address) + let message = SIWEMessageFormatter().formatMessage(from: params.payloadParams, address: account.address)! return AuthRequest(id: $0.request.id!, message: message) } return pendingRequests diff --git a/Sources/Auth/Services/Wallet/WalletPairService.swift b/Sources/Auth/Services/Wallet/WalletPairService.swift index 355de0a29..2a1155bcc 100644 --- a/Sources/Auth/Services/Wallet/WalletPairService.swift +++ b/Sources/Auth/Services/Wallet/WalletPairService.swift @@ -29,6 +29,7 @@ actor WalletPairService { let symKey = try SymmetricKey(hex: uri.symKey) try kms.setSymmetricKey(symKey, for: pairing.topic) pairing.activate() + print("pairing") pairingStorage.setPairing(pairing) } diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift index 5f43b1e6a..c204ee119 100644 --- a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift +++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift @@ -33,7 +33,7 @@ class WalletRequestSubscriber { networkingInteractor.requestSubscription(on: AuthProtocolMethod.authRequest) .sink { [unowned self] (payload: RequestSubscriptionPayload) in logger.debug("WalletRequestSubscriber: Received request") - let message = messageFormatter.formatMessage(from: payload.request.payloadParams, address: address) + let message = messageFormatter.formatMessage(from: payload.request.payloadParams, address: address)! print(message) onRequest?(.init(id: payload.id, message: message)) }.store(in: &publishers) From 8e3daed7e9d7db5fcd18dcaea5693aaa47f0c2cc Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 7 Sep 2022 09:47:09 +0200 Subject: [PATCH 63/92] fix stubs --- Example/DApp/Auth/AuthViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Example/DApp/Auth/AuthViewModel.swift b/Example/DApp/Auth/AuthViewModel.swift index 7031c0eef..5554fc8b6 100644 --- a/Example/DApp/Auth/AuthViewModel.swift +++ b/Example/DApp/Auth/AuthViewModel.swift @@ -61,7 +61,7 @@ private extension AuthViewModel { private extension RequestParams { static func stub( domain: String = "service.invalid", - chainId: String = "1", + chainId: String = "eip155:1", nonce: String = "32891756", aud: String = "https://service.invalid/login", nbf: String? = nil, From 056828bb5472aedf631d3f7b76ef0aca8de96a99 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 7 Sep 2022 10:11:06 +0200 Subject: [PATCH 64/92] savepoint --- Sources/Auth/Services/App/AppRespondSubscriber.swift | 8 +++++--- .../Auth/Services/Wallet/WalletRequestSubscriber.swift | 1 + Sources/Auth/Types/Cacao/Cacao.swift | 6 +++--- Sources/WalletConnectRelay/EnvironmentInfo.swift | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Sources/Auth/Services/App/AppRespondSubscriber.swift b/Sources/Auth/Services/App/AppRespondSubscriber.swift index 2b3b353fa..bbde980b5 100644 --- a/Sources/Auth/Services/App/AppRespondSubscriber.swift +++ b/Sources/Auth/Services/App/AppRespondSubscriber.swift @@ -44,19 +44,21 @@ class AppRespondSubscriber { activatePairingIfNeeded(id: payload.id) networkingInteractor.unsubscribe(topic: payload.topic) + print(payload) let requestId = payload.id let cacao = payload.response let requestPayload = payload.request guard - let address = try? DIDPKH(iss: cacao.p.iss).account.address, - let message = try? messageFormatter.formatMessage(from: cacao.p) + let address = try? DIDPKH(iss: cacao.payload.iss).account.address, + let message = try? messageFormatter.formatMessage(from: cacao.payload) else { self.onResponse?(requestId, .failure(.malformedResponseParams)); return } + print(message) guard messageFormatter.formatMessage(from: requestPayload.payloadParams, address: address) == message else { self.onResponse?(requestId, .failure(.messageCompromised)); return } - guard let _ = try? signatureVerifier.verify(signature: cacao.s, message: message, address: address) + guard let _ = try? signatureVerifier.verify(signature: cacao.signature, message: message, address: address) else { self.onResponse?(requestId, .failure(.signatureVerificationFailed)); return } onResponse?(requestId, .success(cacao)) diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift index c204ee119..d84c0c5be 100644 --- a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift +++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift @@ -33,6 +33,7 @@ class WalletRequestSubscriber { networkingInteractor.requestSubscription(on: AuthProtocolMethod.authRequest) .sink { [unowned self] (payload: RequestSubscriptionPayload) in logger.debug("WalletRequestSubscriber: Received request") + print(payload) let message = messageFormatter.formatMessage(from: payload.request.payloadParams, address: address)! print(message) onRequest?(.init(id: payload.id, message: message)) diff --git a/Sources/Auth/Types/Cacao/Cacao.swift b/Sources/Auth/Types/Cacao/Cacao.swift index a82da2ab4..003316e42 100644 --- a/Sources/Auth/Types/Cacao/Cacao.swift +++ b/Sources/Auth/Types/Cacao/Cacao.swift @@ -4,7 +4,7 @@ import Foundation /// /// specs at: https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-74.md public struct Cacao: Codable, Equatable { - let h: CacaoHeader - let p: CacaoPayload - let s: CacaoSignature + let header: CacaoHeader + let payload: CacaoPayload + let signature: CacaoSignature } diff --git a/Sources/WalletConnectRelay/EnvironmentInfo.swift b/Sources/WalletConnectRelay/EnvironmentInfo.swift index 1348a621d..f1d61fc2f 100644 --- a/Sources/WalletConnectRelay/EnvironmentInfo.swift +++ b/Sources/WalletConnectRelay/EnvironmentInfo.swift @@ -18,7 +18,7 @@ enum EnvironmentInfo { } static var sdkVersion: String { - "v0.9.3-rc.0" + "v0.10.1-rc.0" } static var operatingSystem: String { From f39f7752012a982532374cc54e9764fb8c7512f2 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 7 Sep 2022 10:13:28 +0200 Subject: [PATCH 65/92] fix tests --- Example/IntegrationTests/Stubs/RequestParams.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Example/IntegrationTests/Stubs/RequestParams.swift b/Example/IntegrationTests/Stubs/RequestParams.swift index e594798ee..5c1a30db4 100644 --- a/Example/IntegrationTests/Stubs/RequestParams.swift +++ b/Example/IntegrationTests/Stubs/RequestParams.swift @@ -3,7 +3,7 @@ import Foundation extension RequestParams { static func stub(domain: String = "service.invalid", - chainId: String = "1", + chainId: String = "eip155:1", nonce: String = "32891756", aud: String = "https://service.invalid/login", nbf: String? = nil, From a171c3c0834c6162e950f341db5ff8038a5ee2e5 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 7 Sep 2022 10:25:52 +0200 Subject: [PATCH 66/92] remove prints --- Sources/Auth/Services/App/AppRespondSubscriber.swift | 2 -- Sources/Auth/Services/Wallet/WalletPairService.swift | 1 - Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift | 2 -- 3 files changed, 5 deletions(-) diff --git a/Sources/Auth/Services/App/AppRespondSubscriber.swift b/Sources/Auth/Services/App/AppRespondSubscriber.swift index bbde980b5..5c59d1128 100644 --- a/Sources/Auth/Services/App/AppRespondSubscriber.swift +++ b/Sources/Auth/Services/App/AppRespondSubscriber.swift @@ -44,7 +44,6 @@ class AppRespondSubscriber { activatePairingIfNeeded(id: payload.id) networkingInteractor.unsubscribe(topic: payload.topic) - print(payload) let requestId = payload.id let cacao = payload.response let requestPayload = payload.request @@ -54,7 +53,6 @@ class AppRespondSubscriber { let message = try? messageFormatter.formatMessage(from: cacao.payload) else { self.onResponse?(requestId, .failure(.malformedResponseParams)); return } - print(message) guard messageFormatter.formatMessage(from: requestPayload.payloadParams, address: address) == message else { self.onResponse?(requestId, .failure(.messageCompromised)); return } diff --git a/Sources/Auth/Services/Wallet/WalletPairService.swift b/Sources/Auth/Services/Wallet/WalletPairService.swift index 2a1155bcc..355de0a29 100644 --- a/Sources/Auth/Services/Wallet/WalletPairService.swift +++ b/Sources/Auth/Services/Wallet/WalletPairService.swift @@ -29,7 +29,6 @@ actor WalletPairService { let symKey = try SymmetricKey(hex: uri.symKey) try kms.setSymmetricKey(symKey, for: pairing.topic) pairing.activate() - print("pairing") pairingStorage.setPairing(pairing) } diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift index d84c0c5be..4ad717de0 100644 --- a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift +++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift @@ -33,9 +33,7 @@ class WalletRequestSubscriber { networkingInteractor.requestSubscription(on: AuthProtocolMethod.authRequest) .sink { [unowned self] (payload: RequestSubscriptionPayload) in logger.debug("WalletRequestSubscriber: Received request") - print(payload) let message = messageFormatter.formatMessage(from: payload.request.payloadParams, address: address)! - print(message) onRequest?(.init(id: payload.id, message: message)) }.store(in: &publishers) } From 47aea79c457be1aca0b48939dcc97129a8be51a2 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 7 Sep 2022 12:01:50 +0200 Subject: [PATCH 67/92] fix unit tests --- Tests/AuthTests/AppRespondSubscriberTests.swift | 2 +- Tests/AuthTests/Mocks/SIWEMessageFormatterMock.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/AuthTests/AppRespondSubscriberTests.swift b/Tests/AuthTests/AppRespondSubscriberTests.swift index 98b8f47dd..686c0c830 100644 --- a/Tests/AuthTests/AppRespondSubscriberTests.swift +++ b/Tests/AuthTests/AppRespondSubscriberTests.swift @@ -58,7 +58,7 @@ class AppRespondSubscriberTests: XCTestCase { let message = try! messageFormatter.formatMessage(from: payload) let cacaoSignature = try! messageSigner.sign(message: message, privateKey: prvKey) - let cacao = Cacao(h: header, p: payload, s: cacaoSignature) + let cacao = Cacao(header: header, payload: payload, signature: cacaoSignature) let response = RPCResponse(id: requestId, result: cacao) networkingInteractor.responsePublisherSubject.send((topic, request, response)) diff --git a/Tests/AuthTests/Mocks/SIWEMessageFormatterMock.swift b/Tests/AuthTests/Mocks/SIWEMessageFormatterMock.swift index 086ebba3c..e15d682be 100644 --- a/Tests/AuthTests/Mocks/SIWEMessageFormatterMock.swift +++ b/Tests/AuthTests/Mocks/SIWEMessageFormatterMock.swift @@ -4,7 +4,7 @@ import Foundation class SIWEMessageFormatterMock: SIWEMessageFormatting { var formattedMessage: String! - func formatMessage(from authPayload: AuthPayload, address: String) -> String { + func formatMessage(from authPayload: AuthPayload, address: String) -> String? { return formattedMessage } From 733a1253f4c5385a8188a854c7a3c0605ea5d5dc Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 7 Sep 2022 12:37:42 +0200 Subject: [PATCH 68/92] fix formatter unit tests --- Tests/AuthTests/Stubs/RequestParams.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AuthTests/Stubs/RequestParams.swift b/Tests/AuthTests/Stubs/RequestParams.swift index e594798ee..5c1a30db4 100644 --- a/Tests/AuthTests/Stubs/RequestParams.swift +++ b/Tests/AuthTests/Stubs/RequestParams.swift @@ -3,7 +3,7 @@ import Foundation extension RequestParams { static func stub(domain: String = "service.invalid", - chainId: String = "1", + chainId: String = "eip155:1", nonce: String = "32891756", aud: String = "https://service.invalid/login", nbf: String? = nil, From b37303965960fc8ae144cad89add1661b92ec7dc Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 7 Sep 2022 12:38:17 +0200 Subject: [PATCH 69/92] fix signer tests --- Tests/AuthTests/CacaoSignerTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AuthTests/CacaoSignerTests.swift b/Tests/AuthTests/CacaoSignerTests.swift index 5b74f7333..159eebc41 100644 --- a/Tests/AuthTests/CacaoSignerTests.swift +++ b/Tests/AuthTests/CacaoSignerTests.swift @@ -24,7 +24,7 @@ class CacaoSignerTest: XCTestCase { - https://example.com/my-web2-claim.json """ - let signature = CacaoSignature(t: "eip191", s: "438effc459956b57fcd9f3dac6c675f9cee88abf21acab7305e8e32aa0303a883b06dcbd956279a7a2ca21ffa882ff55cc22e8ab8ec0f3fe90ab45f306938cfa1b") + let signature = CacaoSignature(t: "eip191", s: "0x438effc459956b57fcd9f3dac6c675f9cee88abf21acab7305e8e32aa0303a883b06dcbd956279a7a2ca21ffa882ff55cc22e8ab8ec0f3fe90ab45f306938cfa1b") func testCacaoSign() throws { let signer = MessageSigner(signer: Signer()) From 374c1b01c7a473c675a1ff48cea54410336de1a0 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 7 Sep 2022 12:43:42 +0200 Subject: [PATCH 70/92] remove hardcoded chain id --- Sources/Auth/Services/Common/SIWEMessageFormatter.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Auth/Services/Common/SIWEMessageFormatter.swift b/Sources/Auth/Services/Common/SIWEMessageFormatter.swift index f38a74bd8..cace093c7 100644 --- a/Sources/Auth/Services/Common/SIWEMessageFormatter.swift +++ b/Sources/Auth/Services/Common/SIWEMessageFormatter.swift @@ -27,14 +27,14 @@ struct SIWEMessageFormatter: SIWEMessageFormatting { func formatMessage(from payload: CacaoPayload) throws -> String { let address = try DIDPKH(iss: payload.iss).account.address - + let iss = try DIDPKH(iss: payload.iss) let message = SIWEMessage( domain: payload.domain, uri: payload.aud, address: address, version: payload.version, nonce: payload.nonce, - chainId: "1", + chainId: iss.account.reference, iat: payload.iat, nbf: payload.nbf, exp: payload.exp, From 24ccd2234a7187da5eec881b3c2ec09c1b0be36e Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 7 Sep 2022 12:47:04 +0200 Subject: [PATCH 71/92] update --- Sources/Auth/Services/Signer/MessageSigner.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/Auth/Services/Signer/MessageSigner.swift b/Sources/Auth/Services/Signer/MessageSigner.swift index 4facc7099..3f6252efd 100644 --- a/Sources/Auth/Services/Signer/MessageSigner.swift +++ b/Sources/Auth/Services/Signer/MessageSigner.swift @@ -24,7 +24,8 @@ public struct MessageSigner: MessageSignatureVerifying, MessageSigning { public func sign(message: String, privateKey: Data) throws -> CacaoSignature { guard let messageData = message.data(using: .utf8) else { throw Errors.utf8EncodingFailed } let signature = try signer.sign(message: messageData, with: privateKey) - return CacaoSignature(t: "eip191", s: "0x"+signature.toHexString()) + let prefixedHexSignature = "0x" + signature.toHexString() + return CacaoSignature(t: "eip191", s: prefixedHexSignature) } public func verify(signature: CacaoSignature, message: String, address: String) throws { From 5b7b41e86d3b6f28db1294ec9371f01d87c00f9f Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 7 Sep 2022 13:07:07 +0200 Subject: [PATCH 72/92] Add wallet responder --- Sources/Auth/AuthClientFactory.swift | 5 +- .../Wallet/PendingRequestsProvider.swift | 4 +- .../Wallet/WalletRequestSubscriber.swift | 68 ++++++++++++++++--- .../Wallet/WalletRespondService.swift | 15 ++-- 4 files changed, 69 insertions(+), 23 deletions(-) diff --git a/Sources/Auth/AuthClientFactory.swift b/Sources/Auth/AuthClientFactory.swift index 7b8c0a8fb..af1a0b8f2 100644 --- a/Sources/Auth/AuthClientFactory.swift +++ b/Sources/Auth/AuthClientFactory.swift @@ -27,8 +27,9 @@ public struct AuthClientFactory { let messageSigner = MessageSigner(signer: Signer()) let appRespondSubscriber = AppRespondSubscriber(networkingInteractor: networkingInteractor, logger: logger, rpcHistory: history, signatureVerifier: messageSigner, messageFormatter: messageFormatter, pairingStorage: pairingStore) let walletPairService = WalletPairService(networkingInteractor: networkingInteractor, kms: kms, pairingStorage: pairingStore) - let walletRequestSubscriber = WalletRequestSubscriber(networkingInteractor: networkingInteractor, logger: logger, kms: kms, messageFormatter: messageFormatter, address: account?.address) - let walletRespondService = WalletRespondService(networkingInteractor: networkingInteractor, logger: logger, kms: kms, rpcHistory: history) + let walletErrorResponder = WalletErrorResponder(networkingInteractor: networkingInteractor, logger: logger, kms: kms, rpcHistory: history) + let walletRequestSubscriber = WalletRequestSubscriber(networkingInteractor: networkingInteractor, logger: logger, kms: kms, messageFormatter: messageFormatter, address: account?.address, walletErrorResponder: walletErrorResponder) + let walletRespondService = WalletRespondService(networkingInteractor: networkingInteractor, logger: logger, kms: kms, rpcHistory: history, walletErrorResponder: walletErrorResponder) let pendingRequestsProvider = PendingRequestsProvider(rpcHistory: history) let cleanupService = CleanupService(pairingStore: pairingStore, kms: kms) let deletePairingService = DeletePairingService(networkingInteractor: networkingInteractor, kms: kms, pairingStorage: pairingStore, logger: logger) diff --git a/Sources/Auth/Services/Wallet/PendingRequestsProvider.swift b/Sources/Auth/Services/Wallet/PendingRequestsProvider.swift index 4287aa78b..7df35ca37 100644 --- a/Sources/Auth/Services/Wallet/PendingRequestsProvider.swift +++ b/Sources/Auth/Services/Wallet/PendingRequestsProvider.swift @@ -13,8 +13,8 @@ class PendingRequestsProvider { let pendingRequests: [AuthRequest] = rpcHistory.getPending() .filter {$0.request.method == "wc_authRequest"} .compactMap { - guard let params = try? $0.request.params?.get(AuthRequestParams.self) else {return nil} - let message = SIWEMessageFormatter().formatMessage(from: params.payloadParams, address: account.address)! + guard let params = try? $0.request.params?.get(AuthRequestParams.self), + let message = SIWEMessageFormatter().formatMessage(from: params.payloadParams, address: account.address) else {return nil} return AuthRequest(id: $0.request.id!, message: message) } return pendingRequests diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift index 4ad717de0..9279b5d81 100644 --- a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift +++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift @@ -12,18 +12,21 @@ class WalletRequestSubscriber { private let address: String? private var publishers = [AnyCancellable]() private let messageFormatter: SIWEMessageFormatting + private let walletErrorResponder: WalletErrorResponder var onRequest: ((AuthRequest) -> Void)? init(networkingInteractor: NetworkInteracting, logger: ConsoleLogging, kms: KeyManagementServiceProtocol, messageFormatter: SIWEMessageFormatting, - address: String?) { + address: String?, + walletErrorResponder: WalletErrorResponder) { self.networkingInteractor = networkingInteractor self.logger = logger self.kms = kms self.address = address self.messageFormatter = messageFormatter + self.walletErrorResponder = walletErrorResponder subscribeForRequest() } @@ -33,20 +36,67 @@ class WalletRequestSubscriber { networkingInteractor.requestSubscription(on: AuthProtocolMethod.authRequest) .sink { [unowned self] (payload: RequestSubscriptionPayload) in logger.debug("WalletRequestSubscriber: Received request") - let message = messageFormatter.formatMessage(from: payload.request.payloadParams, address: address)! + guard let message = messageFormatter.formatMessage(from: payload.request.payloadParams, address: address) else { + Task { + try? await walletErrorResponder.respondError(AuthError.malformedRequestParams, requestId: payload.id) + } + return + } onRequest?(.init(id: payload.id, message: message)) }.store(in: &publishers) } +} + + +actor WalletErrorResponder { + enum Errors: Error { + case recordForIdNotFound + case malformedAuthRequestParams + } + + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementService + private let rpcHistory: RPCHistory + private let logger: ConsoleLogging + + init(networkingInteractor: NetworkInteracting, + logger: ConsoleLogging, + kms: KeyManagementService, + rpcHistory: RPCHistory) { + self.networkingInteractor = networkingInteractor + self.logger = logger + self.kms = kms + self.rpcHistory = rpcHistory + } - private func respondError(_ error: AuthError, topic: String, requestId: RPCID) { - guard let pubKey = kms.getAgreementSecret(for: topic)?.publicKey - else { return logger.error("Agreement key for topic \(topic) not found") } + + func respondError(_ error: AuthError, requestId: RPCID) async throws { + let authRequestParams = try getAuthRequestParams(requestId: requestId) + let (topic, keys) = try generateAgreementKeys(requestParams: authRequestParams) + + try kms.setAgreementSecret(keys, topic: topic) let tag = AuthProtocolMethod.authRequest.responseTag - let envelopeType = Envelope.EnvelopeType.type1(pubKey: pubKey.rawRepresentation) + let envelopeType = Envelope.EnvelopeType.type1(pubKey: keys.publicKey.rawRepresentation) + try await networkingInteractor.respondError(topic: topic, requestId: requestId, tag: tag, reason: error, envelopeType: envelopeType) + } + + + private func getAuthRequestParams(requestId: RPCID) throws -> AuthRequestParams { + guard let request = rpcHistory.get(recordId: requestId)?.request + else { throw Errors.recordForIdNotFound } + + guard let authRequestParams = try request.params?.get(AuthRequestParams.self) + else { throw Errors.malformedAuthRequestParams } + + return authRequestParams + } - Task(priority: .high) { - try await networkingInteractor.respondError(topic: topic, requestId: requestId, tag: tag, reason: error, envelopeType: envelopeType) - } + private func generateAgreementKeys(requestParams: AuthRequestParams) throws -> (topic: String, keys: AgreementKeys) { + let peerPubKey = try AgreementPublicKey(hex: requestParams.requester.publicKey) + let topic = peerPubKey.rawRepresentation.sha256().toHexString() + let selfPubKey = try kms.createX25519KeyPair() + let keys = try kms.performKeyAgreement(selfPublicKey: selfPubKey, peerPublicKey: peerPubKey.hexRepresentation) + return (topic, keys) } } diff --git a/Sources/Auth/Services/Wallet/WalletRespondService.swift b/Sources/Auth/Services/Wallet/WalletRespondService.swift index 2d8cf31f7..31e6b1ae5 100644 --- a/Sources/Auth/Services/Wallet/WalletRespondService.swift +++ b/Sources/Auth/Services/Wallet/WalletRespondService.swift @@ -13,15 +13,18 @@ actor WalletRespondService { private let kms: KeyManagementService private let rpcHistory: RPCHistory private let logger: ConsoleLogging + private let walletErrorResponder: WalletErrorResponder init(networkingInteractor: NetworkInteracting, logger: ConsoleLogging, kms: KeyManagementService, - rpcHistory: RPCHistory) { + rpcHistory: RPCHistory, + walletErrorResponder: WalletErrorResponder) { self.networkingInteractor = networkingInteractor self.logger = logger self.kms = kms self.rpcHistory = rpcHistory + self.walletErrorResponder = walletErrorResponder } func respond(requestId: RPCID, signature: CacaoSignature, account: Account) async throws { @@ -40,15 +43,7 @@ actor WalletRespondService { } func respondError(requestId: RPCID) async throws { - let authRequestParams = try getAuthRequestParams(requestId: requestId) - let (topic, keys) = try generateAgreementKeys(requestParams: authRequestParams) - - try kms.setAgreementSecret(keys, topic: topic) - - let tag = AuthProtocolMethod.authRequest.responseTag - let error = AuthError.userRejeted - let envelopeType = Envelope.EnvelopeType.type1(pubKey: keys.publicKey.rawRepresentation) - try await networkingInteractor.respondError(topic: topic, requestId: requestId, tag: tag, reason: error, envelopeType: envelopeType) + try await walletErrorResponder.respondError(AuthError.userRejeted, requestId: requestId) } private func getAuthRequestParams(requestId: RPCID) throws -> AuthRequestParams { From 48517baa82bff515949b87759a97544cea9478cd Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 7 Sep 2022 13:14:17 +0200 Subject: [PATCH 73/92] fix tests --- Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift | 4 ++-- Tests/AuthTests/WalletRequestSubscriberTests.swift | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift index 9279b5d81..afff24a5f 100644 --- a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift +++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift @@ -55,13 +55,13 @@ actor WalletErrorResponder { } private let networkingInteractor: NetworkInteracting - private let kms: KeyManagementService + private let kms: KeyManagementServiceProtocol private let rpcHistory: RPCHistory private let logger: ConsoleLogging init(networkingInteractor: NetworkInteracting, logger: ConsoleLogging, - kms: KeyManagementService, + kms: KeyManagementServiceProtocol, rpcHistory: RPCHistory) { self.networkingInteractor = networkingInteractor self.logger = logger diff --git a/Tests/AuthTests/WalletRequestSubscriberTests.swift b/Tests/AuthTests/WalletRequestSubscriberTests.swift index 4a2062f61..0fb214242 100644 --- a/Tests/AuthTests/WalletRequestSubscriberTests.swift +++ b/Tests/AuthTests/WalletRequestSubscriberTests.swift @@ -16,10 +16,12 @@ class WalletRequestSubscriberTests: XCTestCase { override func setUp() { networkingInteractor = NetworkingInteractorMock() messageFormatter = SIWEMessageFormatterMock() + + let walletErrorResponder = WalletErrorResponder(networkingInteractor: networkingInteractor, logger: ConsoleLoggerMock(), kms: KeyManagementServiceMock(), rpcHistory: RPCHistory(keyValueStore: CodableStore(defaults: RuntimeKeyValueStorage(), identifier: ""))) sut = WalletRequestSubscriber(networkingInteractor: networkingInteractor, logger: ConsoleLoggerMock(), kms: KeyManagementServiceMock(), - messageFormatter: messageFormatter, address: "") + messageFormatter: messageFormatter, address: "", walletErrorResponder: walletErrorResponder) } func testSubscribeRequest() { From e8adfa7b6fddb2520a4637423506a8268d5f5796 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 7 Sep 2022 13:16:20 +0200 Subject: [PATCH 74/92] update --- Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift index afff24a5f..dd31bd743 100644 --- a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift +++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift @@ -81,7 +81,6 @@ actor WalletErrorResponder { try await networkingInteractor.respondError(topic: topic, requestId: requestId, tag: tag, reason: error, envelopeType: envelopeType) } - private func getAuthRequestParams(requestId: RPCID) throws -> AuthRequestParams { guard let request = rpcHistory.get(recordId: requestId)?.request else { throw Errors.recordForIdNotFound } @@ -97,6 +96,7 @@ actor WalletErrorResponder { let topic = peerPubKey.rawRepresentation.sha256().toHexString() let selfPubKey = try kms.createX25519KeyPair() let keys = try kms.performKeyAgreement(selfPublicKey: selfPubKey, peerPublicKey: peerPubKey.hexRepresentation) + //TODO - remove keys return (topic, keys) } } From 066910bafadbde554d6efa5ed91631177cbc025c Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 7 Sep 2022 13:17:09 +0200 Subject: [PATCH 75/92] extract wallet error responder --- .../Wallet/WalletErrorResponder.swift | 58 +++++++++++++++++++ .../Wallet/WalletRequestSubscriber.swift | 53 ----------------- 2 files changed, 58 insertions(+), 53 deletions(-) create mode 100644 Sources/Auth/Services/Wallet/WalletErrorResponder.swift diff --git a/Sources/Auth/Services/Wallet/WalletErrorResponder.swift b/Sources/Auth/Services/Wallet/WalletErrorResponder.swift new file mode 100644 index 000000000..10e318c24 --- /dev/null +++ b/Sources/Auth/Services/Wallet/WalletErrorResponder.swift @@ -0,0 +1,58 @@ +import Foundation +import JSONRPC +import WalletConnectNetworking +import WalletConnectUtils +import WalletConnectKMS + +actor WalletErrorResponder { + enum Errors: Error { + case recordForIdNotFound + case malformedAuthRequestParams + } + + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + private let rpcHistory: RPCHistory + private let logger: ConsoleLogging + + init(networkingInteractor: NetworkInteracting, + logger: ConsoleLogging, + kms: KeyManagementServiceProtocol, + rpcHistory: RPCHistory) { + self.networkingInteractor = networkingInteractor + self.logger = logger + self.kms = kms + self.rpcHistory = rpcHistory + } + + + func respondError(_ error: AuthError, requestId: RPCID) async throws { + let authRequestParams = try getAuthRequestParams(requestId: requestId) + let (topic, keys) = try generateAgreementKeys(requestParams: authRequestParams) + + try kms.setAgreementSecret(keys, topic: topic) + + let tag = AuthProtocolMethod.authRequest.responseTag + let envelopeType = Envelope.EnvelopeType.type1(pubKey: keys.publicKey.rawRepresentation) + try await networkingInteractor.respondError(topic: topic, requestId: requestId, tag: tag, reason: error, envelopeType: envelopeType) + } + + private func getAuthRequestParams(requestId: RPCID) throws -> AuthRequestParams { + guard let request = rpcHistory.get(recordId: requestId)?.request + else { throw Errors.recordForIdNotFound } + + guard let authRequestParams = try request.params?.get(AuthRequestParams.self) + else { throw Errors.malformedAuthRequestParams } + + return authRequestParams + } + + private func generateAgreementKeys(requestParams: AuthRequestParams) throws -> (topic: String, keys: AgreementKeys) { + let peerPubKey = try AgreementPublicKey(hex: requestParams.requester.publicKey) + let topic = peerPubKey.rawRepresentation.sha256().toHexString() + let selfPubKey = try kms.createX25519KeyPair() + let keys = try kms.performKeyAgreement(selfPublicKey: selfPubKey, peerPublicKey: peerPubKey.hexRepresentation) + //TODO - remove keys + return (topic, keys) + } +} diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift index dd31bd743..b52b06b89 100644 --- a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift +++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift @@ -47,56 +47,3 @@ class WalletRequestSubscriber { } } - -actor WalletErrorResponder { - enum Errors: Error { - case recordForIdNotFound - case malformedAuthRequestParams - } - - private let networkingInteractor: NetworkInteracting - private let kms: KeyManagementServiceProtocol - private let rpcHistory: RPCHistory - private let logger: ConsoleLogging - - init(networkingInteractor: NetworkInteracting, - logger: ConsoleLogging, - kms: KeyManagementServiceProtocol, - rpcHistory: RPCHistory) { - self.networkingInteractor = networkingInteractor - self.logger = logger - self.kms = kms - self.rpcHistory = rpcHistory - } - - - func respondError(_ error: AuthError, requestId: RPCID) async throws { - let authRequestParams = try getAuthRequestParams(requestId: requestId) - let (topic, keys) = try generateAgreementKeys(requestParams: authRequestParams) - - try kms.setAgreementSecret(keys, topic: topic) - - let tag = AuthProtocolMethod.authRequest.responseTag - let envelopeType = Envelope.EnvelopeType.type1(pubKey: keys.publicKey.rawRepresentation) - try await networkingInteractor.respondError(topic: topic, requestId: requestId, tag: tag, reason: error, envelopeType: envelopeType) - } - - private func getAuthRequestParams(requestId: RPCID) throws -> AuthRequestParams { - guard let request = rpcHistory.get(recordId: requestId)?.request - else { throw Errors.recordForIdNotFound } - - guard let authRequestParams = try request.params?.get(AuthRequestParams.self) - else { throw Errors.malformedAuthRequestParams } - - return authRequestParams - } - - private func generateAgreementKeys(requestParams: AuthRequestParams) throws -> (topic: String, keys: AgreementKeys) { - let peerPubKey = try AgreementPublicKey(hex: requestParams.requester.publicKey) - let topic = peerPubKey.rawRepresentation.sha256().toHexString() - let selfPubKey = try kms.createX25519KeyPair() - let keys = try kms.performKeyAgreement(selfPublicKey: selfPubKey, peerPublicKey: peerPubKey.hexRepresentation) - //TODO - remove keys - return (topic, keys) - } -} From ed5dc907d3ce5bd6ce9549a25584305677b419c8 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 7 Sep 2022 14:17:55 +0200 Subject: [PATCH 76/92] fix request id, apply pr comments suggestions --- Sources/Auth/AuthClientFactory.swift | 2 +- Sources/JSONRPC/RPCID.swift | 5 ++++- Sources/WalletConnectNetworking/NetworkInteractor.swift | 2 +- Sources/WalletConnectRelay/RelayClient.swift | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Sources/Auth/AuthClientFactory.swift b/Sources/Auth/AuthClientFactory.swift index af1a0b8f2..e4f181520 100644 --- a/Sources/Auth/AuthClientFactory.swift +++ b/Sources/Auth/AuthClientFactory.swift @@ -8,7 +8,7 @@ import WalletConnectNetworking public struct AuthClientFactory { public static func create(metadata: AppMetadata, account: Account?, relayClient: RelayClient) -> AuthClient { - let logger = ConsoleLogger(loggingLevel: .debug) + let logger = ConsoleLogger(loggingLevel: .off) let keyValueStorage = UserDefaults.standard let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") return AuthClientFactory.create(metadata: metadata, account: account, logger: logger, keyValueStorage: keyValueStorage, keychainStorage: keychainStorage, relayClient: relayClient) diff --git a/Sources/JSONRPC/RPCID.swift b/Sources/JSONRPC/RPCID.swift index 9d52511d9..ff0ef3c83 100644 --- a/Sources/JSONRPC/RPCID.swift +++ b/Sources/JSONRPC/RPCID.swift @@ -1,4 +1,5 @@ import Commons +import Foundation public typealias RPCID = Either @@ -9,6 +10,8 @@ public protocol IdentifierGenerator { struct IntIdentifierGenerator: IdentifierGenerator { func next() -> RPCID { - return RPCID(Int64.random(in: Int64.min...Int64.max)) + let timestamp = Int64(Date().timeIntervalSince1970 * 1000) * 1000 + let random = Int64.random(in: 0..<1000) + return RPCID(timestamp + random) } } diff --git a/Sources/WalletConnectNetworking/NetworkInteractor.swift b/Sources/WalletConnectNetworking/NetworkInteractor.swift index 96b68aea5..13859ead6 100644 --- a/Sources/WalletConnectNetworking/NetworkInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkInteractor.swift @@ -60,7 +60,7 @@ public class NetworkingInteractor: NetworkInteracting { return requestPublisher .filter { $0.request.method == request.method } .compactMap { topic, rpcRequest in - guard let id = rpcRequest.id, let request = try! rpcRequest.params?.get(Request.self) else { return nil } + guard let id = rpcRequest.id, let request = try? rpcRequest.params?.get(Request.self) else { return nil } return RequestSubscriptionPayload(id: id, topic: topic, request: request) } .eraseToAnyPublisher() diff --git a/Sources/WalletConnectRelay/RelayClient.swift b/Sources/WalletConnectRelay/RelayClient.swift index 90066deb6..b6aef67c0 100644 --- a/Sources/WalletConnectRelay/RelayClient.swift +++ b/Sources/WalletConnectRelay/RelayClient.swift @@ -87,7 +87,7 @@ public final class RelayClient { keychainStorage: KeychainStorageProtocol = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk"), socketFactory: WebSocketFactory, socketConnectionType: SocketConnectionType = .automatic, - logger: ConsoleLogging = ConsoleLogger(loggingLevel: .debug) + logger: ConsoleLogging = ConsoleLogger(loggingLevel: .off) ) { let socketAuthenticator = SocketAuthenticator( clientIdStorage: ClientIdStorage(keychain: keychainStorage), From d304f645bbe591d25166aa7d81efc502573ad521 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 7 Sep 2022 14:56:21 +0200 Subject: [PATCH 77/92] update priority --- Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift index b52b06b89..224afeee8 100644 --- a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift +++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift @@ -37,7 +37,7 @@ class WalletRequestSubscriber { .sink { [unowned self] (payload: RequestSubscriptionPayload) in logger.debug("WalletRequestSubscriber: Received request") guard let message = messageFormatter.formatMessage(from: payload.request.payloadParams, address: address) else { - Task { + Task(prority: .high) { try? await walletErrorResponder.respondError(AuthError.malformedRequestParams, requestId: payload.id) } return From 251d6fa10fd554d86ba1eb027d06b1dd602ab47e Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Wed, 7 Sep 2022 16:06:35 +0200 Subject: [PATCH 78/92] fix typo --- Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift index 224afeee8..c204cc526 100644 --- a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift +++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift @@ -37,7 +37,7 @@ class WalletRequestSubscriber { .sink { [unowned self] (payload: RequestSubscriptionPayload) in logger.debug("WalletRequestSubscriber: Received request") guard let message = messageFormatter.formatMessage(from: payload.request.payloadParams, address: address) else { - Task(prority: .high) { + Task(priority: .high) { try? await walletErrorResponder.respondError(AuthError.malformedRequestParams, requestId: payload.id) } return From 0beea8291a1be330878e730f588382a27dd9b70b Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Wed, 7 Sep 2022 17:59:14 +0300 Subject: [PATCH 79/92] RelayHost env param --- Example/ExampleApp.xcodeproj/project.pbxproj | 4 ++++ Example/IntegrationTests/Auth/AuthTests.swift | 3 +-- Example/IntegrationTests/Chat/ChatTests.swift | 3 +-- .../IntegrationTests/Relay/RelayClientEndToEndTests.swift | 5 ++--- Example/IntegrationTests/Sign/SignClientTests.swift | 3 +-- Example/IntegrationTests/Stubs/URLConfig.swift | 8 ++++++++ Tests/IntegrationTests/AuthChallengeTests.swift | 4 +--- 7 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 Example/IntegrationTests/Stubs/URLConfig.swift diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index b07675f09..73ad22b10 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -48,6 +48,7 @@ 84F568C2279582D200D0A289 /* Signer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F568C1279582D200D0A289 /* Signer.swift */; }; 84F568C42795832A00D0A289 /* EthereumTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F568C32795832A00D0A289 /* EthereumTransaction.swift */; }; 84FE684628ACDB4700C893FF /* RequestParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FE684528ACDB4700C893FF /* RequestParams.swift */; }; + A501AC2728C8E59800CEAA42 /* URLConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A501AC2628C8E59800CEAA42 /* URLConfig.swift */; }; A50C036528AAD32200FE72D3 /* ClientDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50C036428AAD32200FE72D3 /* ClientDelegate.swift */; }; A50F3946288005B200064555 /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50F3945288005B200064555 /* Types.swift */; }; A55CAAB028B92AFF00844382 /* ScanModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55CAAAB28B92AFF00844382 /* ScanModule.swift */; }; @@ -236,6 +237,7 @@ 84F568C1279582D200D0A289 /* Signer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signer.swift; sourceTree = ""; }; 84F568C32795832A00D0A289 /* EthereumTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthereumTransaction.swift; sourceTree = ""; }; 84FE684528ACDB4700C893FF /* RequestParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestParams.swift; sourceTree = ""; }; + A501AC2628C8E59800CEAA42 /* URLConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLConfig.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 = ""; }; A55CAAAB28B92AFF00844382 /* ScanModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanModule.swift; sourceTree = ""; }; @@ -1048,6 +1050,7 @@ A5E03E0E28646D8A00888481 /* WebSocketFactory.swift */, A5E03DFC286465D100888481 /* Stubs.swift */, 84FE684528ACDB4700C893FF /* RequestParams.swift */, + A501AC2628C8E59800CEAA42 /* URLConfig.swift */, ); path = Stubs; sourceTree = ""; @@ -1456,6 +1459,7 @@ 7694A5262874296A0001257E /* RegistryTests.swift in Sources */, A50C036528AAD32200FE72D3 /* ClientDelegate.swift in Sources */, A5E03E0D28646AD200888481 /* RelayClientEndToEndTests.swift in Sources */, + A501AC2728C8E59800CEAA42 /* URLConfig.swift in Sources */, A5E03DFA286465C700888481 /* SignClientTests.swift in Sources */, A5E03E0F28646D8A00888481 /* WebSocketFactory.swift in Sources */, A5E03E1128646F8000888481 /* KeychainStorageMock.swift in Sources */, diff --git a/Example/IntegrationTests/Auth/AuthTests.swift b/Example/IntegrationTests/Auth/AuthTests.swift index 43d56d372..87c6f2109 100644 --- a/Example/IntegrationTests/Auth/AuthTests.swift +++ b/Example/IntegrationTests/Auth/AuthTests.swift @@ -37,10 +37,9 @@ final class AuthTests: XCTestCase { func makeClient(prefix: String, account: Account? = nil) -> AuthClient { let logger = ConsoleLogger(suffix: prefix, loggingLevel: .debug) - let relayHost = "relay.walletconnect.com" let projectId = "8ba9ee138960775e5231b70cc5ef1c3a" let keychain = KeychainStorageMock() - let relayClient = RelayClient(relayHost: relayHost, projectId: projectId, keychainStorage: keychain, socketFactory: SocketFactory(), logger: logger) + let relayClient = RelayClient(relayHost: URLConfig.relayHost, projectId: projectId, keychainStorage: keychain, socketFactory: SocketFactory(), logger: logger) return AuthClientFactory.create( metadata: AppMetadata(name: name, description: "", url: "", icons: [""]), diff --git a/Example/IntegrationTests/Chat/ChatTests.swift b/Example/IntegrationTests/Chat/ChatTests.swift index aa1a6271f..8483bd065 100644 --- a/Example/IntegrationTests/Chat/ChatTests.swift +++ b/Example/IntegrationTests/Chat/ChatTests.swift @@ -41,10 +41,9 @@ final class ChatTests: XCTestCase { func makeClient(prefix: String) -> ChatClient { let logger = ConsoleLogger(suffix: prefix, loggingLevel: .debug) - let relayHost = "relay.walletconnect.com" let projectId = "8ba9ee138960775e5231b70cc5ef1c3a" let keychain = KeychainStorageMock() - let relayClient = RelayClient(relayHost: relayHost, projectId: projectId, keychainStorage: keychain, socketFactory: SocketFactory(), logger: logger) + let relayClient = RelayClient(relayHost: URLConfig.relayHost, projectId: projectId, keychainStorage: keychain, socketFactory: SocketFactory(), logger: logger) return ChatClientFactory.create(registry: registry, relayClient: relayClient, kms: KeyManagementService(keychain: keychain), logger: logger, keyValueStorage: RuntimeKeyValueStorage()) } diff --git a/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift b/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift index a634f648b..a8ad1d46d 100644 --- a/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift +++ b/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift @@ -9,7 +9,6 @@ final class RelayClientEndToEndTests: XCTestCase { let defaultTimeout: TimeInterval = 10 - let relayHost = "relay.walletconnect.com" let projectId = "8ba9ee138960775e5231b70cc5ef1c3a" private var publishers = Set() @@ -18,10 +17,10 @@ final class RelayClientEndToEndTests: XCTestCase { let socketAuthenticator = SocketAuthenticator( clientIdStorage: clientIdStorage, didKeyFactory: ED25519DIDKeyFactory(), - relayHost: relayHost + relayHost: URLConfig.relayHost ) let urlFactory = RelayUrlFactory(socketAuthenticator: socketAuthenticator) - let socket = WebSocket(url: urlFactory.create(host: relayHost, projectId: projectId)) + let socket = WebSocket(url: urlFactory.create(host: URLConfig.relayHost, projectId: projectId)) let logger = ConsoleLogger() let dispatcher = Dispatcher(socket: socket, socketConnectionHandler: ManualSocketConnectionHandler(socket: socket), logger: logger) diff --git a/Example/IntegrationTests/Sign/SignClientTests.swift b/Example/IntegrationTests/Sign/SignClientTests.swift index d6fcc75d1..ee785b4c3 100644 --- a/Example/IntegrationTests/Sign/SignClientTests.swift +++ b/Example/IntegrationTests/Sign/SignClientTests.swift @@ -13,13 +13,12 @@ final class SignClientTests: XCTestCase { static private func makeClientDelegate( name: String, - relayHost: String = "relay.walletconnect.com", projectId: String = "8ba9ee138960775e5231b70cc5ef1c3a" ) -> ClientDelegate { let logger = ConsoleLogger(suffix: name, loggingLevel: .debug) let keychain = KeychainStorageMock() let relayClient = RelayClient( - relayHost: relayHost, + relayHost: URLConfig.relayHost, projectId: projectId, keyValueStorage: RuntimeKeyValueStorage(), keychainStorage: keychain, diff --git a/Example/IntegrationTests/Stubs/URLConfig.swift b/Example/IntegrationTests/Stubs/URLConfig.swift new file mode 100644 index 000000000..494de507f --- /dev/null +++ b/Example/IntegrationTests/Stubs/URLConfig.swift @@ -0,0 +1,8 @@ +import Foundation + +struct URLConfig { + + static var relayHost: String { + return ProcessInfo.processInfo.environment["RELAY_HOST"] ?? "relay.walletconnect.com" + } +} diff --git a/Tests/IntegrationTests/AuthChallengeTests.swift b/Tests/IntegrationTests/AuthChallengeTests.swift index e708c3b49..a92e91d0c 100644 --- a/Tests/IntegrationTests/AuthChallengeTests.swift +++ b/Tests/IntegrationTests/AuthChallengeTests.swift @@ -4,13 +4,11 @@ import WalletConnectKMS final class AuthChallengeTests: XCTestCase { - let relayHost: String = "dev.relay.walletconnect.com" - var httpClient: HTTPClient! var provider: AuthChallengeProvider! override func setUp() { - httpClient = HTTPClient(host: relayHost) + httpClient = HTTPClient(host: URLConfig.relayHost) provider = AuthChallengeProvider(client: httpClient) } From d40cc7dbfd496404ab360304c55fe64ccb81eba3 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Wed, 7 Sep 2022 18:01:23 +0300 Subject: [PATCH 80/92] Test wrong host --- .github/actions/ci/action.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 75ed3bd4f..3959bb175 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -27,7 +27,9 @@ runs: -project Example/ExampleApp.xcodeproj \ -scheme IntegrationTests \ -clonedSourcePackagesDirPath SourcePackagesCache \ - -destination 'platform=iOS Simulator,name=iPhone 13' test" + -destination 'platform=iOS Simulator,name=iPhone 13' \ + RELAY_HOST=host.host \ + test" # Wallet build - name: Build Example Wallet From b2306b56d79e2bbd771602af893fdf123575f702 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Wed, 7 Sep 2022 18:14:47 +0300 Subject: [PATCH 81/92] GCC_PREPROCESSOR_DEFINITIONS --- Example/ExampleApp.xcodeproj/project.pbxproj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 73ad22b10..fa668e328 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -1850,6 +1850,11 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = W5R8AG9K22; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + "RELAY_HOST=$(RELAY_HOST)", + ); GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; From baf2c1d937b2344b6e9bbda92a7eaa305ba05b54 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Wed, 7 Sep 2022 18:26:29 +0300 Subject: [PATCH 82/92] Quotes --- .github/actions/ci/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 3959bb175..cc392e23a 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -28,7 +28,7 @@ runs: -scheme IntegrationTests \ -clonedSourcePackagesDirPath SourcePackagesCache \ -destination 'platform=iOS Simulator,name=iPhone 13' \ - RELAY_HOST=host.host \ + RELAY_HOST='host.host' \ test" # Wallet build From e81d07053ea2847441a08ed2403d7339d337955f Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Wed, 7 Sep 2022 18:41:02 +0300 Subject: [PATCH 83/92] Env variable --- .../xcshareddata/xcschemes/IntegrationTests.xcscheme | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/IntegrationTests.xcscheme b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/IntegrationTests.xcscheme index 59b88b4e0..9ce1c4f02 100644 --- a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/IntegrationTests.xcscheme +++ b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/IntegrationTests.xcscheme @@ -10,7 +10,14 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "NO"> + + + + From 02879ca6b2aeeeda44633451d64a7df301cb960c Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Wed, 7 Sep 2022 18:48:08 +0300 Subject: [PATCH 84/92] Macro default value --- Example/ExampleApp.xcodeproj/project.pbxproj | 2 ++ .../xcshareddata/xcschemes/IntegrationTests.xcscheme | 9 +++++++++ Example/IntegrationTests/Stubs/URLConfig.swift | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index fa668e328..bafe7c5f1 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -1860,6 +1860,7 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.IntegrationTests; PRODUCT_NAME = "$(TARGET_NAME)"; + RELAY_HOST = relay.walletconnect.com; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -1878,6 +1879,7 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.walletconnect.IntegrationTests; PRODUCT_NAME = "$(TARGET_NAME)"; + RELAY_HOST = relay.walletconnect.com; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/IntegrationTests.xcscheme b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/IntegrationTests.xcscheme index 9ce1c4f02..54745ba8d 100644 --- a/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/IntegrationTests.xcscheme +++ b/Example/ExampleApp.xcodeproj/xcshareddata/xcschemes/IntegrationTests.xcscheme @@ -11,6 +11,15 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "NO"> + + + + Date: Wed, 7 Sep 2022 19:10:19 +0300 Subject: [PATCH 85/92] Correct RELAY_HOST --- .github/actions/ci/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index cc392e23a..0f8b76fc0 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -28,7 +28,7 @@ runs: -scheme IntegrationTests \ -clonedSourcePackagesDirPath SourcePackagesCache \ -destination 'platform=iOS Simulator,name=iPhone 13' \ - RELAY_HOST='host.host' \ + RELAY_HOST='relay.walletconnect.com' \ test" # Wallet build From eaef786647f05d6f7e8c87cee616f6a081b2b762 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Thu, 8 Sep 2022 10:00:38 +0200 Subject: [PATCH 86/92] rename cacao params --- Sources/Auth/Services/App/AppRespondSubscriber.swift | 6 +++--- Sources/Auth/Services/Wallet/WalletRespondService.swift | 2 +- Sources/Auth/Types/Cacao/Cacao.swift | 6 +++--- .../Auth/Types/ProtocolRPCParams/AuthResponseParams.swift | 6 +----- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Sources/Auth/Services/App/AppRespondSubscriber.swift b/Sources/Auth/Services/App/AppRespondSubscriber.swift index 5c59d1128..2b3b353fa 100644 --- a/Sources/Auth/Services/App/AppRespondSubscriber.swift +++ b/Sources/Auth/Services/App/AppRespondSubscriber.swift @@ -49,14 +49,14 @@ class AppRespondSubscriber { let requestPayload = payload.request guard - let address = try? DIDPKH(iss: cacao.payload.iss).account.address, - let message = try? messageFormatter.formatMessage(from: cacao.payload) + let address = try? DIDPKH(iss: cacao.p.iss).account.address, + let message = try? messageFormatter.formatMessage(from: cacao.p) else { self.onResponse?(requestId, .failure(.malformedResponseParams)); return } guard messageFormatter.formatMessage(from: requestPayload.payloadParams, address: address) == message else { self.onResponse?(requestId, .failure(.messageCompromised)); return } - guard let _ = try? signatureVerifier.verify(signature: cacao.signature, message: message, address: address) + guard let _ = try? signatureVerifier.verify(signature: cacao.s, message: message, address: address) else { self.onResponse?(requestId, .failure(.signatureVerificationFailed)); return } onResponse?(requestId, .success(cacao)) diff --git a/Sources/Auth/Services/Wallet/WalletRespondService.swift b/Sources/Auth/Services/Wallet/WalletRespondService.swift index 31e6b1ae5..f9a53b10d 100644 --- a/Sources/Auth/Services/Wallet/WalletRespondService.swift +++ b/Sources/Auth/Services/Wallet/WalletRespondService.swift @@ -36,7 +36,7 @@ actor WalletRespondService { let didpkh = DIDPKH(account: account) let header = CacaoHeader(t: "eip4361") let payload = CacaoPayload(params: authRequestParams.payloadParams, didpkh: didpkh) - let responseParams = AuthResponseParams(header: header, payload: payload, signature: signature) + let responseParams = AuthResponseParams(h: header, p: payload, s: signature) let response = RPCResponse(id: requestId, result: responseParams) try await networkingInteractor.respond(topic: topic, response: response, tag: AuthProtocolMethod.authRequest.responseTag, envelopeType: .type1(pubKey: keys.publicKey.rawRepresentation)) diff --git a/Sources/Auth/Types/Cacao/Cacao.swift b/Sources/Auth/Types/Cacao/Cacao.swift index 003316e42..a82da2ab4 100644 --- a/Sources/Auth/Types/Cacao/Cacao.swift +++ b/Sources/Auth/Types/Cacao/Cacao.swift @@ -4,7 +4,7 @@ import Foundation /// /// specs at: https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-74.md public struct Cacao: Codable, Equatable { - let header: CacaoHeader - let payload: CacaoPayload - let signature: CacaoSignature + let h: CacaoHeader + let p: CacaoPayload + let s: CacaoSignature } diff --git a/Sources/Auth/Types/ProtocolRPCParams/AuthResponseParams.swift b/Sources/Auth/Types/ProtocolRPCParams/AuthResponseParams.swift index d8e6d9fcf..b3284956a 100644 --- a/Sources/Auth/Types/ProtocolRPCParams/AuthResponseParams.swift +++ b/Sources/Auth/Types/ProtocolRPCParams/AuthResponseParams.swift @@ -2,8 +2,4 @@ import Foundation import WalletConnectUtils /// wc_authRequest RPC method respond param -struct AuthResponseParams: Codable, Equatable { - let header: CacaoHeader - let payload: CacaoPayload - let signature: CacaoSignature -} +typealias AuthResponseParams = Cacao From 3de1a4ed7f1b133b5b529949efad9ed1ff27c28e Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Thu, 8 Sep 2022 10:24:00 +0200 Subject: [PATCH 87/92] fix tests --- Tests/AuthTests/AppRespondSubscriberTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AuthTests/AppRespondSubscriberTests.swift b/Tests/AuthTests/AppRespondSubscriberTests.swift index 686c0c830..98b8f47dd 100644 --- a/Tests/AuthTests/AppRespondSubscriberTests.swift +++ b/Tests/AuthTests/AppRespondSubscriberTests.swift @@ -58,7 +58,7 @@ class AppRespondSubscriberTests: XCTestCase { let message = try! messageFormatter.formatMessage(from: payload) let cacaoSignature = try! messageSigner.sign(message: message, privateKey: prvKey) - let cacao = Cacao(header: header, payload: payload, signature: cacaoSignature) + let cacao = Cacao(h: header, p: payload, s: cacaoSignature) let response = RPCResponse(id: requestId, result: cacao) networkingInteractor.responsePublisherSubject.send((topic, request, response)) From addd9c427faffde99a987d7af3437990e416bff6 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Thu, 8 Sep 2022 10:45:12 +0200 Subject: [PATCH 88/92] move Pairing to wc pairing package --- Sources/Auth/AuthClient.swift | 4 ++++ Sources/Auth/Services/Common/PairingsProvider.swift | 9 +++++++++ Sources/Auth/Types/Aliases/Pairing.swift | 4 ++++ .../Types}/Pairing.swift | 6 ++++++ Sources/WalletConnectSign/Types/Aliases/Pairing.swift | 4 ++++ 5 files changed, 27 insertions(+) create mode 100644 Sources/Auth/Services/Common/PairingsProvider.swift create mode 100644 Sources/Auth/Types/Aliases/Pairing.swift rename Sources/{WalletConnectSign => WalletConnectPairing/Types}/Pairing.swift (54%) create mode 100644 Sources/WalletConnectSign/Types/Aliases/Pairing.swift diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index 5e7e6ad08..38db01e7c 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -153,6 +153,10 @@ public class AuthClient { try await pingService.ping(topic: topic) } + public func getPairings() -> [Pairing] { + + } + /// Query pending authentication requests /// - Returns: Pending authentication requests public func getPendingRequests() throws -> [AuthRequest] { diff --git a/Sources/Auth/Services/Common/PairingsProvider.swift b/Sources/Auth/Services/Common/PairingsProvider.swift new file mode 100644 index 000000000..4001316af --- /dev/null +++ b/Sources/Auth/Services/Common/PairingsProvider.swift @@ -0,0 +1,9 @@ +import Foundation +import WalletConnectSign + +class PairingsProvider { + + func getPairings() -> [Pairing] { + fatalError() + } +} diff --git a/Sources/Auth/Types/Aliases/Pairing.swift b/Sources/Auth/Types/Aliases/Pairing.swift new file mode 100644 index 000000000..7a09951e2 --- /dev/null +++ b/Sources/Auth/Types/Aliases/Pairing.swift @@ -0,0 +1,4 @@ +import Foundation +import WalletConnectPairing + +public typealias Pairing = WalletConnectPairing.Pairing diff --git a/Sources/WalletConnectSign/Pairing.swift b/Sources/WalletConnectPairing/Types/Pairing.swift similarity index 54% rename from Sources/WalletConnectSign/Pairing.swift rename to Sources/WalletConnectPairing/Types/Pairing.swift index 4b459ebbe..f9886d6a2 100644 --- a/Sources/WalletConnectSign/Pairing.swift +++ b/Sources/WalletConnectPairing/Types/Pairing.swift @@ -6,4 +6,10 @@ public struct Pairing { public let topic: String public let peer: AppMetadata? public let expiryDate: Date + + public init(topic: String, peer: AppMetadata?, expiryDate: Date) { + self.topic = topic + self.peer = peer + self.expiryDate = expiryDate + } } diff --git a/Sources/WalletConnectSign/Types/Aliases/Pairing.swift b/Sources/WalletConnectSign/Types/Aliases/Pairing.swift new file mode 100644 index 000000000..7a09951e2 --- /dev/null +++ b/Sources/WalletConnectSign/Types/Aliases/Pairing.swift @@ -0,0 +1,4 @@ +import Foundation +import WalletConnectPairing + +public typealias Pairing = WalletConnectPairing.Pairing From b6078fb77b33f3cfc89f4360c545ab0dc7250737 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Thu, 8 Sep 2022 11:30:09 +0200 Subject: [PATCH 89/92] savepoint --- Sources/Auth/AuthClient.swift | 7 +++++-- Sources/Auth/AuthClientFactory.swift | 4 +++- Sources/Auth/Services/Common/PairingsProvider.swift | 12 +++++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index 38db01e7c..c75a2895e 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -60,6 +60,7 @@ public class AuthClient { private let pairingStorage: WCPairingStorage private let pendingRequestsProvider: PendingRequestsProvider private let pingService: PairingPingService + private let pairingsProvider: PairingsProvider private var account: Account? init(appPairService: AppPairService, @@ -75,7 +76,8 @@ public class AuthClient { logger: ConsoleLogging, pairingStorage: WCPairingStorage, socketConnectionStatusPublisher: AnyPublisher, - pingService: PairingPingService + pingService: PairingPingService, + pairingsProvider: PairingsProvider ) { self.appPairService = appPairService self.appRequestService = appRequestService @@ -91,6 +93,7 @@ public class AuthClient { self.socketConnectionStatusPublisher = socketConnectionStatusPublisher self.deletePairingService = deletePairingService self.pingService = pingService + self.pairingsProvider = pairingsProvider setUpPublishers() } @@ -154,7 +157,7 @@ public class AuthClient { } public func getPairings() -> [Pairing] { - + pairingsProvider.getPairings() } /// Query pending authentication requests diff --git a/Sources/Auth/AuthClientFactory.swift b/Sources/Auth/AuthClientFactory.swift index ba307a926..e0fe3f586 100644 --- a/Sources/Auth/AuthClientFactory.swift +++ b/Sources/Auth/AuthClientFactory.swift @@ -34,6 +34,7 @@ public struct AuthClientFactory { let cleanupService = CleanupService(pairingStore: pairingStore, kms: kms) let deletePairingService = DeletePairingService(networkingInteractor: networkingInteractor, kms: kms, pairingStorage: pairingStore, logger: logger) let pingService = PairingPingService(pairingStorage: pairingStore, networkingInteractor: networkingInteractor, logger: logger) + let pairingsProvider = PairingsProvider(pairingStorage: pairingStore) return AuthClient(appPairService: appPairService, appRequestService: appRequestService, @@ -47,6 +48,7 @@ public struct AuthClientFactory { logger: logger, pairingStorage: pairingStore, socketConnectionStatusPublisher: relayClient.socketConnectionStatusPublisher, - pingService: pingService) + pingService: pingService, + pairingsProvider: pairingsProvider) } } diff --git a/Sources/Auth/Services/Common/PairingsProvider.swift b/Sources/Auth/Services/Common/PairingsProvider.swift index 4001316af..081f3fa6d 100644 --- a/Sources/Auth/Services/Common/PairingsProvider.swift +++ b/Sources/Auth/Services/Common/PairingsProvider.swift @@ -1,9 +1,15 @@ import Foundation -import WalletConnectSign +import WalletConnectPairing -class PairingsProvider { +public class PairingsProvider { + private let pairingStorage: WCPairingStorage + + public init(pairingStorage: WCPairingStorage) { + self.pairingStorage = pairingStorage + } func getPairings() -> [Pairing] { - fatalError() + pairingStorage.getAll() + .map {Pairing(topic: $0.topic, peer: $0.peerMetadata, expiryDate: $0.expiryDate)} } } From 0123c88e08f7f18904c38a772cf1937035098527 Mon Sep 17 00:00:00 2001 From: Derek Date: Thu, 8 Sep 2022 12:26:25 -0400 Subject: [PATCH 90/92] feat: allow user to pass relay host --- .github/actions/ci/action.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml index 0f8b76fc0..7e1880db3 100644 --- a/.github/actions/ci/action.yml +++ b/.github/actions/ci/action.yml @@ -4,6 +4,10 @@ inputs: type: description: 'The type of CI step to run' required: true + relay-endpoint: + description: 'The endpoint of the relay e.g. relay.walletconnect.com' + required: false + default: 'relay.walletconnect.com' runs: using: "composite" @@ -23,12 +27,14 @@ runs: - name: Run integration tests if: inputs.type == 'integration-tests' shell: bash + env: + RELAY_ENDPOINT: ${{ inputs.relay-endpoint }} run: "xcodebuild \ -project Example/ExampleApp.xcodeproj \ -scheme IntegrationTests \ -clonedSourcePackagesDirPath SourcePackagesCache \ -destination 'platform=iOS Simulator,name=iPhone 13' \ - RELAY_HOST='relay.walletconnect.com' \ + RELAY_HOST='$RELAY_ENDPOINT' \ test" # Wallet build From be30e1fe979266e165a52f1dfed4be496bcc8673 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Fri, 9 Sep 2022 12:26:41 +0200 Subject: [PATCH 91/92] update user agent --- Sources/WalletConnectRelay/EnvironmentInfo.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/WalletConnectRelay/EnvironmentInfo.swift b/Sources/WalletConnectRelay/EnvironmentInfo.swift index f1d61fc2f..9e5f873ef 100644 --- a/Sources/WalletConnectRelay/EnvironmentInfo.swift +++ b/Sources/WalletConnectRelay/EnvironmentInfo.swift @@ -18,7 +18,7 @@ enum EnvironmentInfo { } static var sdkVersion: String { - "v0.10.1-rc.0" + "v0.10.2-rc.0" } static var operatingSystem: String { From a8f22e2c7d17129e3d7310b07b6a30c3593e4be7 Mon Sep 17 00:00:00 2001 From: Bartosz Rozwarski Date: Fri, 9 Sep 2022 13:52:59 +0200 Subject: [PATCH 92/92] change project id --- Example/DApp/SceneDelegate.swift | 2 +- Example/ExampleApp/SceneDelegate.swift | 2 +- Example/IntegrationTests/Auth/AuthTests.swift | 2 +- Example/IntegrationTests/Chat/ChatTests.swift | 2 +- Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift | 2 +- Example/IntegrationTests/Sign/SignClientTests.swift | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Example/DApp/SceneDelegate.swift b/Example/DApp/SceneDelegate.swift index eeaaf2369..d130770b8 100644 --- a/Example/DApp/SceneDelegate.swift +++ b/Example/DApp/SceneDelegate.swift @@ -18,7 +18,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { private let authCoordinator = AuthCoordinator() func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - Relay.configure(projectId: "8ba9ee138960775e5231b70cc5ef1c3a", socketFactory: SocketFactory()) + Relay.configure(projectId: "3ca2919724fbfa5456a25194e369a8b4", socketFactory: SocketFactory()) setupWindow(scene: scene) } diff --git a/Example/ExampleApp/SceneDelegate.swift b/Example/ExampleApp/SceneDelegate.swift index 83b087ae3..7439651a2 100644 --- a/Example/ExampleApp/SceneDelegate.swift +++ b/Example/ExampleApp/SceneDelegate.swift @@ -23,7 +23,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { url: "example.wallet", icons: ["https://avatars.githubusercontent.com/u/37784886"]) - Relay.configure(projectId: "8ba9ee138960775e5231b70cc5ef1c3a", socketFactory: SocketFactory()) + Relay.configure(projectId: "3ca2919724fbfa5456a25194e369a8b4", socketFactory: SocketFactory()) Sign.configure(metadata: metadata) if CommandLine.arguments.contains("-cleanInstall") { diff --git a/Example/IntegrationTests/Auth/AuthTests.swift b/Example/IntegrationTests/Auth/AuthTests.swift index 2a39446a4..6abe13983 100644 --- a/Example/IntegrationTests/Auth/AuthTests.swift +++ b/Example/IntegrationTests/Auth/AuthTests.swift @@ -37,7 +37,7 @@ final class AuthTests: XCTestCase { func makeClient(prefix: String, account: Account? = nil) -> AuthClient { let logger = ConsoleLogger(suffix: prefix, loggingLevel: .debug) - let projectId = "8ba9ee138960775e5231b70cc5ef1c3a" + let projectId = "3ca2919724fbfa5456a25194e369a8b4" let keychain = KeychainStorageMock() let relayClient = RelayClient(relayHost: URLConfig.relayHost, projectId: projectId, keychainStorage: keychain, socketFactory: SocketFactory(), logger: logger) diff --git a/Example/IntegrationTests/Chat/ChatTests.swift b/Example/IntegrationTests/Chat/ChatTests.swift index 8483bd065..191ea398f 100644 --- a/Example/IntegrationTests/Chat/ChatTests.swift +++ b/Example/IntegrationTests/Chat/ChatTests.swift @@ -41,7 +41,7 @@ final class ChatTests: XCTestCase { func makeClient(prefix: String) -> ChatClient { let logger = ConsoleLogger(suffix: prefix, loggingLevel: .debug) - let projectId = "8ba9ee138960775e5231b70cc5ef1c3a" + let projectId = "3ca2919724fbfa5456a25194e369a8b4" let keychain = KeychainStorageMock() let relayClient = RelayClient(relayHost: URLConfig.relayHost, projectId: projectId, keychainStorage: keychain, socketFactory: SocketFactory(), logger: logger) return ChatClientFactory.create(registry: registry, relayClient: relayClient, kms: KeyManagementService(keychain: keychain), logger: logger, keyValueStorage: RuntimeKeyValueStorage()) diff --git a/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift b/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift index a8ad1d46d..4e4252a19 100644 --- a/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift +++ b/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift @@ -9,7 +9,7 @@ final class RelayClientEndToEndTests: XCTestCase { let defaultTimeout: TimeInterval = 10 - let projectId = "8ba9ee138960775e5231b70cc5ef1c3a" + let projectId = "3ca2919724fbfa5456a25194e369a8b4" private var publishers = Set() func makeRelayClient() -> RelayClient { diff --git a/Example/IntegrationTests/Sign/SignClientTests.swift b/Example/IntegrationTests/Sign/SignClientTests.swift index ee785b4c3..2e4ce354f 100644 --- a/Example/IntegrationTests/Sign/SignClientTests.swift +++ b/Example/IntegrationTests/Sign/SignClientTests.swift @@ -13,7 +13,7 @@ final class SignClientTests: XCTestCase { static private func makeClientDelegate( name: String, - projectId: String = "8ba9ee138960775e5231b70cc5ef1c3a" + projectId: String = "3ca2919724fbfa5456a25194e369a8b4" ) -> ClientDelegate { let logger = ConsoleLogger(suffix: name, loggingLevel: .debug) let keychain = KeychainStorageMock()