status-desktop/ui/app/AppLayouts/Wallet/services/dapps/plugins/SiweLifeCycle.qml

321 lines
11 KiB
QML

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
}
}
}