chore: refactoring bag (#6039)

* chore: refactoring bag component to sub components (#6027)

* moving totalEthPrice to hook

* moving everything from bag to bag footer

* moving transaction state to sep hook

* explicit type

* itemsInBag

* fixing transaction tracking

* chore: refactor useFetchAssets to make it more readable (#6043)

* chore: refactoring useFetchAssets to make it more readable

* remvoing eslint stuff

* extracting what can be

* comments

* removing feature flag

* changing return type of useUsd hook

* zustand shallow
This commit is contained in:
Jack Short 2023-03-16 14:39:50 -04:00 committed by GitHub
parent 783f42abcc
commit a362f8797a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 352 additions and 296 deletions

@ -1,5 +1,4 @@
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags' import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { GqlRoutingVariant, useGqlRoutingFlag } from 'featureFlags/flags/gqlRouting'
import { NftGraphqlVariant, useNftGraphqlFlag } from 'featureFlags/flags/nftlGraphql' import { NftGraphqlVariant, useNftGraphqlFlag } from 'featureFlags/flags/nftlGraphql'
import { PayWithAnyTokenVariant, usePayWithAnyTokenFlag } from 'featureFlags/flags/payWithAnyToken' import { PayWithAnyTokenVariant, usePayWithAnyTokenFlag } from 'featureFlags/flags/payWithAnyToken'
import { SwapWidgetVariant, useSwapWidgetFlag } from 'featureFlags/flags/swapWidget' import { SwapWidgetVariant, useSwapWidgetFlag } from 'featureFlags/flags/swapWidget'
@ -218,12 +217,6 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.swapWidget} featureFlag={FeatureFlag.swapWidget}
label="Swap Widget" label="Swap Widget"
/> />
<FeatureFlagOption
variant={GqlRoutingVariant}
value={useGqlRoutingFlag()}
featureFlag={FeatureFlag.gqlRouting}
label="GraphQL NFT Routing"
/>
<FeatureFlagOption <FeatureFlagOption
variant={NftGraphqlVariant} variant={NftGraphqlVariant}
value={useNftGraphqlFlag()} value={useNftGraphqlFlag()}

@ -6,7 +6,6 @@ export enum FeatureFlag {
permit2 = 'permit2', permit2 = 'permit2',
payWithAnyToken = 'payWithAnyToken', payWithAnyToken = 'payWithAnyToken',
swapWidget = 'swap_widget_replacement_enabled', swapWidget = 'swap_widget_replacement_enabled',
gqlRouting = 'gqlRouting',
statsigDummy = 'web_dummy_gate_amplitude_id', statsigDummy = 'web_dummy_gate_amplitude_id',
nftGraphql = 'nft_graphql_migration', nftGraphql = 'nft_graphql_migration',
taxService = 'tax_service_banner', taxService = 'tax_service_banner',

@ -1,7 +0,0 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useGqlRoutingFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.gqlRouting, BaseVariant.Enabled)
}
export { BaseVariant as GqlRoutingVariant }

@ -1,38 +1,16 @@
import { BigNumber } from '@ethersproject/bignumber'
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent } from '@uniswap/analytics' import { sendAnalyticsEvent } from '@uniswap/analytics'
import { NFTEventName } from '@uniswap/analytics-events' import { NFTEventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { GqlRoutingVariant, useGqlRoutingFlag } from 'featureFlags/flags/gqlRouting'
import { useNftRouteLazyQuery } from 'graphql/data/__generated__/types-and-hooks'
import { useIsNftDetailsPage, useIsNftPage, useIsNftProfilePage } from 'hooks/useIsNftPage' import { useIsNftDetailsPage, useIsNftPage, useIsNftProfilePage } from 'hooks/useIsNftPage'
import { BagFooter } from 'nft/components/bag/BagFooter' import { BagFooter } from 'nft/components/bag/BagFooter'
import { Box } from 'nft/components/Box' import { Box } from 'nft/components/Box'
import { Portal } from 'nft/components/common/Portal' import { Portal } from 'nft/components/common/Portal'
import { Column } from 'nft/components/Flex' import { Column } from 'nft/components/Flex'
import { Overlay } from 'nft/components/modals/Overlay' import { Overlay } from 'nft/components/modals/Overlay'
import { import { useBag, useIsMobile, useProfilePageState, useSellAsset } from 'nft/hooks'
useBag, import { BagStatus, ProfilePageStateType } from 'nft/types'
useIsMobile, import { formatAssetEventProperties, recalculateBagUsingPooledAssets } from 'nft/utils'
useProfilePageState, import { useCallback, useEffect, useMemo, useState } from 'react'
useSellAsset,
useSendTransaction,
useTransactionResponse,
} from 'nft/hooks'
import { useTokenInput } from 'nft/hooks/useTokenInput'
import { fetchRoute } from 'nft/queries'
import { BagItemStatus, BagStatus, ProfilePageStateType, RouteResponse, TxStateType } from 'nft/types'
import {
buildNftTradeInputFromBagItems,
buildSellObject,
formatAssetEventProperties,
recalculateBagUsingPooledAssets,
sortUpdatedAssets,
} from 'nft/utils'
import { buildRouteResponse } from 'nft/utils/nftRoute'
import { combineBuyItemsWithTxRoute } from 'nft/utils/txRoute/combineItemsWithTxRoute'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useQueryClient } from 'react-query'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { Z_INDEX } from 'theme/zIndex' import { Z_INDEX } from 'theme/zIndex'
import { shallow } from 'zustand/shallow' import { shallow } from 'zustand/shallow'
@ -120,8 +98,6 @@ const ScrollingIndicator = ({ top, show }: SeparatorProps) => (
) )
const Bag = () => { const Bag = () => {
const { account, provider } = useWeb3React()
const { resetSellAssets, sellAssets } = useSellAsset( const { resetSellAssets, sellAssets } = useSellAsset(
({ reset, sellAssets }) => ({ ({ reset, sellAssets }) => ({
resetSellAssets: reset, resetSellAssets: reset,
@ -132,36 +108,16 @@ const Bag = () => {
const { setProfilePageState } = useProfilePageState(({ setProfilePageState }) => ({ setProfilePageState })) const { setProfilePageState } = useProfilePageState(({ setProfilePageState }) => ({ setProfilePageState }))
const { const { bagStatus, bagIsLocked, reset, bagExpanded, toggleBag, setBagExpanded } = useBag(
bagStatus, (state) => ({ ...state, bagIsLocked: state.isLocked, uncheckedItemsInBag: state.itemsInBag }),
setBagStatus, shallow
didOpenUnavailableAssets, )
setDidOpenUnavailableAssets,
bagIsLocked,
setLocked,
reset,
setItemsInBag,
bagExpanded,
toggleBag,
setTotalEthPrice,
setBagExpanded,
} = useBag((state) => ({ ...state, bagIsLocked: state.isLocked, uncheckedItemsInBag: state.itemsInBag }), shallow)
const { uncheckedItemsInBag } = useBag(({ itemsInBag }) => ({ uncheckedItemsInBag: itemsInBag })) const { uncheckedItemsInBag } = useBag(({ itemsInBag }) => ({ uncheckedItemsInBag: itemsInBag }))
const isProfilePage = useIsNftProfilePage() const isProfilePage = useIsNftProfilePage()
const isDetailsPage = useIsNftDetailsPage() const isDetailsPage = useIsNftDetailsPage()
const isNFTPage = useIsNftPage() const isNFTPage = useIsNftPage()
const isMobile = useIsMobile() const isMobile = useIsMobile()
const usingGqlRouting = useGqlRoutingFlag() === GqlRoutingVariant.Enabled
const sendTransaction = useSendTransaction((state) => state.sendTransaction)
const transactionState = useSendTransaction((state) => state.state)
const setTransactionState = useSendTransaction((state) => state.setState)
const transactionStateRef = useRef(transactionState)
const [setTransactionResponse] = useTransactionResponse((state) => [state.setTransactionResponse])
const tokenTradeInput = useTokenInput((state) => state.tokenTradeInput)
const queryClient = useQueryClient()
const itemsInBag = useMemo(() => recalculateBagUsingPooledAssets(uncheckedItemsInBag), [uncheckedItemsInBag]) const itemsInBag = useMemo(() => recalculateBagUsingPooledAssets(uncheckedItemsInBag), [uncheckedItemsInBag])
@ -175,210 +131,14 @@ const Bag = () => {
} }
} }
const { totalEthPrice } = useMemo(() => {
const totalEthPrice = itemsInBag.reduce(
(total, item) =>
item.status !== BagItemStatus.UNAVAILABLE
? total.add(
BigNumber.from(
item.asset.updatedPriceInfo ? item.asset.updatedPriceInfo.ETHPrice : item.asset.priceInfo.ETHPrice
)
)
: total,
BigNumber.from(0)
)
return { totalEthPrice }
}, [itemsInBag])
const purchaseAssets = async (routingData: RouteResponse, purchasingWithErc20: boolean) => {
if (!provider || !routingData) return
const purchaseResponse = await sendTransaction(
provider?.getSigner(),
itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE).map((item) => item.asset),
routingData,
purchasingWithErc20
)
if (
purchaseResponse &&
(transactionStateRef.current === TxStateType.Success || transactionStateRef.current === TxStateType.Failed)
) {
setLocked(false)
setModalIsOpen(false)
setTransactionResponse(purchaseResponse)
setBagExpanded({ bagExpanded: false })
reset()
}
}
const handleCloseBag = useCallback(() => { const handleCloseBag = useCallback(() => {
setBagExpanded({ bagExpanded: false, manualClose: true }) setBagExpanded({ bagExpanded: false, manualClose: true })
}, [setBagExpanded]) }, [setBagExpanded])
const [fetchGqlRoute] = useNftRouteLazyQuery()
const fetchAssets = async () => {
const itemsToBuy = itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE).map((item) => item.asset)
const ethSellObject = buildSellObject(
itemsToBuy
.reduce((ethTotal, asset) => ethTotal.add(BigNumber.from(asset.priceInfo.ETHPrice)), BigNumber.from(0))
.toString()
)
didOpenUnavailableAssets && setDidOpenUnavailableAssets(false)
!bagIsLocked && setLocked(true)
setBagStatus(BagStatus.FETCHING_ROUTE)
try {
if (usingGqlRouting) {
fetchGqlRoute({
variables: {
senderAddress: usingGqlRouting && account ? account : '',
nftTrades: usingGqlRouting ? buildNftTradeInputFromBagItems(itemsInBag) : [],
tokenTrades: tokenTradeInput ? tokenTradeInput : undefined,
},
onCompleted: (data) => {
if (!data.nftRoute || !data.nftRoute.route) {
setBagStatus(BagStatus.ADDING_TO_BAG)
setLocked(false)
return
}
const purchasingWithErc20 = !!tokenTradeInput
const { route, routeResponse } = buildRouteResponse(data.nftRoute, purchasingWithErc20)
const { hasPriceAdjustment, updatedAssets } = combineBuyItemsWithTxRoute(itemsToBuy, route)
const shouldRefetchCalldata = hasPriceAdjustment && purchasingWithErc20
const fetchedPriceChangedAssets = updatedAssets
.filter((asset) => asset.updatedPriceInfo)
.sort(sortUpdatedAssets)
const fetchedUnavailableAssets = updatedAssets.filter((asset) => asset.isUnavailable)
const fetchedUnchangedAssets = updatedAssets.filter(
(asset) => !asset.updatedPriceInfo && !asset.isUnavailable
)
const hasReviewedAssets = fetchedUnchangedAssets.length > 0
const hasAssetsInReview = fetchedPriceChangedAssets.length > 0
const hasUnavailableAssets = fetchedUnavailableAssets.length > 0
const hasAssets = hasReviewedAssets || hasAssetsInReview || hasUnavailableAssets
const shouldReview = hasAssetsInReview || hasUnavailableAssets
setItemsInBag([
...fetchedUnavailableAssets.map((unavailableAsset) => ({
asset: unavailableAsset,
status: BagItemStatus.UNAVAILABLE,
})),
...fetchedPriceChangedAssets.map((changedAsset) => ({
asset: changedAsset,
status: BagItemStatus.REVIEWING_PRICE_CHANGE,
})),
...fetchedUnchangedAssets.map((unchangedAsset) => ({
asset: unchangedAsset,
status: BagItemStatus.REVIEWED,
})),
])
let shouldLock = false
if (hasAssets) {
if (!shouldReview) {
if (shouldRefetchCalldata) {
setBagStatus(BagStatus.CONFIRM_QUOTE)
} else {
purchaseAssets(routeResponse, purchasingWithErc20)
setBagStatus(BagStatus.CONFIRMING_IN_WALLET)
shouldLock = true
}
} else if (!hasAssetsInReview) setBagStatus(BagStatus.CONFIRM_REVIEW)
else {
setBagStatus(BagStatus.IN_REVIEW)
}
} else {
setBagStatus(BagStatus.ADDING_TO_BAG)
}
setLocked(shouldLock)
},
})
} else {
const routeData = await queryClient.fetchQuery(['assetsRoute', ethSellObject, itemsToBuy, account], () =>
fetchRoute({
toSell: [ethSellObject],
toBuy: itemsToBuy,
senderAddress: account ?? '',
})
)
const { updatedAssets } = combineBuyItemsWithTxRoute(itemsToBuy, routeData.route)
const fetchedPriceChangedAssets = updatedAssets
.filter((asset) => asset.updatedPriceInfo)
.sort(sortUpdatedAssets)
const fetchedUnavailableAssets = updatedAssets.filter((asset) => asset.isUnavailable)
const fetchedUnchangedAssets = updatedAssets.filter((asset) => !asset.updatedPriceInfo && !asset.isUnavailable)
const hasReviewedAssets = fetchedUnchangedAssets.length > 0
const hasAssetsInReview = fetchedPriceChangedAssets.length > 0
const hasUnavailableAssets = fetchedUnavailableAssets.length > 0
const hasAssets = hasReviewedAssets || hasAssetsInReview || hasUnavailableAssets
const shouldReview = hasAssetsInReview || hasUnavailableAssets
setItemsInBag([
...fetchedUnavailableAssets.map((unavailableAsset) => ({
asset: unavailableAsset,
status: BagItemStatus.UNAVAILABLE,
})),
...fetchedPriceChangedAssets.map((changedAsset) => ({
asset: changedAsset,
status: BagItemStatus.REVIEWING_PRICE_CHANGE,
})),
...fetchedUnchangedAssets.map((unchangedAsset) => ({
asset: unchangedAsset,
status: BagItemStatus.REVIEWED,
})),
])
setLocked(false)
if (hasAssets) {
if (!shouldReview) {
purchaseAssets(routeData, false)
setBagStatus(BagStatus.CONFIRMING_IN_WALLET)
} else if (!hasAssetsInReview) setBagStatus(BagStatus.CONFIRM_REVIEW)
else {
setBagStatus(BagStatus.IN_REVIEW)
}
} else {
setBagStatus(BagStatus.ADDING_TO_BAG)
}
}
} catch (error) {
setBagStatus(BagStatus.ADDING_TO_BAG)
}
}
useEffect(() => {
useSendTransaction.subscribe((state) => (transactionStateRef.current = state.state))
}, [])
useEffect(() => { useEffect(() => {
if (bagIsLocked && !isModalOpen) setModalIsOpen(true) if (bagIsLocked && !isModalOpen) setModalIsOpen(true)
}, [bagIsLocked, isModalOpen]) }, [bagIsLocked, isModalOpen])
useEffect(() => {
if (transactionStateRef.current === TxStateType.Confirming) setBagStatus(BagStatus.PROCESSING_TRANSACTION)
if (transactionStateRef.current === TxStateType.Denied || transactionStateRef.current === TxStateType.Invalid) {
if (transactionStateRef.current === TxStateType.Invalid) setBagStatus(BagStatus.WARNING)
else setBagStatus(BagStatus.CONFIRM_REVIEW)
setTransactionState(TxStateType.New)
setLocked(false)
setModalIsOpen(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [transactionStateRef.current])
useEffect(() => {
setTotalEthPrice(totalEthPrice)
}, [totalEthPrice, setTotalEthPrice])
const hasAssetsToShow = itemsInBag.length > 0 const hasAssetsToShow = itemsInBag.length > 0
const scrollHandler = (event: React.UIEvent<HTMLDivElement>) => { const scrollHandler = (event: React.UIEvent<HTMLDivElement>) => {
@ -422,7 +182,7 @@ const Bag = () => {
{isProfilePage ? <ProfileBagContent /> : <BagContent />} {isProfilePage ? <ProfileBagContent /> : <BagContent />}
</Column> </Column>
{hasAssetsToShow && !isProfilePage && ( {hasAssetsToShow && !isProfilePage && (
<BagFooter totalEthPrice={totalEthPrice} fetchAssets={fetchAssets} eventProperties={eventProperties} /> <BagFooter setModalIsOpen={setModalIsOpen} eventProperties={eventProperties} />
)} )}
{isSellingAssets && isProfilePage && ( {isSellingAssets && isProfilePage && (
<ContinueButton <ContinueButton

@ -20,10 +20,13 @@ import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import { useTokenBalance } from 'lib/hooks/useCurrencyBalance' import { useTokenBalance } from 'lib/hooks/useCurrencyBalance'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useBag } from 'nft/hooks/useBag' import { useBag } from 'nft/hooks/useBag'
import { useBagTotalEthPrice } from 'nft/hooks/useBagTotalEthPrice'
import useDerivedPayWithAnyTokenSwapInfo from 'nft/hooks/useDerivedPayWithAnyTokenSwapInfo' import useDerivedPayWithAnyTokenSwapInfo from 'nft/hooks/useDerivedPayWithAnyTokenSwapInfo'
import { useFetchAssets } from 'nft/hooks/useFetchAssets'
import usePayWithAnyTokenSwap from 'nft/hooks/usePayWithAnyTokenSwap' import usePayWithAnyTokenSwap from 'nft/hooks/usePayWithAnyTokenSwap'
import usePermit2Approval from 'nft/hooks/usePermit2Approval' import usePermit2Approval from 'nft/hooks/usePermit2Approval'
import { PriceImpact, usePriceImpact } from 'nft/hooks/usePriceImpact' import { PriceImpact, usePriceImpact } from 'nft/hooks/usePriceImpact'
import { useSubscribeTransactionState } from 'nft/hooks/useSubscribeTransactionState'
import { useTokenInput } from 'nft/hooks/useTokenInput' import { useTokenInput } from 'nft/hooks/useTokenInput'
import { useWalletBalance } from 'nft/hooks/useWalletBalance' import { useWalletBalance } from 'nft/hooks/useWalletBalance'
import { BagStatus } from 'nft/types' import { BagStatus } from 'nft/types'
@ -272,8 +275,7 @@ const FiatValue = ({
} }
interface BagFooterProps { interface BagFooterProps {
totalEthPrice: BigNumber setModalIsOpen: (open: boolean) => void
fetchAssets: () => void
eventProperties: Record<string, unknown> eventProperties: Record<string, unknown>
} }
@ -284,11 +286,12 @@ const PENDING_BAG_STATUSES = [
BagStatus.PROCESSING_TRANSACTION, BagStatus.PROCESSING_TRANSACTION,
] ]
export const BagFooter = ({ totalEthPrice, fetchAssets, eventProperties }: BagFooterProps) => { export const BagFooter = ({ setModalIsOpen, eventProperties }: BagFooterProps) => {
const toggleWalletModal = useToggleWalletModal() const toggleWalletModal = useToggleWalletModal()
const theme = useTheme() const theme = useTheme()
const { account, chainId, connector } = useWeb3React() const { account, chainId, connector } = useWeb3React()
const connected = Boolean(account && chainId) const connected = Boolean(account && chainId)
const totalEthPrice = useBagTotalEthPrice()
const shouldUsePayWithAnyToken = usePayWithAnyTokenEnabled() const shouldUsePayWithAnyToken = usePayWithAnyTokenEnabled()
const inputCurrency = useTokenInput((state) => state.inputCurrency) const inputCurrency = useTokenInput((state) => state.inputCurrency)
const setInputCurrency = useTokenInput((state) => state.setInputCurrency) const setInputCurrency = useTokenInput((state) => state.setInputCurrency)
@ -297,7 +300,6 @@ export const BagFooter = ({ totalEthPrice, fetchAssets, eventProperties }: BagFo
account ?? undefined, account ?? undefined,
!!inputCurrency && inputCurrency.isToken ? inputCurrency : undefined !!inputCurrency && inputCurrency.isToken ? inputCurrency : undefined
) )
const { const {
isLocked: bagIsLocked, isLocked: bagIsLocked,
bagStatus, bagStatus,
@ -312,13 +314,14 @@ export const BagFooter = ({ totalEthPrice, fetchAssets, eventProperties }: BagFo
}), }),
shallow shallow
) )
const [tokenSelectorOpen, setTokenSelectorOpen] = useState(false) const [tokenSelectorOpen, setTokenSelectorOpen] = useState(false)
const isPending = PENDING_BAG_STATUSES.includes(bagStatus) const isPending = PENDING_BAG_STATUSES.includes(bagStatus)
const activeCurrency = inputCurrency ?? defaultCurrency const activeCurrency = inputCurrency ?? defaultCurrency
const usingPayWithAnyToken = !!inputCurrency && shouldUsePayWithAnyToken && chainId === SupportedChainId.MAINNET const usingPayWithAnyToken = !!inputCurrency && shouldUsePayWithAnyToken && chainId === SupportedChainId.MAINNET
useSubscribeTransactionState(setModalIsOpen)
const fetchAssets = useFetchAssets()
const parsedOutputAmount = useMemo(() => { const parsedOutputAmount = useMemo(() => {
return tryParseCurrencyAmount(formatEther(totalEthPrice.toString()), defaultCurrency ?? undefined) return tryParseCurrencyAmount(formatEther(totalEthPrice.toString()), defaultCurrency ?? undefined)
}, [defaultCurrency, totalEthPrice]) }, [defaultCurrency, totalEthPrice])
@ -374,7 +377,7 @@ export const BagFooter = ({ totalEthPrice, fetchAssets, eventProperties }: BagFo
handleClick, handleClick,
buttonColor, buttonColor,
} = useMemo(() => { } = useMemo(() => {
let handleClick = fetchAssets let handleClick: (() => void) | (() => Promise<void>) = fetchAssets
let buttonText = <Trans>Something went wrong</Trans> let buttonText = <Trans>Something went wrong</Trans>
let disabled = true let disabled = true
let warningText = undefined let warningText = undefined

@ -2,14 +2,15 @@ import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex' import { Column, Row } from 'nft/components/Flex'
import { body, bodySmall } from 'nft/css/common.css' import { body, bodySmall } from 'nft/css/common.css'
import { useBag } from 'nft/hooks' import { useBag } from 'nft/hooks'
import { useBagTotalEthPrice, useBagTotalUsdPrice } from 'nft/hooks/useBagTotalEthPrice'
import { ethNumberStandardFormatter, formatWeiToDecimal, roundAndPluralize } from 'nft/utils' import { ethNumberStandardFormatter, formatWeiToDecimal, roundAndPluralize } from 'nft/utils'
import * as styles from './MobileHoverBag.css' import * as styles from './MobileHoverBag.css'
export const MobileHoverBag = () => { export const MobileHoverBag = () => {
const itemsInBag = useBag((state) => state.itemsInBag) const itemsInBag = useBag((state) => state.itemsInBag)
const toggleBag = useBag((state) => state.toggleBag) const toggleBag = useBag((state) => state.toggleBag)
const totalEthPrice = useBag((state) => state.totalEthPrice) const totalEthPrice = useBagTotalEthPrice()
const totalUsdPrice = useBag((state) => state.totalUsdPrice) const totalUsdPrice = useBagTotalUsdPrice()
const shouldShowBag = itemsInBag.length > 0 const shouldShowBag = itemsInBag.length > 0
@ -47,11 +48,10 @@ export const MobileHoverBag = () => {
{roundAndPluralize(itemsInBag.length, 'NFT')} {roundAndPluralize(itemsInBag.length, 'NFT')}
</Box> </Box>
<Row gap="8"> <Row gap="8">
<Box className={body}>{`${formatWeiToDecimal(totalEthPrice.toString())}`}</Box> <Box className={body}>{`${formatWeiToDecimal(totalEthPrice.toString())}`} ETH</Box>
<Box color="textSecondary" className={bodySmall}>{`${ethNumberStandardFormatter( <Box color="textSecondary" className={bodySmall}>
totalUsdPrice, {ethNumberStandardFormatter(totalUsdPrice, true)}
true </Box>
)}`}</Box>
</Row> </Row>
</Column> </Column>
</Row> </Row>

@ -1,4 +1,3 @@
import { BigNumber } from '@ethersproject/bignumber'
import { NftStandard } from 'graphql/data/__generated__/types-and-hooks' import { NftStandard } from 'graphql/data/__generated__/types-and-hooks'
import { BagItem, BagItemStatus, BagStatus, UpdatedGenieAsset } from 'nft/types' import { BagItem, BagItemStatus, BagStatus, UpdatedGenieAsset } from 'nft/types'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
@ -12,10 +11,6 @@ interface BagState {
setBagStatus: (state: BagStatus) => void setBagStatus: (state: BagStatus) => void
itemsInBag: BagItem[] itemsInBag: BagItem[]
setItemsInBag: (items: BagItem[]) => void setItemsInBag: (items: BagItem[]) => void
totalEthPrice: BigNumber
setTotalEthPrice: (totalEthPrice: BigNumber) => void
totalUsdPrice: number | undefined
setTotalUsdPrice: (totalUsdPrice: number | undefined) => void
addAssetsToBag: (asset: UpdatedGenieAsset[], fromSweep?: boolean) => void addAssetsToBag: (asset: UpdatedGenieAsset[], fromSweep?: boolean) => void
removeAssetsFromBag: (assets: UpdatedGenieAsset[], fromSweep?: boolean) => void removeAssetsFromBag: (assets: UpdatedGenieAsset[], fromSweep?: boolean) => void
markAssetAsReviewed: (asset: UpdatedGenieAsset, toKeep: boolean) => void markAssetAsReviewed: (asset: UpdatedGenieAsset, toKeep: boolean) => void
@ -72,16 +67,6 @@ export const useBag = create<BagState>()(
set(() => ({ set(() => ({
itemsInBag: items, itemsInBag: items,
})), })),
totalEthPrice: BigNumber.from(0),
setTotalEthPrice: (totalEthPrice) =>
set(() => ({
totalEthPrice,
})),
totalUsdPrice: undefined,
setTotalUsdPrice: (totalUsdPrice) =>
set(() => ({
totalUsdPrice,
})),
addAssetsToBag: (assets, fromSweep = false) => addAssetsToBag: (assets, fromSweep = false) =>
set(({ itemsInBag }) => { set(({ itemsInBag }) => {
if (get().isLocked) return { itemsInBag: get().itemsInBag } if (get().isLocked) return { itemsInBag: get().itemsInBag }

@ -0,0 +1,44 @@
import { BigNumber } from '@ethersproject/bignumber'
import { formatEther } from '@ethersproject/units'
import { useCurrency } from 'hooks/Tokens'
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { BagItemStatus } from 'nft/types'
import { useMemo } from 'react'
import { useBag } from './useBag'
export function useBagTotalEthPrice(): BigNumber {
const itemsInBag = useBag((state) => state.itemsInBag)
return useMemo(() => {
const totalEthPrice = itemsInBag.reduce(
(total, item) =>
item.status !== BagItemStatus.UNAVAILABLE
? total.add(
BigNumber.from(
item.asset.updatedPriceInfo ? item.asset.updatedPriceInfo.ETHPrice : item.asset.priceInfo.ETHPrice
)
)
: total,
BigNumber.from(0)
)
return totalEthPrice
}, [itemsInBag])
}
export function useBagTotalUsdPrice(): string | undefined {
const totalEthPrice = useBagTotalEthPrice()
const defaultCurrency = useCurrency('ETH')
const parsedOutputAmount = useMemo(() => {
return tryParseCurrencyAmount(formatEther(totalEthPrice.toString()), defaultCurrency ?? undefined)
}, [defaultCurrency, totalEthPrice])
const usdcValue = useStablecoinValue(parsedOutputAmount)
return useMemo(() => {
return usdcValue?.toExact()
}, [usdcValue])
}

@ -0,0 +1,102 @@
import { useWeb3React } from '@web3-react/core'
import { useNftRouteLazyQuery } from 'graphql/data/__generated__/types-and-hooks'
import { BagStatus } from 'nft/types'
import { buildNftTradeInputFromBagItems, recalculateBagUsingPooledAssets } from 'nft/utils'
import { getNextBagState, getPurchasableAssets } from 'nft/utils/bag'
import { buildRouteResponse } from 'nft/utils/nftRoute'
import { useCallback, useMemo } from 'react'
import { shallow } from 'zustand/shallow'
import { useBag } from './useBag'
import { usePurchaseAssets } from './usePurchaseAssets'
import { useTokenInput } from './useTokenInput'
export function useFetchAssets(): () => Promise<void> {
const { account } = useWeb3React()
const {
itemsInBag: uncheckedItemsInBag,
setBagStatus,
didOpenUnavailableAssets,
setDidOpenUnavailableAssets,
isLocked: bagIsLocked,
setLocked: setBagLocked,
setItemsInBag,
} = useBag(
({
itemsInBag,
setBagStatus,
didOpenUnavailableAssets,
setDidOpenUnavailableAssets,
isLocked,
setLocked,
setItemsInBag,
}) => ({
itemsInBag,
setBagStatus,
didOpenUnavailableAssets,
setDidOpenUnavailableAssets,
isLocked,
setLocked,
setItemsInBag,
}),
shallow
)
const tokenTradeInput = useTokenInput((state) => state.tokenTradeInput)
const itemsInBag = useMemo(() => recalculateBagUsingPooledAssets(uncheckedItemsInBag), [uncheckedItemsInBag])
const [fetchGqlRoute] = useNftRouteLazyQuery()
const purchaseAssets = usePurchaseAssets()
const resetStateBeforeFetch = useCallback(() => {
didOpenUnavailableAssets && setDidOpenUnavailableAssets(false)
!bagIsLocked && setBagLocked(true)
setBagStatus(BagStatus.FETCHING_ROUTE)
}, [bagIsLocked, didOpenUnavailableAssets, setBagLocked, setBagStatus, setDidOpenUnavailableAssets])
return useCallback(async () => {
resetStateBeforeFetch()
fetchGqlRoute({
variables: {
senderAddress: account ? account : '',
nftTrades: buildNftTradeInputFromBagItems(itemsInBag),
tokenTrades: tokenTradeInput ? tokenTradeInput : undefined,
},
onCompleted: (data) => {
if (!data.nftRoute || !data.nftRoute.route) {
setBagStatus(BagStatus.ADDING_TO_BAG)
setBagLocked(false)
return
}
const wishAssetsToBuy = getPurchasableAssets(itemsInBag)
const purchasingWithErc20 = !!tokenTradeInput
const { route, routeResponse } = buildRouteResponse(data.nftRoute, purchasingWithErc20)
const { newBagItems, nextBagStatus } = getNextBagState(wishAssetsToBuy, route, purchasingWithErc20)
setItemsInBag(newBagItems)
setBagStatus(nextBagStatus)
if (nextBagStatus === BagStatus.CONFIRMING_IN_WALLET) {
purchaseAssets(routeResponse, wishAssetsToBuy, purchasingWithErc20)
setBagLocked(true)
return
}
setBagLocked(false)
},
})
}, [
account,
fetchGqlRoute,
itemsInBag,
purchaseAssets,
resetStateBeforeFetch,
setBagLocked,
setBagStatus,
setItemsInBag,
tokenTradeInput,
])
}

@ -0,0 +1,52 @@
import { useWeb3React } from '@web3-react/core'
import { RouteResponse, UpdatedGenieAsset } from 'nft/types'
import { useCallback } from 'react'
import shallow from 'zustand/shallow'
import { useBag } from './useBag'
import { useSendTransaction } from './useSendTransaction'
import { useTransactionResponse } from './useTransactionResponse'
export function usePurchaseAssets(): (
routingData: RouteResponse,
assetsToBuy: UpdatedGenieAsset[],
purchasingWithErc20?: boolean
) => Promise<void> {
const { provider } = useWeb3React()
const sendTransaction = useSendTransaction((state) => state.sendTransaction)
const setTransactionResponse = useTransactionResponse((state) => state.setTransactionResponse)
const {
setLocked: setBagLocked,
setBagExpanded,
reset: resetBag,
} = useBag(
({ setLocked, setBagExpanded, reset }) => ({
setLocked,
setBagExpanded,
reset,
}),
shallow
)
return useCallback(
async (routingData: RouteResponse, assetsToBuy: UpdatedGenieAsset[], purchasingWithErc20 = false) => {
if (!provider) return
const purchaseResponse = await sendTransaction(
provider.getSigner(),
assetsToBuy,
routingData,
purchasingWithErc20
)
if (purchaseResponse) {
setBagLocked(false)
setTransactionResponse(purchaseResponse)
setBagExpanded({ bagExpanded: false })
resetBag()
}
},
[provider, resetBag, sendTransaction, setBagExpanded, setBagLocked, setTransactionResponse]
)
}

@ -12,7 +12,7 @@ import ERC721 from '../../abis/erc721.json'
import ERC1155 from '../../abis/erc1155.json' import ERC1155 from '../../abis/erc1155.json'
import CryptoPunksMarket from '../abis/CryptoPunksMarket.json' import CryptoPunksMarket from '../abis/CryptoPunksMarket.json'
import { GenieAsset, RouteResponse, RoutingItem, TxResponse, TxStateType, UpdatedGenieAsset } from '../types' import { GenieAsset, RouteResponse, RoutingItem, TxResponse, TxStateType, UpdatedGenieAsset } from '../types'
import { combineBuyItemsWithTxRoute } from '../utils/txRoute/combineItemsWithTxRoute' import { compareAssetsWithTransactionRoute } from '../utils/txRoute/combineItemsWithTxRoute'
interface TxState { interface TxState {
state: TxStateType state: TxStateType
@ -147,7 +147,7 @@ const findNFTsPurchased = (
) )
}) })
return combineBuyItemsWithTxRoute(transferredItems, txRoute).updatedAssets return compareAssetsWithTransactionRoute(transferredItems, txRoute).updatedAssets
} }
const findNFTsNotPurchased = (toBuy: GenieAsset[], nftsPurchased: UpdatedGenieAsset[]) => { const findNFTsNotPurchased = (toBuy: GenieAsset[], nftsPurchased: UpdatedGenieAsset[]) => {

@ -0,0 +1,38 @@
import { BagStatus, TxStateType } from 'nft/types'
import { useEffect, useRef } from 'react'
import { shallow } from 'zustand/shallow'
import { useBag } from './useBag'
import { useSendTransaction } from './useSendTransaction'
export function useSubscribeTransactionState(setModalIsOpen: (isOpen: boolean) => void) {
const transactionState = useSendTransaction((state) => state.state)
const setTransactionState = useSendTransaction((state) => state.setState)
const transactionStateRef = useRef(transactionState)
const { setBagStatus, setLocked: setBagLocked } = useBag(
({ setBagExpanded, setBagStatus, setLocked }) => ({
setBagExpanded,
setBagStatus,
setLocked,
}),
shallow
)
useEffect(() => {
useSendTransaction.subscribe((state) => (transactionStateRef.current = state.state))
}, [])
useEffect(() => {
if (transactionStateRef.current === TxStateType.Confirming) setBagStatus(BagStatus.PROCESSING_TRANSACTION)
if (transactionStateRef.current === TxStateType.Denied || transactionStateRef.current === TxStateType.Invalid) {
if (transactionStateRef.current === TxStateType.Invalid) {
setBagStatus(BagStatus.WARNING)
} else setBagStatus(BagStatus.CONFIRM_REVIEW)
setTransactionState(TxStateType.New)
setBagLocked(false)
setModalIsOpen(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setBagLocked, setBagStatus, setModalIsOpen, setTransactionState, transactionStateRef.current])
}

75
src/nft/utils/bag.ts Normal file

@ -0,0 +1,75 @@
import { BagItem, BagItemStatus, BagStatus, RoutingItem, UpdatedGenieAsset } from 'nft/types'
import { compareAssetsWithTransactionRoute } from './txRoute/combineItemsWithTxRoute'
import { filterUpdatedAssetsByState } from './updatedAssets'
export function getPurchasableAssets(itemsInBag: BagItem[]): UpdatedGenieAsset[] {
return itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE).map((item) => item.asset)
}
function createBagFromUpdatedAssets(
unavailable: UpdatedGenieAsset[],
priceChanged: UpdatedGenieAsset[],
unchanged: UpdatedGenieAsset[]
): BagItem[] {
return [
...unavailable.map((unavailableAsset) => ({
asset: unavailableAsset,
status: BagItemStatus.UNAVAILABLE,
})),
...priceChanged.map((changedAsset) => ({
asset: changedAsset,
status: BagItemStatus.REVIEWING_PRICE_CHANGE,
})),
...unchanged.map((unchangedAsset) => ({
asset: unchangedAsset,
status: BagItemStatus.REVIEWED,
})),
]
}
function evaluateNextBagState(
hasAssets: boolean,
shouldReview: boolean,
hasAssetsInReview: boolean,
shouldRefetchCalldata: boolean
): BagStatus {
if (!hasAssets) {
return BagStatus.ADDING_TO_BAG
}
if (shouldReview) {
if (hasAssetsInReview) {
return BagStatus.IN_REVIEW
}
return BagStatus.CONFIRM_REVIEW
}
if (shouldRefetchCalldata) {
return BagStatus.CONFIRM_QUOTE
}
return BagStatus.CONFIRMING_IN_WALLET
}
export function getNextBagState(
wishAssetsToBuy: UpdatedGenieAsset[],
route: RoutingItem[],
purchasingWithErc20: boolean
): { newBagItems: BagItem[]; nextBagStatus: BagStatus } {
const { hasPriceAdjustment, updatedAssets } = compareAssetsWithTransactionRoute(wishAssetsToBuy, route)
const shouldRefetchCalldata = hasPriceAdjustment && purchasingWithErc20
const { unchanged, priceChanged, unavailable } = filterUpdatedAssetsByState(updatedAssets)
const hasAssets = updatedAssets.length > 0
const hasAssetsInReview = priceChanged.length > 0
const hasUnavailableAssets = unavailable.length > 0
const shouldReview = hasAssetsInReview || hasUnavailableAssets
const newBagItems = createBagFromUpdatedAssets(unavailable, priceChanged, unchanged)
const nextBagStatus = evaluateNextBagState(hasAssets, shouldReview, hasAssetsInReview, shouldRefetchCalldata)
return { newBagItems, nextBagStatus }
}

@ -74,7 +74,7 @@ const itemInRouteAndSamePool = (
) )
} }
export const combineBuyItemsWithTxRoute = ( export const compareAssetsWithTransactionRoute = (
items: UpdatedGenieAsset[], items: UpdatedGenieAsset[],
txRoute?: RoutingItem[] txRoute?: RoutingItem[]
): { hasPriceAdjustment: boolean; updatedAssets: UpdatedGenieAsset[] } => { ): { hasPriceAdjustment: boolean; updatedAssets: UpdatedGenieAsset[] } => {

@ -20,3 +20,15 @@ export const getTotalNftValue = (nfts: UpdatedGenieAsset[]): BigNumber => {
) )
) )
} }
export function filterUpdatedAssetsByState(assets: UpdatedGenieAsset[]): {
unchanged: UpdatedGenieAsset[]
priceChanged: UpdatedGenieAsset[]
unavailable: UpdatedGenieAsset[]
} {
const unchanged = assets.filter((asset) => !asset.updatedPriceInfo && !asset.isUnavailable)
const priceChanged = assets.filter((asset) => asset.updatedPriceInfo).sort(sortUpdatedAssets)
const unavailable = assets.filter((asset) => asset.isUnavailable)
return { unchanged, priceChanged, unavailable }
}