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

@ -8,7 +8,7 @@
(h/render-with-theme-provider [token-input/view
{:token :snt
:currency :eur
:currency-symbol "€"
:crypto? true
:conversion 1}])
(h/is-truthy (h/get-by-text "SNT")))
@ -16,6 +16,6 @@
(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"))))

View File

@ -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))

View File

@ -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?
@ -30,29 +35,62 @@
(defn view
[]
(let [state (reagent/atom {:token :eth
:currency :usd
:conversion 0.02
:currency "$"
:conversion-rate 3450.28
: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 %)))))]
:allow-selection? true
:crypto? true})]
(fn []
(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 (assoc @state :value @value)]
[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 set-value
:on-delete delete}]])))
: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]
(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))
[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))))))
(number/remove-trailing-zeroes (.toFixed token-units standardized-decimals-count)))))
(defn get-market-value
[currency {:keys [market-values-per-currency]}]

View File

@ -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)))))

View File

@ -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))

View File

@ -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

View File

@ -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]

View File

@ -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