Skip to content

Commit

Permalink
feat: update the token list to display the user balances
Browse files Browse the repository at this point in the history
  • Loading branch information
juanmahidalgo committed Nov 27, 2023
1 parent 382c0b6 commit c10c204
Show file tree
Hide file tree
Showing 16 changed files with 661 additions and 88 deletions.
447 changes: 433 additions & 14 deletions webapp/package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"dependencies": {
"@0xsquid/sdk": "^2.8.4",
"@0xsquid/squid-types": "^0.1.29",
"@covalenthq/client-sdk": "^0.6.4",
"@dcl/crypto": "^3.0.0",
"@dcl/schemas": "^9.6.0",
"@dcl/single-sign-on-client": "^0.1.0",
Expand All @@ -20,7 +21,7 @@
"decentraland-connect": "^5.1.0",
"decentraland-crypto-fetch": "^1.0.3",
"decentraland-dapps": "^16.18.0",
"decentraland-transactions": "^1.49.0",
"decentraland-transactions": "file:decentraland-transactions-0.0.0-development3.tgz",
"decentraland-ui": "^4.27.10",
"dotenv": "^10.0.0",
"ethers": "^5.6.8",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { connect } from 'react-redux'
import { replace } from 'connected-react-router'
import { Order } from '@dcl/schemas'
import { openModal } from 'decentraland-dapps/dist/modules/modal/actions'
import { getOpenModals } from 'decentraland-dapps/dist/modules/modal/selectors'
import {
MapDispatchProps,
MapDispatch,
Expand All @@ -18,7 +19,9 @@ import BuyNFTButtons from './BuyNFTButtons'
const mapState = (state: RootState): MapStateProps => ({
wallet: getWallet(state),
isConnecting: isConnecting(state),
isBuyCrossChainEnabled: getIsBuyCrossChainEnabled(state)
isBuyCrossChainEnabled: getIsBuyCrossChainEnabled(state),
isBuyingWithCryptoModalOpen: getOpenModals(state)['BuyNFTWithCryptoModal']
?.open
})

const mapDispatch = (dispatch: MapDispatch): MapDispatchProps => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const BuyNFTButtons = ({
tokenId,
buyWithCardClassName,
isBuyCrossChainEnabled,
isBuyingWithCryptoModalOpen,
onBuyWithCrypto,
onExecuteOrderWithCard,
onBuyItemWithCard,
Expand All @@ -43,13 +44,21 @@ const BuyNFTButtons = ({

const handleBuyWithCrypto = useCallback(
order => {
if (!isConnecting && !wallet) {
if (!isConnecting && !wallet && !isBuyingWithCryptoModalOpen) {
onRedirect(locations.signIn(`${location.pathname}?buyWithCrypto=true`))
} else {
onBuyWithCrypto(asset, order)
}
},
[asset, isConnecting, onBuyWithCrypto, onRedirect, location, wallet]
[
wallet,
asset,
isConnecting,
isBuyingWithCryptoModalOpen,
location.pathname,
onRedirect,
onBuyWithCrypto
]
)

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type Props = {
tokenId?: string
buyWithCardClassName?: string
isBuyCrossChainEnabled: boolean
isBuyingWithCryptoModalOpen: boolean
wallet: Wallet | null
isConnecting: boolean
onBuyWithCrypto: (asset: Asset, order?: Order | null) => void
Expand All @@ -29,7 +30,10 @@ export type Props = {

export type MapStateProps = Pick<
Props,
'isBuyCrossChainEnabled' | 'wallet' | 'isConnecting'
| 'isBuyCrossChainEnabled'
| 'wallet'
| 'isConnecting'
| 'isBuyingWithCryptoModalOpen'
>

export type MapDispatchProps = Pick<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@

.buyWithCryptoModal .durationAndExchangeContainer {
border: 1px solid #716b7c;
margin-top: 24px;
margin: 24px 0;
padding: 16px;
border-radius: 6px;
font-size: 12px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,13 +218,13 @@ export const BuyWithCryptoModal = (props: Props) => {
const routeTotalUSDCost = useMemo(() => {
if (route && routeFeeCost && fromAmount && selectedToken?.usdPrice) {
const { feeCost, gasCost } = routeFeeCost
const tokenPrice = providerTokens.find(
const feeTokenUSDPrice = providerTokens.find(
t => t.symbol === routeFeeCost.token.symbol
)?.usdPrice
return (
tokenPrice! * (Number(gasCost) + Number(feeCost)) +
selectedToken.usdPrice * Number(fromAmount)
)
return feeTokenUSDPrice
? feeTokenUSDPrice * (Number(gasCost) + Number(feeCost)) +
selectedToken.usdPrice * Number(fromAmount)
: undefined
}
}, [fromAmount, providerTokens, route, routeFeeCost, selectedToken.usdPrice])

Expand Down Expand Up @@ -932,15 +932,17 @@ export const BuyWithCryptoModal = (props: Props) => {
<>
{showChainSelector || showTokenSelector ? (
<div>
{showChainSelector ? (
{showChainSelector && wallet ? (
<ChainAndTokenSelector
wallet={wallet}
currentChain={selectedChain}
chains={providerChains}
onSelect={onTokenOrChainSelection}
/>
) : null}
{showTokenSelector ? (
{showTokenSelector && wallet ? (
<ChainAndTokenSelector
wallet={wallet}
currentChain={selectedChain}
tokens={providerTokens}
onSelect={onTokenOrChainSelection}
Expand Down Expand Up @@ -1099,7 +1101,10 @@ export const BuyWithCryptoModal = (props: Props) => {
)}
/>
)}
{!!routeFeeCost ? (
{!!routeFeeCost &&
providerTokens.find(
t => t.symbol === routeFeeCost.token.symbol
)?.usdPrice ? (
<span className={styles.fromAmountUSD}>
≈ $
{(
Expand Down Expand Up @@ -1197,7 +1202,7 @@ export const BuyWithCryptoModal = (props: Props) => {
{shouldUseCrossChainProvider ? (
<>
{' '}
{!!route
{!!route && routeTotalUSDCost
? `$${routeTotalUSDCost?.toFixed(6)}`
: null}{' '}
</>
Expand Down Expand Up @@ -1264,9 +1269,9 @@ export const BuyWithCryptoModal = (props: Props) => {
!isPriceTooLow(price) ? (
<span className={styles.rememberFreeTxs}>
{t('buy_with_crypto_modal.remember_transaction_fee_covered', {
free: (
covered: (
<span className={styles.feeCoveredFree}>
{t('buy_with_crypto_modal.free')}
{t('buy_with_crypto_modal.covered_for_you_by_dao')}
</span>
)
})}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
.chainAndTokenSelector {
margin: 0 -32px;
}

.chainAndTokenSelector .listContainer {
display: flex;
flex-direction: column;
max-height: 550px;
max-height: 432px;
overflow-y: auto;
margin-top: 8px;
padding: 0 32px;
padding-bottom: 16px;
}

.chainAndTokenSelector .searchContainer {
.chainAndTokenSelector .searchContainer,
.chainAndTokenSelector .title {
position: relative;
padding: 0 32px;
}

.chainAndTokenSelector .searchContainer :global(.dcl.close) {
Expand Down Expand Up @@ -58,3 +65,28 @@
top: 0;
right: 0;
}

.chainAndTokenSelector .balance {
color: white;
margin-left: auto;
display: flex;
flex-direction: column;
align-items: flex-end;
}

.chainAndTokenSelector .tokenDataContainer {
display: flex;
}

.chainAndTokenSelector .tokenNameAndSymbolContainer {
display: flex;
flex-direction: column;
font-size: 16px;
}

.chainAndTokenSelector .tokenName {
color: #a3a3a3;
font-weight: normal;
text-transform: capitalize;
font-size: 12px;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { useMemo, useState } from 'react'
import { ethers } from 'ethers'
import { useEffect, useMemo, useState } from 'react'
import { ChainId } from '@dcl/schemas'
import { Wallet } from 'decentraland-dapps/dist/modules/wallet/types'
import { Close, Icon, Loader } from 'decentraland-ui'
import { t } from 'decentraland-dapps/dist/modules/translation/utils'
import { getNetwork } from '@dcl/schemas/dist/dapps/chain-id'
import { Close, Icon } from 'decentraland-ui'
import { marketplaceAPI } from '../../../../modules/vendor/decentraland/marketplace/api'
import { Balance } from '../../../../modules/vendor/decentraland/marketplace/types'
import {
ChainData,
Token
Expand All @@ -12,6 +16,7 @@ import styles from './ChainAndTokenSelector.module.css'
export const CHAIN_AND_TOKEN_SELECTOR_DATA_TEST_ID = 'chain-and-token-selector'

type Props = {
wallet: Wallet
currentChain: ChainId
chains?: ChainData[]
tokens?: Token[]
Expand All @@ -20,7 +25,7 @@ type Props = {

const ChainAndTokenSelector = (props: Props) => {
const [search, setSearch] = useState('')
const { currentChain, chains, tokens, onSelect } = props
const { currentChain, chains, tokens, onSelect, wallet } = props
const title = useMemo(
() =>
t(
Expand All @@ -45,13 +50,40 @@ const ChainAndTokenSelector = (props: Props) => {
)
}, [chains, search])

const [balances, setBalances] = useState<Record<string, Balance>>({})
const [isFetchingBalances, setIsFetchingBalances] = useState(true)

useEffect(() => {
const fetchBalances = async () => {
const balances = await marketplaceAPI.fetchWalletTokenBalances(
currentChain,
wallet.address
)
setIsFetchingBalances(false)
setBalances(
balances.reduce((acc, balance) => {
acc[balance.contract_address] = balance
return acc
}, {} as Record<string, Balance>)
)
}
fetchBalances()
}, [currentChain, wallet.address])

const filteredTokens = useMemo(() => {
return tokens?.filter(
const filtered = tokens?.filter(
token =>
token.symbol.toLowerCase().includes(search.toLowerCase()) &&
token.chainId === currentChain.toString()
)
}, [tokens, search, currentChain])
// this sortes the tokens by USD balance
const sortedByBalance = filtered?.sort((a, b) => {
const aQuote = balances[a.address.toLowerCase()]?.quote ?? '0'
const bQuote = balances[b.address.toLowerCase()]?.quote ?? '0'
return aQuote < bQuote ? 1 : -1
})
return sortedByBalance
}, [tokens, search, currentChain, balances])

return (
<div
Expand All @@ -69,28 +101,60 @@ const ChainAndTokenSelector = (props: Props) => {
{search ? <Close onClick={() => setSearch('')} /> : null}
</div>
<span className={styles.title}>{title}</span>
<div className={styles.listContainer}>
{filteredChains?.map(chain => (
<div
key={chain.chainId}
className={styles.rowItem}
onClick={() => onSelect(chain)}
>
<img src={chain.nativeCurrency.icon} alt={chain.networkName} />
<span>{chain.networkName}</span>
</div>
))}
{filteredTokens?.map(token => (
<div
key={`${token.symbol}-${token.address}`}
className={styles.rowItem}
onClick={() => onSelect(token)}
>
<img src={token.logoURI} alt={token.symbol} />
<span>{token.symbol}</span>
</div>
))}
</div>
{isFetchingBalances ? (
<Loader active size="medium" />
) : (
<div className={styles.listContainer}>
{filteredChains?.map(chain => (
<div
key={chain.chainId}
className={styles.rowItem}
onClick={() => onSelect(chain)}
>
<img src={chain.nativeCurrency.icon} alt={chain.networkName} />
<span>{chain.networkName}</span>
</div>
))}
{filteredTokens?.map(token => (
<div
key={`${token.symbol}-${token.address}`}
className={styles.rowItem}
onClick={() => onSelect(token)}
>
<div className={styles.tokenDataContainer}>
<img src={token.logoURI} alt={token.symbol} />
<div className={styles.tokenNameAndSymbolContainer}>
<span>{token.symbol}</span>
<span className={styles.tokenName}>{token.name}</span>
</div>
</div>
<span className={styles.balance}>
{!!balances[token.address.toLocaleLowerCase()] ? (
<>
{Number(
ethers.utils.formatUnits(
balances[token.address.toLocaleLowerCase()]
.balance as string,
balances[token.address.toLocaleLowerCase()]
.contract_decimals
)
).toLocaleString()}{' '}
<span className={styles.tokenName}>
$
{balances[
token.address.toLocaleLowerCase()
].quote.toLocaleString()}
</span>
</>
) : (
0
)}
</span>
</div>
))}
</div>
)}

{!!search && !filteredChains?.length && !filteredTokens?.length ? (
<span className={styles.noResults}>
{t('buy_with_crypto_modal.token_and_chain_selector.no_matches', {
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/components/Modals/BuyWithCryptoModal/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function formatPrice(price: string | number, token: Token): number {
let decimalsToShow: number

// Show more decimals for smaller fractions of higher-value tokens like Ethereum
if (token.usdPrice && token.usdPrice < 1) {
if (token.usdPrice && token.usdPrice <= 1.5) {
decimalsToShow = 4 // Show 4 decimals for tokens with prices less than 1 USD
} else {
decimalsToShow = 2 // Show 2 decimals for other tokens or higher-value fractions
Expand Down
Loading

0 comments on commit c10c204

Please sign in to comment.