diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS.xcodeproj/project.pbxproj b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS.xcodeproj/project.pbxproj index 3aecedf..b6f2e05 100644 --- a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS.xcodeproj/project.pbxproj +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS.xcodeproj/project.pbxproj @@ -91,6 +91,10 @@ 923C372E28EB0BEE003CFC03 /* continents.json in Resources */ = {isa = PBXBuildFile; fileRef = 923C372D28EB0BED003CFC03 /* continents.json */; }; 923C373128EB0CB2003CFC03 /* CountryExtra.swift in Sources */ = {isa = PBXBuildFile; fileRef = 923C373028EB0CB2003CFC03 /* CountryExtra.swift */; }; 923C373328EB150A003CFC03 /* GetContinentResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 923C373228EB150A003CFC03 /* GetContinentResponse.swift */; }; + 923C373728EC791E003CFC03 /* WalletRouteCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 923C373628EC791E003CFC03 /* WalletRouteCollection.swift */; }; + 923C373A28EC79E8003CFC03 /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 923C373928EC79E8003CFC03 /* Wallet.swift */; }; + 923C373C28ED79A2003CFC03 /* Mnemonic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 923C373B28ED79A2003CFC03 /* Mnemonic.swift */; }; + 923C373E28EDB109003CFC03 /* PostMnemonicResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 923C373D28EDB109003CFC03 /* PostMnemonicResponse.swift */; }; 92D6B3FD28E19E20004CF9DF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92D6B3FC28E19E20004CF9DF /* AppDelegate.swift */; }; 92D6B3FF28E19E20004CF9DF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92D6B3FE28E19E20004CF9DF /* SceneDelegate.swift */; }; 92D6B40128E19E20004CF9DF /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92D6B40028E19E20004CF9DF /* ViewController.swift */; }; @@ -247,6 +251,10 @@ 923C372D28EB0BED003CFC03 /* continents.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = continents.json; sourceTree = ""; }; 923C373028EB0CB2003CFC03 /* CountryExtra.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryExtra.swift; sourceTree = ""; }; 923C373228EB150A003CFC03 /* GetContinentResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetContinentResponse.swift; sourceTree = ""; }; + 923C373628EC791E003CFC03 /* WalletRouteCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletRouteCollection.swift; sourceTree = ""; }; + 923C373928EC79E8003CFC03 /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = ""; }; + 923C373B28ED79A2003CFC03 /* Mnemonic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mnemonic.swift; sourceTree = ""; }; + 923C373D28EDB109003CFC03 /* PostMnemonicResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostMnemonicResponse.swift; sourceTree = ""; }; 92D6B3F928E19E20004CF9DF /* SOLARdVPNCommunityCoreiOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SOLARdVPNCommunityCoreiOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; 92D6B3FC28E19E20004CF9DF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 92D6B3FE28E19E20004CF9DF /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -597,6 +605,7 @@ 923C372328E72FDA003CFC03 /* RouteCollections */ = { isa = PBXGroup; children = ( + 923C373528EC7904003CFC03 /* Wallet */, 923C372428E72FF0003CFC03 /* Nodes */, 225A83A428EC297500F66619 /* Tunnel */, ); @@ -630,6 +639,25 @@ path = Models; sourceTree = ""; }; + 923C373528EC7904003CFC03 /* Wallet */ = { + isa = PBXGroup; + children = ( + 923C373828EC79D9003CFC03 /* Models */, + 923C373628EC791E003CFC03 /* WalletRouteCollection.swift */, + ); + path = Wallet; + sourceTree = ""; + }; + 923C373828EC79D9003CFC03 /* Models */ = { + isa = PBXGroup; + children = ( + 923C373B28ED79A2003CFC03 /* Mnemonic.swift */, + 923C373928EC79E8003CFC03 /* Wallet.swift */, + 923C373D28EDB109003CFC03 /* PostMnemonicResponse.swift */, + ); + path = Models; + sourceTree = ""; + }; 92D6B3F028E19E20004CF9DF = { isa = PBXGroup; children = ( @@ -1001,6 +1029,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 923C373A28EC79E8003CFC03 /* Wallet.swift in Sources */, 92D6B40128E19E20004CF9DF /* ViewController.swift in Sources */, 22C3EEED28E48D9A007DB01B /* SentinelNode+Ext.swift in Sources */, 923C372628EAE93E003CFC03 /* Encoder.swift in Sources */, @@ -1015,6 +1044,7 @@ 92D6B46F28E47E2E004CF9DF /* NodesService.swift in Sources */, 22C3EEA728E4733C007DB01B /* Config.swift in Sources */, 22C3EEE528E48D9A007DB01B /* NEVPNManager+Ext.swift in Sources */, + 923C373C28ED79A2003CFC03 /* Mnemonic.swift in Sources */, 22488AA028E72C4E00FE29C3 /* UserDefaultsStorageStrategy.swift in Sources */, 22C3EECA28E48B52007DB01B /* TunnelManager.swift in Sources */, 22C3EED028E48B52007DB01B /* TunnelModel.swift in Sources */, @@ -1041,6 +1071,7 @@ 22C3EED428E48B52007DB01B /* TunnelsService.swift in Sources */, 225A839D28EAE54400F66619 /* SecurityService.swift in Sources */, 22C3EEE628E48D9A007DB01B /* TunnelConfiguration+Ext.swift in Sources */, + 923C373728EC791E003CFC03 /* WalletRouteCollection.swift in Sources */, 22C3EEE828E48D9A007DB01B /* Serializer.swift in Sources */, 22C3EEE728E48D9A007DB01B /* NEVPNStatus+Ext.swift in Sources */, 92D6B41428E1E133004CF9DF /* DVPNServer.swift in Sources */, @@ -1064,6 +1095,7 @@ 22C3EEAA28E4733C007DB01B /* NETunnelProviderProtocol+Extension.swift in Sources */, 22488AA328E72C4E00FE29C3 /* StoresWallet.swift in Sources */, 22C3EED128E48B52007DB01B /* TunnelStatus.swift in Sources */, + 923C373E28EDB109003CFC03 /* PostMnemonicResponse.swift in Sources */, 923C373328EB150A003CFC03 /* GetContinentResponse.swift in Sources */, 923C372A28EAFAA5003CFC03 /* NodesServiceError.swift in Sources */, 225A836C28EACE6C00F66619 /* ConnectionNodeModel.swift in Sources */, diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Context/CommonContext.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Context/CommonContext.swift index 791feeb..c14d3ad 100644 --- a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Context/CommonContext.swift +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Context/CommonContext.swift @@ -98,6 +98,9 @@ extension CommonContext: HasTunnelManager {} protocol HasSessionsService { var sessionsService: SessionsServiceType { get } } extension CommonContext: HasSessionsService {} +protocol HasSecurityService { var securityService: SecurityService { get } } +extension CommonContext: HasSecurityService {} + // MARK: - Storages protocol HasConnectionInfoStorage { var connectionInfoStorage: StoresConnectInfo { get } } diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Utilities/ClientConstants.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Utilities/ClientConstants.swift index 26a89cf..9714524 100644 --- a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Utilities/ClientConstants.swift +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Common/Utilities/ClientConstants.swift @@ -18,6 +18,8 @@ enum ClientConstants { static let apiPath = "api" static let backendURL = URL(string: "https://BACKEND")! + + static let denom = "udvpn" } final class ApplicationConfiguration: ClientConnectionConfigurationType { diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/DVPNServer.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/DVPNServer.swift index f64b93a..b5ec78d 100644 --- a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/DVPNServer.swift +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/DVPNServer.swift @@ -27,6 +27,7 @@ extension DVPNServer { do { let api = app.grouped(.init(stringLiteral: ClientConstants.apiPath)) try api.register(collection: NodesRouteCollection(context: context)) + try api.register(collection: WalletRouteCollection(context: context)) try api.register( collection: TunnelRouteCollection( diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Nodes/Models/GetContinentResponse.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Nodes/Models/GetContinentResponse.swift index 1565933..0594de7 100644 --- a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Nodes/Models/GetContinentResponse.swift +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Nodes/Models/GetContinentResponse.swift @@ -10,11 +10,6 @@ import Foundation struct GetContinentResponse: Codable { let code: String let nodesCount: Int - - init(code: String, nodesCount: Int) { - self.code = code - self.nodesCount = nodesCount - } } // MARK: - Codable implementation diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/Connection/ConnectionNodeModel.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/Connection/ConnectionNodeModel.swift index 89000a6..6eb3fed 100644 --- a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/Connection/ConnectionNodeModel.swift +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Tunnel/Connection/ConnectionNodeModel.swift @@ -14,7 +14,6 @@ import GRPC private struct Constants { let timeout: TimeInterval = 15 - let denom = "udvpn" } private let constants = Constants() @@ -129,7 +128,7 @@ extension ConnectionNodeModel { case .success(let balances): guard balances .contains( - where: { $0.denom == constants.denom && Int($0.amount) ?? 0 >= self.context.walletService.fee } + where: { $0.denom == ClientConstants.denom && Int($0.amount) ?? 0 >= self.context.walletService.fee } ) else { self.delegate?.show(warning: WalletServiceError.notEnoughTokens.body) self.delegate?.set(isLoading: false) diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Wallet/Models/Mnemonic.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Wallet/Models/Mnemonic.swift new file mode 100644 index 0000000..dd8f14d --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Wallet/Models/Mnemonic.swift @@ -0,0 +1,12 @@ +// +// Mnemonic.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Viktoriia Kostyleva on 05.10.2022. +// + +import Foundation + +struct Mnemonic: Codable { + let mnemonic: String +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Wallet/Models/PostMnemonicResponse.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Wallet/Models/PostMnemonicResponse.swift new file mode 100644 index 0000000..3d822c4 --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Wallet/Models/PostMnemonicResponse.swift @@ -0,0 +1,13 @@ +// +// PostMnemonicResponse.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Viktoriia Kostyleva on 05.10.2022. +// + +import Foundation + +struct PostMnemonicResponse: Codable { + let wallet: Wallet + let mnemonic: String +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Wallet/Models/Wallet.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Wallet/Models/Wallet.swift new file mode 100644 index 0000000..a45125a --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Wallet/Models/Wallet.swift @@ -0,0 +1,14 @@ +// +// Wallet.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Viktoriia Kostyleva on 04.10.2022. +// + +import Foundation + +struct Wallet: Codable { + let address: String + let balance: Int + let currency: String +} diff --git a/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Wallet/WalletRouteCollection.swift b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Wallet/WalletRouteCollection.swift new file mode 100644 index 0000000..d420e66 --- /dev/null +++ b/SolardVPNCommunityCoreiOS/SOLARdVPNCommunityCoreiOS/Root/RouteCollections/Wallet/WalletRouteCollection.swift @@ -0,0 +1,140 @@ +// +// WalletRouteCollection.swift +// SOLARdVPNCommunityCoreiOS +// +// Created by Viktoriia Kostyleva on 04.10.2022. +// + +import Foundation +import Vapor + +struct WalletRouteCollection: RouteCollection { + let context: HasSecurityService & HasWalletStorage & HasWalletService + + func boot(routes: RoutesBuilder) throws { + routes.get("wallet", use: getWallet) + routes.put("wallet", use: putWallet) + routes.post("wallet", use: postWallet) + routes.delete("wallet", use: deleteWallet) + } +} + +extension WalletRouteCollection { + func getWallet(_ req: Request) async throws -> String { + try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) in + getWallet() { result in + switch result { + case let .failure(error): + continuation.resume(throwing: Abort(.init(statusCode: 500), reason: error.localizedDescription)) + + case let .success(wallet): + Encoder.encode(model: wallet, continuation: continuation) + } + } + }) + } + + func putWallet(_ req: Request) async throws -> String { + let body = try req.content.decode(Mnemonic.self) + + let mnemonic = body.mnemonic.components(separatedBy: .whitespaces) + + return try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) in + switch context.securityService.restore(from: mnemonic) { + case .failure(let error): + continuation.resume(throwing: Abort(.init(statusCode: 500), reason: error.localizedDescription)) + + case .success(let result): + guard context.securityService.save(mnemonics: mnemonic, for: result) else { + continuation.resume(throwing: Abort(.init(statusCode: 500), reason: "Creation failed")) + return + } + context.walletStorage.set(wallet: result) + context.updateWalletContext() + + getWallet() { result in + switch result { + case let .failure(error): + continuation.resume(throwing: Abort(.init(statusCode: 500), reason: error.localizedDescription)) + + case let .success(wallet): + Encoder.encode(model: wallet, continuation: continuation) + } + } + } + }) + } + + func postWallet(_ req: Request) async throws -> String { + context.resetWalletContext() + + let address = context.walletStorage.walletAddress + + guard let mnemonic = context.securityService.loadMnemonics(for: address) else { + throw Abort(.init(statusCode: 500), reason: "Failed to liad mnemonic") + } + + return try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) in + getWallet() { result in + switch result { + case let .failure(error): + continuation.resume(throwing: Abort(.init(statusCode: 500), reason: error.localizedDescription)) + case let .success(wallet): + let response = PostMnemonicResponse(wallet: wallet, mnemonic: mnemonic.joined(separator: " ")) + Encoder.encode(model: response, continuation: continuation) + } + } + }) + } + + func deleteWallet(_ req: Request) -> Response { + context.resetWalletContext() + return Response() + } +} + +// MARK: - Private + +extension WalletRouteCollection { + private func getWallet( + completion: @escaping (Result) -> Void + ) { + fetchBalance() { result in + switch result { + case let .failure(error): + completion(.failure(error)) + + case let .success(balance): + let address = context.walletStorage.walletAddress + let wallet = Wallet(address: address, balance: balance, currency: ClientConstants.denom) + + completion(.success(wallet)) + } + } + } + + private func fetchBalance( + completion: @escaping (Result) -> Void + ) { + context.walletService.fetchBalance { result in + switch result { + case let .failure(error): + completion(.failure(error)) + + case let .success(balances): + guard let balance = balances.first(where: { $0.denom == ClientConstants.denom }) else { + completion(.success(0)) + return + } + + guard let amount = Int(balance.amount) else { + // TODO: Call completion with error + + return + } + + completion(.success(amount)) + } + } + } +}