chore(@desktop/walletconnect): communication between qt desktop app and loaded wallet connect sdk is made via web channel
This commit is contained in:
parent
98207381da
commit
48c9e372dc
2
Makefile
2
Makefile
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -1,255 +1,71 @@
|
||||||
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();
|
||||||
|
|
||||||
|
wcCalls.getPairings((pairList) => {
|
||||||
for (let i = 0; i < pairList.length; i++) {
|
for (let i = 0; i < pairList.length; i++) {
|
||||||
pairings.append({
|
pairings.append({
|
||||||
active: pairList[i].active,
|
active: pairList[i].active,
|
||||||
|
@ -257,70 +73,266 @@ WebView {
|
||||||
expiry: pairList[i].expiry
|
expiry: pairList[i].expiry
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPairingTopicFromPairingUrl(url)
|
||||||
function isWaitingForSdk() {
|
{
|
||||||
return timer.running
|
if (!url.startsWith("wc:"))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const atIndex = url.indexOf("@");
|
||||||
|
if (atIndex < 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return url.slice(3, atIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateSdkCall(methodName, paramsStr, successState, errorState) {
|
QtObject {
|
||||||
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"
|
id: wcCalls
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
console.debug(`@dd WalletConnectSDK.wcCall.init; root.projectId: ${root.projectId}`)
|
||||||
|
|
||||||
|
webEngineView.runJavaScript(`wc.init("${root.projectId}")`, function(result) {
|
||||||
|
|
||||||
|
console.debug(`@dd WalletConnectSDK.wcCall.init; response: ${JSON.stringify(result, null, 2)}`)
|
||||||
|
|
||||||
|
if (result && !!result.error)
|
||||||
|
{
|
||||||
|
console.error("init: ", result.error)
|
||||||
}
|
}
|
||||||
function requestSdkAsync(jsCode) {
|
})
|
||||||
root.runJavaScript(jsCode,
|
|
||||||
function(result) {
|
|
||||||
timer.restart()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
webEngineView.runJavaScript(`wc.getPairings()`, function(result) {
|
||||||
timer.restart()
|
|
||||||
}
|
console.debug(`@dd WalletConnectSDK.wcCall.getPairings; response: ${JSON.stringify(result, null, 2)}`)
|
||||||
)
|
|
||||||
|
if (result)
|
||||||
|
{
|
||||||
|
if (!!result.error) {
|
||||||
|
console.error("getPairings: ", result.error)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
function startListeningForEvents() {
|
callback(result.result)
|
||||||
const jsCode = "
|
return
|
||||||
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,
|
function pair(pairLink) {
|
||||||
function(result) {
|
console.debug(`@dd WalletConnectSDK.wcCall.pair; pairLink: ${pairLink}`)
|
||||||
if (result) {
|
|
||||||
console.error("startListeningForEvents: processWCEvents error", result)
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
webEngineView.runJavaScript(`wc.pair("${pairLink}")`, function(result) {
|
||||||
|
if (result && !!result.error)
|
||||||
|
{
|
||||||
|
console.error("pair: ", result.error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
)
|
)
|
||||||
eventsTimer.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function init(projectId) {
|
function approvePairSession(sessionProposal, supportedNamespaces) {
|
||||||
d.requestSdkAsync(generateSdkCall("init", `"${projectId}"`, RequestCodes.SdkInitSuccess, RequestCodes.SdkInitError))
|
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 getPairings(projectId) {
|
function rejectPairSession(id) {
|
||||||
d.requestSdk("getPairings", `null`, RequestCodes.GetPairings, RequestCodes.GetPairingsError)
|
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
|
@ -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>
|
|
@ -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 {
|
||||||
|
result: window.wc.web3wallet.approveSession({
|
||||||
id,
|
id,
|
||||||
namespaces: approvedNamespaces,
|
namespaces: approvedNamespaces,
|
||||||
});
|
}),
|
||||||
|
error: ""
|
||||||
|
};
|
||||||
},
|
},
|
||||||
rejectPairSession: function (id) {
|
rejectPairSession: function (id) {
|
||||||
return window.wc.web3wallet.rejectSession({
|
return {
|
||||||
|
result: window.wc.web3wallet.rejectSession({
|
||||||
id: id,
|
id: id,
|
||||||
reason: getSdkError("USER_REJECTED"), // TODO USER_REJECTED_METHODS, USER_REJECTED_CHAINS, USER_REJECTED_EVENTS
|
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 {
|
||||||
|
result: window.wc.web3wallet.respondSessionRequest({
|
||||||
topic: topic,
|
topic: topic,
|
||||||
response: formatJsonRpcError(id, getSdkError(errorType)),
|
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;
|
||||||
|
|
||||||
|
if (!statusObject) {
|
||||||
|
reject(new Error("Unable to resolve statusObject"));
|
||||||
|
} else {
|
||||||
|
window.wc.statusObject = statusObject;
|
||||||
|
resolve();
|
||||||
}
|
}
|
||||||
const atIndex = url.indexOf("@");
|
});
|
||||||
if (atIndex < 0) {
|
});
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return url.slice(3, atIndex);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
|
Loading…
Reference in New Issue