From 98d4969ca5cb587f11e82221376627c6671596a3 Mon Sep 17 00:00:00 2001 From: Brian Sztamfater Date: Mon, 15 Apr 2024 12:04:44 -0300 Subject: [PATCH] feat: add ability to tap to disable from networks (#19392) --- .../wallet/network_link/helpers.cljs | 23 ++ .../components/wallet/network_link/style.cljs | 34 +++ .../components/wallet/network_link/view.cljs | 199 +++++++++++----- src/status_im/constants.cljs | 2 + .../preview/quo/wallet/network_link.cljs | 11 +- .../wallet/bridge/input_amount/view.cljs | 1 + .../wallet/common/utils/networks.cljs | 18 ++ .../contexts/wallet/common/utils/send.cljs | 17 +- .../wallet/common/utils/send_test.cljs | 41 ++++ .../contexts/wallet/send/events.cljs | 104 +++++--- .../send/input_amount/component_spec.cljs | 111 +++++---- .../wallet/send/input_amount/view.cljs | 165 ++++++++----- .../contexts/wallet/send/routes/style.cljs | 24 +- .../contexts/wallet/send/routes/view.cljs | 190 +++++++++++---- .../wallet/send/select_address/view.cljs | 1 + .../wallet/send/send_amount/view.cljs | 4 +- .../send/transaction_confirmation/view.cljs | 225 ++++++------------ src/status_im/contexts/wallet/send/utils.cljs | 29 ++- .../contexts/wallet/send/utils_test.cljs | 68 +++++- src/status_im/subs/wallet/wallet.cljs | 15 ++ src/utils/vector.cljs | 7 + src/utils/vector_test.cljs | 17 ++ 22 files changed, 887 insertions(+), 419 deletions(-) create mode 100644 src/quo/components/wallet/network_link/helpers.cljs create mode 100644 src/quo/components/wallet/network_link/style.cljs create mode 100644 src/status_im/contexts/wallet/common/utils/networks.cljs create mode 100644 src/utils/vector.cljs create mode 100644 src/utils/vector_test.cljs diff --git a/src/quo/components/wallet/network_link/helpers.cljs b/src/quo/components/wallet/network_link/helpers.cljs new file mode 100644 index 0000000000..1421748098 --- /dev/null +++ b/src/quo/components/wallet/network_link/helpers.cljs @@ -0,0 +1,23 @@ +(ns quo.components.wallet.network-link.helpers) + +(def ^:private central-figure-width 63) + +(defn calculate-side-lines-path-1x + "Calculates the `d` attribute for the side lines based on the SVG width." + [width] + (let [side-offset (/ (- width central-figure-width) 2)] + {:left (str "M0 57 L" side-offset " 57") + :right (str "M" (+ side-offset central-figure-width) " 1 L" width " 1")})) + +(defn calculate-transform + "Calculates the transform attribute for the central figure based on the SVG width." + [width] + (let [translate-x (/ (- width central-figure-width) 2)] + (str "translate(" translate-x " 0)"))) + +(defn calculate-side-lines-path-2x + "Calculates the `d` attribute for the side lines based on the SVG width." + [width] + (let [side-offset (/ (- width central-figure-width) 2)] + {:left (str "M0 113 L" side-offset " 113") + :right (str "M" (+ side-offset central-figure-width) " 1 L" width " 1")})) diff --git a/src/quo/components/wallet/network_link/style.cljs b/src/quo/components/wallet/network_link/style.cljs new file mode 100644 index 0000000000..19c01e2926 --- /dev/null +++ b/src/quo/components/wallet/network_link/style.cljs @@ -0,0 +1,34 @@ +(ns quo.components.wallet.network-link.style) + +(def left-circle-container + {:position :absolute + :left -3}) + +(def right-circle-container + {:position :absolute + :right -3}) + +(def bottom-left-circle-container + {:position :absolute + :bottom -3 + :left -3}) + +(def top-right-circle-container + {:position :absolute + :top -3 + :right -3}) + +(def link-linear-container + {:flex-direction :row + :align-items :center + :height 10}) + +(def link-1x-container + {:flex 1 + :height 58 + :justify-content :center}) + +(def link-2x-container + {:flex 1 + :height 114 + :justify-content :center}) diff --git a/src/quo/components/wallet/network_link/view.cljs b/src/quo/components/wallet/network_link/view.cljs index 9fe4949267..2c59d3d492 100644 --- a/src/quo/components/wallet/network_link/view.cljs +++ b/src/quo/components/wallet/network_link/view.cljs @@ -1,82 +1,151 @@ (ns quo.components.wallet.network-link.view (:require + [oops.core :refer [oget]] + [quo.components.wallet.network-link.helpers :as helpers] [quo.components.wallet.network-link.schema :as component-schema] + [quo.components.wallet.network-link.style :as style] [quo.foundations.colors :as colors] [quo.theme :as quo.theme] [react-native.core :as rn] [react-native.svg :as svg] + [reagent.core :as reagent] [schema.core :as schema])) +(defn- circle + [fill stroke] + [svg/svg + {:height "8" + :width "8"} + [svg/circle + {:cx "4" + :cy "4" + :r "3.5" + :fill fill + :stroke stroke + :strokeWidth "1"}]]) + +(defn- line + [stroke width] + [svg/svg + {:height "10" + :width "100%" + :view-box (str "0 0 " width " 10")} + [svg/path + {:d (str "M0,5 L" width ",5") + :stroke stroke + :stroke-width "1"}]]) + (defn link-linear - [{:keys [source theme]}] - [svg/svg {:xmlns "http://www.w3.org/2000/svg" :width "73" :height "10" :fill :none} - [svg/path {:stroke (colors/resolve-color source theme) :d "M68 5H5"}] - [svg/circle - {:cx "68" - :cy "5" - :r "4" - :fill (colors/theme-colors colors/white colors/neutral-90 theme) - :stroke (colors/resolve-color source theme)}] - [svg/circle - {:cx "5" - :cy "5" - :r "4" - :fill (colors/theme-colors colors/white colors/neutral-90 theme) - :stroke (colors/resolve-color source theme)}]]) + [] + (let [container-width (reagent/atom 100)] + (fn [{:keys [source theme]}] + (let [stroke-color (colors/resolve-color source theme) + fill-color (colors/theme-colors colors/white colors/neutral-90 theme)] + [rn/view + {:style style/link-linear-container + :on-layout (fn [e] + (reset! container-width + (oget e :nativeEvent :layout :width)))} + [line stroke-color @container-width] + [rn/view {:style style/left-circle-container} + [circle fill-color stroke-color]] + [rn/view {:style style/right-circle-container} + [circle fill-color stroke-color]]])))) (defn link-1x - [{:keys [source destination theme]}] - [svg/svg {:xmlns "http://www.w3.org/2000/svg" :width "73" :height "66" :fill :none} - [svg/path - {:stroke "url(#gradient)" :d "M68 5h-9.364c-11.046 0-20 8.954-20 20v16c0 11.046-8.955 20-20 20H5"}] - [svg/circle - {:cx "68" - :cy "5" - :r "4" - :fill (colors/theme-colors colors/white colors/neutral-90 theme) - :stroke (colors/resolve-color destination theme)}] - [svg/circle - {:cx "5" - :cy "61" - :r "4" - :fill (colors/theme-colors colors/white colors/neutral-90 theme) - :stroke (colors/resolve-color source theme)}] - [svg/defs - [svg/linear-gradient - {:id "gradient" :x1 "72.271" :x2 "82.385" :y1 "5" :y2 "34.155" :gradientUnits "userSpaceOnUse"} - [svg/stop {:stopColor (colors/resolve-color destination theme)}] - [svg/stop {:offset "1" :stopColor (colors/resolve-color source theme)}]]]]) + [] + (let [container-width (reagent/atom 100) + stroke-color "url(#gradient)"] + (fn [{:keys [source destination theme]}] + (let [source-color (colors/resolve-color source theme) + destination-color (colors/resolve-color destination theme) + fill-color (colors/theme-colors colors/white colors/neutral-90 theme) + view-box (str "0 0 " @container-width " 58") + side-lines-path (helpers/calculate-side-lines-path-1x @container-width) + central-transform (helpers/calculate-transform @container-width)] + [rn/view + {:style style/link-1x-container + :on-layout (fn [e] + (reset! container-width + (oget e :nativeEvent :layout :width)))} + [svg/svg + {:xmlns "http://www.w3.org/2000/svg" + :height "100%" + :width "100%" + :view-box view-box + :fill :none} + [svg/path + {:d (:left side-lines-path) + :stroke source-color}] + [svg/path + {:d + "M63 1L53.6356 1C42.5899 1 33.6356 9.9543 33.6356 21L33.6356 37C33.6356 48.0457 24.6813 57 13.6356 57L2.85889e-05 57" + :transform central-transform + :stroke stroke-color}] + [svg/path + {:d (:right side-lines-path) + :stroke destination-color}] + [svg/defs + [svg/linear-gradient + {:id "gradient" + :x1 "72.271" + :x2 "82.385" + :y1 "5" + :y2 "34.155" + :gradient-units "userSpaceOnUse"} + [svg/stop {:stop-color (colors/resolve-color destination theme)}] + [svg/stop {:offset "1" :stop-color (colors/resolve-color source theme)}]]]] + [rn/view {:style style/bottom-left-circle-container} + [circle fill-color source-color]] + [rn/view {:style style/top-right-circle-container} + [circle fill-color destination-color]]])))) (defn link-2x - [{:keys [source destination theme]}] - [svg/svg - {:width "73" :height "122" :viewBox "0 0 73 122" :fill "none" :xmlns "http://www.w3.org/2000/svg"} - [svg/path - {:d - "M67.9999 5L58.6356 5C47.5899 5 38.6356 13.9543 38.6356 25L38.6356 97C38.6356 108.046 29.6813 117 18.6356 117L5.00006 117" - :stroke "url(#gradient)"}] - [svg/circle - {:cx "68" - :cy "5" - :r "4" - :fill (colors/theme-colors colors/white colors/neutral-90 theme) - :stroke (colors/resolve-color destination theme)}] - [svg/circle - {:cx "5" - :cy "117" - :r "4" - :fill (colors/theme-colors colors/white colors/neutral-90 theme) - :stroke (colors/resolve-color source theme)}] - [svg/defs - [svg/linear-gradient - {:id "gradient" - :x1 "72.2711" - :y1 "5.00001" - :x2 "102.867" - :y2 "49.0993" - :gradientUnits "userSpaceOnUse"} - [svg/stop {:stop-color (colors/resolve-color destination theme)}] - [svg/stop {:offset "1" :stop-color (colors/resolve-color source theme)}]]]]) + [] + (let [container-width (reagent/atom 100) + stroke-color "url(#gradient)"] + (fn [{:keys [source destination theme]}] + (let [source-color (colors/resolve-color source theme) + destination-color (colors/resolve-color destination theme) + fill-color (colors/theme-colors colors/white colors/neutral-90 theme) + view-box (str "0 0 " @container-width " 114") + side-lines-path (helpers/calculate-side-lines-path-2x @container-width) + central-transform (helpers/calculate-transform @container-width)] + [rn/view + {:style style/link-2x-container + :on-layout #(reset! container-width + (oget % :nativeEvent :layout :width))} + [svg/svg + {:xmlns "http://www.w3.org/2000/svg" + :height "100%" + :width "100%" + :view-box view-box + :fill :none} + [svg/path + {:d (:left side-lines-path) + :stroke source-color}] + [svg/path + {:d + "M62.9999 1L53.6356 1C42.5899 1 33.6356 9.9543 33.6356 21L33.6356 93C33.6356 104.046 24.6813 113 13.6356 113L5.71778e-05 113" + :transform central-transform + :stroke stroke-color}] + [svg/path + {:d (:right side-lines-path) + :stroke destination-color}] + [svg/defs + [svg/linear-gradient + {:id "gradient" + :x1 "72.2711" + :y1 "5.00001" + :x2 "102.867" + :y2 "49.0993" + :gradient-units "userSpaceOnUse"} + [svg/stop {:stop-color (colors/resolve-color destination theme)}] + [svg/stop {:offset "1" :stop-color (colors/resolve-color source theme)}]]]] + [rn/view {:style style/bottom-left-circle-container} + [circle fill-color source-color]] + [rn/view {:style style/top-right-circle-container} + [circle fill-color destination-color]]])))) (defn- view-internal [{:keys [shape container-style] :as props}] diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index c2acbe45b7..78de93d412 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -448,6 +448,8 @@ (def ^:const optimism-short-name "opt") (def ^:const arbitrum-short-name "arb1") +(def ^:const default-multichain-address-prefix "eth:opt:arb1:") + (def ^:const mainnet-abbreviated-name "Eth.") (def ^:const optimism-abbreviated-name "Opt.") (def ^:const arbitrum-abbreviated-name "Arb1.") diff --git a/src/status_im/contexts/preview/quo/wallet/network_link.cljs b/src/status_im/contexts/preview/quo/wallet/network_link.cljs index f02375d083..e816c5bd0e 100644 --- a/src/status_im/contexts/preview/quo/wallet/network_link.cljs +++ b/src/status_im/contexts/preview/quo/wallet/network_link.cljs @@ -1,6 +1,7 @@ (ns status-im.contexts.preview.quo.wallet.network-link (:require [quo.core :as quo] + [react-native.core :as rn] [reagent.core :as reagent] [status-im.contexts.preview.quo.preview :as preview])) @@ -23,17 +24,21 @@ :options networks} {:key :destination :type :select - :options networks}]) + :options networks} + {:key :width + :type :number}]) (defn view [] (let [state (reagent/atom {:shape :linear :source :ethereum - :destination :optimism})] + :destination :optimism + :width 63})] (fn [] [preview/preview-container {:state state :descriptor descriptor :component-container-style {:padding-top 40 :align-items :center}} - [quo/network-link @state]]))) + [rn/view {:style {:width (max (:width @state) 63)}} + [quo/network-link @state]]]))) diff --git a/src/status_im/contexts/wallet/bridge/input_amount/view.cljs b/src/status_im/contexts/wallet/bridge/input_amount/view.cljs index 07ee40acf2..05984cce5d 100644 --- a/src/status_im/contexts/wallet/bridge/input_amount/view.cljs +++ b/src/status_im/contexts/wallet/bridge/input_amount/view.cljs @@ -15,6 +15,7 @@ :button-one-label (i18n/label :t/confirm-bridge) :button-one-props {:icon-left :i/bridge} :on-navigate-back (fn [] + (rf/dispatch [:wallet/clean-disabled-from-networks]) (rf/dispatch [:navigate-back]))}]]) (def view (quo.theme/with-theme view-internal)) diff --git a/src/status_im/contexts/wallet/common/utils/networks.cljs b/src/status_im/contexts/wallet/common/utils/networks.cljs new file mode 100644 index 0000000000..fa7355293e --- /dev/null +++ b/src/status_im/contexts/wallet/common/utils/networks.cljs @@ -0,0 +1,18 @@ +(ns status-im.contexts.wallet.common.utils.networks + (:require [clojure.string :as string] + [status-im.constants :as constants] + [status-im.contexts.wallet.common.utils :as utils])) + +(defn resolve-receiver-networks + [{:keys [prefix testnet-enabled? goerli-enabled?]}] + (let [prefix (if (string/blank? prefix) + constants/default-multichain-address-prefix + prefix) + prefix-seq (string/split prefix #":")] + (->> prefix-seq + (remove string/blank?) + (mapv + #(utils/network->chain-id + {:network % + :testnet-enabled? testnet-enabled? + :goerli-enabled? goerli-enabled?}))))) diff --git a/src/status_im/contexts/wallet/common/utils/send.cljs b/src/status_im/contexts/wallet/common/utils/send.cljs index 548d409f50..4128976b6f 100644 --- a/src/status_im/contexts/wallet/common/utils/send.cljs +++ b/src/status_im/contexts/wallet/common/utils/send.cljs @@ -1,5 +1,6 @@ (ns status-im.contexts.wallet.common.utils.send - (:require [utils.money :as money])) + (:require [clojure.string :as string] + [utils.money :as money])) (defn calculate-gas-fee [data] @@ -20,3 +21,17 @@ (defn calculate-full-route-gas-fee [route] (reduce money/add (map calculate-gas-fee route))) + +(defn find-affordable-networks + [{:keys [balances-per-chain input-value selected-networks disabled-chain-ids]}] + (let [input-value (if (string/blank? input-value) 0 input-value)] + (->> balances-per-chain + (filter (fn [[_ + {:keys [balance chain-id] + :or {balance 0}}]] + (and + (money/greater-than-or-equals (money/bignumber balance) + (money/bignumber input-value)) + (some #(= % chain-id) selected-networks) + (not-any? #(= % chain-id) disabled-chain-ids)))) + (map first)))) diff --git a/src/status_im/contexts/wallet/common/utils/send_test.cljs b/src/status_im/contexts/wallet/common/utils/send_test.cljs index 5a499d2cd3..d41de995c9 100644 --- a/src/status_im/contexts/wallet/common/utils/send_test.cljs +++ b/src/status_im/contexts/wallet/common/utils/send_test.cljs @@ -20,3 +20,44 @@ expected-eip1559-disabled-result (money/bignumber 0.000760406)] (is (money/equal-to (utils/calculate-gas-fee data-eip1559-disabled) expected-eip1559-disabled-result)))))) + +(deftest test-find-affordable-networks + (testing "All networks affordable and selected, none disabled" + (let [balances-per-chain {"1" {:balance "50.0" :chain-id "1"} + "2" {:balance "40.0" :chain-id "2"}} + input-value 20 + selected-networks ["1" "2"] + disabled-chain-ids [] + expected ["1" "2"]] + (is (= (set (utils/find-affordable-networks {:balances-per-chain balances-per-chain + :input-value input-value + :selected-networks selected-networks + :disabled-chain-ids disabled-chain-ids})) + (set expected))))) + + (testing "No networks affordable" + (let [balances-per-chain {"1" {:balance "5.0" :chain-id "1"} + "2" {:balance "1.0" :chain-id "2"}} + input-value 10 + selected-networks ["1" "2"] + disabled-chain-ids [] + expected []] + (is (= (set (utils/find-affordable-networks {:balances-per-chain balances-per-chain + :input-value input-value + :selected-networks selected-networks + :disabled-chain-ids disabled-chain-ids})) + (set expected))))) + + (testing "Selected networks subset, with some disabled" + (let [balances-per-chain {"1" {:balance "100.0" :chain-id "1"} + "2" {:balance "50.0" :chain-id "2"} + "3" {:balance "20.0" :chain-id "3"}} + input-value 15 + selected-networks ["1" "2" "3"] + disabled-chain-ids ["2"] + expected ["1" "3"]] + (is (= (set (utils/find-affordable-networks {:balances-per-chain balances-per-chain + :input-value input-value + :selected-networks selected-networks + :disabled-chain-ids disabled-chain-ids})) + (set expected)))))) diff --git a/src/status_im/contexts/wallet/send/events.cljs b/src/status_im/contexts/wallet/send/events.cljs index 44f18d7752..0337c9b1a0 100644 --- a/src/status_im/contexts/wallet/send/events.cljs +++ b/src/status_im/contexts/wallet/send/events.cljs @@ -5,6 +5,7 @@ [native-module.core :as native-module] [status-im.constants :as constants] [status-im.contexts.wallet.common.utils :as utils] + [status-im.contexts.wallet.common.utils.networks :as network-utils] [status-im.contexts.wallet.send.utils :as send-utils] [taoensso.timbre :as log] [utils.address :as address] @@ -24,45 +25,71 @@ (rf/reg-event-fx :wallet/suggested-routes-success (fn [{:keys [db]} [suggested-routes timestamp]] (when (= (get-in db [:wallet :ui :send :suggested-routes-call-timestamp]) timestamp) - (let [suggested-routes-data (cske/transform-keys transforms/->kebab-case-keyword suggested-routes) - chosen-route (:best suggested-routes-data)] + (let [suggested-routes-data (cske/transform-keys transforms/->kebab-case-keyword + suggested-routes) + chosen-route (:best suggested-routes-data) + token (get-in db [:wallet :ui :send :token]) + collectible (get-in db [:wallet :ui :send :collectible]) + token-display-name (get-in db [:wallet :ui :send :token-display-name]) + token-decimals (if collectible 0 (:decimals token)) + native-token? (and token (= token-display-name "ETH")) + from-network-amounts-by-chain (send-utils/network-amounts-by-chain {:route chosen-route + :token-decimals + token-decimals + :native-token? + native-token? + :to? false}) + from-network-values-for-ui (send-utils/network-values-for-ui from-network-amounts-by-chain) + to-network-amounts-by-chain (send-utils/network-amounts-by-chain {:route chosen-route + :token-decimals + token-decimals + :native-token? + native-token? + :to? true}) + to-network-values-for-ui (send-utils/network-values-for-ui to-network-amounts-by-chain)] {:db (-> db (assoc-in [:wallet :ui :send :suggested-routes] suggested-routes-data) (assoc-in [:wallet :ui :send :route] chosen-route) + (assoc-in [:wallet :ui :send :from-values-by-chain] from-network-values-for-ui) + (assoc-in [:wallet :ui :send :to-values-by-chain] to-network-values-for-ui) (assoc-in [:wallet :ui :send :loading-suggested-routes?] false))})))) (rf/reg-event-fx :wallet/suggested-routes-error (fn [{:keys [db]} [_error]] {:db (-> db - (update-in [:wallet :ui :send] dissoc :suggested-routes) - (update-in [:wallet :ui :send] dissoc :route) + (update-in [:wallet :ui :send] dissoc :suggested-routes :route) (assoc-in [:wallet :ui :send :loading-suggested-routes?] false))})) (rf/reg-event-fx :wallet/clean-suggested-routes (fn [{:keys [db]}] - {:db (-> db - (update-in [:wallet :ui :send] dissoc :suggested-routes) - (update-in [:wallet :ui :send] dissoc :route) - (update-in [:wallet :ui :send] dissoc :loading-suggested-routes?))})) + {:db (update-in db + [:wallet :ui :send] + dissoc + :suggested-routes + :route + :from-values-by-chain + :to-values-by-chain + :loading-suggested-routes? + :suggested-routes-call-timestamp)})) (rf/reg-event-fx :wallet/clean-send-address (fn [{:keys [db]}] {:db (update-in db [:wallet :ui :send] dissoc :recipient :to-address)})) +(rf/reg-event-fx :wallet/clean-disabled-from-networks + (fn [{:keys [db]}] + {:db (update-in db [:wallet :ui :send] dissoc :disabled-from-chain-ids)})) + (rf/reg-event-fx :wallet/select-send-address (fn [{:keys [db]} [{:keys [address recipient stack-id start-flow?]}]] (let [[prefix to-address] (utils/split-prefix-and-address address) testnet-enabled? (get-in db [:profile/profile :test-networks-enabled?]) goerli-enabled? (get-in db [:profile/profile :is-goerli-enabled?]) - prefix-seq (string/split prefix #":") - selected-networks (->> prefix-seq - (remove string/blank?) - (mapv - #(utils/network->chain-id - {:network % - :testnet-enabled? testnet-enabled? - :goerli-enabled? goerli-enabled?})))] + selected-networks (network-utils/resolve-receiver-networks + {:prefix prefix + :testnet-enabled? testnet-enabled? + :goerli-enabled? goerli-enabled?})] {:db (-> db (assoc-in [:wallet :ui :send :recipient] (or recipient address)) (assoc-in [:wallet :ui :send :to-address] to-address) @@ -84,7 +111,8 @@ (fn [{:keys [db]} [{:keys [token stack-id start-flow?]}]] {:db (-> db (update-in [:wallet :ui :send] dissoc :collectible) - (assoc-in [:wallet :ui :send :token] token)) + (assoc-in [:wallet :ui :send :token] token) + (assoc-in [:wallet :ui :send :token-display-name] (:symbol token))) :fx [[:dispatch [:wallet/clean-suggested-routes]] [:dispatch [:wallet/wizard-navigate-forward @@ -95,13 +123,15 @@ (rf/reg-event-fx :wallet/edit-token-to-send (fn [{:keys [db]} [token]] - {:db (assoc-in db [:wallet :ui :send :token] token) + {:db (-> db + (assoc-in [:wallet :ui :send :token] token) + (assoc-in [:wallet :ui :send :token-display-name] token)) :fx [[:dispatch [:hide-bottom-sheet]] [:dispatch [:wallet/clean-suggested-routes]]]})) (rf/reg-event-fx :wallet/clean-selected-token (fn [{:keys [db]}] - {:db (update-in db [:wallet :ui :send] dissoc :token :tx-type)})) + {:db (update-in db [:wallet :ui :send] dissoc :token :token-display-name :tx-type)})) (rf/reg-event-fx :wallet/clean-selected-collectible (fn [{:keys [db]}] @@ -110,18 +140,30 @@ [:wallet :ui :send] dissoc :collectible + :token-display-name :amount (when (= transaction-type :collecible) :tx-type))}))) (rf/reg-event-fx :wallet/send-collectibles-amount (fn [{:keys [db]} [{:keys [collectible stack-id amount]}]] - {:db (-> db - (update-in [:wallet :ui :send] dissoc :token) - (assoc-in [:wallet :ui :send :collectible] collectible) - (assoc-in [:wallet :ui :send :tx-type] :collectible) - (assoc-in [:wallet :ui :send :amount] amount)) - :fx [[:dispatch [:wallet/get-suggested-routes {:amount amount}]] - [:navigate-to-within-stack [:screen/wallet.transaction-confirmation stack-id]]]})) + (let [collection-data (:collection-data collectible) + collectible-data (:collectible-data collectible) + collectible-id (get-in collectible [:id :token-id]) + token-display-name (cond + (and collectible + (not (string/blank? (:name collectible-data)))) + (:name collectible-data) + + collectible + (str (:name collection-data) " #" collectible-id))] + {:db (-> db + (update-in [:wallet :ui :send] dissoc :token) + (assoc-in [:wallet :ui :send :collectible] collectible) + (assoc-in [:wallet :ui :send :token-display-name] token-display-name) + (assoc-in [:wallet :ui :send :tx-type] :collectible) + (assoc-in [:wallet :ui :send :amount] amount)) + :fx [[:dispatch [:wallet/get-suggested-routes {:amount amount}]] + [:navigate-to-within-stack [:screen/wallet.transaction-confirmation stack-id]]]}))) (rf/reg-event-fx :wallet/select-collectibles-amount (fn [{:keys [db]} [{:keys [collectible stack-id]}]] @@ -140,6 +182,12 @@ :start-flow? start-flow? :flow-id :wallet-flow}]]]})) +(rf/reg-event-fx :wallet/disable-from-networks + (fn [{:keys [db]} [chain-ids]] + {:db (-> db + (assoc-in [:wallet :ui :send :disabled-from-chain-ids] chain-ids) + (assoc-in [:wallet :ui :send :loading-suggested-routes?] true))})) + (rf/reg-event-fx :wallet/get-suggested-routes (fn [{:keys [db now]} [{:keys [amount]}]] (let [wallet-address (get-in db [:wallet :current-viewing-account-address]) @@ -147,6 +195,7 @@ transaction-type (get-in db [:wallet :ui :send :tx-type]) collectible (get-in db [:wallet :ui :send :collectible]) to-address (get-in db [:wallet :ui :send :to-address]) + disabled-from-chain-ids (or (get-in db [:wallet :ui :send :disabled-from-chain-ids]) []) test-networks-enabled? (get-in db [:profile/profile :test-networks-enabled?]) networks ((if test-networks-enabled? :test :prod) (get-in db [:wallet :networks])) @@ -163,7 +212,7 @@ gas-rates constants/gas-rate-medium amount-in (send-utils/amount-in-hex amount (if token token-decimal 0)) from-address wallet-address - disabled-from-chain-ids [] + disabled-from-chain-ids disabled-from-chain-ids disabled-to-chain-ids (if (= transaction-type :bridge) (filter #(not= % bridge-to-chain-id) network-chain-ids) []) @@ -218,6 +267,7 @@ {:fx [[:dispatch [:wallet/clean-scanned-address]] [:dispatch [:wallet/clean-local-suggestions]] [:dispatch [:wallet/clean-send-address]] + [:dispatch [:wallet/clean-disabled-from-networks]] [:dispatch [:wallet/select-address-tab nil]] [:dispatch [:dismiss-modal :screen/wallet.transaction-progress]]]})) diff --git a/src/status_im/contexts/wallet/send/input_amount/component_spec.cljs b/src/status_im/contexts/wallet/send/input_amount/component_spec.cljs index 2fa8e65707..14cbba608e 100644 --- a/src/status_im/contexts/wallet/send/input_amount/component_spec.cljs +++ b/src/status_im/contexts/wallet/send/input_amount/component_spec.cljs @@ -5,60 +5,75 @@ [status-im.contexts.wallet.send.input-amount.view :as input-amount] [test-helpers.component :as h] [utils.debounce :as debounce] + [utils.money :as money] [utils.re-frame :as rf])) (set! rf/dispatch #()) (set! debounce/debounce-and-dispatch #()) (def sub-mocks - {:profile/profile {:currency :usd} - :wallet/selected-network-details [{:source 525 - :short-name "eth" - :network-name :mainnet - :chain-id 1 - :related-chain-id 5}] - :wallet/current-viewing-account {:path "m/44'/60'/0'/0/1" - :emoji "💎" - :key-uid "0x2f5ea39" - :address "0x1" - :wallet false - :name "Account One" - :type :generated - :watch-only? false - :chat false - :test-preferred-chain-ids #{5 420 421613} - :color :purple - :hidden false - :prod-preferred-chain-ids #{1 10 42161} - :network-preferences-names #{:mainnet :arbitrum - :optimism} - :position 1 - :clock 1698945829328 - :created-at 1698928839000 - :operable "fully" - :mixedcase-address "0x7bcDfc75c431" - :public-key "0x04371e2d9d66b82f056bc128064" - :removed false} - :wallet/wallet-send-token {:symbol :eth - :total-balance 100 - :market-values-per-currency {:usd {:price 10}}} - :wallet/wallet-send-loading-suggested-routes? false - :wallet/wallet-send-route [{:from {:chainid 1 - :native-currency-symbol "ETH"} - :to {:chain-id 1 - :native-currency-symbol "ETH"} - :gas-amount "23487" - :gas-fees {:base-fee "32.325296406" - :max-priority-fee-per-gas "0.011000001" - :eip1559-enabled true}}] - :wallet/wallet-send-suggested-routes {:candidates []} - :wallet/wallet-send-selected-networks [] - :view-id :screen/wallet.send-input-amount - :wallet/wallet-send-to-address "0x04371e2d9d66b82f056bc128064" - :profile/currency-symbol "$" - :wallet/token-by-symbol {:symbol :eth - :total-balance 100 - :market-values-per-currency {:usd {:price 10}}}}) + {:profile/profile {:currency :usd} + :wallet/selected-network-details [{:source 525 + :short-name "eth" + :network-name :mainnet + :chain-id 1 + :related-chain-id 5}] + :wallet/current-viewing-account {:path "m/44'/60'/0'/0/1" + :emoji "💎" + :key-uid "0x2f5ea39" + :address "0x1" + :wallet false + :name "Account One" + :type :generated + :watch-only? false + :chat false + :test-preferred-chain-ids #{5 420 421613} + :color :purple + :hidden false + :prod-preferred-chain-ids #{1 10 42161} + :network-preferences-names #{:mainnet :arbitrum + :optimism} + :position 1 + :clock 1698945829328 + :created-at 1698928839000 + :operable "fully" + :mixedcase-address "0x7bcDfc75c431" + :public-key "0x04371e2d9d66b82f056bc128064" + :removed false} + :wallet/wallet-send-token {:symbol :eth + :networks [{:source 879 + :short-name "eth" + :network-name :mainnet + :abbreviated-name "Eth." + :chain-id 1 + :related-chain-id 1 + :layer 1}]} + :wallet/current-viewing-account-tokens-filtered {:balances-per-chain {1 {:raw-balance + (money/bignumber + "2500") + :has-error false}} + :total-balance 100 + :market-values-per-currency {:usd {:price 10}}} + :wallet/wallet-send-loading-suggested-routes? false + :wallet/wallet-send-route [{:from {:chainid 1 + :native-currency-symbol "ETH"} + :to {:chain-id 1 + :native-currency-symbol "ETH"} + :gas-amount "23487" + :gas-fees {:base-fee "32.325296406" + :max-priority-fee-per-gas "0.011000001" + :eip1559-enabled true}}] + :wallet/wallet-send-suggested-routes {:candidates []} + :wallet/wallet-send-selected-networks [1] + :view-id :screen/wallet.send-input-amount + :wallet/wallet-send-to-address "0x04371e2d9d66b82f056bc128064" + :profile/currency-symbol "$" + :wallet/token-by-symbol {:symbol :eth + :total-balance 100 + :market-values-per-currency {:usd {:price 10}}} + :wallet/wallet-send-disabled-from-chain-ids [] + :wallet/wallet-send-from-values-by-chain {1 (money/bignumber "250")} + :wallet/wallet-send-to-values-by-chain {1 (money/bignumber "250")}}) (h/describe "Send > input amount screen" (h/setup-restorable-re-frame) diff --git a/src/status_im/contexts/wallet/send/input_amount/view.cljs b/src/status_im/contexts/wallet/send/input_amount/view.cljs index 4c77587c70..b2505e062a 100644 --- a/src/status_im/contexts/wallet/send/input_amount/view.cljs +++ b/src/status_im/contexts/wallet/send/input_amount/view.cljs @@ -190,8 +190,9 @@ (reset-input-error num-value current-limit-amount input-error) (reagent/flush)))) on-navigate-back on-navigate-back - fetch-routes (fn [input-num-value current-limit-amount] - (let [nav-current-screen-id (rf/sub [:view-id])] + fetch-routes (fn [input-num-value current-limit-amount bounce-duration-ms] + (let [nav-current-screen-id (rf/sub [:view-id]) + input-num-value (or input-num-value 0)] ; this check is to prevent effect being triggered when screen is ; loaded but not being shown to the user (deep in the navigation ; stack) and avoid undesired behaviors @@ -201,7 +202,7 @@ (> input-num-value current-limit-amount)) (debounce/debounce-and-dispatch [:wallet/get-suggested-routes {:amount @input-value}] - 2000) + bounce-duration-ms) (rf/dispatch [:wallet/clean-suggested-routes]))))) handle-on-confirm (fn [] (rf/dispatch [:wallet/send-select-amount @@ -215,65 +216,92 @@ (reset! input-selection selection) (reagent/flush))] (fn [] - (let [{fiat-currency :currency} (rf/sub [:profile/profile]) - {token-balance :total-balance - token-symbol :symbol - token-networks :networks - :as token} (rf/sub [:wallet/wallet-send-token]) - conversion-rate (-> token :market-values-per-currency :usd :price) - loading-routes? (rf/sub [:wallet/wallet-send-loading-suggested-routes?]) - suggested-routes (rf/sub [:wallet/wallet-send-suggested-routes]) - best-routes (when suggested-routes (or (:best suggested-routes) [])) - route (rf/sub [:wallet/wallet-send-route]) - to-address (rf/sub [:wallet/wallet-send-to-address]) - on-confirm (or default-on-confirm handle-on-confirm) - crypto-decimals (or default-crypto-decimals - (utils/get-crypto-decimals-count token)) - crypto-limit (or default-limit-crypto - (utils/get-standard-crypto-format token token-balance)) - fiat-limit (.toFixed (* token-balance conversion-rate) 2) - current-limit #(if @crypto-currency? crypto-limit fiat-limit) - current-currency (if @crypto-currency? token-symbol fiat-currency) - limit-label (make-limit-label {:amount (current-limit) - :currency current-currency}) - input-num-value (parse-double @input-value) - confirm-disabled? (or (nil? route) - (empty? route) - (empty? @input-value) - (<= input-num-value 0) - (> input-num-value (current-limit))) - amount-text (str @input-value " " token-symbol) - native-currency-symbol (when-not confirm-disabled? - (get-in route [:from :native-currency-symbol])) - native-token (when native-currency-symbol - (rf/sub [:wallet/token-by-symbol native-currency-symbol])) - fee-in-native-token (when-not confirm-disabled? - (send-utils/calculate-full-route-gas-fee route)) - fee-in-crypto-formatted (when fee-in-native-token - (utils/get-standard-crypto-format native-token - fee-in-native-token)) - fee-in-fiat (when-not confirm-disabled? - (utils/calculate-token-fiat-value - {:currency fiat-currency - :balance fee-in-native-token - :token native-token})) - currency-symbol (rf/sub [:profile/currency-symbol]) - fee-formatted (when fee-in-fiat - (utils/get-standard-fiat-format fee-in-crypto-formatted - currency-symbol - fee-in-fiat)) - show-select-asset-sheet #(rf/dispatch - [:show-bottom-sheet - {:content (fn [] - [select-asset-bottom-sheet clear-input!])}])] + (let [{fiat-currency :currency} (rf/sub [:profile/profile]) + {token-symbol :symbol + token-networks :networks} (rf/sub [:wallet/wallet-send-token]) + {token-balance :total-balance + token-balances-per-chain :balances-per-chain + :as + token} (rf/sub + [:wallet/current-viewing-account-tokens-filtered + (str token-symbol)]) + conversion-rate (-> token :market-values-per-currency :usd :price) + loading-routes? (rf/sub + [:wallet/wallet-send-loading-suggested-routes?]) + suggested-routes (rf/sub [:wallet/wallet-send-suggested-routes]) + best-routes (when suggested-routes + (or (:best suggested-routes) [])) + route (rf/sub [:wallet/wallet-send-route]) + to-address (rf/sub [:wallet/wallet-send-to-address]) + disabled-from-chain-ids (rf/sub + [:wallet/wallet-send-disabled-from-chain-ids]) + from-values-by-chain (rf/sub [:wallet/wallet-send-from-values-by-chain]) + to-values-by-chain (rf/sub [:wallet/wallet-send-to-values-by-chain]) + on-confirm (or default-on-confirm handle-on-confirm) + crypto-decimals (or default-crypto-decimals + (utils/get-crypto-decimals-count token)) + crypto-limit (or default-limit-crypto + (utils/get-standard-crypto-format + token + token-balance)) + fiat-limit (.toFixed (* token-balance conversion-rate) 2) + current-limit #(if @crypto-currency? crypto-limit fiat-limit) + current-currency (if @crypto-currency? token-symbol fiat-currency) + limit-label (make-limit-label {:amount (current-limit) + :currency current-currency}) + input-num-value (parse-double @input-value) + confirm-disabled? (or (nil? route) + (empty? route) + (empty? @input-value) + (<= input-num-value 0) + (> input-num-value (current-limit))) + amount-text (str @input-value " " token-symbol) + native-currency-symbol (when-not confirm-disabled? + (get-in route [:from :native-currency-symbol])) + native-token (when native-currency-symbol + (rf/sub [:wallet/token-by-symbol + native-currency-symbol])) + fee-in-native-token (when-not confirm-disabled? + (send-utils/calculate-full-route-gas-fee route)) + fee-in-crypto-formatted (when fee-in-native-token + (utils/get-standard-crypto-format + native-token + fee-in-native-token)) + fee-in-fiat (when-not confirm-disabled? + (utils/calculate-token-fiat-value + {:currency fiat-currency + :balance fee-in-native-token + :token native-token})) + currency-symbol (rf/sub [:profile/currency-symbol]) + fee-formatted (when fee-in-fiat + (utils/get-standard-fiat-format + fee-in-crypto-formatted + currency-symbol + fee-in-fiat)) + show-select-asset-sheet #(rf/dispatch + [:show-bottom-sheet + {:content (fn [] + [select-asset-bottom-sheet + clear-input!])}]) + selected-networks (rf/sub [:wallet/wallet-send-selected-networks]) + affordable-networks (send-utils/find-affordable-networks + {:balances-per-chain token-balances-per-chain + :input-value @input-value + :selected-networks selected-networks + :disabled-chain-ids disabled-from-chain-ids})] (rn/use-mount (fn [] (let [dismiss-keyboard-fn #(when (= % "active") (rn/dismiss-keyboard!)) app-keyboard-listener (.addEventListener rn/app-state "change" dismiss-keyboard-fn)] #(.remove app-keyboard-listener)))) (rn/use-effect - #(fetch-routes input-num-value (current-limit)) + #(when (> (count affordable-networks) 0) + (fetch-routes input-num-value (current-limit) 2000)) [@input-value]) + (rn/use-effect + #(when (> (count affordable-networks) 0) + (fetch-routes input-num-value (current-limit) 0)) + [disabled-from-chain-ids]) [rn/view {:style style/screen :accessibility-label (str "container" (when @input-error "-error"))} @@ -303,11 +331,28 @@ :limit-crypto crypto-limit}) :on-token-press show-select-asset-sheet}] [routes/view - {:amount amount-text - :routes best-routes - :token token - :input-value @input-value - :fetch-routes #(fetch-routes % (current-limit))}] + {:from-values-by-chain from-values-by-chain + :to-values-by-chain to-values-by-chain + :affordable-networks affordable-networks + :routes best-routes + :token token + :input-value @input-value + :fetch-routes #(fetch-routes % (current-limit) 2000) + :disabled-from-networks disabled-from-chain-ids + :on-press-from-network (fn [chain-id _] + (let [disabled-chain-ids (if (contains? (set + disabled-from-chain-ids) + chain-id) + (vec (remove #(= % chain-id) + disabled-from-chain-ids)) + (conj disabled-from-chain-ids + chain-id)) + re-enabling-chain? (< (count disabled-chain-ids) + (count disabled-from-chain-ids))] + (when (or re-enabling-chain? + (> (count affordable-networks) 1)) + (rf/dispatch [:wallet/disable-from-networks + disabled-chain-ids]))))}] (when (or loading-routes? (seq route)) [estimated-fees {:loading-suggested-routes? loading-routes? diff --git a/src/status_im/contexts/wallet/send/routes/style.cljs b/src/status_im/contexts/wallet/send/routes/style.cljs index a492e618d8..64a25e8cb6 100644 --- a/src/status_im/contexts/wallet/send/routes/style.cljs +++ b/src/status_im/contexts/wallet/send/routes/style.cljs @@ -12,20 +12,23 @@ {:flex-direction :row :justify-content :space-between}) -(def routes-inner-container - {:margin-top 8 +(defn routes-inner-container + [first-item?] + {:margin-top (if first-item? 7.5 11) :flex-direction :row :align-items :center :justify-content :space-between}) -(defn section-label - [margin-left] - {:flex 0.5 - :margin-left margin-left}) +(def section-label-right + {:width 135}) + +(def section-label-left + {:width 136}) (def network-link - {:right 6 - :z-index 1}) + {:margin-horizontal -1.5 + :z-index 1 + :flex 1}) (def empty-container {:flex-grow 1 @@ -33,9 +36,8 @@ :justify-content :center}) (def add-network - {:margin-top 8 - :align-self :flex-end - :right 12}) + {:margin-top 11 + :align-self :flex-end}) (defn warning-container [color theme] diff --git a/src/status_im/contexts/wallet/send/routes/view.cljs b/src/status_im/contexts/wallet/send/routes/view.cljs index cf6564250c..81f460fa46 100644 --- a/src/status_im/contexts/wallet/send/routes/view.cljs +++ b/src/status_im/contexts/wallet/send/routes/view.cljs @@ -7,19 +7,17 @@ [quo.theme :as quo.theme] [react-native.core :as rn] [reagent.core :as reagent] + [status-im.constants :as constants] [status-im.contexts.wallet.common.utils :as utils] [status-im.contexts.wallet.send.routes.style :as style] [utils.i18n :as i18n] - [utils.re-frame :as rf])) + [utils.re-frame :as rf] + [utils.vector :as vector-utils])) -(defn- find-affordable-networks - [{:keys [balances-per-chain]} input-value selected-networks] - (->> balances-per-chain - (filter (fn [[_ {:keys [balance chain-id]}]] - (and - (>= (js/parseFloat balance) input-value) - (some #(= % chain-id) selected-networks)))) - (map first))) +(def ^:private network-priority-score + {:ethereum 1 + :optimism 2 + :arbitrum 3}) (defn- make-network-item [{:keys [network-name chain-id] :as _network} @@ -34,6 +32,36 @@ :checked? (some #(= % chain-id) @network-preferences) :on-change on-change}}) +(defn- find-network-link-insertion-index + [network-links chain-id loading-suggested-routes?] + (let [network (utils/id->network chain-id) + inserted-network-link-priority-score (network-priority-score network)] + (or (->> network-links + (keep-indexed (fn [idx network-link] + (let [network-link (utils/id->network (if loading-suggested-routes? + network-link + (get-in network-link + [:from :chain-id])))] + (when (> (network-priority-score network-link) + inserted-network-link-priority-score) + idx)))) + first) + (count network-links)))) + +(defn- add-disabled-networks + [network-links disabled-from-networks loading-suggested-routes?] + (let [sorted-networks (sort-by (comp network-priority-score utils/id->network) disabled-from-networks)] + (reduce (fn [acc-network-links chain-id] + (let [index (find-network-link-insertion-index acc-network-links + chain-id + loading-suggested-routes?) + disabled-network-link {:status :disabled + :chain-id chain-id + :network (utils/id->network chain-id)}] + (vector-utils/insert-element-at acc-network-links disabled-network-link index))) + network-links + sorted-networks))) + (defn networks-drawer [{:keys [fetch-routes theme]}] (let [network-details (rf/sub [:wallet/network-details]) @@ -90,7 +118,9 @@ :customization-color color}}]]))) (defn route-item - [{:keys [amount from-network to-network status theme fetch-routes]}] + [{:keys [first-item? from-amount to-amount token-symbol from-chain-id to-chain-id from-network + to-network on-press-from-network on-press-to-network status theme fetch-routes disabled? + loading?]}] (if (= status :add) [quo/network-bridge {:status :add @@ -99,59 +129,115 @@ {:content (fn [] [networks-drawer {:theme theme :fetch-routes fetch-routes}])}])}] - [rn/view {:style style/routes-inner-container} + [rn/view {:style (style/routes-inner-container first-item?)} [quo/network-bridge - {:amount amount - :network from-network - :status status}] + {:amount (str from-amount " " token-symbol) + :network from-network + :status status + :on-press #(when (and on-press-from-network (not loading?)) + (on-press-from-network from-chain-id from-amount))}] (if (= status :default) [quo/network-link {:shape :linear :source from-network :destination to-network :container-style style/network-link}] - [rn/view {:style {:width 73}}]) + [rn/view {:style {:flex 1}}]) [quo/network-bridge - {:amount amount - :network to-network - :status status - :container-style {:right 12}}]])) + {:amount (str to-amount " " token-symbol) + :network to-network + :status (cond + (and disabled? loading?) :loading + (and disabled? (not loading?)) :default + :else status) + :on-press #(when (and on-press-to-network (not loading?)) + (on-press-to-network to-chain-id to-amount))}]])) + +(defn- render-network-link + [item index _ + {:keys [from-values-by-chain to-values-by-chain theme fetch-routes on-press-from-network + on-press-to-network token-symbol loading-suggested-routes?]}] + (let [first-item? (zero? index) + disabled-network? (= (:status item) :disabled) + from-chain-id (get-in item [:from :chain-id]) + to-chain-id (get-in item [:to :chain-id]) + from-amount (when from-chain-id + (from-values-by-chain from-chain-id)) + to-amount (when to-chain-id + (to-values-by-chain to-chain-id))] + [route-item + {:first-item? first-item? + :from-amount (if disabled-network? 0 from-amount) + :to-amount (if disabled-network? 0 to-amount) + :token-symbol token-symbol + :disabled? disabled-network? + :loading? loading-suggested-routes? + :theme theme + :fetch-routes fetch-routes + :status (cond + (= (:status item) :add) :add + (= (:status item) :disabled) :disabled + loading-suggested-routes? :loading + :else :default) + :from-chain-id (or from-chain-id (:chain-id item)) + :to-chain-id (or to-chain-id (:chain-id item)) + :from-network (cond (and loading-suggested-routes? + (not disabled-network?)) + (utils/id->network item) + disabled-network? + (utils/id->network (:chain-id + item)) + :else + (utils/id->network from-chain-id)) + :to-network (cond (and loading-suggested-routes? + (not disabled-network?)) + (utils/id->network item) + disabled-network? + (utils/id->network (:chain-id + item)) + :else + (utils/id->network to-chain-id)) + :on-press-from-network on-press-from-network + :on-press-to-network on-press-to-network}])) (defn- view-internal - [{:keys [amount routes token input-value theme fetch-routes]}] - (let [selected-networks (rf/sub [:wallet/wallet-send-selected-networks]) - loading-networks (find-affordable-networks token input-value selected-networks) + [{:keys [from-values-by-chain to-values-by-chain routes token theme fetch-routes + affordable-networks disabled-from-networks on-press-from-network on-press-to-network]}] + (let [token-symbol (:symbol token) loading-suggested-routes? (rf/sub [:wallet/wallet-send-loading-suggested-routes?]) - data (if loading-suggested-routes? loading-networks routes)] - (if (or (and (not-empty loading-networks) loading-suggested-routes?) (not-empty routes)) - [rn/flat-list - {:data (if (and (< (count data) 3) (pos? (count data))) - (concat data [{:status :add}]) - data) - :content-container-style style/routes-container - :header [rn/view {:style style/routes-header-container} - [quo/section-label - {:section (i18n/label :t/from-label) - :container-style (style/section-label 0)}] - [quo/section-label - {:section (i18n/label :t/to-label) - :container-style (style/section-label 64)}]] - :render-fn (fn [item] - [route-item - {:amount amount - :theme theme - :fetch-routes fetch-routes - :status (cond - (= (:status item) :add) :add - loading-suggested-routes? :loading - :else :default) - :from-network (if loading-suggested-routes? - (utils/id->network item) - (utils/id->network (get-in item [:from :chain-id]))) - :to-network (if loading-suggested-routes? - (utils/id->network item) - (utils/id->network (get-in item - [:to :chain-id])))}])}] + network-links (if loading-suggested-routes? affordable-networks routes)] + (if (or (and (not-empty affordable-networks) loading-suggested-routes?) (not-empty routes)) + (let [initial-network-links-count (count network-links) + disabled-count (count disabled-from-networks) + network-links (if (not-empty disabled-from-networks) + (add-disabled-networks network-links + disabled-from-networks + loading-suggested-routes?) + network-links) + network-links-with-add-button (if (and (< (- (count network-links) disabled-count) + constants/default-network-count) + (pos? initial-network-links-count)) + (concat network-links [{:status :add}]) + network-links)] + [rn/flat-list + {:data network-links-with-add-button + :content-container-style style/routes-container + :header [rn/view {:style style/routes-header-container} + [quo/section-label + {:section (i18n/label :t/from-label) + :container-style style/section-label-left}] + [quo/section-label + {:section (i18n/label :t/to-label) + :container-style style/section-label-right}]] + :render-data {:from-values-by-chain from-values-by-chain + :to-values-by-chain to-values-by-chain + :theme theme + :fetch-routes fetch-routes + :on-press-from-network on-press-from-network + :on-press-to-network on-press-to-network + :token-symbol token-symbol + :loading-suggested-routes? loading-suggested-routes?} + :render-fn render-network-link}]) [rn/view {:style style/empty-container} (when (and (not (nil? routes)) (not loading-suggested-routes?)) [quo/text (i18n/label :t/no-routes-found)])]))) diff --git a/src/status_im/contexts/wallet/send/select_address/view.cljs b/src/status_im/contexts/wallet/send/select_address/view.cljs index c8ce59f5b0..fb0aade3be 100644 --- a/src/status_im/contexts/wallet/send/select_address/view.cljs +++ b/src/status_im/contexts/wallet/send/select_address/view.cljs @@ -132,6 +132,7 @@ (rf/dispatch [:wallet/clean-selected-token]) (rf/dispatch [:wallet/clean-selected-collectible]) (rf/dispatch [:wallet/clean-send-address]) + (rf/dispatch [:wallet/clean-disabled-from-networks]) (rf/dispatch [:wallet/select-address-tab nil]) (rf/dispatch [:navigate-back])) on-change-tab #(rf/dispatch [:wallet/select-address-tab %]) diff --git a/src/status_im/contexts/wallet/send/send_amount/view.cljs b/src/status_im/contexts/wallet/send/send_amount/view.cljs index 3e8e5d7591..8f7ec9041e 100644 --- a/src/status_im/contexts/wallet/send/send_amount/view.cljs +++ b/src/status_im/contexts/wallet/send/send_amount/view.cljs @@ -10,6 +10,8 @@ [input-amount/view {:current-screen-id :screen/wallet.send-input-amount :button-one-label (i18n/label :t/confirm) - :on-navigate-back #(rf/dispatch [:navigate-back])}]) + :on-navigate-back (fn [] + (rf/dispatch [:wallet/clean-disabled-from-networks]) + (rf/dispatch [:navigate-back]))}]) (def view (quo.theme/with-theme view-internal)) diff --git a/src/status_im/contexts/wallet/send/transaction_confirmation/view.cljs b/src/status_im/contexts/wallet/send/transaction_confirmation/view.cljs index 9788005357..c136fb0498 100644 --- a/src/status_im/contexts/wallet/send/transaction_confirmation/view.cljs +++ b/src/status_im/contexts/wallet/send/transaction_confirmation/view.cljs @@ -1,23 +1,21 @@ (ns status-im.contexts.wallet.send.transaction-confirmation.view (:require [clojure.string :as string] - [legacy.status-im.utils.hex :as utils.hex] [legacy.status-im.utils.utils :as utils] - [native-module.core :as native-module] [quo.core :as quo] [quo.theme :as quo.theme] [react-native.core :as rn] [react-native.safe-area :as safe-area] [status-im.common.floating-button-page.view :as floating-button-page] [status-im.common.standard-authentication.core :as standard-auth] + [status-im.contexts.wallet.common.utils :as wallet-utils] [status-im.contexts.wallet.send.transaction-confirmation.style :as style] [utils.i18n :as i18n] - [utils.money :as money] [utils.re-frame :as rf] [utils.security.core :as security])) (defn- transaction-title - [{:keys [token-symbol amount account to-address route to-network image-url transaction-type + [{:keys [token-display-name amount account to-address route to-network image-url transaction-type collectible?]}] (let [to-network-name (:network-name to-network) to-network-color (if (= to-network-name :mainnet) :ethereum to-network-name)] @@ -32,8 +30,8 @@ (i18n/label :t/bridge) (i18n/label :t/send))] [quo/summary-tag - {:token (if collectible? "" token-symbol) - :label (str amount " " token-symbol) + {:token (if collectible? "" token-display-name) + :label (str amount " " token-display-name) :type (if collectible? :collectible :token) :image-source (if collectible? image-url :eth)}]] (if (= transaction-type :bridge) @@ -125,84 +123,34 @@ :emoji (:emoji account) :customization-color (:color account)}]])])) -(defn network-name-from-chain-id - [chain-id] - (let [network-name (-> (rf/sub [:wallet/network-details-by-chain-id chain-id]) - :network-name)] - (if (= network-name :mainnet) :ethereum network-name))) - -(defn- network-amounts-from-route - [{:keys [route token-symbol token-decimals to?]}] - (reduce (fn [acc path] - (let [network (if to? (:to path) (:from path)) - chain-id (:chain-id network) - amount-hex (if to? (:amount-in path) (:amount-out path)) - amount-units (native-module/hex-to-number - (utils.hex/normalize-hex amount-hex)) - amount (money/with-precision - (if (= token-symbol "ETH") - (money/wei->ether amount-units) - (money/token->unit amount-units - token-decimals)) - 6) - network-name (network-name-from-chain-id chain-id)] - (merge-with money/add acc {network-name amount}))) - {} - route)) - -(defn- network-values-from-amounts - [network-amounts token-symbol] - (reduce-kv (fn [acc k v] - (assoc acc - k - {:amount v - :token-symbol token-symbol})) - {} - network-amounts)) - -(defn- sanitize-network-values - [network-values] - (into {} - (map (fn [[k v]] - [k - (if (money/equal-to (v :amount) 0) - (assoc v :amount "<0.01") - v)]) - network-values))) - -(defn- values-by-network - [{:keys [collectible amount token-symbol route token-decimals to?]}] - (if collectible - (let [collectible-chain-id (get-in collectible [:id :contract-id :chain-id]) - network-name (network-name-from-chain-id collectible-chain-id)] - {network-name {:amount amount :token-symbol token-symbol}}) - (let [network-amounts (network-amounts-from-route {:route route - :token-symbol token-symbol - :token-decimals token-decimals - :to? to?}) - network-values (network-values-from-amounts network-amounts token-symbol)] - (sanitize-network-values network-values)))) - (defn- user-summary - [{:keys [account-props theme label accessibility-label - summary-type network-values] - :as _props}] - [rn/view - {:style {:padding-horizontal 20 - :padding-bottom 16}} - [quo/text - {:size :paragraph-2 - :weight :medium - :style (style/section-label theme) - :accessibility-label accessibility-label} - label] - [quo/summary-info - {:type summary-type - :networks? true - :values network-values - :account-props account-props}]]) + [{:keys [network-values token-display-name account-props theme label accessibility-label + summary-type]}] + (let [network-values + (reduce-kv + (fn [acc chain-id amount] + (let [network-name (wallet-utils/id->network chain-id)] + (assoc acc + (if (= network-name :mainnet) :ethereum network-name) + {:amount amount :token-symbol token-display-name}))) + {} + network-values)] + [rn/view + {:style {:padding-horizontal 20 + :padding-bottom 16}} + [quo/text + {:size :paragraph-2 + :weight :medium + :style (style/section-label theme) + :accessibility-label accessibility-label} + label] + [quo/summary-info + {:type summary-type + :networks? true + :values network-values + :account-props account-props}]])) -(defn data-item +(defn- data-item [{:keys [title subtitle]}] [quo/data-item {:container-style style/detail-item @@ -217,7 +165,8 @@ :subtitle subtitle}]) (defn- transaction-details - [{:keys [estimated-time-min max-fees token-symbol amount to-address to-network route transaction-type + [{:keys [estimated-time-min max-fees token-display-name amount to-address to-network route + transaction-type theme]}] (let [currency-symbol (rf/sub [:profile/currency-symbol]) route-loaded? (and route (seq route)) @@ -253,56 +202,42 @@ (i18n/label :t/bridged-to {:network (:abbreviated-name to-network)}) (i18n/label :t/user-gets {:name (utils/get-shortened-address to-address)})) - :subtitle (str amount " " token-symbol)}]] + :subtitle (str amount " " token-display-name)}]] :else [quo/text {:style {:align-self :center}} (i18n/label :t/no-routes-found-confirmation)])]])) -(defn collectible-token-symbol - [collectible] - (let [collection-data (:collection-data collectible) - collectible-data (:collectible-data collectible) - collectible-id (get-in collectible [:id :token-id])] - (first (remove - string/blank? - [(:name collectible-data) - (str (:name collection-data) " #" collectible-id)])))) - (defn- view-internal [_] (let [on-close (fn [] (rf/dispatch [:wallet/clean-suggested-routes]) (rf/dispatch [:navigate-back]))] (fn [{:keys [theme]}] - (let [send-transaction-data (rf/sub [:wallet/wallet-send]) - {:keys [token collectible amount route - to-address bridge-to-chain-id]} send-transaction-data - collectible? (some? collectible) - token-symbol (if collectible - (collectible-token-symbol collectible) - (:symbol token)) - token-decimals (if collectible 0 (:decimals token)) - image-url (when collectible - (get-in collectible [:preview-url :uri])) - transaction-type (:tx-type send-transaction-data) - estimated-time-min (reduce + (map :estimated-time route)) - max-fees "-" - account (rf/sub [:wallet/current-viewing-account]) - account-color (:color account) - bridge-to-network (when bridge-to-chain-id - (rf/sub [:wallet/network-details-by-chain-id - bridge-to-chain-id])) - from-account-props {:customization-color account-color - :size 32 - :emoji (:emoji account) - :type :default - :name (:name account) - :address (utils/get-shortened-address - (:address - account))} - user-props {:full-name to-address - :address (utils/get-shortened-address - to-address)}] + (let [send-transaction-data (rf/sub [:wallet/wallet-send]) + {:keys [token-display-name collectible amount route + to-address bridge-to-chain-id + from-values-by-chain + to-values-by-chain]} send-transaction-data + collectible? (some? collectible) + image-url (when collectible + (get-in collectible [:preview-url :uri])) + transaction-type (:tx-type send-transaction-data) + estimated-time-min (reduce + (map :estimated-time route)) + max-fees "-" + account (rf/sub [:wallet/current-viewing-account]) + account-color (:color account) + bridge-to-network (when bridge-to-chain-id + (rf/sub [:wallet/network-details-by-chain-id + bridge-to-chain-id])) + from-account-props {:customization-color account-color + :size 32 + :emoji (:emoji account) + :type :default + :name (:name account) + :address (utils/get-shortened-address (:address + account))} + user-props {:full-name to-address + :address (utils/get-shortened-address to-address)}] [rn/view {:style {:flex 1}} [floating-button-page/view {:footer-container-padding 0 @@ -329,29 +264,26 @@ :customization-color (:color account)} [rn/view [transaction-title - {:token-symbol token-symbol - :amount amount - :account account - :to-address to-address - :route route - :to-network bridge-to-network - :image-url image-url - :transaction-type transaction-type - :collectible? collectible?}] + {:token-display-name token-display-name + :amount amount + :account account + :to-address to-address + :route route + :to-network bridge-to-network + :image-url image-url + :transaction-type transaction-type + :collectible? collectible?}] [user-summary - {:summary-type :status-account + {:token-display-name token-display-name + :summary-type :status-account :accessibility-label :summary-from-label :label (i18n/label :t/from-capitalized) + :network-values from-values-by-chain :account-props from-account-props - :theme theme - :network-values (values-by-network {:collectible collectible - :amount amount - :token-symbol token-symbol - :route route - :token-decimals token-decimals - :to? false})}] + :theme theme}] [user-summary - {:summary-type (if (= transaction-type :bridge) + {:token-display-name token-display-name + :summary-type (if (= transaction-type :bridge) :status-account :account) :accessibility-label :summary-to-label @@ -359,17 +291,12 @@ :account-props (if (= transaction-type :bridge) from-account-props user-props) - :theme theme - :network-values (values-by-network {:collectible collectible - :amount amount - :token-symbol token-symbol - :route route - :token-decimals token-decimals - :to? true})}] + :network-values to-values-by-chain + :theme theme}] [transaction-details {:estimated-time-min estimated-time-min :max-fees max-fees - :token-symbol token-symbol + :token-display-name token-display-name :amount amount :to-address to-address :to-network bridge-to-network diff --git a/src/status_im/contexts/wallet/send/utils.cljs b/src/status_im/contexts/wallet/send/utils.cljs index f5768fd149..f9b7607437 100644 --- a/src/status_im/contexts/wallet/send/utils.cljs +++ b/src/status_im/contexts/wallet/send/utils.cljs @@ -1,5 +1,8 @@ (ns status-im.contexts.wallet.send.utils - (:require [utils.money :as money])) + (:require + [legacy.status-im.utils.hex :as utils.hex] + [native-module.core :as native-module] + [utils.money :as money])) (defn amount-in-hex [amount token-decimal] @@ -20,3 +23,27 @@ value1))) {} transaction-hashes)) + +(defn network-amounts-by-chain + [{:keys [route token-decimals native-token? to?]}] + (reduce (fn [acc path] + (let [amount-hex (if to? (:amount-in path) (:amount-out path)) + amount-units (native-module/hex-to-number + (utils.hex/normalize-hex amount-hex)) + amount (money/with-precision + (if native-token? + (money/wei->ether amount-units) + (money/token->unit amount-units + token-decimals)) + 6) + chain-id (if to? (get-in path [:to :chain-id]) (get-in path [:from :chain-id]))] + (update acc chain-id money/add amount))) + {} + route)) + +(defn network-values-for-ui + [amounts] + (reduce-kv (fn [acc k v] + (assoc acc k (if (money/equal-to v 0) "<0.01" v))) + {} + amounts)) diff --git a/src/status_im/contexts/wallet/send/utils_test.cljs b/src/status_im/contexts/wallet/send/utils_test.cljs index 81a87678ce..70d1a25ba7 100644 --- a/src/status_im/contexts/wallet/send/utils_test.cljs +++ b/src/status_im/contexts/wallet/send/utils_test.cljs @@ -1,6 +1,7 @@ (ns status-im.contexts.wallet.send.utils-test (:require [cljs.test :refer [deftest is testing]] - [status-im.contexts.wallet.send.utils :as utils])) + [status-im.contexts.wallet.send.utils :as utils] + [utils.money :as money])) (deftest test-amount-in-hex (testing "Test amount-in-hex function" @@ -27,3 +28,68 @@ "0x11" {:status :pending :id 61 :chain-id :420}}))))) + +(deftest test-network-amounts-by-chain + (testing "Correctly calculates network amounts for transaction with native token" + (let [route [{:amount-in "0xde0b6b3a7640000" + :to {:chain-id "1"}} + {:amount-in "0xde0b6b3a7640000" + :to {:chain-id "2"}}] + token-decimals 18 + native-token? true + to? true + result (utils/network-amounts-by-chain {:route route + :token-decimals token-decimals + :native-token? native-token? + :to? to?}) + expected {"1" (money/bignumber "1") + "2" (money/bignumber "1")}] + (doseq [[chain-id exp-value] expected] + (is (money/equal-to (get result chain-id) exp-value))))) + + (testing + "Correctly calculates network amounts for transaction with native token and multiple routes to same chain-id" + (let [route [{:amount-in "0xde0b6b3a7640000" + :to {:chain-id "1"}} + {:amount-in "0xde0b6b3a7640000" + :to {:chain-id "1"}}] + token-decimals 18 + native-token? true + to? true + result (utils/network-amounts-by-chain {:route route + :token-decimals token-decimals + :native-token? native-token? + :to? to?}) + expected {"1" (money/bignumber "2")}] + (doseq [[chain-id exp-value] expected] + (is (money/equal-to (get result chain-id) exp-value))))) + + (testing "Correctly calculates network amounts for transaction with non-native token" + (let [route [{:amount-out "0x1e8480" + :from {:chain-id "1"}} + {:amount-out "0x1e8480" + :from {:chain-id "2"}}] + token-decimals 6 + native-token? false + to? false + result (utils/network-amounts-by-chain {:route route + :token-decimals token-decimals + :native-token? native-token? + :to? to?}) + expected {"1" (money/bignumber "2") + "2" (money/bignumber "2")}] + (doseq [[chain-id exp-value] expected] + (is (money/equal-to (get result chain-id) exp-value)))))) + +(deftest test-network-values-for-ui + (testing "Sanitizes values correctly for display" + (let [amounts {"1" (money/bignumber "0") + "2" (money/bignumber "2.5") + "3" (money/bignumber "0.005")} + result (utils/network-values-for-ui amounts) + expected {"1" "<0.01" + "2" (money/bignumber "2.5") + "3" (money/bignumber "0.005")}] + (doseq [[chain-id exp-value] expected] + (is #(or (= (get result chain-id) exp-value) + (money/equal-to (get result chain-id) exp-value))))))) diff --git a/src/status_im/subs/wallet/wallet.cljs b/src/status_im/subs/wallet/wallet.cljs index f76b117bc9..8307202cd7 100644 --- a/src/status_im/subs/wallet/wallet.cljs +++ b/src/status_im/subs/wallet/wallet.cljs @@ -95,6 +95,21 @@ :<- [:wallet/wallet-send] :-> :token) +(rf/reg-sub + :wallet/wallet-send-disabled-from-chain-ids + :<- [:wallet/wallet-send] + :-> :disabled-from-chain-ids) + +(rf/reg-sub + :wallet/wallet-send-from-values-by-chain + :<- [:wallet/wallet-send] + :-> :from-values-by-chain) + +(rf/reg-sub + :wallet/wallet-send-to-values-by-chain + :<- [:wallet/wallet-send] + :-> :to-values-by-chain) + (rf/reg-sub :wallet/wallet-send-loading-suggested-routes? :<- [:wallet/wallet-send] diff --git a/src/utils/vector.cljs b/src/utils/vector.cljs new file mode 100644 index 0000000000..cedbb977e9 --- /dev/null +++ b/src/utils/vector.cljs @@ -0,0 +1,7 @@ +(ns utils.vector) + +(defn insert-element-at + [data element index] + (let [before (take index data) + after (drop index data)] + (vec (concat before [element] after)))) diff --git a/src/utils/vector_test.cljs b/src/utils/vector_test.cljs new file mode 100644 index 0000000000..5ed27da897 --- /dev/null +++ b/src/utils/vector_test.cljs @@ -0,0 +1,17 @@ +(ns utils.vector-test + (:require + [cljs.test :refer-macros [deftest is testing]] + [utils.vector :as vector])) + +(deftest test-insert-element-at + (testing "Inserting into an empty vector" + (is (= [42] (vector/insert-element-at [] 42 0)))) + + (testing "Inserting at the beginning of a vector" + (is (= [42 1 2 3] (vector/insert-element-at [1 2 3] 42 0)))) + + (testing "Inserting in the middle of a vector" + (is (= [1 42 2 3] (vector/insert-element-at [1 2 3] 42 1)))) + + (testing "Inserting at the end of a vector" + (is (= [1 2 3 42] (vector/insert-element-at [1 2 3] 42 3)))))