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}
[:action? {:optional true} [:maybe boolean?]]
[:text {:optional true} [:maybe string?]]
[:container-style {:optional true} [:maybe :map]]
[:button-text {:optional true} [:maybe string?]]
[:on-button-press {:optional true} [:maybe fn?]]]]]
:any])

View File

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

View File

@ -11,12 +11,12 @@
[schema.core :as schema]))
(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)]
[rn/view
{:accessibility-label :alert-banner}
[linear-gradient/linear-gradient
{:style style/container
{:style (style/container container-style)
:start {:x 0 :y 0}
:end {:x 0 :y 1}
:colors [(colors/theme-colors

View File

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

View File

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

View File

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

View File

@ -551,6 +551,7 @@
(def ^:const send-type-bridge 5)
(def ^:const send-type-erc-721-transfer 6)
(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-approve 1)
@ -596,3 +597,5 @@
:color :blue
:contract-address "0xdef171fe48cf0115b1d80b88dc8eab59176fee57"
: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
:wallet/handle-suggested-routes
(fn [_ [data]]
(if-let [{:keys [code details]} (-> data :ErrorResponse)]
(let [error-message (if (= code "0") "An error occurred" details)]
(log/error "failed to get suggested routes (async)"
{:event :wallet/handle-suggested-routes
:error error-message})
{:fx [[:dispatch [:wallet/suggested-routes-error error-message]]]})
(let [best-routes-fix (comp ->old-route-paths
remove-invalid-bonder-fees-routes
remove-multichain-routes)
candidates-fix (comp ->old-route-paths
remove-invalid-bonder-fees-routes)
routes (-> data
(data-store/rpc->suggested-routes)
(update :best best-routes-fix)
(update :candidates candidates-fix))]
{:fx [[:dispatch [:wallet/suggested-routes-success routes]]]}))))
(fn [{:keys [db]} [data]]
(let [swap? (get-in db [:wallet :ui :swap])
{:keys [code details] :as error-response} (-> data :ErrorResponse)]
(if (and (not swap?) error-response)
(let [error-message (if (= code "0") "An error occurred" details)]
(log/error "failed to get suggested routes (async)"
{:event :wallet/handle-suggested-routes
:error error-message})
{:fx [(if swap?
[:dispatch [:wallet/swap-proposal-error error-message]]
[:dispatch [:wallet/suggested-routes-error error-message]])]})
(let [best-routes-fix (comp ->old-route-paths
remove-invalid-bonder-fees-routes
remove-multichain-routes)
candidates-fix (comp ->old-route-paths
remove-invalid-bonder-fees-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
(fn [{:keys [db]} [transaction]]

View File

@ -1,7 +1,9 @@
(ns status-im.contexts.wallet.swap.events
(:require [re-frame.core :as rf]
[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]
[taoensso.timbre :as log]
[utils.number]))
(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 :network] 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
[:show-bottom-sheet
@ -30,10 +34,6 @@
:stack-id
: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
(fn [{:keys [db]}]
{:db
@ -47,18 +47,113 @@
(fn [{:keys [db]} [{:keys [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
(fn [{:keys [db]} [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
: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
[search-text on-change-text]
(let [on-token-press (fn [token]
@ -44,10 +28,8 @@
:network (when (= (count token-networks) 1)
(first token-networks))
: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/set-pay-amount 100])
(rf/dispatch [:wallet.swap/set-swap-proposal dummy-swap-proposal])
(rf/dispatch [:wallet.swap/set-provider])))]
(rf/dispatch [:wallet.swap/select-asset-to-receive
{:token asset-to-receive}])))]
[:<>
[search-input search-text on-change-text]
[asset-list/view
@ -59,7 +41,7 @@
(let [[search-text set-search-text] (rn/use-state "")
on-change-text #(set-search-text %)
on-close (fn []
(rf/dispatch [:wallet.swap/clean-asset-to-pay])
(rf/dispatch [:wallet/clean-swap])
(rf/dispatch [:navigate-back]))]
[rn/safe-area-view {:style style/container}
[account-switcher/view

View File

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

View File

@ -1,13 +1,35 @@
(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.safe-area :as safe-area]
[status-im.common.controlled-input.utils :as controlled-input]
[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.utils :as utils]
[status-im.contexts.wallet.swap.setup-swap.style :as style]
[utils.hex :as hex]
[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
[{:keys [title subtitle size subtitle-icon loading?]}]
@ -23,110 +45,243 @@
:icon subtitle-icon}])
(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}
[data-item
{:title (i18n/label :t/max-fees)
:subtitle max-fees
:loading? loading-fees?
:loading? loading?
:size :small}]
[data-item
{:title (i18n/label :t/max-slippage)
:subtitle max-slippage
: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
[]
(let [[pay-value set-pay-value] (rn/use-state "")
{:keys [color]} (rf/sub [:wallet/current-viewing-account])
{:keys [max-slippage swap-proposal loading-fees?
receive-amount network]} (rf/sub [:wallet/swap])
currency (rf/sub [:profile/currency])
currency-symbol (rf/sub [:profile/currency-symbol])
asset-to-pay (rf/sub [:wallet/swap-asset-to-pay])
asset-to-receive (rf/sub [:wallet/swap-asset-to-receive])
pay-token-fiat-value (utils/calculate-token-fiat-value
{:currency currency
:balance (or pay-value 0)
:token asset-to-pay})
receive-token-fiat-value (utils/calculate-token-fiat-value
{:currency currency
:balance (or receive-amount 0)
:token asset-to-receive})
native-currency-symbol (get-in swap-proposal
[:from :native-currency-symbol])
pay-token-symbol (:symbol asset-to-pay)
receive-token-symbol (:symbol asset-to-receive)
on-press (fn [v] (set-pay-value (str pay-value v)))
delete (fn []
(set-pay-value #(subs % 0 (dec (count %)))))]
(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)
error-response (rf/sub [:wallet/swap-error-response])
network (rf/sub [:wallet/swap-network])
loading-swap-proposal? (rf/sub [:wallet/swap-loading-swap-proposal?])
swap-proposal (rf/sub [:wallet/swap-proposal])
asset-to-pay (rf/sub [:wallet/swap-asset-to-pay])
pay-input-amount (controlled-input/input-value pay-input-state)
pay-token-decimals (:decimals asset-to-pay)
network-chain-id (:chain-id network)
pay-token-balance-selected-chain (get-in asset-to-pay
[:balances-per-chain network-chain-id
:balance]
0)
on-press (rn/use-callback
(fn [c]
(let
[new-text (str pay-input-amount c)
valid-amount?
(utils.string/valid-amount-for-token-decimals?
pay-token-decimals
new-text)]
(when valid-amount?
(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}
[account-switcher/view
{:on-press events-helper/navigate-back
{:on-press on-close
:icon-name :i/arrow-left
:margin-top (safe-area/get-top)
:switcher-type :select-account}]
[rn/view {:style style/inputs-container}
[quo/swap-input
{:type :pay
:error? false
:token pay-token-symbol
:customization-color :blue
:show-approval-label? false
: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 pay-value
: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}]]
[pay-token-input
{:input-state pay-input-state
:on-max-press #(set-pay-input-state pay-token-balance-selected-chain)
:input-focused? pay-input-focused?
:on-token-press #(js/alert "Token Pressed")
:on-approve-press #(js/alert "Approve Pressed")
:on-input-focus #(set-pay-input-focused? true)}]
[swap-order-button {:on-press #(js/alert "Swap Order Pressed")}]
[receive-token-input
{:input-focused? (not pay-input-focused?)
:on-token-press #(js/alert "Token Pressed")
:on-input-focus #(set-pay-input-focused? false)}]]
[rn/view {:style style/footer-container}
(when swap-proposal
[transaction-details
{:native-currency-symbol native-currency-symbol
:max-slippage max-slippage
:loading-fees? loading-fees?}])
[quo/bottom-actions
{:actions :one-action
:button-one-label (i18n/label :t/review-swap)
:button-one-props {:disabled? (or (not swap-proposal)
loading-fees?)
:customization-color color
:on-press #(js/alert "Review swap pressed")}}]]
(when error-response
[quo/alert-banner
{:container-style style/alert-banner
:text (i18n/label :t/something-went-wrong-please-try-again-later)}])
(when (or loading-swap-proposal? swap-proposal)
[transaction-details])
[action-button
{:on-press #(js/alert "Review swap pressed")}]]
[quo/numbered-keyboard
{:container-style style/keyboard-container
:left-action :dot
:delete-key? true
: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
(:require [re-frame.core :as rf]
(:require [clojure.string :as string]
[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.money :as money]))
(rf/reg-sub
@ -19,6 +21,16 @@
:<- [:wallet/swap]
:-> :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
:wallet/swap-asset-to-pay-token-symbol
:<- [:wallet/swap-asset-to-pay]
@ -62,3 +74,59 @@
:wallet/swap-max-slippage
:<- [:wallet/swap]
:-> :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
: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
{:mainnet-network
{:full-name "Mainnet"
@ -110,7 +131,19 @@
:token-list-id ""
:built-on "ETH"
: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
[sub-name]
@ -160,3 +193,91 @@
(assoc :currencies currencies)
(assoc-in [:wallet :ui :swap] swap-data)))
(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]
(.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
[from to]
(let [^js bnf (bignumber from)
@ -221,6 +216,12 @@
(when-let [^js bn (bignumber n)]
(.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?
[^js amount ^js balance]
(when (and amount balance)

View File

@ -15,6 +15,17 @@
(/ (Math/round (* n 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
"Parses `n` as an integer. Defaults to zero or `default` instead of NaN."
([n]

View File

@ -3,6 +3,26 @@
[cljs.test :refer [deftest is testing]]
[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
(testing "defaults to zero"
(is (= 0 (utils.number/parse-int nil))))

View File

@ -88,3 +88,9 @@
[url]
(when (string? url)
(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",
"something-about-you": "Something about you",
"something-went-wrong": "Something went wrong",
"something-went-wrong-please-try-again-later": "Something went wrong, please try again later",
"soon": "Soon",
"sort-communities": "Sort communities",
"special-characters": "Special characters",