feat(WC): Handle unavailable chains or internet connection

This commits implements the `connect` `disconnect` session events for WC and also disables primary buttons for WC whenever there is no connection to internet or chains.

+ update tests
This commit is contained in:
Alex Jbanca 2024-10-23 15:58:45 +03:00 committed by Alex Jbanca
parent 519bfaedfa
commit cb772dc6b5
13 changed files with 289 additions and 12 deletions

View File

@ -72,6 +72,7 @@ Item {
accountsModel: wcService.validAccounts accountsModel: wcService.validAccounts
networksModel: wcService.flatNetworks networksModel: wcService.flatNetworks
sessionRequestsModel: wcService.sessionRequestsModel sessionRequestsModel: wcService.sessionRequestsModel
enabled: wcService.isServiceOnline
//formatBigNumber: (number, symbol, noSymbolOption) => wcService.walletRootStore.currencyStore.formatBigNumber(number, symbol, noSymbolOption) //formatBigNumber: (number, symbol, noSymbolOption) => wcService.walletRootStore.currencyStore.formatBigNumber(number, symbol, noSymbolOption)
@ -200,6 +201,13 @@ Item {
} }
} }
StatusBaseText { text: "Networks Down" }
NetworkFilter {
id: networkFilter
flatNetworks: walletConnectService.walletRootStore.filteredFlatModel
}
// spacer // spacer
ColumnLayout {} ColumnLayout {}
@ -337,6 +345,8 @@ Item {
projectId: projectIdText.projectId projectId: projectIdText.projectId
} }
blockchainNetworksDown: networkFilter.selection
store: SharedStores.DAppsStore { store: SharedStores.DAppsStore {
signal dappsListReceived(string dappsJson) signal dappsListReceived(string dappsJson)
signal userAuthenticated(string topic, string id, string password, string pin) signal userAuthenticated(string topic, string id, string password, string pin)

View File

@ -121,6 +121,7 @@ Item {
onPairingValidated: function(validationState) { onPairingValidated: function(validationState) {
onPairingValidatedTriggers.push({validationState}) onPairingValidatedTriggers.push({validationState})
} }
blockchainNetworksDown: []
} }
} }
@ -221,6 +222,33 @@ Item {
} }
} }
readonly property ListModel filteredFlatModelWithOnlineStat: ListModel {
ListElement {
chainId: 1
layer: 1
isOnline: true
}
ListElement {
chainId: 2
chainName: "Test Chain"
iconUrl: "network/Network=Ethereum"
layer: 2
isOnline: true
}
// Used by tst_balanceCheck
ListElement {
chainId: 11155111
layer: 1
isOnline: true
}
// Used by tst_balanceCheck
ListElement {
chainId: 421613
layer: 2
isOnline: true
}
}
readonly property ListModel nonWatchAccounts: ListModel { readonly property ListModel nonWatchAccounts: ListModel {
ListElement { ListElement {
address: "0x1" address: "0x1"
@ -299,9 +327,10 @@ Item {
sdk: sdk, sdk: sdk,
store: store, store: store,
accountsModel: walletStore.nonWatchAccounts, accountsModel: walletStore.nonWatchAccounts,
networksModel: walletStore.filteredFlatModel networksModel: walletStore.filteredFlatModelWithOnlineStat
}) })
verify(!!handler) verify(!!handler)
sdk.getActiveSessionsCallbacks = []
} }
function cleanup() { function cleanup() {
@ -791,6 +820,7 @@ Item {
verify(!!walletStore) verify(!!walletStore)
provider = createTemporaryObject(dappsListProviderComponent, root, {sdk: sdk, store: store, supportedAccountsModel: walletStore.nonWatchAccounts}) provider = createTemporaryObject(dappsListProviderComponent, root, {sdk: sdk, store: store, supportedAccountsModel: walletStore.nonWatchAccounts})
verify(!!provider) verify(!!provider)
sdk.getActiveSessionsCallbacks = []
} }
function cleanup() { function cleanup() {

View File

@ -0,0 +1,56 @@
import QtQuick 2.15
import StatusQ 0.1
import StatusQ.Core.Utils 0.1
// This component receives the networksModel
// and breaks down the online status of each chain
QObject {
id: root
// Required roleNames: chainId, isOnline
required property var networksModel
readonly property bool allOffline: d.allOffline
readonly property bool allOnline: d.allOnline
property bool active: true
signal chainOnlineChanged(int chainId, bool isOnline)
// Network online observer
// `chainOnlineChanged` signal when the online status of a chain changes
Instantiator {
id: networkOnlineObserver
model: root.networksModel
active: root.active
delegate: QtObject {
required property var model
property var /*var intended*/ isOnline: model.isOnline
onIsOnlineChanged: {
root.chainOnlineChanged(model.chainId, isOnline)
}
}
}
// Aggregator to count the number of online chains
SumAggregator{
id: aggregator
model: root.active ? root.networksModel : null
roleName: "isOnline"
}
// Network checker to check if the device is online
NetworkChecker {
id: networkChecker
active: root.active
}
QtObject {
id: d
readonly property bool allOffline: !networkChecker.isOnline || aggregator.value === 0
readonly property bool allOnline: networkChecker.isOnline &&
aggregator.value > 0 &&
aggregator.value === networksModel.ModelCount.count
}
}

View File

@ -2,3 +2,4 @@ ChartDataBase 1.0 ChartDataBase.qml
TokenBalanceHistoryData 1.0 TokenBalanceHistoryData.qml TokenBalanceHistoryData 1.0 TokenBalanceHistoryData.qml
TokenMarketValuesData 1.0 TokenMarketValuesData.qml TokenMarketValuesData 1.0 TokenMarketValuesData.qml
singleton NetworkModelHelpers 1.0 NetworkModelHelpers.qml singleton NetworkModelHelpers 1.0 NetworkModelHelpers.qml
ChainsAvailabilityWatchdog 1.0 ChainsAvailabilityWatchdog.qml

View File

@ -146,7 +146,7 @@ Item {
visible: !root.walletStore.showSavedAddresses visible: !root.walletStore.showSavedAddresses
&& root.dappsEnabled && root.dappsEnabled
&& wcService.serviceAvailableToCurrentAddress && wcService.serviceAvailableToCurrentAddress
enabled: !!Global.walletConnectService enabled: !!wcService && wcService.isServiceOnline
loginType: root.loginType loginType: root.loginType

View File

@ -16,11 +16,13 @@ SQUtils.QObject {
required property WalletConnectSDKBase sdk required property WalletConnectSDKBase sdk
required property DAppsStore store required property DAppsStore store
required property var accountsModel required property var accountsModel
// Required roles: chainId, layer, isOnline
required property var networksModel required property var networksModel
required property CurrenciesStore currenciesStore required property CurrenciesStore currenciesStore
required property WalletStore.WalletAssetsStore assetsStore required property WalletStore.WalletAssetsStore assetsStore
property alias requestsModel: requests property alias requestsModel: requests
readonly property bool isServiceOnline: chainsSupervisorPlugin.anyChainAvailable && sdk.sdkReady
function subscribeForFeeUpdates(topic, id) { function subscribeForFeeUpdates(topic, id) {
d.subscribeForFeeUpdates(topic, id) d.subscribeForFeeUpdates(topic, id)
@ -1175,4 +1177,11 @@ SQUtils.QObject {
} }
} }
} }
ChainsSupervisorPlugin {
id: chainsSupervisorPlugin
sdk: root.sdk
networksModel: root.networksModel
}
} }

View File

@ -364,14 +364,6 @@ WalletConnectSDKBase {
` `
) )
} }
function connected() {
console.debug(`WC WalletConnectSDK.wcCall.connected;`)
}
function disconnected() {
console.debug(`WC WalletConnectSDK.wcCall.disconnected;`)
}
} }
QtObject { QtObject {

View File

@ -32,6 +32,8 @@ QObject {
required property WalletConnectSDKBase wcSDK required property WalletConnectSDKBase wcSDK
required property DAppsStore store required property DAppsStore store
required property var walletRootStore required property var walletRootStore
// // Array[chainId] of the networks that are down
required property var blockchainNetworksDown
//output properties //output properties
/// Model contaning all dApps available for the currently selected account /// Model contaning all dApps available for the currently selected account
@ -49,6 +51,8 @@ QObject {
/// TODO: refactor /// TODO: refactor
readonly property alias connectorDAppsProvider: connectorDAppsProvider readonly property alias connectorDAppsProvider: connectorDAppsProvider
readonly property bool isServiceOnline: requestHandler.isServiceOnline
// methods // methods
/// Triggers the signing process for the given session request /// Triggers the signing process for the given session request
/// @param topic The topic of the session /// @param topic The topic of the session
@ -250,7 +254,16 @@ QObject {
sdk: root.wcSDK sdk: root.wcSDK
store: root.store store: root.store
accountsModel: root.validAccounts accountsModel: root.validAccounts
networksModel: root.flatNetworks networksModel: SortFilterProxyModel {
sourceModel: root.flatNetworks
proxyRoles: [
FastExpressionRole {
name: "isOnline"
expression: !root.blockchainNetworksDown.map(Number).includes(model.chainId)
expectedRoles: "chainId"
}
]
}
currenciesStore: root.walletRootStore.currencyStore currenciesStore: root.walletRootStore.currencyStore
assetsStore: root.walletRootStore.walletAssetsStore assetsStore: root.walletRootStore.walletAssetsStore

View File

@ -0,0 +1,150 @@
import QtQuick 2.15
import AppLayouts.Wallet.helpers 1.0
import AppLayouts.Wallet.services.dapps 1.0
import AppLayouts.Wallet.services.dapps.types 1.0 as DAppsTypes
import StatusQ 0.1
import StatusQ.Core.Utils 0.1
// This plugin handles the chain ability for the dapps
// It monitors the chain availability and updates the dapps accordingly
// When all chains are offline, it will inform each session that the chains are offline
// When a chain comes back online, it will check if there are any active sessions and inform them
// See [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193)
QObject {
id: root
required property WalletConnectSDKBase sdk
// Required roles: chainId, isOnline
required property var networksModel
// Happens when any chain is available
readonly property bool anyChainAvailable: !chainsAvailabilityWatchdog.allOffline
ChainsAvailabilityWatchdog {
id: chainsAvailabilityWatchdog
// property used for spam protection
property var lastNotifiedCStatuses: new Map()
networksModel: root.networksModel
onChainOnlineChanged: (chainId, isOnline) => {
if (!isOnline || !sdk.sdkReady) {
return
}
// Spam protection. Spamming the SDK could result to blocks
if (lastNotifiedCStatuses.has(chainId) && lastNotifiedCStatuses.get(chainId) === isOnline) {
return
}
lastNotifiedCStatuses.set(chainId, isOnline)
d.notifyChainConnected(chainId)
}
onAllOfflineChanged: {
if(!sdk.sdkReady) {
return
}
if (allOffline) {
lastNotifiedCStatuses.clear()
d.notifyAllChainsDisconnected()
}
}
}
QtObject {
id: d
function sessionHasEventSupport(session, event) {
try {
if (!event) {
return false
}
if (session.namespaces.eip155.events.includes(event)) {
return true
}
return false
} catch (e) {
console.error("Error checking sessionHasEventSupport: ", e)
return false
}
}
function sessionHasChainSupport(session, chainId) {
try {
if (!chainId) {
return false
}
let chainIds = session.namespaces.eip155.chains.map(DAppsHelpers.chainIdFromEip155);
if (!chainIds.includes(chainId)) {
return false
}
let accounts = session.namespaces.eip155.accounts
let chainAccounts = accounts.map((account) => account.startsWith(`eip155:${chainId}`))
if (!chainAccounts.includes(true)) {
return false
}
return true
} catch (e) {
console.error("Error checking sessionHasChainSupport: ", e)
return false
}
}
function notifyAllChainsDisconnected() {
sdk.getActiveSessions((allSessions) => {
Object.values(allSessions).forEach((session) => {
if (!d.sessionHasEventSupport(session, "disconnect")) {
return
}
try {
session.namespaces.eip155.chains.forEach((chainId) => {
const chainInt = DAppsHelpers.chainIdFromEip155(chainId)
if (!d.sessionHasChainSupport(session, chainInt)) {
return
}
sdk.emitSessionEvent(session.topic, {
name: "disconnect",
data: {
code: DAppsTypes.ErrorCodes.rpcErrors.disconnected,
}
}, chainId)
})
} catch (e) {
console.error("Error emitting session event: ", e)
}
})
})
}
function notifyChainConnected(chainId) {
sdk.getActiveSessions((sessions) => {
Object.values(sessions).forEach((session) => {
if (!d.sessionHasEventSupport(session, "connect")) {
return
}
if (!d.sessionHasChainSupport(session, chainId)) {
return
}
const hexChain = `0x${chainId.toString(16)}`
sdk.emitSessionEvent(session.topic, {
name: "connect",
data: {
chainId: hexChain,
}
}, `eip155:${chainId}`)
})
})
}
}
}

View File

@ -1,2 +1,3 @@
ChainsSupervisorPlugin 1.0 ChainsSupervisorPlugin.qml
SiweRequestPlugin 1.0 SiweRequestPlugin.qml SiweRequestPlugin 1.0 SiweRequestPlugin.qml
SiweLifeCycle 1.0 SiweLifeCycle.qml SiweLifeCycle 1.0 SiweLifeCycle.qml

View File

@ -0,0 +1,13 @@
pragma Singleton
import QtQuick 2.15
QtObject {
// Maps to https://eips.ethereum.org/EIPS/eip-1193#rpc-errors
readonly property QtObject rpcErrors: QtObject {
readonly property int userRejectedRequest: 4001
readonly property int unauthorized: 4100
readonly property int unsupportedMethod: 4200
readonly property int disconnected: 4900
readonly property int chainDisconnected: 4901
}
}

View File

@ -3,3 +3,4 @@ SessionRequestWithAuth 1.0 SessionRequestWithAuth.qml
SessionRequestsModel 1.0 SessionRequestsModel.qml SessionRequestsModel 1.0 SessionRequestsModel.qml
singleton SessionRequest 1.0 SessionRequest.qml singleton SessionRequest 1.0 SessionRequest.qml
singleton Pairing 1.0 Pairing.qml singleton Pairing 1.0 Pairing.qml
singleton ErrorCodes 1.0 ErrorCodes.qml

View File

@ -2212,6 +2212,7 @@ Item {
controller: WalletStores.RootStore.walletConnectController controller: WalletStores.RootStore.walletConnectController
} }
walletRootStore: WalletStores.RootStore walletRootStore: WalletStores.RootStore
blockchainNetworksDown: appMain.networkConnectionStore.blockchainNetworksDown
Component.onCompleted: { Component.onCompleted: {
Global.walletConnectService = walletConnectService Global.walletConnectService = walletConnectService