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
29 changes: 29 additions & 0 deletions lib/contracts/abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
// ---------------------------------------------------------------------------
Expand Down
110 changes: 105 additions & 5 deletions lib/price.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -70,6 +74,8 @@ export const mcv2BondAbi = [
inputs: [{ name: "token", type: "address" }],
outputs: [],
},
priceForNextMintFunction,
tokenBondFunction,
] as const;

export const erc20Abi = [
Expand Down Expand Up @@ -122,21 +128,20 @@ 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.
*/
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: "getReserveForToken",
args: [tokenAddress, oneToken],
functionName: "priceForNextMint",
args: [tokenAddress],
}),
publicClient.readContract({
address: tokenAddress,
Expand All @@ -147,11 +152,106 @@ export async function getTokenPrice(

return {
pricePerToken: formatUnits(priceRaw, 18),
priceRaw,
priceRaw: BigInt(priceRaw),
totalSupply: formatUnits(totalSupplyRaw, 18),
totalSupplyRaw,
};
} catch {
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;
}
}

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; decimals: number } | null> {
try {
const result = await publicClient.readContract({
address: MCV2_BOND,
abi: mcv2BondAbi,
functionName: "tokenBond",
args: [tokenAddress],
});

const [, , , , reserveToken, reserveBalance] = result;
const reserveAddr = reserveToken as Address;

const decimals = await publicClient.readContract({
address: reserveAddr,
abi: erc20DecimalsAbi,
functionName: "decimals",
});

return {
tvl: formatUnits(reserveBalance, decimals),
tvlRaw: reserveBalance,
reserveToken: reserveAddr,
decimals,
};
} catch {
return null;
}
}
Loading