feat(WC): Implementing SIWE flows

To squash - Implementing SIWE flows
This commit is contained in:
Alex Jbanca 2024-10-19 00:22:06 +03:00 committed by Alex Jbanca
parent 7e1dd3dd55
commit 517a10f805
13 changed files with 1815 additions and 266 deletions

View File

@ -310,7 +310,9 @@ Item {
function test_TestAuthentication() { function test_TestAuthentication() {
let td = mockSessionRequestEvent(this, handler.sdk, handler.accountsModel, handler.networksModel) let td = mockSessionRequestEvent(this, handler.sdk, handler.accountsModel, handler.networksModel)
handler.authenticate(td.request) handler.sdk.sessionRequestEvent(td.request.event)
let request = handler.requestsModel.findById(td.request.requestId)
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")
let store = handler.store let store = handler.store
@ -532,6 +534,7 @@ Item {
verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest") verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest")
ignoreWarning("Error: request expired") ignoreWarning("Error: request expired")
request.accept()
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, "")
@ -558,6 +561,7 @@ Item {
verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest") verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest")
ignoreWarning("Error: request expired") ignoreWarning("Error: request expired")
handler.requestsModel.findRequest(topic, session.id).accept()
handler.store.userAuthenticationFailed(topic, session.id) handler.store.userAuthenticationFailed(topic, session.id)
verify(sdk.rejectSessionRequestCalls.length === 1, "expected a call to sdk.rejectSessionRequest") verify(sdk.rejectSessionRequestCalls.length === 1, "expected a call to sdk.rejectSessionRequest")
} }
@ -581,6 +585,7 @@ Item {
verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest") verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest")
ignoreWarning("Error: request expired") ignoreWarning("Error: request expired")
handler.requestsModel.findRequest(topic, session.id).accept()
handler.store.userAuthenticationFailed(topic, session.id) handler.store.userAuthenticationFailed(topic, session.id)
verify(sdk.rejectSessionRequestCalls.length === 1, "expected a call to sdk.rejectSessionRequest") verify(sdk.rejectSessionRequestCalls.length === 1, "expected a call to sdk.rejectSessionRequest")
} }
@ -708,9 +713,8 @@ Item {
function test_TestPairingUnsupportedNetworks() { function test_TestPairingUnsupportedNetworks() {
const {sdk, walletStore, store} = testSetupPair(Testing.formatSessionProposal()) const {sdk, walletStore, store} = testSetupPair(Testing.formatSessionProposal())
let allApprovedNamespaces = JSON.parse(Testing.formatBuildApprovedNamespacesResult([], []))
const approvedArgs = sdk.buildApprovedNamespacesCalls[0] const approvedArgs = sdk.buildApprovedNamespacesCalls[0]
sdk.buildApprovedNamespacesResult(approvedArgs.id, allApprovedNamespaces, "") sdk.buildApprovedNamespacesResult(approvedArgs.id, {}, "Non conforming namespaces. approve() namespaces chains don't satisfy required namespaces")
compare(connectDAppSpy.count, 0, "expected not to have calls to service.connectDApp") compare(connectDAppSpy.count, 0, "expected not to have calls to service.connectDApp")
compare(service.onPairingValidatedTriggers.length, 1, "expected a call to service.onPairingValidated") compare(service.onPairingValidatedTriggers.length, 1, "expected a call to service.onPairingValidated")
compare(service.onPairingValidatedTriggers[0].validationState, Pairing.errors.unsupportedNetwork, "expected unsupportedNetwork state error") compare(service.onPairingValidatedTriggers[0].validationState, Pairing.errors.unsupportedNetwork, "expected unsupportedNetwork state error")
@ -794,8 +798,6 @@ Item {
// Implemented as a regression to metamask not having icons which failed dapps list // Implemented as a regression to metamask not having icons which failed dapps list
function test_TestUpdateDapps() { function test_TestUpdateDapps() {
provider.updateDapps()
// Validate that persistance fallback is working // Validate that persistance fallback is working
compare(provider.dappsModel.count, 2, "expected dappsModel have the right number of elements") compare(provider.dappsModel.count, 2, "expected dappsModel have the right number of elements")
let persistanceList = JSON.parse(dappsListReceivedJsonStr) let persistanceList = JSON.parse(dappsListReceivedJsonStr)

View File

@ -0,0 +1,148 @@
import QtQuick 2.15
import QtTest 1.15
import AppLayouts.Wallet.services.dapps.types 1.0
import shared.stores 1.0
Item {
id: root
width: 600
height: 400
Component {
id: sessionRequestComponent
SessionRequestWithAuth {
id: sessionRequest
readonly property SignalSpy executeSpy: SignalSpy { target: sessionRequest; signalName: "execute" }
readonly property SignalSpy rejectedSpy: SignalSpy { target: sessionRequest; signalName: "rejected" }
readonly property SignalSpy authFailedSpy: SignalSpy { target: sessionRequest; signalName: "authFailed" }
// SessionRequestResolved required properties
// Not of interest for this test
event: "event"
topic: "topic"
requestId: "id"
method: "method"
accountAddress: "address"
chainId: "chainID"
sourceId: 0
data: "data"
preparedData: "preparedData"
store: DAppsStore {
signal userAuthenticated(string topic, string id, string password, string pin)
signal userAuthenticationFailed(string topic, string id)
property var authenticateUserCalls: []
function authenticateUser(topic, id, address) {
authenticateUserCalls.push({topic, id, address})
}
}
}
}
TestCase {
id: sessionRequestTest
name: "SessionRequestWithAuth"
// Ensure mocked GroupedAccountsAssetsModel is properly initialized
when: windowShown
property SessionRequestWithAuth componentUnderTest: null
function init() {
componentUnderTest = createTemporaryObject(sessionRequestComponent, root)
}
function test_acceptAndAuthenticated() {
componentUnderTest.accept()
compare(componentUnderTest.executeSpy.count, 0)
compare(componentUnderTest.rejectedSpy.count, 0)
compare(componentUnderTest.authFailedSpy.count, 0)
compare(componentUnderTest.store.authenticateUserCalls.length, 1)
componentUnderTest.store.userAuthenticated("topic", "id", "password", "pin")
compare(componentUnderTest.executeSpy.count, 1)
compare(componentUnderTest.rejectedSpy.count, 0)
compare(componentUnderTest.authFailedSpy.count, 0)
compare(componentUnderTest.store.authenticateUserCalls.length, 1)
}
function test_AcceptAndAuthFails() {
componentUnderTest.accept()
compare(componentUnderTest.executeSpy.count, 0)
compare(componentUnderTest.rejectedSpy.count, 0)
compare(componentUnderTest.authFailedSpy.count, 0)
compare(componentUnderTest.store.authenticateUserCalls.length, 1)
componentUnderTest.store.userAuthenticationFailed("topic", "id")
compare(componentUnderTest.executeSpy.count, 0)
compare(componentUnderTest.rejectedSpy.count, 1)
compare(componentUnderTest.authFailedSpy.count, 1)
compare(componentUnderTest.store.authenticateUserCalls.length, 1)
}
function test_AcceptRequestExpired() {
ignoreWarning("Error: request expired")
componentUnderTest.expirationTimestamp = Date.now() / 1000 - 1
componentUnderTest.accept()
compare(componentUnderTest.executeSpy.count, 0)
compare(componentUnderTest.rejectedSpy.count, 1)
compare(componentUnderTest.authFailedSpy.count, 0)
compare(componentUnderTest.store.authenticateUserCalls.length, 0)
}
function test_AcceptAndReject() {
componentUnderTest.accept()
compare(componentUnderTest.executeSpy.count, 0)
compare(componentUnderTest.rejectedSpy.count, 0)
compare(componentUnderTest.authFailedSpy.count, 0)
compare(componentUnderTest.store.authenticateUserCalls.length, 1)
componentUnderTest.reject(false)
compare(componentUnderTest.executeSpy.count, 0)
compare(componentUnderTest.rejectedSpy.count, 1)
compare(componentUnderTest.authFailedSpy.count, 0)
compare(componentUnderTest.store.authenticateUserCalls.length, 1)
}
function test_AcceptAndExpiresAfterAuth() {
ignoreWarning("Error: request expired")
componentUnderTest.accept()
compare(componentUnderTest.executeSpy.count, 0)
compare(componentUnderTest.rejectedSpy.count, 0)
compare(componentUnderTest.authFailedSpy.count, 0)
compare(componentUnderTest.store.authenticateUserCalls.length, 1)
componentUnderTest.expirationTimestamp = Date.now() / 1000 - 1
componentUnderTest.store.userAuthenticated("topic", "id", "password", "pin")
compare(componentUnderTest.executeSpy.count, 0)
compare(componentUnderTest.rejectedSpy.count, 1)
compare(componentUnderTest.authFailedSpy.count, 0)
compare(componentUnderTest.store.authenticateUserCalls.length, 1)
}
function test_Reject() {
componentUnderTest.reject(false)
compare(componentUnderTest.executeSpy.count, 0)
compare(componentUnderTest.rejectedSpy.count, 1)
compare(componentUnderTest.authFailedSpy.count, 0)
compare(componentUnderTest.store.authenticateUserCalls.length, 0)
}
function test_RejectExpiredRequest() {
componentUnderTest.expirationTimestamp = Date.now() / 1000 - 1
componentUnderTest.reject(false)
compare(componentUnderTest.executeSpy.count, 0)
compare(componentUnderTest.rejectedSpy.count, 1)
compare(componentUnderTest.authFailedSpy.count, 0)
compare(componentUnderTest.store.authenticateUserCalls.length, 0)
}
}
}

View File

@ -0,0 +1,487 @@
import QtQuick 2.15
import QtTest 1.15
import AppLayouts.Wallet.services.dapps 1.0
import AppLayouts.Wallet.services.dapps.plugins 1.0
import shared.stores 1.0
Item {
id: root
width: 600
height: 400
Component {
id: siweLifeCycleComponent
SiweLifeCycle {
id: siweLifeCycle
readonly property SignalSpy startedSpy: SignalSpy { target: siweLifeCycle; signalName: "started" }
readonly property SignalSpy finishedSpy: SignalSpy { target: siweLifeCycle; signalName: "finished" }
readonly property SignalSpy requestSessionApprovalSpy: SignalSpy { target: siweLifeCycle; signalName: "requestSessionApproval" }
readonly property SignalSpy registerSignRequestSpy: SignalSpy { target: siweLifeCycle; signalName: "registerSignRequest" }
readonly property SignalSpy unregisterSignRequestSpy: SignalSpy { target: siweLifeCycle; signalName: "unregisterSignRequest" }
sdk: WalletConnectSDKBase {
id: sdkMock
projectId: "projectId"
property var getActiveSessionCalls: []
getActiveSessions: function(callback) {
getActiveSessionCalls.push(callback)
}
property var populateAuthPayloadCalls: []
populateAuthPayload: function(id, payload, chains, methods) {
populateAuthPayloadCalls.push({id, payload, chains, methods})
}
property var formatAuthMessageCalls: []
formatAuthMessage: function(id, request, iss) {
formatAuthMessageCalls.push({id, request, iss})
}
property var acceptSessionAuthenticateCalls: []
acceptSessionAuthenticate: function(id, auths) {
acceptSessionAuthenticateCalls.push({id, auths})
}
property var rejectSessionAuthenticateCalls: []
rejectSessionAuthenticate: function(id, error) {
rejectSessionAuthenticateCalls.push({id, error})
}
property var buildAuthObjectCalls: []
buildAuthObject: function(id, payload, signedData, account) {
buildAuthObjectCalls.push({id, payload, signedData, account})
}
}
store: DAppsStore {
id: dappsStoreMock
signal userAuthenticated(string topic, string id, string password, string pin)
signal userAuthenticationFailed(string topic, string id)
signal signingResult(string topic, string id, string data)
property var authenticateUserCalls: []
function authenticateUser(topic, id, address) {
authenticateUserCalls.push({topic, id, address})
}
property var signMessageCalls: []
function signMessage(topic, id, address, data, password, pin) {
signMessageCalls.push({topic, id, address, data, password, pin})
}
}
request: buildSiweRequestMessage()
accountsModel: ListModel {
ListElement { chainId: 1 }
ListElement { chainId: 2 }
}
networksModel: ListModel {
ListElement { address: "0x1" }
ListElement { address: "0x2" }
}
}
}
function buildSiweRequestMessage() {
const timestamp = Date.now() / 1000 + 1000
return {
"id":1729244859941412,
"params": {
"authPayload": {
"aud":"https://appkit-lab.reown.com",
"chains":["eip155:1","eip155:10","eip155:137","eip155:324","eip155:42161","eip155:8453","eip155:84532","eip155:1301","eip155:11155111","eip155:100","eip155:295"],
"domain":"appkit-lab.reown.com",
"iat":"2024-10-18T09:47:39.941Z",
"nonce":"e2f9d65105e06be0b3a86a675cf90c7a28a8c6d9d0fb84c2a1187c15ef27f120",
"resources":["urn:recap:eyJhdHQiOnsiZWlwMTU1Ijp7InJlcXVlc3QvZXRoX2FjY291bnRzIjpbe31dLCJyZXF1ZXN0L2V0aF9yZXF1ZXN0QWNjb3VudHMiOlt7fV0sInJlcXVlc3QvZXRoX3NlbmRSYXdUcmFuc2FjdGlvbiI6W3t9XSwicmVxdWVzdC9ldGhfc2VuZFRyYW5zYWN0aW9uIjpbe31dLCJyZXF1ZXN0L2V0aF9zaWduIjpbe31dLCJyZXF1ZXN0L2V0aF9zaWduVHJhbnNhY3Rpb24iOlt7fV0sInJlcXVlc3QvZXRoX3NpZ25UeXBlZERhdGEiOlt7fV0sInJlcXVlc3QvZXRoX3NpZ25UeXBlZERhdGFfdjMiOlt7fV0sInJlcXVlc3QvZXRoX3NpZ25UeXBlZERhdGFfdjQiOlt7fV0sInJlcXVlc3QvcGVyc29uYWxfc2lnbiI6W3t9XSwicmVxdWVzdC93YWxsZXRfYWRkRXRoZXJldW1DaGFpbiI6W3t9XSwicmVxdWVzdC93YWxsZXRfZ2V0Q2FsbHNTdGF0dXMiOlt7fV0sInJlcXVlc3Qvd2FsbGV0X2dldENhcGFiaWxpdGllcyI6W3t9XSwicmVxdWVzdC93YWxsZXRfZ2V0UGVybWlzc2lvbnMiOlt7fV0sInJlcXVlc3Qvd2FsbGV0X2dyYW50UGVybWlzc2lvbnMiOlt7fV0sInJlcXVlc3Qvd2FsbGV0X3JlZ2lzdGVyT25ib2FyZGluZyI6W3t9XSwicmVxdWVzdC93YWxsZXRfcmVxdWVzdFBlcm1pc3Npb25zIjpbe31dLCJyZXF1ZXN0L3dhbGxldF9zY2FuUVJDb2RlIjpbe31dLCJyZXF1ZXN0L3dhbGxldF9zZW5kQ2FsbHMiOlt7fV0sInJlcXVlc3Qvd2FsbGV0X3N3aXRjaEV0aGVyZXVtQ2hhaW4iOlt7fV0sInJlcXVlc3Qvd2FsbGV0X3dhdGNoQXNzZXQiOlt7fV19fX0"],
"statement":"Please sign with your account",
"type":"caip122",
"version":"1"
},
"expiryTimestamp": timestamp,
"requester": {
"metadata": {
"description":"Explore the AppKit Lab to test the latest AppKit features.",
"icons":["https://appkit-lab.reown.com/favicon.svg"],
"name":"AppKit Lab",
"url":"https://appkit-lab.reown.com"
},
"publicKey":"205aeb6376a2d79e8b0fa02aa8123473a7822a30ea3dc9d7be9b3de4e31e9f2b"
}
},
"topic":"c525f017208ca3b4ad53928c16bab48d03af42c6cd47608c5fd73703bf5700bb",
"verifyContext": {
"verified": {
"isScam":null,
"origin":"https://appkit-lab.reown.com",
"validation":"VALID",
"verifyUrl":"https://verify.walletconnect.org"
}
}
}
}
function buildSiweAuthPayload() {
return {
"aud":"https://appkit-lab.reown.com",
"chains":["eip155:1","eip155:10","eip155:42161"],
"domain":"appkit-lab.reown.com","iat":"2024-10-18T09:47:39.941Z",
"nonce":"e2f9d65105e06be0b3a86a675cf90c7a28a8c6d9d0fb84c2a1187c15ef27f120",
"resources":["urn:recap:eyJhdHQiOnsiZWlwMTU1Ijp7InJlcXVlc3QvZXRoX3NlbmRUcmFuc2FjdGlvbiI6W3siY2hhaW5zIjpbImVpcDE1NToxIiwiZWlwMTU1OjEwIiwiZWlwMTU1OjQyMTYxIl19XSwicmVxdWVzdC9ldGhfc2lnbiI6W3siY2hhaW5zIjpbImVpcDE1NToxIiwiZWlwMTU1OjEwIiwiZWlwMTU1OjQyMTYxIl19XSwicmVxdWVzdC9ldGhfc2lnblR5cGVkRGF0YSI6W3siY2hhaW5zIjpbImVpcDE1NToxIiwiZWlwMTU1OjEwIiwiZWlwMTU1OjQyMTYxIl19XSwicmVxdWVzdC9ldGhfc2lnblR5cGVkRGF0YV92NCI6W3siY2hhaW5zIjpbImVpcDE1NToxIiwiZWlwMTU1OjEwIiwiZWlwMTU1OjQyMTYxIl19XSwicmVxdWVzdC9wZXJzb25hbF9zaWduIjpbeyJjaGFpbnMiOlsiZWlwMTU1OjEiLCJlaXAxNTU6MTAiLCJlaXAxNTU6NDIxNjEiXX1dfX19"],
"statement":"Please sign with your account I further authorize the stated URI to perform the following actions on my behalf: (1) 'request': 'eth_sendTransaction', 'eth_sign', 'eth_signTypedData', 'eth_signTypedData_v4', 'personal_sign' for 'eip155'.",
"type":"caip122",
"version":"1"
}
}
function formattedMessage() {
return "appkit-lab.reown.com wants you to sign in with your Ethereum account:\n0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240\n\nPlease sign with your account I further authorize the stated URI to perform the following actions on my behalf: (1) 'request': 'eth_sendTransaction', 'eth_sign', 'eth_signTypedData', 'eth_signTypedData_v4', 'personal_sign' for 'eip155'.\n\nURI: https://appkit-lab.reown.com\nVersion: 1\nChain ID: 1\nNonce: e2f9d65105e06be0b3a86a675cf90c7a28a8c6d9d0fb84c2a1187c15ef27f120\nIssued At: 2024-10-18T09:47:39.941Z\nResources:\n- urn:recap:eyJhdHQiOnsiZWlwMTU1Ijp7InJlcXVlc3QvZXRoX3NlbmRUcmFuc2FjdGlvbiI6W3siY2hhaW5zIjpbImVpcDE1NToxIiwiZWlwMTU1OjEwIiwiZWlwMTU1OjQyMTYxIl19XSwicmVxdWVzdC9ldGhfc2lnbiI6W3siY2hhaW5zIjpbImVpcDE1NToxIiwiZWlwMTU1OjEwIiwiZWlwMTU1OjQyMTYxIl19XSwicmVxdWVzdC9ldGhfc2lnblR5cGVkRGF0YSI6W3siY2hhaW5zIjpbImVpcDE1NToxIiwiZWlwMTU1OjEwIiwiZWlwMTU1OjQyMTYxIl19XSwicmVxdWVzdC9ldGhfc2lnblR5cGVkRGF0YV92NCI6W3siY2hhaW5zIjpbImVpcDE1NToxIiwiZWlwMTU1OjEwIiwiZWlwMTU1OjQyMTYxIl19XSwicmVxdWVzdC9wZXJzb25hbF9zaWduIjpbeyJjaGFpbnMiOlsiZWlwMTU1OjEiLCJlaXAxNTU6MTAiLCJlaXAxNTU6NDIxNjEiXX1dfX19"
}
TestCase {
id: siweLifeCycleTest
name: "SiweLifeCycle"
property SiweLifeCycle componentUnderTest: null
function init() {
componentUnderTest = createTemporaryObject(siweLifeCycleComponent, root)
}
function test_EndToEndSuccessful() {
const requestEvent = componentUnderTest.request
componentUnderTest.start()
compare(componentUnderTest.startedSpy.count, 1)
compare(componentUnderTest.finishedSpy.count, 0)
compare(componentUnderTest.requestSessionApprovalSpy.count, 0)
compare(componentUnderTest.registerSignRequestSpy.count, 0)
compare(componentUnderTest.unregisterSignRequestSpy.count, 0)
// Step 1: Get the active sessions from the SDK
compare(componentUnderTest.sdk.getActiveSessionCalls.length, 1)
componentUnderTest.sdk.getActiveSessionCalls[0]({})
// Step 2: Request chains and accounts approval
compare(componentUnderTest.requestSessionApprovalSpy.count, 1)
compare(componentUnderTest.startedSpy.count, 1)
compare(componentUnderTest.finishedSpy.count, 0)
compare(componentUnderTest.registerSignRequestSpy.count, 0)
compare(componentUnderTest.unregisterSignRequestSpy.count, 0)
const key = requestEvent.id
const chains = ["eip155:1", "eip155:10", "eip155:42161"]
const accounts = ["eip155:1:0x1", "eip155:1:0x2"]
const methods = ["eth_sendTransaction", "eth_sign", "eth_signTypedData", "eth_signTypedData_v4", "personal_sign"]
componentUnderTest.sessionApproved(key, { eip155: {
chains,
accounts,
methods
}})
// Step 3: Populate the auth payload
compare(componentUnderTest.sdk.populateAuthPayloadCalls.length, 1)
verify(componentUnderTest.sdk.populateAuthPayloadCalls[0].id == key)
compare(componentUnderTest.sdk.populateAuthPayloadCalls[0].payload, requestEvent.params.authPayload)
compare(componentUnderTest.sdk.populateAuthPayloadCalls[0].chains, chains)
compare(componentUnderTest.sdk.populateAuthPayloadCalls[0].methods, methods)
// No extra event is sent
compare(componentUnderTest.startedSpy.count, 1)
compare(componentUnderTest.finishedSpy.count, 0)
compare(componentUnderTest.requestSessionApprovalSpy.count, 1)
compare(componentUnderTest.registerSignRequestSpy.count, 0)
compare(componentUnderTest.unregisterSignRequestSpy.count, 0)
// Step 4: Format the auth message
const authPayload = buildSiweAuthPayload()
componentUnderTest.sdk.populateAuthPayloadResult(key, authPayload, "")
compare(componentUnderTest.sdk.formatAuthMessageCalls.length, 1)
verify(componentUnderTest.sdk.formatAuthMessageCalls[0].id == key)
compare(componentUnderTest.sdk.formatAuthMessageCalls[0].request, authPayload)
compare(componentUnderTest.sdk.formatAuthMessageCalls[0].iss, "eip155:1:0x1")
// No extra event is sent
compare(componentUnderTest.startedSpy.count, 1)
compare(componentUnderTest.finishedSpy.count, 0)
compare(componentUnderTest.requestSessionApprovalSpy.count, 1)
compare(componentUnderTest.registerSignRequestSpy.count, 0)
compare(componentUnderTest.unregisterSignRequestSpy.count, 0)
// Step 5: Accept the session authentication and sign the message
componentUnderTest.sdk.formatAuthMessageResult(key, formattedMessage(), "")
compare(componentUnderTest.registerSignRequestSpy.count, 1)
// No extra event is sent
compare(componentUnderTest.startedSpy.count, 1)
compare(componentUnderTest.finishedSpy.count, 0)
compare(componentUnderTest.requestSessionApprovalSpy.count, 1)
compare(componentUnderTest.unregisterSignRequestSpy.count, 0)
const request = componentUnderTest.registerSignRequestSpy.signalArguments[0][0]
verify(!!request)
request.execute("password", "pin")
compare(componentUnderTest.store.signMessageCalls.length, 1)
componentUnderTest.store.signingResult(requestEvent.topic, requestEvent.id, "signedData")
// Step 6: Build the response
compare(componentUnderTest.sdk.buildAuthObjectCalls.length, 1)
verify(componentUnderTest.sdk.buildAuthObjectCalls[0].id == key)
compare(componentUnderTest.sdk.buildAuthObjectCalls[0].payload, authPayload)
compare(componentUnderTest.sdk.buildAuthObjectCalls[0].signedData, "signedData")
compare(componentUnderTest.sdk.buildAuthObjectCalls[0].account, "eip155:1:0x1")
componentUnderTest.sdk.buildAuthObjectResult(key, "authObject", "")
// Step 7: Accept the session authentication
compare(componentUnderTest.sdk.acceptSessionAuthenticateCalls.length, 1)
verify(componentUnderTest.sdk.acceptSessionAuthenticateCalls[0].id == key)
compare(componentUnderTest.sdk.acceptSessionAuthenticateCalls[0].auths, ["authObject"])
compare(componentUnderTest.startedSpy.count, 1)
compare(componentUnderTest.finishedSpy.count, 0)
// Step 8: Finish the process
componentUnderTest.sdk.acceptSessionAuthenticateResult(key, "", "")
compare(componentUnderTest.finishedSpy.count, 1)
}
function test_StartExpired() {
ignoreWarning(new RegExp(/^Error in SiweLifeCycle/))
const expiredRequest = buildSiweRequestMessage()
expiredRequest.params.expiryTimestamp = Date.now() / 1000 - 1
componentUnderTest.request = expiredRequest
componentUnderTest.start()
compare(componentUnderTest.startedSpy.count, 0)
compare(componentUnderTest.finishedSpy.count, 1)
compare(componentUnderTest.requestSessionApprovalSpy.count, 0)
compare(componentUnderTest.registerSignRequestSpy.count, 0)
compare(componentUnderTest.unregisterSignRequestSpy.count, 1)
compare(componentUnderTest.sdk.getActiveSessionCalls.length, 0)
compare(componentUnderTest.sdk.populateAuthPayloadCalls.length, 0)
compare(componentUnderTest.sdk.formatAuthMessageCalls.length, 0)
compare(componentUnderTest.sdk.buildAuthObjectCalls.length, 0)
compare(componentUnderTest.sdk.acceptSessionAuthenticateCalls.length, 0)
}
function test_StartWithExistingSession() {
ignoreWarning(new RegExp(/^Error in SiweLifeCycle/))
const requestEvent = componentUnderTest.request
componentUnderTest.start()
compare(componentUnderTest.startedSpy.count, 1)
compare(componentUnderTest.finishedSpy.count, 0)
compare(componentUnderTest.requestSessionApprovalSpy.count, 0)
compare(componentUnderTest.registerSignRequestSpy.count, 0)
compare(componentUnderTest.unregisterSignRequestSpy.count, 0)
// Step 1: Get the active sessions from the SDK
// return an existing session with the same topic
compare(componentUnderTest.sdk.getActiveSessionCalls.length, 1)
componentUnderTest.sdk.getActiveSessionCalls[0]({ [requestEvent.topic]: { }})
compare(componentUnderTest.finishedSpy.count, 1)
}
function test_StartWithInvalidSession() {
// regex to check if the warning starts with "Error in SiweLifeCycle"
ignoreWarning(new RegExp(/^Error in SiweLifeCycle/))
const requestEvent = componentUnderTest.request
componentUnderTest.start()
compare(componentUnderTest.startedSpy.count, 1)
compare(componentUnderTest.finishedSpy.count, 0)
compare(componentUnderTest.requestSessionApprovalSpy.count, 0)
compare(componentUnderTest.registerSignRequestSpy.count, 0)
compare(componentUnderTest.unregisterSignRequestSpy.count, 0)
// Step 1: Get the active sessions from the SDK
// return an existing session with the same topic
compare(componentUnderTest.sdk.getActiveSessionCalls.length, 1)
componentUnderTest.sdk.getActiveSessionCalls[0]("invalidresponse")
compare(componentUnderTest.finishedSpy.count, 1)
}
function test_StartWithInvalidRequest() {
// regex to check if the warning starts with "Error in SiweLifeCycle"
ignoreWarning(new RegExp(/^Error in SiweLifeCycle/))
componentUnderTest.request = {}
componentUnderTest.start()
compare(componentUnderTest.startedSpy.count, 0)
compare(componentUnderTest.finishedSpy.count, 1)
compare(componentUnderTest.requestSessionApprovalSpy.count, 0)
compare(componentUnderTest.registerSignRequestSpy.count, 0)
compare(componentUnderTest.unregisterSignRequestSpy.count, 1)
compare(componentUnderTest.sdk.getActiveSessionCalls.length, 0)
}
function test_RejectedSessionApproval() {
const requestEvent = componentUnderTest.request
componentUnderTest.start()
componentUnderTest.sdk.getActiveSessionCalls[0]({})
compare(componentUnderTest.requestSessionApprovalSpy.count, 1)
compare(componentUnderTest.startedSpy.count, 1)
compare(componentUnderTest.finishedSpy.count, 0)
const key = requestEvent.id
componentUnderTest.sessionRejected(key)
compare(componentUnderTest.finishedSpy.count, 1)
}
function test_RejectSign() {
const requestEvent = componentUnderTest.request
componentUnderTest.start()
componentUnderTest.sdk.getActiveSessionCalls[0]({})
compare(componentUnderTest.requestSessionApprovalSpy.count, 1)
compare(componentUnderTest.startedSpy.count, 1)
compare(componentUnderTest.finishedSpy.count, 0)
const key = requestEvent.id
const chains = ["eip155:1", "eip155:10", "eip155:42161"]
const accounts = ["eip155:1:0x1", "eip155:1:0x2"]
const methods = ["eth_sendTransaction", "eth_sign", "eth_signTypedData", "eth_signTypedData_v4", "personal_sign"]
componentUnderTest.sessionApproved(key, { eip155: {
chains,
accounts,
methods
}})
componentUnderTest.sdk.populateAuthPayloadResult(key, buildSiweAuthPayload(), "")
componentUnderTest.sdk.formatAuthMessageResult(key, formattedMessage(), "")
const request = componentUnderTest.registerSignRequestSpy.signalArguments[0][0]
request.reject(false)
compare(componentUnderTest.finishedSpy.count, 1)
}
function test_AuthenticationFails() {
const requestEvent = componentUnderTest.request
componentUnderTest.start()
componentUnderTest.sdk.getActiveSessionCalls[0]({})
compare(componentUnderTest.requestSessionApprovalSpy.count, 1)
compare(componentUnderTest.startedSpy.count, 1)
compare(componentUnderTest.finishedSpy.count, 0)
const key = requestEvent.id
const chains = ["eip155:1", "eip155:10", "eip155:42161"]
const accounts = ["eip155:1:0x1", "eip155:1:0x2"]
const methods = ["eth_sendTransaction", "eth_sign", "eth_signTypedData", "eth_signTypedData_v4", "personal_sign"]
componentUnderTest.sessionApproved(key, { eip155: {
chains,
accounts,
methods
}})
componentUnderTest.sdk.populateAuthPayloadResult(key, buildSiweAuthPayload(), "")
componentUnderTest.sdk.formatAuthMessageResult(key, formattedMessage(), "")
const request = componentUnderTest.registerSignRequestSpy.signalArguments[0][0]
request.reject(false)
componentUnderTest.store.userAuthenticationFailed(requestEvent.topic, requestEvent.id)
compare(componentUnderTest.finishedSpy.count, 1)
}
function test_InvalidPopulatedAuthPayload() {
const requestEvent = componentUnderTest.request
componentUnderTest.start()
componentUnderTest.sdk.getActiveSessionCalls[0]({})
compare(componentUnderTest.requestSessionApprovalSpy.count, 1)
compare(componentUnderTest.startedSpy.count, 1)
compare(componentUnderTest.finishedSpy.count, 0)
const key = requestEvent.id
const chains = ["eip155:1", "eip155:10", "eip155:42161"]
const accounts = ["eip155:1:0x1", "eip155:1:0x2"]
const methods = ["eth_sendTransaction", "eth_sign", "eth_signTypedData", "eth_signTypedData_v4", "personal_sign"]
componentUnderTest.sessionApproved(key, { eip155: {
chains,
accounts,
methods
}})
componentUnderTest.sdk.populateAuthPayloadResult(key, undefined, "")
compare(componentUnderTest.finishedSpy.count, 1)
}
function test_invalidFormatAuthMessage() {
const requestEvent = componentUnderTest.request
componentUnderTest.start()
componentUnderTest.sdk.getActiveSessionCalls[0]({})
compare(componentUnderTest.requestSessionApprovalSpy.count, 1)
compare(componentUnderTest.startedSpy.count, 1)
compare(componentUnderTest.finishedSpy.count, 0)
const key = requestEvent.id
const chains = ["eip155:1", "eip155:10", "eip155:42161"]
const accounts = ["eip155:1:0x1", "eip155:1:0x2"]
const methods = ["eth_sendTransaction", "eth_sign", "eth_signTypedData", "eth_signTypedData_v4", "personal_sign"]
componentUnderTest.sessionApproved(key, { eip155: {
chains,
accounts,
methods
}})
componentUnderTest.sdk.populateAuthPayloadResult(key, buildSiweAuthPayload(), "")
componentUnderTest.sdk.formatAuthMessageResult(key, undefined, "")
compare(componentUnderTest.finishedSpy.count, 1)
}
function test_CallsWithDifferentId() {
const requestEvent = componentUnderTest.request
componentUnderTest.start()
componentUnderTest.sdk.getActiveSessionCalls[0]({})
const key = requestEvent.id
const chains = ["eip155:1", "eip155:10", "eip155:42161"]
const accounts = ["eip155:1:0x1", "eip155:1:0x2"]
const methods = ["eth_sendTransaction", "eth_sign", "eth_signTypedData", "eth_signTypedData_v4", "personal_sign"]
// wrong key
componentUnderTest.sessionApproved(key + 1, { eip155: {
chains,
accounts,
methods
}})
compare(componentUnderTest.sdk.populateAuthPayloadCalls.length, 0)
//correct key
componentUnderTest.sessionApproved(key, { eip155: {
chains,
accounts,
methods
}})
compare(componentUnderTest.sdk.populateAuthPayloadCalls.length, 1)
// wrong key
componentUnderTest.sdk.populateAuthPayloadResult(key + 1, buildSiweAuthPayload(), "")
compare(componentUnderTest.sdk.formatAuthMessageCalls.length, 0)
// correct key
componentUnderTest.sdk.populateAuthPayloadResult(key, buildSiweAuthPayload(), "")
compare(componentUnderTest.sdk.formatAuthMessageCalls.length, 1)
// wrong key
componentUnderTest.sdk.formatAuthMessageResult(key + 1, formattedMessage(), "")
compare(componentUnderTest.registerSignRequestSpy.count, 0)
// correct key
componentUnderTest.sdk.formatAuthMessageResult(key, formattedMessage(), "")
compare(componentUnderTest.registerSignRequestSpy.count, 1)
const request = componentUnderTest.registerSignRequestSpy.signalArguments[0][0]
request.execute("password", "pin")
// wrong key
componentUnderTest.store.signingResult(requestEvent.topic, requestEvent.id + 1, "signedData")
compare(componentUnderTest.sdk.buildAuthObjectCalls.length, 0)
// correct key
componentUnderTest.store.signingResult(requestEvent.topic, requestEvent.id, "signedData")
compare(componentUnderTest.sdk.buildAuthObjectCalls.length, 1)
// wrong key
componentUnderTest.sdk.buildAuthObjectResult(key + 1, "authObject", "")
compare(componentUnderTest.sdk.acceptSessionAuthenticateCalls.length, 0)
// correct key
componentUnderTest.sdk.buildAuthObjectResult(key, "authObject", "")
compare(componentUnderTest.sdk.acceptSessionAuthenticateCalls.length, 1)
// wrong key
componentUnderTest.sdk.acceptSessionAuthenticateResult(key + 1, "", "")
compare(componentUnderTest.finishedSpy.count, 0)
// correct key
componentUnderTest.sdk.acceptSessionAuthenticateResult(key, "", "")
compare(componentUnderTest.finishedSpy.count, 1)
}
}
}

View File

@ -0,0 +1,245 @@
import QtQuick 2.15
import QtTest 1.15
import AppLayouts.Wallet.services.dapps 1.0
import AppLayouts.Wallet.services.dapps.plugins 1.0
import shared.stores 1.0
Item {
id: root
width: 600
height: 400
Component {
id: siweRequestPlugin
SiweRequestPlugin {
id: siwePlugin
readonly property SignalSpy connectDAppSpy: SignalSpy { target: siwePlugin; signalName: "connectDApp" }
readonly property SignalSpy registerSignRequestSpy: SignalSpy { target: siwePlugin; signalName: "registerSignRequest" }
readonly property SignalSpy unregisterSignRequestSpy: SignalSpy { target: siwePlugin; signalName: "unregisterSignRequest" }
readonly property SignalSpy siweSuccessfulSpy: SignalSpy { target: siwePlugin; signalName: "siweSuccessful" }
readonly property SignalSpy siweFailedSpy: SignalSpy { target: siwePlugin; signalName: "siweFailed" }
sdk: WalletConnectSDKBase {
id: sdkMock
projectId: "projectId"
getActiveSessions: function(callback) {
callback({})
}
populateAuthPayload: function(id, payload, chains, methods) {
sdkMock.populateAuthPayloadResult(id, {}, "")
}
formatAuthMessage: function(id, request, iss) {
sdkMock.formatAuthMessageResult(id, {}, "")
}
acceptSessionAuthenticate: function(id, auths) {
sdkMock.acceptSessionAuthenticateResult(id, {}, "")
}
rejectSessionAuthenticate: function(id, error) {
sdkMock.rejectSessionAuthenticateResult(id, {}, "")
}
buildAuthObject: function(id, payload, signedData, account) {
sdkMock.buildAuthObjectResult(id, {}, "")
}
}
store: DAppsStore {
id: dappsStoreMock
signal userAuthenticated(string topic, string id, string password, string pin)
signal userAuthenticationFailed(string topic, string id)
signal signingResult(string topic, string id, string data)
property var authenticateUserCalls: []
function authenticateUser(topic, id, address) {
authenticateUserCalls.push({topic, id, address})
}
property var signMessageCalls: []
function signMessage(topic, id, address, data, password, pin) {
signMessageCalls.push({topic, id, address, data, password, pin})
}
}
accountsModel: ListModel {
ListElement { chainId: 1 }
ListElement { chainId: 2 }
}
networksModel: ListModel {
ListElement { address: "0x1" }
ListElement { address: "0x2" }
}
}
}
function buildSiweRequestMessage() {
const timestamp = Date.now() / 1000 + 1000
return {
"id":1729244859941412,
"params": {
"authPayload": {
"aud":"https://appkit-lab.reown.com",
"chains":["eip155:1","eip155:10","eip155:137","eip155:324","eip155:42161","eip155:8453","eip155:84532","eip155:1301","eip155:11155111","eip155:100","eip155:295"],
"domain":"appkit-lab.reown.com",
"iat":"2024-10-18T09:47:39.941Z",
"nonce":"e2f9d65105e06be0b3a86a675cf90c7a28a8c6d9d0fb84c2a1187c15ef27f120",
"resources":["urn:recap:eyJhdHQiOnsiZWlwMTU1Ijp7InJlcXVlc3QvZXRoX2FjY291bnRzIjpbe31dLCJyZXF1ZXN0L2V0aF9yZXF1ZXN0QWNjb3VudHMiOlt7fV0sInJlcXVlc3QvZXRoX3NlbmRSYXdUcmFuc2FjdGlvbiI6W3t9XSwicmVxdWVzdC9ldGhfc2VuZFRyYW5zYWN0aW9uIjpbe31dLCJyZXF1ZXN0L2V0aF9zaWduIjpbe31dLCJyZXF1ZXN0L2V0aF9zaWduVHJhbnNhY3Rpb24iOlt7fV0sInJlcXVlc3QvZXRoX3NpZ25UeXBlZERhdGEiOlt7fV0sInJlcXVlc3QvZXRoX3NpZ25UeXBlZERhdGFfdjMiOlt7fV0sInJlcXVlc3QvZXRoX3NpZ25UeXBlZERhdGFfdjQiOlt7fV0sInJlcXVlc3QvcGVyc29uYWxfc2lnbiI6W3t9XSwicmVxdWVzdC93YWxsZXRfYWRkRXRoZXJldW1DaGFpbiI6W3t9XSwicmVxdWVzdC93YWxsZXRfZ2V0Q2FsbHNTdGF0dXMiOlt7fV0sInJlcXVlc3Qvd2FsbGV0X2dldENhcGFiaWxpdGllcyI6W3t9XSwicmVxdWVzdC93YWxsZXRfZ2V0UGVybWlzc2lvbnMiOlt7fV0sInJlcXVlc3Qvd2FsbGV0X2dyYW50UGVybWlzc2lvbnMiOlt7fV0sInJlcXVlc3Qvd2FsbGV0X3JlZ2lzdGVyT25ib2FyZGluZyI6W3t9XSwicmVxdWVzdC93YWxsZXRfcmVxdWVzdFBlcm1pc3Npb25zIjpbe31dLCJyZXF1ZXN0L3dhbGxldF9zY2FuUVJDb2RlIjpbe31dLCJyZXF1ZXN0L3dhbGxldF9zZW5kQ2FsbHMiOlt7fV0sInJlcXVlc3Qvd2FsbGV0X3N3aXRjaEV0aGVyZXVtQ2hhaW4iOlt7fV0sInJlcXVlc3Qvd2FsbGV0X3dhdGNoQXNzZXQiOlt7fV19fX0"],
"statement":"Please sign with your account",
"type":"caip122",
"version":"1"
},
"expiryTimestamp": timestamp,
"requester": {
"metadata": {
"description":"Explore the AppKit Lab to test the latest AppKit features.",
"icons":["https://appkit-lab.reown.com/favicon.svg"],
"name":"AppKit Lab",
"url":"https://appkit-lab.reown.com"
},
"publicKey":"205aeb6376a2d79e8b0fa02aa8123473a7822a30ea3dc9d7be9b3de4e31e9f2b"
}
},
"topic":"c525f017208ca3b4ad53928c16bab48d03af42c6cd47608c5fd73703bf5700bb",
"verifyContext": {
"verified": {
"isScam":null,
"origin":"https://appkit-lab.reown.com",
"validation":"VALID",
"verifyUrl":"https://verify.walletconnect.org"
}
}
}
}
// function buildSiweAuthPayload() {
// return {
// "aud":"https://appkit-lab.reown.com",
// "chains":["eip155:1","eip155:10","eip155:42161"],
// "domain":"appkit-lab.reown.com","iat":"2024-10-18T09:47:39.941Z",
// "nonce":"e2f9d65105e06be0b3a86a675cf90c7a28a8c6d9d0fb84c2a1187c15ef27f120",
// "resources":["urn:recap:eyJhdHQiOnsiZWlwMTU1Ijp7InJlcXVlc3QvZXRoX3NlbmRUcmFuc2FjdGlvbiI6W3siY2hhaW5zIjpbImVpcDE1NToxIiwiZWlwMTU1OjEwIiwiZWlwMTU1OjQyMTYxIl19XSwicmVxdWVzdC9ldGhfc2lnbiI6W3siY2hhaW5zIjpbImVpcDE1NToxIiwiZWlwMTU1OjEwIiwiZWlwMTU1OjQyMTYxIl19XSwicmVxdWVzdC9ldGhfc2lnblR5cGVkRGF0YSI6W3siY2hhaW5zIjpbImVpcDE1NToxIiwiZWlwMTU1OjEwIiwiZWlwMTU1OjQyMTYxIl19XSwicmVxdWVzdC9ldGhfc2lnblR5cGVkRGF0YV92NCI6W3siY2hhaW5zIjpbImVpcDE1NToxIiwiZWlwMTU1OjEwIiwiZWlwMTU1OjQyMTYxIl19XSwicmVxdWVzdC9wZXJzb25hbF9zaWduIjpbeyJjaGFpbnMiOlsiZWlwMTU1OjEiLCJlaXAxNTU6MTAiLCJlaXAxNTU6NDIxNjEiXX1dfX19"],
// "statement":"Please sign with your account I further authorize the stated URI to perform the following actions on my behalf: (1) 'request': 'eth_sendTransaction', 'eth_sign', 'eth_signTypedData', 'eth_signTypedData_v4', 'personal_sign' for 'eip155'.",
// "type":"caip122",
// "version":"1"
// }
// }
// function formattedMessage() {
// return "appkit-lab.reown.com wants you to sign in with your Ethereum account:\n0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7240\n\nPlease sign with your account I further authorize the stated URI to perform the following actions on my behalf: (1) 'request': 'eth_sendTransaction', 'eth_sign', 'eth_signTypedData', 'eth_signTypedData_v4', 'personal_sign' for 'eip155'.\n\nURI: https://appkit-lab.reown.com\nVersion: 1\nChain ID: 1\nNonce: e2f9d65105e06be0b3a86a675cf90c7a28a8c6d9d0fb84c2a1187c15ef27f120\nIssued At: 2024-10-18T09:47:39.941Z\nResources:\n- urn:recap:eyJhdHQiOnsiZWlwMTU1Ijp7InJlcXVlc3QvZXRoX3NlbmRUcmFuc2FjdGlvbiI6W3siY2hhaW5zIjpbImVpcDE1NToxIiwiZWlwMTU1OjEwIiwiZWlwMTU1OjQyMTYxIl19XSwicmVxdWVzdC9ldGhfc2lnbiI6W3siY2hhaW5zIjpbImVpcDE1NToxIiwiZWlwMTU1OjEwIiwiZWlwMTU1OjQyMTYxIl19XSwicmVxdWVzdC9ldGhfc2lnblR5cGVkRGF0YSI6W3siY2hhaW5zIjpbImVpcDE1NToxIiwiZWlwMTU1OjEwIiwiZWlwMTU1OjQyMTYxIl19XSwicmVxdWVzdC9ldGhfc2lnblR5cGVkRGF0YV92NCI6W3siY2hhaW5zIjpbImVpcDE1NToxIiwiZWlwMTU1OjEwIiwiZWlwMTU1OjQyMTYxIl19XSwicmVxdWVzdC9wZXJzb25hbF9zaWduIjpbeyJjaGFpbnMiOlsiZWlwMTU1OjEiLCJlaXAxNTU6MTAiLCJlaXAxNTU6NDIxNjEiXX1dfX19"
// }
TestCase {
id: siwePlugin
name: "SiwePlugin"
property SiweRequestPlugin componentUnderTest: null
function init() {
componentUnderTest = createTemporaryObject(siweRequestPlugin, root)
}
function test_NewValidRequest() {
const request = buildSiweRequestMessage()
const dAppUrl = request.params.requester.metadata.url
const dAppName = request.params.requester.metadata.name
const dAppIcon = request.params.requester.metadata.icons[0]
const key = request.id
componentUnderTest.sdk.sessionAuthenticateRequest(request)
compare(componentUnderTest.connectDAppSpy.count, 1)
compare(componentUnderTest.connectDAppSpy.signalArguments[0][0], [1, 10, 137, 324, 42161, 8453, 84532, 1301, 11155111, 100, 295])
compare(componentUnderTest.connectDAppSpy.signalArguments[0][1], dAppUrl)
compare(componentUnderTest.connectDAppSpy.signalArguments[0][2], dAppName)
compare(componentUnderTest.connectDAppSpy.signalArguments[0][3], dAppIcon)
compare(componentUnderTest.connectDAppSpy.signalArguments[0][4], key)
}
function test_ApproveNewRequest() {
const request = buildSiweRequestMessage()
const key = request.id
const chains = ["eip155:1", "eip155:10", "eip155:42161"]
const accounts = ["eip155:1:0x1", "eip155:1:0x2"]
const methods = ["eth_sendTransaction", "eth_sign", "eth_signTypedData", "eth_signTypedData_v4", "personal_sign"]
componentUnderTest.sdk.sessionAuthenticateRequest(request)
componentUnderTest.connectionApproved(request.id, { eip155: { key, chains, accounts, methods }})
compare(componentUnderTest.registerSignRequestSpy.count, 1)
const requestObj = componentUnderTest.registerSignRequestSpy.signalArguments[0][0]
requestObj.execute("pass", "pin")
componentUnderTest.store.signingResult(request.topic, request.id, "data")
tryCompare(componentUnderTest.siweSuccessfulSpy, "count", 1)
}
function test_RejectNewRequest() {
const request = buildSiweRequestMessage()
const key = request.id
componentUnderTest.sdk.sessionAuthenticateRequest(request)
componentUnderTest.connectionRejected(request.id)
compare(componentUnderTest.unregisterSignRequestSpy.count, 1)
tryCompare(componentUnderTest.siweFailedSpy, "count", 1)
}
function test_ApproveRequestExpired() {
ignoreWarning(new RegExp(/^Error in SiweLifeCycle/))
const request = buildSiweRequestMessage()
const key = request.id
request.params.expiryTimestamp = Date.now() / 1000 - 1
componentUnderTest.sdk.sessionAuthenticateRequest(request)
compare(componentUnderTest.registerSignRequestSpy.count, 0)
tryCompare(componentUnderTest.siweFailedSpy, "count", 1)
}
function test_ApproveWrongKey() {
const request = buildSiweRequestMessage()
const key = request.id
const chains = ["eip155:1", "eip155:10", "eip155:42161"]
const accounts = ["eip155:1:0x1", "eip155:1:0x2"]
const methods = ["eth_sendTransaction", "eth_sign", "eth_signTypedData", "eth_signTypedData_v4", "personal_sign"]
componentUnderTest.sdk.sessionAuthenticateRequest(request)
const ok = componentUnderTest.connectionApproved("wrongKey", { eip155: { key, chains, accounts, methods }})
compare(ok, false)
}
function test_RejectWrongKey() {
const request = buildSiweRequestMessage()
const key = request.id
componentUnderTest.sdk.sessionAuthenticateRequest(request)
const ok = componentUnderTest.connectionRejected("wrongKey")
compare(ok, false)
}
function test_DoubleRequests() {
ignoreWarning(new RegExp(/^Error in SiweRequestPlugin/))
const request = buildSiweRequestMessage()
const key = request.id
const chains = ["eip155:1", "eip155:10", "eip155:42161"]
const accounts = ["eip155:1:0x1", "eip155:1:0x2"]
const methods = ["eth_sendTransaction", "eth_sign", "eth_signTypedData", "eth_signTypedData_v4", "personal_sign"]
componentUnderTest.sdk.sessionAuthenticateRequest(request)
componentUnderTest.sdk.sessionAuthenticateRequest(request)
componentUnderTest.connectionApproved(request.id, { eip155: { chains, accounts, methods }})
compare(componentUnderTest.registerSignRequestSpy.count, 1)
}
function test_InvalidRequest() {
ignoreWarning(new RegExp(/^Error in SiweRequestPlugin/))
const request = {"someRandomData": ""}
const chains = ["eip155:1", "eip155:10", "eip155:42161"]
const accounts = ["eip155:1:0x1", "eip155:1:0x2"]
const methods = ["eth_sendTransaction", "eth_sign", "eth_signTypedData", "eth_signTypedData_v4", "personal_sign"]
componentUnderTest.sdk.sessionAuthenticateRequest(request)
compare(componentUnderTest.registerSignRequestSpy.count, 0)
}
}
}

View File

@ -18,7 +18,8 @@ QObject {
readonly property int connectorId: Constants.WalletConnect readonly property int connectorId: Constants.WalletConnect
readonly property var dappsModel: d.dappsModel readonly property var dappsModel: d.dappsModel
function updateDapps() { Component.onCompleted: {
// Just in case the SDK is already initialized
d.updateDappsModel() d.updateDappsModel()
} }
@ -30,6 +31,29 @@ QObject {
objectName: "DAppsModel" objectName: "DAppsModel"
} }
property Connections sdkConnections: Connections {
target: root.sdk
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()

View File

@ -1,6 +1,7 @@
import QtQuick 2.15 import QtQuick 2.15
import AppLayouts.Wallet.services.dapps 1.0 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.services.dapps.types 1.0
import AppLayouts.Wallet.stores 1.0 as WalletStore import AppLayouts.Wallet.stores 1.0 as WalletStore
@ -9,8 +10,6 @@ import StatusQ.Core.Utils 0.1 as SQUtils
import shared.stores 1.0 import shared.stores 1.0
import utils 1.0 import utils 1.0
import "types"
SQUtils.QObject { SQUtils.QObject {
id: root id: root
@ -23,28 +22,222 @@ SQUtils.QObject {
property alias requestsModel: requests property alias requestsModel: requests
function rejectSessionRequest(topic, id, hasError) {
d.unsubscribeForFeeUpdates(topic, id)
sdk.rejectSessionRequest(topic, id, hasError)
}
function subscribeForFeeUpdates(topic, id) { function subscribeForFeeUpdates(topic, id) {
d.subscribeForFeeUpdates(topic, id) d.subscribeForFeeUpdates(topic, id)
} }
/// Beware, it will fail if called multiple times before getting an answer function pair(uri) {
function authenticate(topic, id, address, payload) { return sdk.pair(uri)
d.unsubscribeForFeeUpdates(topic, id)
return store.authenticateUser(topic, id, address, payload)
} }
signal sessionRequest(string id) /// Approves or rejects the session proposal
function approvePairSession(key, approvedChainIds, accountAddress) {
const approvedNamespaces = JSON.parse(
DAppsHelpers.buildSupportedNamespaces(approvedChainIds,
[accountAddress],
SessionRequest.getSupportedMethods())
)
if (siwePlugin.connectionApproved(key, approvedNamespaces)) {
return
}
if (!d.activeProposals.has(key)) {
console.error("No active proposal found for key: " + key)
return
}
const proposal = d.activeProposals.get(key)
d.acceptedSessionProposal = proposal
d.acceptedNamespaces = approvedNamespaces
sdk.buildApprovedNamespaces(key, proposal.params, approvedNamespaces)
}
/// Rejects the session proposal
function rejectPairSession(id) {
if (siwePlugin.connectionRejected(id)) {
return
}
sdk.rejectSession(id)
}
/// Disconnects the WC session with the given topic
function disconnectSession(sessionTopic) {
wcSDK.disconnectSession(sessionTopic)
}
function validatePairingUri(uri){
const info = DAppsHelpers.extractInfoFromPairUri(uri)
sdk.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)
});
}
signal sessionRequest(var id)
/*type - maps to Constants.ephemeralNotificationType*/ /*type - maps to Constants.ephemeralNotificationType*/
signal displayToastMessage(string message, int type) signal displayToastMessage(string message, int type)
signal pairingValidated(int validationState)
signal pairingResponse(int state) // Maps to Pairing.errors
signal connectDApp(var chains, string dAppUrl, string dAppName, string dAppIcon, var key)
signal approveSessionResult(var proposalId, bool error, var topic)
signal dappDisconnected(var topic, string url, bool error)
Connections { Connections {
target: sdk target: sdk
function onRejectSessionResult(proposalId, err) {
if (!d.activeProposals.has(proposalId)) {
console.error("No active proposal found for key: " + proposalId)
return
}
const proposal = d.activeProposals.get(proposalId)
d.activeProposals.delete(proposalId)
const app_url = proposal.params.proposer.metadata.url ?? "-"
const app_domain = SQUtils.StringUtils.extractDomainFromLink(app_url)
if(err) {
root.pairingResponse(Pairing.errors.unknownError)
root.displayToastMessage(qsTr("Failed to reject connection request for %1").arg(app_domain), Constants.ephemeralNotificationType.danger)
} else {
root.displayToastMessage(qsTr("Connection request for %1 was rejected").arg(app_domain), Constants.ephemeralNotificationType.success)
}
}
function onApproveSessionResult(proposalId, session, err) {
if (!d.activeProposals.has(proposalId)) {
console.error("No active proposal found for key: " + proposalId)
return
}
if (!d.acceptedSessionProposal || d.acceptedSessionProposal.id !== proposalId) {
console.error("No accepted proposal found for key: " + proposalId)
d.activeProposals.delete(proposalId)
return
}
const proposal = d.activeProposals.get(proposalId)
d.activeProposals.delete(proposalId)
d.acceptedSessionProposal = null
d.acceptedNamespaces = null
if (err) {
root.pairingResponse(Pairing.errors.unknownError)
return
}
// TODO #14754: implement custom dApp notification
const app_url = proposal.params.proposer.metadata.url ?? "-"
const app_domain = SQUtils.StringUtils.extractDomainFromLink(app_url)
root.displayToastMessage(qsTr("Connected to %1 via WalletConnect").arg(app_domain), Constants.ephemeralNotificationType.success)
// Persist session
if(!root.store.addWalletConnectSession(JSON.stringify(session))) {
console.error("Failed to persist session")
}
// Notify client
root.approveSessionResult(proposalId, err, session.topic)
}
function onBuildApprovedNamespacesResult(key, approvedNamespaces, error) {
if (!d.activeProposals.has(key)) {
console.error("No active proposal found for key: " + key)
return
}
if(error || !approvedNamespaces) {
// Check that it contains Non conforming namespaces"
if (error.includes("Non conforming namespaces")) {
root.pairingResponse(Pairing.errors.unsupportedNetwork)
} else {
root.pairingResponse(Pairing.errors.unknownError)
}
return
}
approvedNamespaces = applyChainAgnosticFix(approvedNamespaces)
if (d.acceptedSessionProposal) {
sdk.approveSession(d.acceptedSessionProposal, approvedNamespaces)
} else {
const proposal = d.activeProposals.get(key)
const res = DAppsHelpers.extractChainsAndAccountsFromApprovedNamespaces(approvedNamespaces)
const chains = res.chains
const dAppUrl = proposal.params.proposer.metadata.url
const dAppName = proposal.params.proposer.metadata.name
const dAppIcons = proposal.params.proposer.metadata.icons
const dAppIcon = dAppIcons && dAppIcons.length > 0 ? dAppIcons[0] : ""
root.connectDApp(chains, dAppUrl, dAppName, dAppIcon, key)
}
}
//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
}
function onSessionProposal(sessionProposal) {
const key = sessionProposal.id
d.activeProposals.set(key, sessionProposal)
const supportedNamespacesStr = DAppsHelpers.buildSupportedNamespacesFromModels(
root.networksModel, root.accountsModel, SessionRequest.getSupportedMethods())
sdk.buildApprovedNamespaces(key, sessionProposal.params, JSON.parse(supportedNamespacesStr))
}
function onPairResponse(ok) {
root.pairingResponse(ok)
}
function onSessionRequestEvent(event) { function onSessionRequestEvent(event) {
const res = d.resolveAsync(event) const res = d.resolveAsync(event)
if (res.code == d.resolveAsyncResult.error) { if (res.code == d.resolveAsyncResult.error) {
@ -83,7 +276,7 @@ SQUtils.QObject {
if (error) { if (error) {
root.displayToastMessage(qsTr("Fail to %1 from %2").arg(methodStr).arg(appDomain), Constants.ephemeralNotificationType.danger) root.displayToastMessage(qsTr("Fail to %1 from %2").arg(methodStr).arg(appDomain), Constants.ephemeralNotificationType.danger)
root.rejectSessionRequest(topic, id, true /*hasError*/) sdk.rejectSessionRequest(topic, id, true /*hasError*/)
console.error(`Error accepting session request for topic: ${topic}, id: ${id}, accept: ${accept}, error: ${error}`) console.error(`Error accepting session request for topic: ${topic}, id: ${id}, accept: ${accept}, error: ${error}`)
return return
} }
@ -112,52 +305,56 @@ SQUtils.QObject {
request.setExpired() request.setExpired()
} }
function onSessionDelete(topic, err) {
d.disconnectSessionRequested(topic, err)
}
} }
Connections { SiweRequestPlugin {
target: root.store id: siwePlugin
function onUserAuthenticated(topic, id, password, pin, payload) { sdk: root.sdk
var request = requests.findRequest(topic, id) store: root.store
accountsModel: root.accountsModel
networksModel: root.networksModel
onRegisterSignRequest: (request) => {
requests.enqueue(request)
}
onUnregisterSignRequest: (requestId) => {
const request = requests.findById(requestId)
if (request === null) { if (request === null) {
console.error("Error finding event for topic", topic, "id", id) console.error("SiweRequestPlugin::onUnregisterSignRequest: Error finding event for requestId", requestId)
return return
} }
if (request.isExpired()) { requests.removeRequest(request.topic, requestId)
console.warn("Error: request expired")
root.rejectSessionRequest(topic, id, true /*hasError*/)
return
} }
d.executeSessionRequest(request, password, pin, payload) onConnectDApp: (chains, dAppUrl, dAppName, dAppIcon, key) => {
root.connectDApp(chains, dAppUrl, dAppName, dAppIcon, key)
} }
function onUserAuthenticationFailed(topic, id) { onSiweFailed: (id, error, topic) => {
let request = requests.findRequest(topic, id) root.approveSessionResult(id, error, topic)
let methodStr = SessionRequest.methodToUserString(request.method)
if (request === null || !methodStr) {
return
} }
if (request.isExpired()) { onSiweSuccessful: (id, topic) => {
console.warn("Error: request expired") d.lookupSession(topic, function(session) {
root.rejectSessionRequest(topic, id, true /*hasError*/) // TODO #14754: implement custom dApp notification
return let meta = session.peer.metadata
const dappUrl = meta.url ?? "-"
const dappDomain = SQUtils.StringUtils.extractDomainFromLink(dappUrl)
root.displayToastMessage(qsTr("Connected to %1 via WalletConnect").arg(dappDomain), Constants.ephemeralNotificationType.success)
// Persist session
if(!root.store.addWalletConnectSession(JSON.stringify(session))) {
console.error("Failed to persist session")
} }
const appDomain = SQUtils.StringUtils.extractDomainFromLink(request.dappUrl) root.approveSessionResult(id, "", topic)
root.displayToastMessage(qsTr("Failed to authenticate %1 from %2").arg(methodStr).arg(appDomain), Constants.ephemeralNotificationType.danger) })
root.rejectSessionRequest(topic, id, true /*hasError*/)
}
function onSigningResult(topic, id, data) {
let hasErrors = (data == "")
if (!hasErrors) {
// acceptSessionRequest will trigger an sdk.sessionRequestUserAnswerResult signal
sdk.acceptSessionRequest(topic, id, data)
} else {
root.rejectSessionRequest(topic, id, hasErrors)
}
} }
} }
@ -272,6 +469,10 @@ SQUtils.QObject {
readonly property int ignored: 2 readonly property int ignored: 2
} }
property var activeProposals: new Map() // key: proposalId, value: sessionProposal
property var acceptedSessionProposal: null
property var acceptedNamespaces: null
// returns { // returns {
// obj: obj or nil // obj: obj or nil
// code: resolveAsyncResult codes // code: resolveAsyncResult codes
@ -845,6 +1046,44 @@ SQUtils.QObject {
} }
return BigOps.fromNumber(0) return BigOps.fromNumber(0)
} }
function disconnectSessionRequested(topic, err) {
// Get all sessions and filter the active ones for known accounts
// Act on the first matching session with the same topic
const activeSessionsCallback = (allSessions, success) => {
root.store.activeSessionsReceived.disconnect(activeSessionsCallback)
if (!success) {
// TODO #14754: implement custom dApp notification
root.dappDisconnected("", "", true)
return
}
// Convert to original format
const webSdkSessions = allSessions.map((session) => {
return JSON.parse(session.sessionJson)
})
const sessions = DAppsHelpers.filterActiveSessionsForKnownAccounts(webSdkSessions, root.accountsModel)
for (const sessionID in sessions) {
const session = sessions[sessionID]
if (session.topic == topic) {
root.store.deactivateWalletConnectSession(topic)
const dappUrl = session.peer.metadata.url ?? "-"
root.dappDisconnected(topic, dappUrl, err)
break
}
}
}
root.store.activeSessionsReceived.connect(activeSessionsCallback)
if (!root.store.getActiveSessions()) {
root.store.activeSessionsReceived.disconnect(activeSessionsCallback)
// TODO #14754: implement custom dApp notification
}
}
} }
/// The queue is used to ensure that the events are processed in the order they are received but they could be /// The queue is used to ensure that the events are processed in the order they are received but they could be
@ -856,8 +1095,58 @@ SQUtils.QObject {
Component { Component {
id: sessionRequestComponent id: sessionRequestComponent
SessionRequestResolved { SessionRequestWithAuth {
id: request
sourceId: Constants.DAppConnectors.WalletConnect sourceId: Constants.DAppConnectors.WalletConnect
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) {
// acceptSessionRequest will trigger an sdk.sessionRequestUserAnswerResult signal
sdk.acceptSessionRequest(topic, id, data)
} else {
request.reject(true)
}
}
onAccepted: () => {
d.unsubscribeForFeeUpdates(request.topic, request.requestId)
}
onRejected: (hasError) => {
d.unsubscribeForFeeUpdates(request.topic, request.requestId)
sdk.rejectSessionRequest(request.topic, request.requestId, hasError)
}
onAuthFailed: () => {
const appDomain = SQUtils.StringUtils.extractDomainFromLink(request.dappUrl)
const methodStr = SessionRequest.methodToUserString(request.method)
if (!methodStr) {
return
}
root.displayToastMessage(qsTr("Failed to authenticate %1 from %2").arg(methodStr).arg(appDomain), Constants.ephemeralNotificationType.danger)
}
onExecute: (password, pin) => {
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) {
sdk.rejectSessionRequest(request.topic, request.requestId, true /*hasError*/)
root.store.signingResult.disconnect(request.signedHandler)
}
}
} }
} }

View File

@ -56,11 +56,11 @@ QObject {
function sign(topic, id) { function sign(topic, id) {
// The authentication triggers the signing process // The authentication triggers the signing process
// authenticate -> sign -> inform the dApp // authenticate -> sign -> inform the dApp
d.authenticate(topic, id) d.sign(topic, id)
} }
function rejectSign(topic, id, hasError) { function rejectSign(topic, id, hasError) {
requestHandler.rejectSessionRequest(topic, id, hasError) d.rejectSign(topic, id, hasError)
} }
function subscribeForFeeUpdates(topic, id) { function subscribeForFeeUpdates(topic, id) {
@ -75,29 +75,18 @@ QObject {
/// Initiates the pairing process with the given URI /// Initiates the pairing process with the given URI
function pair(uri) { function pair(uri) {
timeoutTimer.start() timeoutTimer.start()
wcSDK.pair(uri) requestHandler.pair(uri)
} }
/// Approves or rejects the session proposal /// Approves or rejects the session proposal
function approvePairSession(key, approvedChainIds, accountAddress) { function approvePairSession(key, approvedChainIds, accountAddress) {
if (!d.activeProposals.has(key)) { requestHandler.approvePairSession(key, approvedChainIds, accountAddress)
console.error("No active proposal found for key: " + key)
return
} }
const proposal = d.activeProposals.get(key)
d.acceptedSessionProposal = proposal
const approvedNamespaces = JSON.parse(
DAppsHelpers.buildSupportedNamespaces(approvedChainIds,
[accountAddress],
SessionRequest.getSupportedMethods())
)
wcSDK.buildApprovedNamespaces(key, proposal.params, approvedNamespaces)
}
/// Rejects the session proposal /// Rejects the session proposal
function rejectPairSession(id) { function rejectPairSession(id) {
wcSDK.rejectSession(id) requestHandler.rejectPairSession(id)
} }
/// Disconnects the dApp with the given topic /// Disconnects the dApp with the given topic
@ -166,14 +155,6 @@ QObject {
} }
} }
property var activeProposals: new Map() // key: proposalId, value: sessionProposal
property var acceptedSessionProposal: null
/// Disconnects the WC session with the given topic
function disconnectSession(sessionTopic) {
wcSDK.disconnectSession(sessionTopic)
}
function disconnectDapp(topic) { function disconnectDapp(topic) {
const dApp = d.getDAppByTopic(topic) const dApp = d.getDAppByTopic(topic)
if (!dApp) { if (!dApp) {
@ -196,7 +177,7 @@ QObject {
if (dApp.connectorId === dappsProvider.connectorId) { if (dApp.connectorId === dappsProvider.connectorId) {
// Currently disconnect acts on all sessions! // Currently disconnect acts on all sessions!
for (let i = 0; i < dApp.sessions.ModelCount.count; i++) { for (let i = 0; i < dApp.sessions.ModelCount.count; i++) {
d.disconnectSession(dApp.sessions.get(i).topic) requestHandler.disconnectSession(dApp.sessions.get(i).topic)
} }
} }
} }
@ -211,36 +192,25 @@ QObject {
return return
} }
const info = DAppsHelpers.extractInfoFromPairUri(uri) requestHandler.validatePairingUri(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 function sign(topic, id) {
if (validationState === Pairing.errors.uriOk) {
const now = (new Date().getTime())/1000
if (info.expiry < now) {
validationState = Pairing.errors.expired
}
}
root.pairingValidated(validationState)
});
}
function authenticate(topic, id) {
const request = sessionRequestsModel.findRequest(topic, id) const request = sessionRequestsModel.findRequest(topic, id)
if (!request) { if (!request) {
console.error("Session request not found") console.error("Session request not found")
return return
} }
requestHandler.authenticate(topic, id, request.accountAddress, request.feesInfo) request.accept()
}
function rejectSign(topic, id, hasError) {
const request = sessionRequestsModel.findRequest(topic, id)
if (!request) {
console.error("Session request not found")
return
}
request.reject(hasError)
} }
function reportPairErrorState(state) { function reportPairErrorState(state) {
@ -248,45 +218,6 @@ QObject {
root.pairingValidated(state) root.pairingValidated(state)
} }
function disconnectSessionRequested(topic, err) {
// Get all sessions and filter the active ones for known accounts
// Act on the first matching session with the same topic
const activeSessionsCallback = (allSessions, success) => {
store.activeSessionsReceived.disconnect(activeSessionsCallback)
if (!success) {
// TODO #14754: implement custom dApp notification
d.notifyDappDisconnect("-", true)
return
}
// Convert to original format
const webSdkSessions = allSessions.map((session) => {
return JSON.parse(session.sessionJson)
})
const sessions = DAppsHelpers.filterActiveSessionsForKnownAccounts(webSdkSessions, root.validAccounts)
for (const sessionID in sessions) {
const session = sessions[sessionID]
if (session.topic === topic) {
store.deactivateWalletConnectSession(topic)
dappsProvider.updateDapps()
const dappUrl = session.peer.metadata.url ?? "-"
d.notifyDappDisconnect(dappUrl, err)
break
}
}
}
store.activeSessionsReceived.connect(activeSessionsCallback)
if (!store.getActiveSessions()) {
store.activeSessionsReceived.disconnect(activeSessionsCallback)
// TODO #14754: implement custom dApp notification
}
}
function notifyDappDisconnect(dappUrl, err) { function notifyDappDisconnect(dappUrl, err) {
const appDomain = StringUtils.extractDomainFromLink(dappUrl) const appDomain = StringUtils.extractDomainFromLink(dappUrl)
if(err) { if(err) {
@ -313,125 +244,6 @@ QObject {
} }
} }
Connections {
target: wcSDK
function onPairResponse(ok) {
if (!ok) {
d.reportPairErrorState(Pairing.errors.unknownError)
} // else waiting for onSessionProposal
}
function onSessionProposal(sessionProposal) {
const key = sessionProposal.id
d.activeProposals.set(key, sessionProposal)
const supportedNamespacesStr = DAppsHelpers.buildSupportedNamespacesFromModels(
root.flatNetworks, root.validAccounts, SessionRequest.getSupportedMethods())
wcSDK.buildApprovedNamespaces(key, sessionProposal.params, JSON.parse(supportedNamespacesStr))
}
function onBuildApprovedNamespacesResult(key, approvedNamespaces, error) {
if (!d.activeProposals.has(key)) {
console.error("No active proposal found for key: " + key)
return
}
if(error || !approvedNamespaces) {
// Check that it contains Non conforming namespaces"
if (error.includes("Non conforming namespaces")) {
d.reportPairErrorState(Pairing.errors.unsupportedNetwork)
} else {
d.reportPairErrorState(Pairing.errors.unknownError)
}
return
}
const an = approvedNamespaces.eip155
if (!(an.accounts) || an.accounts.length === 0 || (!(an.chains) || an.chains.length === 0)) {
d.reportPairErrorState(Pairing.errors.unsupportedNetwork)
return
}
if (d.acceptedSessionProposal) {
wcSDK.approveSession(d.acceptedSessionProposal, approvedNamespaces)
} else {
const proposal = d.activeProposals.get(key)
const res = DAppsHelpers.extractChainsAndAccountsFromApprovedNamespaces(approvedNamespaces)
const chains = res.chains
const dAppUrl = proposal.params.proposer.metadata.url
const dAppName = proposal.params.proposer.metadata.name
const dAppIcons = proposal.params.proposer.metadata.icons
const dAppIcon = dAppIcons && dAppIcons.length > 0 ? dAppIcons[0] : ""
root.connectDApp(chains, dAppUrl, dAppName, dAppIcon, key)
}
}
function onApproveSessionResult(proposalId, session, err) {
if (!d.activeProposals.has(proposalId)) {
console.error("No active proposal found for key: " + proposalId)
return
}
if (!d.acceptedSessionProposal || d.acceptedSessionProposal.id !== proposalId) {
console.error("No accepted proposal found for key: " + proposalId)
d.activeProposals.delete(proposalId)
return
}
const proposal = d.activeProposals.get(proposalId)
d.activeProposals.delete(proposalId)
d.acceptedSessionProposal = null
if (err) {
d.reportPairErrorState(Pairing.errors.unknownError)
return
}
// TODO #14754: implement custom dApp notification
const app_url = proposal.params.proposer.metadata.url ?? "-"
const app_domain = StringUtils.extractDomainFromLink(app_url)
root.displayToastMessage(qsTr("Connected to %1 via WalletConnect").arg(app_domain), Constants.ephemeralNotificationType.success)
// Persist session
if(!store.addWalletConnectSession(JSON.stringify(session))) {
console.error("Failed to persist session")
}
// Notify client
root.approveSessionResult(proposalId, err, session.topic)
dappsProvider.updateDapps()
}
function onRejectSessionResult(proposalId, err) {
if (!d.activeProposals.has(proposalId)) {
console.error("No active proposal found for key: " + proposalId)
return
}
const proposal = d.activeProposals.get(proposalId)
d.activeProposals.delete(proposalId)
const app_url = proposal.params.proposer.metadata.url ?? "-"
const app_domain = StringUtils.extractDomainFromLink(app_url)
if(err) {
d.reportPairErrorState(Pairing.errors.unknownError)
root.displayToastMessage(qsTr("Failed to reject connection request for %1").arg(app_domain), Constants.ephemeralNotificationType.danger)
} else {
root.displayToastMessage(qsTr("Connection request for %1 was rejected").arg(app_domain), Constants.ephemeralNotificationType.success)
}
}
function onSessionDelete(topic, err) {
d.disconnectSessionRequested(topic, err)
}
}
Component.onCompleted: {
dappsProvider.updateDapps()
}
DAppsRequestHandler { DAppsRequestHandler {
id: requestHandler id: requestHandler
@ -449,6 +261,24 @@ QObject {
onDisplayToastMessage: (message, type) => { onDisplayToastMessage: (message, type) => {
root.displayToastMessage(message, type) root.displayToastMessage(message, type)
} }
onPairingResponse: (state) => {
if (state != Pairing.errors.uriOk) {
d.reportPairErrorState(state)
}
}
onConnectDApp: (dappChains, dappUrl, dappName, dappIcon, key) => {
root.connectDApp(dappChains, dappUrl, dappName, dappIcon, key)
}
onApproveSessionResult: (key, error, topic) => {
root.approveSessionResult(key, error, topic)
}
onPairingValidated: (validationState) => {
timeoutTimer.stop()
root.pairingValidated(validationState)
}
onDappDisconnected: (_, dappUrl, err) => {
d.notifyDappDisconnect(dappUrl, err)
}
} }
DAppsListProvider { DAppsListProvider {

View File

@ -0,0 +1,320 @@
import QtQuick 2.15
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
SQUtils.QObject {
id: root
// The SIWE lifecycle is a state machine that handles the lifecycle of a SIWE request
// Steps:
// 1. Make sure we have session approvals from the user
// 2. Populate the auth payload
// 3. Format the auth message
// 4. Present the formatted auth message to the user
// 5. Sign the auth message
required property WalletConnectSDKBase sdk
// Store object expected with sign and autheticate methods and signals
required property DAppsStore store
// JSON object received from WC
// We're interested in the following properties:
// {
// topic,
// params: {
// requester: {
// metadata: {
// name,
// url,
// icons: [url]
// }
// },
// authPayload: {
// chains: [chainIds]
// },
// expiryTimestamp
// },
// id
// }
required property var request
// Account model with the following roles:
// - address
required property var accountsModel
// Networks model with the following roles:
// - chainId
required property var networksModel
// Signals the starting of the lifecycle
signal started()
// Signals the end of the lifecycle
signal finished(string error)
// Request session approval from the user
// This request should provide the approved chains, accounts and methods the dApp can use
signal requestSessionApproval(var chains, string dAppUrl, string dAppName, string dAppIcon, var key)
// Register a SessionRequestResolved object to be presented to the user for signing
signal registerSignRequest(var request)
// Unregister the SessionRequestResolved object
signal unregisterSignRequest(var id)
onFinished: {
if (!request.requestId) {
return
}
sdkConnections.enabled = false
root.unregisterSignRequest(request.requestId)
}
function start() {
d.start()
}
// Session approved provides the approved namespaces containing the chains, accounts and methods
function sessionApproved(key, approvedNamespaces) {
if (root.request.id != key) {
return false
}
d.sessionApproved(key, approvedNamespaces)
}
// Session rejected by the user
function sessionRejected(key) {
if (root.request.id != key) {
return
}
root.finished("Session rejected")
}
Connections {
id: sdkConnections
target: root.sdk
enabled: false
//Third step: format auth message
function onPopulateAuthPayloadResult(id, authPayload, error) {
try {
if (root.request.id != id) {
return
}
if (error || !authPayload) {
root.finished(error)
return
}
const iss = request.approvedNamespaces.eip155.accounts[0]
request.authPayload = authPayload
sdk.formatAuthMessage(id, authPayload, iss)
} catch (e) {
console.warn("Error in SiweLifeCycle::onPopulateAuthPayloadResult", e)
root.finished(e)
}
}
//Fourth step: present formatted auth message to user
function onFormatAuthMessageResult(id, authData, error) {
try {
if (root.request.id != id) {
return
}
if (error || !authData) {
root.finished(error)
return
}
request.preparedData = authData
root.registerSignRequest(request)
} catch (e) {
console.warn("Error in SiweLifeCycle::onFormatAuthMessageResult", e)
root.finished(e)
}
}
function onAcceptSessionAuthenticateResult(id, result, error) {
if (root.request.id != id) {
return
}
if (error || !result) {
root.finished(error)
return
}
root.finished("")
}
}
// Request object to be used for signing
// The data on this object is filled step by step with the sdk callbacks
// After user has signed the data, the request is sent to the sdk for further processing
SessionRequestWithAuth {
id: request
property var approvedNamespaces
property var authPayload
property var extractedChainsAndAccounts: approvedNamespaces ?
DAppsHelpers.extractChainsAndAccountsFromApprovedNamespaces(approvedNamespaces) :
{ chains: [], accounts: [] }
sourceId: Constants.DAppConnectors.WalletConnect
store: root.store
dappUrl: root.request.params.requester.metadata.url
dappName: root.request.params.requester.metadata.name
dappIcon: root.request.params.requester.metadata.icons && root.request.params.requester.metadata.icons.length > 0 ?
root.request.params.requester.metadata.icons[0] : ""
requestId: root.request.id
accountAddress: extractedChainsAndAccounts.accounts[0] ?? ""
chainId: extractedChainsAndAccounts.chains[0] ?? ""
method: SessionRequest.methods.personalSign.name
event: root.request
topic: root.request.topic
data: ""
preparedData: ""
maxFeesText: "?"
maxFeesEthText: "?"
expirationTimestamp: root.request.params.expiryTimestamp
function onBuildAuthenticationObjectResult(id, authObject, error) {
if (id != request.requestId) {
return
}
try {
if (error) {
root.finished(error)
return
}
sdk.buildAuthObjectResult.disconnect(request.onBuildAuthenticationObjectResult)
if (error) {
request.reject(true)
}
sdk.acceptSessionAuthenticate(id, [authObject])
} catch (e) {
console.warn("Error in SiweLifeCycle::onBuildAuthenticationObjectResult", e)
root.finished(e)
}
}
function signedHandler(topic, id, data) {
if (topic != request.topic || id != request.requestId) {
return
}
try {
root.store.signingResult.disconnect(request.signedHandler)
let hasErrors = (data == "")
if (hasErrors) {
request.reject(true)
}
sdk.buildAuthObjectResult.connect(request.onBuildAuthenticationObjectResult)
sdk.buildAuthObject(id, request.authPayload, data, request.approvedNamespaces.eip155.accounts[0])
} catch (e) {
console.warn("Error in SiweLifeCycle::signedHandler", e)
root.finished(e)
}
}
onRejected: (hasError) => {
try {
sdk.rejectSessionAuthenticate(request.requestId, hasError)
root.finished("Signing rejected")
} catch (e) {
console.warn("Error in SiweLifeCycle::onRejected", e)
root.finished(e)
}
}
onAuthFailed: () => {
try {
const appDomain = SQUtils.StringUtils.extractDomainFromLink(request.dappUrl)
const methodStr = SessionRequest.methodToUserString(request.method)
if (!methodStr) {
return
}
root.finished(qsTr("Failed to authenticate %1 from %2").arg(methodStr).arg(appDomain))
} catch (e) {
console.warn("Error in SiweLifeCycle::onAuthFailed", e)
root.finished(e)
}
}
onExecute: (password, pin) => {
try {
root.store.signingResult.connect(request.signedHandler)
root.store.signMessage(request.topic, request.requestId, request.accountAddress, request.preparedData, password, pin)
} catch (e) {
console.warn("Error in SiweLifeCycle::onExecute", e)
root.finished(e)
}
}
}
QtObject {
id: d
function start() {
if (request.isExpired()) {
console.warn("Error in SiweLifeCycle", "Request expired")
root.finished("Request expired")
return
}
if (!root.request.id || !root.request.topic) {
console.warn("Error in SiweLifeCycle", "Invalid request")
root.finished("Invalid request")
return
}
sdkConnections.enabled = true
root.started()
// First step: make sure we have session approvals
sdk.getActiveSessions((allSessionsAllProfiles) => {
try {
const sessions = DAppsHelpers.filterActiveSessionsForKnownAccounts(allSessionsAllProfiles, root.accountsModel)
for (const topic in sessions) {
if (topic == root.request.pairingTopic || topic == root.request.topic) {
//TODO: In theory it's possible for a dApp to request SIWE after establishing a session
// This is how MetaMask handles it, but for WC connections it's not clear yet if it's possible
console.warn("Session already exists for request", root.request.id)
root.finished("")
return
}
}
const key = root.request.id
const chains = root.request.params.authPayload.chains.map(DAppsHelpers.chainIdFromEip155)
const dAppUrl = root.request.params.requester.metadata.url
const dAppName = root.request.params.requester.metadata.name
const dAppIcons = root.request.params.requester.metadata.icons
const dAppIcon = dAppIcons && dAppIcons.length > 0 ? dAppIcons[0] : ""
root.requestSessionApproval(chains, dAppUrl, dAppName, dAppIcon, key)
} catch (e) {
console.warn("Error in SiweLifeCycle", e)
root.finished(e)
}
})
}
//Second step: populate auth payload
function sessionApproved(key, approvedNamespaces) {
try {
request.approvedNamespaces = approvedNamespaces
const supportedChains = approvedNamespaces.eip155.chains
const supportedMethods = approvedNamespaces.eip155.methods
sdk.populateAuthPayload(request.requestId, root.request.params.authPayload, supportedChains, supportedMethods)
return true
} catch (e) {
console.warn("Error in SiweLifeCycle::sessionApproved", e)
root.finished(e)
}
return false
}
}
}

View File

@ -0,0 +1,142 @@
import QtQuick 2.15
import AppLayouts.Wallet.services.dapps 1.0
import StatusQ.Core.Utils 0.1 as SQUtils
import shared.stores 1.0
import utils 1.0
/*
SiweRequestPlugin is a plugin that listens for siwe requests and manages the lifecycle of the request.
*/
SQUtils.QObject {
id: root
required property WalletConnectSDKBase sdk
// Store object expected with sign and autheticate methods and signals
required property DAppsStore store
// Account model with the following roles:
// - address
required property var accountsModel
// Networks model with the following roles:
// - chainId
required property var networksModel
// Trigger a connection request to the dApp
// Expected `connectionApproved` to be called with the key and the approved namespaces
signal connectDApp(var chains, string dAppUrl, string dAppName, string dAppIcon, var key)
// Register a SessionRequestResolved object to be presented to the user for signing
signal registerSignRequest(var request)
// Unregister the SessionRequestResolved object
signal unregisterSignRequest(var requestId)
// Signal that the request was successful
signal siweSuccessful(var requestId, var topicId)
// Signal that the request failed
signal siweFailed(var requestId, string error, var topicId)
// return true if the request was found and approved
function connectionApproved(key, approvedNamespaces) {
const siweLifeCycle = d.getSiweLifeCycle(key)
if (!siweLifeCycle) {
return false
}
siweLifeCycle.sessionApproved(key, approvedNamespaces)
return true
}
// return true if the request was found and rejected
function connectionRejected(key) {
const siweLifeCycle = d.getSiweLifeCycle(key)
if (!siweLifeCycle) {
return false
}
siweLifeCycle.sessionRejected(key)
return true
}
Instantiator {
id: requestLifecycle
model: d.requests
// When a new request is added, we create a new SiweLifeCycle object that starts working on it
delegate: SiweLifeCycle {
required property var model
required property int index
sdk: root.sdk
store: root.store
accountsModel: root.accountsModel
networksModel: root.networksModel
request: model
onFinished: (error) => {
if (error) {
root.siweFailed(request.id, error, request.topic)
Qt.callLater(() => {
d.requests.remove(index, 1)
})
return
}
sdk.getActiveSessions((allSessions) => {
for (const topic in allSessions) {
if (allSessions[topic].pairingTopic != request.topic) {
continue
}
root.siweSuccessful(request.id, topic)
return
}
// No new session was created
// Should not happen
root.siweSuccessful(request.id, "")
})
}
onRequestSessionApproval: (chains, dAppUrl, dAppName, dAppIcon, key) => {
root.connectDApp(chains, dAppUrl, dAppName, dAppIcon, key)
}
onRegisterSignRequest: (request) => {
root.registerSignRequest(request)
}
onUnregisterSignRequest: (id) => {
root.unregisterSignRequest(id)
}
}
onObjectAdded: (_, obj) => {
obj.start()
}
}
Connections {
target: sdk
function onSessionAuthenticateRequest(sessionData) {
if (!sessionData || !sessionData.id) {
console.warn("Error in SiweRequestPlugin: Invalid session authenticate request", sessionData)
return
}
if (d.getSiweLifeCycle(sessionData.id)) {
console.warn("Error in SiweRequestPlugin: Session request already exists", sessionData.id)
return
}
d.requests.append(sessionData)
}
}
QtObject {
id: d
property ListModel requests: ListModel {}
function getSiweLifeCycle(requestId) {
for (let i = 0; i < requestLifecycle.count; i++) {
const siweLifeCycle = requestLifecycle.objectAt(i)
if (siweLifeCycle.request.id == requestId) {
return siweLifeCycle
}
}
return null
}
}
}

View File

@ -0,0 +1,2 @@
SiweRequestPlugin 1.0 SiweRequestPlugin.qml
SiweLifeCycle 1.0 SiweLifeCycle.qml

View File

@ -31,9 +31,9 @@ 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
readonly property alias dappName: d.dappName property alias dappName: d.dappName
readonly property alias dappUrl: d.dappUrl property alias dappUrl: d.dappUrl
readonly property alias dappIcon: d.dappIcon property alias dappIcon: d.dappIcon
/// extra data resolved from wallet /// extra data resolved from wallet
property string maxFeesText: "" property string maxFeesText: ""

View File

@ -0,0 +1,59 @@
import QtQuick 2.15
import shared.stores 1.0
SessionRequestResolved {
id: root
required property DAppsStore store
// Signal to execute the request. Emitted after successful authentication
// password and pin are the user's input for the authentication
signal execute(string password, string pin)
// Signal to reject the request. Emitted when the request is expired or rejected by the user
// hasError is true if the request was rejected due to an error
signal rejected(bool hasError)
// Signal when the authentication flow fails
signal authFailed()
signal accepted()
function accept() {
if (root.isExpired()) {
console.warn("Error: request expired")
root.reject(true)
return
}
storeConnections.enabled = true
store.authenticateUser(root.topic, root.requestId, root.accountAddress, "")
root.accepted()
}
function reject(hasError) {
storeConnections.enabled = false
root.rejected(hasError)
}
Connections {
id: storeConnections
enabled: false
target: root.store
function onUserAuthenticated(topic, id, password, pin, _) {
if (id == root.requestId && topic == root.topic) {
if (root.isExpired()) {
console.warn("Error: request expired")
root.reject(true)
return
}
root.execute(password, pin)
}
}
function onUserAuthenticationFailed(topic, id) {
if (id === root.requestId && topic === root.topic) {
storeConnections.enabled = false
root.authFailed()
}
}
}
}

View File

@ -1,4 +1,5 @@
SessionRequestResolved 1.0 SessionRequestResolved.qml SessionRequestResolved 1.0 SessionRequestResolved.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 Pairing 1.0 Pairing.qml singleton Pairing 1.0 Pairing.qml