Commit 10fe7f52 authored by Jack Short's avatar Jack Short Committed by GitHub

feat: purchasing through bag (#4696)

* feat: purchasing assets from bag

* better state management for bag

* fix: comineItemsWithTxRoute.ts

* fixed purchasing assets in review
parent 4deab755
...@@ -11,16 +11,16 @@ import { BagCloseIcon, LargeBagIcon } from 'nft/components/icons' ...@@ -11,16 +11,16 @@ import { BagCloseIcon, LargeBagIcon } from 'nft/components/icons'
import { Overlay } from 'nft/components/modals/Overlay' import { Overlay } from 'nft/components/modals/Overlay'
import { subhead } from 'nft/css/common.css' import { subhead } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css' import { themeVars } from 'nft/css/sprinkles.css'
import { useBag, useIsMobile, useWalletBalance } from 'nft/hooks' import { useBag, useIsMobile, useSendTransaction, useTransactionResponse, useWalletBalance } from 'nft/hooks'
import { fetchRoute } from 'nft/queries' import { fetchRoute } from 'nft/queries'
import { BagItemStatus, BagStatus } from 'nft/types' import { BagItemStatus, BagStatus, RouteResponse, TxStateType } from 'nft/types'
import { buildSellObject } from 'nft/utils/buildSellObject' import { buildSellObject } from 'nft/utils/buildSellObject'
import { recalculateBagUsingPooledAssets } from 'nft/utils/calcPoolPrice' import { recalculateBagUsingPooledAssets } from 'nft/utils/calcPoolPrice'
import { fetchPrice } from 'nft/utils/fetchPrice' import { fetchPrice } from 'nft/utils/fetchPrice'
import { roundAndPluralize } from 'nft/utils/roundAndPluralize' import { roundAndPluralize } from 'nft/utils/roundAndPluralize'
import { combineBuyItemsWithTxRoute } from 'nft/utils/txRoute/combineItemsWithTxRoute' import { combineBuyItemsWithTxRoute } from 'nft/utils/txRoute/combineItemsWithTxRoute'
import { sortUpdatedAssets } from 'nft/utils/updatedAssets' import { sortUpdatedAssets } from 'nft/utils/updatedAssets'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { useQuery, useQueryClient } from 'react-query' import { useQuery, useQueryClient } from 'react-query'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
...@@ -122,25 +122,18 @@ const Bag = () => { ...@@ -122,25 +122,18 @@ const Bag = () => {
const shouldShowBag = isNFTPage && !isNFTSellPage const shouldShowBag = isNFTPage && !isNFTSellPage
const isMobile = useIsMobile() const isMobile = useIsMobile()
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 queryClient = useQueryClient() const queryClient = useQueryClient()
const itemsInBag = useMemo(() => { const itemsInBag = useMemo(() => {
return recalculateBagUsingPooledAssets(uncheckedItemsInBag) return recalculateBagUsingPooledAssets(uncheckedItemsInBag)
}, [uncheckedItemsInBag]) }, [uncheckedItemsInBag])
const ethSellObject = useMemo(
() =>
buildSellObject(
itemsInBag
.reduce(
(ethTotal, bagItem) => ethTotal.add(BigNumber.from(bagItem.asset.priceInfo.ETHPrice)),
BigNumber.from(0)
)
.toString()
),
[itemsInBag]
)
const [isOpen, setModalIsOpen] = useState(false) const [isOpen, setModalIsOpen] = useState(false)
const [userCanScroll, setUserCanScroll] = useState(false) const [userCanScroll, setUserCanScroll] = useState(false)
const [scrollProgress, setScrollProgress] = useState(0) const [scrollProgress, setScrollProgress] = useState(0)
...@@ -152,18 +145,6 @@ const Bag = () => { ...@@ -152,18 +145,6 @@ const Bag = () => {
} }
const { data: fetchedPriceData } = useQuery(['fetchPrice', {}], () => fetchPrice(), {}) const { data: fetchedPriceData } = useQuery(['fetchPrice', {}], () => fetchPrice(), {})
const { data: routingData, refetch } = useQuery(
['assetsRoute'],
() =>
fetchRoute({
toSell: [ethSellObject],
toBuy: itemsInBag.map((item) => item.asset),
senderAddress: account ?? '',
}),
{
enabled: false,
}
)
const { totalEthPrice, totalUsdPrice } = useMemo(() => { const { totalEthPrice, totalUsdPrice } = useMemo(() => {
const totalEthPrice = itemsInBag.reduce( const totalEthPrice = itemsInBag.reduce(
...@@ -189,37 +170,72 @@ const Bag = () => { ...@@ -189,37 +170,72 @@ const Bag = () => {
return { balance, sufficientBalance } return { balance, sufficientBalance }
}, [balanceInEth, totalEthPrice, isConnected]) }, [balanceInEth, totalEthPrice, isConnected])
useEffect(() => { const purchaseAssets = async (routingData: RouteResponse) => {
if (routingData && bagStatus === BagStatus.FETCHING_ROUTE) { if (!provider || !routingData) return
const updatedAssets = combineBuyItemsWithTxRoute( const purchaseResponse = await sendTransaction(
itemsInBag.map((item) => item.asset), provider?.getSigner(),
routingData.route itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE).map((item) => item.asset),
routingData
) )
if (
purchaseResponse &&
(transactionStateRef.current === TxStateType.Success || transactionStateRef.current === TxStateType.Failed)
) {
setLocked(false)
setModalIsOpen(false)
setTransactionResponse(purchaseResponse)
bagExpanded && toggleBag()
reset()
}
}
const priceChangedAssets = updatedAssets.filter((asset) => asset.updatedPriceInfo).sort(sortUpdatedAssets) const fetchAssets = async () => {
const unavailableAssets = updatedAssets.filter((asset) => asset.isUnavailable) const itemsToBuy = itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE).map((item) => item.asset)
const unchangedAssets = updatedAssets.filter((asset) => !asset.updatedPriceInfo && !asset.isUnavailable) const ethSellObject = buildSellObject(
const hasReviewedAssets = unchangedAssets.length > 0 itemsToBuy
const hasAssetsInReview = priceChangedAssets.length > 0 .reduce((ethTotal, asset) => ethTotal.add(BigNumber.from(asset.priceInfo.ETHPrice)), BigNumber.from(0))
const hasUnavailableAssets = unavailableAssets.length > 0 .toString()
)
didOpenUnavailableAssets && setDidOpenUnavailableAssets(false)
!bagIsLocked && setLocked(true)
setBagStatus(BagStatus.FETCHING_ROUTE)
try {
const data = await queryClient.fetchQuery(['assetsRoute', ethSellObject, itemsToBuy, account], () =>
fetchRoute({
toSell: [ethSellObject],
toBuy: itemsToBuy,
senderAddress: account ?? '',
})
)
const updatedAssets = combineBuyItemsWithTxRoute(itemsToBuy, data.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 hasAssets = hasReviewedAssets || hasAssetsInReview || hasUnavailableAssets
const shouldReview = hasAssetsInReview || hasUnavailableAssets const shouldReview = hasAssetsInReview || hasUnavailableAssets
setItemsInBag([ setItemsInBag([
...unavailableAssets.map((unavailableAsset) => ({ ...fetchedUnavailableAssets.map((unavailableAsset) => ({
asset: unavailableAsset, asset: unavailableAsset,
status: BagItemStatus.UNAVAILABLE, status: BagItemStatus.UNAVAILABLE,
})), })),
...priceChangedAssets.map((changedAsset) => ({ ...fetchedPriceChangedAssets.map((changedAsset) => ({
asset: changedAsset, asset: changedAsset,
status: BagItemStatus.REVIEWING_PRICE_CHANGE, status: BagItemStatus.REVIEWING_PRICE_CHANGE,
})), })),
...unchangedAssets.map((unchangedAsset) => ({ asset: unchangedAsset, status: BagItemStatus.REVIEWED })), ...fetchedUnchangedAssets.map((unchangedAsset) => ({ asset: unchangedAsset, status: BagItemStatus.REVIEWED })),
]) ])
setLocked(false) setLocked(false)
if (hasAssets) { if (hasAssets) {
if (!shouldReview) { if (!shouldReview) {
purchaseAssets(data)
setBagStatus(BagStatus.CONFIRMING_IN_WALLET) setBagStatus(BagStatus.CONFIRMING_IN_WALLET)
} else if (!hasAssetsInReview) setBagStatus(BagStatus.CONFIRM_REVIEW) } else if (!hasAssetsInReview) setBagStatus(BagStatus.CONFIRM_REVIEW)
else { else {
...@@ -228,11 +244,14 @@ const Bag = () => { ...@@ -228,11 +244,14 @@ const Bag = () => {
} else { } else {
setBagStatus(BagStatus.ADDING_TO_BAG) setBagStatus(BagStatus.ADDING_TO_BAG)
} }
} else if (routingData && bagStatus === BagStatus.FETCHING_FINAL_ROUTE) { } catch (error) {
setBagStatus(BagStatus.CONFIRMING_IN_WALLET) setBagStatus(BagStatus.ADDING_TO_BAG)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }
}, [routingData])
useEffect(() => {
useSendTransaction.subscribe((state) => (transactionStateRef.current = state.state))
}, [])
const { unchangedAssets, priceChangedAssets, unavailableAssets, availableItems } = useMemo(() => { const { unchangedAssets, priceChangedAssets, unavailableAssets, availableItems } = useMemo(() => {
const unchangedAssets = itemsInBag const unchangedAssets = itemsInBag
...@@ -251,31 +270,13 @@ const Bag = () => { ...@@ -251,31 +270,13 @@ const Bag = () => {
useEffect(() => { useEffect(() => {
const hasAssetsInReview = priceChangedAssets.length > 0 const hasAssetsInReview = priceChangedAssets.length > 0
const hasUnavailableAssets = unavailableAssets.length > 0
const hasAssets = itemsInBag.length > 0 const hasAssets = itemsInBag.length > 0
if (bagStatus === BagStatus.ADDING_TO_BAG) {
isOpen && setModalIsOpen(false)
queryClient.setQueryData('assetsRoute', undefined)
}
if (bagStatus === BagStatus.FETCHING_ROUTE) {
hasUnavailableAssets && setItemsInBag(itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE))
setLocked(true)
refetch()
}
if (bagStatus === BagStatus.IN_REVIEW && !hasAssetsInReview) { if (bagStatus === BagStatus.IN_REVIEW && !hasAssetsInReview) {
queryClient.setQueryData('assetsRoute', undefined)
if (hasAssets) setBagStatus(BagStatus.CONFIRM_REVIEW) if (hasAssets) setBagStatus(BagStatus.CONFIRM_REVIEW)
else setBagStatus(BagStatus.ADDING_TO_BAG) else setBagStatus(BagStatus.ADDING_TO_BAG)
} }
if (bagStatus === BagStatus.FETCHING_FINAL_ROUTE) { }, [bagStatus, itemsInBag, priceChangedAssets, setBagStatus])
hasUnavailableAssets && setItemsInBag(itemsInBag.filter((item) => item.status !== BagItemStatus.UNAVAILABLE))
didOpenUnavailableAssets && setDidOpenUnavailableAssets(false)
!bagIsLocked && setLocked(true)
refetch()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bagStatus, itemsInBag, priceChangedAssets, unavailableAssets])
useEffect(() => { useEffect(() => {
if (bagIsLocked && !isOpen) setModalIsOpen(true) if (bagIsLocked && !isOpen) setModalIsOpen(true)
...@@ -286,6 +287,19 @@ const Bag = () => { ...@@ -286,6 +287,19 @@ const Bag = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname]) }, [pathname])
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])
const hasAssetsToShow = itemsInBag.length > 0 || unavailableAssets.length > 0 const hasAssetsToShow = itemsInBag.length > 0 || unavailableAssets.length > 0
const scrollHandler = (event: React.UIEvent<HTMLDivElement>) => { const scrollHandler = (event: React.UIEvent<HTMLDivElement>) => {
...@@ -346,10 +360,7 @@ const Bag = () => { ...@@ -346,10 +360,7 @@ const Bag = () => {
totalEthPrice={totalEthPrice} totalEthPrice={totalEthPrice}
totalUsdPrice={totalUsdPrice} totalUsdPrice={totalUsdPrice}
bagStatus={bagStatus} bagStatus={bagStatus}
setBagStatus={setBagStatus} fetchAssets={fetchAssets}
fetchReview={() => {
setBagStatus(BagStatus.FETCHING_ROUTE)
}}
assetsAreInReview={itemsInBag.some((item) => item.status === BagItemStatus.REVIEWING_PRICE_CHANGE)} assetsAreInReview={itemsInBag.some((item) => item.status === BagItemStatus.REVIEWING_PRICE_CHANGE)}
/> />
)} )}
......
...@@ -18,8 +18,7 @@ interface BagFooterProps { ...@@ -18,8 +18,7 @@ interface BagFooterProps {
totalEthPrice: BigNumber totalEthPrice: BigNumber
totalUsdPrice: number | undefined totalUsdPrice: number | undefined
bagStatus: BagStatus bagStatus: BagStatus
setBagStatus: (status: BagStatus) => void fetchAssets: () => void
fetchReview: () => void
assetsAreInReview: boolean assetsAreInReview: boolean
} }
...@@ -37,8 +36,7 @@ export const BagFooter = ({ ...@@ -37,8 +36,7 @@ export const BagFooter = ({
totalEthPrice, totalEthPrice,
totalUsdPrice, totalUsdPrice,
bagStatus, bagStatus,
setBagStatus, fetchAssets,
fetchReview,
assetsAreInReview, assetsAreInReview,
}: BagFooterProps) => { }: BagFooterProps) => {
const toggleWalletModal = useToggleWalletModal() const toggleWalletModal = useToggleWalletModal()
...@@ -86,10 +84,8 @@ export const BagFooter = ({ ...@@ -86,10 +84,8 @@ export const BagFooter = ({
onClick={() => { onClick={() => {
if (!isConnected) { if (!isConnected) {
toggleWalletModal() toggleWalletModal()
} else if (bagStatus === BagStatus.ADDING_TO_BAG) { } else {
fetchReview() fetchAssets()
} else if (bagStatus === BagStatus.CONFIRM_REVIEW || bagStatus === BagStatus.WARNING) {
setBagStatus(BagStatus.FETCHING_FINAL_ROUTE)
} }
}} }}
> >
......
...@@ -10,6 +10,8 @@ export * from './useSearchHistory' ...@@ -10,6 +10,8 @@ export * from './useSearchHistory'
export * from './useSelectAsset' export * from './useSelectAsset'
export * from './useSellAsset' export * from './useSellAsset'
export * from './useSellPageState' export * from './useSellPageState'
export * from './useSendTransaction'
export * from './useSweep' export * from './useSweep'
export * from './useTransactionResponse'
export * from './useWalletBalance' export * from './useWalletBalance'
export * from './useWalletCollections' export * from './useWalletCollections'
import create from 'zustand'
import { devtools } from 'zustand/middleware'
import { TxResponse } from '../types'
type TransactionResponseValue = TxResponse | undefined
type TransactionResponseState = {
transactionResponse: TransactionResponseValue
setTransactionResponse: (txResponse: TransactionResponseValue) => void
}
export const useTransactionResponse = create<TransactionResponseState>()(
devtools(
(set) => ({
transactionResponse: undefined,
setTransactionResponse: (txResponse) =>
set(() => ({
transactionResponse: txResponse,
})),
}),
{ name: 'useTransactionResponse' }
)
)
import { BigNumber } from '@ethersproject/bignumber' import { BigNumber } from '@ethersproject/bignumber'
import { BagItem, GenieAsset, Markets, UpdatedGenieAsset } from 'nft/types' import { BagItem, BagItemStatus, GenieAsset, Markets, UpdatedGenieAsset } from 'nft/types'
export const calcPoolPrice = (asset: GenieAsset, position = 0) => { export const calcPoolPrice = (asset: GenieAsset, position = 0) => {
let amountToBuy: BigNumber = BigNumber.from(0) let amountToBuy: BigNumber = BigNumber.from(0)
...@@ -58,6 +58,9 @@ export const recalculateBagUsingPooledAssets = (uncheckedItemsInBag: BagItem[]) ...@@ -58,6 +58,9 @@ export const recalculateBagUsingPooledAssets = (uncheckedItemsInBag: BagItem[])
if ( if (
!uncheckedItemsInBag.some( !uncheckedItemsInBag.some(
(item) => item.asset.marketplace === Markets.NFTX || item.asset.marketplace === Markets.NFT20 (item) => item.asset.marketplace === Markets.NFTX || item.asset.marketplace === Markets.NFT20
) ||
uncheckedItemsInBag.every(
(item) => item.status === BagItemStatus.REVIEWED || item.status === BagItemStatus.REVIEWING_PRICE_CHANGE
) )
) )
return uncheckedItemsInBag return uncheckedItemsInBag
......
import { BuyItem, GenieAsset, PriceInfo, RoutingItem, UpdatedGenieAsset } from '../../types' import { BuyItem, GenieAsset, PriceInfo, RoutingItem, UpdatedGenieAsset } from 'nft/types'
import { formatWeiToDecimal } from 'nft/utils/currency'
const isTheSame = (item: GenieAsset, routeAsset: BuyItem | PriceInfo) => { const isTheSame = (item: GenieAsset, routeAsset: BuyItem | PriceInfo) => {
// if route asset has id, match by id // if route asset has id, match by id
...@@ -13,7 +14,14 @@ const isTheSame = (item: GenieAsset, routeAsset: BuyItem | PriceInfo) => { ...@@ -13,7 +14,14 @@ const isTheSame = (item: GenieAsset, routeAsset: BuyItem | PriceInfo) => {
} }
} }
export const combineBuyItemsWithTxRoute = (items: GenieAsset[], txRoute?: RoutingItem[]): UpdatedGenieAsset[] => { const isPriceDiff = (oldPrice: string, newPrice: string) => {
return formatWeiToDecimal(oldPrice) !== formatWeiToDecimal(newPrice)
}
export const combineBuyItemsWithTxRoute = (
items: UpdatedGenieAsset[],
txRoute?: RoutingItem[]
): UpdatedGenieAsset[] => {
return items.map((item) => { return items.map((item) => {
const route = txRoute && txRoute.find((r) => r.action === 'Buy' && isTheSame(item, r.assetOut)) const route = txRoute && txRoute.find((r) => r.action === 'Buy' && isTheSame(item, r.assetOut))
...@@ -25,8 +33,14 @@ export const combineBuyItemsWithTxRoute = (items: GenieAsset[], txRoute?: Routin ...@@ -25,8 +33,14 @@ export const combineBuyItemsWithTxRoute = (items: GenieAsset[], txRoute?: Routin
} }
} }
const newPriceInfo = item.updatedPriceInfo ? item.updatedPriceInfo : item.priceInfo
// if the price changed // if the price changed
if (route && 'priceInfo' in route.assetOut && item.priceInfo.basePrice !== route.assetOut.priceInfo.basePrice) { if (
route &&
'priceInfo' in route.assetOut &&
isPriceDiff(newPriceInfo.basePrice, route.assetOut.priceInfo.basePrice)
) {
return { return {
...item, ...item,
updatedPriceInfo: route.assetOut.priceInfo, updatedPriceInfo: route.assetOut.priceInfo,
...@@ -35,6 +49,8 @@ export const combineBuyItemsWithTxRoute = (items: GenieAsset[], txRoute?: Routin ...@@ -35,6 +49,8 @@ export const combineBuyItemsWithTxRoute = (items: GenieAsset[], txRoute?: Routin
return { return {
...item, ...item,
priceInfo: newPriceInfo,
updatedPriceInfo: undefined,
orderSource: route && 'orderSource' in route.assetOut ? route.assetOut.orderSource : undefined, orderSource: route && 'orderSource' in route.assetOut ? route.assetOut.orderSource : undefined,
} }
}) })
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment