diff --git a/lib/price.ts b/lib/price.ts new file mode 100644 index 00000000..908c8827 --- /dev/null +++ b/lib/price.ts @@ -0,0 +1,79 @@ +import { type Address, formatUnits } from "viem"; +import { publicClient } from "./viem"; +import { MCV2_BOND } from "./contracts/constants"; + +/** + * Minimal ABIs for price display. + * + * - MCV2_Bond.priceForNextMint: cost (in reserve token) to mint 1 token + * - ERC-20 totalSupply: total minted supply of the storyline token + */ +const mcv2BondAbi = [ + { + type: "function", + name: "priceForNextMint", + stateMutability: "view", + inputs: [ + { name: "token", type: "address" }, + { name: "amount", type: "uint256" }, + ], + outputs: [{ name: "price", type: "uint256" }], + }, +] as const; + +const erc20Abi = [ + { + type: "function", + name: "totalSupply", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "uint256" }], + }, +] as const; + +export interface TokenPriceInfo { + /** Cost to mint 1 full token (18 decimals), formatted as string */ + pricePerToken: string; + /** Raw price in wei */ + priceRaw: bigint; + /** Total minted supply, formatted */ + totalSupply: string; + /** Total minted supply raw */ + totalSupplyRaw: bigint; +} + +/** + * Fetch current token price and bond info from MCV2_Bond for a storyline token. + * + * Returns null if the token has no bond or the query fails. + */ +export async function getTokenPrice( + tokenAddress: Address, +): Promise { + try { + const oneToken = BigInt(10 ** 18); + + const [priceRaw, totalSupplyRaw] = await Promise.all([ + publicClient.readContract({ + address: MCV2_BOND, + abi: mcv2BondAbi, + functionName: "priceForNextMint", + args: [tokenAddress, oneToken], + }), + publicClient.readContract({ + address: tokenAddress, + abi: erc20Abi, + functionName: "totalSupply", + }), + ]); + + return { + pricePerToken: formatUnits(priceRaw, 18), + priceRaw, + totalSupply: formatUnits(totalSupplyRaw, 18), + totalSupplyRaw, + }; + } catch { + return null; + } +} diff --git a/src/app/story/[storylineId]/page.tsx b/src/app/story/[storylineId]/page.tsx index 4ee1c667..e94170e4 100644 --- a/src/app/story/[storylineId]/page.tsx +++ b/src/app/story/[storylineId]/page.tsx @@ -1,5 +1,8 @@ import { createServerClient, type Storyline, type Plot } from "../../../../lib/supabase"; import { DeadlineCountdown } from "../../../components/DeadlineCountdown"; +import { getTokenPrice, type TokenPriceInfo } from "../../../../lib/price"; +import { IS_TESTNET } from "../../../../lib/contracts/constants"; +import { type Address } from "viem"; type Params = Promise<{ storylineId: string }>; @@ -41,9 +44,14 @@ export default async function StoryPage({ params }: { params: Params }) { const plots = plotRows ?? []; + const sl = storyline as Storyline; + const priceInfo = sl.token_address + ? await getTokenPrice(sl.token_address as Address) + : null; + return (
- +
{plots.map((plot) => ( @@ -56,7 +64,15 @@ export default async function StoryPage({ params }: { params: Params }) { ); } -function StoryHeader({ storyline }: { storyline: Storyline }) { +function StoryHeader({ + storyline, + priceInfo, +}: { + storyline: Storyline; + priceInfo: TokenPriceInfo | null; +}) { + const reserveLabel = IS_TESTNET ? "WETH" : "$PLOT"; + return (

@@ -78,6 +94,27 @@ function StoryHeader({ storyline }: { storyline: Storyline }) { )}

+ + {priceInfo && ( +
+
+ + Token Price + + + {priceInfo.pricePerToken} {reserveLabel} + +
+
+ + Supply Minted + + + {priceInfo.totalSupply} tokens + +
+
+ )} {storyline.sunset ? (
Story complete