diff --git a/src/lib/bonding-curve/index.ts b/src/lib/bonding-curve/index.ts index 66994a01..e15ea848 100644 --- a/src/lib/bonding-curve/index.ts +++ b/src/lib/bonding-curve/index.ts @@ -117,7 +117,7 @@ export async function getTokenSellPrice(amount: string, currentSupply: string, c export async function getTokenNextBuyPrice(currentSupply: string, curveState: CurveState) { if (!curveState || !curveState.steps) return '0' - let currentSupplyBn = BigNumber(currentSupply) + let currentSupplyBn = BigNumber(currentSupply).multipliedBy(BigNumber(10).pow(BigNumber(curveState.repoToken.denomination))) const maxSupplyBn = BigNumber(curveState.maxSupply) // Get curve steps from state @@ -141,7 +141,7 @@ export async function getTokenNextBuyPrice(currentSupply: string, curveState: Cu } } - return nextPrice.toString() + return nextPrice.toFixed() } export async function getCurrentStep(currentSupply: string, steps: CurveStep[]) { diff --git a/src/pages/repository/components/RepoHeader.tsx b/src/pages/repository/components/RepoHeader.tsx index cc07c342..811ee225 100644 --- a/src/pages/repository/components/RepoHeader.tsx +++ b/src/pages/repository/components/RepoHeader.tsx @@ -198,18 +198,21 @@ export default function RepoHeader({ repo, isLoading, owner, parentRepo }: Props Tokenized )} - {isDecentralized && repo.tokenType === 'BONDING_CURVE' && repo.token && repo.token.processId && ( -
-
- - {/* {tokenBalLoading && } + {isDecentralized && + repo.token && + (repo.tokenType ? repo.tokenType === 'BONDING_CURVE' : true) && + repo.token.processId && ( +
+
+ + {/* {tokenBalLoading && } {!tokenBalLoading && ( */} +
-
- )} + )} {isDecentralized && repo.tokenType === 'IMPORT' && importedTokenDetails && (
diff --git a/src/pages/repository/components/decentralize-modals/MarketStats.tsx b/src/pages/repository/components/decentralize-modals/MarketStats.tsx index 6ac033ef..649bf24b 100644 --- a/src/pages/repository/components/decentralize-modals/MarketStats.tsx +++ b/src/pages/repository/components/decentralize-modals/MarketStats.tsx @@ -3,19 +3,25 @@ import React from 'react' import { formatNumberUsingNumeral } from '../../helpers/customFormatNumbers' interface MarketStatsProps { + marketCapUSD: string marketCap: string volume: string circulatingSupply: string + baseTokenTicker: string } -const MarketStats: React.FC = ({ marketCap, volume, circulatingSupply }) => { +const MarketStats: React.FC = ({ marketCapUSD, marketCap, volume, circulatingSupply, baseTokenTicker }) => { return (

Market Stats

-
+
- Market Cap - ${marketCap} + Market Cap (USD) + ${marketCapUSD} +
+
+ Market Cap ({baseTokenTicker}) + {marketCap}
24h Volume diff --git a/src/pages/repository/components/decentralize-modals/Trade-Modal.tsx b/src/pages/repository/components/decentralize-modals/Trade-Modal.tsx index 9dba1b6a..e203e243 100644 --- a/src/pages/repository/components/decentralize-modals/Trade-Modal.tsx +++ b/src/pages/repository/components/decentralize-modals/Trade-Modal.tsx @@ -6,12 +6,13 @@ import { Fragment } from 'react' import React from 'react' import { toast } from 'react-hot-toast' import SVG from 'react-inlinesvg' +import { BeatLoader } from 'react-spinners' import CloseCrossIcon from '@/assets/icons/close-cross.svg' import { Button } from '@/components/common/buttons' import { imgUrlFormatter } from '@/helpers/imgUrlFormatter' import { shortenAddress } from '@/helpers/shortenAddress' -import { getBuySellTransactionsOfCurve, getTokenBuyPrice, getTokenSellPrice } from '@/lib/bonding-curve' +import { getCurrentStep, getTokenBuyPrice, getTokenNextBuyPrice, getTokenSellPrice } from '@/lib/bonding-curve' import { buyTokens } from '@/lib/bonding-curve/buy' import { getCurveState, getTokenCurrentSupply } from '@/lib/bonding-curve/helpers' import { sellTokens } from '@/lib/bonding-curve/sell' @@ -20,7 +21,9 @@ import { useGlobalStore } from '@/stores/globalStore' import { CurveState } from '@/stores/repository-core/types' import { RepoToken } from '@/types/repository' -import TradeChartComponent from './TradeChartComponent' +import { customFormatNumber } from '../../helpers/customFormatNumbers' +import MarketStats from './MarketStats' +import { TradeChart } from './TradeChart' import TransferToLP from './TransferToLP' type TradeModalProps = { @@ -28,14 +31,16 @@ type TradeModalProps = { isOpen: boolean } -type ChartData = { - time: string | number - value: string | number +interface MarketStatsProps { + marketCap: string + marketCapUSD: string + volume: string + circulatingSupply: string + baseTokenTicker: string } export default function TradeModal({ onClose, isOpen }: TradeModalProps) { const amountRef = React.useRef(null) - const [chartData, setChartData] = React.useState([]) const [balances, setBalances] = React.useState>({}) const [transactionPending, setTransactionPending] = React.useState(false) const [curveState, setCurveState] = React.useState({} as CurveState) @@ -45,6 +50,23 @@ export default function TradeModal({ onClose, isOpen }: TradeModalProps) { const [maxSupply, setMaxSupply] = React.useState('0') const [reserveTokenBalance, setReserveTokenBalance] = React.useState('0') + const [afterTradeSupply, setAfterTradeSupply] = React.useState<{ + rangeTo: number + price: number + index: number + } | null>(null) + const [isFetchingNextPrice, setIsFetchingNextPrice] = React.useState(false) + const [nextPrice, setNextPrice] = React.useState('0') + const [baseAssetPriceUSD, setBaseAssetPriceUSD] = React.useState('0') + const [priceInUSD, setPriceInUSD] = React.useState('0') + const [stats, setStats] = React.useState({ + marketCap: '0', + marketCapUSD: '0', + volume: '0', + circulatingSupply: '0', + baseTokenTicker: '-' + }) + const [currentIndex, setCurrentIndex] = React.useState(0) const [repoTokenBalance, setRepoTokenBalance] = React.useState('0') const [amount, setAmount] = React.useState('') const [tokenPair, setTokenPair] = React.useState([]) @@ -75,9 +97,42 @@ export default function TradeModal({ onClose, isOpen }: TradeModalProps) { setMaxSupply(formattedMaxSupply) // setProgress((+curveState.reserveBalance / +curveState.fundingGoal) * 100) handleGetTokenHoldersBalances() + fetchStats() } }, [curveState]) + React.useEffect(() => { + if (!curveState.steps || !stats?.circulatingSupply) return + + if (!amount || amount === '0') return setAfterTradeSupply(null) + const numericAmount = Number(selectedSide === 'buy' ? amount : `-${amount}`) + const afterAmount = BigNumber(stats.circulatingSupply).plus(BigNumber(numericAmount)) + const formattedAfterAmount = BigNumber(afterAmount).multipliedBy( + BigNumber(10).pow(BigNumber(curveState.repoToken.denomination)) + ) + const currentStep = curveState.steps[currentIndex + 1] + let index = 0 + const point = curveState.steps.find((step, idx) => { + if (formattedAfterAmount.lte(step.rangeTo)) { + index = idx + return true + } + return false + }) + const formattedPointPrice = BigNumber(point?.price || 0).dividedBy( + BigNumber(10).pow(BigNumber(curveState.reserveToken.denomination)) + ) + const formattedCurrentStepRange = BigNumber(currentStep.rangeTo).dividedBy( + BigNumber(10).pow(BigNumber(curveState.repoToken.denomination)) + ) + + if (formattedCurrentStepRange.eq(afterAmount) && currentStep.price === point?.price) { + setAfterTradeSupply(null) + } else { + setAfterTradeSupply({ rangeTo: afterAmount.toNumber(), price: formattedPointPrice.toNumber(), index }) + } + }, [curveState.steps, stats?.circulatingSupply, amount]) + React.useEffect(() => { if (!repo?.token || !curveState?.maxSupply) return if (+currentSupply > 0) { @@ -85,6 +140,7 @@ export default function TradeModal({ onClose, isOpen }: TradeModalProps) { .dividedBy(BigNumber(10).pow(BigNumber(repo.token!.denomination))) .toString() setProgress(BigNumber(+currentSupply).dividedBy(BigNumber(formattedMaxSupply)).multipliedBy(100).toNumber()) + fetchCurrentStep(curveState, currentSupply) } }, [currentSupply, curveState]) @@ -96,6 +152,23 @@ export default function TradeModal({ onClose, isOpen }: TradeModalProps) { } }, [amount]) + React.useEffect(() => { + if (!baseAssetPriceUSD || !+baseAssetPriceUSD || !+nextPrice) return + + // Convert nextPrice from AR to USD by multiplying AR price by USD/AR rate + const priceInUSD = BigNumber(nextPrice) + .multipliedBy(BigNumber(baseAssetPriceUSD)) + // .dividedBy(BigNumber(10).pow(BigNumber(repo?.token!.denomination))) + .toString() + + setPriceInUSD(priceInUSD) + }, [baseAssetPriceUSD, nextPrice]) + + React.useEffect(() => { + if (!curveState?.reserveToken || !+nextPrice) return + fetchBaseAssetPriceUSD() + }, [curveState, nextPrice]) + async function handleGetTokenBalances() { if (!repo || !address || !repo.bondingCurve || !repo.token) return const reserveTokenBalance = await fetchTokenBalance(repo.bondingCurve.reserveToken.processId!, address!) @@ -121,50 +194,6 @@ export default function TradeModal({ onClose, isOpen }: TradeModalProps) { setBalances(balancesAsPercentages) } - async function handleGetTransactions() { - if (!repo || !repo.bondingCurve || !repo.token) return - - const chart: ChartData[] = [] - - const transactions = await getBuySellTransactionsOfCurve(repo.bondingCurve.processId!) - let lastTimestamp = 0 - transactions.forEach((transaction: any) => { - const costTag = transaction.node.tags.find((tag: any) => tag.name === 'Cost') - const tokensSoldTag = transaction.node.tags.find((tag: any) => tag.name === 'TokensSold') - const tokensBoughtTag = transaction.node.tags.find((tag: any) => tag.name === 'TokensBought') - - const cost = costTag ? parseInt(costTag.value) : 0 - const tokensSold = tokensSoldTag ? parseInt(tokensSoldTag.value) : 0 - const tokensBought = tokensBoughtTag ? parseInt(tokensBoughtTag.value) : 0 - let price = 0 - // Calculate price based on transaction type - if (tokensBought > 0) { - // Buy transaction: Price = Cost / TokensBought - price = cost / tokensBought - } else if (tokensSold > 0) { - // Sell transaction: Price = Proceeds / TokensSold - price = cost / tokensSold - } else { - // If no tokens were bought or sold, skip this transaction as it doesn't affect the price - return - } - - let timestamp = transaction.node.ingested_at || 0 - // Ensure timestamps are incrementing - if (timestamp <= lastTimestamp) { - timestamp = lastTimestamp + 1 - } - lastTimestamp = timestamp - - // const timestamp = transaction.node.ingested_at || 0 - chart.push({ - time: timestamp, - value: price - }) - }) - setChartData(chart) - } - async function handleGetCurveState() { if (!repo?.bondingCurve) return @@ -282,7 +311,7 @@ export default function TradeModal({ onClose, isOpen }: TradeModalProps) { await handleGetTokenBalances() await handleGetCurveState() - await handleGetTransactions() + // await handleGetTransactions() } async function handleBuy() { @@ -355,6 +384,58 @@ export default function TradeModal({ onClose, isOpen }: TradeModalProps) { .toString() } + async function fetchCurrentStep(_curveData: CurveState, _currentSupply: string) { + const scaledCurrentSupply = BigNumber(_currentSupply) + .multipliedBy(BigNumber(10).pow(BigNumber(repo?.token?.denomination || 0))) + .toFixed() + + const currentStep = await getCurrentStep(scaledCurrentSupply, _curveData.steps) + setCurrentIndex(currentStep - 1) + } + + async function fetchStats() { + if (!repo?.token?.processId) return + setIsFetchingNextPrice(true) + + const normalizedSupply = BigNumber(currentSupply).toFixed() + const nextPrice = await getTokenNextBuyPrice(currentSupply, curveState) + const nextPriceFormatted = BigNumber(nextPrice) + .dividedBy(BigNumber(10).pow(BigNumber(curveState.reserveToken.denomination))) + .toString() + + setNextPrice(nextPriceFormatted) + setIsFetchingNextPrice(false) + + const marketCapUSD = BigNumber(nextPriceFormatted).multipliedBy(BigNumber(normalizedSupply)).toFixed() + setStats({ + marketCap: '0', + marketCapUSD: marketCapUSD, + volume: '0', + circulatingSupply: normalizedSupply, + baseTokenTicker: repo?.bondingCurve?.reserveToken?.tokenTicker || '-' + }) + } + + async function fetchBaseAssetPriceUSD() { + if (!curveState?.reserveToken) return + const token = curveState.reserveToken + + if (token.tokenTicker === 'TUSDA') { + setBaseAssetPriceUSD('24') + + return + } + + try { + const response = await fetch('https://api.redstone.finance/prices?symbol=AR&provider=redstone-rapid&limit=1') + const data = await response.json() + setBaseAssetPriceUSD(data[0].value.toString()) + } catch (error) { + console.error('Failed to fetch AR price:', error) + setBaseAssetPriceUSD('0') + } + } + const selectedToken = tokenPair[selectedTokenToTransact] if (!repo || !selectedToken) return null @@ -384,10 +465,10 @@ export default function TradeModal({ onClose, isOpen }: TradeModalProps) { leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95" > - +
- {repo.token?.tokenTicker}/{repo.bondingCurve?.reserveToken?.tokenTicker} + ${repo.token?.tokenTicker} / ${repo.bondingCurve?.reserveToken?.tokenTicker}
-
+
{/* TradingView Chart Section - 70% */} -
- +
+ {isFetchingNextPrice ? ( +
+ + + {repo?.bondingCurve?.reserveToken?.tokenTicker} + +
+ ) : ( +

+ {customFormatNumber(+nextPrice, 12, 3)}{' '} + {repo?.bondingCurve?.reserveToken?.tokenTicker} +

+ )} +

${customFormatNumber(+priceInUSD, 12, 3)}

+
+ + {/* + /> */}
{/* Buy/Sell Widget Section - 30% */} @@ -574,6 +678,7 @@ export default function TradeModal({ onClose, isOpen }: TradeModalProps) {
)}
+
diff --git a/src/pages/repository/components/decentralize-modals/TradeChart.tsx b/src/pages/repository/components/decentralize-modals/TradeChart.tsx new file mode 100644 index 00000000..1a46043f --- /dev/null +++ b/src/pages/repository/components/decentralize-modals/TradeChart.tsx @@ -0,0 +1,285 @@ +import BigNumber from 'bignumber.js' +import { useEffect, useState } from 'react' +import { + Area, + CartesianGrid, + Line, + LineChart, + ReferenceDot, + ReferenceLine, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis +} from 'recharts' + +import { CurveStep } from '@/lib/discrete-bonding-curve/curve' +import { RepoLiquidityPoolToken, RepoToken } from '@/types/repository' + +import { formatNumberUsingNumeral, preventScientificNotationFloat } from '../../helpers/customFormatNumbers' + +// import { CurveStepExternal, CurveStepInternal } from '@/types/repository' + +export function TradeChart({ + steps, + currentIndex, + tokenA, + tokenB, + afterTradeSupply +}: { + steps: CurveStep[] + currentIndex: number | null + tokenA: RepoToken + tokenB: RepoLiquidityPoolToken + afterTradeSupply: { rangeTo: number; price: number; index: number } | null +}) { + const [data, setData] = useState([]) + const [tooltip, setTooltip] = useState<{ + show: boolean + x: number + y: number + index: number + }>({ + show: false, + x: 0, + y: 0, + index: 0 + }) + + useEffect(() => { + if (steps.length > 0) { + setData( + steps.map((step) => ({ + rangeTo: BigNumber(step.rangeTo).dividedBy(BigNumber(10).pow(tokenA.denomination)).toNumber(), + price: BigNumber(step.price).dividedBy(BigNumber(10).pow(tokenB.denomination)).toNumber() + })) + ) + } + }, [steps]) + + const handleMouseEnter = (event: any) => { + console.log(event) + setTooltip({ + show: true, + x: event.cx, + y: event.cy, + index: Number(event.id) + }) + } + + const handleMouseLeave = () => { + console.log('leave') + setTooltip({ ...tooltip, show: false }) + } + + const handleTooltipLabel = (label: string) => { + const currentIndex = data.findIndex((entry) => entry.rangeTo === Number(label)) + const previousIndex = currentIndex - 1 + const previousStep = previousIndex > 0 ? data[previousIndex] : { rangeTo: 0, price: 0 } + const currentStep = data[currentIndex] + + const previousStepFormatted = formatNumberUsingNumeral(previousStep.rangeTo).toUpperCase() + const currentStepFormatted = formatNumberUsingNumeral(currentStep.rangeTo).toUpperCase() + + return `Mint Range: ${previousStepFormatted} - ${currentStepFormatted}` + } + + const formatXAxisTick = (value: number) => { + return formatNumberUsingNumeral(value).toUpperCase() + } + + return ( +
+ + + + + + + + + + + + + + + + [`${preventScientificNotationFloat(value)} qAR`, 'Price']} + labelFormatter={handleTooltipLabel} + /> + + + + + {afterTradeSupply && ( + <> + + } + stroke="green" + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + /> + + )} + + } + stroke="#06b6d4" + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + /> + + + +
+ ) +} + +// A simple custom tooltip component +function CustomTooltip({ + x, + y, + show, + index, + data +}: { + x: number + y: number + show: boolean + index: number + data: CurveStep[] +}) { + if (!show) return null + + const style: React.CSSProperties = { + position: 'absolute', + left: x - 75, // offset the mouse position a bit + top: y + 20, + padding: '6px 8px', + backgroundColor: 'rgba(0,0,0,0.8)', + border: '1px solid rgba(255,255,255,0.1)', + borderRadius: '8px', + boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', + color: 'rgba(255,255,255,0.7)', + pointerEvents: 'none', // so mouse events pass through + zIndex: 999, + width: '250px' + } + + const step = data[index || 0] + const previousStep = index > 0 ? data[index - 1] : { rangeTo: 0, price: 0 } + const previousStepFormatted = formatNumberUsingNumeral(previousStep.rangeTo).toUpperCase() + const currentStepFormatted = formatNumberUsingNumeral(step.rangeTo).toUpperCase() + return ( +
+

+ Current Range: {previousStepFormatted} - {currentStepFormatted} +

+

Price: {preventScientificNotationFloat(step.price)} qAR

+
+ ) +} + +function PulsingDot(props: any) { + // The ReferenceDot will pass certain props to your custom shape, + // typically including cx, cy, r, and fill. + // You can add stroke, strokeWidth, etc., as needed. + const { cx, cy, id, r = 6, fill = '#a855f7', stroke = '#fff', onMouseEnter, onMouseLeave } = props + + return ( + onMouseEnter({ id, cx, cy })} onMouseLeave={() => onMouseLeave()}> + {/* 1) The central dot */} + + + {/* 2) A pulsing ring that grows & fades out repeatedly */} + + {/* Expand radius from r to (r+10), for example, over 1s */} + + {/* Fade out the ring from opacity=0.3 to 0 over 1s */} + + + + ) +} diff --git a/src/pages/repository/components/decentralize-modals/TradeChartComponent.tsx b/src/pages/repository/components/decentralize-modals/TradeChartComponent.tsx deleted file mode 100644 index fd51828d..00000000 --- a/src/pages/repository/components/decentralize-modals/TradeChartComponent.tsx +++ /dev/null @@ -1,418 +0,0 @@ -import BigNumber from 'bignumber.js' -// import { ColorType, createChart } from 'lightweight-charts' -import Chart from 'chart.js/auto' -import numeral from 'numeral' -import React from 'react' -import { BeatLoader } from 'react-spinners' - -import { getCurrentStep, getTokenNextBuyPrice } from '@/lib/bonding-curve' -import { getTokenCurrentSupply } from '@/lib/bonding-curve/helpers' -import { CurveStep } from '@/lib/discrete-bonding-curve/curve' -import { customFormatNumber, formatNumberUsingNumeral } from '@/pages/repository/helpers/customFormatNumbers' -import { CurveState } from '@/stores/repository-core/types' -import { Repo } from '@/types/repository' - -import MarketStats from './MarketStats' - -const highlightPlugin = { - id: 'highlight', - beforeDraw: function (chart: any) { - // @ts-ignore - if (chart?.tooltip?._active && Array.isArray(chart.tooltip._active) && chart.tooltip._active.length) { - // Get the current point and the next point - const activePoint = chart.tooltip._active[0] - const currentIndex = activePoint.index - const currentPoint = activePoint.element - const nextPoint = chart.getDatasetMeta(0).data[currentIndex + 1] - // @ts-ignore - if (!nextPoint) return - - const ctx = chart.ctx - const x = currentPoint.x - const nextX = nextPoint.x - const yAxis = chart.scales.y - ctx.save() - ctx.fillStyle = 'rgba(6, 182, 212, 0.2)' - ctx.fillRect(x, yAxis.top, nextX - x, yAxis.bottom - yAxis.top) - ctx.restore() - } - } -} -Chart.register(highlightPlugin) -type ChartData = { - time: string | number - value: string | number -} - -interface MarketStatsProps { - marketCap: string - volume: string - circulatingSupply: string -} - -export default function TradeChartComponent({ - data, - repo, - reserveBalance, - curveState, - repoTokenBuyAmount -}: { - data: ChartData[] - repo: Repo - reserveBalance: string - curveState: CurveState - repoTokenBuyAmount: string -}) { - const [currentStep, setCurrentStep] = React.useState(0) - const [isFetchingNextPrice, setIsFetchingNextPrice] = React.useState(false) - const [nextPrice, setNextPrice] = React.useState('0') - const [baseAssetPriceUSD, setBaseAssetPriceUSD] = React.useState('0') - const [priceInUSD, setPriceInUSD] = React.useState('0') - const [stats, setStats] = React.useState({ - marketCap: '0', - volume: '0', - circulatingSupply: '0' - }) - - const [curveStepsCache, setCurveStepsCache] = React.useState<{ rangeTo: number; price: number }[]>([]) - const [currentSupply, setCurrentSupply] = React.useState<{ rangeTo: number; price: number }>({ rangeTo: 0, price: 0 }) - const [afterTradeSupply, setAfterTradeSupply] = React.useState<{ rangeTo: number; price: number }>({ - rangeTo: 0, - price: 0 - }) - - const canvasRef = React.useRef(null) - const chart = React.useRef | null>(null) - console.log({ currentStep }) - // React.useEffect(() => { - // if (!chartContainerRef.current) return - // setSize({ - // width: chartContainerRef.current.clientWidth, - // height: chartContainerRef.current.clientHeight - // }) - // }, [chartContainerRef]) - - React.useEffect(() => { - if (!curveState) return - const interval = setInterval(fetchNextPrice, 15000) - return () => clearInterval(interval) - }, [curveState]) - - React.useEffect(() => { - if (!curveState) return - fetchStats() - }, [data, reserveBalance, curveState]) - - React.useEffect(() => { - if (!curveStepsCache || !stats?.circulatingSupply) return - const point = curveStepsCache.find((step) => Number(stats.circulatingSupply) <= step.rangeTo) - - setCurrentSupply({ rangeTo: Number(stats.circulatingSupply), price: point ? point.price : 0 }) - }, [curveStepsCache, stats]) - - React.useEffect(() => { - if (!curveStepsCache || !stats?.circulatingSupply) return - const afterAmount = Number(stats.circulatingSupply) + Number(repoTokenBuyAmount) - const point = curveStepsCache.find((step) => afterAmount <= step.rangeTo) - - setAfterTradeSupply({ rangeTo: afterAmount, price: point ? point.price : 0 }) - }, [curveStepsCache, repoTokenBuyAmount, stats]) - - React.useEffect(() => { - if (!chart.current) return - chart.current.data.datasets[1].data = [currentSupply] - chart.current?.update() - }, [currentSupply]) - React.useEffect(() => { - if (!chart.current) return - chart.current.data.datasets[2].data = [afterTradeSupply] - chart.current?.update() - }, [afterTradeSupply]) - - async function fetchStats() { - if (!repo?.token?.processId) return - const supply = await getTokenCurrentSupply(repo?.token?.processId) - - const normalizedSupply = BigNumber(supply) - .dividedBy(BigNumber(10).pow(BigNumber(repo?.token?.denomination))) - .toString() - const nextPrice = await getTokenNextBuyPrice(supply, curveState) - const nextPriceFormatted = BigNumber(nextPrice) - .dividedBy(BigNumber(10).pow(BigNumber(curveState.reserveToken.denomination))) - .toString() - - setNextPrice(nextPriceFormatted) - - const marketCap = BigNumber(nextPriceFormatted).multipliedBy(BigNumber(normalizedSupply)).toString() - - setStats({ - marketCap: marketCap, - volume: '0', - circulatingSupply: normalizedSupply - }) - } - - React.useEffect(() => { - if (!curveState || !chart.current) return - setTimeout(() => { - chart.current?.update() - }, 100) - }, [stats]) - - async function fetchNextPrice() { - if (!curveState || !curveState.repoToken?.processId) return - setIsFetchingNextPrice(true) - const currentSupply = await getTokenCurrentSupply(curveState.repoToken.processId) - - const nextPrice = await getTokenNextBuyPrice(currentSupply, curveState) - setNextPrice( - BigNumber(nextPrice) - .dividedBy(BigNumber(10).pow(BigNumber(curveState.reserveToken.denomination))) - .toString() - ) - setIsFetchingNextPrice(false) - - const step = await getCurrentStep(currentSupply, curveState.steps) - setCurrentStep(step) - } - - // function formatYAxis(value: number) { - // return value.toFixed(10) - // } - - React.useEffect(() => { - if (!curveState?.steps) return - initializeChart(curveState.steps) - }, [curveState, chart, stats]) - - React.useEffect(() => { - if (!curveState?.reserveToken || !+nextPrice) return - fetchBaseAssetPriceUSD() - }, [curveState, nextPrice]) - - React.useEffect(() => { - if (!baseAssetPriceUSD || !+baseAssetPriceUSD || !+nextPrice) return - - // Convert nextPrice from AR to USD by multiplying AR price by USD/AR rate - const priceInUSD = BigNumber(nextPrice) - .multipliedBy(BigNumber(baseAssetPriceUSD)) - // .dividedBy(BigNumber(10).pow(BigNumber(repo?.token!.denomination))) - .toString() - - setPriceInUSD(priceInUSD) - }, [baseAssetPriceUSD, nextPrice]) - - async function fetchBaseAssetPriceUSD() { - if (!curveState?.reserveToken) return - const token = curveState.reserveToken - - if (token.tokenTicker === 'TUSDA') { - setBaseAssetPriceUSD('24') - - return - } - - try { - const response = await fetch('https://api.redstone.finance/prices?symbol=AR&provider=redstone-rapid&limit=1') - const data = await response.json() - setBaseAssetPriceUSD(data[0].value.toString()) - } catch (error) { - console.error('Failed to fetch AR price:', error) - setBaseAssetPriceUSD('0') - } - } - - function initializeChart(steps: CurveStep[]) { - if (!curveState?.repoToken) return - const curveSteps = steps.map((step) => ({ - rangeTo: BigNumber(step.rangeTo / 10 ** +curveState.repoToken.denomination).toNumber(), - price: BigNumber(step.price / 10 ** +curveState.reserveToken.denomination).toNumber() - })) - setCurveStepsCache(curveSteps) - const ctx = canvasRef.current - if (!ctx) return - - let _chart: Chart<'line' | 'scatter', CurveStep[], unknown> | null = chart.current - if (!_chart) { - const lineColor = '#06b6d4' - const currentColor = '#667085' - const afterBuyColor = 'green' - _chart = new Chart(ctx, { - data: { - datasets: [ - { - type: 'line', - label: 'Price', - data: curveSteps, - borderColor: lineColor, - backgroundColor: 'transparent', - borderWidth: 2, - pointBackgroundColor: lineColor, - stepped: 'before', - fill: true, - parsing: { - xAxisKey: 'rangeTo', // Use 'rangeTo' as the x-axis value - yAxisKey: 'price' // Use 'price' as the y-axis value - }, - order: 3 - }, - { - label: 'Current Supply', - data: [], - borderColor: currentColor, - backgroundColor: 'transparent', - pointRadius: 6, - borderWidth: 2, - type: 'scatter', - parsing: { - xAxisKey: 'rangeTo', // Use 'rangeTo' as the x-axis value - yAxisKey: 'price' // Use 'price' as the y-axis value - }, - order: 1 - }, - { - label: 'After Buy Supply', - data: [], - borderColor: afterBuyColor, - backgroundColor: 'transparent', - pointRadius: 6, - borderWidth: 2, - type: 'scatter', - parsing: { - xAxisKey: 'rangeTo', // Use 'rangeTo' as the x-axis value - yAxisKey: 'price' // Use 'price' as the y-axis value - }, - order: 2 - } - ] - }, - options: { - responsive: true, - maintainAspectRatio: false, - interaction: { - intersect: false, - mode: 'index' - }, - plugins: { - legend: { - display: false - }, - tooltip: { - filter(e) { - if (!e.label) { - return false - } - - return true - }, - displayColors: false, - callbacks: { - label: function (context) { - return `Price per token: ${customFormatNumber(context.parsed.y, 18, 5)}` - }, - title: function (items) { - if (!items[0]) return `` - const index = items[0].dataIndex - const fromRange = (items[0].dataset?.data[index - 1] as any)?.rangeTo - - // if (index === items[0].dataset?.data.length - 1) - // return `Max Supply: ${formatNumberUsingNumeral(fromRange).toUpperCase()}` - - const toRange = (items[0].dataset?.data[index] as any)?.rangeTo - return `Range: ${formatNumberUsingNumeral(fromRange || 0).toUpperCase()} - ${formatNumberUsingNumeral( - toRange || 0 - ).toUpperCase()}` - } - } - } - }, - onHover: function (event, chartElement) { - if (!event.native) return - if (chartElement.length) { - const target = event.native.target as HTMLElement - target.style.cursor = 'pointer' - } else { - const target = event.native.target as HTMLElement - target.style.cursor = 'default' - } - }, - scales: { - x: { - type: 'linear', - title: { - display: true, - text: 'Supply', - color: '#64748b' - }, - ticks: { - font: { - size: 14 - }, - maxTicksLimit: 6, - callback: function (value) { - return numeral(value).format('0a').toUpperCase() - }, - color: '#64748b' - }, - grid: { - display: false - } - }, - y: { - position: 'right', - beginAtZero: true, - ticks: { - font: { - size: 13 - }, - callback: function (value) { - return customFormatNumber(+value) - }, - color: '#64748b' - }, - grid: { - display: false - } - } - } - } - }) - } else { - _chart.data.datasets[0].data = curveSteps - _chart.update() - } - - chart.current = _chart - } - - return ( -
-
- {isFetchingNextPrice ? ( -
- - {repo?.bondingCurve?.reserveToken?.tokenTicker} -
- ) : ( -

- {customFormatNumber(+nextPrice, 12, 3)}{' '} - {repo?.bondingCurve?.reserveToken?.tokenTicker} -

- )} -

${customFormatNumber(+priceInUSD, 12, 3)}

-
-
- {!chart.current?.data.datasets[0].data.length && ( -
-

No data

-
- )} - -
- -
- ) -}