feat(BC): Unify dapp sessions between WalletConnect and BrowserConnect

New component introduced (DAppsModel) to provide a common model for WC and BC. The WCDappsProvider and BCDappsProvider components are responsible to fill the model from different sources
This commit is contained in:
Alex Jbanca 2024-10-30 17:18:24 +02:00
parent 4636f80260
commit 1ecd960cb2
No known key found for this signature in database
GPG Key ID: 6004079575C21C5D
14 changed files with 305 additions and 152 deletions

View File

@ -21,6 +21,8 @@ type ConnectorGrantDAppPermissionSignal* = ref object of Signal
url*: string url*: string
name*: string name*: string
iconUrl*: string iconUrl*: string
chains*: string
sharedAccount*: string
type ConnectorRevokeDAppPermissionSignal* = ref object of Signal type ConnectorRevokeDAppPermissionSignal* = ref object of Signal
url*: string url*: string
@ -48,6 +50,8 @@ proc fromEvent*(T: type ConnectorGrantDAppPermissionSignal, event: JsonNode): Co
result.url = event["event"]{"url"}.getStr() result.url = event["event"]{"url"}.getStr()
result.name = event["event"]{"name"}.getStr() result.name = event["event"]{"name"}.getStr()
result.iconUrl = event["event"]{"iconUrl"}.getStr() result.iconUrl = event["event"]{"iconUrl"}.getStr()
result.chains = $(event["event"]{"chains"})
result.sharedAccount = event["event"]{"sharedAccount"}.getStr()
proc fromEvent*(T: type ConnectorRevokeDAppPermissionSignal, event: JsonNode): ConnectorRevokeDAppPermissionSignal = proc fromEvent*(T: type ConnectorRevokeDAppPermissionSignal, event: JsonNode): ConnectorRevokeDAppPermissionSignal =
result = ConnectorRevokeDAppPermissionSignal() result = ConnectorRevokeDAppPermissionSignal()

View File

@ -26,10 +26,16 @@ QtObject:
proc delete*(self: Controller) = proc delete*(self: Controller) =
self.QObject.delete self.QObject.delete
proc dappRequestsToConnect*(self: Controller, requestId: string, payload: string) {.signal.} proc connectRequested*(self: Controller, requestId: string, payload: string) {.signal.}
proc dappValidatesTransaction*(self: Controller, requestId: string, payload: string) {.signal.} proc connected*(self: Controller, payload: string) {.signal.}
proc dappGrantDAppPermission*(self: Controller, payload: string) {.signal.} proc disconnected*(self: Controller, payload: string) {.signal.}
proc dappRevokeDAppPermission*(self: Controller, payload: string) {.signal.}
proc signRequested*(self: Controller, requestId: string, payload: string) {.signal.}
proc approveConnectResponse*(self: Controller, payload: string, error: bool) {.signal.}
proc rejectConnectResponse*(self: Controller, payload: string, error: bool) {.signal.}
proc approveTransactionResponse*(self: Controller, requestId: string, error: bool) {.signal.}
proc rejectTransactionResponse*(self: Controller, requestId: string, error: bool) {.signal.}
proc newController*(service: connector_service.Service, events: EventEmitter): Controller = proc newController*(service: connector_service.Service, events: EventEmitter): Controller =
new(result, delete) new(result, delete)
@ -51,7 +57,7 @@ QtObject:
"url": params.url, "url": params.url,
} }
controller.dappRequestsToConnect(params.requestId, dappInfo.toJson()) controller.connectRequested(params.requestId, dappInfo.toJson())
result.events.on(SIGNAL_CONNECTOR_EVENT_CONNECTOR_SEND_TRANSACTION) do(e: Args): result.events.on(SIGNAL_CONNECTOR_EVENT_CONNECTOR_SEND_TRANSACTION) do(e: Args):
let params = ConnectorSendTransactionSignal(e) let params = ConnectorSendTransactionSignal(e)
@ -63,7 +69,7 @@ QtObject:
"txArgs": params.txArgs, "txArgs": params.txArgs,
} }
controller.dappValidatesTransaction(params.requestId, dappInfo.toJson()) controller.signRequested(params.requestId, dappInfo.toJson())
result.events.on(SIGNAL_CONNECTOR_GRANT_DAPP_PERMISSION) do(e: Args): result.events.on(SIGNAL_CONNECTOR_GRANT_DAPP_PERMISSION) do(e: Args):
let params = ConnectorGrantDAppPermissionSignal(e) let params = ConnectorGrantDAppPermissionSignal(e)
@ -71,9 +77,11 @@ QtObject:
"icon": params.iconUrl, "icon": params.iconUrl,
"name": params.name, "name": params.name,
"url": params.url, "url": params.url,
"chains": params.chains,
"sharedAccount": params.sharedAccount,
} }
controller.dappGrantDAppPermission(dappInfo.toJson()) controller.connected(dappInfo.toJson())
result.events.on(SIGNAL_CONNECTOR_REVOKE_DAPP_PERMISSION) do(e: Args): result.events.on(SIGNAL_CONNECTOR_REVOKE_DAPP_PERMISSION) do(e: Args):
let params = ConnectorRevokeDAppPermissionSignal(e) let params = ConnectorRevokeDAppPermissionSignal(e)
@ -83,7 +91,7 @@ QtObject:
"url": params.url, "url": params.url,
} }
controller.dappRevokeDAppPermission(dappInfo.toJson()) controller.disconnected(dappInfo.toJson())
result.QObject.setup result.QObject.setup
@ -97,20 +105,27 @@ QtObject:
except JsonParsingError: except JsonParsingError:
raise newException(ValueError, "Failed to parse JSON") raise newException(ValueError, "Failed to parse JSON")
proc approveDappConnectRequest*(self: Controller, requestId: string, account: string, chainIDString: string): bool {.slot.} = proc approveConnection*(self: Controller, requestId: string, account: string, chainIDString: string): bool {.slot.} =
let chainId = parseSingleUInt(chainIDString) let chainId = parseSingleUInt(chainIDString)
return self.service.approveDappConnect(requestId, account, chainId) result = self.service.approveDappConnect(requestId, account, chainId)
self.approveConnectResponse(requestId, not result)
proc rejectDappConnectRequest*(self: Controller, requestId: string): bool {.slot.} = proc rejectConnection*(self: Controller, requestId: string): bool {.slot.} =
return self.service.rejectDappConnect(requestId) result = self.service.rejectDappConnect(requestId)
self.rejectConnectResponse(requestId, not result)
proc approveTransactionRequest*(self: Controller, requestId: string, signature: string): bool {.slot.} = proc approveTransaction*(self: Controller, requestId: string, signature: string): bool {.slot.} =
let hash = utils.createHash(signature) let hash = utils.createHash(signature)
return self.service.approveTransactionRequest(requestId, hash) result = self.service.approveTransactionRequest(requestId, hash)
self.approveTransactionResponse(requestId, not result)
proc rejectTransactionSigning*(self: Controller, requestId: string): bool {.slot.} = proc rejectTransaction*(self: Controller, requestId: string): bool {.slot.} =
return self.service.rejectTransactionSigning(requestId) result = self.service.rejectTransactionSigning(requestId)
self.rejectTransactionResponse(requestId, not result)
proc recallDAppPermission*(self: Controller, dAppUrl: string): bool {.slot.} = proc disconnect*(self: Controller, dAppUrl: string): bool {.slot.} =
return self.service.recallDAppPermission(dAppUrl) result = self.service.recallDAppPermission(dAppUrl)
proc getDApps*(self: Controller): string {.slot.} =
return self.service.getDApps()

View File

@ -137,4 +137,17 @@ QtObject:
except Exception as e: except Exception as e:
error "recallDAppPermissionFinishedRpc failed: ", err=e.msg error "recallDAppPermissionFinishedRpc failed: ", err=e.msg
return false return false
proc getDApps*(self: Service): string =
try:
let response = status_go.getPermittedDAppsList()
if not response.error.isNil:
raise newException(Exception, "Error getting connector dapp list: " & response.error.message)
# Expect nil golang array to be valid empty array
let jsonArray = $response.result
return if jsonArray != "null": jsonArray else: "[]"
except Exception as e:
error "getDApps failed: ", err=e.msg
return "[]"

View File

@ -38,6 +38,9 @@ rpc(requestAccountsRejected, "connector"):
rpc(recallDAppPermission, "connector"): rpc(recallDAppPermission, "connector"):
dAppUrl: string dAppUrl: string
rpc(getPermittedDAppsList, "connector"):
discard
proc isSuccessResponse(rpcResponse: RpcResponse[JsonNode]): bool = proc isSuccessResponse(rpcResponse: RpcResponse[JsonNode]): bool =
return rpcResponse.error.isNil return rpcResponse.error.isNil

View File

@ -0,0 +1,72 @@
import QtQuick 2.15
import AppLayouts.Wallet.services.dapps 1.0
import StatusQ.Core.Utils 0.1
import shared.stores 1.0
import utils 1.0
DAppsModel {
id: root
required property BrowserConnectStore store
readonly property int connectorId: Constants.StatusConnect
property bool enabled: true
Connections {
target: root.store
enabled: root.enabled
function onConnected(dappJson) {
const dapp = JSON.parse(dappJson)
const { url, name, icon, sharedAccount } = dapp
if (!url) {
console.warn(invalidDAppUrlError)
return
}
root.append({
name,
url,
iconUrl: icon,
topic: url,
connectorId: root.connectorId,
accountAddresses: [{address: sharedAccount}],
rawSessions: [dapp]
})
}
function onDisconnected(dappJson) {
const dapp = JSON.parse(dappJson)
const { url } = dapp
if (!url) {
console.warn(invalidDAppUrlError)
return
}
root.remove(dapp.url)
}
}
Component.onCompleted: {
if (root.enabled) {
const dappsStr = root.store.getDApps()
if (dappsStr) {
const dapps = JSON.parse(dappsStr)
dapps.forEach(dapp => {
const { url, name, iconUrl, sharedAccount } = dapp
root.append({
name,
url,
iconUrl,
topic: url,
connectorId: root.connectorId,
accountAddresses: [{address: sharedAccount}],
rawSessions: [dapp]
})
})
}
}
}
}

View File

@ -1,106 +0,0 @@
import QtQuick 2.15
import AppLayouts.Wallet.services.dapps 1.0
import StatusQ.Core.Utils 0.1
import shared.stores 1.0
import utils 1.0
QObject {
id: root
readonly property alias dappsModel: d.dappsModel
readonly property int connectorId: Constants.StatusConnect
property bool enabled: true
function addSession(url, name, iconUrl, accountAddress) {
if (!enabled) {
return
}
if (!url || !name || !iconUrl || !accountAddress) {
console.error("addSession: missing required parameters")
return
}
const topic = url
const activeSession = getActiveSession(topic)
if (!activeSession) {
d.addSession({
url,
name,
iconUrl,
topic,
connectorId: root.connectorId,
accountAddresses: [{address: accountAddress}]
})
return
}
if (!ModelUtils.contains(activeSession.accountAddresses, "address", accountAddress, Qt.CaseInsensitive)) {
activeSession.accountAddresses.append({address: accountAddress})
}
}
function revokeSession(topic) {
if (!enabled) {
return
}
d.revokeSession(topic)
}
function getActiveSession(topic) {
if (!enabled) {
return
}
return d.getActiveSession(topic)
}
QObject {
id: d
property ListModel dappsModel: ListModel {
id: dapps
}
function addSession(dappItem) {
dapps.append(dappItem)
}
function revokeSession(topic) {
for (let i = 0; i < dapps.count; i++) {
let existingDapp = dapps.get(i)
if (existingDapp.topic === topic) {
dapps.remove(i)
break
}
}
}
function revokeAllSessions() {
for (let i = 0; i < dapps.count; i++) {
dapps.remove(i)
}
}
function getActiveSession(topic) {
for (let i = 0; i < dapps.count; i++) {
const existingDapp = dapps.get(i)
if (existingDapp.topic === topic) {
return {
name: existingDapp.name,
url: existingDapp.url,
icon: existingDapp.iconUrl,
topic: existingDapp.topic,
connectorId: existingDapp.connectorId,
accountAddresses: existingDapp.accountAddresses
};
}
}
return null
}
}
}

View File

@ -0,0 +1,71 @@
import QtQuick 2.15
import StatusQ.Core.Utils 0.1
QObject {
id: root
// RoleNames
// name: string
// url: string
// iconUrl: string
// topic: string
// connectorId: int
// accountAddressses: [{address: string}]
// chains: string
// rawSessions: [{session: object}]
readonly property ListModel model: ListModel {}
function append(dapp) {
try {
const {name, url, iconUrl, topic, accountAddresses, connectorId, rawSessions } = dapp
if (!name || !url || !iconUrl || !topic || !connectorId || !accountAddresses || !rawSessions) {
console.warn("DAppsModel - Failed to append dapp, missing required fields", JSON.stringify(dapp))
return
}
root.model.append({
name,
url,
iconUrl,
topic,
connectorId,
accountAddresses,
rawSessions
})
} catch (e) {
console.warn("DAppsModel - Failed to append dapp", e)
}
}
function remove(topic) {
for (let i = 0; i < root.model.count; i++) {
const dapp = root.model.get(i)
if (dapp.topic == topic) {
root.model.remove(i)
break
}
}
}
function clear() {
root.model.clear()
}
function getByTopic(topic) {
for (let i = 0; i < root.model.count; i++) {
const dapp = root.model.get(i)
if (dapp.topic == topic) {
return {
name: dapp.name,
url: dapp.url,
iconUrl: dapp.iconUrl,
topic: dapp.topic,
connectorId: dapp.connectorId,
accountAddresses: dapp.accountAddresses,
rawSessions: dapp.rawSessions
}
}
}
return null
}
}

View File

@ -9,14 +9,15 @@ import shared.stores 1.0
import utils 1.0 import utils 1.0
QObject { DAppsModel {
id: root id: root
// Input
required property WalletConnectSDKBase sdk required property WalletConnectSDKBase sdk
required property DAppsStore store required property DAppsStore store
required property var supportedAccountsModel required property var supportedAccountsModel
readonly property int connectorId: Constants.WalletConnect readonly property int connectorId: Constants.WalletConnect
readonly property var dappsModel: d.dappsModel
property bool enabled: true property bool enabled: true
@ -39,11 +40,6 @@ QObject {
QObject { QObject {
id: d id: d
property ListModel dappsModel: ListModel {
id: dapps
objectName: "DAppsModel"
}
property Connections sdkConnections: Connections { property Connections sdkConnections: Connections {
target: root.sdk target: root.sdk
enabled: root.enabled enabled: root.enabled
@ -75,23 +71,22 @@ QObject {
{ {
dappsListReceivedFn = (dappsJson) => { dappsListReceivedFn = (dappsJson) => {
root.store.dappsListReceived.disconnect(dappsListReceivedFn); root.store.dappsListReceived.disconnect(dappsListReceivedFn);
dapps.clear(); root.clear();
let dappsList = JSON.parse(dappsJson); let dappsList = JSON.parse(dappsJson);
for (let i = 0; i < dappsList.length; i++) { for (let i = 0; i < dappsList.length; i++) {
const cachedEntry = dappsList[i]; const cachedEntry = dappsList[i];
// TODO #15075: on SDK dApps refresh update the model that has data source from persistence instead of using reset // TODO #15075: on SDK dApps refresh update the model that has data source from persistence instead of using reset
const dappEntryWithRequiredRoles = { const dappEntryWithRequiredRoles = {
description: "",
url: cachedEntry.url, url: cachedEntry.url,
name: cachedEntry.name, name: cachedEntry.name,
iconUrl: cachedEntry.iconUrl, iconUrl: cachedEntry.iconUrl,
accountAddresses: [], accountAddresses: [],
topic: "", topic: "",
connectorId: root.connectorId, connectorId: root.connectorId,
sessions: [] rawSessions: []
} }
dapps.append(dappEntryWithRequiredRoles); root.append(dappEntryWithRequiredRoles);
} }
} }
root.store.dappsListReceived.connect(dappsListReceivedFn); root.store.dappsListReceived.connect(dappsListReceivedFn);
@ -125,18 +120,18 @@ QObject {
// more modern syntax (ES-6) is not available yet // more modern syntax (ES-6) is not available yet
const combinedAddresses = new Set(existingDApp.accountAddresses.concat(accounts)); const combinedAddresses = new Set(existingDApp.accountAddresses.concat(accounts));
existingDApp.accountAddresses = Array.from(combinedAddresses); existingDApp.accountAddresses = Array.from(combinedAddresses);
dapp.sessions = [...existingDApp.sessions, session] dapp.rawSessions = [...existingDApp.rawSessions, session]
} else { } else {
dapp.accountAddresses = accounts dapp.accountAddresses = accounts
dapp.topic = sessionID dapp.topic = sessionID
dapp.sessions = [session] dapp.rawSessions = [session]
dAppsMap[dapp.url] = dapp dAppsMap[dapp.url] = dapp
} }
topics.push(sessionID) topics.push(sessionID)
} }
// TODO #15075: on SDK dApps refresh update the model that has data source from persistence instead of using reset // TODO #15075: on SDK dApps refresh update the model that has data source from persistence instead of using reset
dapps.clear(); root.clear();
// Iterate dAppsMap and fill dapps // Iterate dAppsMap and fill dapps
for (const uri in dAppsMap) { for (const uri in dAppsMap) {
@ -145,7 +140,7 @@ QObject {
// having array of key value pair fixes the problem // having array of key value pair fixes the problem
dAppEntry.accountAddresses = dAppEntry.accountAddresses.filter(account => (!!account)).map(account => ({address: account})); dAppEntry.accountAddresses = dAppEntry.accountAddresses.filter(account => (!!account)).map(account => ({address: account}));
dAppEntry.connectorId = root.connectorId; dAppEntry.connectorId = root.connectorId;
dapps.append(dAppEntry); root.append(dAppEntry);
} }
root.store.updateWalletConnectSessions(JSON.stringify(topics)) root.store.updateWalletConnectSessions(JSON.stringify(topics))

View File

@ -30,6 +30,7 @@ QObject {
//input properties //input properties
required property WalletConnectSDKBase wcSDK required property WalletConnectSDKBase wcSDK
required property BrowserConnectStore bcStore
required property DAppsStore store required property DAppsStore store
required property var walletRootStore required property var walletRootStore
// // Array[chainId] of the networks that are down // // Array[chainId] of the networks that are down
@ -51,8 +52,6 @@ QObject {
/// Default value: true /// Default value: true
readonly property bool serviceAvailableToCurrentAddress: !root.walletRootStore.selectedAddress || readonly property bool serviceAvailableToCurrentAddress: !root.walletRootStore.selectedAddress ||
ModelUtils.contains(root.validAccounts, "address", root.walletRootStore.selectedAddress, Qt.CaseInsensitive) ModelUtils.contains(root.validAccounts, "address", root.walletRootStore.selectedAddress, Qt.CaseInsensitive)
/// TODO: refactor
readonly property alias connectorDAppsProvider: connectorDAppsProvider
readonly property bool isServiceOnline: requestHandler.isServiceOnline readonly property bool isServiceOnline: requestHandler.isServiceOnline
@ -130,11 +129,11 @@ QObject {
sources: [ sources: [
SourceModel { SourceModel {
model: dappsProvider.dappsModel model: dappsProvider.model
markerRoleValue: "walletConnect" markerRoleValue: "walletConnect"
}, },
SourceModel { SourceModel {
model: connectorDAppsProvider.dappsModel model: connectorDAppsProvider.model
markerRoleValue: "statusConnect" markerRoleValue: "statusConnect"
} }
] ]
@ -308,6 +307,7 @@ QObject {
ConnectorDAppsListProvider { ConnectorDAppsListProvider {
id: connectorDAppsProvider id: connectorDAppsProvider
enabled: root.connectorFeatureEnabled enabled: root.connectorFeatureEnabled
store: root.bcStore
} }
// Timeout for the corner case where the URL was already dismissed and the SDK doesn't respond with an error nor advances with the proposal // Timeout for the corner case where the URL was already dismissed and the SDK doesn't respond with an error nor advances with the proposal

View File

@ -1,10 +1,9 @@
BCDappsProvider 1.0 BCDappsProvider.qml
DappsConnectorSDK 1.0 DappsConnectorSDK.qml
DAppsHelpers 1.0 helpers.js
DAppsModel 1.0 DAppsModel.qml
DAppsRequestHandler 1.0 DAppsRequestHandler.qml
WalletConnectSDKBase 1.0 WalletConnectSDKBase.qml WalletConnectSDKBase 1.0 WalletConnectSDKBase.qml
WalletConnectSDK 1.0 WalletConnectSDK.qml WalletConnectSDK 1.0 WalletConnectSDK.qml
WalletConnectService 1.0 WalletConnectService.qml WalletConnectService 1.0 WalletConnectService.qml
DappsConnectorSDK 1.0 DappsConnectorSDK.qml WCDappsProvider 1.0 WCDappsProvider.qml
DAppsListProvider 1.0 DAppsListProvider.qml
DAppsRequestHandler 1.0 DAppsRequestHandler.qml
ConnectorDAppsListProvider 1.0 ConnectorDAppsListProvider.qml
DAppsHelpers 1.0 helpers.js

View File

@ -2206,6 +2206,9 @@ Item {
store: SharedStores.DAppsStore { store: SharedStores.DAppsStore {
controller: WalletStores.RootStore.walletConnectController controller: WalletStores.RootStore.walletConnectController
} }
bcStore: SharedStores.BrowserConnectStore {
controller: WalletStores.RootStore.dappsConnectorController
}
walletRootStore: WalletStores.RootStore walletRootStore: WalletStores.RootStore
blockchainNetworksDown: appMain.networkConnectionStore.blockchainNetworksDown blockchainNetworksDown: appMain.networkConnectionStore.blockchainNetworksDown

View File

@ -0,0 +1,83 @@
import QtQuick 2.15
import StatusQ.Core.Utils 0.1 as SQUtils
SQUtils.QObject {
id: root
required property var controller
// Signals driven by the dApp
signal connectRequested(string requestId, string dappJson)
signal signRequested(string requestId, string requestJson)
signal connected(string dappJson)
signal disconnected(string dappJson)
// Responses to user actions
signal approveConnectResponse(string id, bool error)
signal rejectConnectResponse(string id, bool error)
signal approveTransactionResponse(string requestId, bool error)
signal rejectTransactionResponse(string requestId, bool error)
function approveConnection(id, account, chainId) {
return controller.approveConnection(id, account, chainId)
}
function rejectConnection(id, error) {
return controller.rejectConnection(id, error)
}
function approveTransaction(requestId, signature) {
return controller.approveTransaction(requestId, signature)
}
function rejectTransaction(requestId, error) {
return controller.rejectTransaction(requestId, error)
}
function disconnect(id) {
return controller.disconnect(id)
}
function getDApps() {
return controller.getDApps()
}
Connections {
target: controller
function onConnectRequested(requestId, dappJson) {
root.connectRequested(requestId, dappJson)
}
function onSignRequested(requestId, requestJson) {
root.signRequested(requestId, requestJson)
}
function onConnected(dappJson) {
root.connected(dappJson)
}
function onDisconnected(dappJson) {
root.disconnected(dappJson)
}
function onApproveConnectResponse(id, error) {
root.approveConnectResponse(id, error)
}
function onRejectConnectResponse(id, error) {
root.rejectConnectResponse(id, error)
}
function onApproveTransactionResponse(requestId, error) {
root.approveTransactionResponse(requestId, error)
}
function onRejectTransactionResponse(requestId, error) {
root.rejectTransactionResponse(requestId, error)
}
}
}

View File

@ -1,4 +1,5 @@
BIP39_en 1.0 BIP39_en.qml BIP39_en 1.0 BIP39_en.qml
BrowserConnectStore 1.0 BrowserConnectStore.qml
CommunityTokensStore 1.0 CommunityTokensStore.qml CommunityTokensStore 1.0 CommunityTokensStore.qml
CurrenciesStore 1.0 CurrenciesStore.qml CurrenciesStore 1.0 CurrenciesStore.qml
DAppsStore 1.0 DAppsStore.qml DAppsStore 1.0 DAppsStore.qml

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit b329b158c8f62f5bab03be596cf4f7d7b07c96f7 Subproject commit ba2baec26fac64fe074b485639e95a7b11e81ae5