Alex Jbanca 106988d534 fix(WC): Refactor dapps service to work with multiple SDKs
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.
2024-11-20 18:10:29 +02:00

310 lines
11 KiB
QML

import QtQuick 2.15
import AppLayouts.Wallet.services.dapps 1.0
import AppLayouts.Wallet.services.dapps.plugins 1.0
import AppLayouts.Wallet.services.dapps.types 1.0
import AppLayouts.Wallet.stores 1.0 as WalletStore
import StatusQ.Core.Utils 0.1 as SQUtils
import shared.stores 1.0
import utils 1.0
/// Component that provides the dapps integration for the wallet.
/// It provides the following features:
/// - WalletConnect integration
/// - WalletConnect pairing
/// - WalletConnect sessions management
/// - WalletConnect signing requests
/// - WalletConnect SIWE
/// - WalletConnect online status
/// - BrowserConnect integration
/// - BrowserConnect pairing
/// - BrowserConnect - access to persistent sessions
/// - BrowserConnect - access to persistent signing requests
/// - BrowserConnect signing requests
/// - BrowserConnect online status
SQUtils.QObject {
id: root
// SDKs providing the DApps API
required property WalletConnectSDKBase wcSdk
required property WalletConnectSDKBase bcSdk
// DApps shared store - used for wc peristence and signing requests/transactions
required property DAppsStore store
required property CurrenciesStore currenciesStore
// Required roles: address
required property var accountsModel
// Required roles: chainId, layer, isOnline
required property var networksModel
// Required roles: tokenKey, balances
required property var groupedAccountAssetsModel
readonly property alias requestsModel: requests
readonly property alias dappsModel: dappConnections.dappsModel
readonly property bool enabled: wcSdk.enabled || bcSdk.enabled
readonly property bool isServiceOnline: chainsSupervisorPlugin.anyChainAvailable && (wcSdk.sdkReady || bcSdk.enabled)
// Connection signals
/// Emitted when a new DApp requests a connection
signal connectDApp(var chains, string dAppUrl, string dAppName, string dAppIcon, string key)
/// Emitted when a new DApp is connected
signal dappConnected(string proposalId, string newTopic, string url, int connectorId)
/// Emitted when a DApp is disconnected
signal dappDisconnected(string topic, string url)
/// Emitted when a new DApp fails to connect
signal newConnectionFailed(string key, string dappUrl, var error)
// Pairing signals
signal pairingValidated(int validationState)
signal pairingResponse(int state) // Maps to Pairing.errors
// Sign request signals
signal signCompleted(string topic, string id, bool userAccepted, string error)
signal siweCompleted(string topic, string id, string error)
/// WalletConnect pairing
/// @param uri - the pairing URI to pair
/// Result is emitted via the pairingResponse signal
/// A new session proposal is expected to be emitted if the pairing is successful
function pair(uri) {
return wcSdk.pair(uri)
}
/// Approves or rejects the session proposal. App response to `connectDApp`
/// @param key - the key of the session proposal
/// @param approvedChainIds - array containing the chainIds that the user approved
/// @param accountAddress - the address of the account that approved the session
function approvePairSession(key, approvedChainIds, accountAddress) {
if (siwePlugin.connectionRequests.has(key.toString())) {
siwePlugin.accept(key, approvedChainIds, accountAddress)
siwePlugin.connectionRequests.delete(key.toString())
return
}
dappConnections.connect(key, approvedChainIds, accountAddress)
}
/// Rejects the session proposal. App response to `connectDApp`
/// @param id - the id of the session proposal
function rejectPairSession(id) {
if (siwePlugin.connectionRequests.has(id.toString())) {
siwePlugin.reject(id)
siwePlugin.connectionRequests.delete(id.toString())
return
}
dappConnections.reject(id)
}
/// Disconnects the WC session with the given topic. Expected `dappDisconnected` signal
/// @param sessionTopic - the topic of the session to disconnect
function disconnectSession(sessionTopic) {
dappConnections.disconnect(sessionTopic)
}
/// Validates the pairing URI and emits the pairingValidated signal. Expected `pairingValidated` signal
/// Async function
/// @param uri - the pairing URI to validate
function validatePairingUri(uri){
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)
});
}
/// Returns the DApp with the given topic
/// @param topic - the topic of the DApp to return
/// @return the DApp with the given topic
/// DApp {
/// name: string
/// url: string
/// iconUrl: string
/// topic: string
/// connectorId: int
/// accountAddressses: [{address: string}]
/// chains: string
/// rawSessions: [{session: object}]
/// }
function getDApp(topic) {
return SQUtils.ModelUtils.getFirstModelEntryIf(dappsModel, (dapp) => {
return dapp.topic === topic
SQUtils.ModelUtils.getFirstModelEntryIf(dapp.rawSessions, (session) => {
return session.topic === topic
})
})
}
DAppConnectionsPlugin {
id: dappConnections
wcSDK: root.wcSdk
bcSDK: root.bcSdk
dappsStore: root.store
accountsModel: root.accountsModel
networksModel: root.networksModel
onConnected: (proposalId, topic, url, connectorId) => {
root.dappConnected(proposalId, topic, url, connectorId)
}
onDisconnected: (topic, url) => {
root.dappDisconnected(topic, url)
}
onNewConnectionProposed: (key, chains, dAppUrl, dAppName, dAppIcon) => {
root.connectDApp(chains, dAppUrl, dAppName, dAppIcon, key)
}
onNewConnectionFailed: (key, dappUrl, error) => {
root.newConnectionFailed(key, dappUrl, error)
}
}
SessionRequestsModel {
id: requests
}
ChainsSupervisorPlugin {
id: chainsSupervisorPlugin
sdk: root.wcSdk
networksModel: root.networksModel
}
Connections {
target: root.wcSdk
enabled: root.wcSdk.enabled
function onPairResponse(ok) {
root.pairingResponse(ok)
}
}
SiweRequestPlugin {
id: siwePlugin
readonly property var connectionRequests: new Map()
sdk: root.wcSdk
store: root.store
accountsModel: root.accountsModel
networksModel: root.networksModel
onRegisterSignRequest: (request) => {
requests.enqueue(request)
}
onUnregisterSignRequest: (requestId) => {
const request = requests.findById(requestId)
if (request === null) {
console.error("SiweRequestPlugin::onUnregisterSignRequest: Error finding event for requestId", requestId)
return
}
requests.removeRequest(request.topic, requestId)
}
onConnectDApp: (chains, dAppUrl, dAppName, dAppIcon, key) => {
siwePlugin.connectionRequests.set(key.toString(), {chains, dAppUrl, dAppName, dAppIcon})
root.connectDApp(chains, dAppUrl, dAppName, dAppIcon, key)
}
onSiweFailed: (id, error, topic) => {
root.siweCompleted(topic, id, error)
}
onSiweSuccessful: (id, topic) => {
d.lookupSession(topic, function(session) {
// Persist session
if(!root.store.addWalletConnectSession(JSON.stringify(session))) {
console.error("Failed to persist session")
}
root.siweCompleted(topic, id, "")
})
}
function accept(key, approvedChainIds, accountAddress) {
const approvedNamespaces = JSON.parse(
DAppsHelpers.buildSupportedNamespaces(approvedChainIds,
[accountAddress],
SessionRequest.getSupportedMethods()))
siwePlugin.connectionApproved(key, approvedNamespaces)
}
function reject(key) {
siwePlugin.connectionRejected(key)
}
}
SQUtils.QObject {
id: d
function lookupSession(topicToLookup, callback) {
wcSdk.getActiveSessions((res) => {
Object.keys(res).forEach((topic) => {
if (topic === topicToLookup) {
let session = res[topic]
callback(session)
}
})
})
}
}
// bcSignRequestPlugin and wcSignRequestPlugin are used to handle sign requests
// Almost identical, and it's worth extracting in an inline component, but Qt5.15.2 doesn't support it
SignRequestPlugin {
id: bcSignRequestPlugin
sdk: root.bcSdk
groupedAccountAssetsModel: root.groupedAccountAssetsModel
networksModel: root.networksModel
accountsModel: root.accountsModel
currentCurrency: root.currenciesStore.currentCurrency
store: root.store
requests: root.requestsModel
dappsModel: root.dappsModel
getFiatValue: (value, currency) => {
return root.currenciesStore.getFiatValue(value, currency)
}
onSignCompleted: (topic, id, userAccepted, error) => {
root.signCompleted(topic, id, userAccepted, error)
}
}
SignRequestPlugin {
id: wcSignRequestPlugin
sdk: root.wcSdk
groupedAccountAssetsModel: root.groupedAccountAssetsModel
networksModel: root.networksModel
accountsModel: root.accountsModel
currentCurrency: root.currenciesStore.currentCurrency
store: root.store
requests: root.requestsModel
dappsModel: root.dappsModel
getFiatValue: (value, currency) => {
return root.currenciesStore.getFiatValue(value, currency)
}
onSignCompleted: (topic, id, userAccepted, error) => {
root.signCompleted(topic, id, userAccepted, error)
}
}
}