feat(WC): Implementing SIWE flows
to squash.- implementing siwe flows
This commit is contained in:
parent
9d6840ef4a
commit
9d64cc1a57
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
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: dappsStoreComponent
|
||||
|
||||
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})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
TestCase {
|
||||
id: sessionRequestTest
|
||||
name: "SessionRequestWithAuth"
|
||||
// Ensure mocked GroupedAccountsAssetsModel is properly initialized
|
||||
when: windowShown
|
||||
|
||||
property SessionRequestWithAuth componentUnderTest: null
|
||||
|
||||
function init() {
|
||||
const store = createTemporaryObject(dappsStoreComponent, root)
|
||||
componentUnderTest = createTemporaryObject(sessionRequestComponent, root, { store })
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
requests.removeRequest(request.topic, requestId)
|
||||
}
|
||||
|
||||
d.executeSessionRequest(request, password, pin, payload)
|
||||
onConnectDApp: (chains, dAppUrl, dAppName, dAppIcon, key) => {
|
||||
root.connectDApp(chains, dAppUrl, dAppName, dAppIcon, key)
|
||||
}
|
||||
|
||||
function onUserAuthenticationFailed(topic, id) {
|
||||
let request = requests.findRequest(topic, id)
|
||||
let methodStr = SessionRequest.methodToUserString(request.method)
|
||||
if (request === null || !methodStr) {
|
||||
return
|
||||
onSiweFailed: (id, error, topic) => {
|
||||
root.approveSessionResult(id, error, topic)
|
||||
}
|
||||
|
||||
if (request.isExpired()) {
|
||||
console.warn("Error: request expired")
|
||||
root.rejectSessionRequest(topic, id, true /*hasError*/)
|
||||
return
|
||||
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")
|
||||
}
|
||||
|
||||
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*/)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
requestHandler.approvePairSession(key, approvedChainIds, accountAddress)
|
||||
}
|
||||
|
||||
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
|
||||
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,36 +192,25 @@ 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
|
||||
}
|
||||
requestHandler.validatePairingUri(uri)
|
||||
}
|
||||
|
||||
// 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)
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -248,45 +218,6 @@ QObject {
|
|||
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)
|
||||
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 {
|
||||
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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
SiweRequestPlugin 1.0 SiweRequestPlugin.qml
|
||||
SiweLifeCycle 1.0 SiweLifeCycle.qml
|
|
@ -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: ""
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
Loading…
Reference in New Issue