fix(WC): Refactor dapps service to work with multiple SDKs

This PR is refactoring the dapps service to avoid code duplication between SDKs and also to avoid overlapping requests/responses.
It brings Browser Connect inline with Wallet Connect in terms of session management and sign transactions.

New architecture:

WalletConnectService becomes DAppsService. Its responsibility is to provide dapp access to the app. This is the component currently used by the UI
What does it do:
1. Provide dapp APIs line connect, disconnect, session requests etc
2. Spawn app notifications on dapp events
3. Timeout requests if the dapp does not respons

DAppsRequestHandler becomes DAppsModule. This component is consumed by the DAppService. Its responsibility is to aggregate all the building blocks for the dapps, but does not control any of the dapp features or consume the SDKs requests.
What does it do:
1. Aggregate all the building blocks for dapps (currently known as plugins)

DAppConnectionsPlugin - This component provides the session management features line connect, disconnect and provide a model with the connected dapps.
SignRequestPlugin - This component provides the sign request management. It receives the sign request from the dapp, translates it to what Status understands and manages the lifecycle of the request.
This commit is contained in:
Alex Jbanca 2024-11-07 11:10:10 +02:00
parent 1ecd960cb2
commit 00fb1ff60a
No known key found for this signature in database
GPG Key ID: 6004079575C21C5D
38 changed files with 3311 additions and 1133 deletions

View File

@ -34,8 +34,8 @@ QtObject:
proc approveConnectResponse*(self: Controller, payload: string, error: bool) {.signal.} proc approveConnectResponse*(self: Controller, payload: string, error: bool) {.signal.}
proc rejectConnectResponse*(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 approveTransactionResponse*(self: Controller, topic: string, requestId: string, error: bool) {.signal.}
proc rejectTransactionResponse*(self: Controller, requestId: string, error: bool) {.signal.} proc rejectTransactionResponse*(self: Controller, topic: string, requestId: string, error: bool) {.signal.}
proc newController*(service: connector_service.Service, events: EventEmitter): Controller = proc newController*(service: connector_service.Service, events: EventEmitter): Controller =
new(result, delete) new(result, delete)
@ -114,15 +114,15 @@ QtObject:
result = self.service.rejectDappConnect(requestId) result = self.service.rejectDappConnect(requestId)
self.rejectConnectResponse(requestId, not result) self.rejectConnectResponse(requestId, not result)
proc approveTransaction*(self: Controller, requestId: string, signature: string): bool {.slot.} = proc approveTransaction*(self: Controller, sessionTopic: string, requestId: string, signature: string): bool {.slot.} =
let hash = utils.createHash(signature) let hash = utils.createHash(signature)
result = self.service.approveTransactionRequest(requestId, hash) result = self.service.approveTransactionRequest(requestId, hash)
self.approveTransactionResponse(requestId, not result) self.approveTransactionResponse(sessionTopic, requestId, not result)
proc rejectTransaction*(self: Controller, requestId: string): bool {.slot.} = proc rejectTransaction*(self: Controller, sessionTopic: string, requestId: string): bool {.slot.} =
result = self.service.rejectTransactionSigning(requestId) result = self.service.rejectTransactionSigning(requestId)
self.rejectTransactionResponse(requestId, not result) self.rejectTransactionResponse(sessionTopic, requestId, not result)
proc disconnect*(self: Controller, dAppUrl: string): bool {.slot.} = proc disconnect*(self: Controller, dAppUrl: string): bool {.slot.} =
result = self.service.recallDAppPermission(dAppUrl) result = self.service.recallDAppPermission(dAppUrl)

View File

@ -6,6 +6,7 @@ import Qt.labs.settings 1.0
import QtTest 1.15 import QtTest 1.15
import QtQml.Models 2.14 import QtQml.Models 2.14
import StatusQ 0.1
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Core.Backpressure 0.1 import StatusQ.Core.Backpressure 0.1
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
@ -64,13 +65,13 @@ Item {
spacing: 8 spacing: 8
readonly property var wcService: walletConnectService readonly property var wcService: dappsService
loginType: Constants.LoginType.Biometrics loginType: Constants.LoginType.Biometrics
selectedAccountAddress: "" selectedAccountAddress: ""
model: wcService.dappsModel model: wcService.dappsModel
accountsModel: wcService.validAccounts accountsModel: dappModule.accountsModel
networksModel: wcService.flatNetworks networksModel: dappModule.networksModel
sessionRequestsModel: wcService.sessionRequestsModel sessionRequestsModel: wcService.sessionRequestsModel
enabled: wcService.isServiceOnline enabled: wcService.isServiceOnline
@ -127,7 +128,7 @@ Item {
Layout.preferredWidth: 20 Layout.preferredWidth: 20
Layout.preferredHeight: Layout.preferredWidth Layout.preferredHeight: Layout.preferredWidth
radius: Layout.preferredWidth / 2 radius: Layout.preferredWidth / 2
color: walletConnectService.wcSDK.sdkReady ? "green" : "red" color: dappModule.wcSdk.sdkReady ? "green" : "red"
} }
} }
@ -166,7 +167,7 @@ Item {
ListView { ListView {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: Math.min(50, contentHeight) Layout.preferredHeight: Math.min(50, contentHeight)
model: walletConnectService.sessionRequestsModel model: dappsService.sessionRequestsModel
delegate: RowLayout { delegate: RowLayout {
StatusBaseText { StatusBaseText {
text: SQUtils.Utils.elideAndFormatWalletAddress(model.topic, 6, 4) text: SQUtils.Utils.elideAndFormatWalletAddress(model.topic, 6, 4)
@ -208,7 +209,7 @@ Item {
NetworkFilter { NetworkFilter {
id: networkFilter id: networkFilter
flatNetworks: walletConnectService.walletRootStore.filteredFlatModel flatNetworks: dappModule.networksModel
} }
// spacer // spacer
@ -228,7 +229,7 @@ Item {
text: "WC feature flag" text: "WC feature flag"
checked: true checked: true
onCheckedChanged: { onCheckedChanged: {
walletConnectService.walletConnectFeatureEnabled = checked dappsService.walletConnectFeatureEnabled = checked
} }
} }
@ -236,7 +237,7 @@ Item {
text: "Connector feature flag" text: "Connector feature flag"
checked: true checked: true
onCheckedChanged: { onCheckedChanged: {
walletConnectService.connectorFeatureEnabled = checked dappsService.connectorFeatureEnabled = checked
} }
} }
} }
@ -307,7 +308,7 @@ Item {
let modals = StoryBook.InspectionUtils.findVisualsByTypeName(dappsWorkflow, "PairWCModal") let modals = StoryBook.InspectionUtils.findVisualsByTypeName(dappsWorkflow, "PairWCModal")
if (modals.length === 1) { if (modals.length === 1) {
let buttons = StoryBook.InspectionUtils.findVisualsByTypeName(modals[0].footer, "StatusButton") let buttons = StoryBook.InspectionUtils.findVisualsByTypeName(modals[0].footer, "StatusButton")
if (buttons.length === 1 && buttons[0].enabled && walletConnectService.wcSDK.sdkReady) { if (buttons.length === 1 && buttons[0].enabled && dappModule.wcSdk.sdkReady) {
d.activeTestCase = d.noTestCase d.activeTestCase = d.noTestCase
buttons[0].clicked() buttons[0].clicked()
return return
@ -342,14 +343,14 @@ Item {
StatusButton { StatusButton {
text: qsTr("Reject") text: qsTr("Reject")
onClicked: { onClicked: {
walletConnectService.store.userAuthenticationFailed(authMockDialog.topic, authMockDialog.id) dappModule.store.userAuthenticationFailed(authMockDialog.topic, authMockDialog.id)
authMockDialog.close() authMockDialog.close()
} }
} }
StatusButton { StatusButton {
text: qsTr("Authenticate") text: qsTr("Authenticate")
onClicked: { onClicked: {
walletConnectService.store.userAuthenticated(authMockDialog.topic, authMockDialog.id, "0x1234567890", "123") dappModule.store.userAuthenticated(authMockDialog.topic, authMockDialog.id, "0x1234567890", "123")
authMockDialog.close() authMockDialog.close()
} }
} }
@ -357,17 +358,35 @@ Item {
} }
} }
WalletConnectService { DAppsModule {
id: walletConnectService id: dappModule
wcSdk: WalletConnectSDK {
wcSDK: WalletConnectSDK { enabled: settings.enableSDK && dappsService.walletConnectFeatureEnabled
enableSdk: settings.enableSDK
projectId: projectIdText.projectId projectId: projectIdText.projectId
} }
blockchainNetworksDown: networkFilter.selection bcSdk: DappsConnectorSDK {
enabled: false
projectId: projectIdText.projectId
networksModel: dappModule.networksModel
accountsModel: dappModule.accountsModel
store: SharedStores.BrowserConnectStore {
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 topic, string requestId, bool error)
signal rejectTransactionResponse(string topic, string requestId, bool error)
}
}
store: SharedStores.DAppsStore { store: SharedStores.DAppsStore {
signal dappsListReceived(string dappsJson) signal dappsListReceived(string dappsJson)
signal userAuthenticated(string topic, string id, string password, string pin) signal userAuthenticated(string topic, string id, string password, string pin)
@ -485,25 +504,29 @@ Item {
} }
} }
walletRootStore: SQUtils.QObject { currenciesStore: SharedStores.CurrenciesStore {}
property var filteredFlatModel: SortFilterProxyModel { groupedAccountAssetsModel: GroupedAccountsAssetsModel {}
accountsModel: customAccountsModel.count > 0 ? customAccountsModel : defaultAccountsModel
networksModel: SortFilterProxyModel {
sourceModel: NetworksModel.flatNetworks sourceModel: NetworksModel.flatNetworks
proxyRoles: [
FastExpressionRole {
name: "isOnline"
expression: !networkFilter.selection.map(Number).includes(model.chainId)
expectedRoles: "chainId"
}
]
filters: ValueFilter { roleName: "isTest"; value: settings.testNetworks; } filters: ValueFilter { roleName: "isTest"; value: settings.testNetworks; }
} }
property var accounts: customAccountsModel.count > 0 ? customAccountsModel : defaultAccountsModel
readonly property ListModel nonWatchAccounts: accounts
readonly property SharedStores.CurrenciesStore currencyStore: SharedStores.CurrenciesStore {}
readonly property WalletStore.WalletAssetsStore walletAssetsStore: WalletStore.WalletAssetsStore {
// Silence warnings
assetsWithFilteredBalances: ListModel {}
// Name mismatch between storybook and production
readonly property var groupedAccountAssetsModel: groupedAccountsAssetsModel
} }
readonly property string selectedAddress: "" DAppsService {
} id: dappsService
dappsModule: dappModule
accountsModel: customAccountsModel.count > 0 ? customAccountsModel : defaultAccountsModel
selectedAddress: ""
onDisplayToastMessage: (message, isErr) => { onDisplayToastMessage: (message, isErr) => {
if(isErr) { if(isErr) {
console.log(`Storybook.displayToastMessage(${message}, "", "warning", false, Constants.ephemeralNotificationType.danger, "")`) console.log(`Storybook.displayToastMessage(${message}, "", "warning", false, Constants.ephemeralNotificationType.danger, "")`)

View File

@ -0,0 +1,103 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import AppLayouts.Wallet.services.dapps.types 1.0
SplitView {
id: root
orientation: Qt.Horizontal
readonly property string sign: "{\n\
\"id\": 1730473461432473,\n\
\"params\": {\n\
\"chainId\": \"eip155:1\",\n\
\"request\": {\n\
\"expiryTimestamp\": 1730473761,\n\
\"method\": \"personal_sign\",\n\
\"params\": [\n\
\"0x4d7920656d61696c206973206a6f686e40646f652e636f6d202d2031373330343733343631343331\",\n\
\"0x8b6950bb8a74489a83e6a1281e3aa008f02bf368\"\n\
]\n\
},\n\
\"topic\": \"3a9a320f8fc8e7a814895b148911373ba7df58c176ddca989f0e72ea1f9b8148\",\n\
\"verifyContext\": {\n\
\"verified\": {\n\
\"isScam\": false,\n\
\"origin\": \"https://react-app.walletconnect.com\",\n\
\"validation\": \"VALID\",\n\
\"verifyUrl\": \"https://verify.walletconnect.org\"\n\
}\n\
}\n\
}\n\
}"
readonly property string transaction: "{\n\
\"id\": 1730473547658704,\n\
\"params\": {\n\
\"chainId\": \"eip155:10\",\n\
\"request\": {\n\
\"expiryTimestamp\": 1730473847,\n\
\"method\": \"eth_sendTransaction\",\n\
\"params\": [\n\
{\n\
\"data\": \"0x\",\n\
\"from\": \"0x8b6950bb8a74489a83e6a1281e3aa008f02bf368\",\n\
\"gasLimit\": \"0x5208\",\n\
\"gasPrice\": \"0x0f437c\",\n\
\"nonce\": \"0x4e\",\n\
\"to\": \"0x8b6950bb8a74489a83e6a1281e3aa008f02bf368\",\n\
\"value\": \"0x00\"\n\
}\n\
]\n\
}\n\
},\n\
\"topic\": \"3a9a320f8fc8e7a814895b148911373ba7df58c176ddca989f0e72ea1f9b8148\",\n\
\"verifyContext\": {\n\
\"verified\": {\n\
\"isScam\": false,\n\
\"origin\": \"https://react-app.walletconnect.com\",\n\
\"validation\": \"VALID\",\n\
\"verifyUrl\": \"https://verify.walletconnect.org\"\n\
}\n\
}\n\
}"
ScrollView {
SplitView.fillHeight: true
SplitView.fillWidth: true
TextArea {
id: result
text: "Result: " + JSON.stringify(SessionRequest.parse(JSON.parse(textEdit.text.replace(/\\n/g, "\n"))), undefined, 2)
readOnly: true
}
}
ColumnLayout {
SplitView.fillHeight: true
SplitView.fillWidth: true
SplitView.preferredWidth: root.width / 2
Label {
text: "Paste the event here to simulate the session request parsing"
font.bold: true
}
Rectangle {
Layout.fillWidth: true
height: 2
color: "black"
}
TextArea {
id: textEdit
Layout.fillHeight: true
Layout.fillWidth: true
text: root.transaction
onTextChanged: text = JSON.stringify(JSON.parse(text.replace(/\\/g, "")), undefined, 2)
}
ComboBox {
id: comboBox
Layout.fillWidth: true
Layout.fillHeight: true
model: ["sign", "transaction"]
currentIndex: 0
onCurrentIndexChanged: textEdit.text = root[comboBox.currentText]
}
}
}

View File

@ -0,0 +1,98 @@
import QtQuick 2.15
import QtTest 1.15
import AppLayouts.Wallet.services.dapps 1.0
Item {
id: root
Component {
id: bcDAppsProviderComponent
BCDappsProvider {
id: bcDAppsProvider
readonly property SignalSpy connectedSpy: SignalSpy { target: bcDAppsProvider; signalName: "connected" }
readonly property SignalSpy disconnectedSpy: SignalSpy { target: bcDAppsProvider; signalName: "disconnected" }
bcSDK: WalletConnectSDKBase {
enabled: true
projectId: ""
property var activeSessions: {}
getActiveSessions: function(callback) {
callback(activeSessions)
}
}
}
}
function buildSession(dappUrl, dappName, dappIcon, proposalId, account, chains) {
let sessionTemplate = (dappUrl, dappName, dappIcon, proposalId, eipAccount, eipChains) => {
return {
peer: {
metadata: {
description: "-",
icons: [
dappIcon
],
name: dappName,
url: dappUrl
}
},
namespaces: {
eip155: {
accounts: [eipAccount],
chains: eipChains
}
},
pairingTopic: proposalId,
topic: dappUrl
};
}
const eipAccount = account ? `eip155:${account}` : ""
const eipChains = chains ? chains.map((chain) => `eip155:${chain}`) : []
return sessionTemplate(dappUrl, dappName, dappIcon, proposalId, eipAccount, eipChains)
}
TestCase {
id: bcDAppsProviderTest
property BCDappsProvider componentUnderTest: null
function init() {
componentUnderTest = createTemporaryObject(bcDAppsProviderComponent, root)
}
function test_addRemoveSession() {
const newSession = buildSession("https://example.com", "Example", "https://example.com/icon.png", "123", "0x123", ["1"])
componentUnderTest.bcSDK.approveSessionResult("requestID", newSession, null)
compare(componentUnderTest.connectedSpy.count, 1, "Connected signal should be emitted once")
compare(componentUnderTest.connectedSpy.signalArguments[0][0], "requestID", "Connected signal should have correct proposalId")
compare(componentUnderTest.connectedSpy.signalArguments[0][1], "https://example.com", "Connected signal should have correct topic")
compare(componentUnderTest.connectedSpy.signalArguments[0][2], "https://example.com", "Connected signal should have correct dAppUrl")
const dapp = componentUnderTest.getByTopic("https://example.com")
verify(!!dapp, "DApp should be found")
compare(dapp.name, "Example", "DApp should have correct name")
compare(dapp.url, "https://example.com", "DApp should have correct url")
compare(dapp.iconUrl, "https://example.com/icon.png", "DApp should have correct iconUrl")
compare(dapp.topic, "https://example.com", "DApp should have correct topic")
compare(dapp.connectorId, componentUnderTest.connectorId, "DApp should have correct connectorId")
compare(dapp.accountAddresses.count, 1, "DApp should have correct accountAddresses count")
compare(dapp.accountAddresses.get(0).address, "0x123", "DApp should have correct accountAddresses address")
compare(dapp.rawSessions.count, 1, "DApp should have correct rawSessions count")
componentUnderTest.bcSDK.sessionDelete("https://example.com", "")
compare(componentUnderTest.disconnectedSpy.count, 1, "Disconnected signal should be emitted once")
compare(componentUnderTest.disconnectedSpy.signalArguments[0][0], "https://example.com", "Disconnected signal should have correct topic")
compare(componentUnderTest.disconnectedSpy.signalArguments[0][1], "https://example.com", "Disconnected signal should have correct dAppUrl")
}
function test_disabledSDK() {
componentUnderTest.bcSDK.enabled = false
componentUnderTest.bcSDK.approveSessionResult("requestID", buildSession("https://example.com", "Example", "https://example.com/icon.png", "123", "0x123", ["1"]), "")
compare(componentUnderTest.connectedSpy.count, 0, "Connected signal should not be emitted")
componentUnderTest.bcSDK.sessionDelete("https://example.com", "")
compare(componentUnderTest.disconnectedSpy.count, 0, "Disconnected signal should not be emitted")
}
}
}

View File

@ -25,19 +25,31 @@ Item {
width: 600 width: 600
height: 400 height: 400
function mockActiveSession(accountsModel, networksModel, sdk, topic) {
const account = accountsModel.get(1)
const networks = ModelUtils.modelToFlatArray(networksModel, "chainId")
const requestId = 1717149885151715
const session = JSON.parse(Testing.formatApproveSessionResponse(networks, [account.address]))
const sessionProposal = JSON.parse(Testing.formatSessionProposal())
sdk.sessionProposal(sessionProposal)
// Expect to have calls to getActiveSessions from service initialization
const prevRequests = sdk.getActiveSessionsCallbacks.length
sdk.approveSessionResult(sessionProposal.id, session, null)
// Service might trigger a sessionRequest event following the getActiveSessions call
const callback = sdk.getActiveSessionsCallbacks[prevRequests].callback
callback({"b536a": session})
return session
}
function mockSessionRequestEvent(tc, sdk, accountsModel, networksModel) { function mockSessionRequestEvent(tc, sdk, accountsModel, networksModel) {
const account = accountsModel.get(1) const account = accountsModel.get(1)
const network = networksModel.get(1) const network = networksModel.get(1)
const topic = "b536a" const topic = "b536a"
const requestId = 1717149885151715 const requestId = 1717149885151715
const session = mockActiveSession(accountsModel, networksModel, sdk, topic)
const request = buildSessionRequestResolved(tc, account.address, network.chainId, topic, requestId) const request = buildSessionRequestResolved(tc, account.address, network.chainId, topic, requestId)
// Expect to have calls to getActiveSessions from service initialization
const prevRequests = sdk.getActiveSessionsCallbacks.length
sdk.sessionRequestEvent(request.event)
// Service might trigger a sessionRequest event following the getActiveSessions call
const callback = sdk.getActiveSessionsCallbacks[prevRequests].callback
const session = JSON.parse(Testing.formatApproveSessionResponse([network.chainId, 7], [account.address]))
callback({"b536a": session})
return {sdk, session, account, network, topic, request} return {sdk, session, account, network, topic, request}
} }
@ -58,10 +70,12 @@ Item {
data: "hello world", data: "hello world",
preparedData: "hello world", preparedData: "hello world",
expirationTimestamp: (Date.now() + 10000) / 1000, expirationTimestamp: (Date.now() + 10000) / 1000,
sourceId: Constants.DAppConnectors.WalletConnect sourceId: Constants.DAppConnectors.WalletConnect,
dappName: "Test DApp",
dappUrl: "https://test.dapp",
dappIcon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAcCAYAAACdz7SqAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAM2SURBVHgBtVbNbtNAEJ7ZpBQ4pRGF9kQqWqkBRNwnwLlxI9y4NX2CiiOntE9QeINw49a8QdwT3NhKQCKaSj4WUVXmABRqe5hxE+PGTuyk5ZOSXe/ftzs/3y5CBiw/NEzw/cdAaCJAifgXdCA4QGAjggbEvbMf0LJt7aSth6lkHjW4akIG8GI2/1k5H7e7XW2PGRdHqWQU8jdoNytZIrnC7YNPupnUnxtuWF01SjhD77hqwPQosNlrxdt34OTb172xpELoKvrA1QW4EqCZRJyLEnpI7ZBQggThlGvXYVLI3HAeE88vfj85Pno/6FaDiqeoEUZlMA9bvc/7cxyxVa6/SeM5j2Tcdn/hnHsNly520s7KAyN0V17+7pWNGhHVhxYJTNLraosLi8e0kMBxT0FH00IW830oeT/ButBertjRQ5BPO1xUQ1IE2oQUHHZ0K6mdI1RzoSEdpqRg76O2lPgSElKDdz919JYMoxA95QDow7qUykWoxTo5z2YIXsGUsLV2CPD1cDu7MODiQKKnsVmI1jhFyQJvFrb6URxFQWJAYYIZSEF6tKZATitFQpehEm1PkCraWYCE+8Nt5ENBwX8EAd2NNaKQxu0ukVuCqwATQHwnjhphShMuiSAVKZ527E6bzYt78Q3SulxvcAm44K8ntXMqagmkJDUpzNwMZGsqBDqLuDXcLvkvqajcWWgm+ZUI6svlym5fsbITlh9tsgi0Ezs5//vkMtBocqSJOZw84ZrHPiXFJ6UwECx5A/FbqNXX2hAiefkzqCNRha1Wi8yJgddeCk4qHzkK1aMgdypfshYRbkTGm3z0Rs6LW0REgDXVEMuMI0TE5kDlgkv8+PjIKRYXfzPxEyH2EYzDzv7L4q1FHsvpg8Gkt186OlGp5uYXZMjzkYS8txwfQnj63//APmzDIF1yWJVrCDJgeZVfjTjCj0KicC3qlny0053FZ/k/PFnyy6P2yv1Kk1T/1eCGF/pEYCncGI6DCzIo/uGnRvg8CfzE5MEPoQGT4Pz5Uj3oxp+hMe0V4oOOrssOMfmWyMJo5X1cG2WZkYIvO2Tn85sGXwg5B5Q9kiKMas5DntPr6Oq4+/gvs8hkkbAzoC8AAAAASUVORK5CYII="
}) })
requestItem.resolveDappInfoFromSession({peer: {metadata: {name: "Test DApp", url: "https://test.dapp", icons: ["data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAcCAYAAACdz7SqAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAM2SURBVHgBtVbNbtNAEJ7ZpBQ4pRGF9kQqWqkBRNwnwLlxI9y4NX2CiiOntE9QeINw49a8QdwT3NhKQCKaSj4WUVXmABRqe5hxE+PGTuyk5ZOSXe/ftzs/3y5CBiw/NEzw/cdAaCJAifgXdCA4QGAjggbEvbMf0LJt7aSth6lkHjW4akIG8GI2/1k5H7e7XW2PGRdHqWQU8jdoNytZIrnC7YNPupnUnxtuWF01SjhD77hqwPQosNlrxdt34OTb172xpELoKvrA1QW4EqCZRJyLEnpI7ZBQggThlGvXYVLI3HAeE88vfj85Pno/6FaDiqeoEUZlMA9bvc/7cxyxVa6/SeM5j2Tcdn/hnHsNly520s7KAyN0V17+7pWNGhHVhxYJTNLraosLi8e0kMBxT0FH00IW830oeT/ButBertjRQ5BPO1xUQ1IE2oQUHHZ0K6mdI1RzoSEdpqRg76O2lPgSElKDdz919JYMoxA95QDow7qUykWoxTo5z2YIXsGUsLV2CPD1cDu7MODiQKKnsVmI1jhFyQJvFrb6URxFQWJAYYIZSEF6tKZATitFQpehEm1PkCraWYCE+8Nt5ENBwX8EAd2NNaKQxu0ukVuCqwATQHwnjhphShMuiSAVKZ527E6bzYt78Q3SulxvcAm44K8ntXMqagmkJDUpzNwMZGsqBDqLuDXcLvkvqajcWWgm+ZUI6svlym5fsbITlh9tsgi0Ezs5//vkMtBocqSJOZw84ZrHPiXFJ6UwECx5A/FbqNXX2hAiefkzqCNRha1Wi8yJgddeCk4qHzkK1aMgdypfshYRbkTGm3z0Rs6LW0REgDXVEMuMI0TE5kDlgkv8+PjIKRYXfzPxEyH2EYzDzv7L4q1FHsvpg8Gkt186OlGp5uYXZMjzkYS8txwfQnj63//APmzDIF1yWJVrCDJgeZVfjTjCj0KicC3qlny0053FZ/k/PFnyy6P2yv1Kk1T/1eCGF/pEYCncGI6DCzIo/uGnRvg8CfzE5MEPoQGT4Pz5Uj3oxp+hMe0V4oOOrssOMfmWyMJo5X1cG2WZkYIvO2Tn85sGXwg5B5Q9kiKMas5DntPr6Oq4+/gvs8hkkbAzoC8AAAAASUVORK5CYII="]}}})
return requestItem return requestItem
} }
@ -70,6 +84,7 @@ Item {
WalletConnectSDKBase { WalletConnectSDKBase {
property bool sdkReady: true property bool sdkReady: true
enabled: true
property var getActiveSessionsCallbacks: [] property var getActiveSessionsCallbacks: []
getActiveSessions: function(callback) { getActiveSessions: function(callback) {
@ -106,7 +121,7 @@ Item {
Component { Component {
id: serviceComponent id: serviceComponent
WalletConnectService { DAppsService {
property var onApproveSessionResultTriggers: [] property var onApproveSessionResultTriggers: []
onApproveSessionResult: function(session, error) { onApproveSessionResult: function(session, error) {
onApproveSessionResultTriggers.push({session, error}) onApproveSessionResultTriggers.push({session, error})
@ -121,7 +136,6 @@ Item {
onPairingValidated: function(validationState) { onPairingValidated: function(validationState) {
onPairingValidatedTriggers.push({validationState}) onPairingValidatedTriggers.push({validationState})
} }
blockchainNetworksDown: []
} }
} }
@ -243,7 +257,7 @@ Item {
} }
// Used by tst_balanceCheck // Used by tst_balanceCheck
ListElement { ListElement {
chainId: 421613 chainId: 421614
layer: 2 layer: 2
isOnline: true isOnline: true
} }
@ -294,37 +308,34 @@ Item {
} }
Component { Component {
id: dappsRequestHandlerComponent id: dappsModuleComponent
DAppsRequestHandler { DAppsModule {
currenciesStore: CurrenciesStore {} currenciesStore: CurrenciesStore {}
assetsStore: assetsStoreMock groupedAccountAssetsModel: assetsStoreMock.groupedAccountAssetsModel
} }
} }
TestCase { TestCase {
id: requestHandlerTest id: dappsModuleTest
name: "DAppsRequestHandler" name: "DAppsModuleTest"
// Ensure mocked GroupedAccountsAssetsModel is properly initialized // Ensure mocked GroupedAccountsAssetsModel is properly initialized
when: windowShown when: windowShown
property DAppsRequestHandler handler: null property DAppsModule handler: null
SignalSpy {
id: displayToastMessageSpy
target: requestHandlerTest.handler
signalName: "onDisplayToastMessage"
}
function init() { function init() {
let walletStore = createTemporaryObject(walletStoreComponent, root) let walletStore = createTemporaryObject(walletStoreComponent, root)
verify(!!walletStore) verify(!!walletStore)
let sdk = createTemporaryObject(sdkComponent, root, { projectId: "12ab" }) let sdk = createTemporaryObject(sdkComponent, root, { projectId: "12ab", enabled: true })
verify(!!sdk) verify(!!sdk)
let bcSdk = createTemporaryObject(sdkComponent, root, { projectId: "12ab", enabled: false })
verify(!!bcSdk)
let store = createTemporaryObject(dappsStoreComponent, root) let store = createTemporaryObject(dappsStoreComponent, root)
verify(!!store) verify(!!store)
handler = createTemporaryObject(dappsRequestHandlerComponent, root, { handler = createTemporaryObject(dappsModuleComponent, root, {
sdk: sdk, wcSdk: sdk,
bcSdk: bcSdk,
store: store, store: store,
accountsModel: walletStore.nonWatchAccounts, accountsModel: walletStore.nonWatchAccounts,
networksModel: walletStore.filteredFlatModelWithOnlineStat networksModel: walletStore.filteredFlatModelWithOnlineStat
@ -333,13 +344,9 @@ Item {
sdk.getActiveSessionsCallbacks = [] sdk.getActiveSessionsCallbacks = []
} }
function cleanup() {
displayToastMessageSpy.clear()
}
function test_TestAuthentication() { function test_TestAuthentication() {
let td = mockSessionRequestEvent(this, handler.sdk, handler.accountsModel, handler.networksModel) let td = mockSessionRequestEvent(this, handler.wcSdk, handler.accountsModel, handler.networksModel)
handler.sdk.sessionRequestEvent(td.request.event) handler.wcSdk.sessionRequestEvent(td.request.event)
let request = handler.requestsModel.findById(td.request.requestId) let request = handler.requestsModel.findById(td.request.requestId)
request.accept() request.accept()
compare(handler.store.authenticateUserCalls.length, 1, "expected a call to store.authenticateUser") compare(handler.store.authenticateUserCalls.length, 1, "expected a call to store.authenticateUser")
@ -351,7 +358,7 @@ Item {
} }
function test_onSessionRequestEventDifferentCaseForAddress() { function test_onSessionRequestEventDifferentCaseForAddress() {
const sdk = handler.sdk const sdk = handler.wcSdk
const testAddressUpper = "0x3A" const testAddressUpper = "0x3A"
const chainId = 2 const chainId = 2
@ -362,6 +369,7 @@ Item {
const session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic)) const session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic))
// Expect to have calls to getActiveSessions from service initialization // Expect to have calls to getActiveSessions from service initialization
const prevRequests = sdk.getActiveSessionsCallbacks.length const prevRequests = sdk.getActiveSessionsCallbacks.length
mockActiveSession(handler.accountsModel, handler.networksModel, sdk, topic)
sdk.sessionRequestEvent(session) sdk.sessionRequestEvent(session)
compare(sdk.getActiveSessionsCallbacks.length, 1, "expected DAppsRequestHandler call sdk.getActiveSessions") compare(sdk.getActiveSessionsCallbacks.length, 1, "expected DAppsRequestHandler call sdk.getActiveSessions")
@ -369,7 +377,7 @@ Item {
// Tests that the request is ignored if not in the current profile (don't have the PK for the address) // Tests that the request is ignored if not in the current profile (don't have the PK for the address)
function test_onSessionRequestEventMissingAddress() { function test_onSessionRequestEventMissingAddress() {
const sdk = handler.sdk const sdk = handler.wcSdk
const testAddressUpper = "0xY" const testAddressUpper = "0xY"
const chainId = 2 const chainId = 2
@ -378,12 +386,13 @@ Item {
const params = [`"${DAppsHelpers.strToHex(message)}"`, `"${testAddressUpper}"`] const params = [`"${DAppsHelpers.strToHex(message)}"`, `"${testAddressUpper}"`]
const topic = "b536a" const topic = "b536a"
const session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic)) const session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic))
// Expect to have calls to getActiveSessions from service initialization
const prevRequests = sdk.getActiveSessionsCallbacks.length mockActiveSession(handler.accountsModel, handler.networksModel, sdk, topic)
sdk.getActiveSessionsCallbacks = []
sdk.sessionRequestEvent(session) sdk.sessionRequestEvent(session)
compare(sdk.getActiveSessionsCallbacks.length, 0, "expected DAppsRequestHandler don't call sdk.getActiveSessions") compare(sdk.getActiveSessionsCallbacks.length, 0, "expected DAppsRequestHandler don't call sdk.getActiveSessions")
compare(sdk.rejectSessionRequestCalls.length, 0, "expected no call to service.rejectSessionRequest") compare(sdk.rejectSessionRequestCalls.length, 1, "expected to reject the request")
} }
function test_balanceCheck_data() { function test_balanceCheck_data() {
@ -434,7 +443,7 @@ Item {
} }
function test_balanceCheck(data) { function test_balanceCheck(data) {
let sdk = handler.sdk let sdk = handler.wcSdk
// Override the suggestedFees // Override the suggestedFees
if (!!data.maxFeePerGasM) { if (!!data.maxFeePerGasM) {
@ -456,6 +465,7 @@ Item {
}`] }`]
let topic = "b536a" let topic = "b536a"
let session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic)) let session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic))
mockActiveSession(handler.accountsModel, handler.networksModel, sdk, topic)
sdk.sessionRequestEvent(session) sdk.sessionRequestEvent(session)
compare(sdk.getActiveSessionsCallbacks.length, 1, "expected DAppsRequestHandler call sdk.getActiveSessions") compare(sdk.getActiveSessionsCallbacks.length, 1, "expected DAppsRequestHandler call sdk.getActiveSessions")
@ -473,7 +483,7 @@ Item {
} }
function test_sessionRequestExpiryInTheFuture() { function test_sessionRequestExpiryInTheFuture() {
const sdk = handler.sdk const sdk = handler.wcSdk
const testAddressUpper = "0x3A" const testAddressUpper = "0x3A"
const chainId = 2 const chainId = 2
const method = "personal_sign" const method = "personal_sign"
@ -486,6 +496,7 @@ Item {
// Expect to have calls to getActiveSessions from service initialization // Expect to have calls to getActiveSessions from service initialization
const prevRequests = sdk.getActiveSessionsCallbacks.length const prevRequests = sdk.getActiveSessionsCallbacks.length
mockActiveSession(handler.accountsModel, handler.networksModel, sdk, topic)
sdk.sessionRequestEvent(session) sdk.sessionRequestEvent(session)
verify(handler.requestsModel.count === 1, "expected a request to be added") verify(handler.requestsModel.count === 1, "expected a request to be added")
@ -496,7 +507,7 @@ Item {
function test_sessionRequestExpiryInThePast() function test_sessionRequestExpiryInThePast()
{ {
const sdk = handler.sdk const sdk = handler.wcSdk
const testAddressUpper = "0x3A" const testAddressUpper = "0x3A"
const chainId = 2 const chainId = 2
const method = "personal_sign" const method = "personal_sign"
@ -508,18 +519,18 @@ Item {
verify(session.params.request.expiryTimestamp < Date.now() / 1000, "expected expiryTimestamp to be in the past") verify(session.params.request.expiryTimestamp < Date.now() / 1000, "expected expiryTimestamp to be in the past")
mockActiveSession(handler.accountsModel, handler.networksModel, sdk, topic)
sdk.sessionRequestEvent(session) sdk.sessionRequestEvent(session)
verify(handler.requestsModel.count === 1, "expected a request to be added") verify(handler.requestsModel.count === 1, "expected a request to be added")
const request = handler.requestsModel.findRequest(topic, session.id) const request = handler.requestsModel.findRequest(topic, session.id)
verify(!!request, "expected request to be found") verify(!!request, "expected request to be found")
verify(request.isExpired(), "expected request to be expired") verify(request.isExpired(), "expected request to be expired")
verify(displayToastMessageSpy.count === 0, "no toast message should be displayed")
} }
function test_wcSignalsSessionRequestExpiry() function test_wcSignalsSessionRequestExpiry()
{ {
const sdk = handler.sdk const sdk = handler.wcSdk
const testAddressUpper = "0x3A" const testAddressUpper = "0x3A"
const chainId = 2 const chainId = 2
const method = "personal_sign" const method = "personal_sign"
@ -529,6 +540,8 @@ Item {
const session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic)) const session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic))
verify(session.params.request.expiryTimestamp > Date.now() / 1000, "expected expiryTimestamp to be in the future") verify(session.params.request.expiryTimestamp > Date.now() / 1000, "expected expiryTimestamp to be in the future")
mockActiveSession(handler.accountsModel, handler.networksModel, sdk, topic)
sdk.sessionRequestEvent(session) sdk.sessionRequestEvent(session)
const request = handler.requestsModel.findRequest(topic, session.id) const request = handler.requestsModel.findRequest(topic, session.id)
verify(!!request, "expected request to be found") verify(!!request, "expected request to be found")
@ -536,12 +549,11 @@ Item {
sdk.sessionRequestExpired(session.id) sdk.sessionRequestExpired(session.id)
verify(request.isExpired(), "expected request to be expired") verify(request.isExpired(), "expected request to be expired")
verify(displayToastMessageSpy.count === 0, "no toast message should be displayed")
} }
function test_acceptExpiredSessionRequest() function test_acceptExpiredSessionRequest()
{ {
const sdk = handler.sdk const sdk = handler.wcSdk
const testAddressUpper = "0x3A" const testAddressUpper = "0x3A"
const chainId = 2 const chainId = 2
const method = "personal_sign" const method = "personal_sign"
@ -553,11 +565,11 @@ Item {
verify(session.params.request.expiryTimestamp < Date.now() / 1000, "expected expiryTimestamp to be in the past") verify(session.params.request.expiryTimestamp < Date.now() / 1000, "expected expiryTimestamp to be in the past")
mockActiveSession(handler.accountsModel, handler.networksModel, sdk, topic)
sdk.sessionRequestEvent(session) sdk.sessionRequestEvent(session)
verify(handler.requestsModel.count === 1, "expected a request to be added") verify(handler.requestsModel.count === 1, "expected a request to be added")
const request = handler.requestsModel.findRequest(topic, session.id) const request = handler.requestsModel.findRequest(topic, session.id)
request.resolveDappInfoFromSession({peer: {metadata: {name: "Test DApp", url: "https://test.dapp", icons:[]}}})
verify(!!request, "expected request to be found") verify(!!request, "expected request to be found")
verify(request.isExpired(), "expected request to be expired") verify(request.isExpired(), "expected request to be expired")
verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest") verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest")
@ -567,13 +579,11 @@ Item {
handler.store.userAuthenticated(topic, session.id, "1234", "", message) handler.store.userAuthenticated(topic, session.id, "1234", "", message)
verify(sdk.rejectSessionRequestCalls.length === 1, "expected a call to sdk.rejectSessionRequest") verify(sdk.rejectSessionRequestCalls.length === 1, "expected a call to sdk.rejectSessionRequest")
sdk.sessionRequestUserAnswerResult(topic, session.id, false, "") sdk.sessionRequestUserAnswerResult(topic, session.id, false, "")
verify(displayToastMessageSpy.count === 1, "expected a toast message to be displayed")
compare(displayToastMessageSpy.signalArguments[0][0], "test.dapp sign request timed out")
} }
function test_rejectExpiredSessionRequest() function test_rejectExpiredSessionRequest()
{ {
const sdk = handler.sdk const sdk = handler.wcSdk
const testAddressUpper = "0x3A" const testAddressUpper = "0x3A"
const chainId = 2 const chainId = 2
const method = "personal_sign" const method = "personal_sign"
@ -585,6 +595,7 @@ Item {
verify(session.params.request.expiryTimestamp < Date.now() / 1000, "expected expiryTimestamp to be in the past") verify(session.params.request.expiryTimestamp < Date.now() / 1000, "expected expiryTimestamp to be in the past")
mockActiveSession(handler.accountsModel, handler.networksModel, sdk, topic)
sdk.sessionRequestEvent(session) sdk.sessionRequestEvent(session)
verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest") verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest")
@ -597,7 +608,7 @@ Item {
function test_signFailedAuthOnExpiredRequest() function test_signFailedAuthOnExpiredRequest()
{ {
const sdk = handler.sdk const sdk = handler.wcSdk
const testAddressUpper = "0x3A" const testAddressUpper = "0x3A"
const chainId = 2 const chainId = 2
const method = "personal_sign" const method = "personal_sign"
@ -609,6 +620,7 @@ Item {
verify(session.params.request.expiryTimestamp < Date.now() / 1000, "expected expiryTimestamp to be in the past") verify(session.params.request.expiryTimestamp < Date.now() / 1000, "expected expiryTimestamp to be in the past")
mockActiveSession(handler.accountsModel, handler.networksModel, sdk, topic)
sdk.sessionRequestEvent(session) sdk.sessionRequestEvent(session)
verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest") verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest")
@ -621,14 +633,14 @@ Item {
} }
TestCase { TestCase {
id: walletConnectServiceTest id: dappsServiceTest
name: "WalletConnectService" name: "DAppsService"
property WalletConnectService service: null property DAppsService service: null
SignalSpy { SignalSpy {
id: connectDAppSpy id: connectDAppSpy
target: walletConnectServiceTest.service target: dappsServiceTest.service
signalName: "connectDApp" signalName: "connectDApp"
property var argPos: { property var argPos: {
@ -640,35 +652,52 @@ Item {
} }
} }
readonly property SignalSpy sessionRequestSpy: SignalSpy { function populateDAppData(topic) {
target: walletConnectServiceTest.service const dapp = {
signalName: "sessionRequest" topic,
name: Testing.dappName,
property var argPos: { url: Testing.dappUrl,
"id": 0 iconUrl: Testing.dappFirstIcon,
connectorId: 0,
accountAddresses: [{address: "0x123"}],
rawSessions: [{session: {topic}}]
} }
findChild(service.dappsModule, "DAppsModel").model.append(dapp)
} }
function init() { function init() {
let walletStore = createTemporaryObject(walletStoreComponent, root) let walletStore = createTemporaryObject(walletStoreComponent, root)
verify(!!walletStore) verify(!!walletStore)
let sdk = createTemporaryObject(sdkComponent, root, { projectId: "12ab" }) let sdk = createTemporaryObject(sdkComponent, root, { projectId: "12ab", enabled: true })
verify(!!sdk) verify(!!sdk)
let bcSdk = createTemporaryObject(sdkComponent, root, { projectId: "12ab", enabled: false })
let store = createTemporaryObject(dappsStoreComponent, root) let store = createTemporaryObject(dappsStoreComponent, root)
verify(!!store) verify(!!store)
service = createTemporaryObject(serviceComponent, root, {wcSDK: sdk, store: store, walletRootStore: walletStore}) let dappsModuleObj = createTemporaryObject(dappsModuleComponent, root, {
wcSdk: sdk,
bcSdk: bcSdk,
store: store,
accountsModel: walletStore.nonWatchAccounts,
networksModel: walletStore.filteredFlatModelWithOnlineStat
})
service = createTemporaryObject(serviceComponent, root, {
dappsModule: dappsModuleObj,
selectedAddress: "",
accountsModel: walletStore.nonWatchAccounts
})
verify(!!service) verify(!!service)
} }
function cleanup() { function cleanup() {
connectDAppSpy.clear() connectDAppSpy.clear()
sessionRequestSpy.clear()
} }
function testSetupPair(sessionProposalPayload) { function testSetupPair(sessionProposalPayload) {
let sdk = service.wcSDK let sdk = service.dappsModule.wcSdk
let walletStore = service.walletRootStore let accountsModel = service.dappsModule.accountsModel
let store = service.store let networksModel = service.dappsModule.networksModel
let store = service.dappsModule.store
service.pair("wc:12ab@1?bridge=https%3A%2F%2Fbridge.walletconnect.org&key=12ab") service.pair("wc:12ab@1?bridge=https%3A%2F%2Fbridge.walletconnect.org&key=12ab")
compare(sdk.pairCalled, 1, "expected a call to sdk.pair") compare(sdk.pairCalled, 1, "expected a call to sdk.pair")
@ -680,21 +709,21 @@ Item {
// All calls to SDK are expected as events to be made by the wallet connect SDK // All calls to SDK are expected as events to be made by the wallet connect SDK
let chainsForApproval = args.supportedNamespaces.eip155.chains let chainsForApproval = args.supportedNamespaces.eip155.chains
let networksArray = ModelUtils.modelToArray(walletStore.filteredFlatModel).map(entry => entry.chainId) let networksArray = ModelUtils.modelToArray(networksModel).map(entry => entry.chainId)
verify(networksArray.every(chainId => chainsForApproval.some(eip155Chain => eip155Chain === `eip155:${chainId}`)), verify(networksArray.every(chainId => chainsForApproval.some(eip155Chain => eip155Chain === `eip155:${chainId}`)),
"expect all the networks to be present") "expect all the networks to be present")
// We test here all accounts for one chain only, we have separate tests to validate that all accounts are present // We test here all accounts for one chain only, we have separate tests to validate that all accounts are present
let allAccountsForApproval = args.supportedNamespaces.eip155.accounts let allAccountsForApproval = args.supportedNamespaces.eip155.accounts
let accountsArray = ModelUtils.modelToArray(walletStore.nonWatchAccounts).map(entry => entry.address) let accountsArray = ModelUtils.modelToArray(accountsModel).map(entry => entry.address)
verify(accountsArray.every(address => allAccountsForApproval.some(eip155Address => eip155Address === `eip155:${networksArray[0]}:${address}`)), verify(accountsArray.every(address => allAccountsForApproval.some(eip155Address => eip155Address === `eip155:${networksArray[0]}:${address}`)),
"expect at least all accounts for the first chain to be present" "expect at least all accounts for the first chain to be present"
) )
return {sdk, walletStore, store, networksArray, accountsArray} return {sdk, store, networksArray, accountsArray, networksModel, accountsModel}
} }
function test_TestPairing() { function test_TestPairing() {
const {sdk, walletStore, store, networksArray, accountsArray} = testSetupPair(Testing.formatSessionProposal()) const {sdk, store, networksArray, accountsArray, networksModel, accountsModel} = testSetupPair(Testing.formatSessionProposal())
compare(sdk.buildApprovedNamespacesCalls.length, 1, "expected a call to sdk.buildApprovedNamespaces") compare(sdk.buildApprovedNamespacesCalls.length, 1, "expected a call to sdk.buildApprovedNamespaces")
let allApprovedNamespaces = JSON.parse(Testing.formatBuildApprovedNamespacesResult(networksArray, accountsArray)) let allApprovedNamespaces = JSON.parse(Testing.formatBuildApprovedNamespacesResult(networksArray, accountsArray))
@ -708,7 +737,7 @@ Item {
compare(connectArgs[connectDAppSpy.argPos.dappIcon], Testing.dappFirstIcon, "expected dappIcon to be set") compare(connectArgs[connectDAppSpy.argPos.dappIcon], Testing.dappFirstIcon, "expected dappIcon to be set")
verify(!!connectArgs[connectDAppSpy.argPos.key], "expected key to be set") verify(!!connectArgs[connectDAppSpy.argPos.key], "expected key to be set")
let selectedAccount = walletStore.nonWatchAccounts.get(1).address let selectedAccount = accountsModel.get(1).address
service.approvePairSession(connectArgs[connectDAppSpy.argPos.key], connectArgs[connectDAppSpy.argPos.dappChains], selectedAccount) service.approvePairSession(connectArgs[connectDAppSpy.argPos.key], connectArgs[connectDAppSpy.argPos.dappChains], selectedAccount)
compare(sdk.buildApprovedNamespacesCalls.length, 2, "expected a call to sdk.buildApprovedNamespaces") compare(sdk.buildApprovedNamespacesCalls.length, 2, "expected a call to sdk.buildApprovedNamespaces")
const approvedArgs = sdk.buildApprovedNamespacesCalls[1] const approvedArgs = sdk.buildApprovedNamespacesCalls[1]
@ -740,7 +769,7 @@ Item {
} }
function test_TestPairingUnsupportedNetworks() { function test_TestPairingUnsupportedNetworks() {
const {sdk, walletStore, store} = testSetupPair(Testing.formatSessionProposal()) const {sdk, store} = testSetupPair(Testing.formatSessionProposal())
const approvedArgs = sdk.buildApprovedNamespacesCalls[0] const approvedArgs = sdk.buildApprovedNamespacesCalls[0]
sdk.buildApprovedNamespacesResult(approvedArgs.id, {}, "Non conforming namespaces. approve() namespaces chains don't satisfy required namespaces") sdk.buildApprovedNamespacesResult(approvedArgs.id, {}, "Non conforming namespaces. approve() namespaces chains don't satisfy required namespaces")
@ -751,7 +780,7 @@ Item {
function test_SessionRequestMainFlow() { function test_SessionRequestMainFlow() {
// All calls to SDK are expected as events to be made by the wallet connect SDK // All calls to SDK are expected as events to be made by the wallet connect SDK
const sdk = service.wcSDK const sdk = service.dappsModule.wcSdk
const walletStore = service.walletRootStore const walletStore = service.walletRootStore
const store = service.store const store = service.store
@ -762,17 +791,11 @@ Item {
const params = [`"${DAppsHelpers.strToHex(message)}"`, `"${testAddress}"`] const params = [`"${DAppsHelpers.strToHex(message)}"`, `"${testAddress}"`]
const topic = "b536a" const topic = "b536a"
const session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic)) const session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic))
// Expect to have calls to getActiveSessions from service initialization
const prevRequests = sdk.getActiveSessionsCallbacks.length populateDAppData(topic)
sdk.sessionRequestEvent(session) sdk.sessionRequestEvent(session)
compare(sdk.getActiveSessionsCallbacks.length, prevRequests + 1, "expected DAppsRequestHandler call sdk.getActiveSessions") const request = service.sessionRequestsModel.findById(session.id)
const callback = sdk.getActiveSessionsCallbacks[prevRequests].callback
callback({"b536a": JSON.parse(Testing.formatApproveSessionResponse([chainId, 7], [testAddress]))})
compare(sessionRequestSpy.count, 1, "expected service.sessionRequest trigger")
const requestId = sessionRequestSpy.signalArguments[0][sessionRequestSpy.argPos.id]
const request = service.sessionRequestsModel.findById(requestId)
verify(!!request, "expected request to be found") verify(!!request, "expected request to be found")
compare(request.topic, topic, "expected topic to be set") compare(request.topic, topic, "expected topic to be set")
compare(request.method, method, "expected method to be set") compare(request.method, method, "expected method to be set")
@ -795,64 +818,6 @@ Item {
// } // }
} }
Component {
id: dappsListProviderComponent
DAppsListProvider {
}
}
TestCase {
name: "DAppsListProvider"
property DAppsListProvider provider: null
readonly property var dappsListReceivedJsonStr: '[{"url":"https://tst1.com","name":"name1","iconUrl":"https://tst1.com/u/1"},{"url":"https://tst2.com","name":"name2","iconUrl":"https://tst2.com/u/2"}]'
function init() {
// Simulate the SDK not being ready
let sdk = createTemporaryObject(sdkComponent, root, {projectId: "12ab", sdkReady: false})
verify(!!sdk)
let store = createTemporaryObject(dappsStoreComponent, root, {
dappsListReceivedJsonStr: dappsListReceivedJsonStr
})
verify(!!store)
const walletStore = createTemporaryObject(walletStoreComponent, root)
verify(!!walletStore)
provider = createTemporaryObject(dappsListProviderComponent, root, {sdk: sdk, store: store, supportedAccountsModel: walletStore.nonWatchAccounts})
verify(!!provider)
sdk.getActiveSessionsCallbacks = []
}
function cleanup() {
}
// Implemented as a regression to metamask not having icons which failed dapps list
function test_TestUpdateDapps() {
// Validate that persistance fallback is working
compare(provider.dappsModel.count, 2, "expected dappsModel have the right number of elements")
let persistanceList = JSON.parse(dappsListReceivedJsonStr)
compare(provider.dappsModel.get(0).url, persistanceList[0].url, "expected url to be set")
compare(provider.dappsModel.get(0).iconUrl, persistanceList[0].iconUrl, "expected iconUrl to be set")
compare(provider.dappsModel.get(1).name, persistanceList[1].name, "expected name to be set")
// Validate that SDK's `getActiveSessions` is not called if not ready
let sdk = provider.sdk
compare(sdk.getActiveSessionsCallbacks.length, 0, "expected no calls to sdk.getActiveSessions yet")
sdk.sdkReady = true
compare(sdk.getActiveSessionsCallbacks.length, 1, "expected a call to sdk.getActiveSessions when SDK becomes ready")
let callback = sdk.getActiveSessionsCallbacks[0].callback
const address = ModelUtils.get(provider.supportedAccountsModel, 0, "address")
let session = JSON.parse(Testing.formatApproveSessionResponse([1, 2], [address], {dappMetadataJsonString: Testing.noIconsDappMetadataJsonString}))
callback({"b536a": session, "b537b": session})
compare(provider.dappsModel.count, 1, "expected dappsModel have the SDK's reported dapp, 2 sessions of the same dApp per 2 wallet account, meaning 1 dApp model entry")
compare(provider.dappsModel.get(0).iconUrl, "", "expected iconUrl to be missing")
let updateCalls = provider.store.updateWalletConnectSessionsCalls
compare(updateCalls.length, 1, "expected a call to store.updateWalletConnectSessions")
verify(updateCalls[0].activeTopicsJson.includes("b536a"))
verify(updateCalls[0].activeTopicsJson.includes("b537b"))
}
}
TestCase { TestCase {
name: "ServiceHelpers" name: "ServiceHelpers"

View File

@ -0,0 +1,72 @@
import QtQuick 2.15
import QtTest 1.15
import AppLayouts.Wallet.services.dapps.types 1.0
Item {
id: root
TestCase {
id: sessionRequestTest
function test_parse_data() {
return [
{
// valid eth_sign
expected: {"request":{"event":{"id":1730896224189361,"params":{"chainId":"eip155:1","request":{"method":"eth_sign","params":["0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","requestId":1730896224189361,"method":"eth_sign","account":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","data":{"message":"\u0005=H\u0000Æ5NvÇ])\"È/µ³ôW{/àTòà"},"preparedData":"\u0005=H\u0000Æ5NvÇ])\"È/µ³ôW{/àTòà","value":"0","chainId":1},"error":0},
event: {"id":1730896224189361,"params":{"chainId":"eip155:1","request":{"method":"eth_sign","params":["0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}}
},
{
// valid personal_sign
expected: {"request":{"event":{"id":1730896110928724,"params":{"chainId":"eip155:1","request":{"method":"personal_sign","params":["0x4578616d706c652060706572736f6e616c5f7369676e60206d657373616765","0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","Example password"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","requestId":1730896110928724,"method":"personal_sign","account":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","data":{"message":"Example `personal_sign` message"},"preparedData":"Example `personal_sign` message","value":"0","chainId":1},"error":0},
event: {"id":1730896110928724,"params":{"chainId":"eip155:1","request":{"method":"personal_sign","params":["0x4578616d706c652060706572736f6e616c5f7369676e60206d657373616765","0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","Example password"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}}
},
{
// valid typed_data
expected: {"request":{"event":{"id":1730896224189361,"params":{"chainId":"eip155:1","request":{"method":"eth_sign","params":["0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","requestId":1730896224189361,"method":"eth_sign","account":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","data":{"message":`<EFBFBD><EFBFBD>\u0005=H\u0000<EFBFBD>5NvǘZ<EFBFBD>])\"<22>/<2F><><EFBFBD>W{/<2F><><EFBFBD><EFBFBD>T<EFBFBD><54>`},"preparedData":"<EFBFBD><EFBFBD>\u0005=H\u0000<EFBFBD>5NvǘZ<EFBFBD>])\"<22>/<2F><><EFBFBD>W{/<2F><><EFBFBD><EFBFBD>T<EFBFBD><54>","value":"0","chainId":1}, "error":0},
event: {"id":1730896224189361,"params":{"chainId":"eip155:1","request":{"method":"eth_sign","params":["0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}}
},
{
// valid eth_signTypedData_v4
expected: {"request":{"event":{"id":1730896495619543,"params":{"chainId":"eip155:1","request":{"method":"eth_signTypedData_v4","params":["0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","{\"domain\":{\"chainId\":\"1\",\"name\":\"Ether Mail\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"version\":\"1\"},\"message\":{\"contents\":\"Hello, Bob!\",\"from\":{\"name\":\"Cow\",\"wallets\":[\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\",\"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF\"]},\"to\":[{\"name\":\"Bob\",\"wallets\":[\"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\",\"0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57\",\"0xB0B0b0b0b0b0B000000000000000000000000000\"]}],\"attachment\":\"0x\"},\"primaryType\":\"Mail\",\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Group\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"members\",\"type\":\"Person[]\"}],\"Mail\":[{\"name\":\"from\",\"type\":\"Person\"},{\"name\":\"to\",\"type\":\"Person[]\"},{\"name\":\"contents\",\"type\":\"string\"},{\"name\":\"attachment\",\"type\":\"bytes\"}],\"Person\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"wallets\",\"type\":\"address[]\"}]}}"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","requestId":1730896495619543,"method":"eth_signTypedData_v4","account":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","data":{"message":"{\"domain\":{\"chainId\":\"1\",\"name\":\"Ether Mail\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"version\":\"1\"},\"message\":{\"contents\":\"Hello, Bob!\",\"from\":{\"name\":\"Cow\",\"wallets\":[\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\",\"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF\"]},\"to\":[{\"name\":\"Bob\",\"wallets\":[\"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\",\"0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57\",\"0xB0B0b0b0b0b0B000000000000000000000000000\"]}],\"attachment\":\"0x\"},\"primaryType\":\"Mail\",\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Group\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"members\",\"type\":\"Person[]\"}],\"Mail\":[{\"name\":\"from\",\"type\":\"Person\"},{\"name\":\"to\",\"type\":\"Person[]\"},{\"name\":\"contents\",\"type\":\"string\"},{\"name\":\"attachment\",\"type\":\"bytes\"}],\"Person\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"wallets\",\"type\":\"address[]\"}]}}"},"preparedData":"{\n \"domain\": {\n \"chainId\": \"1\",\n \"name\": \"Ether Mail\",\n \"verifyingContract\": \"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\n \"version\": \"1\"\n },\n \"message\": {\n \"contents\": \"Hello, Bob!\",\n \"from\": {\n \"name\": \"Cow\",\n \"wallets\": [\n \"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\",\n \"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF\"\n ]\n },\n \"to\": [\n {\n \"name\": \"Bob\",\n \"wallets\": [\n \"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\",\n \"0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57\",\n \"0xB0B0b0b0b0b0B000000000000000000000000000\"\n ]\n }\n ],\n \"attachment\": \"0x\"\n },\n \"primaryType\": \"Mail\",\n \"types\": {\n \"EIP712Domain\": [\n {\n \"name\": \"name\",\n \"type\": \"string\"\n },\n {\n \"name\": \"version\",\n \"type\": \"string\"\n },\n {\n \"name\": \"chainId\",\n \"type\": \"uint256\"\n },\n {\n \"name\": \"verifyingContract\",\n \"type\": \"address\"\n }\n ],\n \"Group\": [\n {\n \"name\": \"name\",\n \"type\": \"string\"\n },\n {\n \"name\": \"members\",\n \"type\": \"Person[]\"\n }\n ],\n \"Mail\": [\n {\n \"name\": \"from\",\n \"type\": \"Person\"\n },\n {\n \"name\": \"to\",\n \"type\": \"Person[]\"\n },\n {\n \"name\": \"contents\",\n \"type\": \"string\"\n },\n {\n \"name\": \"attachment\",\n \"type\": \"bytes\"\n }\n ],\n \"Person\": [\n {\n \"name\": \"name\",\n \"type\": \"string\"\n },\n {\n \"name\": \"wallets\",\n \"type\": \"address[]\"\n }\n ]\n }\n}","value":"0","chainId":1},"error":0},
event: {"id":1730896495619543,"params":{"chainId":"eip155:1","request":{"method":"eth_signTypedData_v4","params":["0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","{\"domain\":{\"chainId\":\"1\",\"name\":\"Ether Mail\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"version\":\"1\"},\"message\":{\"contents\":\"Hello, Bob!\",\"from\":{\"name\":\"Cow\",\"wallets\":[\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\",\"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF\"]},\"to\":[{\"name\":\"Bob\",\"wallets\":[\"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\",\"0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57\",\"0xB0B0b0b0b0b0B000000000000000000000000000\"]}],\"attachment\":\"0x\"},\"primaryType\":\"Mail\",\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Group\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"members\",\"type\":\"Person[]\"}],\"Mail\":[{\"name\":\"from\",\"type\":\"Person\"},{\"name\":\"to\",\"type\":\"Person[]\"},{\"name\":\"contents\",\"type\":\"string\"},{\"name\":\"attachment\",\"type\":\"bytes\"}],\"Person\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"wallets\",\"type\":\"address[]\"}]}}"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}}
},
{
// valid sign permit
expected: {"request":{"event":{"id":1730896621650994,"params":{"chainId":"eip155:1","request":{"method":"eth_signTypedData_v4","params":["0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","{\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Permit\":[{\"name\":\"owner\",\"type\":\"address\"},{\"name\":\"spender\",\"type\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\"},{\"name\":\"nonce\",\"type\":\"uint256\"},{\"name\":\"deadline\",\"type\":\"uint256\"}]},\"primaryType\":\"Permit\",\"domain\":{\"name\":\"MyToken\",\"version\":\"1\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"chainId\":null},\"message\":{\"owner\":\"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240\",\"spender\":\"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4\",\"value\":3000,\"nonce\":0,\"deadline\":50000000000}}"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","requestId":1730896621650994,"method":"eth_signTypedData_v4","account":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","data":{"message":"{\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Permit\":[{\"name\":\"owner\",\"type\":\"address\"},{\"name\":\"spender\",\"type\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\"},{\"name\":\"nonce\",\"type\":\"uint256\"},{\"name\":\"deadline\",\"type\":\"uint256\"}]},\"primaryType\":\"Permit\",\"domain\":{\"name\":\"MyToken\",\"version\":\"1\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"chainId\":null},\"message\":{\"owner\":\"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240\",\"spender\":\"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4\",\"value\":3000,\"nonce\":0,\"deadline\":50000000000}}"},"preparedData":"{\n \"types\": {\n \"EIP712Domain\": [\n {\n \"name\": \"name\",\n \"type\": \"string\"\n },\n {\n \"name\": \"version\",\n \"type\": \"string\"\n },\n {\n \"name\": \"chainId\",\n \"type\": \"uint256\"\n },\n {\n \"name\": \"verifyingContract\",\n \"type\": \"address\"\n }\n ],\n \"Permit\": [\n {\n \"name\": \"owner\",\n \"type\": \"address\"\n },\n {\n \"name\": \"spender\",\n \"type\": \"address\"\n },\n {\n \"name\": \"value\",\n \"type\": \"uint256\"\n },\n {\n \"name\": \"nonce\",\n \"type\": \"uint256\"\n },\n {\n \"name\": \"deadline\",\n \"type\": \"uint256\"\n }\n ]\n },\n \"primaryType\": \"Permit\",\n \"domain\": {\n \"name\": \"MyToken\",\n \"version\": \"1\",\n \"verifyingContract\": \"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\n \"chainId\": null\n },\n \"message\": {\n \"owner\": \"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240\",\n \"spender\": \"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4\",\n \"value\": 3000,\n \"nonce\": 0,\n \"deadline\": 50000000000\n }\n}","value":"0","chainId":1},"error":0},
event: {"id":1730896621650994,"params":{"chainId":"eip155:1","request":{"method":"eth_signTypedData_v4","params":["0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","{\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Permit\":[{\"name\":\"owner\",\"type\":\"address\"},{\"name\":\"spender\",\"type\":\"address\"},{\"name\":\"value\",\"type\":\"uint256\"},{\"name\":\"nonce\",\"type\":\"uint256\"},{\"name\":\"deadline\",\"type\":\"uint256\"}]},\"primaryType\":\"Permit\",\"domain\":{\"name\":\"MyToken\",\"version\":\"1\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"chainId\":null},\"message\":{\"owner\":\"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240\",\"spender\":\"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4\",\"value\":3000,\"nonce\":0,\"deadline\":50000000000}}"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}}
},
{
// valid SIWE
expected: {"request":{"event":{"id":1730896696709302,"params":{"chainId":"eip155:1","request":{"method":"personal_sign","params":["0x6d6574616d61736b2e6769746875622e696f2077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078374634374332653138613442426635343837453666623038326543324439416230453664373234300a0a492061636365707420746865204d6574614d61736b205465726d73206f6620536572766963653a2068747470733a2f2f636f6d6d756e6974792e6d6574616d61736b2e696f2f746f730a0a5552493a2068747470733a2f2f6d6574616d61736b2e6769746875622e696f0a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a2033323839313735370a4973737565642041743a20323032312d30392d33305431363a32353a32342e3030305a","0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","Example password"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","requestId":1730896696709302,"method":"personal_sign","account":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","data":{"message":"metamask.github.io wants you to sign in with your Ethereum account:\n0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240\n\nI accept the MetaMask Terms of Service: https://community.metamask.io/tos\n\nURI: https://metamask.github.io\nVersion: 1\nChain ID: 1\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24.000Z"},"preparedData":"metamask.github.io wants you to sign in with your Ethereum account:\n0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240\n\nI accept the MetaMask Terms of Service: https://community.metamask.io/tos\n\nURI: https://metamask.github.io\nVersion: 1\nChain ID: 1\nNonce: 32891757\nIssued At: 2021-09-30T16:25:24.000Z","value":"0","chainId":1},"error":0},
event: {"id":1730896696709302,"params":{"chainId":"eip155:1","request":{"method":"personal_sign","params":["0x6d6574616d61736b2e6769746875622e696f2077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078374634374332653138613442426635343837453666623038326543324439416230453664373234300a0a492061636365707420746865204d6574614d61736b205465726d73206f6620536572766963653a2068747470733a2f2f636f6d6d756e6974792e6d6574616d61736b2e696f2f746f730a0a5552493a2068747470733a2f2f6d6574616d61736b2e6769746875622e696f0a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a2033323839313735370a4973737565642041743a20323032312d30392d33305431363a32353a32342e3030305a","0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","Example password"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}}
},
{
// valid EIP 1159 transaction
expected: {"request":{"event":{"id":1730899979094571,"params":{"chainId":"eip155:1","request":{"method":"eth_sendTransaction","params":[{"from":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","gasLimit":"0x5028","maxFeePerGas":"0x2540be400","maxPriorityFeePerGas":"0x3b9aca00","to":"0x0c54FcCd2e384b4BB6f2E405Bf5Cbc15a017AaFb","value":"0x0"}]}},"topic":"147fa9ddffcf9d782b5e002eb36b041e43a1db2bad79a598da6926105ce6680f","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}},"topic":"147fa9ddffcf9d782b5e002eb36b041e43a1db2bad79a598da6926105ce6680f","requestId":1730899979094571,"method":"eth_sendTransaction","account":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","data":{"tx":{"from":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","gasLimit":"0x5028","maxFeePerGas":"0x2540be400","maxPriorityFeePerGas":"0x3b9aca00","to":"0x0c54FcCd2e384b4BB6f2E405Bf5Cbc15a017AaFb","value":"0x0"}},"preparedData":"{\n \"from\": \"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240\",\n \"gasLimit\": 20520,\n \"maxFeePerGas\": \"10\",\n \"maxPriorityFeePerGas\": \"1\",\n \"to\": \"0x0c54FcCd2e384b4BB6f2E405Bf5Cbc15a017AaFb\",\n \"value\": \"0\"\n}","value":"0","transaction":{"from":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","gasLimit":"0x5028","maxFeePerGas":"0x2540be400","maxPriorityFeePerGas":"0x3b9aca00","to":"0x0c54FcCd2e384b4BB6f2E405Bf5Cbc15a017AaFb","value":"0x0"},"chainId":1},"error":0},
event: {"id":1730899979094571,"params":{"chainId":"eip155:1","request":{"method":"eth_sendTransaction","params":[{"from":"0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240","gasLimit":"0x5028","maxFeePerGas":"0x2540be400","maxPriorityFeePerGas":"0x3b9aca00","to":"0x0c54FcCd2e384b4BB6f2E405Bf5Cbc15a017AaFb","value":"0x0"}]}},"topic":"147fa9ddffcf9d782b5e002eb36b041e43a1db2bad79a598da6926105ce6680f","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}}
}
]
}
function test_parse(data) {
const { expected, event } = data
const result = SessionRequest.parse(event)
compare(result.error, expected.error)
compare(result.request.topic, expected.request.topic)
compare(result.request.requestId, expected.request.requestId)
compare(result.request.method, expected.request.method)
compare(result.request.account, expected.request.account)
compare(result.request.value, expected.request.value)
compare(result.request.chainId, expected.request.chainId)
compare(!!result.request.transaction, !!expected.request.transaction)
if (!!result.request.transaction) {
compare(result.request.transaction.from, expected.request.transaction.from)
compare(result.request.transaction.gasLimit, expected.request.transaction.gasLimit)
compare(result.request.transaction.maxFeePerGas, expected.request.transaction.maxFeePerGas)
compare(result.request.transaction.maxPriorityFeePerGas, expected.request.transaction.maxPriorityFeePerGas)
compare(result.request.transaction.to, expected.request.transaction.to)
compare(result.request.transaction.value, expected.request.transaction.value)
}
}
}
}

View File

@ -47,6 +47,9 @@ Item {
sourceId: 0 sourceId: 0
data: "data" data: "data"
preparedData: "preparedData" preparedData: "preparedData"
dappUrl: "dappUrl"
dappIcon: "dappIcon"
dappName: "dappName"
} }
} }
@ -88,7 +91,6 @@ Item {
componentUnderTest.store.userAuthenticationFailed("topic", "id") componentUnderTest.store.userAuthenticationFailed("topic", "id")
compare(componentUnderTest.executeSpy.count, 0) compare(componentUnderTest.executeSpy.count, 0)
compare(componentUnderTest.rejectedSpy.count, 1)
compare(componentUnderTest.authFailedSpy.count, 1) compare(componentUnderTest.authFailedSpy.count, 1)
compare(componentUnderTest.store.authenticateUserCalls.length, 1) compare(componentUnderTest.store.authenticateUserCalls.length, 1)
} }

View File

@ -0,0 +1,268 @@
import QtQuick 2.15
import QtTest 1.15
import AppLayouts.Wallet.services.dapps 1.0
import AppLayouts.Wallet.services.dapps.plugins 1.0
import AppLayouts.Wallet.services.dapps.types 1.0
import shared.stores 1.0
import utils 1.0
Item {
id: root
Component {
id: signRequestPluginComponent
SignRequestPlugin {
id: plugin
property SignalSpy acceptedSpy: SignalSpy { target: plugin; signalName: "accepted" }
property SignalSpy rejectedSpy: SignalSpy { target: plugin; signalName: "rejected" }
property SignalSpy signCompletedSpy: SignalSpy { target: plugin; signalName: "signCompleted" }
sdk: WalletConnectSDKBase {
id: sdk
enabled: true
projectId: ""
property bool sdkReady: true
property var getActiveSessionsCallbacks: []
getActiveSessions: function(callback) {
getActiveSessionsCallbacks.push({callback})
}
property var acceptSessionRequestCalls: []
acceptSessionRequest: function(topic, id, signature) {
acceptSessionRequestCalls.push({topic, id, signature})
}
property var rejectSessionRequestCalls: []
rejectSessionRequest: function(topic, id, error) {
rejectSessionRequestCalls.push({topic, id, error})
}
}
store: DAppsStore {
id: dappsStore
signal userAuthenticated(string topic, string id, string password, string pin, string payload)
signal userAuthenticationFailed(string topic, string id)
signal signingResult(string topic, string id, string data)
function hexToDec(hex) {
return parseInt(hex, 16)
}
function getEstimatedTime() {
return Constants.TransactionEstimatedTime.LessThanThreeMins
}
function convertFeesInfoToHex(feesInfo) {
return null
}
property var mockedSuggestedFees: ({
gasPrice: 2.0,
baseFee: 5.0,
maxPriorityFeePerGas: 2.0,
maxFeePerGasL: 1.0,
maxFeePerGasM: 1.1,
maxFeePerGasH: 1.2,
l1GasFee: 0.0,
eip1559Enabled: true
})
function getSuggestedFees() {
return mockedSuggestedFees
}
property var authenticateUserCalls: []
function authenticateUser(topic, id, address) {
authenticateUserCalls.push({topic, id, address})
}
property var signMessageCalls: []
function signMessage(topic, id, address, message, password, pin) {
signMessageCalls.push({topic, id, address, password, pin})
}
property var signMessageUnsafeCalls: []
function signMessageUnsafe(topic, id, address, data, password, pin) {
signMessageUnsafeCalls.push({topic, id, address, data, password, pin})
}
property var safeSignTypedDataCalls: []
function safeSignTypedData(topic, id, address, message, chainId, legacy, password, pin) {
safeSignTypedDataCalls.push({topic, id, address, message, chainId, legacy, password, pin})
}
property var signTransactionCalls: []
function signTransaction(topic, id, address, chainId, txObj, password, pin) {
signTransactionCalls.push({topic, id, address, chainId, txObj, password, pin})
}
property var sendTransactionCalls: []
function sendTransaction(topic, id, address, chainID, txObj, password, pin) {
sendTransactionCalls.push({topic, id, address, chainID, txObj, password, pin})
}
}
dappsModel: ListModel {
id: dappsModel
}
groupedAccountAssetsModel: ListModel {
id: groupedAccountAssetsModel
}
networksModel: ListModel {
id: networksModel
ListElement {
chainId: 1
layer: 1
}
}
accountsModel: ListModel {
id: accountsModel
ListElement {
address: "0x123"
}
}
currentCurrency: "USD"
requests: SessionRequestsModel {}
getFiatValue: (balance, cryptoSymbol) => {
return parseFloat(balance)
}
}
}
TestCase {
id: signRequestPluginTest
property SignRequestPlugin componentUnderTest: null
function populateDAppData(topic) {
const dapp = {
topic,
name: "Example",
url: "https://example.com",
iconUrl: "https://example.com/icon.png",
connectorId: 0,
accountAddresses: [{address: "0x123"}],
rawSessions: [{session: {topic}}]
}
componentUnderTest.dappsModel.append(dapp)
}
function executeRequest(signEvent) {
populateDAppData(signEvent.topic)
componentUnderTest.sdk.sessionRequestEvent(signEvent)
// Execute the request
const request = componentUnderTest.requests.get(0)
request.requestItem.execute("passowrd", "pin")
componentUnderTest.store.signingResult(signEvent.topic, signEvent.id, "result")
compare(componentUnderTest.sdk.acceptSessionRequestCalls.length, 1, "Accept session request should be called")
compare(componentUnderTest.sdk.acceptSessionRequestCalls[0].signature, "result", "Accept session request should be called with the correct signature")
compare(componentUnderTest.sdk.acceptSessionRequestCalls[0].topic.toString(), signEvent.topic.toString(), "Accept session request should be called with the correct topic")
compare(componentUnderTest.sdk.acceptSessionRequestCalls[0].id.toString(), signEvent.id.toString(), "Accept session request should be called with the correct id")
compare(componentUnderTest.acceptedSpy.count, 1, "Accepted signal should be emitted")
}
function init() {
componentUnderTest = createTemporaryObject(signRequestPluginComponent, root)
}
function test_signMessage() {
const signEvent = {"id":1730896110928724,"params":{"chainId":"eip155:1","request":{"method":"personal_sign","params":["0x4578616d706c652060706572736f6e616c5f7369676e60206d657373616765","0x123","Example password"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}}
// Case 1: DApp not found
ignoreWarning(`Error finding dapp for topic ${signEvent.topic} id ${signEvent.id}`)
componentUnderTest.sdk.sessionRequestEvent(signEvent)
compare(componentUnderTest.sdk.rejectSessionRequestCalls.length, 1, "Reject session request should not be called")
// Case 2: DApp found
executeRequest(signEvent)
compare(componentUnderTest.store.signMessageCalls.length, 1, "Sign message should be called")
componentUnderTest.sdk.sessionRequestUserAnswerResult(signEvent.topic, signEvent.id, true, null)
const requestItem = componentUnderTest.requests.get(0).requestItem
compare(requestItem.requestId, signEvent.id.toString(), "Request id should be set")
compare(requestItem.topic, signEvent.topic.toString(), "Topic should be set")
compare(requestItem.method, signEvent.params.request.method, "Method should be set")
compare(requestItem.accountAddress, signEvent.params.request.params[1], "Account address should be set")
compare(requestItem.chainId, signEvent.params.chainId.split(':').pop().trim(), "Chain id should be set")
compare(requestItem.sourceId, componentUnderTest.dappsModel.get(0).connectorId, "Source id should be set")
compare(componentUnderTest.signCompletedSpy.count, 1, "Sign completed signal should be emitted")
}
function test_signMessageUnsafe() {
const signEvent = {"id":1730896224189361,"params":{"chainId":"eip155:1","request":{"method":"eth_sign","params":["0x123","0x123"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}}
executeRequest(signEvent)
compare(componentUnderTest.store.signMessageUnsafeCalls.length, 1, "Sign message unsafe should be called")
componentUnderTest.sdk.sessionRequestUserAnswerResult(signEvent.topic, signEvent.id, true, null)
compare(componentUnderTest.signCompletedSpy.count, 1, "Sign completed signal should be emitted")
}
function test_safeSignTypedData() {
const signEvent = {"id":1730896495619543,"params":{"chainId":"eip155:1","request":{"method":"eth_signTypedData_v4","params":["0x123","{\"domain\":{\"chainId\":\"1\",\"name\":\"Ether Mail\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"version\":\"1\"},\"message\":{\"contents\":\"Hello, Bob!\",\"from\":{\"name\":\"Cow\",\"wallets\":[\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\",\"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF\"]},\"to\":[{\"name\":\"Bob\",\"wallets\":[\"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\",\"0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57\",\"0xB0B0b0b0b0b0B000000000000000000000000000\"]}],\"attachment\":\"0x\"},\"primaryType\":\"Mail\",\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Group\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"members\",\"type\":\"Person[]\"}],\"Mail\":[{\"name\":\"from\",\"type\":\"Person\"},{\"name\":\"to\",\"type\":\"Person[]\"},{\"name\":\"contents\",\"type\":\"string\"},{\"name\":\"attachment\",\"type\":\"bytes\"}],\"Person\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"wallets\",\"type\":\"address[]\"}]}}"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}}
executeRequest(signEvent)
compare(componentUnderTest.store.safeSignTypedDataCalls.length, 1, "Safe sign typed data should be called")
componentUnderTest.sdk.sessionRequestUserAnswerResult(signEvent.topic, signEvent.id, true, null)
compare(componentUnderTest.signCompletedSpy.count, 1, "Sign completed signal should be emitted")
}
function test_sendTransaction() {
const signEvent = {"id":1730899979094571,"params":{"chainId":"eip155:1","request":{"method":"eth_sendTransaction","params":[{"from":"0x123","gasLimit":"0x5028","maxFeePerGas":"0x2540be400","maxPriorityFeePerGas":"0x3b9aca00","to":"0x0c54FcCd2e384b4BB6f2E405Bf5Cbc15a017AaFb","value":"0x0"}]}},"topic":"147fa9ddffcf9d782b5e002eb36b041e43a1db2bad79a598da6926105ce6680f","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}}
executeRequest(signEvent)
compare(componentUnderTest.store.sendTransactionCalls.length, 1, "Send transaction should be called")
componentUnderTest.sdk.sessionRequestUserAnswerResult(signEvent.topic, signEvent.id, true, null)
compare(componentUnderTest.signCompletedSpy.count, 1, "Sign completed signal should be emitted")
}
function reject_sign() {
const signEvent = {"id":1730896495619543,"params":{"chainId":"eip155:1","request":{"method":"eth_signTypedData_v4","params":["0x123","{\"domain\":{\"chainId\":\"1\",\"name\":\"Ether Mail\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"version\":\"1\"},\"message\":{\"contents\":\"Hello, Bob!\",\"from\":{\"name\":\"Cow\",\"wallets\":[\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\",\"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF\"]},\"to\":[{\"name\":\"Bob\",\"wallets\":[\"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\",\"0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57\",\"0xB0B0b0b0b0b0B000000000000000000000000000\"]}],\"attachment\":\"0x\"},\"primaryType\":\"Mail\",\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Group\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"members\",\"type\":\"Person[]\"}],\"Mail\":[{\"name\":\"from\",\"type\":\"Person\"},{\"name\":\"to\",\"type\":\"Person[]\"},{\"name\":\"contents\",\"type\":\"string\"},{\"name\":\"attachment\",\"type\":\"bytes\"}],\"Person\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"wallets\",\"type\":\"address[]\"}]}}"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}}
populateDAppData(signEvent.topic)
componentUnderTest.sdk.sessionRequestEvent(signEvent)
// Execute the request
const request = componentUnderTest.requests.get(0)
request.requestItem.rejected(false)
compare(componentUnderTest.sdk.rejectSessionRequestCalls.length, 1, "Reject session request should be called")
compare(componentUnderTest.rejectedSpy.count, 1, "Rejected signal should be emitted")
compare(componentUnderTest.signCompletedSpy.count, 0, "Sign completed signal should not be emitted")
}
function test_authFailed() {
const signEvent = {"id":1730896495619543,"params":{"chainId":"eip155:1","request":{"method":"eth_signTypedData_v4","params":["0x123","{\"domain\":{\"chainId\":\"1\",\"name\":\"Ether Mail\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"version\":\"1\"},\"message\":{\"contents\":\"Hello, Bob!\",\"from\":{\"name\":\"Cow\",\"wallets\":[\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\",\"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF\"]},\"to\":[{\"name\":\"Bob\",\"wallets\":[\"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\",\"0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57\",\"0xB0B0b0b0b0b0B000000000000000000000000000\"]}],\"attachment\":\"0x\"},\"primaryType\":\"Mail\",\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Group\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"members\",\"type\":\"Person[]\"}],\"Mail\":[{\"name\":\"from\",\"type\":\"Person\"},{\"name\":\"to\",\"type\":\"Person[]\"},{\"name\":\"contents\",\"type\":\"string\"},{\"name\":\"attachment\",\"type\":\"bytes\"}],\"Person\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"wallets\",\"type\":\"address[]\"}]}}"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}}
populateDAppData(signEvent.topic)
componentUnderTest.sdk.sessionRequestEvent(signEvent)
// Execute the request
const request = componentUnderTest.requests.get(0)
request.requestItem.authFailed()
compare(componentUnderTest.sdk.rejectSessionRequestCalls.length, 1, "Reject session request should be called")
compare(componentUnderTest.rejectedSpy.count, 1, "Rejected signal should be emitted")
compare(componentUnderTest.signCompletedSpy.count, 0, "Sign completed signal should not be emitted")
}
function test_signMessageFails() {
const signEvent = {"id":1730896495619543,"params":{"chainId":"eip155:1","request":{"method":"eth_signTypedData_v4","params":["0x123","{\"domain\":{\"chainId\":\"1\",\"name\":\"Ether Mail\",\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\",\"version\":\"1\"},\"message\":{\"contents\":\"Hello, Bob!\",\"from\":{\"name\":\"Cow\",\"wallets\":[\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\",\"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF\"]},\"to\":[{\"name\":\"Bob\",\"wallets\":[\"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\",\"0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57\",\"0xB0B0b0b0b0b0B000000000000000000000000000\"]}],\"attachment\":\"0x\"},\"primaryType\":\"Mail\",\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Group\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"members\",\"type\":\"Person[]\"}],\"Mail\":[{\"name\":\"from\",\"type\":\"Person\"},{\"name\":\"to\",\"type\":\"Person[]\"},{\"name\":\"contents\",\"type\":\"string\"},{\"name\":\"attachment\",\"type\":\"bytes\"}],\"Person\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"wallets\",\"type\":\"address[]\"}]}}"]}},"topic":"43a74a4c6c71e3ab67ef80283dc43f392445642c8dce3dabe63f89ab83cfcfc3","verifyContext":{"verified":{"origin":"https://metamask.github.io/test-dapp/","validation":"UNKNOWN","verifyUrl":"https://verify.walletconnect.org"}}}
populateDAppData(signEvent.topic)
componentUnderTest.sdk.sessionRequestEvent(signEvent)
// Execute the request
const request = componentUnderTest.requests.get(0)
request.requestItem.execute("passowrd", "pin")
componentUnderTest.store.signingResult(signEvent.topic, signEvent.id, "")
compare(componentUnderTest.sdk.rejectSessionRequestCalls.length, 1, "Reject session request should be called")
compare(componentUnderTest.rejectedSpy.count, 1, "Rejected signal should be emitted")
compare(componentUnderTest.signCompletedSpy.count, 0, "Sign completed signal should not be emitted")
}
}
}

View File

@ -0,0 +1,133 @@
import QtQuick 2.15
import QtTest 1.15
import AppLayouts.Wallet.services.dapps 1.0
import shared.stores 1.0
Item {
id: root
Component {
id: wcDAppsProviderComponent
WCDappsProvider {
id: wcDAppsProvider
readonly property SignalSpy connectedSpy: SignalSpy { target: wcDAppsProvider; signalName: "connected" }
readonly property SignalSpy disconnectedSpy: SignalSpy { target: wcDAppsProvider; signalName: "disconnected" }
supportedAccountsModel: ListModel {
ListElement {
address: "0x123"
}
}
store: DAppsStore {
signal dappsListReceived(string dappsJson)
property var addWalletConnectSessionCalls: []
function addWalletConnectSession(sessionJson) {
addWalletConnectSessionCalls.push(sessionJson)
}
property var deactivateWalletConnectSessionCalls: []
function deactivateWalletConnectSession(topic) {
deactivateWalletConnectSessionCalls.push(topic)
}
property var updateWalletConnectSessionsCalls: []
function updateWalletConnectSessions(topics) {
updateWalletConnectSessionsCalls.push(topics)
}
property var getDAppsCalls: []
function getDapps() {
getDAppsCalls.push(true)
return []
}
}
sdk: WalletConnectSDKBase {
id: sdk
enabled: true
projectId: ""
property bool sdkReady: true
property var activeSessions: ({})
getActiveSessions: function(callback) {
callback(sdk.activeSessions)
}
}
}
}
function buildSession(dappUrl, dappName, dappIcon, proposalId, account, chains) {
let sessionTemplate = (dappUrl, dappName, dappIcon, proposalId, eipAccount, eipChains) => {
return {
peer: {
metadata: {
description: "-",
icons: [
dappIcon
],
name: dappName,
url: dappUrl
}
},
namespaces: {
eip155: {
accounts: [eipAccount],
chains: eipChains
}
},
pairingTopic: proposalId,
topic: dappUrl
};
}
const eipAccount = account ? `eip155:${account}` : ""
const eipChains = chains ? chains.map((chain) => `eip155:${chain}`) : []
return sessionTemplate(dappUrl, dappName, dappIcon, proposalId, eipAccount, eipChains)
}
TestCase {
id: wcDAppsProviderTest
property WCDappsProvider componentUnderTest: null
function init() {
componentUnderTest = createTemporaryObject(wcDAppsProviderComponent, root)
}
function test_addRemoveSession() {
const newSession = buildSession("https://example.com", "Example", "https://example.com/icon.png", "123", "0x123", ["1"])
componentUnderTest.sdk.activeSessions["https://example.com"] = newSession
componentUnderTest.sdk.approveSessionResult("requestID", newSession, null)
compare(componentUnderTest.store.addWalletConnectSessionCalls.length, 1, "addWalletConnectSession should be called once")
compare(componentUnderTest.connectedSpy.count, 1, "Connected signal should be emitted once")
compare(componentUnderTest.connectedSpy.signalArguments[0][0], "requestID", "Connected signal should have correct proposalId")
compare(componentUnderTest.connectedSpy.signalArguments[0][1], "https://example.com", "Connected signal should have correct topic")
compare(componentUnderTest.connectedSpy.signalArguments[0][2], "https://example.com", "Connected signal should have correct dAppUrl")
const dapp = componentUnderTest.getByTopic("https://example.com")
verify(!!dapp, "DApp should be found")
compare(dapp.name, "Example", "DApp should have correct name")
compare(dapp.url, "https://example.com", "DApp should have correct url")
compare(dapp.iconUrl, "https://example.com/icon.png", "DApp should have correct iconUrl")
compare(dapp.topic, "https://example.com", "DApp should have correct topic")
compare(dapp.connectorId, componentUnderTest.connectorId, "DApp should have correct connectorId")
compare(dapp.accountAddresses.count, 1, "DApp should have correct accountAddresses count")
compare(dapp.accountAddresses.get(0).address, "0x123", "DApp should have correct accountAddresses address")
compare(dapp.rawSessions.count, 1, "DApp should have correct rawSessions count")
componentUnderTest.sdk.sessionDelete("https://example.com", "")
compare(componentUnderTest.store.deactivateWalletConnectSessionCalls.length, 1, "deactivateWalletConnectSession should be called once")
compare(componentUnderTest.disconnectedSpy.count, 1, "Disconnected signal should be emitted once")
compare(componentUnderTest.disconnectedSpy.signalArguments[0][0], "https://example.com", "Disconnected signal should have correct topic")
compare(componentUnderTest.disconnectedSpy.signalArguments[0][1], "https://example.com", "Disconnected signal should have correct dAppUrl")
}
function test_disabledSDK() {
componentUnderTest.sdk.enabled = false
componentUnderTest.sdk.approveSessionResult("requestID", buildSession("https://example.com", "Example", "https://example.com/icon.png", "123", "0x123", ["1"]), "")
compare(componentUnderTest.connectedSpy.count, 0, "Connected signal should not be emitted")
componentUnderTest.sdk.sessionDelete("https://example.com", "")
compare(componentUnderTest.disconnectedSpy.count, 0, "Disconnected signal should not be emitted")
}
}
}

View File

@ -0,0 +1,4 @@
import QtQuick 2.15
QtObject {
}

View File

@ -8,3 +8,4 @@ PermissionsStore 1.0 PermissionsStore.qml
ProfileStore 1.0 ProfileStore.qml ProfileStore 1.0 ProfileStore.qml
RootStore 1.0 RootStore.qml RootStore 1.0 RootStore.qml
UtilsStore 1.0 UtilsStore.qml UtilsStore 1.0 UtilsStore.qml
BrowserConnectStore 1.0 BrowserConnectStore.qml

View File

@ -69,12 +69,11 @@ DappsComboBox {
signal disconnectRequested(string connectionId) signal disconnectRequested(string connectionId)
signal pairingRequested(string uri) signal pairingRequested(string uri)
signal pairingValidationRequested(string uri) signal pairingValidationRequested(string uri)
signal connectionAccepted(var pairingId, var chainIds, string selectedAccount) signal connectionAccepted(string pairingId, var chainIds, string selectedAccount)
signal connectionDeclined(var pairingId) signal connectionDeclined(string pairingId)
signal signRequestAccepted(string connectionId, string requestId) signal signRequestAccepted(string connectionId, string requestId)
signal signRequestRejected(string connectionId, string requestId) signal signRequestRejected(string connectionId, string requestId)
signal signRequestIsLive(string connectionId, string requestId)
signal subscribeForFeeUpdates(string connectionId, string requestId)
/// Response to pairingValidationRequested /// Response to pairingValidationRequested
function pairingValidated(validationState) { function pairingValidated(validationState) {
@ -349,7 +348,7 @@ DappsComboBox {
hasExpiryDate: !!request.expirationTimestamp hasExpiryDate: !!request.expirationTimestamp
onOpened: { onOpened: {
root.subscribeForFeeUpdates(request.topic, request.requestId) root.signRequestIsLive(request.topic, request.requestId)
} }
onClosed: { onClosed: {

View File

@ -137,38 +137,38 @@ Item {
id: dappsWorkflow id: dappsWorkflow
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
readonly property WalletConnectService wcService: Global.walletConnectService readonly property DAppsService dAppsService: Global.dAppsService
spacing: 8 spacing: 8
visible: !root.walletStore.showSavedAddresses visible: !root.walletStore.showSavedAddresses
&& (wcService.walletConnectFeatureEnabled || wcService.connectorFeatureEnabled) && (dAppsService.walletConnectFeatureEnabled || dAppsService.connectorFeatureEnabled)
&& wcService.serviceAvailableToCurrentAddress && dAppsService.serviceAvailableToCurrentAddress
enabled: !!wcService && wcService.isServiceOnline enabled: !!dAppsService && dAppsService.isServiceOnline
walletConnectEnabled: wcService.walletConnectFeatureEnabled walletConnectEnabled: dAppsService.walletConnectFeatureEnabled
connectorEnabled: wcService.connectorFeatureEnabled connectorEnabled: dAppsService.connectorFeatureEnabled
loginType: root.loginType loginType: root.loginType
selectedAccountAddress: root.walletStore.selectedAddress selectedAccountAddress: root.walletStore.selectedAddress
model: wcService.dappsModel model: dAppsService.dappsModel
accountsModel: wcService.validAccounts accountsModel: root.walletStore.nonWatchAccounts
networksModel: wcService.flatNetworks networksModel: root.walletStore.filteredFlatModel
sessionRequestsModel: wcService.sessionRequestsModel sessionRequestsModel: dAppsService.sessionRequestsModel
formatBigNumber: (number, symbol, noSymbolOption) => wcService.walletRootStore.currencyStore.formatBigNumber(number, symbol, noSymbolOption) formatBigNumber: (number, symbol, noSymbolOption) => root.walletStore.currencyStore.formatBigNumber(number, symbol, noSymbolOption)
onDisconnectRequested: (connectionId) => wcService.disconnectDapp(connectionId) onDisconnectRequested: (connectionId) => dAppsService.disconnectDapp(connectionId)
onPairingRequested: (uri) => wcService.pair(uri) onPairingRequested: (uri) => dAppsService.pair(uri)
onPairingValidationRequested: (uri) => wcService.validatePairingUri(uri) onPairingValidationRequested: (uri) => dAppsService.validatePairingUri(uri)
onConnectionAccepted: (pairingId, chainIds, selectedAccount) => wcService.approvePairSession(pairingId, chainIds, selectedAccount) onConnectionAccepted: (pairingId, chainIds, selectedAccount) => dAppsService.approvePairSession(pairingId, chainIds, selectedAccount)
onConnectionDeclined: (pairingId) => wcService.rejectPairSession(pairingId) onConnectionDeclined: (pairingId) => dAppsService.rejectPairSession(pairingId)
onSignRequestAccepted: (connectionId, requestId) => wcService.sign(connectionId, requestId) onSignRequestAccepted: (connectionId, requestId) => dAppsService.sign(connectionId, requestId)
onSignRequestRejected: (connectionId, requestId) => wcService.rejectSign(connectionId, requestId, false /*hasError*/) onSignRequestRejected: (connectionId, requestId) => dAppsService.rejectSign(connectionId, requestId, false /*hasError*/)
onSubscribeForFeeUpdates: (connectionId, requestId) => wcService.subscribeForFeeUpdates(connectionId, requestId) onSignRequestIsLive: (connectionId, requestId) => dAppsService.signRequestIsLive(connectionId, requestId)
Connections { Connections {
target: dappsWorkflow.wcService target: dappsWorkflow.dAppsService
function onPairingValidated(validationState) { function onPairingValidated(validationState) {
dappsWorkflow.pairingValidated(validationState) dappsWorkflow.pairingValidated(validationState)
} }

View File

@ -9,64 +9,86 @@ import utils 1.0
DAppsModel { DAppsModel {
id: root id: root
required property BrowserConnectStore store required property WalletConnectSDKBase bcSDK
readonly property int connectorId: Constants.StatusConnect readonly property int connectorId: Constants.StatusConnect
property bool enabled: true readonly property bool enabled: bcSDK.enabled
signal connected(string pairingId, string topic, string dAppUrl)
signal disconnected(string topic, string dAppUrl)
Connections { Connections {
target: root.store target: root.bcSDK
enabled: root.enabled enabled: root.enabled
function onConnected(dappJson) { function onSessionDelete(topic, err) {
const dapp = JSON.parse(dappJson) const dapp = root.getByTopic(topic)
const { url, name, icon, sharedAccount } = dapp if (!dapp) {
console.warn("DApp not found for topic - cannot delete session", topic)
if (!url) {
console.warn(invalidDAppUrlError)
return return
} }
root.append({
name, root.remove(topic)
url, root.disconnected(topic, dapp.url)
iconUrl: icon, }
topic: url,
connectorId: root.connectorId, function onApproveSessionResult(proposalId, session, error) {
accountAddresses: [{address: sharedAccount}], if (error) {
rawSessions: [dapp] console.warn("Failed to approve session", error)
return
}
const dapp = d.sessionToDApp(session)
root.append(dapp)
root.connected(proposalId, dapp.topic, dapp.url)
}
}
QtObject {
id: d
function sessionToDApp(session) {
const dapp = session.peer.metadata
if (!!dapp.icons && dapp.icons.length > 0) {
dapp.iconUrl = dapp.icons[0]
} else {
dapp.iconUrl = ""
}
const accounts = DAppsHelpers.getAccountsInSession(session)
dapp.accountAddresses = accounts.map(account => ({address: account}))
dapp.topic = session.topic
dapp.rawSessions = [session]
dapp.connectorId = root.connectorId
return dapp
}
function getPersistedDapps() {
if (!root.enabled) {
return []
}
let dapps = []
root.bcSDK.getActiveSessions((allSessions) => {
if (!allSessions) {
return
}
for (const sessionID in allSessions) {
const session = allSessions[sessionID]
const dapp = sessionToDApp(session)
dapps.push(dapp)
}
}) })
return dapps
} }
function onDisconnected(dappJson) { function resetModel() {
const dapp = JSON.parse(dappJson) root.clear()
const { url } = dapp const dapps = d.getPersistedDapps()
for (let i = 0; i < dapps.length; i++) {
if (!url) { root.append(dapps[i])
console.warn(invalidDAppUrlError)
return
} }
root.remove(dapp.url)
} }
} }
Component.onCompleted: { Component.onCompleted: {
if (root.enabled) { d.resetModel()
const dappsStr = root.store.getDApps()
if (dappsStr) {
const dapps = JSON.parse(dappsStr)
dapps.forEach(dapp => {
const { url, name, iconUrl, sharedAccount } = dapp
root.append({
name,
url,
iconUrl,
topic: url,
connectorId: root.connectorId,
accountAddresses: [{address: sharedAccount}],
rawSessions: [dapp]
})
})
}
}
} }
} }

View File

@ -4,6 +4,7 @@ import StatusQ.Core.Utils 0.1
QObject { QObject {
id: root id: root
objectName: "DAppsModel"
// RoleNames // RoleNames
// name: string // name: string
// url: string // url: string
@ -15,14 +16,22 @@ QObject {
// rawSessions: [{session: object}] // rawSessions: [{session: object}]
readonly property ListModel model: ListModel {} readonly property ListModel model: ListModel {}
// Appending a new DApp to the model
// Required properties: url, topic, connectorId, accountAddresses
// Optional properties: name, iconUrl, chains, rawSessions
function append(dapp) { function append(dapp) {
try { try {
const {name, url, iconUrl, topic, accountAddresses, connectorId, rawSessions } = dapp let {name, url, iconUrl, topic, accountAddresses, connectorId, rawSessions } = dapp
if (!name || !url || !iconUrl || !topic || !connectorId || !accountAddresses || !rawSessions) { if (!url || !topic || !connectorId || !accountAddresses) {
console.warn("DAppsModel - Failed to append dapp, missing required fields", JSON.stringify(dapp)) console.warn("DAppsModel - Failed to append dapp, missing required fields", JSON.stringify(dapp))
return return
} }
name = name || ""
iconUrl = iconUrl || ""
accountAddresses = accountAddresses || []
rawSessions = rawSessions || []
root.model.append({ root.model.append({
name, name,
url, url,
@ -38,12 +47,21 @@ QObject {
} }
function remove(topic) { function remove(topic) {
for (let i = 0; i < root.model.count; i++) { const { dapp, index, sessionIndex } = findDapp(topic)
const dapp = root.model.get(i) if (!dapp) {
if (dapp.topic == topic) { console.warn("DAppsModel - Failed to remove dapp, not found", topic)
root.model.remove(i) return
break
} }
if (dapp.rawSessions.count === 1) {
root.model.remove(index)
return
}
const rawSession = dapp.rawSessions.get(sessionIndex)
dapp.rawSessions.remove(sessionIndex)
if (rawSession.topic == dapp.topic) {
root.model.setProperty(index, "topic", dapp.rawSessions.get(0).topic)
} }
} }
@ -52,9 +70,7 @@ QObject {
} }
function getByTopic(topic) { function getByTopic(topic) {
for (let i = 0; i < root.model.count; i++) { const dappTemplate = (dapp) => {
const dapp = root.model.get(i)
if (dapp.topic == topic) {
return { return {
name: dapp.name, name: dapp.name,
url: dapp.url, url: dapp.url,
@ -65,7 +81,25 @@ QObject {
rawSessions: dapp.rawSessions rawSessions: dapp.rawSessions
} }
} }
}
const { dapp } = findDapp(topic)
if (!dapp) {
return null return null
} }
return dappTemplate(dapp)
}
function findDapp(topic) {
for (let i = 0; i < root.model.count; i++) {
const dapp = root.model.get(i)
for (let j = 0; j < dapp.rawSessions.count; j++) {
if (dapp.rawSessions.get(j).topic == topic) {
return { dapp, index: i, sessionIndex: j }
break
}
}
}
return { dapp: null, index: -1, sessionIndex: -1 }
}
} }

View File

@ -0,0 +1,309 @@
import QtQuick 2.15
import AppLayouts.Wallet.services.dapps 1.0
import AppLayouts.Wallet.services.dapps.plugins 1.0
import AppLayouts.Wallet.services.dapps.types 1.0
import AppLayouts.Wallet.stores 1.0 as WalletStore
import StatusQ.Core.Utils 0.1 as SQUtils
import shared.stores 1.0
import utils 1.0
/// Component that provides the dapps integration for the wallet.
/// It provides the following features:
/// - WalletConnect integration
/// - WalletConnect pairing
/// - WalletConnect sessions management
/// - WalletConnect signing requests
/// - WalletConnect SIWE
/// - WalletConnect online status
/// - BrowserConnect integration
/// - BrowserConnect pairing
/// - BrowserConnect - access to persistent sessions
/// - BrowserConnect - access to persistent signing requests
/// - BrowserConnect signing requests
/// - BrowserConnect online status
SQUtils.QObject {
id: root
// SDKs providing the DApps API
required property WalletConnectSDKBase wcSdk
required property WalletConnectSDKBase bcSdk
// DApps shared store - used for wc peristence and signing requests/transactions
required property DAppsStore store
required property CurrenciesStore currenciesStore
// Required roles: address
required property var accountsModel
// Required roles: chainId, layer, isOnline
required property var networksModel
// Required roles: tokenKey, balances
required property var groupedAccountAssetsModel
readonly property alias requestsModel: requests
readonly property alias dappsModel: dappConnections.dappsModel
readonly property bool enabled: wcSdk.enabled || bcSdk.enabled
readonly property bool isServiceOnline: chainsSupervisorPlugin.anyChainAvailable && (wcSdk.sdkReady || bcSdk.enabled)
// Connection signals
/// Emitted when a new DApp requests a connection
signal connectDApp(var chains, string dAppUrl, string dAppName, string dAppIcon, string key)
/// Emitted when a new DApp is connected
signal dappConnected(string proposalId, string newTopic, string url, int connectorId)
/// Emitted when a DApp is disconnected
signal dappDisconnected(string topic, string url)
/// Emitted when a new DApp fails to connect
signal newConnectionFailed(string key, string dappUrl, var error)
// Pairing signals
signal pairingValidated(int validationState)
signal pairingResponse(int state) // Maps to Pairing.errors
// Sign request signals
signal signCompleted(string topic, string id, bool userAccepted, string error)
signal siweCompleted(string topic, string id, string error)
/// WalletConnect pairing
/// @param uri - the pairing URI to pair
/// Result is emitted via the pairingResponse signal
/// A new session proposal is expected to be emitted if the pairing is successful
function pair(uri) {
return wcSdk.pair(uri)
}
/// Approves or rejects the session proposal. App response to `connectDApp`
/// @param key - the key of the session proposal
/// @param approvedChainIds - array containing the chainIds that the user approved
/// @param accountAddress - the address of the account that approved the session
function approvePairSession(key, approvedChainIds, accountAddress) {
if (siwePlugin.connectionRequests.has(key.toString())) {
siwePlugin.accept(key, approvedChainIds, accountAddress)
siwePlugin.connectionRequests.delete(key.toString())
return
}
dappConnections.connect(key, approvedChainIds, accountAddress)
}
/// Rejects the session proposal. App response to `connectDApp`
/// @param id - the id of the session proposal
function rejectPairSession(id) {
if (siwePlugin.connectionRequests.has(id.toString())) {
siwePlugin.reject(id)
siwePlugin.connectionRequests.delete(id.toString())
return
}
dappConnections.reject(id)
}
/// Disconnects the WC session with the given topic. Expected `dappDisconnected` signal
/// @param sessionTopic - the topic of the session to disconnect
function disconnectSession(sessionTopic) {
dappConnections.disconnect(sessionTopic)
}
/// Validates the pairing URI and emits the pairingValidated signal. Expected `pairingValidated` signal
/// Async function
/// @param uri - the pairing URI to validate
function validatePairingUri(uri){
const info = DAppsHelpers.extractInfoFromPairUri(uri)
wcSdk.getActiveSessions((sessions) => {
// Check if the URI is already paired
let validationState = Pairing.errors.uriOk
for (const key in sessions) {
if (sessions[key].pairingTopic === info.topic) {
validationState = Pairing.errors.alreadyUsed
break
}
}
// Check if expired
if (validationState === Pairing.errors.uriOk) {
const now = (new Date().getTime())/1000
if (info.expiry < now) {
validationState = Pairing.errors.expired
}
}
root.pairingValidated(validationState)
});
}
/// Returns the DApp with the given topic
/// @param topic - the topic of the DApp to return
/// @return the DApp with the given topic
/// DApp {
/// name: string
/// url: string
/// iconUrl: string
/// topic: string
/// connectorId: int
/// accountAddressses: [{address: string}]
/// chains: string
/// rawSessions: [{session: object}]
/// }
function getDApp(topic) {
return SQUtils.ModelUtils.getFirstModelEntryIf(dappsModel, (dapp) => {
return dapp.topic === topic
SQUtils.ModelUtils.getFirstModelEntryIf(dapp.rawSessions, (session) => {
return session.topic === topic
})
})
}
DAppConnectionsPlugin {
id: dappConnections
wcSDK: root.wcSdk
bcSDK: root.bcSdk
dappsStore: root.store
accountsModel: root.accountsModel
networksModel: root.networksModel
onConnected: (proposalId, topic, url, connectorId) => {
root.dappConnected(proposalId, topic, url, connectorId)
}
onDisconnected: (topic, url) => {
root.dappDisconnected(topic, url)
}
onNewConnectionProposed: (key, chains, dAppUrl, dAppName, dAppIcon) => {
root.connectDApp(chains, dAppUrl, dAppName, dAppIcon, key)
}
onNewConnectionFailed: (key, dappUrl, error) => {
root.newConnectionFailed(key, dappUrl, error)
}
}
SessionRequestsModel {
id: requests
}
ChainsSupervisorPlugin {
id: chainsSupervisorPlugin
sdk: root.wcSdk
networksModel: root.networksModel
}
Connections {
target: root.wcSdk
enabled: root.wcSdk.enabled
function onPairResponse(ok) {
root.pairingResponse(ok)
}
}
SiweRequestPlugin {
id: siwePlugin
readonly property var connectionRequests: new Map()
sdk: root.wcSdk
store: root.store
accountsModel: root.accountsModel
networksModel: root.networksModel
onRegisterSignRequest: (request) => {
requests.enqueue(request)
}
onUnregisterSignRequest: (requestId) => {
const request = requests.findById(requestId)
if (request === null) {
console.error("SiweRequestPlugin::onUnregisterSignRequest: Error finding event for requestId", requestId)
return
}
requests.removeRequest(request.topic, requestId)
}
onConnectDApp: (chains, dAppUrl, dAppName, dAppIcon, key) => {
siwePlugin.connectionRequests.set(key.toString(), {chains, dAppUrl, dAppName, dAppIcon})
root.connectDApp(chains, dAppUrl, dAppName, dAppIcon, key)
}
onSiweFailed: (id, error, topic) => {
root.siweCompleted(topic, id, error)
}
onSiweSuccessful: (id, topic) => {
d.lookupSession(topic, function(session) {
// Persist session
if(!root.store.addWalletConnectSession(JSON.stringify(session))) {
console.error("Failed to persist session")
}
root.siweCompleted(topic, id, "")
})
}
function accept(key, approvedChainIds, accountAddress) {
const approvedNamespaces = JSON.parse(
DAppsHelpers.buildSupportedNamespaces(approvedChainIds,
[accountAddress],
SessionRequest.getSupportedMethods()))
siwePlugin.connectionApproved(key, approvedNamespaces)
}
function reject(key) {
siwePlugin.connectionRejected(key)
}
}
SQUtils.QObject {
id: d
function lookupSession(topicToLookup, callback) {
wcSdk.getActiveSessions((res) => {
Object.keys(res).forEach((topic) => {
if (topic === topicToLookup) {
let session = res[topic]
callback(session)
}
})
})
}
}
// bcSignRequestPlugin and wcSignRequestPlugin are used to handle sign requests
// Almost identical, and it's worth extracting in an inline component, but Qt5.15.2 doesn't support it
SignRequestPlugin {
id: bcSignRequestPlugin
sdk: root.bcSdk
groupedAccountAssetsModel: root.groupedAccountAssetsModel
networksModel: root.networksModel
accountsModel: root.accountsModel
currentCurrency: root.currenciesStore.currentCurrency
store: root.store
requests: root.requestsModel
dappsModel: root.dappsModel
getFiatValue: (value, currency) => {
return root.currenciesStore.getFiatValue(value, currency)
}
onSignCompleted: (topic, id, userAccepted, error) => {
root.signCompleted(topic, id, userAccepted, error)
}
}
SignRequestPlugin {
id: wcSignRequestPlugin
sdk: root.wcSdk
groupedAccountAssetsModel: root.groupedAccountAssetsModel
networksModel: root.networksModel
accountsModel: root.accountsModel
currentCurrency: root.currenciesStore.currentCurrency
store: root.store
requests: root.requestsModel
dappsModel: root.dappsModel
getFiatValue: (value, currency) => {
return root.currenciesStore.getFiatValue(value, currency)
}
onSignCompleted: (topic, id, userAccepted, error) => {
root.signCompleted(topic, id, userAccepted, error)
}
}
}

View File

@ -1127,6 +1127,8 @@ SQUtils.QObject {
} }
onAuthFailed: () => { onAuthFailed: () => {
d.unsubscribeForFeeUpdates(request.topic, request.requestId)
sdk.rejectSessionRequest(request.topic, request.requestId, true)
const appDomain = SQUtils.StringUtils.extractDomainFromLink(request.dappUrl) const appDomain = SQUtils.StringUtils.extractDomainFromLink(request.dappUrl)
const methodStr = SessionRequest.methodToUserString(request.method) const methodStr = SessionRequest.methodToUserString(request.method)
if (!methodStr) { if (!methodStr) {

View File

@ -0,0 +1,271 @@
import QtQuick 2.15
import StatusQ 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import AppLayouts.Wallet 1.0
import AppLayouts.Wallet.services.dapps 1.0
import AppLayouts.Wallet.services.dapps.types 1.0
import shared.popups.walletconnect 1.0
import SortFilterProxyModel 0.2
import utils 1.0
import "types"
SQUtils.QObject {
id: root
required property DAppsModule dappsModule
required property string selectedAddress
required property var accountsModel
property bool walletConnectFeatureEnabled: true
property bool connectorFeatureEnabled: true
// Output properties
/// Model contaning all dApps available for the currently selected account
readonly property var dappsModel: d.filteredDappsModel
/// Model containig the dApps session requests to be resolved by the user
readonly property SessionRequestsModel sessionRequestsModel: dappsModule.requestsModel
/// Service can interact with the current address selection
/// Default value: true
readonly property bool serviceAvailableToCurrentAddress: !root.selectedAddress ||
SQUtils.ModelUtils.contains(root.accountsModel, "address", root.selectedAddress, Qt.CaseInsensitive)
readonly property bool isServiceOnline: dappsModule.isServiceOnline
// signals
signal connectDApp(var dappChains, url dappUrl, string dappName, url dappIcon, string key)
// Emitted as a response to DAppsService.approveSession
// @param key The key of the session proposal
// @param error The error message
// @param topic The new topic of the session
signal approveSessionResult(string key, var error, string topic)
// Emitted when a new session is requested by a dApp
signal sessionRequest(string id)
// Emitted when the services requests to display a toast message
// @param message The message to display
// @param type The type of the message. Maps to Constants.ephemeralNotificationType
signal displayToastMessage(string message, int type)
// Emitted as a response to DAppsService.validatePairingUri or other DAppsService.pair
// and DAppsService.approvePair errors
signal pairingValidated(int validationState)
// methods
/// Triggers the signing process for the given session request
/// @param topic The topic of the session
/// @param id The id of the session request
function sign(topic, id) {
// The authentication triggers the signing process
// authenticate -> sign -> inform the dApp
d.sign(topic, id)
}
function rejectSign(topic, id, hasError) {
d.rejectSign(topic, id, hasError)
}
function signRequestIsLive(topic, id) {
d.signRequestIsLive(topic, id)
}
/// Validates the pairing URI
function validatePairingUri(uri) {
d.validatePairingUri(uri)
}
/// Initiates the pairing process with the given URI
function pair(uri) {
timeoutTimer.start()
dappsModule.pair(uri)
}
/// Approves or rejects the session proposal
function approvePairSession(key, approvedChainIds, accountAddress) {
dappsModule.approvePairSession(key, approvedChainIds, accountAddress)
}
/// Rejects the session proposal
function rejectPairSession(id) {
dappsModule.rejectPairSession(id)
}
/// Disconnects the dApp with the given topic
/// @param topic The topic of the dApp
/// @param source The source of the dApp; either "walletConnect" or "connector"
function disconnectDapp(topic) {
d.disconnectDapp(topic)
}
SQUtils.QObject {
id: d
readonly property var filteredDappsModel: SortFilterProxyModel {
id: dappsFilteredModel
objectName: "DAppsModelFiltered"
sourceModel: root.dappsModule.dappsModel
readonly property string selectedAddress: root.selectedAddress
filters: FastExpressionFilter {
enabled: !!dappsFilteredModel.selectedAddress
function isAddressIncluded(accountAddressesSubModel, selectedAddress) {
if (!accountAddressesSubModel) {
return false
}
const addresses = SQUtils.ModelUtils.modelToFlatArray(accountAddressesSubModel, "address")
return addresses.includes(selectedAddress)
}
expression: isAddressIncluded(model.accountAddresses, dappsFilteredModel.selectedAddress)
expectedRoles: "accountAddresses"
}
}
function disconnectDapp(connectionId) {
dappsModule.disconnectSession(connectionId)
}
function validatePairingUri(uri) {
// Check if emoji inside the URI
if(Constants.regularExpressions.emoji.test(uri)) {
root.pairingValidated(Pairing.errors.tooCool)
return
} else if(!DAppsHelpers.validURI(uri)) {
root.pairingValidated(Pairing.errors.invalidUri)
return
}
dappsModule.validatePairingUri(uri)
}
function signRequestIsLive(topic, id) {
const request = root.sessionRequestsModel.findRequest(topic, id)
if (!request) {
console.error("Session request not found")
return
}
request.setActive()
}
function sign(topic, id) {
const request = root.sessionRequestsModel.findRequest(topic, id)
if (!request) {
console.error("Session request not found")
return
}
request.accept()
}
function rejectSign(topic, id, hasError) {
const request = root.sessionRequestsModel.findRequest(topic, id)
if (!request) {
console.error("Session request not found")
return
}
request.reject(hasError)
}
function reportPairErrorState(state) {
timeoutTimer.stop()
root.pairingValidated(state)
}
}
Connections {
target: root.dappsModule
enabled: root.walletConnectFeatureEnabled || root.connectorFeatureEnabled
function onPairingValidated(state) {
timeoutTimer.stop()
root.pairingValidated(state)
}
function onPairingResponse(key, state) {
timeoutTimer.stop()
if (state != Pairing.errors.uriOk) {
d.reportPairErrorState(state)
}
}
function onConnectDApp(dappChains, dappUrl, dappName, dappIcon, key) {
root.connectDApp(dappChains, dappUrl, dappName, dappIcon, key)
}
function onDappConnected(proposal, topic, url, connectorId) {
const dappDomain = SQUtils.StringUtils.extractDomainFromLink(url)
const connectorName = connectorId === Constants.WalletConnect ? "WalletConnect" : "Status Connector"
root.displayToastMessage(qsTr("Connected to %1 via %2").arg(dappDomain).arg(connectorName), Constants.ephemeralNotificationType.success)
root.approveSessionResult(proposal, null, topic)
}
function onDappDisconnected(topic, url) {
const appDomain = SQUtils.StringUtils.extractDomainFromLink(url)
root.displayToastMessage(qsTr("Disconnected from %1").arg(appDomain), Constants.ephemeralNotificationType.success)
}
function onNewConnectionFailed(key, url, error) {
timeoutTimer.stop()
const dappDomain = SQUtils.StringUtils.extractDomainFromLink(url)
if (error === Pairing.errors.userRejected) {
root.displayToastMessage(qsTr("Connection request for %1 was rejected").arg(dappDomain), Constants.ephemeralNotificationType.success)
}
if (error === Pairing.errors.rejectFailed) {
root.displayToastMessage(qsTr("Failed to reject connection request for %1").arg(dappDomain), Constants.ephemeralNotificationType.danger)
}
d.reportPairErrorState(error)
root.approveSessionResult(key, error, "")
}
function onSignCompleted(topic, id, userAccepted, error) {
const request = root.sessionRequestsModel.findRequest(topic, id)
if (!request) {
console.error("Session request not found")
return
}
const methodStr = SessionRequest.methodToUserString(request.method)
const appUrl = request.dappUrl
const appDomain = SQUtils.StringUtils.extractDomainFromLink(appUrl)
if (!methodStr) {
console.error("Error finding user string for method", request.method)
return
}
if (error) {
root.displayToastMessage(qsTr("Fail to %1 from %2").arg(methodStr).arg(appDomain), Constants.ephemeralNotificationType.danger)
return
}
const requestExpired = request.isExpired()
if (requestExpired) {
root.displayToastMessage("%1 sign request timed out".arg(appDomain), Constants.ephemeralNotificationType.normal)
return
}
const actionStr = userAccepted ? qsTr("accepted") : qsTr("rejected")
root.displayToastMessage("%1 %2 %3".arg(appDomain).arg(methodStr).arg(actionStr), Constants.ephemeralNotificationType.success)
}
function onSiweCompleted(topic, id, userAccepted, error) {
if (error) {
root.displayToastMessage(error, Constants.ephemeralNotificationType.danger)
}
root.approveSessionResult(id, error, topic)
}
}
// 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
Timer {
id: timeoutTimer
interval: 10000 // (10 seconds)
running: false
repeat: false
onTriggered: {
d.reportPairErrorState(Pairing.errors.unknownError)
}
}
}

View File

@ -21,773 +21,126 @@ import utils 1.0
import "types" import "types"
// Act as another layer of abstraction to the WalletConnectSDKBase /// 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 /// Converts the store requests into WalletConnect standard requests
// the UX requirements
WalletConnectSDKBase { WalletConnectSDKBase {
id: root id: root
required property WalletConnectService wcService required property BrowserConnectStore store
required property var walletStore /// Required roles: chainId
required property DAppsStore store required property var networksModel
required property int loginType /// Required roles: address
required property var accountsModel
property var controller
property var dappInfo: null
property var txArgs: null
property bool sdkReady: true
property bool active: true
property string requestId: ""
property alias requestsModel: requests
readonly property string invalidDAppUrlError: "Invalid dappInfo: URL is missing"
readonly property string invalidDAppTopicError: "Invalid dappInfo: failed to parse topic"
projectId: "" projectId: ""
implicitWidth: 1
implicitHeight: 1
// TODO Refactor this code to avoid code duplication from Wallet Connect DAppsRequestHandler
// https://github.com/status-im/status-desktop/issues/15711
QtObject {
id: d
function sessionRequestEvent(event) {
let obj = d.resolveAsync(event)
if (obj === null) {
let error = true
controller.rejectTransactionSigning(root.requestId)
return
}
sessionRequestLoader.request = obj
requests.enqueue(obj)
}
function resolveAsync(event) {
let method = event.params.request.method
let accountAddress = lookupAccountFromEvent(event, method)
if(!accountAddress) {
console.error("Error finding accountAddress for event", JSON.stringify(event))
return null
}
let chainId = lookupNetworkFromEvent(event, method)
if(!chainId) {
console.error("Error finding network for event", JSON.stringify(event))
return null
}
let data = extractMethodData(event, method)
if(!data) {
console.error("Error in event data lookup", JSON.stringify(event))
return null
}
const interpreted = d.prepareData(method, data)
let enoughFunds = !isTransactionMethod(method)
let obj = sessionRequestComponent.createObject(null, {
event,
topic: event.topic,
requestId: event.id,
method,
accountAddress,
chainId,
data,
preparedData: interpreted.preparedData,
maxFeesText: "?",
maxFeesEthText: "?",
haveEnoughFunds: enoughFunds
})
if (obj === null) {
console.error("Error creating SessionRequestResolved for event")
return null
}
// Check later to have a valid request object
if (!SessionRequest.getSupportedMethods().includes(method)) {
console.error("Unsupported method", method)
return null
}
let session = getActiveSession(root.dappInfo)
if (session === null) {
console.error("Connector.lookupSession: error finding session for requestId ", obj.requestId)
return
}
obj.resolveDappInfoFromSession(session)
if (d.isTransactionMethod(method)) {
let tx = obj.data.tx
if (tx === null) {
console.error("Error cannot resolve tx object")
return null
}
let BigOps = SQUtils.AmountsArithmetic
let gasLimit = hexToGwei(tx.gasLimit)
if (tx.gasPrice === null || tx.gasPrice === undefined) {
let maxFeePerGas = hexToGwei(tx.maxFeePerGas)
let maxPriorityFeePerGas = hexToGwei(tx.maxPriorityFeePerGas)
let totalMaxFees = BigOps.sum(maxFeePerGas, maxPriorityFeePerGas)
let maxFees = BigOps.times(gasLimit, totalMaxFees)
let maxFeesString = maxFees.toString()
obj.maxFeesText = maxFeesString
obj.maxFeesEthText = maxFeesString
obj.haveEnoughFunds = true
} else {
let gasPrice = hexToGwei(tx.gasPrice)
let maxFees = BigOps.times(gasLimit, gasPrice)
let maxFeesString = maxFees.toString()
obj.maxFeesText = maxFeesString
obj.maxFeesEthText = maxFeesString
obj.haveEnoughFunds = true
}
}
return obj
}
function getTxObject(method, data) {
let tx
if (method === SessionRequest.methods.signTransaction.name) {
tx = SessionRequest.methods.signTransaction.getTxObjFromData(data)
} else if (method === SessionRequest.methods.sendTransaction.name) {
tx = SessionRequest.methods.sendTransaction.getTxObjFromData(data)
} else {
console.error("Not a transaction method")
}
return tx
}
// returns {
// preparedData,
// value // null or ETH Big number
// }
function prepareData(method, data) {
let payload = null
switch(method) {
case SessionRequest.methods.personalSign.name: {
payload = SessionRequest.methods.personalSign.getMessageFromData(data)
break
}
case SessionRequest.methods.sign.name: {
payload = SessionRequest.methods.sign.getMessageFromData(data)
break
}
case SessionRequest.methods.signTypedData_v4.name: {
const stringPayload = SessionRequest.methods.signTypedData_v4.getMessageFromData(data)
payload = JSON.stringify(JSON.parse(stringPayload), null, 2)
break
}
case SessionRequest.methods.signTypedData.name: {
const stringPayload = SessionRequest.methods.signTypedData.getMessageFromData(data)
payload = JSON.stringify(JSON.parse(stringPayload), null, 2)
break
}
case SessionRequest.methods.signTransaction.name:
case SessionRequest.methods.sendTransaction.name:
// For transactions we process the data in a different way as follows
break
default:
console.error("Unhandled method", method)
break;
}
let value = SQUtils.AmountsArithmetic.fromNumber(0)
if (d.isTransactionMethod(method)) {
let txObj = d.getTxObject(method, data)
let tx = Object.assign({}, txObj)
if (tx.value) {
value = hexToEth(tx.value)
tx.value = value.toString()
}
if (tx.maxFeePerGas) {
tx.maxFeePerGas = hexToGwei(tx.maxFeePerGas).toString()
}
if (tx.maxPriorityFeePerGas) {
tx.maxPriorityFeePerGas = hexToGwei(tx.maxPriorityFeePerGas).toString()
}
if (tx.gasPrice) {
tx.gasPrice = hexToGwei(tx.gasPrice)
}
if (tx.gasLimit) {
tx.gasLimit = parseInt(root.store.hexToDec(tx.gasLimit))
}
if (tx.nonce) {
tx.nonce = parseInt(root.store.hexToDec(tx.nonce))
}
payload = JSON.stringify(tx, null, 2)
}
return {
preparedData: payload,
value: value
}
}
function hexToEth(value) {
return hexToEthDenomination(value, "eth")
}
function hexToGwei(value) {
return hexToEthDenomination(value, "gwei")
}
function hexToEthDenomination(value, ethUnit) {
let unitMapping = {
"gwei": 9,
"eth": 18
}
let BigOps = SQUtils.AmountsArithmetic
let decValue = root.store.hexToDec(value)
if (!!decValue) {
return BigOps.div(BigOps.fromNumber(decValue), BigOps.fromNumber(1, unitMapping[ethUnit]))
}
return BigOps.fromNumber(0)
}
function isTransactionMethod(method) {
return method === SessionRequest.methods.signTransaction.name
|| method === SessionRequest.methods.sendTransaction.name
}
/// Returns null if the account is not found
function lookupAccountFromEvent(event, method) {
var address = ""
if (method === SessionRequest.methods.personalSign.name) {
if (event.params.request.params.length < 2) {
return address
}
address = event.params.request.params[1]
} else if (method === SessionRequest.methods.sign.name) {
if (event.params.request.params.length === 1) {
return address
}
address = event.params.request.params[0]
} else if(method === SessionRequest.methods.signTypedData_v4.name ||
method === SessionRequest.methods.signTypedData.name)
{
if (event.params.request.params.length < 2) {
return address
}
address = event.params.request.params[0]
} else if (method === SessionRequest.methods.signTransaction.name
|| method === SessionRequest.methods.sendTransaction.name) {
if (event.params.request.params.length == 0) {
return address
}
address = event.params.request.params[0].from
} else {
console.error("Unsupported method to lookup account: ", method)
return null
}
const account = SQUtils.ModelUtils.getFirstModelEntryIf(root.wcService.validAccounts, (account) => {
return account.address.toLowerCase() === address.toLowerCase();
})
if (!account) {
return address
}
return account.address
}
/// Returns null if the network is not found
function lookupNetworkFromEvent(event, method) {
if (SessionRequest.getSupportedMethods().includes(method) === false) {
return null
}
const chainId = DAppsHelpers.chainIdFromEip155(event.params.chainId)
const network = SQUtils.ModelUtils.getByKey(root.walletStore.filteredFlatModel, "chainId", chainId)
if (!network) {
return null
}
return network.chainId
}
function extractMethodData(event, method) {
if (method === SessionRequest.methods.personalSign.name ||
method === SessionRequest.methods.sign.name)
{
if (event.params.request.params.length < 1) {
return null
}
let message = ""
const messageIndex = (method === SessionRequest.methods.personalSign.name ? 0 : 1)
const messageParam = event.params.request.tx.data
// There is no standard on how data is encoded. Therefore we support hex or utf8
if (DAppsHelpers.isHex(messageParam)) {
message = DAppsHelpers.hexToString(messageParam)
} else {
message = messageParam
}
return SessionRequest.methods.personalSign.buildDataObject(message)
} else if (method === SessionRequest.methods.signTypedData_v4.name ||
method === SessionRequest.methods.signTypedData.name)
{
if (event.params.request.params.length < 2) {
return null
}
let jsonMessage = event.params.request.params[1]
let methodObj = method === SessionRequest.methods.signTypedData_v4.name
? SessionRequest.methods.signTypedData_v4
: SessionRequest.methods.signTypedData
return methodObj.buildDataObject(jsonMessage)
} else if (method === SessionRequest.methods.signTransaction.name) {
if (event.params.request.params.length == 0) {
return null
}
let tx = event.params.request.params[0]
return SessionRequest.methods.signTransaction.buildDataObject(tx)
} else if (method === SessionRequest.methods.sendTransaction.name) {
if (event.params.request.params.length == 0) {
return null
}
const tx = event.params.request.params[0]
return SessionRequest.methods.sendTransaction.buildDataObject(tx)
} else {
return null
}
}
function executeSessionRequest(request, password, pin) {
if (!SessionRequest.getSupportedMethods().includes(request.method)) {
console.error("Unsupported method to execute: ", request.method)
return
}
if (password === "") {
console.error("No password provided to sign message")
return
}
if (request.method === SessionRequest.methods.sign.name) {
store.signMessageUnsafe(request.topic,
request.requestId,
request.accountAddress,
SessionRequest.methods.personalSign.getMessageFromData(request.data),
password,
pin)
} else if (request.method === SessionRequest.methods.personalSign.name) {
store.signMessage(request.topic,
request.requestId,
request.accountAddress,
SessionRequest.methods.personalSign.getMessageFromData(request.data),
password,
pin)
} else if (request.method === SessionRequest.methods.signTypedData_v4.name ||
request.method === SessionRequest.methods.signTypedData.name)
{
let legacy = request.method === SessionRequest.methods.signTypedData.name
store.safeSignTypedData(request.topic,
request.requestId,
request.accountAddress,
SessionRequest.methods.signTypedData.getMessageFromData(request.data),
request.chainId,
legacy,
password,
pin)
} else if (request.method === SessionRequest.methods.signTransaction.name) {
let txObj = SessionRequest.methods.signTransaction.getTxObjFromData(request.data)
store.signTransaction(request.topic,
request.requestId,
request.accountAddress,
request.chainId,
txObj,
password,
pin)
} else if (request.method === SessionRequest.methods.sendTransaction.name) {
store.sendTransaction(request.topic,
request.requestId,
request.accountAddress,
request.chainId,
request.data.tx,
password,
pin)
}
}
function acceptSessionRequest(topic, id, signature) {
console.debug(`Connector DappsConnectorSDK.acceptSessionRequest; requestId: ${root.requestId}, signature: "${signature}"`)
sessionRequestLoader.active = false
controller.approveTransactionRequest(requestId, signature)
root.wcService.displayToastMessage(qsTr("Successfully signed transaction from %1").arg(root.dappInfo.url), false)
}
function getActiveSession(dappInfos) {
let sessionTemplate = (dappUrl, dappName, dappIcon) => {
return {
"peer": {
"metadata": {
"description": "-",
"icons": [
dappIcon
],
"name": dappName,
"url": dappUrl
}
},
"topic": dappUrl
};
}
let session = root.wcService.connectorDAppsProvider.getActiveSession(dappInfos.url)
if (!session) {
console.error("Connector.lookupSession: error finding session for requestId ", root.requestId)
return
}
return sessionTemplate(session.url, session.name, session.icon)
}
function authenticate(request) {
return store.authenticateUser(request.topic, request.requestId, request.accountAddress)
}
}
Connections { Connections {
target: root.store target: root.store
enabled: root.enabled
function onUserAuthenticated(topic, id, password, pin) { function onSignRequested(requestId, dappInfoString) {
var request = requests.findRequest(topic, id) try {
if (request === null) {
console.error(">Error finding event for topic", topic, "id", id)
return
}
d.executeSessionRequest(request, password, pin)
}
function onUserAuthenticationFailed(topic, id) {
var request = requests.findRequest(topic, id)
let methodStr = SessionRequest.methodToUserString(request.method)
if (request === null || !methodStr) {
return
}
d.lookupSession(topic, function(session) {
if (session === null)
return
root.displayToastMessage(qsTr("Failed to authenticate %1").arg(session.peer.metadata.url), true)
})
}
function onSigningResult(topic, id, data) {
let isSuccessful = (data != "")
if (isSuccessful) {
// acceptSessionRequest will trigger an sdk.sessionRequestUserAnswerResult signal
d.acceptSessionRequest(topic, id, data)
} else {
console.error("signing error")
}
}
}
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: {
rejectSession(root.requestId)
connectDappLoader.active = false
}
flatNetworks: root.walletStore.filteredFlatModel
accounts: root.wcService.validAccounts
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)
}
onDisconnect: {
connectDappLoader.active = false;
controller.recallDAppPermission(root.dappInfo.url)
}
}
}
Loader {
id: sessionRequestLoader
active: false
onLoaded: item.open()
property SessionRequestResolved request: null
property var dappInfo: null
sourceComponent: DAppSignRequestModal {
id: dappRequestModal
objectName: "connectorDappsRequestModal"
readonly property var account: accountEntry.available ? accountEntry.item : {
"address": "",
"name": "",
"emoji": "",
"colorId": 0
}
readonly property var network: networkEntry.available ? networkEntry.item : {
"chainId": 0,
"chainName": "",
"iconUrl": ""
}
loginType: account.migragedToKeycard ? Constants.LoginType.Keycard : root.loginType
formatBigNumber: (number, symbol, noSymbolOption) => root.wcService.walletRootStore.currencyStore.formatBigNumber(number, symbol, noSymbolOption)
visible: true
dappName: request.dappName
dappUrl: request.dappUrl
dappIcon: request.dappIcon
accountColor: Utils.getColorForId(account.colorId)
accountName: account.name
accountAddress: account.address
accountEmoji: account.emoji
networkName: network.chainName
networkIconPath: Theme.svg(network.iconUrl)
fiatFees: request.maxFeesText
cryptoFees: request.maxFeesEthText
estimatedTime: ""
feesLoading: !request.maxFeesText || !request.maxFeesEthText
hasFees: signingTransaction
enoughFundsForTransaction: request.haveEnoughFunds
enoughFundsForFees: request.haveEnoughFunds
signingTransaction: request.method === SessionRequest.methods.signTransaction.name || request.method === SessionRequest.methods.sendTransaction.name
requestPayload: {
switch(request.method) {
case SessionRequest.methods.personalSign.name:
return SessionRequest.methods.personalSign.getMessageFromData(request.data)
case SessionRequest.methods.sign.name: {
return SessionRequest.methods.sign.getMessageFromData(request.data)
}
case SessionRequest.methods.signTypedData_v4.name: {
const stringPayload = SessionRequest.methods.signTypedData_v4.getMessageFromData(request.data)
return JSON.stringify(JSON.parse(stringPayload), null, 2)
}
case SessionRequest.methods.signTypedData.name: {
const stringPayload = SessionRequest.methods.signTypedData.getMessageFromData(root.payloadData)
return JSON.stringify(JSON.parse(stringPayload), null, 2)
}
case SessionRequest.methods.signTransaction.name: {
const jsonPayload = SessionRequest.methods.signTransaction.getTxObjFromData(request.data)
return JSON.stringify(jsonPayload, null, 2)
}
case SessionRequest.methods.sendTransaction.name: {
const jsonPayload = SessionRequest.methods.sendTransaction.getTxObjFromData(request.data)
return JSON.stringify(request.data, null, 2)
}
}
}
onClosed: {
Qt.callLater( () => {
sessionRequestLoader.active = false
})
}
onAccepted: {
if (!request) {
console.error("Error signing: request is null")
return
}
d.authenticate(request)
}
onRejected: {
sessionRequestLoader.active = false
controller.rejectTransactionSigning(root.requestId)
root.wcService.displayToastMessage(qsTr("Failed to sign transaction from %1").arg(request.dappUrl), true)
}
ModelEntry {
id: networkEntry
sourceModel: root.wcService.flatNetworks
key: "chainId"
value: request.chainId
}
ModelEntry {
id: accountEntry
sourceModel: root.wcService.validAccounts
key: "address"
value: request.accountAddress
}
}
}
Component {
id: sessionRequestComponent
SessionRequestResolved {
sourceId: Constants.DAppConnectors.StatusConnect
}
}
SessionRequestsModel {
id: requests
}
Connections {
target: root.wcService
function onRevokeSession(topic) {
if (!topic) {
console.warn(invalidDAppTopicError)
return
}
controller.recallDAppPermission(topic)
root.wcService.connectorDAppsProvider.revokeSession(topic)
}
}
Connections {
target: controller
onDappValidatesTransaction: function(requestId, dappInfoString) {
var dappInfo = JSON.parse(dappInfoString) var dappInfo = JSON.parse(dappInfoString)
root.dappInfo = dappInfo
var txArgsParams = JSON.parse(dappInfo.txArgs) var txArgsParams = JSON.parse(dappInfo.txArgs)
root.txArgs = txArgsParams let event = d.buildSessionRequest(requestId, dappInfo.url, dappInfo.chainId, SessionRequest.methods.sendTransaction.name, txArgsParams)
let event = {
"id": root.requestId, root.sessionRequestEvent(event)
"topic": dappInfo.url, } catch (e) {
"params": { console.error("Failed to parse dappInfo for session request", e)
"chainId": `eip155:${dappInfo.chainId}`,
"request": {
"method": SessionRequest.methods.sendTransaction.name,
"params": [
{
"from": txArgsParams.from,
"to": txArgsParams.to,
"value": txArgsParams.value,
"gasLimit": txArgsParams.gas,
"gasPrice": txArgsParams.gasPrice,
"maxFeePerGas": txArgsParams.maxFeePerGas,
"maxPriorityFeePerGas": txArgsParams.maxPriorityFeePerGas,
"nonce": txArgsParams.nonce,
"data": txArgsParams.data
}
]
}
} }
} }
d.sessionRequestEvent(event) function onConnectRequested(requestId, dappInfoString) {
try {
sessionRequestLoader.active = true
root.requestId = requestId
}
onDappRequestsToConnect: function(requestId, dappInfoString) {
var dappInfo = JSON.parse(dappInfoString) var dappInfo = JSON.parse(dappInfoString)
root.dappInfo = dappInfo dappInfo.proposal = d.buildSessionProposal(requestId, dappInfo.url, dappInfo.name, dappInfo.icon, SessionRequest.getSupportedMethods())
let sessionProposal = { d.sessionRequests.set(requestId, dappInfo)
"params": { root.sessionProposal(dappInfo.proposal)
"optionalNamespaces": {}, } catch (e) {
"proposer": { console.error("Failed to parse dappInfo for connection request", e)
"metadata": {
"description": "-",
"icons": [
dappInfo.icon
],
"name": dappInfo.name,
"url": dappInfo.url
} }
},
"requiredNamespaces": {
"eip155": {
"chains": [
`eip155:${dappInfo.chainId}`
],
"events": [],
"methods": [SessionRequest.methods.personalSign.name]
}
}
}
};
connectDappLoader.sessionProposal = sessionProposal
connectDappLoader.active = true
root.requestId = requestId
} }
onDappGrantDAppPermission: function(dappInfoString) { function onDisconnected(dappInfoString) {
try {
let dappItem = JSON.parse(dappInfoString) let dappItem = JSON.parse(dappInfoString)
const { url, name, icon: iconUrl } = dappItem root.sessionDelete(dappItem.url, false)
} catch (e) {
if (!url) { console.error("Failed to parse dappInfo for disconnection", e)
console.warn(invalidDAppUrlError) }
return
} }
root.wcService.connectorDAppsProvider.addSession(url, name, iconUrl) function onApproveTransactionResponse(topic, requestId, error) {
try {
const errorStr = error ? "Faled to approve trasnsaction" : ""
root.sessionRequestUserAnswerResult(topic, requestId, true, errorStr)
} catch (e) {
console.error("Failed to approve transaction response", e)
}
} }
onDappRevokeDAppPermission: function(dappInfoString) { function onRejectTransactionResponse(topic, requestId, error) {
let dappItem = JSON.parse(dappInfoString) try {
let session = { const errorStr = error ? "Faled to reject trasnsaction" : ""
"url": dappItem.url, root.sessionRequestUserAnswerResult(topic, requestId, false, errorStr)
"name": dappItem.name, } catch (e) {
"iconUrl": dappItem.icon, console.error("Failed to reject transaction response", e)
"topic": dappItem.url
} }
if (!session.url) {
console.warn(invalidDAppUrlError)
return
}
root.wcService.connectorDAppsProvider.revokeSession(JSON.stringify(session))
root.wcService.displayToastMessage(qsTr("Disconnected from %1").arg(dappItem.url), false)
} }
} }
approveSession: function(requestId, account, selectedChains) { approveSession: function(requestId, account, selectedChains) {
controller.approveDappConnectRequest(requestId, account, JSON.stringify(selectedChains)) try {
const { url, name, icon: iconUrl } = root.dappInfo; if (!d.sessionRequests.has(requestId)) {
//TODO: temporary solution until we have a proper way to handle accounts console.error("Session request not found")
//The dappProvider should add a new session only when the backend has validated the connection return
//Currently the dapp info is limited to the url, name and icon }
root.wcService.connectorDAppsProvider.addSession(url, name, iconUrl, account) const dappInfo = d.sessionRequests.get(requestId)
root.wcService.displayToastMessage(qsTr("Successfully authenticated %1").arg(url), false); root.store.approveConnection(requestId, account, JSON.stringify(selectedChains))
const newSession = d.buildSession(dappInfo.url, dappInfo.name, dappInfo.icon, requestId, account, selectedChains)
root.approveSessionResult(requestId, newSession, "")
d.sessionRequests.delete(requestId)
} catch (e) {
console.error("Failed to approve session", e)
}
} }
rejectSession: function(requestId) { rejectSession: function(requestId) {
controller.rejectDappConnectRequest(requestId) try {
root.wcService.displayToastMessage(qsTr("Failed to authenticate %1").arg(root.dappInfo.url), true) root.store.rejectConnection(requestId)
root.rejectSessionResult(requestId, "")
d.sessionRequests.delete(requestId)
} catch (e) {
console.error("Failed to reject session", e)
}
}
acceptSessionRequest: function(topic, requestId, signature) {
root.store.approveTransaction(topic, requestId, signature)
}
rejectSessionRequest: function(topic, requestId, error) {
root.store.rejectTransaction(topic, requestId, error)
}
disconnectSession: function(topic) {
root.store.disconnect(topic)
}
getActiveSessions: function(callback) {
try {
const dappsStr = root.store.getDApps()
const dapps = JSON.parse(dappsStr)
let activeSessions = {}
for (let i = 0; i < dapps.length; i++) {
const dapp = dapps[i]
activeSessions[dapp.url] = d.buildSession(dapp.url, dapp.name, dapp.iconUrl, "", dapp.sharedAccount, [dapp.chainId])
}
callback(activeSessions)
} catch (e) {
console.error("Failed to get active sessions", e)
callback([])
}
} }
// We don't expect requests for these. They are here only to spot errors // We don't expect requests for these. They are here only to spot errors
@ -795,4 +148,86 @@ WalletConnectSDKBase {
getPairings: function(callback) { console.error("ConnectorSDK.getPairings: not implemented") } getPairings: function(callback) { console.error("ConnectorSDK.getPairings: not implemented") }
disconnectPairing: function(topic) { console.error("ConnectorSDK.disconnectPairing: not implemented") } disconnectPairing: function(topic) { console.error("ConnectorSDK.disconnectPairing: not implemented") }
buildApprovedNamespaces: function(params, supportedNamespaces) { console.error("ConnectorSDK.buildApprovedNamespaces: not implemented") } buildApprovedNamespaces: function(params, supportedNamespaces) { console.error("ConnectorSDK.buildApprovedNamespaces: not implemented") }
QtObject {
id: d
readonly property var sessionRequests: new Map()
function buildSession(dappUrl, dappName, dappIcon, proposalId, account, chains) {
let sessionTemplate = (dappUrl, dappName, dappIcon, proposalId, eipAccount, eipChains) => {
return {
peer: {
metadata: {
description: "-",
icons: [
dappIcon
],
name: dappName,
url: dappUrl
}
},
namespaces: {
eip155: {
accounts: [eipAccount],
chains: eipChains
}
},
pairingTopic: proposalId,
topic: dappUrl
};
}
const eipAccount = account ? `eip155:${account}` : ""
const eipChains = chains ? chains.map((chain) => `eip155:${chain}`) : []
return sessionTemplate(dappUrl, dappName, dappIcon, proposalId, eipAccount, eipChains)
}
function buildSessionRequest(requestId, topic, chainId, method, txArgs) {
return {
id: requestId,
topic,
params: {
chainId: `eip155:${chainId}`,
request: {
method: SessionRequest.methods.sendTransaction.name,
params: [
{
from: txArgs.from,
to: txArgs.to,
value: txArgs.value,
gasLimit: txArgs.gas,
gasPrice: txArgs.gasPrice,
maxFeePerGas: txArgs.maxFeePerGas,
maxPriorityFeePerGas: txArgs.maxPriorityFeePerGas,
nonce: txArgs.nonce,
data: txArgs.data
}
]
}
}
}
}
function buildSessionProposal(id, url, name, icon, methods) {
const supportedNamespaces = DAppsHelpers.buildSupportedNamespacesFromModels(root.networksModel, root.accountsModel, methods)
const proposal = {
id,
params: {
optionalNamespaces: {},
proposer: {
metadata: {
description: "-",
icons: [
icon
],
name,
url
}
},
requiredNamespaces: supportedNamespaces
}
}
return proposal
}
}
} }

View File

@ -1,7 +1,7 @@
import QtQuick 2.15 import QtQuick 2.15
import StatusQ 0.1 import StatusQ 0.1
import StatusQ.Core.Utils 0.1 import StatusQ.Core.Utils 0.1 as SQUtils
import AppLayouts.Wallet.services.dapps 1.0 import AppLayouts.Wallet.services.dapps 1.0
@ -19,7 +19,50 @@ DAppsModel {
readonly property int connectorId: Constants.WalletConnect readonly property int connectorId: Constants.WalletConnect
property bool enabled: true readonly property bool enabled: sdk.enabled
signal disconnected(string topic, string dAppUrl)
signal connected(string proposalId, string topic, string dAppUrl)
Connections {
target: root.sdk
enabled: root.enabled
function onSessionDelete(topic, err) {
const dapp = root.getByTopic(topic)
if (!dapp) {
console.warn("DApp not found for topic - cannot delete session", topic)
return
}
root.store.deactivateWalletConnectSession(topic)
d.updateDappsModel()
root.disconnected(topic, dapp.url)
}
function onSdkInit(success, result) {
if (!success) {
return
}
d.updateDappsModel()
}
function onApproveSessionResult(proposalId, session, error) {
if (error) {
return
}
root.store.addWalletConnectSession(JSON.stringify(session))
d.updateDappsModel()
root.connected(proposalId, session.topic, session.peer.metadata.url)
}
function onAcceptSessionAuthenticateResult(id, result, error) {
if (error) {
return
}
d.updateDappsModel()
root.connected(id, result.topic, result.session.peer.metadata.url)
}
}
Component.onCompleted: { Component.onCompleted: {
if (!enabled) { if (!enabled) {
@ -37,34 +80,9 @@ DAppsModel {
} }
} }
QObject { SQUtils.QObject {
id: d id: d
property Connections sdkConnections: Connections {
target: root.sdk
enabled: root.enabled
function onSessionDelete(topic, err) {
d.updateDappsModel()
}
function onSdkInit(success, result) {
if (success) {
d.updateDappsModel()
}
}
function onApproveSessionResult(topic, success, result) {
if (success) {
d.updateDappsModel()
}
}
function onAcceptSessionAuthenticateResult(id, result, error) {
if (!error) {
d.updateDappsModel()
}
}
}
property var dappsListReceivedFn: null property var dappsListReceivedFn: null
property var getActiveSessionsFn: null property var getActiveSessionsFn: null
function updateDappsModel() function updateDappsModel()
@ -82,7 +100,7 @@ DAppsModel {
name: cachedEntry.name, name: cachedEntry.name,
iconUrl: cachedEntry.iconUrl, iconUrl: cachedEntry.iconUrl,
accountAddresses: [], accountAddresses: [],
topic: "", topic: cachedEntry.url,
connectorId: root.connectorId, connectorId: root.connectorId,
rawSessions: [] rawSessions: []
} }
@ -99,12 +117,16 @@ DAppsModel {
getActiveSessionsFn = () => { getActiveSessionsFn = () => {
sdk.getActiveSessions((allSessionsAllProfiles) => { sdk.getActiveSessions((allSessionsAllProfiles) => {
if (!allSessionsAllProfiles) {
console.warn("Failed to get active sessions")
return
}
root.store.dappsListReceived.disconnect(dappsListReceivedFn); root.store.dappsListReceived.disconnect(dappsListReceivedFn);
const dAppsMap = {} const dAppsMap = {}
const topics = [] const topics = []
const sessions = DAppsHelpers.filterActiveSessionsForKnownAccounts(allSessionsAllProfiles, supportedAccountsModel) const sessions = DAppsHelpers.filterActiveSessionsForKnownAccounts(allSessionsAllProfiles, root.supportedAccountsModel)
for (const sessionID in sessions) { for (const sessionID in sessions) {
const session = sessions[sessionID] const session = sessions[sessionID]
const dapp = session.peer.metadata const dapp = session.peer.metadata
@ -120,7 +142,7 @@ DAppsModel {
// more modern syntax (ES-6) is not available yet // more modern syntax (ES-6) is not available yet
const combinedAddresses = new Set(existingDApp.accountAddresses.concat(accounts)); const combinedAddresses = new Set(existingDApp.accountAddresses.concat(accounts));
existingDApp.accountAddresses = Array.from(combinedAddresses); existingDApp.accountAddresses = Array.from(combinedAddresses);
dapp.rawSessions = [...existingDApp.rawSessions, session] existingDApp.rawSessions = [...existingDApp.rawSessions, session]
} else { } else {
dapp.accountAddresses = accounts dapp.accountAddresses = accounts
dapp.topic = sessionID dapp.topic = sessionID

View File

@ -13,10 +13,8 @@ import "types"
WalletConnectSDKBase { WalletConnectSDKBase {
id: root id: root
readonly property alias sdkReady: d.sdkReady readonly property bool sdkReady: d.sdkReady
// Enable the WalletConnect SDK
property alias enableSdk: loader.active
property alias userUID: loader.profileName property alias userUID: loader.profileName
readonly property alias url: loader.url readonly property alias url: loader.url
@ -509,7 +507,7 @@ WalletConnectSDKBase {
} }
function onRejectSessionAuthenticateResult(id, result, error) { function onRejectSessionAuthenticateResult(id, result, error) {
//console.debug(`WC WalletConnectSDK.onRejectSessionAuthenticateResult; id: ${id}, result: ${result}, error: ${error}`) console.debug(`WC WalletConnectSDK.onRejectSessionAuthenticateResult; id: ${id}, result: ${result}, error: ${error}`)
root.rejectSessionAuthenticateResult(id, result, error) root.rejectSessionAuthenticateResult(id, result, error)
} }
} }
@ -518,7 +516,7 @@ WalletConnectSDKBase {
id: loader id: loader
anchors.fill: parent anchors.fill: parent
active: root.enabled
url: "qrc:/app/AppLayouts/Wallet/services/dapps/sdk/src/index.html" url: "qrc:/app/AppLayouts/Wallet/services/dapps/sdk/src/index.html"
webChannelObjects: [ statusObject ] webChannelObjects: [ statusObject ]
@ -528,5 +526,11 @@ WalletConnectSDKBase {
onPageLoadingError: function(error) { onPageLoadingError: function(error) {
console.error("WebEngineLoader.onPageLoadingError: ", error) console.error("WebEngineLoader.onPageLoadingError: ", error)
} }
onActiveChanged: function() {
if (!active) {
d.sdkReady = false
}
}
} }
} }

View File

@ -3,24 +3,25 @@ import QtQuick 2.15
/// SDK requires a visible parent to embed WebEngineView /// SDK requires a visible parent to embed WebEngineView
Item { Item {
required property string projectId required property string projectId
property bool enabled: true
signal statusChanged(string message) signal statusChanged(string message)
signal sdkInit(bool success, var result) signal sdkInit(bool success, var result)
signal pairResponse(bool success) signal pairResponse(bool success)
signal sessionProposal(var sessionProposal) signal sessionProposal(var sessionProposal)
signal sessionProposalExpired() signal sessionProposalExpired()
signal buildApprovedNamespacesResult(var id, var session, string error) signal buildApprovedNamespacesResult(string id, var session, string error)
signal approveSessionResult(var proposalId, var approvedNamespaces, string error) signal approveSessionResult(string proposalId, var approvedNamespaces, string error)
signal rejectSessionResult(var proposalId, string error) signal rejectSessionResult(string proposalId, string error)
signal sessionRequestExpired(var id) signal sessionRequestExpired(string id)
signal sessionRequestEvent(var sessionRequest) signal sessionRequestEvent(var sessionRequest)
signal sessionRequestUserAnswerResult(string topic, string id, bool accept /* not reject */, string error) signal sessionRequestUserAnswerResult(string topic, string id, bool accept /* not reject */, string error)
signal sessionAuthenticateRequest(var sessionData) signal sessionAuthenticateRequest(var sessionData)
signal populateAuthPayloadResult(var id, var authPayload, string error) signal populateAuthPayloadResult(string id, var authPayload, string error)
signal formatAuthMessageResult(var id, var request, string error) signal formatAuthMessageResult(string id, var request, string error)
signal acceptSessionAuthenticateResult(var id, var result, string error) signal acceptSessionAuthenticateResult(string id, var result, string error)
signal rejectSessionAuthenticateResult(var id, var result, string error) signal rejectSessionAuthenticateResult(string id, var result, string error)
signal buildAuthObjectResult(var id, var authObject, string error) signal buildAuthObjectResult(string id, var authObject, string error)
signal sessionDelete(var topic, string error) signal sessionDelete(var topic, string error)

View File

@ -0,0 +1,353 @@
import QtQuick 2.15
import StatusQ 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import AppLayouts.Wallet.services.dapps 1.0
import AppLayouts.Wallet.services.dapps.types 1.0
import shared.stores 1.0
import utils 1.0
// Plugin providing the connection handling for dApps
// Features provided:
// 1. connect
// 2. disconnect
// 3. active connections model
SQUtils.QObject {
id: root
required property WalletConnectSDKBase wcSDK
required property WalletConnectSDKBase bcSDK
required property DAppsStore dappsStore
// Required roles: address
required property var accountsModel
// Required roles: chainId
required property var networksModel
// Output model with the following roles:
// - name: string (optional)
// - url: string (required)
// - iconUrl: string (optional)
// - topic: string (required)
// - connectorId: int (required)
// - accountAddressses: [{address: string}] (required)
// - chains: string (optional)
// - rawSessions: [{session: object}] (optional)
readonly property ConcatModel dappsModel: dappsModel
// Output signal when a dApp is disconnected
signal disconnected(string topic, string dAppUrl)
// Output signal when a new connection is proposed
signal connected(string proposalId, string topic, string dAppUrl, int connectorId)
// Output signal when a new connection is proposed by the SDK
signal newConnectionProposed(string key, var chains, string dAppUrl, string dAppName, string dAppIcon)
// Output signal when a new connection is failed
signal newConnectionFailed(string key, string dappUrl, int errorCode)
// Request to disconnect a dApp identified by the topic
function disconnect(topic) {
d.disconnect(topic)
}
// Request to connect a dApp identified by the proposal key
// chains: array of chainIds
// accountAddress: string
function connect(key, chains, accoutAddress) {
d.connect(key, chains, accoutAddress)
}
// Request to reject a dApp connection request identified by the proposal key
function reject(key) {
d.reject(key)
}
WCDappsProvider {
id: dappsProvider
sdk: root.wcSDK
store: root.dappsStore
supportedAccountsModel: root.accountsModel
onConnected: (proposalId, topic, dappUrl) => {
root.connected(proposalId, topic, dappUrl, Constants.WalletConnect)
}
onDisconnected: (topic, dappUrl) => {
root.disconnected(topic, dappUrl)
}
}
BCDappsProvider {
id: connectorDAppsProvider
bcSDK: root.bcSDK
onConnected: (pairingId, topic, dappUrl) => {
root.connected(pairingId, topic, dappUrl, Constants.StatusConnect)
}
onDisconnected: (topic, dappUrl) => {
root.disconnected(topic, dappUrl)
}
}
ConcatModel {
id: dappsModel
markerRoleName: "source"
sources: [
SourceModel {
model: dappsProvider.model
markerRoleValue: "walletConnect"
},
SourceModel {
model: connectorDAppsProvider.model
markerRoleValue: "statusConnect"
}
]
}
// These two objects don't share a common interface because Qt5.15.2 would freeze for some reason
QtObject {
id: bcConnectionPromise
function resolve(context, key, approvedChainIds, accountAddress) {
root.bcSDK.approveSession(key, accountAddress, approvedChainIds)
}
function reject(context, key) {
root.bcSDK.rejectSession(key)
}
}
QtObject {
id: wcConnectionPromise
function resolve(context, key, approvedChainIds, accountAddress) {
const approvedNamespaces = JSON.parse(
DAppsHelpers.buildSupportedNamespaces(approvedChainIds,
[accountAddress],
SessionRequest.getSupportedMethods()))
d.acceptedSessionProposal = context
d.acceptedNamespaces = approvedNamespaces
root.wcSDK.buildApprovedNamespaces(key, context.params, approvedNamespaces)
}
function reject(context, key) {
root.wcSDK.rejectSession(key)
}
}
// Flow for BrowserConnect
// 1. onSessionProposal -> new connection proposal received
// 3. onApproveSessionResult -> session approve result
// 4. onRejectSessionResult -> session reject result
Connections {
target: root.bcSDK
function onSessionProposal(sessionProposal) {
const key = sessionProposal.id
d.activeProposals.set(key.toString(), { context: sessionProposal, promise: bcConnectionPromise })
root.newConnectionProposed(key, [1], sessionProposal.params.proposer.metadata.url, sessionProposal.params.proposer.metadata.name, sessionProposal.params.proposer.metadata.icons[0])
}
function onApproveSessionResult(proposalId, session, err) {
if (!d.activeProposals.has(proposalId.toString())) {
console.error("No active proposal found for key: " + proposalId)
return
}
const dappUrl = d.activeProposals.get(proposalId.toString()).context.params.proposer.metadata.url
d.activeProposals.delete(proposalId.toString())
if (err) {
root.newConnectionFailed(proposalId, dappUrl, Pairing.errors.unknownError)
return
}
}
function onRejectSessionResult(proposalId, err) {
if (!d.activeProposals.has(proposalId.toString())) {
console.error("No active proposal found for key: " + proposalId)
return
}
const dappUrl = d.activeProposals.get(proposalId.toString()).context.params.proposer.metadata.url
d.activeProposals.delete(proposalId.toString())
if (err) {
root.newConnectionFailed(proposalId, dappUrl, Pairing.errors.rejectFailed)
return
}
root.newConnectionFailed(proposalId, dappUrl, Pairing.errors.userRejected)
}
}
// Flow for WalletConnect
// 1. onSessionProposal -> new connection proposal received
// 2. onBuildApprovedNamespacesResult -> get the supported namespaces to be sent for approval
// 3. onApproveSessionResult -> session approve result
// 4. onRejectSessionResult -> session reject result
Connections {
target: root.wcSDK
function onSessionProposal(sessionProposal) {
const key = sessionProposal.id
d.activeProposals.set(key.toString(), { context: sessionProposal, promise: wcConnectionPromise })
const supportedNamespacesStr = DAppsHelpers.buildSupportedNamespacesFromModels(
root.networksModel, root.accountsModel, SessionRequest.getSupportedMethods())
root.wcSDK.buildApprovedNamespaces(key, sessionProposal.params, JSON.parse(supportedNamespacesStr))
}
function onBuildApprovedNamespacesResult(key, approvedNamespaces, error) {
if (!d.activeProposals.has(key.toString())) {
console.error("No active proposal found for key: " + key)
return
}
const proposal = d.activeProposals.get(key.toString()).context
const dAppUrl = proposal.params.proposer.metadata.url
if(error || !approvedNamespaces) {
// Check that it contains Non conforming namespaces"
if (error.includes("Non conforming namespaces")) {
root.newConnectionFailed(proposal.id, dAppUrl, Pairing.errors.unsupportedNetwork)
} else {
root.newConnectionFailed(proposal.id, dAppUrl, Pairing.errors.unknownError)
}
return
}
approvedNamespaces = d.applyChainAgnosticFix(approvedNamespaces)
if (d.acceptedSessionProposal) {
root.wcSDK.approveSession(d.acceptedSessionProposal, approvedNamespaces)
} else {
const res = DAppsHelpers.extractChainsAndAccountsFromApprovedNamespaces(approvedNamespaces)
const chains = res.chains
const dAppName = proposal.params.proposer.metadata.name
const dAppIcons = proposal.params.proposer.metadata.icons
const dAppIcon = dAppIcons && dAppIcons.length > 0 ? dAppIcons[0] : ""
root.newConnectionProposed(key, chains, dAppUrl, dAppName, dAppIcon)
}
}
function onApproveSessionResult(proposalId, session, err) {
if (!d.activeProposals.has(proposalId.toString())) {
console.error("No active proposal found for key: " + proposalId)
return
}
const dappUrl = d.activeProposals.get(proposalId.toString())
.context.params.proposer.metadata.url
d.activeProposals.delete(proposalId.toString())
d.acceptedSessionProposal = null
d.acceptedNamespaces = null
if (err) {
root.newConnectionFailed(proposalId, dappUrl, Pairing.errors.unknownError)
return
}
}
function onRejectSessionResult(proposalId, err) {
if (!d.activeProposals.has(proposalId.toString())) {
console.error("No active proposal found for key: " + proposalId)
return
}
const dappUrl = d.activeProposals.get(proposalId.toString())
.context.params.proposer.metadata.url
d.activeProposals.delete(proposalId.toString())
if (err) {
root.newConnectionFailed(proposalId, dappUrl, Pairing.errors.rejectFailed)
return
}
root.newConnectionFailed(proposalId, dappUrl, Pairing.errors.userRejected)
}
}
QtObject {
id: d
property var activeProposals: new Map()
property var acceptedSessionProposal: null
property var acceptedNamespaces: null
function disconnect(connectionId) {
const dApp = d.getDAppByTopic(connectionId)
if (!dApp) {
console.error("Disconnecting dApp: dApp not found")
return
}
if (!dApp.connectorId == undefined) {
console.error("Disconnecting dApp: connectorId not found")
return
}
const sdk = dApp.connectorId === Constants.WalletConnect ? root.wcSDK : root.bcSDK
sdkDisconnect(dApp, sdk)
}
// Disconnect all sessions for a dApp
function sdkDisconnect(dapp, sdk) {
SQUtils.ModelUtils.forEach(dapp.rawSessions, (session) => {
sdk.disconnectSession(session.topic)
})
}
function reject(key) {
if (!d.activeProposals.has(key.toString())) {
console.error("Rejecting dApp: dApp not found")
return
}
const proposal = d.activeProposals.get(key.toString())
proposal.promise.reject(proposal.context, key)
}
function connect(key, chains, accoutAddress) {
if (!d.activeProposals.has(key.toString())) {
console.error("Connecting dApp: dApp not found", key)
return
}
const proposal = d.activeProposals.get(key.toString())
proposal.promise.resolve(proposal.context, key, chains, accoutAddress)
}
function getDAppByTopic(topic) {
return SQUtils.ModelUtils.getFirstModelEntryIf(dappsModel, (modelItem) => {
if (modelItem.topic == topic) {
return true
}
if (!modelItem.rawSessions) {
return false
}
for (let i = 0; i < modelItem.rawSessions.ModelCount.count; i++) {
if (modelItem.rawSessions.get(i).topic == topic) {
return true
}
}
})
}
//Special case for chain agnostic dapps
//WC considers the approved namespace as valid, but there's no chainId or account established
//Usually this request is declared by using `eip155:0`, but we don't support this chainID, resulting in empty `chains` and `accounts`
//The established connection will use for all user approved chains and accounts
//This fix is applied to all valid namespaces that don't have a chainId or account
function applyChainAgnosticFix(approvedNamespaces) {
try {
const an = approvedNamespaces.eip155
const chainAgnosticRequest = (!an.chains || an.chains.length === 0) && (!an.accounts || an.accounts.length === 0)
if (!chainAgnosticRequest) {
return approvedNamespaces
}
// If the `d.acceptedNamespaces` is set it means the user already confirmed the chain and account
if (!!d.acceptedNamespaces) {
approvedNamespaces.eip155.chains = d.acceptedNamespaces.eip155.chains
approvedNamespaces.eip155.accounts = d.acceptedNamespaces.eip155.accounts
return approvedNamespaces
}
// Show to the user all possible chains
const supportedNamespacesStr = DAppsHelpers.buildSupportedNamespacesFromModels(
root.networksModel, root.accountsModel, SessionRequest.getSupportedMethods())
const supportedNamespaces = JSON.parse(supportedNamespacesStr)
approvedNamespaces.eip155.chains = supportedNamespaces.eip155.chains
approvedNamespaces.eip155.accounts = supportedNamespaces.eip155.accounts
} catch (e) {
console.warn("WC Error applying chain agnostic fix", e)
}
return approvedNamespaces
}
}
}

View File

@ -0,0 +1,671 @@
import QtQuick 2.15
import QtQml 2.15
import AppLayouts.Wallet.services.dapps 1.0
import AppLayouts.Wallet.services.dapps.types 1.0
import StatusQ 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import shared.stores 1.0
import utils 1.0
/// Plugin that listens for session requests and manages the lifecycle of the request.
SQUtils.QObject {
id: root
required property WalletConnectSDKBase sdk
required property DAppsStore store
/// Expected to have the following roles:
/// - topic
/// - name
/// - url
/// - iconUrl
/// - rawSessions
required property var dappsModel
/// Expected to have the following roles:
/// - tokensKey
/// - balances
required property var groupedAccountAssetsModel
/// Expected to have the following roles:
/// - layer
/// - chainId
required property var networksModel
/// Expected to have the following roles:
/// - address
required property var accountsModel
/// App currency
required property string currentCurrency
// SessionRequestsModel where the requests are stored
// This component will append and remove requests from this model
required property SessionRequestsModel requests
// Function to transform the eth value to fiat
property var getFiatValue: (maxFeesEthStr, token /*Constants.ethToken*/) => console.error("getFiatValue not implemented")
// Signals
/// Signal emitted when a session request is accepted
signal accepted(string topic, string id, string data)
/// Signal emitted when a session request is rejected
signal rejected(string topic, string id, bool hasError)
/// Signal emitted when a session request is completed
/// Completed mean that we have the ACK from the SDK
signal signCompleted(string topic, string id, bool userAccepted, string error)
function requestReceived(event, dappName, dappUrl, dappIcon, connectorId) {
d.onSessionRequestEvent(event, dappName, dappUrl, dappIcon, connectorId)
}
function requestResolved(topic, id) {
root.requests.removeRequest(topic, id)
}
function requestExpired(sessionId) {
d.onSessionRequestExpired(sessionId)
}
onRejected: (topic, id, hasError) => {
root.sdk.rejectSessionRequest(topic, id, hasError)
}
onAccepted: (topic, id, data) => {
root.sdk.acceptSessionRequest(topic, id, data)
}
Component {
id: sessionRequestComponent
SessionRequestWithAuth {
id: request
store: root.store
function signedHandler(topic, id, data) {
if (topic != request.topic || id != request.requestId) {
return
}
root.store.signingResult.disconnect(request.signedHandler)
let hasErrors = (data == "")
if (!hasErrors) {
root.accepted(topic, id, data)
} else {
request.reject(true)
}
}
onActiveChanged: {
if (active === false) {
d.unsubscribeForFeeUpdates(request.topic, request.requestId)
}
if (active === true) {
d.subscribeForFeeUpdates(request.topic, request.requestId)
}
}
onRejected: (hasError) => {
root.rejected(request.topic, request.requestId, hasError)
d.unsubscribeForFeeUpdates(request.topic, request.requestId)
}
onAuthFailed: () => {
root.rejected(request.topic, request.requestId, true /*hasError*/)
d.unsubscribeForFeeUpdates(request.topic, request.requestId)
}
onExecute: (password, pin) => {
d.unsubscribeForFeeUpdates(request.topic, request.requestId)
root.store.signingResult.connect(request.signedHandler)
let executed = false
try {
executed = d.executeSessionRequest(request, password, pin, request.feesInfo)
} catch (e) {
console.error("Error executing session request", e)
}
if (!executed) {
root.rejected(request.topic, request.requestId, true /*hasError*/)
root.store.signingResult.disconnect(request.signedHandler)
}
}
}
}
Connections {
target: root.sdk
function onSessionRequestEvent(sessionRequest) {
const { id, topic } = sessionRequest
const dapp = SQUtils.ModelUtils.getFirstModelEntryIf(root.dappsModel, (dapp) => {
if (dapp.topic === topic) {
return true
}
return !!SQUtils.ModelUtils.getFirstModelEntryIf(dapp.rawSessions, (session) => {
if (session.topic === topic) {
return true
}
})
})
if (!dapp) {
console.warn("Error finding dapp for topic", topic, "id", id)
root.sdk.rejectSessionRequest(topic, id, true)
return
}
root.requestReceived(sessionRequest, dapp.name, dapp.url, dapp.iconUrl, dapp.connectorId)
}
function onSessionRequestExpired(sessionId) {
root.requestExpired(sessionId)
}
function onSessionRequestUserAnswerResult(topic, id, accept, error) {
let request = root.requests.findRequest(topic, id)
if (request === null) {
console.error("Error finding event for topic", topic, "id", id)
return
}
Qt.callLater(() => root.requestResolved(topic, id))
if (error) {
root.signCompleted(topic, id, accept, error)
console.error(`Error accepting session request for topic: ${topic}, id: ${id}, accept: ${accept}, error: ${error}`)
return
}
root.signCompleted(topic, id, accept, "")
}
}
QtObject {
id: d
function onSessionRequestEvent(event, dappName, dappUrl, dappIcon, connectorId) {
try {
const res = d.resolve(event, dappName, dappUrl, dappIcon, connectorId)
if (res.conde === SessionRequest.Ignored) {
return
}
if (res.code !== SessionRequest.NoError) {
root.rejected(event.topic, event.id, true)
return
}
root.requests.enqueue(res.obj)
} catch (e) {
console.error("Error processing session request event", e)
root.rejected(event.topic, event.id, true)
}
}
function onSessionRequestExpired(sessionId) {
// Expired event coming from WC
// Handling as a failsafe in case the event is not processed by the SDK
let request = root.requests.findById(sessionId)
if (request === null) {
console.error("Error finding event for session id", sessionId)
return
}
if (request.isExpired()) {
return //nothing to do. The request is already expired
}
request.setExpired()
d.unsubscribeForFeeUpdates(request.topic, request.requestId)
}
// returns {
// obj: obj or nil
// code: SessionRequest.ErrorCode
// }
function resolve(event, dappName, dappUrl, dappIcon, connectorId) {
const {request, error} = SessionRequestResolver.resolveEvent(event, root.accountsModel, root.networksModel, root.store.hexToDec)
if (error !== SessionRequest.NoError) {
return { obj: null, code: error }
}
if (!request) {
return { obj: null, code: SessionRequest.RuntimeError }
}
const mainNet = lookupMainnetNetwork()
if (!mainNet) {
console.error("Mainnet network not found")
return { obj: null, code: SessionRequest.RuntimeError }
}
updateFeesOnPreparedData(request)
let obj = sessionRequestComponent.createObject(null, {
event: request.event,
topic: request.topic,
requestId: request.requestId,
method: request.method,
accountAddress: request.account,
chainId: request.chainId,
data: request.data,
preparedData: JSON.stringify(request.preparedData),
expirationTimestamp: request.expiryTimestamp,
dappName,
dappUrl,
dappIcon,
sourceId: connectorId,
value: request.value
})
if (obj === null) {
console.error("Error creating SessionRequestResolved for event")
return { obj: null, code: SessionRequest.RuntimeError }
}
if (!request.transaction) {
obj.haveEnoughFunds = true
return { obj: obj, code: SessionRequest.NoError }
}
updateFeesParamsToPassedObj(obj)
return {
obj: obj,
code: SessionRequest.NoError
}
}
// Updates the fees to a SessionRequestResolved
function updateFeesParamsToPassedObj(requestItem) {
if (!(requestItem instanceof SessionRequestResolved)) {
return
}
if (!SessionRequest.isTransactionMethod(requestItem.method)) {
return
}
const mainNet = lookupMainnetNetwork()
if (!mainNet) {
console.error("Mainnet network not found")
return { obj: null, code: SessionRequest.RuntimeError }
}
const tx = SessionRequest.getTxObject(requestItem.method, requestItem.data)
requestItem.estimatedTimeCategory = root.store.getEstimatedTime(requestItem.chainId, tx.maxFeePerGas || tx.gasPrice || "")
let st = getEstimatedFeesStatus(tx, requestItem.method, requestItem.chainId, mainNet.chainId)
let fundsStatus = checkFundsStatus(st.feesInfo.maxFees, st.feesInfo.l1GasFee, requestItem.accountAddress, requestItem.chainId, mainNet.chainId, requestItem.value)
requestItem.fiatMaxFees = st.fiatMaxFees
requestItem.ethMaxFees = st.maxFeesEth
requestItem.haveEnoughFunds = fundsStatus.haveEnoughFunds
requestItem.haveEnoughFees = fundsStatus.haveEnoughForFees
requestItem.feesInfo = st.feesInfo
}
// Updates the fee in the transaction preview on a JS Object built by SessionRequest
function updateFeesOnPreparedData(request) {
if (!request.transaction && !request.preparedData instanceof Object) {
return
}
let fees = root.store.getSuggestedFees(request.chainId)
if (!request.preparedData.maxFeePerGas
&& request.preparedData.hasOwnProperty("maxFeePerGas")
&& fees.eip1559Enabled) {
request.preparedData.maxFeePerGas = d.getFeesForFeesMode(fees)
}
if (!request.preparedData.maxPriorityFeePerGas
&& request.preparedData.hasOwnProperty("maxPriorityFeePerGas")
&& fees.eip1559Enabled) {
request.preparedData.maxPriorityFeePerGas = fees.maxPriorityFeePerGas
}
if (!request.preparedData.gasPrice
&& request.preparedData.hasOwnProperty("gasPrice")
&& !fees.eip1559Enabled) {
request.preparedData.gasPrice = fees.gasPrice
}
}
/// Returns null if the network is not found
function lookupMainnetNetwork() {
return SQUtils.ModelUtils.getByKey(root.networksModel, "layer", 1)
}
function executeSessionRequest(request, password, pin, payload) {
if (!SessionRequest.getSupportedMethods().includes(request.method)) {
console.error("Unsupported method to execute: ", request.method)
return false
}
if (password === "") {
console.error("No password provided to sign message")
return false
}
if (request.method === SessionRequest.methods.sign.name) {
root.store.signMessageUnsafe(request.topic,
request.requestId,
request.accountAddress,
SessionRequest.methods.personalSign.getMessageFromData(request.data),
password,
pin)
} else if (request.method === SessionRequest.methods.personalSign.name) {
root.store.signMessage(request.topic,
request.requestId,
request.accountAddress,
SessionRequest.methods.personalSign.getMessageFromData(request.data),
password,
pin)
} else if (request.method === SessionRequest.methods.signTypedData_v4.name ||
request.method === SessionRequest.methods.signTypedData.name)
{
let legacy = request.method === SessionRequest.methods.signTypedData.name
root.store.safeSignTypedData(request.topic,
request.requestId,
request.accountAddress,
SessionRequest.methods.signTypedData.getMessageFromData(request.data),
request.chainId,
legacy,
password,
pin)
} else if (SessionRequest.isTransactionMethod(request.method)) {
let txObj = SessionRequest.getTxObject(request.method, request.data)
if (!!payload) {
let hexFeesJson = root.store.convertFeesInfoToHex(JSON.stringify(payload))
if (!!hexFeesJson) {
let feesInfo = JSON.parse(hexFeesJson)
if (feesInfo.maxFeePerGas) {
txObj.maxFeePerGas = feesInfo.maxFeePerGas
}
if (feesInfo.maxPriorityFeePerGas) {
txObj.maxPriorityFeePerGas = feesInfo.maxPriorityFeePerGas
}
}
delete txObj.gasLimit
delete txObj.gasPrice
}
// Remove nonce from txObj to be auto-filled by the wallet
delete txObj.nonce
if (request.method === SessionRequest.methods.signTransaction.name) {
root.store.signTransaction(request.topic,
request.requestId,
request.accountAddress,
request.chainId,
txObj,
password,
pin)
} else if (request.method === SessionRequest.methods.sendTransaction.name) {
root.store.sendTransaction(
request.topic,
request.requestId,
request.accountAddress,
request.chainId,
txObj,
password,
pin)
}
}
return true
}
// Returns {
// maxFees -> Big number in Gwei
// maxFeePerGas
// maxPriorityFeePerGas
// gasPrice
// }
function getEstimatedMaxFees(tx, method, chainId, mainNetChainId) {
const BigOps = SQUtils.AmountsArithmetic
const gasLimit = BigOps.fromString("21000")
const parsedTransaction = SessionRequest.parseTransaction(tx, root.store.hexToDec)
let gasPrice = BigOps.fromString(parsedTransaction.maxFeePerGas)
let maxFeePerGas = BigOps.fromString(parsedTransaction.maxFeePerGas)
let maxPriorityFeePerGas = BigOps.fromString(parsedTransaction.maxPriorityFeePerGas)
let l1GasFee = BigOps.fromNumber(0)
if (!maxFeePerGas || !maxPriorityFeePerGas || !gasPrice) {
const suggesteFees = getSuggestedFees(chainId)
maxFeePerGas = suggesteFees.maxFeePerGas
maxPriorityFeePerGas = suggesteFees.maxPriorityFeePerGas
gasPrice = suggesteFees.gasPrice
l1GasFee = suggesteFees.l1GasFee
}
let maxFees = BigOps.times(gasLimit, gasPrice)
return {maxFees, maxFeePerGas, maxPriorityFeePerGas, gasPrice, l1GasFee}
}
function getSuggestedFees(chainId) {
const BigOps = SQUtils.AmountsArithmetic
const fees = root.store.getSuggestedFees(chainId)
const maxPriorityFeePerGas = fees.maxPriorityFeePerGas
let maxFeePerGas
let gasPrice
if (fees.eip1559Enabled) {
if (!!fees.maxFeePerGasM) {
gasPrice = BigOps.fromNumber(fees.maxFeePerGasM)
maxFeePerGas = fees.maxFeePerGasM
} else if(!!tx.maxFeePerGas) {
let maxFeePerGasDec = root.store.hexToDec(tx.maxFeePerGas)
gasPrice = BigOps.fromString(maxFeePerGasDec)
maxFeePerGas = maxFeePerGasDec
} else {
console.error("Error fetching maxFeePerGas from fees or tx objects")
return
}
} else {
if (!!fees.gasPrice) {
gasPrice = BigOps.fromNumber(fees.gasPrice)
} else {
console.error("Error fetching suggested fees")
return
}
}
const l1GasFee = BigOps.fromNumber(fees.l1GasFee)
return {maxFeePerGas, maxPriorityFeePerGas, gasPrice, l1GasFee}
}
// Returned values are Big numbers
function getEstimatedFeesStatus(tx, method, chainId, mainNetChainId) {
const BigOps = SQUtils.AmountsArithmetic
const feesInfo = getEstimatedMaxFees(tx, method, chainId, mainNetChainId)
const totalMaxFees = BigOps.sum(feesInfo.maxFees, feesInfo.l1GasFee)
const maxFeesEth = BigOps.div(totalMaxFees, BigOps.fromNumber(1, 9))
const maxFeesEthStr = maxFeesEth.toString()
const fiatMaxFeesStr = root.getFiatValue(maxFeesEthStr, Constants.ethToken)
const fiatMaxFees = BigOps.fromString(fiatMaxFeesStr)
const symbol = root.currentCurrency
return {fiatMaxFees, maxFeesEth, symbol, feesInfo}
}
function getBalanceInEth(balances, address, chainId) {
const BigOps = SQUtils.AmountsArithmetic
const accEth = SQUtils.ModelUtils.getFirstModelEntryIf(balances, (balance) => {
return balance.account.toLowerCase() === address.toLowerCase() && balance.chainId == chainId
})
if (!accEth) {
console.error("Error balance lookup for account ", address, " on chain ", chainId)
return null
}
const accountFundsWei = BigOps.fromString(accEth.balance)
return BigOps.div(accountFundsWei, BigOps.fromNumber(1, 18))
}
// Returns {haveEnoughForFees, haveEnoughFunds} and true in case of error not to block request
function checkFundsStatus(maxFees, l1GasFee, address, chainId, mainNetChainId, value) {
const BigOps = SQUtils.AmountsArithmetic
let valueEth = BigOps.fromString(value)
let haveEnoughForFees = true
let haveEnoughFunds = true
let token = SQUtils.ModelUtils.getByKey(root.groupedAccountAssetsModel, "tokensKey", Constants.ethToken)
if (!token || !token.balances) {
console.error("Error token balances lookup for ETH", SQUtils.ModelUtils.modelToArray(root.groupedAccountAssetsModel))
console.error("Looking for tokensKey: ", Constants.ethToken)
return {haveEnoughForFees, haveEnoughFunds}
}
let chainBalance = getBalanceInEth(token.balances, address, chainId)
if (!chainBalance) {
console.error("Error fetching chain balance")
return {haveEnoughForFees, haveEnoughFunds}
}
haveEnoughFunds = BigOps.cmp(chainBalance, valueEth) >= 0
if (haveEnoughFunds) {
chainBalance = BigOps.sub(chainBalance, valueEth)
if (chainId == mainNetChainId) {
const finalFees = BigOps.sum(maxFees, l1GasFee)
let feesEth = BigOps.div(finalFees, BigOps.fromNumber(1, 9))
haveEnoughForFees = BigOps.cmp(chainBalance, feesEth) >= 0
} else {
const feesChain = BigOps.div(maxFees, BigOps.fromNumber(1, 9))
const haveEnoughOnChain = BigOps.cmp(chainBalance, feesChain) >= 0
const mainBalance = getBalanceInEth(token.balances, address, mainNetChainId)
if (!mainBalance) {
console.error("Error fetching mainnet balance")
return {haveEnoughForFees, haveEnoughFunds}
}
const feesMain = BigOps.div(l1GasFee, BigOps.fromNumber(1, 9))
const haveEnoughOnMain = BigOps.cmp(mainBalance, feesMain) >= 0
haveEnoughForFees = haveEnoughOnChain && haveEnoughOnMain
}
} else {
haveEnoughForFees = false
}
return {haveEnoughForFees, haveEnoughFunds}
}
property int selectedFeesMode: Constants.FeesMode.Medium
function getFeesForFeesMode(feesObj) {
if (!(feesObj.hasOwnProperty("maxFeePerGasL") &&
feesObj.hasOwnProperty("maxFeePerGasM") &&
feesObj.hasOwnProperty("maxFeePerGasH"))) {
throw new Error("inappropriate fees object provided")
}
switch (d.selectedFeesMode) {
case Constants.FeesMode.Low:
return feesObj.maxFeePerGasL
case Constants.FeesMode.Medium:
return feesObj.maxFeePerGasM
case Constants.FeesMode.High:
return feesObj.maxFeePerGasH
default:
throw new Error("unknown selected mode")
}
}
property var feesSubscriptions: []
function findSubscriptionIndex(topic, id) {
for (let i = 0; i < d.feesSubscriptions.length; i++) {
const subscription = d.feesSubscriptions[i]
if (subscription.topic == topic && subscription.id == id) {
return i
}
}
return -1
}
function findChainIndex(chainId) {
for (let i = 0; i < feesSubscription.chainIds.length; i++) {
if (feesSubscription.chainIds[i] == chainId) {
return i
}
}
return -1
}
function subscribeForFeeUpdates(topic, id) {
const request = requests.findRequest(topic, id)
if (request === null) {
console.error("Error finding event for subscribing for fees for topic", topic, "id", id)
return
}
const index = d.findSubscriptionIndex(topic, id)
if (index >= 0) {
return
}
d.feesSubscriptions.push({
topic: topic,
id: id,
chainId: request.chainId
})
for (let i = 0; i < feesSubscription.chainIds.length; i++) {
if (feesSubscription.chainIds == request.chainId) {
return
}
}
feesSubscription.chainIds.push(request.chainId)
feesSubscription.restart()
}
function unsubscribeForFeeUpdates(topic, id) {
const index = d.findSubscriptionIndex(topic, id)
if (index == -1) {
return
}
const chainId = d.feesSubscriptions[index].chainId
d.feesSubscriptions.splice(index, 1)
const chainIndex = d.findChainIndex(chainId)
if (index == -1) {
return
}
let found = false
for (let i = 0; i < d.feesSubscriptions.length; i++) {
if (d.feesSubscriptions[i].chainId == chainId) {
found = true
break
}
}
if (found) {
return
}
feesSubscription.chainIds.splice(chainIndex, 1)
if (feesSubscription.chainIds.length == 0) {
feesSubscription.stop()
}
}
}
Timer {
id: feesSubscription
property var chainIds: []
interval: 5000
repeat: true
running: Qt.application.state === Qt.ApplicationActive
onTriggered: {
for (let i = 0; i < chainIds.length; i++) {
for (let j = 0; j < d.feesSubscriptions.length; j++) {
let subscription = d.feesSubscriptions[j]
if (subscription.chainId == chainIds[i]) {
let request = requests.findRequest(subscription.topic, subscription.id)
if (request === null) {
console.error("Error updating fees for topic", subscription.topic, "id", subscription.id)
continue
}
d.updateFeesParamsToPassedObj(request)
}
}
}
}
}
}

View File

@ -174,8 +174,6 @@ SQUtils.QObject {
topic: root.request.topic topic: root.request.topic
data: "" data: ""
preparedData: "" preparedData: ""
maxFeesText: "?"
maxFeesEthText: "?"
expirationTimestamp: root.request.params.expiryTimestamp expirationTimestamp: root.request.params.expiryTimestamp
function onBuildAuthenticationObjectResult(id, authObject, error) { function onBuildAuthenticationObjectResult(id, authObject, error) {
@ -232,6 +230,7 @@ SQUtils.QObject {
onAuthFailed: () => { onAuthFailed: () => {
try { try {
sdk.rejectSessionAuthenticate(request.requestId, true)
const appDomain = SQUtils.StringUtils.extractDomainFromLink(request.dappUrl) const appDomain = SQUtils.StringUtils.extractDomainFromLink(request.dappUrl)
const methodStr = SessionRequest.methodToUserString(request.method) const methodStr = SessionRequest.methodToUserString(request.method)
if (!methodStr) { if (!methodStr) {

View File

@ -22,6 +22,8 @@ SQUtils.QObject {
// - chainId // - chainId
required property var networksModel required property var networksModel
readonly property bool enabled: sdk.enabled
// Trigger a connection request to the dApp // Trigger a connection request to the dApp
// Expected `connectionApproved` to be called with the key and the approved namespaces // Expected `connectionApproved` to be called with the key and the approved namespaces
signal connectDApp(var chains, string dAppUrl, string dAppName, string dAppIcon, var key) signal connectDApp(var chains, string dAppUrl, string dAppName, string dAppIcon, var key)
@ -109,6 +111,7 @@ SQUtils.QObject {
Connections { Connections {
target: sdk target: sdk
enabled: root.enabled
function onSessionAuthenticateRequest(sessionData) { function onSessionAuthenticateRequest(sessionData) {
if (!sessionData || !sessionData.id) { if (!sessionData || !sessionData.id) {

View File

@ -1,3 +1,5 @@
ChainsSupervisorPlugin 1.0 ChainsSupervisorPlugin.qml ChainsSupervisorPlugin 1.0 ChainsSupervisorPlugin.qml
DAppConnectionsPlugin 1.0 DAppConnectionsPlugin.qml
SiweRequestPlugin 1.0 SiweRequestPlugin.qml SiweRequestPlugin 1.0 SiweRequestPlugin.qml
SiweLifeCycle 1.0 SiweLifeCycle.qml SiweLifeCycle 1.0 SiweLifeCycle.qml
SignRequestPlugin 1.0 SignRequestPlugin.qml

View File

@ -2,8 +2,8 @@ BCDappsProvider 1.0 BCDappsProvider.qml
DappsConnectorSDK 1.0 DappsConnectorSDK.qml DappsConnectorSDK 1.0 DappsConnectorSDK.qml
DAppsHelpers 1.0 helpers.js DAppsHelpers 1.0 helpers.js
DAppsModel 1.0 DAppsModel.qml DAppsModel 1.0 DAppsModel.qml
DAppsRequestHandler 1.0 DAppsRequestHandler.qml DAppsModule 1.0 DAppsModule.qml
DAppsService 1.0 DAppsService.qml
WalletConnectSDKBase 1.0 WalletConnectSDKBase.qml WalletConnectSDKBase 1.0 WalletConnectSDKBase.qml
WalletConnectSDK 1.0 WalletConnectSDK.qml WalletConnectSDK 1.0 WalletConnectSDK.qml
WalletConnectService 1.0 WalletConnectService.qml
WCDappsProvider 1.0 WCDappsProvider.qml WCDappsProvider 1.0 WCDappsProvider.qml

View File

@ -13,5 +13,7 @@ QtObject {
readonly property int unsupportedNetwork: 6 readonly property int unsupportedNetwork: 6
readonly property int unknownError: 7 readonly property int unknownError: 7
readonly property int dappReadyForApproval: 8 readonly property int dappReadyForApproval: 8
readonly property int userRejected: 9
readonly property int rejectFailed: 10
} }
} }

View File

@ -2,6 +2,8 @@ pragma Singleton
import QtQml 2.15 import QtQml 2.15
import AppLayouts.Wallet.services.dapps 1.0
import StatusQ.Core.Utils 0.1 as SQUtils
import utils 1.0 import utils 1.0
QtObject { QtObject {
@ -62,6 +64,93 @@ QtObject {
readonly property var all: [personalSign, sign, signTypedData_v4, signTypedData, sendTransaction] readonly property var all: [personalSign, sign, signTypedData_v4, signTypedData, sendTransaction]
} }
enum ErrorCode {
NoError,
InvalidAccount,
InvalidChainId,
InvalidData,
InvalidMessage,
InvalidMethod,
UnsupportedMethod,
InvalidRequest,
RuntimeError,
Ignored
}
/*
Function parsing the session request event
Throws exception if the event is invalid
returns { request: [Object], error: [ErrorCode] }
request: {
event,
topic,
requestId,
method,
account,
chainId,
data,
preparedData,
expiryTimestamp,
transaction: {
value,
maxFeePerGas,
maxPriorityFeePerGas,
gasPrice,
gasLimit,
nonce
}
}
*/
function parse(event, hexToDec) {
if (!event) {
console.warn("SessionRequest - parse - invalid event")
return { request: null, error: SessionRequest.InvalidRequest }
}
if (!hexToDec) {
hexToDec = (hex) => { return parseInt(hex, 16) }
}
let request = {}
let error = SessionRequest.NoError
request.event = event
request.topic = event.topic
request.requestId = event.id
request.method = event.params.request.method
if (!request.method) {
console.warn("SessionRequest - build - invalid method")
return { request: null, error: SessionRequest.InvalidMethod }
}
if (getSupportedMethods().includes(request.method) === false) {
console.warn("Unsupported method:", request.method)
return { request: null, error: SessionRequest.UnsupportedMethod }
}
const { accountAddress, success } = getAccountFromEvent(event)
if (!accountAddress || !success) {
console.warn("SessionRequest - build - failed to get account from event")
return { request: null, error: SessionRequest.InvalidAccount }
}
request.account = accountAddress
request.data = getData(event)
if (!request.data) {
console.warn("SessionRequest - build - failed to get data from event")
return { request: null, error: SessionRequest.InvalidData }
}
const message = getMessage(event, hexToDec)
if (!message || !message.signData) {
console.warn("SessionRequest - build - failed to get message from event")
return { request: null, error: SessionRequest.InvalidMessage }
}
request.preparedData = message.signData
request.value = message.value
request.transaction = message.transaction
request.chainId = getChainId(event)
if (!request.chainId) {
console.warn("SessionRequest - build - failed to get chainId from event")
return { request: null, error: SessionRequest.InvalidChainId }
}
request.expiryTimestamp = getExpiryDate(event)
return { request, error}
}
function getSupportedMethods() { function getSupportedMethods() {
return methods.all.map(function(method) { return methods.all.map(function(method) {
return method.name return method.name
@ -76,4 +165,228 @@ QtObject {
} }
return "" return ""
} }
/// returns {
/// accountAddress
/// success
/// }
/// if account is null and success is true it means that the account was not found
function getAccountFromEvent(event) {
const method = event.params.request.method
let address = ""
if (method === methods.personalSign.name) {
if (event.params.request.params.length < 2) {
return { accountAddress: "", success: false }
}
address = event.params.request.params[1]
} else if (method === methods.sign.name) {
if (event.params.request.params.length === 1) {
return { accountAddress: "", success: false }
}
address = event.params.request.params[0]
} else if(method === methods.signTypedData_v4.name ||
method === methods.signTypedData.name)
{
if (event.params.request.params.length < 2) {
return { accountAddress: "", success: false }
}
address = event.params.request.params[0]
} else if (isTransactionMethod(method)) {
if (event.params.request.params.length == 0) {
return { accountAddress: "", success: false }
}
address = event.params.request.params[0].from
} else {
console.error("Unsupported method to lookup account: ", method)
return { accountAddress: "", success: false }
}
return { accountAddress: address, success: true }
}
function getChainId(event) {
return DAppsHelpers.chainIdFromEip155(event.params.chainId)
}
function getData(event) {
const method = event.params.request.method
if (method === methods.personalSign.name ||
method === methods.sign.name)
{
if (event.params.request.params.length < 1) {
return null
}
let message = ""
const messageIndex = (method === methods.personalSign.name ? 0 : 1)
const messageParam = event.params.request.params[messageIndex]
// There is no standard on how data is encoded. Therefore we support hex or utf8
if (DAppsHelpers.isHex(messageParam)) {
message = DAppsHelpers.hexToString(messageParam)
} else {
message = messageParam
}
return methods.personalSign.buildDataObject(message)
} else if (method === methods.signTypedData_v4.name ||
method === methods.signTypedData.name)
{
if (event.params.request.params.length < 2) {
return null
}
const jsonMessage = event.params.request.params[1]
const methodObj = method === methods.signTypedData_v4.name
? methods.signTypedData_v4
: methods.signTypedData
return methodObj.buildDataObject(jsonMessage)
} else if (method === methods.signTransaction.name) {
if (event.params.request.params.length == 0) {
return null
}
const tx = event.params.request.params[0]
return methods.signTransaction.buildDataObject(tx)
} else if (method === methods.sendTransaction.name) {
if (event.params.request.params.length == 0) {
return null
}
const tx = event.params.request.params[0]
return methods.sendTransaction.buildDataObject(tx)
} else {
return null
}
}
// returns {
// signData,
// transaction: {
// value,
// maxFeePerGas,
// maxPriorityFeePerGas,
// gasPrice,
// gasLimit,
// nonce
// },
// value // null or ETH Big number
// }
function getMessage(event, hexToDec) {
const data = getData(event)
const method = event.params.request.method
return prepareData(method, data, hexToDec)
}
// returns {
// signData,
// transaction: {
// value,
// maxFeePerGas,
// maxPriorityFeePerGas,
// gasPrice,
// gasLimit,
// nonce
// },
// value // null or ETH Big number
// }
function prepareData(method, data, hexToDec) {
let payload = null
switch(method) {
case methods.personalSign.name: {
payload = methods.personalSign.getMessageFromData(data)
break
}
case methods.sign.name: {
payload = methods.sign.getMessageFromData(data)
break
}
case methods.signTypedData_v4.name: {
const stringPayload = methods.signTypedData_v4.getMessageFromData(data)
payload = JSON.stringify(JSON.parse(stringPayload), null, 2)
break
}
case methods.signTypedData.name: {
const stringPayload = methods.signTypedData.getMessageFromData(data)
payload = JSON.stringify(JSON.parse(stringPayload), null, 2)
break
}
case methods.signTransaction.name:
case methods.sendTransaction.name:
// For transactions we process the data in a different way as follows
break
default:
console.error("Unhandled method", method)
break;
}
let value = SQUtils.AmountsArithmetic.fromNumber(0)
let txObj = getTxObject(method, data)
if (isTransactionMethod(method)) {
payload = parseTransaction(txObj, hexToDec)
if (payload.hasOwnProperty("value")) {
value = payload.value
}
}
return {
signData: payload,
transaction: txObj,
value: value
}
}
/// Parses the transaction object and converts the values to human readable format
function parseTransaction(tx, hexToDec) {
let parsedTransaction = Object.assign({}, tx)
if (parsedTransaction.hasOwnProperty("value")) {
parsedTransaction.value = hexToEth(parsedTransaction.value, hexToDec).toString()
}
if (parsedTransaction.hasOwnProperty("maxFeePerGas")) {
parsedTransaction.maxFeePerGas = hexToGwei(parsedTransaction.maxFeePerGas, hexToDec).toString()
}
if (parsedTransaction.hasOwnProperty("maxPriorityFeePerGas")) {
parsedTransaction.maxPriorityFeePerGas = hexToGwei(parsedTransaction.maxPriorityFeePerGas, hexToDec).toString()
}
if (parsedTransaction.hasOwnProperty("gasPrice")) {
parsedTransaction.gasPrice = hexToGwei(parsedTransaction.gasPrice, hexToDec)
}
if (parsedTransaction.hasOwnProperty("gasLimit")) {
parsedTransaction.gasLimit = parseInt(hexToDec(parsedTransaction.gasLimit))
}
if (parsedTransaction.hasOwnProperty("nonce")) {
parsedTransaction.nonce = parseInt(hexToDec(parsedTransaction.nonce))
}
return parsedTransaction
}
function hexToEth(value, hexToDec) {
return hexToEthDenomination(value, "eth", hexToDec)
}
function hexToGwei(value, hexToDec) {
return hexToEthDenomination(value, "gwei", hexToDec)
}
function hexToEthDenomination(value, ethUnit, hexToDec) {
let unitMapping = {
"gwei": 9,
"eth": 18
}
let BigOps = SQUtils.AmountsArithmetic
let decValue = hexToDec(value)
if (!!decValue) {
return BigOps.div(BigOps.fromNumber(decValue), BigOps.fromNumber(1, unitMapping[ethUnit]))
}
return BigOps.fromNumber(0)
}
function getTxObject(method, data) {
let tx
if (method === methods.signTransaction.name) {
tx = methods.signTransaction.getTxObjFromData(data)
} else if (method === methods.sendTransaction.name) {
tx = methods.sendTransaction.getTxObjFromData(data)
}
return tx
}
function isTransactionMethod(method) {
return method === methods.signTransaction.name
|| method === methods.sendTransaction.name
}
function getExpiryDate(event) {
return event.params.request.expiryTimestamp
}
} }

View File

@ -23,6 +23,7 @@ QObject {
required property string chainId required property string chainId
// optional expiry date in ms // optional expiry date in ms
property var expirationTimestamp property var expirationTimestamp
property bool active: false
// Maps to Constants.DAppConnectors values // Maps to Constants.DAppConnectors values
required property int sourceId required property int sourceId
@ -31,32 +32,22 @@ QObject {
// Data prepared for display in a human readable format // Data prepared for display in a human readable format
required property var preparedData required property var preparedData
property alias dappName: d.dappName required property string dappName
property alias dappUrl: d.dappUrl required property string dappUrl
property alias dappIcon: d.dappIcon required property string dappIcon
/// extra data resolved from wallet /// extra data resolved from wallet
property string maxFeesText: ""
property string maxFeesEthText: ""
property bool haveEnoughFunds: false property bool haveEnoughFunds: false
property bool haveEnoughFees: false property bool haveEnoughFees: false
property var /* Big */ fiatMaxFees property var /* Big */ fiatMaxFees
property var /* Big */ ethMaxFees property var /* Big */ ethMaxFees
property var feesInfo property var feesInfo
property var /* Big */ value
/// maps to Constants.TransactionEstimatedTime values /// maps to Constants.TransactionEstimatedTime values
property int estimatedTimeCategory: 0 property int estimatedTimeCategory: 0
function resolveDappInfoFromSession(session) {
let meta = session.peer.metadata
d.dappName = meta.name
d.dappUrl = meta.url
if (meta.icons && meta.icons.length > 0) {
d.dappIcon = meta.icons[0]
}
}
function isExpired() { function isExpired() {
return !!expirationTimestamp && expirationTimestamp > 0 && Math.floor(Date.now() / 1000) >= expirationTimestamp return !!expirationTimestamp && expirationTimestamp > 0 && Math.floor(Date.now() / 1000) >= expirationTimestamp
} }
@ -65,13 +56,7 @@ QObject {
expirationTimestamp = Math.floor(Date.now() / 1000) expirationTimestamp = Math.floor(Date.now() / 1000)
} }
// dApp info function setActive() {
QtObject { active = true
id: d
property string dappName
property string dappUrl
property url dappIcon
property bool hasExpiry
} }
} }

View File

@ -0,0 +1,78 @@
pragma Singleton
import QtQuick 2.15
import StatusQ.Core.Utils 0.1 as SQUtils
/// Component that resolves a session request event
/// and returns a validated SessionRequest js object
/// @returns {
/// error: SessionRequest.Error,
/// request: {
/// event, - original event
/// topic, - dapp session identifier
/// requestId, - unique request identifier
/// method, - RPC method
/// account, - account address to sign the request
/// chainId, - chain id
/// data, - challenge data
/// preparedData, - human readable data
/// expiryTimestamp, - request expiry timestamp
/// transaction: {
/// value,
/// maxFeePerGas,
/// maxPriorityFeePerGas,
/// gasPrice,
/// gasLimit,
/// nonce
/// }
/// }
///}
SQUtils.QObject {
id: root
function resolveEvent(event, accountsModel, networksModel, hexToDec) {
if (!event) {
console.warn("SessionRequestResolver - resolveEvent - invalid event")
return { request: null, error: SessionRequest.InvalidEvent }
}
if (!accountsModel) {
console.warn("SessionRequestResolver - resolveEvent - invalid accountsModel")
return { request: null, error: SessionRequest.RuntimeError }
}
if (!networksModel) {
console.warn("SessionRequestResolver - resolveEvent - invalid networksModel")
return { request: null, error: SessionRequest.RuntimeError }
}
try {
const { request, error } = SessionRequest.parse(event, hexToDec)
if (error) {
console.warn("SessionRequestResolver - resolveEvent - failed to build request", error)
return { request: null, error }
}
if (!request) {
console.warn("SessionRequestResolver - resolveEvent - failed to build request")
return { request: null, error: SessionRequest.RuntimeError }
}
const validChainId = !!SQUtils.ModelUtils.getByKey(networksModel, "chainId", request.chainId)
if (!validChainId) {
console.warn("SessionRequestResolver - resolveEvent - invalid chainId", request.chainId)
return { request: null, error: SessionRequest.InvalidChainId }
}
const validAccount = !!SQUtils.ModelUtils.getFirstModelEntryIf(accountsModel, (account) => {
return account.address.toLowerCase() === request.account.toLowerCase();
})
if (!validAccount) {
console.warn("SessionRequestResolver - resolveEvent - invalid account", request.account)
return { request: null, error: SessionRequest.InvalidAccount }
}
return { request, error: SessionRequest.NoError }
} catch (e) {
console.warn("SessionRequestResolver - resolveEvent - failed to resolve event", e)
return { request: null, error: SessionRequest.RuntimeError }
}
}
}

View File

@ -2,5 +2,6 @@ SessionRequestResolved 1.0 SessionRequestResolved.qml
SessionRequestWithAuth 1.0 SessionRequestWithAuth.qml SessionRequestWithAuth 1.0 SessionRequestWithAuth.qml
SessionRequestsModel 1.0 SessionRequestsModel.qml SessionRequestsModel 1.0 SessionRequestsModel.qml
singleton SessionRequest 1.0 SessionRequest.qml singleton SessionRequest 1.0 SessionRequest.qml
singleton SessionRequestResolver 1.0 SessionRequestResolver.qml
singleton Pairing 1.0 Pairing.qml singleton Pairing 1.0 Pairing.qml
singleton ErrorCodes 1.0 ErrorCodes.qml singleton ErrorCodes 1.0 ErrorCodes.qml

View File

@ -2168,57 +2168,57 @@ Item {
} }
} }
Component {
id: dappsConnectorSDK
DappsConnectorSDK {
active: WalletStores.RootStore.walletSectionInst.walletReady
controller: WalletStores.RootStore.dappsConnectorController
wcService: Global.walletConnectService
walletStore: WalletStores.RootStore
store: SharedStores.DAppsStore {
controller: WalletStores.RootStore.walletConnectController
}
loginType: appMain.rootStore.loginType
}
}
Loader { Loader {
id: dappsConnectorSDKLoader id: dAppsServiceLoader
active: featureFlagsStore.connectorEnabled
sourceComponent: dappsConnectorSDK // It seems some of the functionality of the dapp connector depends on the DAppsService
active: {
return (featureFlagsStore.dappsEnabled || featureFlagsStore.connectorEnabled) && appMain.visible
} }
Loader { sourceComponent: DAppsService {
id: walletConnectServiceLoader id: dAppsService
Component.onCompleted: {
Global.dAppsService = dAppsService
}
// It seems some of the functionality of the dapp connector depends on the WalletConnectService // DAppsModule provides the middleware for the dapps
active: (featureFlagsStore.dappsEnabled || featureFlagsStore.connectorEnabled) && appMain.visible dappsModule: DAppsModule {
currenciesStore: WalletStores.RootStore.currencyStore
sourceComponent: WalletConnectService { groupedAccountAssetsModel: WalletStores.RootStore.walletAssetsStore.groupedAccountAssetsModel
id: walletConnectService accountsModel: WalletStores.RootStore.nonWatchAccounts
networksModel: SortFilterProxyModel {
wcSDK: WalletConnectSDK { sourceModel: WalletStores.RootStore.filteredFlatModel
enableSdk: WalletStores.RootStore.walletSectionInst.walletReady proxyRoles: [
FastExpressionRole {
name: "isOnline"
expression: !appMain.networkConnectionStore.blockchainNetworksDown.map(Number).includes(model.chainId)
expectedRoles: "chainId"
}
]
}
wcSdk: WalletConnectSDK {
enabled: featureFlagsStore.dappsEnabled && WalletStores.RootStore.walletSectionInst.walletReady
userUID: appMain.rootStore.profileSectionStore.profileStore.pubkey userUID: appMain.rootStore.profileSectionStore.profileStore.pubkey
projectId: WalletStores.RootStore.appSettings.walletConnectProjectID projectId: WalletStores.RootStore.appSettings.walletConnectProjectID
} }
bcSdk: DappsConnectorSDK {
enabled: featureFlagsStore.connectorEnabled && WalletStores.RootStore.walletSectionInst.walletReady
store: SharedStores.BrowserConnectStore {
controller: WalletStores.RootStore.dappsConnectorController
}
networksModel: WalletStores.RootStore.filteredFlatModel
accountsModel: WalletStores.RootStore.nonWatchAccounts
}
store: SharedStores.DAppsStore { store: SharedStores.DAppsStore {
controller: WalletStores.RootStore.walletConnectController controller: WalletStores.RootStore.walletConnectController
} }
bcStore: SharedStores.BrowserConnectStore {
controller: WalletStores.RootStore.dappsConnectorController
} }
walletRootStore: WalletStores.RootStore selectedAddress: WalletStores.RootStore.selectedAddress
blockchainNetworksDown: appMain.networkConnectionStore.blockchainNetworksDown accountsModel: WalletStores.RootStore.nonWatchAccounts
connectorFeatureEnabled: featureFlagsStore.connectorEnabled connectorFeatureEnabled: featureFlagsStore.connectorEnabled
walletConnectFeatureEnabled: featureFlagsStore.dappsEnabled walletConnectFeatureEnabled: featureFlagsStore.dappsEnabled
Component.onCompleted: {
Global.walletConnectService = walletConnectService
}
onDisplayToastMessage: (message, type) => { onDisplayToastMessage: (message, type) => {
const icon = type === Constants.ephemeralNotificationType.danger ? "warning" : const icon = type === Constants.ephemeralNotificationType.danger ? "warning" :
type === Constants.ephemeralNotificationType.success ? "checkmark-circle" : "info" type === Constants.ephemeralNotificationType.success ? "checkmark-circle" : "info"

View File

@ -7,7 +7,7 @@ SequentialAnimation {
property var target: null property var target: null
property color fromColor: Theme.palette.directColor1 property color fromColor: Theme.palette.directColor1
property color toColor: Theme.palette.baseColor5 property color toColor: Theme.palette.getColor(fromColor, 0.1)
property int duration: 500 // in milliseconds property int duration: 500 // in milliseconds
loops: 3 loops: 3

View File

@ -115,6 +115,7 @@ SignTransactionModalBase {
AnimatedText { AnimatedText {
id: maxFeesAnimation id: maxFeesAnimation
target: maxFees target: maxFees
fromColor: maxFees.customColor
} }
} }
} }
@ -214,6 +215,7 @@ SignTransactionModalBase {
AnimatedText { AnimatedText {
id: fiatFeesAnimation id: fiatFeesAnimation
target: fiatFees target: fiatFees
fromColor: fiatFees.customColor
} }
} }
StatusTextWithLoadingState { StatusTextWithLoadingState {
@ -236,6 +238,7 @@ SignTransactionModalBase {
AnimatedText { AnimatedText {
id: cryptoFeesAnimation id: cryptoFeesAnimation
target: cryptoFees target: cryptoFees
fromColor: cryptoFees.customColor
} }
} }
} }

View File

@ -18,8 +18,8 @@ SQUtils.QObject {
signal approveConnectResponse(string id, bool error) signal approveConnectResponse(string id, bool error)
signal rejectConnectResponse(string id, bool error) signal rejectConnectResponse(string id, bool error)
signal approveTransactionResponse(string requestId, bool error) signal approveTransactionResponse(string topic, string requestId, bool error)
signal rejectTransactionResponse(string requestId, bool error) signal rejectTransactionResponse(string topic, string requestId, bool error)
function approveConnection(id, account, chainId) { function approveConnection(id, account, chainId) {
return controller.approveConnection(id, account, chainId) return controller.approveConnection(id, account, chainId)
@ -29,12 +29,12 @@ SQUtils.QObject {
return controller.rejectConnection(id, error) return controller.rejectConnection(id, error)
} }
function approveTransaction(requestId, signature) { function approveTransaction(topic, requestId, signature) {
return controller.approveTransaction(requestId, signature) return controller.approveTransaction(topic, requestId, signature)
} }
function rejectTransaction(requestId, error) { function rejectTransaction(topic, requestId, error) {
return controller.rejectTransaction(requestId, error) return controller.rejectTransaction(topic, requestId, error)
} }
function disconnect(id) { function disconnect(id) {
@ -72,12 +72,12 @@ SQUtils.QObject {
root.rejectConnectResponse(id, error) root.rejectConnectResponse(id, error)
} }
function onApproveTransactionResponse(requestId, error) { function onApproveTransactionResponse(topic, requestId, error) {
root.approveTransactionResponse(requestId, error) root.approveTransactionResponse(topic, requestId, error)
} }
function onRejectTransactionResponse(requestId, error) { function onRejectTransactionResponse(topic, requestId, error) {
root.rejectTransactionResponse(requestId, error) root.rejectTransactionResponse(topic, requestId, error)
} }
} }
} }

View File

@ -10,7 +10,7 @@ QtObject {
property bool appIsReady: false property bool appIsReady: false
// use the generic var as type to break the cyclic dependency // use the generic var as type to break the cyclic dependency
property var walletConnectService: null property var dAppsService: null
signal openPinnedMessagesPopupRequested(var store, var messageStore, var pinnedMessagesModel, string messageToPin, string chatId) signal openPinnedMessagesPopupRequested(var store, var messageStore, var pinnedMessagesModel, string messageToPin, string chatId)
signal openCommunityProfilePopupRequested(var store, var community, var chatCommunitySectionModule) signal openCommunityProfilePopupRequested(var store, var community, var chatCommunitySectionModule)