feat(swap): switch pay/receive assets (#21179)

Signed-off-by: Brian Sztamfater <brian@status.im>
This commit is contained in:
Brian Sztamfater 2024-09-18 13:07:13 -03:00 committed by GitHub
parent 2f3d3fc7f9
commit 9068801e4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 213 additions and 98 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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