status-desktop/ui/app/AppLayouts/Wallet/services/dapps/plugins/DAppConnectionsPlugin.qml

353 lines
14 KiB
QML

import QtQuick 2.15
import StatusQ 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import AppLayouts.Wallet.services.dapps 1.0
import AppLayouts.Wallet.services.dapps.types 1.0
import shared.stores 1.0
import utils 1.0
// Plugin providing the connection handling for dApps
// Features provided:
// 1. connect
// 2. disconnect
// 3. active connections model
SQUtils.QObject {
id: root
required property WalletConnectSDKBase wcSDK
required property WalletConnectSDKBase bcSDK
required property DAppsStore dappsStore
// Required roles: address
required property var accountsModel
// Required roles: chainId
required property var networksModel
// Output model with the following roles:
// - name: string (optional)
// - url: string (required)
// - iconUrl: string (optional)
// - topic: string (required)
// - connectorId: int (required)
// - accountAddressses: [{address: string}] (required)
// - chains: string (optional)
// - rawSessions: [{session: object}] (optional)
readonly property ConcatModel dappsModel: dappsModel
// Output signal when a dApp is disconnected
signal disconnected(string topic, string dAppUrl)
// Output signal when a new connection is proposed
signal connected(string proposalId, string topic, string dAppUrl, int connectorId)
// Output signal when a new connection is proposed by the SDK
signal newConnectionProposed(string key, var chains, string dAppUrl, string dAppName, string dAppIcon)
// Output signal when a new connection is failed
signal newConnectionFailed(string key, string dappUrl, int errorCode)
// Request to disconnect a dApp identified by the topic
function disconnect(topic) {
d.disconnect(topic)
}
// Request to connect a dApp identified by the proposal key
// chains: array of chainIds
// accountAddress: string
function connect(key, chains, accoutAddress) {
d.connect(key, chains, accoutAddress)
}
// Request to reject a dApp connection request identified by the proposal key
function reject(key) {
d.reject(key)
}
WCDappsProvider {
id: dappsProvider
sdk: root.wcSDK
store: root.dappsStore
supportedAccountsModel: root.accountsModel
onConnected: (proposalId, topic, dappUrl) => {
root.connected(proposalId, topic, dappUrl, Constants.WalletConnect)
}
onDisconnected: (topic, dappUrl) => {
root.disconnected(topic, dappUrl)
}
}
BCDappsProvider {
id: connectorDAppsProvider
bcSDK: root.bcSDK
onConnected: (pairingId, topic, dappUrl) => {
root.connected(pairingId, topic, dappUrl, Constants.StatusConnect)
}
onDisconnected: (topic, dappUrl) => {
root.disconnected(topic, dappUrl)
}
}
ConcatModel {
id: dappsModel
markerRoleName: "source"
sources: [
SourceModel {
model: dappsProvider.model
markerRoleValue: "walletConnect"
},
SourceModel {
model: connectorDAppsProvider.model
markerRoleValue: "statusConnect"
}
]
}
// These two objects don't share a common interface because Qt5.15.2 would freeze for some reason
QtObject {
id: bcConnectionPromise
function resolve(context, key, approvedChainIds, accountAddress) {
root.bcSDK.approveSession(key, accountAddress, approvedChainIds)
}
function reject(context, key) {
root.bcSDK.rejectSession(key)
}
}
QtObject {
id: wcConnectionPromise
function resolve(context, key, approvedChainIds, accountAddress) {
const approvedNamespaces = JSON.parse(
DAppsHelpers.buildSupportedNamespaces(approvedChainIds,
[accountAddress],
SessionRequest.getSupportedMethods()))
d.acceptedSessionProposal = context
d.acceptedNamespaces = approvedNamespaces
root.wcSDK.buildApprovedNamespaces(key, context.params, approvedNamespaces)
}
function reject(context, key) {
root.wcSDK.rejectSession(key)
}
}
// Flow for BrowserConnect
// 1. onSessionProposal -> new connection proposal received
// 3. onApproveSessionResult -> session approve result
// 4. onRejectSessionResult -> session reject result
Connections {
target: root.bcSDK
function onSessionProposal(sessionProposal) {
const key = sessionProposal.id
d.activeProposals.set(key.toString(), { context: sessionProposal, promise: bcConnectionPromise })
root.newConnectionProposed(key, [1], sessionProposal.params.proposer.metadata.url, sessionProposal.params.proposer.metadata.name, sessionProposal.params.proposer.metadata.icons[0])
}
function onApproveSessionResult(proposalId, session, err) {
if (!d.activeProposals.has(proposalId.toString())) {
console.error("No active proposal found for key: " + proposalId)
return
}
const dappUrl = d.activeProposals.get(proposalId.toString()).context.params.proposer.metadata.url
d.activeProposals.delete(proposalId.toString())
if (err) {
root.newConnectionFailed(proposalId, dappUrl, Pairing.errors.unknownError)
return
}
}
function onRejectSessionResult(proposalId, err) {
if (!d.activeProposals.has(proposalId.toString())) {
console.error("No active proposal found for key: " + proposalId)
return
}
const dappUrl = d.activeProposals.get(proposalId.toString()).context.params.proposer.metadata.url
d.activeProposals.delete(proposalId.toString())
if (err) {
root.newConnectionFailed(proposalId, dappUrl, Pairing.errors.rejectFailed)
return
}
root.newConnectionFailed(proposalId, dappUrl, Pairing.errors.userRejected)
}
}
// Flow for WalletConnect
// 1. onSessionProposal -> new connection proposal received
// 2. onBuildApprovedNamespacesResult -> get the supported namespaces to be sent for approval
// 3. onApproveSessionResult -> session approve result
// 4. onRejectSessionResult -> session reject result
Connections {
target: root.wcSDK
function onSessionProposal(sessionProposal) {
const key = sessionProposal.id
d.activeProposals.set(key.toString(), { context: sessionProposal, promise: wcConnectionPromise })
const supportedNamespacesStr = DAppsHelpers.buildSupportedNamespacesFromModels(
root.networksModel, root.accountsModel, SessionRequest.getSupportedMethods())
root.wcSDK.buildApprovedNamespaces(key, sessionProposal.params, JSON.parse(supportedNamespacesStr))
}
function onBuildApprovedNamespacesResult(key, approvedNamespaces, error) {
if (!d.activeProposals.has(key.toString())) {
console.error("No active proposal found for key: " + key)
return
}
const proposal = d.activeProposals.get(key.toString()).context
const dAppUrl = proposal.params.proposer.metadata.url
if(error || !approvedNamespaces) {
// Check that it contains Non conforming namespaces"
if (error.includes("Non conforming namespaces")) {
root.newConnectionFailed(proposal.id, dAppUrl, Pairing.errors.unsupportedNetwork)
} else {
root.newConnectionFailed(proposal.id, dAppUrl, Pairing.errors.unknownError)
}
return
}
approvedNamespaces = d.applyChainAgnosticFix(approvedNamespaces)
if (d.acceptedSessionProposal) {
root.wcSDK.approveSession(d.acceptedSessionProposal, approvedNamespaces)
} else {
const res = DAppsHelpers.extractChainsAndAccountsFromApprovedNamespaces(approvedNamespaces)
const chains = res.chains
const dAppName = proposal.params.proposer.metadata.name
const dAppIcons = proposal.params.proposer.metadata.icons
const dAppIcon = dAppIcons && dAppIcons.length > 0 ? dAppIcons[0] : ""
root.newConnectionProposed(key, chains, dAppUrl, dAppName, dAppIcon)
}
}
function onApproveSessionResult(proposalId, session, err) {
if (!d.activeProposals.has(proposalId.toString())) {
console.error("No active proposal found for key: " + proposalId)
return
}
const dappUrl = d.activeProposals.get(proposalId.toString())
.context.params.proposer.metadata.url
d.activeProposals.delete(proposalId.toString())
d.acceptedSessionProposal = null
d.acceptedNamespaces = null
if (err) {
root.newConnectionFailed(proposalId, dappUrl, Pairing.errors.unknownError)
return
}
}
function onRejectSessionResult(proposalId, err) {
if (!d.activeProposals.has(proposalId.toString())) {
console.error("No active proposal found for key: " + proposalId)
return
}
const dappUrl = d.activeProposals.get(proposalId.toString())
.context.params.proposer.metadata.url
d.activeProposals.delete(proposalId.toString())
if (err) {
root.newConnectionFailed(proposalId, dappUrl, Pairing.errors.rejectFailed)
return
}
root.newConnectionFailed(proposalId, dappUrl, Pairing.errors.userRejected)
}
}
QtObject {
id: d
property var activeProposals: new Map()
property var acceptedSessionProposal: null
property var acceptedNamespaces: null
function disconnect(connectionId) {
const dApp = d.getDAppByTopic(connectionId)
if (!dApp) {
console.error("Disconnecting dApp: dApp not found")
return
}
if (!dApp.connectorId == undefined) {
console.error("Disconnecting dApp: connectorId not found")
return
}
const sdk = dApp.connectorId === Constants.WalletConnect ? root.wcSDK : root.bcSDK
sdkDisconnect(dApp, sdk)
}
// Disconnect all sessions for a dApp
function sdkDisconnect(dapp, sdk) {
SQUtils.ModelUtils.forEach(dapp.rawSessions, (session) => {
sdk.disconnectSession(session.topic)
})
}
function reject(key) {
if (!d.activeProposals.has(key.toString())) {
console.error("Rejecting dApp: dApp not found")
return
}
const proposal = d.activeProposals.get(key.toString())
proposal.promise.reject(proposal.context, key)
}
function connect(key, chains, accoutAddress) {
if (!d.activeProposals.has(key.toString())) {
console.error("Connecting dApp: dApp not found", key)
return
}
const proposal = d.activeProposals.get(key.toString())
proposal.promise.resolve(proposal.context, key, chains, accoutAddress)
}
function getDAppByTopic(topic) {
return SQUtils.ModelUtils.getFirstModelEntryIf(dappsModel, (modelItem) => {
if (modelItem.topic == topic) {
return true
}
if (!modelItem.rawSessions) {
return false
}
for (let i = 0; i < modelItem.rawSessions.ModelCount.count; i++) {
if (modelItem.rawSessions.get(i).topic == topic) {
return true
}
}
})
}
//Special case for chain agnostic dapps
//WC considers the approved namespace as valid, but there's no chainId or account established
//Usually this request is declared by using `eip155:0`, but we don't support this chainID, resulting in empty `chains` and `accounts`
//The established connection will use for all user approved chains and accounts
//This fix is applied to all valid namespaces that don't have a chainId or account
function applyChainAgnosticFix(approvedNamespaces) {
try {
const an = approvedNamespaces.eip155
const chainAgnosticRequest = (!an.chains || an.chains.length === 0) && (!an.accounts || an.accounts.length === 0)
if (!chainAgnosticRequest) {
return approvedNamespaces
}
// If the `d.acceptedNamespaces` is set it means the user already confirmed the chain and account
if (!!d.acceptedNamespaces) {
approvedNamespaces.eip155.chains = d.acceptedNamespaces.eip155.chains
approvedNamespaces.eip155.accounts = d.acceptedNamespaces.eip155.accounts
return approvedNamespaces
}
// Show to the user all possible chains
const supportedNamespacesStr = DAppsHelpers.buildSupportedNamespacesFromModels(
root.networksModel, root.accountsModel, SessionRequest.getSupportedMethods())
const supportedNamespaces = JSON.parse(supportedNamespacesStr)
approvedNamespaces.eip155.chains = supportedNamespaces.eip155.chains
approvedNamespaces.eip155.accounts = supportedNamespaces.eip155.accounts
} catch (e) {
console.warn("WC Error applying chain agnostic fix", e)
}
return approvedNamespaces
}
}
}