diff --git a/src/status_im/models/wallet.cljs b/src/status_im/models/wallet.cljs new file mode 100644 index 0000000000..34724c7ebc --- /dev/null +++ b/src/status_im/models/wallet.cljs @@ -0,0 +1,53 @@ +(ns status-im.models.wallet + (:require [status-im.utils.money :as money])) + +(def min-gas-price-wei (money/bignumber 1)) + +(defmulti invalid-send-parameter? (fn [type _] type)) + +(defmethod invalid-send-parameter? :gas-price [_ value] + (cond + (not value) :invalid-number + (< (money/->wei :gwei value) min-gas-price-wei) :not-enough-wei)) + +(defmethod invalid-send-parameter? :default [_ value] + (when (or (not value) + (<= value 0)) + :invalid-number)) + +(defn- calculate-max-fee + [gas gas-price] + (if (and gas gas-price) + (money/to-fixed (money/wei->ether (.times gas gas-price))) + "0")) + +(defn- edit-max-fee [edit] + (let [gas (get-in edit [:gas-price :value-number]) + gas-price (get-in edit [:gas :value-number])] + (assoc edit :max-fee (calculate-max-fee gas gas-price)))) + +(defn add-max-fee [{:keys [gas gas-price] :as transaction}] + (assoc transaction :max-fee (calculate-max-fee gas gas-price))) + +(defn build-edit [edit-value key value] + "Takes the previous edit, either :gas or :gas-price and a value as string. + Wei for gas, and gwei for gas price. + Validates them and sets max fee" + (let [bn-value (money/bignumber value) + invalid? (invalid-send-parameter? key bn-value) + data (if invalid? + {:value value + :max-fee 0 + :invalid? invalid?} + {:value value + :value-number (if (= :gas-price key) + (money/->wei :gwei bn-value) + bn-value) + :invalid? false})] + (-> edit-value + (assoc key data) + edit-max-fee))) + +(defn edit-value + [key value {:keys [db]}] + {:db (update-in db [:wallet :edit] build-edit key value)}) diff --git a/src/status_im/translations/en.cljs b/src/status_im/translations/en.cljs index aa34a19acb..e6ba9e8236 100644 --- a/src/status_im/translations/en.cljs +++ b/src/status_im/translations/en.cljs @@ -454,6 +454,7 @@ :share "Share" :eth "ETH" :gwei "Gwei" + :wallet-send-min-wei "Min 1 wei" :currency "Currency" :usd-currency "USD" :currency-display-name-aed "Emirati Dirham" diff --git a/src/status_im/ui/screens/wallet/events.cljs b/src/status_im/ui/screens/wallet/events.cljs index 7dca36acac..c6446f6b9b 100644 --- a/src/status_im/ui/screens/wallet/events.cljs +++ b/src/status_im/ui/screens/wallet/events.cljs @@ -8,6 +8,7 @@ [status-im.utils.handlers :as handlers] [status-im.utils.prices :as prices] [status-im.utils.transactions :as transactions] + [status-im.models.wallet :as models.wallet] [taoensso.timbre :as log] status-im.ui.screens.wallet.request.events [status-im.utils.money :as money] @@ -248,7 +249,11 @@ :wallet/update-gas-price-success (fn [db [_ price edit?]] (if edit? - (assoc-in db [:wallet :edit :gas-price] {:value price :invalid? false}) + (:db (models.wallet/edit-value + :gas-price + (money/to-fixed + (money/wei-> :gwei price)) + {:db db})) (assoc-in db [:wallet :send-transaction :gas-price] price)))) (handlers/register-handler-fx diff --git a/src/status_im/ui/screens/wallet/send/events.cljs b/src/status_im/ui/screens/wallet/send/events.cljs index ce27e476a2..508ff8bf2e 100644 --- a/src/status_im/ui/screens/wallet/send/events.cljs +++ b/src/status_im/ui/screens/wallet/send/events.cljs @@ -14,6 +14,7 @@ [status-im.utils.security :as security] [status-im.utils.types :as types] [status-im.utils.utils :as utils] + [status-im.models.wallet :as models.wallet] [status-im.constants :as constants] [status-im.transport.utils :as transport.utils] [taoensso.timbre :as log])) @@ -378,13 +379,8 @@ (handlers/register-handler-fx :wallet.send/edit-value - (fn [{:keys [db]} [_ key value]] - (let [bn-value (money/bignumber value) - data (if bn-value - {:value bn-value - :invalid? false} - {:invalid? true})] - {:db (update-in db [:wallet :edit key] merge data)}))) + (fn [cofx [_ key value]] + (models.wallet/edit-value key value cofx))) (handlers/register-handler-fx :wallet.send/set-gas-details @@ -400,11 +396,15 @@ (handlers/register-handler-fx :wallet.send/reset-gas-default - (fn [{:keys [db]}] - {:dispatch [:wallet/update-gas-price true] - :db (assoc-in db [:wallet :edit :gas] - {:value (ethereum/estimate-gas (-> db :wallet :send-transaction :symbol)) - :invalid? false})})) + (fn [{:keys [db] :as cofx}] + (let [gas-estimate (money/to-fixed + (ethereum/estimate-gas + (-> db :wallet :send-transaction :symbol)))] + (assoc (models.wallet/edit-value + :gas + gas-estimate + cofx) + :dispatch [:wallet/update-gas-price true])))) (handlers/register-handler-fx :close-transaction-sent-screen diff --git a/src/status_im/ui/screens/wallet/send/subs.cljs b/src/status_im/ui/screens/wallet/send/subs.cljs index 8081971f75..2a13b1be39 100644 --- a/src/status_im/ui/screens/wallet/send/subs.cljs +++ b/src/status_im/ui/screens/wallet/send/subs.cljs @@ -1,6 +1,7 @@ (ns status-im.ui.screens.wallet.send.subs (:require [re-frame.core :as re-frame] [status-im.utils.money :as money] + [status-im.models.wallet :as models.wallet] [status-im.utils.hex :as utils.hex])) (re-frame/reg-sub ::send-transaction @@ -54,9 +55,30 @@ {:gas (or (:gas send-transaction) (:gas unsigned-transaction)) :gas-price (or (:gas-price send-transaction) (:gas-price unsigned-transaction))})))) +(defn edit-or-transaction-data + "Set up edit data structure, defaulting to transaction when not available" + [transaction edit] + (cond-> edit + (not (get-in edit [:gas-price :value])) + (models.wallet/build-edit + :gas-price + (money/to-fixed (money/wei-> :gwei (:gas-price transaction)))) + + (not (get-in edit [:gas :value])) + (models.wallet/build-edit + :gas + (money/to-fixed (:gas transaction))))) + (re-frame/reg-sub :wallet/edit + :<- [::send-transaction] + :<- [::unsigned-transaction] :<- [:wallet] - :edit) + (fn [[send-transaction unsigned-transaction {:keys [edit]}]] + (edit-or-transaction-data + (if (:id send-transaction) + unsigned-transaction + send-transaction) + edit))) (defn sign-enabled? [amount-error to amount] (and @@ -68,8 +90,11 @@ :<- [::send-transaction] :<- [:balance] (fn [[{:keys [amount symbol] :as transaction} balance]] - (assoc transaction :sufficient-funds? (or (nil? amount) - (money/sufficient-funds? amount (get balance symbol)))))) + (-> transaction + (models.wallet/add-max-fee) + (assoc :sufficient-funds? + (or (nil? amount) + (money/sufficient-funds? amount (get balance symbol))))))) (re-frame/reg-sub :wallet.send/unsigned-transaction :<- [::unsigned-transaction] @@ -79,7 +104,7 @@ (when transaction (let [contact (contacts (utils.hex/normalize-hex to)) sufficient-funds? (money/sufficient-funds? value (get balance symbol))] - (cond-> (assoc transaction + (cond-> (assoc (models.wallet/add-max-fee transaction) :amount value :sufficient-funds? sufficient-funds?) contact (assoc :to-name (:name contact))))))) diff --git a/src/status_im/ui/screens/wallet/send/views.cljs b/src/status_im/ui/screens/wallet/send/views.cljs index a5610de6a5..1d062235d9 100644 --- a/src/status_im/ui/screens/wallet/send/views.cljs +++ b/src/status_im/ui/screens/wallet/send/views.cljs @@ -92,10 +92,6 @@ (i18n/label :t/transactions-sign-transaction) [vector-icons/icon :icons/forward {:color (if immediate-sign-enabled? :white :gray)}]]])) -(defn- max-fee [gas gas-price] - (when (and gas gas-price) - (money/wei->ether (.times gas gas-price)))) - (defn return-to-transaction [dapp-transaction?] (if dapp-transaction? (re-frame/dispatch [:navigate-to-modal :wallet-send-transaction-modal]) @@ -116,12 +112,13 @@ unsigned-transaction [:wallet.send/unsigned-transaction] network [:get-current-account-network] {gas-edit :gas + max-fee :max-fee gas-price-edit :gas-price} [:wallet/edit]] (let [modal? (:id send-transaction) ;;TODO(goranjovic) - unify unsigned and regular transaction subs {:keys [amount symbol] :as transaction} (if modal? unsigned-transaction send-transaction) - gas (or (:value gas-edit) (:gas transaction)) - gas-price (or (:value gas-price-edit) (:gas-price transaction)) + gas (:value gas-edit) + gas-price (:value gas-price-edit) {:keys [decimals]} (tokens/asset-for (ethereum/network->chain-keyword network) symbol)] [wallet.components/simple-screen {:status-bar-type :modal-wallet} [toolbar false modal? act/close-white @@ -135,7 +132,7 @@ [react/view styles/gas-input-wrapper [react/text-input (merge styles/transaction-fee-input {:on-change-text #(re-frame/dispatch [:wallet.send/edit-value :gas %]) - :default-value (str (money/to-fixed gas)) + :default-value gas :accessibility-label :gas-limit-input})]]] (when (:invalid? gas-edit) [tooltip/tooltip (i18n/label :t/invalid-number)])] @@ -145,13 +142,15 @@ (i18n/label :t/gas-price) [react/view styles/gas-input-wrapper [react/text-input (merge styles/transaction-fee-input - {:on-change-text #(re-frame/dispatch [:wallet.send/edit-value :gas-price (money/->wei :gwei %)]) - :default-value (str (money/to-fixed (money/wei-> :gwei gas-price))) + {:on-change-text #(re-frame/dispatch [:wallet.send/edit-value :gas-price %]) + :default-value gas-price :accessibility-label :gas-price-input})] [wallet.components/cartouche-secondary-text (i18n/label :t/gwei)]]] (when (:invalid? gas-price-edit) - [tooltip/tooltip (i18n/label :t/invalid-number)])]] + [tooltip/tooltip (i18n/label (if (= :invalid-number (:invalid? gas-price-edit)) + :t/invalid-number + :t/wallet-send-min-wei))])]] [react/view styles/transaction-fee-info [react/view styles/transaction-fee-info-icon @@ -171,21 +170,22 @@ (i18n/label :t/wallet-transaction-total-fee) [react/view {:accessibility-label :total-fee-input} [wallet.components/cartouche-text-content - (str (money/to-fixed (max-fee gas gas-price))) - (i18n/label :t/eth)]]]] + (str max-fee " " (i18n/label :t/eth))]]]] [bottom-buttons/bottom-buttons styles/fee-buttons [button/button {:on-press #(re-frame/dispatch [:wallet.send/reset-gas-default]) :accessibility-label :reset-to-default-button} (i18n/label :t/reset-default)] - [button/button {:on-press #(do (re-frame/dispatch [:wallet.send/set-gas-details gas gas-price]) + [button/button {:on-press #(do (re-frame/dispatch [:wallet.send/set-gas-details + (:value-number gas-edit) + (:value-number gas-price-edit)]) (return-to-transaction modal?)) :accessibility-label :done-button :disabled? (or (:invalid? gas-edit) (:invalid? gas-price-edit))} (i18n/label :t/done)]]]]))) -(defn- advanced-cartouche [{:keys [gas gas-price]} modal?] +(defn- advanced-cartouche [{:keys [max-fee gas gas-price]} modal?] [react/view [wallet.components/cartouche {:on-press #(do (re-frame/dispatch [:wallet.send/clear-gas]) (re-frame/dispatch [:navigate-to-modal :wallet-transaction-fee]))} @@ -193,7 +193,7 @@ [react/view {:style styles/advanced-options-text-wrapper :accessibility-label :transaction-fee-button} [react/text {:style styles/advanced-fees-text} - (str (money/to-fixed (max-fee gas gas-price)) " " (i18n/label :t/eth))] + (str max-fee " " (i18n/label :t/eth))] [react/text {:style styles/advanced-fees-details-text} (str (money/to-fixed gas) " * " (money/to-fixed (money/wei-> :gwei gas-price)) (i18n/label :t/gwei))]]]]) diff --git a/test/cljs/status_im/test/models/wallet.cljs b/test/cljs/status_im/test/models/wallet.cljs new file mode 100644 index 0000000000..3f150894dc --- /dev/null +++ b/test/cljs/status_im/test/models/wallet.cljs @@ -0,0 +1,52 @@ +(ns status-im.test.models.wallet + (:require [cljs.test :refer-macros [deftest is testing]] + [status-im.utils.money :as money] + [status-im.models.wallet :as model])) + +(deftest valid-min-gas-price-test + (testing "not an number" + (is (= :invalid-number (model/invalid-send-parameter? :gas-price nil)))) + (testing "a number less than the minimum" + (is (= :not-enough-wei (model/invalid-send-parameter? :gas-price (money/bignumber "0.0000000001"))))) + (testing "a number greater than the mininum" + (is (not (model/invalid-send-parameter? :gas-price 3)))) + (testing "the minimum" + (is (not (model/invalid-send-parameter? :gas-price (money/bignumber "0.000000001")))))) + +(deftest valid-gas + (testing "not an number" + (is (= :invalid-number (model/invalid-send-parameter? :gas nil)))) + (testing "0" + (is (= :invalid-number (model/invalid-send-parameter? :gas 0)))) + (testing "a number" + (is (not (model/invalid-send-parameter? :gas 1))))) + +(deftest build-edit-test + (testing "an invalid edit" + (let [actual (-> {} + (model/build-edit :gas "invalid") + (model/build-edit :gas-price "0.00000000001"))] + (testing "it marks gas-price as invalid" + (is (get-in actual [:gas-price :invalid?]))) + (testing "it does not change value" + (is (= "0.00000000001" (get-in actual [:gas-price :value])))) + (testing "it marks gas as invalid" + (is (get-in actual [:gas :invalid?]))) + (testing "it does not change gas value" + (is (= "invalid" (get-in actual [:gas :value])))) + (testing "it sets max-fee to 0" + (is (= "0" (:max-fee actual)))))) + (testing "an valid edit" + (let [actual (-> {} + (model/build-edit :gas "21000") + (model/build-edit :gas-price "10"))] + (testing "it does not mark gas-price as invalid" + (is (not (get-in actual [:gas-price :invalid?])))) + (testing "it sets the value in number for gas-price, in gwei" + (is (= "10000000000" (str (get-in actual [:gas-price :value-number]))))) + (testing "it does not mark gas as invalid" + (is (not (get-in actual [:gas :invalid?])))) + (testing "it sets the value in number for gasi" + (is (= "21000" (str (get-in actual [:gas :value-number]))))) + (testing "it calculates max-fee" + (is (= "0.00021" (:max-fee actual))))))) diff --git a/test/cljs/status_im/test/runner.cljs b/test/cljs/status_im/test/runner.cljs index 05321c4e01..1d03aab10d 100644 --- a/test/cljs/status_im/test/runner.cljs +++ b/test/cljs/status_im/test/runner.cljs @@ -15,6 +15,7 @@ [status-im.test.models.account] [status-im.test.models.contact] [status-im.test.models.network] + [status-im.test.models.wallet] [status-im.test.transport.core] [status-im.test.transport.inbox] [status-im.test.transport.handlers] @@ -65,6 +66,7 @@ 'status-im.test.models.account 'status-im.test.models.contact 'status-im.test.models.network + 'status-im.test.models.wallet 'status-im.test.bots.events 'status-im.test.wallet.subs 'status-im.test.wallet.transactions.subs