Skip to content
Protocol contracts not yet configured for this build
Enter app

Developers

Frontend integration

One rule underpins everything else: the only Plouto address a frontend may hardcode is the registry.

Why

A build that hardcodes the token address is correct only until someone forgets to redeploy it. A build that reads the token from the registry cannot be wrong about whether the protocol is live.

typescript
export const CONTRACTS = {  registry: readAddress('NEXT_PUBLIC_PLOUTO_REGISTRY', process.env.NEXT_PUBLIC_PLOUTO_REGISTRY),} as const;

That is the entire address configuration of this application.

Resolving everything else

typescript
export function useProtocol() {  const registry = CONTRACTS.registry;  const {data, isLoading, isError} = useReadContracts({    contracts: registry      ? ['revenueRouter','gravityStaking','buybackExecutor','reserve','ploutoToken','ploutoCurve','initialized']          .map((functionName) => ({address: registry, abi: ploutoRegistryAbi, functionName, chainId}))      : [],    query: {enabled: Boolean(registry), refetchInterval: 15_000},  });  // ...}

When setPloutoTokenOnce lands on chain, the next poll picks it up and the application activates. No rebuild, no redeploy, no environment change.

Gate every token action

tsx
<LaunchGate>  <StakePanel /></LaunchGate>

LaunchGate handles four distinct states, and none of them is optimistic:

StateRendered as
Registry not configuredAn explicit configuration error.
LoadingSkeletons plus a screen-reader announcement.
RPC error"Could not reach Robinhood Chain" — no protocol state assumed.
Not initializedThe literal prelaunch message; controls absent, not merely disabled.

Never fabricate a figure

Missing data must render as an em dash, never as zero:

typescript
export function formatEth(value: bigint | undefined | null, decimals = 4): string {  if (value === undefined || value === null) return EMPTY;   // '—'  const n = Number(formatUnits(value, 18));  if (n === 0) return '0';  if (n < 10 ** -decimals) return `<${(10 ** -decimals).toFixed(decimals)}`;  return n.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: decimals});}

A genuine zero renders 0. An unknown renders . The distinction has tests asserting it, because the failure mode — showing 0 for "I could not read this" — is exactly the kind of quiet lie that erodes trust.

Transaction states

Model all of them explicitly rather than inferring:

typescript
export type TxStatus =  | {kind: 'idle'}  | {kind: 'awaiting-signature'}  | {kind: 'submitted'; hash: Hash}  | {kind: 'confirming'; hash: Hash}  | {kind: 'confirmed'; hash: Hash}  | {kind: 'failed'; message: string; hash?: Hash}  | {kind: 'wrong-network'}  | {kind: 'insufficient-gas'}  | {kind: 'approval-required'}  | {kind: 'not-matured'};

Every hash-bearing state links to Blockscout, and the status line is an ARIA live region so screen readers hear the progression.

Preflight before offering to sign

typescript
export function usePreflight(estimatedGasCostWei = 300_000n * 1_000_000_000n) {  const {address, isConnected} = useAccount();  const chainId = useChainId();  const {data: balance} = useBalance({address, chainId: EXPECTED_CHAIN_ID});  return {    isConnected,    wrongNetwork: isConnected && chainId !== EXPECTED_CHAIN_ID,    insufficientGas: balance !== undefined && balance.value < estimatedGasCostWei,  };}

Disable the control and say why, rather than letting the wallet fail.

Refresh cadence

The interface polls every 15 seconds and refetches on window focus. Canonical financial state comes from chain on every poll; nothing is cached across sessions and no figure is ever served from a database.