mirror of
https://github.com/status-im/status-desktop.git
synced 2025-01-27 14:55:44 +00:00
106988d534
This PR is refactoring the dapps service to avoid code duplication between SDKs and also to avoid overlapping requests/responses. It brings Browser Connect inline with Wallet Connect in terms of session management and sign transactions. New architecture: WalletConnectService becomes DAppsService. Its responsibility is to provide dapp access to the app. This is the component currently used by the UI What does it do: 1. Provide dapp APIs line connect, disconnect, session requests etc 2. Spawn app notifications on dapp events 3. Timeout requests if the dapp does not respons DAppsRequestHandler becomes DAppsModule. This component is consumed by the DAppService. Its responsibility is to aggregate all the building blocks for the dapps, but does not control any of the dapp features or consume the SDKs requests. What does it do: 1. Aggregate all the building blocks for dapps (currently known as plugins) DAppConnectionsPlugin - This component provides the session management features line connect, disconnect and provide a model with the connected dapps. SignRequestPlugin - This component provides the sign request management. It receives the sign request from the dapp, translates it to what Status understands and manages the lifecycle of the request.
353 lines
14 KiB
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
|
|
}
|
|
}
|
|
} |