chore(@desktop/walletconnect): communication between qt desktop app and loaded wallet connect sdk is made via web channel

This commit is contained in:
Sale Djenic 2023-11-16 16:15:37 +01:00 committed by saledjenic
parent 98207381da
commit 48c9e372dc
8 changed files with 856 additions and 2256 deletions

View File

@ -186,7 +186,7 @@ ifneq ($(detected_OS),Windows)
endif endif
DOTHERSIDE_LIBFILE := vendor/DOtherSide/build/lib/libDOtherSideStatic.a DOTHERSIDE_LIBFILE := vendor/DOtherSide/build/lib/libDOtherSideStatic.a
# order matters here, due to "-Wl,-as-needed" # order matters here, due to "-Wl,-as-needed"
NIM_PARAMS += --passL:"$(DOTHERSIDE_LIBFILE)" --passL:"$(shell PKG_CONFIG_PATH="$(QT5_PCFILEDIR)" pkg-config --libs Qt5Core Qt5Qml Qt5Gui Qt5Quick Qt5QuickControls2 Qt5Widgets Qt5Svg Qt5Multimedia Qt5WebView)" NIM_PARAMS += --passL:"$(DOTHERSIDE_LIBFILE)" --passL:"$(shell PKG_CONFIG_PATH="$(QT5_PCFILEDIR)" pkg-config --libs Qt5Core Qt5Qml Qt5Gui Qt5Quick Qt5QuickControls2 Qt5Widgets Qt5Svg Qt5Multimedia Qt5WebView Qt5WebChannel)"
else else
NIM_EXTRA_PARAMS := --passL:"-lsetupapi -lhid" NIM_EXTRA_PARAMS := --passL:"-lsetupapi -lhid"
endif endif

View File

@ -17,11 +17,6 @@ Item {
property bool sdkReady: state === d.sdkReadyState property bool sdkReady: state === d.sdkReadyState
function setUriAndPair(wcUri) {
pairLinkInput.text = wcUri
d.sdkView.pair(wcUri)
}
// wallet_connect.Controller \see wallet_section/wallet_connect/controller.nim // wallet_connect.Controller \see wallet_section/wallet_connect/controller.nim
required property var controller required property var controller
@ -205,7 +200,6 @@ Item {
component SdkViewComponent: WalletConnectSDK { component SdkViewComponent: WalletConnectSDK {
projectId: controller.projectId projectId: controller.projectId
backgroundColor: root.backgroundColor
onSdkInit: function(success, info) { onSdkInit: function(success, info) {
d.setDetailsText(info) d.setDetailsText(info)
@ -270,14 +264,13 @@ Item {
root.state = d.waitingUserResponseToSessionRequest root.state = d.waitingUserResponseToSessionRequest
} }
onResponseTimeout: { onPairSessionProposalExpired: {
d.setStatusText(`Timeout waiting for response. Reusing URI?`, "red") d.setStatusText(`Timeout waiting for response. Reusing URI?`, "red")
} }
onStatusChanged: function(message) { onStatusChanged: function(message) {
statusText.text = message statusText.text = message
} }
} }
QtObject { QtObject {
@ -326,10 +319,6 @@ Item {
root.state = d.waitingPairState root.state = d.waitingPairState
} }
}
Connections {
target: root.controller
function onRespondSessionRequest(sessionRequestJson, signedJson, error) { function onRespondSessionRequest(sessionRequestJson, signedJson, error) {
console.log("@dd respondSessionRequest", sessionRequestJson, signedJson, error) console.log("@dd respondSessionRequest", sessionRequestJson, signedJson, error)

View File

@ -1,326 +1,338 @@
import QtQuick 2.15 import QtQuick 2.15
import QtWebView 1.15 import QtWebEngine 1.10
// TODO #12434: remove debugging WebEngineView code import QtWebChannel 1.15
// import QtWebEngine 1.10
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import StatusQ.Core.Utils 0.1 as SQUtils import StatusQ.Core.Utils 0.1 as SQUtils
// Control used to instantiate and run the the WalletConnect web SDK Item {
// The view is not used to draw anything, but has to be visible to be able to run JS code
// Use the \c backgroundColor property to blend in with the background
// \warning A too smaller height might cause rendering errors
// TODO #12434: remove debugging WebEngineView code
WebView {
//WebEngineView {
id: root id: root
required property string projectId
readonly property alias sdkReady: d.sdkReady
readonly property alias pairingsModel: d.pairingsModel
implicitWidth: 1 implicitWidth: 1
implicitHeight: 1 implicitHeight: 1
required property string projectId signal statusChanged(string message)
required property color backgroundColor
readonly property alias sdkReady: d.sdkReady
readonly property alias pairingsModel: d.pairingsModel
signal sdkInit(bool success, var result) signal sdkInit(bool success, var result)
signal pairSessionProposal(bool success, var sessionProposal) signal pairSessionProposal(bool success, var sessionProposal)
signal pairSessionProposalExpired()
signal pairAcceptedResult(bool success, var sessionType) signal pairAcceptedResult(bool success, var sessionType)
signal pairRejectedResult(bool success, var result) signal pairRejectedResult(bool success, var result)
signal sessionRequestEvent(var sessionRequest) signal sessionRequestEvent(var sessionRequest)
signal sessionRequestUserAnswerResult(bool accept, string error) signal sessionRequestUserAnswerResult(bool accept, string error)
signal responseTimeout()
// TODO: proper report function pair(pairLink)
signal statusChanged(string message) {
wcCalls.pair(pairLink)
function pair(pairLink) {
let callStr = d.generateSdkCall("pair", `"${pairLink}"`, RequestCodes.PairSuccess, RequestCodes.PairError)
d.requestSdkAsync(callStr)
} }
function approvePairSession(sessionProposal, supportedNamespaces) { function approvePairSession(sessionProposal, supportedNamespaces)
let callStr = d.generateSdkCall("approvePairSession", `${JSON.stringify(sessionProposal)}, ${JSON.stringify(supportedNamespaces)}`, RequestCodes.ApprovePairSuccess, RequestCodes.ApprovePairSuccess) {
wcCalls.approvePairSession(sessionProposal, supportedNamespaces)
d.requestSdkAsync(callStr)
} }
function rejectPairSession(id) { function rejectPairSession(id)
let callStr = d.generateSdkCall("rejectPairSession", id, RequestCodes.RejectPairSuccess, RequestCodes.RejectPairError) {
wcCalls.rejectPairSession(id)
d.requestSdkAsync(callStr)
} }
function acceptSessionRequest(topic, id, signature) { function acceptSessionRequest(topic, id, signature) {
let callStr = d.generateSdkCall("respondSessionRequest", `"${topic}", ${id}, "${signature}"`, RequestCodes.AcceptSessionSuccess, RequestCodes.AcceptSessionError) wcCalls.acceptSessionRequest(topic, id, signature)
d.requestSdkAsync(callStr)
} }
function rejectSessionRequest(topic, id, error) { function rejectSessionRequest(topic, id, error) {
let callStr = d.generateSdkCall("rejectSessionRequest", `"${topic}", ${id}, ${error}`, RequestCodes.RejectSessionSuccess, RequestCodes.RejectSessionError) wcCalls.rejectSessionRequest(topic, id, error)
d.requestSdkAsync(callStr)
}
// TODO #12434: remove debugging WebEngineView code
onLoadingChanged: function(loadRequest) {
console.debug(`@dd WalletConnectSDK.onLoadingChanged; status: ${loadRequest.status}; error: ${loadRequest.errorString}`)
switch(loadRequest.status) {
case WebView.LoadSucceededStatus:
// case WebEngineView.LoadSucceededStatus:
d.init(root.projectId)
break
case WebView.LoadFailedStatus:
// case WebEngineView.LoadFailedStatus:
root.statusChanged(`<font color="red">Failed loading SDK JS code; error: "${loadRequest.errorString}"</font>`)
break
case WebView.LoadStartedStatus:
// case WebEngineView.LoadStartedStatus:
root.statusChanged(`<font color="blue">Loading SDK JS code</font>`)
break
}
}
Component.onCompleted: {
console.debug(`@dd WalletConnectSDK onCompleted`)
var scriptSrc = SQUtils.StringUtils.readTextFile(":/app/AppLayouts/Wallet/views/walletconnect/sdk/generated/bundle.js")
// Load bundle from disk if not found in resources (Storybook)
if (scriptSrc === "") {
scriptSrc = SQUtils.StringUtils.readTextFile("./AppLayouts/Wallet/views/walletconnect/sdk/generated/bundle.js")
if (scriptSrc === "") {
console.error("Failed to read WalletConnect SDK bundle")
return
}
}
let htmlSrc = `<!DOCTYPE html><html><head><!--<title>TODO: Test</title>--><script type='text/javascript'>${scriptSrc}</script></head><body style='background-color: ${root.backgroundColor.toString()};'></body></html>`
console.debug(`@dd WalletConnectSDK.loadHtml; htmlSrc len: ${htmlSrc.length}`)
root.loadHtml(htmlSrc, "https://status.app")
}
Timer {
id: timer
interval: 100
repeat: true
running: false
triggeredOnStart: true
property int errorCount: 0
onTriggered: {
root.runJavaScript(
"wcResult",
function(wcResult) {
if (!wcResult) {
return
}
let done = false
if (wcResult.error) {
console.debug(`WC JS error response - ${JSON.stringify(wcResult)}`)
done = true
if (!d.sdkReady) {
root.statusChanged(`<font color="red">[${timer.errorCount++}] Failed SDK init; error: ${wcResult.error}</font>`)
} else {
root.statusChanged(`<font color="red">[${timer.errorCount++}] Operation error: ${wcResult.error}</font>`)
}
}
if (wcResult.state !== undefined) {
switch (wcResult.state) {
case RequestCodes.SdkInitSuccess:
d.sdkReady = true
root.sdkInit(true, "")
d.startListeningForEvents()
break
case RequestCodes.SdkInitError:
d.sdkReady = false
root.sdkInit(false, wcResult.error)
break
case RequestCodes.PairSuccess:
root.pairSessionProposal(true, wcResult.result)
d.getPairings()
break
case RequestCodes.PairError:
root.pairSessionProposal(false, wcResult.error)
break
case RequestCodes.ApprovePairSuccess:
root.pairAcceptedResult(true, "")
d.getPairings()
break
case RequestCodes.ApprovePairError:
root.pairAcceptedResult(false, wcResult.error)
d.getPairings()
break
case RequestCodes.RejectPairSuccess:
root.pairRejectedResult(true, "")
break
case RequestCodes.RejectPairError:
root.pairRejectedResult(false, wcResult.error)
break
case RequestCodes.AcceptSessionSuccess:
root.sessionRequestUserAnswerResult(true, "")
break
case RequestCodes.AcceptSessionError:
root.sessionRequestUserAnswerResult(true, wcResult.error)
break
case RequestCodes.RejectSessionSuccess:
root.sessionRequestUserAnswerResult(false, "")
break
case RequestCodes.RejectSessionError:
root.sessionRequestUserAnswerResult(false, wcResult.error)
break
case RequestCodes.GetPairings:
d.populatePairingsModel(wcResult.result)
break
case RequestCodes.GetPairingsError:
console.error(`WalletConnectSDK - getPairings error: ${wcResult.error}`)
break
default: {
root.statusChanged(`<font color="red">[${timer.errorCount++}] Unknown state: ${wcResult.state}</font>`)
}
}
done = true
}
if (done) {
timer.stop()
}
}
)
}
}
Timer {
id: responseTimeoutTimer
interval: 10000
repeat: false
running: timer.running
onTriggered: {
timer.stop()
root.responseTimeout()
}
}
Timer {
id: eventsTimer
interval: 100
repeat: true
running: false
onTriggered: {
root.runJavaScript("window.wcEventResult ? window.wcEventResult.shift() : null", function(event) {
if (event) {
switch(event.name) {
case "session_request":
root.sessionRequestEvent(event.payload)
break
default:
console.error("WC unknown event type: ", event.type)
break
}
}
})
}
} }
QtObject { QtObject {
id: d id: d
property var sessionProposal: null
property var sessionType: null
property bool sdkReady: false property bool sdkReady: false
property ListModel pairingsModel: pairings property ListModel pairingsModel: pairings
onSdkReadyChanged: { onSdkReadyChanged: {
if (sdkReady) { if (sdkReady)
d.getPairings() {
d.resetPairingsModel()
} }
} }
function populatePairingsModel(pairList) { function resetPairingsModel()
{
pairings.clear(); pairings.clear();
for (let i = 0; i < pairList.length; i++) {
pairings.append({ wcCalls.getPairings((pairList) => {
active: pairList[i].active, for (let i = 0; i < pairList.length; i++) {
topic: pairList[i].topic, pairings.append({
expiry: pairList[i].expiry active: pairList[i].active,
}); topic: pairList[i].topic,
expiry: pairList[i].expiry
});
}
})
}
function getPairingTopicFromPairingUrl(url)
{
if (!url.startsWith("wc:"))
{
return null;
} }
const atIndex = url.indexOf("@");
if (atIndex < 0)
{
return null;
}
return url.slice(3, atIndex);
} }
}
QtObject {
id: wcCalls
function isWaitingForSdk() { function init() {
return timer.running console.debug(`@dd WalletConnectSDK.wcCall.init; root.projectId: ${root.projectId}`)
}
function generateSdkCall(methodName, paramsStr, successState, errorState) { webEngineView.runJavaScript(`wc.init("${root.projectId}")`, function(result) {
return "wcResult = {}; try { wc." + methodName + "(" + paramsStr + ").then((callRes) => { wcResult = {state: " + successState + ", error: null, result: callRes}; }).catch((error) => { wcResult = {state: " + errorState + ", error: error}; }); } catch (e) { wcResult = {state: " + errorState + ", error: \"Exception: \" + e.message}; }; wcResult"
} console.debug(`@dd WalletConnectSDK.wcCall.init; response: ${JSON.stringify(result, null, 2)}`)
function requestSdkAsync(jsCode) {
root.runJavaScript(jsCode, if (result && !!result.error)
function(result) { {
timer.restart() console.error("init: ", result.error)
} }
) })
} }
function requestSdk(methodName, paramsStr, successState, errorState) { function getPairings(callback) {
const jsCode = "wcResult = {}; try { const callRes = wc." + methodName + "(" + (paramsStr ? (paramsStr) : "") + "); wcResult = {state: " + successState + ", error: null, result: callRes}; } catch (e) { wcResult = {state: " + errorState + ", error: \"Exception: \" + e.message}; }; wcResult" console.debug(`@dd WalletConnectSDK.wcCall.getPairings;`)
root.runJavaScript(jsCode,
function(result) {
timer.restart()
}
)
}
function startListeningForEvents() { webEngineView.runJavaScript(`wc.getPairings()`, function(result) {
const jsCode = "
try {
function processWCEvents() {
window.wcEventResult = [];
window.wcEventError = null
window.wc.registerForSessionRequest((event) => {
window.wcEventResult.push({name: 'session_request', payload: event});
});
}
processWCEvents();
} catch (e) {
window.wcEventError = e
}
window.wcEventError"
root.runJavaScript(jsCode, console.debug(`@dd WalletConnectSDK.wcCall.getPairings; response: ${JSON.stringify(result, null, 2)}`)
function(result) {
if (result) { if (result)
console.error("startListeningForEvents: processWCEvents error", result) {
if (!!result.error) {
console.error("getPairings: ", result.error)
return return
} }
callback(result.result)
return
} }
) })
eventsTimer.start()
} }
function init(projectId) { function pair(pairLink) {
d.requestSdkAsync(generateSdkCall("init", `"${projectId}"`, RequestCodes.SdkInitSuccess, RequestCodes.SdkInitError)) console.debug(`@dd WalletConnectSDK.wcCall.pair; pairLink: ${pairLink}`)
wcCalls.getPairings((allPairings) => {
console.debug(`@dd WalletConnectSDK.wcCall.pair; response: ${JSON.stringify(allPairings, null, 2)}`)
let pairingTopic = d.getPairingTopicFromPairingUrl(pairLink);
// Find pairing by topic
const pairing = allPairings.find((p) => p.topic === pairingTopic);
if (pairing)
{
if (pairing.active) {
console.warn("pair: already paired")
return
}
}
webEngineView.runJavaScript(`wc.pair("${pairLink}")`, function(result) {
if (result && !!result.error)
{
console.error("pair: ", result.error)
}
})
}
)
} }
function getPairings(projectId) { function approvePairSession(sessionProposal, supportedNamespaces) {
d.requestSdk("getPairings", `null`, RequestCodes.GetPairings, RequestCodes.GetPairingsError) console.debug(`@dd WalletConnectSDK.wcCall.approvePairSession; sessionProposal: ${JSON.stringify(sessionProposal)}, supportedNamespaces: ${JSON.stringify(supportedNamespaces)}`)
webEngineView.runJavaScript(`wc.approvePairSession(${JSON.stringify(sessionProposal)}, ${JSON.stringify(supportedNamespaces)})`, function(result) {
console.debug(`@dd WalletConnectSDK.wcCall.approvePairSession; response: ${JSON.stringify(result, null, 2)}`)
if (result) {
if (!!result.error)
{
console.error("approvePairSession: ", result.error)
root.pairAcceptedResult(false, result.error)
return
}
root.pairAcceptedResult(true, result.error)
}
d.resetPairingsModel()
})
}
function rejectPairSession(id) {
console.debug(`@dd WalletConnectSDK.wcCall.rejectPairSession; id: ${id}`)
webEngineView.runJavaScript(`wc.rejectPairSession(${id})`, function(result) {
console.debug(`@dd WalletConnectSDK.wcCall.rejectPairSession; response: ${JSON.stringify(result, null, 2)}`)
if (result) {
if (!!result.error)
{
console.error("rejectPairSession: ", result.error)
root.pairRejectedResult(false, result.error)
return
}
root.pairRejectedResult(true, result.error)
}
d.resetPairingsModel()
})
}
function acceptSessionRequest(topic, id, signature) {
console.debug(`@dd WalletConnectSDK.wcCall.acceptSessionRequest; topic: "${topic}", id: ${id}, signature: "${signature}"`)
webEngineView.runJavaScript(`wc.respondSessionRequest("${topic}", ${id}, "${signature}")`, function(result) {
console.debug(`@dd WalletConnectSDK.wcCall.acceptSessionRequest; response: ${JSON.stringify(allPairings, null, 2)}`)
if (result) {
if (!!result.error)
{
console.error("respondSessionRequest: ", result.error)
root.sessionRequestUserAnswerResult(true, result.error)
return
}
root.sessionRequestUserAnswerResult(true, result.error)
}
d.resetPairingsModel()
})
}
function rejectSessionRequest(topic, id, error) {
console.debug(`@dd WalletConnectSDK.wcCall.rejectSessionRequest; topic: "${topic}", id: ${id}, error: "${error}"`)
webEngineView.runJavaScript(`wc.rejectSessionRequest("${topic}", ${id}, "${error}")`, function(result) {
console.debug(`@dd WalletConnectSDK.wcCall.rejectSessionRequest; response: ${JSON.stringify(result, null, 2)}`)
if (result) {
if (!!result.error)
{
console.error("rejectSessionRequest: ", result.error)
root.sessionRequestUserAnswerResult(false, result.error)
return
}
root.sessionRequestUserAnswerResult(false, result.error)
}
d.resetPairingsModel()
})
}
}
QtObject {
id: statusObject
WebChannel.id: "statusObject"
function sdkInitialized(error)
{
d.sdkReady = !error
root.sdkInit(d.sdkReady, error)
}
function onSessionProposal(details)
{
console.debug(`@dd WalletConnectSDK.onSessionProposal; details: ${JSON.stringify(details, null, 2)}`)
root.pairSessionProposal(true, details)
}
function onSessionUpdate(details)
{
console.debug(`@dd TODO WalletConnectSDK.onSessionUpdate; details: ${JSON.stringify(details, null, 2)}`)
}
function onSessionExtend(details)
{
console.debug(`@dd TODO WalletConnectSDK.onSessionExtend; details: ${JSON.stringify(details, null, 2)}`)
}
function onSessionPing(details)
{
console.debug(`@dd TODO WalletConnectSDK.onSessionPing; details: ${JSON.stringify(details, null, 2)}`)
}
function onSessionDelete(details)
{
console.debug(`@dd TODO WalletConnectSDK.onSessionDelete; details: ${JSON.stringify(details, null, 2)}`)
}
function onSessionExpire(details)
{
console.debug(`@dd TODO WalletConnectSDK.onSessionExpire; details: ${JSON.stringify(details, null, 2)}`)
}
function onSessionRequest(details)
{
console.debug(`@dd WalletConnectSDK.onSessionRequest; details: ${JSON.stringify(details, null, 2)}`)
root.sessionRequestEvent(details)
}
function onSessionRequestSent(details)
{
console.debug(`@dd TODO WalletConnectSDK.onSessionRequestSent; details: ${JSON.stringify(details, null, 2)}`)
}
function onSessionEvent(details)
{
console.debug(`@dd TODO WalletConnectSDK.onSessionEvent; details: ${JSON.stringify(details, null, 2)}`)
}
function onProposalExpire(details)
{
console.debug(`@dd WalletConnectSDK.onProposalExpire; details: ${JSON.stringify(details, null, 2)}`)
root.pairSessionProposalExpired()
} }
} }
ListModel { ListModel {
id: pairings id: pairings
} }
}
WebChannel {
id: statusChannel
registeredObjects: [statusObject]
}
WebEngineView {
id: webEngineView
anchors.fill: parent
url: "qrc:/app/AppLayouts/Wallet/views/walletconnect/sdk/src/index.html"
webChannel: statusChannel
onLoadingChanged: function(loadRequest) {
console.debug(`@dd WalletConnectSDK.onLoadingChanged; status: ${loadRequest.status}; error: ${loadRequest.errorString}`)
switch(loadRequest.status) {
case WebEngineView.LoadSucceededStatus:
wcCalls.init()
break
case WebEngineView.LoadFailedStatus:
root.statusChanged(`<font color="red">Failed loading SDK JS code; error: "${loadRequest.errorString}"</font>`)
break
case WebEngineView.LoadStartedStatus:
root.statusChanged(`<font color="blue">Loading SDK JS code</font>`)
break
}
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<script src="qrc:/app/AppLayouts/Wallet/views/walletconnect/sdk/generated/bundle.js"></script>
</head>
<body>
</body>
</html>

View File

@ -3,19 +3,27 @@ import { Web3Wallet } from "@walletconnect/web3wallet";
import AuthClient from '@walletconnect/auth-client' import AuthClient from '@walletconnect/auth-client'
import { QWebChannel } from './qwebchannel';
// import the builder util // import the builder util
import { buildApprovedNamespaces, getSdkError } from "@walletconnect/utils"; import { buildApprovedNamespaces, getSdkError } from "@walletconnect/utils";
import { formatJsonRpcResult, formatJsonRpcError } from "@walletconnect/jsonrpc-utils"; import { formatJsonRpcResult, formatJsonRpcError } from "@walletconnect/jsonrpc-utils";
// "Export" API to window
// Workaround, tried using export via output.module: true in webpack.config.js, but it didn't work
window.wc = { window.wc = {
core: null, core: null,
web3wallet: null, web3wallet: null,
authClient: null, authClient: null,
statusObject: null,
init: function (projectId) { init: function (projectId) {
return new Promise(async (resolve) => { (async () => {
try {
await createWebChannel();
} catch (error) {
wc.statusObject.sdkInitialized(error);
return
}
window.wc.core = new Core({ window.wc.core = new Core({
projectId: projectId, projectId: projectId,
}); });
@ -33,59 +41,75 @@ window.wc = {
window.wc.authClient = await AuthClient.init({ window.wc.authClient = await AuthClient.init({
projectId: projectId, projectId: projectId,
metadata: window.wc.web3wallet.metadata, metadata: window.wc.web3wallet.metadata,
}) });
resolve() // connect session responses https://specs.walletconnect.com/2.0/specs/clients/sign/session-events#events
}) window.wc.web3wallet.on("session_proposal", async (details) => {
wc.statusObject.onSessionProposal(details)
});
window.wc.web3wallet.on("session_update", async (details) => {
wc.statusObject.onSessionUpdate(details)
});
window.wc.web3wallet.on("session_extend", async (details) => {
wc.statusObject.onSessionExtend(details)
});
window.wc.web3wallet.on("session_ping", async (details) => {
wc.statusObject.onSessionPing(details)
});
window.wc.web3wallet.on("session_delete", async (details) => {
wc.statusObject.onSessionDelete(details)
});
window.wc.web3wallet.on("session_expire", async (details) => {
wc.statusObject.onSessionExpire(details)
});
window.wc.web3wallet.on("session_request", async (details) => {
wc.statusObject.onSessionRequest(details)
});
window.wc.web3wallet.on("session_request_sent", async (details) => {
wc.statusObject.onSessionRequestSent(details)
});
window.wc.web3wallet.on("session_event", async (details) => {
wc.statusObject.onSessionEvent(details)
});
window.wc.web3wallet.on("proposal_expire", async (details) => {
wc.statusObject.onProposalExpire(details)
});
wc.statusObject.sdkInitialized("");
})();
return { result: "ok", error: "" };
}, },
alreadyPaired: new Error("Already paired"),
waitingForApproval: new Error("Waiting for approval"),
// TODO: there is a corner case when attempting to pair with a link that is already paired or was rejected won't trigger any event back // TODO: there is a corner case when attempting to pair with a link that is already paired or was rejected won't trigger any event back
pair: function (uri) { pair: function (uri) {
let pairingTopic = getPairingTopicFromPairingUrl(uri); return {
result: window.wc.web3wallet.pair({ uri }),
const pairings = window.wc.core.pairing.getPairings(); error: ""
// Find pairing by topic };
const pairing = pairings.find((p) => p.topic === pairingTopic);
if (pairing) {
if (pairing.active) {
return new Promise((_, reject) => {
reject(window.wc.alreadyPaired);
});
}
}
let pairPromise = window.wc.web3wallet
.pair({ uri: uri })
return new Promise((resolve, reject) => {
pairPromise
.then(() => {
window.wc.web3wallet.on("session_proposal", async (sessionProposal) => {
resolve(sessionProposal);
});
})
.catch((error) => {
reject(error);
});
});
}, },
getPairings: function () { getPairings: function () {
return window.wc.core.pairing.getPairings(); return {
result: window.wc.core.pairing.getPairings(),
error: ""
};
}, },
disconnect: function (topic) { disconnect: function (topic) {
return window.wc.core.pairing.disconnect({ topic: topic}); return {
}, result: window.wc.core.pairing.disconnect({ topic: topic }),
error: ""
registerForSessionRequest: function (callback) { };
window.wc.web3wallet.on("session_request", callback);
},
registerForSessionDelete: function (callback) {
window.wc.web3wallet.on("session_delete", callback);
}, },
approvePairSession: function (sessionProposal, supportedNamespaces) { approvePairSession: function (sessionProposal, supportedNamespaces) {
@ -96,47 +120,29 @@ window.wc = {
supportedNamespaces: supportedNamespaces, supportedNamespaces: supportedNamespaces,
}); });
return window.wc.web3wallet.approveSession({ return {
id, result: window.wc.web3wallet.approveSession({
namespaces: approvedNamespaces, id,
}); namespaces: approvedNamespaces,
}),
error: ""
};
}, },
rejectPairSession: function (id) { rejectPairSession: function (id) {
return window.wc.web3wallet.rejectSession({ return {
id: id, result: window.wc.web3wallet.rejectSession({
reason: getSdkError("USER_REJECTED"), // TODO USER_REJECTED_METHODS, USER_REJECTED_CHAINS, USER_REJECTED_EVENTS id: id,
}); reason: getSdkError("USER_REJECTED"), // TODO USER_REJECTED_METHODS, USER_REJECTED_CHAINS, USER_REJECTED_EVENTS
}),
error: ""
};
}, },
auth: function (uri) { auth: function (uri) {
let pairingTopic = getPairingTopicFromPairingUrl(uri); return {
result: window.wc.authClient.core.pairing.pair({ uri }),
const pairings = window.wc.core.pairing.getPairings(); error: ""
// Find pairing by topic };
const pairing = pairings.find((p) => p.topic === pairingTopic);
if (pairing) {
if (pairing.active) {
return new Promise((_, reject) => {
reject(window.wc.alreadyPaired);
});
}
}
let pairPromise = window.wc.authClient.core.pairing
.pair({ uri })
return new Promise((resolve, reject) => {
pairPromise
.then(() => {
// TODO: check if we can separate using the URI info
window.wc.authClient.on("auth_request", async (authProposal) => {
resolve(authProposal);
});
})
.catch((error) => {
reject(error);
});
});
}, },
approveAuth: function (authProposal) { approveAuth: function (authProposal) {
@ -151,7 +157,8 @@ window.wc = {
// TODO: signature // TODO: signature
const signature = "0x123456789" const signature = "0x123456789"
return window.wc.authClient.respond( return {
result: window.wc.authClient.respond(
{ {
id: id, id: id,
signature: { signature: {
@ -159,43 +166,49 @@ window.wc = {
t: "eip191", t: "eip191",
}, },
}, },
iss iss),
); error: ""
};
}, },
rejectAuth: function (id) { rejectAuth: function (id) {
return window.wc.authClient.reject(id); return {
result: window.wc.authClient.reject(id),
error: ""
};
}, },
respondSessionRequest: function (topic, id, signature) { respondSessionRequest: function (topic, id, signature) {
const response = formatJsonRpcResult(id, signature) const response = formatJsonRpcResult(id, signature)
return window.wc.web3wallet.respondSessionRequest({ topic, response }); return {
result: window.wc.web3wallet.respondSessionRequest({ topic, response }),
error: ""
};
}, },
rejectSessionRequest: function (topic, id, error = false) { rejectSessionRequest: function (topic, id, error = false) {
const errorType = error ? "SESSION_SETTLEMENT_FAILED" : "USER_REJECTED"; const errorType = error ? "SESSION_SETTLEMENT_FAILED" : "USER_REJECTED";
return window.wc.web3wallet.respondSessionRequest({ return {
topic: topic, result: window.wc.web3wallet.respondSessionRequest({
response: formatJsonRpcError(id, getSdkError(errorType)), topic: topic,
}); response: formatJsonRpcError(id, getSdkError(errorType)),
}, }),
error: ""
};
disconnectAll: function () {
const pairings = window.wc.core.pairing.getPairings();
pairings.forEach((p) => {
window.wc.core.pairing.disconnect({ topic: p.topic });
});
}, },
}; };
// Returns null if not a pairing url function createWebChannel(projectId) {
function getPairingTopicFromPairingUrl(url) { return new Promise((resolve, reject) => {
if (!url.startsWith("wc:")) { window.wc.channel = new QWebChannel(qt.webChannelTransport, function (channel) {
return null; let statusObject = channel.objects.statusObject;
}
const atIndex = url.indexOf("@"); if (!statusObject) {
if (atIndex < 0) { reject(new Error("Unable to resolve statusObject"));
return null; } else {
} window.wc.statusObject = statusObject;
return url.slice(3, atIndex); resolve();
}
});
});
} }

View File

@ -0,0 +1,448 @@
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtWebChannel module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
"use strict";
var QWebChannelMessageTypes = {
signal: 1,
propertyUpdate: 2,
init: 3,
idle: 4,
debug: 5,
invokeMethod: 6,
connectToSignal: 7,
disconnectFromSignal: 8,
setProperty: 9,
response: 10,
};
var QWebChannel = function(transport, initCallback)
{
if (typeof transport !== "object" || typeof transport.send !== "function") {
console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." +
" Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send));
return;
}
var channel = this;
this.transport = transport;
this.send = function(data)
{
if (typeof(data) !== "string") {
data = JSON.stringify(data);
}
channel.transport.send(data);
}
this.transport.onmessage = function(message)
{
var data = message.data;
if (typeof data === "string") {
data = JSON.parse(data);
}
switch (data.type) {
case QWebChannelMessageTypes.signal:
channel.handleSignal(data);
break;
case QWebChannelMessageTypes.response:
channel.handleResponse(data);
break;
case QWebChannelMessageTypes.propertyUpdate:
channel.handlePropertyUpdate(data);
break;
default:
console.error("invalid message received:", message.data);
break;
}
}
this.execCallbacks = {};
this.execId = 0;
this.exec = function(data, callback)
{
if (!callback) {
// if no callback is given, send directly
channel.send(data);
return;
}
if (channel.execId === Number.MAX_VALUE) {
// wrap
channel.execId = Number.MIN_VALUE;
}
if (data.hasOwnProperty("id")) {
console.error("Cannot exec message with property id: " + JSON.stringify(data));
return;
}
data.id = channel.execId++;
channel.execCallbacks[data.id] = callback;
channel.send(data);
};
this.objects = {};
this.handleSignal = function(message)
{
var object = channel.objects[message.object];
if (object) {
object.signalEmitted(message.signal, message.args);
} else {
console.warn("Unhandled signal: " + message.object + "::" + message.signal);
}
}
this.handleResponse = function(message)
{
if (!message.hasOwnProperty("id")) {
console.error("Invalid response message received: ", JSON.stringify(message));
return;
}
channel.execCallbacks[message.id](message.data);
delete channel.execCallbacks[message.id];
}
this.handlePropertyUpdate = function(message)
{
message.data.forEach(data => {
var object = channel.objects[data.object];
if (object) {
object.propertyUpdate(data.signals, data.properties);
} else {
console.warn("Unhandled property update: " + data.object + "::" + data.signal);
}
});
channel.exec({type: QWebChannelMessageTypes.idle});
}
this.debug = function(message)
{
channel.send({type: QWebChannelMessageTypes.debug, data: message});
};
channel.exec({type: QWebChannelMessageTypes.init}, function(data) {
for (const objectName of Object.keys(data)) {
new QObject(objectName, data[objectName], channel);
}
// now unwrap properties, which might reference other registered objects
for (const objectName of Object.keys(channel.objects)) {
channel.objects[objectName].unwrapProperties();
}
if (initCallback) {
initCallback(channel);
}
channel.exec({type: QWebChannelMessageTypes.idle});
});
};
function QObject(name, data, webChannel)
{
this.__id__ = name;
webChannel.objects[name] = this;
// List of callbacks that get invoked upon signal emission
this.__objectSignals__ = {};
// Cache of all properties, updated when a notify signal is emitted
this.__propertyCache__ = {};
var object = this;
// ----------------------------------------------------------------------
this.unwrapQObject = function(response)
{
if (response instanceof Array) {
// support list of objects
return response.map(qobj => object.unwrapQObject(qobj))
}
if (!(response instanceof Object))
return response;
if (!response["__QObject*__"] || response.id === undefined) {
var jObj = {};
for (const propName of Object.keys(response)) {
jObj[propName] = object.unwrapQObject(response[propName]);
}
return jObj;
}
var objectId = response.id;
if (webChannel.objects[objectId])
return webChannel.objects[objectId];
if (!response.data) {
console.error("Cannot unwrap unknown QObject " + objectId + " without data.");
return;
}
var qObject = new QObject( objectId, response.data, webChannel );
qObject.destroyed.connect(function() {
if (webChannel.objects[objectId] === qObject) {
delete webChannel.objects[objectId];
// reset the now deleted QObject to an empty {} object
// just assigning {} though would not have the desired effect, but the
// below also ensures all external references will see the empty map
// NOTE: this detour is necessary to workaround QTBUG-40021
Object.keys(qObject).forEach(name => delete qObject[name]);
}
});
// here we are already initialized, and thus must directly unwrap the properties
qObject.unwrapProperties();
return qObject;
}
this.unwrapProperties = function()
{
for (const propertyIdx of Object.keys(object.__propertyCache__)) {
object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]);
}
}
function addSignal(signalData, isPropertyNotifySignal)
{
var signalName = signalData[0];
var signalIndex = signalData[1];
object[signalName] = {
connect: function(callback) {
if (typeof(callback) !== "function") {
console.error("Bad callback given to connect to signal " + signalName);
return;
}
object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
object.__objectSignals__[signalIndex].push(callback);
// only required for "pure" signals, handled separately for properties in propertyUpdate
if (isPropertyNotifySignal)
return;
// also note that we always get notified about the destroyed signal
if (signalName === "destroyed" || signalName === "destroyed()" || signalName === "destroyed(QObject*)")
return;
// and otherwise we only need to be connected only once
if (object.__objectSignals__[signalIndex].length == 1) {
webChannel.exec({
type: QWebChannelMessageTypes.connectToSignal,
object: object.__id__,
signal: signalIndex
});
}
},
disconnect: function(callback) {
if (typeof(callback) !== "function") {
console.error("Bad callback given to disconnect from signal " + signalName);
return;
}
object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
var idx = object.__objectSignals__[signalIndex].indexOf(callback);
if (idx === -1) {
console.error("Cannot find connection of signal " + signalName + " to " + callback.name);
return;
}
object.__objectSignals__[signalIndex].splice(idx, 1);
if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) {
// only required for "pure" signals, handled separately for properties in propertyUpdate
webChannel.exec({
type: QWebChannelMessageTypes.disconnectFromSignal,
object: object.__id__,
signal: signalIndex
});
}
}
};
}
/**
* Invokes all callbacks for the given signalname. Also works for property notify callbacks.
*/
function invokeSignalCallbacks(signalName, signalArgs)
{
var connections = object.__objectSignals__[signalName];
if (connections) {
connections.forEach(function(callback) {
callback.apply(callback, signalArgs);
});
}
}
this.propertyUpdate = function(signals, propertyMap)
{
// update property cache
for (const propertyIndex of Object.keys(propertyMap)) {
var propertyValue = propertyMap[propertyIndex];
object.__propertyCache__[propertyIndex] = this.unwrapQObject(propertyValue);
}
for (const signalName of Object.keys(signals)) {
// Invoke all callbacks, as signalEmitted() does not. This ensures the
// property cache is updated before the callbacks are invoked.
invokeSignalCallbacks(signalName, signals[signalName]);
}
}
this.signalEmitted = function(signalName, signalArgs)
{
invokeSignalCallbacks(signalName, this.unwrapQObject(signalArgs));
}
function addMethod(methodData)
{
var methodName = methodData[0];
var methodIdx = methodData[1];
// Fully specified methods are invoked by id, others by name for host-side overload resolution
var invokedMethod = methodName[methodName.length - 1] === ')' ? methodIdx : methodName
object[methodName] = function() {
var args = [];
var callback;
var errCallback;
for (var i = 0; i < arguments.length; ++i) {
var argument = arguments[i];
if (typeof argument === "function")
callback = argument;
else if (argument instanceof QObject && webChannel.objects[argument.__id__] !== undefined)
args.push({
"id": argument.__id__
});
else
args.push(argument);
}
var result;
// during test, webChannel.exec synchronously calls the callback
// therefore, the promise must be constucted before calling
// webChannel.exec to ensure the callback is set up
if (!callback && (typeof(Promise) === 'function')) {
result = new Promise(function(resolve, reject) {
callback = resolve;
errCallback = reject;
});
}
webChannel.exec({
"type": QWebChannelMessageTypes.invokeMethod,
"object": object.__id__,
"method": invokedMethod,
"args": args
}, function(response) {
if (response !== undefined) {
var result = object.unwrapQObject(response);
if (callback) {
(callback)(result);
}
} else if (errCallback) {
(errCallback)();
}
});
return result;
};
}
function bindGetterSetter(propertyInfo)
{
var propertyIndex = propertyInfo[0];
var propertyName = propertyInfo[1];
var notifySignalData = propertyInfo[2];
// initialize property cache with current value
// NOTE: if this is an object, it is not directly unwrapped as it might
// reference other QObject that we do not know yet
object.__propertyCache__[propertyIndex] = propertyInfo[3];
if (notifySignalData) {
if (notifySignalData[0] === 1) {
// signal name is optimized away, reconstruct the actual name
notifySignalData[0] = propertyName + "Changed";
}
addSignal(notifySignalData, true);
}
Object.defineProperty(object, propertyName, {
configurable: true,
get: function () {
var propertyValue = object.__propertyCache__[propertyIndex];
if (propertyValue === undefined) {
// This shouldn't happen
console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__);
}
return propertyValue;
},
set: function(value) {
if (value === undefined) {
console.warn("Property setter for " + propertyName + " called with undefined value!");
return;
}
object.__propertyCache__[propertyIndex] = value;
var valueToSend = value;
if (valueToSend instanceof QObject && webChannel.objects[valueToSend.__id__] !== undefined)
valueToSend = { "id": valueToSend.__id__ };
webChannel.exec({
"type": QWebChannelMessageTypes.setProperty,
"object": object.__id__,
"property": propertyIndex,
"value": valueToSend
});
}
});
}
// ----------------------------------------------------------------------
data.methods.forEach(addMethod);
data.properties.forEach(bindGetterSetter);
data.signals.forEach(function(signal) { addSignal(signal, false); });
Object.assign(object, data.enums);
}
//required for use with nodejs
if (typeof module === 'object') {
module.exports = {
QWebChannel: QWebChannel
};
}

View File

@ -26,6 +26,7 @@ var qrcExtensions = map[string]bool{
".gif": true, ".gif": true,
".json": true, ".json": true,
".mdwn": true, ".mdwn": true,
".html": true,
} }
func main() { func main() {