From a45ecb9403c552cfd65f512af6e4f0347673c7fb Mon Sep 17 00:00:00 2001 From: Simon McLoughlin Date: Mon, 27 May 2024 17:03:16 +0100 Subject: [PATCH] - create new VC for importing wallets via private keys - wire up basic setup for non-encrypted keys --- Kukai Mobile.xcodeproj/project.pbxproj | 4 + .../Localization/en.lproj/Localizable.strings | 1 + .../AlreadyHaveWalletViewController.swift | 16 +- .../Base.lproj/Onboarding.storyboard | 253 +++++++++++++++--- .../ImportPrivateKeyViewController.swift | 198 ++++++++++++++ 5 files changed, 430 insertions(+), 42 deletions(-) create mode 100644 Kukai Mobile/Modules/Onboarding/ImportPrivateKeyViewController.swift diff --git a/Kukai Mobile.xcodeproj/project.pbxproj b/Kukai Mobile.xcodeproj/project.pbxproj index 3e8290e8..333913ac 100644 --- a/Kukai Mobile.xcodeproj/project.pbxproj +++ b/Kukai Mobile.xcodeproj/project.pbxproj @@ -190,6 +190,7 @@ C06997F12832946B00F6929D /* UITextView+extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06997F02832946B00F6929D /* UITextView+extensions.swift */; }; C06AEDFD2C00997E005CFDAA /* AddAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06AEDFC2C00997E005CFDAA /* AddAccountViewController.swift */; }; C06AEDFF2C00998D005CFDAA /* AddAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06AEDFE2C00998D005CFDAA /* AddAccountViewModel.swift */; }; + C06BA0142C04CC5600FA6EEB /* ImportPrivateKeyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06BA0132C04CC5600FA6EEB /* ImportPrivateKeyViewController.swift */; }; C06BC53D2A5EE41B00A0D979 /* SearchResultCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06BC53B2A5EE41B00A0D979 /* SearchResultCell.swift */; }; C06BC53E2A5EE41B00A0D979 /* SearchResultCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C06BC53C2A5EE41B00A0D979 /* SearchResultCell.xib */; }; C06BC5412A5EE50E00A0D979 /* SearchResultsCountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06BC53F2A5EE50E00A0D979 /* SearchResultsCountCell.swift */; }; @@ -613,6 +614,7 @@ C06997F02832946B00F6929D /* UITextView+extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextView+extensions.swift"; sourceTree = ""; }; C06AEDFC2C00997E005CFDAA /* AddAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountViewController.swift; sourceTree = ""; }; C06AEDFE2C00998D005CFDAA /* AddAccountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountViewModel.swift; sourceTree = ""; }; + C06BA0132C04CC5600FA6EEB /* ImportPrivateKeyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportPrivateKeyViewController.swift; sourceTree = ""; }; C06BC53B2A5EE41B00A0D979 /* SearchResultCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultCell.swift; sourceTree = ""; }; C06BC53C2A5EE41B00A0D979 /* SearchResultCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SearchResultCell.xib; sourceTree = ""; }; C06BC53F2A5EE50E00A0D979 /* SearchResultsCountCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsCountCell.swift; sourceTree = ""; }; @@ -1064,6 +1066,7 @@ C02914372A6589EE00A8AF08 /* ConfirmPasscodeViewController.swift */, C015F33C29DAEF2600895009 /* AlreadyHaveWalletViewController.swift */, C0D6C04229DDB93800D890ED /* ImportWalletViewController.swift */, + C06BA0132C04CC5600FA6EEB /* ImportPrivateKeyViewController.swift */, C0E44BAB2A41FDEE00C2A7C0 /* WatchWalletViewController.swift */, C0E669F82A5C50C500F7AA25 /* AccountScanningViewController.swift */, C08FE2392AD4343500327BF9 /* BackupViewController.swift */, @@ -2064,6 +2067,7 @@ C049B5B926A084A500F1C5E0 /* UIView+extensions.swift in Sources */, C008D4132A979B5B000B4503 /* AccountButtonCell.swift in Sources */, C0DB48172785C6DD00D3B4F9 /* FadeSegue.swift in Sources */, + C06BA0142C04CC5600FA6EEB /* ImportPrivateKeyViewController.swift in Sources */, C0FBC893292C162000B29921 /* HiddenBalancesViewModel.swift in Sources */, C04E286A293F950900DC4171 /* TokenDetailsActivityHeaderCell.swift in Sources */, C06E0D61287C3E28007A580B /* WalletConnectViewModel.swift in Sources */, diff --git a/Kukai Mobile/Localization/en.lproj/Localizable.strings b/Kukai Mobile/Localization/en.lproj/Localizable.strings index 751a5c89..2c80ad88 100644 --- a/Kukai Mobile/Localization/en.lproj/Localizable.strings +++ b/Kukai Mobile/Localization/en.lproj/Localizable.strings @@ -52,6 +52,7 @@ "error-onramp-generic"="Unable to access this provider at this time, please try again later"; "error-fetching-domains"="Encountered an error checking for tezos domains"; "error-unsupported-wallet-type"="This type of wallet doesn't support this feature"; +"error-invalid-private-key"="Invalid private key. Please check you have the correct details and try again"; "error-funds-body-wc2"="Balance for address %@, is %@ and the required balance to pay for this transaction is %@"; "error-unknwon-wc2"="Unable to respond to Wallet Connect"; diff --git a/Kukai Mobile/Modules/Onboarding/AlreadyHaveWalletViewController.swift b/Kukai Mobile/Modules/Onboarding/AlreadyHaveWalletViewController.swift index 9d7efff6..5fa52750 100644 --- a/Kukai Mobile/Modules/Onboarding/AlreadyHaveWalletViewController.swift +++ b/Kukai Mobile/Modules/Onboarding/AlreadyHaveWalletViewController.swift @@ -24,7 +24,7 @@ class AlreadyHaveWalletViewController: UIViewController, UITableViewDelegate, UI } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 3 + return 4 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -44,12 +44,17 @@ class AlreadyHaveWalletViewController: UIViewController, UITableViewDelegate, UI cell.descriptionLabel.text = "Import accounts using your recovery phrase from Kukai or another wallet" case 2: + cell.iconView.image = UIImage(named: "WalletRestore") + cell.titleLabel.text = "Import a Private Key" + cell.descriptionLabel.text = "Import a wallet from a private key" + + case 3: cell.iconView.image = UIImage(named: "WalletWatch") cell.titleLabel.text = "Watch a Tezos Address" cell.descriptionLabel.text = "Watch a public address or .tez domain" /* - case 3: + case 4: cell.iconView.image = UIImage(named: "WalletLedger") cell.titleLabel.text = "Connect with Ledger" cell.descriptionLabel.text = "Add accounts from your Bluetooth hardware wallet" @@ -79,12 +84,15 @@ class AlreadyHaveWalletViewController: UIViewController, UITableViewDelegate, UI case 1: self.performSegue(withIdentifier: "phrase", sender: nil) - + case 2: + self.performSegue(withIdentifier: "private-key", sender: nil) + + case 3: self.performSegue(withIdentifier: "watch", sender: nil) /* - case 3: + case 4: self.alert(withTitle: "Under Construction", andMessage: "coming soon") */ diff --git a/Kukai Mobile/Modules/Onboarding/Base.lproj/Onboarding.storyboard b/Kukai Mobile/Modules/Onboarding/Base.lproj/Onboarding.storyboard index 91a8948c..765d1f06 100644 --- a/Kukai Mobile/Modules/Onboarding/Base.lproj/Onboarding.storyboard +++ b/Kukai Mobile/Modules/Onboarding/Base.lproj/Onboarding.storyboard @@ -1176,13 +1176,13 @@ Passcode must have at least 3 unique digits, and no more than 3 sequential digit - + - + @@ -1482,35 +1482,7 @@ Passcode must have at least 3 unique digits, and no more than 3 sequential digit - - - - - - - - - - - - - - - - - - - - - - - - + @@ -1557,7 +1529,203 @@ You will not be able to transact in this wallet. - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1635,7 +1803,7 @@ You will not be able to transact in this wallet. - + @@ -2033,7 +2201,7 @@ You will not be able to transact in this wallet. - + @@ -2051,7 +2219,7 @@ You will not be able to transact in this wallet. - + @@ -2100,7 +2268,7 @@ Social wallets are secured by your chosen social provider credentials. If you lo - + @@ -2149,10 +2317,13 @@ HD Wallets allow you to create many accounts within the same wallet, all of them - + + + + @@ -2183,6 +2354,9 @@ HD Wallets allow you to create many accounts within the same wallet, all of them + + + @@ -2204,6 +2378,9 @@ HD Wallets allow you to create many accounts within the same wallet, all of them + + + @@ -2228,7 +2405,7 @@ HD Wallets allow you to create many accounts within the same wallet, all of them - + diff --git a/Kukai Mobile/Modules/Onboarding/ImportPrivateKeyViewController.swift b/Kukai Mobile/Modules/Onboarding/ImportPrivateKeyViewController.swift new file mode 100644 index 00000000..bf11b2d6 --- /dev/null +++ b/Kukai Mobile/Modules/Onboarding/ImportPrivateKeyViewController.swift @@ -0,0 +1,198 @@ +// +// ImportPrivateKeyViewController.swift +// Kukai Mobile +// +// Created by Simon Mcloughlin on 27/05/2024. +// + +import UIKit +import KukaiCoreSwift +import KukaiCryptoSwift +import Sodium + +class ImportPrivateKeyViewController: UIViewController { + + @IBOutlet weak var scrollView: AutoScrollView! + @IBOutlet weak var textView: UITextView! + @IBOutlet weak var textViewErrorLabel: UILabel! + @IBOutlet weak var passwordTextField: ValidatorTextField! + @IBOutlet weak var passwordErrorLabel: UILabel! + @IBOutlet weak var addressTextField: ValidatorTextField! + @IBOutlet weak var addressErrorLabel: UILabel! + @IBOutlet weak var importButton: CustomisableButton! + + override func viewDidLoad() { + super.viewDidLoad() + let _ = self.view.addGradientBackgroundFull() + importButton.customButtonType = .primary + + textViewErrorLabel.isHidden = true + passwordErrorLabel.isHidden = true + addressErrorLabel.isHidden = true + + textView.delegate = self + textView.text = "Enter Private Key" + textView.textColor = UIColor.colorNamed("Txt10") + textView.contentInset = UIEdgeInsets(top: 4, left: 6, bottom: 4, right: 6) + + passwordTextField.validatorTextFieldDelegate = self + passwordTextField.validator = NoWhiteSpaceStringValidator() + + addressTextField.validatorTextFieldDelegate = self + addressTextField.validator = TezosAddressValidator(ownAddress: "") + + let tap = UITapGestureRecognizer(target: self, action: #selector(ImportPrivateKeyViewController.resignAll)) + view.addGestureRecognizer(tap) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.scrollView.setupAutoScroll(focusView: passwordTextField, parentView: self.view) + self.scrollView.autoScrollDelegate = self + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.scrollView.stopAutoScroll() + } + + private func validateTextView() { + let textViewValidation = doesTextViewPassValidation() + + if !textViewValidation { + textViewErrorLabel.text = "Invalid private key" + textViewErrorLabel.isHidden = false + importButton.isEnabled = false + + } else { + importButton.isEnabled = true + } + } + + private func doesTextViewPassValidation(fullstring: String? = nil) -> Bool { + var textViewText = fullstring ?? textView.text ?? "" + textViewText = textViewText.trimmingCharacters(in: .whitespacesAndNewlines) + + return textViewText.count > 20 + } + + @objc private func resignAll() { + textView.resignFirstResponder() + passwordTextField.resignFirstResponder() + addressTextField.resignFirstResponder() + } + + @IBAction func importTapped(_ sender: Any) { + guard let inputText = textView.text else { + self.windowError(withTitle: "error".localized(), description: "error-invalid-private-key".localized()) + return + } + + let first4 = inputText.prefix(4) + if first4 == "edsk" { + guard let decoded = Base58Check.decode(string: inputText, prefix: Prefix.Keys.Ed25519.secret), let keyPair = Sodium.shared.sign.keyPair(seed: Array(decoded.prefix(32))) else { + self.windowError(withTitle: "error".localized(), description: "error-invalid-private-key".localized()) + return + } + + let privateKey = PrivateKey(keyPair.secretKey) + let publicKey = PublicKey(keyPair.publicKey) + + } else if first4 == "spsk" { + guard let decoded = Base58Check.decode(string: inputText, prefix: Prefix.Keys.Secp256k1.secret) else { + self.windowError(withTitle: "error".localized(), description: "error-invalid-private-key".localized()) + return + } + + let privateKey = PrivateKey(decoded, signingCurve: .secp256k1) + let publicKey = KeyPair.secp256k1PublicKey(fromPrivateKeyBytes: privateKey.bytes) + + } else { + self.windowError(withTitle: "error".localized(), description: "error-invalid-private-key".localized()) + } + } +} + +extension ImportPrivateKeyViewController: UITextViewDelegate { + + func textViewDidBeginEditing(_ textView: UITextView) { + scrollView.viewToFocusOn = nil + scrollView.contentOffset = CGPoint(x: 0, y: 0) + + if textView.text == "Enter Private Key" { + textView.text = nil + textView.textColor = UIColor.colorNamed("Txt6") + } + + textViewErrorLabel.isHidden = true + } + + func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + if text == "\n" { + textView.resignFirstResponder() + return false + } + + if let textViewString = textView.text, let swtRange = Range(range, in: textViewString) { + let fullString = textViewString.replacingCharacters(in: swtRange, with: text) + importButton.isEnabled = doesTextViewPassValidation(fullstring: fullString) + } + + return true + } + + func textViewDidEndEditing(_ textView: UITextView) { + if textView.text.isEmpty { + textView.text = "Enter Private Key" + textView.textColor = UIColor.colorNamed("Txt10") + textViewErrorLabel.isHidden = true + } else { + validateTextView() + } + } +} + +extension ImportPrivateKeyViewController: ValidatorTextFieldDelegate { + + func textFieldDidBeginEditing(_ textField: UITextField) { + scrollView.viewToFocusOn = textField + } + + func textFieldDidEndEditing(_ textField: UITextField) { + } + + func textFieldShouldClear(_ textField: UITextField) -> Bool { + textField.text = "" + textField.resignFirstResponder() + importButton.isEnabled = isEverythingValid() + + return false + } + + func validated(_ validated: Bool, textfield: ValidatorTextField, forText text: String) { + importButton.isEnabled = isEverythingValid() + } + + func doneOrReturnTapped(isValid: Bool, textfield: ValidatorTextField, forText text: String?) { + + } + + private func isEverythingValid() -> Bool { + return (doesTextViewPassValidation() && + ((passwordTextField.text ?? "").isEmpty || passwordTextField.isValid) && + ((addressTextField.text ?? "").isEmpty || addressTextField.isValid) + ) + } +} + +extension ImportPrivateKeyViewController: AutoScrollViewDelegate { + + func keyboardWillShow() { + + } + + func keyboardWillHide() { + + } +}