From cc6dcc3636bdb76d0b89ffa1c19841b3de267736 Mon Sep 17 00:00:00 2001 From: Volodymyr Kozieiev Date: Wed, 28 Aug 2024 13:38:48 +0100 Subject: [PATCH] Fix rounding of fiat and crypto on send page (#20915) * Fix conversion rounding on send page * fixing review notes * fix amount of allowed input decimals for crypto --- .../wallet/token_input/component_spec.cljs | 10 +- .../components/wallet/token_input/view.cljs | 45 ++------- .../preview/quo/wallet/token_input.cljs | 96 +++++++++++++------ .../contexts/wallet/common/utils.cljs | 82 +++++++++++----- .../contexts/wallet/common/utils_test.cljs | 9 +- .../send/input_amount/component_spec.cljs | 29 +----- .../wallet/send/input_amount/view.cljs | 36 +++++-- src/test_helpers/unit.cljs | 2 + src/utils/money.cljs | 6 ++ 9 files changed, 180 insertions(+), 135 deletions(-) diff --git a/src/quo/components/wallet/token_input/component_spec.cljs b/src/quo/components/wallet/token_input/component_spec.cljs index 478928321c..1aefca789a 100644 --- a/src/quo/components/wallet/token_input/component_spec.cljs +++ b/src/quo/components/wallet/token_input/component_spec.cljs @@ -6,16 +6,16 @@ (h/describe "Wallet: Token Input" (h/test "Token label renders" (h/render-with-theme-provider [token-input/view - {:token :snt - :currency :eur - :currency-symbol "€" - :conversion 1}]) + {:token :snt + :currency :eur + :crypto? true + :conversion 1}]) (h/is-truthy (h/get-by-text "SNT"))) (h/test "Amount renders" (h/render-with-theme-provider [token-input/view {:token :snt :currency :eur - :currency-symbol "€" + :converted-value "€0.00" :conversion 1}]) (h/is-truthy (h/get-by-text "€0.00")))) diff --git a/src/quo/components/wallet/token_input/view.cljs b/src/quo/components/wallet/token_input/view.cljs index db1802a0af..1af1b3231b 100644 --- a/src/quo/components/wallet/token_input/view.cljs +++ b/src/quo/components/wallet/token_input/view.cljs @@ -11,32 +11,10 @@ [quo.components.wallet.token-input.style :as style] [quo.theme :as quo.theme] [react-native.core :as rn] - [schema.core :as schema] - [utils.number :as number])) - -(defn fiat-format - [currency-symbol num-value conversion] - (str currency-symbol (.toFixed (* num-value conversion) 2))) - -(defn crypto-format - [num-value conversion crypto-decimals token] - (str (number/remove-trailing-zeroes - (.toFixed (/ num-value conversion) (or crypto-decimals 2))) - " " - (string/upper-case (or (clj->js token) "")))) - -(defn calc-value - [{:keys [crypto? currency-symbol token value conversion crypto-decimals]}] - (let [num-value (if (string? value) - (or (parse-double value) 0) - value)] - (if crypto? - (fiat-format currency-symbol num-value conversion) - (crypto-format num-value conversion crypto-decimals token)))) + [schema.core :as schema])) (defn- data-info - [{:keys [theme token crypto-decimals conversion networks title crypto? currency-symbol amount - error?]}] + [{:keys [theme networks title converted-value error?]}] [rn/view {:style style/data-container} [network-tag/view {:networks networks @@ -46,12 +24,7 @@ {:size :paragraph-2 :weight :medium :style (style/fiat-amount theme)} - (calc-value {:crypto? crypto? - :currency-symbol currency-symbol - :token token - :value amount - :conversion conversion - :crypto-decimals crypto-decimals})]]) + converted-value]]) (defn- token-name-text [theme text] @@ -123,16 +96,14 @@ :i/reorder]])))) (defn- view-internal - [{:keys [container-style value on-swap] :as props}] + [{:keys [container-style on-swap crypto?] :as props}] (let [theme (quo.theme/use-theme) width (:width (rn/get-window)) [value-internal set-value-internal] (rn/use-state nil) - [crypto? set-crypto] (rn/use-state true) handle-on-swap (rn/use-callback (fn [] - (set-crypto (not crypto?)) (when on-swap (on-swap (not crypto?)))) - [crypto? on-swap])] + [on-swap])] [rn/view {:style (merge (style/main-container width) container-style)} [rn/view {:style style/amount-container} [input-section @@ -143,10 +114,6 @@ :handle-on-swap handle-on-swap :crypto? crypto?)]] [divider-line/view {:container-style (style/divider theme)}] - [data-info - (assoc props - :theme theme - :crypto? crypto? - :amount (or value value-internal))]])) + [data-info (assoc props :theme theme)]])) (def view (schema/instrument #'view-internal component-schema/?schema)) diff --git a/src/status_im/contexts/preview/quo/wallet/token_input.cljs b/src/status_im/contexts/preview/quo/wallet/token_input.cljs index 038edad4ef..e8f05fb6af 100644 --- a/src/status_im/contexts/preview/quo/wallet/token_input.cljs +++ b/src/status_im/contexts/preview/quo/wallet/token_input.cljs @@ -2,9 +2,14 @@ (:require [quo.core :as quo] [quo.foundations.resources :as resources] + [react-native.core :as rn] [react-native.safe-area :as safe-area] [reagent.core :as reagent] - [status-im.contexts.preview.quo.preview :as preview])) + [status-im.common.controlled-input.utils :as controlled-input] + [status-im.contexts.preview.quo.preview :as preview] + [status-im.contexts.wallet.common.utils :as utils] + [utils.money :as money] + [utils.number :as number])) (def networks [{:source (resources/get-network :arbitrum)} @@ -20,8 +25,8 @@ {:key :snt}]} {:key :currency :type :select - :options [{:key :usd} - {:key :eur}]} + :options [{:key "$"} + {:key "€"}]} {:key :error? :type :boolean} {:key :allow-selection? @@ -29,30 +34,63 @@ (defn view [] - (let [state (reagent/atom {:token :eth - :currency :usd - :conversion 0.02 - :networks networks - :title title - :customization-color :blue - :show-keyboard? false - :allow-selection? true}) - value (reagent/atom "") - set-value (fn [v] - (swap! value str v)) - delete (fn [_] - (swap! value #(subs % 0 (dec (count %)))))] + (let [state (reagent/atom {:token :eth + :currency "$" + :conversion-rate 3450.28 + :networks networks + :title title + :customization-color :blue + :show-keyboard? false + :allow-selection? true + :crypto? true})] (fn [] - [preview/preview-container - {:state state - :descriptor descriptor - :full-screen? true - :component-container-style {:flex 1 - :justify-content :space-between}} - [quo/token-input (assoc @state :value @value)] - [quo/numbered-keyboard - {:container-style {:padding-bottom (safe-area/get-top)} - :left-action :dot - :delete-key? true - :on-press set-value - :on-delete delete}]]))) + (let [{:keys [currency token conversion-rate + crypto?]} @state + [input-state set-input-state] (rn/use-state controlled-input/init-state) + input-amount (controlled-input/input-value input-state) + swap-between-fiat-and-crypto (fn [] + (set-input-state + (fn [input-state] + (controlled-input/set-input-value + input-state + (let [new-value + (if-not crypto? + (utils/cut-crypto-decimals-to-fit-usd-cents + conversion-rate + (money/fiat->crypto input-amount + conversion-rate)) + (utils/cut-fiat-balance-to-two-decimals + (money/crypto->fiat input-amount + conversion-rate)))] + (number/remove-trailing-zeroes + new-value)))))) + converted-value (if crypto? + (utils/prettify-balance currency + (money/crypto->fiat input-amount + conversion-rate)) + (utils/prettify-crypto-balance + (or (clj->js token) "") + (money/fiat->crypto input-amount conversion-rate) + conversion-rate))] + [preview/preview-container + {:state state + :descriptor descriptor + :full-screen? true + :component-container-style {:flex 1 + :justify-content :space-between}} + [quo/token-input + (merge @state + {:value input-amount + :converted-value converted-value + :on-swap (fn [crypto] + (swap! state assoc :crypto? crypto) + (swap-between-fiat-and-crypto))})] + [quo/numbered-keyboard + {:container-style {:padding-bottom (safe-area/get-top)} + :left-action :dot + :delete-key? true + :on-press (fn [c] + (set-input-state #(controlled-input/add-character % c))) + + :on-delete (fn [] + (set-input-state controlled-input/delete-last))}]])))) diff --git a/src/status_im/contexts/wallet/common/utils.cljs b/src/status_im/contexts/wallet/common/utils.cljs index 318e742a4d..3398b987e5 100644 --- a/src/status_im/contexts/wallet/common/utils.cljs +++ b/src/status_im/contexts/wallet/common/utils.cljs @@ -12,14 +12,17 @@ [full-name] (first (string/split full-name #" "))) -(defn prettify-balance - [currency-symbol balance] +(defn cut-fiat-balance-to-two-decimals + [balance] (let [valid-balance? (and balance (or (number? balance) (.-toFixed balance)))] (as-> balance $ (if valid-balance? $ 0) - (.toFixed $ 2) - (str currency-symbol $)))) + (.toFixed $ 2)))) + +(defn prettify-balance + [currency-symbol balance] + (str currency-symbol (cut-fiat-balance-to-two-decimals balance))) (defn get-derivation-path [number-of-accounts] @@ -47,8 +50,8 @@ nil)) (defn calc-max-crypto-decimals - [value] - (let [str-representation (str value) + [one-cent-value] + (let [str-representation (str one-cent-value) decimal-part (second (clojure.string/split str-representation #"\.")) exponent (extract-exponent str-representation) zeroes-count (count (take-while #(= \0 %) decimal-part)) @@ -58,29 +61,60 @@ (inc max-decimals) max-decimals))) -(defn get-crypto-decimals-count - [{:keys [market-values-per-currency]}] - (let [price (get-in market-values-per-currency [:usd :price]) - one-cent-value (if (pos? price) (/ 0.01 price) 0)] - (calc-max-crypto-decimals one-cent-value))) +(defn token-usd-price + [token] + (get-in token [:market-values-per-currency :usd :price])) + +(defn one-cent-value + [token-price-in-usd] + (if (pos? token-price-in-usd) + (/ 0.01 token-price-in-usd) + 0)) + +(defn analyze-token-amount-for-price + "For full details: https://github.com/status-im/status-mobile/issues/18225" + [token-price-in-usd token-units] + (if (or (nil? token-units) + (not (money/bignumber? token-units)) + (money/equal-to token-units 0)) + {:zero-value? true} + (let [cent-value (one-cent-value token-price-in-usd)] + {:usd-cent-value cent-value + :standardized-decimals-count (if (nil? token-price-in-usd) + missing-price-decimals + (calc-max-crypto-decimals cent-value))}))) + +(defn cut-crypto-decimals-to-fit-usd-cents + [token-price-in-usd token-units] + (let [{:keys [zero-value? usd-cent-value standardized-decimals-count]} + (analyze-token-amount-for-price token-price-in-usd token-units)] + (cond + zero-value? "0" + (< token-units usd-cent-value) "0" + :else (number/remove-trailing-zeroes + (.toFixed token-units standardized-decimals-count))))) + +(defn prettify-crypto-balance + [token-symbol crypto-balance conversion-rate] + (str (cut-crypto-decimals-to-fit-usd-cents conversion-rate crypto-balance) + " " + (string/upper-case token-symbol))) (defn get-standard-crypto-format "For full details: https://github.com/status-im/status-mobile/issues/18225" - [{:keys [market-values-per-currency]} token-units] - (cond (or (nil? token-units) - (money/equal-to token-units 0)) - "0" + [token token-units] + (let [token-price-in-usd (token-usd-price token) + {:keys [zero-value? usd-cent-value standardized-decimals-count]} + (analyze-token-amount-for-price token-price-in-usd token-units)] + (cond + zero-value? + "0" - (nil? (-> market-values-per-currency :usd :price)) - (number/remove-trailing-zeroes (.toFixed token-units missing-price-decimals)) + (< token-units usd-cent-value) + (str "<" (number/remove-trailing-zeroes (.toFixed usd-cent-value standardized-decimals-count))) - :else - (let [price (-> market-values-per-currency :usd :price) - one-cent-value (if (pos? price) (/ 0.01 price) 0) - decimals-count (calc-max-crypto-decimals one-cent-value)] - (if (< token-units one-cent-value) - (str "<" (number/remove-trailing-zeroes (.toFixed one-cent-value decimals-count))) - (number/remove-trailing-zeroes (.toFixed token-units decimals-count)))))) + :else + (number/remove-trailing-zeroes (.toFixed token-units standardized-decimals-count))))) (defn get-market-value [currency {:keys [market-values-per-currency]}] diff --git a/src/status_im/contexts/wallet/common/utils_test.cljs b/src/status_im/contexts/wallet/common/utils_test.cljs index d4e9674f89..b0616c3f06 100644 --- a/src/status_im/contexts/wallet/common/utils_test.cljs +++ b/src/status_im/contexts/wallet/common/utils_test.cljs @@ -73,7 +73,12 @@ token-units (money/bignumber 0.01)] (is (= (utils/get-standard-crypto-format {:market-values-per-currency market-values-per-currency} token-units) - "<2"))))) + "<2"))) + (let [market-values-per-currency {:usd {:price 0.005}} + token-units "0.01"] + (is (= (utils/get-standard-crypto-format {:market-values-per-currency market-values-per-currency} + token-units) + "0"))))) (deftest calculate-total-token-balance-test (testing "calculate-total-token-balance function" @@ -163,5 +168,3 @@ expected-order ["DAI" "ETH" "SNT"]] (is (= expected-order sorted-tokens))))) - - 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 04df9d42d6..eb08546af0 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 @@ -113,34 +113,11 @@ :limit-crypto 250 :initial-crypto-currency? false}]) (h/is-truthy (h/get-by-text "0")) - (h/is-truthy (h/get-by-text "ETH")) - (h/is-truthy (h/get-by-text "$0.00")) + (h/is-truthy (h/get-by-text "USD")) + (h/is-truthy (h/get-by-text "0 ETH")) (h/is-truthy (h/get-by-label-text :container)) (h/is-disabled (h/get-by-label-text :button-one))) - (h/test "Fill token input and confirm" - (h/setup-subs sub-mocks) - (let [on-confirm (h/mock-fn)] - (h/render-with-theme-provider [input-amount/view - {:on-confirm on-confirm - :crypto-decimals 10 - :limit-crypto 1000 - :initial-crypto-currency? false}]) - - (h/fire-event :press (h/query-by-label-text :keyboard-key-1)) - (h/fire-event :press (h/query-by-label-text :keyboard-key-2)) - (h/fire-event :press (h/query-by-label-text :keyboard-key-3)) - (h/fire-event :press (h/query-by-label-text :keyboard-key-.)) - (h/fire-event :press (h/query-by-label-text :keyboard-key-4)) - (h/fire-event :press (h/query-by-label-text :keyboard-key-5)) - - (-> (h/wait-for #(h/get-by-text "$1234.50")) - (.then (fn [] - (h/is-truthy (h/get-by-label-text :button-one)) - (h/is-truthy (h/get-by-label-text :container)) - (h/fire-event :press (h/get-by-label-text :button-one)) - (h/was-called on-confirm)))))) - (h/test "Fill token input and confirm" (h/setup-subs sub-mocks) @@ -149,7 +126,7 @@ {:crypto-decimals 10 :limit-crypto 1000 :on-confirm on-confirm - :initial-crypto-currency? false}]) + :initial-crypto-currency? true}]) (h/fire-event :press (h/query-by-label-text :keyboard-key-1)) (h/fire-event :press (h/query-by-label-text :keyboard-key-2)) 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 b90218764d..ab7b275bc6 100644 --- a/src/status_im/contexts/wallet/send/input_amount/view.cljs +++ b/src/status_im/contexts/wallet/send/input_amount/view.cljs @@ -163,7 +163,6 @@ {fiat-currency :currency} (rf/sub [:profile/profile]) {token-symbol :symbol token-networks :networks - token-decimals :decimals :as token} (rf/sub [:wallet/wallet-send-token]) send-from-locked-amounts (rf/sub [:wallet/wallet-send-from-locked-amounts]) @@ -178,6 +177,10 @@ :market-values-per-currency currency :price) + token-decimals (-> token + utils/token-usd-price + utils/one-cent-value + utils/calc-max-crypto-decimals) loading-routes? (rf/sub [:wallet/wallet-send-loading-suggested-routes?]) route (rf/sub [:wallet/wallet-send-route]) @@ -295,6 +298,14 @@ :valid-input? valid-input? :bounce-duration-ms bounce-duration-ms :token token})) + swap-currency (fn [to-crypto? value] + (if to-crypto? + (utils/cut-crypto-decimals-to-fit-usd-cents + conversion-rate + (money/fiat->crypto value conversion-rate)) + (utils/cut-fiat-balance-to-two-decimals + (money/crypto->fiat value + conversion-rate)))) swap-between-fiat-and-crypto (fn [swap-to-crypto-currency?] (set-just-toggled-mode? true) (set-crypto-currency swap-to-crypto-currency?) @@ -304,13 +315,9 @@ input-state (let [value (controlled-input/input-value input-state) - new-value (if swap-to-crypto-currency? - (.toFixed (/ value - conversion-rate) - crypto-decimals) - (.toFixed (* value - conversion-rate) - 12))] + new-value (swap-currency + swap-to-crypto-currency? + value)] (number/remove-trailing-zeroes new-value))))))] (rn/use-mount @@ -363,7 +370,18 @@ :value input-amount :on-swap swap-between-fiat-and-crypto :on-token-press show-select-asset-sheet - :allow-selection? false}] + :allow-selection? false + :crypto? crypto-currency? + :converted-value (if crypto-currency? + (utils/prettify-balance + currency-symbol + (money/crypto->fiat input-amount + conversion-rate)) + (utils/prettify-crypto-balance + (or (clj->js token-symbol) "") + (money/fiat->crypto input-amount + conversion-rate) + conversion-rate))}] [routes/view {:token token-by-symbol :send-amount-in-crypto amount-in-crypto diff --git a/src/test_helpers/unit.cljs b/src/test_helpers/unit.cljs index 661ba88dcd..c6081afe17 100644 --- a/src/test_helpers/unit.cljs +++ b/src/test_helpers/unit.cljs @@ -6,6 +6,8 @@ prefer to use it for more general purpose concepts, such as the re-frame event layer." (:require-macros test-helpers.unit) + ;; We must require test-helpers.matchers namespace to register the custom cljs.test directive + ;; `match-strict?` (:require [re-frame.core :as rf] [re-frame.db :as rf-db] diff --git a/src/utils/money.cljs b/src/utils/money.cljs index 1af5125d59..95f92141d3 100644 --- a/src/utils/money.cljs +++ b/src/utils/money.cljs @@ -262,6 +262,12 @@ [bn1 bn2] (.round (.dividedBy ^js bn1 bn2) 0)) +(defn fiat->crypto + [crypto fiat-price] + (when-let [crypto-bn (bignumber crypto)] + (div crypto-bn + (bignumber fiat-price)))) + (defn absolute-value [bn] (when bn