diff --git a/src/app/provider/core.nim b/src/app/provider/core.nim new file mode 100644 index 0000000000..8311a5fce7 --- /dev/null +++ b/src/app/provider/core.nim @@ -0,0 +1,25 @@ +import NimQml, chronicles +import ../../status/signals/types +import ../../status/status +import view + +logScope: + topics = "web3-provider" + +type Web3ProviderController* = ref object + status*: Status + view*: Web3ProviderView + variant*: QVariant + +proc newController*(status: Status): Web3ProviderController = + result = Web3ProviderController() + result.status = status + result.view = newWeb3ProviderView(status) + result.variant = newQVariant(result.view) + +proc delete*(self: Web3ProviderController) = + delete self.variant + delete self.view + +proc init*(self: Web3ProviderController) = + discard diff --git a/src/app/provider/view.nim b/src/app/provider/view.nim new file mode 100644 index 0000000000..ff22b01f5e --- /dev/null +++ b/src/app/provider/view.nim @@ -0,0 +1,24 @@ +import NimQml +import ../../status/status + +QtObject: + type Web3ProviderView* = ref object of QObject + status*: Status + + proc setup(self: Web3ProviderView) = + self.QObject.setup + + proc delete*(self: Web3ProviderView) = + self.QObject.delete + + proc newWeb3ProviderView*(status: Status): Web3ProviderView = + new(result, delete) + result = Web3ProviderView() + result.status = status + result.setup + + proc postMessage*(self: Web3ProviderView, data: string): string {.slot.} = + # TODO: implement code from status-react/src/status_im/browser/core.cljs + echo "===========================================" + echo "Message received from JS web3 provider: ", data + return "Hello World!" # This can only be seen in chrome devtools \ No newline at end of file diff --git a/src/nim_status_client.nim b/src/nim_status_client.nim index 808fb1c422..18d784b8f5 100644 --- a/src/nim_status_client.nim +++ b/src/nim_status_client.nim @@ -7,6 +7,7 @@ import app/utilsView/core as utilsView import app/profile/core as profile import app/onboarding/core as onboarding import app/login/core as login +import app/provider/core as provider import status/signals/core as signals import status/libstatus/types import nim_status @@ -81,6 +82,9 @@ proc mainProc() = var profile = profile.newController(status, changeLanguage) engine.setRootContextProperty("profileModel", profile.variant) + var provider = provider.newController(status) + engine.setRootContextProperty("web3Provider", provider.variant) + var login = login.newController(status) var onboarding = onboarding.newController(status) @@ -108,6 +112,7 @@ proc mainProc() = defer: error "TODO: if user is logged in, logout" + provider.delete() engine.delete() app.delete() signalController.delete() diff --git a/ui/app/AppLayouts/Browser/BrowserLayout.qml b/ui/app/AppLayouts/Browser/BrowserLayout.qml index a1100028ad..1005a12196 100644 --- a/ui/app/AppLayouts/Browser/BrowserLayout.qml +++ b/ui/app/AppLayouts/Browser/BrowserLayout.qml @@ -1,25 +1,64 @@ import QtQuick 2.13 import QtQuick.Layouts 1.13 -import QtWebView 1.14 +import QtWebEngine 1.10 +import QtWebChannel 1.13 Item { id: browserView - x: 0 - y: 0 Layout.fillHeight: true Layout.fillWidth: true - WebView { + // TODO: example qml webbrowser available here: + // https://doc.qt.io/qt-5/qtwebengine-webengine-quicknanobrowser-example.html + + WebEngineProfile { + id: webProfile + offTheRecord: true // Private Mode on + persistentCookiesPolicy: WebEngineProfile.NoPersistentCookies + userScripts: [ + WebEngineScript { + injectionPoint: WebEngineScript.DocumentCreation + sourceUrl: Qt.resolvedUrl("provider.js") + worldId: WebEngineScript.MainWorld // TODO: check https://doc.qt.io/qt-5/qml-qtwebengine-webenginescript.html#worldId-prop + }, + WebEngineScript { + injectionPoint: WebEngineScript.DocumentCreation + name: "QWebChannel" + sourceUrl: "qrc:///qtwebchannel/qwebchannel.js" + worldId: WebEngineScript.MainWorld // TODO: check https://doc.qt.io/qt-5/qml-qtwebengine-webenginescript.html#worldId-prop + } + ] + } + + QtObject { + id: provider + WebChannel.id: "backend" + + signal web3Response(string data); + + function postMessage(data){ + console.log("Calling nim web3provider with: ", data); + var result = web3Provider.postMessage(data); + web3Response(result); + } + } + + WebChannel { + id: channel + registeredObjects: [provider] + } + + WebEngineView { id: browserContainer - anchors.top: parent.top - anchors.topMargin: 0 - anchors.bottom: parent.bottom - anchors.bottomMargin: 0 - anchors.right: parent.right - anchors.rightMargin: 0 - anchors.left: parent.left - anchors.leftMargin: 0 - url: "https://dap.ps/" + anchors.fill: parent + profile: webProfile + url: "https://status-im.github.io/dapp/" + webChannel: channel + onNewViewRequested: function(request) { + // TODO: rramos: tabs can be handled here. see: https://doc.qt.io/qt-5/qml-qtwebengine-webengineview.html#newViewRequested-signal + // In the meantime, I'm opening the content in the same webengineview + request.openIn(browserContainer) + } } } diff --git a/ui/app/AppLayouts/Browser/provider.js b/ui/app/AppLayouts/Browser/provider.js new file mode 100644 index 0000000000..82e9fd640a --- /dev/null +++ b/ui/app/AppLayouts/Browser/provider.js @@ -0,0 +1,283 @@ +(function(){ + // Based on + // https://github.com/status-im/status-react/blob/f9fb4d6974138a276b0cdcc6e4ea1611063e70ca/resources/js/provider.js + + if(typeof EthereumProvider === "undefined"){ + var callbackId = 0; + var callbacks = {}; + + + var backend; + window.onload = function(){ + new QWebChannel(qt.webChannelTransport, function(channel) { + backend = channel.objects.backend; + backend.web3Response.connect(function(data) { + // This can only be seen in chrome devtools + console.log("Received response from nim provider!!!!:"); + console.log(data); + // TODO: implement the code from ReactNativeWebView.onMessage (~line 95) + }); + }); + } + + var bridgeSend = function (data) { + backend.postMessage(JSON.stringify(data)) + } + + var history = window.history; + var pushState = history.pushState; + history.pushState = function(state) { + setTimeout(function () { + bridgeSend({ + type: 'history-state-changed', + navState: { url: location.href, title: document.title } + }); + }, 100); + return pushState.apply(history, arguments); + }; + + function sendAPIrequest(permission, params) { + var messageId = callbackId++; + var params = params || {}; + + bridgeSend({ + type: 'api-request', + permission: permission, + messageId: messageId, + params: params + }); + + return new Promise(function (resolve, reject) { + params['resolve'] = resolve; + params['reject'] = reject; + callbacks[messageId] = params; + }); + } + + function qrCodeResponse(data, callback){ + var result = data.data; + var regex = new RegExp(callback.regex); + if (!result) { + if (callback.reject) { + callback.reject(new Error("Cancelled")); + } + } + else if (regex.test(result)) { + if (callback.resolve) { + callback.resolve(result); + } + } else { + if (callback.reject) { + callback.reject(new Error("Doesn't match")); + } + } + } + + function Unauthorized() { + this.name = "Unauthorized"; + this.id = 4100; + this.code = 4100; + this.message = "The requested method and/or account has not been authorized by the user."; + } + Unauthorized.prototype = Object.create(Error.prototype); + + function UserRejectedRequest() { + this.name = "UserRejectedRequest"; + this.id = 4001; + this.code = 4001; + this.message = "The user rejected the request."; + } + UserRejectedRequest.prototype = Object.create(Error.prototype); + + + /* + TODO: + ReactNativeWebView.onMessage = function (message) + { + data = JSON.parse(message); + var id = data.messageId; + var callback = callbacks[id]; + + if (callback) { + if (data.type === "api-response") { + if (data.permission == 'qr-code'){ + qrCodeResponse(data, callback); + } else if (data.isAllowed) { + if (data.permission == 'web3') { + window.statusAppcurrentAccountAddress = data.data[0]; + } + callback.resolve(data.data); + } else { + callback.reject(new UserRejectedRequest()); + } + } + else if (data.type === "web3-send-async-callback") + { + if (callback.beta) + { + if (data.error) + { + if (data.error.code == 4100) + callback.reject(new Unauthorized()); + else + callback.reject(data.error); + } + else + { + callback.resolve(data.result.result); + } + } + else if (callback.results) + { + callback.results.push(data.error || data.result); + if (callback.results.length == callback.num) + callback.callback(undefined, callback.results); + } + else + { + callback.callback(data.error, data.result); + } + } + } + }; + */ + + function web3Response (payload, result){ + return {id: payload.id, + jsonrpc: "2.0", + result: result}; + } + + function getSyncResponse (payload) { + if (payload.method == "eth_accounts" && (typeof window.statusAppcurrentAccountAddress !== "undefined")) { + return web3Response(payload, [window.statusAppcurrentAccountAddress]) + } else if (payload.method == "eth_coinbase" && (typeof window.statusAppcurrentAccountAddress !== "undefined")) { + return web3Response(payload, window.statusAppcurrentAccountAddress) + } else if (payload.method == "net_version" || payload.method == "eth_chainId"){ + return web3Response(payload, window.statusAppNetworkId) + } else if (payload.method == "eth_uninstallFilter"){ + return web3Response(payload, true); + } else { + return null; + } + } + + var StatusAPI = function () {}; + + StatusAPI.prototype.getContactCode = function () { + return sendAPIrequest('contact-code'); + }; + + var EthereumProvider = function () {}; + + EthereumProvider.prototype.isStatus = true; + EthereumProvider.prototype.status = new StatusAPI(); + EthereumProvider.prototype.isConnected = function () { return true; }; + + EthereumProvider.prototype.enable = function () { + return sendAPIrequest('web3'); + }; + + EthereumProvider.prototype.scanQRCode = function (regex) { + return sendAPIrequest('qr-code', {regex: regex}); + }; + + EthereumProvider.prototype.request = function (requestArguments) + { + if (!requestArguments) { + return new Error('Request is not valid.'); + } + var method = requestArguments.method; + + if (!method) { + return new Error('Request is not valid.'); + } + + //Support for legacy send method + if (typeof method !== 'string') { + return this.sendSync(method); + } + + if (method == 'eth_requestAccounts'){ + return sendAPIrequest('web3'); + } + + var syncResponse = getSyncResponse({method: method}); + if (syncResponse){ + return new Promise(function (resolve, reject) { + resolve(syncResponse.result); + }); + } + + var messageId = callbackId++; + var payload = {id: messageId, + jsonrpc: "2.0", + method: method, + params: requestArguments.params}; + + bridgeSend({type: 'web3-send-async-read-only', + messageId: messageId, + payload: payload}); + + return new Promise(function (resolve, reject) { + callbacks[messageId] = {beta: true, + resolve: resolve, + reject: reject}; + }); + }; + + // (DEPRECATED) Support for legacy send method + EthereumProvider.prototype.send = function (method, params = []) + { + return this.request({method: method, params: params}); + } + + // (DEPRECATED) Support for legacy sendSync method + EthereumProvider.prototype.sendSync = function (payload) + { + if (payload.method == "eth_uninstallFilter"){ + this.sendAsync(payload, function (res, err) {}) + } + var syncResponse = getSyncResponse(payload); + if (syncResponse){ + return syncResponse; + } else { + return web3Response(payload, null); + } + }; + + // (DEPRECATED) Support for legacy sendAsync method + EthereumProvider.prototype.sendAsync = function (payload, callback) + { + var syncResponse = getSyncResponse(payload); + if (syncResponse && callback) { + callback(null, syncResponse); + } + else + { + var messageId = callbackId++; + + if (Array.isArray(payload)) + { + callbacks[messageId] = {num: payload.length, + results: [], + callback: callback}; + for (var i in payload) { + bridgeSend({type: 'web3-send-async-read-only', + messageId: messageId, + payload: payload[i]}); + } + } + else + { + callbacks[messageId] = {callback: callback}; + bridgeSend({type: 'web3-send-async-read-only', + messageId: messageId, + payload: payload}); + } + } + }; + } + + window.ethereum = new EthereumProvider(); +})(); \ No newline at end of file diff --git a/ui/nim-status-client.pro b/ui/nim-status-client.pro index c1dbd7f398..2eeec3bb51 100644 --- a/ui/nim-status-client.pro +++ b/ui/nim-status-client.pro @@ -1,4 +1,5 @@ QT += quick +QT += webchannel # The following define makes your compiler emit warnings if you use # any Qt feature that has been marked deprecated (the exact warnings