From a20facd895647535e284e310e35d4fccefcd35f0 Mon Sep 17 00:00:00 2001 From: Andrey Shovkoplyas Date: Wed, 5 Sep 2018 18:31:57 +0300 Subject: [PATCH] web3 opt-in updates Signed-off-by: Andrey Shovkoplyas --- resources/js/web3_init.js | 33 ++-- resources/js/web3_opt_in.js | 152 ++++++++++++++++++ resources/js/webview.js | 24 +-- src/status_im/accounts/core.cljs | 6 + src/status_im/constants.cljs | 8 +- src/status_im/events.cljs | 5 + src/status_im/models/browser.cljs | 15 ++ src/status_im/ui/screens/browser/events.cljs | 22 ++- src/status_im/ui/screens/browser/views.cljs | 17 +- .../ui/screens/profile/user/views.cljs | 80 ++++----- src/status_im/utils/js_resources.cljs | 4 + translations/en.json | 3 +- 12 files changed, 277 insertions(+), 92 deletions(-) create mode 100644 resources/js/web3_opt_in.js diff --git a/resources/js/web3_init.js b/resources/js/web3_init.js index 675e663594..b16151dce5 100644 --- a/resources/js/web3_init.js +++ b/resources/js/web3_init.js @@ -6,6 +6,17 @@ function bridgeSend(data){ WebViewBridge.send(JSON.stringify(data)); } +window.addEventListener('message', function (event) { + if (!event.data || !event.data.type) { return; } + if (event.data.type === 'STATUS_API_REQUEST') { + bridgeSend({ + type: 'status-api-request', + permissions: event.data.permissions, + host: window.location.hostname + }); + } +}); + WebViewBridge.onMessage = function (message) { data = JSON.parse(message); @@ -14,16 +25,9 @@ WebViewBridge.onMessage = function (message) { else if (data.type === "status-api-success") { - if (data.keys == 'WEB3') - { - window.dispatchEvent(new CustomEvent('ethereumprovider', { detail: { ethereum: new StatusHttpProvider("")} })); - } - else - { - window.dispatchEvent(new CustomEvent('statusapi', { detail: { permissions: data.keys, - data: data.data - } })); - } + window.dispatchEvent(new CustomEvent('statusapi', { detail: { permissions: data.keys, + data: data.data + } })); } else if (data.type === "web3-send-async-callback") @@ -113,6 +117,10 @@ StatusHttpProvider.prototype.sendAsync = function (payload, callback) { } }; + +StatusHttpProvider.prototype.enable = function () { + return new Promise(function (resolve, reject) { setTimeout(resolve, 1000);}); +}; } var protocol = window.location.protocol @@ -120,7 +128,8 @@ if (typeof web3 === "undefined") { //why do we need this condition? if (protocol == "https:" || protocol == "http:") { console.log("StatusHttpProvider"); - web3 = new Web3(new StatusHttpProvider()); - web3.eth.defaultAccount = currentAccountAddress; // currentAccountAddress - injected from status-react + ethereum = new StatusHttpProvider(); + web3 = new Web3(ethereum); + web3.eth.defaultAccount = currentAccountAddress; } } diff --git a/resources/js/web3_opt_in.js b/resources/js/web3_opt_in.js new file mode 100644 index 0000000000..c6466f96e8 --- /dev/null +++ b/resources/js/web3_opt_in.js @@ -0,0 +1,152 @@ +if(typeof ReadOnlyProvider === "undefined"){ +var callbackId = 0; +var callbacks = {}; +var ethereumPromise = {}; + +function bridgeSend(data){ + WebViewBridge.send(JSON.stringify(data)); +} + +window.addEventListener('message', function (event) { + if (!event.data || !event.data.type) { return; } + if (event.data.type === 'STATUS_API_REQUEST') { + bridgeSend({ + type: 'status-api-request', + permissions: event.data.permissions, + host: window.location.hostname + }); + } +}); + +WebViewBridge.onMessage = function (message) { + data = JSON.parse(message); + + if (data.type === "navigate-to-blank") + window.location.href = "about:blank"; + + else if (data.type === "status-api-success") + { + if (data.keys == 'WEB3') + { + ethereumPromise.allowed = true; + window.currentAccountAddress = data.data["WEB3"]; + ethereumPromise.resolve(); + } + else + { + window.dispatchEvent(new CustomEvent('statusapi', { detail: { permissions: data.keys, + data: data.data + } })); + } + } + + else if (data.type === "web3-permission-request-denied") + { + ethereumPromise.reject(new Error("Denied")); + } + + else if (data.type === "web3-send-async-callback") + { + var id = data.messageId; + var callback = callbacks[id]; + if (callback) { + 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) { + console.log("getSyncResponse " + payload.method + " !") + if (payload.method == "eth_accounts" && currentAccountAddress){ + return web3Response(payload, [currentAccountAddress]) + } else if (payload.method == "eth_coinbase" && currentAccountAddress){ + return web3Response(payload, currentAccountAddress) + } else if (payload.method == "net_version"){ + return web3Response(payload, networkId) + } else if (payload.method == "eth_uninstallFilter"){ + return web3Response(payload, true); + } else { + return null; + } +} + +var ReadOnlyProvider = function () {}; + +ReadOnlyProvider.prototype.isConnected = function () { return true; }; + +ReadOnlyProvider.prototype.send = 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); + } +}; + +ReadOnlyProvider.prototype.enable = function () { + bridgeSend({ + type: 'status-api-request', + permissions: ['WEB3'], + host: window.location.hostname + }); + return new Promise(function (resolve, reject) { + ethereumPromise.resolve = resolve; + ethereumPromise.reject = reject; + }); +}; + +ReadOnlyProvider.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], + host: window.location.hostname}); + } + } + else + { + callbacks[messageId] = {callback: callback}; + bridgeSend({type: 'web3-send-async-read-only', + messageId: messageId, + payload: payload, + host: window.location.hostname}); + } + + } +}; + +} + +console.log("ReadOnlyProvider"); +ethereum = new ReadOnlyProvider(); \ No newline at end of file diff --git a/resources/js/webview.js b/resources/js/webview.js index a3f19d6471..ed65ffce0b 100644 --- a/resources/js/webview.js +++ b/resources/js/webview.js @@ -1,8 +1,4 @@ (function () { - function bridgeSend(data){ - WebViewBridge.send(JSON.stringify(data)); - } - var history = window.history; var pushState = history.pushState; history.pushState = function(state) { @@ -14,22 +10,4 @@ }, 100); return pushState.apply(history, arguments); }; - - window.addEventListener('message', function (event) { - if (!event.data || !event.data.type) { return; } - if (event.data.type === 'STATUS_API_REQUEST') { - bridgeSend({ - type: 'status-api-request', - permissions: event.data.permissions, - host: window.location.hostname - }); - } - else if (event.data.type === 'ETHEREUM_PROVIDER_REQUEST') { - bridgeSend({ - type: 'status-api-request', - permissions: ['WEB3'], - host: window.location.hostname - }); - } - }); -}()); +}()); \ No newline at end of file diff --git a/src/status_im/accounts/core.cljs b/src/status_im/accounts/core.cljs index f6c06d0899..1c3de929b0 100644 --- a/src/status_im/accounts/core.cljs +++ b/src/status_im/accounts/core.cljs @@ -43,3 +43,9 @@ (if dev-mode? {:dev-server/start nil} {:dev-server/stop nil}))) + +(defn switch-web3-opt-in-mode [opt-in {:keys [db] :as cofx}] + (let [settings (get-in db [:account/account :settings])] + (accounts.update/update-settings + (assoc settings :web3-opt-in? opt-in) + cofx))) diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index 5a856a1ab7..83613ed2a6 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -176,9 +176,9 @@ (def ^:const send-transaction-failed-parse-response 1) (def ^:const send-transaction-failed-parse-params 2) -(def ^:const send-transaction-no-account-selected 3) -(def ^:const send-transaction-invalid-tx-sender 4) -(def ^:const send-transaction-err-decrypt 5) +(def ^:const send-transaction-no-account-selected 3) +(def ^:const send-transaction-invalid-tx-sender 4) +(def ^:const send-transaction-err-decrypt 5) (def ^:const web3-send-transaction "eth_sendTransaction") (def ^:const web3-personal-sign "personal_sign") @@ -193,6 +193,8 @@ (def ^:const dapp-permission-web3 "WEB3") (def ^:const status-api-success "status-api-success") (def ^:const status-api-request "status-api-request") +(def ^:const web3-permission-request-denied "web3-permission-request-denied") (def ^:const history-state-changed "history-state-changed") (def ^:const web3-send-async "web3-send-async") +(def ^:const web3-send-async-read-only "web3-send-async-read-only") (def ^:const web3-send-async-callback "web3-send-async-callback") diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index b2ec4cdc1f..11664aa71f 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -107,6 +107,11 @@ (fn [cofx [_ dev-mode?]] (accounts/switch-dev-mode dev-mode? cofx))) +(handlers/register-handler-fx + :accounts.ui/web3-opt-in-mode-switched + (fn [cofx [_ opt-in]] + (accounts/switch-web3-opt-in-mode opt-in cofx))) + (handlers/register-handler-fx :accounts.ui/wallet-set-up-confirmed (fn [cofx [_ modal?]] diff --git a/src/status_im/models/browser.cljs b/src/status_im/models/browser.cljs index 0e857d4192..9d9701e6b2 100644 --- a/src/status_im/models/browser.cljs +++ b/src/status_im/models/browser.cljs @@ -123,6 +123,10 @@ :keys (keys permissions-allowed)} (:webview-bridge db)]) + (and (zero? (count permissions-allowed)) (= constants/dapp-permission-web3 (first requested-permissions))) + (assoc :send-to-bridge-fx [{:type constants/web3-permission-request-denied} + (:webview-bridge db)]) + true (assoc :dispatch [:check-permissions-queue])))) @@ -148,6 +152,17 @@ :error %1 :result %2}])]})) +(defn web3-send-async-read-only [dapp-name {:keys [method] :as payload} message-id {:keys [db] :as cofx}] + (let [{:dapps/keys [permissions]} db] + (if (and (#{"eth_accounts" "eth_coinbase" "eth_sendTransaction" "eth_sign" + "eth_signTypedData" "personal_sign" "personal_ecRecover"} method) + (not (some #{"WEB3"} (get-in permissions [dapp-name :permissions])))) + {:dispatch [:send-to-bridge + {:type constants/web3-send-async-callback + :messageId message-id + :error "Denied"}]} + (web3-send-async payload message-id cofx)))) + (defn initialize-browsers [{:keys [db all-stored-browsers]}] (let [browsers (into {} (map #(vector (:browser-id %) %) all-stored-browsers))] diff --git a/src/status_im/ui/screens/browser/events.cljs b/src/status_im/ui/screens/browser/events.cljs index 9ce89bd158..7de4e25817 100644 --- a/src/status_im/ui/screens/browser/events.cljs +++ b/src/status_im/ui/screens/browser/events.cljs @@ -14,7 +14,8 @@ [status-im.utils.types :as types] [status-im.utils.universal-links.core :as utils.universal-links] [taoensso.timbre :as log] - [status-im.utils.ethereum.resolver :as resolver])) + [status-im.utils.ethereum.resolver :as resolver] + [status-im.utils.ethereum.core :as ethereum])) (re-frame/reg-fx :browse @@ -127,7 +128,9 @@ {:keys [browser-id]} options browser (get browsers browser-id) data (types/json->clj message) - {{:keys [url]} :navState :keys [type host permissions payload messageId]} data] + {{:keys [url]} :navState :keys [type host permissions payload messageId]} data + {:keys [dapp? name]} browser + dapp-name (if dapp? name host)] (cond (and (= type constants/history-state-changed) platform/ios? (not= "about:blank" url)) @@ -136,12 +139,13 @@ (= type constants/web3-send-async) (model/web3-send-async payload messageId cofx) + (= type constants/web3-send-async-read-only) + (model/web3-send-async-read-only dapp-name payload messageId cofx) + (= type constants/status-api-request) - (let [{:keys [dapp? name]} browser - dapp-name (if dapp? name host)] - {:db (update-in db [:browser/options :permissions-queue] conj {:dapp-name dapp-name - :permissions permissions}) - :dispatch [:check-permissions-queue]}))))) + {:db (update-in db [:browser/options :permissions-queue] conj {:dapp-name dapp-name + :permissions permissions}) + :dispatch [:check-permissions-queue]})))) (handlers/register-handler-fx :check-permissions-queue @@ -158,7 +162,9 @@ :index 0 :user-permissions (get-in db [:dapps/permissions dapp-name :permissions]) :requested-permissions permissions - :permissions-data {constants/dapp-permission-contact-code (:public-key account)}}))))))) + :permissions-data {constants/dapp-permission-contact-code (:public-key account) + constants/dapp-permission-web3 (ethereum/normalized-address + (:address account))}}))))))) (handlers/register-handler-fx :next-dapp-permission diff --git a/src/status_im/ui/screens/browser/views.cljs b/src/status_im/ui/screens/browser/views.cljs index 8c42eecb6f..b790441acb 100644 --- a/src/status_im/ui/screens/browser/views.cljs +++ b/src/status_im/ui/screens/browser/views.cljs @@ -114,14 +114,15 @@ (views/defview browser [] (views/letsubs [webview (atom nil) - {:keys [address]} [:get-current-account] + {:keys [address settings]} [:get-current-account] {:keys [browser-id dapp? name] :as browser} [:get-current-browser] {:keys [error? loading? url-editing? show-tooltip show-permission resolving?]} [:get :browser/options] rpc-url [:get :rpc-url] network-id [:get-network-id]] (let [can-go-back? (model/can-go-back? browser) can-go-forward? (model/can-go-forward? browser) - url (model/get-current-url browser)] + url (model/get-current-url browser) + opt-in? (:web3-opt-in? settings)] [react/view styles/browser [status-bar/status-bar] [toolbar webview error? url browser browser-id url-editing?] @@ -142,12 +143,14 @@ :on-load #(re-frame/dispatch [:update-browser-options {:error? false}]) :on-error #(re-frame/dispatch [:update-browser-options {:error? true :loading? false}]) - :injected-on-start-loading-java-script (str js-res/web3 + :injected-on-start-loading-java-script (str (not opt-in?) js-res/web3 (get-inject-js url) - (js-res/web3-init - rpc-url - (ethereum/normalized-address address) - (str network-id))) + (if opt-in? + (js-res/web3-opt-in-init (str network-id)) + (js-res/web3-init + rpc-url + (ethereum/normalized-address address) + (str network-id)))) :injected-java-script js-res/webview-js}] (when (or loading? resolving?) [react/view styles/web-view-loading diff --git a/src/status_im/ui/screens/profile/user/views.cljs b/src/status_im/ui/screens/profile/user/views.cljs index 474b421001..6f98183e10 100644 --- a/src/status_im/ui/screens/profile/user/views.cljs +++ b/src/status_im/ui/screens/profile/user/views.cljs @@ -136,48 +136,52 @@ :hide-arrow? true :action-fn #(re-frame/dispatch [:accounts.logout.ui/logout-pressed])}]]]])) -(defview advanced-settings [{:keys [network networks dev-mode?]} on-show] - (letsubs [{:keys [sharing-usage-data?]} [:get-current-account]] - {:component-did-mount on-show} - [react/view - (when (and config/extensions-enabled? dev-mode?) - [profile.components/settings-item - {:label-kw :t/extensions - :action-fn #(re-frame/dispatch [:navigate-to :extensions-settings]) - :accessibility-label :extensions-button}]) - (when dev-mode? - [profile.components/settings-item - {:label-kw :t/network - :value (get-in networks [network :name]) - :action-fn #(re-frame/dispatch [:navigate-to :network-settings]) - :accessibility-label :network-button}]) - [profile.components/settings-item-separator] +(defview advanced-settings [{:keys [network networks dev-mode? settings]} on-show] + {:component-did-mount on-show} + [react/view + (when (and config/extensions-enabled? dev-mode?) [profile.components/settings-item - {:label-kw :t/offline-messaging - :action-fn #(re-frame/dispatch [:navigate-to :offline-messaging-settings]) - :accessibility-label :offline-messages-settings-button}] - [profile.components/settings-item-separator] + {:label-kw :t/extensions + :action-fn #(re-frame/dispatch [:navigate-to :extensions-settings]) + :accessibility-label :extensions-button}]) + (when dev-mode? [profile.components/settings-item - {:label-kw :t/log-level - :action-fn #(re-frame/dispatch [:navigate-to :log-level-settings]) - :accessibility-label :log-level-settings-button}] - [profile.components/settings-item-separator] + {:label-kw :t/network + :value (get-in networks [network :name]) + :action-fn #(re-frame/dispatch [:navigate-to :network-settings]) + :accessibility-label :network-button}]) + [profile.components/settings-item-separator] + [profile.components/settings-item + {:label-kw :t/offline-messaging + :action-fn #(re-frame/dispatch [:navigate-to :offline-messaging-settings]) + :accessibility-label :offline-messages-settings-button}] + [profile.components/settings-item-separator] + [profile.components/settings-item + {:label-kw :t/log-level + :action-fn #(re-frame/dispatch [:navigate-to :log-level-settings]) + :accessibility-label :log-level-settings-button}] + [profile.components/settings-item-separator] + [profile.components/settings-item + {:label-kw :t/fleet + :action-fn #(re-frame/dispatch [:navigate-to :fleet-settings]) + :accessibility-label :fleet-settings-button}] + (when config/bootnodes-settings-enabled? + [profile.components/settings-item-separator]) + (when config/bootnodes-settings-enabled? [profile.components/settings-item - {:label-kw :t/fleet - :action-fn #(re-frame/dispatch [:navigate-to :fleet-settings]) - :accessibility-label :fleet-settings-button}] - (when config/bootnodes-settings-enabled? - [profile.components/settings-item-separator]) - (when config/bootnodes-settings-enabled? - [profile.components/settings-item - {:label-kw :t/bootnodes - :action-fn #(re-frame/dispatch [:navigate-to :bootnodes-settings]) - :accessibility-label :bootnodes-settings-button}]) - [profile.components/settings-item-separator] + {:label-kw :t/bootnodes + :action-fn #(re-frame/dispatch [:navigate-to :bootnodes-settings]) + :accessibility-label :bootnodes-settings-button}]) + (when dev-mode? [profile.components/settings-switch-item - {:label-kw :t/dev-mode - :value dev-mode? - :action-fn #(re-frame/dispatch [:accounts.ui/dev-mode-switched %])}]])) + {:label-kw :t/web3-opt-in + :value (:web3-opt-in? settings) + :action-fn #(re-frame/dispatch [:accounts.ui/web3-opt-in-mode-switched %])}]) + [profile.components/settings-item-separator] + [profile.components/settings-switch-item + {:label-kw :t/dev-mode + :value dev-mode? + :action-fn #(re-frame/dispatch [:accounts.ui/dev-mode-switched %])}]]) (defview advanced [params on-show] (letsubs [advanced? [:get :my-profile/advanced?]] diff --git a/src/status_im/utils/js_resources.cljs b/src/status_im/utils/js_resources.cljs index 7f9c390a16..a41e27008d 100644 --- a/src/status_im/utils/js_resources.cljs +++ b/src/status_im/utils/js_resources.cljs @@ -20,6 +20,10 @@ "var networkId = \"" network-id "\";" (slurp "resources/js/web3_init.js"))) +(defn web3-opt-in-init [network-id] + (str "var networkId = \"" network-id "\";" + (slurp "resources/js/web3_opt_in.js"))) + (defn local-storage-data [data] (str "var localStorageData = " (or data "{}") ";")) diff --git a/translations/en.json b/translations/en.json index a30af18a41..527caaab16 100644 --- a/translations/en.json +++ b/translations/en.json @@ -683,5 +683,6 @@ "gwei": "Gwei", "cost-fee": "Cost/Fee", "currency-display-name-usd": "United States Dollar", - "currency-display-name-uah": "Ukraine Hryvnia" + "currency-display-name-uah": "Ukraine Hryvnia", + "web3-opt-in": "Opt-in web3 provider access" } \ No newline at end of file