From 9f4cdaba197d9351358891fcad1de9bfe966c031 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Sun, 15 Mar 2026 07:33:56 +0000 Subject: [PATCH 1/2] [#80] Add priceForNextMint + tokenBond ABI and 24h price change utility - Add priceForNextMint and tokenBond view functions to lib/contracts/abi.ts - Add get24hPriceChange() using block number diff (~43200 blocks on Base) - Add getTokenTVL() reading tokenBond().reserveBalance - Update getTokenPrice() to use priceForNextMint (simpler single-call) Fixes #80 Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/contracts/abi.ts | 29 ++++++++++++++ lib/price.ts | 91 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 115 insertions(+), 5 deletions(-) diff --git a/lib/contracts/abi.ts b/lib/contracts/abi.ts index 5475fd27..9f8806de 100644 --- a/lib/contracts/abi.ts +++ b/lib/contracts/abi.ts @@ -85,6 +85,35 @@ export const donateFunction = { outputs: [], } as const; +// --------------------------------------------------------------------------- +// MCV2_Bond view functions +// --------------------------------------------------------------------------- + +/** Current cost (in reserve token) to mint 1 unit of the given token. */ +export const priceForNextMintFunction = { + type: "function", + name: "priceForNextMint", + stateMutability: "view", + inputs: [{ name: "token", type: "address" }], + outputs: [{ name: "", type: "uint128" }], +} as const; + +/** Full bond info for a token: creator, royalties, creation time, reserve. */ +export const tokenBondFunction = { + 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; + // --------------------------------------------------------------------------- // Combined ABI (for viem contract instances) // --------------------------------------------------------------------------- diff --git a/lib/price.ts b/lib/price.ts index d9d5a45f..4db98e32 100644 --- a/lib/price.ts +++ b/lib/price.ts @@ -1,6 +1,10 @@ import { type Address, formatUnits } from "viem"; import { publicClient } from "./viem"; import { MCV2_BOND } from "./contracts/constants"; +import { + priceForNextMintFunction, + tokenBondFunction, +} from "./contracts/abi"; /** * Minimal ABIs for price display. @@ -70,6 +74,8 @@ export const mcv2BondAbi = [ inputs: [{ name: "token", type: "address" }], outputs: [], }, + priceForNextMintFunction, + tokenBondFunction, ] as const; export const erc20Abi = [ @@ -122,6 +128,7 @@ export interface TokenPriceInfo { /** * Fetch current token price and bond info from MCV2_Bond for a storyline token. + * Uses priceForNextMint for a simpler, single-call price read. * * Returns null if the token has no bond or the query fails. */ @@ -129,14 +136,12 @@ 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: "getReserveForToken", - args: [tokenAddress, oneToken], + functionName: "priceForNextMint", + args: [tokenAddress], }), publicClient.readContract({ address: tokenAddress, @@ -147,7 +152,7 @@ export async function getTokenPrice( return { pricePerToken: formatUnits(priceRaw, 18), - priceRaw, + priceRaw: BigInt(priceRaw), totalSupply: formatUnits(totalSupplyRaw, 18), totalSupplyRaw, }; @@ -155,3 +160,79 @@ export async function getTokenPrice( return null; } } + +/** ~24 hours of blocks on Base at 2s block time */ +const BLOCKS_PER_24H = BigInt(43200); + +/** + * Get 24h price change percentage for a token using on-chain block diff. + * Compares priceForNextMint at current block vs ~24h ago. + * + * Returns null if the read fails (e.g. token didn't exist 24h ago). + */ +export async function get24hPriceChange( + tokenAddress: Address, +): Promise<{ changePercent: number; currentPrice: bigint; previousPrice: bigint } | null> { + try { + const currentBlock = await publicClient.getBlockNumber(); + const pastBlock = currentBlock - BLOCKS_PER_24H; + + const [currentPrice, previousPrice] = await Promise.all([ + publicClient.readContract({ + address: MCV2_BOND, + abi: mcv2BondAbi, + functionName: "priceForNextMint", + args: [tokenAddress], + }), + publicClient.readContract({ + address: MCV2_BOND, + abi: mcv2BondAbi, + functionName: "priceForNextMint", + args: [tokenAddress], + blockNumber: pastBlock, + }), + ]); + + const current = BigInt(currentPrice); + const previous = BigInt(previousPrice); + + if (previous === BigInt(0)) { + return { changePercent: 0, currentPrice: current, previousPrice: previous }; + } + + const changePercent = + Number(((current - previous) * BigInt(10000)) / previous) / 100; + + return { changePercent, currentPrice: current, previousPrice: previous }; + } catch { + return null; + } +} + +/** + * Get TVL (reserve balance) for a token from its MCV2_Bond tokenBond data. + * + * Returns null if the read fails. + */ +export async function getTokenTVL( + tokenAddress: Address, +): Promise<{ tvl: string; tvlRaw: bigint; reserveToken: Address } | null> { + try { + const result = await publicClient.readContract({ + address: MCV2_BOND, + abi: mcv2BondAbi, + functionName: "tokenBond", + args: [tokenAddress], + }); + + const [, , , , reserveToken, reserveBalance] = result; + + return { + tvl: formatUnits(reserveBalance, 18), + tvlRaw: reserveBalance, + reserveToken: reserveToken as Address, + }; + } catch { + return null; + } +} From 9758f70606b6bf8b7b5f2e3bf3c386039279346a Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Sun, 15 Mar 2026 07:36:09 +0000 Subject: [PATCH 2/2] [#80] Fix getTokenTVL to fetch reserve token decimals on-chain Instead of hardcoding 18 decimals, read the reserve token's actual decimals via ERC-20 decimals() call for correct TVL formatting. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/price.ts | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/lib/price.ts b/lib/price.ts index 4db98e32..f63e10f7 100644 --- a/lib/price.ts +++ b/lib/price.ts @@ -209,14 +209,25 @@ export async function get24hPriceChange( } } +const erc20DecimalsAbi = [ + { + type: "function", + name: "decimals", + stateMutability: "view", + inputs: [], + outputs: [{ name: "", type: "uint8" }], + }, +] as const; + /** * Get TVL (reserve balance) for a token from its MCV2_Bond tokenBond data. + * Fetches the reserve token's decimals on-chain for correct formatting. * * Returns null if the read fails. */ export async function getTokenTVL( tokenAddress: Address, -): Promise<{ tvl: string; tvlRaw: bigint; reserveToken: Address } | null> { +): Promise<{ tvl: string; tvlRaw: bigint; reserveToken: Address; decimals: number } | null> { try { const result = await publicClient.readContract({ address: MCV2_BOND, @@ -226,11 +237,19 @@ export async function getTokenTVL( }); const [, , , , reserveToken, reserveBalance] = result; + const reserveAddr = reserveToken as Address; + + const decimals = await publicClient.readContract({ + address: reserveAddr, + abi: erc20DecimalsAbi, + functionName: "decimals", + }); return { - tvl: formatUnits(reserveBalance, 18), + tvl: formatUnits(reserveBalance, decimals), tvlRaw: reserveBalance, - reserveToken: reserveToken as Address, + reserveToken: reserveAddr, + decimals, }; } catch { return null;