mirror of
https://github.com/luxfi/safe-ios.git
synced 2026-10-30 15:19:20 +00:00
GH-3365 implemented wallet connect v2 for signing messages and sending transactions from connected wallets. - connect/disconnect - eth_sign, eth_signTypedData_v4
457 lines
18 KiB
Swift
457 lines
18 KiB
Swift
//
|
|
// Created by Dmitry Bespalov on 10.02.22.
|
|
// Copyright (c) 2022 Gnosis Ltd. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import Ethereum
|
|
import Solidity
|
|
import JsonRpc2
|
|
|
|
/// Responsible for back-and-forth between the in-memory WebConnection-related objects and persisted CoreData objects.
|
|
class WebConnectionRepository {
|
|
|
|
// -1 values are 'marker' or 'sentinel' values that indicate that the database's primitve value is nil.
|
|
private static let CHAIN_ID_NIL: Int64 = -1
|
|
private static let REQUEST_ID_INT_NIL: Int64 = -1
|
|
private static let REQUEST_ID_DOUBLE_NIL: Double = -1
|
|
private static let ICONS_SEPARATOR: String = "|"
|
|
|
|
func connections() -> [WebConnection] {
|
|
var connections: [WebConnection] = []
|
|
do {
|
|
connections = try CDWCConnection.getAll().compactMap(connectionV2(from:))
|
|
} catch {
|
|
LogService.shared.error("Failed to get Web Connections: \(error.localizedDescription)")
|
|
}
|
|
return connections
|
|
}
|
|
|
|
func connections(status: WebConnectionStatus) -> [WebConnection] {
|
|
let result = CDWCConnection.connections(by: status.rawValue).compactMap(connectionV2(from:))
|
|
return result
|
|
}
|
|
|
|
func connection(url: WebConnectionURL) -> WebConnection? {
|
|
connection(url: url.absoluteString)
|
|
}
|
|
|
|
func connection(url: String) -> WebConnection? {
|
|
guard let cdConnection = CDWCConnection.connection(by: url) else { return nil }
|
|
return connectionV2(from: cdConnection)
|
|
}
|
|
|
|
func connection(topic: String) -> WebConnection? {
|
|
guard let cdConnection = CDWCConnection.connection(topic: topic) else { return nil }
|
|
return connectionV2(from: cdConnection)
|
|
}
|
|
|
|
func connections(expiredAt date: Date) -> [WebConnection] {
|
|
let result = CDWCConnection.connections(expiredAt: date).compactMap(connectionV2(from:))
|
|
return result
|
|
}
|
|
|
|
func connections(account: Address) -> [WebConnection] {
|
|
guard let keyInfo = (try? KeyInfo.firstKey(address: account)), let cdConnections = keyInfo.connections else { return [] }
|
|
let result = cdConnections.allObjects.compactMap { $0 as? CDWCConnection }.compactMap(connectionV2(from:))
|
|
return result
|
|
}
|
|
|
|
func delete(_ connection: WebConnection) {
|
|
CDWCConnection.delete(by: connection.connectionURL.absoluteString)
|
|
}
|
|
|
|
// connection.url = pairingTopic (the session doesn't have the initial wc url
|
|
func save(_ connection: WebConnection) {
|
|
assert(Thread.isMainThread)
|
|
|
|
// find or replace the object
|
|
var cdConnection: CDWCConnection! = CDWCConnection.connection(by: connection.connectionURL.absoluteString)
|
|
if cdConnection == nil {
|
|
cdConnection = CDWCConnection.create()
|
|
}
|
|
// update the fields
|
|
update(cdConnection: cdConnection, with: connection)
|
|
|
|
// update local peer
|
|
if let peer = connection.localPeer {
|
|
let cdPeer = cdConnection.localPeer ?? CDWCPeerInfo(context: App.shared.coreDataStack.viewContext)
|
|
update(cdPeer: cdPeer, with: peer)
|
|
cdConnection.localPeer = cdPeer
|
|
} else {
|
|
cdConnection.localPeer = nil
|
|
}
|
|
|
|
// update remote peer info
|
|
if let peer = connection.remotePeer {
|
|
let cdPeer = cdConnection.remotePeer ?? CDWCPeerInfo(context: App.shared.coreDataStack.viewContext)
|
|
update(cdPeer: cdPeer, with: peer)
|
|
cdConnection.remotePeer = cdPeer
|
|
} else {
|
|
cdConnection.remotePeer = nil
|
|
}
|
|
|
|
// update connected keys / accounts
|
|
if let keys = cdConnection.keys, keys.count > 0 {
|
|
cdConnection.removeFromKeys(keys)
|
|
}
|
|
do {
|
|
let keys = try KeyInfo.keys(addresses: connection.accounts)
|
|
cdConnection.keys = NSSet(array: keys)
|
|
} catch {
|
|
LogService.shared.error("Failed to set connection keys: \(error)")
|
|
}
|
|
|
|
// save the database
|
|
App.shared.coreDataStack.saveContext()
|
|
}
|
|
|
|
private func update(cdConnection: CDWCConnection, with other: WebConnection) {
|
|
cdConnection.connectionURL = other.connectionURL.absoluteString
|
|
cdConnection.chainId = other.chainId.map { Int64($0) } ?? Self.CHAIN_ID_NIL
|
|
cdConnection.createdDate = other.createdDate
|
|
cdConnection.expirationDate = other.expirationDate
|
|
cdConnection.lastActivityDate = other.lastActivityDate
|
|
cdConnection.status = other.status.rawValue
|
|
cdConnection.lastError = other.lastError
|
|
cdConnection.accounts = other.accounts.map(\.checksummed).joined(separator: ",")
|
|
}
|
|
|
|
private func update(cdPeer: CDWCPeerInfo, with other: WebConnectionPeerInfo) {
|
|
cdPeer.peerId = other.peerId
|
|
cdPeer.peerType = other.peerType.rawValue
|
|
cdPeer.role = other.role.rawValue
|
|
cdPeer.name = other.name
|
|
cdPeer.peerDescription = other.description
|
|
cdPeer.icons = other.icons.map(\.absoluteString).joined(separator: Self.ICONS_SEPARATOR)
|
|
cdPeer.deeplinkScheme = other.deeplinkScheme
|
|
cdPeer.url = other.url
|
|
}
|
|
|
|
private func connection(from other: CDWCConnection) -> WebConnection? {
|
|
guard
|
|
let rawConnectionURL: String = other.connectionURL,
|
|
let connectionURL = WebConnectionURL(string: rawConnectionURL)
|
|
else { return nil }
|
|
|
|
let chainId: Int? = other.chainId == Self.CHAIN_ID_NIL ? nil : Int(other.chainId)
|
|
let createdDate: Date? = other.createdDate
|
|
let expirationDate: Date? = other.expirationDate
|
|
let lastActivityDate: Date? = other.lastActivityDate
|
|
let status = WebConnectionStatus(rawValue: other.status) ?? .unknown
|
|
|
|
let accountsFromKeyInfos = (other.keys ?? NSSet()).allObjects.map { $0 as! KeyInfo }.map(\.address)
|
|
let accountsFromStrings = (other.accounts ?? "").split(separator: ",").map(String.init).compactMap(Address.init)
|
|
let accounts = Array(Set(accountsFromKeyInfos + accountsFromStrings))
|
|
|
|
let localPeer = other.localPeer.flatMap(peer(from:))
|
|
let remotePeer = other.remotePeer.flatMap(peer(from:))
|
|
let lastError: String? = other.lastError
|
|
|
|
let result = WebConnection(
|
|
connectionURL: connectionURL,
|
|
status: status,
|
|
chainId: chainId,
|
|
accounts: accounts,
|
|
createdDate: createdDate,
|
|
expirationDate: expirationDate,
|
|
lastActivityDate: lastActivityDate,
|
|
localPeer: localPeer,
|
|
remotePeer: remotePeer,
|
|
lastError: lastError
|
|
)
|
|
return result
|
|
}
|
|
|
|
private func connectionV2(from other: CDWCConnection) -> WebConnection? {
|
|
guard
|
|
let rawConnectionURL: String = other.connectionURL,
|
|
let connectionURL = WebConnectionURL(stringV2: rawConnectionURL)
|
|
else { return nil }
|
|
|
|
let chainId: Int? = other.chainId == Self.CHAIN_ID_NIL ? nil : Int(other.chainId)
|
|
let createdDate: Date? = other.createdDate
|
|
let expirationDate: Date? = other.expirationDate
|
|
let lastActivityDate: Date? = other.lastActivityDate
|
|
let status = WebConnectionStatus(rawValue: other.status) ?? .unknown
|
|
|
|
let accountsFromKeyInfos = (other.keys ?? NSSet()).allObjects.map { $0 as! KeyInfo }.map(\.address)
|
|
let accountsFromStrings = (other.accounts ?? "").split(separator: ",").map(String.init).compactMap(Address.init)
|
|
let accounts = Array(Set(accountsFromKeyInfos + accountsFromStrings))
|
|
|
|
let localPeer = other.localPeer.flatMap(peer(from:))
|
|
let remotePeer = other.remotePeer.flatMap(peer(from:))
|
|
let lastError: String? = other.lastError
|
|
|
|
let result = WebConnection(
|
|
connectionURL: connectionURL,
|
|
status: status,
|
|
chainId: chainId,
|
|
accounts: accounts,
|
|
createdDate: createdDate,
|
|
expirationDate: expirationDate,
|
|
lastActivityDate: lastActivityDate,
|
|
localPeer: localPeer,
|
|
remotePeer: remotePeer,
|
|
lastError: lastError
|
|
)
|
|
return result
|
|
}
|
|
|
|
private func peer(from other: CDWCPeerInfo) -> WebConnectionPeerInfo? {
|
|
guard
|
|
let rawIcons = other.icons,
|
|
let name: String = other.name,
|
|
let peerId: String = other.peerId,
|
|
let url: URL = other.url
|
|
else {
|
|
return nil
|
|
}
|
|
let iconParts: [Substring] = rawIcons.split(separator: Character(Self.ICONS_SEPARATOR))
|
|
let icons: [URL] = iconParts.compactMap { URL(string: String($0)) }
|
|
|
|
let peerType = WebConnectionPeerType(rawValue: other.peerType) ?? .unknown
|
|
let role = WebConnectionPeerRole(rawValue: other.role) ?? .unknown
|
|
let peerDescription: String? = other.peerDescription
|
|
let deeplinkScheme: String? = other.deeplinkScheme
|
|
|
|
if peerType == .gnosisSafeWeb {
|
|
return GnosisSafeWebPeerInfo(
|
|
peerId: peerId,
|
|
peerType: peerType,
|
|
role: role,
|
|
url: url,
|
|
name: name,
|
|
description: peerDescription,
|
|
icons: icons,
|
|
deeplinkScheme: deeplinkScheme)
|
|
}
|
|
|
|
return WebConnectionPeerInfo(
|
|
peerId: peerId,
|
|
peerType: peerType,
|
|
role: role,
|
|
url: url,
|
|
name: name,
|
|
description: peerDescription,
|
|
icons: icons,
|
|
deeplinkScheme: deeplinkScheme
|
|
)
|
|
}
|
|
|
|
func save(_ request: WebConnectionRequest) {
|
|
assert(Thread.isMainThread)
|
|
// get request
|
|
var existing: CDWCRequest? = nil
|
|
if let url = request.connectionURL, let id = request.id {
|
|
existing = cdRequest(connectionURL: url, requestId: id)
|
|
}
|
|
let cdRequest = existing ?? CDWCRequest.create()
|
|
|
|
// update it
|
|
update(cdRequest: cdRequest, with: request)
|
|
|
|
// connect to a connection entity
|
|
if let url = request.connectionURL, let cdConnection = CDWCConnection.connection(by: url.absoluteString) {
|
|
cdRequest.connection = cdConnection
|
|
}
|
|
|
|
App.shared.coreDataStack.saveContext()
|
|
}
|
|
|
|
private func cdRequest(connectionURL: WebConnectionURL, requestId: WebConnectionRequestId) -> CDWCRequest? {
|
|
CDWCRequest.request(
|
|
url: connectionURL.absoluteString,
|
|
id_int: requestId.intValue.map(Int64.init) ?? Self.REQUEST_ID_INT_NIL,
|
|
id_double: requestId.doubleValue ?? Self.REQUEST_ID_DOUBLE_NIL,
|
|
id_string: requestId.stringValue
|
|
)
|
|
}
|
|
|
|
func request(connectionURL: WebConnectionURL, requestId: WebConnectionRequestId) -> WebConnectionRequest? {
|
|
let result = cdRequest(connectionURL: connectionURL, requestId: requestId)
|
|
.flatMap(request(from:))
|
|
return result
|
|
}
|
|
|
|
private func update(cdRequest: CDWCRequest, with other: WebConnectionRequest) {
|
|
cdRequest.id_int = other.id?.intValue.map(Int64.init) ?? Self.REQUEST_ID_INT_NIL
|
|
cdRequest.id_string = other.id?.stringValue
|
|
cdRequest.id_double = other.id?.doubleValue ?? Self.REQUEST_ID_DOUBLE_NIL
|
|
cdRequest.json = other.json
|
|
cdRequest.method = other.method
|
|
cdRequest.error = other.error
|
|
cdRequest.status = other.status.rawValue
|
|
cdRequest.createdDate = other.createdDate
|
|
}
|
|
|
|
fileprivate func openConnectionRequest(_ other: CDWCRequest) -> WebConnectionOpenRequest {
|
|
let id = requestId(from: other)
|
|
// TODO: stringV2
|
|
let connectionURL = other.connection?.connectionURL.flatMap { WebConnectionURL(string: $0) }
|
|
let status = WebConnectionRequestStatus(rawValue: other.status) ?? .unknown
|
|
let result = WebConnectionOpenRequest(
|
|
id: id,
|
|
method: other.method,
|
|
error: other.error,
|
|
json: other.json,
|
|
status: status,
|
|
connectionURL: connectionURL,
|
|
createdDate: other.createdDate)
|
|
return result
|
|
}
|
|
|
|
fileprivate func signatureRequest(_ other: CDWCRequest) -> WebConnectionSignatureRequest? {
|
|
let id = requestId(from: other)
|
|
// TODO: stringV2
|
|
let connectionURL = other.connection?.connectionURL.flatMap { WebConnectionURL(string: $0) }
|
|
let status = WebConnectionRequestStatus(rawValue: other.status) ?? .unknown
|
|
guard let json = other.json else {
|
|
return nil
|
|
}
|
|
let account: Address
|
|
let message: Data
|
|
do {
|
|
let data = json.data(using: .utf8)!
|
|
let request = try JSONDecoder().decode(JsonRpc2.Request.self, from: data)
|
|
guard request.method == "eth_sign", let eth_sign = try request.params?.convert(to: EthRpc1.eth_sign.self) else {
|
|
return nil
|
|
}
|
|
let suffixBytes = Array(eth_sign.address.storage).suffix(20)
|
|
let addressData = Data(suffixBytes)
|
|
guard let address = Address(addressData) else {
|
|
return nil
|
|
}
|
|
account = address
|
|
message = eth_sign.message.storage
|
|
} catch {
|
|
LogService.shared.error("Failed to decode eth_sign request from CoreData: \(error)")
|
|
return nil
|
|
}
|
|
let result = WebConnectionSignatureRequest(
|
|
id: id,
|
|
method: other.method,
|
|
error: other.error,
|
|
json: other.json,
|
|
status: status,
|
|
connectionURL: connectionURL,
|
|
createdDate: other.createdDate,
|
|
account: account,
|
|
message: message)
|
|
return result
|
|
}
|
|
|
|
fileprivate func sendTransactionRequest(_ other: CDWCRequest) -> WebConnectionSendTransactionRequest? {
|
|
let id = requestId(from: other)
|
|
// TODO: stringV2
|
|
let connectionURL = other.connection?.connectionURL.flatMap { WebConnectionURL(string: $0) }
|
|
let status = WebConnectionRequestStatus(rawValue: other.status) ?? .unknown
|
|
guard let json = other.json else {
|
|
return nil
|
|
}
|
|
|
|
do {
|
|
let data = json.data(using: .utf8)!
|
|
let request = try JSONDecoder().decode(JsonRpc2.Request.self, from: data)
|
|
guard
|
|
request.method == "eth_sendTransaction",
|
|
let params = try request.params?.convert(to: EthRpc1.eth_sendTransaction.self)
|
|
else {
|
|
return nil
|
|
}
|
|
let transaction = params.transaction.ethTransaction
|
|
let result = WebConnectionSendTransactionRequest(
|
|
id: id,
|
|
method: other.method,
|
|
error: other.error,
|
|
json: other.json,
|
|
status: status,
|
|
connectionURL: connectionURL,
|
|
createdDate: other.createdDate,
|
|
transaction: transaction)
|
|
return result
|
|
} catch {
|
|
LogService.shared.error("Failed to decode eth_sign request from CoreData: \(error)")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
fileprivate func genericRequest(_ other: CDWCRequest) -> WebConnectionRequest? {
|
|
let id = requestId(from: other)
|
|
// TODO: stringV2
|
|
let connectionURL = other.connection?.connectionURL.flatMap { WebConnectionURL(string: $0) }
|
|
let status = WebConnectionRequestStatus(rawValue: other.status) ?? .unknown
|
|
let result = WebConnectionRequest(
|
|
id: id,
|
|
method: other.method,
|
|
error: other.error,
|
|
json: other.json,
|
|
status: status,
|
|
connectionURL: connectionURL,
|
|
createdDate: other.createdDate
|
|
)
|
|
return result
|
|
}
|
|
|
|
func request(from other: CDWCRequest) -> WebConnectionRequest? {
|
|
switch other.method {
|
|
case "wc_sessionRequest":
|
|
return openConnectionRequest(other)
|
|
|
|
case "eth_sendTransaction":
|
|
if let request = sendTransactionRequest(other) {
|
|
return request
|
|
} else {
|
|
return genericRequest(other)
|
|
}
|
|
|
|
case "eth_sign":
|
|
if let request = signatureRequest(other) {
|
|
return request
|
|
} else {
|
|
return genericRequest(other)
|
|
}
|
|
|
|
default:
|
|
return genericRequest(other)
|
|
}
|
|
}
|
|
|
|
func requestId(from other: CDWCRequest) -> WebConnectionRequestId? {
|
|
if other.id_double != Self.REQUEST_ID_DOUBLE_NIL {
|
|
return WebConnectionRequestId(doubleValue: other.id_double)
|
|
} else if other.id_int != Self.REQUEST_ID_INT_NIL {
|
|
return WebConnectionRequestId(intValue: Int(other.id_int))
|
|
} else if let value = other.id_string {
|
|
return WebConnectionRequestId(stringValue: value)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func pendingConnectionRequest(url: WebConnectionURL) -> WebConnectionOpenRequest? {
|
|
let result = CDWCRequest
|
|
.request(connectionURL: url, method: "wc_sessionRequest", status: .pending)
|
|
.map(openConnectionRequest(_:))
|
|
return result
|
|
}
|
|
|
|
func pendingRequests(connection: WebConnection? = nil) -> [WebConnectionRequest] {
|
|
let cdRequests = connection == nil ?
|
|
CDWCRequest.all(status: WebConnectionRequestStatus.pending.rawValue) :
|
|
CDWCRequest.all(url: connection!.connectionURL.absoluteString, status: WebConnectionRequestStatus.pending.rawValue)
|
|
let result = cdRequests.compactMap(request(from:))
|
|
return result
|
|
}
|
|
|
|
func delete(request: WebConnectionRequest) {
|
|
guard let url = request.connectionURL, let requestId = request.id else { return }
|
|
CDWCRequest.delete(
|
|
url: url.absoluteString,
|
|
id_int: requestId.intValue.map(Int64.init) ?? Self.REQUEST_ID_INT_NIL,
|
|
id_double: requestId.doubleValue ?? Self.REQUEST_ID_DOUBLE_NIL,
|
|
id_string: requestId.stringValue
|
|
)
|
|
}
|
|
}
|