mirror of
https://github.com/status-im/status-desktop.git
synced 2025-01-11 14:54:48 +00:00
8d78f0fab2
1. The DAppsListProvider needs to receive all the user accounts so that it can process all dapps the user is connected to (the dapps filtering based on selected account is an internal impl. detail) 2. Call `revokeSession` on disconnect only if the dapp cannot be found in the WC active sessions. There is some overlapping between Browser connector and Wallet connect because both operate on the same data stored in the DB. If Browser connector controller removes WC data, the WC disconnect flows cannot be successfully completed. 3. Reuse the same notification flow for Browser connector as it's used for WC 4. Fix dapp filtering when processing the dapps that will be displayed in the UI. (cherry picked from commit 21227893c2f18ab487c367853f9648fc94d67afb)
363 lines
13 KiB
QML
363 lines
13 KiB
QML
import QtQuick 2.15
|
|
|
|
import StatusQ 0.1
|
|
import StatusQ.Core.Theme 0.1
|
|
import StatusQ.Core.Utils 0.1
|
|
|
|
import AppLayouts.Wallet 1.0
|
|
import AppLayouts.Wallet.services.dapps 1.0
|
|
import AppLayouts.Wallet.services.dapps.types 1.0
|
|
import AppLayouts.Profile.stores 1.0
|
|
import shared.stores 1.0
|
|
import shared.popups.walletconnect 1.0
|
|
|
|
import SortFilterProxyModel 0.2
|
|
import utils 1.0
|
|
|
|
import "types"
|
|
|
|
// The WC SDK has an async (function call then signal response)
|
|
// A complete pairing flow to connect a dApp:
|
|
// - user provides pairing url -> root.validatePairingUri -> signal pairingValidated
|
|
// - user requests pair -> root.pair(uri) -> pairResponse(ok)
|
|
// -> if pairResponse ok -> onSessionProposal -> sdk.buildApprovedNamespaces
|
|
// -> onBuildApprovedNamespace -> signal connectDApp
|
|
// - user requests root.approvePairSession/root.rejectPairSession
|
|
// -> if approvePairSession -> sdk.buildApprovedNamespaces
|
|
// -> onBuildApprovedNamespace -> sdk.approveSession -> onApproveSessionResult
|
|
QObject {
|
|
id: root
|
|
|
|
required property WalletConnectSDKBase wcSDK
|
|
required property DAppsStore store
|
|
required property var walletRootStore
|
|
|
|
readonly property var dappsModel: ConcatModel {
|
|
markerRoleName: "source"
|
|
|
|
sources: [
|
|
SourceModel {
|
|
model: dappsProvider.dappsModel
|
|
markerRoleValue: "walletConnect"
|
|
},
|
|
SourceModel {
|
|
model: connectorDAppsProvider.dappsModel
|
|
markerRoleValue: "connector"
|
|
}
|
|
]
|
|
}
|
|
readonly property alias requestHandler: requestHandler
|
|
|
|
readonly property bool isServiceAvailableForAddressSelection: dappsProvider.supportedAccountsModel.ModelCount.count
|
|
|
|
readonly property alias connectorDAppsProvider: connectorDAppsProvider
|
|
|
|
readonly property var validAccounts: SortFilterProxyModel {
|
|
sourceModel: d.supportedAccountsModel
|
|
proxyRoles: [
|
|
FastExpressionRole {
|
|
name: "colorizedChainPrefixes"
|
|
function getChainShortNames(chainIds) {
|
|
const chainShortNames = root.walletRootStore.getNetworkShortNames(chainIds)
|
|
return WalletUtils.colorizedChainPrefix(chainShortNames)
|
|
}
|
|
expression: getChainShortNames(model.preferredSharingChainIds)
|
|
expectedRoles: ["preferredSharingChainIds"]
|
|
}
|
|
]
|
|
}
|
|
readonly property var flatNetworks: root.walletRootStore.filteredFlatModel
|
|
|
|
function validatePairingUri(uri) {
|
|
// Check if emoji inside the URI
|
|
if(Constants.regularExpressions.emoji.test(uri)) {
|
|
root.pairingValidated(Pairing.errors.tooCool)
|
|
return
|
|
} else if(!DAppsHelpers.validURI(uri)) {
|
|
root.pairingValidated(Pairing.errors.invalidUri)
|
|
return
|
|
}
|
|
|
|
const info = DAppsHelpers.extractInfoFromPairUri(uri)
|
|
wcSDK.getActiveSessions((sessions) => {
|
|
// Check if the URI is already paired
|
|
let validationState = Pairing.errors.uriOk
|
|
for (const key in sessions) {
|
|
if (sessions[key].pairingTopic === info.topic) {
|
|
validationState = Pairing.errors.alreadyUsed
|
|
break
|
|
}
|
|
}
|
|
|
|
// Check if expired
|
|
if (validationState === Pairing.errors.uriOk) {
|
|
const now = (new Date().getTime())/1000
|
|
if (info.expiry < now) {
|
|
validationState = Pairing.errors.expired
|
|
}
|
|
}
|
|
|
|
root.pairingValidated(validationState)
|
|
});
|
|
}
|
|
|
|
function pair(uri) {
|
|
d.acceptedSessionProposal = null
|
|
timeoutTimer.start()
|
|
wcSDK.pair(uri)
|
|
}
|
|
|
|
function approvePairSession(sessionProposal, approvedChainIds, approvedAccount) {
|
|
d.acceptedSessionProposal = sessionProposal
|
|
const approvedNamespaces = JSON.parse(
|
|
DAppsHelpers.buildSupportedNamespaces(approvedChainIds,
|
|
[approvedAccount.address],
|
|
SessionRequest.getSupportedMethods())
|
|
)
|
|
wcSDK.buildApprovedNamespaces(sessionProposal.params, approvedNamespaces)
|
|
}
|
|
|
|
function rejectPairSession(id) {
|
|
wcSDK.rejectSession(id)
|
|
}
|
|
|
|
function disconnectSession(sessionTopic) {
|
|
wcSDK.disconnectSession(sessionTopic)
|
|
}
|
|
|
|
function disconnectDapp(url) {
|
|
wcSDK.getActiveSessions((allSessionsAllProfiles) => {
|
|
const sessions = DAppsHelpers.filterActiveSessionsForKnownAccounts(allSessionsAllProfiles, validAccounts)
|
|
let dappFoundInWcSessions = false
|
|
for (const sessionID in sessions) {
|
|
const session = sessions[sessionID]
|
|
const accountsInSession = DAppsHelpers.getAccountsInSession(session)
|
|
const dapp = session.peer.metadata
|
|
const topic = session.topic
|
|
if (dapp.url === url) {
|
|
if (!dappsProvider.selectedAddress ||
|
|
(accountsInSession.includes(dappsProvider.selectedAddress)))
|
|
{
|
|
dappFoundInWcSessions = true
|
|
wcSDK.disconnectSession(topic)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: #16044 - Refactor Wallet connect service to handle multiple SDKs
|
|
if (!dappFoundInWcSessions) {
|
|
// Revoke browser plugin session
|
|
root.revokeSession(url)
|
|
d.notifyDappDisconnect(url, false)
|
|
}
|
|
});
|
|
}
|
|
|
|
function getDApp(dAppUrl) {
|
|
return ModelUtils.getByKey(dappsModel, "url", dAppUrl);
|
|
}
|
|
|
|
signal connectDApp(var dappChains, var sessionProposal, var approvedNamespaces)
|
|
signal approveSessionResult(var session, var error)
|
|
signal sessionRequest(SessionRequestResolved request)
|
|
signal displayToastMessage(string message, bool error)
|
|
// Emitted as a response to WalletConnectService.validatePairingUri or other WalletConnectService.pair
|
|
// and WalletConnectService.approvePair errors
|
|
signal pairingValidated(int validationState)
|
|
|
|
signal revokeSession(string dAppUrl)
|
|
|
|
readonly property Connections sdkConnections: Connections {
|
|
target: wcSDK
|
|
|
|
function onPairResponse(ok) {
|
|
if (!ok) {
|
|
d.reportPairErrorState(Pairing.errors.unknownError)
|
|
} // else waiting for onSessionProposal
|
|
}
|
|
|
|
function onSessionProposal(sessionProposal) {
|
|
d.currentSessionProposal = sessionProposal
|
|
|
|
const supportedNamespacesStr = DAppsHelpers.buildSupportedNamespacesFromModels(
|
|
root.flatNetworks, root.validAccounts, SessionRequest.getSupportedMethods())
|
|
wcSDK.buildApprovedNamespaces(sessionProposal.params, JSON.parse(supportedNamespacesStr))
|
|
}
|
|
|
|
function onBuildApprovedNamespacesResult(approvedNamespaces, error) {
|
|
if(error || !approvedNamespaces) {
|
|
// Check that it contains Non conforming namespaces"
|
|
if (error.includes("Non conforming namespaces")) {
|
|
d.reportPairErrorState(Pairing.errors.unsupportedNetwork)
|
|
} else {
|
|
d.reportPairErrorState(Pairing.errors.unknownError)
|
|
}
|
|
return
|
|
}
|
|
const an = approvedNamespaces.eip155
|
|
if (!(an.accounts) || an.accounts.length === 0 || (!(an.chains) || an.chains.length === 0)) {
|
|
d.reportPairErrorState(Pairing.errors.unsupportedNetwork)
|
|
return
|
|
}
|
|
|
|
if (d.acceptedSessionProposal) {
|
|
wcSDK.approveSession(d.acceptedSessionProposal, approvedNamespaces)
|
|
} else {
|
|
const res = DAppsHelpers.extractChainsAndAccountsFromApprovedNamespaces(approvedNamespaces)
|
|
|
|
root.connectDApp(res.chains, d.currentSessionProposal, approvedNamespaces)
|
|
}
|
|
}
|
|
|
|
function onApproveSessionResult(session, err) {
|
|
if (err) {
|
|
d.reportPairErrorState(Pairing.errors.unknownError)
|
|
return
|
|
}
|
|
|
|
// TODO #14754: implement custom dApp notification
|
|
const app_url = d.currentSessionProposal ? d.currentSessionProposal.params.proposer.metadata.url : "-"
|
|
const app_domain = StringUtils.extractDomainFromLink(app_url)
|
|
root.displayToastMessage(qsTr("Connected to %1 via WalletConnect").arg(app_domain), false)
|
|
|
|
// Persist session
|
|
if(!store.addWalletConnectSession(JSON.stringify(session))) {
|
|
console.error("Failed to persist session")
|
|
}
|
|
|
|
// Notify client
|
|
root.approveSessionResult(session, err)
|
|
|
|
dappsProvider.updateDapps()
|
|
}
|
|
|
|
function onRejectSessionResult(err) {
|
|
const app_url = d.currentSessionProposal ? d.currentSessionProposal.params.proposer.metadata.url : "-"
|
|
const app_domain = StringUtils.extractDomainFromLink(app_url)
|
|
if(err) {
|
|
d.reportPairErrorState(Pairing.errors.unknownError)
|
|
root.displayToastMessage(qsTr("Failed to reject connection request for %1").arg(app_domain), true)
|
|
} else {
|
|
root.displayToastMessage(qsTr("Connection request for %1 was rejected").arg(app_domain), false)
|
|
}
|
|
}
|
|
|
|
function onSessionDelete(topic, err) {
|
|
d.disconnectSessionRequested(topic, err)
|
|
}
|
|
}
|
|
|
|
QObject {
|
|
id: d
|
|
|
|
readonly property var supportedAccountsModel: SortFilterProxyModel {
|
|
sourceModel: root.walletRootStore.nonWatchAccounts
|
|
}
|
|
|
|
property var currentSessionProposal: null
|
|
property var acceptedSessionProposal: null
|
|
|
|
function reportPairErrorState(state) {
|
|
timeoutTimer.stop()
|
|
root.pairingValidated(state)
|
|
}
|
|
|
|
function disconnectSessionRequested(topic, err) {
|
|
// Get all sessions and filter the active ones for known accounts
|
|
// Act on the first matching session with the same topic
|
|
const activeSessionsCallback = (allSessions, success) => {
|
|
store.activeSessionsReceived.disconnect(activeSessionsCallback)
|
|
|
|
if (!success) {
|
|
// TODO #14754: implement custom dApp notification
|
|
d.notifyDappDisconnect("-", true)
|
|
return
|
|
}
|
|
|
|
// Convert to original format
|
|
const webSdkSessions = allSessions.map((session) => {
|
|
return JSON.parse(session.sessionJson)
|
|
})
|
|
|
|
const sessions = DAppsHelpers.filterActiveSessionsForKnownAccounts(webSdkSessions, root.validAccounts)
|
|
|
|
for (const sessionID in sessions) {
|
|
const session = sessions[sessionID]
|
|
if (session.topic === topic) {
|
|
store.deactivateWalletConnectSession(topic)
|
|
dappsProvider.updateDapps()
|
|
|
|
const dappUrl = session.peer.metadata.url ?? "-"
|
|
d.notifyDappDisconnect(dappUrl, err)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
store.activeSessionsReceived.connect(activeSessionsCallback)
|
|
if (!store.getActiveSessions()) {
|
|
store.activeSessionsReceived.disconnect(activeSessionsCallback)
|
|
// TODO #14754: implement custom dApp notification
|
|
}
|
|
}
|
|
|
|
function notifyDappDisconnect(dappUrl, err) {
|
|
const appDomain = StringUtils.extractDomainFromLink(dappUrl)
|
|
if(err) {
|
|
root.displayToastMessage(qsTr("Failed to disconnect from %1").arg(appDomain), true)
|
|
} else {
|
|
root.displayToastMessage(qsTr("Disconnected from %1").arg(appDomain), false)
|
|
}
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
dappsProvider.updateDapps()
|
|
}
|
|
|
|
DAppsRequestHandler {
|
|
id: requestHandler
|
|
|
|
sdk: root.wcSDK
|
|
store: root.store
|
|
accountsModel: root.validAccounts
|
|
networksModel: root.flatNetworks
|
|
currenciesStore: root.walletRootStore.currencyStore
|
|
assetsStore: root.walletRootStore.walletAssetsStore
|
|
|
|
onSessionRequest: (request) => {
|
|
timeoutTimer.stop()
|
|
root.sessionRequest(request)
|
|
}
|
|
onDisplayToastMessage: (message, error) => {
|
|
root.displayToastMessage(message, error)
|
|
}
|
|
}
|
|
|
|
DAppsListProvider {
|
|
id: dappsProvider
|
|
|
|
sdk: root.wcSDK
|
|
store: root.store
|
|
supportedAccountsModel: d.supportedAccountsModel
|
|
selectedAddress: root.walletRootStore.selectedAddress
|
|
}
|
|
|
|
ConnectorDAppsListProvider {
|
|
id: connectorDAppsProvider
|
|
}
|
|
|
|
// Timeout for the corner case where the URL was already dismissed and the SDK doesn't respond with an error nor advances with the proposal
|
|
Timer {
|
|
id: timeoutTimer
|
|
|
|
interval: 10000 // (10 seconds)
|
|
running: false
|
|
repeat: false
|
|
|
|
onTriggered: {
|
|
d.reportPairErrorState(Pairing.errors.unknownError)
|
|
}
|
|
}
|
|
}
|