tokenary/Encrypted Ink/Agent.swift

447 lines
19 KiB
Swift
Raw Normal View History

2021-06-12 19:16:23 +03:00
// Copyright © 2021 Encrypted Ink. All rights reserved.
import Cocoa
2021-06-13 13:13:47 +03:00
import WalletConnect
2021-06-13 15:30:19 +03:00
import LocalAuthentication
2021-06-12 19:16:23 +03:00
class Agent: NSObject {
2021-11-22 13:11:34 +03:00
enum ExternalRequest {
case wcSession(WCSession)
case safari(SafariRequest)
}
2021-06-13 06:30:20 +03:00
static let shared = Agent()
2021-06-13 13:53:38 +03:00
private lazy var statusImage = NSImage(named: "Status")
2021-06-12 19:16:23 +03:00
2021-11-22 13:11:34 +03:00
private let walletConnect = WalletConnect.shared
private let walletsManager = WalletsManager.shared
private let ethereum = Ethereum.shared
private override init() { super.init() }
2021-06-13 13:13:47 +03:00
private var statusBarItem: NSStatusItem!
private var hasPassword = Keychain.shared.password != nil
private var didEnterPasswordOnStart = false
private var didStartInitialLAEvaluation = false
private var didCompleteInitialLAEvaluation = false
2021-11-22 13:11:34 +03:00
private var initialExternalRequest: ExternalRequest?
var statusBarButtonIsBlocked = false
2021-06-13 06:30:20 +03:00
2021-06-12 19:16:23 +03:00
func start() {
checkPasteboardAndOpen()
2021-06-28 16:20:08 +03:00
setupStatusBarItem()
2021-06-13 05:45:54 +03:00
}
func reopen() {
checkPasteboardAndOpen()
2021-06-13 05:45:54 +03:00
}
2021-11-22 13:11:34 +03:00
func showInitialScreen(externalRequest: ExternalRequest?) {
let isEvaluatingInitialLA = didStartInitialLAEvaluation && !didCompleteInitialLAEvaluation
guard !isEvaluatingInitialLA else {
2021-11-22 13:11:34 +03:00
if externalRequest != nil {
initialExternalRequest = externalRequest
}
return
}
2021-06-19 00:38:05 +03:00
guard hasPassword else {
let welcomeViewController = WelcomeViewController.new { [weak self] createdPassword in
guard createdPassword else { return }
self?.didEnterPasswordOnStart = true
self?.didCompleteInitialLAEvaluation = true
2021-06-19 00:38:05 +03:00
self?.hasPassword = true
2021-11-22 13:11:34 +03:00
self?.showInitialScreen(externalRequest: externalRequest)
2021-06-19 00:38:05 +03:00
}
let windowController = Window.showNew()
2021-06-19 00:38:05 +03:00
windowController.contentViewController = welcomeViewController
return
}
guard didEnterPasswordOnStart else {
askAuthentication(on: nil, onStart: true, reason: .start) { [weak self] success in
if success {
self?.didEnterPasswordOnStart = true
2021-11-22 13:11:34 +03:00
self?.showInitialScreen(externalRequest: externalRequest)
self?.walletConnect.restartSessions()
}
}
return
}
2021-11-22 13:11:34 +03:00
let request = externalRequest ?? initialExternalRequest
initialExternalRequest = nil
if case let .safari(request) = request {
processSafariRequest(request)
} else {
2021-11-22 13:11:34 +03:00
let windowController = Window.showNew()
let accountsList = instantiate(AccountsListViewController.self)
if case let .wcSession(session) = request {
accountsList.onSelectedWallet = onSelectedWallet(session: session)
}
windowController.contentViewController = accountsList
}
2021-06-12 19:16:23 +03:00
}
2021-08-06 23:39:58 +03:00
func showApprove(transaction: Transaction, chain: EthereumChain, peerMeta: WCPeerMeta?, completion: @escaping (Transaction?) -> Void) {
let windowController = Window.showNew()
2021-08-06 23:39:58 +03:00
let approveViewController = ApproveTransactionViewController.with(transaction: transaction, chain: chain, peerMeta: peerMeta) { [weak self] transaction in
if transaction != nil {
self?.askAuthentication(on: windowController.window, onStart: false, reason: .sendTransaction) { success in
completion(success ? transaction : nil)
Window.closeAllAndActivateBrowser()
}
} else {
Window.closeAllAndActivateBrowser()
completion(nil)
}
}
windowController.contentViewController = approveViewController
}
func showApprove(subject: ApprovalSubject, meta: String, peerMeta: WCPeerMeta?, completion: @escaping (Bool) -> Void) {
2021-06-13 09:32:07 +03:00
let windowController = Window.showNew()
let approveViewController = ApproveViewController.with(subject: subject, meta: meta, peerMeta: peerMeta) { [weak self] result in
2021-06-13 15:30:19 +03:00
if result {
self?.askAuthentication(on: windowController.window, onStart: false, reason: subject.asAuthenticationReason) { success in
completion(success)
Window.closeAllAndActivateBrowser()
}
2021-06-13 15:30:19 +03:00
} else {
Window.closeAllAndActivateBrowser()
2021-06-13 15:30:19 +03:00
completion(result)
}
2021-06-13 09:32:07 +03:00
}
windowController.contentViewController = approveViewController
}
func showErrorMessage(_ message: String) {
let windowController = Window.showNew()
windowController.contentViewController = ErrorViewController.withMessage(message)
}
2021-11-25 14:09:43 +03:00
func getWalletSelectionCompletionIfShouldSelect() -> ((EthereumChain, InkWallet) -> Void)? {
2021-06-17 23:56:42 +03:00
let session = getSessionFromPasteboard()
2021-08-01 19:42:27 +03:00
return onSelectedWallet(session: session)
2021-06-17 23:56:42 +03:00
}
lazy private var statusBarMenu: NSMenu = {
2021-08-28 16:25:37 +03:00
let menu = NSMenu(title: Strings.encryptedInk)
2021-08-29 21:56:32 +03:00
let showItem = NSMenuItem(title: Strings.showEncryptedInk, action: #selector(didSelectShowMenuItem), keyEquivalent: "")
let howToItem = NSMenuItem(title: Strings.howToWalletConnect, action: #selector(showInstructionsAlert), keyEquivalent: "")
let mailItem = NSMenuItem(title: Strings.dropUsALine, action: #selector(didSelectMailMenuItem), keyEquivalent: "")
let githubItem = NSMenuItem(title: Strings.viewOnGithub, action: #selector(didSelectGitHubMenuItem), keyEquivalent: "")
let twitterItem = NSMenuItem(title: Strings.viewOnTwitter, action: #selector(didSelectTwitterMenuItem), keyEquivalent: "")
let quitItem = NSMenuItem(title: Strings.quit, action: #selector(didSelectQuitMenuItem), keyEquivalent: "q")
showItem.attributedTitle = NSAttributedString(string: "👀 " + Strings.showEncryptedInk, attributes: [.font: NSFont.systemFont(ofSize: 15, weight: .semibold)])
showItem.target = self
2021-07-17 19:05:59 +03:00
howToItem.target = self
2021-07-06 19:59:45 +03:00
githubItem.target = self
2021-07-30 02:47:14 +03:00
twitterItem.target = self
2021-07-06 19:59:45 +03:00
mailItem.target = self
quitItem.target = self
2021-07-06 19:59:45 +03:00
menu.delegate = self
menu.addItem(showItem)
2021-07-06 19:59:45 +03:00
menu.addItem(NSMenuItem.separator())
2021-07-17 19:05:59 +03:00
menu.addItem(howToItem)
menu.addItem(NSMenuItem.separator())
2021-07-30 02:47:14 +03:00
menu.addItem(twitterItem)
2021-07-06 19:59:45 +03:00
menu.addItem(githubItem)
menu.addItem(mailItem)
menu.addItem(NSMenuItem.separator())
menu.addItem(quitItem)
return menu
}()
2021-06-23 22:21:23 +03:00
func warnBeforeQuitting(updateStatusBarAfterwards: Bool = false) {
Window.activateWindow(nil)
let alert = Alert()
2021-08-29 21:56:32 +03:00
alert.messageText = Strings.quitEncryptedInk
alert.informativeText = Strings.youWontBeAbleToSignRequests
alert.alertStyle = .warning
2021-08-26 21:17:23 +03:00
alert.addButton(withTitle: Strings.ok)
alert.addButton(withTitle: Strings.cancel)
if alert.runModal() == .alertFirstButtonReturn {
NSApp.terminate(nil)
}
2021-06-23 22:21:23 +03:00
if updateStatusBarAfterwards {
setupStatusBarItem()
}
}
2021-07-30 02:47:14 +03:00
@objc private func didSelectTwitterMenuItem() {
if let url = URL(string: "https://encrypted.ink/twitter") {
NSWorkspace.shared.open(url)
}
}
2021-07-06 19:59:45 +03:00
@objc private func didSelectGitHubMenuItem() {
2021-08-24 15:59:30 +03:00
if let url = URL(string: "https://encrypted.ink/github") {
2021-07-06 19:59:45 +03:00
NSWorkspace.shared.open(url)
}
}
2021-07-17 19:05:59 +03:00
@objc private func showInstructionsAlert() {
Window.activateWindow(nil)
Alert.showWalletConnectInstructions()
}
2021-07-06 19:59:45 +03:00
@objc private func didSelectMailMenuItem() {
if let url = URL(string: "mailto:support@encrypted.ink") {
NSWorkspace.shared.open(url)
}
}
@objc private func didSelectShowMenuItem() {
checkPasteboardAndOpen()
}
2021-06-23 21:28:52 +03:00
@objc private func didSelectQuitMenuItem() {
warnBeforeQuitting()
}
func setupStatusBarItem() {
let statusBar = NSStatusBar.system
statusBarItem = statusBar.statusItem(withLength: NSStatusItem.squareLength)
statusBarItem.button?.image = statusImage
statusBarItem.button?.target = self
statusBarItem.button?.action = #selector(statusBarButtonClicked(sender:))
statusBarItem.button?.sendAction(on: [.leftMouseUp, .rightMouseUp])
}
@objc private func statusBarButtonClicked(sender: NSStatusBarButton) {
guard !statusBarButtonIsBlocked, let event = NSApp.currentEvent, event.type == .rightMouseUp || event.type == .leftMouseUp else { return }
if let session = getSessionFromPasteboard() {
2021-11-22 13:11:34 +03:00
showInitialScreen(externalRequest: .wcSession(session))
} else {
statusBarItem.menu = statusBarMenu
statusBarItem.button?.performClick(nil)
}
}
2021-11-25 14:09:43 +03:00
private func onSelectedWallet(session: WCSession?) -> ((EthereumChain, InkWallet) -> Void)? {
2021-06-17 23:11:03 +03:00
guard let session = session else { return nil }
2021-11-25 14:09:43 +03:00
return { [weak self] chain, wallet in
self?.connectWallet(session: session, chainId: chain.id, wallet: wallet)
2021-06-17 23:11:03 +03:00
}
}
private func getSessionFromPasteboard() -> WCSession? {
2021-06-13 13:13:47 +03:00
let pasteboard = NSPasteboard.general
let link = pasteboard.string(forType: .string) ?? ""
2021-11-22 13:11:34 +03:00
let session = walletConnect.sessionWithLink(link)
if session != nil {
pasteboard.clearContents()
}
2021-06-17 23:11:03 +03:00
return session
}
private func checkPasteboardAndOpen() {
2021-11-22 13:11:34 +03:00
let request: ExternalRequest?
if let session = getSessionFromPasteboard() {
request = .wcSession(session)
} else {
request = .none
}
showInitialScreen(externalRequest: request)
}
func askAuthentication(on: NSWindow?, getBackTo: NSViewController? = nil, onStart: Bool, reason: AuthenticationReason, completion: @escaping (Bool) -> Void) {
2021-06-13 15:30:19 +03:00
let context = LAContext()
var error: NSError?
let policy = LAPolicy.deviceOwnerAuthenticationWithBiometrics
let canDoLocalAuthentication = context.canEvaluatePolicy(policy, error: &error)
func showPasswordScreen() {
let window = on ?? Window.showNew().window
let passwordViewController = PasswordViewController.with(mode: .enter, reason: reason) { [weak window] success in
if let getBackTo = getBackTo {
window?.contentViewController = getBackTo
} else {
Window.closeAll()
}
2021-06-13 15:30:19 +03:00
completion(success)
}
window?.contentViewController = passwordViewController
}
if canDoLocalAuthentication {
2021-08-26 21:17:23 +03:00
context.localizedCancelTitle = Strings.cancel
didStartInitialLAEvaluation = true
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason.title) { [weak self] success, _ in
DispatchQueue.main.async {
self?.didCompleteInitialLAEvaluation = true
if !success, onStart, self?.didEnterPasswordOnStart == false {
showPasswordScreen()
}
completion(success)
}
}
} else {
showPasswordScreen()
2021-06-13 15:30:19 +03:00
}
}
2021-08-06 18:58:32 +03:00
private func connectWallet(session: WCSession, chainId: Int, wallet: InkWallet) {
let windowController = Window.showNew()
let window = windowController.window
2021-08-27 23:50:55 +03:00
windowController.contentViewController = WaitingViewController.withReason(Strings.connecting)
2021-11-22 13:11:34 +03:00
walletConnect.connect(session: session, chainId: chainId, walletId: wallet.id) { [weak window] _ in
if window?.isVisible == true {
2021-06-19 19:38:51 +03:00
Window.closeAllAndActivateBrowser()
}
2021-06-13 06:30:20 +03:00
}
}
2021-11-22 13:11:34 +03:00
// TODO: should receive account address from content script here.
// content script should know it since it injets it
private func processSafariRequest(_ safariRequest: SafariRequest) {
switch safariRequest.method {
case .signPersonalMessage:
guard let data = safariRequest.message else {
return // TODO: respond with error
}
// TODO: display meta and peerMeta
showApprove(subject: .signPersonalMessage, meta: "", peerMeta: nil) { [weak self] approved in
if approved {
2021-11-24 22:46:26 +03:00
self?.signPersonalMessage(address: safariRequest.address, data: data, request: safariRequest)
2021-11-22 13:11:34 +03:00
// TODO: sign and respond
} else {
2021-11-24 22:46:26 +03:00
ExtensionBridge.respond(id: safariRequest.id, response: ResponseToExtension(name: safariRequest.name, error: "Failed to sign"))
2021-11-22 13:11:34 +03:00
}
}
2021-11-24 22:46:26 +03:00
case .requestAccounts, .switchAccount:
2021-11-22 13:11:34 +03:00
let windowController = Window.showNew()
let accountsList = instantiate(AccountsListViewController.self)
2021-11-25 14:09:43 +03:00
accountsList.onSelectedWallet = { chain, wallet in
let response = ResponseToExtension(name: safariRequest.name,
results: [wallet.ethereumAddress ?? "weird address"],
chainId: chain.hexStringId,
rpcURL: chain.nodeURLString)
ExtensionBridge.respond(id: safariRequest.id, response: response)
2021-11-22 13:11:34 +03:00
Window.closeAllAndActivateBrowser()
}
2021-11-24 22:46:26 +03:00
// TODO: pass cancel as well
2021-11-22 13:11:34 +03:00
windowController.contentViewController = accountsList
case .signMessage:
guard let data = safariRequest.message else {
return // TODO: respond with error
}
// TODO: display meta and peerMeta
showApprove(subject: .signMessage, meta: "", peerMeta: nil) { [weak self] approved in
if approved {
2021-11-24 22:46:26 +03:00
self?.signMessage(address: safariRequest.address, data: data, request: safariRequest)
2021-11-22 13:11:34 +03:00
// TODO: sign and respond
} else {
2021-11-24 22:46:26 +03:00
ExtensionBridge.respond(id: safariRequest.id, response: ResponseToExtension(name: safariRequest.name, error: "Failed to sign"))
2021-11-22 13:11:34 +03:00
}
}
case .signTypedMessage:
guard let raw = safariRequest.raw else {
print("yoyoyo no raw")
return // TODO: respond with error
}
print("yoyoyo raw:", raw)
// TODO: display meta and peerMeta
showApprove(subject: .signTypedData, meta: "", peerMeta: nil) { [weak self] approved in
if approved {
2021-11-24 22:46:26 +03:00
self?.signTypedData(address: safariRequest.address, raw: raw, request: safariRequest)
2021-11-22 13:11:34 +03:00
// TODO: sign and respond
} else {
2021-11-24 22:46:26 +03:00
ExtensionBridge.respond(id: safariRequest.id, response: ResponseToExtension(name: safariRequest.name, error: "Failed to sign"))
2021-11-22 13:11:34 +03:00
}
}
case .signTransaction:
let chain = EthereumChain.ethereum // TODO: receive chain id here as well
guard let transaction = safariRequest.transaction else {
return // TODO: respond with error
}
let peer = WCPeerMeta(name: "Unknown", url: "") // TODO: pass valid peer meta
showApprove(transaction: transaction, chain: chain, peerMeta: peer) { [weak self] transaction in
if let transaction = transaction {
2021-11-24 22:46:26 +03:00
self?.sendTransaction(transaction, address: safariRequest.address, chain: chain, request: safariRequest)
// TODO: show some kind of spinner
// TODO: actually send a transaction. What should be in a response?
} else {
2021-11-24 22:46:26 +03:00
ExtensionBridge.respond(id: safariRequest.id, response: ResponseToExtension(name: safariRequest.name, error: "Canceled"))
// TODO: looks like uniswap expects different response format
}
}
case .ecRecover:
if let (signature, message) = safariRequest.signatureAndMessage,
let recovered = ethereum.recover(signature: signature, message: message) {
2021-11-24 22:46:26 +03:00
ExtensionBridge.respond(id: safariRequest.id, response: ResponseToExtension(name: safariRequest.name, result: recovered))
} else {
2021-11-24 22:46:26 +03:00
ExtensionBridge.respond(id: safariRequest.id, response: ResponseToExtension(name: safariRequest.name, error: "Failed to verify"))
}
2021-11-22 13:11:34 +03:00
default:
// TODO: implement
// at least bring focus back to browser
2021-11-22 13:11:34 +03:00
break
}
}
// TODO: refactor in a way that there'd be only one sendTransaction for extension and for WalletConnect
2021-11-24 22:46:26 +03:00
private func sendTransaction(_ transaction: Transaction, address: String, chain: EthereumChain, request: SafariRequest) {
guard let wallet = walletsManager.getWallet(address: address) else {
return // TODO: respond with error
}
guard let transactionHash = try? ethereum.send(transaction: transaction, wallet: wallet, chain: chain) else {
2021-11-24 22:46:26 +03:00
ExtensionBridge.respond(id: request.id, response: ResponseToExtension(name: request.name, error: "Failed to send"))
return // TODO: respond with error
}
2021-11-24 22:46:26 +03:00
ExtensionBridge.respond(id: request.id, response: ResponseToExtension(name: request.name, result: transactionHash))
}
2021-11-24 22:46:26 +03:00
private func signTypedData(address: String, raw: String, request: SafariRequest) {
guard let wallet = walletsManager.getWallet(address: address) else {
2021-11-22 13:11:34 +03:00
return // TODO: respond with error
}
let signed = try? ethereum.sign(typedData: raw, wallet: wallet)
2021-11-24 22:46:26 +03:00
ExtensionBridge.respond(id: request.id, response: ResponseToExtension(name: request.name, result: signed ?? "weird address"))
2021-11-22 13:11:34 +03:00
}
2021-11-24 22:46:26 +03:00
private func signMessage(address: String, data: Data, request: SafariRequest) {
guard let wallet = walletsManager.getWallet(address: address) else {
2021-11-22 13:11:34 +03:00
return // TODO: respond with error
}
let signed = try? ethereum.sign(data: data, wallet: wallet)
2021-11-24 22:46:26 +03:00
ExtensionBridge.respond(id: request.id, response: ResponseToExtension(name: request.name, result: signed ?? "weird address"))
2021-11-22 13:11:34 +03:00
}
2021-11-24 22:46:26 +03:00
private func signPersonalMessage(address: String, data: Data, request: SafariRequest) {
guard let wallet = walletsManager.getWallet(address: address) else {
2021-11-22 13:11:34 +03:00
return // TODO: respond with error
}
let signed = try? ethereum.signPersonalMessage(data: data, wallet: wallet)
2021-11-24 22:46:26 +03:00
ExtensionBridge.respond(id: request.id, response: ResponseToExtension(name: request.name, result: signed ?? "weird address"))
2021-11-22 13:11:34 +03:00
}
2021-06-12 19:16:23 +03:00
}
extension Agent: NSMenuDelegate {
func menuDidClose(_ menu: NSMenu) {
statusBarItem.menu = nil
}
}