mirror of
https://github.com/luxfi/safe-ios.git
synced 2026-10-30 15:19:20 +00:00
380 lines
12 KiB
Swift
380 lines
12 KiB
Swift
//
|
|
// SafeMO.swift
|
|
// Multisig
|
|
//
|
|
// Created by Andrey Scherbovich on 15.04.20.
|
|
// Copyright © 2020 Gnosis Ltd. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import CoreData
|
|
import FirebaseAnalytics
|
|
import Version
|
|
|
|
// "address:chainId" -> name
|
|
fileprivate var cachedNames = [String: String]()
|
|
|
|
extension Safe {
|
|
|
|
var isSelected: Bool { selection != nil }
|
|
|
|
var hasAddress: Bool { address?.isEmpty == false }
|
|
|
|
var displayAddress: String { address! }
|
|
|
|
var addressValue: Address { Address(address!)! }
|
|
|
|
var browserURL: URL { chain!.browserURL(address: displayAddress) }
|
|
|
|
var displayENSName: String { ensName ?? "" }
|
|
|
|
var safeStatus: SafeStatus {
|
|
get { SafeStatus(rawValue: status) ?? .deployed }
|
|
set { status = newValue.rawValue }
|
|
}
|
|
|
|
var semVer: Version? {
|
|
contractVersion.flatMap(Version.init) ?? version.flatMap(Version.init)
|
|
}
|
|
|
|
var isReadOnly: Bool {
|
|
guard let owners = ownersInfo else { return false }
|
|
if let keys = try? KeyInfo.keys(addresses: owners.map(\.address)), !keys.isEmpty {
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|
|
var security: SafeSecurityStatus {
|
|
if securityHasBackup && securityHasEnoughOwners && securityHasRecommendedThreshold {
|
|
return .high
|
|
} else {
|
|
return .medium
|
|
}
|
|
}
|
|
|
|
// has back up is false iff there are generated keys that need backup
|
|
var securityHasBackup: Bool {
|
|
if let ownersInfo = ownersInfo, let keyInfos = try? KeyInfo.keys(addresses: ownersInfo.map(\.address)),
|
|
keyInfos.contains(where: { $0.needsBackup }) {
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|
|
var securityHasEnoughOwners: Bool {
|
|
if let ownersInfo = ownersInfo, ownersInfo.count > 1 {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
var securityHasRecommendedThreshold: Bool {
|
|
if let threshold = threshold,
|
|
let ownersInfo = ownersInfo,
|
|
threshold > 1 && ownersInfo.count > 2 {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
var walletConnectSessiontopics: [String] {
|
|
guard let topics = sessionTopics else { return [] }
|
|
return topics.components(separatedBy: " ")
|
|
}
|
|
|
|
// Demo address
|
|
static let demoAddress = "0xfF501B324DC6d78dC9F983f140B9211c3EdB4dc7"
|
|
|
|
// this value is for contract versions 1.0.0 and 1.1.1 (probably for later versions as well)
|
|
static let DefaultEIP712SafeAppTxTypeHash =
|
|
Data(ethHex: "0xbb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8")
|
|
|
|
enum DomainSeparatorTypeHash {
|
|
static let v1_1_1 = Data(ethHex: "0x035aff83d86937d35b32e04f0ddc6ff469290eef2f1b692d8a815c89404d4749")
|
|
static let v1_3_0 = Data(ethHex: "0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218")
|
|
}
|
|
|
|
static func domainData(for safe: AddressString, version: Version, chainId: String) -> Data {
|
|
if version >= Version("1.3.0")! {
|
|
let chainIdData = UInt256(chainId, radix: 10)!.data32
|
|
return DomainSeparatorTypeHash.v1_3_0 + chainIdData + safe.data32
|
|
} else {
|
|
return DomainSeparatorTypeHash.v1_1_1 + safe.data32
|
|
}
|
|
}
|
|
|
|
static var count: Int {
|
|
let context = App.shared.coreDataStack.viewContext
|
|
return (try? context.count(for: Safe.fetchRequest().all())) ?? 0
|
|
}
|
|
|
|
/// Exclude Demo Safe from counting.
|
|
/// It is acceptable that we will not count a real owner of this Safe.
|
|
static var countExcludingDemo: Int {
|
|
let context = App.shared.coreDataStack.viewContext
|
|
return (try? context.count(for: Safe.fetchRequest().allExcludingDemo())) ?? 0
|
|
}
|
|
|
|
static var all: [Safe] {
|
|
let context = App.shared.coreDataStack.viewContext
|
|
return (try? context.fetch(Safe.fetchRequest().all())) ?? []
|
|
}
|
|
|
|
static func by(address: String, chainId: String) -> Safe? {
|
|
dispatchPrecondition(condition: .onQueue(.main))
|
|
let context = App.shared.coreDataStack.viewContext
|
|
let fr = Safe.fetchRequest().by(address: address, chainId: chainId)
|
|
return try? context.fetch(fr).first
|
|
}
|
|
|
|
static func by(topic: String) -> Safe? {
|
|
dispatchPrecondition(condition: .onQueue(.main))
|
|
return Safe.all.first {$0.walletConnectSessiontopics.contains(topic)}
|
|
}
|
|
|
|
static func updateCachedNames() {
|
|
guard let safes = try? Safe.getAll() else { return }
|
|
|
|
cachedNames = safes.reduce(into: [String: String]()) { names, safe in
|
|
let chainId = safe.chain != nil ? safe.chain!.id! : "1"
|
|
let key = "\(safe.displayAddress):\(chainId)"
|
|
names[key] = safe.name!
|
|
}
|
|
}
|
|
|
|
static func cachedName(by address: AddressString, chainId: String) -> String? {
|
|
let key = "\(address.description):\(chainId)"
|
|
return cachedNames[key]
|
|
}
|
|
|
|
static func cachedName(by address: String, chainId: String) -> String? {
|
|
let key = "\(address):\(chainId)"
|
|
return cachedNames[key]
|
|
}
|
|
|
|
public override func awakeFromInsert() {
|
|
super.awakeFromInsert()
|
|
additionDate = Date()
|
|
}
|
|
|
|
func select() {
|
|
let selection = Selection.current()
|
|
selection.safe = self
|
|
App.shared.coreDataStack.saveContext()
|
|
NotificationCenter.default.post(name: .selectedSafeChanged, object: nil)
|
|
}
|
|
|
|
static func getSelected() throws -> Safe? {
|
|
do {
|
|
let context = App.shared.coreDataStack.viewContext
|
|
let fr = Safe.fetchRequest().selected()
|
|
let safe = try context.fetch(fr).first
|
|
return safe
|
|
} catch {
|
|
throw GSError.DatabaseError(reason: error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
static func getAll() throws -> [Safe] {
|
|
do {
|
|
let context = App.shared.coreDataStack.viewContext
|
|
let fr = Safe.fetchRequest().all()
|
|
let safes = try context.fetch(fr)
|
|
return safes
|
|
} catch {
|
|
throw GSError.DatabaseError(reason: error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
static func exists(_ address: String, chainId: String) -> Bool {
|
|
by(address: address, chainId: chainId) != nil
|
|
}
|
|
|
|
@discardableResult
|
|
static func create(address: String, version: String?, name: String, chain: Chain, selected: Bool = true, status: SafeStatus = .deployed) -> Safe {
|
|
dispatchPrecondition(condition: .onQueue(.main))
|
|
let context = App.shared.coreDataStack.viewContext
|
|
|
|
let safe = Safe(context: context)
|
|
safe.address = address
|
|
safe.contractVersion = version
|
|
safe.name = name
|
|
safe.chain = chain
|
|
safe.safeStatus = status
|
|
|
|
if selected {
|
|
safe.select()
|
|
}
|
|
|
|
App.shared.coreDataStack.saveContext()
|
|
|
|
Tracker.setNumSafesUserProperty(countExcludingDemo)
|
|
Tracker.trackEvent(.userSafeAdded, parameters: ["chain_id" : chain.id!])
|
|
|
|
updateCachedNames()
|
|
|
|
return safe
|
|
}
|
|
|
|
func update(name: String) {
|
|
dispatchPrecondition(condition: .onQueue(.main))
|
|
|
|
self.name = name
|
|
|
|
App.shared.coreDataStack.saveContext()
|
|
NotificationCenter.default.post(name: .selectedSafeUpdated, object: self)
|
|
|
|
Safe.updateCachedNames()
|
|
}
|
|
|
|
func addSession(topic: String) {
|
|
dispatchPrecondition(condition: .onQueue(.main))
|
|
var topics = walletConnectSessiontopics
|
|
if topics.contains(topic) { return }
|
|
topics.append(topic)
|
|
sessionTopics = topics.joined(separator: " ")
|
|
App.shared.coreDataStack.saveContext()
|
|
}
|
|
|
|
static func removeSession(topic: String) {
|
|
Safe.all.forEach{
|
|
let topics = $0.walletConnectSessiontopics
|
|
if topics.contains(topic) {
|
|
$0.sessionTopics = topics.filter { $0 != topic }.joined(separator: " ")
|
|
}
|
|
}
|
|
|
|
App.shared.coreDataStack.saveContext()
|
|
}
|
|
|
|
static func select(address: String, chainId: String) {
|
|
dispatchPrecondition(condition: .onQueue(.main))
|
|
let context = App.shared.coreDataStack.viewContext
|
|
let fr = Safe.fetchRequest().by(address: address, chainId: chainId)
|
|
guard let safe = try? context.fetch(fr).first else { return }
|
|
safe.select()
|
|
}
|
|
|
|
static func remove(safe: Safe) {
|
|
let deletedSafeAddress = safe.address
|
|
let context = App.shared.coreDataStack.viewContext
|
|
|
|
let chainId = safe.chain!.id!
|
|
|
|
|
|
if let deletedSafeAddress = deletedSafeAddress {
|
|
// delete related EthTransaction's
|
|
let txs = CDEthTransaction.by(safeAddresses: [deletedSafeAddress], chainId: chainId)
|
|
for tx in txs {
|
|
context.delete(tx)
|
|
}
|
|
|
|
// delete stored SafeCreationCall's
|
|
let calls = SafeCreationCall.by(safe: safe)
|
|
for call in calls {
|
|
context.delete(call)
|
|
}
|
|
}
|
|
|
|
context.delete(safe)
|
|
|
|
if let safe = try? Safe.getAll().first {
|
|
safe.select()
|
|
}
|
|
|
|
App.shared.coreDataStack.saveContext()
|
|
|
|
Tracker.setNumSafesUserProperty(countExcludingDemo)
|
|
Tracker.trackEvent(.userSafeRemoved, parameters: ["chain_id" : chainId])
|
|
|
|
NotificationCenter.default.post(name: .selectedSafeChanged, object: nil)
|
|
|
|
if let addressString = deletedSafeAddress, let address = Address(addressString) {
|
|
App.shared.notificationHandler.safeRemoved(address: address, chainId: chainId)
|
|
}
|
|
|
|
updateCachedNames()
|
|
}
|
|
|
|
static func removeAll() throws {
|
|
for safe in all {
|
|
remove(safe: safe)
|
|
}
|
|
|
|
NotificationCenter.default.post(name: .selectedSafeChanged, object: nil)
|
|
}
|
|
}
|
|
|
|
extension Safe {
|
|
func update(from info: SafeInfoRequest.ResponseType) {
|
|
threshold = info.threshold.value
|
|
ownersInfo = info.owners.map { $0.addressInfo }
|
|
implementationInfo = info.implementation.addressInfo
|
|
implementationVersionState = ImplementationVersionState(info.implementationVersionState)
|
|
nonce = info.nonce.value
|
|
modulesInfo = info.modules?.map { $0.addressInfo }
|
|
fallbackHandlerInfo = info.fallbackHandler?.addressInfo
|
|
guardInfo = info.guard?.addressInfo
|
|
version = info.version
|
|
DispatchQueue.main.async {
|
|
self.contractVersion = info.version
|
|
App.shared.coreDataStack.saveContext()
|
|
NotificationCenter.default.post(name: .selectedSafeUpdated, object: self)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension NSFetchRequest where ResultType == Safe {
|
|
func all() -> Self {
|
|
sortDescriptors = [NSSortDescriptor(keyPath: \Safe.additionDate, ascending: true)]
|
|
return self
|
|
}
|
|
|
|
func allExcludingDemo() -> Self {
|
|
predicate = NSPredicate(format: "%K != %@", "address", Safe.demoAddress)
|
|
return self
|
|
}
|
|
|
|
func selected() -> Self {
|
|
sortDescriptors = []
|
|
predicate = NSPredicate(format: "selection != nil")
|
|
return self
|
|
}
|
|
}
|
|
|
|
extension ImplementationVersionState {
|
|
init(_ implementationVersionState: SCGModels.ImplementationVersionState) {
|
|
switch implementationVersionState {
|
|
case .unknown: self = .unknown
|
|
case .upToDate: self = .upToDate
|
|
case .upgradeAvailable: self = .upgradeAvailable
|
|
}
|
|
}
|
|
}
|
|
|
|
enum SafeStatus: Int16 {
|
|
case deployed = 0
|
|
case deploying = 1
|
|
case deploymentFailed = 2
|
|
case indexing = 3
|
|
}
|
|
|
|
enum SafeSecurityStatus: Int16 {
|
|
case medium = 0
|
|
case high = 1
|
|
|
|
var color: UIColor {
|
|
switch self {
|
|
case .medium:
|
|
return .warning
|
|
case .high:
|
|
return .success
|
|
}
|
|
}
|
|
}
|