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:
Jack Short 2022-11-17 11:48:10 -05:00 committed by GitHub
parent b19e7809ea
commit 44ecc9a203
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 240 additions and 161 deletions

@ -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
}

@ -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'

@ -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
}