From 63a83902336e65c4ef20e2da310b55501e28a058 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Sat, 14 Mar 2026 19:39:58 +0000 Subject: [PATCH 1/2] [#23] Add token price display to story page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #23 - P5-1a: Create lib/price.ts — reads current token price and reserve balance from MCV2_Bond via priceForNextMint and tokenBond view functions - P5-1b: Display token price and reserve balance on story page header - Uses MCV2_BOND address from lib/contracts/constants.ts (no hardcoding) - Reserve label adapts to testnet (WETH) vs mainnet ($PLOT) Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/price.ts | 89 ++++++++++++++++++++++++++++ src/app/story/[storylineId]/page.tsx | 41 ++++++++++++- 2 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 lib/price.ts diff --git a/lib/price.ts b/lib/price.ts new file mode 100644 index 00000000..d2a6bfa4 --- /dev/null +++ b/lib/price.ts @@ -0,0 +1,89 @@ +import { type Address, formatUnits } from "viem"; +import { publicClient } from "./viem"; +import { MCV2_BOND } from "./contracts/constants"; + +/** + * Minimal ABI for MCV2_Bond view functions used for price display. + * + * - priceForNextMint: returns the cost (in reserve token) to mint 1 token + * - tokenBond: returns bond metadata including total supply info + */ +const mcv2BondAbi = [ + { + type: "function", + name: "priceForNextMint", + stateMutability: "view", + inputs: [ + { name: "token", type: "address" }, + { name: "amount", type: "uint256" }, + ], + outputs: [{ name: "price", type: "uint256" }], + }, + { + type: "function", + name: "tokenBond", + stateMutability: "view", + inputs: [{ name: "token", type: "address" }], + outputs: [ + { name: "creator", type: "address" }, + { name: "mintRoyalty", type: "uint16" }, + { name: "burnRoyalty", type: "uint16" }, + { name: "createdAt", type: "uint40" }, + { name: "reserveToken", type: "address" }, + { name: "reserveBalance", 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; + /** Reserve token address */ + reserveToken: Address; + /** Reserve balance in the bond, formatted */ + reserveBalance: string; + /** Reserve balance raw */ + reserveBalanceRaw: 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, bondInfo] = await Promise.all([ + publicClient.readContract({ + address: MCV2_BOND, + abi: mcv2BondAbi, + functionName: "priceForNextMint", + args: [tokenAddress, oneToken], + }), + publicClient.readContract({ + address: MCV2_BOND, + abi: mcv2BondAbi, + functionName: "tokenBond", + args: [tokenAddress], + }), + ]); + + const [, , , , reserveToken, reserveBalanceRaw] = bondInfo; + + return { + pricePerToken: formatUnits(priceRaw, 18), + priceRaw, + reserveToken: reserveToken as Address, + reserveBalance: formatUnits(reserveBalanceRaw, 18), + reserveBalanceRaw, + }; + } catch { + return null; + } +} diff --git a/src/app/story/[storylineId]/page.tsx b/src/app/story/[storylineId]/page.tsx index 4ee1c667..f5360d9b 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} + +
+
+ + Reserve + + + {priceInfo.reserveBalance} {reserveLabel} + +
+
+ )} {storyline.sunset ? (
Story complete From b891ff32f45d3d82ee8f879adad8ac2d77df34e3 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Sat, 14 Mar 2026 19:42:03 +0000 Subject: [PATCH 2/2] [#23] Replace reserve balance with total supply minted Reads ERC-20 totalSupply() on the storyline token instead of reserveBalance from tokenBond(), matching the issue requirement. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/price.ts | 48 +++++++++++----------------- src/app/story/[storylineId]/page.tsx | 4 +-- 2 files changed, 21 insertions(+), 31 deletions(-) diff --git a/lib/price.ts b/lib/price.ts index d2a6bfa4..908c8827 100644 --- a/lib/price.ts +++ b/lib/price.ts @@ -3,10 +3,10 @@ import { publicClient } from "./viem"; import { MCV2_BOND } from "./contracts/constants"; /** - * Minimal ABI for MCV2_Bond view functions used for price display. + * Minimal ABIs for price display. * - * - priceForNextMint: returns the cost (in reserve token) to mint 1 token - * - tokenBond: returns bond metadata including total supply info + * - MCV2_Bond.priceForNextMint: cost (in reserve token) to mint 1 token + * - ERC-20 totalSupply: total minted supply of the storyline token */ const mcv2BondAbi = [ { @@ -19,19 +19,15 @@ const mcv2BondAbi = [ ], outputs: [{ name: "price", type: "uint256" }], }, +] as const; + +const erc20Abi = [ { type: "function", - name: "tokenBond", + name: "totalSupply", stateMutability: "view", - inputs: [{ name: "token", type: "address" }], - outputs: [ - { name: "creator", type: "address" }, - { name: "mintRoyalty", type: "uint16" }, - { name: "burnRoyalty", type: "uint16" }, - { name: "createdAt", type: "uint40" }, - { name: "reserveToken", type: "address" }, - { name: "reserveBalance", type: "uint256" }, - ], + inputs: [], + outputs: [{ name: "", type: "uint256" }], }, ] as const; @@ -40,12 +36,10 @@ export interface TokenPriceInfo { pricePerToken: string; /** Raw price in wei */ priceRaw: bigint; - /** Reserve token address */ - reserveToken: Address; - /** Reserve balance in the bond, formatted */ - reserveBalance: string; - /** Reserve balance raw */ - reserveBalanceRaw: bigint; + /** Total minted supply, formatted */ + totalSupply: string; + /** Total minted supply raw */ + totalSupplyRaw: bigint; } /** @@ -59,7 +53,7 @@ export async function getTokenPrice( try { const oneToken = BigInt(10 ** 18); - const [priceRaw, bondInfo] = await Promise.all([ + const [priceRaw, totalSupplyRaw] = await Promise.all([ publicClient.readContract({ address: MCV2_BOND, abi: mcv2BondAbi, @@ -67,21 +61,17 @@ export async function getTokenPrice( args: [tokenAddress, oneToken], }), publicClient.readContract({ - address: MCV2_BOND, - abi: mcv2BondAbi, - functionName: "tokenBond", - args: [tokenAddress], + address: tokenAddress, + abi: erc20Abi, + functionName: "totalSupply", }), ]); - const [, , , , reserveToken, reserveBalanceRaw] = bondInfo; - return { pricePerToken: formatUnits(priceRaw, 18), priceRaw, - reserveToken: reserveToken as Address, - reserveBalance: formatUnits(reserveBalanceRaw, 18), - reserveBalanceRaw, + 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 f5360d9b..e94170e4 100644 --- a/src/app/story/[storylineId]/page.tsx +++ b/src/app/story/[storylineId]/page.tsx @@ -107,10 +107,10 @@ function StoryHeader({
- Reserve + Supply Minted - {priceInfo.reserveBalance} {reserveLabel} + {priceInfo.totalSupply} tokens