mirror of
https://github.com/luxfi/safe-ios.git
synced 2026-10-30 15:19:20 +00:00
1348 lines
51 KiB
Swift
1348 lines
51 KiB
Swift
//
|
|
// Created by Dmitry Bespalov on 10.02.22.
|
|
// Copyright (c) 2022 Gnosis Ltd. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import WalletConnectSwift
|
|
import Ethereum
|
|
import Solidity
|
|
import WalletConnectSign
|
|
import Combine
|
|
|
|
protocol WebConnectionObserver: AnyObject {
|
|
func didUpdate(connection: WebConnection)
|
|
}
|
|
|
|
protocol WebConnectionSubject: AnyObject {
|
|
func attach(observer: WebConnectionObserver, to connection: WebConnection)
|
|
func detach(observer: WebConnectionObserver)
|
|
func notifyObservers(of connection: WebConnection)
|
|
}
|
|
|
|
protocol WebConnectionListObserver: AnyObject {
|
|
func didUpdateConnections()
|
|
}
|
|
|
|
protocol WebConnectionListSubject: AnyObject {
|
|
func attach(observer: WebConnectionListObserver)
|
|
func detach(observer: WebConnectionListObserver)
|
|
func notifyListObservers()
|
|
}
|
|
|
|
protocol WebConnectionRequestObserver: AnyObject {
|
|
func didUpdate(request: WebConnectionRequest)
|
|
}
|
|
|
|
protocol WebConnectionRequestSubject: AnyObject {
|
|
func attach(observer: WebConnectionRequestObserver)
|
|
func detach(observer: WebConnectionRequestObserver)
|
|
func notifyObservers(of request: WebConnectionRequest)
|
|
}
|
|
|
|
protocol WebConnectionSingleRequestSubject: AnyObject {
|
|
func attach(observer: WebConnectionRequestObserver, to request: WebConnectionRequest)
|
|
func notifyObservers(of request: WebConnectionRequest)
|
|
}
|
|
/// Controller implementing the business-logic of managing connections and handling incoming requests.
|
|
///
|
|
/// Use the `shared` instance since the controller's lifetime is the same as the app's lifetime.
|
|
///
|
|
/// Remember to set the `delegate` in order to respond to connection events.
|
|
class WebConnectionController: ServerDelegateV2, RequestHandler, WebConnectionSubject, WebConnectionListSubject, WebConnectionRequestSubject, WebConnectionSingleRequestSubject, ClientDelegateV2 {
|
|
|
|
static let shared = WebConnectionController()
|
|
|
|
private var server: Server!
|
|
private let connectionRepository = WebConnectionRepository()
|
|
private let sessionTransformer = WebConnectionToSessionTransformer()
|
|
|
|
private var client: Client!
|
|
|
|
private var subscriptions = Set<AnyCancellable>()
|
|
|
|
private static let safeWebConnectionLoadingTimeout: TimeInterval = 30
|
|
private static let walletConnectionLoadingTimeout: TimeInterval = 180
|
|
|
|
init() {
|
|
server = WalletConnectSwift.Server(delegate: self)
|
|
server.register(handler: self)
|
|
client = WalletConnectSwift.Client(delegate: self)
|
|
regsiterEventListeners()
|
|
}
|
|
|
|
deinit {
|
|
server.unregister(handler: self)
|
|
}
|
|
|
|
// MARK: - Connection Observers
|
|
|
|
private var connectionObservers: [String: [WebConnectionObserver]] = [:]
|
|
|
|
func attach(observer: WebConnectionObserver, to connection: WebConnection) {
|
|
if var existing = connectionObservers[connection.connectionURL.absoluteString] {
|
|
guard !existing.contains(where: { $0 === observer }) else { return }
|
|
existing.append(observer)
|
|
connectionObservers[connection.connectionURL.absoluteString] = existing
|
|
} else {
|
|
connectionObservers[connection.connectionURL.absoluteString] = [observer]
|
|
}
|
|
}
|
|
|
|
func detach(observer: WebConnectionObserver) {
|
|
for key in connectionObservers.keys {
|
|
if var existing = connectionObservers[key], let index = existing.firstIndex(where: { $0 === observer }) {
|
|
existing.remove(at: index)
|
|
connectionObservers[key] = existing
|
|
}
|
|
}
|
|
}
|
|
|
|
func notifyObservers(of connection: WebConnection) {
|
|
guard
|
|
let toNotify = connectionObservers[connection.connectionURL.absoluteString],
|
|
let connection = self.connection(for: connection.connectionURL)
|
|
else { return }
|
|
for observer in toNotify {
|
|
observer.didUpdate(connection: connection)
|
|
}
|
|
}
|
|
|
|
// MARK: - Connection List Observers
|
|
|
|
private var listObservers: [WebConnectionListObserver] = []
|
|
|
|
func attach(observer: WebConnectionListObserver) {
|
|
guard !listObservers.contains(where: { $0 === observer }) else { return }
|
|
listObservers.append(observer)
|
|
}
|
|
|
|
func detach(observer: WebConnectionListObserver) {
|
|
if let index = listObservers.firstIndex(where: { $0 === observer }) {
|
|
listObservers.remove(at: index)
|
|
}
|
|
}
|
|
|
|
func notifyListObservers() {
|
|
for observer in listObservers {
|
|
observer.didUpdateConnections()
|
|
}
|
|
}
|
|
|
|
// MARK: - WalletConnectSwift.Request Observer
|
|
|
|
private var requestListObservers: [WebConnectionRequestObserver] = []
|
|
private var requestObservers: [WebRequestIdentifier: [WebConnectionRequestObserver]] = [:]
|
|
|
|
func attach(observer: WebConnectionRequestObserver) {
|
|
guard !requestListObservers.contains(where: { $0 === observer }) else { return }
|
|
requestListObservers.append(observer)
|
|
}
|
|
|
|
func attach(observer: WebConnectionRequestObserver, to request: WebConnectionRequest) {
|
|
guard let url = request.connectionURL, let id = request.id else { return }
|
|
var requestListObservers = requestObservers[WebRequestIdentifier(url, id)] ?? []
|
|
guard !requestListObservers.contains(where: { $0 === observer }) else { return }
|
|
requestListObservers.append(observer)
|
|
requestObservers[WebRequestIdentifier(url, id)] = requestListObservers
|
|
}
|
|
|
|
func detach(observer: WebConnectionRequestObserver) {
|
|
if let index = requestListObservers.firstIndex(where: { $0 === observer }) {
|
|
requestListObservers.remove(at: index)
|
|
}
|
|
for key in requestObservers.keys {
|
|
var list = requestObservers[key]!
|
|
if let index = list.firstIndex(where: { $0 === observer }) {
|
|
list.remove(at: index)
|
|
requestObservers[key] = list
|
|
}
|
|
}
|
|
}
|
|
|
|
func notifyObservers(of request: WebConnectionRequest) {
|
|
for observer in requestListObservers {
|
|
observer.didUpdate(request: request)
|
|
}
|
|
|
|
if let url = request.connectionURL, let id = request.id, let list = requestObservers[WebRequestIdentifier(url, id)] {
|
|
for observer in list {
|
|
observer.didUpdate(request: request)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns list of keys that can be used as wallets for this connection.
|
|
///
|
|
/// - Returns: all keys that can be connected.
|
|
func accountKeys() -> [KeyInfo] {
|
|
do {
|
|
let result = try KeyInfo.all()
|
|
return result
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
// MARK: - Connecting
|
|
|
|
func connect(to string: String) throws -> WebConnection {
|
|
var remotePeerType: WebConnectionPeerType = .dapp
|
|
// create connection from the url string
|
|
var string = string
|
|
if string.hasPrefix("safe-wc:") {
|
|
string.removeFirst("safe-".count)
|
|
remotePeerType = .gnosisSafeWeb
|
|
}
|
|
// for now only handle 'safe web' to 'wallet' connections
|
|
guard remotePeerType == .gnosisSafeWeb, let url = WebConnectionURL(string: string) else {
|
|
throw WebConnectionError.urlNotSupported
|
|
}
|
|
if let connection = connection(for: url) {
|
|
// if already exists, resume from the current status
|
|
update(connection, to: connection.status)
|
|
return connection
|
|
} else {
|
|
let connection = createConnection(from: url)
|
|
update(connection, to: .handshaking)
|
|
return connection
|
|
}
|
|
}
|
|
|
|
/// Implements the state machine transitions of a connection. Persists the connection.
|
|
///
|
|
/// - Parameters:
|
|
/// - connection: a connection to transition to another state
|
|
/// - newStatus: new state.
|
|
private func update(_ connection: WebConnection, to newStatus: WebConnectionStatus) {
|
|
connection.status = newStatus
|
|
save(connection)
|
|
|
|
switch newStatus {
|
|
case .initial:
|
|
// do nothing
|
|
break
|
|
|
|
case .handshaking:
|
|
do {
|
|
try start(connection)
|
|
} catch {
|
|
handle(error: error, in: connection)
|
|
}
|
|
|
|
case .approving:
|
|
// wait for user response, do nothing
|
|
break
|
|
|
|
case .approved:
|
|
respondToSessionCreation(connection)
|
|
update(connection, to: .opened)
|
|
|
|
case .rejected:
|
|
respondToSessionCreation(connection)
|
|
update(connection, to: .final)
|
|
|
|
case .opened:
|
|
updateActivityDate(connection: connection)
|
|
notifyListObservers()
|
|
|
|
case .closed:
|
|
disconnect(connection)
|
|
update(connection, to: .final)
|
|
|
|
case .final:
|
|
notifyObservers(of: connection)
|
|
deleteOutstandingRequests(connection)
|
|
delete(connection)
|
|
notifyListObservers()
|
|
|
|
case .unknown:
|
|
// stay here
|
|
break
|
|
}
|
|
|
|
if connection.status == newStatus {
|
|
notifyObservers(of: connection)
|
|
}
|
|
}
|
|
|
|
private func start(_ connection: WebConnection) throws {
|
|
do {
|
|
guard
|
|
let local = connection.localPeer
|
|
else {
|
|
assertionFailure("Misconfigured connection: local peer is missing")
|
|
throw WebConnectionError.configurationError
|
|
}
|
|
switch local.role {
|
|
case .wallet:
|
|
try server.connect(to: connection.connectionURL.wcURL)
|
|
scheduleTimeout(connectionURL: connection.connectionURL, timeout: Self.safeWebConnectionLoadingTimeout)
|
|
|
|
case .dapp:
|
|
guard
|
|
let chainId = connection.chainId,
|
|
let blockchain = Blockchain("eip155:\(chainId)")
|
|
else { throw WebConnectionError.configurationError }
|
|
Task { @MainActor in
|
|
do {
|
|
scheduleTimeout(connectionURL: connection.connectionURL, timeout: Self.walletConnectionLoadingTimeout)
|
|
|
|
let namespace = ProposalNamespace(
|
|
chains: [blockchain],
|
|
methods: ["eth_sendTransaction", "eth_sign", "eth_signTypedData_v4"],
|
|
events: [])
|
|
|
|
try await Sign.instance.connect(
|
|
requiredNamespaces: ["eip155": namespace],
|
|
topic: connection.connectionURL.wcURI!.topic
|
|
)
|
|
} catch {
|
|
handle(error: error, in: connection)
|
|
}
|
|
}
|
|
|
|
default:
|
|
throw WebConnectionError.unsupportedConnectionType
|
|
}
|
|
} catch {
|
|
throw WebConnectionError.connectionFailure(error)
|
|
}
|
|
}
|
|
|
|
// if connection still waiting to receive 'connection request' from the other side after
|
|
// timeout, then we close it with error.
|
|
private func scheduleTimeout(connectionURL: WebConnectionURL, timeout: TimeInterval) {
|
|
Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { [weak self] _ in
|
|
guard let self = self else { return }
|
|
if let connection = self.connection(for: connectionURL), connection.status == .handshaking {
|
|
self.handle(error: WebConnectionError.connectionStartTimeout, in: connection)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func updateActivityDate(connection: WebConnection) {
|
|
connection.lastActivityDate = Date()
|
|
let secondsIn24Hours: TimeInterval = 24 * 60 * 60
|
|
connection.expirationDate = connection.lastActivityDate!.addingTimeInterval(secondsIn24Hours)
|
|
save(connection)
|
|
}
|
|
|
|
private func handle(error: Error, in connection: WebConnection) {
|
|
connection.lastError = error.localizedDescription
|
|
update(connection, to: .final)
|
|
}
|
|
|
|
private func respondToSessionCreation(_ connection: WebConnection) {
|
|
// only applicable for wallet role
|
|
guard let myself = connection.localPeer, myself.role == .wallet else {
|
|
return
|
|
}
|
|
|
|
guard
|
|
let session = sessionTransformer.session(from: connection),
|
|
let request = pendingConnectionRequest(connection: connection),
|
|
let requestId = request.id.flatMap(sessionTransformer.requestId(id:)),
|
|
let walletInfo = session.walletInfo
|
|
else {
|
|
update(connection, to: .closed)
|
|
return
|
|
}
|
|
server.sendCreateSessionResponse(for: requestId, session: session, walletInfo: walletInfo)
|
|
delete(request)
|
|
}
|
|
|
|
func reconnect() {
|
|
let connections = connectionRepository.connections(status: WebConnectionStatus.opened)
|
|
let sessions = connections.map(sessionTransformer.session(from:))
|
|
|
|
for (session, connection) in zip(sessions, connections) where session != nil {
|
|
do {
|
|
if connection.localPeer?.role == .wallet {
|
|
try server.reconnect(to: session!)
|
|
}
|
|
} catch {
|
|
handle(error: userError(from: error), in: connection)
|
|
}
|
|
}
|
|
|
|
let allPendingRequests: [WebConnectionRequest] = pendingRequests()
|
|
for request in allPendingRequests {
|
|
notifyObservers(of: request)
|
|
}
|
|
|
|
// clear the pending wallet connections because we lost the context of adding a key
|
|
let pendingWalletConnections = connectionRepository.connections(status: .handshaking).filter { $0.localPeer?.role == .dapp }
|
|
for connection in pendingWalletConnections {
|
|
update(connection, to: .closed)
|
|
}
|
|
}
|
|
|
|
func handleExpiredConnections() {
|
|
let now = Date()
|
|
let connections = connectionRepository.connections(expiredAt: now)
|
|
for connection in connections {
|
|
expire(connection)
|
|
}
|
|
}
|
|
|
|
// MARK: - Managing WebConnection
|
|
|
|
func connections() -> [WebConnection] {
|
|
connectionRepository.connections().filter { connection in
|
|
connection.localPeer?.role == .wallet &&
|
|
connection.remotePeer?.peerType == .gnosisSafeWeb &&
|
|
connection.remotePeer?.role == .dapp
|
|
}
|
|
}
|
|
|
|
func connection(for session: WalletConnectSwift.Session) -> WebConnection? {
|
|
connection(for: session.url)
|
|
}
|
|
|
|
func connection(for url: WCURL) -> WebConnection? {
|
|
connection(for: WebConnectionURL(wcURL: url))
|
|
}
|
|
|
|
func connection(for url: WebConnectionURL) -> WebConnection? {
|
|
connectionRepository.connection(url: url)
|
|
}
|
|
|
|
func connection(for request: WebConnectionRequest) -> WebConnection? {
|
|
request.connectionURL.flatMap(connection(for:))
|
|
}
|
|
|
|
func chain(for request: WebConnectionRequest) -> Chain? {
|
|
connection(for: request).flatMap { $0.chainId }.map(String.init).flatMap { Chain.by($0) }
|
|
}
|
|
|
|
func pendingConnectionRequest(connection: WebConnection) -> WebConnectionOpenRequest? {
|
|
connectionRepository.pendingConnectionRequest(url: connection.connectionURL)
|
|
}
|
|
|
|
func pendingRequests() -> [WebConnectionRequest] {
|
|
connectionRepository.pendingRequests()
|
|
}
|
|
|
|
// MARK: - v2 Session
|
|
|
|
func connection(for session: WalletConnectSign.Session) -> WebConnection? {
|
|
connectionRepository.connection(topic: session.pairingTopic)
|
|
}
|
|
|
|
func connection(for proposal: WalletConnectSign.Session.Proposal) -> WebConnection? {
|
|
connectionRepository.connection(topic: proposal.pairingTopic)
|
|
}
|
|
|
|
func connection(uri: WebConnectionURL) -> WebConnection? {
|
|
connectionRepository.connection(url: uri.wcURI!.topic)
|
|
}
|
|
|
|
/// Creates new wallet to gnosis safe web app connection. The dapp information is set to placeholder data except
|
|
/// for the `peerType` and `role`.
|
|
///
|
|
/// - Parameter url: the URL to connect to
|
|
/// - Returns: new WebConnection with populated url, local peer, remote peer, and the created date set to the current time.
|
|
func createConnection(from url: WebConnectionURL) -> WebConnection {
|
|
// create the new connection with the information about this app and expectation about the dapp to be
|
|
// a gnosis safe web app.
|
|
let connection = WebConnection(connectionURL: url)
|
|
connection.createdDate = Date()
|
|
connection.localPeer = WebConnectionPeerInfo(
|
|
peerId: UUID().uuidString,
|
|
peerType: .thisApp,
|
|
role: .wallet,
|
|
url: App.configuration.services.webAppURL,
|
|
name: "Gnosis Safe",
|
|
description: "The most trusted platform to manage digital assets",
|
|
icons: [App.configuration.services.webAppURL.appendingPathComponent("favicon.ico")],
|
|
deeplinkScheme: "gnosissafe:"
|
|
)
|
|
connection.remotePeer = GnosisSafeWebPeerInfo(
|
|
peerId: "",
|
|
peerType: .gnosisSafeWeb,
|
|
role: .dapp,
|
|
url: App.configuration.services.webAppURL,
|
|
name: "",
|
|
description: nil,
|
|
icons: [],
|
|
deeplinkScheme: nil)
|
|
return connection
|
|
}
|
|
|
|
/// Saves the connection to the persistence store
|
|
///
|
|
/// - Parameter connection: connection to save
|
|
func save(_ connection: WebConnection) {
|
|
connectionRepository.save(connection)
|
|
}
|
|
|
|
/// Deletes the connection from the persistence store
|
|
///
|
|
/// - Parameter connection: connection to delete
|
|
func delete(_ connection: WebConnection) {
|
|
connectionRepository.delete(connection)
|
|
}
|
|
|
|
func delete(_ request: WebConnectionRequest) {
|
|
connectionRepository.delete(request: request)
|
|
}
|
|
|
|
func disconnect(_ connection: WebConnection) {
|
|
guard let session = sessionTransformer.sessionV2(from: connection) else { return }
|
|
Task { @MainActor in
|
|
do {
|
|
try await Sign.instance.disconnect(topic: session.topic)
|
|
} catch {
|
|
LogService.shared.error("Failed to disconnect: \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
func expire(_ connection: WebConnection) {
|
|
update(connection, to: .closed)
|
|
}
|
|
|
|
func deleteOutstandingRequests(_ connection: WebConnection) {
|
|
let requests = connectionRepository.pendingRequests(connection: connection)
|
|
for request in requests {
|
|
request.status = .failed
|
|
save(request)
|
|
notifyObservers(of: request)
|
|
delete(request)
|
|
}
|
|
}
|
|
|
|
// MARK: - User events
|
|
|
|
/// Expected to be called by the UI when user approves connection
|
|
///
|
|
/// - Parameter connection: a connection
|
|
func userDidApprove(_ connection: WebConnection) {
|
|
update(connection, to: .approved)
|
|
}
|
|
|
|
/// Expected to be called by UI when user rejects connection
|
|
///
|
|
/// - Parameter connection: a connection
|
|
func userDidReject(_ connection: WebConnection) {
|
|
update(connection, to: .rejected)
|
|
}
|
|
|
|
/// Expected to be called by UI when user cancels connection
|
|
///
|
|
/// - Parameter connection: a connection
|
|
func userDidCancel(_ connection: WebConnection) {
|
|
update(connection, to: .rejected)
|
|
}
|
|
|
|
func userDidDisconnect(_ connection: WebConnection) {
|
|
update(connection, to: .closed)
|
|
}
|
|
|
|
func userDidDelete(account: Address) {
|
|
disconnectConnections(account: account)
|
|
}
|
|
|
|
func disconnectConnections(account: Address) {
|
|
let connections = connectionRepository.connections(account: account)
|
|
for connection in connections {
|
|
update(connection, to: .closed)
|
|
}
|
|
}
|
|
|
|
func userDidChange(network: Chain, in connection: WebConnection) {
|
|
guard let stringId = network.id, let chainId = Int(stringId), connection.chainId != chainId else { return }
|
|
connection.chainId = chainId
|
|
updateSession(from: connection)
|
|
}
|
|
|
|
func userDidChange(account: KeyInfo, in connection: WebConnection) {
|
|
guard !connection.accounts.contains(account.address) else { return }
|
|
connection.accounts = [account.address]
|
|
updateSession(from: connection)
|
|
}
|
|
|
|
private func updateSession(from connection: WebConnection) {
|
|
guard
|
|
let session = sessionTransformer.session(from: connection),
|
|
let walletInfo = session.walletInfo
|
|
else {
|
|
return
|
|
}
|
|
|
|
do {
|
|
if connection.connectedAsWallet {
|
|
try server.updateSession(session, with: walletInfo)
|
|
} else {
|
|
let request = try WalletConnectSwift.Request(url: connection.connectionURL.wcURL, method: "wc_sessionUpdate", params: [walletInfo], id: nil)
|
|
try client.send(request, completion: nil)
|
|
}
|
|
|
|
update(connection, to: .opened)
|
|
} catch {
|
|
LogService.shared.error("Error updating session: \(error)")
|
|
}
|
|
}
|
|
|
|
// MARK: - Server Delegate (Server events)
|
|
|
|
func server(_ server: Server, shouldStart session: WalletConnectSwift.Session, completion: @escaping (WalletConnectSwift.Session.WalletInfo) -> ()) {
|
|
// ignore this, because we implement another method - didReceiveConnectionRequest
|
|
}
|
|
|
|
func server(_ server: Server, didReceiveConnectionRequest requestId: RequestID, for session: WalletConnectSwift.Session) {
|
|
// we save the information from the request and wait until user responds with some action.
|
|
// main thread needed because of the CoreData dependency
|
|
DispatchQueue.main.async { [unowned self] in
|
|
guard let connection = connection(for: session) else {
|
|
return
|
|
}
|
|
assert(connection.status == .handshaking)
|
|
sessionTransformer.update(connection: connection, with: session)
|
|
|
|
let wcRequest = WalletConnectSwift.Request(url: session.url, method: "wc_sessionRequest", id: requestId)
|
|
guard let request = sessionTransformer.request(from: wcRequest) else {
|
|
respondToSessionCreation(connection)
|
|
handle(error: WebConnectionError.connectionStartFailed, in: connection)
|
|
return
|
|
}
|
|
request.status = .pending
|
|
save(request)
|
|
|
|
// check that the chain id exists in the app
|
|
if let chainId = connection.chainId, let exists = (try? Chain.exists("\(chainId)")), !exists {
|
|
// respond with rejection and exit with error
|
|
respondToSessionCreation(connection)
|
|
handle(error: WebConnectionError.unsupportedNetwork, in: connection)
|
|
} else {
|
|
update(connection, to: .approving)
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate func handleConnectionFailure(_ url: WCURL) {
|
|
// the connection process failed
|
|
DispatchQueue.main.async { [unowned self] in
|
|
guard let connection = connection(for: url) else {
|
|
return
|
|
}
|
|
let error = WebConnectionError.connectionStartFailed
|
|
handle(error: error, in: connection)
|
|
}
|
|
}
|
|
|
|
func server(_ server: Server, didFailToConnect url: WCURL) {
|
|
handleConnectionFailure(url)
|
|
}
|
|
|
|
func server(_ server: Server, didConnect session: WalletConnectSwift.Session) {
|
|
// ignore
|
|
}
|
|
|
|
fileprivate func handleConnectionClosed(_ session: WalletConnectSwift.Session) {
|
|
// when the connection is closed from outside
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self = self else { return }
|
|
guard let connection = self.connection(for: session) else { return }
|
|
self.update(connection, to: .final)
|
|
}
|
|
}
|
|
|
|
func server(_ server: Server, didDisconnect session: WalletConnectSwift.Session) {
|
|
handleConnectionClosed(session)
|
|
}
|
|
|
|
// this will be called when session's chain Id or accounts change
|
|
// when session is closed (approved = false), the 'disconnect' method will be called instead.
|
|
fileprivate func handleSessionUpdate(_ updatedSession: WalletConnectSwift.Session) {
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self = self else { return }
|
|
guard let connection = self.connection(for: updatedSession), connection.status == .opened else { return }
|
|
|
|
guard let walletInfo = updatedSession.walletInfo else {
|
|
self.update(connection, to: .closed)
|
|
return
|
|
}
|
|
|
|
// check that we support chain id
|
|
do {
|
|
let chainExists = try Chain.exists(String(walletInfo.chainId))
|
|
|
|
if !chainExists {
|
|
self.update(connection, to: .closed)
|
|
return
|
|
} else {
|
|
connection.chainId = walletInfo.chainId
|
|
}
|
|
} catch {
|
|
LogService.shared.error("Error checking whether chain exists: \(error)")
|
|
}
|
|
|
|
if connection.localPeer?.role == .wallet {
|
|
// check that we have the key
|
|
guard let address = walletInfo.accounts.first, let account = Address(address) else {
|
|
self.update(connection, to: .closed)
|
|
return
|
|
}
|
|
|
|
do {
|
|
let key = try KeyInfo.firstKey(address: account)
|
|
|
|
if key == nil {
|
|
self.update(connection, to: .closed)
|
|
return
|
|
} else {
|
|
connection.accounts = [account]
|
|
}
|
|
} catch {
|
|
LogService.shared.error("Error checking whether key exists: \(error)")
|
|
}
|
|
} else if connection.localPeer?.role == .dapp {
|
|
// we don't support changing accounts because we keep the addresses static in the owner key list.
|
|
// i.e. if we do, then we would need to allow potentially duplicate owner keys and modify the
|
|
// existing key info on such account change.
|
|
// another alternative would be to allow changing the account as long as it doesn't duplicate another
|
|
// owner key.
|
|
|
|
let changedAccounts = walletInfo.accounts.compactMap(Address.init)
|
|
let existingAccounts = connection.accounts
|
|
|
|
if Set(changedAccounts) != Set(existingAccounts) {
|
|
connection.lastError = WebConnectionError.unexpectedAccount.localizedDescription
|
|
self.update(connection, to: .closed)
|
|
return
|
|
}
|
|
}
|
|
|
|
// all checks passed, update the connection.
|
|
self.update(connection, to: .opened)
|
|
}
|
|
}
|
|
|
|
func server(_ server: Server, didUpdate updatedSession: WalletConnectSwift.Session) {
|
|
handleSessionUpdate(updatedSession)
|
|
}
|
|
|
|
func server(_ server: Server, willReconnect session: WalletConnectSwift.Session) {
|
|
// not implemented
|
|
}
|
|
|
|
// MARK: - Server WalletConnectSwift.Request Handling
|
|
|
|
func canHandle(request: WalletConnectSwift.Request) -> Bool {
|
|
["eth_sign", "eth_sendTransaction"].contains(request.method)
|
|
}
|
|
|
|
func handle(request wcRequest: WalletConnectSwift.Request) {
|
|
if !Thread.isMainThread {
|
|
DispatchQueue.main.async { [weak self] in self?.handle(request: wcRequest) }
|
|
return
|
|
}
|
|
// convert WC request to a connection request
|
|
guard let request = sessionTransformer.request(from: wcRequest) else {
|
|
try? server.send(WalletConnectSwift.Response(request: wcRequest, error: .invalidParams))
|
|
return
|
|
}
|
|
guard let url = request.connectionURL, let connection = connection(for: url), connection.status == .opened else {
|
|
try? server.send(WalletConnectSwift.Response(request: wcRequest, error: .internalError))
|
|
return
|
|
}
|
|
|
|
switch request {
|
|
case let signRequest as WebConnectionSignatureRequest:
|
|
guard connection.accounts.contains(signRequest.account) else {
|
|
try? server.send(WalletConnectSwift.Response(request: wcRequest, error: .invalidParams))
|
|
return
|
|
}
|
|
|
|
case let sendTxRequest as WebConnectionSendTransactionRequest:
|
|
guard let from = sendTxRequest.transaction.from,
|
|
connection.accounts.contains(Address(from))
|
|
else {
|
|
try? server.send(WalletConnectSwift.Response(request: wcRequest, error: .invalidParams))
|
|
return
|
|
}
|
|
let address = Address(from)
|
|
|
|
// reject ledger nano x because it is not yet supported for sending transactions
|
|
if let key = try? KeyInfo.firstKey(address: address), key.keyType == .ledgerNanoX {
|
|
try? server.send(WalletConnectSwift.Response(request: wcRequest, error: .requestRejected))
|
|
App.shared.snackbar.show(message: "Executing transactions with Ledger Nano X is not supported.")
|
|
return
|
|
}
|
|
prepareTransactionForExecution(sendTxRequest)
|
|
|
|
default:
|
|
break
|
|
}
|
|
updateActivityDate(connection: connection)
|
|
request.status = .pending
|
|
request.createdDate = Date()
|
|
save(request)
|
|
notifyObservers(of: request)
|
|
}
|
|
|
|
func prepareTransactionForExecution(_ request: WebConnectionSendTransactionRequest) {
|
|
guard
|
|
let connection = connection(for: request),
|
|
let chainId = connection.chainId,
|
|
let chain = chain(for: request),
|
|
let features = chain.features
|
|
else {
|
|
return
|
|
}
|
|
|
|
let minerTip: Sol.UInt256 = 1_500_000_000
|
|
|
|
// select tx type based on the chain.
|
|
let isEIP1559 = features.contains("EIP1559")
|
|
if isEIP1559 {
|
|
request.transaction = Eth.TransactionEip1559(
|
|
chainId: Sol.UInt256(chainId),
|
|
from: request.transaction.from,
|
|
to: request.transaction.to,
|
|
value: request.transaction.value,
|
|
data: request.transaction.data,
|
|
fee: .init(maxPriorityFee: minerTip)
|
|
)
|
|
} else {
|
|
request.transaction = Eth.TransactionLegacy(
|
|
chainId: Sol.UInt256(chainId),
|
|
from: request.transaction.from,
|
|
to: request.transaction.to,
|
|
value: request.transaction.value,
|
|
data: request.transaction.data
|
|
)
|
|
}
|
|
}
|
|
|
|
func request(_ connectionURL: WebConnectionURL, _ requestId: WebConnectionRequestId) -> WebConnectionRequest? {
|
|
connectionRepository.request(connectionURL: connectionURL, requestId: requestId)
|
|
}
|
|
|
|
func respond<T>(request: WebConnectionRequest, with value: T) where T: Codable {
|
|
guard
|
|
let connectionURL = request.connectionURL,
|
|
let requestId = request.id,
|
|
let id = sessionTransformer.requestId(id: requestId),
|
|
let request = self.request(connectionURL, requestId),
|
|
request.status == .pending
|
|
else { return }
|
|
|
|
if let connection = connection(for: connectionURL), connection.status == .opened {
|
|
do {
|
|
try server.send(WalletConnectSwift.Response(url: connectionURL.wcURL, value: value, id: id))
|
|
} catch {
|
|
LogService.shared.error("Failed to respond to WC server: \(error)")
|
|
}
|
|
}
|
|
request.status = .success
|
|
notifyObservers(of: request)
|
|
delete(request)
|
|
}
|
|
|
|
func respond(request: WebConnectionRequest, errorCode: Int, message: String) {
|
|
guard
|
|
let connectionURL = request.connectionURL,
|
|
let requestId = request.id,
|
|
let id = sessionTransformer.requestId(id: requestId),
|
|
let request = self.request(connectionURL, requestId),
|
|
request.status == .pending
|
|
else { return }
|
|
if let connection = connection(for: connectionURL), connection.status == .opened {
|
|
do {
|
|
try server.send(WalletConnectSwift.Response(url: connectionURL.wcURL, errorCode: errorCode, message: message, id: id))
|
|
} catch {
|
|
LogService.shared.error("Failed to return error to WC server: \(error)")
|
|
}
|
|
}
|
|
request.status = .failed
|
|
notifyObservers(of: request)
|
|
delete(request)
|
|
}
|
|
|
|
func save(_ request: WebConnectionRequest) {
|
|
connectionRepository.save(request)
|
|
}
|
|
|
|
// MARK: - Client Delegate
|
|
|
|
// called when creating connection or creating a session
|
|
func client(_ client: Client, dappInfoForUrl url: WCURL) -> WalletConnectSwift.Session.DAppInfo? {
|
|
guard let connection = connection(for: url),
|
|
let session = sessionTransformer.session(from: connection) else {
|
|
return nil
|
|
}
|
|
return session.dAppInfo
|
|
}
|
|
|
|
// called when failed to connect to bridge server or to establsih session
|
|
func client(_ client: Client, didFailToConnect url: WCURL) {
|
|
handleConnectionFailure(url)
|
|
}
|
|
|
|
// called when successfully connected to a bridge server
|
|
func client(_ client: Client, didConnect url: WCURL) {
|
|
// nothing to do
|
|
}
|
|
|
|
// called when successfully established session (handshake success).
|
|
// also called on re-connect to the bridge
|
|
func client(_ client: Client, didConnect session: WalletConnectSwift.Session) {
|
|
DispatchQueue.main.async { [unowned self] in
|
|
guard let connection = connection(for: session), connection.status == .handshaking else {
|
|
return
|
|
}
|
|
sessionTransformer.update(connection: connection, with: session)
|
|
|
|
// check that the chain id exists in the app
|
|
if let chainId = connection.chainId, let exists = (try? Chain.exists("\(chainId)")), !exists {
|
|
// respond with rejection and close connection
|
|
connection.lastError = WebConnectionError.unsupportedNetwork.localizedDescription
|
|
update(connection, to: .closed)
|
|
} else {
|
|
update(connection, to: .approved)
|
|
}
|
|
}
|
|
}
|
|
|
|
// called when received 'wc_updateSession' with approved = false
|
|
func client(_ client: Client, didDisconnect session: WalletConnectSwift.Session) {
|
|
handleConnectionClosed(session)
|
|
}
|
|
|
|
// called when received 'wc_updateSession' with approved = true
|
|
func client(_ client: Client, didUpdate session: WalletConnectSwift.Session) {
|
|
handleSessionUpdate(session)
|
|
}
|
|
|
|
func client(_ client: Client, willReconnect session: WalletConnectSwift.Session) {
|
|
// not implemented
|
|
}
|
|
|
|
// MARK: - Connect Wallet Logic
|
|
|
|
func walletConnection(keyInfo: KeyInfo) -> [WebConnection] {
|
|
let result = connectionRepository.connections(account: keyInfo.address).filter { connection in
|
|
connection.localPeer?.role == .dapp && connection.remotePeer?.role == .wallet
|
|
}
|
|
return result
|
|
}
|
|
|
|
// create connection to a wallet
|
|
@MainActor
|
|
func connect(wallet info: WCAppRegistryEntry?, chainId: Int?) async throws -> WebConnection {
|
|
let wcURI = try await Pair.instance.create()
|
|
let connectURL: WebConnectionURL = WebConnectionURL(wcURI: wcURI)
|
|
let connection = createWalletConnection(from: connectURL, info: info)
|
|
connection.chainId = chainId
|
|
update(connection, to: .handshaking)
|
|
return connection
|
|
}
|
|
|
|
func createWalletConnection(from url: WebConnectionURL, info: WCAppRegistryEntry?) -> WebConnection {
|
|
let connection = WebConnection(connectionURL: url)
|
|
connection.createdDate = Date()
|
|
connection.localPeer = WebConnectionPeerInfo(
|
|
peerId: UUID().uuidString,
|
|
peerType: .thisApp,
|
|
role: .dapp,
|
|
url: App.configuration.services.webAppURL,
|
|
name: "Safe {Wallet}",
|
|
description: "The most trusted platform to manage digital assets",
|
|
icons: [App.configuration.services.webAppURL.appendingPathComponent("favicon.ico")],
|
|
deeplinkScheme: "gnosissafe:"
|
|
)
|
|
if let info = info {
|
|
connection.remotePeer = WebConnectionPeerInfo(
|
|
peerId: "",
|
|
peerType: .wallet,
|
|
role: .wallet,
|
|
url: info.homepage ?? App.configuration.services.webAppURL,
|
|
name: info.name,
|
|
description: info.description,
|
|
icons: info.imageLargeUrl.map { [$0] } ?? [],
|
|
deeplinkScheme: info.linkMobileNative?.absoluteString)
|
|
} else {
|
|
connection.remotePeer = WebConnectionPeerInfo(
|
|
peerId: "",
|
|
peerType: .wallet,
|
|
role: .wallet,
|
|
url: App.configuration.services.webAppURL,
|
|
name: "WalletConnect",
|
|
description: "WalletConnect",
|
|
icons: [],
|
|
deeplinkScheme: "wc:")
|
|
}
|
|
return connection
|
|
}
|
|
|
|
// MARK: - Sending Requests to Wallet
|
|
|
|
func sendTransaction(connection: WebConnection, transaction: WCTransaction, completion: @escaping (Result<String, Error>) -> ()) {
|
|
guard
|
|
let session = sessionTransformer.sessionV2(from: connection),
|
|
let walletAddress = session.accounts.first
|
|
else {
|
|
completion(.failure(GSError.WalletNotConnected(description: "Failed to send transaction")))
|
|
return
|
|
}
|
|
|
|
do {
|
|
let request = try WalletConnectSign.Request(
|
|
topic: session.topic,
|
|
method: "eth_sendTransaction",
|
|
params: AnyCodable([transaction]),
|
|
chainId: walletAddress.blockchain
|
|
)
|
|
send(request: request, completion: completion)
|
|
} catch {
|
|
LogService.shared.error("WalletConnectSign.Request failed: \(error)")
|
|
completion(.failure(GSError.WC2GenericError(
|
|
description: "Failed to send transaction",
|
|
reason: "Sign request has an error",
|
|
howToFix: "Please contact Safe support")))
|
|
}
|
|
}
|
|
|
|
func send(request: WalletConnectSign.Request, completion: @escaping (Result<String, Error>) -> Void) {
|
|
pendingRequestCompletions.append((request, completion))
|
|
Task {
|
|
do {
|
|
try await Sign.instance.request(params: request)
|
|
// response will come via publisher subscription
|
|
} catch {
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self = self else { return }
|
|
completion(.failure(userError(from: error)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func userError(from error: Error) -> Error {
|
|
switch error {
|
|
case WalletConnectSwift.Client.ClientError.missingWalletInfoInSession,
|
|
WalletConnectSwift.Client.ClientError.sessionNotFound,
|
|
WalletConnectSwift.Server.ServerError.missingWalletInfoInSession:
|
|
return WebConnectionError.connectionLost
|
|
case WalletConnectSwift.Server.ServerError.failedToCreateSessionResponse:
|
|
return WebConnectionError.connectionStartFailed
|
|
default:
|
|
return error
|
|
}
|
|
}
|
|
|
|
// MARK: - Signing
|
|
|
|
var pendingRequestCompletions: [(request: WalletConnectSign.Request, completion: (Result<String, Error>) -> Void)] = []
|
|
|
|
func wcSign(connection: WebConnection, message: String, completion: @escaping (Result<String, Error>) -> Void) {
|
|
guard
|
|
let session = sessionTransformer.sessionV2(from: connection),
|
|
let walletAddress = session.accounts.first
|
|
else {
|
|
completion(.failure(GSError.WalletNotConnected(description: "Could not sign message")))
|
|
return
|
|
}
|
|
do {
|
|
let request = try WalletConnectSign.Request(
|
|
topic: session.topic,
|
|
method: "eth_sign",
|
|
params: AnyCodable([walletAddress.address, message]),
|
|
chainId: walletAddress.blockchain
|
|
)
|
|
send(request: request, completion: completion)
|
|
} catch {
|
|
LogService.shared.error("WalletConnectSign.Request failed: \(error)")
|
|
completion(.failure(GSError.WC2GenericError(
|
|
description: "Failed to sign message",
|
|
reason: "Sign request has an error",
|
|
howToFix: "Please contact Safe support")))
|
|
}
|
|
}
|
|
|
|
func wcSign(connection: WebConnection, transaction: Transaction, completion: @escaping (Result<String, Error>) -> Void) {
|
|
guard
|
|
let session = sessionTransformer.sessionV2(from: connection),
|
|
let walletAddress = session.accounts.first
|
|
else {
|
|
completion(.failure(GSError.WalletNotConnected(description: "Could not sign transaction")))
|
|
return
|
|
}
|
|
let supportsTypedData = session.namespaces.contains { (key: String, value: SessionNamespace) in
|
|
key == "eip155" && value.methods.contains("eth_signTypedData_v4")
|
|
}
|
|
if supportsTypedData {
|
|
do {
|
|
let message = EIP712Transformer.typedData(transaction)
|
|
let request = try WalletConnectSign.Request(
|
|
topic: session.topic,
|
|
method: "eth_signTypedData_v4",
|
|
params: AnyCodable([walletAddress.address, message]),
|
|
chainId: walletAddress.blockchain
|
|
)
|
|
send(request: request, completion: completion)
|
|
} catch {
|
|
LogService.shared.error("WalletConnectSign.Request failed: \(error)")
|
|
completion(.failure(GSError.WC2GenericError(
|
|
description: "Failed to sign transaction",
|
|
reason: "Sign typed data request has an error",
|
|
howToFix: "Please contact Safe support")))
|
|
}
|
|
} else {
|
|
do {
|
|
let message = transaction.safeTxHash.description
|
|
let request = try WalletConnectSign.Request(
|
|
topic: session.topic,
|
|
method: "eth_sign",
|
|
params: AnyCodable([walletAddress.address, message]),
|
|
chainId: walletAddress.blockchain
|
|
)
|
|
send(request: request, completion: completion)
|
|
} catch {
|
|
LogService.shared.error("WalletConnectSign.Request failed: \(error)")
|
|
completion(.failure(GSError.WC2GenericError(
|
|
description: "Failed to sign transaction",
|
|
reason: "Sign request has an error",
|
|
howToFix: "Please contact Safe support")))
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate func handleSignature(result: AnyCodable, _ completion: @escaping (Result<String, Error>) -> Void) {
|
|
do {
|
|
var signature = try result.get(String.self)
|
|
|
|
var signatureBytes = Data(hex: signature).bytes
|
|
|
|
if signatureBytes.count == 65 {
|
|
var v = signatureBytes.last!
|
|
if v < 27 {
|
|
v += 27
|
|
signatureBytes[signatureBytes.count - 1] = v
|
|
signature = Data(signatureBytes).toHexStringWithPrefix()
|
|
}
|
|
|
|
completion(.success(signature))
|
|
} else {
|
|
completion(.success(signature))
|
|
}
|
|
} catch {
|
|
completion(.failure(error))
|
|
}
|
|
}
|
|
|
|
fileprivate func handleTxHash(result: AnyCodable, _ completion: @escaping (Result<String, Error>) -> Void) {
|
|
do {
|
|
let txHash = try result.get(String.self)
|
|
completion(.success(txHash))
|
|
} catch {
|
|
completion(.failure(error))
|
|
}
|
|
}
|
|
|
|
func handleResponse(response: WalletConnectSign.Response) {
|
|
let requestIndex = pendingRequestCompletions.firstIndex { item in
|
|
item.request.id == response.id
|
|
}
|
|
guard let i = requestIndex else { return }
|
|
|
|
let request = pendingRequestCompletions.remove(at: i)
|
|
|
|
if case RPCResult.error(let error) = response.result {
|
|
request.completion(.failure(error))
|
|
return
|
|
}
|
|
|
|
guard case RPCResult.response(let result) = response.result else {
|
|
return
|
|
}
|
|
|
|
if request.request.method == "eth_sign" || request.request.method == "eth_signTypedData_v4" {
|
|
handleSignature(result: result, request.completion)
|
|
} else {
|
|
handleTxHash(result: result, request.completion)
|
|
}
|
|
}
|
|
|
|
private func handleSignResponse(_ response: WalletConnectSwift.Response, completion: @escaping (Result<String, Error>) -> Void) {
|
|
if let error = response.error {
|
|
completion(.failure(error))
|
|
return
|
|
}
|
|
do {
|
|
var signature = try response.result(as: String.self)
|
|
|
|
var signatureBytes = Data(hex: signature).bytes
|
|
|
|
if signatureBytes.count == 65 {
|
|
var v = signatureBytes.last!
|
|
if v < 27 {
|
|
v += 27
|
|
signatureBytes[signatureBytes.count - 1] = v
|
|
signature = Data(signatureBytes).toHexStringWithPrefix()
|
|
}
|
|
|
|
completion(.success(signature))
|
|
} else {
|
|
completion(.success(signature))
|
|
}
|
|
} catch {
|
|
completion(.failure(error))
|
|
}
|
|
}
|
|
|
|
// MARK: - V2 Publishers
|
|
|
|
func regsiterEventListeners() {
|
|
let s = Sign.instance
|
|
|
|
s.sessionSettlePublisher
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] (session: WalletConnectSign.Session) in
|
|
guard let self = self else { return }
|
|
LogService.shared.debug("{WC} Session Settled: \(session.topic)")
|
|
self.handleOpened(session: session)
|
|
}
|
|
.store(in: &subscriptions)
|
|
|
|
s.sessionRejectionPublisher
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] (proposal: WalletConnectSign.Session.Proposal, reason: Reason) in
|
|
guard let self = self else { return }
|
|
LogService.shared.debug("{WC} Session Rejected: \(proposal.id) \(reason)")
|
|
self.handleRejected(proposal: proposal, reason: reason)
|
|
}
|
|
.store(in: &subscriptions)
|
|
|
|
s.sessionExtendPublisher
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] (sessionTopic: String, date: Date) in
|
|
guard let self = self else { return }
|
|
LogService.shared.debug("{WC} Session Extended: \(sessionTopic) \(date)")
|
|
self.handleExtended(topic: sessionTopic, to: date)
|
|
}
|
|
.store(in: &subscriptions)
|
|
|
|
s.sessionDeletePublisher
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] (topic: String, reason: Reason) in
|
|
guard let self = self else { return }
|
|
LogService.shared.debug("{WC} Session Deleted: \(topic) \(reason)")
|
|
self.handleDeleted(topic: topic)
|
|
}
|
|
.store(in: &subscriptions)
|
|
|
|
s.sessionResponsePublisher
|
|
.receive(on: DispatchQueue.main)
|
|
.sink { [weak self] (response: WalletConnectSign.Response) in
|
|
guard let self = self else { return }
|
|
LogService.shared.debug("{WC} Response: \(response)")
|
|
handleResponse(response: response)
|
|
}
|
|
.store(in: &subscriptions)
|
|
}
|
|
|
|
func handleOpened(session: WalletConnectSign.Session) {
|
|
Task { @MainActor in
|
|
guard let connection = connection(for: session), connection.status == .handshaking else {
|
|
return
|
|
}
|
|
|
|
sessionTransformer.update(connection: connection, with: session)
|
|
|
|
// check that the chain id exists in the app
|
|
if let chainId = connection.chainId, let exists = (try? Chain.exists("\(chainId)")), !exists {
|
|
// respond with rejection and close connection
|
|
connection.lastError = WebConnectionError.unsupportedNetwork.localizedDescription
|
|
update(connection, to: .closed)
|
|
} else {
|
|
update(connection, to: .approved)
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleRejected(proposal: WalletConnectSign.Session.Proposal, reason: Reason) {
|
|
guard let connection = connection(for: proposal), connection.status == .handshaking else {
|
|
return
|
|
}
|
|
let error = WebConnectionError(errorCode: reason.code, message: reason.message)
|
|
handle(error: error, in: connection)
|
|
}
|
|
|
|
func handleExtended(topic: String, to date: Date) {
|
|
Task { @MainActor in
|
|
guard let session = Sign.instance.getSessions().first(where: { $0.topic == topic }),
|
|
let connection = connection(for: session)
|
|
else {
|
|
return
|
|
}
|
|
connection.lastActivityDate = Date()
|
|
connection.expirationDate = date
|
|
save(connection)
|
|
}
|
|
}
|
|
|
|
func handleDeleted(topic: String) {
|
|
Task { @MainActor in
|
|
guard let session = Sign.instance.getSessions().first(where: { $0.topic == topic }),
|
|
let connection = connection(for: session)
|
|
else {
|
|
return
|
|
}
|
|
update(connection, to: .closed)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/// User-visible error
|
|
struct WebConnectionError: CustomNSError {
|
|
private(set) static var errorDomain: String = "io.gnosis.safe.WebConnection"
|
|
var errorCode: Int
|
|
var message: String
|
|
var cause: Error? = nil
|
|
var errorUserInfo: [String: Any] {
|
|
var result: [String: Any] = [NSLocalizedDescriptionKey: message]
|
|
if let error = cause {
|
|
result[NSUnderlyingErrorKey] = error
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|
|
extension WebConnectionError {
|
|
/// When the connection URL is not of expected type
|
|
static let urlNotSupported = WebConnectionError(errorCode: -1, message: "This URL type is not supported.")
|
|
|
|
/// When the connection failed to establish
|
|
///
|
|
/// - Parameter error: underlying error
|
|
/// - Returns: an error value
|
|
static func connectionFailure(_ error: Error) -> WebConnectionError {
|
|
WebConnectionError(errorCode: -2, message: "Failed to connect.", cause: error)
|
|
}
|
|
|
|
/// When the connection failed to start and can be retried.
|
|
static let connectionStartFailed = WebConnectionError(errorCode: -3, message: "Failed to start connection. Please try again.")
|
|
|
|
static let unsupportedNetwork = WebConnectionError(errorCode: -4, message: "Failed to connect. Requested network is not supported.")
|
|
|
|
static let connectionStartTimeout = WebConnectionError(errorCode: -5, message: "Connection timeout. Please reload web app.")
|
|
|
|
static let keyGenerationFailed = WebConnectionError(errorCode: -6, message: "Failed to create encryption key. Please try again.")
|
|
|
|
static let configurationError = WebConnectionError(errorCode: -7, message: "Configuration error. Please try again.")
|
|
|
|
static let unsupportedConnectionType = WebConnectionError(errorCode: -8, message: "Unsupported connection type. Please try again.")
|
|
|
|
static let unexpectedAccount = WebConnectionError(errorCode: -9, message: "Unexpected account change. Please connect new account instead of changing existing one.")
|
|
|
|
static let connectionLost = WebConnectionError(errorCode: -10, message: "Connection lost. Please reconnect and try again.")
|
|
|
|
static let chainNotFound = WebConnectionError(errorCode: -11, message: "Selected chain not found. Please try again later.")
|
|
}
|
|
|