mirror of
https://github.com/luxfi/safe-ios.git
synced 2026-10-30 15:19:20 +00:00
158 lines
6.2 KiB
Swift
158 lines
6.2 KiB
Swift
//
|
|
// LedgerSignerViewController.swift
|
|
// Multisig
|
|
//
|
|
// Created by Dmitry Bespalov on 07.10.21.
|
|
// Copyright © 2021 Gnosis Ltd. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import SafeWeb3
|
|
|
|
struct SignRequest {
|
|
var title: String
|
|
var tracking: [String: Any]
|
|
var signer: KeyInfo
|
|
var payload: Payload
|
|
|
|
enum Payload {
|
|
case hash(String)
|
|
case rawTx(data: Data, chainId: Int, isLegacy: Bool)
|
|
}
|
|
}
|
|
|
|
extension SignRequest {
|
|
init(title: String, tracking: [String: Any], signer: KeyInfo, hexToSign: String) {
|
|
self.init(title: title, tracking: tracking, signer: signer, payload: .hash(hexToSign))
|
|
}
|
|
}
|
|
|
|
class LedgerSignerViewController: UINavigationController {
|
|
var completion: ((String) -> Void)!
|
|
var txCompletion: (((v: UInt8, r: Data, s: Data)) -> Void)?
|
|
|
|
var onClose: (() -> Void)? {
|
|
didSet {
|
|
if let vc = viewControllers.first as? SelectLedgerDeviceViewController {
|
|
vc.onClose = onClose
|
|
}
|
|
}
|
|
}
|
|
private var request: SignRequest!
|
|
|
|
convenience init(request: SignRequest) {
|
|
assert(request.signer.keyType == .ledgerNanoX)
|
|
let vc = SelectLedgerDeviceViewController(trackingParameters: request.tracking,
|
|
title: request.title,
|
|
showsCloseButton: true)
|
|
self.init(rootViewController: vc)
|
|
self.request = request
|
|
vc.completion = { [unowned self, unowned vc] deviceId, bluetoothController in
|
|
guard let data = request.signer.metadata,
|
|
let metadata = KeyInfo.LedgerKeyMetadata.from(data: data) else { return }
|
|
|
|
let confirmVC = LedgerPendingConfirmationViewController(
|
|
headerText: request.title,
|
|
bluetoothController: bluetoothController,
|
|
signRequest: request,
|
|
deviceId: deviceId,
|
|
derivationPath: metadata.path)
|
|
|
|
confirmVC.modalPresentationStyle = .popover
|
|
confirmVC.modalTransitionStyle = .crossDissolve
|
|
|
|
if let popoverPresentationController = confirmVC.popoverPresentationController {
|
|
popoverPresentationController.sourceView = self.view
|
|
}
|
|
|
|
present(confirmVC, animated: true)
|
|
|
|
confirmVC.onClose = { [unowned vc] in
|
|
vc.reloadData()
|
|
vc.onClose?()
|
|
}
|
|
|
|
confirmVC.onTxSign = { [weak self, unowned vc] result in
|
|
guard let self = self else { return }
|
|
|
|
// dismiss Ledger Pending Confirmation overlay
|
|
self.dismiss(animated: true, completion: nil)
|
|
|
|
switch result {
|
|
case .failure(let error):
|
|
let gsError = GSError.error(description: "The operation failed.", error: error)
|
|
App.shared.snackbar.show(error: gsError)
|
|
|
|
vc.reloadData()
|
|
|
|
case .success(let signature):
|
|
// got signature, dismiss the first SelectDevice screen.
|
|
self.dismiss(animated: true, completion: nil)
|
|
|
|
self.txCompletion?(signature)
|
|
}
|
|
}
|
|
|
|
confirmVC.onSign = { [weak self, unowned vc] signature, errorMessage in
|
|
guard let self = self else { return }
|
|
|
|
// dismiss Ledger Pending Confirmation overlay
|
|
self.dismiss(animated: true, completion: nil)
|
|
|
|
guard let signature = signature else {
|
|
// Possible reasons:
|
|
// - bluetooth pairing failure
|
|
// - ethereum app on the ledger device is not open
|
|
// - cancelled on ledger device
|
|
// In these situations it is more convenient to keep the bluetooth discover open
|
|
// so we just show the error and reload data
|
|
let message = errorMessage ?? "The operation was canceled on the Ledger device."
|
|
App.shared.snackbar.show(message: message)
|
|
|
|
// reload the devices in case we lost connection
|
|
vc.reloadData()
|
|
return
|
|
}
|
|
|
|
// layout is <r: 35 bytes><s: 35 bytes><v: 1 byte>; v = (0 | 1) + 27 + 4
|
|
let signatureData: Data = Data(hex: signature)
|
|
assert(signatureData.count >= 65)
|
|
let r = Data(Array(signatureData[0..<32]))
|
|
let s = Data(Array(signatureData[32..<64]))
|
|
let v = signatureData[64]
|
|
|
|
// original message signed by Ledger has Ethereum prefix according to eth_sign
|
|
guard case let SignRequest.Payload.hash(hexToSign) = self.request!.payload else {
|
|
preconditionFailure("Unexpected payload: \(self.request!.payload)")
|
|
}
|
|
let originalHash: Data = Data(hex: hexToSign)
|
|
let prefix = "\u{19}Ethereum Signed Message:\n\(originalHash.count)"
|
|
let prefixedMessage = prefix.data(using: .utf8)! + originalHash
|
|
|
|
// recover the public key
|
|
let pubKey = try? EthereumPublicKey.init(message: prefixedMessage.makeBytes(),
|
|
v: EthereumQuantity(quantity: BigUInt(v - 27 - 4)),
|
|
r: EthereumQuantity(r.makeBytes()),
|
|
s: EthereumQuantity(s.makeBytes()))
|
|
|
|
// Since it's possible to sign with a key different from the one user selected in app UI in the previous
|
|
// step, we'll check that the actual signer is the same as the selected owner.
|
|
|
|
guard let signedOwner = pubKey?.address,
|
|
Address(signedOwner) == self.request.signer.address else {
|
|
App.shared.snackbar.show(error: GSError.SignerMismatch())
|
|
|
|
// reload the devices in case we lost connection
|
|
vc.reloadData()
|
|
return
|
|
}
|
|
|
|
// got signature, dismiss the first SelectDevice screen.
|
|
self.dismiss(animated: true, completion: nil)
|
|
|
|
self.completion(signature)
|
|
}
|
|
}
|
|
}
|
|
}
|