From 9068801e4ffe24d6dd884ca443687eab0e1b74f3 Mon Sep 17 00:00:00 2001 From: Brian Sztamfater Date: Wed, 18 Sep 2024 13:07:13 -0300 Subject: [PATCH] feat(swap): switch pay/receive assets (#21179) Signed-off-by: Brian Sztamfater --- .../contexts/wallet/swap/events.cljs | 97 ++++++++---- .../wallet/swap/set_spending_cap/view.cljs | 2 +- .../contexts/wallet/swap/setup_swap/view.cljs | 143 ++++++++++++------ .../wallet/swap/swap_confirmation/view.cljs | 9 +- src/status_im/subs/wallet/swap.cljs | 33 ++-- src/utils/money.cljs | 9 +- src/utils/number.cljs | 18 ++- 7 files changed, 213 insertions(+), 98 deletions(-) diff --git a/src/status_im/contexts/wallet/swap/events.cljs b/src/status_im/contexts/wallet/swap/events.cljs index 5b8e7a8594..4ecd1593d3 100644 --- a/src/status_im/contexts/wallet/swap/events.cljs +++ b/src/status_im/contexts/wallet/swap/events.cljs @@ -8,9 +8,8 @@ [taoensso.timbre :as log] [utils.address :as address] [utils.debounce :as debounce] - [utils.hex :as hex] [utils.i18n :as i18n] - [utils.number] + [utils.money :as money] [utils.number :as number])) (rf/reg-event-fx :wallet.swap/start @@ -47,14 +46,14 @@ (rf/reg-event-fx :wallet.swap/set-max-slippage (fn [{:keys [db]} [max-slippage]] - {:db (assoc-in db [:wallet :ui :swap :max-slippage] (utils.number/parse-float max-slippage))})) + {:db (assoc-in db [:wallet :ui :swap :max-slippage] (number/parse-float max-slippage))})) (rf/reg-event-fx :wallet.swap/select-asset-to-receive (fn [{:keys [db]} [{:keys [token]}]] {:db (assoc-in db [:wallet :ui :swap :asset-to-receive] token)})) (rf/reg-event-fx :wallet/start-get-swap-proposal - (fn [{:keys [db]} [{:keys [amount-in amount-out]}]] + (fn [{:keys [db]} [{:keys [amount-in amount-out clean-approval-transaction?]}]] (let [wallet-address (get-in db [:wallet :current-viewing-account-address]) {:keys [asset-to-pay asset-to-receive network]} (get-in db [:wallet :ui :swap]) @@ -97,12 +96,17 @@ (when-let [amount (or amount-in amount-out)] {:db (update-in db [:wallet :ui :swap] - #(-> % - (assoc - :last-request-uuid request-uuid - :amount amount - :loading-swap-proposal? true) - (dissoc :error-response))) + #(cond-> % + :always + (assoc + :last-request-uuid request-uuid + :amount amount + :amount-hex amount-in-hex + :loading-swap-proposal? true) + :always + (dissoc :error-response) + clean-approval-transaction? + (dissoc :approval-transaction-id :approved-amount :swap-proposal))) :json-rpc/call [{:method "wallet_getSuggestedRoutesAsync" :params params :on-error (fn [error] @@ -115,17 +119,30 @@ (rf/reg-event-fx :wallet/swap-proposal-success (fn [{:keys [db]} [swap-proposal]] (let [last-request-uuid (get-in db [:wallet :ui :swap :last-request-uuid]) + amount-hex (get-in db [:wallet :ui :swap :amount-hex]) view-id (:view-id db) request-uuid (:uuid swap-proposal) best-routes (:best swap-proposal) error-response (:error-response swap-proposal)] - (when (= request-uuid last-request-uuid) + (when (and (= request-uuid last-request-uuid) + (or (and (empty? best-routes) error-response) + (and + (pos? (count best-routes)) + (= (:amount-in (first best-routes)) amount-hex)))) (cond-> {:db (update-in db [:wallet :ui :swap] assoc - :swap-proposal (first best-routes) + :swap-proposal (when-not (empty? best-routes) + (assoc (first best-routes) :uuid request-uuid)) :error-response (when (empty? best-routes) error-response) :loading-swap-proposal? false)} + (empty? best-routes) + (assoc :fx + [[:dispatch + [:toasts/upsert + {:id :swap-proposal-error + :type :negative + :text error-response}]]]) ;; Router is unstable and it can return a swap proposal and after auto-refetching it can ;; return an error. Ideally this shouldn't happen, but adding this behavior so if the ;; user is in swap confirmation screen or in token approval confirmation screen, we @@ -156,16 +173,18 @@ {:event :wallet/stop-get-swap-proposal :error error}))}]})) -(rf/reg-event-fx :wallet/clean-swap-proposal - (fn [{:keys [db]}] - {:db (update-in db - [:wallet :ui :swap] - dissoc - :last-request-uuid - :swap-proposal - :error-response - :loading-swap-proposal? - :approval-transaction-id)})) +(rf/reg-event-fx + :wallet/clean-swap-proposal + (fn [{:keys [db]} [{:keys [clean-approval-transaction?]}]] + (let [keys-to-dissoc (cond-> [:amount + :amount-hex + :last-request-uuid + :swap-proposal + :error-response + :loading-swap-proposal?] + clean-approval-transaction? (conj :approval-transaction-id :approved-amount))] + {:db (apply update-in db [:wallet :ui :swap] dissoc keys-to-dissoc) + :fx [[:dispatch [:wallet/stop-get-swap-proposal]]]}))) (rf/reg-event-fx :wallet/clean-swap (fn [{:keys [db]}] @@ -245,10 +264,8 @@ constants/min-token-decimals-to-display) receive-amount (when amount-out (number/remove-trailing-zeroes - (.toFixed (number/convert-to-whole-number - (native-module/hex-to-number - (hex/normalize-hex - amount-out)) + (.toFixed (number/hex->whole + amount-out receive-token-decimals) decimals-to-display)))] (rf/dispatch [:wallet.swap/add-authorized-transaction @@ -268,7 +285,7 @@ :screen/wallet.swap-confirmation)]) (when-not approval-required? (rf/dispatch [:wallet/select-account-tab :activity]) - (debounce/debounce-and-dispatch [:wallet/clean-swap] 1000) + (rf/dispatch [:wallet/clean-swap]) (debounce/debounce-and-dispatch [:toasts/upsert {:id :swap-transaction-pending @@ -342,6 +359,8 @@ :token-symbol token-symbol :provider-name provider-name :account-name account-name}))}]]]} + transaction-confirmed? + (assoc :db (assoc-in db [:wallet :ui :swap :approved-amount] amount)) (not transaction-confirmed?) (assoc :db (update-in db [:wallet :ui :swap] dissoc :approval-transaction-id))))))) @@ -368,3 +387,27 @@ :receive-token-symbol receive-token-symbol :receive-amount receive-amount}) (i18n/label :t/swap-failed))}]]]})))) + +(rf/reg-event-fx :wallet.swap/flip-assets + (fn [{:keys [db]}] + (let [{:keys [asset-to-pay asset-to-receive + swap-proposal amount]} (get-in db [:wallet :ui :swap]) + receive-token-decimals (:decimals asset-to-receive) + amount-out (when swap-proposal (:amount-out swap-proposal)) + receive-amount (when amount-out + (-> amount-out + (number/hex->whole receive-token-decimals) + (money/to-fixed receive-token-decimals)))] + {:db (update-in db + [:wallet :ui :swap] + #(-> % + (assoc + :asset-to-pay asset-to-receive + :asset-to-receive asset-to-pay + :amount (or receive-amount amount)) + (dissoc :swap-proposal + :error-response + :loading-swap-proposal? + :last-request-uuid + :approved-amount + :approval-transaction-id)))}))) diff --git a/src/status_im/contexts/wallet/swap/set_spending_cap/view.cljs b/src/status_im/contexts/wallet/swap/set_spending_cap/view.cljs index 24abdc1262..d1d43f80c8 100644 --- a/src/status_im/contexts/wallet/swap/set_spending_cap/view.cljs +++ b/src/status_im/contexts/wallet/swap/set_spending_cap/view.cljs @@ -212,7 +212,7 @@ (defn- slide-button [] (let [loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?]) - swap-proposal (rf/sub [:wallet/swap-proposal]) + swap-proposal (rf/sub [:wallet/swap-proposal-without-fees]) account (rf/sub [:wallet/current-viewing-account]) on-auth-success (rn/use-callback #(rf/dispatch [:wallet/swap-transaction diff --git a/src/status_im/contexts/wallet/swap/setup_swap/view.cljs b/src/status_im/contexts/wallet/swap/setup_swap/view.cljs index 0b1b8d438a..2208ad371c 100644 --- a/src/status_im/contexts/wallet/swap/setup_swap/view.cljs +++ b/src/status_im/contexts/wallet/swap/setup_swap/view.cljs @@ -1,6 +1,5 @@ (ns status-im.contexts.wallet.swap.setup-swap.view (:require [clojure.string :as string] - [native-module.core :as native-module] [quo.core :as quo] [react-native.core :as rn] [react-native.platform :as platform] @@ -11,7 +10,7 @@ [status-im.contexts.wallet.common.account-switcher.view :as account-switcher] [status-im.contexts.wallet.common.utils :as utils] [status-im.contexts.wallet.swap.setup-swap.style :as style] - [utils.hex :as hex] + [utils.debounce :as debounce] [utils.i18n :as i18n] [utils.money :as money] [utils.number :as number] @@ -22,14 +21,18 @@ (defn- on-close [] - (rf/dispatch [:wallet/clean-swap-proposal]) + (rf/dispatch [:wallet/clean-swap-proposal {:clean-approval-transaction? true}]) (events-helper/navigate-back)) (defn- fetch-swap-proposal - [{:keys [amount valid-input?]}] + [{:keys [amount valid-input? clean-approval-transaction?]}] (if valid-input? - (rf/dispatch [:wallet/start-get-swap-proposal {:amount-in amount}]) - (rf/dispatch [:wallet/clean-swap-proposal]))) + (debounce/debounce-and-dispatch [:wallet/start-get-swap-proposal + {:amount-in amount + :clean-approval-transaction? clean-approval-transaction?}] + 100) + (rf/dispatch [:wallet/clean-swap-proposal + {:clean-approval-transaction? clean-approval-transaction?}]))) (defn- data-item [{:keys [title subtitle size subtitle-icon loading?]}] @@ -69,11 +72,13 @@ asset-to-pay (rf/sub [:wallet/swap-asset-to-pay]) currency (rf/sub [:profile/currency]) loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?]) - swap-proposal (rf/sub [:wallet/swap-proposal]) + swap-proposal (rf/sub [:wallet/swap-proposal-without-fees]) approval-required (rf/sub [:wallet/swap-proposal-approval-required]) approval-amount-required (rf/sub [:wallet/swap-proposal-approval-amount-required]) currency-symbol (rf/sub [:profile/currency-symbol]) approval-transaction-status (rf/sub [:wallet/swap-approval-transaction-status]) + approval-transaction-id (rf/sub [:wallet/swap-approval-transaction-id]) + approved-amount (rf/sub [:wallet/swap-approved-amount]) pay-input-num-value (controlled-input/value-numeric input-state) pay-input-amount (controlled-input/input-value input-state) pay-token-symbol (:symbol asset-to-pay) @@ -93,16 +98,16 @@ (min pay-token-decimals constants/min-token-decimals-to-display))) approval-amount-required-num (when approval-amount-required - (str (number/convert-to-whole-number - (native-module/hex-to-number - (hex/normalize-hex - approval-amount-required)) - pay-token-decimals))) - pay-input-error? (and (not (string/blank? pay-input-amount)) - (money/greater-than - (money/bignumber pay-input-num-value) - (money/bignumber - pay-token-balance-selected-chain))) + (str (number/hex->whole approval-amount-required + pay-token-decimals))) + pay-input-error? (or (and (not (string/blank? pay-input-amount)) + (money/greater-than + (money/bignumber pay-input-num-value) + (money/bignumber + pay-token-balance-selected-chain))) + (money/equal-to (money/bignumber + available-crypto-limit) + (money/bignumber 0))) valid-pay-input? (and (not (string/blank? pay-input-amount)) @@ -111,8 +116,9 @@ request-fetch-swap-proposal (rn/use-callback (fn [] (fetch-swap-proposal - {:amount pay-input-amount - :valid-input? valid-pay-input?})) + {:amount pay-input-amount + :valid-input? valid-pay-input? + :clean-approval-transaction? true})) [pay-input-amount])] (rn/use-effect (fn [] @@ -123,7 +129,8 @@ :error? pay-input-error? :token pay-token-symbol :customization-color :blue - :show-approval-label? (and swap-proposal approval-required) + :show-approval-label? (or (and swap-proposal approval-required) + approval-transaction-id) :auto-focus? true :show-keyboard? false :status (cond @@ -145,7 +152,7 @@ :confirmed :approved :finalised :approved :approve) - :token-value approval-amount-required-num + :token-value (or approval-amount-required-num approved-amount) :button-props {:on-press on-approve-press} :customization-color account-color :token-symbol pay-token-symbol}}])) @@ -169,14 +176,9 @@ receive-token-symbol (:symbol asset-to-receive) receive-token-decimals (:decimals asset-to-receive) amount-out-whole-number (when amount-out - (number/convert-to-whole-number - (native-module/hex-to-number - (utils.hex/normalize-hex - amount-out)) - receive-token-decimals)) + (number/hex->whole amount-out receive-token-decimals)) amount-out-num (if amount-out-whole-number - (number/remove-trailing-zeroes - (.toFixed amount-out-whole-number receive-token-decimals)) + (number/to-fixed amount-out-whole-number receive-token-decimals) default-text-for-unfocused-input) receive-token-fiat-value (str (utils/calculate-token-fiat-value {:currency currency @@ -205,7 +207,7 @@ (defn- action-button [{:keys [on-press]}] (let [account-color (rf/sub [:wallet/current-viewing-account-color]) - swap-proposal (rf/sub [:wallet/swap-proposal]) + swap-proposal (rf/sub [:wallet/swap-proposal-without-fees]) loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?]) approval-required? (rf/sub [:wallet/swap-proposal-approval-required]) approval-transaction-status (rf/sub [:wallet/swap-approval-transaction-status])] @@ -223,10 +225,10 @@ [] (let [[pay-input-state set-pay-input-state] (rn/use-state controlled-input/init-state) [pay-input-focused? set-pay-input-focused?] (rn/use-state true) - [refetch-interval set-refetch-interval] (rn/use-state nil) + refetch-interval (rn/use-ref-atom nil) error-response (rf/sub [:wallet/swap-error-response]) loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?]) - swap-proposal (rf/sub [:wallet/swap-proposal]) + swap-proposal (rf/sub [:wallet/swap-proposal-without-fees]) asset-to-pay (rf/sub [:wallet/swap-asset-to-pay]) network (rf/sub [:wallet/swap-network]) pay-input-amount (controlled-input/input-value pay-input-state) @@ -270,7 +272,9 @@ (fn [] (set-pay-input-state controlled-input/delete-last) - (rf/dispatch [:wallet/clean-swap-proposal]))) + (rf/dispatch [:wallet/clean-swap-proposal + {:clean-approval-transaction? + true}]))) on-max-press (rn/use-callback (fn [max-value] (set-pay-input-state @@ -280,32 +284,66 @@ max-value))))) on-refresh-swap-proposal (rn/use-callback (fn [] - (let [bottom-sheets (rf/sub - [:bottom-sheet-sheets])] + (let + [bottom-sheets (rf/sub + [:bottom-sheet-sheets]) + approval-transaction-status + (rf/sub + [:wallet/swap-approval-transaction-status])] (when-not valid-pay-input? - (js/clearInterval refetch-interval) - (set-refetch-interval nil)) + (when @refetch-interval + (js/clearTimeout @refetch-interval)) + (reset! refetch-interval nil)) + (when @refetch-interval + (js/clearTimeout @refetch-interval)) + (reset! refetch-interval nil) (when (and valid-pay-input? (not loading-swap-proposal?) - (not bottom-sheets)) + (not bottom-sheets) + (not= approval-transaction-status + :pending)) (fetch-swap-proposal - {:amount pay-input-amount - :valid-input? valid-pay-input?})))) + {:amount pay-input-amount + :valid-input? valid-pay-input? + :clean-approval-transaction? false})))) [valid-pay-input? loading-swap-proposal? - pay-input-amount])] + pay-input-amount]) + on-asset-to-pay-change (fn [] + (when valid-pay-input? + (fetch-swap-proposal + {:amount pay-input-amount + :valid-input? valid-pay-input? + :clean-approval-transaction? true})))] (rn/use-effect (fn [] - (when refetch-interval - (js/clearInterval refetch-interval) - (set-refetch-interval nil)) + (when @refetch-interval + (js/clearInterval @refetch-interval) + (reset! refetch-interval nil)) (when (or swap-proposal error-response) - (set-refetch-interval (js/setInterval - on-refresh-swap-proposal - constants/swap-proposal-refresh-interval-ms)))) + (reset! refetch-interval + (js/setInterval + on-refresh-swap-proposal + constants/swap-proposal-refresh-interval-ms)))) [swap-proposal error-response]) (rn/use-unmount (fn [] - (when refetch-interval - (js/clearInterval refetch-interval) - (set-refetch-interval nil)))) + (rf/dispatch [:wallet/clean-swap-proposal {:clean-approval-transaction? true}]) + (when @refetch-interval + (js/clearInterval @refetch-interval) + (reset! refetch-interval nil)))) + (rn/use-effect + (fn [] + (when asset-to-pay + (let [swap-amount (rf/sub [:wallet/swap-amount])] + (when (and swap-amount refetch-interval) + (js/clearTimeout @refetch-interval) + (reset! refetch-interval nil)) + (if (and swap-amount (not= swap-amount pay-input-amount)) + (set-pay-input-state + (fn [input-state] + (controlled-input/set-input-value + input-state + swap-amount))) + (on-asset-to-pay-change))))) + [asset-to-pay]) [rn/view {:style style/container} [account-switcher/view {:on-press on-close @@ -322,7 +360,12 @@ :on-input-focus (fn [] (when platform/android? (rf/dispatch [:dismiss-keyboard])) (set-pay-input-focused? true))}] - [swap-order-button {:on-press #(js/alert "Swap Order Pressed")}] + [swap-order-button + {:on-press (fn [] + (when @refetch-interval + (js/clearTimeout @refetch-interval) + (reset! refetch-interval nil)) + (rf/dispatch [:wallet.swap/flip-assets]))}] [receive-token-input {:input-focused? (not pay-input-focused?) :on-token-press #(js/alert "Token Pressed") diff --git a/src/status_im/contexts/wallet/swap/swap_confirmation/view.cljs b/src/status_im/contexts/wallet/swap/swap_confirmation/view.cljs index a1cad55e5a..c2c54d5043 100644 --- a/src/status_im/contexts/wallet/swap/swap_confirmation/view.cljs +++ b/src/status_im/contexts/wallet/swap/swap_confirmation/view.cljs @@ -157,12 +157,13 @@ (defn- slide-button [] (let [loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?]) - swap-proposal (rf/sub [:wallet/swap-proposal]) + swap-proposal (rf/sub [:wallet/swap-proposal-without-fees]) account (rf/sub [:wallet/current-viewing-account]) account-color (:color account) - on-auth-success (rn/use-callback #(rf/dispatch - [:wallet/swap-transaction - (security/safe-unmask-data %)]))] + on-auth-success (rn/use-callback (fn [data] + (rf/dispatch [:wallet/stop-get-swap-proposal]) + (rf/dispatch [:wallet/swap-transaction + (security/safe-unmask-data data)])))] [standard-auth/slide-button {:size :size-48 :track-text (i18n/label :t/slide-to-swap) diff --git a/src/status_im/subs/wallet/swap.cljs b/src/status_im/subs/wallet/swap.cljs index 7264aeca55..27a538d08f 100644 --- a/src/status_im/subs/wallet/swap.cljs +++ b/src/status_im/subs/wallet/swap.cljs @@ -1,11 +1,9 @@ (ns status-im.subs.wallet.swap (:require [clojure.string :as string] - [native-module.core :as native-module] [re-frame.core :as rf] [status-im.constants :as constants] [status-im.contexts.wallet.common.utils :as utils] [status-im.contexts.wallet.send.utils :as send-utils] - [utils.hex :as hex] [utils.money :as money] [utils.number :as number])) @@ -95,6 +93,25 @@ :<- [:wallet/swap] :-> :swap-proposal) +(rf/reg-sub + :wallet/swap-proposal-without-fees + :<- [:wallet/swap] + (fn [swap] + (let [swap-proposal (:swap-proposal swap)] + (reduce dissoc + swap-proposal + [:gas-fees :gas-amount :token-fees :bonder-fees :approval-gas-fees])))) + +(rf/reg-sub + :wallet/swap-amount + :<- [:wallet/swap] + :-> :amount) + +(rf/reg-sub + :wallet/swap-approved-amount + :<- [:wallet/swap] + :-> :approved-amount) + (rf/reg-sub :wallet/swap-loading-swap-proposal? :<- [:wallet/swap] @@ -117,11 +134,7 @@ (fn [[amount-out asset-to-receive]] (let [receive-token-decimals (:decimals asset-to-receive) receive-amount (when amount-out - (number/convert-to-whole-number - (native-module/hex-to-number - (hex/normalize-hex - amount-out)) - receive-token-decimals)) + (number/hex->whole amount-out receive-token-decimals)) receive-amount (when amount-out (number/remove-trailing-zeroes (.toFixed receive-amount @@ -136,11 +149,7 @@ (fn [[amount-in asset-to-pay]] (let [pay-token-decimals (:decimals asset-to-pay) pay-amount (when amount-in - (number/convert-to-whole-number - (native-module/hex-to-number - (hex/normalize-hex - amount-in)) - pay-token-decimals)) + (number/hex->whole amount-in pay-token-decimals)) pay-amount (when amount-in (number/remove-trailing-zeroes (.toFixed pay-amount diff --git a/src/utils/money.cljs b/src/utils/money.cljs index c035d83b84..1d036e070d 100644 --- a/src/utils/money.cljs +++ b/src/utils/money.cljs @@ -44,15 +44,18 @@ (defn greater-than-or-equals [^js bn1 ^js bn2] - (.greaterThanOrEqualTo bn1 bn2)) + (when (bignumber? bn1) + (.greaterThanOrEqualTo bn1 bn2))) (defn greater-than [bn1 bn2] - (.greaterThan ^js bn1 bn2)) + (when (bignumber? bn1) + (.greaterThan ^js bn1 bn2))) (defn less-than [bn1 bn2] - (.lessThan ^js bn1 bn2)) + (when (bignumber? bn1) + (.lessThan ^js bn1 bn2))) (defn equal-to [n1 n2] diff --git a/src/utils/number.cljs b/src/utils/number.cljs index a8e7b29952..8e2c7496f2 100644 --- a/src/utils/number.cljs +++ b/src/utils/number.cljs @@ -1,5 +1,8 @@ (ns utils.number - (:require [clojure.string :as string])) + (:require [clojure.string :as string] + [native-module.core :as native-module] + [utils.hex :as utils.hex] + [utils.money :as utils.money])) (defn naive-round "Quickly and naively round number `n` up to `decimal-places`. @@ -61,3 +64,16 @@ (str "." (string/replace decimals #"0+$" "")) "") "")))) + +(defn hex->whole + [num decimals] + (-> num + utils.hex/normalize-hex + native-module/hex-to-number + (convert-to-whole-number decimals))) + +(defn to-fixed + [num decimals] + (-> num + (utils.money/to-fixed decimals) + remove-trailing-zeroes))