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

352 lines
14 KiB
QML
Raw Normal View History

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 || !approvedNamespaces.eip155) {
if (!approvedNamespaces.eip155 || 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
}
}
}