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() {
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")
let store = handler.store
@ -532,6 +534,7 @@ Item {
verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest")
ignoreWarning("Error: request expired")
request.accept()
handler.store.userAuthenticated(topic, session.id, "1234", "", message)
verify(sdk.rejectSessionRequestCalls.length === 1, "expected a call to sdk.rejectSessionRequest")
sdk.sessionRequestUserAnswerResult(topic, session.id, false, "")
@ -558,6 +561,7 @@ Item {
verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest")
ignoreWarning("Error: request expired")
handler.requestsModel.findRequest(topic, session.id).accept()
handler.store.userAuthenticationFailed(topic, session.id)
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")
ignoreWarning("Error: request expired")
handler.requestsModel.findRequest(topic, session.id).accept()
handler.store.userAuthenticationFailed(topic, session.id)
verify(sdk.rejectSessionRequestCalls.length === 1, "expected a call to sdk.rejectSessionRequest")
}
@ -708,9 +713,8 @@ Item {
function test_TestPairingUnsupportedNetworks() {
const {sdk, walletStore, store} = testSetupPair(Testing.formatSessionProposal())
let allApprovedNamespaces = JSON.parse(Testing.formatBuildApprovedNamespacesResult([], []))
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(service.onPairingValidatedTriggers.length, 1, "expected a call to service.onPairingValidated")
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
function test_TestUpdateDapps() {
provider.updateDapps()
// Validate that persistance fallback is working
compare(provider.dappsModel.count, 2, "expected dappsModel have the right number of elements")
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 var dappsModel: d.dappsModel
function updateDapps() {
Component.onCompleted: {
// Just in case the SDK is already initialized
d.updateDappsModel()
}
@ -30,6 +31,29 @@ QObject {
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 getActiveSessionsFn: null
function updateDappsModel()

View File

@ -1,6 +1,7 @@
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
@ -9,8 +10,6 @@ import StatusQ.Core.Utils 0.1 as SQUtils
import shared.stores 1.0
import utils 1.0
import "types"
SQUtils.QObject {
id: root
@ -23,28 +22,222 @@ SQUtils.QObject {
property alias requestsModel: requests
function rejectSessionRequest(topic, id, hasError) {
d.unsubscribeForFeeUpdates(topic, id)
sdk.rejectSessionRequest(topic, id, hasError)
}
function subscribeForFeeUpdates(topic, id) {
d.subscribeForFeeUpdates(topic, id)
}
/// Beware, it will fail if called multiple times before getting an answer
function authenticate(topic, id, address, payload) {
d.unsubscribeForFeeUpdates(topic, id)
return store.authenticateUser(topic, id, address, payload)
function pair(uri) {
return sdk.pair(uri)
}
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*/
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 {
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) {
const res = d.resolveAsync(event)
if (res.code == d.resolveAsyncResult.error) {
@ -83,7 +276,7 @@ SQUtils.QObject {
if (error) {
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}`)
return
}
@ -112,52 +305,56 @@ SQUtils.QObject {
request.setExpired()
}
function onSessionDelete(topic, err) {
d.disconnectSessionRequested(topic, err)
}
}
Connections {
target: root.store
SiweRequestPlugin {
id: siwePlugin
function onUserAuthenticated(topic, id, password, pin, payload) {
var request = requests.findRequest(topic, id)
sdk: root.sdk
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("Error finding event for topic", topic, "id", id)
console.error("SiweRequestPlugin::onUnregisterSignRequest: Error finding event for requestId", requestId)
return
}
if (request.isExpired()) {
console.warn("Error: request expired")
root.rejectSessionRequest(topic, id, true /*hasError*/)
return
}
d.executeSessionRequest(request, password, pin, payload)
requests.removeRequest(request.topic, requestId)
}
function onUserAuthenticationFailed(topic, id) {
let request = requests.findRequest(topic, id)
let methodStr = SessionRequest.methodToUserString(request.method)
if (request === null || !methodStr) {
return
}
if (request.isExpired()) {
console.warn("Error: request expired")
root.rejectSessionRequest(topic, id, true /*hasError*/)
return
}
const appDomain = SQUtils.StringUtils.extractDomainFromLink(request.dappUrl)
root.displayToastMessage(qsTr("Failed to authenticate %1 from %2").arg(methodStr).arg(appDomain), Constants.ephemeralNotificationType.danger)
root.rejectSessionRequest(topic, id, true /*hasError*/)
onConnectDApp: (chains, dAppUrl, dAppName, dAppIcon, key) => {
root.connectDApp(chains, dAppUrl, dAppName, dAppIcon, key)
}
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)
}
onSiweFailed: (id, error, topic) => {
root.approveSessionResult(id, error, topic)
}
onSiweSuccessful: (id, topic) => {
d.lookupSession(topic, function(session) {
// TODO #14754: implement custom dApp notification
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")
}
root.approveSessionResult(id, "", topic)
})
}
}
@ -272,6 +469,10 @@ SQUtils.QObject {
readonly property int ignored: 2
}
property var activeProposals: new Map() // key: proposalId, value: sessionProposal
property var acceptedSessionProposal: null
property var acceptedNamespaces: null
// returns {
// obj: obj or nil
// code: resolveAsyncResult codes
@ -845,6 +1046,44 @@ SQUtils.QObject {
}
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
@ -856,8 +1095,58 @@ SQUtils.QObject {
Component {
id: sessionRequestComponent
SessionRequestResolved {
SessionRequestWithAuth {
id: request
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) {
// The authentication triggers the signing process
// authenticate -> sign -> inform the dApp
d.authenticate(topic, id)
d.sign(topic, id)
}
function rejectSign(topic, id, hasError) {
requestHandler.rejectSessionRequest(topic, id, hasError)
d.rejectSign(topic, id, hasError)
}
function subscribeForFeeUpdates(topic, id) {
@ -75,29 +75,18 @@ QObject {
/// Initiates the pairing process with the given URI
function pair(uri) {
timeoutTimer.start()
wcSDK.pair(uri)
requestHandler.pair(uri)
}
/// Approves or rejects the session proposal
function approvePairSession(key, approvedChainIds, accountAddress) {
if (!d.activeProposals.has(key)) {
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)
requestHandler.approvePairSession(key, approvedChainIds, accountAddress)
}
/// Rejects the session proposal
function rejectPairSession(id) {
wcSDK.rejectSession(id)
requestHandler.rejectPairSession(id)
}
/// 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) {
const dApp = d.getDAppByTopic(topic)
if (!dApp) {
@ -196,7 +177,7 @@ QObject {
if (dApp.connectorId === dappsProvider.connectorId) {
// Currently disconnect acts on all sessions!
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,81 +192,31 @@ QObject {
return
}
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)
});
requestHandler.validatePairingUri(uri)
}
function authenticate(topic, id) {
function sign(topic, id) {
const request = sessionRequestsModel.findRequest(topic, id)
if (!request) {
console.error("Session request not found")
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) {
timeoutTimer.stop()
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) {
const appDomain = StringUtils.extractDomainFromLink(dappUrl)
@ -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 {
id: requestHandler
@ -449,6 +261,24 @@ QObject {
onDisplayToastMessage: (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 {

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
required property var preparedData
readonly property alias dappName: d.dappName
readonly property alias dappUrl: d.dappUrl
readonly property alias dappIcon: d.dappIcon
property alias dappName: d.dappName
property alias dappUrl: d.dappUrl
property alias dappIcon: d.dappIcon
/// extra data resolved from wallet
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
SessionRequestWithAuth 1.0 SessionRequestWithAuth.qml
SessionRequestsModel 1.0 SessionRequestsModel.qml
singleton SessionRequest 1.0 SessionRequest.qml
singleton Pairing 1.0 Pairing.qml