Make Ethereum, Keychain and AccountsService singletons

This commit is contained in:
Ivan Grachyov 2021-07-17 20:20:51 +03:00
parent 8dd2a476f7
commit b9a9b6787c
9 changed files with 67 additions and 46 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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() })
}

View File

@ -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,

View File

@ -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.")