refactor: suggested routes rendering (#19768)

Signed-off-by: Brian Sztamfater <brian@status.im>
This commit is contained in:
Brian Sztamfater 2024-05-03 13:18:14 -03:00 committed by GitHub
parent d8d1e030e4
commit 355e144ef7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 865 additions and 443 deletions

View File

@ -30,7 +30,7 @@
[{:keys [network status amount container-style on-press] :as args}]
(let [theme (quo.theme/use-theme)]
(if (= status :add)
[network-bridge-add args]
[network-bridge-add (assoc args :theme theme)]
[rn/pressable
{:style (merge (style/container network status theme) container-style)
:accessible true

View File

@ -25,10 +25,10 @@
(def link-1x-container
{:flex 1
:height 58
:height 57
:justify-content :center})
(def link-2x-container
{:flex 1
:height 114
:height 112
:justify-content :center})

View File

@ -24,33 +24,50 @@
:strokeWidth "1"}]])
(defn- line
[stroke width]
[{:keys [stroke-color source-color destination-color width theme]}]
[svg/svg
{:height "10"
:width "100%"
:view-box (str "0 0 " width " 10")}
[svg/path
{:d (str "M0,5 L" width ",5")
:stroke stroke
:stroke-width "1"}]])
:stroke stroke-color
:stroke-width "1"}]
[svg/defs
[svg/linear-gradient
{:id "gradient"
:x1 "0%"
:x2 "100%"
:y1 "0%"
:y2 "0%"
:gradient-units "objectBoundingBox"}
[svg/stop {:offset "0%" :stop-color (colors/resolve-color source-color theme)}]
[svg/stop {:offset "100%" :stop-color (colors/resolve-color destination-color theme)}]]]])
(defn link-linear
[{:keys [source]}]
[{:keys [source destination]}]
(let [theme (quo.theme/use-theme)
[container-width
set-container-width] (rn/use-state 100)
stroke-color (colors/resolve-color source theme)
stroke-color "url(#gradient)"
source-color (colors/resolve-color source theme)
destination-color (colors/resolve-color destination theme)
fill-color (colors/theme-colors colors/white colors/neutral-90 theme)
on-layout (rn/use-callback #(set-container-width
(oget % :nativeEvent :layout :width)))]
[rn/view
{:style style/link-linear-container
:on-layout on-layout}
[line stroke-color container-width]
[line
{:stroke-color stroke-color
:source-color source-color
:destination-color destination-color
:width container-width
:theme theme}]
[rn/view {:style style/left-circle-container}
[circle fill-color stroke-color]]
[circle fill-color source-color]]
[rn/view {:style style/right-circle-container}
[circle fill-color stroke-color]]]))
[circle fill-color destination-color]]]))
(defn link-1x
[{:keys [source destination]}]

View File

@ -263,3 +263,11 @@
{}
tokens)
(update-vals #(prettify-balance currency-symbol %))))
(defn format-token-id
[token collectible]
(if token
(:symbol token)
(str (get-in collectible [:id :contract-id :address])
":"
(get-in collectible [:id :token-id]))))

View File

@ -1,34 +0,0 @@
(ns status-im.contexts.wallet.common.utils.send
(:require [clojure.string :as string]
[utils.money :as money]))
(defn calculate-gas-fee
[data]
(let [gas-amount (money/bignumber (get data :gas-amount))
gas-fees (get data :gas-fees)
eip1559-enabled? (get gas-fees :eip-1559-enabled)
optimal-price-gwei (money/bignumber (if eip1559-enabled?
(get gas-fees :max-fee-per-gas-medium)
(get gas-fees :gas-price)))
total-gas-fee-wei (money/mul (money/->wei :gwei optimal-price-gwei) gas-amount)
l1-fee-wei (money/->wei :gwei (get gas-fees :l-1-gas-fee))]
(money/add total-gas-fee-wei l1-fee-wei)))
(defn calculate-full-route-gas-fee
"Sums all the routes fees in wei and then convert the total value to ether"
[route]
(money/wei->ether (reduce money/add (map calculate-gas-fee route))))
(defn find-affordable-networks
[{:keys [balances-per-chain input-value selected-networks disabled-chain-ids]}]
(let [input-value (if (string/blank? input-value) 0 input-value)]
(->> balances-per-chain
(filter (fn [[_
{:keys [balance chain-id]
:or {balance 0}}]]
(and
(money/greater-than-or-equals (money/bignumber balance)
(money/bignumber input-value))
(some #(= % chain-id) selected-networks)
(not-any? #(= % chain-id) disabled-chain-ids))))
(map first))))

View File

@ -1,116 +0,0 @@
(ns status-im.contexts.wallet.common.utils.send-test
(:require [cljs.test :refer [deftest is testing]]
[status-im.contexts.wallet.common.utils.send :as utils]
[utils.money :as money]))
(deftest test-calculate-gas-fee
(testing "EIP-1559 transaction without L1 fee"
(let [data {:gas-amount "23487"
:gas-fees {:max-fee-per-gas-medium "2.259274911"
:eip-1559-enabled true
:l-1-gas-fee "0"}}
expected-result (money/bignumber "53063589834657")] ; This is in Wei
(is (money/equal-to (utils/calculate-gas-fee data)
expected-result))))
(testing "EIP-1559 transaction with L1 fee of 60,000 Gwei"
(let [data {:gas-amount "23487"
:gas-fees {:max-fee-per-gas-medium "2.259274911"
:eip-1559-enabled true
:l-1-gas-fee "60000"}}
expected-result (money/bignumber "113063589834657")] ; Added 60,000 Gwei in Wei to the
; previous result
(is (money/equal-to (utils/calculate-gas-fee data)
expected-result))))
(testing "Non-EIP-1559 transaction with specified gas price"
(let [data {:gas-amount "23487"
:gas-fees {:gas-price "2.872721089"
:eip-1559-enabled false
:l-1-gas-fee "0"}}
expected-result (money/bignumber "67471600217343")] ; This is in Wei, for the specified
; gas amount and price
(is (money/equal-to (utils/calculate-gas-fee data)
expected-result)))))
(deftest test-calculate-full-route-gas-fee
(testing "Route with a single EIP-1559 transaction, no L1 fees"
(let [route [{:gas-amount "23487"
:gas-fees {:max-fee-per-gas-medium "2.259274911"
:eip-1559-enabled true
:l-1-gas-fee "0"}}]
expected-result (money/bignumber "0.000053063589834657")] ; The Wei amount for the
; transaction, converted to
; Ether
(is (money/equal-to (utils/calculate-full-route-gas-fee route)
expected-result))))
(testing "Route with two EIP-1559 transactions, no L1 fees"
(let [route [{:gas-amount "23487"
:gas-fees {:max-fee-per-gas-medium "2.259274911"
:eip-1559-enabled true
:l-1-gas-fee "0"}}
{:gas-amount "23487"
:gas-fees {:max-fee-per-gas-medium "2.259274911"
:eip-1559-enabled true
:l-1-gas-fee "0"}}]
expected-result (money/bignumber "0.000106127179669314")] ; Sum of both transactions' Wei
; amounts, converted to Ether
(is (money/equal-to (utils/calculate-full-route-gas-fee route)
expected-result))))
(testing "Route with two EIP-1559 transactions, one with L1 fee of 60,000 Gwei"
(let [route [{:gas-amount "23487"
:gas-fees {:max-fee-per-gas-medium "2.259274911"
:eip-1559-enabled true
:l-1-gas-fee "0"}}
{:gas-amount "23487"
:gas-fees {:max-fee-per-gas-medium "2.259274911"
:eip-1559-enabled true
:l-1-gas-fee "60000"}}]
expected-result (money/bignumber "0.000166127179669314")] ; Added 60,000 Gwei in Wei to
; the previous total and
; converted to Ether
(is (money/equal-to (utils/calculate-full-route-gas-fee route)
expected-result)))))
(deftest test-find-affordable-networks
(testing "All networks affordable and selected, none disabled"
(let [balances-per-chain {"1" {:balance "50.0" :chain-id "1"}
"2" {:balance "40.0" :chain-id "2"}}
input-value 20
selected-networks ["1" "2"]
disabled-chain-ids []
expected ["1" "2"]]
(is (= (set (utils/find-affordable-networks {:balances-per-chain balances-per-chain
:input-value input-value
:selected-networks selected-networks
:disabled-chain-ids disabled-chain-ids}))
(set expected)))))
(testing "No networks affordable"
(let [balances-per-chain {"1" {:balance "5.0" :chain-id "1"}
"2" {:balance "1.0" :chain-id "2"}}
input-value 10
selected-networks ["1" "2"]
disabled-chain-ids []
expected []]
(is (= (set (utils/find-affordable-networks {:balances-per-chain balances-per-chain
:input-value input-value
:selected-networks selected-networks
:disabled-chain-ids disabled-chain-ids}))
(set expected)))))
(testing "Selected networks subset, with some disabled"
(let [balances-per-chain {"1" {:balance "100.0" :chain-id "1"}
"2" {:balance "50.0" :chain-id "2"}
"3" {:balance "20.0" :chain-id "3"}}
input-value 15
selected-networks ["1" "2" "3"]
disabled-chain-ids ["2"]
expected ["1" "3"]]
(is (= (set (utils/find-affordable-networks {:balances-per-chain balances-per-chain
:input-value input-value
:selected-networks selected-networks
:disabled-chain-ids disabled-chain-ids}))
(set expected))))))

View File

@ -31,8 +31,13 @@
token (get-in db [:wallet :ui :send :token])
collectible (get-in db [:wallet :ui :send :collectible])
token-display-name (get-in db [:wallet :ui :send :token-display-name])
receiver-networks (get-in db [:wallet :ui :send :receiver-networks])
receiver-network-values (get-in db [:wallet :ui :send :receiver-network-values])
sender-network-values (get-in db [:wallet :ui :send :sender-network-values])
disabled-from-chain-ids (or (get-in db [:wallet :ui :send :disabled-from-chain-ids]) [])
token-decimals (if collectible 0 (:decimals token))
native-token? (and token (= token-display-name "ETH"))
routes-available? (pos? (count chosen-route))
from-network-amounts-by-chain (send-utils/network-amounts-by-chain {:route chosen-route
:token-decimals
token-decimals
@ -46,19 +51,54 @@
:native-token?
native-token?
:to? true})
to-network-values-for-ui (send-utils/network-values-for-ui to-network-amounts-by-chain)]
to-network-values-for-ui (send-utils/network-values-for-ui to-network-amounts-by-chain)
sender-network-values (if routes-available?
(send-utils/network-amounts from-network-values-for-ui
disabled-from-chain-ids
receiver-networks
false)
(send-utils/reset-network-amounts-to-zero
sender-network-values))
receiver-network-values (if routes-available?
(send-utils/network-amounts to-network-values-for-ui
disabled-from-chain-ids
receiver-networks
true)
(send-utils/reset-network-amounts-to-zero
receiver-network-values))
network-links (when routes-available?
(send-utils/network-links chosen-route
sender-network-values
receiver-network-values))]
{:db (-> db
(assoc-in [:wallet :ui :send :suggested-routes] suggested-routes-data)
(assoc-in [:wallet :ui :send :route] chosen-route)
(assoc-in [:wallet :ui :send :from-values-by-chain] from-network-values-for-ui)
(assoc-in [:wallet :ui :send :to-values-by-chain] to-network-values-for-ui)
(assoc-in [:wallet :ui :send :sender-network-values] sender-network-values)
(assoc-in [:wallet :ui :send :receiver-network-values] receiver-network-values)
(assoc-in [:wallet :ui :send :network-links] network-links)
(assoc-in [:wallet :ui :send :loading-suggested-routes?] false))}))))
(rf/reg-event-fx :wallet/suggested-routes-error
(fn [{:keys [db]} [_error]]
{:db (-> db
(update-in [:wallet :ui :send] dissoc :suggested-routes :route)
(assoc-in [:wallet :ui :send :loading-suggested-routes?] false))}))
(fn [{:keys [db]} [error]]
(let [cleaned-sender-network-values (-> (get-in db [:wallet :ui :send :sender-network-values])
(send-utils/reset-network-amounts-to-zero))
cleaned-receiver-network-values (-> (get-in db [:wallet :ui :send :receiver-network-values])
(send-utils/reset-network-amounts-to-zero))]
{:db (-> db
(update-in [:wallet :ui :send]
dissoc
:route)
(assoc-in [:wallet :ui :send :sender-network-values] cleaned-sender-network-values)
(assoc-in [:wallet :ui :send :receiver-network-values] cleaned-receiver-network-values)
(assoc-in [:wallet :ui :send :loading-suggested-routes?] false)
(assoc-in [:wallet :ui :send :suggested-routes] {:best []}))
:fx [[:dispatch
[:toasts/upsert
{:id :send-transaction-error
:type :negative
:text (:message error)}]]]})))
(rf/reg-event-fx :wallet/clean-suggested-routes
(fn [{:keys [db]}]
@ -69,6 +109,9 @@
:route
:from-values-by-chain
:to-values-by-chain
:sender-network-values
:receiver-network-values
:network-links
:loading-suggested-routes?
:suggested-routes-call-timestamp)}))
@ -203,56 +246,74 @@
(rf/reg-event-fx :wallet/get-suggested-routes
(fn [{:keys [db now]} [{:keys [amount]}]]
(let [wallet-address (get-in db [:wallet :current-viewing-account-address])
token (get-in db [:wallet :ui :send :token])
transaction-type (get-in db [:wallet :ui :send :tx-type])
collectible (get-in db [:wallet :ui :send :collectible])
to-address (get-in db [:wallet :ui :send :to-address])
receiver-networks (get-in db [:wallet :ui :send :receiver-networks])
(let [wallet-address (get-in db [:wallet :current-viewing-account-address])
token (get-in db [:wallet :ui :send :token])
transaction-type (get-in db [:wallet :ui :send :tx-type])
collectible (get-in db [:wallet :ui :send :collectible])
to-address (get-in db [:wallet :ui :send :to-address])
receiver-networks (get-in db [:wallet :ui :send :receiver-networks])
disabled-from-chain-ids (or (get-in db [:wallet :ui :send :disabled-from-chain-ids]) [])
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)
bridge-to-chain-id (get-in db [:wallet :ui :send :bridge-to-chain-id])
token-decimal (when token (:decimals token))
token-id (if token
(:symbol token)
(str (get-in collectible [:id :contract-id :address])
":"
(get-in collectible [:id :token-id])))
to-token-id ""
network-preferences (if token [] [(get-in collectible [:id :contract-id :chain-id])])
gas-rates constants/gas-rate-medium
amount-in (send-utils/amount-in-hex amount (if token token-decimal 0))
from-address wallet-address
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)
bridge-to-chain-id (get-in db [:wallet :ui :send :bridge-to-chain-id])
token-decimal (when token (:decimals token))
token-id (utils/format-token-id token collectible)
to-token-id ""
network-preferences (if token [] [(get-in collectible [:id :contract-id :chain-id])])
gas-rates constants/gas-rate-medium
amount-in (send-utils/amount-in-hex amount (if token token-decimal 0))
from-address wallet-address
disabled-from-chain-ids disabled-from-chain-ids
disabled-to-chain-ids (if (= transaction-type :bridge)
(filter #(not= % bridge-to-chain-id) network-chain-ids)
(filter (fn [chain-id]
(not (some #(= chain-id %)
receiver-networks)))
network-chain-ids))
from-locked-amount {}
transaction-type-param (case transaction-type
:collectible constants/send-type-erc-721-transfer
:bridge constants/send-type-bridge
constants/send-type-transfer)
request-params [transaction-type-param
from-address
to-address
amount-in
token-id
to-token-id
disabled-to-chain-ids (if (= transaction-type :bridge)
(filter #(not= % bridge-to-chain-id) network-chain-ids)
(filter (fn [chain-id]
(not (some #(= chain-id %)
receiver-networks)))
network-chain-ids))
from-locked-amount {}
transaction-type-param (case transaction-type
:collectible constants/send-type-erc-721-transfer
:bridge constants/send-type-bridge
constants/send-type-transfer)
balances-per-chain (when token (:balances-per-chain token))
token-available-networks-for-suggested-routes
(when token
(send-utils/token-available-networks-for-suggested-routes {:balances-per-chain
balances-per-chain
:disabled-chain-ids
disabled-from-chain-ids}))
sender-network-values (when token-available-networks-for-suggested-routes
(send-utils/loading-network-amounts
token-available-networks-for-suggested-routes
disabled-from-chain-ids
disabled-to-chain-ids
network-preferences
gas-rates
from-locked-amount]]
receiver-networks
false))
receiver-network-values (when token-available-networks-for-suggested-routes
(send-utils/loading-network-amounts
token-available-networks-for-suggested-routes
disabled-from-chain-ids
receiver-networks
true))
request-params [transaction-type-param
from-address
to-address
amount-in
token-id
to-token-id
disabled-from-chain-ids
disabled-to-chain-ids
network-preferences
gas-rates
from-locked-amount]]
{:db (-> db
(assoc-in [:wallet :ui :send :amount] amount)
(assoc-in [:wallet :ui :send :loading-suggested-routes?] true)
(assoc-in [:wallet :ui :send :suggested-routes-call-timestamp] now))
(assoc-in [:wallet :ui :send :sender-network-values] sender-network-values)
(assoc-in [:wallet :ui :send :receiver-network-values] receiver-network-values)
(assoc-in [:wallet :ui :send :suggested-routes-call-timestamp] now)
(update-in [:wallet :ui :send] dissoc :network-links))
:json-rpc/call [{:method "wallet_getSuggestedRoutes"
:params request-params
:on-success (fn [suggested-routes]

View File

@ -63,7 +63,7 @@
:gas-fees {:base-fee "32.325296406"
:max-priority-fee-per-gas "0.011000001"
:eip1559-enabled true}}]
:wallet/wallet-send-suggested-routes {:candidates []}
:wallet/wallet-send-suggested-routes nil
:wallet/wallet-send-receiver-networks [1]
:view-id :screen/wallet.send-input-amount
:wallet/wallet-send-to-address "0x04371e2d9d66b82f056bc128064"
@ -73,7 +73,10 @@
:market-values-per-currency {:usd {:price 10}}}
:wallet/wallet-send-disabled-from-chain-ids []
:wallet/wallet-send-from-values-by-chain {1 (money/bignumber "250")}
:wallet/wallet-send-to-values-by-chain {1 (money/bignumber "250")}})
:wallet/wallet-send-to-values-by-chain {1 (money/bignumber "250")}
:wallet/wallet-send-sender-network-values nil
:wallet/wallet-send-receiver-network-values nil
:wallet/wallet-send-network-links nil})
(h/describe "Send > input amount screen"
(h/setup-restorable-re-frame)

View File

@ -33,3 +33,8 @@
{:flex 1
:height 40
:background-color :transparent})
(def no-routes-found-container
{:height 40
:width "100%"
:align-items :center})

View File

@ -9,11 +9,12 @@
[status-im.contexts.wallet.common.account-switcher.view :as account-switcher]
[status-im.contexts.wallet.common.asset-list.view :as asset-list]
[status-im.contexts.wallet.common.utils :as utils]
[status-im.contexts.wallet.common.utils.send :as send-utils]
[status-im.contexts.wallet.send.input-amount.style :as style]
[status-im.contexts.wallet.send.routes.view :as routes]
[status-im.contexts.wallet.send.utils :as send-utils]
[utils.address :as address]
[utils.i18n :as i18n]
[utils.money :as money]
[utils.re-frame :as rf]))
(defn- make-limit-label
@ -52,6 +53,15 @@
:title (i18n/label :t/user-gets {:name receiver})
:subtitle amount}]])
(defn- every-network-value-is-zero?
[sender-network-values]
(every? (fn [{:keys [total-amount]}]
(and
total-amount
(money/equal-to total-amount
(money/bignumber "0"))))
sender-network-values))
(defn select-asset-bottom-sheet
[clear-input!]
(let [{preselected-token-symbol :symbol} (rf/sub [:wallet/wallet-send-token])]
@ -157,7 +167,18 @@
[:show-bottom-sheet
{:content (fn []
[select-asset-bottom-sheet
clear-input!])}])]
clear-input!])}])
loading-suggested-routes? (rf/sub
[:wallet/wallet-send-loading-suggested-routes?])
sender-network-values (rf/sub
[:wallet/wallet-send-sender-network-values])
suggested-routes (rf/sub [:wallet/wallet-send-suggested-routes])
routes (when suggested-routes
(or (:best suggested-routes) []))
no-routes-found? (and
(every-network-value-is-zero? sender-network-values)
(not (nil? routes))
(not loading-suggested-routes?))]
(rn/use-mount
(fn []
(let [dismiss-keyboard-fn #(when (= % "active") (rn/dismiss-keyboard!))
@ -200,12 +221,28 @@
:fees fee-formatted
:amount amount-text
:receiver (address/get-shortened-key to-address)}])
(when no-routes-found?
[rn/view {:style style/no-routes-found-container}
[quo/info-message
{:type :error
:icon :i/alert
:size :default
:style {:margin-top 15}}
(i18n/label :t/no-routes-found)]])
[quo/bottom-actions
{:actions :one-action
:button-one-label button-one-label
:button-one-label (if no-routes-found?
(i18n/label :t/try-again)
button-one-label)
:button-one-props (merge button-one-props
{:disabled? confirm-disabled?
:on-press on-confirm})}]
{:disabled? (and (not no-routes-found?) confirm-disabled?)
:on-press (if no-routes-found?
#(rf/dispatch [:wallet/get-suggested-routes
{:amount (controlled-input/input-value
input-state)}])
on-confirm)}
(when no-routes-found?
{:type :grey}))}]
[quo/numbered-keyboard
{:container-style (style/keyboard-container bottom)
:left-action :dot

View File

@ -3,20 +3,16 @@
(def routes-container
{:padding-horizontal 20
:flex 1
:flex-grow 1
:padding-vertical 16
:width "100%"
:height "100%"})
:width "100%"})
(def routes-header-container
{:flex-direction :row
:justify-content :space-between})
(defn routes-inner-container
[first-item?]
{:margin-top (if first-item? 7.5 11)
:flex-direction :row
:align-items :center
(def routes-inner-container
{:flex-direction :row
:justify-content :space-between})
(def section-label-right
@ -25,20 +21,26 @@
(def section-label-left
{:width 136})
(def network-link
(def network-links-container
{:margin-horizontal -1.5
:z-index 1
:margin-top 7.5
:z-index 3
:flex 1})
(defn network-link-container
[margin-top inverted?]
(cond-> {:position :absolute
:left 0
:right 0
:top margin-top}
inverted?
(assoc :transform [{:scaleY -1}])))
(def empty-container
{:flex-grow 1
:align-items :center
:justify-content :center})
(def add-network
{:margin-top 11
:align-self :flex-end})
(defn warning-container
[color theme]
{:flex-direction :row

View File

@ -6,19 +6,18 @@
[quo.foundations.resources :as resources]
[react-native.core :as rn]
[reagent.core :as reagent]
[status-im.constants :as constants]
[status-im.contexts.wallet.common.utils.networks :as networks-utils]
[status-im.contexts.wallet.common.utils.send :as send-utils]
[status-im.contexts.wallet.common.utils.networks :as network-utils]
[status-im.contexts.wallet.send.routes.style :as style]
[status-im.contexts.wallet.send.utils :as send-utils]
[utils.debounce :as debounce]
[utils.i18n :as i18n]
[utils.re-frame :as rf]
[utils.vector :as vector-utils]))
[utils.re-frame :as rf]))
(def ^:private network-priority-score
{:ethereum 1
:optimism 2
:arbitrum 3})
(def row-height 44)
(def space-between-rows 11)
(def network-link-linear-height 10)
(def network-link-1x-height 56)
(def network-link-2x-height 111)
(defn- make-network-item
[{:keys [network-name chain-id] :as _network}
@ -33,40 +32,16 @@
:checked? (some #(= % chain-id) @network-preferences)
:on-change on-change}})
(defn- find-network-link-insertion-index
[network-links chain-id loading-suggested-routes?]
(let [network (networks-utils/id->network chain-id)
inserted-network-link-priority-score (network-priority-score network)]
(or (->> network-links
(keep-indexed (fn [idx network-link]
(let [network-link (networks-utils/id->network (if loading-suggested-routes?
network-link
(get-in network-link
[:from
:chain-id])))]
(when (> (network-priority-score network-link)
inserted-network-link-priority-score)
idx))))
first)
(count network-links))))
(defn- add-disabled-networks
[network-links disabled-from-networks loading-suggested-routes?]
(let [sorted-networks (sort-by (comp network-priority-score networks-utils/id->network)
disabled-from-networks)]
(reduce (fn [acc-network-links chain-id]
(let [index (find-network-link-insertion-index acc-network-links
chain-id
loading-suggested-routes?)
disabled-network-link {:status :disabled
:chain-id chain-id
:network (networks-utils/id->network chain-id)}]
(vector-utils/insert-element-at acc-network-links disabled-network-link index)))
network-links
sorted-networks)))
(defn fetch-routes
[amount valid-input? bounce-duration-ms]
(if valid-input?
(debounce/debounce-and-dispatch
[:wallet/get-suggested-routes {:amount amount}]
bounce-duration-ms)
(rf/dispatch [:wallet/clean-suggested-routes])))
(defn networks-drawer
[{:keys [fetch-routes theme]}]
[{:keys [on-save theme]}]
(let [network-details (rf/sub [:wallet/network-details])
{:keys [color]} (rf/sub [:wallet/current-viewing-account])
selected-networks (rf/sub [:wallet/wallet-send-receiver-networks])
@ -119,188 +94,149 @@
(rf/dispatch [:wallet/update-receiver-networks
@network-preferences])
(rf/dispatch [:hide-bottom-sheet])
(fetch-routes))
(on-save))
:customization-color color}}]])))
(defn route-item
[{:keys [first-item? from-amount to-amount token-symbol from-chain-id to-chain-id from-network
to-network on-press-from-network on-press-to-network status theme fetch-routes disabled?
loading?]}]
(if (= status :add)
[quo/network-bridge
{:status :add
:container-style style/add-network
:on-press #(rf/dispatch [:show-bottom-sheet
{:content (fn [] [networks-drawer
{:theme theme
:fetch-routes fetch-routes}])}])}]
[rn/view {:style (style/routes-inner-container first-item?)}
[quo/network-bridge
{:amount (str from-amount " " token-symbol)
:network from-network
:status status
:on-press #(when (and on-press-from-network (not loading?))
(on-press-from-network from-chain-id from-amount))}]
(if (= status :default)
[quo/network-link
{:shape :linear
:source from-network
:destination to-network
:container-style style/network-link}]
[rn/view {:style {:flex 1}}])
[quo/network-bridge
{:amount (str to-amount " " token-symbol)
:network to-network
:status (cond
(and disabled? loading?) :loading
(and disabled? (not loading?)) :default
:else status)
:on-press #(when (and on-press-to-network (not loading?))
(on-press-to-network to-chain-id to-amount))}]]))
(defn render-network-values
[{:keys [network-values token-symbol on-press theme on-save to? loading-suggested-routes?]}]
[rn/view
(map-indexed (fn [index {:keys [chain-id total-amount type]}]
[rn/view
{:key (str (if to? "to" "from") "-" chain-id)
:style {:margin-top (if (pos? index) 11 7.5)}}
[quo/network-bridge
{:amount (str total-amount " " token-symbol)
:network (network-utils/id->network chain-id)
:status type
:on-press #(when (not loading-suggested-routes?)
(cond
(= type :add)
(rf/dispatch [:show-bottom-sheet
{:content (fn []
[networks-drawer
{:theme theme
:on-save on-save}])}])
on-press (on-press chain-id total-amount)))}]])
network-values)])
(defn- render-network-link
[item index _
{:keys [from-values-by-chain to-values-by-chain theme fetch-routes on-press-from-network
on-press-to-network token-symbol loading-suggested-routes?]}]
(let [first-item? (zero? index)
disabled-network? (= (:status item) :disabled)
from-chain-id (get-in item [:from :chain-id])
to-chain-id (get-in item [:to :chain-id])
from-amount (when from-chain-id
(from-values-by-chain from-chain-id))
to-amount (when to-chain-id
(to-values-by-chain to-chain-id))]
[route-item
{:first-item? first-item?
:from-amount (if disabled-network? 0 from-amount)
:to-amount (if disabled-network? 0 to-amount)
:token-symbol token-symbol
:disabled? disabled-network?
:loading? loading-suggested-routes?
:theme theme
:fetch-routes fetch-routes
:status (cond
(= (:status item) :add) :add
(= (:status item) :disabled) :disabled
loading-suggested-routes? :loading
:else :default)
:from-chain-id (or from-chain-id (:chain-id item))
:to-chain-id (or to-chain-id (:chain-id item))
:from-network (cond (and loading-suggested-routes?
(not disabled-network?))
(networks-utils/id->network item)
disabled-network?
(networks-utils/id->network (:chain-id
item))
:else
(networks-utils/id->network from-chain-id))
:to-network (cond (and loading-suggested-routes?
(not disabled-network?))
(networks-utils/id->network item)
disabled-network?
(networks-utils/id->network (:chain-id
item))
:else
(networks-utils/id->network to-chain-id))
:on-press-from-network on-press-from-network
:on-press-to-network on-press-to-network}]))
(defn render-network-links
[{:keys [network-links sender-network-values]}]
[rn/view {:style style/network-links-container}
(map
(fn [{:keys [from-chain-id to-chain-id position-diff]}]
(let [position-diff-absolute (js/Math.abs position-diff)
shape (case position-diff-absolute
0 :linear
1 :1x
2 :2x)
height (case position-diff-absolute
0 network-link-linear-height
1 network-link-1x-height
2 network-link-2x-height)
inverted? (neg? position-diff)
source (network-utils/id->network from-chain-id)
destination (network-utils/id->network to-chain-id)
from-chain-id-index (first (keep-indexed #(when (= from-chain-id (:chain-id %2)) %1)
sender-network-values))
base-margin-top (* (+ row-height space-between-rows)
from-chain-id-index)
margin-top (if (zero? position-diff)
(+ base-margin-top
(- (/ row-height 2) (/ height 2)))
(+ base-margin-top
(- (/ row-height 2) height)
(if inverted? height 0)))]
[rn/view
{:key (str "from-" from-chain-id "-to-" to-chain-id)
:style (style/network-link-container margin-top inverted?)}
[rn/view {:style {:flex 1}}
[quo/network-link
{:shape shape
:source source
:destination destination}]]]))
network-links)])
(defn fetch-routes
[amount valid-input? bounce-duration-ms]
(if valid-input?
(debounce/debounce-and-dispatch
[:wallet/get-suggested-routes {:amount amount}]
bounce-duration-ms)
(rf/dispatch [:wallet/clean-suggested-routes])))
(defn disable-chain
[chain-id disabled-from-chain-ids token-available-networks-for-suggested-routes]
(let [disabled-chain-ids
(if (contains? (set
disabled-from-chain-ids)
chain-id)
(vec (remove #(= % chain-id)
disabled-from-chain-ids))
(conj disabled-from-chain-ids
chain-id))
re-enabling-chain?
(< (count disabled-chain-ids)
(count disabled-from-chain-ids))]
(if (or re-enabling-chain?
(> (count token-available-networks-for-suggested-routes) 1))
(rf/dispatch [:wallet/disable-from-networks
disabled-chain-ids])
(rf/dispatch [:toasts/upsert
{:id :disable-chain-error
:type :negative
:text (i18n/label :t/at-least-one-network-must-be-activated)}]))))
(defn view
[{:keys [token theme input-value valid-input?
on-press-to-network current-screen-id]}]
(let [token-symbol (:symbol token)
nav-current-screen-id (rf/sub [:view-id])
active-screen? (= nav-current-screen-id current-screen-id)
loading-suggested-routes? (rf/sub
[:wallet/wallet-send-loading-suggested-routes?])
from-values-by-chain (rf/sub
[:wallet/wallet-send-from-values-by-chain])
to-values-by-chain (rf/sub [:wallet/wallet-send-to-values-by-chain])
suggested-routes (rf/sub [:wallet/wallet-send-suggested-routes])
selected-networks (rf/sub [:wallet/wallet-send-receiver-networks])
disabled-from-chain-ids (rf/sub
[:wallet/wallet-send-disabled-from-chain-ids])
routes (when suggested-routes
(or (:best suggested-routes) []))
(let [token-symbol (:symbol token)
nav-current-screen-id (rf/sub [:view-id])
active-screen? (= nav-current-screen-id current-screen-id)
loading-suggested-routes? (rf/sub
[:wallet/wallet-send-loading-suggested-routes?])
sender-network-values (rf/sub
[:wallet/wallet-send-sender-network-values])
receiver-network-values (rf/sub
[:wallet/wallet-send-receiver-network-values])
network-links (rf/sub [:wallet/wallet-send-network-links])
disabled-from-chain-ids (rf/sub
[:wallet/wallet-send-disabled-from-chain-ids])
{token-balances-per-chain :balances-per-chain} (rf/sub
[:wallet/current-viewing-account-tokens-filtered
(str token-symbol)])
affordable-networks (send-utils/find-affordable-networks
{:balances-per-chain token-balances-per-chain
:input-value input-value
:selected-networks selected-networks
:disabled-chain-ids disabled-from-chain-ids})
network-links (if loading-suggested-routes?
affordable-networks
routes)
show-routes? (or (and (not-empty affordable-networks)
loading-suggested-routes?)
(not-empty routes))]
token-available-networks-for-suggested-routes
(send-utils/token-available-networks-for-suggested-routes
{:balances-per-chain token-balances-per-chain
:disabled-chain-ids disabled-from-chain-ids})
show-routes? (not-empty sender-network-values)]
(rn/use-effect
#(when (and active-screen? (> (count affordable-networks) 0))
#(when (and active-screen? (> (count token-available-networks-for-suggested-routes) 0))
(fetch-routes input-value valid-input? 2000))
[input-value valid-input?])
(rn/use-effect
#(when (and active-screen? (> (count affordable-networks) 0))
#(when (and active-screen? (> (count token-available-networks-for-suggested-routes) 0))
(fetch-routes input-value valid-input? 0))
[disabled-from-chain-ids])
(if show-routes?
(let [initial-network-links-count (count network-links)
disabled-count (count disabled-from-chain-ids)
network-links (if (not-empty disabled-from-chain-ids)
(add-disabled-networks network-links
disabled-from-chain-ids
loading-suggested-routes?)
network-links)
network-links-with-add-button (if (and (< (- (count network-links) disabled-count)
constants/default-network-count)
(pos? initial-network-links-count))
(concat network-links [{:status :add}])
network-links)]
[rn/flat-list
{:data network-links-with-add-button
:content-container-style style/routes-container
:header [rn/view {:style style/routes-header-container}
[quo/section-label
{:section (i18n/label :t/from-label)
:container-style style/section-label-left}]
[quo/section-label
{:section (i18n/label :t/to-label)
:container-style style/section-label-right}]]
:render-data
{:from-values-by-chain from-values-by-chain
:to-values-by-chain to-values-by-chain
:theme theme
:fetch-routes #(fetch-routes % valid-input? 2000)
:on-press-from-network (fn [chain-id _]
(let [disabled-chain-ids (if (contains? (set
disabled-from-chain-ids)
chain-id)
(vec (remove #(= % chain-id)
disabled-from-chain-ids))
(conj disabled-from-chain-ids
chain-id))
re-enabling-chain? (< (count disabled-chain-ids)
(count disabled-from-chain-ids))]
(when (or re-enabling-chain?
(> (count affordable-networks) 1))
(rf/dispatch [:wallet/disable-from-networks
disabled-chain-ids]))))
:on-press-to-network on-press-to-network
:token-symbol token-symbol
:loading-suggested-routes? loading-suggested-routes?}
:render-fn render-network-link}])
[rn/view {:style style/empty-container}
(when (and (not (nil? routes)) (not loading-suggested-routes?))
[quo/text (i18n/label :t/no-routes-found)])])))
[rn/scroll-view {:content-container-style style/routes-container}
(when show-routes?
[rn/view {:style style/routes-header-container}
[quo/section-label
{:section (i18n/label :t/from-label)
:container-style style/section-label-left}]
[quo/section-label
{:section (i18n/label :t/to-label)
:container-style style/section-label-right}]])
[rn/view {:style style/routes-inner-container}
[render-network-values
{:token-symbol token-symbol
:network-values sender-network-values
:on-press #(disable-chain %1
disabled-from-chain-ids
token-available-networks-for-suggested-routes)
:to? false
:theme theme
:loading-suggested-routes? loading-suggested-routes?}]
[render-network-links
{:network-links network-links
:sender-network-values sender-network-values}]
[render-network-values
{:token-symbol token-symbol
:network-values receiver-network-values
:on-press on-press-to-network
:to? true
:loading-suggested-routes? loading-suggested-routes?
:theme theme
:on-save #(fetch-routes input-value valid-input? 0)}]]]))

View File

@ -209,9 +209,7 @@
(defn view
[_]
(let [on-close (fn []
(rf/dispatch [:wallet/clean-suggested-routes])
(rf/dispatch [:navigate-back]))]
(let [on-close #(rf/dispatch [:navigate-back])]
(fn []
(let [theme (quo.theme/use-theme)
send-transaction-data (rf/sub [:wallet/wallet-send])

View File

@ -2,6 +2,7 @@
(:require
[legacy.status-im.utils.hex :as utils.hex]
[native-module.core :as native-module]
[status-im.contexts.wallet.common.utils.networks :as network-utils]
[utils.money :as money]))
(defn amount-in-hex
@ -24,6 +25,23 @@
{}
transaction-hashes))
(defn calculate-gas-fee
[data]
(let [gas-amount (money/bignumber (get data :gas-amount))
gas-fees (get data :gas-fees)
eip1559-enabled? (get gas-fees :eip-1559-enabled)
optimal-price-gwei (money/bignumber (if eip1559-enabled?
(get gas-fees :max-fee-per-gas-medium)
(get gas-fees :gas-price)))
total-gas-fee-wei (money/mul (money/->wei :gwei optimal-price-gwei) gas-amount)
l1-fee-wei (money/->wei :gwei (get gas-fees :l-1-gas-fee))]
(money/add total-gas-fee-wei l1-fee-wei)))
(defn calculate-full-route-gas-fee
"Sums all the routes fees in wei and then convert the total value to ether"
[route]
(money/wei->ether (reduce money/add (map calculate-gas-fee route))))
(defn network-amounts-by-chain
[{:keys [route token-decimals native-token? to?]}]
(reduce (fn [acc path]
@ -47,3 +65,104 @@
(assoc acc k (if (money/equal-to v 0) "<0.01" v)))
{}
amounts))
(defn token-available-networks-for-suggested-routes
[{:keys [balances-per-chain disabled-chain-ids]}]
(let [disabled-set (set disabled-chain-ids)]
(->> balances-per-chain
(filter (fn [[_ {:keys [chain-id]}]]
(not (contains? disabled-set chain-id))))
(map first))))
(def ^:private network-priority-score
{:ethereum 1
:optimism 2
:arbitrum 3})
(def ^:private available-networks-count
(count (set (keys network-priority-score))))
(defn reset-network-amounts-to-zero
[network-amounts]
(map
(fn [network-amount]
(cond-> network-amount
(= (:type network-amount) :loading)
(assoc :total-amount (money/bignumber "0")
:type :default)))
network-amounts))
(defn network-amounts
[network-values disabled-chain-ids receiver-networks to?]
(let [disabled-set (set disabled-chain-ids)
receiver-networks-set (set receiver-networks)
network-values-keys (set (keys network-values))
routes-found? (pos? (count network-values-keys))
updated-network-values (when routes-found?
(reduce (fn [acc k]
(if (or (contains? network-values-keys k)
(and to?
(not (contains? receiver-networks-set k))))
acc
(assoc acc k (money/bignumber "0"))))
network-values
disabled-chain-ids))]
(cond-> (->> updated-network-values
(map
(fn [[k v]]
{:chain-id k
:total-amount v
:type (if (or to? (not (contains? disabled-set k))) :default :disabled)}))
(sort-by #(get network-priority-score (network-utils/id->network (:chain-id %))))
(filter
#(or (and to?
(or (contains? receiver-networks-set (:chain-id %))
(money/greater-than (:total-amount %) (money/bignumber "0"))))
(not to?)))
(vec))
(and to?
routes-found?
(< (count updated-network-values) available-networks-count))
(conj {:type :add}))))
(defn loading-network-amounts
[valid-networks disabled-chain-ids receiver-networks to?]
(let [disabled-set (set disabled-chain-ids)
receiver-networks-set (set receiver-networks)
receiver-networks-count (count receiver-networks)
valid-networks (concat valid-networks disabled-chain-ids)]
(cond-> (->> valid-networks
(map (fn [k]
(cond-> {:chain-id k
:type (if (or to?
(not (contains? disabled-set k)))
:loading
:disabled)}
(and (not to?) (contains? disabled-set k))
(assoc :total-amount (money/bignumber "0")))))
(sort-by (fn [item]
(get network-priority-score
(network-utils/id->network (:chain-id item)))))
(filter
#(or (and to? (contains? receiver-networks-set (:chain-id %)))
(and (not to?)
(not (contains? disabled-chain-ids (:chain-id %))))))
(vec))
(and to? (< receiver-networks-count available-networks-count)) (conj {:type :add}))))
(defn network-links
[route from-values-by-chain to-values-by-chain]
(reduce (fn [acc path]
(let [from-chain-id (get-in path [:from :chain-id])
to-chain-id (get-in path [:to :chain-id])
from-chain-id-index (first (keep-indexed #(when (= from-chain-id (:chain-id %2)) %1)
from-values-by-chain))
to-chain-id-index (first (keep-indexed #(when (= to-chain-id (:chain-id %2)) %1)
to-values-by-chain))
position-diff (- from-chain-id-index to-chain-id-index)]
(conj acc
{:from-chain-id from-chain-id
:to-chain-id to-chain-id
:position-diff position-diff})))
[]
route))

View File

@ -1,6 +1,7 @@
(ns status-im.contexts.wallet.send.utils-test
(:require [cljs.test :refer [deftest is testing]]
[status-im.contexts.wallet.send.utils :as utils]
[utils.map :as map]
[utils.money :as money]))
(deftest test-amount-in-hex
@ -93,3 +94,350 @@
(doseq [[chain-id exp-value] expected]
(is #(or (= (get result chain-id) exp-value)
(money/equal-to (get result chain-id) exp-value)))))))
(deftest test-calculate-gas-fee
(testing "EIP-1559 transaction without L1 fee"
(let [data {:gas-amount "23487"
:gas-fees {:max-fee-per-gas-medium "2.259274911"
:eip-1559-enabled true
:l-1-gas-fee "0"}}
expected-result (money/bignumber "53063589834657")] ; This is in Wei
(is (money/equal-to (utils/calculate-gas-fee data)
expected-result))))
(testing "EIP-1559 transaction with L1 fee of 60,000 Gwei"
(let [data {:gas-amount "23487"
:gas-fees {:max-fee-per-gas-medium "2.259274911"
:eip-1559-enabled true
:l-1-gas-fee "60000"}}
expected-result (money/bignumber "113063589834657")] ; Added 60,000 Gwei in Wei to the
; previous result
(is (money/equal-to (utils/calculate-gas-fee data)
expected-result))))
(testing "Non-EIP-1559 transaction with specified gas price"
(let [data {:gas-amount "23487"
:gas-fees {:gas-price "2.872721089"
:eip-1559-enabled false
:l-1-gas-fee "0"}}
expected-result (money/bignumber "67471600217343")] ; This is in Wei, for the specified
; gas amount and price
(is (money/equal-to (utils/calculate-gas-fee data)
expected-result)))))
(deftest test-calculate-full-route-gas-fee
(testing "Route with a single EIP-1559 transaction, no L1 fees"
(let [route [{:gas-amount "23487"
:gas-fees {:max-fee-per-gas-medium "2.259274911"
:eip-1559-enabled true
:l-1-gas-fee "0"}}]
expected-result (money/bignumber "0.000053063589834657")] ; The Wei amount for the
; transaction, converted to
; Ether
(is (money/equal-to (utils/calculate-full-route-gas-fee route)
expected-result))))
(testing "Route with two EIP-1559 transactions, no L1 fees"
(let [route [{:gas-amount "23487"
:gas-fees {:max-fee-per-gas-medium "2.259274911"
:eip-1559-enabled true
:l-1-gas-fee "0"}}
{:gas-amount "23487"
:gas-fees {:max-fee-per-gas-medium "2.259274911"
:eip-1559-enabled true
:l-1-gas-fee "0"}}]
expected-result (money/bignumber "0.000106127179669314")] ; Sum of both transactions' Wei
; amounts, converted to Ether
(is (money/equal-to (utils/calculate-full-route-gas-fee route)
expected-result))))
(testing "Route with two EIP-1559 transactions, one with L1 fee of 60,000 Gwei"
(let [route [{:gas-amount "23487"
:gas-fees {:max-fee-per-gas-medium "2.259274911"
:eip-1559-enabled true
:l-1-gas-fee "0"}}
{:gas-amount "23487"
:gas-fees {:max-fee-per-gas-medium "2.259274911"
:eip-1559-enabled true
:l-1-gas-fee "60000"}}]
expected-result (money/bignumber "0.000166127179669314")] ; Added 60,000 Gwei in Wei to
; the previous total and
; converted to Ether
(is (money/equal-to (utils/calculate-full-route-gas-fee route)
expected-result)))))
(deftest test-token-available-networks-for-suggested-routes
(testing "Excludes disabled chain-ids correctly"
(let [balances-per-chain {"1" {:chain-id "1" :balance 100}
"10" {:chain-id "10" :balance 200}
"42161" {:chain-id "42161" :balance 300}}
disabled-chain-ids ["10"]
expected ["1" "42161"]]
(is (= expected
(utils/token-available-networks-for-suggested-routes {:balances-per-chain balances-per-chain
:disabled-chain-ids
disabled-chain-ids})))))
(testing "Returns all chains when no disabled chains are specified"
(let [balances-per-chain {"1" {:chain-id "1" :balance 100}
"10" {:chain-id "10" :balance 200}
"42161" {:chain-id "42161" :balance 300}}
disabled-chain-ids []
expected ["1" "10" "42161"]]
(is (= expected
(utils/token-available-networks-for-suggested-routes {:balances-per-chain balances-per-chain
:disabled-chain-ids
disabled-chain-ids})))))
(testing "Returns empty list when all chains are disabled"
(let [balances-per-chain {"1" {:chain-id "1" :balance 100}
"10" {:chain-id "10" :balance 200}
"42161" {:chain-id "42161" :balance 300}}
disabled-chain-ids ["1" "10" "42161"]
expected []]
(is (= expected
(utils/token-available-networks-for-suggested-routes {:balances-per-chain balances-per-chain
:disabled-chain-ids
disabled-chain-ids})))))
(testing "Handles non-existent chain-ids gracefully"
(let [balances-per-chain {"59144" {:chain-id "59144" :balance 400}}
disabled-chain-ids ["1" "10" "42161"]
expected ["59144"]]
(is (= expected
(utils/token-available-networks-for-suggested-routes {:balances-per-chain balances-per-chain
:disabled-chain-ids
disabled-chain-ids}))))))
(deftest test-reset-network-amounts-to-zero
(testing "Correctly resets loading network amounts to zero and changes type to default"
(let [network-amounts [{:chain-id "1" :total-amount (money/bignumber "100") :type :loading}
{:chain-id "10" :total-amount (money/bignumber "200") :type :default}]
expected [{:chain-id "1" :total-amount (money/bignumber "0") :type :default}
{:chain-id "10" :total-amount (money/bignumber "200") :type :default}]
result (utils/reset-network-amounts-to-zero network-amounts)
comparisons (map #(map/deep-compare %1 %2)
expected
result)]
(is (every? identity comparisons))))
(testing "Leaves non-loading types unchanged"
(let [network-amounts [{:chain-id "1" :total-amount (money/bignumber "100") :type :default}
{:chain-id "10" :total-amount (money/bignumber "0") :type :disabled}]
expected [{:chain-id "1" :total-amount (money/bignumber "100") :type :default}
{:chain-id "10" :total-amount (money/bignumber "0") :type :disabled}]
result (utils/reset-network-amounts-to-zero network-amounts)
comparisons (map #(map/deep-compare %1 %2)
expected
result)]
(is (every? identity comparisons))))
(testing "Processes an empty list without error"
(let [network-amounts []
expected []
result (utils/reset-network-amounts-to-zero network-amounts)
comparisons (map #(map/deep-compare %1 %2)
expected
result)]
(is (every? identity comparisons))))
(testing "Applies transformations to multiple loading entries"
(let [network-amounts [{:chain-id "1" :total-amount (money/bignumber "100") :type :loading}
{:chain-id "10" :total-amount (money/bignumber "200") :type :loading}]
expected [{:chain-id "1" :total-amount (money/bignumber "0") :type :default}
{:chain-id "10" :total-amount (money/bignumber "0") :type :default}]
result (utils/reset-network-amounts-to-zero network-amounts)
comparisons (map #(map/deep-compare %1 %2)
expected
result)]
(is (every? identity comparisons))))
(testing "Mix of loading and non-loading types"
(let [network-amounts [{:chain-id "1" :total-amount (money/bignumber "100") :type :loading}
{:chain-id "10" :total-amount (money/bignumber "200") :type :default}
{:chain-id "42161" :total-amount (money/bignumber "300") :type :loading}
{:chain-id "59144" :total-amount (money/bignumber "0") :type :disabled}]
expected [{:chain-id "1" :total-amount (money/bignumber "0") :type :default}
{:chain-id "10" :total-amount (money/bignumber "200") :type :default}
{:chain-id "42161" :total-amount (money/bignumber "0") :type :default}
{:chain-id "59144" :total-amount (money/bignumber "0") :type :disabled}]
result (utils/reset-network-amounts-to-zero network-amounts)
comparisons (map #(map/deep-compare %1 %2)
expected
result)]
(is (every? identity comparisons)))))
(deftest test-network-amounts
(testing "Handles disabled and receiver networks correctly when to? is true"
(let [network-values {"1" (money/bignumber "100")
"10" (money/bignumber "200")}
disabled-chain-ids ["1"]
receiver-networks ["10"]
to? true
expected [{:chain-id "1"
:total-amount (money/bignumber "100")
:type :default}
{:chain-id "10"
:total-amount (money/bignumber "200")
:type :default}
{:type :add}]
result (utils/network-amounts network-values
disabled-chain-ids
receiver-networks
to?)]
(is (every? identity (map #(map/deep-compare %1 %2) expected result)))))
(testing "Adds default amount for non-disabled non-receiver networks when to? is false"
(let [network-values {"1" (money/bignumber "100")}
disabled-chain-ids ["10"]
receiver-networks []
to? false
expected [{:chain-id "1"
:total-amount (money/bignumber "100")
:type :default}
{:chain-id "10"
:total-amount (money/bignumber "0")
:type :disabled}]
result (utils/network-amounts network-values
disabled-chain-ids
receiver-networks
to?)]
(is (every? identity (map #(map/deep-compare %1 %2) expected result)))))
(testing "Handles empty inputs correctly"
(let [network-values {}
disabled-chain-ids []
receiver-networks []
to? true
expected []
result (utils/network-amounts network-values
disabled-chain-ids
receiver-networks
to?)]
(is (= expected result))))
(testing "Processes case with multiple network interactions"
(let [network-values {"1" (money/bignumber "300")
"10" (money/bignumber "400")
"42161" (money/bignumber "500")}
disabled-chain-ids ["1" "42161"]
receiver-networks ["10"]
to? true
expected [{:chain-id "1"
:total-amount (money/bignumber "300")
:type :default}
{:chain-id "10"
:total-amount (money/bignumber "400")
:type :default}
{:chain-id "42161"
:total-amount (money/bignumber "500")
:type :default}]
result (utils/network-amounts network-values
disabled-chain-ids
receiver-networks
to?)]
(is (every? identity (map #(map/deep-compare %1 %2) expected result))))))
(deftest test-loading-network-amounts
(testing "Assigns :loading type to valid networks except for disabled ones"
(let [valid-networks ["1" "10" "42161"]
disabled-chain-ids ["42161"]
receiver-networks ["1" "10"]
to? true
expected [{:chain-id "1" :type :loading}
{:chain-id "10" :type :loading}
{:type :add}]
result (utils/loading-network-amounts valid-networks
disabled-chain-ids
receiver-networks
to?)
comparisons (map #(map/deep-compare %1 %2)
expected
result)]
(is (every? identity comparisons))))
(testing "Assigns :disabled type with zero total-amount to disabled networks when to? is false"
(let [valid-networks ["1" "10" "42161"]
disabled-chain-ids ["10" "42161"]
receiver-networks ["1"]
to? false
expected [{:chain-id "1" :type :loading}
{:chain-id "10" :type :disabled :total-amount (money/bignumber "0")}
{:chain-id "42161" :type :disabled :total-amount (money/bignumber "0")}]
result (utils/loading-network-amounts valid-networks
disabled-chain-ids
receiver-networks
to?)
comparisons (map #(map/deep-compare %1 %2)
expected
result)]
(is (every? identity comparisons))))
(testing "Filters out networks not in receiver networks when to? is true"
(let [valid-networks ["1" "10" "42161" "59144"]
disabled-chain-ids ["10"]
receiver-networks ["1" "42161"]
to? true
expected [{:chain-id "1" :type :loading}
{:chain-id "42161" :type :loading}]
result (utils/loading-network-amounts valid-networks
disabled-chain-ids
receiver-networks
to?)
comparisons (map #(map/deep-compare %1 %2)
expected
result)]
(is (every? identity comparisons))))
(testing "Appends :add type if receiver network count is less than available networks and to? is true"
(let [valid-networks ["1" "10" "42161"]
disabled-chain-ids ["10"]
receiver-networks ["1"]
to? true
expected [{:chain-id "1" :type :loading}
{:type :add}]
result (utils/loading-network-amounts valid-networks
disabled-chain-ids
receiver-networks
to?)
comparisons (map #(map/deep-compare %1 %2)
expected
result)]
(is (every? identity comparisons)))))
(deftest test-network-links
(testing "Calculates position differences correctly"
(let [route [{:from {:chain-id "1"} :to {:chain-id "42161"}}
{:from {:chain-id "10"} :to {:chain-id "1"}}
{:from {:chain-id "42161"} :to {:chain-id "10"}}]
from-values-by-chain [{:chain-id "1"} {:chain-id "10"} {:chain-id "42161"}]
to-values-by-chain [{:chain-id "42161"} {:chain-id "1"} {:chain-id "10"}]
expected [{:from-chain-id "1" :to-chain-id "42161" :position-diff 0}
{:from-chain-id "10" :to-chain-id "1" :position-diff 0}
{:from-chain-id "42161" :to-chain-id "10" :position-diff 0}]
result (utils/network-links route from-values-by-chain to-values-by-chain)]
(is (= expected result))))
(testing "Handles cases with no position difference"
(let [route [{:from {:chain-id "1"} :to {:chain-id "1"}}]
from-values-by-chain [{:chain-id "1"} {:chain-id "10"} {:chain-id "42161"}]
to-values-by-chain [{:chain-id "1"} {:chain-id "10"} {:chain-id "42161"}]
expected [{:from-chain-id "1" :to-chain-id "1" :position-diff 0}]
result (utils/network-links route from-values-by-chain to-values-by-chain)]
(is (= expected result))))
(testing "Handles empty route"
(let [route []
from-values-by-chain []
to-values-by-chain []
expected []
result (utils/network-links route from-values-by-chain to-values-by-chain)]
(is (= expected result))))
(testing "Verifies negative position differences"
(let [route [{:from {:chain-id "1"} :to {:chain-id "42161"}}]
from-values-by-chain [{:chain-id "1"} {:chain-id "10"} {:chain-id "42161"}]
to-values-by-chain [{:chain-id "1"} {:chain-id "10"} {:chain-id "42161"}]
expected [{:from-chain-id "1" :to-chain-id "42161" :position-diff -2}]
result (utils/network-links route from-values-by-chain to-values-by-chain)]
(is (= expected result)))))

View File

@ -121,6 +121,21 @@
:<- [:wallet/wallet-send]
:-> :suggested-routes)
(rf/reg-sub
:wallet/wallet-send-sender-network-values
:<- [:wallet/wallet-send]
:-> :sender-network-values)
(rf/reg-sub
:wallet/wallet-send-receiver-network-values
:<- [:wallet/wallet-send]
:-> :receiver-network-values)
(rf/reg-sub
:wallet/wallet-send-network-links
:<- [:wallet/wallet-send]
:-> :network-links)
(rf/reg-sub
:wallet/keypairs
:<- [:wallet]

17
src/utils/map.cljs Normal file
View File

@ -0,0 +1,17 @@
(ns utils.map
(:require [utils.money :as money]))
(defn compare-values
"Compares two values, using special handling for BigNumbers and regular equality for others."
[v1 v2]
(cond
(and (money/bignumber? v1) (money/bignumber? v2)) (money/equal-to v1 v2)
:else (= v1 v2)))
(defn deep-compare
"Recursively compare two maps, specially handling BigNumber values within the maps."
[map1 map2]
(and
(= (set (keys map1)) (set (keys map2)))
(every? (fn [k] (compare-values (get map1 k) (get map2 k)))
(keys map1))))

View File

@ -37,6 +37,11 @@
(new BigNumber (normalize (str n)))
(catch :default _ nil))))
(defn bignumber?
"Check if the value is a bignumber."
[x]
(instance? BigNumber x))
(defn greater-than-or-equals
[^js bn1 ^js bn2]
(.greaterThanOrEqualTo bn1 bn2))

View File

@ -2595,5 +2595,6 @@
"this-account-has-no-activity": "This account has no activity",
"this-address-has-activity": "This address has activity",
"scanning-for-activity": "Scanning for activity...",
"send-community-link": "Send community link"
"send-community-link": "Send community link",
"at-least-one-network-must-be-activated": "At least 1 network must be activated"
}