diff --git a/FRW/Modules/TrustProvider/TrustJSMessageHandler.swift b/FRW/Modules/TrustProvider/TrustJSMessageHandler.swift index 9f224a9a..be97882b 100644 --- a/FRW/Modules/TrustProvider/TrustJSMessageHandler.swift +++ b/FRW/Modules/TrustProvider/TrustJSMessageHandler.swift @@ -17,6 +17,8 @@ import web3swift import Web3Wallet import WebKit +// MARK: - TrustJSMessageHandler + class TrustJSMessageHandler: NSObject { weak var webVC: BrowserViewController? @@ -31,8 +33,7 @@ class TrustJSMessageHandler: NSObject { extension TrustJSMessageHandler { private func extractMethod(json: [String: Any]) -> TrustAppMethod? { - guard - let name = json["name"] as? String + guard let name = json["name"] as? String else { return nil } @@ -40,8 +41,7 @@ extension TrustJSMessageHandler { } private func extractNetwork(json: [String: Any]) -> ProviderNetwork? { - guard - let network = json["network"] as? String + guard let network = json["network"] as? String else { return nil } @@ -49,10 +49,9 @@ extension TrustJSMessageHandler { } private func extractMessage(json: [String: Any]) -> Data? { - guard - let params = json["object"] as? [String: Any], - let string = params["data"] as? String, - let data = Data(hexString: string) + guard let params = json["object"] as? [String: Any], + let string = params["data"] as? String, + let data = Data(hexString: string) else { return nil } @@ -60,9 +59,8 @@ extension TrustJSMessageHandler { } private func extractRaw(json: [String: Any]) -> String? { - guard - let params = json["object"] as? [String: Any], - let raw = params["raw"] as? String + guard let params = json["object"] as? [String: Any], + let raw = params["raw"] as? String else { return nil } @@ -77,11 +75,10 @@ extension TrustJSMessageHandler { } private func extractEthereumChainId(json: [String: Any]) -> Int? { - guard - let params = json["object"] as? [String: Any], - let string = params["chainId"] as? String, - let chainId = Int(String(string.dropFirst(2)), radix: 16), - chainId > 0 + guard let params = json["object"] as? [String: Any], + let string = params["chainId"] as? String, + let chainId = Int(String(string.dropFirst(2)), radix: 16), + chainId > 0 else { return nil } @@ -89,6 +86,8 @@ extension TrustJSMessageHandler { } } +// MARK: WKScriptMessageHandler + extension TrustJSMessageHandler: WKScriptMessageHandler { func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) { let json = message.json @@ -150,8 +149,7 @@ extension TrustJSMessageHandler: WKScriptMessageHandler { log.info("[Trust] switchEthereumChain") switch network { case .ethereum: - guard - let chainId = extractEthereumChainId(json: json) + guard let chainId = extractEthereumChainId(json: json) else { print("chain id is invalid") return @@ -177,12 +175,13 @@ extension TrustJSMessageHandler { let title = webVC?.webView.title ?? "unknown" let chainID = LocalUserDefaults.shared.flowNetwork.toFlowType() - let vm = BrowserAuthnViewModel(title: title, - url: url?.host ?? "unknown", - logo: url?.absoluteString.toFavIcon()?.absoluteString, - walletAddress: address, - network: chainID) - { [weak self] result in + let vm = BrowserAuthnViewModel( + title: title, + url: url?.host ?? "unknown", + logo: url?.absoluteString.toFavIcon()?.absoluteString, + walletAddress: address, + network: chainID + ) { [weak self] result in guard let self = self else { return } @@ -204,10 +203,19 @@ extension TrustJSMessageHandler { Router.route(to: RouteMap.Explore.authn(vm)) } - MoveAssetsAction.shared.startBrowserWithMoveAssets(appName: webVC?.webView.title, callback: callback) + MoveAssetsAction.shared.startBrowserWithMoveAssets( + appName: webVC?.webView.title, + callback: callback + ) } - private func handleSignPersonal(url: URL?, network: ProviderNetwork, id: Int64, data: Data, addPrefix _: Bool) { + private func handleSignPersonal( + url: URL?, + network: ProviderNetwork, + id: Int64, + data: Data, + addPrefix _: Bool + ) { Task { await TrustJSMessageHandler.checkCoa() } @@ -216,11 +224,12 @@ extension TrustJSMessageHandler { title = "unknown" } - let vm = BrowserSignMessageViewModel(title: title, - url: url?.absoluteString ?? "unknown", - logo: url?.absoluteString.toFavIcon()?.absoluteString, - cadence: data.hexString) - { [weak self] result in + let vm = BrowserSignMessageViewModel( + title: title, + url: url?.absoluteString ?? "unknown", + logo: url?.absoluteString.toFavIcon()?.absoluteString, + cadence: data.hexString + ) { [weak self] result in guard let self = self else { return } @@ -239,11 +248,20 @@ extension TrustJSMessageHandler { return } let keyIndex = BigUInt(WalletManager.shared.keyIndex) - let proof = COAOwnershipProof(keyIninces: [keyIndex], address: address.data, capabilityPath: "evm", signatures: [sig]) + let proof = COAOwnershipProof( + keyIninces: [keyIndex], + address: address.data, + capabilityPath: "evm", + signatures: [sig] + ) guard let encoded = RLP.encode(proof.rlpList) else { return } - webVC?.webView.tw.send(network: .ethereum, result: encoded.hexString.addHexPrefix(), to: id) + webVC?.webView.tw.send( + network: .ethereum, + result: encoded.hexString.addHexPrefix(), + to: id + ) } else { webVC?.webView.tw.send(network: .ethereum, error: "Canceled", to: id) } @@ -261,7 +279,12 @@ extension TrustJSMessageHandler { title = "unknown" } - let vm = BrowserSignTypedMessageViewModel(title: title, urlString: url?.absoluteString ?? "unknown", logo: url?.absoluteString.toFavIcon()?.absoluteString, rawString: raw) { [weak self] result in + let vm = BrowserSignTypedMessageViewModel( + title: title, + urlString: url?.absoluteString ?? "unknown", + logo: url?.absoluteString.toFavIcon()?.absoluteString, + rawString: raw + ) { [weak self] result in guard let self = self else { return } @@ -278,11 +301,20 @@ extension TrustJSMessageHandler { return } let keyIndex = BigUInt(WalletManager.shared.keyIndex) - let proof = COAOwnershipProof(keyIninces: [keyIndex], address: address.data, capabilityPath: "evm", signatures: [sig]) + let proof = COAOwnershipProof( + keyIninces: [keyIndex], + address: address.data, + capabilityPath: "evm", + signatures: [sig] + ) guard let encoded = RLP.encode(proof.rlpList) else { return } - webVC?.webView.tw.send(network: .ethereum, result: encoded.hexString.addHexPrefix(), to: id) + webVC?.webView.tw.send( + network: .ethereum, + result: encoded.hexString.addHexPrefix(), + to: id + ) } else { webVC?.webView.tw.send(network: .ethereum, error: "Canceled", to: id) } @@ -291,7 +323,12 @@ extension TrustJSMessageHandler { Router.route(to: RouteMap.Explore.signTypedMessage(vm)) } - private func handleSendTransaction(url: URL?, network _: ProviderNetwork, id: Int64, info: [String: Any]) { + private func handleSendTransaction( + url: URL?, + network _: ProviderNetwork, + id: Int64, + info: [String: Any] + ) { var title = webVC?.webView.title ?? "unknown" if title.isEmpty { title = "unknown" @@ -314,12 +351,13 @@ extension TrustJSMessageHandler { .uint64(receiveModel.gasValue), ] - let vm = BrowserAuthzViewModel(title: title, - url: url?.absoluteString ?? "unknown", - logo: url?.absoluteString.toFavIcon()?.absoluteString, - cadence: originCadence, - arguments: args.toArguments()) - { [weak self] result in + let vm = BrowserAuthzViewModel( + title: title, + url: url?.absoluteString ?? "unknown", + logo: url?.absoluteString.toFavIcon()?.absoluteString, + cadence: originCadence, + arguments: args.toArguments() + ) { [weak self] result in guard let self = self else { self?.webVC?.webView.tw.send(network: .ethereum, error: "Canceled", to: id) @@ -333,7 +371,12 @@ extension TrustJSMessageHandler { Task { do { - let txid = try await FlowNetwork.sendTransaction(amount: receiveModel.amount, data: receiveModel.dataValue, toAddress: toAddr, gas: receiveModel.gasValue) + let txid = try await FlowNetwork.sendTransaction( + amount: receiveModel.amount, + data: receiveModel.dataValue, + toAddress: toAddr, + gas: receiveModel.gasValue + ) let holder = TransactionManager.TransactionHolder(id: txid, type: .transferCoin) TransactionManager.shared.newTransaction(holder: holder) let result = try await txid.onceSealed() @@ -387,7 +430,11 @@ extension TrustJSMessageHandler { self?.webVC?.webView.tw.sendNull(network: .ethereum, id: id) } else { log.error("Unknown chain id: \(chainId)") - self?.webVC?.webView.tw.send(network: .ethereum, error: "Unknown chain id", to: id) + self?.webVC?.webView.tw.send( + network: .ethereum, + error: "Unknown chain id", + to: id + ) } } Router.route(to: RouteMap.Explore.switchNetwork(fromId, toId, callback)) @@ -395,7 +442,7 @@ extension TrustJSMessageHandler { } private func signWithMessage(data: Data) -> Data? { - return WalletManager.shared.signSync(signableData: data) + WalletManager.shared.signSync(signableData: data) } private func cancel(id: Int64) { diff --git a/FRW/Modules/Wallet/Card/StyleView/MatrixRainView.swift b/FRW/Modules/Wallet/Card/StyleView/MatrixRainView.swift index 04aa395f..3d49f5ce 100644 --- a/FRW/Modules/Wallet/Card/StyleView/MatrixRainView.swift +++ b/FRW/Modules/Wallet/Card/StyleView/MatrixRainView.swift @@ -7,6 +7,8 @@ import SwiftUI +// MARK: - MatrixRainView + struct MatrixRainView: View { var body: some View { ZStack { @@ -19,7 +21,7 @@ struct MatrixRainView: View { // Repeating the effects until it occupied the full screen // With the help of ForEach // For Count since our font size is 25 so width/fontSize will the give the count - ForEach(1 ... Int(size.width / 25), id: \.self) { _ in + ForEach(1...Int(size.width / 25), id: \.self) { _ in MatrixRainCharacters(size: size) } } @@ -29,23 +31,27 @@ struct MatrixRainView: View { } } +// MARK: - MatrixRainCharacters + struct MatrixRainCharacters: View { var size: CGSize // MARK: Animation Properties - @State var startAnimation: Bool = false + @State + var startAnimation: Bool = false - @State var random: Int = 0 + @State + var random: Int = 0 var body: some View { // Random Height - let randomHeight: CGFloat = .random(in: (size.height / 2) ... size.height) + let randomHeight: CGFloat = .random(in: (size.height / 2)...size.height) VStack { // MARK: Iterating String - ForEach(0 ..< constant.count, id: \.self) { index in + ForEach(0.. Void +// MARK: - MoveSingleNFTViewModel - @Published var fromContact = Contact(address: "", avatar: "", contactName: "", contactType: nil, domain: nil, id: -1, username: nil) - @Published var toContact = Contact(address: "", avatar: "", contactName: "", contactType: nil, domain: nil, id: -1, username: nil) - @Published var buttonState: VPrimaryButtonState = .enabled - - var accountCount: Int = 0 +class MoveSingleNFTViewModel: ObservableObject { + // MARK: Lifecycle init(nft: NFTModel, fromChildAccount: ChildAccount? = nil, callback: @escaping () -> Void) { self.nft = nft @@ -27,35 +21,39 @@ class MoveSingleNFTViewModel: ObservableObject { loadUserInfo() let accountViewModel = MoveAccountsViewModel(selected: "") { _ in } - accountCount = accountViewModel.list.count + self.accountCount = accountViewModel.list.count } - private func loadUserInfo() { - guard let primaryAddr = WalletManager.shared.getPrimaryWalletAddressOrCustomWatchAddress() else { - return - } - if let account = fromChildAccount { - fromContact = Contact(address: account.showAddress, avatar: account.icon, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName, walletType: .link) - } else if let account = ChildAccountManager.shared.selectedChildAccount { - fromContact = Contact(address: account.showAddress, avatar: account.icon, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName, walletType: .link) - } else if let account = EVMAccountManager.shared.selectedAccount { - let user = WalletManager.shared.walletAccount.readInfo(at: account.showAddress) - fromContact = Contact(address: account.showAddress, avatar: nil, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName, user: user, walletType: .evm) - } else { - let user = WalletManager.shared.walletAccount.readInfo(at: primaryAddr) - fromContact = Contact(address: primaryAddr, avatar: nil, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: user.name, user: user, walletType: .flow) - } + // MARK: Internal - if ChildAccountManager.shared.selectedChildAccount != nil || EVMAccountManager.shared.selectedAccount != nil || fromChildAccount != nil { - let user = WalletManager.shared.walletAccount.readInfo(at: primaryAddr) - toContact = Contact(address: primaryAddr, avatar: nil, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: user.name, user: user, walletType: .flow) - } else if let account = EVMAccountManager.shared.accounts.first { - let user = WalletManager.shared.walletAccount.readInfo(at: account.showAddress) - toContact = Contact(address: account.showAddress, avatar: nil, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName, user: user, walletType: .evm) - } else if let account = ChildAccountManager.shared.childAccounts.first { - toContact = Contact(address: account.showAddress, avatar: account.icon, contactName: nil, contactType: .user, domain: nil, id: UUID().hashValue, username: account.showName, walletType: .link) - } - } + var nft: NFTModel + var fromChildAccount: ChildAccount? + var callback: () -> Void + + @Published + var fromContact = Contact( + address: "", + avatar: "", + contactName: "", + contactType: nil, + domain: nil, + id: -1, + username: nil + ) + @Published + var toContact = Contact( + address: "", + avatar: "", + contactName: "", + contactType: nil, + domain: nil, + id: -1, + username: nil + ) + @Published + var buttonState: VPrimaryButtonState = .enabled + + var accountCount: Int = 0 func closeAction() { Router.dismiss() @@ -90,11 +88,117 @@ class MoveSingleNFTViewModel: ObservableObject { } } + func updateToContact(_ contact: Contact) { + toContact = contact + } + + // MARK: Private + + private func loadUserInfo() { + guard let primaryAddr = WalletManager.shared.getPrimaryWalletAddressOrCustomWatchAddress() + else { + return + } + if let account = fromChildAccount { + fromContact = Contact( + address: account.showAddress, + avatar: account.icon, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName, + walletType: .link + ) + } else if let account = ChildAccountManager.shared.selectedChildAccount { + fromContact = Contact( + address: account.showAddress, + avatar: account.icon, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName, + walletType: .link + ) + } else if let account = EVMAccountManager.shared.selectedAccount { + let user = WalletManager.shared.walletAccount.readInfo(at: account.showAddress) + fromContact = Contact( + address: account.showAddress, + avatar: nil, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName, + user: user, + walletType: .evm + ) + } else { + let user = WalletManager.shared.walletAccount.readInfo(at: primaryAddr) + fromContact = Contact( + address: primaryAddr, + avatar: nil, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: user.name, + user: user, + walletType: .flow + ) + } + + if ChildAccountManager.shared.selectedChildAccount != nil || EVMAccountManager.shared + .selectedAccount != nil || fromChildAccount != nil { + let user = WalletManager.shared.walletAccount.readInfo(at: primaryAddr) + toContact = Contact( + address: primaryAddr, + avatar: nil, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: user.name, + user: user, + walletType: .flow + ) + } else if let account = EVMAccountManager.shared.accounts.first { + let user = WalletManager.shared.walletAccount.readInfo(at: account.showAddress) + toContact = Contact( + address: account.showAddress, + avatar: nil, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName, + user: user, + walletType: .evm + ) + } else if let account = ChildAccountManager.shared.childAccounts.first { + toContact = Contact( + address: account.showAddress, + avatar: account.icon, + contactName: nil, + contactType: .user, + domain: nil, + id: UUID().hashValue, + username: account.showName, + walletType: .link + ) + } + } + private func moveForEVM(identifier: String, nftId: UInt64) async { do { let ids: [UInt64] = [nftId] let fromEvm = EVMAccountManager.shared.selectedAccount != nil - let tid = try await FlowNetwork.bridgeNFTToEVM(identifier: identifier, ids: ids, fromEvm: fromEvm) + let tid = try await FlowNetwork.bridgeNFTToEVM( + identifier: identifier, + ids: ids, + fromEvm: fromEvm + ) let holder = TransactionManager.TransactionHolder(id: tid, type: .moveAsset) TransactionManager.shared.newTransaction(holder: holder) closeAction() @@ -119,11 +223,27 @@ class MoveSingleNFTViewModel: ObservableObject { var tid = Flow.ID(hex: "") switch (fromContact.walletType, toContact.walletType) { case (.flow, .link): - tid = try await FlowNetwork.moveNFTToChild(nftId: nftId, childAddress: toContact.address ?? "", identifier: identifier, collection: collection) + tid = try await FlowNetwork.moveNFTToChild( + nftId: nftId, + childAddress: toContact.address ?? "", + identifier: identifier, + collection: collection + ) case (.link, .flow): - tid = try await FlowNetwork.moveNFTToParent(nftId: nftId, childAddress: fromContact.address ?? "", identifier: identifier, collection: collection) + tid = try await FlowNetwork.moveNFTToParent( + nftId: nftId, + childAddress: fromContact.address ?? "", + identifier: identifier, + collection: collection + ) case (.link, .link): - tid = try await FlowNetwork.sendChildNFTToChild(nftId: nftId, childAddress: fromContact.address ?? "", toAddress: toContact.address ?? "", identifier: identifier, collection: collection) + tid = try await FlowNetwork.sendChildNFTToChild( + nftId: nftId, + childAddress: fromContact.address ?? "", + toAddress: toContact.address ?? "", + identifier: identifier, + collection: collection + ) case (.link, .evm): guard let nftIdentifier = nft.response.flowIdentifier else { return @@ -157,10 +277,6 @@ class MoveSingleNFTViewModel: ObservableObject { log.error(error) } } - - func updateToContact(_ contact: Contact) { - toContact = contact - } } extension MoveSingleNFTViewModel { diff --git a/FRW/Modules/Wallet/Send/WalletSendAmountViewModel.swift b/FRW/Modules/Wallet/Send/WalletSendAmountViewModel.swift index 8345d09d..f7f054b7 100644 --- a/FRW/Modules/Wallet/Send/WalletSendAmountViewModel.swift +++ b/FRW/Modules/Wallet/Send/WalletSendAmountViewModel.swift @@ -26,6 +26,8 @@ extension WalletSendAmountView { case invalidAddress case belowMinimum + // MARK: Internal + var desc: String { switch self { case .none: @@ -43,35 +45,13 @@ extension WalletSendAmountView { } } -class WalletSendAmountViewModel: ObservableObject { - @Published var targetContact: Contact - @Published var token: TokenModel - @Published var amountBalance: Double = 0 - @Published var coinRate: Double = 0 - - @Published var inputText: String = "" - @Published var inputTokenNum: Double = 0 - @Published var inputDollarNum: Double = 0 - var actualBalance: String = "" - - @Published var exchangeType: WalletSendAmountView.ExchangeType = .token - @Published var errorType: WalletSendAmountView.ErrorType = .none - - @Published var showConfirmView: Bool = false - - @Published var isValidToken: Bool = true - - @Published var isEmptyTransation = true - - private var isSending = false - private var cancelSets = Set() - - private var addressIsValid: Bool? +// MARK: - WalletSendAmountViewModel - private var minBalance: Double = 0.001 +class WalletSendAmountViewModel: ObservableObject { + // MARK: Lifecycle init(target: Contact, token: TokenModel) { - targetContact = target + self.targetContact = target self.token = token WalletManager.shared.$coinBalances.sink { [weak self] _ in @@ -83,20 +63,67 @@ class WalletSendAmountViewModel: ObservableObject { checkAddress() checkTransaction() fetchMinFlowBalance() - NotificationCenter.default.addObserver(self, selector: #selector(onHolderChanged(noti:)), name: .transactionStatusDidChanged, object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(onHolderChanged(noti:)), + name: .transactionStatusDidChanged, + object: nil + ) } deinit { NotificationCenter.default.removeObserver(self) } + // MARK: Internal + + @Published + var targetContact: Contact + @Published + var token: TokenModel + @Published + var amountBalance: Double = 0 + @Published + var coinRate: Double = 0 + + @Published + var inputText: String = "" + @Published + var inputTokenNum: Double = 0 + @Published + var inputDollarNum: Double = 0 + var actualBalance: String = "" + + @Published + var exchangeType: WalletSendAmountView.ExchangeType = .token + @Published + var errorType: WalletSendAmountView.ErrorType = .none + + @Published + var showConfirmView: Bool = false + + @Published + var isValidToken: Bool = true + + @Published + var isEmptyTransation = true + var amountBalanceAsDollar: Double { - return coinRate * amountBalance + coinRate * amountBalance } var isReadyForSend: Bool { - return errorType == .none && inputText.isNumber && addressIsValid == true + errorType == .none && inputText.isNumber && addressIsValid == true } + + // MARK: Private + + private var isSending = false + private var cancelSets = Set() + + private var addressIsValid: Bool? + + private var minBalance: Double = 0.001 } extension WalletSendAmountViewModel { @@ -129,7 +156,8 @@ extension WalletSendAmountViewModel { } return } - let list = try await FlowNetwork.checkTokensEnable(address: Flow.Address(hex: address)) + let list = try await FlowNetwork + .checkTokensEnable(address: Flow.Address(hex: address)) let model = list.first { $0.key.lowercased() == token.contractId.lowercased() } let isValid = model?.value DispatchQueue.main.async { @@ -216,8 +244,7 @@ extension WalletSendAmountViewModel { func maxAction() { exchangeType = .token if token.isFlowCoin, WalletManager.shared - .isCoa(targetContact.address), WalletManager.shared.isMain() - { + .isCoa(targetContact.address), WalletManager.shared.isMain() { Task { do { let topAmount = try await FlowNetwork.minFlowBalance() @@ -300,7 +327,8 @@ extension WalletSendAmountViewModel { return } - guard let address = WalletManager.shared.getPrimaryWalletAddress(), let targetAddress = targetContact.address else { + guard let address = WalletManager.shared.getPrimaryWalletAddress(), + let targetAddress = targetContact.address else { return } @@ -321,41 +349,68 @@ extension WalletSendAmountViewModel { var txId: Flow.ID? let amount = inputTokenNum.decimalValue - let fromAccountType = WalletManager.shared.isSelectedEVMAccount ? AccountType.coa : AccountType.flow + let fromAccountType = WalletManager.shared.isSelectedEVMAccount ? AccountType + .coa : AccountType.flow var toAccountType = targetAddress.isEVMAddress ? AccountType.coa : AccountType.flow - if toAccountType == .coa, targetAddress != EVMAccountManager.shared.accounts.first?.address { + if toAccountType == .coa, + targetAddress != EVMAccountManager.shared.accounts.first?.address { toAccountType = .eoa } switch (fromAccountType, toAccountType) { case (.flow, .flow): - txId = try await FlowNetwork.transferToken(to: Flow.Address(hex: targetContact.address ?? "0x"), - amount: amount, - token: token) + txId = try await FlowNetwork.transferToken( + to: Flow.Address(hex: targetContact.address ?? "0x"), + amount: amount, + token: token + ) case (.flow, .coa): txId = try await FlowNetwork.fundCoa(amount: amount) case (.coa, .flow): if token.isFlowCoin { - txId = try await FlowNetwork.sendFlowTokenFromCoaToFlow(amount: amount, address: targetAddress) + txId = try await FlowNetwork.sendFlowTokenFromCoaToFlow( + amount: amount, + address: targetAddress + ) } else { - guard let bigUIntValue = Utilities.parseToBigUInt(amount.description, units: .ether), - let flowIdentifier = self.token.flowIdentifier + guard let bigUIntValue = Utilities.parseToBigUInt( + amount.description, + units: .ether + ), + let flowIdentifier = self.token.flowIdentifier else { failureBlock() return } - txId = try await FlowNetwork.bridgeTokensFromEvmToFlow(identifier: flowIdentifier, amount: bigUIntValue, receiver: targetAddress) + txId = try await FlowNetwork.bridgeTokensFromEvmToFlow( + identifier: flowIdentifier, + amount: bigUIntValue, + receiver: targetAddress + ) } case (.coa, .coa): - txId = try await FlowNetwork.sendTransaction(amount: amount.description, data: nil, toAddress: targetAddress.stripHexPrefix(), gas: gas) + txId = try await FlowNetwork.sendTransaction( + amount: amount.description, + data: nil, + toAddress: targetAddress.stripHexPrefix(), + gas: gas + ) case (.flow, .eoa): if token.isFlowCoin { - txId = try await FlowNetwork.sendFlowToEvm(evmAddress: targetAddress.stripHexPrefix(), amount: amount, gas: gas) + txId = try await FlowNetwork.sendFlowToEvm( + evmAddress: targetAddress.stripHexPrefix(), + amount: amount, + gas: gas + ) } else { let flowIdentifier = "\(self.token.contractId).Vault" - txId = try await FlowNetwork.sendNoFlowTokenToEVM(vaultIdentifier: flowIdentifier, amount: amount, recipient: targetAddress) + txId = try await FlowNetwork.sendNoFlowTokenToEVM( + vaultIdentifier: flowIdentifier, + amount: amount, + recipient: targetAddress + ) } case (.coa, .eoa): if token.isFlowCoin { @@ -368,11 +423,23 @@ extension WalletSendAmountViewModel { ) } else { let erc20Contract = try await FlowProvider.Web3.defaultContract() - let testData = erc20Contract?.contract.method("transfer", parameters: [targetAddress, Utilities.parseToBigUInt(amount.description, units: .ether)!], extraData: nil) + let testData = erc20Contract?.contract.method( + "transfer", + parameters: [ + targetAddress, + Utilities.parseToBigUInt(amount.description, units: .ether)!, + ], + extraData: nil + ) guard let toAddress = token.getAddress() else { throw LLError.invalidAddress } - txId = try await FlowNetwork.sendTransaction(amount: "0", data: testData, toAddress: toAddress.stripHexPrefix(), gas: gas) + txId = try await FlowNetwork.sendTransaction( + amount: "0", + data: testData, + toAddress: toAddress.stripHexPrefix(), + gas: gas + ) } default: failureBlock() @@ -385,7 +452,12 @@ extension WalletSendAmountViewModel { } DispatchQueue.main.async { - let obj = CoinTransferModel(amount: self.inputTokenNum, symbol: self.token.symbol ?? "", target: self.targetContact, from: address) + let obj = CoinTransferModel( + amount: self.inputTokenNum, + symbol: self.token.symbol ?? "", + target: self.targetContact, + from: address + ) guard let data = try? JSONEncoder().encode(obj) else { debugPrint("WalletSendAmountViewModel -> obj encode failed") failureBlock() @@ -402,7 +474,11 @@ extension WalletSendAmountViewModel { let generator = UINotificationFeedbackGenerator() generator.notificationOccurred(.success) - let holder = TransactionManager.TransactionHolder(id: id, type: .transferCoin, data: data) + let holder = TransactionManager.TransactionHolder( + id: id, + type: .transferCoin, + data: data + ) TransactionManager.shared.newTransaction(holder: holder) } } catch { @@ -427,7 +503,8 @@ extension WalletSendAmountViewModel { isEmptyTransation = TransactionManager.shared.holders.count == 0 } - @objc private func onHolderChanged(noti _: Notification) { + @objc + private func onHolderChanged(noti _: Notification) { checkTransaction() } } diff --git a/FRW/Services/Manager/WalletConnect/Model/Signable.swift b/FRW/Services/Manager/WalletConnect/Model/Signable.swift index 9d825d32..84a4b844 100644 --- a/FRW/Services/Manager/WalletConnect/Model/Signable.swift +++ b/FRW/Services/Manager/WalletConnect/Model/Signable.swift @@ -10,6 +10,8 @@ import Combine import Flow import Foundation +// MARK: - FCLError + public enum FCLError: String, Error, LocalizedError { case generic case invaildURL @@ -26,18 +28,50 @@ public enum FCLError: String, Error, LocalizedError { case invaildProposer case fetchAccountFailure + // MARK: Public + public var errorDescription: String? { - return rawValue + rawValue } } +// MARK: - SignableMessage + struct SignableMessage: Codable { let addr: String // let data: [String: String]? let message: String } +// MARK: - Signable + struct Signable: Codable { + // MARK: Lifecycle + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.fType = try container.decode(String.self, forKey: .fType) + self.fVsn = try container.decode(String.self, forKey: .fVsn) + self.data = try? container.decode([String: String].self, forKey: .data) + self.message = try container.decode(String.self, forKey: .message) + self.keyId = try? container.decode(Int.self, forKey: .keyId) + self.addr = try? container.decode(String.self, forKey: .addr) + self.roles = try container.decode(Role.self, forKey: .roles) + self.cadence = try? container.decode(String.self, forKey: .cadence) + self.args = try container.decode([Flow.Argument].self, forKey: .args) + +// voucher = try container.decode(Voucher.self, forKey: .voucher) +// interaction = try container.decode(Interaction.self, forKey: .interaction) + } + + // MARK: Internal + + enum CodingKeys: String, CodingKey { + case fType = "f_type" + case fVsn = "f_vsn" + case roles, data, message, keyId, addr, cadence, args + } + var fType: String = "Signable" var fVsn: String = "1.0.1" var data: [String: String]? @@ -49,56 +83,40 @@ struct Signable: Codable { let args: [Flow.Argument] var interaction = Interaction() - enum CodingKeys: String, CodingKey { - case fType = "f_type" - case fVsn = "f_vsn" - case roles, data, message, keyId, addr, cadence, args - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - fType = try container.decode(String.self, forKey: .fType) - fVsn = try container.decode(String.self, forKey: .fVsn) - data = try? container.decode([String: String].self, forKey: .data) - message = try container.decode(String.self, forKey: .message) - keyId = try? container.decode(Int.self, forKey: .keyId) - addr = try? container.decode(String.self, forKey: .addr) - roles = try container.decode(Role.self, forKey: .roles) - cadence = try? container.decode(String.self, forKey: .cadence) - args = try container.decode([Flow.Argument].self, forKey: .args) - -// voucher = try container.decode(Voucher.self, forKey: .voucher) -// interaction = try container.decode(Interaction.self, forKey: .interaction) - } - var voucher: Voucher { let insideSigners: [Singature] = interaction.findInsideSigners.compactMap { id in guard let account = interaction.accounts[id] else { return nil } - return Singature(address: account.addr?.sansPrefix(), - keyId: account.keyID, - sig: account.signature) + return Singature( + address: account.addr?.sansPrefix(), + keyId: account.keyID, + sig: account.signature + ) } let outsideSigners: [Singature] = interaction.findOutsideSigners.compactMap { id in guard let account = interaction.accounts[id] else { return nil } - return Singature(address: account.addr?.sansPrefix(), - keyId: account.keyID, - sig: account.signature) + return Singature( + address: account.addr?.sansPrefix(), + keyId: account.keyID, + sig: account.signature + ) } - return Voucher(cadence: interaction.message.cadence, - refBlock: interaction.message.refBlock, - computeLimit: interaction.message.computeLimit, - arguments: interaction.message.arguments.compactMap { tempId in - interaction.arguments[tempId]?.asArgument - }, - proposalKey: interaction.createProposalKey(), - payer: interaction.accounts[interaction.payer ?? ""]?.addr?.sansPrefix(), - authorizers: interaction.authorizations - .compactMap { cid in interaction.accounts[cid]?.addr?.sansPrefix() } - .uniqued(), - payloadSigs: insideSigners, - envelopeSigs: outsideSigners) + return Voucher( + cadence: interaction.message.cadence, + refBlock: interaction.message.refBlock, + computeLimit: interaction.message.computeLimit, + arguments: interaction.message.arguments.compactMap { tempId in + interaction.arguments[tempId]?.asArgument + }, + proposalKey: interaction.createProposalKey(), + payer: interaction.accounts[interaction.payer ?? ""]?.addr?.sansPrefix(), + authorizers: interaction.authorizations + .compactMap { cid in interaction.accounts[cid]?.addr?.sansPrefix() } + .uniqued(), + payloadSigs: insideSigners, + envelopeSigs: outsideSigners + ) } func encode(to encoder: Encoder) throws { @@ -117,7 +135,16 @@ struct Signable: Codable { } } +// MARK: - PreSignable + struct PreSignable: Encodable { + enum CodingKeys: String, CodingKey { + case fType = "f_type" + case fVsn = "f_vsn" + case roles, cadence, args, interaction + case voucher + } + let fType: String = "PreSignable" let fVsn: String = "1.0.1" let roles: Role @@ -129,38 +156,37 @@ struct PreSignable: Encodable { var voucher: Voucher { let insideSigners: [Singature] = interaction.findInsideSigners.compactMap { id in guard let account = interaction.accounts[id] else { return nil } - return Singature(address: account.addr, - keyId: account.keyID, - sig: account.signature) + return Singature( + address: account.addr, + keyId: account.keyID, + sig: account.signature + ) } let outsideSigners: [Singature] = interaction.findOutsideSigners.compactMap { id in guard let account = interaction.accounts[id] else { return nil } - return Singature(address: account.addr, - keyId: account.keyID, - sig: account.signature) + return Singature( + address: account.addr, + keyId: account.keyID, + sig: account.signature + ) } - return Voucher(cadence: interaction.message.cadence, - refBlock: interaction.message.refBlock, - computeLimit: interaction.message.computeLimit, - arguments: interaction.message.arguments.compactMap { tempId in - interaction.arguments[tempId]?.asArgument - }, - proposalKey: interaction.createProposalKey(), - payer: interaction.payer, - authorizers: interaction.authorizations - .compactMap { cid in interaction.accounts[cid]?.addr } - .uniqued(), - payloadSigs: insideSigners, - envelopeSigs: outsideSigners) - } - - enum CodingKeys: String, CodingKey { - case fType = "f_type" - case fVsn = "f_vsn" - case roles, cadence, args, interaction - case voucher + return Voucher( + cadence: interaction.message.cadence, + refBlock: interaction.message.refBlock, + computeLimit: interaction.message.computeLimit, + arguments: interaction.message.arguments.compactMap { tempId in + interaction.arguments[tempId]?.asArgument + }, + proposalKey: interaction.createProposalKey(), + payer: interaction.payer, + authorizers: interaction.authorizations + .compactMap { cid in interaction.accounts[cid]?.addr } + .uniqued(), + payloadSigs: insideSigners, + envelopeSigs: outsideSigners + ) } func encode(to encoder: Encoder) throws { @@ -175,6 +201,8 @@ struct PreSignable: Encodable { } } +// MARK: - Argument + struct Argument: Codable { var kind: String var tempId: String @@ -183,6 +211,8 @@ struct Argument: Codable { var xform: Xform } +// MARK: - Xform + struct Xform: Codable { var label: String } @@ -191,35 +221,22 @@ extension Flow.Argument { func toFCLArgument() -> Argument { func randomString(length: Int) -> String { let letters = "abcdefghijklmnopqrstuvwxyz0123456789" - return String((0 ..< length).map { _ in letters.randomElement()! }) + return String((0.. Bool { - self.tag == tag - } - - @discardableResult - mutating func setTag(_ tag: Tag) -> Self { - self.tag = tag - return self - } - var findInsideSigners: [String] { // Inside Signers Are: (authorizers + proposer) - payer var inside = Set(authorizations) @@ -277,6 +301,16 @@ struct Interaction: Codable { return Array(outside) } + func `is`(_ tag: Tag) -> Bool { + self.tag == tag + } + + @discardableResult + mutating func setTag(_ tag: Tag) -> Self { + self.tag = tag + return self + } + func createProposalKey() -> ProposalKey { guard let proposer = proposer, let account = accounts[proposer] @@ -284,9 +318,11 @@ struct Interaction: Codable { return ProposalKey() } - return ProposalKey(address: account.addr?.sansPrefix(), - keyID: account.keyID, - sequenceNum: account.sequenceNum) + return ProposalKey( + address: account.addr?.sansPrefix(), + keyID: account.keyID, + sequenceNum: account.sequenceNum + ) } func createFlowProposalKey() async throws -> Flow.TransactionProposalKey { @@ -303,21 +339,28 @@ struct Interaction: Codable { if account.sequenceNum == nil { let accountData = try await flow.accessAPI.getAccountAtLatestBlock(address: flowAddress) account.sequenceNum = Int(accountData.keys[keyID].sequenceNumber) - return Flow.TransactionProposalKey(address: Flow.Address(hex: address), - keyIndex: keyID, - sequenceNumber: Int64(account.sequenceNum ?? 0)) + return Flow.TransactionProposalKey( + address: Flow.Address(hex: address), + keyIndex: keyID, + sequenceNumber: Int64(account.sequenceNum ?? 0) + ) } - return Flow.TransactionProposalKey(address: Flow.Address(hex: address), - keyIndex: keyID, - sequenceNumber: Int64(account.sequenceNum ?? 0)) + return Flow.TransactionProposalKey( + address: Flow.Address(hex: address), + keyIndex: keyID, + sequenceNumber: Int64(account.sequenceNum ?? 0) + ) } func buildPreSignable(role: Role) -> PreSignable { - return PreSignable(roles: role, - cadence: message.cadence ?? "", - args: message.arguments.compactMap { tempId in arguments[tempId]?.asArgument }, - interaction: self) + PreSignable( + roles: role, + cadence: message.cadence ?? "", + args: message.arguments + .compactMap { tempId in arguments[tempId]?.asArgument }, + interaction: self + ) } } @@ -331,27 +374,31 @@ extension Interaction { throw FCLError.missingPayer } - var tx = Flow.Transaction(script: Flow.Script(text: message.cadence ?? ""), - arguments: message.arguments.compactMap { tempId in arguments[tempId]?.asArgument }, - referenceBlockId: Flow.ID(hex: message.refBlock ?? ""), - gasLimit: BigUInt(message.computeLimit ?? 100), - proposalKey: proposalKey, - payer: Flow.Address(hex: payerAddress), - authorizers: authorizations - .compactMap { cid in accounts[cid]?.addr } - .uniqued() - .compactMap { Flow.Address(hex: $0) }) + var tx = Flow.Transaction( + script: Flow.Script(text: message.cadence ?? ""), + arguments: message.arguments + .compactMap { tempId in arguments[tempId]?.asArgument }, + referenceBlockId: Flow.ID(hex: message.refBlock ?? ""), + gasLimit: BigUInt(message.computeLimit ?? 100), + proposalKey: proposalKey, + payer: Flow.Address(hex: payerAddress), + authorizers: authorizations + .compactMap { cid in accounts[cid]?.addr } + .uniqued() + .compactMap { Flow.Address(hex: $0) } + ) let insideSigners = findInsideSigners for address in insideSigners { if let account = accounts[address], let address = account.addr, let keyId = account.keyID, - let signature = account.signature - { - tx.addPayloadSignature(address: Flow.Address(hex: address), - keyIndex: keyId, - signature: Data(signature.hexValue)) + let signature = account.signature { + tx.addPayloadSignature( + address: Flow.Address(hex: address), + keyIndex: keyId, + signature: Data(signature.hexValue) + ) } } @@ -361,43 +408,54 @@ extension Interaction { if let account = accounts[address], let address = account.addr, let keyId = account.keyID, - let signature = account.signature - { - tx.addEnvelopeSignature(address: Flow.Address(hex: address), - keyIndex: keyId, - signature: Data(signature.hexValue)) + let signature = account.signature { + tx.addEnvelopeSignature( + address: Flow.Address(hex: address), + keyIndex: keyId, + signature: Data(signature.hexValue) + ) } } return tx } } +// MARK: - Block + struct Block: Codable { var id: String? var height: Int64? var isSealed: Bool? } +// MARK: - Account + struct Account: Codable { var addr: String? } +// MARK: - Id + struct Id: Codable { var id: String? } -struct Events: Codable { - var eventType: String? - var start: String? - var end: String? - var blockIDS: [String] = [] +// MARK: - Events +struct Events: Codable { enum CodingKeys: String, CodingKey { case eventType, start, end case blockIDS = "blockIds" } + + var eventType: String? + var start: String? + var end: String? + var blockIDS: [String] = [] } +// MARK: - Message + struct Message: Codable { var cadence: String? var refBlock: String? @@ -409,6 +467,8 @@ struct Message: Codable { var arguments: [String] = [] } +// MARK: - Voucher + struct Voucher: Codable { let cadence: String? let refBlock: String? @@ -421,39 +481,67 @@ struct Voucher: Codable { let envelopeSigs: [Singature]? func toFCLVoucher() -> FCLVoucher { - let pkey = FCLVoucher.ProposalKey(address: Flow.Address(hex: proposalKey.address ?? ""), keyId: proposalKey.keyID ?? 0, sequenceNum: UInt64(proposalKey.sequenceNum ?? 0)) + let pkey = FCLVoucher.ProposalKey( + address: Flow.Address(hex: proposalKey.address ?? ""), + keyId: proposalKey.keyID ?? 0, + sequenceNum: UInt64(proposalKey.sequenceNum ?? 0) + ) let authorArray = authorizers?.map { Flow.Address(hex: $0) } ?? [Flow.Address]() - let payloadSigsArray = payloadSigs?.map { FCLVoucher.Signature(address: Flow.Address(hex: $0.address ?? ""), keyId: $0.keyId ?? 0, sig: $0.sig ?? "") } ?? [FCLVoucher.Signature]() - - let v = FCLVoucher(cadence: Flow.Script(text: cadence ?? ""), payer: Flow.Address(hex: payer ?? ""), refBlock: Flow.ID(hex: refBlock ?? ""), arguments: arguments, proposalKey: pkey, computeLimit: UInt64(computeLimit ?? 0), authorizers: authorArray, payloadSigs: payloadSigsArray) + let payloadSigsArray = payloadSigs?.map { FCLVoucher.Signature( + address: Flow.Address(hex: $0.address ?? ""), + keyId: $0.keyId ?? 0, + sig: $0.sig ?? "" + ) } ?? [FCLVoucher.Signature]() + + let v = FCLVoucher( + cadence: Flow.Script(text: cadence ?? ""), + payer: Flow.Address(hex: payer ?? ""), + refBlock: Flow.ID(hex: refBlock ?? ""), + arguments: arguments, + proposalKey: pkey, + computeLimit: UInt64(computeLimit ?? 0), + authorizers: authorArray, + payloadSigs: payloadSigsArray + ) return v } } -struct Accounts: Codable { - let currentUser: SignableUser +// MARK: - Accounts +struct Accounts: Codable { enum CodingKeys: String, CodingKey { case currentUser = "CURRENT_USER" } + + let currentUser: SignableUser } +// MARK: - Singature + struct Singature: Codable { let address: String? let keyId: Int? let sig: String? } -// MARK: - CurrentUser +// MARK: - SignableUser struct SignableUser: Codable { - var kind: String? - var tempID: String? - var addr: String? - var signature: String? - var keyID: Int? - var sequenceNum: Int? - var role: Role + // MARK: Lifecycle + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.kind = try? container.decode(String.self, forKey: .kind) + self.tempID = try? container.decode(String.self, forKey: .tempID) + self.addr = try? container.decode(String.self, forKey: .addr) + self.signature = try? container.decode(String.self, forKey: .signature) + self.keyID = try? container.decode(Int.self, forKey: .keyID) + self.sequenceNum = try? container.decode(Int.self, forKey: .sequenceNum) + self.role = try container.decode(Role.self, forKey: .role) + } + + // MARK: Internal enum CodingKeys: String, CodingKey { case kind @@ -463,16 +551,13 @@ struct SignableUser: Codable { case sequenceNum, signature, role } - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - kind = try? container.decode(String.self, forKey: .kind) - tempID = try? container.decode(String.self, forKey: .tempID) - addr = try? container.decode(String.self, forKey: .addr) - signature = try? container.decode(String.self, forKey: .signature) - keyID = try? container.decode(Int.self, forKey: .keyID) - sequenceNum = try? container.decode(Int.self, forKey: .sequenceNum) - role = try container.decode(Role.self, forKey: .role) - } + var kind: String? + var tempID: String? + var addr: String? + var signature: String? + var keyID: Int? + var sequenceNum: Int? + var role: Role func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) @@ -486,18 +571,22 @@ struct SignableUser: Codable { } } -struct ProposalKey: Codable { - var address: String? - var keyID: Int? - var sequenceNum: Int? +// MARK: - ProposalKey +struct ProposalKey: Codable { enum CodingKeys: String, CodingKey { case address case keyID = "keyId" case sequenceNum } + + var address: String? + var keyID: Int? + var sequenceNum: Int? } +// MARK: - Role + struct Role: Codable { var proposer: Bool = false var authorizer: Bool = false @@ -511,9 +600,11 @@ struct Role: Codable { } } +// MARK: - NullDecodable + @propertyWrapper struct NullDecodable: Decodable where T: Decodable { - var wrappedValue: T? + // MARK: Lifecycle init(wrappedValue: T?) { self.wrappedValue = wrappedValue @@ -526,6 +617,10 @@ struct NullDecodable: Decodable where T: Decodable { // case .none: try container.encodeNil() // } // } + + // MARK: Internal + + var wrappedValue: T? } extension String { @@ -537,7 +632,7 @@ extension String { } func withPrefix() -> String { - return "0x" + sansPrefix() + "0x" + sansPrefix() } } @@ -548,6 +643,8 @@ extension Array where Element: Hashable { } } +// MARK: - BaseConfigRequest + public struct BaseConfigRequest: Codable { var app: [String: String]? var service: [String: String]? @@ -558,6 +655,8 @@ public struct BaseConfigRequest: Codable { var nonce: String? } +// MARK: - ClientInfo + public struct ClientInfo: Codable { var fclVersion: String? var fclLibrary: URL? diff --git a/FRW/Services/Manager/WalletConnect/WalletConnectManager.swift b/FRW/Services/Manager/WalletConnect/WalletConnectManager.swift index 950ff86f..6da82345 100644 --- a/FRW/Services/Manager/WalletConnect/WalletConnectManager.swift +++ b/FRW/Services/Manager/WalletConnect/WalletConnectManager.swift @@ -21,39 +21,28 @@ import WalletConnectUtils import WalletCore import Web3Wallet -class WalletConnectManager: ObservableObject { - static let shared = WalletConnectManager() - - @Published - var activeSessions: [Session] = [] - - @Published - var activePairings: [Pairing] = [] - - @Published var pendingRequests: [WalletConnectSign.Request] = [] - - var onClientConnected: (() -> Void)? - - private var publishers = [AnyCancellable]() - private var pendingRequestCheckTimer: Timer? - private var handler = WalletConnectHandler() - - var currentProposal: Session.Proposal? - var currentRequest: WalletConnectSign.Request? - var currentSessionInfo: SessionInfo? - var currentRequestInfo: RequestInfo? - var currentMessageInfo: RequestMessageInfo? - - @Published var setSessions: [Session] = [] - - private var syncAccountFlag: Bool = false +// MARK: - WalletConnectManager - private var cacheReqeust: [String] = [] +class WalletConnectManager: ObservableObject { + // MARK: Lifecycle init() { - let redirect = try! AppMetadata.Redirect(native: "frw://", universal: "https://frw-link.lilico.app") - let metadata = AppMetadata(name: "Flow Wallet", description: "Digital wallet created for everyone.", url: "https://frw-link.lilico.app", icons: ["https://frw-link.lilico.app/logo.png"], redirect: redirect) - Networking.configure(groupIdentifier: AppGroupName, projectId: LocalEnvManager.shared.walletConnectProjectID, socketFactory: SocketFactory()) + let redirect = try! AppMetadata.Redirect( + native: "frw://", + universal: "https://frw-link.lilico.app" + ) + let metadata = AppMetadata( + name: "Flow Wallet", + description: "Digital wallet created for everyone.", + url: "https://frw-link.lilico.app", + icons: ["https://frw-link.lilico.app/logo.png"], + redirect: redirect + ) + Networking.configure( + groupIdentifier: AppGroupName, + projectId: LocalEnvManager.shared.walletConnectProjectID, + socketFactory: SocketFactory() + ) Pair.configure(metadata: metadata) Sign.configure(crypto: DefaultCryptoProvider()) Web3Wallet.configure(metadata: metadata, crypto: DefaultCryptoProvider()) @@ -81,17 +70,45 @@ class WalletConnectManager: ObservableObject { } }.store(in: &publishers) - NotificationCenter.default.addObserver(self, selector: #selector(reloadPendingRequests), name: UIApplication.didBecomeActiveNotification, object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(reloadPendingRequests), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) } + // MARK: Internal + + static let shared = WalletConnectManager() + + @Published + var activeSessions: [Session] = [] + + @Published + var activePairings: [Pairing] = [] + + @Published + var pendingRequests: [WalletConnectSign.Request] = [] + + var onClientConnected: (() -> Void)? + + var currentProposal: Session.Proposal? + var currentRequest: WalletConnectSign.Request? + var currentSessionInfo: SessionInfo? + var currentRequestInfo: RequestInfo? + var currentMessageInfo: RequestMessageInfo? + + @Published + var setSessions: [Session] = [] + func connect(link: String) { debugPrint("WalletConnectManager -> connect(), Thread: \(Thread.isMainThread)") print("[RESPONDER] Pairing to: \(link)") Task { do { if let removedLink = link.removingPercentEncoding, - let uri = WalletConnectURI(string: removedLink) - { + let uri = WalletConnectURI(string: removedLink) { // TODO: commit #if DEBUG // if Pair.instance.getPairings().contains(where: { $0.topic == uri.topic }) { @@ -130,8 +147,17 @@ class WalletConnectManager: ObservableObject { self.activePairings = activePairings } - func encodeAccountProof(address: String, nonce: String, appIdentifier: String, includeDomaintag: Bool = true) -> Data? { - let list: [Any] = [appIdentifier.data(using: .utf8) ?? Data(), Data(hex: address), Data(hex: nonce)] + func encodeAccountProof( + address: String, + nonce: String, + appIdentifier: String, + includeDomaintag: Bool = true + ) -> Data? { + let list: [Any] = [ + appIdentifier.data(using: .utf8) ?? Data(), + Data(hex: address), + Data(hex: nonce), + ] guard let rlp = RLP.encode(list) else { return nil } @@ -226,6 +252,16 @@ class WalletConnectManager: ObservableObject { }.store(in: &publishers) } + // MARK: Private + + private var publishers = [AnyCancellable]() + private var pendingRequestCheckTimer: Timer? + private var handler = WalletConnectHandler() + + private var syncAccountFlag: Bool = false + + private var cacheReqeust: [String] = [] + private func navigateBackTodApp(topic: String) { // TODO: #six // WalletConnectRouter.Router.goBack() @@ -241,7 +277,13 @@ extension WalletConnectManager { private func startPendingRequestCheckTimer() { stopPendingRequestCheckTimer() - let timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(reloadPendingRequests), userInfo: nil, repeats: true) + let timer = Timer.scheduledTimer( + timeInterval: 5, + target: self, + selector: #selector(reloadPendingRequests), + userInfo: nil, + repeats: true + ) RunLoop.main.add(timer, forMode: .common) pendingRequestCheckTimer = timer @@ -254,13 +296,18 @@ extension WalletConnectManager { } } - @objc func reloadPendingRequests() { + @objc + func reloadPendingRequests() { if UserManager.shared.isLoggedIn { - pendingRequests = Sign.instance.getPendingRequests().map { (request: Request, _: VerifyContext?) in + pendingRequests = Sign.instance.getPendingRequests().map { ( + request: Request, + _: VerifyContext? + ) in request } - WalletNewsHandler.shared.refreshWalletConnectNews(pendingRequests.map { $0.toLocalNews() }) + WalletNewsHandler.shared + .refreshWalletConnectNews(pendingRequests.map { $0.toLocalNews() }) } } } @@ -278,7 +325,8 @@ extension WalletConnectManager { guard network == LocalUserDefaults.shared.flowNetwork.toFlowType() else { rejectSession(proposal: sessionProposal) let current = LocalUserDefaults.shared.flowNetwork - guard let toNetwork = LocalUserDefaults.FlowNetworkType(chainId: network) else { return } + guard let toNetwork = LocalUserDefaults.FlowNetworkType(chainId: network) + else { return } Router.route(to: RouteMap.Explore.switchNetwork(current, toNetwork, nil)) return } @@ -295,12 +343,13 @@ extension WalletConnectManager { address = EVMAccountManager.shared.accounts.first?.showAddress ?? "" } currentSessionInfo = info - let authnVM = BrowserAuthnViewModel(title: info.name, - url: info.dappURL, - logo: info.iconURL, - walletAddress: address, - network: network) - { result in + let authnVM = BrowserAuthnViewModel( + title: info.name, + url: info.dappURL, + logo: info.iconURL, + walletAddress: address, + network: network + ) { result in if result { // TODO: Handle network mismatch self.approveSession(proposal: sessionProposal) @@ -331,7 +380,11 @@ extension WalletConnectManager { var services = [ // Since fcl-js is not implement pre-authz, hence we disable it for now - serviceDefinition(address: RemoteConfigManager.shared.payer, keyId: RemoteConfigManager.shared.keyIndex, type: .preAuthz), + serviceDefinition( + address: RemoteConfigManager.shared.payer, + keyId: RemoteConfigManager.shared.keyIndex, + type: .preAuthz + ), serviceDefinition(address: address, keyId: keyId, type: .authn), serviceDefinition(address: address, keyId: keyId, type: .authz), serviceDefinition(address: address, keyId: keyId, type: .userSignature), @@ -341,18 +394,38 @@ extension WalletConnectManager { if let model = try? JSONDecoder().decode(BaseConfigRequest.self, from: data), let nonce = model.accountProofNonce ?? model.nonce, let appIdentifier = model.appIdentifier, - let data = self.encodeAccountProof(address: address, nonce: nonce, appIdentifier: appIdentifier), - let signedData = try? await WalletManager.shared.sign(signableData: data) - { - services.append(accountProofServiceDefinition(address: address, keyId: keyId, nonce: nonce, signature: signedData.hexValue)) + let data = self.encodeAccountProof( + address: address, + nonce: nonce, + appIdentifier: appIdentifier + ), + let signedData = try? await WalletManager.shared.sign(signableData: data) { + services.append(accountProofServiceDefinition( + address: address, + keyId: keyId, + nonce: nonce, + signature: signedData.hexValue + )) } - let result = AuthnResponse(fType: "PollingResponse", fVsn: "1.0.0", status: .approved, - data: AuthnData(addr: address, fType: "AuthnResponse", fVsn: "1.0.0", - services: services), - reason: nil, - compositeSignature: nil) - try await Sign.instance.respond(topic: sessionRequest.topic, requestId: sessionRequest.id, response: .response(AnyCodable(result))) + let result = AuthnResponse( + fType: "PollingResponse", + fVsn: "1.0.0", + status: .approved, + data: AuthnData( + addr: address, + fType: "AuthnResponse", + fVsn: "1.0.0", + services: services + ), + reason: nil, + compositeSignature: nil + ) + try await Sign.instance.respond( + topic: sessionRequest.topic, + requestId: sessionRequest.id, + response: .response(AnyCodable(result)) + ) self.navigateBackTodApp(topic: sessionRequest.topic) } catch { print("[WALLET] Respond Error: \(error.localizedDescription)") @@ -361,19 +434,45 @@ extension WalletConnectManager { } case FCLWalletConnectMethod.preAuthz.rawValue: - let result = AuthnResponse(fType: "PollingResponse", fVsn: "1.0.0", status: .approved, - data: AuthnData(addr: address, fType: "AuthnResponse", fVsn: "1.0.0", - services: nil, - proposer: serviceDefinition(address: address, keyId: keyId, type: .authz), - payer: - [serviceDefinition(address: RemoteConfigManager.shared.payer, keyId: RemoteConfigManager.shared.keyIndex, type: .authz)], - authorization: [serviceDefinition(address: address, keyId: keyId, type: .authz)]), - reason: nil, - compositeSignature: nil) + let result = AuthnResponse( + fType: "PollingResponse", + fVsn: "1.0.0", + status: .approved, + data: AuthnData( + addr: address, + fType: "AuthnResponse", + fVsn: "1.0.0", + services: nil, + proposer: serviceDefinition( + address: address, + keyId: keyId, + type: .authz + ), + payer: + [serviceDefinition( + address: RemoteConfigManager.shared + .payer, + keyId: RemoteConfigManager.shared + .keyIndex, + type: .authz + )], + authorization: [serviceDefinition( + address: address, + keyId: keyId, + type: .authz + )] + ), + reason: nil, + compositeSignature: nil + ) Task { do { - try await Sign.instance.respond(topic: sessionRequest.topic, requestId: sessionRequest.id, response: .response(AnyCodable(result))) + try await Sign.instance.respond( + topic: sessionRequest.topic, + requestId: sessionRequest.id, + response: .response(AnyCodable(result)) + ) } catch { print("[WALLET] Respond Error: \(error.localizedDescription)") rejectRequest(request: sessionRequest) @@ -395,8 +494,7 @@ extension WalletConnectManager { var model: Signable? if let data = Data(base64Encoded: json), data.isGzipped, - let uncompressData = try? data.gunzipped() - { + let uncompressData = try? data.gunzipped() { model = try JSONDecoder().decode(Signable.self, from: uncompressData) } else if let data = json.data(using: .utf8) { model = try JSONDecoder().decode(Signable.self, from: data) @@ -407,17 +505,38 @@ extension WalletConnectManager { } if model.roles.payer, !model.roles.proposer, !model.roles.authorizer { - approvePayerRequest(request: sessionRequest, model: model, message: model.message) + approvePayerRequest( + request: sessionRequest, + model: model, + message: model.message + ) navigateBackTodApp(topic: sessionRequest.topic) return } if let session = activeSessions.first(where: { $0.topic == sessionRequest.topic }) { - let request = RequestInfo(cadence: model.cadence ?? "", agrument: model.args, name: session.peer.name, descriptionText: session.peer.description, dappURL: session.peer.url, iconURL: session.peer.icons.first ?? "", chains: Set(arrayLiteral: sessionRequest.chainId), methods: nil, pendingRequests: [], message: model.message) + let request = RequestInfo( + cadence: model.cadence ?? "", + agrument: model.args, + name: session.peer.name, + descriptionText: session.peer.description, + dappURL: session.peer.url, + iconURL: session.peer.icons.first ?? "", + chains: Set(arrayLiteral: sessionRequest.chainId), + methods: nil, + pendingRequests: [], + message: model.message + ) currentRequestInfo = request - let authzVM = BrowserAuthzViewModel(title: request.name, url: request.dappURL, logo: request.iconURL, cadence: request.cadence, arguments: request.agrument) { result in + let authzVM = BrowserAuthzViewModel( + title: request.name, + url: request.dappURL, + logo: request.iconURL, + cadence: request.cadence, + arguments: request.agrument + ) { result in if result { self.approveRequest(request: sessionRequest, requestInfo: request) } else { @@ -449,8 +568,7 @@ extension WalletConnectManager { var model: SignableMessage? if let data = Data(base64Encoded: json), data.isGzipped, - let uncompressData = try? data.gunzipped() - { + let uncompressData = try? data.gunzipped() { model = try JSONDecoder().decode(SignableMessage.self, from: uncompressData) } else if let data = json.data(using: .utf8) { model = try JSONDecoder().decode(SignableMessage.self, from: data) @@ -460,12 +578,29 @@ extension WalletConnectManager { } if let session = activeSessions.first(where: { $0.topic == sessionRequest.topic }) { - let request = RequestMessageInfo(name: session.peer.name, descriptionText: session.peer.description, dappURL: session.peer.url, iconURL: session.peer.icons.first ?? "", chains: Set(arrayLiteral: sessionRequest.chainId), methods: nil, pendingRequests: [], message: model.message) + let request = RequestMessageInfo( + name: session.peer.name, + descriptionText: session.peer.description, + dappURL: session.peer.url, + iconURL: session.peer.icons.first ?? "", + chains: Set(arrayLiteral: sessionRequest.chainId), + methods: nil, + pendingRequests: [], + message: model.message + ) currentMessageInfo = request - let vm = BrowserSignMessageViewModel(title: request.name, url: request.dappURL, logo: request.iconURL, cadence: request.message) { result in + let vm = BrowserSignMessageViewModel( + title: request.name, + url: request.dappURL, + logo: request.iconURL, + cadence: request.message + ) { result in if result { - self.approveRequestMessage(request: sessionRequest, requestInfo: request) + self.approveRequestMessage( + request: sessionRequest, + requestInfo: request + ) } else { self.rejectRequest(request: sessionRequest) } @@ -482,7 +617,11 @@ extension WalletConnectManager { Task { do { let param = try WalletConnectSyncDevice.packageUserInfo() - try await Sign.instance.respond(topic: sessionRequest.topic, requestId: sessionRequest.id, response: .response(param)) + try await Sign.instance.respond( + topic: sessionRequest.topic, + requestId: sessionRequest.id, + response: .response(param) + ) } catch { log.error("[WALLET] Respond Error: [accountInfo] \(error.localizedDescription)") rejectRequest(request: sessionRequest) @@ -491,15 +630,22 @@ extension WalletConnectManager { case FCLWalletConnectMethod.addDeviceInfo.rawValue: Task { do { - let res = try sessionRequest.params.get(SyncInfo.SyncResponse.self) + let res = try sessionRequest.params + .get(SyncInfo.SyncResponse.self) let viewModel = SyncAddDeviceViewModel(with: res.data!) { result in if result { Task { do { - try await Sign.instance.respond(topic: sessionRequest.topic, requestId: sessionRequest.id, response: .response(AnyCodable(""))) + try await Sign.instance.respond( + topic: sessionRequest.topic, + requestId: sessionRequest.id, + response: .response(AnyCodable("")) + ) } catch { self.rejectRequest(request: sessionRequest) - print("[WALLET] Request Error: [addDeviceInfo] \(error.localizedDescription)") + print( + "[WALLET] Request Error: [addDeviceInfo] \(error.localizedDescription)" + ) } } } else { @@ -518,7 +664,11 @@ extension WalletConnectManager { handler.handlePersonalSignRequest(request: sessionRequest) { signStr in Task { do { - try await Sign.instance.respond(topic: sessionRequest.topic, requestId: sessionRequest.id, response: .response(AnyCodable(signStr))) + try await Sign.instance.respond( + topic: sessionRequest.topic, + requestId: sessionRequest.id, + response: .response(AnyCodable(signStr)) + ) } catch { self.rejectRequest(request: sessionRequest) log.error("[EVM] Request Error: [personalSign] \(error)") @@ -533,7 +683,11 @@ extension WalletConnectManager { handler.handleSendTransactionRequest(request: sessionRequest) { signStr in Task { do { - try await Sign.instance.respond(topic: sessionRequest.topic, requestId: sessionRequest.id, response: .response(AnyCodable(signStr))) + try await Sign.instance.respond( + topic: sessionRequest.topic, + requestId: sessionRequest.id, + response: .response(AnyCodable(signStr)) + ) } catch { self.rejectRequest(request: sessionRequest) log.error("[EVM] Request Error: [sendTransaction] \(error)") @@ -570,10 +724,16 @@ extension WalletConnectManager { let user = try WalletConnectSyncDevice.parseAccount(data: data) Router.route(to: RouteMap.RestoreLogin.syncAccount(user)) } catch { - log.error("[WALLET] Respond Error: [account info] \(error.localizedDescription)") + log + .error( + "[WALLET] Respond Error: [account info] \(error.localizedDescription)" + ) } } else if WalletConnectSyncDevice.isDevice(request: request, with: response) { - NotificationCenter.default.post(name: .syncDeviceStatusDidChanged, object: WalletConnectSyncDevice.SyncResult.success) + NotificationCenter.default.post( + name: .syncDeviceStatusDidChanged, + object: WalletConnectSyncDevice.SyncResult.success + ) } case let .error(error): if WalletConnectSyncDevice.isDevice(request: request, with: response) { @@ -589,7 +749,11 @@ extension WalletConnectManager { handler.handleSignTypedDataV4(request: sessionRequest) { signStr in Task { do { - try await Sign.instance.respond(topic: sessionRequest.topic, requestId: sessionRequest.id, response: .response(AnyCodable(signStr))) + try await Sign.instance.respond( + topic: sessionRequest.topic, + requestId: sessionRequest.id, + response: .response(AnyCodable(signStr)) + ) } catch { self.rejectRequest(request: sessionRequest) log.error("[EVM] Request Error: [signTypedDataV4] \(error)") @@ -647,7 +811,10 @@ extension WalletConnectManager { private func rejectSession(proposal: Session.Proposal) { Task { do { - try await Sign.instance.rejectSession(proposalId: proposal.id, reason: .userRejected) + try await Sign.instance.rejectSession( + proposalId: proposal.id, + reason: .userRejected + ) HUD.success(title: "rejected".localized) } catch { HUD.error(title: "reject_failed".localized) @@ -665,12 +832,27 @@ extension WalletConnectManager { let data = Data(requestInfo.message.hexValue) let signedData = try await WalletManager.shared.sign(signableData: data) let signature = signedData.hexValue - let result = AuthnResponse(fType: "PollingResponse", fVsn: "1.0.0", status: .approved, - data: AuthnData(addr: account, fType: "CompositeSignature", fVsn: "1.0.0", services: nil, keyId: 0, signature: signature), - reason: nil, - compositeSignature: nil) - - try await Sign.instance.respond(topic: request.topic, requestId: request.id, response: .response(AnyCodable(result))) + let result = AuthnResponse( + fType: "PollingResponse", + fVsn: "1.0.0", + status: .approved, + data: AuthnData( + addr: account, + fType: "CompositeSignature", + fVsn: "1.0.0", + services: nil, + keyId: 0, + signature: signature + ), + reason: nil, + compositeSignature: nil + ) + + try await Sign.instance.respond( + topic: request.topic, + requestId: request.id, + response: .response(AnyCodable(result)) + ) HUD.success(title: "approved".localized) } catch { @@ -690,13 +872,31 @@ extension WalletConnectManager { do { let tx = model.voucher.toFCLVoucher() let data = Data(message.hexValue) - let signedData = try await RemoteConfigManager.shared.sign(voucher: tx, signableData: data) + let signedData = try await RemoteConfigManager.shared.sign( + voucher: tx, + signableData: data + ) let signature = signedData.hexValue - let result = AuthnResponse(fType: "PollingResponse", fVsn: "1.0.0", status: .approved, - data: AuthnData(addr: account, fType: "CompositeSignature", fVsn: "1.0.0", services: nil, keyId: 0, signature: signature), - reason: nil, - compositeSignature: nil) - try await Sign.instance.respond(topic: request.topic, requestId: request.id, response: .response(AnyCodable(result))) + let result = AuthnResponse( + fType: "PollingResponse", + fVsn: "1.0.0", + status: .approved, + data: AuthnData( + addr: account, + fType: "CompositeSignature", + fVsn: "1.0.0", + services: nil, + keyId: 0, + signature: signature + ), + reason: nil, + compositeSignature: nil + ) + try await Sign.instance.respond( + topic: request.topic, + requestId: request.id, + response: .response(AnyCodable(result)) + ) HUD.success(title: "payer_approved".localized) } catch { log.error("WalletConnectManager -> approveRequest failed", context: error) @@ -706,13 +906,21 @@ extension WalletConnectManager { } private func rejectRequest(request: Request, reason: String = "User reject request") { - let result = AuthnResponse(fType: "PollingResponse", fVsn: "1.0.0", status: .declined, - reason: reason, - compositeSignature: nil) + let result = AuthnResponse( + fType: "PollingResponse", + fVsn: "1.0.0", + status: .declined, + reason: reason, + compositeSignature: nil + ) Task { do { - try await Sign.instance.respond(topic: request.topic, requestId: request.id, response: .response(AnyCodable(result))) + try await Sign.instance.respond( + topic: request.topic, + requestId: request.id, + response: .response(AnyCodable(result)) + ) HUD.success(title: "rejected".localized) } catch { log.error("WalletConnectManager -> approveRequest failed", context: error) @@ -734,11 +942,26 @@ extension WalletConnectManager { let data = Flow.DomainTag.user.normalize + Data(requestInfo.message.hexValue) let signedData = try await WalletManager.shared.sign(signableData: data) let signature = signedData.hexValue - let result = AuthnResponse(fType: "PollingResponse", fVsn: "1.0.0", status: .approved, - data: AuthnData(addr: account, fType: "CompositeSignature", fVsn: "1.0.0", services: nil, keyId: 0, signature: signature), - reason: nil, - compositeSignature: nil) - try await Sign.instance.respond(topic: request.topic, requestId: request.id, response: .response(AnyCodable(result))) + let result = AuthnResponse( + fType: "PollingResponse", + fVsn: "1.0.0", + status: .approved, + data: AuthnData( + addr: account, + fType: "CompositeSignature", + fVsn: "1.0.0", + services: nil, + keyId: 0, + signature: signature + ), + reason: nil, + compositeSignature: nil + ) + try await Sign.instance.respond( + topic: request.topic, + requestId: request.id, + response: .response(AnyCodable(result)) + ) HUD.success(title: "approved".localized) } catch { debugPrint("WalletConnectManager -> approveRequestMessage failed: \(error)") @@ -772,7 +995,8 @@ extension WalletConnectManager { Task { do { - self.currentRequest = try await WalletConnectSyncDevice.requestSyncAccount(in: session) + self.currentRequest = try await WalletConnectSyncDevice + .requestSyncAccount(in: session) } catch { // TODO: log.error("[sync]-account: send sync account requst failed") @@ -790,22 +1014,25 @@ extension WalletConnectManager { } func findSession(topic: String) -> Session? { - return activeSessions.first(where: { $0.topic == topic }) + activeSessions.first(where: { $0.topic == topic }) } } extension WalletConnectSign.Request { func toLocalNews() -> RemoteConfigManager.News { - return RemoteConfigManager.News(id: topic, - priority: .urgent, - type: .message, - title: "Pending Request - \(name ?? "Unknown")", - body: "You have a pending request from \((dappURL?.host) ?? "Unknown").", - icon: logoURL?.absoluteString ?? AppPlaceholder.image, - image: nil, - url: nil, - expiryTime: .distantFuture, - displayType: .click, - flag: .walletconnect, conditions: nil) + RemoteConfigManager.News( + id: topic, + priority: .urgent, + type: .message, + title: "Pending Request - \(name ?? "Unknown")", + body: "You have a pending request from \((dappURL?.host) ?? "Unknown").", + icon: logoURL?.absoluteString ?? AppPlaceholder.image, + image: nil, + url: nil, + expiryTime: .distantFuture, + displayType: .click, + flag: .walletconnect, + conditions: nil + ) } } diff --git a/FRW/Services/Network/GithubEndpoint.swift b/FRW/Services/Network/GithubEndpoint.swift index cacb0b4d..7df40d2d 100644 --- a/FRW/Services/Network/GithubEndpoint.swift +++ b/FRW/Services/Network/GithubEndpoint.swift @@ -8,6 +8,8 @@ import Foundation import Moya +// MARK: - GithubEndpoint + enum GithubEndpoint { case collections case ftTokenList @@ -15,9 +17,11 @@ enum GithubEndpoint { case EVMTokenList } +// MARK: TargetType + extension GithubEndpoint: TargetType { var baseURL: URL { - return URL(string: "https://raw.githubusercontent.com")! + URL(string: "https://raw.githubusercontent.com")! } var path: String { diff --git a/FRW/Tools/ThirdParty/FluidGradient/BlobLayer.swift b/FRW/Tools/ThirdParty/FluidGradient/BlobLayer.swift index b67040fe..4f205dbf 100644 --- a/FRW/Tools/ThirdParty/FluidGradient/BlobLayer.swift +++ b/FRW/Tools/ThirdParty/FluidGradient/BlobLayer.swift @@ -9,12 +9,14 @@ import SwiftUI /// A CALayer that draws a single blob on the screen public class BlobLayer: CAGradientLayer { + // MARK: Lifecycle + init(color: Color) { super.init() type = .radial #if os(OSX) - autoresizingMask = [.layerWidthSizable, .layerHeightSizable] + autoresizingMask = [.layerWidthSizable, .layerHeightSizable] #endif // Set color @@ -29,20 +31,36 @@ public class BlobLayer: CAGradientLayer { endPoint = position.displace(by: radius) } + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // Required by the framework + override public init(layer: Any) { + super.init(layer: layer) + } + + // MARK: Internal + /// Generate a random point on the canvas func newPosition() -> CGPoint { - return CGPoint(x: CGFloat.random(in: 0.0 ... 1.0), - y: CGFloat.random(in: 0.0 ... 1.0)).capped() + CGPoint( + x: CGFloat.random(in: 0.0...1.0), + y: CGFloat.random(in: 0.0...1.0) + ).capped() } /// Generate a random radius for the blob func newRadius() -> CGPoint { - let size = CGFloat.random(in: 0.15 ... 0.75) + let size = CGFloat.random(in: 0.15...0.75) let viewRatio = frame.width / frame.height let safeRatio = max(viewRatio.isNaN ? 1 : viewRatio, 1) - let ratio = safeRatio * CGFloat.random(in: 0.25 ... 1.75) - return CGPoint(x: size, - y: size * ratio) + let ratio = safeRatio * CGFloat.random(in: 0.25...1.75) + return CGPoint( + x: size, + y: size * ratio + ) } /// Animate the blob to a random point and size on screen at set speed @@ -79,7 +97,7 @@ public class BlobLayer: CAGradientLayer { endPoint = position.displace(by: radius) // Opacity - let value = Float.random(in: 0.5 ... 1) + let value = Float.random(in: 0.5...1) let opacity = animation.copy() as! CASpringAnimation opacity.fromValue = self.opacity opacity.toValue = value @@ -94,19 +112,11 @@ public class BlobLayer: CAGradientLayer { /// Set the color of the blob func set(color: Color) { // Converted to the system color so that cgColor isn't nil - colors = [SystemColor(color).cgColor, - SystemColor(color).cgColor, - SystemColor(color.opacity(0.0)).cgColor] + colors = [ + SystemColor(color).cgColor, + SystemColor(color).cgColor, + SystemColor(color.opacity(0.0)).cgColor, + ] locations = [0.0, 0.9, 1.0] } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // Required by the framework - override public init(layer: Any) { - super.init(layer: layer) - } } diff --git a/FRW/Tools/ThirdParty/SPQRCode/Interface/SPQRCameraController.swift b/FRW/Tools/ThirdParty/SPQRCode/Interface/SPQRCameraController.swift index b63da85f..e1fddb84 100644 --- a/FRW/Tools/ThirdParty/SPQRCode/Interface/SPQRCameraController.swift +++ b/FRW/Tools/ThirdParty/SPQRCode/Interface/SPQRCameraController.swift @@ -28,26 +28,10 @@ import UIKit public typealias SPQRCodeCallback = (SPQRCodeData, SPQRCameraController) -> Void -open class SPQRCameraController: SPController { - open var detectQRCodeData: ((SPQRCodeData, SPQRCameraController) -> SPQRCodeData?) = { data, _ in data } - open var handledQRCodeData: SPQRCodeCallback? - open var clickQRCodeData: SPQRCodeCallback? - - var updateTimer: Timer? - lazy var captureSession: AVCaptureSession = makeCaptureSession() - var qrCodeData: SPQRCodeData? { - didSet { - updateInterface() - didTapHandledButton() - } - } - - // MARK: - Views +// MARK: - SPQRCameraController - let frameLayer = SPQRFrameLayer() - let detailView = SPQRDetailButton() - lazy var previewLayer = makeVideoPreviewLayer() - let maskView = SPQRMaskView() +open class SPQRCameraController: SPController { + // MARK: Lifecycle override public init() { super.init() @@ -59,12 +43,20 @@ open class SPQRCameraController: SPController { fatalError("init(coder:) has not been implemented") } + // MARK: Open + + open var detectQRCodeData: ((SPQRCodeData, SPQRCameraController) -> SPQRCodeData?) = + { data, _ in + data + } + + open var handledQRCodeData: SPQRCodeCallback? + open var clickQRCodeData: SPQRCodeCallback? + override open var prefersStatusBarHidden: Bool { - return true + true } - // MARK: - Lifecycle - override open func viewDidLoad() { super.viewDidLoad() @@ -84,6 +76,41 @@ open class SPQRCameraController: SPController { updateInterface() } + override open func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + previewLayer.frame = .init( + x: .zero, y: .zero, + width: view.layer.bounds.width, + height: view.layer.bounds.height + ) + maskView.frame = previewLayer.frame + } + + // MARK: Internal + + static let supportedCodeTypes = [ + AVMetadataObject.ObjectType.aztec, + AVMetadataObject.ObjectType.qr, + ] + + var updateTimer: Timer? + lazy var captureSession: AVCaptureSession = makeCaptureSession() + + // MARK: - Views + + let frameLayer = SPQRFrameLayer() + let detailView = SPQRDetailButton() + lazy var previewLayer = makeVideoPreviewLayer() + let maskView = SPQRMaskView() + + var qrCodeData: SPQRCodeData? { + didSet { + updateInterface() + didTapHandledButton() + } + } + func stopRunning() { if captureSession.isRunning { captureSession.stopRunning() @@ -92,16 +119,82 @@ open class SPQRCameraController: SPController { // MARK: - Actions - @objc func didTapHandledButton() { + @objc + func didTapHandledButton() { guard let data = qrCodeData else { return } handledQRCodeData?(data, self) } - @objc func didTapCancelButton() { + @objc + func didTapCancelButton() { dismissAnimated() } - @objc private func didTapDetailButtonClick() { + func updateInterface() { + let duration: TimeInterval = 0.22 + if qrCodeData != nil { + detailView.isHidden = false + if case .flowWallet = qrCodeData { + detailView.applyDefaultAppearance(with: .init( + content: .white, + background: UIColor(hex: "#00EF8B") + )) + frameLayer.strokeColor = UIColor(hex: "#00EF8B").cgColor + } + if case .ethWallet = qrCodeData { + detailView.applyDefaultAppearance(with: .init( + content: .white, + background: UIColor(hex: "#00EF8B") + )) + frameLayer.strokeColor = UIColor(hex: "#00EF8B").cgColor + } + UIView.animate( + withDuration: duration, + delay: .zero, + options: .curveEaseInOut, + animations: { + self.detailView.transform = .identity + self.detailView.alpha = 1 + } + ) + } else { + UIView.animate( + withDuration: duration, + delay: .zero, + options: .curveEaseInOut, + animations: { + self.detailView.transform = .init(scale: 0.9) + self.detailView.alpha = .zero + }, + completion: { _ in + self.detailView.isHidden = true + } + ) + } + } + + func makeVideoPreviewLayer() -> AVCaptureVideoPreviewLayer { + let videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + videoPreviewLayer.videoGravity = .resizeAspectFill + return videoPreviewLayer + } + + func makeCaptureSession() -> AVCaptureSession { + let captureSession = AVCaptureSession() + guard let device = AVCaptureDevice.default(for: AVMediaType.video) else { fatalError() } + guard let input = try? AVCaptureDeviceInput(device: device) else { fatalError() } + captureSession.addInput(input) + let captureMetadataOutput = AVCaptureMetadataOutput() + captureSession.addOutput(captureMetadataOutput) + captureMetadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) + captureMetadataOutput.metadataObjectTypes = Self.supportedCodeTypes + return captureSession + } + + // MARK: Private + + @objc + private func didTapDetailButtonClick() { guard let data = qrCodeData else { return } clickQRCodeData?(data, self) } @@ -135,75 +228,12 @@ open class SPQRCameraController: SPController { make.centerY.equalTo(backButton.snp.centerY) } } - - override open func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - previewLayer.frame = .init( - x: .zero, y: .zero, - width: view.layer.bounds.width, - height: view.layer.bounds.height - ) - maskView.frame = previewLayer.frame - } - - // MARK: - Internal - - func updateInterface() { - let duration: TimeInterval = 0.22 - if qrCodeData != nil { - detailView.isHidden = false - if case .flowWallet = qrCodeData { - detailView.applyDefaultAppearance(with: .init(content: .white, background: UIColor(hex: "#00EF8B"))) - frameLayer.strokeColor = UIColor(hex: "#00EF8B").cgColor - } - if case .ethWallet = qrCodeData { - detailView.applyDefaultAppearance(with: .init(content: .white, background: UIColor(hex: "#00EF8B"))) - frameLayer.strokeColor = UIColor(hex: "#00EF8B").cgColor - } - UIView.animate(withDuration: duration, delay: .zero, options: .curveEaseInOut, animations: { - self.detailView.transform = .identity - self.detailView.alpha = 1 - }) - } else { - UIView.animate(withDuration: duration, delay: .zero, options: .curveEaseInOut, animations: { - self.detailView.transform = .init(scale: 0.9) - self.detailView.alpha = .zero - }, completion: { _ in - self.detailView.isHidden = true - }) - } - } - - static let supportedCodeTypes = [ - AVMetadataObject.ObjectType.aztec, - AVMetadataObject.ObjectType.qr, - ] - - func makeVideoPreviewLayer() -> AVCaptureVideoPreviewLayer { - let videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession) - videoPreviewLayer.videoGravity = .resizeAspectFill - return videoPreviewLayer - } - - func makeCaptureSession() -> AVCaptureSession { - let captureSession = AVCaptureSession() - guard let device = AVCaptureDevice.default(for: AVMediaType.video) else { fatalError() } - guard let input = try? AVCaptureDeviceInput(device: device) else { fatalError() } - captureSession.addInput(input) - let captureMetadataOutput = AVCaptureMetadataOutput() - captureSession.addOutput(captureMetadataOutput) - captureMetadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) - captureMetadataOutput.metadataObjectTypes = Self.supportedCodeTypes - return captureSession - } } extension UIViewController { var statusBarHeight: CGFloat { - guard - let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let height = scene.statusBarManager?.statusBarFrame.height + guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let height = scene.statusBarManager?.statusBarFrame.height else { return 0 } diff --git a/FRW/Tools/ThirdParty/Snappable/Public/Types/SnapMode.swift b/FRW/Tools/ThirdParty/Snappable/Public/Types/SnapMode.swift index 86cc7064..95347de0 100644 --- a/FRW/Tools/ThirdParty/Snappable/Public/Types/SnapMode.swift +++ b/FRW/Tools/ThirdParty/Snappable/Public/Types/SnapMode.swift @@ -6,13 +6,15 @@ import UIKit /// - `.immediately` /// - `.afterScrolling` public struct SnapMode { + // MARK: Public + /// The default setting for snapping after scrolling. public static let afterScrolling: SnapMode = Self.afterScrolling(decelerationRate: .fast) /// The default setting for snapping immediately. - public static let immediately: SnapMode = Self.immediately(decelerationRate: .normal, withFlick: true) - - let decelerationRate: DecelerationRate - let snapTiming: SnapTiming + public static let immediately: SnapMode = Self.immediately( + decelerationRate: .normal, + withFlick: true + ) /// An editable setting for snapping after scrolling. /// - Parameter decelerationRate: The deceleration rate of the ScrollView after dragging end. @@ -26,7 +28,15 @@ public struct SnapMode { /// - decelerationRate: The deceleration rate of the ScrollView after dragging end. /// - withFlick: The flag whether or not do snapping in consideration of flicking. /// - Returns: SnapMode, edited as immediately. - public static func immediately(decelerationRate: DecelerationRate, withFlick: Bool = true) -> SnapMode { + public static func immediately( + decelerationRate: DecelerationRate, + withFlick: Bool = true + ) -> SnapMode { SnapMode(decelerationRate: decelerationRate, snapTiming: .immediately(withFlick: withFlick)) } + + // MARK: Internal + + let decelerationRate: DecelerationRate + let snapTiming: SnapTiming } diff --git a/FRW/Tools/ThirdParty/VComponents/Components/Buttons/VPrimaryButton/VPrimaryButtonModel.swift b/FRW/Tools/ThirdParty/VComponents/Components/Buttons/VPrimaryButton/VPrimaryButtonModel.swift index 1a69f43f..65a687a0 100644 --- a/FRW/Tools/ThirdParty/VComponents/Components/Buttons/VPrimaryButton/VPrimaryButtonModel.swift +++ b/FRW/Tools/ThirdParty/VComponents/Components/Buttons/VPrimaryButton/VPrimaryButtonModel.swift @@ -11,24 +11,31 @@ import SwiftUI /// Model that describes UI. public struct VPrimaryButtonModel { - // MARK: Properties - - /// Sub-model containing layout properties. - public var layout: Layout = .init() - - /// Sub-model containing color properties. - public var colors: Colors = .init() - - /// Sub-model containing font properties. - public var fonts: Fonts = .init() + // MARK: Lifecycle /// Initializes model with default values. public init() {} + // MARK: Public + // MARK: Layout /// Sub-model containing layout properties. public struct Layout { + // MARK: Lifecycle + + // MARK: Initializers + + /// Initializes sub-model with default values. + public init() {} + + // MARK: Public + + // MARK: Content Margin + + /// Sub-model containing `horizontal` and `vertical` margins. + public typealias ContentMargin = LayoutGroup_HV + // MARK: Properties /// Button height. Defaults to `56`. @@ -40,8 +47,6 @@ public struct VPrimaryButtonModel { /// Button border width. Defaults to `0`. public var borderWidth: CGFloat = 0 - var hasBorder: Bool { borderWidth > 0 } - /// Content margin. Defaults to `15` horizontally and `3` vertically. public var contentMargin: ContentMargin = .init( horizontal: 15, @@ -53,23 +58,36 @@ public struct VPrimaryButtonModel { /// Only visible when state is set to `loading`. public var loaderSpacing: CGFloat = 20 + // MARK: Internal + let loaderWidth: CGFloat = 10 + var hasBorder: Bool { borderWidth > 0 } + } + + // MARK: Colors + + /// Sub-model containing color properties. + public struct Colors { + // MARK: Lifecycle + // MARK: Initializers /// Initializes sub-model with default values. public init() {} - // MARK: Content Margin + // MARK: Public - /// Sub-model containing `horizontal` and `vertical` margins. - public typealias ContentMargin = LayoutGroup_HV - } + // MARK: State Colors - // MARK: Colors + /// Sub-model containing colors for component states. + public typealias StateColors = StateColors_EPLD + + // MARK: State Opacities + + /// Sub-model containing opacities for component states. + public typealias StateOpacities = StateOpacities_PD - /// Sub-model containing color properties. - public struct Colors { // MARK: Properties /// Content opacities. @@ -106,39 +124,41 @@ public struct VPrimaryButtonModel { /// Loader colors. public var loader: Color = ColorBook.primaryInverted + } + + // MARK: Fonts + + /// Sub-model containing font properties. + public struct Fonts { + // MARK: Lifecycle // MARK: Initializers /// Initializes sub-model with default values. public init() {} - // MARK: State Colors - - /// Sub-model containing colors for component states. - public typealias StateColors = StateColors_EPLD - - // MARK: State Opacities - - /// Sub-model containing opacities for component states. - public typealias StateOpacities = StateOpacities_PD - } - - // MARK: Fonts + // MARK: Public - /// Sub-model containing font properties. - public struct Fonts { // MARK: Properties /// Title font. Defaults to system font of size `16` with `semibold` weight. /// /// Only applicable when using init with title. public var title: Font = .system(size: 16, weight: .semibold) + } - // MARK: Initializers + // MARK: Properties - /// Initializes sub-model with default values. - public init() {} - } + /// Sub-model containing layout properties. + public var layout: Layout = .init() + + /// Sub-model containing color properties. + public var colors: Colors = .init() + + /// Sub-model containing font properties. + public var fonts: Fonts = .init() + + // MARK: Internal // MARK: Sub-Models diff --git a/FRW/UI/Component/TabBarView/TabBarItemView.swift b/FRW/UI/Component/TabBarView/TabBarItemView.swift index a70b625f..d4707591 100644 --- a/FRW/UI/Component/TabBarView/TabBarItemView.swift +++ b/FRW/UI/Component/TabBarView/TabBarItemView.swift @@ -8,18 +8,23 @@ import Lottie import SwiftUI +// MARK: - TabBarItemView + struct TabBarItemView: View { var pageModel: TabBarPageModel - @Binding var selected: T + @Binding + var selected: T var action: () -> Void var animationView: some View { - ResizableLottieView(lottieView: pageModel.lottieView, - color: selected == pageModel.tag ? pageModel.color : Color.gray) - .aspectRatio(contentMode: .fit) - .frame(width: 30, height: 30) - .frame(maxWidth: .infinity) - .contentShape(Rectangle()) + ResizableLottieView( + lottieView: pageModel.lottieView, + color: selected == pageModel.tag ? pageModel.color : Color.gray + ) + .aspectRatio(contentMode: .fit) + .frame(width: 30, height: 30) + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) } var body: some View { @@ -43,20 +48,24 @@ struct TabBarItemView: View { } } +// MARK: - WalletView_Previews + struct WalletView_Previews: PreviewProvider { static let animation = AnimationView(name: "Copy", bundle: .main) static var previews: some View { - ResizableLottieView(lottieView: animation, - color: Color.gray) + ResizableLottieView( + lottieView: animation, + color: Color.gray + ) // LottieView(name: "Coin2", loopMode: .loop) - .aspectRatio(contentMode: .fit) - .frame(width: 300, height: 300) - .frame(maxWidth: .infinity) - .onAppear { - animation.play() - }.onTapGesture { - animation.play() - } + .aspectRatio(contentMode: .fit) + .frame(width: 300, height: 300) + .frame(maxWidth: .infinity) + .onAppear { + animation.play() + }.onTapGesture { + animation.play() + } } } diff --git a/FRW/UI/Extension/Others.swift b/FRW/UI/Extension/Others.swift index 99108932..261491c8 100644 --- a/FRW/UI/Extension/Others.swift +++ b/FRW/UI/Extension/Others.swift @@ -17,12 +17,12 @@ extension UICollectionReusableView { extension Decimal { var doubleValue: Double { - return NSDecimalNumber(decimal: self).doubleValue + NSDecimalNumber(decimal: self).doubleValue } } extension CaseIterable { static var count: Int { - return allCases.count + allCases.count } } diff --git a/FRWTests/SymmetricEncryptionTests.swift b/FRWTests/SymmetricEncryptionTests.swift index bf68d21b..19dbd8e1 100644 --- a/FRWTests/SymmetricEncryptionTests.swift +++ b/FRWTests/SymmetricEncryptionTests.swift @@ -11,9 +11,11 @@ import XCTest final class SymmetricEncryptionTests: XCTestCase { let key = "0123456789" let seedPhrase = "upgrade snack buzz employ female cute quote kit rack couple toddler glare" - let base64Encrypted = "R1GTsKkvL8lL1MTztxur3NDAvaJv6g6adciYhRxe/Jg1/aY87WbNzdwV2HhWpfSAn6AwezSOZ+nhJLmP1Ck37Zx4SBXU14rVW1Lzw8vcxfLJRSDEW3Cmx4N8jlx78xyMrQCEJTM=" + let base64Encrypted = + "R1GTsKkvL8lL1MTztxur3NDAvaJv6g6adciYhRxe/Jg1/aY87WbNzdwV2HhWpfSAn6AwezSOZ+nhJLmP1Ck37Zx4SBXU14rVW1Lzw8vcxfLJRSDEW3Cmx4N8jlx78xyMrQCEJTM=" - let AESBase64Encrypted = "W1+ejoJBIqZ1vwDxWeic38QjTjXeP0p827gzwHOw5v9YTFQltrvlEaGa336AUAbJbZxfUFBVIgLQbIelN2FQ6rlUUkP7TWIIGD6rdjkr7GSFtCTjHqvkxyTLVlMGeKDIYX9md3I=" + let AESBase64Encrypted = + "W1+ejoJBIqZ1vwDxWeic38QjTjXeP0p827gzwHOw5v9YTFQltrvlEaGa336AUAbJbZxfUFBVIgLQbIelN2FQ6rlUUkP7TWIIGD6rdjkr7GSFtCTjHqvkxyTLVlMGeKDIYX9md3I=" // MARK: - ChaChaPoly