From c2b9093a7b62bacf4d80ea00f87d86a152731754 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Mon, 25 Nov 2019 21:50:57 -0500 Subject: [PATCH 1/6] Requested fixes/changes --- src/status_im/ethereum/eip681.cljs | 33 +- .../wallet/choose_recipient/views.cljs | 72 +++++ .../ui/screens/wallet/components/views.cljs | 295 ++++++++++++++++++ src/status_im/utils/universal_links/core.cljs | 13 +- .../wallet/choose_recipient/core.cljs | 100 ++++-- test/cljs/status_im/test/ethereum/eip681.cljs | 11 +- 6 files changed, 488 insertions(+), 36 deletions(-) create mode 100644 src/status_im/ui/screens/wallet/choose_recipient/views.cljs diff --git a/src/status_im/ethereum/eip681.cljs b/src/status_im/ethereum/eip681.cljs index 2f6cf95b109..24f3fc81da7 100644 --- a/src/status_im/ethereum/eip681.cljs +++ b/src/status_im/ethereum/eip681.cljs @@ -5,7 +5,9 @@ e.g. ethereum:0x1234@1/transfer?to=0x5678&value=1e18&gas=5000" (:require [clojure.string :as string] + [re-frame.core :as re-frame] [status-im.ethereum.core :as ethereum] + [status-im.ethereum.ens :as ens] [status-im.ethereum.tokens :as tokens] [status-im.utils.money :as money])) @@ -41,21 +43,34 @@ {:function-arguments (apply dissoc m valid-native-arguments)})) arguments))) -;; TODO add ENS support - (defn parse-uri - "Parse a EIP 681 URI as a map (keyword / strings). Parsed map will contain at least the key `address`. - Note that values are not decoded and you might need to rely on specific methods for some fields (parse-value, parse-number). + "Parse a EIP 681 URI as a map (keyword / strings). Parsed map will contain at least the key `address` + which will be either a valid ENS or Ethereum address. + Note that values are not decoded and you might need to rely on specific methods for some fields + (parse-value, parse-number). Invalid URI will be parsed as `nil`." [s] (when (string? s) (let [[_ authority-path query] (re-find uri-pattern s)] (when authority-path - (let [[_ address chain-id function-name] (re-find authority-path-pattern authority-path)] - (when (ethereum/address? address) - (when-let [arguments (parse-arguments function-name query)] - (merge {:address address :chain-id (if chain-id (js/parseInt chain-id) (ethereum/chain-keyword->chain-id :mainnet))} - arguments)))))))) + (let [[_ raw-address chain-id function-name] (re-find authority-path-pattern authority-path)] + (when (or (ethereum/address? raw-address) + (if (string/starts-with? raw-address "pay-") + (let [pay-address (string/replace-first raw-address "pay-" "")] + (or (ens/is-valid-eth-name? pay-address) + (ethereum/address? pay-address))))) + (let [address (if (string/starts-with? raw-address "pay-") + (string/replace-first raw-address "pay-" "") + raw-address)] + (when-let [arguments (parse-arguments function-name query)] + (let [contract-address (get-in arguments [:function-arguments :address])] + (if-not (or (not contract-address) (or (ens/is-valid-eth-name? contract-address) (ethereum/address? contract-address))) + nil + (merge {:address address + :chain-id (if chain-id + (js/parseInt chain-id) + (ethereum/chain-keyword->chain-id :mainnet))} + arguments))))))))))) (defn parse-eth-value [s] "Takes a map as returned by `parse-uri` and returns value as BigNumber" diff --git a/src/status_im/ui/screens/wallet/choose_recipient/views.cljs b/src/status_im/ui/screens/wallet/choose_recipient/views.cljs new file mode 100644 index 00000000000..49f3f489ee7 --- /dev/null +++ b/src/status_im/ui/screens/wallet/choose_recipient/views.cljs @@ -0,0 +1,72 @@ +(ns status-im.ui.screens.wallet.choose-recipient.views + (:require-macros [status-im.utils.views :refer [defview letsubs]]) + (:require [re-frame.core :as re-frame] + [status-im.i18n :as i18n] + [status-im.ui.components.toolbar :as toolbar] + [status-im.ui.components.camera :as camera] + [status-im.ui.components.react :as react] + [status-im.ui.components.status-bar.view :as status-bar] + [status-im.ui.components.toolbar.actions :as actions] + [status-im.ui.components.toolbar.view :as topbar] + [status-im.ui.screens.wallet.choose-recipient.styles :as styles] + [status-im.utils.platform :as platform] + [status-im.ethereum.eip681 :as eip681])) + +(defn- topbar [camera-flashlight] + [topbar/toolbar + {:transparent? true} + [topbar/nav-button (actions/back-white actions/default-handler)] + [topbar/content-title {:color :white} + (i18n/label :t/wallet-choose-recipient)] + [topbar/actions [{:icon (if (= :on camera-flashlight) + :main-icons/flash-active + :main-icons/flash-inactive) + :icon-opts {:color :white} + :handler #(re-frame/dispatch [:wallet/toggle-flashlight])}]]]) + +(defn- viewfinder [{:keys [height width]} size] + (let [height (cond-> height + platform/iphone-x? (- 78))] + [react/view {:style styles/viewfinder-port} + [react/view {:style (styles/viewfinder-translucent height width size :top)}] + [react/view {:style (styles/viewfinder-translucent height width size :right)}] + [react/view {:style (styles/viewfinder-translucent height width size :bottom)}] + [react/view {:style (styles/viewfinder-translucent height width size :left)}] + [react/image {:source {:uri :corner_left_top} + :style (styles/corner-left-top height width size)}] + [react/image {:source {:uri :corner_right_top} + :style (styles/corner-right-top height width size)}] + [react/image {:source {:uri :corner_left_bottom} + :style (styles/corner-left-bottom height width size)}] + [react/image {:source {:uri :corner_right_bottom} + :style (styles/corner-right-bottom height width size)}]])) + +(defn- size [{:keys [height width]}] + (int (* 2 (/ (min height width) 3)))) + +(defview choose-recipient [] + (letsubs [read-once? (atom false) + dimensions [:dimensions/window] + camera-flashlight [:wallet.send/camera-flashlight]] + [react/view {:style styles/qr-code} + [status-bar/status-bar {:type :transparent}] + [topbar camera-flashlight] + [react/text {:style (styles/qr-code-text dimensions) + :accessibility-label :scan-qr-code-with-wallet-address-text} + (i18n/label :t/scan-qr-code)] + [react/view {:style styles/qr-container + :pointer-events :none} + [react/with-activity-indicator + {} + [camera/camera {:style styles/preview + ;:torchMode (camera/set-torch camera-flashlight) + :onBarCodeRead #(when-not @read-once? + (reset! read-once? true) + (re-frame/dispatch [:wallet.send/resolve-ens-addresses (camera/get-qr-code-data %) :qr]))}]] + [viewfinder dimensions (size dimensions)]] + [toolbar/toolbar + {:center {:type :secondary + :disabled? false + :on-press #(re-frame/dispatch [:navigate-back]) + :accessibility-label :cancel-button + :label :t/cancel}}]])) diff --git a/src/status_im/ui/screens/wallet/components/views.cljs b/src/status_im/ui/screens/wallet/components/views.cljs index f9ada6352e3..ff76822d2ce 100644 --- a/src/status_im/ui/screens/wallet/components/views.cljs +++ b/src/status_im/ui/screens/wallet/components/views.cljs @@ -6,11 +6,306 @@ [status-im.ui.components.react :as react] [status-im.ui.components.styles :as components.styles] [status-im.ui.components.toolbar.view :as topbar] + [status-im.ui.components.tooltip.views :as tooltip] + [status-im.ui.screens.chat.photos :as photos] + [status-im.ui.screens.wallet.choose-recipient.views + :as + choose-recipient] + [status-im.ethereum.ens :as ens] [status-im.ui.screens.wallet.components.styles :as styles] [status-im.ui.components.text-input.view :as text-input] [status-im.ui.components.colors :as colors]) (:require-macros [status-im.utils.views :as views])) +;; Wallet tab has a different coloring scheme (dark) that forces color changes (background, text) +;; It might be replaced by some theme mechanism + +(defn text-input [props text] + [react/text-input (utils.core/deep-merge {:placeholder-text-color colors/gray + :selection-color colors/black + :style {:color colors/black + :height 52}} + props) + text]) + +(def default-action (actions/back-white actions/default-handler)) + +(defn topbar [title] + [topbar/simple-toolbar title]) + +(defn- top-view [avoid-keyboard?] + (if avoid-keyboard? + react/keyboard-avoiding-view + react/view)) + +(defn simple-screen + ([toolbar content] (simple-screen nil toolbar content)) + ([{:keys [avoid-keyboard?]} toolbar content] + [(top-view avoid-keyboard?) {:flex 1} + [status-bar/status-bar] + toolbar + content])) + +(defn- cartouche-content [{:keys [disabled?]} content] + [react/view {:style (styles/cartouche-content-wrapper disabled?)} + [react/view {:flex 1} + content]]) + +(defn cartouche [{:keys [disabled? on-press icon icon-opts] :or {icon :main-icons/next} :as m} header content] + [react/view {:style styles/cartouche-container} + [react/text + header] + (if (or disabled? (nil? on-press)) + [cartouche-content m content] + [react/touchable-highlight {:on-press on-press} + [react/view + [cartouche-content m + (if-not (true? disabled?) + [react/view styles/cartouche-icon-wrapper + [react/view {:flex 1} ;; Let content shrink if needed + content] + [vector-icons/icon icon icon-opts]] + content)]]])]) + +(defn view-asset [symbol] + [react/view + [react/i18n-text {:style styles/label :key :wallet-asset}] + [react/view styles/asset-container-read-only + [react/text {:style styles/asset-text} + (name symbol)]]]) + +(defn- type->handler [k] + (case k + :send :wallet.send/set-symbol + :request :wallet.request/set-symbol + (throw (str "Unknown type: " k)))) + +(defn- render-token [{:keys [symbol name icon decimals amount color] :as token} type] + [list/touchable-item #(do (re-frame/dispatch [(type->handler type) symbol]) + (re-frame/dispatch [:navigate-back])) + [react/view + [list/item + (if icon + [list/item-image icon] + [chat-icon/custom-icon-view-list name color]) + [list/item-content + [react/view {:flex-direction :row} + [react/text {:style styles/text} + name] + [react/text {:style {:text-transform :uppercase}} + (wallet.utils/display-symbol token)]] + [list/item-secondary (wallet.utils/format-amount amount decimals)]]]]]) + +(views/defview assets [type address] + (views/letsubs [assets [:wallet/transferrable-assets-with-amount address]] + [simple-screen + [topbar (i18n/label :t/wallet-assets)] + [react/view {:style (assoc components.styles/flex :background-color :white)} + [list/flat-list {:default-separator? true + :data assets + :key-fn (comp str :symbol) + :render-fn #(render-token % type)}]]])) + +(views/defview send-assets [] + (views/letsubs [address [:get-screen-params]] + [assets :send address])) + +(views/defview request-assets [] + (views/letsubs [address [:get-screen-params]] + [assets :request address])) + +(defn- type->view [k] + (case k + :send :wallet-send-assets + :request :wallet-request-assets + (throw (str "Unknown type: " k)))) + +(views/defview asset-selector [{:keys [disabled? type symbol error address]}] + (views/letsubs [balance [:balance address] + chain [:ethereum/chain-keyword] + all-tokens [:wallet/all-tokens]] + (let [{:keys [name icon decimals color] :as token} (tokens/asset-for all-tokens chain symbol)] + (when name + [react/view + [cartouche {:disabled? disabled? :on-press #(re-frame/dispatch [:navigate-to (type->view type) address])} + (i18n/label :t/wallet-asset) + [react/view {:style styles/asset-content-container + :accessibility-label :choose-asset-button} + (if icon + [list/item-image (assoc icon :style styles/asset-icon :image-style {:width 32 :height 32})] + [chat-icon/custom-icon-view-list name color 32]) + [react/view styles/asset-text-content + [react/view styles/asset-label-content + [react/text {:style (merge styles/text-content styles/asset-label)} + name] + [react/text {:style styles/text-secondary-content} + (wallet.utils/display-symbol token)]] + [react/text {:style (merge styles/text-secondary-content styles/asset-label)} + (str (wallet.utils/format-amount (get balance symbol) decimals))]]]] + (when error + [tooltip/tooltip error {}])])))) + +(defn- recipient-address [address modal?] + [react/text {:style (merge styles/recipient-address (when-not address styles/recipient-no-address)) + :accessibility-label :recipient-address-text} + (if (ens/is-valid-eth-name? address) + address + (or (eip55/address->checksum (ethereum/normalized-address address)) + (if modal? + (i18n/label :t/new-contract) + (i18n/label :t/specify-recipient))))]) + +(views/defview recipient-contact [address name request?] + (views/letsubs [contact [:contacts/contact-by-address address]] + (let [address? (and (not (nil? address)) (not= address ""))] + [react/view styles/recipient-container + [react/view styles/recipient-icon + (when contact + [photos/photo + ;;TODO this should be done in a subscription + (multiaccounts/displayed-photo contact) + {:size list.styles/image-size}])] + [react/view {:style styles/recipient-name} + [react/text {:style (styles/participant true) + :accessibility-label (if request? :contact-name-text :recipient-name-text) + :number-of-lines 1} + name] + [react/text {:style (styles/participant (and (not name) address?)) + :accessibility-label (if request? :contact-address-text :recipient-address-text)} + (eip55/address->checksum (ethereum/normalized-address address))]]]))) + +(defn render-contact [contact request?] + [list/touchable-item #(re-frame/dispatch [:wallet/fill-request-from-contact contact request?]) + [list/item + [photos/photo + ;;TODO this should be done in a subscription + (multiaccounts/displayed-photo contact) + {:size list.styles/image-size}] + [list/item-content + [list/item-primary {:accessibility-label :contact-name-text} + (multiaccounts/displayed-name contact)] + [react/text {:style list.styles/secondary-text + :accessibility-label :contact-address-text} + (eip55/address->checksum (ethereum/normalized-address (:address contact)))]]]]) + +(defn render-account [account] + [list/touchable-item #(re-frame/dispatch [:wallet/fill-request-from-contact account false]) + [list/item + [chat-icon/custom-icon-view-list (:name account) (:color account)] + [list/item-content + [list/item-primary {:accessibility-label :contact-name-text} + (:name account)] + [react/text {:style list.styles/secondary-text + :accessibility-label :contact-address-text} + (eip55/address->checksum (ethereum/normalized-address (:address account)))]]]]) + +(views/defview recent-recipients [] + (views/letsubs [contacts [:contacts/active] + {:keys [request?]} [:get-screen-params :recent-recipients]] + [simple-screen + [topbar (i18n/label :t/recipient)] + [react/view styles/recent-recipients + [list/flat-list {:data contacts + :key-fn :address + :render-fn #(render-contact % request?)}]]])) + +(views/defview accounts [] + (views/letsubs [{:keys [accounts]} [:multiaccount]] + [simple-screen + [topbar (i18n/label :t/accounts)] + [react/view styles/recent-recipients + [list/flat-list {:data accounts + :key-fn :address + :render-fn render-account}]]])) + +;;TODO workaround for https://github.com/facebook/react-native/issues/23653 (https://github.com/status-im/status-react/issues/8548) +(def tw (reagent/atom "95%")) + +(views/defview contact-code [] + (views/letsubs [content (reagent/atom nil)] + [simple-screen {:avoid-keyboard? true} + [topbar (i18n/label :t/recipient)] + [react/view components.styles/flex + [cartouche {} + (i18n/label :t/recipient) + [text-input {:ref (fn [v] (js/setTimeout #(reset! tw (if v "100%" "95%")) 100)) + :multiline true + :style (styles/contact-code-text-input @tw) + :placeholder (i18n/label :t/recipient-code) + :on-change-text #(reset! content %) + :accessibility-label :recipient-address-input}]] + [toolbar/toolbar {:center {:type :secondary + :disabled? (string/blank? @content) + :on-press #(re-frame/dispatch [:wallet.send/set-recipient @content]) + :label :t/done}}]]])) + +(defn recipient-qr-code [] + [choose-recipient/choose-recipient]) + +(defn- request-camera-permissions [] + (re-frame/dispatch [:request-permissions {:permissions [:camera] + :on-allowed #(re-frame/dispatch [:navigate-to :recipient-qr-code]) + :on-denied #(utils.utils/set-timeout + (fn [] + (utils.utils/show-popup (i18n/label :t/error) + (i18n/label :t/camera-access-error))) + 50)}])) + +(defn- on-choose-recipient [contact-only? request?] + (list-selection/show {:title (i18n/label :t/wallet-choose-recipient) + ;;TODO temporary hide for v1 + #_(concat + [{:label (i18n/label :t/recent-recipients) + :action #(re-frame/dispatch [:navigate-to :recent-recipients {:request? request?}])}] + (when-not contact-only?)) + :options [{:label (i18n/label :t/accounts) + :action #(re-frame/dispatch [:navigate-to :select-account])} + {:label (i18n/label :t/scan-qr) + :action request-camera-permissions} + {:label (i18n/label :t/recipient-code) + :action #(re-frame/dispatch [:navigate-to :contact-code])}]})) + +(defn recipient-selector [{:keys [name address disabled? contact-only? request? modal?]}] + [cartouche {:on-press #(on-choose-recipient contact-only? request?) + :disabled? disabled? + :icon :main-icons/more + :icon-opts {:accessibility-label :choose-contact-button}} + (i18n/label :t/wallet-choose-recipient) + [react/view {:accessibility-label :choose-recipient-button} + (if name + [recipient-contact address name request?] + [recipient-address address modal?])]]) + +(defn amount-input [{:keys [input-options amount amount-text disabled?]} + {:keys [symbol decimals]}] + [react/view {:style components.styles/flex + :accessibility-label :specify-amount-button} + [text-input + (merge + input-options + ;; We only auto-correct and prettify user's input when it is valid and positive. + ;; Otherwise, user might want to fix his input and autocorrection will give more harm than good. + ;; Positive check is because we don't want to replace unfinished 0.000 with just plain 0, that is annoying and + ;; potentially dangerous on this screen (e.g. sending 7 ETH instead of 0.0007) + {:default-value (if (empty? amount-text) + (str (money/to-fixed (money/internal->formatted amount symbol decimals))) + amount-text)} + (if disabled? + {:editable false + :placeholder ""} + {:keyboard-type :numeric + :placeholder (i18n/label :t/amount-placeholder) + :style components.styles/flex + :accessibility-label :amount-input}))]]) + +(defn amount-selector [{:keys [error disabled?] :as m} token] + [react/view + [cartouche {:disabled? disabled?} + (i18n/label :t/amount) + [amount-input m token]] + (when error + [tooltip/tooltip error])]) + (defn separator [] [react/view styles/separator]) diff --git a/src/status_im/utils/universal_links/core.cljs b/src/status_im/utils/universal_links/core.cljs index 336c8b1dd5f..41b10389a40 100644 --- a/src/status_im/utils/universal_links/core.cljs +++ b/src/status_im/utils/universal_links/core.cljs @@ -1,5 +1,6 @@ (ns status-im.utils.universal-links.core (:require [cljs.spec.alpha :as spec] + [clojure.string :as string] [goog.string :as gstring] [re-frame.core :as re-frame] [status-im.multiaccounts.model :as multiaccounts.model] @@ -16,7 +17,8 @@ [status-im.utils.config :as config] [status-im.utils.fx :as fx] [status-im.utils.platform :as platform] - [taoensso.timbre :as log])) + [taoensso.timbre :as log] + [status-im.wallet.choose-recipient.core :as choose-recipient])) ;; TODO(yenda) investigate why `handle-universal-link` event is ;; dispatched 7 times for the same link @@ -43,6 +45,9 @@ (re-matches regex) peek)) +(defn is-request-url? [url] + (string/starts-with? url "ethereum:")) + (defn universal-link? [url] (boolean (re-matches constants/regx-universal-link url))) @@ -81,8 +86,8 @@ (navigation/navigate-to-cofx (assoc-in cofx [:db :contacts/identity] public-key) :profile nil)))) (fx/defn handle-eip681 [cofx url] - {:dispatch-n [[:navigate-to :wallet] - [:wallet/fill-request-from-url url]]}) + (choose-recipient/resolve-ens-addresses url :deep-link) + {:dispatch [:navigate-to :wallet-send-transaction]}) (defn handle-not-found [full-url] (log/info "universal-links: no handler for " full-url)) @@ -107,7 +112,7 @@ (match-url url browse-regex) (handle-browse cofx (match-url url browse-regex)) - (some? (eip681/parse-uri url)) + (is-request-url? url) (handle-eip681 cofx url) :else (handle-not-found url))) diff --git a/src/status_im/wallet/choose_recipient/core.cljs b/src/status_im/wallet/choose_recipient/core.cljs index fb9f9022206..bb307f0d87a 100644 --- a/src/status_im/wallet/choose_recipient/core.cljs +++ b/src/status_im/wallet/choose_recipient/core.cljs @@ -49,11 +49,10 @@ (defn- extract-details "First try to parse as EIP681 URI, if not assume this is an address directly. Returns a map containing at least the `address` and `chain-id` keys" - [s chain-id all-tokens] - (or (let [m (eip681/parse-uri s)] - (merge m (eip681/extract-request-details m all-tokens))) - (when (ethereum/address? s) - {:address s :chain-id chain-id}))) + [m chain-id all-tokens] + (or (merge m (eip681/extract-request-details m all-tokens)) + (when (ethereum/address? m) + {:address m :chain-id chain-id}))) ;; NOTE(janherich) - whenever changing assets, we want to clear the previusly set amount/amount-text (defn changed-asset [{:keys [db] :as fx} old-symbol new-symbol] @@ -72,18 +71,33 @@ ethereum/default-transaction-gas)) (re-frame/reg-fx - :resolve-address + ::resolve-address (fn [{:keys [registry ens-name cb]}] (ens/get-addr registry ens-name cb))) +(re-frame/reg-fx + ::resolve-addresses + (fn [{:keys [registry ens-names callback]}] + ;; resolve all addresses then call the callback function with the array of + ;;addresses as parameter + (-> (js/Promise.all + (clj->js (mapv (fn [ens-name] + (js/Promise. + (fn [resolve reject] + (ens/get-addr registry ens-name resolve)))) + ens-names))) + (.then callback) + (.catch (fn [error] + (js/console.log error)))))) + (fx/defn set-recipient - {:events [:wallet.send/set-recipient]} + {:events [:wallet.send/set-recipient ::recipient-address-resolved]} [{:keys [db]} recipient] (let [chain (ethereum/chain-keyword db)] (if (ens/is-valid-eth-name? recipient) - {:resolve-address {:registry (get ens/ens-registries chain) - :ens-name recipient - :cb #(re-frame/dispatch [:wallet.send/set-recipient %])}} + {::resolve-address {:registry (get ens/ens-registries chain) + :ens-name recipient + :cb #(re-frame/dispatch [::recipient-address-resolved %])}} (if (ethereum/address? recipient) (let [checksum (eip55/address->checksum recipient)] (if (eip55/valid-address-checksum? checksum) @@ -94,10 +108,10 @@ {:ui/show-error (i18n/label :t/wallet-invalid-address-checksum {:data recipient})})) {:ui/show-error (i18n/label :t/wallet-invalid-address {:data recipient})})))) -(fx/defn fill-request-from-url - {:events [:wallet/fill-request-from-url]} - [{{:networks/keys [current-network] :wallet/keys [all-tokens] :as db} :db} data] - (let [current-chain-id (get-in constants/default-networks [current-network :config :NetworkId]) +(fx/defn request-uri-parsed + {:events [:wallet/request-uri-parsed]} + [{{:networks/keys [current-network] :wallet/keys [all-tokens] :as db} :db} data origin] + (let [current-chain-id (get-in constants/default-networks [current-network :config :NetworkId]) {:keys [address chain-id] :as details} (extract-details data current-chain-id all-tokens) valid-network? (boolean (= current-chain-id chain-id)) previous-state (get db :wallet/prepare-transaction) @@ -108,14 +122,16 @@ symbol-changed? (and old-symbol new-symbol (not= old-symbol new-symbol)) amount-changed? (and old-amount new-amount (not= old-amount new-amount))] (cond-> {:db db} - (and address valid-network?) (update :db #(fill-request-details % details false)) + (not= :deep-link origin) (assoc :dispatch [:navigate-back]) ;; Only navigate-back when called from within wallet symbol-changed? (changed-asset old-symbol new-symbol) - amount-changed? (changed-amount-warning old-amount new-amount) - (not (:from previous-state)) - (update :db assoc-in [:wallet/prepare-transaction :from] - (ethereum/get-default-account (get-in db [:multiaccount :accounts]))) - (not old-symbol) - (update :db assoc-in [:wallet/prepare-transaction :symbol] (or new-symbol :ETH)) + (and address valid-network?) (update :db #(fill-request-details % details false)) + (and old-amount new-amount (not= old-amount new-amount)) (changed-amount-warning old-amount new-amount) + ;; NOTE(goranjovic) - the next line is there is because QR code scanning switches the amount to ETH + ;; automatically, so we need to update the gas limit accordingly. The check for origin screen is there + ;; so that we wouldn't also switch gas limit to ETH specific if the user pastes address as text. + ;; We need to check if address is defined so that we wouldn't trigger this behavior when invalid QR is scanned + ;; (e.g. public-key) + (and address (= origin :qr) (not new-gas) symbol-changed?) (use-default-eth-gas) (not address) (assoc :ui/show-error (i18n/label :t/wallet-invalid-address {:data data})) (and address (not valid-network?)) (assoc :ui/show-error (i18n/label :t/wallet-invalid-chain-id @@ -132,4 +148,44 @@ (fx/defn qr-scanner-cancel {:events [:wallet.send/qr-scanner-cancel]} [{db :db} _] - {:db (assoc-in db [:wallet/prepare-transaction :modal-opened?] false)}) \ No newline at end of file + {:db (assoc-in db [:wallet/prepare-transaction :modal-opened?] false)}) +(fx/defn fill-request-from-contact + {:events [:wallet/fill-request-from-contact]} + [{db :db} {:keys [address name public-key]} request?] + {:db (fill-request-details db {:address address :name name :public-key public-key} request?) + :dispatch [:navigate-back]}) + +(fx/defn resolve-ens-addresses + {:events [:wallet.send/resolve-ens-addresses]} + [{{:networks/keys [current-network] :wallet/keys [all-tokens] :as db} :db} + uri origin] + (when-let [message (eip681/parse-uri uri)] + ;; first we get a vector of ens-names to resolve and a vector of paths of + ;; these names + (let [{:keys [paths ens-names]} + (reduce (fn [acc path] + (let [address (get-in message path)] + (if (ens/is-valid-eth-name? address) + (-> acc + (update :paths conj path) + (update :ens-names conj address)) + acc))) + {:paths [] :ens-names []} + [[:address] [:function-arguments :address]])] + (if (empty? ens-names) + ;; if there is no ens-names, we dispatch request-uri-parsed immediately + (request-uri-parsed message origin) + {::resolve-addresses + {:registry (get ens/ens-registries (ethereum/chain-keyword db)) + :ens-names ens-names + :callback + (fn [addresses] + (re-frame/dispatch + [:wallet/request-uri-parsed + ;; we replace the ens-names at their path in the message by their + ;; actual address + (reduce (fn [message [path address]] + (assoc-in message path address)) + message + (map vector paths addresses)) + origin]))}})))) diff --git a/test/cljs/status_im/test/ethereum/eip681.cljs b/test/cljs/status_im/test/ethereum/eip681.cljs index 21032512373..74a8a571eeb 100644 --- a/test/cljs/status_im/test/ethereum/eip681.cljs +++ b/test/cljs/status_im/test/ethereum/eip681.cljs @@ -11,6 +11,13 @@ (is (= nil (eip681/parse-uri "ethereum:?value=1"))) (is (= nil (eip681/parse-uri "bitcoin:0x1234"))) (is (= nil (eip681/parse-uri "ethereum:0x1234"))) + (is (= nil (eip681/parse-uri "ethereum:gimme.eth?value=1e18"))) + (is (= nil (eip681/parse-uri "ethereum:gimme.ether?value=1e18"))) + (is (= nil (eip681/parse-uri "ethereum:pay-gimme.ether?value=1e18"))) + (is (= nil (eip681/parse-uri "ethereum:pay-snt.thetoken.ether/transfer?address=gimme.eth&uint256=1&gas=100"))) + (is (= nil (eip681/parse-uri "ethereum:pay-snt.thetoken.eth/transfer?address=gimme.ether&uint256=1&gas=100"))) + (is (= {:address "gimme.eth" :value "1e18" :chain-id 1} (eip681/parse-uri "ethereum:pay-gimme.eth?value=1e18"))) + (is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1e18" :chain-id 1} (eip681/parse-uri "ethereum:pay-0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1e18"))) (is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7"))) (is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :value "1" :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?value=1"))) (is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7", :chain-id 1} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7?unknown=1"))) @@ -27,7 +34,9 @@ (is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :chain-id 1 :function-name "transfer" :function-arguments {:address "0x8e23ee67d1332ad560396262c48ffbb01f93d052" :uint256 "1"}} (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1"))) (is (= {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :chain-id 1 :function-name "transfer" :gas "100" :function-arguments {:address "0x8e23ee67d1332ad560396262c48ffbb01f93d052" :uint256 "1"}} - (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1&gas=100")))) + (eip681/parse-uri "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1&gas=100"))) + (is (= {:address "snt.thetoken.eth" :chain-id 1 :function-name "transfer" :gas "100" :function-arguments {:address "gimme.eth" :uint256 "1"}} + (eip681/parse-uri "ethereum:pay-snt.thetoken.eth/transfer?address=gimme.eth&uint256=1&gas=100")))) (def all-tokens {:mainnet {"0x744d70fdbe2ba4cf95131626614a1763df805b9e" {:address "0x744d70fdbe2ba4cf95131626614a1763df805b9e" From 20cfea5b372c1ec3bb304a139a2ee36c68eb13fc Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Tue, 26 Nov 2019 06:23:18 -0500 Subject: [PATCH 2/6] Additional requested fixes --- .../ui/screens/wallet/choose_recipient/views.cljs | 2 +- src/status_im/utils/universal_links/core.cljs | 7 ++++--- src/status_im/wallet/choose_recipient/core.cljs | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/status_im/ui/screens/wallet/choose_recipient/views.cljs b/src/status_im/ui/screens/wallet/choose_recipient/views.cljs index 49f3f489ee7..1581fe929ff 100644 --- a/src/status_im/ui/screens/wallet/choose_recipient/views.cljs +++ b/src/status_im/ui/screens/wallet/choose_recipient/views.cljs @@ -62,7 +62,7 @@ ;:torchMode (camera/set-torch camera-flashlight) :onBarCodeRead #(when-not @read-once? (reset! read-once? true) - (re-frame/dispatch [:wallet.send/resolve-ens-addresses (camera/get-qr-code-data %) :qr]))}]] + (re-frame/dispatch [:wallet.send/qr-code-request-scanned (camera/get-qr-code-data %) :qr]))}]] [viewfinder dimensions (size dimensions)]] [toolbar/toolbar {:center {:type :secondary diff --git a/src/status_im/utils/universal_links/core.cljs b/src/status_im/utils/universal_links/core.cljs index 41b10389a40..b7f7ef60235 100644 --- a/src/status_im/utils/universal_links/core.cljs +++ b/src/status_im/utils/universal_links/core.cljs @@ -6,7 +6,6 @@ [status-im.multiaccounts.model :as multiaccounts.model] [status-im.chat.models :as chat] [status-im.constants :as constants] - [status-im.ethereum.eip681 :as eip681] [status-im.pairing.core :as pairing] [status-im.utils.security :as security] [status-im.ui.components.list-selection :as list-selection] @@ -86,8 +85,10 @@ (navigation/navigate-to-cofx (assoc-in cofx [:db :contacts/identity] public-key) :profile nil)))) (fx/defn handle-eip681 [cofx url] - (choose-recipient/resolve-ens-addresses url :deep-link) - {:dispatch [:navigate-to :wallet-send-transaction]}) + (fx/merge + cofx + (choose-recipient/resolve-ens-addresses url :deep-link) + (navigation/navigate-to-cofx :wallet-send-transaction nil))) (defn handle-not-found [full-url] (log/info "universal-links: no handler for " full-url)) diff --git a/src/status_im/wallet/choose_recipient/core.cljs b/src/status_im/wallet/choose_recipient/core.cljs index bb307f0d87a..64830e5d4dd 100644 --- a/src/status_im/wallet/choose_recipient/core.cljs +++ b/src/status_im/wallet/choose_recipient/core.cljs @@ -156,7 +156,7 @@ :dispatch [:navigate-back]}) (fx/defn resolve-ens-addresses - {:events [:wallet.send/resolve-ens-addresses]} + {:events [:wallet.send/resolve-ens-addresses :wallet.send/qr-code-request-scanned]} [{{:networks/keys [current-network] :wallet/keys [all-tokens] :as db} :db} uri origin] (when-let [message (eip681/parse-uri uri)] From f33a8d11d061951e9545ce486d94976605dec342 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Tue, 26 Nov 2019 15:11:36 -0500 Subject: [PATCH 3/6] Add error handling for invalid address --- src/status_im/wallet/choose_recipient/core.cljs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/status_im/wallet/choose_recipient/core.cljs b/src/status_im/wallet/choose_recipient/core.cljs index 64830e5d4dd..97320d4a8e2 100644 --- a/src/status_im/wallet/choose_recipient/core.cljs +++ b/src/status_im/wallet/choose_recipient/core.cljs @@ -159,7 +159,7 @@ {:events [:wallet.send/resolve-ens-addresses :wallet.send/qr-code-request-scanned]} [{{:networks/keys [current-network] :wallet/keys [all-tokens] :as db} :db} uri origin] - (when-let [message (eip681/parse-uri uri)] + (if-let [message (eip681/parse-uri uri)] ;; first we get a vector of ens-names to resolve and a vector of paths of ;; these names (let [{:keys [paths ens-names]} @@ -188,4 +188,7 @@ (assoc-in message path address)) message (map vector paths addresses)) - origin]))}})))) + origin]))}})) + (-> {:db db} + (assoc :ui/show-error (i18n/label :t/wallet-invalid-address {:data uri})) + (assoc :dispatch [:navigate-back])))) From 963a5dffe5c2e7af6c16c0c4b7982b611bfde3c0 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Tue, 26 Nov 2019 15:36:46 -0500 Subject: [PATCH 4/6] Add navigation check for origin --- src/status_im/ui/screens/wallet/choose_recipient/views.cljs | 2 +- src/status_im/wallet/choose_recipient/core.cljs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/status_im/ui/screens/wallet/choose_recipient/views.cljs b/src/status_im/ui/screens/wallet/choose_recipient/views.cljs index 1581fe929ff..49f3f489ee7 100644 --- a/src/status_im/ui/screens/wallet/choose_recipient/views.cljs +++ b/src/status_im/ui/screens/wallet/choose_recipient/views.cljs @@ -62,7 +62,7 @@ ;:torchMode (camera/set-torch camera-flashlight) :onBarCodeRead #(when-not @read-once? (reset! read-once? true) - (re-frame/dispatch [:wallet.send/qr-code-request-scanned (camera/get-qr-code-data %) :qr]))}]] + (re-frame/dispatch [:wallet.send/resolve-ens-addresses (camera/get-qr-code-data %) :qr]))}]] [viewfinder dimensions (size dimensions)]] [toolbar/toolbar {:center {:type :secondary diff --git a/src/status_im/wallet/choose_recipient/core.cljs b/src/status_im/wallet/choose_recipient/core.cljs index 97320d4a8e2..66ec87a917d 100644 --- a/src/status_im/wallet/choose_recipient/core.cljs +++ b/src/status_im/wallet/choose_recipient/core.cljs @@ -189,6 +189,6 @@ message (map vector paths addresses)) origin]))}})) - (-> {:db db} - (assoc :ui/show-error (i18n/label :t/wallet-invalid-address {:data uri})) - (assoc :dispatch [:navigate-back])))) + (cond-> {:db db} + true (assoc :ui/show-error (i18n/label :t/wallet-invalid-address {:data uri})) + (= origin :qr) (assoc :dispatch [:navigate-back])))) From 02a749ecf10a406e02f8bacabfeca1943f52653d Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Wed, 27 Nov 2019 05:29:26 -0500 Subject: [PATCH 5/6] add cofx parameter Co-Authored-By: yenda --- src/status_im/wallet/choose_recipient/core.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/status_im/wallet/choose_recipient/core.cljs b/src/status_im/wallet/choose_recipient/core.cljs index 66ec87a917d..cec78d87890 100644 --- a/src/status_im/wallet/choose_recipient/core.cljs +++ b/src/status_im/wallet/choose_recipient/core.cljs @@ -174,7 +174,7 @@ [[:address] [:function-arguments :address]])] (if (empty? ens-names) ;; if there is no ens-names, we dispatch request-uri-parsed immediately - (request-uri-parsed message origin) + (request-uri-parsed cofx message origin) {::resolve-addresses {:registry (get ens/ens-registries (ethereum/chain-keyword db)) :ens-names ens-names From 8a14435f289bcbb1ba686ceea47f0bd9221f2504 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Thu, 28 Nov 2019 07:07:48 -0500 Subject: [PATCH 6/6] add cofx --- src/status_im/wallet/choose_recipient/core.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/status_im/wallet/choose_recipient/core.cljs b/src/status_im/wallet/choose_recipient/core.cljs index cec78d87890..a2a3610ae8a 100644 --- a/src/status_im/wallet/choose_recipient/core.cljs +++ b/src/status_im/wallet/choose_recipient/core.cljs @@ -157,7 +157,7 @@ (fx/defn resolve-ens-addresses {:events [:wallet.send/resolve-ens-addresses :wallet.send/qr-code-request-scanned]} - [{{:networks/keys [current-network] :wallet/keys [all-tokens] :as db} :db} + [{{:networks/keys [current-network] :wallet/keys [all-tokens] :as db} :db :as cofx} uri origin] (if-let [message (eip681/parse-uri uri)] ;; first we get a vector of ens-names to resolve and a vector of paths of