feat(swap): fetch swap proposals (#21040)

Signed-off-by: Brian Sztamfater <brian@status.im>
This commit is contained in:
Brian Sztamfater 2024-09-04 13:40:06 -03:00 committed by GitHub
parent 40f98f8238
commit c861d95d69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 657 additions and 170 deletions

View File

@ -7,6 +7,7 @@
[:map {:closed true} [:map {:closed true}
[:action? {:optional true} [:maybe boolean?]] [:action? {:optional true} [:maybe boolean?]]
[:text {:optional true} [:maybe string?]] [:text {:optional true} [:maybe string?]]
[:container-style {:optional true} [:maybe :map]]
[:button-text {:optional true} [:maybe string?]] [:button-text {:optional true} [:maybe string?]]
[:on-button-press {:optional true} [:maybe fn?]]]]] [:on-button-press {:optional true} [:maybe fn?]]]]]
:any]) :any])

View File

@ -1,12 +1,15 @@
(ns quo.components.banners.alert-banner.style (ns quo.components.banners.alert-banner.style
(:require [quo.foundations.colors :as colors])) (:require [quo.foundations.colors :as colors]))
(def container (defn container
{:flex-direction :row [container-style]
:align-items :center (merge
:height 50 {:flex-direction :row
:padding-horizontal 20 :align-items :center
:padding-vertical 12}) :height 50
:padding-horizontal 20
:padding-vertical 12}
container-style))
(defn label (defn label
[theme] [theme]

View File

@ -11,12 +11,12 @@
[schema.core :as schema])) [schema.core :as schema]))
(defn- view-internal (defn- view-internal
[{:keys [action? text button-text on-button-press]}] [{:keys [action? text button-text container-style on-button-press]}]
(let [theme (quo.theme/use-theme)] (let [theme (quo.theme/use-theme)]
[rn/view [rn/view
{:accessibility-label :alert-banner} {:accessibility-label :alert-banner}
[linear-gradient/linear-gradient [linear-gradient/linear-gradient
{:style style/container {:style (style/container container-style)
:start {:x 0 :y 0} :start {:x 0 :y 0}
:end {:x 0 :y 1} :end {:x 0 :y 1}
:colors [(colors/theme-colors :colors [(colors/theme-colors

View File

@ -126,7 +126,7 @@
[:subtitle-type {:optional true} [:maybe [:enum :default :icon :network :account :editable]]] [:subtitle-type {:optional true} [:maybe [:enum :default :icon :network :account :editable]]]
[:size {:optional true} [:maybe [:enum :default :small :large]]] [:size {:optional true} [:maybe [:enum :default :small :large]]]
[:title :string] [:title :string]
[:subtitle {:optional true} [:maybe :string]] [:subtitle {:optional true} [:maybe [:or :string :double]]]
[:custom-subtitle {:optional true} [:maybe fn?]] [:custom-subtitle {:optional true} [:maybe fn?]]
[:icon {:optional true} [:maybe :keyword]] [:icon {:optional true} [:maybe :keyword]]
[:emoji {:optional true} [:maybe :string]] [:emoji {:optional true} [:maybe :string]]

View File

@ -1,5 +1,6 @@
(ns quo.components.wallet.swap-input.style (ns quo.components.wallet.swap-input.style
(:require [quo.foundations.colors :as colors] (:require [quo.foundations.colors :as colors]
[quo.foundations.shadows :as shadows]
[quo.foundations.typography :as typography])) [quo.foundations.typography :as typography]))
(defn- border-color (defn- border-color
@ -11,11 +12,13 @@
(colors/theme-colors colors/neutral-5 colors/neutral-90 theme)) (colors/theme-colors colors/neutral-5 colors/neutral-90 theme))
(defn content (defn content
[theme] [typing? theme]
{:border-width 1 (merge
:border-radius 16 {:border-width 1
:border-color (border-color theme) :border-radius 16
:background-color (colors/theme-colors colors/white colors/neutral-95 theme)}) :border-color (border-color theme)
:background-color (colors/theme-colors colors/white colors/neutral-95 theme)}
(when typing? (shadows/get 1 theme))))
(defn row-1 (defn row-1
[loading?] [loading?]

View File

@ -19,13 +19,15 @@
[:props [:props
[:map {:closed true} [:map {:closed true}
[:type {:optional true} [:maybe [:enum :pay :receive]]] [:type {:optional true} [:maybe [:enum :pay :receive]]]
[:status {:optional true} [:maybe [:enum :default :disabled :loading]]] [:status {:optional true} [:maybe [:enum :default :typing :disabled :loading]]]
[:token {:optional true} [:maybe :string]] [:token {:optional true} [:maybe :string]]
[:value {:optional true} [:maybe :string]] [:value {:optional true} [:maybe :string]]
[:default-value {:optional true} [:maybe :string]] [:default-value {:optional true} [:maybe :string]]
[:currency-symbol {:optional true} [:maybe :string]] [:currency-symbol {:optional true} [:maybe :string]]
[:fiat-value {:optional true} [:maybe :string]] [:fiat-value {:optional true} [:maybe :string]]
[:show-approval-label? {:optional true} [:maybe :boolean]] [:show-approval-label? {:optional true} [:maybe :boolean]]
[:auto-focus? {:optional true} [:maybe :boolean]]
[:input-disabled? {:optional true} [:maybe :boolean]]
[:error? {:optional true} [:maybe :boolean]] [:error? {:optional true} [:maybe :boolean]]
[:show-keyboard? {:optional true} [:maybe :boolean]] [:show-keyboard? {:optional true} [:maybe :boolean]]
[:approval-label-props {:optional true} [:maybe approval-label.schema/?schema]] [:approval-label-props {:optional true} [:maybe approval-label.schema/?schema]]
@ -33,6 +35,7 @@
[:on-change-text {:optional true} [:maybe fn?]] [:on-change-text {:optional true} [:maybe fn?]]
[:enable-swap? {:optional true} [:maybe :boolean]] [:enable-swap? {:optional true} [:maybe :boolean]]
[:on-swap-press {:optional true} [:maybe fn?]] [:on-swap-press {:optional true} [:maybe fn?]]
[:on-input-focus {:optional true} [:maybe fn?]]
[:on-token-press {:optional true} [:maybe fn?]] [:on-token-press {:optional true} [:maybe fn?]]
[:on-max-press {:optional true} [:maybe fn?]] [:on-max-press {:optional true} [:maybe fn?]]
[:customization-color {:optional true} [:maybe :schema.common/customization-color]] [:customization-color {:optional true} [:maybe :schema.common/customization-color]]
@ -41,13 +44,14 @@
(defn view-internal (defn view-internal
[{:keys [type status token value fiat-value show-approval-label? error? network-tag-props [{:keys [type status token value fiat-value show-approval-label? error? network-tag-props
approval-label-props default-value enable-swap? approval-label-props default-value auto-focus? input-disabled? enable-swap?
currency-symbol on-change-text show-keyboard? currency-symbol on-change-text show-keyboard?
container-style on-swap-press on-token-press on-max-press]}] container-style on-swap-press on-token-press on-max-press on-input-focus]}]
(let [theme (quo.theme/use-theme) (let [theme (quo.theme/use-theme)
pay? (= type :pay) pay? (= type :pay)
disabled? (= status :disabled) disabled? (= status :disabled)
loading? (= status :loading) loading? (= status :loading)
typing? (= status :typing)
controlled-input? (some? value) controlled-input? (some? value)
input-ref (rn/use-ref-atom nil) input-ref (rn/use-ref-atom nil)
set-input-ref (rn/use-callback (fn [ref] (reset! input-ref ref)) []) set-input-ref (rn/use-callback (fn [ref] (reset! input-ref ref)) [])
@ -58,7 +62,7 @@
[rn/view [rn/view
{:style container-style {:style container-style
:accessibility-label :swap-input} :accessibility-label :swap-input}
[rn/view {:style (style/content theme)} [rn/view {:style (style/content typing? theme)}
[rn/view [rn/view
{:style (style/row-1 loading?)} {:style (style/row-1 loading?)}
[rn/pressable {:on-press on-token-press} [rn/pressable {:on-press on-token-press}
@ -78,7 +82,9 @@
colors/neutral-50 colors/neutral-50
theme) theme)
:keyboard-type :numeric :keyboard-type :numeric
:auto-focus true :editable (not input-disabled?)
:auto-focus auto-focus?
:on-focus on-input-focus
:on-change-text on-change-text :on-change-text on-change-text
:show-soft-input-on-focus show-keyboard? :show-soft-input-on-focus show-keyboard?
:default-value default-value :default-value default-value

View File

@ -551,6 +551,7 @@
(def ^:const send-type-bridge 5) (def ^:const send-type-bridge 5)
(def ^:const send-type-erc-721-transfer 6) (def ^:const send-type-erc-721-transfer 6)
(def ^:const send-type-erc-1155-transfer 7) (def ^:const send-type-erc-1155-transfer 7)
(def ^:const send-type-swap 8)
(def ^:const multi-transaction-type-send 0) (def ^:const multi-transaction-type-send 0)
(def ^:const multi-transaction-type-approve 1) (def ^:const multi-transaction-type-approve 1)
@ -596,3 +597,5 @@
:color :blue :color :blue
:contract-address "0xdef171fe48cf0115b1d80b88dc8eab59176fee57" :contract-address "0xdef171fe48cf0115b1d80b88dc8eab59176fee57"
:terms-and-conditions-url "https://files.paraswap.io/tos_v4.pdf"}) :terms-and-conditions-url "https://files.paraswap.io/tos_v4.pdf"})
(def ^:const token-for-fees-symbol "ETH")

View File

@ -518,23 +518,29 @@
(rf/reg-event-fx (rf/reg-event-fx
:wallet/handle-suggested-routes :wallet/handle-suggested-routes
(fn [_ [data]] (fn [{:keys [db]} [data]]
(if-let [{:keys [code details]} (-> data :ErrorResponse)] (let [swap? (get-in db [:wallet :ui :swap])
(let [error-message (if (= code "0") "An error occurred" details)] {:keys [code details] :as error-response} (-> data :ErrorResponse)]
(log/error "failed to get suggested routes (async)" (if (and (not swap?) error-response)
{:event :wallet/handle-suggested-routes (let [error-message (if (= code "0") "An error occurred" details)]
:error error-message}) (log/error "failed to get suggested routes (async)"
{:fx [[:dispatch [:wallet/suggested-routes-error error-message]]]}) {:event :wallet/handle-suggested-routes
(let [best-routes-fix (comp ->old-route-paths :error error-message})
remove-invalid-bonder-fees-routes {:fx [(if swap?
remove-multichain-routes) [:dispatch [:wallet/swap-proposal-error error-message]]
candidates-fix (comp ->old-route-paths [:dispatch [:wallet/suggested-routes-error error-message]])]})
remove-invalid-bonder-fees-routes) (let [best-routes-fix (comp ->old-route-paths
routes (-> data remove-invalid-bonder-fees-routes
(data-store/rpc->suggested-routes) remove-multichain-routes)
(update :best best-routes-fix) candidates-fix (comp ->old-route-paths
(update :candidates candidates-fix))] remove-invalid-bonder-fees-routes)
{:fx [[:dispatch [:wallet/suggested-routes-success routes]]]})))) routes (-> data
(data-store/rpc->suggested-routes)
(update :best best-routes-fix)
(update :candidates candidates-fix))]
{:fx [(if swap?
[:dispatch [:wallet/swap-proposal-success routes]]
[:dispatch [:wallet/suggested-routes-success routes]])]})))))
(rf/reg-event-fx :wallet/add-authorized-transaction (rf/reg-event-fx :wallet/add-authorized-transaction
(fn [{:keys [db]} [transaction]] (fn [{:keys [db]} [transaction]]

View File

@ -1,7 +1,9 @@
(ns status-im.contexts.wallet.swap.events (ns status-im.contexts.wallet.swap.events
(:require [re-frame.core :as rf] (:require [re-frame.core :as rf]
[status-im.constants :as constants] [status-im.constants :as constants]
[status-im.contexts.wallet.send.utils :as send-utils]
[status-im.contexts.wallet.sheets.network-selection.view :as network-selection] [status-im.contexts.wallet.sheets.network-selection.view :as network-selection]
[taoensso.timbre :as log]
[utils.number])) [utils.number]))
(rf/reg-event-fx :wallet.swap/start (rf/reg-event-fx :wallet.swap/start
@ -14,7 +16,9 @@
(assoc-in [:wallet :ui :swap :asset-to-pay] token) (assoc-in [:wallet :ui :swap :asset-to-pay] token)
(assoc-in [:wallet :ui :swap :network] network)) (assoc-in [:wallet :ui :swap :network] network))
:fx (if network :fx (if network
[[:dispatch [:navigate-to :screen/wallet.swap-propasal]] [[:dispatch
[:navigate-to-within-stack
[:screen/wallet.setup-swap :screen/wallet.swap-select-asset-to-pay]]]
[:dispatch [:wallet.swap/set-default-slippage]]] [:dispatch [:wallet.swap/set-default-slippage]]]
[[:dispatch [[:dispatch
[:show-bottom-sheet [:show-bottom-sheet
@ -30,10 +34,6 @@
:stack-id :stack-id
:screen/wallet.swap-select-asset-to-pay}]))}])}]]])})) :screen/wallet.swap-select-asset-to-pay}]))}])}]]])}))
(rf/reg-event-fx :wallet.swap/clean-asset-to-pay
(fn [{:keys [db]}]
{:db (update-in db [:wallet :ui :swap] dissoc :asset-to-pay)}))
(rf/reg-event-fx :wallet.swap/set-default-slippage (rf/reg-event-fx :wallet.swap/set-default-slippage
(fn [{:keys [db]}] (fn [{:keys [db]}]
{:db {:db
@ -47,18 +47,113 @@
(fn [{:keys [db]} [{:keys [token]}]] (fn [{:keys [db]} [{:keys [token]}]]
{:db (assoc-in db [:wallet :ui :swap :asset-to-receive] token)})) {:db (assoc-in db [:wallet :ui :swap :asset-to-receive] token)}))
(rf/reg-event-fx :wallet.swap/set-pay-amount
(fn [{:keys [db]} [amount]]
{:db (assoc-in db [:wallet :ui :swap :pay-amount] amount)}))
(rf/reg-event-fx :wallet.swap/set-swap-proposal
(fn [{:keys [db]} [swap-proposal]]
{:db (assoc-in db [:wallet :ui :swap :swap-proposal] swap-proposal)}))
(rf/reg-event-fx :wallet.swap/set-provider
(fn [{:keys [db]}]
{:db (assoc-in db [:wallet :ui :swap :providers] [constants/swap-default-provider])}))
(rf/reg-event-fx :wallet.swap/recalculate-fees (rf/reg-event-fx :wallet.swap/recalculate-fees
(fn [{:keys [db]} [loading-fees?]] (fn [{:keys [db]} [loading-fees?]]
{:db (assoc-in db [:wallet :ui :swap :loading-fees?] loading-fees?)})) {:db (assoc-in db [:wallet :ui :swap :loading-fees?] loading-fees?)}))
(rf/reg-event-fx :wallet/start-get-swap-proposal
(fn [{:keys [db]} [{:keys [amount-in amount-out]}]]
(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])
test-networks-enabled? (get-in db [:profile/profile :test-networks-enabled?])
networks ((if test-networks-enabled? :test :prod)
(get-in db [:wallet :networks]))
network-chain-ids (map :chain-id networks)
pay-token-decimal (:decimals asset-to-pay)
pay-token-id (:symbol asset-to-pay)
receive-token-id (:symbol asset-to-receive)
receive-token-decimals (:decimals asset-to-receive)
gas-rates constants/gas-rate-medium
amount-in-hex (if amount-in
(send-utils/amount-in-hex amount-in pay-token-decimal)
0)
amount-out-hex (when amount-out
(send-utils/amount-in-hex amount-out receive-token-decimals))
to-address wallet-address
from-address wallet-address
swap-chain-id (:chain-id network)
disabled-to-chain-ids (filter #(not= % swap-chain-id) network-chain-ids)
disabled-from-chain-ids (filter #(not= % swap-chain-id) network-chain-ids)
from-locked-amount {}
send-type constants/send-type-swap
request-uuid (str (random-uuid))
params [(cond->
{:uuid request-uuid
:sendType send-type
:addrFrom from-address
:addrTo to-address
:tokenID pay-token-id
:toTokenID receive-token-id
:disabledFromChainIDs disabled-from-chain-ids
:disabledToChainIDs disabled-to-chain-ids
:gasFeeMode gas-rates
:fromLockedAmount from-locked-amount}
amount-in (assoc :amountIn amount-in-hex)
amount-out (assoc :amountOut amount-out-hex))]]
(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)))
:json-rpc/call [{:method "wallet_getSuggestedRoutesV2Async"
:params params
:on-error (fn [error]
(rf/dispatch [:wallet/swap-proposal-error error])
(log/error "failed to get suggested routes (async)"
{:event :wallet/start-get-swap-proposal
:error (:message error)
:params params}))}]}))))
(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])
request-uuid (:uuid swap-proposal)
best-routes (:best swap-proposal)
error-response (:error-response swap-proposal)]
(when (= request-uuid last-request-uuid)
{:db (update-in db
[:wallet :ui :swap]
assoc
:swap-proposal (first best-routes)
:error-response (when (empty? best-routes) error-response)
:loading-swap-proposal? false)}))))
(rf/reg-event-fx :wallet/swap-proposal-error
(fn [{:keys [db]} [error-message]]
{:db (-> db
(update-in [:wallet :ui :swap] dissoc :route :swap-proposal)
(assoc-in [:wallet :ui :swap :loading-swap-proposal?] false)
(assoc-in [:wallet :ui :swap :error-response] error-message))
:fx [[:dispatch
[:toasts/upsert
{:id :swap-proposal-error
:type :negative
:text error-message}]]]}))
(rf/reg-event-fx :wallet/stop-get-swap-proposal
(fn []
{:json-rpc/call [{:method "wallet_stopSuggestedRoutesV2AsyncCalcualtion"
:params []
:on-error (fn [error]
(log/error "failed to stop fetching swap proposals"
{: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?)}))
(rf/reg-event-fx :wallet/clean-swap
(fn [{:keys [db]}]
{:db (update-in db [:wallet :ui] dissoc :swap)}))

View File

@ -18,22 +18,6 @@
:value search-text :value search-text
:on-change-text on-change-text}]]) :on-change-text on-change-text}]])
(def dummy-swap-proposal
{:from {:chain-id 1
:native-currency-symbol "ETH"}
:to {:chain-id 1
:native-currency-symbol "ETH"}
:gas-amount "23487"
:gas-fees {:base-fee "32.325296406"
:max-priority-fee-per-gas "0.011000001"
:eip1559-enabled true}
:estimated-time 3
:receive-amount "99.98"
:pay-token {:symbol "SNT"
:address "0x432492384728934239789"}
:receive-token {:symbol "USDT"
:address "0x432492384728934239789"}})
(defn- assets-view (defn- assets-view
[search-text on-change-text] [search-text on-change-text]
(let [on-token-press (fn [token] (let [on-token-press (fn [token]
@ -44,10 +28,8 @@
:network (when (= (count token-networks) 1) :network (when (= (count token-networks) 1)
(first token-networks)) (first token-networks))
:stack-id :screen/wallet.swap-select-asset-to-pay}]) :stack-id :screen/wallet.swap-select-asset-to-pay}])
(rf/dispatch [:wallet.swap/select-asset-to-receive {:token asset-to-receive}]) (rf/dispatch [:wallet.swap/select-asset-to-receive
(rf/dispatch [:wallet.swap/set-pay-amount 100]) {:token asset-to-receive}])))]
(rf/dispatch [:wallet.swap/set-swap-proposal dummy-swap-proposal])
(rf/dispatch [:wallet.swap/set-provider])))]
[:<> [:<>
[search-input search-text on-change-text] [search-input search-text on-change-text]
[asset-list/view [asset-list/view
@ -59,7 +41,7 @@
(let [[search-text set-search-text] (rn/use-state "") (let [[search-text set-search-text] (rn/use-state "")
on-change-text #(set-search-text %) on-change-text #(set-search-text %)
on-close (fn [] on-close (fn []
(rf/dispatch [:wallet.swap/clean-asset-to-pay]) (rf/dispatch [:wallet/clean-swap])
(rf/dispatch [:navigate-back]))] (rf/dispatch [:navigate-back]))]
[rn/safe-area-view {:style style/container} [rn/safe-area-view {:style style/container}
[account-switcher/view [account-switcher/view

View File

@ -22,14 +22,19 @@
:height 36 :height 36
:background-color :transparent}) :background-color :transparent})
(def swap-order-button (defn swap-order-button
{:margin-top -9 [approval-required?]
{:margin-top (if approval-required? 3 -9)
:z-index 2 :z-index 2
:align-self :center}) :align-self :center})
(def receive-token-swap-input-container (defn receive-token-swap-input-container
{:margin-top -9}) [approval-required?]
{:margin-top (if approval-required? 3 -9)})
(def footer-container (def footer-container
{:flex 1 {:flex 1
:justify-content :flex-end}) :justify-content :flex-end})
(def alert-banner
{:height 40})

View File

@ -1,13 +1,35 @@
(ns status-im.contexts.wallet.swap.setup-swap.view (ns status-im.contexts.wallet.swap.setup-swap.view
(:require [quo.core :as quo] (:require [clojure.string :as string]
[native-module.core :as native-module]
[quo.core :as quo]
[react-native.core :as rn] [react-native.core :as rn]
[react-native.safe-area :as safe-area] [react-native.safe-area :as safe-area]
[status-im.common.controlled-input.utils :as controlled-input]
[status-im.common.events-helper :as events-helper] [status-im.common.events-helper :as events-helper]
[status-im.constants :as constants]
[status-im.contexts.wallet.common.account-switcher.view :as account-switcher] [status-im.contexts.wallet.common.account-switcher.view :as account-switcher]
[status-im.contexts.wallet.common.utils :as utils] [status-im.contexts.wallet.common.utils :as utils]
[status-im.contexts.wallet.swap.setup-swap.style :as style] [status-im.contexts.wallet.swap.setup-swap.style :as style]
[utils.hex :as hex]
[utils.i18n :as i18n] [utils.i18n :as i18n]
[utils.re-frame :as rf])) [utils.money :as money]
[utils.number :as number]
[utils.re-frame :as rf]
[utils.string :as utils.string]))
(def ^:private min-token-decimals-to-display 6)
(def ^:private default-text-for-unfocused-input "0.00")
(defn- on-close
[]
(rf/dispatch [:wallet/clean-swap-proposal])
(events-helper/navigate-back))
(defn- fetch-swap-proposal
[{:keys [amount valid-input?]}]
(if valid-input?
(rf/dispatch [:wallet/start-get-swap-proposal {:amount-in amount}])
(rf/dispatch [:wallet/clean-swap-proposal])))
(defn- data-item (defn- data-item
[{:keys [title subtitle size subtitle-icon loading?]}] [{:keys [title subtitle size subtitle-icon loading?]}]
@ -23,110 +45,243 @@
:icon subtitle-icon}]) :icon subtitle-icon}])
(defn- transaction-details (defn- transaction-details
[{:keys [max-slippage native-currency-symbol loading-fees?]}] []
(let [max-fees (rf/sub [:wallet/wallet-send-fee-fiat-formatted native-currency-symbol])] (let [max-fees (rf/sub [:wallet/wallet-swap-proposal-fee-fiat-formatted
constants/token-for-fees-symbol])
max-slippage (rf/sub [:wallet/swap-max-slippage])
loading-fees? (rf/sub [:wallet/swap-loading-fees?])
loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?])
loading? (or loading-fees? loading-swap-proposal?)]
[rn/view {:style style/details-container} [rn/view {:style style/details-container}
[data-item [data-item
{:title (i18n/label :t/max-fees) {:title (i18n/label :t/max-fees)
:subtitle max-fees :subtitle max-fees
:loading? loading-fees? :loading? loading?
:size :small}] :size :small}]
[data-item [data-item
{:title (i18n/label :t/max-slippage) {:title (i18n/label :t/max-slippage)
:subtitle max-slippage :subtitle max-slippage
:subtitle-icon :i/edit :subtitle-icon :i/edit
:loading? loading-fees?}]])) :size :small
:loading? loading?}]]))
(defn- pay-token-input
[{:keys [input-state on-max-press on-input-focus on-token-press on-approve-press input-focused?]}]
(let [account-color (rf/sub [:wallet/current-viewing-account-color])
network (rf/sub [:wallet/swap-network])
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])
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])
pay-input-num-value (controlled-input/numeric-value input-state)
pay-input-amount (controlled-input/input-value input-state)
pay-token-symbol (:symbol asset-to-pay)
pay-token-decimals (:decimals asset-to-pay)
pay-token-balance-selected-chain (get-in asset-to-pay
[:balances-per-chain
(:chain-id network) :balance]
0)
pay-token-fiat-value (str
(utils/calculate-token-fiat-value
{:currency currency
:balance pay-input-num-value
:token asset-to-pay}))
available-crypto-limit (number/remove-trailing-zeroes
(.toFixed (money/bignumber
pay-token-balance-selected-chain)
(min pay-token-decimals
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)))
valid-pay-input? (and
(not (string/blank?
pay-input-amount))
(> pay-input-amount 0)
(not pay-input-error?))
request-fetch-swap-proposal (rn/use-callback
(fn []
(fetch-swap-proposal
{:amount pay-input-amount
:valid-input? valid-pay-input?}))
[pay-input-amount])]
(rn/use-effect
(fn []
(request-fetch-swap-proposal))
[pay-input-amount])
[quo/swap-input
{:type :pay
:error? pay-input-error?
:token pay-token-symbol
:customization-color :blue
:show-approval-label? (and swap-proposal approval-required)
:auto-focus? true
:status (cond
(and loading-swap-proposal? (not input-focused?)) :loading
input-focused? :typing
:else :disabled)
:currency-symbol currency-symbol
:on-token-press on-token-press
:on-max-press on-max-press
:on-input-focus on-input-focus
:value pay-input-amount
:fiat-value pay-token-fiat-value
:network-tag-props {:title (i18n/label :t/max-token
{:number available-crypto-limit
:token-symbol pay-token-symbol})
:networks [{:source (:source network)}]}
:approval-label-props {:status :approve
:token-value approval-amount-required-num
:button-props {:on-press on-approve-press}
:customization-color account-color
:token-symbol pay-token-symbol}}]))
(defn- swap-order-button
[{:keys [on-press]}]
(let [approval-required? (rf/sub [:wallet/swap-proposal-approval-required])]
[quo/swap-order-button
{:container-style (style/swap-order-button approval-required?)
:on-press on-press}]))
(defn- receive-token-input
[{:keys [on-input-focus on-token-press input-focused?]}]
(let [account-color (rf/sub [:wallet/current-viewing-account-color])
asset-to-receive (rf/sub [:wallet/swap-asset-to-receive])
loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?])
currency (rf/sub [:profile/currency])
currency-symbol (rf/sub [:profile/currency-symbol])
amount-out (rf/sub [:wallet/swap-proposal-amount-out])
approval-required? (rf/sub [:wallet/swap-proposal-approval-required])
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))
amount-out-num (if amount-out-whole-number
(str amount-out-whole-number)
default-text-for-unfocused-input)
receive-token-fiat-value (str (utils/calculate-token-fiat-value
{:currency currency
:balance (or amount-out-whole-number 0)
:token asset-to-receive}))]
[quo/swap-input
{:type :receive
:error? false
:token receive-token-symbol
:customization-color account-color
:show-approval-label? false
:enable-swap? true
:input-disabled? true
:status (cond
(and loading-swap-proposal? (not input-focused?)) :loading
input-focused? :typing
:else :disabled)
:currency-symbol currency-symbol
:on-token-press on-token-press
:on-input-focus on-input-focus
:value amount-out-num
:fiat-value receive-token-fiat-value
:container-style (style/receive-token-swap-input-container approval-required?)}]))
(defn- action-button
[{:keys [on-press]}]
(let [account-color (rf/sub [:wallet/current-viewing-account-color])
swap-proposal (rf/sub [:wallet/swap-proposal])
loading-fees? (rf/sub [:wallet/swap-loading-fees?])
loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?])
approval-required? (rf/sub [:wallet/swap-proposal-approval-required])]
[quo/bottom-actions
{:actions :one-action
:button-one-label (i18n/label :t/review-swap)
:button-one-props {:disabled? (or (not swap-proposal)
approval-required?
loading-swap-proposal?
loading-fees?)
:customization-color account-color
:on-press on-press}}]))
(defn view (defn view
[] []
(let [[pay-value set-pay-value] (rn/use-state "") (let [[pay-input-state set-pay-input-state] (rn/use-state controlled-input/init-state)
{:keys [color]} (rf/sub [:wallet/current-viewing-account]) [pay-input-focused? set-pay-input-focused?] (rn/use-state true)
{:keys [max-slippage swap-proposal loading-fees? error-response (rf/sub [:wallet/swap-error-response])
receive-amount network]} (rf/sub [:wallet/swap]) network (rf/sub [:wallet/swap-network])
currency (rf/sub [:profile/currency]) loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?])
currency-symbol (rf/sub [:profile/currency-symbol]) swap-proposal (rf/sub [:wallet/swap-proposal])
asset-to-pay (rf/sub [:wallet/swap-asset-to-pay]) asset-to-pay (rf/sub [:wallet/swap-asset-to-pay])
asset-to-receive (rf/sub [:wallet/swap-asset-to-receive]) pay-input-amount (controlled-input/input-value pay-input-state)
pay-token-decimals (:decimals asset-to-pay)
pay-token-fiat-value (utils/calculate-token-fiat-value network-chain-id (:chain-id network)
{:currency currency pay-token-balance-selected-chain (get-in asset-to-pay
:balance (or pay-value 0) [:balances-per-chain network-chain-id
:token asset-to-pay}) :balance]
receive-token-fiat-value (utils/calculate-token-fiat-value 0)
{:currency currency on-press (rn/use-callback
:balance (or receive-amount 0) (fn [c]
:token asset-to-receive}) (let
native-currency-symbol (get-in swap-proposal [new-text (str pay-input-amount c)
[:from :native-currency-symbol]) valid-amount?
pay-token-symbol (:symbol asset-to-pay) (utils.string/valid-amount-for-token-decimals?
receive-token-symbol (:symbol asset-to-receive) pay-token-decimals
on-press (fn [v] (set-pay-value (str pay-value v))) new-text)]
delete (fn [] (when valid-amount?
(set-pay-value #(subs % 0 (dec (count %)))))] (set-pay-input-state
#(controlled-input/add-character % c))))))
on-long-press (rn/use-callback
(fn []
(set-pay-input-state controlled-input/delete-all)
(rf/dispatch [:wallet/clean-suggested-routes])))
delete (rn/use-callback
(fn []
(set-pay-input-state
controlled-input/delete-last)
(rf/dispatch [:wallet/clean-swap-proposal])))]
[rn/view {:style style/container} [rn/view {:style style/container}
[account-switcher/view [account-switcher/view
{:on-press events-helper/navigate-back {:on-press on-close
:icon-name :i/arrow-left :icon-name :i/arrow-left
:margin-top (safe-area/get-top) :margin-top (safe-area/get-top)
:switcher-type :select-account}] :switcher-type :select-account}]
[rn/view {:style style/inputs-container} [rn/view {:style style/inputs-container}
[quo/swap-input [pay-token-input
{:type :pay {:input-state pay-input-state
:error? false :on-max-press #(set-pay-input-state pay-token-balance-selected-chain)
:token pay-token-symbol :input-focused? pay-input-focused?
:customization-color :blue :on-token-press #(js/alert "Token Pressed")
:show-approval-label? false :on-approve-press #(js/alert "Approve Pressed")
:status :default :on-input-focus #(set-pay-input-focused? true)}]
:currency-symbol currency-symbol [swap-order-button {:on-press #(js/alert "Swap Order Pressed")}]
:on-swap-press #(js/alert "Swap Pressed") [receive-token-input
:on-token-press #(js/alert "Token Pressed") {:input-focused? (not pay-input-focused?)
:on-max-press #(js/alert "Max Pressed") :on-token-press #(js/alert "Token Pressed")
:value pay-value :on-input-focus #(set-pay-input-focused? false)}]]
:fiat-value pay-token-fiat-value
:network-tag-props {:title (i18n/label :t/max-token
{:number 200
:token-symbol pay-token-symbol})
:networks [{:source (:source network)}]}
:approval-label-props {:status :approve
:token-value pay-value
:button-props {:on-press
#(js/alert "Approve Pressed")}
:customization-color color
:token-symbol pay-token-symbol}}]
[quo/swap-order-button
{:container-style style/swap-order-button
:on-press #(js/alert "Pressed")}]
[quo/swap-input
{:type :receive
:error? false
:token receive-token-symbol
:customization-color color
:show-approval-label? false
:enable-swap? true
:status :default
:currency-symbol currency-symbol
:on-swap-press #(js/alert "Swap Pressed")
:on-token-press #(js/alert "Token Pressed")
:on-max-press #(js/alert "Max Pressed")
:value receive-amount
:fiat-value receive-token-fiat-value
:container-style style/receive-token-swap-input-container}]]
[rn/view {:style style/footer-container} [rn/view {:style style/footer-container}
(when swap-proposal (when error-response
[transaction-details [quo/alert-banner
{:native-currency-symbol native-currency-symbol {:container-style style/alert-banner
:max-slippage max-slippage :text (i18n/label :t/something-went-wrong-please-try-again-later)}])
:loading-fees? loading-fees?}]) (when (or loading-swap-proposal? swap-proposal)
[quo/bottom-actions [transaction-details])
{:actions :one-action [action-button
:button-one-label (i18n/label :t/review-swap) {:on-press #(js/alert "Review swap pressed")}]]
:button-one-props {:disabled? (or (not swap-proposal)
loading-fees?)
:customization-color color
:on-press #(js/alert "Review swap pressed")}}]]
[quo/numbered-keyboard [quo/numbered-keyboard
{:container-style style/keyboard-container {:container-style style/keyboard-container
:left-action :dot :left-action :dot
:delete-key? true :delete-key? true
:on-press on-press :on-press on-press
:on-delete delete}]])) :on-delete delete
:on-long-press on-long-press}]]))

View File

@ -1,7 +1,9 @@
(ns status-im.subs.wallet.swap (ns status-im.subs.wallet.swap
(:require [re-frame.core :as rf] (:require [clojure.string :as string]
[re-frame.core :as rf]
[status-im.constants :as constants] [status-im.constants :as constants]
[status-im.contexts.wallet.common.utils :as utils] [status-im.contexts.wallet.common.utils :as utils]
[status-im.contexts.wallet.send.utils :as send-utils]
[utils.money :as money])) [utils.money :as money]))
(rf/reg-sub (rf/reg-sub
@ -19,6 +21,16 @@
:<- [:wallet/swap] :<- [:wallet/swap]
:-> :asset-to-receive) :-> :asset-to-receive)
(rf/reg-sub
:wallet/swap-network
:<- [:wallet/swap]
:-> :network)
(rf/reg-sub
:wallet/swap-error-response
:<- [:wallet/swap]
:-> :error-response)
(rf/reg-sub (rf/reg-sub
:wallet/swap-asset-to-pay-token-symbol :wallet/swap-asset-to-pay-token-symbol
:<- [:wallet/swap-asset-to-pay] :<- [:wallet/swap-asset-to-pay]
@ -62,3 +74,59 @@
:wallet/swap-max-slippage :wallet/swap-max-slippage
:<- [:wallet/swap] :<- [:wallet/swap]
:-> :max-slippage) :-> :max-slippage)
(rf/reg-sub
:wallet/swap-loading-fees?
:<- [:wallet/swap]
:-> :loading-fees?)
(rf/reg-sub
:wallet/swap-proposal
:<- [:wallet/swap]
:-> :swap-proposal)
(rf/reg-sub
:wallet/swap-loading-swap-proposal?
:<- [:wallet/swap]
:-> :loading-swap-proposal?)
(rf/reg-sub
:wallet/swap-proposal-amount-out
:<- [:wallet/swap-proposal]
:-> :amount-out)
(rf/reg-sub
:wallet/swap-proposal-approval-required
:<- [:wallet/swap-proposal]
:-> :approval-required)
(rf/reg-sub
:wallet/swap-proposal-approval-amount-required
:<- [:wallet/swap-proposal]
:-> :approval-amount-required)
(rf/reg-sub
:wallet/wallet-swap-proposal-fee-fiat-formatted
:<- [:wallet/current-viewing-account]
:<- [:wallet/swap-proposal]
:<- [:profile/currency]
:<- [:profile/currency-symbol]
(fn [[account swap-proposal currency currency-symbol] [_ token-symbol-for-fees]]
(when token-symbol-for-fees
(let [tokens (:tokens account)
token-for-fees (first (filter #(= (string/lower-case (:symbol %))
(string/lower-case token-symbol-for-fees))
tokens))
fee-in-native-token (send-utils/calculate-full-route-gas-fee [swap-proposal])
fee-in-crypto-formatted (utils/get-standard-crypto-format
token-for-fees
fee-in-native-token)
fee-in-fiat (utils/calculate-token-fiat-value
{:currency currency
:balance fee-in-native-token
:token token-for-fees})
fee-formatted (utils/get-standard-fiat-format
fee-in-crypto-formatted
currency-symbol
fee-in-fiat)]
fee-formatted))))

View File

@ -16,6 +16,27 @@
:popular? true :popular? true
:token? false}}) :token? false}})
(def ^:private accounts-with-tokens
{:0x1 {:tokens [{:symbol "ETH"
:balances-per-chain {1 {:raw-balance "100"}}
:market-values-per-currency {:usd {:price 10000}}}
{:symbol "SNT"
:balances-per-chain {1 {:raw-balance "100"}}
:market-values-per-currency {:usd {:price 10000}}}]
:network-preferences-names #{}
:customization-color nil
:operable? true
:operable :fully
:address "0x1"}
:0x2 {:tokens [{:symbol "SNT"
:balances-per-chain {1 {:raw-balance "200"}}
:market-values-per-currency {:usd {:price 10000}}}]
:network-preferences-names #{}
:customization-color nil
:operable? true
:operable :partially
:address "0x2"}})
(def networks (def networks
{:mainnet-network {:mainnet-network
{:full-name "Mainnet" {:full-name "Mainnet"
@ -110,7 +131,19 @@
:token-list-id "" :token-list-id ""
:built-on "ETH" :built-on "ETH"
:verified true} :verified true}
:network nil}) :network (networks :mainnet-network)
:swap-proposal {:amount-out "0x10000"
:amount-in "0x10000"
:approval-required true
:approval-amount-required "0x10000"
:gas-amount "25000"
:gas-fees {:max-fee-per-gas-medium "4"
:eip-1559-enabled true
:l-1-gas-fee "0"}}
:error-response "Error"
:loading-fees? false
:loading-swap-proposal? false
:max-slippage 0.5})
(h/deftest-sub :wallet/swap (h/deftest-sub :wallet/swap
[sub-name] [sub-name]
@ -160,3 +193,91 @@
(assoc :currencies currencies) (assoc :currencies currencies)
(assoc-in [:wallet :ui :swap] swap-data))) (assoc-in [:wallet :ui :swap] swap-data)))
(is (match? {:crypto "1 SNT" :fiat "$0.03"} (rf/sub [sub-name 1]))))) (is (match? {:crypto "1 SNT" :fiat "$0.03"} (rf/sub [sub-name 1])))))
(h/deftest-sub :wallet/swap-network
[sub-name]
(testing "Return the current swap network"
(swap! rf-db/app-db assoc-in
[:wallet :ui :swap]
swap-data)
(is (match? (swap-data :network) (rf/sub [sub-name])))))
(h/deftest-sub :wallet/swap-error-response
[sub-name]
(testing "Return the swap error response"
(swap! rf-db/app-db assoc-in
[:wallet :ui :swap]
swap-data)
(is (match? (swap-data :error-response) (rf/sub [sub-name])))))
(h/deftest-sub :wallet/swap-max-slippage
[sub-name]
(testing "Return the max slippage for the swap"
(swap! rf-db/app-db assoc-in
[:wallet :ui :swap]
swap-data)
(is (match? 0.5 (rf/sub [sub-name])))))
(h/deftest-sub :wallet/swap-loading-fees?
[sub-name]
(testing "Return if swap is loading fees"
(swap! rf-db/app-db assoc-in
[:wallet :ui :swap]
swap-data)
(is (false? (rf/sub [sub-name])))))
(h/deftest-sub :wallet/swap-loading-swap-proposal?
[sub-name]
(testing "Return if swap is loading the swap proposal"
(swap! rf-db/app-db assoc-in
[:wallet :ui :swap]
swap-data)
(is (false? (rf/sub [sub-name])))))
(h/deftest-sub :wallet/swap-proposal
[sub-name]
(testing "Return the swap proposal"
(swap! rf-db/app-db assoc-in
[:wallet :ui :swap]
swap-data)
(is (match? (swap-data :swap-proposal) (rf/sub [sub-name])))))
(h/deftest-sub :wallet/swap-proposal-amount-out
[sub-name]
(testing "Return the amount out in the swap proposal"
(swap! rf-db/app-db assoc-in
[:wallet :ui :swap]
swap-data)
(is (match? "0x10000" (rf/sub [sub-name])))))
(h/deftest-sub :wallet/swap-proposal-approval-required
[sub-name]
(testing "Return if approval is required in the swap proposal"
(swap! rf-db/app-db assoc-in
[:wallet :ui :swap]
swap-data)
(is (true? (rf/sub [sub-name])))))
(h/deftest-sub :wallet/swap-proposal-approval-amount-required
[sub-name]
(testing "Return the approval amount required in the swap proposal"
(swap! rf-db/app-db assoc-in
[:wallet :ui :swap]
swap-data)
(is (match? "0x10000" (rf/sub [sub-name])))))
(h/deftest-sub :wallet/wallet-swap-proposal-fee-fiat-formatted
[sub-name]
(testing "wallet send fee calculated and formatted in fiat"
(swap! rf-db/app-db
#(-> %
(assoc-in [:wallet :accounts] accounts-with-tokens)
(assoc-in [:wallet :current-viewing-account-address] "0x1")
(assoc-in [:wallet :ui :swap] swap-data)
(assoc-in [:currencies] currencies)
(assoc-in [:profile/profile :currency] :usd)
(assoc-in [:profile/profile :currency-symbol] "$")))
(let [token-symbol-for-fees "ETH"
result (rf/sub [sub-name token-symbol-for-fees])]
(is (match? result "$1.00")))))

View File

@ -202,11 +202,6 @@
[gas gas-price] [gas gas-price]
(.times ^js (bignumber gas) ^js (bignumber gas-price))) (.times ^js (bignumber gas) ^js (bignumber gas-price)))
(defn crypto->fiat
[crypto fiat-price]
(when-let [^js bn (bignumber crypto)]
(.times bn ^js (bignumber fiat-price))))
(defn percent-change (defn percent-change
[from to] [from to]
(let [^js bnf (bignumber from) (let [^js bnf (bignumber from)
@ -221,6 +216,12 @@
(when-let [^js bn (bignumber n)] (when-let [^js bn (bignumber n)]
(.round bn decimals))) (.round bn decimals)))
(defn crypto->fiat
[crypto fiat-price]
(when-let [^js bn (bignumber crypto)]
(-> (.times bn ^js (bignumber fiat-price))
(with-precision 2))))
(defn sufficient-funds? (defn sufficient-funds?
[^js amount ^js balance] [^js amount ^js balance]
(when (and amount balance) (when (and amount balance)

View File

@ -15,6 +15,17 @@
(/ (Math/round (* n scale)) (/ (Math/round (* n scale))
scale))) scale)))
(defn convert-to-whole-number
"Converts a fractional `amount` to its corresponding whole number representation
by dividing it by 10 raised to the power of `decimals`. This is often used in financial
calculations where amounts are stored in their smallest units (e.g., cents) and need
to be converted to their whole number equivalents (e.g., dollars).
Example usage:
(convert-to-whole-number 12345 2) ; => 123.45"
[amount decimals]
(/ amount (Math/pow 10 decimals)))
(defn parse-int (defn parse-int
"Parses `n` as an integer. Defaults to zero or `default` instead of NaN." "Parses `n` as an integer. Defaults to zero or `default` instead of NaN."
([n] ([n]

View File

@ -3,6 +3,26 @@
[cljs.test :refer [deftest is testing]] [cljs.test :refer [deftest is testing]]
[utils.number])) [utils.number]))
(deftest convert-to-whole-number-test
(testing "correctly converts fractional amounts to whole numbers"
(is (= 123.45 (utils.number/convert-to-whole-number 12345 2)))
(is (= 1.2345 (utils.number/convert-to-whole-number 12345 4)))
(is (= 12345.0 (utils.number/convert-to-whole-number 1234500 2)))
(is (= 0.123 (utils.number/convert-to-whole-number 123 3)))
(is (= 1000.0 (utils.number/convert-to-whole-number 1000000 3))))
(testing "handles zero decimals"
(is (= 12345 (utils.number/convert-to-whole-number 12345 0))))
(testing "handles negative amounts"
(is (= -123.45 (utils.number/convert-to-whole-number -12345 2)))
(is (= -1.2345 (utils.number/convert-to-whole-number -12345 4)))
(is (= -0.123 (utils.number/convert-to-whole-number -123 3))))
(testing "handles zero amount"
(is (= 0 (utils.number/convert-to-whole-number 0 2)))
(is (= 0 (utils.number/convert-to-whole-number 0 0)))))
(deftest parse-int-test (deftest parse-int-test
(testing "defaults to zero" (testing "defaults to zero"
(is (= 0 (utils.number/parse-int nil)))) (is (= 0 (utils.number/parse-int nil))))

View File

@ -88,3 +88,9 @@
[url] [url]
(when (string? url) (when (string? url)
(string/replace url #"^https?://" ""))) (string/replace url #"^https?://" "")))
(defn valid-amount-for-token-decimals?
[token-decimals amount-text]
(let [regex-pattern (str "^\\d*\\.?\\d{0," token-decimals "}$")
regex (re-pattern regex-pattern)]
(re-matches regex amount-text)))

View File

@ -2302,6 +2302,7 @@
"slow": "Slow", "slow": "Slow",
"something-about-you": "Something about you", "something-about-you": "Something about you",
"something-went-wrong": "Something went wrong", "something-went-wrong": "Something went wrong",
"something-went-wrong-please-try-again-later": "Something went wrong, please try again later",
"soon": "Soon", "soon": "Soon",
"sort-communities": "Sort communities", "sort-communities": "Sort communities",
"special-characters": "Special characters", "special-characters": "Special characters",