diff --git a/src/app/provider/view.nim b/src/app/provider/view.nim index ff22b01f5e..0ddf6c33a4 100644 --- a/src/app/provider/view.nim +++ b/src/app/provider/view.nim @@ -1,5 +1,10 @@ import NimQml import ../../status/status +import ../../status/libstatus/types +import ../../status/libstatus/core +import ../../status/libstatus/settings as status_settings +import json +import sets QtObject: type Web3ProviderView* = ref object of QObject @@ -17,8 +22,85 @@ QtObject: 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 + proc web3AsyncReadOnly*(self: Web3ProviderView, data: JsonNode): JsonNode = + let messageId = data["messageId"] + let messageType = "web3-send-async-callback" + let payloadId = data["payload"]["id"] + let rpcMethod = data["payload"]["method"].getStr() + + let authMethods = toHashSet(["eth_accounts", "eth_coinbase", "eth_sendTransaction", "eth_sign", "keycard_signTypedData", "eth_signTypedData", "personal_sign", "personal_ecRecover"]) + let signMethods = toHashSet(["eth_sendTransaction", "personal_sign", "eth_signTypedData", "eth_signTypedData_v3"]) + let accMethods = toHashSet(["eth_accounts", "eth_coinbase"]) + + if authMethods.contains(rpcMethod): # TODO: && if the dapp does not have the "web3" permission: + return %* { + "type": messageType, + "messageId": messageId, + "error": { + "code": 4100 + } + } + + if signMethods.contains(rpcMethod): + return %* { # TODO: send transaction, return transaction hash, etc etc. Disabled in the meantime + "type": messageType, + "messageId": messageId, + "error": { + "code": 4100 + } + } + + if accMethods.contains(rpcMethod): + let dappAddress = status_settings.getSetting[string](Setting.DappsAddress) + return %* { + "type": messageType, + "messageId": messageId, + "payload": { + "jsonrpc": "2.0", + "id": payloadId, + "result": if rpcMethod == "eth_coinbase": newJString(dappAddress) else: %*[dappAddress] + } + } + + let rpcResult = callRPC($data["payload"]) + + return %* { + "type": messageType, + "messageId": messageId, + "error": (if rpcResult == "": newJString("web3-response-error") else: newJNull()), + "result": rpcResult.parseJson + } + + + proc apiRequest*(self: Web3ProviderView, request: JsonNode): JsonNode = + # TODO: Do a proper implementation. Must ask for approval from the user. + # Probably this should happen in BrowserLayout.qml + + let permission = request{"permission"}.getStr() + var data:JsonNode; + if permission == "web3": + data = %* [status_settings.getSetting[string](Setting.DappsAddress, "0x0000000000000000000000000000000000000000")] + + if permission == "contact-code": + data = %* status_settings.getSetting[string](Setting.PublicKey, "0x0") + + result = %* { + "type": "api-response", + "isAllowed": true, # TODO + "permission": permission, + "messageId": request["messageId"].getInt(), + "data": data + } + + proc postMessage*(self: Web3ProviderView, message: string): string {.slot.} = + let data = message.parseJson + case data{"type"}.getStr(): + of "web3-send-async-read-only": $self.web3AsyncReadOnly(data) + of "history-state-changed": """{"type":"TODO-IMPLEMENT-THIS"}""" ############# TODO: + of "api-request": $self.apiRequest(data) + else: """{"type":"TODO-IMPLEMENT-THIS"}""" ##################### TODO: + + proc getNetworkId*(self: Web3ProviderView): int {.slot.} = getCurrentNetworkDetails().config.networkId + + QtProperty[int] networkId: + read = getNetworkId \ No newline at end of file diff --git a/src/status/libstatus/types.nim b/src/status/libstatus/types.nim index aa3b79fb9a..e860867d10 100644 --- a/src/status/libstatus/types.nim +++ b/src/status/libstatus/types.nim @@ -158,6 +158,7 @@ type Networks_CurrentNetwork = "networks/current-network" NodeConfig = "node-config" PublicKey = "public-key" + DappsAddress = "dapps-address" Stickers_PacksInstalled = "stickers/packs-installed" Stickers_Recent = "stickers/recent-stickers" WalletRootAddress = "wallet-root-address" diff --git a/ui/app/AppLayouts/Browser/BrowserLayout.qml b/ui/app/AppLayouts/Browser/BrowserLayout.qml index 1005a12196..892fe22df4 100644 --- a/ui/app/AppLayouts/Browser/BrowserLayout.qml +++ b/ui/app/AppLayouts/Browser/BrowserLayout.qml @@ -18,13 +18,13 @@ Item { userScripts: [ WebEngineScript { injectionPoint: WebEngineScript.DocumentCreation - sourceUrl: Qt.resolvedUrl("provider.js") + name: "QWebChannel" + sourceUrl: "qrc:///qtwebchannel/qwebchannel.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" + sourceUrl: Qt.resolvedUrl("provider.js") worldId: WebEngineScript.MainWorld // TODO: check https://doc.qt.io/qt-5/qml-qtwebengine-webenginescript.html#worldId-prop } ] @@ -37,10 +37,10 @@ Item { signal web3Response(string data); function postMessage(data){ - console.log("Calling nim web3provider with: ", data); - var result = web3Provider.postMessage(data); - web3Response(result); + web3Response(web3Provider.postMessage(data)); } + + property int networkId: web3Provider.networkId } WebChannel { @@ -52,7 +52,7 @@ Item { id: browserContainer anchors.fill: parent profile: webProfile - url: "https://status-im.github.io/dapp/" + url: "https://app.uniswap.org/#/" 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 diff --git a/ui/app/AppLayouts/Browser/provider.js b/ui/app/AppLayouts/Browser/provider.js index 82e9fd640a..ae8373a327 100644 --- a/ui/app/AppLayouts/Browser/provider.js +++ b/ui/app/AppLayouts/Browser/provider.js @@ -3,280 +3,252 @@ // https://github.com/status-im/status-react/blob/f9fb4d6974138a276b0cdcc6e4ea1611063e70ca/resources/js/provider.js if(typeof EthereumProvider === "undefined"){ - var callbackId = 0; - var callbacks = {}; + let callbackId = 0; + let 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) - }); - }); - } + const onMessage = function(message){ + const data = JSON.parse(message); + const id = data.messageId; + const callback = callbacks[id]; - var bridgeSend = function (data) { - backend.postMessage(JSON.stringify(data)) - } + if (callback) { + if (data.type === "api-response") { + if (data.permission == "qr-code") { + qrCodeResponse(data, callback); // TODO: are we going to support the qr-code permission? + } 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); + } + } + } + } + + let backend; + new QWebChannel(qt.webChannelTransport, function(channel) { + backend = channel.objects.backend; + backend.web3Response.connect(onMessage); + }); - var history = window.history; - var pushState = history.pushState; - history.pushState = function(state) { + const bridgeSend = data => backend.postMessage(JSON.stringify(data)); + + let history = window.history; + let pushState = history.pushState; + history.pushState = function(state) { setTimeout(function () { - bridgeSend({ - type: 'history-state-changed', - navState: { url: location.href, title: document.title } - }); + 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 || {}; + }; + function sendAPIrequest(permission, params) { + const messageId = callbackId++; + 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){ + const result = data.data; + const 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); + + 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, backend.networkId) + } 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."); + + const 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"); + + const syncResponse = getSyncResponse({ method: method }); + if (syncResponse) { + return new Promise(function (resolve, reject) { + resolve(syncResponse.result); + }); + } + + const messageId = callbackId++; + const 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) { - params['resolve'] = resolve; - params['reject'] = reject; - callbacks[messageId] = params; + callbacks[messageId] = { + beta: true, + resolve: resolve, + reject: reject, + }; }); - } + }; - 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 = []) - { + // (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) {}) + // (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); - } - }; + const syncResponse = getSyncResponse(payload); + if (syncResponse) return syncResponse; + + return web3Response(payload, null); + }; - // (DEPRECATED) Support for legacy sendAsync method - EthereumProvider.prototype.sendAsync = function (payload, callback) - { - var syncResponse = getSyncResponse(payload); - if (syncResponse && callback) { + // (DEPRECATED) Support for legacy sendAsync method + EthereumProvider.prototype.sendAsync = function (payload, callback) { + const syncResponse = getSyncResponse(payload); + if (syncResponse && callback) { callback(null, syncResponse); - } - else - { - var messageId = callbackId++; + } else { + const 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]}); - } + if (Array.isArray(payload)) { + callbacks[messageId] = { + num: payload.length, + results: [], + callback: callback, + }; + + for (let 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, + }); } - else - { - callbacks[messageId] = {callback: callback}; - bridgeSend({type: 'web3-send-async-read-only', - messageId: messageId, - payload: payload}); - } - } - }; + } + }; } window.ethereum = new EthereumProvider();