mirror of
https://github.com/lil-org/tokenary.git
synced 2024-12-02 09:33:49 +03:00
Complete keychain migration
This commit is contained in:
parent
61ac099c9f
commit
2af469ad35
@ -11,7 +11,6 @@ class Agent: NSObject {
|
||||
|
||||
private override init() { super.init() }
|
||||
private var statusBarItem: NSStatusItem!
|
||||
private let accountsService = AccountsService.shared
|
||||
private var hasPassword = Keychain.shared.password != nil
|
||||
private var didEnterPasswordOnStart = false
|
||||
|
||||
@ -72,9 +71,9 @@ class Agent: NSObject {
|
||||
}
|
||||
|
||||
let windowController = Window.showNew()
|
||||
let completion = onSelectedAccount(session: session)
|
||||
let completion = onSelectedWallet(session: session)
|
||||
let accountsList = instantiate(AccountsListViewController.self)
|
||||
accountsList.onSelectedAccount = completion
|
||||
accountsList.onSelectedWallet = completion
|
||||
windowController.contentViewController = accountsList
|
||||
}
|
||||
|
||||
@ -120,9 +119,9 @@ class Agent: NSObject {
|
||||
showInitialScreen(wcSession: session)
|
||||
}
|
||||
|
||||
func getAccountSelectionCompletionIfShouldSelect() -> ((LegacyAccountWithKey) -> Void)? {
|
||||
func getWalletSelectionCompletionIfShouldSelect() -> ((InkWallet) -> Void)? {
|
||||
let session = getSessionFromPasteboard()
|
||||
return onSelectedAccount(session: session)
|
||||
return onSelectedWallet(session: session)
|
||||
}
|
||||
|
||||
lazy private var statusBarMenu: NSMenu = {
|
||||
@ -223,10 +222,10 @@ class Agent: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func onSelectedAccount(session: WCSession?) -> ((LegacyAccountWithKey) -> Void)? {
|
||||
private func onSelectedWallet(session: WCSession?) -> ((InkWallet) -> Void)? {
|
||||
guard let session = session else { return nil }
|
||||
return { [weak self] account in
|
||||
self?.connectWallet(session: session, account: account)
|
||||
return { [weak self] wallet in
|
||||
self?.connectWallet(session: session, wallet: wallet)
|
||||
}
|
||||
}
|
||||
|
||||
@ -285,12 +284,12 @@ class Agent: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func connectWallet(session: WCSession, account: LegacyAccountWithKey) {
|
||||
private func connectWallet(session: WCSession, wallet: InkWallet) {
|
||||
let windowController = Window.showNew()
|
||||
let window = windowController.window
|
||||
windowController.contentViewController = WaitingViewController.withReason("Connecting")
|
||||
|
||||
WalletConnect.shared.connect(session: session, address: account.address) { [weak window] _ in
|
||||
WalletConnect.shared.connect(session: session, walletId: wallet.id) { [weak window] _ in
|
||||
if window?.isVisible == true {
|
||||
Window.closeAllAndActivateBrowser()
|
||||
}
|
||||
|
@ -6,9 +6,10 @@ import CryptoSwift
|
||||
|
||||
struct Ethereum {
|
||||
|
||||
enum Errors: Error {
|
||||
enum Error: Swift.Error {
|
||||
case invalidInputData
|
||||
case failedToSendTransaction
|
||||
case keyNotFound
|
||||
}
|
||||
|
||||
private let queue = DispatchQueue(label: "Ethereum", qos: .default)
|
||||
@ -21,8 +22,9 @@ struct Ethereum {
|
||||
apiKey: Secrets.alchemy
|
||||
)
|
||||
|
||||
func sign(message: String, account: LegacyAccountWithKey) throws -> String {
|
||||
let ethPrivateKey = EthPrivateKey(hex: account.privateKey)
|
||||
func sign(message: String, wallet: InkWallet) throws -> String {
|
||||
guard let privateKeyString = wallet.ethereumPrivateKey else { throw Error.keyNotFound }
|
||||
let ethPrivateKey = EthPrivateKey(hex: privateKeyString)
|
||||
|
||||
let signature = SECP256k1Signature(
|
||||
privateKey: ethPrivateKey,
|
||||
@ -39,32 +41,35 @@ struct Ethereum {
|
||||
return data.toPrefixedHexString()
|
||||
}
|
||||
|
||||
func signPersonal(message: String, account: LegacyAccountWithKey) throws -> String {
|
||||
let ethPrivateKey = EthPrivateKey(hex: account.privateKey)
|
||||
func signPersonal(message: String, wallet: InkWallet) throws -> String {
|
||||
guard let privateKeyString = wallet.ethereumPrivateKey else { throw Error.keyNotFound }
|
||||
let ethPrivateKey = EthPrivateKey(hex: privateKeyString)
|
||||
let signed = SignedPersonalMessageBytes(message: message, signerKey: ethPrivateKey)
|
||||
let data = try signed.value().toPrefixedHexString()
|
||||
return data
|
||||
}
|
||||
|
||||
func sign(typedData: String, account: LegacyAccountWithKey) throws -> String {
|
||||
func sign(typedData: String, wallet: InkWallet) throws -> String {
|
||||
guard let privateKeyString = wallet.ethereumPrivateKey else { throw Error.keyNotFound }
|
||||
let data = try EIP712TypedData(jsonString: typedData)
|
||||
let hash = EIP712Hash(domain: data.domain, typedData: data)
|
||||
let privateKey = EthPrivateKey(hex: account.privateKey)
|
||||
let privateKey = EthPrivateKey(hex: privateKeyString)
|
||||
let signer = EIP712Signer(privateKey: privateKey)
|
||||
return try signer.signatureData(hash: hash).toPrefixedHexString()
|
||||
}
|
||||
|
||||
func send(transaction: Transaction, account: LegacyAccountWithKey) throws -> String {
|
||||
let bytes = signedTransactionBytes(transaction: transaction, account: account)
|
||||
func send(transaction: Transaction, wallet: InkWallet) throws -> String {
|
||||
let bytes = try signedTransactionBytes(transaction: transaction, wallet: wallet)
|
||||
let response = try SendRawTransactionProcedure(network: network, transactionBytes: bytes).call()
|
||||
guard let hash = response["result"].string else {
|
||||
throw Errors.failedToSendTransaction
|
||||
throw Error.failedToSendTransaction
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
private func signedTransactionBytes(transaction: Transaction, account: LegacyAccountWithKey) -> EthContractCallBytes {
|
||||
let senderKey = EthPrivateKey(hex: account.privateKey)
|
||||
private func signedTransactionBytes(transaction: Transaction, wallet: InkWallet) throws -> EthContractCallBytes {
|
||||
guard let privateKeyString = wallet.ethereumPrivateKey else { throw Error.keyNotFound }
|
||||
let senderKey = EthPrivateKey(hex: privateKeyString)
|
||||
let contractAddress = EthAddress(hex: transaction.to)
|
||||
let functionCall = BytesFromHexString(hex: transaction.data)
|
||||
let bytes: EthContractCallBytes
|
||||
|
@ -5,14 +5,13 @@ import Cocoa
|
||||
class AccountsListViewController: NSViewController {
|
||||
|
||||
private let agent = Agent.shared
|
||||
private let accountsService = AccountsService.shared
|
||||
private var accounts = [LegacyAccountWithKey]()
|
||||
private let walletsManager = WalletsManager.shared
|
||||
private var cellModels = [CellModel]()
|
||||
|
||||
var onSelectedAccount: ((LegacyAccountWithKey) -> Void)?
|
||||
var onSelectedWallet: ((InkWallet) -> Void)?
|
||||
|
||||
enum CellModel {
|
||||
case account(LegacyAccountWithKey)
|
||||
case wallet(InkWallet)
|
||||
case addAccountOption(AddAccountOption)
|
||||
}
|
||||
|
||||
@ -45,11 +44,14 @@ class AccountsListViewController: NSViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private var wallets: [InkWallet] {
|
||||
return walletsManager.wallets
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
setupAccountsMenu()
|
||||
reloadAccounts()
|
||||
reloadTitle()
|
||||
updateCellModels()
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: NSApplication.didBecomeActiveNotification, object: nil)
|
||||
@ -61,30 +63,26 @@ class AccountsListViewController: NSViewController {
|
||||
menu.addItem(NSMenuItem(title: "Copy address", action: #selector(didClickCopyAddress(_:)), keyEquivalent: ""))
|
||||
menu.addItem(NSMenuItem(title: "View on Zerion", action: #selector(didClickViewOnZerion(_:)), keyEquivalent: ""))
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(NSMenuItem(title: "Show private key", action: #selector(didClickExportAccount(_:)), keyEquivalent: "")) // TODO: show different texts for secret words export
|
||||
menu.addItem(NSMenuItem(title: "Show account key", action: #selector(didClickExportAccount(_:)), keyEquivalent: ""))
|
||||
menu.addItem(NSMenuItem(title: "Remove account", action: #selector(didClickRemoveAccount(_:)), keyEquivalent: ""))
|
||||
menu.addItem(.separator())
|
||||
menu.addItem(NSMenuItem(title: "How to WalletConnect?", action: #selector(showInstructionsAlert), keyEquivalent: ""))
|
||||
tableView.menu = menu
|
||||
}
|
||||
|
||||
private func reloadAccounts() {
|
||||
accounts = accountsService.getAccounts()
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
private func reloadTitle() {
|
||||
titleLabel.stringValue = onSelectedAccount != nil && !accounts.isEmpty ? "Select\nAccount" : "Accounts"
|
||||
addButton.isHidden = accounts.isEmpty
|
||||
titleLabel.stringValue = onSelectedWallet != nil && !wallets.isEmpty ? "Select\nAccount" : "Accounts"
|
||||
addButton.isHidden = wallets.isEmpty
|
||||
}
|
||||
|
||||
@objc private func didBecomeActive() {
|
||||
guard view.window?.isVisible == true else { return }
|
||||
if let completion = agent.getAccountSelectionCompletionIfShouldSelect() {
|
||||
onSelectedAccount = completion
|
||||
if let completion = agent.getWalletSelectionCompletionIfShouldSelect() {
|
||||
onSelectedWallet = completion
|
||||
}
|
||||
reloadTitle()
|
||||
}
|
||||
@ -107,8 +105,7 @@ class AccountsListViewController: NSViewController {
|
||||
}
|
||||
|
||||
@objc private func didClickCreateAccount() {
|
||||
accountsService.createAccount()
|
||||
reloadAccounts()
|
||||
_ = try? walletsManager.createWallet()
|
||||
reloadTitle()
|
||||
updateCellModels()
|
||||
tableView.reloadData()
|
||||
@ -117,14 +114,13 @@ class AccountsListViewController: NSViewController {
|
||||
|
||||
@objc private func didClickImportAccount() {
|
||||
let importViewController = instantiate(ImportViewController.self)
|
||||
importViewController.onSelectedAccount = onSelectedAccount
|
||||
importViewController.onSelectedWallet = onSelectedWallet
|
||||
view.window?.contentViewController = importViewController
|
||||
}
|
||||
|
||||
@objc private func didClickViewOnZerion(_ sender: AnyObject) {
|
||||
let row = tableView.deselectedRow
|
||||
guard row >= 0 else { return }
|
||||
let address = accounts[row].address
|
||||
guard row >= 0, let address = wallets[row].ethereumAddress else { return }
|
||||
if let url = URL(string: "https://app.zerion.io/\(address)/overview") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
@ -132,8 +128,8 @@ class AccountsListViewController: NSViewController {
|
||||
|
||||
@objc private func didClickCopyAddress(_ sender: AnyObject) {
|
||||
let row = tableView.deselectedRow
|
||||
guard row >= 0 else { return }
|
||||
NSPasteboard.general.clearAndSetString(accounts[row].address)
|
||||
guard row >= 0, let address = wallets[row].ethereumAddress else { return }
|
||||
NSPasteboard.general.clearAndSetString(address)
|
||||
}
|
||||
|
||||
@objc private func didClickRemoveAccount(_ sender: AnyObject) {
|
||||
@ -155,34 +151,46 @@ class AccountsListViewController: NSViewController {
|
||||
}
|
||||
|
||||
@objc private func didClickExportAccount(_ sender: AnyObject) {
|
||||
// TODO: show different texts for secret words export
|
||||
let row = tableView.deselectedRow
|
||||
guard row >= 0 else { return }
|
||||
let isMnemonic = wallets[row].isMnemonic
|
||||
let alert = Alert()
|
||||
alert.messageText = "Private key gives full access to your funds."
|
||||
|
||||
alert.messageText = "\(isMnemonic ? "Secret words give" : "Private key gives") full access to your funds."
|
||||
alert.alertStyle = .critical
|
||||
alert.addButton(withTitle: "I understand the risks")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
if alert.runModal() == .alertFirstButtonReturn {
|
||||
agent.askAuthentication(on: view.window, getBackTo: self, onStart: false, reason: "Show private key") { [weak self] allowed in
|
||||
let reason = "Show \(isMnemonic ? "secret words" : "private key")"
|
||||
agent.askAuthentication(on: view.window, getBackTo: self, onStart: false, reason: reason) { [weak self] allowed in
|
||||
Window.activateWindow(self?.view.window)
|
||||
if allowed {
|
||||
self?.showPrivateKey(index: row)
|
||||
self?.showKey(index: row, mnemonic: isMnemonic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showPrivateKey(index: Int) {
|
||||
let privateKey = accounts[index].privateKey
|
||||
private func showKey(index: Int, mnemonic: Bool) {
|
||||
let wallet = wallets[index]
|
||||
|
||||
let secret: String
|
||||
if mnemonic, let mnemonicString = try? walletsManager.exportMnemonic(wallet: wallet) {
|
||||
secret = mnemonicString
|
||||
} else if let data = try? walletsManager.exportPrivateKey(wallet: wallet) {
|
||||
secret = data.hexString
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
let alert = Alert()
|
||||
alert.messageText = "Private key"
|
||||
alert.informativeText = privateKey
|
||||
alert.messageText = mnemonic ? "Secret words" : "Private key"
|
||||
alert.informativeText = secret
|
||||
alert.alertStyle = .informational
|
||||
alert.addButton(withTitle: "OK")
|
||||
alert.addButton(withTitle: "Copy")
|
||||
if alert.runModal() != .alertFirstButtonReturn {
|
||||
NSPasteboard.general.clearAndSetString(privateKey)
|
||||
NSPasteboard.general.clearAndSetString(secret)
|
||||
}
|
||||
}
|
||||
|
||||
@ -191,19 +199,19 @@ class AccountsListViewController: NSViewController {
|
||||
}
|
||||
|
||||
private func removeAccountAtIndex(_ index: Int) {
|
||||
accountsService.removeAccount(accounts[index])
|
||||
accounts.remove(at: index)
|
||||
let wallet = wallets[index]
|
||||
try? walletsManager.delete(wallet: wallet)
|
||||
reloadTitle()
|
||||
updateCellModels()
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
private func updateCellModels() {
|
||||
if accounts.isEmpty {
|
||||
if wallets.isEmpty {
|
||||
cellModels = [.addAccountOption(.createNew), .addAccountOption(.importExisting)]
|
||||
tableView.shouldShowRightClickMenu = false
|
||||
} else {
|
||||
cellModels = accounts.map { .account($0) }
|
||||
cellModels = wallets.map { .wallet($0) }
|
||||
tableView.shouldShowRightClickMenu = true
|
||||
}
|
||||
}
|
||||
@ -217,9 +225,9 @@ extension AccountsListViewController: NSTableViewDelegate {
|
||||
let model = cellModels[row]
|
||||
|
||||
switch model {
|
||||
case let .account(account):
|
||||
if let onSelectedAccount = onSelectedAccount {
|
||||
onSelectedAccount(account)
|
||||
case let .wallet(wallet):
|
||||
if let onSelectedWallet = onSelectedWallet {
|
||||
onSelectedWallet(wallet)
|
||||
} else {
|
||||
Timer.scheduledTimer(withTimeInterval: 0.01, repeats: false) { [weak self] _ in
|
||||
var point = NSEvent.mouseLocation
|
||||
@ -246,9 +254,9 @@ extension AccountsListViewController: NSTableViewDataSource {
|
||||
func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
|
||||
let model = cellModels[row]
|
||||
switch model {
|
||||
case let .account(account):
|
||||
case let .wallet(wallet):
|
||||
let rowView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("AccountCellView"), owner: self) as? AccountCellView
|
||||
rowView?.setup(address: account.address)
|
||||
rowView?.setup(address: wallet.ethereumAddress ?? "")
|
||||
return rowView
|
||||
case let .addAccountOption(addAccountOption):
|
||||
let rowView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("AddAccountOptionCellView"), owner: self) as? AddAccountOptionCellView
|
||||
@ -258,7 +266,7 @@ extension AccountsListViewController: NSTableViewDataSource {
|
||||
}
|
||||
|
||||
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
|
||||
if case .account = cellModels[row] {
|
||||
if case .wallet = cellModels[row] {
|
||||
return 50
|
||||
} else {
|
||||
return 44
|
||||
|
@ -4,9 +4,9 @@ import Cocoa
|
||||
|
||||
class ImportViewController: NSViewController {
|
||||
|
||||
private let accountsService = AccountsService.shared
|
||||
var onSelectedAccount: ((LegacyAccountWithKey) -> Void)?
|
||||
private var inputValidationResult = AccountsService.InputValidationResult.invalid
|
||||
private let walletsManager = WalletsManager.shared
|
||||
var onSelectedWallet: ((InkWallet) -> Void)?
|
||||
private var inputValidationResult = WalletsManager.InputValidationResult.invalid
|
||||
|
||||
@IBOutlet weak var textField: NSTextField! {
|
||||
didSet {
|
||||
@ -51,16 +51,17 @@ class ImportViewController: NSViewController {
|
||||
}
|
||||
|
||||
private func importWith(input: String, password: String?) {
|
||||
if accountsService.addAccount(input: input, password: password) != nil {
|
||||
do {
|
||||
_ = try walletsManager.addWallet(input: input, inputPassword: password)
|
||||
showAccountsList()
|
||||
} else {
|
||||
} catch {
|
||||
Alert.showWithMessage("Failed to import account", style: .critical)
|
||||
}
|
||||
}
|
||||
|
||||
private func showAccountsList() {
|
||||
let accountsListViewController = instantiate(AccountsListViewController.self)
|
||||
accountsListViewController.onSelectedAccount = onSelectedAccount
|
||||
accountsListViewController.onSelectedWallet = onSelectedWallet
|
||||
view.window?.contentViewController = accountsListViewController
|
||||
}
|
||||
|
||||
@ -73,7 +74,7 @@ class ImportViewController: NSViewController {
|
||||
extension ImportViewController: NSTextFieldDelegate {
|
||||
|
||||
func controlTextDidChange(_ obj: Notification) {
|
||||
inputValidationResult = accountsService.validateAccountInput(textField.stringValue)
|
||||
inputValidationResult = walletsManager.validateWalletInput(textField.stringValue)
|
||||
okButton.isEnabled = inputValidationResult != .invalid
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ class SessionStorage {
|
||||
|
||||
struct Item: Codable {
|
||||
let session: WCSession
|
||||
let address: String
|
||||
let walletId: String
|
||||
let clientId: String
|
||||
let sessionDetails: WCSessionRequestParam
|
||||
}
|
||||
@ -51,8 +51,8 @@ class SessionStorage {
|
||||
}
|
||||
}
|
||||
|
||||
func add(interactor: WCInteractor, address: String, sessionDetails: WCSessionRequestParam) {
|
||||
let item = Item(session: interactor.session, address: address, clientId: interactor.clientId, sessionDetails: sessionDetails)
|
||||
func add(interactor: WCInteractor, walletId: String, sessionDetails: WCSessionRequestParam) {
|
||||
let item = Item(session: interactor.session, walletId: walletId, clientId: interactor.clientId, sessionDetails: sessionDetails)
|
||||
WCSessionStore.store(interactor.session, peerId: sessionDetails.peerId, peerMeta: sessionDetails.peerMeta)
|
||||
Defaults.storedSessions[interactor.clientId] = item
|
||||
didInteractWith(clientId: interactor.clientId)
|
||||
|
@ -15,7 +15,7 @@ class AccountCellView: NSTableRowView {
|
||||
@IBOutlet weak var addressTextField: NSTextField!
|
||||
|
||||
func setup(address: String) {
|
||||
addressImageView.image = Blockies(seed: address).createImage()
|
||||
addressImageView.image = Blockies(seed: address.lowercased()).createImage()
|
||||
let without0x = address.dropFirst(2)
|
||||
addressTextField.stringValue = without0x.prefix(4) + "..." + without0x.suffix(4)
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ class WalletConnect {
|
||||
private let sessionStorage = SessionStorage.shared
|
||||
private let networkMonitor = NetworkMonitor.shared
|
||||
private let ethereum = Ethereum.shared
|
||||
private let accountsService = AccountsService.shared
|
||||
private let walletsManager = WalletsManager.shared
|
||||
|
||||
static let shared = WalletConnect()
|
||||
|
||||
@ -28,10 +28,10 @@ class WalletConnect {
|
||||
return WCSession.from(string: link)
|
||||
}
|
||||
|
||||
func connect(session: WCSession, address: String, uuid: UUID = UUID(), completion: @escaping ((Bool) -> Void)) {
|
||||
func connect(session: WCSession, walletId: String, uuid: UUID = UUID(), completion: @escaping ((Bool) -> Void)) {
|
||||
let clientMeta = WCPeerMeta(name: "Encrypted Ink", url: "https://encrypted.ink", description: "Ethereum agent for macOS", icons: ["https://encrypted.ink/icon.png"])
|
||||
let interactor = WCInteractor(session: session, meta: clientMeta, uuid: uuid)
|
||||
configure(interactor: interactor, address: address)
|
||||
configure(interactor: interactor, walletId: walletId)
|
||||
|
||||
interactor.connect().done { connected in
|
||||
completion(connected)
|
||||
@ -49,7 +49,7 @@ class WalletConnect {
|
||||
|
||||
for item in items {
|
||||
guard let uuid = UUID(uuidString: item.clientId) else { continue }
|
||||
connect(session: item.session, address: item.address, uuid: uuid) { _ in }
|
||||
connect(session: item.session, walletId: item.walletId, uuid: uuid) { _ in }
|
||||
peers[item.clientId] = item.sessionDetails.peerMeta
|
||||
}
|
||||
}
|
||||
@ -87,7 +87,8 @@ class WalletConnect {
|
||||
return peers[id]
|
||||
}
|
||||
|
||||
private func configure(interactor: WCInteractor, address: String) {
|
||||
private func configure(interactor: WCInteractor, walletId: String) {
|
||||
guard let address = walletsManager.getWallet(id: walletId)?.ethereumAddress else { return }
|
||||
let accounts = [address]
|
||||
let chainId = 1
|
||||
|
||||
@ -96,7 +97,7 @@ class WalletConnect {
|
||||
interactor.onSessionRequest = { [weak self, weak interactor] (id, peerParam) in
|
||||
guard let interactor = interactor else { return }
|
||||
self?.peers[interactor.clientId] = peerParam.peerMeta
|
||||
self?.sessionStorage.add(interactor: interactor, address: address, sessionDetails: peerParam)
|
||||
self?.sessionStorage.add(interactor: interactor, walletId: walletId, sessionDetails: peerParam)
|
||||
interactor.approveSession(accounts: accounts, chainId: chainId).cauterize()
|
||||
}
|
||||
|
||||
@ -107,12 +108,12 @@ class WalletConnect {
|
||||
}
|
||||
|
||||
interactor.eth.onSign = { [weak self, weak interactor] (id, payload) in
|
||||
self?.approveSign(id: id, payload: payload, address: address, interactor: interactor)
|
||||
self?.approveSign(id: id, payload: payload, walletId: walletId, interactor: interactor)
|
||||
self?.sessionStorage.didInteractWith(clientId: interactor?.clientId)
|
||||
}
|
||||
|
||||
interactor.eth.onTransaction = { [weak self, weak interactor] (id, event, transaction) in
|
||||
self?.approveTransaction(id: id, wct: transaction, address: address, interactor: interactor)
|
||||
self?.approveTransaction(id: id, wct: transaction, walletId: walletId, interactor: interactor)
|
||||
self?.sessionStorage.didInteractWith(clientId: interactor?.clientId)
|
||||
}
|
||||
}
|
||||
@ -128,7 +129,7 @@ class WalletConnect {
|
||||
}
|
||||
}
|
||||
|
||||
private func approveTransaction(id: Int64, wct: WCEthereumTransaction, address: String, interactor: WCInteractor?) {
|
||||
private func approveTransaction(id: Int64, wct: WCEthereumTransaction, walletId: String, interactor: WCInteractor?) {
|
||||
guard let to = wct.to else {
|
||||
rejectRequest(id: id, interactor: interactor, message: "Something went wrong.")
|
||||
return
|
||||
@ -138,14 +139,14 @@ class WalletConnect {
|
||||
let transaction = Transaction(from: wct.from, to: to, nonce: wct.nonce, gasPrice: wct.gasPrice, gas: wct.gas, value: wct.value, data: wct.data)
|
||||
Agent.shared.showApprove(transaction: transaction, peerMeta: peer) { [weak self, weak interactor] transaction in
|
||||
if let transaction = transaction {
|
||||
self?.sendTransaction(transaction, address: address, requestId: id, interactor: interactor)
|
||||
self?.sendTransaction(transaction, walletId: walletId, requestId: id, interactor: interactor)
|
||||
} else {
|
||||
self?.rejectRequest(id: id, interactor: interactor, message: "Cancelled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func approveSign(id: Int64, payload: WCEthereumSignPayload, address: String, interactor: WCInteractor?) {
|
||||
private func approveSign(id: Int64, payload: WCEthereumSignPayload, walletId: String, interactor: WCInteractor?) {
|
||||
var message: String?
|
||||
let title: String
|
||||
switch payload {
|
||||
@ -165,7 +166,7 @@ class WalletConnect {
|
||||
let peer = getPeerOfInteractor(interactor)
|
||||
Agent.shared.showApprove(title: title, meta: message ?? "", peerMeta: peer) { [weak self, weak interactor] approved in
|
||||
if approved {
|
||||
self?.sign(id: id, message: message, payload: payload, address: address, interactor: interactor)
|
||||
self?.sign(id: id, message: message, payload: payload, walletId: walletId, interactor: interactor)
|
||||
} else {
|
||||
self?.rejectRequest(id: id, interactor: interactor, message: "Cancelled")
|
||||
}
|
||||
@ -176,31 +177,31 @@ class WalletConnect {
|
||||
interactor?.rejectRequest(id: id, message: message).cauterize()
|
||||
}
|
||||
|
||||
private func sendTransaction(_ transaction: Transaction, address: String, requestId: Int64, interactor: WCInteractor?) {
|
||||
guard let account = accountsService.getAccountForAddress(address) else {
|
||||
private func sendTransaction(_ transaction: Transaction, walletId: String, requestId: Int64, interactor: WCInteractor?) {
|
||||
guard let wallet = walletsManager.getWallet(id: walletId) 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, wallet: wallet) else {
|
||||
rejectRequest(id: requestId, interactor: interactor, message: "Failed to send")
|
||||
return
|
||||
}
|
||||
interactor?.approveRequest(id: requestId, result: hash).cauterize()
|
||||
}
|
||||
|
||||
private func sign(id: Int64, message: String?, payload: WCEthereumSignPayload, address: String, interactor: WCInteractor?) {
|
||||
guard let message = message, let account = accountsService.getAccountForAddress(address) else {
|
||||
private func sign(id: Int64, message: String?, payload: WCEthereumSignPayload, walletId: String, interactor: WCInteractor?) {
|
||||
guard let message = message, let wallet = walletsManager.getWallet(id: walletId) 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, wallet: wallet)
|
||||
case .signTypeData:
|
||||
signed = try? ethereum.sign(typedData: message, account: account)
|
||||
signed = try? ethereum.sign(typedData: message, wallet: wallet)
|
||||
case .sign:
|
||||
signed = try? ethereum.sign(message: message, account: account)
|
||||
signed = try? ethereum.sign(message: message, wallet: wallet)
|
||||
}
|
||||
guard let result = signed else {
|
||||
rejectRequest(id: id, interactor: interactor, message: "Something went wrong.")
|
||||
|
@ -43,3 +43,21 @@ final class InkWallet: Hashable, Equatable {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension InkWallet {
|
||||
|
||||
var ethereumAddress: String? {
|
||||
return accounts.first(where: { $0.coin == .ethereum })?.address
|
||||
}
|
||||
|
||||
var ethereumPrivateKey: String? {
|
||||
guard let password = Keychain.shared.password,
|
||||
let privateKey = try? privateKey(password: password, coin: .ethereum).data else { return nil }
|
||||
return privateKey.hexString
|
||||
}
|
||||
|
||||
var isMnemonic: Bool {
|
||||
return key.isMnemonic
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -6,6 +6,15 @@ import WalletCore
|
||||
|
||||
final class WalletsManager {
|
||||
|
||||
enum Error: Swift.Error {
|
||||
case keychainAccessFailure
|
||||
case invalidInput
|
||||
}
|
||||
|
||||
enum InputValidationResult {
|
||||
case valid, invalid, requiresPassword
|
||||
}
|
||||
|
||||
static let shared = WalletsManager()
|
||||
private let keychain = Keychain.shared
|
||||
private(set) var wallets = [InkWallet]()
|
||||
@ -17,6 +26,127 @@ final class WalletsManager {
|
||||
try? migrateFromLegacyIfNeeded()
|
||||
}
|
||||
|
||||
func validateWalletInput(_ input: String) -> InputValidationResult {
|
||||
if Mnemonic.isValid(mnemonic: input) {
|
||||
return .valid
|
||||
} else if let data = Data(hexString: input) {
|
||||
return PrivateKey.isValid(data: data, curve: CoinType.ethereum.curve) ? .valid : .invalid
|
||||
} else {
|
||||
return input.maybeJSON ? .requiresPassword : .invalid
|
||||
}
|
||||
}
|
||||
|
||||
func createWallet() throws -> InkWallet {
|
||||
guard let password = keychain.password else { throw Error.keychainAccessFailure }
|
||||
let name = "Wallet \(wallets.count + 1)" // TODO: finalize naming convention
|
||||
return try createWallet(name: name, password: password, coin: .ethereum)
|
||||
}
|
||||
|
||||
func getWallet(id: String) -> InkWallet? {
|
||||
return wallets.first(where: { $0.id == id })
|
||||
}
|
||||
|
||||
func addWallet(input: String, inputPassword: String?) throws -> InkWallet {
|
||||
guard let password = keychain.password else { throw Error.keychainAccessFailure }
|
||||
let name = "Wallet \(wallets.count + 1)" // TODO: finalize naming convention
|
||||
let coin = CoinType.ethereum
|
||||
if Mnemonic.isValid(mnemonic: input) {
|
||||
return try importMnemonic(input, name: name, encryptPassword: password, coin: coin)
|
||||
} else if let data = Data(hexString: input), let privateKey = PrivateKey(data: data) {
|
||||
return try importPrivateKey(privateKey, name: name, password: password, coin: coin)
|
||||
} else if input.maybeJSON, let inputPassword = inputPassword, let json = input.data(using: .utf8) {
|
||||
return try importJSON(json, name: name, password: inputPassword, newPassword: password, coin: coin)
|
||||
} else {
|
||||
throw Error.invalidInput
|
||||
}
|
||||
}
|
||||
|
||||
private func createWallet(name: String, password: String, coin: CoinType) throws -> InkWallet {
|
||||
let key = StoredKey(name: name, password: Data(password.utf8))
|
||||
let id = makeNewWalletId()
|
||||
let wallet = InkWallet(id: id, key: key)
|
||||
_ = try wallet.getAccount(password: password, coin: coin)
|
||||
wallets.append(wallet)
|
||||
try save(wallet: wallet)
|
||||
return wallet
|
||||
}
|
||||
|
||||
private func importJSON(_ json: Data, name: String, password: String, newPassword: String, coin: CoinType) throws -> InkWallet {
|
||||
guard let key = StoredKey.importJSON(json: json) else { throw KeyStore.Error.invalidKey }
|
||||
guard let data = key.decryptPrivateKey(password: Data(password.utf8)) else { throw KeyStore.Error.invalidPassword }
|
||||
if let mnemonic = checkMnemonic(data) { return try self.importMnemonic(mnemonic, name: name, encryptPassword: newPassword, coin: coin) }
|
||||
guard let privateKey = PrivateKey(data: data) else { throw KeyStore.Error.invalidKey }
|
||||
return try self.importPrivateKey(privateKey, name: name, password: newPassword, coin: coin)
|
||||
}
|
||||
|
||||
private func checkMnemonic(_ data: Data) -> String? {
|
||||
guard let mnemonic = String(data: data, encoding: .ascii), Mnemonic.isValid(mnemonic: mnemonic) else { return nil }
|
||||
return mnemonic
|
||||
}
|
||||
|
||||
private func importPrivateKey(_ privateKey: PrivateKey, name: String, password: String, coin: CoinType) throws -> InkWallet {
|
||||
guard let newKey = StoredKey.importPrivateKey(privateKey: privateKey.data, name: name, password: Data(password.utf8), coin: coin) else { throw KeyStore.Error.invalidKey }
|
||||
let id = makeNewWalletId()
|
||||
let wallet = InkWallet(id: id, key: newKey)
|
||||
_ = try wallet.getAccount(password: password, coin: coin)
|
||||
wallets.append(wallet)
|
||||
try save(wallet: wallet)
|
||||
return wallet
|
||||
}
|
||||
|
||||
private func importMnemonic(_ mnemonic: String, name: String, encryptPassword: String, coin: CoinType) throws -> InkWallet {
|
||||
guard let key = StoredKey.importHDWallet(mnemonic: mnemonic, name: name, password: Data(encryptPassword.utf8), coin: coin) else { throw KeyStore.Error.invalidMnemonic }
|
||||
let id = makeNewWalletId()
|
||||
let wallet = InkWallet(id: id, key: key)
|
||||
_ = try wallet.getAccount(password: encryptPassword, coin: coin)
|
||||
wallets.append(wallet)
|
||||
try save(wallet: wallet)
|
||||
return wallet
|
||||
}
|
||||
|
||||
func exportPrivateKey(wallet: InkWallet) throws -> Data {
|
||||
guard let password = keychain.password else { throw Error.keychainAccessFailure }
|
||||
guard let key = wallet.key.decryptPrivateKey(password: Data(password.utf8)) else { throw KeyStore.Error.invalidPassword }
|
||||
return key
|
||||
}
|
||||
|
||||
func exportMnemonic(wallet: InkWallet) throws -> String {
|
||||
guard let password = keychain.password else { throw Error.keychainAccessFailure }
|
||||
guard let mnemonic = wallet.key.decryptMnemonic(password: Data(password.utf8)) else { throw KeyStore.Error.invalidPassword }
|
||||
return mnemonic
|
||||
}
|
||||
|
||||
func update(wallet: InkWallet, password: String, newPassword: String) throws {
|
||||
try update(wallet: wallet, password: password, newPassword: newPassword, newName: wallet.key.name)
|
||||
}
|
||||
|
||||
func update(wallet: InkWallet, password: String, newName: String) throws {
|
||||
try update(wallet: wallet, password: password, newPassword: password, newName: newName)
|
||||
}
|
||||
|
||||
func delete(wallet: InkWallet) throws {
|
||||
guard let password = keychain.password else { throw Error.keychainAccessFailure }
|
||||
guard let index = wallets.firstIndex(of: wallet) else { throw KeyStore.Error.accountNotFound }
|
||||
guard var privateKey = wallet.key.decryptPrivateKey(password: Data(password.utf8)) else { throw KeyStore.Error.invalidKey }
|
||||
defer { privateKey.resetBytes(in: 0..<privateKey.count) }
|
||||
wallets.remove(at: index)
|
||||
try keychain.removeWallet(id: wallet.id)
|
||||
}
|
||||
|
||||
func destroy() throws {
|
||||
wallets.removeAll(keepingCapacity: false)
|
||||
try keychain.removeAllWallets()
|
||||
}
|
||||
|
||||
private func load() throws {
|
||||
let ids = keychain.getAllWalletsIds()
|
||||
for id in ids {
|
||||
guard let data = keychain.getWalletData(id: id), let key = StoredKey.importJSON(json: data) else { continue }
|
||||
let wallet = InkWallet(id: id, key: key)
|
||||
wallets.append(wallet)
|
||||
}
|
||||
}
|
||||
|
||||
private func migrateFromLegacyIfNeeded() throws {
|
||||
let legacyAccountsWithKeys = try keychain.getLegacyAccounts()
|
||||
guard !legacyAccountsWithKeys.isEmpty, let password = keychain.password else { return }
|
||||
@ -28,76 +158,6 @@ final class WalletsManager {
|
||||
try keychain.removeLegacyAccounts()
|
||||
}
|
||||
|
||||
private func load() throws {
|
||||
let ids = keychain.getAllWalletsIds()
|
||||
for id in ids {
|
||||
guard let data = keychain.getWalletData(id: id), let key = StoredKey.importJSON(json: data) else { continue }
|
||||
let wallet = InkWallet(id: id, key: key)
|
||||
wallets.append(wallet)
|
||||
}
|
||||
}
|
||||
|
||||
func createWallet(name: String, password: String, coin: CoinType) throws -> InkWallet {
|
||||
let key = StoredKey(name: name, password: Data(password.utf8))
|
||||
let id = makeNewWalletId()
|
||||
let wallet = InkWallet(id: id, key: key)
|
||||
_ = try wallet.getAccount(password: password, coin: coin)
|
||||
wallets.append(wallet)
|
||||
try save(wallet: wallet)
|
||||
return wallet
|
||||
}
|
||||
|
||||
func importJSON(_ json: Data, name: String, password: String, newPassword: String, coin: CoinType) throws -> InkWallet {
|
||||
guard let key = StoredKey.importJSON(json: json) else { throw KeyStore.Error.invalidKey }
|
||||
guard let data = key.decryptPrivateKey(password: Data(password.utf8)) else { throw KeyStore.Error.invalidPassword }
|
||||
if let mnemonic = checkMnemonic(data) { return try self.importMnemonic(mnemonic, name: name, encryptPassword: newPassword, coin: coin) }
|
||||
guard let privateKey = PrivateKey(data: data) else { throw KeyStore.Error.invalidKey }
|
||||
return try self.importPrivateKey(privateKey, name: name, password: newPassword, coin: coin)
|
||||
}
|
||||
|
||||
func checkMnemonic(_ data: Data) -> String? {
|
||||
guard let mnemonic = String(data: data, encoding: .ascii), Mnemonic.isValid(mnemonic: mnemonic) else { return nil }
|
||||
return mnemonic
|
||||
}
|
||||
|
||||
func importPrivateKey(_ privateKey: PrivateKey, name: String, password: String, coin: CoinType) throws -> InkWallet {
|
||||
guard let newKey = StoredKey.importPrivateKey(privateKey: privateKey.data, name: name, password: Data(password.utf8), coin: coin) else { throw KeyStore.Error.invalidKey }
|
||||
let id = makeNewWalletId()
|
||||
let wallet = InkWallet(id: id, key: newKey)
|
||||
_ = try wallet.getAccount(password: password, coin: coin)
|
||||
wallets.append(wallet)
|
||||
try save(wallet: wallet)
|
||||
return wallet
|
||||
}
|
||||
|
||||
func importMnemonic(_ mnemonic: String, name: String, encryptPassword: String, coin: CoinType) throws -> InkWallet {
|
||||
guard let key = StoredKey.importHDWallet(mnemonic: mnemonic, name: name, password: Data(encryptPassword.utf8), coin: coin) else { throw KeyStore.Error.invalidMnemonic }
|
||||
let id = makeNewWalletId()
|
||||
let wallet = InkWallet(id: id, key: key)
|
||||
_ = try wallet.getAccount(password: encryptPassword, coin: coin)
|
||||
wallets.append(wallet)
|
||||
try save(wallet: wallet)
|
||||
return wallet
|
||||
}
|
||||
|
||||
func exportPrivateKey(wallet: InkWallet, password: String) throws -> Data {
|
||||
guard let key = wallet.key.decryptPrivateKey(password: Data(password.utf8)) else { throw KeyStore.Error.invalidPassword }
|
||||
return key
|
||||
}
|
||||
|
||||
func exportMnemonic(wallet: InkWallet, password: String) throws -> String {
|
||||
guard let mnemonic = wallet.key.decryptMnemonic(password: Data(password.utf8)) else { throw KeyStore.Error.invalidPassword }
|
||||
return mnemonic
|
||||
}
|
||||
|
||||
func update(wallet: InkWallet, password: String, newPassword: String) throws {
|
||||
try update(wallet: wallet, password: password, newPassword: newPassword, newName: wallet.key.name)
|
||||
}
|
||||
|
||||
func update(wallet: InkWallet, password: String, newName: String) throws {
|
||||
try update(wallet: wallet, password: password, newPassword: password, newName: newName)
|
||||
}
|
||||
|
||||
private func update(wallet: InkWallet, password: String, newPassword: String, newName: String) throws {
|
||||
guard let index = wallets.firstIndex(of: wallet) else { throw KeyStore.Error.accountNotFound }
|
||||
guard var privateKeyData = wallet.key.decryptPrivateKey(password: Data(password.utf8)) else { throw KeyStore.Error.invalidPassword }
|
||||
@ -119,19 +179,6 @@ final class WalletsManager {
|
||||
try save(wallet: wallets[index])
|
||||
}
|
||||
|
||||
func delete(wallet: InkWallet, password: String) throws {
|
||||
guard let index = wallets.firstIndex(of: wallet) else { throw KeyStore.Error.accountNotFound }
|
||||
guard var privateKey = wallet.key.decryptPrivateKey(password: Data(password.utf8)) else { throw KeyStore.Error.invalidKey }
|
||||
defer { privateKey.resetBytes(in: 0..<privateKey.count) }
|
||||
wallets.remove(at: index)
|
||||
try keychain.removeWallet(id: wallet.id)
|
||||
}
|
||||
|
||||
func destroy() throws {
|
||||
wallets.removeAll(keepingCapacity: false)
|
||||
try keychain.removeAllWallets()
|
||||
}
|
||||
|
||||
private func save(wallet: InkWallet) throws {
|
||||
guard let data = wallet.key.exportJSON() else { throw KeyStore.Error.invalidPassword }
|
||||
try keychain.saveWallet(id: wallet.id, data: data)
|
||||
|
Loading…
Reference in New Issue
Block a user