feat(connector)_: create connection between connector service and status-desktop

fix #15179 that triggers connection request from client

 - Setup the initial architecture for connecting Status-go and UI
   screens
 - Implement the request Connection popup for Accept or Rejecting
   connection
This commit is contained in:
Godfrain Jacques 2024-07-12 08:40:35 -07:00 committed by MishkaRogachev
parent bb4c94438f
commit a50a6b96f0
15 changed files with 406 additions and 11 deletions

View File

@ -0,0 +1,17 @@
import json, tables, chronicles
import base
include app_service/common/json_utils
type ConnectorSendRequestAccountsSignal* = ref object of Signal
url*: string
name*: string
iconUrl*: string
requestID*: string
proc fromEvent*(T: type ConnectorSendRequestAccountsSignal, event: JsonNode): ConnectorSendRequestAccountsSignal =
result = ConnectorSendRequestAccountsSignal()
result.url = event["event"]{"url"}.getStr()
result.name = event["event"]{"name"}.getStr()
result.iconUrl = event["event"]{"iconUrl"}.getStr()
result.requestID = event["event"]{"requestId"}.getStr()

View File

@ -66,6 +66,7 @@ type SignalType* {.pure.} = enum
DBReEncryptionFinished = "db.reEncryption.finished" DBReEncryptionFinished = "db.reEncryption.finished"
CommunityTokenTransactionStatusChanged = "communityToken.communityTokenTransactionStatusChanged" CommunityTokenTransactionStatusChanged = "communityToken.communityTokenTransactionStatusChanged"
CommunityTokenAction = "communityToken.communityTokenAction" CommunityTokenAction = "communityToken.communityTokenAction"
ConnectorSendRequestAccounts = "connector.sendRequestAccounts"
Unknown Unknown
proc event*(self:SignalType):string = proc event*(self:SignalType):string =

View File

@ -135,6 +135,7 @@ QtObject:
of SignalType.LocalPairing: LocalPairingSignal.fromEvent(jsonSignal) of SignalType.LocalPairing: LocalPairingSignal.fromEvent(jsonSignal)
of SignalType.CommunityTokenTransactionStatusChanged: CommunityTokenTransactionStatusChangedSignal.fromEvent(jsonSignal) of SignalType.CommunityTokenTransactionStatusChanged: CommunityTokenTransactionStatusChangedSignal.fromEvent(jsonSignal)
of SignalType.CommunityTokenAction: CommunityTokenActionSignal.fromEvent(jsonSignal) of SignalType.CommunityTokenAction: CommunityTokenActionSignal.fromEvent(jsonSignal)
of SignalType.ConnectorSendRequestAccounts: ConnectorSendRequestAccountsSignal.fromEvent(jsonSignal)
else: Signal() else: Signal()
result.signalType = signalType result.signalType = signalType

View File

@ -1,11 +1,11 @@
{.used.} {.used.}
import ./remote_signals/[base, chronicles_logs, community, discovery_summary, envelope, expired, mailserver, messages, import ./remote_signals/[base, chronicles_logs, community, connector, discovery_summary, envelope, expired, mailserver, messages,
peerstats, signal_type, stats, wallet, whisper_filter, update_available, status_updates, waku_backed_up_profile, peerstats, signal_type, stats, wallet, whisper_filter, update_available, status_updates, waku_backed_up_profile,
waku_backed_up_settings, waku_backed_up_keypair, waku_backed_up_watch_only_account, waku_backed_up_settings, waku_backed_up_keypair, waku_backed_up_watch_only_account,
waku_fetching_backup_progress, pairing, node] waku_fetching_backup_progress, pairing, node]
export base, chronicles_logs, community, discovery_summary, envelope, expired, mailserver, messages, peerstats, export base, chronicles_logs, community, connector, discovery_summary, envelope, expired, mailserver, messages, peerstats,
signal_type, stats, wallet, whisper_filter, update_available, status_updates, waku_backed_up_profile, signal_type, stats, wallet, whisper_filter, update_available, status_updates, waku_backed_up_profile,
waku_backed_up_settings, waku_backed_up_keypair, waku_backed_up_watch_only_account, waku_backed_up_settings, waku_backed_up_keypair, waku_backed_up_watch_only_account,
waku_fetching_backup_progress, pairing, node waku_fetching_backup_progress, pairing, node

View File

@ -19,6 +19,7 @@ import ./activity/details_controller as activity_detailsc
import app/modules/shared_modules/collectible_details/controller as collectible_detailsc import app/modules/shared_modules/collectible_details/controller as collectible_detailsc
import app/modules/shared_modules/wallet_connect/controller as wc_controller import app/modules/shared_modules/wallet_connect/controller as wc_controller
import app/modules/shared_modules/connector/controller as connector_controller
import app/global/global_singleton import app/global/global_singleton
import app/core/eventemitter import app/core/eventemitter
@ -39,6 +40,7 @@ import app_service/service/network_connection/service as network_connection_serv
import app_service/service/devices/service as devices_service import app_service/service/devices/service as devices_service
import app_service/service/community_tokens/service as community_tokens_service import app_service/service/community_tokens/service as community_tokens_service
import app_service/service/wallet_connect/service as wc_service import app_service/service/wallet_connect/service as wc_service
import app_service/service/connector/service as connector_service
import backend/collectibles as backend_collectibles import backend/collectibles as backend_collectibles
@ -81,6 +83,8 @@ type
devicesService: devices_service.Service devicesService: devices_service.Service
walletConnectService: wc_service.Service walletConnectService: wc_service.Service
walletConnectController: wc_controller.Controller walletConnectController: wc_controller.Controller
dappsConnectorService: connector_service.Service
dappsConnectorController: connector_controller.Controller
activityController: activityc.Controller activityController: activityc.Controller
collectibleDetailsController: collectible_detailsc.Controller collectibleDetailsController: collectible_detailsc.Controller
@ -171,7 +175,9 @@ proc newModule*(
result.walletConnectService = wc_service.newService(result.events, result.threadpool, settingsService, transactionService) result.walletConnectService = wc_service.newService(result.events, result.threadpool, settingsService, transactionService)
result.walletConnectController = wc_controller.newController(result.walletConnectService, walletAccountService) result.walletConnectController = wc_controller.newController(result.walletConnectService, walletAccountService)
result.view = newView(result, result.activityController, result.tmpActivityControllers, result.activityDetailsController, result.collectibleDetailsController, result.walletConnectController) result.dappsConnectorService = connector_service.newService(result.events)
result.dappsConnectorController = connector_controller.newController(result.dappsConnectorService, result.events)
result.view = newView(result, result.activityController, result.tmpActivityControllers, result.activityDetailsController, result.collectibleDetailsController, result.walletConnectController, result.dappsConnectorController)
result.viewVariant = newQVariant(result.view) result.viewVariant = newQVariant(result.view)
method delete*(self: Module) = method delete*(self: Module) =
@ -348,6 +354,7 @@ method load*(self: Module) =
self.sendModule.load() self.sendModule.load()
self.networksModule.load() self.networksModule.load()
self.walletConnectService.init() self.walletConnectService.init()
self.dappsConnectorService.init()
method isLoaded*(self: Module): bool = method isLoaded*(self: Module): bool =
return self.moduleLoaded return self.moduleLoaded

View File

@ -6,6 +6,7 @@ import app/modules/shared_modules/collectible_details/controller as collectible_
import ./io_interface import ./io_interface
import app/modules/shared_models/currency_amount import app/modules/shared_models/currency_amount
import app/modules/shared_modules/wallet_connect/controller as wc_controller import app/modules/shared_modules/wallet_connect/controller as wc_controller
import app/modules/shared_modules/connector/controller as connector_controller
type type
ActivityControllerArray* = array[2, activityc.Controller] ActivityControllerArray* = array[2, activityc.Controller]
@ -26,6 +27,7 @@ QtObject:
isNonArchivalNode: bool isNonArchivalNode: bool
keypairOperabilityForObservedAccount: string keypairOperabilityForObservedAccount: string
wcController: QVariant wcController: QVariant
dappsConnectorController: QVariant
walletReady: bool walletReady: bool
addressFilters: string addressFilters: string
currentCurrency: string currentCurrency: string
@ -37,6 +39,7 @@ QtObject:
proc delete*(self: View) = proc delete*(self: View) =
self.wcController.delete self.wcController.delete
self.dappsConnectorController.delete
self.QObject.delete self.QObject.delete
@ -45,7 +48,8 @@ QtObject:
tmpActivityControllers: ActivityControllerArray, tmpActivityControllers: ActivityControllerArray,
activityDetailsController: activity_detailsc.Controller, activityDetailsController: activity_detailsc.Controller,
collectibleDetailsController: collectible_detailsc.Controller, collectibleDetailsController: collectible_detailsc.Controller,
wcController: wc_controller.Controller): View = wcController: wc_controller.Controller,
dappsConnectorController: connector_controller.Controller): View =
new(result, delete) new(result, delete)
result.delegate = delegate result.delegate = delegate
result.activityController = activityController result.activityController = activityController
@ -53,6 +57,7 @@ QtObject:
result.activityDetailsController = activityDetailsController result.activityDetailsController = activityDetailsController
result.collectibleDetailsController = collectibleDetailsController result.collectibleDetailsController = collectibleDetailsController
result.wcController = newQVariant(wcController) result.wcController = newQVariant(wcController)
result.dappsConnectorController = newQVariant(dappsConnectorController)
result.setup() result.setup()
@ -248,6 +253,14 @@ QtObject:
QtProperty[QVariant] walletConnectController: QtProperty[QVariant] walletConnectController:
read = getWalletConnectController read = getWalletConnectController
proc getDappsConnectorController(self: View): QVariant {.slot.} =
if self.dappsConnectorController == nil:
return newQVariant()
return self.dappsConnectorController
QtProperty[QVariant] dappsConnectorController:
read = getDappsConnectorController
proc walletReadyChanged*(self: View) {.signal.} proc walletReadyChanged*(self: View) {.signal.}
proc getWalletReady*(self: View): bool {.slot.} = proc getWalletReady*(self: View): bool {.slot.} =

View File

@ -0,0 +1,65 @@
import NimQml
import json, strutils
import chronicles
import app/core/eventemitter
import app/core/signals/types
import app_service/service/connector/service as connector_service
const SIGNAL_CONNECTOR_SEND_REQUEST_ACCOUNTS* = "ConnectorSendRequestAccounts"
logScope:
topics = "connector-controller"
QtObject:
type
Controller* = ref object of QObject
service: connector_service.Service
events: EventEmitter
proc delete*(self: Controller) =
self.QObject.delete
proc dappRequestsToConnect*(self: Controller, requestID: string, payload: string) {.signal.}
proc newController*(service: connector_service.Service, events: EventEmitter): Controller =
new(result, delete)
result.events = events
result.service = service
let controller = result # Capture result in a local variable
service.registerEventsHandler(proc (event: connector_service.Event, payload: string) =
discard
)
result.events.on(SIGNAL_CONNECTOR_SEND_REQUEST_ACCOUNTS) do(e: Args):
let params = ConnectorSendRequestAccountsSignal(e)
let dappInfo = %*{
"icon": params.iconUrl,
"name": params.name,
"url": params.url,
}
controller.dappRequestsToConnect(params.requestID, dappInfo.toJson())
result.QObject.setup
proc parseSingleUInt(chainIDsString: string): uint =
try:
let chainIds = parseJson(chainIDsString)
if chainIds.kind == JArray and chainIds.len == 1 and chainIds[0].kind == JInt:
return uint(chainIds[0].getInt())
else:
raise newException(ValueError, "Invalid JSON array format")
except JsonParsingError:
raise newException(ValueError, "Failed to parse JSON")
proc approveDappConnectRequest*(self: Controller, requestID: string, account: string, chainIDString: string): bool {.slot.} =
let chainID = parseSingleUInt(chainIDString)
return self.service.approveDappConnect(requestID, account, chainID)
proc rejectDappConnectRequest*(self: Controller, requestID: string): bool {.slot.} =
return self.service.rejectDappConnect(requestID)

View File

@ -0,0 +1,63 @@
import NimQml, chronicles, json
import backend/connector as status_go
import app/global/global_singleton
import app/core/eventemitter
import app/core/signals/types
import strutils
logScope:
topics = "connector-service"
const SIGNAL_CONNECTOR_SEND_REQUEST_ACCOUNTS* = "ConnectorSendRequestAccounts"
# Enum with events
type Event* = enum
DappConnect
# Event handler function
type EventHandlerFn* = proc(event: Event, payload: string)
# This can be ditched for now and process everything in the controller;
# However, it would be good to have the DB based calls async and this might be needed
QtObject:
type Service* = ref object of QObject
events: EventEmitter
eventHandler: EventHandlerFn
proc delete*(self: Service) =
self.QObject.delete
proc newService*(
events: EventEmitter
): Service =
new(result, delete)
result.QObject.setup
result.events = events
proc init*(self: Service) =
self.events.on(SignalType.ConnectorSendRequestAccounts.event, proc(e: Args) =
if self.eventHandler == nil:
return
var data = ConnectorSendRequestAccountsSignal(e)
if not data.requestID.len() == 0:
echo "ConnectorSendRequestAccountsSignal failed, requestID is empty"
return
self.events.emit(SIGNAL_CONNECTOR_SEND_REQUEST_ACCOUNTS, data)
)
proc registerEventsHandler*(self: Service, handler: EventHandlerFn) =
self.eventHandler = handler
proc approveDappConnect*(self: Service, requestID: string, account: string, chainID: uint): bool =
return status_go.requestAccountsAcceptedFinishedRpc(requestID, account, chainID)
proc rejectDappConnect*(self: Service, requestID: string): bool =
return status_go.requestAccountsRejectedFinishedRpc(requestID)

53
src/backend/connector.nim Normal file
View File

@ -0,0 +1,53 @@
import options, logging
import json, json_serialization
import core, response_type
from gen import rpc
const
EventConnectorSendRequestAccounts* = "connector.sendRequestAccounts"
type RequestAccountsAcceptedArgs* = ref object of RootObj
requestID* {.serializedFieldName("requestId").}: string
account* {.serializedFieldName("account").}: string
chainID* {.serializedFieldName("chainId").}: uint
type RejectedArgs* = ref object of RootObj
requestID* {.serializedFieldName("requestId").}: string
rpc(requestAccountsAccepted, "connector"):
args: RequestAccountsAcceptedArgs
rpc(requestAccountsRejected, "connector"):
args: RejectedArgs
proc isSuccessResponse(rpcResponse: RpcResponse[JsonNode]): bool =
return rpcResponse.error.isNil
proc requestAccountsAcceptedFinishedRpc*(requestID: string, account: string, chainID: uint): bool =
try:
var args = RequestAccountsAcceptedArgs()
args.requestID = requestID
args.account = account
args.chainID = chainID
let rpcRes = requestAccountsAccepted(args)
return isSuccessResponse(rpcRes)
except Exception as e:
error "requestAccountsAcceptedFinishedRpc failed: ", "msg", e.msg
return false
proc requestAccountsRejectedFinishedRpc*(requestID: string): bool =
try:
var args = RejectedArgs()
args.requestID = requestID
let rpcRes = requestAccountsRejected(args)
return isSuccessResponse(rpcRes)
except Exception as e:
error "requestAccountsRejectedFinishedRpc failed: ", "msg", e.msg
return false

View File

@ -0,0 +1,133 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtWebEngine 1.10
import QtWebChannel 1.15
import StatusQ.Core.Utils 0.1 as SQUtils
import StatusQ.Components 0.1
import StatusQ 0.1
import SortFilterProxyModel 0.2
import AppLayouts.Wallet.controls 1.0
import shared.popups.walletconnect 1.0
import AppLayouts.Wallet.services.dapps 1.0
import AppLayouts.Wallet.services.dapps.types 1.0
import shared.stores 1.0
import utils 1.0
import "types"
// Act as another layer of abstraction to the WalletConnectSDKBase
// Quick hack until the WalletConnectSDKBase could be refactored to a more generic DappProviderBase with API to match
// the UX requirements
WalletConnectSDKBase {
id: root
// Nim connector.controller instance
property var controller
property bool sdkReady: true
property bool active: true
required property WalletConnectService wcService
property string requestID: ""
projectId: ""
implicitWidth: 1
implicitHeight: 1
Loader {
id: connectDappLoader
active: false
property var dappChains: []
property var sessionProposal: null
property var availableNamespaces: null
property var sessionTopic: null
readonly property var proposalMedatada: !!sessionProposal
? sessionProposal.params.proposer.metadata
: { name: "", url: "", icons: [] }
sourceComponent: ConnectDAppModal {
visible: true
onClosed: connectDappLoader.active = false
accounts: root.wcService.validAccounts
property SortFilterProxyModel filteredFlatModel: SortFilterProxyModel {
sourceModel: networksModule.flatNetworks
filters: ValueFilter { roleName: "isTest"; value: networksModule.areTestNetworksEnabled }
}
flatNetworks: filteredFlatModel
selectedAccountAddress: root.wcService.selectedAccountAddress
dAppUrl: proposalMedatada.url
dAppName: proposalMedatada.name
dAppIconUrl: !!proposalMedatada.icons && proposalMedatada.icons.length > 0 ? proposalMedatada.icons[0] : ""
multipleChainSelection: false
onConnect: {
connectDappLoader.active = false
approveSession(root.requestID, selectedAccount.address, selectedChains)
}
onDecline: {
connectDappLoader.active = false
rejectSession(root.requestID)
}
}
}
Connections {
target: controller
onDappRequestsToConnect: function(requestID, dappInfoString) {
var dappInfo = JSON.parse(dappInfoString)
let sessionProposal = {
"params": {
"optionalNamespaces": {},
"proposer": {
"metadata": {
"description": "-",
"icons": [
dappInfo.icon
],
"name": dappInfo.name,
"url": dappInfo.url
}
},
"requiredNamespaces": {
"eip155": {
"chains": [
`eip155:${dappInfo.chainId}`
],
"events": [],
"methods": ["eth_sendTransaction"]
}
}
}
};
connectDappLoader.sessionProposal = sessionProposal
connectDappLoader.active = true
root.requestID = requestID
}
}
approveSession: function(requestID, account, selectedChains) {
controller.approveDappConnectRequest(requestID, account, JSON.stringify(selectedChains))
}
rejectSession: function(requestID) {
controller.rejectDappConnectRequest(requestID)
}
// We don't expect requests for these. They are here only to spot errors
pair: function(pairLink) { console.error("ConnectorSDK.pair: not implemented") }
getPairings: function(callback) { console.error("ConnectorSDK.getPairings: not implemented") }
disconnectPairing: function(topic) { console.error("ConnectorSDK.disconnectPairing: not implemented") }
buildApprovedNamespaces: function(params, supportedNamespaces) { console.error("ConnectorSDK.buildApprovedNamespaces: not implemented") }
}

View File

@ -1,6 +1,7 @@
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
DAppsListProvider 1.0 DAppsListProvider.qml DAppsListProvider 1.0 DAppsListProvider.qml
DAppsRequestHandler 1.0 DAppsRequestHandler.qml DAppsRequestHandler 1.0 DAppsRequestHandler.qml

View File

@ -104,6 +104,7 @@ QtObject {
readonly property var tmpActivityController1: walletSectionInst.tmpActivityController1 readonly property var tmpActivityController1: walletSectionInst.tmpActivityController1
readonly property var activityDetailsController: walletSectionInst.activityDetailsController readonly property var activityDetailsController: walletSectionInst.activityDetailsController
readonly property var walletConnectController: walletSectionInst.walletConnectController readonly property var walletConnectController: walletSectionInst.walletConnectController
readonly property var dappsConnectorController: walletSectionInst.dappsConnectorController
readonly property bool isAccountTokensReloading: walletSectionInst.isAccountTokensReloading readonly property bool isAccountTokensReloading: walletSectionInst.isAccountTokensReloading
readonly property double lastReloadTimestamp: walletSectionInst.lastReloadTimestamp readonly property double lastReloadTimestamp: walletSectionInst.lastReloadTimestamp

View File

@ -2046,19 +2046,55 @@ Item {
} }
} }
Component {
id: walletConnectSDK
WalletConnectSDK {
active: WalletStore.RootStore.walletSectionInst.walletReady
projectId: WalletStore.RootStore.appSettings.walletConnectProjectID
}
}
Component {
id: dappsConnectorSDK
DappsConnectorSDK {
active: WalletStore.RootStore.walletSectionInst.walletReady
controller: WalletStore.RootStore.dappsConnectorController
wcService: Global.walletConnectService
}
}
Loader {
id: walletConnectSDKLoader
active: Global.featureFlags.dappsEnabled
sourceComponent: walletConnectSDK
}
Loader {
id: dappsConnectorSDKLoader
active: Global.featureFlags.connectorEnabled
sourceComponent: dappsConnectorSDK
}
Loader {
id: sdkLoader
onLoaded: {
walletConnectSDKLoader.active = Global.featureFlags.dappsEnabled
dappsConnectorSDKLoader.active = Global.featureFlags.connectorEnabled
}
}
Loader { Loader {
id: walletConnectServiceLoader id: walletConnectServiceLoader
active: Global.featureFlags.dappsEnabled active: Global.featureFlags.dappsEnabled || Global.featureFlags.connectorEnabled
sourceComponent: WalletConnectService { sourceComponent: WalletConnectService {
id: walletConnectService id: walletConnectService
wcSDK: WalletConnectSDK { wcSDK: sdkLoader.item
active: WalletStore.RootStore.walletSectionInst.walletReady
projectId: WalletStore.RootStore.appSettings.walletConnectProjectID
}
store: DAppsStore { store: DAppsStore {
controller: WalletStore.RootStore.walletConnectController controller: WalletStore.RootStore.walletConnectController
} }

View File

@ -72,6 +72,8 @@ StatusDialog {
*/ */
property string selectedAccountAddress: contextCard.selectedAccount.address ?? "" property string selectedAccountAddress: contextCard.selectedAccount.address ?? ""
property bool multipleChainSelection: true
readonly property alias selectedAccount: contextCard.selectedAccount readonly property alias selectedAccount: contextCard.selectedAccount
readonly property alias selectedChains: d.selectedChains readonly property alias selectedChains: d.selectedChains
@ -118,6 +120,7 @@ StatusDialog {
Layout.maximumWidth: root.availableWidth Layout.maximumWidth: root.availableWidth
Layout.fillWidth: true Layout.fillWidth: true
multipleChainSelection: root.multipleChainSelection
selectedAccountAddress: root.selectedAccountAddress selectedAccountAddress: root.selectedAccountAddress
connectionAttempted: d.connectionAttempted connectionAttempted: d.connectionAttempted
accountsModel: d.accountsProxy accountsModel: d.accountsProxy

View File

@ -16,6 +16,7 @@ Rectangle {
property var accountsModel property var accountsModel
property var chainsModel property var chainsModel
property alias chainSelection: networkFilter.selection property alias chainSelection: networkFilter.selection
property bool multipleChainSelection: true
readonly property alias selectedAccount: accountsDropdown.currentAccount readonly property alias selectedAccount: accountsDropdown.currentAccount
@ -79,7 +80,7 @@ Rectangle {
flatNetworks: root.chainsModel flatNetworks: root.chainsModel
showTitle: true showTitle: true
multiSelection: true multiSelection: root.multipleChainSelection
showAllSelectedText: false showAllSelectedText: false
selectionAllowed: !root.connectionAttempted && root.chainsModel.ModelCount.count > 1 selectionAllowed: !root.connectionAttempted && root.chainsModel.ModelCount.count > 1
} }