feat: adding sudoswap price calculation (#5252)
* feat: adding sudoswap amm pricing * integrated amm pricing on collection * removing bag recalculation * revisions * typo * adding back recalculating bag * reformatting * bag recalculation for sudoswap * responding to comments
This commit is contained in:
parent
b19e7809ea
commit
44ecc9a203
@ -4,9 +4,7 @@ import { BagRow, PriceChangeBagRow, UnavailableAssetsHeaderRow } from 'nft/compo
|
||||
import { Column } from 'nft/components/Flex'
|
||||
import { useBag, useIsMobile } from 'nft/hooks'
|
||||
import { BagItemStatus, BagStatus } from 'nft/types'
|
||||
import { recalculateBagUsingPooledAssets } from 'nft/utils/calcPoolPrice'
|
||||
import { fetchPrice } from 'nft/utils/fetchPrice'
|
||||
import { formatAssetEventProperties } from 'nft/utils/formatEventProperties'
|
||||
import { fetchPrice, formatAssetEventProperties, recalculateBagUsingPooledAssets } from 'nft/utils'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useQuery } from 'react-query'
|
||||
|
||||
|
@ -34,8 +34,15 @@ import {
|
||||
} from 'nft/hooks'
|
||||
import { useIsCollectionLoading } from 'nft/hooks/useIsCollectionLoading'
|
||||
import { usePriceRange } from 'nft/hooks/usePriceRange'
|
||||
import { DropDownOption, GenieAsset, GenieCollection, Markets, TokenType } from 'nft/types'
|
||||
import { calcPoolPrice, getRarityStatus, pluralize } from 'nft/utils'
|
||||
import { DropDownOption, GenieAsset, GenieCollection, isPooledMarket, Markets, TokenType } from 'nft/types'
|
||||
import {
|
||||
calcPoolPrice,
|
||||
calcSudoSwapPrice,
|
||||
getRarityStatus,
|
||||
isInSameMarketplaceCollection,
|
||||
isInSameSudoSwapPool,
|
||||
pluralize,
|
||||
} from 'nft/utils'
|
||||
import { scrollToTop } from 'nft/utils/scrollToTop'
|
||||
import { applyFiltersFromURL, syncLocalFiltersWithURL } from 'nft/utils/urlParams'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
@ -272,30 +279,38 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
|
||||
|
||||
const getPoolPosition = useCallback(
|
||||
(asset: GenieAsset) => {
|
||||
return itemsInBag.some((item) => asset.tokenId === item.asset.tokenId && asset.address === item.asset.address)
|
||||
const assetInBag = itemsInBag.some(
|
||||
(item) => asset.tokenId === item.asset.tokenId && asset.address === item.asset.address
|
||||
)
|
||||
|
||||
if (asset.marketplace === Markets.Sudoswap) {
|
||||
const bagItemsInSudoSwapPool = itemsInBag.filter((item) => isInSameSudoSwapPool(asset, item.asset))
|
||||
if (assetInBag) {
|
||||
return bagItemsInSudoSwapPool.findIndex((item) => item.asset.tokenId === asset.tokenId)
|
||||
} else {
|
||||
return bagItemsInSudoSwapPool.length
|
||||
}
|
||||
}
|
||||
|
||||
return assetInBag
|
||||
? itemsInBag
|
||||
.filter((item) => item.asset.address === asset.address && item.asset.marketplace === asset.marketplace)
|
||||
.map((item) => item.asset.tokenId)
|
||||
.indexOf(asset.tokenId)
|
||||
: itemsInBag.filter(
|
||||
(item) => item.asset.address === asset.address && item.asset.marketplace === asset.marketplace
|
||||
).length
|
||||
.filter((item) => isInSameMarketplaceCollection(asset, item.asset))
|
||||
.findIndex((item) => item.asset.tokenId === asset.tokenId)
|
||||
: itemsInBag.filter((item) => isInSameMarketplaceCollection(asset, item.asset)).length
|
||||
},
|
||||
[itemsInBag]
|
||||
)
|
||||
|
||||
const calculatePrice = useCallback(
|
||||
(asset: GenieAsset) => {
|
||||
if (asset.marketplace === Markets.Sudoswap) return calcSudoSwapPrice(asset, getPoolPosition(asset))
|
||||
return calcPoolPrice(asset, getPoolPosition(asset))
|
||||
},
|
||||
[getPoolPosition]
|
||||
)
|
||||
|
||||
const collectionAssets = useMemo(() => {
|
||||
if (
|
||||
!collectionNfts ||
|
||||
!collectionNfts.some((asset) => asset.marketplace === Markets.NFTX || asset.marketplace === Markets.NFT20)
|
||||
) {
|
||||
if (!collectionNfts || !collectionNfts.some((asset) => asset.marketplace && isPooledMarket(asset.marketplace))) {
|
||||
return collectionNfts
|
||||
}
|
||||
|
||||
@ -303,8 +318,9 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
|
||||
|
||||
assets.forEach(
|
||||
(asset) =>
|
||||
(asset.marketplace === Markets.NFTX || asset.marketplace === Markets.NFT20) &&
|
||||
(asset.priceInfo.ETHPrice = calculatePrice(asset))
|
||||
asset.marketplace &&
|
||||
isPooledMarket(asset.marketplace) &&
|
||||
(asset.priceInfo.ETHPrice = calculatePrice(asset) ?? '')
|
||||
)
|
||||
|
||||
if (sortBy === SortBy.HighToLow || sortBy === SortBy.LowToHigh) {
|
||||
|
@ -148,12 +148,17 @@ export enum Markets {
|
||||
Uniswap_V2 = 'Uniswap_V2',
|
||||
SushiSwap = 'SushiSwap',
|
||||
SuperRare = 'superrare',
|
||||
Sudoswap = 'sudoswap',
|
||||
KnownOrigin = 'knownorigin',
|
||||
WETH = 'weth',
|
||||
Cryptopunks = 'cryptopunks',
|
||||
CryptoPhunks = 'cryptophunks',
|
||||
}
|
||||
|
||||
export const isPooledMarket = (market: Markets): boolean => {
|
||||
return market === Markets.NFTX || market === Markets.NFT20 || market === Markets.Sudoswap
|
||||
}
|
||||
|
||||
export enum ToolTipType {
|
||||
pool,
|
||||
sus,
|
||||
|
@ -1,142 +0,0 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { BagItem, BagItemStatus, GenieAsset, Markets, UpdatedGenieAsset } from 'nft/types'
|
||||
|
||||
// TODO: a lot of the below typecasting logic can be simplified when GraphQL migration is complete
|
||||
export const calcPoolPrice = (asset: GenieAsset, position = 0) => {
|
||||
let amountToBuy: BigNumber = BigNumber.from(0)
|
||||
let marginalBuy: BigNumber = BigNumber.from(0)
|
||||
if (!asset.sellorders) return ''
|
||||
|
||||
const nft = asset.sellorders[0].protocolParameters
|
||||
const decimals = BigNumber.from(1).mul(10).pow(18)
|
||||
const ammFee = nft?.ammFeePercent ? (100 + (nft.ammFeePercent as number)) * 100 : 110 * 100
|
||||
|
||||
if (asset.marketplace === Markets.NFTX) {
|
||||
const sixteenmul = BigNumber.from(1).mul(10).pow(16)
|
||||
amountToBuy = BigNumber.from(ammFee)
|
||||
.div(100)
|
||||
.mul(position + 1)
|
||||
amountToBuy = amountToBuy.mul(sixteenmul)
|
||||
|
||||
marginalBuy = BigNumber.from(ammFee).div(100).mul(position)
|
||||
marginalBuy = marginalBuy.mul(sixteenmul)
|
||||
}
|
||||
if (asset.marketplace === Markets.NFT20) {
|
||||
amountToBuy = BigNumber.from(100).mul(position + 1)
|
||||
amountToBuy = amountToBuy.mul(decimals)
|
||||
|
||||
marginalBuy = BigNumber.from(100).mul(position)
|
||||
marginalBuy = marginalBuy.mul(decimals)
|
||||
}
|
||||
|
||||
const ethReserves = BigNumber.from(
|
||||
(
|
||||
nft as Record<
|
||||
string,
|
||||
{
|
||||
ethReserves: number
|
||||
}
|
||||
>
|
||||
)?.poolMetadata?.ethReserves?.toLocaleString('fullwide', { useGrouping: false }) ?? 1
|
||||
)
|
||||
const tokenReserves = BigNumber.from(
|
||||
(
|
||||
nft as Record<
|
||||
string,
|
||||
{
|
||||
tokenReserves: number
|
||||
}
|
||||
>
|
||||
)?.poolMetadata?.tokenReserves?.toLocaleString('fullwide', { useGrouping: false }) ?? 1
|
||||
)
|
||||
const numerator = ethReserves.mul(amountToBuy).mul(1000)
|
||||
const denominator = tokenReserves.sub(amountToBuy).mul(997)
|
||||
|
||||
const marginalnumerator = ethReserves.mul(marginalBuy).mul(1000)
|
||||
const marginaldenominator = tokenReserves.sub(marginalBuy).mul(997)
|
||||
|
||||
let price = numerator.div(denominator)
|
||||
const marginalprice = marginalnumerator.div(marginaldenominator)
|
||||
|
||||
price = price.sub(marginalprice)
|
||||
price = price.mul(101).div(100)
|
||||
|
||||
return price.toString()
|
||||
}
|
||||
|
||||
export const calcAvgGroupPoolPrice = (asset: GenieAsset, numberOfAssets: number) => {
|
||||
let total = BigNumber.from(0)
|
||||
|
||||
for (let i = 0; i < numberOfAssets; i++) {
|
||||
const price = BigNumber.from(calcPoolPrice(asset, i))
|
||||
total = total.add(price)
|
||||
}
|
||||
|
||||
return total.div(numberOfAssets).toString()
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
const isPooledMarket = (market: Markets) => market === Markets.NFTX || market === Markets.NFT20
|
||||
|
||||
const itemsInBag = [...uncheckedItemsInBag]
|
||||
const possibleMarkets = itemsInBag.reduce((markets, item) => {
|
||||
const asset = item.asset
|
||||
const market = asset.marketplace
|
||||
if (!market || !isPooledMarket(market)) return markets
|
||||
|
||||
const key = asset.address + asset.marketplace
|
||||
if (Object.keys(markets).includes(key)) {
|
||||
markets[key].push(asset.tokenId)
|
||||
} else {
|
||||
markets[key] = [asset.tokenId]
|
||||
}
|
||||
return markets
|
||||
}, {} as { [key: string]: [string] })
|
||||
|
||||
const updatedPriceMarkets = itemsInBag.reduce((markets, item) => {
|
||||
const asset = item.asset
|
||||
const market = asset.marketplace
|
||||
if (!market || !asset.updatedPriceInfo || !isPooledMarket(market)) return markets
|
||||
|
||||
const key = asset.address + asset.marketplace
|
||||
if (Object.keys(markets).includes(key)) {
|
||||
markets[key] = [markets[key][0] + 1, asset]
|
||||
} else {
|
||||
markets[key] = [1, asset]
|
||||
}
|
||||
return markets
|
||||
}, {} as { [key: string]: [number, UpdatedGenieAsset] })
|
||||
|
||||
const calculatedAvgPoolPrices = Object.keys(updatedPriceMarkets).reduce((prices, key) => {
|
||||
prices[key] = calcAvgGroupPoolPrice(updatedPriceMarkets[key][1], updatedPriceMarkets[key][0])
|
||||
return prices
|
||||
}, {} as { [key: string]: string })
|
||||
|
||||
itemsInBag.forEach((item) => {
|
||||
if (item.asset.marketplace)
|
||||
if (isPooledMarket(item.asset.marketplace)) {
|
||||
const asset = item.asset
|
||||
const isPriceChangedAsset = !!asset.updatedPriceInfo
|
||||
|
||||
const calculatedPrice = isPriceChangedAsset
|
||||
? calculatedAvgPoolPrices[asset.address + asset.marketplace]
|
||||
: calcPoolPrice(asset, possibleMarkets[asset.address + asset.marketplace].indexOf(item.asset.tokenId))
|
||||
|
||||
if (isPriceChangedAsset && item.asset.updatedPriceInfo)
|
||||
item.asset.updatedPriceInfo.ETHPrice = item.asset.updatedPriceInfo.basePrice = calculatedPrice
|
||||
else item.asset.priceInfo.ETHPrice = calculatedPrice
|
||||
}
|
||||
})
|
||||
|
||||
return itemsInBag
|
||||
}
|
24
src/nft/utils/collection.ts
Normal file
24
src/nft/utils/collection.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { GenieAsset } from 'nft/types'
|
||||
|
||||
export const isInSameSudoSwapPool = (assetA: GenieAsset, assetB: GenieAsset): boolean => {
|
||||
if (!assetA.sellorders || !assetB.sellorders) return false
|
||||
|
||||
const assetASudoSwapPoolParameters = assetA.sellorders[0].protocolParameters
|
||||
const assetBSudoSwapPoolParameters = assetB.sellorders[0].protocolParameters
|
||||
|
||||
const assetAPoolAddress = assetASudoSwapPoolParameters?.poolAddress
|
||||
? (assetASudoSwapPoolParameters.poolAddress as string)
|
||||
: undefined
|
||||
const assetBPoolAddress = assetBSudoSwapPoolParameters?.poolAddress
|
||||
? (assetBSudoSwapPoolParameters.poolAddress as string)
|
||||
: undefined
|
||||
|
||||
if (!assetAPoolAddress || !assetBPoolAddress) return false
|
||||
if (assetAPoolAddress !== assetBPoolAddress) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export const isInSameMarketplaceCollection = (assetA: GenieAsset, assetB: GenieAsset): boolean => {
|
||||
return assetA.address === assetB.address && assetA.marketplace === assetB.marketplace
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
export * from './asset'
|
||||
export * from './buildActivityAsset'
|
||||
export * from './buildSellObject'
|
||||
export * from './calcPoolPrice'
|
||||
export * from './carousel'
|
||||
export * from './collection'
|
||||
export * from './currency'
|
||||
export * from './fetchPrice'
|
||||
export * from './formatEventProperties'
|
||||
@ -10,6 +10,7 @@ export * from './isAudio'
|
||||
export * from './isVideo'
|
||||
export * from './listNfts'
|
||||
export * from './numbers'
|
||||
export * from './pooledAssets'
|
||||
export * from './putCommas'
|
||||
export * from './rarity'
|
||||
export * from './roundAndPluralize'
|
||||
|
177
src/nft/utils/pooledAssets.ts
Normal file
177
src/nft/utils/pooledAssets.ts
Normal file
@ -0,0 +1,177 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { BagItem, BagItemStatus, GenieAsset, isPooledMarket, Markets } from 'nft/types'
|
||||
import { isInSameMarketplaceCollection, isInSameSudoSwapPool } from 'nft/utils'
|
||||
|
||||
const PRECISION = '1000000000000000000'
|
||||
const PROTOCOL_FEE_MULTIPLIER = BigNumber.from('5000000000000000')
|
||||
|
||||
enum BondingCurve {
|
||||
Linear = 'LINEAR',
|
||||
Exponential = 'EXPONENTIAL',
|
||||
}
|
||||
|
||||
interface Pool {
|
||||
delta?: string
|
||||
spotPrice?: string
|
||||
fee?: string
|
||||
bondingCurve?: BondingCurve
|
||||
}
|
||||
|
||||
const getPoolParameters = (protocolParameters: Record<string, unknown>): Pool => {
|
||||
return {
|
||||
delta: protocolParameters?.delta ? (protocolParameters.delta as string) : undefined,
|
||||
fee: protocolParameters?.ammFeeFixed ? (protocolParameters.ammFeeFixed as string) : undefined,
|
||||
spotPrice: (protocolParameters as Record<string, { spotPrice?: string }>)?.poolMetadata?.spotPrice,
|
||||
bondingCurve: (protocolParameters as Record<string, { bondingCurve?: BondingCurve }>)?.poolMetadata?.bondingCurve,
|
||||
}
|
||||
}
|
||||
|
||||
const calculateScaledPrice = (currentPrice: BigNumber, poolFee: BigNumber): BigNumber => {
|
||||
const protocolFee = currentPrice.mul(PROTOCOL_FEE_MULTIPLIER).div(BigNumber.from(PRECISION))
|
||||
const tradeFee = currentPrice.mul(poolFee).div(BigNumber.from(PRECISION))
|
||||
return currentPrice.add(protocolFee).add(tradeFee)
|
||||
}
|
||||
|
||||
export const calcSudoSwapPrice = (asset: GenieAsset, position = 0): string | undefined => {
|
||||
if (!asset.sellorders) return undefined
|
||||
|
||||
const sudoSwapParameters = asset.sellorders[0].protocolParameters
|
||||
const sudoSwapPool = getPoolParameters(sudoSwapParameters)
|
||||
|
||||
if (!sudoSwapPool.fee || !sudoSwapPool.delta || !sudoSwapPool.spotPrice || !sudoSwapPool.bondingCurve)
|
||||
return undefined
|
||||
|
||||
let currentPrice = BigNumber.from(sudoSwapPool.spotPrice)
|
||||
const delta = BigNumber.from(sudoSwapPool.delta)
|
||||
const poolFee = BigNumber.from(sudoSwapPool.fee)
|
||||
|
||||
for (let i = 0; i <= position; i++) {
|
||||
if (sudoSwapPool.bondingCurve === BondingCurve.Linear) {
|
||||
currentPrice = currentPrice.add(delta)
|
||||
} else if (sudoSwapPool.bondingCurve === BondingCurve.Exponential) {
|
||||
currentPrice = currentPrice.mul(delta).div(BigNumber.from(PRECISION))
|
||||
}
|
||||
}
|
||||
|
||||
return calculateScaledPrice(currentPrice, poolFee).toString()
|
||||
}
|
||||
|
||||
// TODO: a lot of the below typecasting logic can be simplified when GraphQL migration is complete
|
||||
export const calcPoolPrice = (asset: GenieAsset, position = 0) => {
|
||||
if (!asset.sellorders) return ''
|
||||
|
||||
let amountToBuy: BigNumber = BigNumber.from(0)
|
||||
let marginalBuy: BigNumber = BigNumber.from(0)
|
||||
|
||||
const nft = asset.sellorders[0].protocolParameters
|
||||
const decimals = BigNumber.from(1).mul(10).pow(18)
|
||||
const ammFee = nft?.ammFeePercent ? (100 + (nft.ammFeePercent as number)) * 100 : 110 * 100
|
||||
|
||||
if (asset.marketplace === Markets.NFTX) {
|
||||
const sixteenmul = BigNumber.from(1).mul(10).pow(16)
|
||||
amountToBuy = BigNumber.from(ammFee)
|
||||
.div(100)
|
||||
.mul(position + 1)
|
||||
amountToBuy = amountToBuy.mul(sixteenmul)
|
||||
|
||||
marginalBuy = BigNumber.from(ammFee).div(100).mul(position)
|
||||
marginalBuy = marginalBuy.mul(sixteenmul)
|
||||
}
|
||||
if (asset.marketplace === Markets.NFT20) {
|
||||
amountToBuy = BigNumber.from(100).mul(position + 1)
|
||||
amountToBuy = amountToBuy.mul(decimals)
|
||||
|
||||
marginalBuy = BigNumber.from(100).mul(position)
|
||||
marginalBuy = marginalBuy.mul(decimals)
|
||||
}
|
||||
|
||||
const ethReserves = BigNumber.from(
|
||||
(
|
||||
nft as Record<
|
||||
string,
|
||||
{
|
||||
ethReserves: number
|
||||
}
|
||||
>
|
||||
)?.poolMetadata?.ethReserves?.toLocaleString('fullwide', { useGrouping: false }) ?? 1
|
||||
)
|
||||
const tokenReserves = BigNumber.from(
|
||||
(
|
||||
nft as Record<
|
||||
string,
|
||||
{
|
||||
tokenReserves: number
|
||||
}
|
||||
>
|
||||
)?.poolMetadata?.tokenReserves?.toLocaleString('fullwide', { useGrouping: false }) ?? 1
|
||||
)
|
||||
const numerator = ethReserves.mul(amountToBuy).mul(1000)
|
||||
const denominator = tokenReserves.sub(amountToBuy).mul(997)
|
||||
|
||||
const marginalnumerator = ethReserves.mul(marginalBuy).mul(1000)
|
||||
const marginaldenominator = tokenReserves.sub(marginalBuy).mul(997)
|
||||
|
||||
let price = numerator.div(denominator)
|
||||
const marginalprice = marginalnumerator.div(marginaldenominator)
|
||||
|
||||
price = price.sub(marginalprice)
|
||||
price = price.mul(101).div(100)
|
||||
|
||||
return price.toString()
|
||||
}
|
||||
|
||||
export const calcAvgGroupPoolPrice = (asset: GenieAsset, numberOfAssets: number) => {
|
||||
let total = BigNumber.from(0)
|
||||
|
||||
for (let i = 0; i < numberOfAssets; i++) {
|
||||
if (asset.marketplace === Markets.Sudoswap) {
|
||||
total = total.add(BigNumber.from(calcSudoSwapPrice(asset, i) ?? '0'))
|
||||
} else {
|
||||
total = total.add(BigNumber.from(calcPoolPrice(asset, i)))
|
||||
}
|
||||
}
|
||||
|
||||
return total.div(numberOfAssets).toString()
|
||||
}
|
||||
|
||||
const recalculatePooledAssetPrice = (asset: GenieAsset, position: number): string => {
|
||||
return asset.marketplace === Markets.Sudoswap
|
||||
? calcSudoSwapPrice(asset, position) ?? ''
|
||||
: calcPoolPrice(asset, position)
|
||||
}
|
||||
|
||||
export const recalculateBagUsingPooledAssets = (uncheckedItemsInBag: BagItem[]) => {
|
||||
if (
|
||||
!uncheckedItemsInBag.some((item) => item.asset.marketplace && isPooledMarket(item.asset.marketplace)) ||
|
||||
uncheckedItemsInBag.every(
|
||||
(item) => item.status === BagItemStatus.REVIEWED || item.status === BagItemStatus.REVIEWING_PRICE_CHANGE
|
||||
)
|
||||
)
|
||||
return uncheckedItemsInBag
|
||||
|
||||
const itemsInBag = [...uncheckedItemsInBag]
|
||||
itemsInBag.forEach((item) => {
|
||||
if (item.asset.marketplace)
|
||||
if (isPooledMarket(item.asset.marketplace)) {
|
||||
const asset = item.asset
|
||||
const isPriceChangedAsset = !!asset.updatedPriceInfo
|
||||
|
||||
const itemsInPool =
|
||||
asset.marketplace === Markets.Sudoswap
|
||||
? itemsInBag.filter((bagItem) => isInSameSudoSwapPool(item.asset, bagItem.asset))
|
||||
: itemsInBag.filter((bagItem) => isInSameMarketplaceCollection(item.asset, bagItem.asset))
|
||||
const calculatedPrice = isPriceChangedAsset
|
||||
? calcAvgGroupPoolPrice(asset, itemsInPool.length)
|
||||
: recalculatePooledAssetPrice(
|
||||
asset,
|
||||
itemsInPool.findIndex((itemInPool) => itemInPool.asset.tokenId === asset.tokenId)
|
||||
)
|
||||
|
||||
if (isPriceChangedAsset && item.asset.updatedPriceInfo)
|
||||
item.asset.updatedPriceInfo.ETHPrice = item.asset.updatedPriceInfo.basePrice = calculatedPrice
|
||||
else item.asset.priceInfo.ETHPrice = calculatedPrice
|
||||
}
|
||||
})
|
||||
|
||||
return itemsInBag
|
||||
}
|
Loading…
Reference in New Issue
Block a user