Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions lib/price.ts
Original file line number Diff line number Diff line change
@@ -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<TokenPriceInfo | null> {
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;
}
}
41 changes: 39 additions & 2 deletions src/app/story/[storylineId]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 }>;

Expand Down Expand Up @@ -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 (
<div className="mx-auto max-w-2xl px-6 py-12">
<StoryHeader storyline={storyline} />
<StoryHeader storyline={storyline} priceInfo={priceInfo} />
<div className="mt-10 space-y-10">
{plots.map((plot) => (
<PlotEntry key={plot.id} plot={plot} />
Expand All @@ -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 (
<header className="border-border border-b pb-6">
<h1 className="text-accent text-2xl font-bold tracking-tight">
Expand All @@ -78,6 +94,27 @@ function StoryHeader({ storyline }: { storyline: Storyline }) {
</span>
)}
</div>

{priceInfo && (
<div className="border-border bg-surface mt-4 grid grid-cols-2 gap-2 rounded border px-3 py-2 text-xs">
<div>
<span className="text-muted block text-[10px] uppercase tracking-wider">
Token Price
</span>
<span className="text-foreground">
{priceInfo.pricePerToken} {reserveLabel}
</span>
</div>
<div>
<span className="text-muted block text-[10px] uppercase tracking-wider">
Supply Minted
</span>
<span className="text-foreground">
{priceInfo.totalSupply} tokens
</span>
</div>
</div>
)}
{storyline.sunset ? (
<div className="border-border bg-surface mt-4 rounded border px-3 py-2 text-xs">
<span className="text-muted">Story complete</span>
Expand Down
Loading