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) } } }