feat(dapps) authenticate user for session request

Trigger authentication with identity of the request
Track the authentication using the identity and allow only once
Add tests for the new functionality
Minor improvements around the code

Closes #15018
This commit is contained in:
Stefan 2024-06-04 23:45:03 +03:00 committed by Stefan Dunca
parent 3b2a7b8f08
commit 4d080e12aa
15 changed files with 423 additions and 91 deletions

View File

@ -14,7 +14,6 @@ import app_service/service/collectible/service as collectible_service
import app_service/service/currency/service as currency_service import app_service/service/currency/service as currency_service
import app_service/service/transaction/service as transaction_service import app_service/service/transaction/service as transaction_service
import app_service/service/wallet_account/service as wallet_account_service import app_service/service/wallet_account/service as wallet_account_service
import app_service/service/wallet_connect/service as wallet_connect_service
import app_service/service/bookmarks/service as bookmark_service import app_service/service/bookmarks/service as bookmark_service
import app_service/service/dapp_permissions/service as dapp_permissions_service import app_service/service/dapp_permissions/service as dapp_permissions_service
import app_service/service/privacy/service as privacy_service import app_service/service/privacy/service as privacy_service
@ -81,7 +80,6 @@ type
currencyService: currency_service.Service currencyService: currency_service.Service
transactionService: transaction_service.Service transactionService: transaction_service.Service
walletAccountService: wallet_account_service.Service walletAccountService: wallet_account_service.Service
walletConnectService: wallet_connect_service.Service
bookmarkService: bookmark_service.Service bookmarkService: bookmark_service.Service
dappPermissionsService: dapp_permissions_service.Service dappPermissionsService: dapp_permissions_service.Service
providerService: provider_service.Service providerService: provider_service.Service
@ -192,7 +190,6 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController =
statusFoundation.events, statusFoundation.threadpool, result.settingsService, result.accountsService, statusFoundation.events, statusFoundation.threadpool, result.settingsService, result.accountsService,
result.tokenService, result.networkService, result.currencyService result.tokenService, result.networkService, result.currencyService
) )
result.walletConnectService = wallet_connect_service.newService(statusFoundation.events, statusFoundation.threadpool, result.settingsService)
result.messageService = message_service.newService( result.messageService = message_service.newService(
statusFoundation.events, statusFoundation.events,
statusFoundation.threadpool, statusFoundation.threadpool,
@ -328,7 +325,6 @@ proc delete*(self: AppController) =
self.tokenService.delete self.tokenService.delete
self.transactionService.delete self.transactionService.delete
self.walletAccountService.delete self.walletAccountService.delete
self.walletConnectService.delete
self.aboutService.delete self.aboutService.delete
self.networkService.delete self.networkService.delete
self.activityCenterService.delete self.activityCenterService.delete
@ -456,7 +452,6 @@ proc load(self: AppController) =
self.collectibleService.init() self.collectibleService.init()
self.currencyService.init() self.currencyService.init()
self.walletAccountService.init() self.walletAccountService.init()
self.walletConnectService.init()
# Apply runtime log level settings # Apply runtime log level settings
if not main_constants.runtimeLogLevelSet(): if not main_constants.runtimeLogLevelSet():

View File

@ -168,7 +168,7 @@ proc newModule*(
result.filter = initFilter(result.controller) result.filter = initFilter(result.controller)
result.walletConnectService = wc_service.newService(result.events, result.threadpool, settingsService) result.walletConnectService = wc_service.newService(result.events, result.threadpool, settingsService)
result.walletConnectController = wc_controller.newController(result.walletConnectService) result.walletConnectController = wc_controller.newController(result.walletConnectService, walletAccountService)
result.view = newView(result, result.activityController, result.tmpActivityControllers, result.activityDetailsController, result.collectibleDetailsController, result.walletConnectController) result.view = newView(result, result.activityController, result.tmpActivityControllers, result.activityDetailsController, result.collectibleDetailsController, result.walletConnectController)
@ -341,6 +341,7 @@ method load*(self: Module) =
self.overviewModule.load() self.overviewModule.load()
self.sendModule.load() self.sendModule.load()
self.networksModule.load() self.networksModule.load()
self.walletConnectService.init()
method isLoaded*(self: Module): bool = method isLoaded*(self: Module): bool =
return self.moduleLoaded return self.moduleLoaded

View File

@ -2,6 +2,7 @@ import NimQml
import chronicles import chronicles
import app_service/service/wallet_connect/service as wallet_connect_service import app_service/service/wallet_connect/service as wallet_connect_service
import app_service/service/wallet_account/service as wallet_account_service
logScope: logScope:
topics = "wallet-connect-controller" topics = "wallet-connect-controller"
@ -10,14 +11,16 @@ QtObject:
type type
Controller* = ref object of QObject Controller* = ref object of QObject
service: wallet_connect_service.Service service: wallet_connect_service.Service
walletAccountService: wallet_account_service.Service
proc delete*(self: Controller) = proc delete*(self: Controller) =
self.QObject.delete self.QObject.delete
proc newController*(service: wallet_connect_service.Service): Controller = proc newController*(service: wallet_connect_service.Service, walletAccountService: wallet_account_service.Service): Controller =
new(result, delete) new(result, delete)
result.service = service result.service = service
result.walletAccountService = walletAccountService
result.QObject.setup result.QObject.setup
@ -34,3 +37,15 @@ QtObject:
else: else:
self.dappsListReceived(res) self.dappsListReceived(res)
return true return true
proc userAuthenticationResult*(self: Controller, topic: string, id: string, error: bool) {.signal.}
# Beware, it will fail if an authentication is already in progress
proc authenticateUser*(self: Controller, topic: string, id: string, address: string): bool {.slot.} =
let acc = self.walletAccountService.getAccountByAddress(address)
if acc.keyUid == "":
return false
return self.service.authenticateUser(acc.keyUid, proc(success: bool) =
self.userAuthenticationResult(topic, id, success)
)

View File

@ -10,17 +10,26 @@ import app/core/eventemitter
import app/core/signals/types import app/core/signals/types
import app/core/tasks/[threadpool] import app/core/tasks/[threadpool]
import app/modules/shared_modules/keycard_popup/io_interface as keycard_shared_module
logScope: logScope:
topics = "wallet-connect-service" topics = "wallet-connect-service"
# include async_tasks # include async_tasks
const UNIQUE_WALLET_CONNECT_MODULE_IDENTIFIER* = "WalletSection-WCModule"
type
AuthenticationResponseFn* = proc(success: bool)
QtObject: QtObject:
type Service* = ref object of QObject type Service* = ref object of QObject
events: EventEmitter events: EventEmitter
threadpool: ThreadPool threadpool: ThreadPool
settingsService: settings_service.Service settingsService: settings_service.Service
authenticationCallback: AuthenticationResponseFn
proc delete*(self: Service) = proc delete*(self: Service) =
self.QObject.delete self.QObject.delete
@ -31,12 +40,29 @@ QtObject:
): Service = ): Service =
new(result, delete) new(result, delete)
result.QObject.setup result.QObject.setup
result.events = events result.events = events
result.threadpool = threadpool result.threadpool = threadpool
result.settingsService = settings_service result.settingsService = settings_service
proc init*(self: Service) = proc init*(self: Service) =
discard self.events.on(SIGNAL_SHARED_KEYCARD_MODULE_USER_AUTHENTICATED) do(e: Args):
let args = SharedKeycarModuleArgs(e)
if args.uniqueIdentifier != UNIQUE_WALLET_CONNECT_MODULE_IDENTIFIER:
return
if self.authenticationCallback == nil:
error "unexpected user authenticated event; no callback set"
return
defer:
self.authenticationCallback = nil
if args.password == "" and args.pin == "":
info "fail to authenticate user"
self.authenticationCallback(false)
return
self.authenticationCallback(true)
proc addSession*(self: Service, session_json: string): bool = proc addSession*(self: Service, session_json: string): bool =
# TODO #14588: call it async # TODO #14588: call it async
@ -47,3 +73,16 @@ QtObject:
let testChains = self.settingsService.areTestNetworksEnabled() let testChains = self.settingsService.areTestNetworksEnabled()
# TODO #14588: call it async # TODO #14588: call it async
return status_go.getDapps(validAtEpoch, testChains) return status_go.getDapps(validAtEpoch, testChains)
# Will fail if another authentication is in progress
proc authenticateUser*(self: Service, keyUid: string, callback: AuthenticationResponseFn): bool =
if self.authenticationCallback != nil:
return false
self.authenticationCallback = callback
let data = SharedKeycarModuleAuthenticationArgs(
uniqueIdentifier: UNIQUE_WALLET_CONNECT_MODULE_IDENTIFIER,
keyUid: keyUid)
self.events.emit(SIGNAL_SHARED_KEYCARD_MODULE_AUTHENTICATE_USER, data)
return true

View File

@ -4,6 +4,7 @@ import QtQuick.Layouts 1.15
import QtQml 2.15 import QtQml 2.15
import Qt.labs.settings 1.0 import Qt.labs.settings 1.0
import QtTest 1.15 import QtTest 1.15
import QtQml.Models 2.14
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Core.Utils 0.1 import StatusQ.Core.Utils 0.1
@ -30,7 +31,6 @@ import shared.stores 1.0
Item { Item {
id: root id: root
// qml Splitter
SplitView { SplitView {
anchors.fill: parent anchors.fill: parent
@ -213,6 +213,42 @@ Item {
} }
} }
StatusDialog {
id: authMockDialog
title: "Authenticate user"
visible: false
property string topic: ""
property string id: ""
ColumnLayout {
RowLayout {
StatusBaseText { text: "Topic" }
StatusBaseText { text: authMockDialog.topic }
StatusBaseText { text: "ID" }
StatusBaseText { text: authMockDialog.id }
}
}
footer: StatusDialogFooter {
rightButtons: ObjectModel {
StatusButton {
text: qsTr("Reject")
onClicked: {
walletConnectService.store.userAuthenticationFailed(authMockDialog.topic, authMockDialog.id)
authMockDialog.close()
}
}
StatusButton {
text: qsTr("Authenticate")
onClicked: {
walletConnectService.store.userAuthenticated(authMockDialog.topic, authMockDialog.id)
authMockDialog.close()
}
}
}
}
}
WalletConnectService { WalletConnectService {
id: walletConnectService id: walletConnectService
@ -224,6 +260,9 @@ Item {
store: DAppsStore { store: DAppsStore {
signal dappsListReceived(string dappsJson) signal dappsListReceived(string dappsJson)
signal userAuthenticated(string topic, string id)
signal userAuthenticationFailed(string topic, string id)
signal sessionRequestExecuted(var payload, bool success)
function addWalletConnectSession(sessionJson) { function addWalletConnectSession(sessionJson) {
console.info("Persist Session", sessionJson) console.info("Persist Session", sessionJson)
@ -243,6 +282,13 @@ Item {
this.dappsListReceived(JSON.stringify(d.persistedDapps)) this.dappsListReceived(JSON.stringify(d.persistedDapps))
return true return true
} }
function authenticateUser(topic, id, address) {
authMockDialog.topic = topic
authMockDialog.id = id
authMockDialog.open()
return true
}
} }
walletStore: WalletStore { walletStore: WalletStore {
@ -253,6 +299,8 @@ Item {
property var accounts: customAccountsModel.count > 0 ? customAccountsModel : defaultAccountsModel property var accounts: customAccountsModel.count > 0 ? customAccountsModel : defaultAccountsModel
readonly property ListModel ownAccounts: accounts readonly property ListModel ownAccounts: accounts
} }
onDisplayToastMessage: (message, error) => console.info("Storybook - toast message: ", message, !!error ? "; error: " + error: "")
} }

View File

@ -22,8 +22,8 @@ import utils 1.0
Item { Item {
id: root id: root
// width: 600 width: 600
// height: 400 height: 400
// Component { // Component {
// id: sdkComponent // id: sdkComponent
@ -79,6 +79,9 @@ Item {
// DAppsStore { // DAppsStore {
// signal dappsListReceived(string dappsJson) // signal dappsListReceived(string dappsJson)
// signal userAuthenticated(string topic, string id)
// signal userAuthenticationFailed(string topic, string id)
// signal sessionRequestExecuted(var payload, bool success)
// // By default, return no dapps in store // // By default, return no dapps in store
// function getDapps() { // function getDapps() {
@ -90,6 +93,16 @@ Item {
// function addWalletConnectSession(sessionJson) { // function addWalletConnectSession(sessionJson) {
// addWalletConnectSessionCalls.push({sessionJson}) // addWalletConnectSessionCalls.push({sessionJson})
// } // }
// property var authenticateUserCalls: []
// function authenticateUser(topic, id, address) {
// authenticateUserCalls.push({topic, id, address})
// }
// property var signMessageCalls: []
// function signMessage(message) {
// signMessageCalls.push({message})
// }
// } // }
// } // }
@ -120,6 +133,52 @@ Item {
// } // }
// } // }
// Component {
// id: dappsRequestHandlerComponent
// DAppsRequestHandler {
// }
// }
// TestCase {
// id: requestHandlerTest
// name: "DAppsRequestHandler"
// property DAppsRequestHandler handler: null
// SignalSpy {
// id: displayToastMessageSpy
// target: requestHandlerTest.handler
// signalName: "onDisplayToastMessage"
// }
// function init() {
// let walletStore = createTemporaryObject(walletStoreComponent, root)
// verify(!!walletStore)
// let sdk = createTemporaryObject(sdkComponent, root, { projectId: "12ab" })
// verify(!!sdk)
// let store = createTemporaryObject(dappsStoreComponent, root)
// verify(!!store)
// handler = createTemporaryObject(dappsRequestHandlerComponent, root, {sdk: sdk, store: store, walletStore: walletStore})
// verify(!!handler)
// }
// function cleanup() {
// displayToastMessageSpy.clear()
// }
// function test_TestAuthentication() {
// let td = mockSessionRequestEvent(this, handler.sdk, handler.walletStore)
// handler.authenticate(td.request)
// compare(handler.store.authenticateUserCalls.length, 1, "expected a call to store.authenticateUser")
// let store = handler.store
// store.userAuthenticated(td.topic, td.request.id)
// compare(store.signMessageCalls.length, 1, "expected a call to store.signMessage")
// compare(store.signMessageCalls[0].message, td.request.data)
// }
// }
// TestCase { // TestCase {
// id: walletConnectServiceTest // id: walletConnectServiceTest
// name: "WalletConnectService" // name: "WalletConnectService"
@ -263,6 +322,10 @@ Item {
// verify(!!request.data, "expected data to be set") // verify(!!request.data, "expected data to be set")
// compare(request.data.message, message, "expected message to be set") // compare(request.data.message, message, "expected message to be set")
// } // }
// // TODO #14757: add tests with multiple session requests coming in; validate that authentication is serialized and in order
// // function tst_SessionRequestQueueMultiple() {
// // }
// } // }
// Component { // Component {
@ -394,7 +457,7 @@ Item {
// waitForRendering(controlUnderTest) // waitForRendering(controlUnderTest)
// compare(dappsListReadySpy.count, 0, "expected NO dappsListReady signal to be emitted") // compare(dappsListReadySpy.count, 0, "expected NO dappsListReady signal to be emitted")
// mouseClick(controlUnderTest, Qt.LeftButton) // mouseClick(controlUnderTest)
// waitForRendering(controlUnderTest) // waitForRendering(controlUnderTest)
// compare(dappsListReadySpy.count, 1, "expected dappsListReady signal to be emitted") // compare(dappsListReadySpy.count, 1, "expected dappsListReady signal to be emitted")
@ -402,7 +465,7 @@ Item {
// verify(!!popup) // verify(!!popup)
// verify(popup.opened) // verify(popup.opened)
// mouseClick(Overlay.overlay, Qt.LeftButton) // popup.close()
// waitForRendering(controlUnderTest) // waitForRendering(controlUnderTest)
// verify(!popup.opened) // verify(!popup.opened)
@ -411,7 +474,7 @@ Item {
// function test_OpenPairModal() { // function test_OpenPairModal() {
// waitForRendering(controlUnderTest) // waitForRendering(controlUnderTest)
// mouseClick(controlUnderTest, Qt.LeftButton) // mouseClick(controlUnderTest)
// waitForRendering(controlUnderTest) // waitForRendering(controlUnderTest)
// let popup = findChild(controlUnderTest, "dappsPopup") // let popup = findChild(controlUnderTest, "dappsPopup")
@ -422,7 +485,7 @@ Item {
// verify(!!connectButton) // verify(!!connectButton)
// verify(pairWCReadySpy.count === 0, "expected NO pairWCReady signal to be emitted") // verify(pairWCReadySpy.count === 0, "expected NO pairWCReady signal to be emitted")
// mouseClick(connectButton, Qt.LeftButton) // mouseClick(connectButton)
// waitForRendering(controlUnderTest) // waitForRendering(controlUnderTest)
// verify(pairWCReadySpy.count === 1, "expected pairWCReady signal to be emitted") // verify(pairWCReadySpy.count === 1, "expected pairWCReady signal to be emitted")
@ -437,42 +500,11 @@ Item {
// } // }
// } // }
// function mockSessionRequestEvent() {
// let service = controlUnderTest.wcService
// let account = service.walletStore.accounts.get(1)
// let network = service.walletStore.flatNetworks.get(1)
// let method = "personal_sign"
// let message = "hello world"
// let params = [Helpers.strToHex(message), account.address]
// let topic = "b536a"
// let requestEvent = JSON.parse(Testing.formatSessionRequest(network.chainId, method, params, topic))
// let request = createTemporaryObject(sessionRequestComponent, root, {
// event: requestEvent,
// topic,
// id: requestEvent.id,
// method: Constants.personal_sign,
// account,
// network,
// data: message
// })
// // All calls to SDK are expected as events to be made by the wallet connect SDK
// let sdk = service.wcSDK
// // Expect to have calls to getActiveSessions from service initialization
// let prevRequests = sdk.getActiveSessionsCallbacks.length
// sdk.sessionRequestEvent(requestEvent)
// // Service will trigger a sessionRequest event following the getActiveSessions call
// let callback = sdk.getActiveSessionsCallbacks[prevRequests].callback
// let session = JSON.parse(Testing.formatApproveSessionResponse([network.chainId, 7], [account.address]))
// callback({"b536a": session})
// return {sdk, session, account, network, topic, id: request.id}
// }
// function test_OpenDappRequestModal() { // function test_OpenDappRequestModal() {
// waitForRendering(controlUnderTest) // waitForRendering(controlUnderTest)
// let td = mockSessionRequestEvent() // let service = controlUnderTest.wcService
// let td = mockSessionRequestEvent(this, service.wcSDK, service.walletStore)
// waitForRendering(controlUnderTest) // waitForRendering(controlUnderTest)
// let popup = findChild(controlUnderTest, "dappsRequestModal") // let popup = findChild(controlUnderTest, "dappsRequestModal")
@ -494,7 +526,8 @@ Item {
// function test_RejectDappRequestModal() { // function test_RejectDappRequestModal() {
// waitForRendering(controlUnderTest) // waitForRendering(controlUnderTest)
// let td = mockSessionRequestEvent() // let service = controlUnderTest.wcService
// let td = mockSessionRequestEvent(this, service.wcSDK, service.walletStore)
// waitForRendering(controlUnderTest) // waitForRendering(controlUnderTest)
// let popup = findChild(controlUnderTest, "dappsRequestModal") // let popup = findChild(controlUnderTest, "dappsRequestModal")
@ -502,11 +535,11 @@ Item {
// let rejectButton = findChild(popup, "rejectButton") // let rejectButton = findChild(popup, "rejectButton")
// mouseClick(rejectButton, Qt.LeftButton) // mouseClick(rejectButton)
// compare(td.sdk.rejectSessionRequestCalls.length, 1, "expected a call to service.rejectSessionRequest") // compare(td.sdk.rejectSessionRequestCalls.length, 1, "expected a call to service.rejectSessionRequest")
// let args = td.sdk.rejectSessionRequestCalls[0] // let args = td.sdk.rejectSessionRequestCalls[0]
// compare(args.topic, td.topic, "expected topic to be set") // compare(args.topic, td.topic, "expected topic to be set")
// compare(args.id, td.id, "expected id to be set") // compare(args.id, td.request.id, "expected id to be set")
// compare(args.error, false, "expected no error; it was user rejected") // compare(args.error, false, "expected no error; it was user rejected")
// waitForRendering(controlUnderTest) // waitForRendering(controlUnderTest)
@ -514,4 +547,32 @@ Item {
// verify(!popup.visible) // verify(!popup.visible)
// } // }
// } // }
// function mockSessionRequestEvent(tc, sdk, walletStore) {
// let account = walletStore.accounts.get(1)
// let network = walletStore.flatNetworks.get(1)
// let method = "personal_sign"
// let message = "hello world"
// let params = [Helpers.strToHex(message), account.address]
// let topic = "b536a"
// let requestEvent = JSON.parse(Testing.formatSessionRequest(network.chainId, method, params, topic))
// let request = tc.createTemporaryObject(sessionRequestComponent, root, {
// event: requestEvent,
// topic,
// id: requestEvent.id,
// method: Constants.personal_sign,
// account,
// network,
// data: message
// })
// // Expect to have calls to getActiveSessions from service initialization
// let prevRequests = sdk.getActiveSessionsCallbacks.length
// sdk.sessionRequestEvent(requestEvent)
// // Service might trigger a sessionRequest event following the getActiveSessions call
// let callback = sdk.getActiveSessionsCallbacks[prevRequests].callback
// let session = JSON.parse(Testing.formatApproveSessionResponse([network.chainId, 7], [account.address]))
// callback({"b536a": session})
// return {sdk, session, account, network, topic, request}
// }
} }

View File

@ -61,12 +61,13 @@ ConnectedDappsButton {
sourceComponent: DAppsListPopup { sourceComponent: DAppsListPopup {
visible: true visible: true
model: wcService.dappsModel model: root.wcService.dappsModel
onPairWCDapp: { onPairWCDapp: {
pairWCLoader.active = true pairWCLoader.active = true
this.close() this.close()
} }
onOpened: { onOpened: {
this.x = root.width - this.contentWidth - 2 * this.padding this.x = root.width - this.contentWidth - 2 * this.padding
this.y = root.height + 4 this.y = root.height + 4
@ -91,8 +92,8 @@ ConnectedDappsButton {
visible: true visible: true
onClosed: connectDappLoader.active = false onClosed: connectDappLoader.active = false
accounts: wcService.validAccounts accounts: root.wcService.validAccounts
flatNetworks: wcService.flatNetworks flatNetworks: root.wcService.flatNetworks
onConnect: { onConnect: {
root.wcService.approvePairSession(sessionProposal, dappChains, selectedAccount) root.wcService.approvePairSession(sessionProposal, dappChains, selectedAccount)
@ -136,17 +137,30 @@ ConnectedDappsButton {
onClosed: sessionRequestLoader.active = false onClosed: sessionRequestLoader.active = false
onSign: { onSign: {
console.debug("@dd TODO sign session request") if (!request) {
console.error("Error signing: request is null")
return
}
root.wcService.requestHandler.authenticate(request)
} }
onReject: { onReject: {
let userRejected = true let userRejected = true
wcService.requestHandler.rejectSessionRequest(request, userRejected) root.wcService.requestHandler.rejectSessionRequest(request, userRejected)
close() close()
} }
} }
} }
Connections {
target: root.wcService ? root.wcService.requestHandler : null
function onSessionRequestResult(payload, isSuccess) {
// TODO #14927 handle this properly
sessionRequestLoader.active = false
}
}
Connections { Connections {
target: root.wcService target: root.wcService

View File

@ -23,16 +23,32 @@ QObject {
sdk.rejectSessionRequest(request.topic, request.id, error) sdk.rejectSessionRequest(request.topic, request.id, error)
} }
/// Beware, it will fail if called multiple times before getting an answer
function authenticate(request) {
return store.authenticateUser(request.topic, request.id, request.account.address)
}
signal sessionRequest(SessionRequestResolved request) signal sessionRequest(SessionRequestResolved request)
signal displayToastMessage(string message, bool error)
signal sessionRequestResult(var payload, bool isSuccess)
/// Supported methods /// Supported methods
property QtObject methods: QtObject { property QtObject methods: QtObject {
readonly property string personalSign: Constants.personal_sign readonly property QtObject personalSign: QtObject {
readonly property string sendTransaction: "eth_sendTransaction" readonly property string name: Constants.personal_sign
readonly property string userString: qsTr("sign")
}
readonly property QtObject sendTransaction: QtObject {
readonly property string name: "eth_sendTransaction"
readonly property string userString: qsTr("send transaction")
}
readonly property var all: [personalSign, sendTransaction]
} }
function getSupportedMethods() { function getSupportedMethods() {
return [root.methods.personalSign, root.methods.sendTransaction] return methods.all.map(function(method) {
return method.name
})
} }
Connections { Connections {
@ -47,6 +63,65 @@ QObject {
} }
requests.enqueue(obj) requests.enqueue(obj)
} }
function onSessionRequestUserAnswerResult(topic, id, accept, error) {
var request = requests.findRequest(topic, id)
if (request === null) {
console.error("Error finding event for topic", topic, "id", id)
return
}
let methodStr = d.methodToUserString(request.method)
if (!methodStr) {
console.error("Error finding user string for method", request.method)
return
}
d.lookupSession(topic, function(session) {
if (session === null)
return
if (error) {
root.displayToastMessage(qsTr("Fail to %1 from %2").arg(methodStr).arg(session.peer.metadata.url), true)
// TODO #14757 handle SDK error on user accept/reject
console.error(`Error accepting session request for topic: ${topic}, id: ${id}, accept: ${accept}, error: ${error}`)
return
}
let actionStr = accept ? qsTr("accepted") : qsTr("rejected")
root.displayToastMessage("%1 %2 %3".arg(session.peer.metadata.url).arg(methodStr).arg(actionStr), false)
root.sessionRequestApprovalResult()
})
}
}
Connections {
target: root.store
function onUserAuthenticated(topic, id) {
var request = requests.findRequest(topic, id)
if (request === null) {
console.error("Error finding event for topic", topic, "id", id)
return
}
d.executeSessionRequest(request)
}
function onUserAuthenticationFailed(topic, id) {
var request = requests.findRequest(topic, id)
let methodStr = d.methodToUserString(request.method)
if (request === null || !methodStr) {
return
}
d.lookupSession(topic, function(session) {
if (session === null)
return
root.displayToastMessage(qsTr("Failed to authenticate %1 from %2").arg(methodStr).arg(session.peer.metadata.url), true)
})
}
function onSessionRequestExecuted(payload, isSuccess) {
// TODO #14927 handle this properly
root.sessionRequestResult(payload, isSuccess)
}
} }
QObject { QObject {
@ -74,19 +149,18 @@ QObject {
// Check later to have a valid request object // Check later to have a valid request object
if (!getSupportedMethods().includes(method) if (!getSupportedMethods().includes(method)
// TODO #14927: support method eth_sendTransaction // TODO #14927: support method eth_sendTransaction
|| method == "eth_sendTransaction") { || method == root.methods.sendTransaction.name) {
console.error("Unsupported method", method) console.error("Unsupported method", method)
return null return null
} }
sdk.getActiveSessions((res) => { d.lookupSession(obj.topic, function(session) {
Object.keys(res).forEach((topic) => { if (session === null) {
if (topic === obj.topic) { console.error("DAppsRequestHandler.lookupSession: error finding session for topic", obj.topic)
let session = res[topic] return
obj.resolveDappInfoFromSession(session) }
root.sessionRequest(obj) obj.resolveDappInfoFromSession(session)
} root.sessionRequest(obj)
})
}) })
return obj return obj
@ -94,7 +168,7 @@ QObject {
/// Returns null if the account is not found /// Returns null if the account is not found
function lookupAccountFromEvent(event, method) { function lookupAccountFromEvent(event, method) {
if (method === root.methods.personalSign) { if (method === root.methods.personalSign.name) {
if (event.params.request.params.length < 2) { if (event.params.request.params.length < 2) {
return null return null
} }
@ -111,7 +185,7 @@ QObject {
/// Returns null if the network is not found /// Returns null if the network is not found
function lookupNetworkFromEvent(event, method) { function lookupNetworkFromEvent(event, method) {
if (method === root.methods.personalSign) { if (method === root.methods.personalSign.name) {
let chainId = Helpers.chainIdFromEip155(event.params.chainId) let chainId = Helpers.chainIdFromEip155(event.params.chainId)
for (let i = 0; i < walletStore.flatNetworks.count; i++) { for (let i = 0; i < walletStore.flatNetworks.count; i++) {
let network = ModelUtils.get(walletStore.flatNetworks, i) let network = ModelUtils.get(walletStore.flatNetworks, i)
@ -124,7 +198,7 @@ QObject {
} }
function extractMethodData(event, method) { function extractMethodData(event, method) {
if (method === root.methods.personalSign) { if (method === root.methods.personalSign.name) {
if (event.params.request.params.length == 0) { if (event.params.request.params.length == 0) {
return null return null
} }
@ -134,6 +208,35 @@ QObject {
} }
} }
} }
function methodToUserString(method) {
for (let i = 0; i < methods.all.length; i++) {
if (methods.all[i].name === method) {
return methods.all[i].userString
}
}
return ""
}
function lookupSession(topicToLookup, callback) {
sdk.getActiveSessions((res) => {
Object.keys(res).forEach((topic) => {
if (topic === topicToLookup) {
let session = res[topic]
callback(session)
}
})
})
}
function executeSessionRequest(request) {
if (request.method === root.methods.personalSign.name) {
store.signMessage(request.data.message)
console.debug("TODO #14927 sign message: ", request.data.message)
} else {
console.error("Unsupported method to execute: ", request.method)
}
}
} }
/// The queue is used to ensure that the events are processed in the order they are received but they could be /// The queue is used to ensure that the events are processed in the order they are received but they could be

View File

@ -206,10 +206,10 @@ WalletConnectSDKBase {
d.engine.runJavaScript(` d.engine.runJavaScript(`
wc.respondSessionRequest("${topic}", ${id}, "${signature}") wc.respondSessionRequest("${topic}", ${id}, "${signature}")
.then((value) => { .then((value) => {
wc.statusObject.onRespondSessionRequestResponse("") wc.statusObject.onAcceptSessionRequestResponse("${topic}", ${id}, "")
}) })
.catch((e) => { .catch((e) => {
wc.statusObject.onRespondSessionRequestResponse(e.message) wc.statusObject.onAcceptSessionRequestResponse("${topic}", ${id}, e.message)
}) })
` `
) )
@ -221,10 +221,10 @@ WalletConnectSDKBase {
d.engine.runJavaScript(` d.engine.runJavaScript(`
wc.rejectSessionRequest("${topic}", ${id}, "${error}") wc.rejectSessionRequest("${topic}", ${id}, "${error}")
.then((value) => { .then((value) => {
wc.statusObject.onRejectSessionRequestResponse("") wc.statusObject.onRejectSessionRequestResponse("${topic}", ${id}, "")
}) })
.catch((e) => { .catch((e) => {
wc.statusObject.onRejectSessionRequestResponse(e.message) wc.statusObject.onRejectSessionRequestResponse("${topic}", ${id}, e.message)
}) })
` `
) )
@ -387,14 +387,16 @@ WalletConnectSDKBase {
root.rejectSessionResult(error) root.rejectSessionResult(error)
} }
function onRespondSessionRequestResponse(error) { function onAcceptSessionRequestResponse(topic, id, error) {
console.debug(`WC WalletConnectSDK.onRespondSessionRequestResponse; error: ${error}`) console.debug(`WC WalletConnectSDK.onAcceptSessionRequestResponse; topic: ${topic}, id: ${id} error: ${error}`)
root.sessionRequestUserAnswerResult(true, error) let responseToAccept = true
root.sessionRequestUserAnswerResult(topic, id, responseToAccept, error)
} }
function onRejectSessionRequestResponse(error) { function onRejectSessionRequestResponse(topic, id, error) {
console.debug(`WC WalletConnectSDK.onRejectSessionRequestResponse; error: ${error}`) console.debug(`WC WalletConnectSDK.onRejectSessionRequestResponse; topic: ${topic}, id: ${id}, error: ${error}`)
root.sessionRequestUserAnswerResult(false, error) let responseToAccept = false
root.sessionRequestUserAnswerResult(topic, id, responseToAccept, error)
} }
function onSessionProposal(details) { function onSessionProposal(details) {

View File

@ -13,7 +13,7 @@ Item {
signal approveSessionResult(var approvedNamespaces, string error) signal approveSessionResult(var approvedNamespaces, string error)
signal rejectSessionResult(string error) signal rejectSessionResult(string error)
signal sessionRequestEvent(var sessionRequest) signal sessionRequestEvent(var sessionRequest)
signal sessionRequestUserAnswerResult(bool accept, string error) signal sessionRequestUserAnswerResult(string topic, string id, bool accept /* not reject */, string error)
signal authRequest(var request) signal authRequest(var request)
signal authMessageFormated(string formatedMessage, string address) signal authMessageFormated(string formatedMessage, string address)

View File

@ -164,6 +164,9 @@ QObject {
onSessionRequest: (request) => { onSessionRequest: (request) => {
root.sessionRequest(request) root.sessionRequest(request)
} }
onDisplayToastMessage: (message, error) => {
root.displayToastMessage(message, error)
}
} }
DAppsListProvider { DAppsListProvider {

View File

@ -4,8 +4,8 @@ import QtQuick 2.15
ListModel { ListModel {
id: root id: root
function enqueue(event) { function enqueue(request) {
root.append(event); root.append(request);
} }
function dequeue() { function dequeue() {
@ -16,4 +16,15 @@ ListModel {
} }
return null; return null;
} }
/// returns null if not found
function findRequest(topic, id) {
for (var i = 0; i < root.count; i++) {
let entry = root.get(i)
if (entry.topic === topic && entry.id === id) {
return entry;
}
}
return null;
}
} }

View File

@ -2052,6 +2052,11 @@ Item {
Component.onCompleted: { Component.onCompleted: {
Global.walletConnectService = walletConnectService Global.walletConnectService = walletConnectService
} }
onDisplayToastMessage: (message, isErr) => {
Global.displayToastMessage(message, "", isErr ? "warning" : "checkmark-circle", false,
isErr ? Constants.ephemeralNotificationType.danger : Constants.ephemeralNotificationType.success, "")
}
} }
} }
} }

View File

@ -196,11 +196,14 @@ StatusDialog {
id: footer id: footer
leftButtons: ObjectModel { leftButtons: ObjectModel {
MaxFeesDisplay {} MaxFeesDisplay {
}
Item { Item {
width: 20 width: 20
} }
EstimatedTimeDisplay {} EstimatedTimeDisplay {
visible: !!root.estimatedTimeText
}
} }
rightButtons: ObjectModel { rightButtons: ObjectModel {
@ -218,8 +221,6 @@ StatusDialog {
height: 44 height: 44
text: qsTr("Sign") text: qsTr("Sign")
enabled: false
onClicked: { onClicked: {
root.sign() root.sign()
} }
@ -228,21 +229,32 @@ StatusDialog {
} }
component MaxFeesDisplay: ColumnLayout { component MaxFeesDisplay: ColumnLayout {
visible: root.maxFeesText
StatusBaseText { StatusBaseText {
text: qsTr("Max fees:") text: qsTr("Max fees:")
font.pixelSize: 12 font.pixelSize: 12
color: Theme.palette.directColor1 color: Theme.palette.directColor1
} }
StatusBaseText { StatusBaseText {
id: maxFeesDisplay
text: root.maxFeesText text: root.maxFeesText
visible: !!root.maxFeesText
font.pixelSize: 16 font.pixelSize: 16
font.weight: Font.DemiBold font.weight: Font.DemiBold
} }
StatusBaseText {
text: qsTr("No fees")
visible: !maxFeesDisplay.visible
font.pixelSize: maxFeesDisplay.font.pixelSize
font.weight: maxFeesDisplay.font.weight
}
} }
component EstimatedTimeDisplay: ColumnLayout { component EstimatedTimeDisplay: ColumnLayout {
visible: root.estimatedTimeText
StatusBaseText { StatusBaseText {
text: qsTr("Est. time:") text: qsTr("Est. time:")
font.pixelSize: 12 font.pixelSize: 12

View File

@ -9,11 +9,26 @@ QObject {
/// \c dappsJson serialized from status-go.wallet.GetDapps /// \c dappsJson serialized from status-go.wallet.GetDapps
signal dappsListReceived(string dappsJson) signal dappsListReceived(string dappsJson)
signal userAuthenticated(string topic, string id)
signal userAuthenticationFailed(string topic, string id)
signal sessionRequestExecuted(var payload, bool success)
function addWalletConnectSession(sessionJson) { function addWalletConnectSession(sessionJson) {
controller.addWalletConnectSession(sessionJson) controller.addWalletConnectSession(sessionJson)
} }
function authenticateUser(topic, id, address) {
let ok = controller.authenticateUser(topic, id, address)
if(!ok) {
root.userAuthenticationFailed()
}
}
function signMessage(message) {
// TODO #14927 implement me
root.sessionRequestExecuted(message, true)
}
/// \c getDapps triggers an async response to \c dappsListReceived /// \c getDapps triggers an async response to \c dappsListReceived
function getDapps() { function getDapps() {
return controller.getDapps() return controller.getDapps()
@ -26,5 +41,13 @@ QObject {
function onDappsListReceived(dappsJson) { function onDappsListReceived(dappsJson) {
root.dappsListReceived(dappsJson) root.dappsListReceived(dappsJson)
} }
function onUserAuthenticationResult(topic, id, success) {
if (success) {
root.userAuthenticated(topic, id)
} else {
root.userAuthenticationFailed(topic, id)
}
}
} }
} }