diff --git a/Encrypted Ink/Agent.swift b/Encrypted Ink/Agent.swift index 377e6290..0e6ed557 100644 --- a/Encrypted Ink/Agent.swift +++ b/Encrypted Ink/Agent.swift @@ -11,7 +11,8 @@ class Agent: NSObject { private override init() { super.init() } private var statusBarItem: NSStatusItem! - private var hasPassword = Keychain.password != nil + private let accountsService = AccountsService.shared + private var hasPassword = Keychain.shared.password != nil private var didEnterPasswordOnStart = false var statusBarButtonIsBlocked = false @@ -50,7 +51,7 @@ class Agent: NSObject { let windowController = Window.showNew() let completion = onSelectedAccount(session: wcSession) - let accounts = AccountsService.getAccounts() + let accounts = accountsService.getAccounts() if !accounts.isEmpty { let accountsList = AccountsListViewController.with(preloadedAccounts: accounts) accountsList.onSelectedAccount = completion diff --git a/Encrypted Ink/Ethereum/Ethereum.swift b/Encrypted Ink/Ethereum/Ethereum.swift index 9274bf1b..9ea7f702 100644 --- a/Encrypted Ink/Ethereum/Ethereum.swift +++ b/Encrypted Ink/Ethereum/Ethereum.swift @@ -11,14 +11,17 @@ struct Ethereum { case failedToSendTransaction } - private static let queue = DispatchQueue(label: "Ethereum", qos: .default) + private let queue = DispatchQueue(label: "Ethereum", qos: .default) + private init() {} - private static let network: Network = AlchemyNetwork( + static let shared = Ethereum() + + private let network: Network = AlchemyNetwork( chain: "mainnet", apiKey: Secrets.alchemy ) - static func sign(message: String, account: Account) throws -> String { + func sign(message: String, account: Account) throws -> String { let ethPrivateKey = EthPrivateKey(hex: account.privateKey) let signature = SECP256k1Signature( @@ -36,14 +39,14 @@ struct Ethereum { return data.toPrefixedHexString() } - static func signPersonal(message: String, account: Account) throws -> String { + func signPersonal(message: String, account: Account) throws -> String { let ethPrivateKey = EthPrivateKey(hex: account.privateKey) let signed = SignedPersonalMessageBytes(message: message, signerKey: ethPrivateKey) let data = try signed.value().toPrefixedHexString() return data } - static func sign(typedData: String, account: Account) throws -> String { + func sign(typedData: String, account: Account) throws -> String { let data = try EIP712TypedData(jsonString: typedData) let hash = EIP712Hash(domain: data.domain, typedData: data) let privateKey = EthPrivateKey(hex: account.privateKey) @@ -51,7 +54,7 @@ struct Ethereum { return try signer.signatureData(hash: hash).toPrefixedHexString() } - static func send(transaction: Transaction, account: Account) throws -> String { + func send(transaction: Transaction, account: Account) throws -> String { let bytes = signedTransactionBytes(transaction: transaction, account: account) let response = try SendRawTransactionProcedure(network: network, transactionBytes: bytes).call() guard let hash = response["result"].string else { @@ -60,7 +63,7 @@ struct Ethereum { return hash } - private static func signedTransactionBytes(transaction: Transaction, account: Account) -> EthContractCallBytes { + private func signedTransactionBytes(transaction: Transaction, account: Account) -> EthContractCallBytes { let senderKey = EthPrivateKey(hex: account.privateKey) let contractAddress = EthAddress(hex: transaction.to) let functionCall = BytesFromHexString(hex: transaction.data) @@ -98,7 +101,7 @@ struct Ethereum { return bytes } - static func prepareTransaction(_ transaction: Transaction, completion: @escaping (Transaction) -> Void) { + func prepareTransaction(_ transaction: Transaction, completion: @escaping (Transaction) -> Void) { var transaction = transaction if transaction.nonce == nil { @@ -130,7 +133,7 @@ struct Ethereum { } - private static func getGas(from: String, to: String, gasPrice: String, weiAmount: EthNumber, data: String, completion: @escaping (String?) -> Void) { + private func getGas(from: String, to: String, gasPrice: String, weiAmount: EthNumber, data: String, completion: @escaping (String?) -> Void) { queue.async { let gas = try? EthGasEstimate( network: network, @@ -154,7 +157,7 @@ struct Ethereum { } } - private static func getGasPrice(completion: @escaping (String?) -> Void) { + private func getGasPrice(completion: @escaping (String?) -> Void) { queue.async { let gasPrice = try? EthGasPrice(network: network).value().toHexString() DispatchQueue.main.async { @@ -163,7 +166,7 @@ struct Ethereum { } } - private static func getNonce(from: String, completion: @escaping (String?) -> Void) { + private func getNonce(from: String, completion: @escaping (String?) -> Void) { queue.async { let nonce = try? EthTransactions(network: network, address: EthAddress(hex: from), blockChainState: PendingBlockChainState()).count().value().toHexString() DispatchQueue.main.async { diff --git a/Encrypted Ink/Screens/AccountsListViewController.swift b/Encrypted Ink/Screens/AccountsListViewController.swift index 748cf93c..b3a542d4 100644 --- a/Encrypted Ink/Screens/AccountsListViewController.swift +++ b/Encrypted Ink/Screens/AccountsListViewController.swift @@ -5,6 +5,7 @@ import Cocoa class AccountsListViewController: NSViewController { private let agent = Agent.shared + private let accountsService = AccountsService.shared private var accounts = [Account]() var onSelectedAccount: ((Account) -> Void)? @@ -58,7 +59,7 @@ class AccountsListViewController: NSViewController { } private func reloadAccounts() { - accounts = AccountsService.getAccounts() + accounts = accountsService.getAccounts() } deinit { @@ -95,7 +96,7 @@ class AccountsListViewController: NSViewController { } @objc private func didClickCreateAccount() { - AccountsService.createAccount() + accountsService.createAccount() reloadAccounts() tableView.reloadData() // TODO: show backup phrase @@ -177,7 +178,7 @@ class AccountsListViewController: NSViewController { } private func removeAccountAtIndex(_ index: Int) { - AccountsService.removeAccount(accounts[index]) + accountsService.removeAccount(accounts[index]) accounts.remove(at: index) tableView.reloadData() } diff --git a/Encrypted Ink/Screens/ApproveTransactionViewController.swift b/Encrypted Ink/Screens/ApproveTransactionViewController.swift index a7ffbc4d..cf84b3a1 100644 --- a/Encrypted Ink/Screens/ApproveTransactionViewController.swift +++ b/Encrypted Ink/Screens/ApproveTransactionViewController.swift @@ -22,6 +22,7 @@ class ApproveTransactionViewController: NSViewController { } private let gasService = GasService.shared + private let ethereum = Ethereum.shared private let priceService = PriceService.shared private var currentGasInfo: GasService.Info? private var transaction: Transaction! @@ -57,7 +58,7 @@ class ApproveTransactionViewController: NSViewController { } private func prepareTransaction() { - Ethereum.prepareTransaction(transaction) { [weak self] updated in + ethereum.prepareTransaction(transaction) { [weak self] updated in self?.transaction = updated self?.updateInterface() } diff --git a/Encrypted Ink/Screens/ImportViewController.swift b/Encrypted Ink/Screens/ImportViewController.swift index 2a1a5683..a5324cda 100644 --- a/Encrypted Ink/Screens/ImportViewController.swift +++ b/Encrypted Ink/Screens/ImportViewController.swift @@ -4,6 +4,7 @@ import Cocoa class ImportViewController: NSViewController { + private let accountsService = AccountsService.shared var onSelectedAccount: ((Account) -> Void)? @IBOutlet weak var textField: NSTextField! { @@ -19,8 +20,8 @@ class ImportViewController: NSViewController { } @IBAction func actionButtonTapped(_ sender: Any) { - let account = AccountsService.addAccount(input: textField.stringValue) - if let account = account, AccountsService.getAccounts().count == 1, let onSelectedAccount = onSelectedAccount { + let account = accountsService.addAccount(input: textField.stringValue) + if let account = account, accountsService.getAccounts().count == 1, let onSelectedAccount = onSelectedAccount { onSelectedAccount(account) } else { showAccountsList() @@ -42,7 +43,7 @@ class ImportViewController: NSViewController { extension ImportViewController: NSTextFieldDelegate { func controlTextDidChange(_ obj: Notification) { - okButton.isEnabled = AccountsService.validateAccountInput(textField.stringValue) + okButton.isEnabled = accountsService.validateAccountInput(textField.stringValue) } } diff --git a/Encrypted Ink/Screens/PasswordViewController.swift b/Encrypted Ink/Screens/PasswordViewController.swift index 58c45ab5..f12919a7 100644 --- a/Encrypted Ink/Screens/PasswordViewController.swift +++ b/Encrypted Ink/Screens/PasswordViewController.swift @@ -16,6 +16,7 @@ class PasswordViewController: NSViewController { case create, repeatAfterCreate, enter } + private let keychain = Keychain.shared private var mode = Mode.create private var reason: String? private var passwordToRepeat: String? @@ -65,11 +66,11 @@ class PasswordViewController: NSViewController { case .repeatAfterCreate: let repeated = passwordTextField.stringValue if repeated == passwordToRepeat { - Keychain.save(password: repeated) + keychain.save(password: repeated) completion?(true) } case .enter: - if Keychain.password == passwordTextField.stringValue { + if keychain.password == passwordTextField.stringValue { completion?(true) } } diff --git a/Encrypted Ink/Services/AccountsService.swift b/Encrypted Ink/Services/AccountsService.swift index d8e942d8..3e93ed71 100644 --- a/Encrypted Ink/Services/AccountsService.swift +++ b/Encrypted Ink/Services/AccountsService.swift @@ -5,7 +5,13 @@ import WalletCore struct AccountsService { - static func validateAccountInput(_ input: String) -> Bool { + private init() {} + private let keychain = Keychain.shared + + static let shared = AccountsService() + + + func validateAccountInput(_ input: String) -> Bool { if Mnemonic.isValid(mnemonic: input) { return true } else if let data = Data(hexString: input) { @@ -15,14 +21,14 @@ struct AccountsService { } } - static func createAccount() { - guard let password = Keychain.password?.data(using: .utf8) else { return } + func createAccount() { + guard let password = keychain.password?.data(using: .utf8) else { return } let key = StoredKey(name: "", password: password) guard let privateKey = key.wallet(password: password)?.getKeyForCoin(coin: .ethereum) else { return } _ = saveAccount(privateKey: privateKey) } - static func addAccount(input: String) -> Account? { + func addAccount(input: String) -> Account? { let key: PrivateKey if Mnemonic.isValid(mnemonic: input) { key = HDWallet(mnemonic: input, passphrase: "").getKeyForCoin(coin: .ethereum) @@ -42,28 +48,28 @@ struct AccountsService { return account } - private static func saveAccount(privateKey: PrivateKey) -> Account? { + private func saveAccount(privateKey: PrivateKey) -> Account? { let address = CoinType.ethereum.deriveAddress(privateKey: privateKey).lowercased() // TODO: use checksum address let account = Account(privateKey: privateKey.data.hexString, address: address) var accounts = getAccounts() guard !accounts.contains(where: { $0.address == address }) else { return nil } accounts.append(account) - Keychain.save(accounts: accounts) + keychain.save(accounts: accounts) return account } - static func removeAccount(_ account: Account) { + func removeAccount(_ account: Account) { var accounts = getAccounts() accounts.removeAll(where: {$0.address == account.address }) - Keychain.save(accounts: accounts) + keychain.save(accounts: accounts) } - static func getAccounts() -> [Account] { - return Keychain.accounts + func getAccounts() -> [Account] { + return keychain.accounts } - static func getAccountForAddress(_ address: String) -> Account? { + func getAccountForAddress(_ address: String) -> Account? { let allAccounts = getAccounts() return allAccounts.first(where: { $0.address == address.lowercased() }) } diff --git a/Encrypted Ink/Services/Keychain.swift b/Encrypted Ink/Services/Keychain.swift index 8e5283e3..873ea5b2 100644 --- a/Encrypted Ink/Services/Keychain.swift +++ b/Encrypted Ink/Services/Keychain.swift @@ -4,14 +4,18 @@ import Foundation struct Keychain { - private static let prefix = "ink.encrypted.macos." + private let prefix = "ink.encrypted.macos." + + private init() {} + + static let shared = Keychain() private enum Key: String { case accounts = "ethereum.keys" case password = "password" } - static var password: String? { + var password: String? { if let data = get(key: .password), let password = String(data: data, encoding: .utf8) { return password } else { @@ -19,12 +23,12 @@ struct Keychain { } } - static func save(password: String) { + func save(password: String) { guard let data = password.data(using: .utf8) else { return } save(data: data, key: .password) } - static var accounts: [Account] { + var accounts: [Account] { if let data = get(key: .accounts), let accounts = try? JSONDecoder().decode([Account].self, from: data) { return accounts } else { @@ -32,14 +36,14 @@ struct Keychain { } } - static func save(accounts: [Account]) { + func save(accounts: [Account]) { guard let data = try? JSONEncoder().encode(accounts) else { return } save(data: data, key: .accounts) } // MARK: Private - private static func save(data: Data, key: Key) { + private func save(data: Data, key: Key) { let query = [kSecClass as String: kSecClassGenericPassword as String, kSecAttrAccount as String: prefix + key.rawValue, kSecValueData as String: data] as [String: Any] @@ -47,7 +51,7 @@ struct Keychain { SecItemAdd(query as CFDictionary, nil) } - private static func get(key: Key) -> Data? { + private func get(key: Key) -> Data? { guard let returnDataQueryValue = kCFBooleanTrue else { return nil } let query = [kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: prefix + key.rawValue, diff --git a/Encrypted Ink/WalletConnect.swift b/Encrypted Ink/WalletConnect.swift index 21cf0b01..93310e19 100644 --- a/Encrypted Ink/WalletConnect.swift +++ b/Encrypted Ink/WalletConnect.swift @@ -7,6 +7,9 @@ class WalletConnect { private let sessionStorage = SessionStorage.shared private let networkMonitor = NetworkMonitor.shared + private let ethereum = Ethereum.shared + private let accountsService = AccountsService.shared + static let shared = WalletConnect() private init() { @@ -174,11 +177,11 @@ class WalletConnect { } private func sendTransaction(_ transaction: Transaction, address: String, requestId: Int64, interactor: WCInteractor?) { - guard let account = AccountsService.getAccountForAddress(address) else { + guard let account = accountsService.getAccountForAddress(address) else { rejectRequest(id: requestId, interactor: interactor, message: "Something went wrong.") return } - guard let hash = try? Ethereum.send(transaction: transaction, account: account) else { + guard let hash = try? ethereum.send(transaction: transaction, account: account) else { rejectRequest(id: requestId, interactor: interactor, message: "Failed to send") return } @@ -186,18 +189,18 @@ class WalletConnect { } private func sign(id: Int64, message: String?, payload: WCEthereumSignPayload, address: String, interactor: WCInteractor?) { - guard let message = message, let account = AccountsService.getAccountForAddress(address) else { + guard let message = message, let account = accountsService.getAccountForAddress(address) else { rejectRequest(id: id, interactor: interactor, message: "Something went wrong.") return } var signed: String? switch payload { case .personalSign: - signed = try? Ethereum.signPersonal(message: message, account: account) + signed = try? ethereum.signPersonal(message: message, account: account) case .signTypeData: - signed = try? Ethereum.sign(typedData: message, account: account) + signed = try? ethereum.sign(typedData: message, account: account) case .sign: - signed = try? Ethereum.sign(message: message, account: account) + signed = try? ethereum.sign(message: message, account: account) } guard let result = signed else { rejectRequest(id: id, interactor: interactor, message: "Something went wrong.")