diff --git a/src/quo/components/wallet/network_bridge/view.cljs b/src/quo/components/wallet/network_bridge/view.cljs index 119c46c86a..4d7d4be533 100644 --- a/src/quo/components/wallet/network_bridge/view.cljs +++ b/src/quo/components/wallet/network_bridge/view.cljs @@ -30,7 +30,7 @@ [{:keys [network status amount container-style on-press] :as args}] (let [theme (quo.theme/use-theme)] (if (= status :add) - [network-bridge-add args] + [network-bridge-add (assoc args :theme theme)] [rn/pressable {:style (merge (style/container network status theme) container-style) :accessible true diff --git a/src/quo/components/wallet/network_link/style.cljs b/src/quo/components/wallet/network_link/style.cljs index 19c01e2926..63e335f4ad 100644 --- a/src/quo/components/wallet/network_link/style.cljs +++ b/src/quo/components/wallet/network_link/style.cljs @@ -25,10 +25,10 @@ (def link-1x-container {:flex 1 - :height 58 + :height 57 :justify-content :center}) (def link-2x-container {:flex 1 - :height 114 + :height 112 :justify-content :center}) diff --git a/src/quo/components/wallet/network_link/view.cljs b/src/quo/components/wallet/network_link/view.cljs index a0c7114536..2e96cc8657 100644 --- a/src/quo/components/wallet/network_link/view.cljs +++ b/src/quo/components/wallet/network_link/view.cljs @@ -24,33 +24,50 @@ :strokeWidth "1"}]]) (defn- line - [stroke width] + [{:keys [stroke-color source-color destination-color width theme]}] [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"}]]) + :stroke stroke-color + :stroke-width "1"}] + [svg/defs + [svg/linear-gradient + {:id "gradient" + :x1 "0%" + :x2 "100%" + :y1 "0%" + :y2 "0%" + :gradient-units "objectBoundingBox"} + [svg/stop {:offset "0%" :stop-color (colors/resolve-color source-color theme)}] + [svg/stop {:offset "100%" :stop-color (colors/resolve-color destination-color theme)}]]]]) (defn link-linear - [{:keys [source]}] + [{:keys [source destination]}] (let [theme (quo.theme/use-theme) [container-width set-container-width] (rn/use-state 100) - stroke-color (colors/resolve-color source theme) + stroke-color "url(#gradient)" + 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) on-layout (rn/use-callback #(set-container-width (oget % :nativeEvent :layout :width)))] [rn/view {:style style/link-linear-container :on-layout on-layout} - [line stroke-color container-width] + [line + {:stroke-color stroke-color + :source-color source-color + :destination-color destination-color + :width container-width + :theme theme}] [rn/view {:style style/left-circle-container} - [circle fill-color stroke-color]] + [circle fill-color source-color]] [rn/view {:style style/right-circle-container} - [circle fill-color stroke-color]]])) + [circle fill-color destination-color]]])) (defn link-1x [{:keys [source destination]}] diff --git a/src/status_im/contexts/wallet/common/utils.cljs b/src/status_im/contexts/wallet/common/utils.cljs index 8af93d3cdf..79c14bc8e1 100644 --- a/src/status_im/contexts/wallet/common/utils.cljs +++ b/src/status_im/contexts/wallet/common/utils.cljs @@ -263,3 +263,11 @@ {} tokens) (update-vals #(prettify-balance currency-symbol %)))) + +(defn format-token-id + [token collectible] + (if token + (:symbol token) + (str (get-in collectible [:id :contract-id :address]) + ":" + (get-in collectible [:id :token-id])))) diff --git a/src/status_im/contexts/wallet/common/utils/send.cljs b/src/status_im/contexts/wallet/common/utils/send.cljs deleted file mode 100644 index df225b8080..0000000000 --- a/src/status_im/contexts/wallet/common/utils/send.cljs +++ /dev/null @@ -1,34 +0,0 @@ -(ns status-im.contexts.wallet.common.utils.send - (:require [clojure.string :as string] - [utils.money :as money])) - -(defn calculate-gas-fee - [data] - (let [gas-amount (money/bignumber (get data :gas-amount)) - gas-fees (get data :gas-fees) - eip1559-enabled? (get gas-fees :eip-1559-enabled) - optimal-price-gwei (money/bignumber (if eip1559-enabled? - (get gas-fees :max-fee-per-gas-medium) - (get gas-fees :gas-price))) - total-gas-fee-wei (money/mul (money/->wei :gwei optimal-price-gwei) gas-amount) - l1-fee-wei (money/->wei :gwei (get gas-fees :l-1-gas-fee))] - (money/add total-gas-fee-wei l1-fee-wei))) - -(defn calculate-full-route-gas-fee - "Sums all the routes fees in wei and then convert the total value to ether" - [route] - (money/wei->ether (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 deleted file mode 100644 index 0d0e46ce66..0000000000 --- a/src/status_im/contexts/wallet/common/utils/send_test.cljs +++ /dev/null @@ -1,116 +0,0 @@ -(ns status-im.contexts.wallet.common.utils.send-test - (:require [cljs.test :refer [deftest is testing]] - [status-im.contexts.wallet.common.utils.send :as utils] - [utils.money :as money])) - -(deftest test-calculate-gas-fee - (testing "EIP-1559 transaction without L1 fee" - (let [data {:gas-amount "23487" - :gas-fees {:max-fee-per-gas-medium "2.259274911" - :eip-1559-enabled true - :l-1-gas-fee "0"}} - expected-result (money/bignumber "53063589834657")] ; This is in Wei - (is (money/equal-to (utils/calculate-gas-fee data) - expected-result)))) - - (testing "EIP-1559 transaction with L1 fee of 60,000 Gwei" - (let [data {:gas-amount "23487" - :gas-fees {:max-fee-per-gas-medium "2.259274911" - :eip-1559-enabled true - :l-1-gas-fee "60000"}} - expected-result (money/bignumber "113063589834657")] ; Added 60,000 Gwei in Wei to the - ; previous result - (is (money/equal-to (utils/calculate-gas-fee data) - expected-result)))) - - (testing "Non-EIP-1559 transaction with specified gas price" - (let [data {:gas-amount "23487" - :gas-fees {:gas-price "2.872721089" - :eip-1559-enabled false - :l-1-gas-fee "0"}} - expected-result (money/bignumber "67471600217343")] ; This is in Wei, for the specified - ; gas amount and price - (is (money/equal-to (utils/calculate-gas-fee data) - expected-result))))) - -(deftest test-calculate-full-route-gas-fee - (testing "Route with a single EIP-1559 transaction, no L1 fees" - (let [route [{:gas-amount "23487" - :gas-fees {:max-fee-per-gas-medium "2.259274911" - :eip-1559-enabled true - :l-1-gas-fee "0"}}] - expected-result (money/bignumber "0.000053063589834657")] ; The Wei amount for the - ; transaction, converted to - ; Ether - (is (money/equal-to (utils/calculate-full-route-gas-fee route) - expected-result)))) - - (testing "Route with two EIP-1559 transactions, no L1 fees" - (let [route [{:gas-amount "23487" - :gas-fees {:max-fee-per-gas-medium "2.259274911" - :eip-1559-enabled true - :l-1-gas-fee "0"}} - {:gas-amount "23487" - :gas-fees {:max-fee-per-gas-medium "2.259274911" - :eip-1559-enabled true - :l-1-gas-fee "0"}}] - expected-result (money/bignumber "0.000106127179669314")] ; Sum of both transactions' Wei - ; amounts, converted to Ether - (is (money/equal-to (utils/calculate-full-route-gas-fee route) - expected-result)))) - - (testing "Route with two EIP-1559 transactions, one with L1 fee of 60,000 Gwei" - (let [route [{:gas-amount "23487" - :gas-fees {:max-fee-per-gas-medium "2.259274911" - :eip-1559-enabled true - :l-1-gas-fee "0"}} - {:gas-amount "23487" - :gas-fees {:max-fee-per-gas-medium "2.259274911" - :eip-1559-enabled true - :l-1-gas-fee "60000"}}] - expected-result (money/bignumber "0.000166127179669314")] ; Added 60,000 Gwei in Wei to - ; the previous total and - ; converted to Ether - (is (money/equal-to (utils/calculate-full-route-gas-fee route) - expected-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 9a667ae4c7..96d5a3b3c6 100644 --- a/src/status_im/contexts/wallet/send/events.cljs +++ b/src/status_im/contexts/wallet/send/events.cljs @@ -31,8 +31,13 @@ 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]) + receiver-networks (get-in db [:wallet :ui :send :receiver-networks]) + receiver-network-values (get-in db [:wallet :ui :send :receiver-network-values]) + sender-network-values (get-in db [:wallet :ui :send :sender-network-values]) + disabled-from-chain-ids (or (get-in db [:wallet :ui :send :disabled-from-chain-ids]) []) token-decimals (if collectible 0 (:decimals token)) native-token? (and token (= token-display-name "ETH")) + routes-available? (pos? (count chosen-route)) from-network-amounts-by-chain (send-utils/network-amounts-by-chain {:route chosen-route :token-decimals token-decimals @@ -46,19 +51,54 @@ :native-token? native-token? :to? true}) - to-network-values-for-ui (send-utils/network-values-for-ui to-network-amounts-by-chain)] + to-network-values-for-ui (send-utils/network-values-for-ui to-network-amounts-by-chain) + sender-network-values (if routes-available? + (send-utils/network-amounts from-network-values-for-ui + disabled-from-chain-ids + receiver-networks + false) + (send-utils/reset-network-amounts-to-zero + sender-network-values)) + receiver-network-values (if routes-available? + (send-utils/network-amounts to-network-values-for-ui + disabled-from-chain-ids + receiver-networks + true) + (send-utils/reset-network-amounts-to-zero + receiver-network-values)) + network-links (when routes-available? + (send-utils/network-links chosen-route + sender-network-values + receiver-network-values))] {: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 :sender-network-values] sender-network-values) + (assoc-in [:wallet :ui :send :receiver-network-values] receiver-network-values) + (assoc-in [:wallet :ui :send :network-links] network-links) (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 :route) - (assoc-in [:wallet :ui :send :loading-suggested-routes?] false))})) + (fn [{:keys [db]} [error]] + (let [cleaned-sender-network-values (-> (get-in db [:wallet :ui :send :sender-network-values]) + (send-utils/reset-network-amounts-to-zero)) + cleaned-receiver-network-values (-> (get-in db [:wallet :ui :send :receiver-network-values]) + (send-utils/reset-network-amounts-to-zero))] + {:db (-> db + (update-in [:wallet :ui :send] + dissoc + :route) + (assoc-in [:wallet :ui :send :sender-network-values] cleaned-sender-network-values) + (assoc-in [:wallet :ui :send :receiver-network-values] cleaned-receiver-network-values) + (assoc-in [:wallet :ui :send :loading-suggested-routes?] false) + (assoc-in [:wallet :ui :send :suggested-routes] {:best []})) + :fx [[:dispatch + [:toasts/upsert + {:id :send-transaction-error + :type :negative + :text (:message error)}]]]}))) (rf/reg-event-fx :wallet/clean-suggested-routes (fn [{:keys [db]}] @@ -69,6 +109,9 @@ :route :from-values-by-chain :to-values-by-chain + :sender-network-values + :receiver-network-values + :network-links :loading-suggested-routes? :suggested-routes-call-timestamp)})) @@ -203,56 +246,74 @@ (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]) - token (get-in db [:wallet :ui :send :token]) - 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]) - receiver-networks (get-in db [:wallet :ui :send :receiver-networks]) + (let [wallet-address (get-in db [:wallet :current-viewing-account-address]) + token (get-in db [:wallet :ui :send :token]) + 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]) + receiver-networks (get-in db [:wallet :ui :send :receiver-networks]) 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])) - network-chain-ids (map :chain-id networks) - bridge-to-chain-id (get-in db [:wallet :ui :send :bridge-to-chain-id]) - token-decimal (when token (:decimals token)) - token-id (if token - (:symbol token) - (str (get-in collectible [:id :contract-id :address]) - ":" - (get-in collectible [:id :token-id]))) - to-token-id "" - network-preferences (if token [] [(get-in collectible [:id :contract-id :chain-id])]) - gas-rates constants/gas-rate-medium - amount-in (send-utils/amount-in-hex amount (if token token-decimal 0)) - from-address wallet-address + test-networks-enabled? (get-in db [:profile/profile :test-networks-enabled?]) + networks ((if test-networks-enabled? :test :prod) + (get-in db [:wallet :networks])) + network-chain-ids (map :chain-id networks) + bridge-to-chain-id (get-in db [:wallet :ui :send :bridge-to-chain-id]) + token-decimal (when token (:decimals token)) + token-id (utils/format-token-id token collectible) + to-token-id "" + network-preferences (if token [] [(get-in collectible [:id :contract-id :chain-id])]) + 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-to-chain-ids (if (= transaction-type :bridge) - (filter #(not= % bridge-to-chain-id) network-chain-ids) - (filter (fn [chain-id] - (not (some #(= chain-id %) - receiver-networks))) - network-chain-ids)) - from-locked-amount {} - transaction-type-param (case transaction-type - :collectible constants/send-type-erc-721-transfer - :bridge constants/send-type-bridge - constants/send-type-transfer) - request-params [transaction-type-param - from-address - to-address - amount-in - token-id - to-token-id + disabled-to-chain-ids (if (= transaction-type :bridge) + (filter #(not= % bridge-to-chain-id) network-chain-ids) + (filter (fn [chain-id] + (not (some #(= chain-id %) + receiver-networks))) + network-chain-ids)) + from-locked-amount {} + transaction-type-param (case transaction-type + :collectible constants/send-type-erc-721-transfer + :bridge constants/send-type-bridge + constants/send-type-transfer) + balances-per-chain (when token (:balances-per-chain token)) + token-available-networks-for-suggested-routes + (when token + (send-utils/token-available-networks-for-suggested-routes {:balances-per-chain + balances-per-chain + :disabled-chain-ids + disabled-from-chain-ids})) + sender-network-values (when token-available-networks-for-suggested-routes + (send-utils/loading-network-amounts + token-available-networks-for-suggested-routes disabled-from-chain-ids - disabled-to-chain-ids - network-preferences - gas-rates - from-locked-amount]] + receiver-networks + false)) + receiver-network-values (when token-available-networks-for-suggested-routes + (send-utils/loading-network-amounts + token-available-networks-for-suggested-routes + disabled-from-chain-ids + receiver-networks + true)) + request-params [transaction-type-param + from-address + to-address + amount-in + token-id + to-token-id + disabled-from-chain-ids + disabled-to-chain-ids + network-preferences + gas-rates + from-locked-amount]] {:db (-> db (assoc-in [:wallet :ui :send :amount] amount) (assoc-in [:wallet :ui :send :loading-suggested-routes?] true) - (assoc-in [:wallet :ui :send :suggested-routes-call-timestamp] now)) + (assoc-in [:wallet :ui :send :sender-network-values] sender-network-values) + (assoc-in [:wallet :ui :send :receiver-network-values] receiver-network-values) + (assoc-in [:wallet :ui :send :suggested-routes-call-timestamp] now) + (update-in [:wallet :ui :send] dissoc :network-links)) :json-rpc/call [{:method "wallet_getSuggestedRoutes" :params request-params :on-success (fn [suggested-routes] 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 f1733ae8b1..55fd5c88c7 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 @@ -63,7 +63,7 @@ :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-suggested-routes nil :wallet/wallet-send-receiver-networks [1] :view-id :screen/wallet.send-input-amount :wallet/wallet-send-to-address "0x04371e2d9d66b82f056bc128064" @@ -73,7 +73,10 @@ :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")}}) + :wallet/wallet-send-to-values-by-chain {1 (money/bignumber "250")} + :wallet/wallet-send-sender-network-values nil + :wallet/wallet-send-receiver-network-values nil + :wallet/wallet-send-network-links nil}) (h/describe "Send > input amount screen" (h/setup-restorable-re-frame) diff --git a/src/status_im/contexts/wallet/send/input_amount/style.cljs b/src/status_im/contexts/wallet/send/input_amount/style.cljs index 724a723c0f..34124d6b8d 100644 --- a/src/status_im/contexts/wallet/send/input_amount/style.cljs +++ b/src/status_im/contexts/wallet/send/input_amount/style.cljs @@ -33,3 +33,8 @@ {:flex 1 :height 40 :background-color :transparent}) + +(def no-routes-found-container + {:height 40 + :width "100%" + :align-items :center}) 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 c8e59cf15a..ab2a49e29e 100644 --- a/src/status_im/contexts/wallet/send/input_amount/view.cljs +++ b/src/status_im/contexts/wallet/send/input_amount/view.cljs @@ -9,11 +9,12 @@ [status-im.contexts.wallet.common.account-switcher.view :as account-switcher] [status-im.contexts.wallet.common.asset-list.view :as asset-list] [status-im.contexts.wallet.common.utils :as utils] - [status-im.contexts.wallet.common.utils.send :as send-utils] [status-im.contexts.wallet.send.input-amount.style :as style] [status-im.contexts.wallet.send.routes.view :as routes] + [status-im.contexts.wallet.send.utils :as send-utils] [utils.address :as address] [utils.i18n :as i18n] + [utils.money :as money] [utils.re-frame :as rf])) (defn- make-limit-label @@ -52,6 +53,15 @@ :title (i18n/label :t/user-gets {:name receiver}) :subtitle amount}]]) +(defn- every-network-value-is-zero? + [sender-network-values] + (every? (fn [{:keys [total-amount]}] + (and + total-amount + (money/equal-to total-amount + (money/bignumber "0")))) + sender-network-values)) + (defn select-asset-bottom-sheet [clear-input!] (let [{preselected-token-symbol :symbol} (rf/sub [:wallet/wallet-send-token])] @@ -157,7 +167,18 @@ [:show-bottom-sheet {:content (fn [] [select-asset-bottom-sheet - clear-input!])}])] + clear-input!])}]) + loading-suggested-routes? (rf/sub + [:wallet/wallet-send-loading-suggested-routes?]) + sender-network-values (rf/sub + [:wallet/wallet-send-sender-network-values]) + suggested-routes (rf/sub [:wallet/wallet-send-suggested-routes]) + routes (when suggested-routes + (or (:best suggested-routes) [])) + no-routes-found? (and + (every-network-value-is-zero? sender-network-values) + (not (nil? routes)) + (not loading-suggested-routes?))] (rn/use-mount (fn [] (let [dismiss-keyboard-fn #(when (= % "active") (rn/dismiss-keyboard!)) @@ -200,12 +221,28 @@ :fees fee-formatted :amount amount-text :receiver (address/get-shortened-key to-address)}]) + (when no-routes-found? + [rn/view {:style style/no-routes-found-container} + [quo/info-message + {:type :error + :icon :i/alert + :size :default + :style {:margin-top 15}} + (i18n/label :t/no-routes-found)]]) [quo/bottom-actions {:actions :one-action - :button-one-label button-one-label + :button-one-label (if no-routes-found? + (i18n/label :t/try-again) + button-one-label) :button-one-props (merge button-one-props - {:disabled? confirm-disabled? - :on-press on-confirm})}] + {:disabled? (and (not no-routes-found?) confirm-disabled?) + :on-press (if no-routes-found? + #(rf/dispatch [:wallet/get-suggested-routes + {:amount (controlled-input/input-value + input-state)}]) + on-confirm)} + (when no-routes-found? + {:type :grey}))}] [quo/numbered-keyboard {:container-style (style/keyboard-container bottom) :left-action :dot diff --git a/src/status_im/contexts/wallet/send/routes/style.cljs b/src/status_im/contexts/wallet/send/routes/style.cljs index 64a25e8cb6..d2e2b2cdcf 100644 --- a/src/status_im/contexts/wallet/send/routes/style.cljs +++ b/src/status_im/contexts/wallet/send/routes/style.cljs @@ -3,20 +3,16 @@ (def routes-container {:padding-horizontal 20 - :flex 1 + :flex-grow 1 :padding-vertical 16 - :width "100%" - :height "100%"}) + :width "100%"}) (def routes-header-container {:flex-direction :row :justify-content :space-between}) -(defn routes-inner-container - [first-item?] - {:margin-top (if first-item? 7.5 11) - :flex-direction :row - :align-items :center +(def routes-inner-container + {:flex-direction :row :justify-content :space-between}) (def section-label-right @@ -25,20 +21,26 @@ (def section-label-left {:width 136}) -(def network-link +(def network-links-container {:margin-horizontal -1.5 - :z-index 1 + :margin-top 7.5 + :z-index 3 :flex 1}) +(defn network-link-container + [margin-top inverted?] + (cond-> {:position :absolute + :left 0 + :right 0 + :top margin-top} + inverted? + (assoc :transform [{:scaleY -1}]))) + (def empty-container {:flex-grow 1 :align-items :center :justify-content :center}) -(def add-network - {:margin-top 11 - :align-self :flex-end}) - (defn warning-container [color theme] {:flex-direction :row diff --git a/src/status_im/contexts/wallet/send/routes/view.cljs b/src/status_im/contexts/wallet/send/routes/view.cljs index e24257df84..596ea394fd 100644 --- a/src/status_im/contexts/wallet/send/routes/view.cljs +++ b/src/status_im/contexts/wallet/send/routes/view.cljs @@ -6,19 +6,18 @@ [quo.foundations.resources :as resources] [react-native.core :as rn] [reagent.core :as reagent] - [status-im.constants :as constants] - [status-im.contexts.wallet.common.utils.networks :as networks-utils] - [status-im.contexts.wallet.common.utils.send :as send-utils] + [status-im.contexts.wallet.common.utils.networks :as network-utils] [status-im.contexts.wallet.send.routes.style :as style] + [status-im.contexts.wallet.send.utils :as send-utils] [utils.debounce :as debounce] [utils.i18n :as i18n] - [utils.re-frame :as rf] - [utils.vector :as vector-utils])) + [utils.re-frame :as rf])) -(def ^:private network-priority-score - {:ethereum 1 - :optimism 2 - :arbitrum 3}) +(def row-height 44) +(def space-between-rows 11) +(def network-link-linear-height 10) +(def network-link-1x-height 56) +(def network-link-2x-height 111) (defn- make-network-item [{:keys [network-name chain-id] :as _network} @@ -33,40 +32,16 @@ :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 (networks-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 (networks-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 networks-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 (networks-utils/id->network chain-id)}] - (vector-utils/insert-element-at acc-network-links disabled-network-link index))) - network-links - sorted-networks))) +(defn fetch-routes + [amount valid-input? bounce-duration-ms] + (if valid-input? + (debounce/debounce-and-dispatch + [:wallet/get-suggested-routes {:amount amount}] + bounce-duration-ms) + (rf/dispatch [:wallet/clean-suggested-routes]))) (defn networks-drawer - [{:keys [fetch-routes theme]}] + [{:keys [on-save theme]}] (let [network-details (rf/sub [:wallet/network-details]) {:keys [color]} (rf/sub [:wallet/current-viewing-account]) selected-networks (rf/sub [:wallet/wallet-send-receiver-networks]) @@ -119,188 +94,149 @@ (rf/dispatch [:wallet/update-receiver-networks @network-preferences]) (rf/dispatch [:hide-bottom-sheet]) - (fetch-routes)) + (on-save)) :customization-color color}}]]))) -(defn route-item - [{: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 - :container-style style/add-network - :on-press #(rf/dispatch [:show-bottom-sheet - {:content (fn [] [networks-drawer - {:theme theme - :fetch-routes fetch-routes}])}])}] - [rn/view {:style (style/routes-inner-container first-item?)} - [quo/network-bridge - {: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 {:flex 1}}]) - [quo/network-bridge - {: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-values + [{:keys [network-values token-symbol on-press theme on-save to? loading-suggested-routes?]}] + [rn/view + (map-indexed (fn [index {:keys [chain-id total-amount type]}] + [rn/view + {:key (str (if to? "to" "from") "-" chain-id) + :style {:margin-top (if (pos? index) 11 7.5)}} + [quo/network-bridge + {:amount (str total-amount " " token-symbol) + :network (network-utils/id->network chain-id) + :status type + :on-press #(when (not loading-suggested-routes?) + (cond + (= type :add) + (rf/dispatch [:show-bottom-sheet + {:content (fn [] + [networks-drawer + {:theme theme + :on-save on-save}])}]) + on-press (on-press chain-id total-amount)))}]]) + network-values)]) -(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?)) - (networks-utils/id->network item) - disabled-network? - (networks-utils/id->network (:chain-id - item)) - :else - (networks-utils/id->network from-chain-id)) - :to-network (cond (and loading-suggested-routes? - (not disabled-network?)) - (networks-utils/id->network item) - disabled-network? - (networks-utils/id->network (:chain-id - item)) - :else - (networks-utils/id->network to-chain-id)) - :on-press-from-network on-press-from-network - :on-press-to-network on-press-to-network}])) +(defn render-network-links + [{:keys [network-links sender-network-values]}] + [rn/view {:style style/network-links-container} + (map + (fn [{:keys [from-chain-id to-chain-id position-diff]}] + (let [position-diff-absolute (js/Math.abs position-diff) + shape (case position-diff-absolute + 0 :linear + 1 :1x + 2 :2x) + height (case position-diff-absolute + 0 network-link-linear-height + 1 network-link-1x-height + 2 network-link-2x-height) + inverted? (neg? position-diff) + source (network-utils/id->network from-chain-id) + destination (network-utils/id->network to-chain-id) + from-chain-id-index (first (keep-indexed #(when (= from-chain-id (:chain-id %2)) %1) + sender-network-values)) + base-margin-top (* (+ row-height space-between-rows) + from-chain-id-index) + margin-top (if (zero? position-diff) + (+ base-margin-top + (- (/ row-height 2) (/ height 2))) + (+ base-margin-top + (- (/ row-height 2) height) + (if inverted? height 0)))] + [rn/view + {:key (str "from-" from-chain-id "-to-" to-chain-id) + :style (style/network-link-container margin-top inverted?)} + [rn/view {:style {:flex 1}} + [quo/network-link + {:shape shape + :source source + :destination destination}]]])) + network-links)]) -(defn fetch-routes - [amount valid-input? bounce-duration-ms] - (if valid-input? - (debounce/debounce-and-dispatch - [:wallet/get-suggested-routes {:amount amount}] - bounce-duration-ms) - (rf/dispatch [:wallet/clean-suggested-routes]))) +(defn disable-chain + [chain-id disabled-from-chain-ids token-available-networks-for-suggested-routes] + (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))] + (if (or re-enabling-chain? + (> (count token-available-networks-for-suggested-routes) 1)) + (rf/dispatch [:wallet/disable-from-networks + disabled-chain-ids]) + (rf/dispatch [:toasts/upsert + {:id :disable-chain-error + :type :negative + :text (i18n/label :t/at-least-one-network-must-be-activated)}])))) (defn view [{:keys [token theme input-value valid-input? on-press-to-network current-screen-id]}] - - (let [token-symbol (:symbol token) - nav-current-screen-id (rf/sub [:view-id]) - active-screen? (= nav-current-screen-id current-screen-id) - loading-suggested-routes? (rf/sub - [:wallet/wallet-send-loading-suggested-routes?]) - 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]) - suggested-routes (rf/sub [:wallet/wallet-send-suggested-routes]) - selected-networks (rf/sub [:wallet/wallet-send-receiver-networks]) - disabled-from-chain-ids (rf/sub - [:wallet/wallet-send-disabled-from-chain-ids]) - routes (when suggested-routes - (or (:best suggested-routes) [])) + (let [token-symbol (:symbol token) + nav-current-screen-id (rf/sub [:view-id]) + active-screen? (= nav-current-screen-id current-screen-id) + loading-suggested-routes? (rf/sub + [:wallet/wallet-send-loading-suggested-routes?]) + sender-network-values (rf/sub + [:wallet/wallet-send-sender-network-values]) + receiver-network-values (rf/sub + [:wallet/wallet-send-receiver-network-values]) + network-links (rf/sub [:wallet/wallet-send-network-links]) + disabled-from-chain-ids (rf/sub + [:wallet/wallet-send-disabled-from-chain-ids]) {token-balances-per-chain :balances-per-chain} (rf/sub [:wallet/current-viewing-account-tokens-filtered (str token-symbol)]) - 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}) - network-links (if loading-suggested-routes? - affordable-networks - routes) - show-routes? (or (and (not-empty affordable-networks) - loading-suggested-routes?) - (not-empty routes))] - + token-available-networks-for-suggested-routes + (send-utils/token-available-networks-for-suggested-routes + {:balances-per-chain token-balances-per-chain + :disabled-chain-ids disabled-from-chain-ids}) + show-routes? (not-empty sender-network-values)] (rn/use-effect - #(when (and active-screen? (> (count affordable-networks) 0)) + #(when (and active-screen? (> (count token-available-networks-for-suggested-routes) 0)) (fetch-routes input-value valid-input? 2000)) [input-value valid-input?]) (rn/use-effect - #(when (and active-screen? (> (count affordable-networks) 0)) + #(when (and active-screen? (> (count token-available-networks-for-suggested-routes) 0)) (fetch-routes input-value valid-input? 0)) [disabled-from-chain-ids]) - (if show-routes? - (let [initial-network-links-count (count network-links) - disabled-count (count disabled-from-chain-ids) - network-links (if (not-empty disabled-from-chain-ids) - (add-disabled-networks network-links - disabled-from-chain-ids - 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 % valid-input? 2000) - :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])))) - :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)])]))) + [rn/scroll-view {:content-container-style style/routes-container} + (when show-routes? + [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}]]) + [rn/view {:style style/routes-inner-container} + [render-network-values + {:token-symbol token-symbol + :network-values sender-network-values + :on-press #(disable-chain %1 + disabled-from-chain-ids + token-available-networks-for-suggested-routes) + :to? false + :theme theme + :loading-suggested-routes? loading-suggested-routes?}] + [render-network-links + {:network-links network-links + :sender-network-values sender-network-values}] + [render-network-values + {:token-symbol token-symbol + :network-values receiver-network-values + :on-press on-press-to-network + :to? true + :loading-suggested-routes? loading-suggested-routes? + :theme theme + :on-save #(fetch-routes input-value valid-input? 0)}]]])) 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 6b9346ff66..7a718cd692 100644 --- a/src/status_im/contexts/wallet/send/transaction_confirmation/view.cljs +++ b/src/status_im/contexts/wallet/send/transaction_confirmation/view.cljs @@ -209,9 +209,7 @@ (defn view [_] - (let [on-close (fn [] - (rf/dispatch [:wallet/clean-suggested-routes]) - (rf/dispatch [:navigate-back]))] + (let [on-close #(rf/dispatch [:navigate-back])] (fn [] (let [theme (quo.theme/use-theme) send-transaction-data (rf/sub [:wallet/wallet-send]) diff --git a/src/status_im/contexts/wallet/send/utils.cljs b/src/status_im/contexts/wallet/send/utils.cljs index f9b7607437..5930bfd61d 100644 --- a/src/status_im/contexts/wallet/send/utils.cljs +++ b/src/status_im/contexts/wallet/send/utils.cljs @@ -2,6 +2,7 @@ (:require [legacy.status-im.utils.hex :as utils.hex] [native-module.core :as native-module] + [status-im.contexts.wallet.common.utils.networks :as network-utils] [utils.money :as money])) (defn amount-in-hex @@ -24,6 +25,23 @@ {} transaction-hashes)) +(defn calculate-gas-fee + [data] + (let [gas-amount (money/bignumber (get data :gas-amount)) + gas-fees (get data :gas-fees) + eip1559-enabled? (get gas-fees :eip-1559-enabled) + optimal-price-gwei (money/bignumber (if eip1559-enabled? + (get gas-fees :max-fee-per-gas-medium) + (get gas-fees :gas-price))) + total-gas-fee-wei (money/mul (money/->wei :gwei optimal-price-gwei) gas-amount) + l1-fee-wei (money/->wei :gwei (get gas-fees :l-1-gas-fee))] + (money/add total-gas-fee-wei l1-fee-wei))) + +(defn calculate-full-route-gas-fee + "Sums all the routes fees in wei and then convert the total value to ether" + [route] + (money/wei->ether (reduce money/add (map calculate-gas-fee route)))) + (defn network-amounts-by-chain [{:keys [route token-decimals native-token? to?]}] (reduce (fn [acc path] @@ -47,3 +65,104 @@ (assoc acc k (if (money/equal-to v 0) "<0.01" v))) {} amounts)) + +(defn token-available-networks-for-suggested-routes + [{:keys [balances-per-chain disabled-chain-ids]}] + (let [disabled-set (set disabled-chain-ids)] + (->> balances-per-chain + (filter (fn [[_ {:keys [chain-id]}]] + (not (contains? disabled-set chain-id)))) + (map first)))) + +(def ^:private network-priority-score + {:ethereum 1 + :optimism 2 + :arbitrum 3}) + +(def ^:private available-networks-count + (count (set (keys network-priority-score)))) + +(defn reset-network-amounts-to-zero + [network-amounts] + (map + (fn [network-amount] + (cond-> network-amount + (= (:type network-amount) :loading) + (assoc :total-amount (money/bignumber "0") + :type :default))) + network-amounts)) + +(defn network-amounts + [network-values disabled-chain-ids receiver-networks to?] + (let [disabled-set (set disabled-chain-ids) + receiver-networks-set (set receiver-networks) + network-values-keys (set (keys network-values)) + routes-found? (pos? (count network-values-keys)) + updated-network-values (when routes-found? + (reduce (fn [acc k] + (if (or (contains? network-values-keys k) + (and to? + (not (contains? receiver-networks-set k)))) + acc + (assoc acc k (money/bignumber "0")))) + network-values + disabled-chain-ids))] + (cond-> (->> updated-network-values + (map + (fn [[k v]] + {:chain-id k + :total-amount v + :type (if (or to? (not (contains? disabled-set k))) :default :disabled)})) + (sort-by #(get network-priority-score (network-utils/id->network (:chain-id %)))) + (filter + #(or (and to? + (or (contains? receiver-networks-set (:chain-id %)) + (money/greater-than (:total-amount %) (money/bignumber "0")))) + (not to?))) + (vec)) + (and to? + routes-found? + (< (count updated-network-values) available-networks-count)) + (conj {:type :add})))) + +(defn loading-network-amounts + [valid-networks disabled-chain-ids receiver-networks to?] + (let [disabled-set (set disabled-chain-ids) + receiver-networks-set (set receiver-networks) + receiver-networks-count (count receiver-networks) + valid-networks (concat valid-networks disabled-chain-ids)] + (cond-> (->> valid-networks + (map (fn [k] + (cond-> {:chain-id k + :type (if (or to? + (not (contains? disabled-set k))) + :loading + :disabled)} + (and (not to?) (contains? disabled-set k)) + (assoc :total-amount (money/bignumber "0"))))) + (sort-by (fn [item] + (get network-priority-score + (network-utils/id->network (:chain-id item))))) + (filter + #(or (and to? (contains? receiver-networks-set (:chain-id %))) + (and (not to?) + (not (contains? disabled-chain-ids (:chain-id %)))))) + (vec)) + (and to? (< receiver-networks-count available-networks-count)) (conj {:type :add})))) + +(defn network-links + [route from-values-by-chain to-values-by-chain] + (reduce (fn [acc path] + (let [from-chain-id (get-in path [:from :chain-id]) + to-chain-id (get-in path [:to :chain-id]) + from-chain-id-index (first (keep-indexed #(when (= from-chain-id (:chain-id %2)) %1) + from-values-by-chain)) + to-chain-id-index (first (keep-indexed #(when (= to-chain-id (:chain-id %2)) %1) + to-values-by-chain)) + position-diff (- from-chain-id-index to-chain-id-index)] + (conj acc + {:from-chain-id from-chain-id + :to-chain-id to-chain-id + :position-diff position-diff}))) + [] + route)) diff --git a/src/status_im/contexts/wallet/send/utils_test.cljs b/src/status_im/contexts/wallet/send/utils_test.cljs index 70d1a25ba7..097ec3d1f3 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] + [utils.map :as map] [utils.money :as money])) (deftest test-amount-in-hex @@ -93,3 +94,350 @@ (doseq [[chain-id exp-value] expected] (is #(or (= (get result chain-id) exp-value) (money/equal-to (get result chain-id) exp-value))))))) + +(deftest test-calculate-gas-fee + (testing "EIP-1559 transaction without L1 fee" + (let [data {:gas-amount "23487" + :gas-fees {:max-fee-per-gas-medium "2.259274911" + :eip-1559-enabled true + :l-1-gas-fee "0"}} + expected-result (money/bignumber "53063589834657")] ; This is in Wei + (is (money/equal-to (utils/calculate-gas-fee data) + expected-result)))) + + (testing "EIP-1559 transaction with L1 fee of 60,000 Gwei" + (let [data {:gas-amount "23487" + :gas-fees {:max-fee-per-gas-medium "2.259274911" + :eip-1559-enabled true + :l-1-gas-fee "60000"}} + expected-result (money/bignumber "113063589834657")] ; Added 60,000 Gwei in Wei to the + ; previous result + (is (money/equal-to (utils/calculate-gas-fee data) + expected-result)))) + + (testing "Non-EIP-1559 transaction with specified gas price" + (let [data {:gas-amount "23487" + :gas-fees {:gas-price "2.872721089" + :eip-1559-enabled false + :l-1-gas-fee "0"}} + expected-result (money/bignumber "67471600217343")] ; This is in Wei, for the specified + ; gas amount and price + (is (money/equal-to (utils/calculate-gas-fee data) + expected-result))))) + +(deftest test-calculate-full-route-gas-fee + (testing "Route with a single EIP-1559 transaction, no L1 fees" + (let [route [{:gas-amount "23487" + :gas-fees {:max-fee-per-gas-medium "2.259274911" + :eip-1559-enabled true + :l-1-gas-fee "0"}}] + expected-result (money/bignumber "0.000053063589834657")] ; The Wei amount for the + ; transaction, converted to + ; Ether + (is (money/equal-to (utils/calculate-full-route-gas-fee route) + expected-result)))) + + (testing "Route with two EIP-1559 transactions, no L1 fees" + (let [route [{:gas-amount "23487" + :gas-fees {:max-fee-per-gas-medium "2.259274911" + :eip-1559-enabled true + :l-1-gas-fee "0"}} + {:gas-amount "23487" + :gas-fees {:max-fee-per-gas-medium "2.259274911" + :eip-1559-enabled true + :l-1-gas-fee "0"}}] + expected-result (money/bignumber "0.000106127179669314")] ; Sum of both transactions' Wei + ; amounts, converted to Ether + (is (money/equal-to (utils/calculate-full-route-gas-fee route) + expected-result)))) + + (testing "Route with two EIP-1559 transactions, one with L1 fee of 60,000 Gwei" + (let [route [{:gas-amount "23487" + :gas-fees {:max-fee-per-gas-medium "2.259274911" + :eip-1559-enabled true + :l-1-gas-fee "0"}} + {:gas-amount "23487" + :gas-fees {:max-fee-per-gas-medium "2.259274911" + :eip-1559-enabled true + :l-1-gas-fee "60000"}}] + expected-result (money/bignumber "0.000166127179669314")] ; Added 60,000 Gwei in Wei to + ; the previous total and + ; converted to Ether + (is (money/equal-to (utils/calculate-full-route-gas-fee route) + expected-result))))) + +(deftest test-token-available-networks-for-suggested-routes + (testing "Excludes disabled chain-ids correctly" + (let [balances-per-chain {"1" {:chain-id "1" :balance 100} + "10" {:chain-id "10" :balance 200} + "42161" {:chain-id "42161" :balance 300}} + disabled-chain-ids ["10"] + expected ["1" "42161"]] + (is (= expected + (utils/token-available-networks-for-suggested-routes {:balances-per-chain balances-per-chain + :disabled-chain-ids + disabled-chain-ids}))))) + + (testing "Returns all chains when no disabled chains are specified" + (let [balances-per-chain {"1" {:chain-id "1" :balance 100} + "10" {:chain-id "10" :balance 200} + "42161" {:chain-id "42161" :balance 300}} + disabled-chain-ids [] + expected ["1" "10" "42161"]] + (is (= expected + (utils/token-available-networks-for-suggested-routes {:balances-per-chain balances-per-chain + :disabled-chain-ids + disabled-chain-ids}))))) + + (testing "Returns empty list when all chains are disabled" + (let [balances-per-chain {"1" {:chain-id "1" :balance 100} + "10" {:chain-id "10" :balance 200} + "42161" {:chain-id "42161" :balance 300}} + disabled-chain-ids ["1" "10" "42161"] + expected []] + (is (= expected + (utils/token-available-networks-for-suggested-routes {:balances-per-chain balances-per-chain + :disabled-chain-ids + disabled-chain-ids}))))) + + (testing "Handles non-existent chain-ids gracefully" + (let [balances-per-chain {"59144" {:chain-id "59144" :balance 400}} + disabled-chain-ids ["1" "10" "42161"] + expected ["59144"]] + (is (= expected + (utils/token-available-networks-for-suggested-routes {:balances-per-chain balances-per-chain + :disabled-chain-ids + disabled-chain-ids})))))) + +(deftest test-reset-network-amounts-to-zero + (testing "Correctly resets loading network amounts to zero and changes type to default" + (let [network-amounts [{:chain-id "1" :total-amount (money/bignumber "100") :type :loading} + {:chain-id "10" :total-amount (money/bignumber "200") :type :default}] + expected [{:chain-id "1" :total-amount (money/bignumber "0") :type :default} + {:chain-id "10" :total-amount (money/bignumber "200") :type :default}] + result (utils/reset-network-amounts-to-zero network-amounts) + comparisons (map #(map/deep-compare %1 %2) + expected + result)] + (is (every? identity comparisons)))) + + (testing "Leaves non-loading types unchanged" + (let [network-amounts [{:chain-id "1" :total-amount (money/bignumber "100") :type :default} + {:chain-id "10" :total-amount (money/bignumber "0") :type :disabled}] + expected [{:chain-id "1" :total-amount (money/bignumber "100") :type :default} + {:chain-id "10" :total-amount (money/bignumber "0") :type :disabled}] + result (utils/reset-network-amounts-to-zero network-amounts) + comparisons (map #(map/deep-compare %1 %2) + expected + result)] + (is (every? identity comparisons)))) + + (testing "Processes an empty list without error" + (let [network-amounts [] + expected [] + result (utils/reset-network-amounts-to-zero network-amounts) + comparisons (map #(map/deep-compare %1 %2) + expected + result)] + (is (every? identity comparisons)))) + + (testing "Applies transformations to multiple loading entries" + (let [network-amounts [{:chain-id "1" :total-amount (money/bignumber "100") :type :loading} + {:chain-id "10" :total-amount (money/bignumber "200") :type :loading}] + expected [{:chain-id "1" :total-amount (money/bignumber "0") :type :default} + {:chain-id "10" :total-amount (money/bignumber "0") :type :default}] + result (utils/reset-network-amounts-to-zero network-amounts) + comparisons (map #(map/deep-compare %1 %2) + expected + result)] + (is (every? identity comparisons)))) + + (testing "Mix of loading and non-loading types" + (let [network-amounts [{:chain-id "1" :total-amount (money/bignumber "100") :type :loading} + {:chain-id "10" :total-amount (money/bignumber "200") :type :default} + {:chain-id "42161" :total-amount (money/bignumber "300") :type :loading} + {:chain-id "59144" :total-amount (money/bignumber "0") :type :disabled}] + expected [{:chain-id "1" :total-amount (money/bignumber "0") :type :default} + {:chain-id "10" :total-amount (money/bignumber "200") :type :default} + {:chain-id "42161" :total-amount (money/bignumber "0") :type :default} + {:chain-id "59144" :total-amount (money/bignumber "0") :type :disabled}] + result (utils/reset-network-amounts-to-zero network-amounts) + comparisons (map #(map/deep-compare %1 %2) + expected + result)] + (is (every? identity comparisons))))) + +(deftest test-network-amounts + (testing "Handles disabled and receiver networks correctly when to? is true" + (let [network-values {"1" (money/bignumber "100") + "10" (money/bignumber "200")} + disabled-chain-ids ["1"] + receiver-networks ["10"] + to? true + expected [{:chain-id "1" + :total-amount (money/bignumber "100") + :type :default} + {:chain-id "10" + :total-amount (money/bignumber "200") + :type :default} + {:type :add}] + result (utils/network-amounts network-values + disabled-chain-ids + receiver-networks + to?)] + (is (every? identity (map #(map/deep-compare %1 %2) expected result))))) + + (testing "Adds default amount for non-disabled non-receiver networks when to? is false" + (let [network-values {"1" (money/bignumber "100")} + disabled-chain-ids ["10"] + receiver-networks [] + to? false + expected [{:chain-id "1" + :total-amount (money/bignumber "100") + :type :default} + {:chain-id "10" + :total-amount (money/bignumber "0") + :type :disabled}] + result (utils/network-amounts network-values + disabled-chain-ids + receiver-networks + to?)] + (is (every? identity (map #(map/deep-compare %1 %2) expected result))))) + + (testing "Handles empty inputs correctly" + (let [network-values {} + disabled-chain-ids [] + receiver-networks [] + to? true + expected [] + result (utils/network-amounts network-values + disabled-chain-ids + receiver-networks + to?)] + (is (= expected result)))) + + (testing "Processes case with multiple network interactions" + (let [network-values {"1" (money/bignumber "300") + "10" (money/bignumber "400") + "42161" (money/bignumber "500")} + disabled-chain-ids ["1" "42161"] + receiver-networks ["10"] + to? true + expected [{:chain-id "1" + :total-amount (money/bignumber "300") + :type :default} + {:chain-id "10" + :total-amount (money/bignumber "400") + :type :default} + {:chain-id "42161" + :total-amount (money/bignumber "500") + :type :default}] + result (utils/network-amounts network-values + disabled-chain-ids + receiver-networks + to?)] + (is (every? identity (map #(map/deep-compare %1 %2) expected result)))))) + +(deftest test-loading-network-amounts + (testing "Assigns :loading type to valid networks except for disabled ones" + (let [valid-networks ["1" "10" "42161"] + disabled-chain-ids ["42161"] + receiver-networks ["1" "10"] + to? true + expected [{:chain-id "1" :type :loading} + {:chain-id "10" :type :loading} + {:type :add}] + result (utils/loading-network-amounts valid-networks + disabled-chain-ids + receiver-networks + to?) + comparisons (map #(map/deep-compare %1 %2) + expected + result)] + (is (every? identity comparisons)))) + + (testing "Assigns :disabled type with zero total-amount to disabled networks when to? is false" + (let [valid-networks ["1" "10" "42161"] + disabled-chain-ids ["10" "42161"] + receiver-networks ["1"] + to? false + expected [{:chain-id "1" :type :loading} + {:chain-id "10" :type :disabled :total-amount (money/bignumber "0")} + {:chain-id "42161" :type :disabled :total-amount (money/bignumber "0")}] + result (utils/loading-network-amounts valid-networks + disabled-chain-ids + receiver-networks + to?) + comparisons (map #(map/deep-compare %1 %2) + expected + result)] + (is (every? identity comparisons)))) + + (testing "Filters out networks not in receiver networks when to? is true" + (let [valid-networks ["1" "10" "42161" "59144"] + disabled-chain-ids ["10"] + receiver-networks ["1" "42161"] + to? true + expected [{:chain-id "1" :type :loading} + {:chain-id "42161" :type :loading}] + result (utils/loading-network-amounts valid-networks + disabled-chain-ids + receiver-networks + to?) + comparisons (map #(map/deep-compare %1 %2) + expected + result)] + (is (every? identity comparisons)))) + + (testing "Appends :add type if receiver network count is less than available networks and to? is true" + (let [valid-networks ["1" "10" "42161"] + disabled-chain-ids ["10"] + receiver-networks ["1"] + to? true + expected [{:chain-id "1" :type :loading} + {:type :add}] + result (utils/loading-network-amounts valid-networks + disabled-chain-ids + receiver-networks + to?) + comparisons (map #(map/deep-compare %1 %2) + expected + result)] + (is (every? identity comparisons))))) + +(deftest test-network-links + (testing "Calculates position differences correctly" + (let [route [{:from {:chain-id "1"} :to {:chain-id "42161"}} + {:from {:chain-id "10"} :to {:chain-id "1"}} + {:from {:chain-id "42161"} :to {:chain-id "10"}}] + from-values-by-chain [{:chain-id "1"} {:chain-id "10"} {:chain-id "42161"}] + to-values-by-chain [{:chain-id "42161"} {:chain-id "1"} {:chain-id "10"}] + expected [{:from-chain-id "1" :to-chain-id "42161" :position-diff 0} + {:from-chain-id "10" :to-chain-id "1" :position-diff 0} + {:from-chain-id "42161" :to-chain-id "10" :position-diff 0}] + result (utils/network-links route from-values-by-chain to-values-by-chain)] + (is (= expected result)))) + + (testing "Handles cases with no position difference" + (let [route [{:from {:chain-id "1"} :to {:chain-id "1"}}] + from-values-by-chain [{:chain-id "1"} {:chain-id "10"} {:chain-id "42161"}] + to-values-by-chain [{:chain-id "1"} {:chain-id "10"} {:chain-id "42161"}] + expected [{:from-chain-id "1" :to-chain-id "1" :position-diff 0}] + result (utils/network-links route from-values-by-chain to-values-by-chain)] + (is (= expected result)))) + + (testing "Handles empty route" + (let [route [] + from-values-by-chain [] + to-values-by-chain [] + expected [] + result (utils/network-links route from-values-by-chain to-values-by-chain)] + (is (= expected result)))) + + (testing "Verifies negative position differences" + (let [route [{:from {:chain-id "1"} :to {:chain-id "42161"}}] + from-values-by-chain [{:chain-id "1"} {:chain-id "10"} {:chain-id "42161"}] + to-values-by-chain [{:chain-id "1"} {:chain-id "10"} {:chain-id "42161"}] + expected [{:from-chain-id "1" :to-chain-id "42161" :position-diff -2}] + result (utils/network-links route from-values-by-chain to-values-by-chain)] + (is (= expected result))))) diff --git a/src/status_im/subs/wallet/wallet.cljs b/src/status_im/subs/wallet/wallet.cljs index ce0fbb411c..1bd4df5912 100644 --- a/src/status_im/subs/wallet/wallet.cljs +++ b/src/status_im/subs/wallet/wallet.cljs @@ -121,6 +121,21 @@ :<- [:wallet/wallet-send] :-> :suggested-routes) +(rf/reg-sub + :wallet/wallet-send-sender-network-values + :<- [:wallet/wallet-send] + :-> :sender-network-values) + +(rf/reg-sub + :wallet/wallet-send-receiver-network-values + :<- [:wallet/wallet-send] + :-> :receiver-network-values) + +(rf/reg-sub + :wallet/wallet-send-network-links + :<- [:wallet/wallet-send] + :-> :network-links) + (rf/reg-sub :wallet/keypairs :<- [:wallet] diff --git a/src/utils/map.cljs b/src/utils/map.cljs new file mode 100644 index 0000000000..a476b6bea2 --- /dev/null +++ b/src/utils/map.cljs @@ -0,0 +1,17 @@ +(ns utils.map + (:require [utils.money :as money])) + +(defn compare-values + "Compares two values, using special handling for BigNumbers and regular equality for others." + [v1 v2] + (cond + (and (money/bignumber? v1) (money/bignumber? v2)) (money/equal-to v1 v2) + :else (= v1 v2))) + +(defn deep-compare + "Recursively compare two maps, specially handling BigNumber values within the maps." + [map1 map2] + (and + (= (set (keys map1)) (set (keys map2))) + (every? (fn [k] (compare-values (get map1 k) (get map2 k))) + (keys map1)))) diff --git a/src/utils/money.cljs b/src/utils/money.cljs index a977d395b8..cd5e6f0e35 100644 --- a/src/utils/money.cljs +++ b/src/utils/money.cljs @@ -37,6 +37,11 @@ (new BigNumber (normalize (str n))) (catch :default _ nil)))) +(defn bignumber? + "Check if the value is a bignumber." + [x] + (instance? BigNumber x)) + (defn greater-than-or-equals [^js bn1 ^js bn2] (.greaterThanOrEqualTo bn1 bn2)) diff --git a/translations/en.json b/translations/en.json index ca5fa7daa5..a91836909f 100644 --- a/translations/en.json +++ b/translations/en.json @@ -2595,5 +2595,6 @@ "this-account-has-no-activity": "This account has no activity", "this-address-has-activity": "This address has activity", "scanning-for-activity": "Scanning for activity...", - "send-community-link": "Send community link" + "send-community-link": "Send community link", + "at-least-one-network-must-be-activated": "At least 1 network must be activated" }