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
This commit is contained in:
Volodymyr Kozieiev 2024-08-28 13:38:48 +01:00 committed by GitHub
parent 9d6b8609be
commit cc6dcc3636
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 180 additions and 135 deletions

View File

@ -6,16 +6,16 @@
(h/describe "Wallet: Token Input" (h/describe "Wallet: Token Input"
(h/test "Token label renders" (h/test "Token label renders"
(h/render-with-theme-provider [token-input/view (h/render-with-theme-provider [token-input/view
{:token :snt {:token :snt
:currency :eur :currency :eur
:currency-symbol "€" :crypto? true
:conversion 1}]) :conversion 1}])
(h/is-truthy (h/get-by-text "SNT"))) (h/is-truthy (h/get-by-text "SNT")))
(h/test "Amount renders" (h/test "Amount renders"
(h/render-with-theme-provider [token-input/view (h/render-with-theme-provider [token-input/view
{:token :snt {:token :snt
:currency :eur :currency :eur
:currency-symbol "€" :converted-value "€0.00"
:conversion 1}]) :conversion 1}])
(h/is-truthy (h/get-by-text "€0.00")))) (h/is-truthy (h/get-by-text "€0.00"))))

View File

@ -11,32 +11,10 @@
[quo.components.wallet.token-input.style :as style] [quo.components.wallet.token-input.style :as style]
[quo.theme :as quo.theme] [quo.theme :as quo.theme]
[react-native.core :as rn] [react-native.core :as rn]
[schema.core :as schema] [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))))
(defn- data-info (defn- data-info
[{:keys [theme token crypto-decimals conversion networks title crypto? currency-symbol amount [{:keys [theme networks title converted-value error?]}]
error?]}]
[rn/view {:style style/data-container} [rn/view {:style style/data-container}
[network-tag/view [network-tag/view
{:networks networks {:networks networks
@ -46,12 +24,7 @@
{:size :paragraph-2 {:size :paragraph-2
:weight :medium :weight :medium
:style (style/fiat-amount theme)} :style (style/fiat-amount theme)}
(calc-value {:crypto? crypto? converted-value]])
:currency-symbol currency-symbol
:token token
:value amount
:conversion conversion
:crypto-decimals crypto-decimals})]])
(defn- token-name-text (defn- token-name-text
[theme text] [theme text]
@ -123,16 +96,14 @@
:i/reorder]])))) :i/reorder]]))))
(defn- view-internal (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) (let [theme (quo.theme/use-theme)
width (:width (rn/get-window)) width (:width (rn/get-window))
[value-internal set-value-internal] (rn/use-state nil) [value-internal set-value-internal] (rn/use-state nil)
[crypto? set-crypto] (rn/use-state true)
handle-on-swap (rn/use-callback handle-on-swap (rn/use-callback
(fn [] (fn []
(set-crypto (not crypto?))
(when on-swap (on-swap (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 (merge (style/main-container width) container-style)}
[rn/view {:style style/amount-container} [rn/view {:style style/amount-container}
[input-section [input-section
@ -143,10 +114,6 @@
:handle-on-swap handle-on-swap :handle-on-swap handle-on-swap
:crypto? crypto?)]] :crypto? crypto?)]]
[divider-line/view {:container-style (style/divider theme)}] [divider-line/view {:container-style (style/divider theme)}]
[data-info [data-info (assoc props :theme theme)]]))
(assoc props
:theme theme
:crypto? crypto?
:amount (or value value-internal))]]))
(def view (schema/instrument #'view-internal component-schema/?schema)) (def view (schema/instrument #'view-internal component-schema/?schema))

View File

@ -2,9 +2,14 @@
(:require (:require
[quo.core :as quo] [quo.core :as quo]
[quo.foundations.resources :as resources] [quo.foundations.resources :as resources]
[react-native.core :as rn]
[react-native.safe-area :as safe-area] [react-native.safe-area :as safe-area]
[reagent.core :as reagent] [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 (def networks
[{:source (resources/get-network :arbitrum)} [{:source (resources/get-network :arbitrum)}
@ -20,8 +25,8 @@
{:key :snt}]} {:key :snt}]}
{:key :currency {:key :currency
:type :select :type :select
:options [{:key :usd} :options [{:key "$"}
{:key :eur}]} {:key "€"}]}
{:key :error? {:key :error?
:type :boolean} :type :boolean}
{:key :allow-selection? {:key :allow-selection?
@ -29,30 +34,63 @@
(defn view (defn view
[] []
(let [state (reagent/atom {:token :eth (let [state (reagent/atom {:token :eth
:currency :usd :currency "$"
:conversion 0.02 :conversion-rate 3450.28
:networks networks :networks networks
:title title :title title
:customization-color :blue :customization-color :blue
:show-keyboard? false :show-keyboard? false
:allow-selection? true}) :allow-selection? true
value (reagent/atom "") :crypto? true})]
set-value (fn [v]
(swap! value str v))
delete (fn [_]
(swap! value #(subs % 0 (dec (count %)))))]
(fn [] (fn []
[preview/preview-container (let [{:keys [currency token conversion-rate
{:state state crypto?]} @state
:descriptor descriptor [input-state set-input-state] (rn/use-state controlled-input/init-state)
:full-screen? true input-amount (controlled-input/input-value input-state)
:component-container-style {:flex 1 swap-between-fiat-and-crypto (fn []
:justify-content :space-between}} (set-input-state
[quo/token-input (assoc @state :value @value)] (fn [input-state]
[quo/numbered-keyboard (controlled-input/set-input-value
{:container-style {:padding-bottom (safe-area/get-top)} input-state
:left-action :dot (let [new-value
:delete-key? true (if-not crypto?
:on-press set-value (utils/cut-crypto-decimals-to-fit-usd-cents
:on-delete delete}]]))) 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))}]]))))

View File

@ -12,14 +12,17 @@
[full-name] [full-name]
(first (string/split full-name #" "))) (first (string/split full-name #" ")))
(defn prettify-balance (defn cut-fiat-balance-to-two-decimals
[currency-symbol balance] [balance]
(let [valid-balance? (and balance (let [valid-balance? (and balance
(or (number? balance) (.-toFixed balance)))] (or (number? balance) (.-toFixed balance)))]
(as-> balance $ (as-> balance $
(if valid-balance? $ 0) (if valid-balance? $ 0)
(.toFixed $ 2) (.toFixed $ 2))))
(str currency-symbol $))))
(defn prettify-balance
[currency-symbol balance]
(str currency-symbol (cut-fiat-balance-to-two-decimals balance)))
(defn get-derivation-path (defn get-derivation-path
[number-of-accounts] [number-of-accounts]
@ -47,8 +50,8 @@
nil)) nil))
(defn calc-max-crypto-decimals (defn calc-max-crypto-decimals
[value] [one-cent-value]
(let [str-representation (str value) (let [str-representation (str one-cent-value)
decimal-part (second (clojure.string/split str-representation #"\.")) decimal-part (second (clojure.string/split str-representation #"\."))
exponent (extract-exponent str-representation) exponent (extract-exponent str-representation)
zeroes-count (count (take-while #(= \0 %) decimal-part)) zeroes-count (count (take-while #(= \0 %) decimal-part))
@ -58,29 +61,60 @@
(inc max-decimals) (inc max-decimals)
max-decimals))) max-decimals)))
(defn get-crypto-decimals-count (defn token-usd-price
[{:keys [market-values-per-currency]}] [token]
(let [price (get-in market-values-per-currency [:usd :price]) (get-in token [:market-values-per-currency :usd :price]))
one-cent-value (if (pos? price) (/ 0.01 price) 0)]
(calc-max-crypto-decimals one-cent-value))) (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 (defn get-standard-crypto-format
"For full details: https://github.com/status-im/status-mobile/issues/18225" "For full details: https://github.com/status-im/status-mobile/issues/18225"
[{:keys [market-values-per-currency]} token-units] [token token-units]
(cond (or (nil? token-units) (let [token-price-in-usd (token-usd-price token)
(money/equal-to token-units 0)) {:keys [zero-value? usd-cent-value standardized-decimals-count]}
"0" (analyze-token-amount-for-price token-price-in-usd token-units)]
(cond
zero-value?
"0"
(nil? (-> market-values-per-currency :usd :price)) (< token-units usd-cent-value)
(number/remove-trailing-zeroes (.toFixed token-units missing-price-decimals)) (str "<" (number/remove-trailing-zeroes (.toFixed usd-cent-value standardized-decimals-count)))
:else :else
(let [price (-> market-values-per-currency :usd :price) (number/remove-trailing-zeroes (.toFixed token-units standardized-decimals-count)))))
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))))))
(defn get-market-value (defn get-market-value
[currency {:keys [market-values-per-currency]}] [currency {:keys [market-values-per-currency]}]

View File

@ -73,7 +73,12 @@
token-units (money/bignumber 0.01)] token-units (money/bignumber 0.01)]
(is (= (utils/get-standard-crypto-format {:market-values-per-currency market-values-per-currency} (is (= (utils/get-standard-crypto-format {:market-values-per-currency market-values-per-currency}
token-units) 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 (deftest calculate-total-token-balance-test
(testing "calculate-total-token-balance function" (testing "calculate-total-token-balance function"
@ -163,5 +168,3 @@
expected-order ["DAI" "ETH" "SNT"]] expected-order ["DAI" "ETH" "SNT"]]
(is (= expected-order sorted-tokens))))) (is (= expected-order sorted-tokens)))))

View File

@ -113,34 +113,11 @@
:limit-crypto 250 :limit-crypto 250
:initial-crypto-currency? false}]) :initial-crypto-currency? false}])
(h/is-truthy (h/get-by-text "0")) (h/is-truthy (h/get-by-text "0"))
(h/is-truthy (h/get-by-text "ETH")) (h/is-truthy (h/get-by-text "USD"))
(h/is-truthy (h/get-by-text "$0.00")) (h/is-truthy (h/get-by-text "0 ETH"))
(h/is-truthy (h/get-by-label-text :container)) (h/is-truthy (h/get-by-label-text :container))
(h/is-disabled (h/get-by-label-text :button-one))) (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/test "Fill token input and confirm"
(h/setup-subs sub-mocks) (h/setup-subs sub-mocks)
@ -149,7 +126,7 @@
{:crypto-decimals 10 {:crypto-decimals 10
:limit-crypto 1000 :limit-crypto 1000
:on-confirm on-confirm :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-1))
(h/fire-event :press (h/query-by-label-text :keyboard-key-2)) (h/fire-event :press (h/query-by-label-text :keyboard-key-2))

View File

@ -163,7 +163,6 @@
{fiat-currency :currency} (rf/sub [:profile/profile]) {fiat-currency :currency} (rf/sub [:profile/profile])
{token-symbol :symbol {token-symbol :symbol
token-networks :networks token-networks :networks
token-decimals :decimals
:as :as
token} (rf/sub [:wallet/wallet-send-token]) token} (rf/sub [:wallet/wallet-send-token])
send-from-locked-amounts (rf/sub [:wallet/wallet-send-from-locked-amounts]) send-from-locked-amounts (rf/sub [:wallet/wallet-send-from-locked-amounts])
@ -178,6 +177,10 @@
:market-values-per-currency :market-values-per-currency
currency currency
:price) :price)
token-decimals (-> token
utils/token-usd-price
utils/one-cent-value
utils/calc-max-crypto-decimals)
loading-routes? (rf/sub loading-routes? (rf/sub
[:wallet/wallet-send-loading-suggested-routes?]) [:wallet/wallet-send-loading-suggested-routes?])
route (rf/sub [:wallet/wallet-send-route]) route (rf/sub [:wallet/wallet-send-route])
@ -295,6 +298,14 @@
:valid-input? valid-input? :valid-input? valid-input?
:bounce-duration-ms bounce-duration-ms :bounce-duration-ms bounce-duration-ms
:token token})) :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?] swap-between-fiat-and-crypto (fn [swap-to-crypto-currency?]
(set-just-toggled-mode? true) (set-just-toggled-mode? true)
(set-crypto-currency swap-to-crypto-currency?) (set-crypto-currency swap-to-crypto-currency?)
@ -304,13 +315,9 @@
input-state input-state
(let [value (controlled-input/input-value (let [value (controlled-input/input-value
input-state) input-state)
new-value (if swap-to-crypto-currency? new-value (swap-currency
(.toFixed (/ value swap-to-crypto-currency?
conversion-rate) value)]
crypto-decimals)
(.toFixed (* value
conversion-rate)
12))]
(number/remove-trailing-zeroes (number/remove-trailing-zeroes
new-value))))))] new-value))))))]
(rn/use-mount (rn/use-mount
@ -363,7 +370,18 @@
:value input-amount :value input-amount
:on-swap swap-between-fiat-and-crypto :on-swap swap-between-fiat-and-crypto
:on-token-press show-select-asset-sheet :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 [routes/view
{:token token-by-symbol {:token token-by-symbol
:send-amount-in-crypto amount-in-crypto :send-amount-in-crypto amount-in-crypto

View File

@ -6,6 +6,8 @@
prefer to use it for more general purpose concepts, such as the re-frame event prefer to use it for more general purpose concepts, such as the re-frame event
layer." layer."
(:require-macros test-helpers.unit) (:require-macros test-helpers.unit)
;; We must require test-helpers.matchers namespace to register the custom cljs.test directive
;; `match-strict?`
(:require (:require
[re-frame.core :as rf] [re-frame.core :as rf]
[re-frame.db :as rf-db] [re-frame.db :as rf-db]

View File

@ -262,6 +262,12 @@
[bn1 bn2] [bn1 bn2]
(.round (.dividedBy ^js bn1 bn2) 0)) (.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 (defn absolute-value
[bn] [bn]
(when bn (when bn