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() {
|
function test_TestAuthentication() {
|
||||||
let td = mockSessionRequestEvent(this, handler.sdk, handler.accountsModel, handler.networksModel)
|
let td = mockSessionRequestEvent(this, handler.sdk, handler.accountsModel, handler.networksModel)
|
||||||
handler.authenticate(td.request)
|
handler.sdk.sessionRequestEvent(td.request.event)
|
||||||
|
let request = handler.requestsModel.findById(td.request.requestId)
|
||||||
|
request.accept()
|
||||||
compare(handler.store.authenticateUserCalls.length, 1, "expected a call to store.authenticateUser")
|
compare(handler.store.authenticateUserCalls.length, 1, "expected a call to store.authenticateUser")
|
||||||
|
|
||||||
let store = handler.store
|
let store = handler.store
|
||||||
|
@ -532,6 +534,7 @@ Item {
|
||||||
verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest")
|
verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest")
|
||||||
|
|
||||||
ignoreWarning("Error: request expired")
|
ignoreWarning("Error: request expired")
|
||||||
|
request.accept()
|
||||||
handler.store.userAuthenticated(topic, session.id, "1234", "", message)
|
handler.store.userAuthenticated(topic, session.id, "1234", "", message)
|
||||||
verify(sdk.rejectSessionRequestCalls.length === 1, "expected a call to sdk.rejectSessionRequest")
|
verify(sdk.rejectSessionRequestCalls.length === 1, "expected a call to sdk.rejectSessionRequest")
|
||||||
sdk.sessionRequestUserAnswerResult(topic, session.id, false, "")
|
sdk.sessionRequestUserAnswerResult(topic, session.id, false, "")
|
||||||
|
@ -558,6 +561,7 @@ Item {
|
||||||
verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest")
|
verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest")
|
||||||
|
|
||||||
ignoreWarning("Error: request expired")
|
ignoreWarning("Error: request expired")
|
||||||
|
handler.requestsModel.findRequest(topic, session.id).accept()
|
||||||
handler.store.userAuthenticationFailed(topic, session.id)
|
handler.store.userAuthenticationFailed(topic, session.id)
|
||||||
verify(sdk.rejectSessionRequestCalls.length === 1, "expected a call to sdk.rejectSessionRequest")
|
verify(sdk.rejectSessionRequestCalls.length === 1, "expected a call to sdk.rejectSessionRequest")
|
||||||
}
|
}
|
||||||
|
@ -581,6 +585,7 @@ Item {
|
||||||
verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest")
|
verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest")
|
||||||
|
|
||||||
ignoreWarning("Error: request expired")
|
ignoreWarning("Error: request expired")
|
||||||
|
handler.requestsModel.findRequest(topic, session.id).accept()
|
||||||
handler.store.userAuthenticationFailed(topic, session.id)
|
handler.store.userAuthenticationFailed(topic, session.id)
|
||||||
verify(sdk.rejectSessionRequestCalls.length === 1, "expected a call to sdk.rejectSessionRequest")
|
verify(sdk.rejectSessionRequestCalls.length === 1, "expected a call to sdk.rejectSessionRequest")
|
||||||
}
|
}
|
||||||
|
@ -708,9 +713,8 @@ Item {
|
||||||
function test_TestPairingUnsupportedNetworks() {
|
function test_TestPairingUnsupportedNetworks() {
|
||||||
const {sdk, walletStore, store} = testSetupPair(Testing.formatSessionProposal())
|
const {sdk, walletStore, store} = testSetupPair(Testing.formatSessionProposal())
|
||||||
|
|
||||||
let allApprovedNamespaces = JSON.parse(Testing.formatBuildApprovedNamespacesResult([], []))
|
|
||||||
const approvedArgs = sdk.buildApprovedNamespacesCalls[0]
|
const approvedArgs = sdk.buildApprovedNamespacesCalls[0]
|
||||||
sdk.buildApprovedNamespacesResult(approvedArgs.id, allApprovedNamespaces, "")
|
sdk.buildApprovedNamespacesResult(approvedArgs.id, {}, "Non conforming namespaces. approve() namespaces chains don't satisfy required namespaces")
|
||||||
compare(connectDAppSpy.count, 0, "expected not to have calls to service.connectDApp")
|
compare(connectDAppSpy.count, 0, "expected not to have calls to service.connectDApp")
|
||||||
compare(service.onPairingValidatedTriggers.length, 1, "expected a call to service.onPairingValidated")
|
compare(service.onPairingValidatedTriggers.length, 1, "expected a call to service.onPairingValidated")
|
||||||
compare(service.onPairingValidatedTriggers[0].validationState, Pairing.errors.unsupportedNetwork, "expected unsupportedNetwork state error")
|
compare(service.onPairingValidatedTriggers[0].validationState, Pairing.errors.unsupportedNetwork, "expected unsupportedNetwork state error")
|
||||||
|
@ -794,8 +798,6 @@ Item {
|
||||||
|
|
||||||
// Implemented as a regression to metamask not having icons which failed dapps list
|
// Implemented as a regression to metamask not having icons which failed dapps list
|
||||||
function test_TestUpdateDapps() {
|
function test_TestUpdateDapps() {
|
||||||
provider.updateDapps()
|
|
||||||
|
|
||||||
// Validate that persistance fallback is working
|
// Validate that persistance fallback is working
|
||||||
compare(provider.dappsModel.count, 2, "expected dappsModel have the right number of elements")
|
compare(provider.dappsModel.count, 2, "expected dappsModel have the right number of elements")
|
||||||
let persistanceList = JSON.parse(dappsListReceivedJsonStr)
|
let persistanceList = JSON.parse(dappsListReceivedJsonStr)
|
||||||
|
|
|
@ -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 int connectorId: Constants.WalletConnect
|
||||||
readonly property var dappsModel: d.dappsModel
|
readonly property var dappsModel: d.dappsModel
|
||||||
|
|
||||||
function updateDapps() {
|
Component.onCompleted: {
|
||||||
|
// Just in case the SDK is already initialized
|
||||||
d.updateDappsModel()
|
d.updateDappsModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +31,29 @@ QObject {
|
||||||
objectName: "DAppsModel"
|
objectName: "DAppsModel"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property Connections sdkConnections: Connections {
|
||||||
|
target: root.sdk
|
||||||
|
function onSessionDelete(topic, err) {
|
||||||
|
d.updateDappsModel()
|
||||||
|
}
|
||||||
|
function onSdkInit(success, result) {
|
||||||
|
if (success) {
|
||||||
|
d.updateDappsModel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onApproveSessionResult(topic, success, result) {
|
||||||
|
if (success) {
|
||||||
|
d.updateDappsModel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAcceptSessionAuthenticateResult(id, result, error) {
|
||||||
|
if (!error) {
|
||||||
|
d.updateDappsModel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
property var dappsListReceivedFn: null
|
property var dappsListReceivedFn: null
|
||||||
property var getActiveSessionsFn: null
|
property var getActiveSessionsFn: null
|
||||||
function updateDappsModel()
|
function updateDappsModel()
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import QtQuick 2.15
|
import QtQuick 2.15
|
||||||
|
|
||||||
import AppLayouts.Wallet.services.dapps 1.0
|
import AppLayouts.Wallet.services.dapps 1.0
|
||||||
|
import AppLayouts.Wallet.services.dapps.plugins 1.0
|
||||||
import AppLayouts.Wallet.services.dapps.types 1.0
|
import AppLayouts.Wallet.services.dapps.types 1.0
|
||||||
import AppLayouts.Wallet.stores 1.0 as WalletStore
|
import AppLayouts.Wallet.stores 1.0 as WalletStore
|
||||||
|
|
||||||
|
@ -9,8 +10,6 @@ import StatusQ.Core.Utils 0.1 as SQUtils
|
||||||
import shared.stores 1.0
|
import shared.stores 1.0
|
||||||
import utils 1.0
|
import utils 1.0
|
||||||
|
|
||||||
import "types"
|
|
||||||
|
|
||||||
SQUtils.QObject {
|
SQUtils.QObject {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
@ -23,28 +22,222 @@ SQUtils.QObject {
|
||||||
|
|
||||||
property alias requestsModel: requests
|
property alias requestsModel: requests
|
||||||
|
|
||||||
function rejectSessionRequest(topic, id, hasError) {
|
|
||||||
d.unsubscribeForFeeUpdates(topic, id)
|
|
||||||
sdk.rejectSessionRequest(topic, id, hasError)
|
|
||||||
}
|
|
||||||
|
|
||||||
function subscribeForFeeUpdates(topic, id) {
|
function subscribeForFeeUpdates(topic, id) {
|
||||||
d.subscribeForFeeUpdates(topic, id)
|
d.subscribeForFeeUpdates(topic, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Beware, it will fail if called multiple times before getting an answer
|
function pair(uri) {
|
||||||
function authenticate(topic, id, address, payload) {
|
return sdk.pair(uri)
|
||||||
d.unsubscribeForFeeUpdates(topic, id)
|
|
||||||
return store.authenticateUser(topic, id, address, payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
signal sessionRequest(string id)
|
/// Approves or rejects the session proposal
|
||||||
|
function approvePairSession(key, approvedChainIds, accountAddress) {
|
||||||
|
const approvedNamespaces = JSON.parse(
|
||||||
|
DAppsHelpers.buildSupportedNamespaces(approvedChainIds,
|
||||||
|
[accountAddress],
|
||||||
|
SessionRequest.getSupportedMethods())
|
||||||
|
)
|
||||||
|
|
||||||
|
if (siwePlugin.connectionApproved(key, approvedNamespaces)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!d.activeProposals.has(key)) {
|
||||||
|
console.error("No active proposal found for key: " + key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const proposal = d.activeProposals.get(key)
|
||||||
|
d.acceptedSessionProposal = proposal
|
||||||
|
d.acceptedNamespaces = approvedNamespaces
|
||||||
|
|
||||||
|
sdk.buildApprovedNamespaces(key, proposal.params, approvedNamespaces)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rejects the session proposal
|
||||||
|
function rejectPairSession(id) {
|
||||||
|
if (siwePlugin.connectionRejected(id)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sdk.rejectSession(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disconnects the WC session with the given topic
|
||||||
|
function disconnectSession(sessionTopic) {
|
||||||
|
wcSDK.disconnectSession(sessionTopic)
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePairingUri(uri){
|
||||||
|
const info = DAppsHelpers.extractInfoFromPairUri(uri)
|
||||||
|
sdk.getActiveSessions((sessions) => {
|
||||||
|
// Check if the URI is already paired
|
||||||
|
let validationState = Pairing.errors.uriOk
|
||||||
|
for (const key in sessions) {
|
||||||
|
if (sessions[key].pairingTopic === info.topic) {
|
||||||
|
validationState = Pairing.errors.alreadyUsed
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if expired
|
||||||
|
if (validationState === Pairing.errors.uriOk) {
|
||||||
|
const now = (new Date().getTime())/1000
|
||||||
|
if (info.expiry < now) {
|
||||||
|
validationState = Pairing.errors.expired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root.pairingValidated(validationState)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
signal sessionRequest(var id)
|
||||||
/*type - maps to Constants.ephemeralNotificationType*/
|
/*type - maps to Constants.ephemeralNotificationType*/
|
||||||
signal displayToastMessage(string message, int type)
|
signal displayToastMessage(string message, int type)
|
||||||
|
signal pairingValidated(int validationState)
|
||||||
|
signal pairingResponse(int state) // Maps to Pairing.errors
|
||||||
|
signal connectDApp(var chains, string dAppUrl, string dAppName, string dAppIcon, var key)
|
||||||
|
signal approveSessionResult(var proposalId, bool error, var topic)
|
||||||
|
signal dappDisconnected(var topic, string url, bool error)
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: sdk
|
target: sdk
|
||||||
|
|
||||||
|
function onRejectSessionResult(proposalId, err) {
|
||||||
|
if (!d.activeProposals.has(proposalId)) {
|
||||||
|
console.error("No active proposal found for key: " + proposalId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const proposal = d.activeProposals.get(proposalId)
|
||||||
|
d.activeProposals.delete(proposalId)
|
||||||
|
|
||||||
|
const app_url = proposal.params.proposer.metadata.url ?? "-"
|
||||||
|
const app_domain = SQUtils.StringUtils.extractDomainFromLink(app_url)
|
||||||
|
if(err) {
|
||||||
|
root.pairingResponse(Pairing.errors.unknownError)
|
||||||
|
root.displayToastMessage(qsTr("Failed to reject connection request for %1").arg(app_domain), Constants.ephemeralNotificationType.danger)
|
||||||
|
} else {
|
||||||
|
root.displayToastMessage(qsTr("Connection request for %1 was rejected").arg(app_domain), Constants.ephemeralNotificationType.success)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onApproveSessionResult(proposalId, session, err) {
|
||||||
|
if (!d.activeProposals.has(proposalId)) {
|
||||||
|
console.error("No active proposal found for key: " + proposalId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!d.acceptedSessionProposal || d.acceptedSessionProposal.id !== proposalId) {
|
||||||
|
console.error("No accepted proposal found for key: " + proposalId)
|
||||||
|
d.activeProposals.delete(proposalId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const proposal = d.activeProposals.get(proposalId)
|
||||||
|
d.activeProposals.delete(proposalId)
|
||||||
|
d.acceptedSessionProposal = null
|
||||||
|
d.acceptedNamespaces = null
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
root.pairingResponse(Pairing.errors.unknownError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO #14754: implement custom dApp notification
|
||||||
|
const app_url = proposal.params.proposer.metadata.url ?? "-"
|
||||||
|
const app_domain = SQUtils.StringUtils.extractDomainFromLink(app_url)
|
||||||
|
root.displayToastMessage(qsTr("Connected to %1 via WalletConnect").arg(app_domain), Constants.ephemeralNotificationType.success)
|
||||||
|
|
||||||
|
// Persist session
|
||||||
|
if(!root.store.addWalletConnectSession(JSON.stringify(session))) {
|
||||||
|
console.error("Failed to persist session")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify client
|
||||||
|
root.approveSessionResult(proposalId, err, session.topic)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBuildApprovedNamespacesResult(key, approvedNamespaces, error) {
|
||||||
|
if (!d.activeProposals.has(key)) {
|
||||||
|
console.error("No active proposal found for key: " + key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(error || !approvedNamespaces) {
|
||||||
|
// Check that it contains Non conforming namespaces"
|
||||||
|
if (error.includes("Non conforming namespaces")) {
|
||||||
|
root.pairingResponse(Pairing.errors.unsupportedNetwork)
|
||||||
|
} else {
|
||||||
|
root.pairingResponse(Pairing.errors.unknownError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
approvedNamespaces = applyChainAgnosticFix(approvedNamespaces)
|
||||||
|
|
||||||
|
if (d.acceptedSessionProposal) {
|
||||||
|
sdk.approveSession(d.acceptedSessionProposal, approvedNamespaces)
|
||||||
|
} else {
|
||||||
|
const proposal = d.activeProposals.get(key)
|
||||||
|
const res = DAppsHelpers.extractChainsAndAccountsFromApprovedNamespaces(approvedNamespaces)
|
||||||
|
const chains = res.chains
|
||||||
|
const dAppUrl = proposal.params.proposer.metadata.url
|
||||||
|
const dAppName = proposal.params.proposer.metadata.name
|
||||||
|
const dAppIcons = proposal.params.proposer.metadata.icons
|
||||||
|
const dAppIcon = dAppIcons && dAppIcons.length > 0 ? dAppIcons[0] : ""
|
||||||
|
|
||||||
|
root.connectDApp(chains, dAppUrl, dAppName, dAppIcon, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Special case for chain agnostic dapps
|
||||||
|
//WC considers the approved namespace as valid, but there's no chainId or account established
|
||||||
|
//Usually this request is declared by using `eip155:0`, but we don't support this chainID, resulting in empty `chains` and `accounts`
|
||||||
|
//The established connection will use for all user approved chains and accounts
|
||||||
|
//This fix is applied to all valid namespaces that don't have a chainId or account
|
||||||
|
function applyChainAgnosticFix(approvedNamespaces) {
|
||||||
|
try {
|
||||||
|
const an = approvedNamespaces.eip155
|
||||||
|
const chainAgnosticRequest = (!an.chains || an.chains.length === 0) && (!an.accounts || an.accounts.length === 0)
|
||||||
|
if (!chainAgnosticRequest) {
|
||||||
|
return approvedNamespaces
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the `d.acceptedNamespaces` is set it means the user already confirmed the chain and account
|
||||||
|
if (!!d.acceptedNamespaces) {
|
||||||
|
approvedNamespaces.eip155.chains = d.acceptedNamespaces.eip155.chains
|
||||||
|
approvedNamespaces.eip155.accounts = d.acceptedNamespaces.eip155.accounts
|
||||||
|
return approvedNamespaces
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show to the user all possible chains
|
||||||
|
const supportedNamespacesStr = DAppsHelpers.buildSupportedNamespacesFromModels(
|
||||||
|
root.networksModel, root.accountsModel, SessionRequest.getSupportedMethods())
|
||||||
|
const supportedNamespaces = JSON.parse(supportedNamespacesStr)
|
||||||
|
|
||||||
|
approvedNamespaces.eip155.chains = supportedNamespaces.eip155.chains
|
||||||
|
approvedNamespaces.eip155.accounts = supportedNamespaces.eip155.accounts
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("WC Error applying chain agnostic fix", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return approvedNamespaces
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSessionProposal(sessionProposal) {
|
||||||
|
const key = sessionProposal.id
|
||||||
|
d.activeProposals.set(key, sessionProposal)
|
||||||
|
|
||||||
|
const supportedNamespacesStr = DAppsHelpers.buildSupportedNamespacesFromModels(
|
||||||
|
root.networksModel, root.accountsModel, SessionRequest.getSupportedMethods())
|
||||||
|
sdk.buildApprovedNamespaces(key, sessionProposal.params, JSON.parse(supportedNamespacesStr))
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPairResponse(ok) {
|
||||||
|
root.pairingResponse(ok)
|
||||||
|
}
|
||||||
|
|
||||||
function onSessionRequestEvent(event) {
|
function onSessionRequestEvent(event) {
|
||||||
const res = d.resolveAsync(event)
|
const res = d.resolveAsync(event)
|
||||||
if (res.code == d.resolveAsyncResult.error) {
|
if (res.code == d.resolveAsyncResult.error) {
|
||||||
|
@ -83,7 +276,7 @@ SQUtils.QObject {
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
root.displayToastMessage(qsTr("Fail to %1 from %2").arg(methodStr).arg(appDomain), Constants.ephemeralNotificationType.danger)
|
root.displayToastMessage(qsTr("Fail to %1 from %2").arg(methodStr).arg(appDomain), Constants.ephemeralNotificationType.danger)
|
||||||
root.rejectSessionRequest(topic, id, true /*hasError*/)
|
sdk.rejectSessionRequest(topic, id, true /*hasError*/)
|
||||||
console.error(`Error accepting session request for topic: ${topic}, id: ${id}, accept: ${accept}, error: ${error}`)
|
console.error(`Error accepting session request for topic: ${topic}, id: ${id}, accept: ${accept}, error: ${error}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -112,52 +305,56 @@ SQUtils.QObject {
|
||||||
|
|
||||||
request.setExpired()
|
request.setExpired()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSessionDelete(topic, err) {
|
||||||
|
d.disconnectSessionRequested(topic, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
SiweRequestPlugin {
|
||||||
target: root.store
|
id: siwePlugin
|
||||||
|
|
||||||
function onUserAuthenticated(topic, id, password, pin, payload) {
|
sdk: root.sdk
|
||||||
var request = requests.findRequest(topic, id)
|
store: root.store
|
||||||
|
accountsModel: root.accountsModel
|
||||||
|
networksModel: root.networksModel
|
||||||
|
|
||||||
|
onRegisterSignRequest: (request) => {
|
||||||
|
requests.enqueue(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnregisterSignRequest: (requestId) => {
|
||||||
|
const request = requests.findById(requestId)
|
||||||
if (request === null) {
|
if (request === null) {
|
||||||
console.error("Error finding event for topic", topic, "id", id)
|
console.error("SiweRequestPlugin::onUnregisterSignRequest: Error finding event for requestId", requestId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (request.isExpired()) {
|
requests.removeRequest(request.topic, requestId)
|
||||||
console.warn("Error: request expired")
|
|
||||||
root.rejectSessionRequest(topic, id, true /*hasError*/)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
d.executeSessionRequest(request, password, pin, payload)
|
onConnectDApp: (chains, dAppUrl, dAppName, dAppIcon, key) => {
|
||||||
|
root.connectDApp(chains, dAppUrl, dAppName, dAppIcon, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUserAuthenticationFailed(topic, id) {
|
onSiweFailed: (id, error, topic) => {
|
||||||
let request = requests.findRequest(topic, id)
|
root.approveSessionResult(id, error, topic)
|
||||||
let methodStr = SessionRequest.methodToUserString(request.method)
|
|
||||||
if (request === null || !methodStr) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.isExpired()) {
|
onSiweSuccessful: (id, topic) => {
|
||||||
console.warn("Error: request expired")
|
d.lookupSession(topic, function(session) {
|
||||||
root.rejectSessionRequest(topic, id, true /*hasError*/)
|
// TODO #14754: implement custom dApp notification
|
||||||
return
|
let meta = session.peer.metadata
|
||||||
|
const dappUrl = meta.url ?? "-"
|
||||||
|
const dappDomain = SQUtils.StringUtils.extractDomainFromLink(dappUrl)
|
||||||
|
root.displayToastMessage(qsTr("Connected to %1 via WalletConnect").arg(dappDomain), Constants.ephemeralNotificationType.success)
|
||||||
|
|
||||||
|
// Persist session
|
||||||
|
if(!root.store.addWalletConnectSession(JSON.stringify(session))) {
|
||||||
|
console.error("Failed to persist session")
|
||||||
}
|
}
|
||||||
|
|
||||||
const appDomain = SQUtils.StringUtils.extractDomainFromLink(request.dappUrl)
|
root.approveSessionResult(id, "", topic)
|
||||||
root.displayToastMessage(qsTr("Failed to authenticate %1 from %2").arg(methodStr).arg(appDomain), Constants.ephemeralNotificationType.danger)
|
})
|
||||||
root.rejectSessionRequest(topic, id, true /*hasError*/)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSigningResult(topic, id, data) {
|
|
||||||
let hasErrors = (data == "")
|
|
||||||
if (!hasErrors) {
|
|
||||||
// acceptSessionRequest will trigger an sdk.sessionRequestUserAnswerResult signal
|
|
||||||
sdk.acceptSessionRequest(topic, id, data)
|
|
||||||
} else {
|
|
||||||
root.rejectSessionRequest(topic, id, hasErrors)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,6 +469,10 @@ SQUtils.QObject {
|
||||||
readonly property int ignored: 2
|
readonly property int ignored: 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property var activeProposals: new Map() // key: proposalId, value: sessionProposal
|
||||||
|
property var acceptedSessionProposal: null
|
||||||
|
property var acceptedNamespaces: null
|
||||||
|
|
||||||
// returns {
|
// returns {
|
||||||
// obj: obj or nil
|
// obj: obj or nil
|
||||||
// code: resolveAsyncResult codes
|
// code: resolveAsyncResult codes
|
||||||
|
@ -845,6 +1046,44 @@ SQUtils.QObject {
|
||||||
}
|
}
|
||||||
return BigOps.fromNumber(0)
|
return BigOps.fromNumber(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function disconnectSessionRequested(topic, err) {
|
||||||
|
// Get all sessions and filter the active ones for known accounts
|
||||||
|
// Act on the first matching session with the same topic
|
||||||
|
const activeSessionsCallback = (allSessions, success) => {
|
||||||
|
root.store.activeSessionsReceived.disconnect(activeSessionsCallback)
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
// TODO #14754: implement custom dApp notification
|
||||||
|
root.dappDisconnected("", "", true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to original format
|
||||||
|
const webSdkSessions = allSessions.map((session) => {
|
||||||
|
return JSON.parse(session.sessionJson)
|
||||||
|
})
|
||||||
|
|
||||||
|
const sessions = DAppsHelpers.filterActiveSessionsForKnownAccounts(webSdkSessions, root.accountsModel)
|
||||||
|
|
||||||
|
for (const sessionID in sessions) {
|
||||||
|
const session = sessions[sessionID]
|
||||||
|
if (session.topic == topic) {
|
||||||
|
root.store.deactivateWalletConnectSession(topic)
|
||||||
|
|
||||||
|
const dappUrl = session.peer.metadata.url ?? "-"
|
||||||
|
root.dappDisconnected(topic, dappUrl, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root.store.activeSessionsReceived.connect(activeSessionsCallback)
|
||||||
|
if (!root.store.getActiveSessions()) {
|
||||||
|
root.store.activeSessionsReceived.disconnect(activeSessionsCallback)
|
||||||
|
// TODO #14754: implement custom dApp notification
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The queue is used to ensure that the events are processed in the order they are received but they could be
|
/// The queue is used to ensure that the events are processed in the order they are received but they could be
|
||||||
|
@ -856,8 +1095,58 @@ SQUtils.QObject {
|
||||||
Component {
|
Component {
|
||||||
id: sessionRequestComponent
|
id: sessionRequestComponent
|
||||||
|
|
||||||
SessionRequestResolved {
|
SessionRequestWithAuth {
|
||||||
|
id: request
|
||||||
sourceId: Constants.DAppConnectors.WalletConnect
|
sourceId: Constants.DAppConnectors.WalletConnect
|
||||||
|
store: root.store
|
||||||
|
|
||||||
|
function signedHandler(topic, id, data) {
|
||||||
|
if (topic != request.topic || id != request.requestId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
root.store.signingResult.disconnect(request.signedHandler)
|
||||||
|
|
||||||
|
let hasErrors = (data == "")
|
||||||
|
if (!hasErrors) {
|
||||||
|
// acceptSessionRequest will trigger an sdk.sessionRequestUserAnswerResult signal
|
||||||
|
sdk.acceptSessionRequest(topic, id, data)
|
||||||
|
} else {
|
||||||
|
request.reject(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted: () => {
|
||||||
|
d.unsubscribeForFeeUpdates(request.topic, request.requestId)
|
||||||
|
}
|
||||||
|
|
||||||
|
onRejected: (hasError) => {
|
||||||
|
d.unsubscribeForFeeUpdates(request.topic, request.requestId)
|
||||||
|
sdk.rejectSessionRequest(request.topic, request.requestId, hasError)
|
||||||
|
}
|
||||||
|
|
||||||
|
onAuthFailed: () => {
|
||||||
|
const appDomain = SQUtils.StringUtils.extractDomainFromLink(request.dappUrl)
|
||||||
|
const methodStr = SessionRequest.methodToUserString(request.method)
|
||||||
|
if (!methodStr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
root.displayToastMessage(qsTr("Failed to authenticate %1 from %2").arg(methodStr).arg(appDomain), Constants.ephemeralNotificationType.danger)
|
||||||
|
}
|
||||||
|
|
||||||
|
onExecute: (password, pin) => {
|
||||||
|
root.store.signingResult.connect(request.signedHandler)
|
||||||
|
let executed = false
|
||||||
|
try {
|
||||||
|
executed = d.executeSessionRequest(request, password, pin, request.feesInfo)
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error executing session request", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!executed) {
|
||||||
|
sdk.rejectSessionRequest(request.topic, request.requestId, true /*hasError*/)
|
||||||
|
root.store.signingResult.disconnect(request.signedHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,11 +56,11 @@ QObject {
|
||||||
function sign(topic, id) {
|
function sign(topic, id) {
|
||||||
// The authentication triggers the signing process
|
// The authentication triggers the signing process
|
||||||
// authenticate -> sign -> inform the dApp
|
// authenticate -> sign -> inform the dApp
|
||||||
d.authenticate(topic, id)
|
d.sign(topic, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function rejectSign(topic, id, hasError) {
|
function rejectSign(topic, id, hasError) {
|
||||||
requestHandler.rejectSessionRequest(topic, id, hasError)
|
d.rejectSign(topic, id, hasError)
|
||||||
}
|
}
|
||||||
|
|
||||||
function subscribeForFeeUpdates(topic, id) {
|
function subscribeForFeeUpdates(topic, id) {
|
||||||
|
@ -75,29 +75,18 @@ QObject {
|
||||||
/// Initiates the pairing process with the given URI
|
/// Initiates the pairing process with the given URI
|
||||||
function pair(uri) {
|
function pair(uri) {
|
||||||
timeoutTimer.start()
|
timeoutTimer.start()
|
||||||
wcSDK.pair(uri)
|
requestHandler.pair(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Approves or rejects the session proposal
|
/// Approves or rejects the session proposal
|
||||||
function approvePairSession(key, approvedChainIds, accountAddress) {
|
function approvePairSession(key, approvedChainIds, accountAddress) {
|
||||||
if (!d.activeProposals.has(key)) {
|
requestHandler.approvePairSession(key, approvedChainIds, accountAddress)
|
||||||
console.error("No active proposal found for key: " + key)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const proposal = d.activeProposals.get(key)
|
|
||||||
d.acceptedSessionProposal = proposal
|
|
||||||
const approvedNamespaces = JSON.parse(
|
|
||||||
DAppsHelpers.buildSupportedNamespaces(approvedChainIds,
|
|
||||||
[accountAddress],
|
|
||||||
SessionRequest.getSupportedMethods())
|
|
||||||
)
|
|
||||||
wcSDK.buildApprovedNamespaces(key, proposal.params, approvedNamespaces)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Rejects the session proposal
|
/// Rejects the session proposal
|
||||||
function rejectPairSession(id) {
|
function rejectPairSession(id) {
|
||||||
wcSDK.rejectSession(id)
|
requestHandler.rejectPairSession(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Disconnects the dApp with the given topic
|
/// Disconnects the dApp with the given topic
|
||||||
|
@ -166,14 +155,6 @@ QObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property var activeProposals: new Map() // key: proposalId, value: sessionProposal
|
|
||||||
property var acceptedSessionProposal: null
|
|
||||||
|
|
||||||
/// Disconnects the WC session with the given topic
|
|
||||||
function disconnectSession(sessionTopic) {
|
|
||||||
wcSDK.disconnectSession(sessionTopic)
|
|
||||||
}
|
|
||||||
|
|
||||||
function disconnectDapp(topic) {
|
function disconnectDapp(topic) {
|
||||||
const dApp = d.getDAppByTopic(topic)
|
const dApp = d.getDAppByTopic(topic)
|
||||||
if (!dApp) {
|
if (!dApp) {
|
||||||
|
@ -196,7 +177,7 @@ QObject {
|
||||||
if (dApp.connectorId === dappsProvider.connectorId) {
|
if (dApp.connectorId === dappsProvider.connectorId) {
|
||||||
// Currently disconnect acts on all sessions!
|
// Currently disconnect acts on all sessions!
|
||||||
for (let i = 0; i < dApp.sessions.ModelCount.count; i++) {
|
for (let i = 0; i < dApp.sessions.ModelCount.count; i++) {
|
||||||
d.disconnectSession(dApp.sessions.get(i).topic)
|
requestHandler.disconnectSession(dApp.sessions.get(i).topic)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,36 +192,25 @@ QObject {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = DAppsHelpers.extractInfoFromPairUri(uri)
|
requestHandler.validatePairingUri(uri)
|
||||||
wcSDK.getActiveSessions((sessions) => {
|
|
||||||
// Check if the URI is already paired
|
|
||||||
let validationState = Pairing.errors.uriOk
|
|
||||||
for (const key in sessions) {
|
|
||||||
if (sessions[key].pairingTopic === info.topic) {
|
|
||||||
validationState = Pairing.errors.alreadyUsed
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if expired
|
function sign(topic, id) {
|
||||||
if (validationState === Pairing.errors.uriOk) {
|
|
||||||
const now = (new Date().getTime())/1000
|
|
||||||
if (info.expiry < now) {
|
|
||||||
validationState = Pairing.errors.expired
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
root.pairingValidated(validationState)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function authenticate(topic, id) {
|
|
||||||
const request = sessionRequestsModel.findRequest(topic, id)
|
const request = sessionRequestsModel.findRequest(topic, id)
|
||||||
if (!request) {
|
if (!request) {
|
||||||
console.error("Session request not found")
|
console.error("Session request not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
requestHandler.authenticate(topic, id, request.accountAddress, request.feesInfo)
|
request.accept()
|
||||||
|
}
|
||||||
|
|
||||||
|
function rejectSign(topic, id, hasError) {
|
||||||
|
const request = sessionRequestsModel.findRequest(topic, id)
|
||||||
|
if (!request) {
|
||||||
|
console.error("Session request not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
request.reject(hasError)
|
||||||
}
|
}
|
||||||
|
|
||||||
function reportPairErrorState(state) {
|
function reportPairErrorState(state) {
|
||||||
|
@ -248,45 +218,6 @@ QObject {
|
||||||
root.pairingValidated(state)
|
root.pairingValidated(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
function disconnectSessionRequested(topic, err) {
|
|
||||||
// Get all sessions and filter the active ones for known accounts
|
|
||||||
// Act on the first matching session with the same topic
|
|
||||||
const activeSessionsCallback = (allSessions, success) => {
|
|
||||||
store.activeSessionsReceived.disconnect(activeSessionsCallback)
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
// TODO #14754: implement custom dApp notification
|
|
||||||
d.notifyDappDisconnect("-", true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to original format
|
|
||||||
const webSdkSessions = allSessions.map((session) => {
|
|
||||||
return JSON.parse(session.sessionJson)
|
|
||||||
})
|
|
||||||
|
|
||||||
const sessions = DAppsHelpers.filterActiveSessionsForKnownAccounts(webSdkSessions, root.validAccounts)
|
|
||||||
|
|
||||||
for (const sessionID in sessions) {
|
|
||||||
const session = sessions[sessionID]
|
|
||||||
if (session.topic === topic) {
|
|
||||||
store.deactivateWalletConnectSession(topic)
|
|
||||||
dappsProvider.updateDapps()
|
|
||||||
|
|
||||||
const dappUrl = session.peer.metadata.url ?? "-"
|
|
||||||
d.notifyDappDisconnect(dappUrl, err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
store.activeSessionsReceived.connect(activeSessionsCallback)
|
|
||||||
if (!store.getActiveSessions()) {
|
|
||||||
store.activeSessionsReceived.disconnect(activeSessionsCallback)
|
|
||||||
// TODO #14754: implement custom dApp notification
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function notifyDappDisconnect(dappUrl, err) {
|
function notifyDappDisconnect(dappUrl, err) {
|
||||||
const appDomain = StringUtils.extractDomainFromLink(dappUrl)
|
const appDomain = StringUtils.extractDomainFromLink(dappUrl)
|
||||||
if(err) {
|
if(err) {
|
||||||
|
@ -313,125 +244,6 @@ QObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: wcSDK
|
|
||||||
|
|
||||||
function onPairResponse(ok) {
|
|
||||||
if (!ok) {
|
|
||||||
d.reportPairErrorState(Pairing.errors.unknownError)
|
|
||||||
} // else waiting for onSessionProposal
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSessionProposal(sessionProposal) {
|
|
||||||
const key = sessionProposal.id
|
|
||||||
d.activeProposals.set(key, sessionProposal)
|
|
||||||
|
|
||||||
const supportedNamespacesStr = DAppsHelpers.buildSupportedNamespacesFromModels(
|
|
||||||
root.flatNetworks, root.validAccounts, SessionRequest.getSupportedMethods())
|
|
||||||
wcSDK.buildApprovedNamespaces(key, sessionProposal.params, JSON.parse(supportedNamespacesStr))
|
|
||||||
}
|
|
||||||
|
|
||||||
function onBuildApprovedNamespacesResult(key, approvedNamespaces, error) {
|
|
||||||
if (!d.activeProposals.has(key)) {
|
|
||||||
console.error("No active proposal found for key: " + key)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if(error || !approvedNamespaces) {
|
|
||||||
// Check that it contains Non conforming namespaces"
|
|
||||||
if (error.includes("Non conforming namespaces")) {
|
|
||||||
d.reportPairErrorState(Pairing.errors.unsupportedNetwork)
|
|
||||||
} else {
|
|
||||||
d.reportPairErrorState(Pairing.errors.unknownError)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const an = approvedNamespaces.eip155
|
|
||||||
if (!(an.accounts) || an.accounts.length === 0 || (!(an.chains) || an.chains.length === 0)) {
|
|
||||||
d.reportPairErrorState(Pairing.errors.unsupportedNetwork)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (d.acceptedSessionProposal) {
|
|
||||||
wcSDK.approveSession(d.acceptedSessionProposal, approvedNamespaces)
|
|
||||||
} else {
|
|
||||||
const proposal = d.activeProposals.get(key)
|
|
||||||
const res = DAppsHelpers.extractChainsAndAccountsFromApprovedNamespaces(approvedNamespaces)
|
|
||||||
const chains = res.chains
|
|
||||||
const dAppUrl = proposal.params.proposer.metadata.url
|
|
||||||
const dAppName = proposal.params.proposer.metadata.name
|
|
||||||
const dAppIcons = proposal.params.proposer.metadata.icons
|
|
||||||
const dAppIcon = dAppIcons && dAppIcons.length > 0 ? dAppIcons[0] : ""
|
|
||||||
|
|
||||||
root.connectDApp(chains, dAppUrl, dAppName, dAppIcon, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onApproveSessionResult(proposalId, session, err) {
|
|
||||||
if (!d.activeProposals.has(proposalId)) {
|
|
||||||
console.error("No active proposal found for key: " + proposalId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!d.acceptedSessionProposal || d.acceptedSessionProposal.id !== proposalId) {
|
|
||||||
console.error("No accepted proposal found for key: " + proposalId)
|
|
||||||
d.activeProposals.delete(proposalId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const proposal = d.activeProposals.get(proposalId)
|
|
||||||
d.activeProposals.delete(proposalId)
|
|
||||||
d.acceptedSessionProposal = null
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
d.reportPairErrorState(Pairing.errors.unknownError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO #14754: implement custom dApp notification
|
|
||||||
const app_url = proposal.params.proposer.metadata.url ?? "-"
|
|
||||||
const app_domain = StringUtils.extractDomainFromLink(app_url)
|
|
||||||
root.displayToastMessage(qsTr("Connected to %1 via WalletConnect").arg(app_domain), Constants.ephemeralNotificationType.success)
|
|
||||||
|
|
||||||
// Persist session
|
|
||||||
if(!store.addWalletConnectSession(JSON.stringify(session))) {
|
|
||||||
console.error("Failed to persist session")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify client
|
|
||||||
root.approveSessionResult(proposalId, err, session.topic)
|
|
||||||
|
|
||||||
dappsProvider.updateDapps()
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRejectSessionResult(proposalId, err) {
|
|
||||||
if (!d.activeProposals.has(proposalId)) {
|
|
||||||
console.error("No active proposal found for key: " + proposalId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const proposal = d.activeProposals.get(proposalId)
|
|
||||||
d.activeProposals.delete(proposalId)
|
|
||||||
|
|
||||||
const app_url = proposal.params.proposer.metadata.url ?? "-"
|
|
||||||
const app_domain = StringUtils.extractDomainFromLink(app_url)
|
|
||||||
if(err) {
|
|
||||||
d.reportPairErrorState(Pairing.errors.unknownError)
|
|
||||||
root.displayToastMessage(qsTr("Failed to reject connection request for %1").arg(app_domain), Constants.ephemeralNotificationType.danger)
|
|
||||||
} else {
|
|
||||||
root.displayToastMessage(qsTr("Connection request for %1 was rejected").arg(app_domain), Constants.ephemeralNotificationType.success)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSessionDelete(topic, err) {
|
|
||||||
d.disconnectSessionRequested(topic, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
dappsProvider.updateDapps()
|
|
||||||
}
|
|
||||||
|
|
||||||
DAppsRequestHandler {
|
DAppsRequestHandler {
|
||||||
id: requestHandler
|
id: requestHandler
|
||||||
|
|
||||||
|
@ -449,6 +261,24 @@ QObject {
|
||||||
onDisplayToastMessage: (message, type) => {
|
onDisplayToastMessage: (message, type) => {
|
||||||
root.displayToastMessage(message, type)
|
root.displayToastMessage(message, type)
|
||||||
}
|
}
|
||||||
|
onPairingResponse: (state) => {
|
||||||
|
if (state != Pairing.errors.uriOk) {
|
||||||
|
d.reportPairErrorState(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onConnectDApp: (dappChains, dappUrl, dappName, dappIcon, key) => {
|
||||||
|
root.connectDApp(dappChains, dappUrl, dappName, dappIcon, key)
|
||||||
|
}
|
||||||
|
onApproveSessionResult: (key, error, topic) => {
|
||||||
|
root.approveSessionResult(key, error, topic)
|
||||||
|
}
|
||||||
|
onPairingValidated: (validationState) => {
|
||||||
|
timeoutTimer.stop()
|
||||||
|
root.pairingValidated(validationState)
|
||||||
|
}
|
||||||
|
onDappDisconnected: (_, dappUrl, err) => {
|
||||||
|
d.notifyDappDisconnect(dappUrl, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DAppsListProvider {
|
DAppsListProvider {
|
||||||
|
|
|
@ -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
|
// Data prepared for display in a human readable format
|
||||||
required property var preparedData
|
required property var preparedData
|
||||||
|
|
||||||
readonly property alias dappName: d.dappName
|
property alias dappName: d.dappName
|
||||||
readonly property alias dappUrl: d.dappUrl
|
property alias dappUrl: d.dappUrl
|
||||||
readonly property alias dappIcon: d.dappIcon
|
property alias dappIcon: d.dappIcon
|
||||||
|
|
||||||
/// extra data resolved from wallet
|
/// extra data resolved from wallet
|
||||||
property string maxFeesText: ""
|
property string maxFeesText: ""
|
||||||
|
|
|
@ -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
|
SessionRequestResolved 1.0 SessionRequestResolved.qml
|
||||||
|
SessionRequestWithAuth 1.0 SessionRequestWithAuth.qml
|
||||||
SessionRequestsModel 1.0 SessionRequestsModel.qml
|
SessionRequestsModel 1.0 SessionRequestsModel.qml
|
||||||
singleton SessionRequest 1.0 SessionRequest.qml
|
singleton SessionRequest 1.0 SessionRequest.qml
|
||||||
singleton Pairing 1.0 Pairing.qml
|
singleton Pairing 1.0 Pairing.qml
|
Loading…
Reference in New Issue