321 lines
11 KiB
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
|
|
}
|
|
}
|
|
}
|