diff --git a/src/app/core/signals/remote_signals/connector.nim b/src/app/core/signals/remote_signals/connector.nim index d7825d3905..4c6537bc7d 100644 --- a/src/app/core/signals/remote_signals/connector.nim +++ b/src/app/core/signals/remote_signals/connector.nim @@ -21,6 +21,8 @@ type ConnectorGrantDAppPermissionSignal* = ref object of Signal url*: string name*: string iconUrl*: string + chains*: string + sharedAccount*: string type ConnectorRevokeDAppPermissionSignal* = ref object of Signal url*: string @@ -48,6 +50,8 @@ proc fromEvent*(T: type ConnectorGrantDAppPermissionSignal, event: JsonNode): Co result.url = event["event"]{"url"}.getStr() result.name = event["event"]{"name"}.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 = result = ConnectorRevokeDAppPermissionSignal() diff --git a/src/app/modules/shared_modules/connector/controller.nim b/src/app/modules/shared_modules/connector/controller.nim index 883a4aedf3..3b3896452a 100644 --- a/src/app/modules/shared_modules/connector/controller.nim +++ b/src/app/modules/shared_modules/connector/controller.nim @@ -26,10 +26,16 @@ QtObject: proc delete*(self: Controller) = self.QObject.delete - proc dappRequestsToConnect*(self: Controller, requestId: string, payload: string) {.signal.} - proc dappValidatesTransaction*(self: Controller, requestId: string, payload: string) {.signal.} - proc dappGrantDAppPermission*(self: Controller, payload: string) {.signal.} - proc dappRevokeDAppPermission*(self: Controller, payload: string) {.signal.} + proc connectRequested*(self: Controller, requestId: string, payload: string) {.signal.} + proc connected*(self: Controller, payload: string) {.signal.} + proc disconnected*(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 = new(result, delete) @@ -51,7 +57,7 @@ QtObject: "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): let params = ConnectorSendTransactionSignal(e) @@ -63,7 +69,7 @@ QtObject: "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): let params = ConnectorGrantDAppPermissionSignal(e) @@ -71,9 +77,11 @@ QtObject: "icon": params.iconUrl, "name": params.name, "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): let params = ConnectorRevokeDAppPermissionSignal(e) @@ -83,7 +91,7 @@ QtObject: "url": params.url, } - controller.dappRevokeDAppPermission(dappInfo.toJson()) + controller.disconnected(dappInfo.toJson()) result.QObject.setup @@ -97,20 +105,27 @@ QtObject: except JsonParsingError: 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) - 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.} = - return self.service.rejectDappConnect(requestId) + proc rejectConnection*(self: Controller, requestId: string): bool {.slot.} = + 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) - 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.} = - return self.service.rejectTransactionSigning(requestId) + proc rejectTransaction*(self: Controller, requestId: string): bool {.slot.} = + result = self.service.rejectTransactionSigning(requestId) + self.rejectTransactionResponse(requestId, not result) - proc recallDAppPermission*(self: Controller, dAppUrl: string): bool {.slot.} = - return self.service.recallDAppPermission(dAppUrl) + proc disconnect*(self: Controller, dAppUrl: string): bool {.slot.} = + result = self.service.recallDAppPermission(dAppUrl) + + proc getDApps*(self: Controller): string {.slot.} = + return self.service.getDApps() diff --git a/src/app_service/service/connector/service.nim b/src/app_service/service/connector/service.nim index 055ab90c79..8af030246b 100644 --- a/src/app_service/service/connector/service.nim +++ b/src/app_service/service/connector/service.nim @@ -137,4 +137,17 @@ QtObject: except Exception as e: error "recallDAppPermissionFinishedRpc failed: ", err=e.msg - return false \ No newline at end of file + 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 "[]" \ No newline at end of file diff --git a/src/backend/connector.nim b/src/backend/connector.nim index b1cd65ee95..183ae4754c 100644 --- a/src/backend/connector.nim +++ b/src/backend/connector.nim @@ -38,6 +38,9 @@ rpc(requestAccountsRejected, "connector"): rpc(recallDAppPermission, "connector"): dAppUrl: string +rpc(getPermittedDAppsList, "connector"): + discard + proc isSuccessResponse(rpcResponse: RpcResponse[JsonNode]): bool = return rpcResponse.error.isNil diff --git a/ui/app/AppLayouts/Wallet/services/dapps/BCDappsProvider.qml b/ui/app/AppLayouts/Wallet/services/dapps/BCDappsProvider.qml new file mode 100644 index 0000000000..66419d4c6f --- /dev/null +++ b/ui/app/AppLayouts/Wallet/services/dapps/BCDappsProvider.qml @@ -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] + }) + }) + } + } + } +} diff --git a/ui/app/AppLayouts/Wallet/services/dapps/ConnectorDAppsListProvider.qml b/ui/app/AppLayouts/Wallet/services/dapps/ConnectorDAppsListProvider.qml deleted file mode 100644 index 59a00cc445..0000000000 --- a/ui/app/AppLayouts/Wallet/services/dapps/ConnectorDAppsListProvider.qml +++ /dev/null @@ -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 - } - } -} diff --git a/ui/app/AppLayouts/Wallet/services/dapps/DAppsModel.qml b/ui/app/AppLayouts/Wallet/services/dapps/DAppsModel.qml new file mode 100644 index 0000000000..741b477510 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/services/dapps/DAppsModel.qml @@ -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 + } +} \ No newline at end of file diff --git a/ui/app/AppLayouts/Wallet/services/dapps/DAppsListProvider.qml b/ui/app/AppLayouts/Wallet/services/dapps/WCDappsProvider.qml similarity index 90% rename from ui/app/AppLayouts/Wallet/services/dapps/DAppsListProvider.qml rename to ui/app/AppLayouts/Wallet/services/dapps/WCDappsProvider.qml index 90cce12d2e..d7d35b5258 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/DAppsListProvider.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/WCDappsProvider.qml @@ -9,14 +9,15 @@ import shared.stores 1.0 import utils 1.0 -QObject { +DAppsModel { id: root + // Input required property WalletConnectSDKBase sdk required property DAppsStore store required property var supportedAccountsModel + readonly property int connectorId: Constants.WalletConnect - readonly property var dappsModel: d.dappsModel property bool enabled: true @@ -39,11 +40,6 @@ QObject { QObject { id: d - property ListModel dappsModel: ListModel { - id: dapps - objectName: "DAppsModel" - } - property Connections sdkConnections: Connections { target: root.sdk enabled: root.enabled @@ -75,23 +71,22 @@ QObject { { dappsListReceivedFn = (dappsJson) => { root.store.dappsListReceived.disconnect(dappsListReceivedFn); - dapps.clear(); + root.clear(); let dappsList = JSON.parse(dappsJson); for (let i = 0; i < dappsList.length; i++) { const cachedEntry = dappsList[i]; // TODO #15075: on SDK dApps refresh update the model that has data source from persistence instead of using reset const dappEntryWithRequiredRoles = { - description: "", url: cachedEntry.url, name: cachedEntry.name, iconUrl: cachedEntry.iconUrl, accountAddresses: [], topic: "", connectorId: root.connectorId, - sessions: [] + rawSessions: [] } - dapps.append(dappEntryWithRequiredRoles); + root.append(dappEntryWithRequiredRoles); } } root.store.dappsListReceived.connect(dappsListReceivedFn); @@ -125,18 +120,18 @@ QObject { // more modern syntax (ES-6) is not available yet const combinedAddresses = new Set(existingDApp.accountAddresses.concat(accounts)); existingDApp.accountAddresses = Array.from(combinedAddresses); - dapp.sessions = [...existingDApp.sessions, session] + dapp.rawSessions = [...existingDApp.rawSessions, session] } else { dapp.accountAddresses = accounts dapp.topic = sessionID - dapp.sessions = [session] + dapp.rawSessions = [session] dAppsMap[dapp.url] = dapp } topics.push(sessionID) } // 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 for (const uri in dAppsMap) { @@ -145,7 +140,7 @@ QObject { // having array of key value pair fixes the problem dAppEntry.accountAddresses = dAppEntry.accountAddresses.filter(account => (!!account)).map(account => ({address: account})); dAppEntry.connectorId = root.connectorId; - dapps.append(dAppEntry); + root.append(dAppEntry); } root.store.updateWalletConnectSessions(JSON.stringify(topics)) diff --git a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml index fb2619c37b..6d4ce835e0 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml +++ b/ui/app/AppLayouts/Wallet/services/dapps/WalletConnectService.qml @@ -30,6 +30,7 @@ QObject { //input properties required property WalletConnectSDKBase wcSDK + required property BrowserConnectStore bcStore required property DAppsStore store required property var walletRootStore // // Array[chainId] of the networks that are down @@ -51,8 +52,6 @@ QObject { /// Default value: true readonly property bool serviceAvailableToCurrentAddress: !root.walletRootStore.selectedAddress || ModelUtils.contains(root.validAccounts, "address", root.walletRootStore.selectedAddress, Qt.CaseInsensitive) - /// TODO: refactor - readonly property alias connectorDAppsProvider: connectorDAppsProvider readonly property bool isServiceOnline: requestHandler.isServiceOnline @@ -130,11 +129,11 @@ QObject { sources: [ SourceModel { - model: dappsProvider.dappsModel + model: dappsProvider.model markerRoleValue: "walletConnect" }, SourceModel { - model: connectorDAppsProvider.dappsModel + model: connectorDAppsProvider.model markerRoleValue: "statusConnect" } ] @@ -308,6 +307,7 @@ QObject { ConnectorDAppsListProvider { id: connectorDAppsProvider 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 diff --git a/ui/app/AppLayouts/Wallet/services/dapps/qmldir b/ui/app/AppLayouts/Wallet/services/dapps/qmldir index b6ff39a232..6735eff896 100644 --- a/ui/app/AppLayouts/Wallet/services/dapps/qmldir +++ b/ui/app/AppLayouts/Wallet/services/dapps/qmldir @@ -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 WalletConnectSDK 1.0 WalletConnectSDK.qml WalletConnectService 1.0 WalletConnectService.qml -DappsConnectorSDK 1.0 DappsConnectorSDK.qml - -DAppsListProvider 1.0 DAppsListProvider.qml -DAppsRequestHandler 1.0 DAppsRequestHandler.qml -ConnectorDAppsListProvider 1.0 ConnectorDAppsListProvider.qml - -DAppsHelpers 1.0 helpers.js +WCDappsProvider 1.0 WCDappsProvider.qml diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml index affa81429f..f0c31db77c 100644 --- a/ui/app/mainui/AppMain.qml +++ b/ui/app/mainui/AppMain.qml @@ -2206,6 +2206,9 @@ Item { store: SharedStores.DAppsStore { controller: WalletStores.RootStore.walletConnectController } + bcStore: SharedStores.BrowserConnectStore { + controller: WalletStores.RootStore.dappsConnectorController + } walletRootStore: WalletStores.RootStore blockchainNetworksDown: appMain.networkConnectionStore.blockchainNetworksDown diff --git a/ui/imports/shared/stores/BrowserConnectStore.qml b/ui/imports/shared/stores/BrowserConnectStore.qml new file mode 100644 index 0000000000..aa2e7c878b --- /dev/null +++ b/ui/imports/shared/stores/BrowserConnectStore.qml @@ -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) + } + } +} \ No newline at end of file diff --git a/ui/imports/shared/stores/qmldir b/ui/imports/shared/stores/qmldir index c57acc564e..388b0a9c09 100644 --- a/ui/imports/shared/stores/qmldir +++ b/ui/imports/shared/stores/qmldir @@ -1,4 +1,5 @@ BIP39_en 1.0 BIP39_en.qml +BrowserConnectStore 1.0 BrowserConnectStore.qml CommunityTokensStore 1.0 CommunityTokensStore.qml CurrenciesStore 1.0 CurrenciesStore.qml DAppsStore 1.0 DAppsStore.qml diff --git a/vendor/status-go b/vendor/status-go index b329b158c8..ba2baec26f 160000 --- a/vendor/status-go +++ b/vendor/status-go @@ -1 +1 @@ -Subproject commit b329b158c8f62f5bab03be596cf4f7d7b07c96f7 +Subproject commit ba2baec26fac64fe074b485639e95a7b11e81ae5