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'
import { Overlay } from 'nft/components/modals/Overlay'
import { subhead } from 'nft/css/common.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 { BagItemStatus, BagStatus } from 'nft/types'
import { BagItemStatus, BagStatus, RouteResponse, TxStateType } from 'nft/types'
import { buildSellObject } from 'nft/utils/buildSellObject'
import { recalculateBagUsingPooledAssets } from 'nft/utils/calcPoolPrice'
import { fetchPrice } from 'nft/utils/fetchPrice'
import { roundAndPluralize } from 'nft/utils/roundAndPluralize'
import { combineBuyItemsWithTxRoute } from 'nft/utils/txRoute/combineItemsWithTxRoute'
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 { useLocation } from 'react-router-dom'
......@@ -122,25 +122,18 @@ const Bag = () => {
const shouldShowBag = isNFTPage && !isNFTSellPage
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 itemsInBag = useMemo(() => {
return recalculateBagUsingPooledAssets(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 [userCanScroll, setUserCanScroll] = useState(false)
const [scrollProgress, setScrollProgress] = useState(0)
......@@ -152,18 +145,6 @@ const Bag = () => {
}
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 = itemsInBag.reduce(
......@@ -189,37 +170,72 @@ const Bag = () => {
return { balance, sufficientBalance }
}, [balanceInEth, totalEthPrice, isConnected])
useEffect(() => {
if (routingData && bagStatus === BagStatus.FETCHING_ROUTE) {
const updatedAssets = combineBuyItemsWithTxRoute(
itemsInBag.map((item) => item.asset),
routingData.route
const purchaseAssets = async (routingData: RouteResponse) => {
if (!provider || !routingData) return
const purchaseResponse = await sendTransaction(
provider?.getSigner(),
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 unavailableAssets = updatedAssets.filter((asset) => asset.isUnavailable)
const unchangedAssets = updatedAssets.filter((asset) => !asset.updatedPriceInfo && !asset.isUnavailable)
const hasReviewedAssets = unchangedAssets.length > 0
const hasAssetsInReview = priceChangedAssets.length > 0
const hasUnavailableAssets = unavailableAssets.length > 0
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 {
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 shouldReview = hasAssetsInReview || hasUnavailableAssets
setItemsInBag([
...unavailableAssets.map((unavailableAsset) => ({
...fetchedUnavailableAssets.map((unavailableAsset) => ({
asset: unavailableAsset,
status: BagItemStatus.UNAVAILABLE,
})),
...priceChangedAssets.map((changedAsset) => ({
...fetchedPriceChangedAssets.map((changedAsset) => ({
asset: changedAsset,
status: BagItemStatus.REVIEWING_PRICE_CHANGE,
})),
...unchangedAssets.map((unchangedAsset) => ({ asset: unchangedAsset, status: BagItemStatus.REVIEWED })),
...fetchedUnchangedAssets.map((unchangedAsset) => ({ asset: unchangedAsset, status: BagItemStatus.REVIEWED })),
])
setLocked(false)
if (hasAssets) {
if (!shouldReview) {
purchaseAssets(data)
setBagStatus(BagStatus.CONFIRMING_IN_WALLET)
} else if (!hasAssetsInReview) setBagStatus(BagStatus.CONFIRM_REVIEW)
else {
......@@ -228,11 +244,14 @@ const Bag = () => {
} else {
setBagStatus(BagStatus.ADDING_TO_BAG)
}
} else if (routingData && bagStatus === BagStatus.FETCHING_FINAL_ROUTE) {
setBagStatus(BagStatus.CONFIRMING_IN_WALLET)
} catch (error) {
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 = itemsInBag
......@@ -251,31 +270,13 @@ const Bag = () => {
useEffect(() => {
const hasAssetsInReview = priceChangedAssets.length > 0
const hasUnavailableAssets = unavailableAssets.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) {
queryClient.setQueryData('assetsRoute', undefined)
if (hasAssets) setBagStatus(BagStatus.CONFIRM_REVIEW)
else setBagStatus(BagStatus.ADDING_TO_BAG)
}
if (bagStatus === BagStatus.FETCHING_FINAL_ROUTE) {
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])
}, [bagStatus, itemsInBag, priceChangedAssets, setBagStatus])
useEffect(() => {
if (bagIsLocked && !isOpen) setModalIsOpen(true)
......@@ -286,6 +287,19 @@ const Bag = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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 scrollHandler = (event: React.UIEvent<HTMLDivElement>) => {
......@@ -346,10 +360,7 @@ const Bag = () => {
totalEthPrice={totalEthPrice}
totalUsdPrice={totalUsdPrice}
bagStatus={bagStatus}
setBagStatus={setBagStatus}
fetchReview={() => {
setBagStatus(BagStatus.FETCHING_ROUTE)
}}
fetchAssets={fetchAssets}
assetsAreInReview={itemsInBag.some((item) => item.status === BagItemStatus.REVIEWING_PRICE_CHANGE)}
/>
)}
......
......@@ -18,8 +18,7 @@ interface BagFooterProps {
totalEthPrice: BigNumber
totalUsdPrice: number | undefined
bagStatus: BagStatus
setBagStatus: (status: BagStatus) => void
fetchReview: () => void
fetchAssets: () => void
assetsAreInReview: boolean
}
......@@ -37,8 +36,7 @@ export const BagFooter = ({
totalEthPrice,
totalUsdPrice,
bagStatus,
setBagStatus,
fetchReview,
fetchAssets,
assetsAreInReview,
}: BagFooterProps) => {
const toggleWalletModal = useToggleWalletModal()
......@@ -86,10 +84,8 @@ export const BagFooter = ({
onClick={() => {
if (!isConnected) {
toggleWalletModal()
} else if (bagStatus === BagStatus.ADDING_TO_BAG) {
fetchReview()
} else if (bagStatus === BagStatus.CONFIRM_REVIEW || bagStatus === BagStatus.WARNING) {
setBagStatus(BagStatus.FETCHING_FINAL_ROUTE)
} else {
fetchAssets()
}
}}
>
......
......@@ -10,6 +10,8 @@ export * from './useSearchHistory'
export * from './useSelectAsset'
export * from './useSellAsset'
export * from './useSellPageState'
export * from './useSendTransaction'
export * from './useSweep'
export * from './useTransactionResponse'
export * from './useWalletBalance'
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 { BagItem, GenieAsset, Markets, UpdatedGenieAsset } from 'nft/types'
import { BagItem, BagItemStatus, GenieAsset, Markets, UpdatedGenieAsset } from 'nft/types'
export const calcPoolPrice = (asset: GenieAsset, position = 0) => {
let amountToBuy: BigNumber = BigNumber.from(0)
......@@ -58,6 +58,9 @@ export const recalculateBagUsingPooledAssets = (uncheckedItemsInBag: BagItem[])
if (
!uncheckedItemsInBag.some(
(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
......
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) => {
// if route asset has id, match by id
......@@ -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) => {
const route = txRoute && txRoute.find((r) => r.action === 'Buy' && isTheSame(item, r.assetOut))
......@@ -25,8 +33,14 @@ export const combineBuyItemsWithTxRoute = (items: GenieAsset[], txRoute?: Routin
}
}
const newPriceInfo = item.updatedPriceInfo ? item.updatedPriceInfo : item.priceInfo
// 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 {
...item,
updatedPriceInfo: route.assetOut.priceInfo,
......@@ -35,6 +49,8 @@ export const combineBuyItemsWithTxRoute = (items: GenieAsset[], txRoute?: Routin
return {
...item,
priceInfo: newPriceInfo,
updatedPriceInfo: 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