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"
CommunityTokenTransactionStatusChanged = "communityToken.communityTokenTransactionStatusChanged"
CommunityTokenAction = "communityToken.communityTokenAction"
ConnectorSendRequestAccounts = "connector.sendRequestAccounts"
Unknown
proc event*(self:SignalType):string =

View File

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

View File

@ -1,11 +1,11 @@
{.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,
waku_backed_up_settings, waku_backed_up_keypair, waku_backed_up_watch_only_account,
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,
waku_backed_up_settings, waku_backed_up_keypair, waku_backed_up_watch_only_account,
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/wallet_connect/controller as wc_controller
import app/modules/shared_modules/connector/controller as connector_controller
import app/global/global_singleton
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/community_tokens/service as community_tokens_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
@ -81,6 +83,8 @@ type
devicesService: devices_service.Service
walletConnectService: wc_service.Service
walletConnectController: wc_controller.Controller
dappsConnectorService: connector_service.Service
dappsConnectorController: connector_controller.Controller
activityController: activityc.Controller
collectibleDetailsController: collectible_detailsc.Controller
@ -171,7 +175,9 @@ proc newModule*(
result.walletConnectService = wc_service.newService(result.events, result.threadpool, settingsService, transactionService)
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)
method delete*(self: Module) =
@ -348,6 +354,7 @@ method load*(self: Module) =
self.sendModule.load()
self.networksModule.load()
self.walletConnectService.init()
self.dappsConnectorService.init()
method isLoaded*(self: Module): bool =
return self.moduleLoaded

View File

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

View File

@ -104,6 +104,7 @@ QtObject {
readonly property var tmpActivityController1: walletSectionInst.tmpActivityController1
readonly property var activityDetailsController: walletSectionInst.activityDetailsController
readonly property var walletConnectController: walletSectionInst.walletConnectController
readonly property var dappsConnectorController: walletSectionInst.dappsConnectorController
readonly property bool isAccountTokensReloading: walletSectionInst.isAccountTokensReloading
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 {
id: walletConnectServiceLoader
active: Global.featureFlags.dappsEnabled
active: Global.featureFlags.dappsEnabled || Global.featureFlags.connectorEnabled
sourceComponent: WalletConnectService {
id: walletConnectService
wcSDK: WalletConnectSDK {
active: WalletStore.RootStore.walletSectionInst.walletReady
projectId: WalletStore.RootStore.appSettings.walletConnectProjectID
}
wcSDK: sdkLoader.item
store: DAppsStore {
controller: WalletStore.RootStore.walletConnectController
}

View File

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

View File

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