Frontend integration
Connect your frontend to Mandala: chain config, wallet, reading and writing contracts with viem and wagmi.
Mandala is EVM-equivalent. Any frontend stack that talks to Ethereum talks to Mandala by swapping the chain config. This page walks through viem and wagmi, the two most common stacks. ethers.js gets a short section at the bottom.
Install
npm install viem wagmi @tanstack/react-queryOr with bun:
bun add viem wagmi @tanstack/react-queryDefine the chain
viem and wagmi both accept a chain object. Define Mandala once and reuse:
// lib/chains.ts
import { defineChain } from "viem";
export const mandala = defineChain({
id: 20010,
name: "Mandala Chain",
nativeCurrency: { name: "Kepeng", symbol: "KPG", decimals: 18 },
rpcUrls: {
default: { http: ["https://rpc1-mainnet.mandalachain.io"] },
},
blockExplorers: {
default: {
name: "Mandala Explorer",
url: "https://explorer.mandalachain.io",
},
},
});
export const mandalaTestnet = defineChain({
id: 20011,
name: "Mandala Testnet",
nativeCurrency: { name: "Kepeng Test", symbol: "KPGT", decimals: 18 },
rpcUrls: {
default: { http: ["https://rpc1-testnet.mandalachain.io"] },
},
blockExplorers: {
default: {
name: "Mandala Testnet Explorer",
url: "https://explorer.testnet.mandalachain.io",
},
},
testnet: true,
});Configure wagmi
// lib/wagmi.ts
import { createConfig, http } from "wagmi";
import { injected, metaMask } from "wagmi/connectors";
import { mandala, mandalaTestnet } from "./chains";
export const config = createConfig({
chains: [mandala, mandalaTestnet],
connectors: [injected(), metaMask()],
transports: {
[mandala.id]: http(),
[mandalaTestnet.id]: http(),
},
});Wrap your app in the providers:
// app/providers.tsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { WagmiProvider } from "wagmi";
import { config } from "@/lib/wagmi";
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</WagmiProvider>
);
}Connect a wallet
// components/ConnectButton.tsx
"use client";
import { useAccount, useConnect, useDisconnect } from "wagmi";
export function ConnectButton() {
const { address, isConnected } = useAccount();
const { connect, connectors } = useConnect();
const { disconnect } = useDisconnect();
if (isConnected) {
return (
<button onClick={() => disconnect()}>
{address?.slice(0, 6)}...{address?.slice(-4)} (disconnect)
</button>
);
}
return (
<button onClick={() => connect({ connector: connectors[0] })}>
Connect wallet
</button>
);
}When users click Connect, their wallet prompts to switch to (or add) Mandala if it is not already configured.
Get a contract ABI
Two paths.
From your build artifacts. Hardhat writes ABIs to artifacts/contracts/Counter.sol/Counter.json. Foundry writes them to out/Counter.sol/Counter.json. Import the abi field directly.
From the explorer. For contracts you do not own, open the contract on explorer.mandalachain.io, click the Contract tab, and copy the ABI from the ABI sub-tab. The contract must be verified for the ABI to be available there.
Read from a contract
"use client";
import { useReadContract } from "wagmi";
import { mandala } from "@/lib/chains";
const counterAbi = [
{
name: "count",
type: "function",
stateMutability: "view",
inputs: [],
outputs: [{ type: "uint256" }],
},
] as const;
export function CounterValue({ address }: { address: `0x${string}` }) {
const { data, isPending, error } = useReadContract({
chainId: mandala.id,
address,
abi: counterAbi,
functionName: "count",
});
if (isPending) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return <p>Count: {data?.toString()}</p>;
}useReadContract is a thin wrapper over viem's publicClient.readContract. The as const on the ABI gives full TypeScript inference for arguments and return types.
Write to a contract
"use client";
import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
const counterAbi = [
{
name: "increment",
type: "function",
stateMutability: "nonpayable",
inputs: [],
outputs: [],
},
] as const;
export function IncrementButton({ address }: { address: `0x${string}` }) {
const { writeContract, data: hash, isPending } = useWriteContract();
const { isLoading: isMining, isSuccess } = useWaitForTransactionReceipt({ hash });
return (
<div>
<button
disabled={isPending || isMining}
onClick={() =>
writeContract({
address,
abi: counterAbi,
functionName: "increment",
})
}
>
{isPending ? "Confirming..." : isMining ? "Mining..." : "Increment"}
</button>
{isSuccess && <p>Done.</p>}
</div>
);
}The user's wallet handles network switching to Mandala automatically. useWaitForTransactionReceipt polls the chain until the transaction lands.
viem without wagmi
If you do not need React hooks, use viem clients directly:
import { createPublicClient, createWalletClient, custom, http } from "viem";
import { mandala } from "./chains";
const publicClient = createPublicClient({
chain: mandala,
transport: http(),
});
const walletClient = createWalletClient({
chain: mandala,
transport: custom(window.ethereum!),
});
const count = await publicClient.readContract({
address: "0xYourContract",
abi: counterAbi,
functionName: "count",
});
const hash = await walletClient.writeContract({
account: "0xYourAddress",
address: "0xYourContract",
abi: counterAbi,
functionName: "increment",
});ethers.js
ethers.js works against any EVM chain via a custom provider:
import { JsonRpcProvider, Contract } from "ethers";
const provider = new JsonRpcProvider("https://rpc1-mainnet.mandalachain.io", {
name: "Mandala Chain",
chainId: 20010,
});
const counter = new Contract("0xYourContract", counterAbi, provider);
const value = await counter.count();Read/write APIs are identical to ethers on Ethereum.

