Skip to main content

Frontend Integration

Now connect the React app to the deployed contracts using wagmi hooks.

1. Contract addresses & ABIs

Keep addresses and minimal ABIs in one place:
src/lib/contracts.ts
export const RUPIAH_TOKEN_ADDRESS =
  "0x7C78582DEa2a03d982D5fb0975fb4895E731dD05" as const // Sepolia
export const VAULT_ADDRESS =
  "0x8E81aB5f111670D732D961e892E94aCFcD593b07" as const // Sepolia

export const TOKEN_SYMBOL = "IDRT"
export const TOKEN_DECIMALS = 18

export const rupiahTokenAbi = [
  { type: "function", name: "balanceOf", stateMutability: "view",
    inputs: [{ name: "account", type: "address" }], outputs: [{ type: "uint256" }] },
  { type: "function", name: "allowance", stateMutability: "view",
    inputs: [{ type: "address" }, { type: "address" }], outputs: [{ type: "uint256" }] },
  { type: "function", name: "approve", stateMutability: "nonpayable",
    inputs: [{ type: "address" }, { type: "uint256" }], outputs: [{ type: "bool" }] },
  { type: "function", name: "mint", stateMutability: "nonpayable",
    inputs: [{ type: "address" }, { type: "uint256" }], outputs: [] },
] as const

export const vaultAbi = [
  { type: "function", name: "balances", stateMutability: "view",
    inputs: [{ type: "address" }], outputs: [{ type: "uint256" }] },
  { type: "function", name: "deposit", stateMutability: "nonpayable",
    inputs: [{ type: "uint256" }], outputs: [] },
  { type: "function", name: "withdraw", stateMutability: "nonpayable",
    inputs: [{ type: "uint256" }], outputs: [] },
] as const

2. Read balances

useReadContract reads on-chain view functions. Enable each only when a wallet is connected:
const { address } = useAccount()
const opts = { query: { enabled: !!address } }

const wallet = useReadContract({
  address: RUPIAH_TOKEN_ADDRESS, abi: rupiahTokenAbi,
  functionName: "balanceOf", args: address ? [address] : undefined, ...opts,
})
const savings = useReadContract({
  address: VAULT_ADDRESS, abi: vaultAbi,
  functionName: "balances", args: address ? [address] : undefined, ...opts,
})
const allowance = useReadContract({
  address: RUPIAH_TOKEN_ADDRESS, abi: rupiahTokenAbi,
  functionName: "allowance", args: address ? [address, VAULT_ADDRESS] : undefined, ...opts,
})

3. Write transactions

useWriteContract sends a transaction; useWaitForTransactionReceipt watches it. After success, refetch the reads so balances update:
const { writeContract, data: hash, isPending, error: writeError } = useWriteContract()
const { isLoading, isSuccess, isError, error: receiptError } =
  useWaitForTransactionReceipt({ hash })

useEffect(() => {
  if (isSuccess) {
    wallet.refetch()
    savings.refetch()
    allowance.refetch()
  }
}, [isSuccess]) // eslint-disable-line react-hooks/exhaustive-deps
The four actions, all via the same writeContract:
// 1) Mint (open faucet) — to your own address
writeContract({ address: RUPIAH_TOKEN_ADDRESS, abi: rupiahTokenAbi,
  functionName: "mint", args: [address, wei] })

// 2) Approve the Vault
writeContract({ address: RUPIAH_TOKEN_ADDRESS, abi: rupiahTokenAbi,
  functionName: "approve", args: [VAULT_ADDRESS, wei] })

// 3) Deposit into the Vault
writeContract({ address: VAULT_ADDRESS, abi: vaultAbi,
  functionName: "deposit", args: [wei] })

// 4) Withdraw back to your wallet
writeContract({ address: VAULT_ADDRESS, abi: vaultAbi,
  functionName: "withdraw", args: [wei] })
wei is the amount parsed to 18 decimals with parseUnits(amount, 18).

4. Friendly numbers

Balances and the input are easier to read with thousand separators:
src/lib/helpers.ts
import { formatUnits } from "viem"

const numberFmt = new Intl.NumberFormat("en-US", { maximumFractionDigits: 4 })

// 1_000_000n → "1,000,000"
export const formatAmount = (value: bigint | undefined, decimals: number) =>
  numberFmt.format(Number(formatUnits(value ?? 0n, decimals)))

// group the input while typing: "1000000" → "1,000,000"
export const groupDigits = (raw: string) => {
  if (!raw) return ""
  const [int, dec] = raw.split(".")
  const grouped = (int || "0").replace(/\B(?=(\d{3})+(?!\d))/g, ",")
  return dec === undefined ? grouped : `${grouped}.${dec}`
}

5. Loading & transaction status

While a tx is in flight, show the clicked button as Loading…, and after it settles show a link to Etherscan:
const busy = isPending || isLoading
const label = (text: string) =>
  lastAction === text && busy ? "Loading…" : text

// derive status without setState-in-effect
let status: { ok: boolean; hash?: `0x${string}`; msg?: string } | null = null
if (!busy) {
  if (isSuccess && hash) status = { ok: true, hash }
  else if (writeError || isError)
    status = { ok: false, hash, msg: shortError(writeError ?? receiptError) }
}
{status && (
  <p>
    Transaksi {lastAction} {status.ok ? "sukses" : "gagal"}:{" "}
    {status.hash
      ? <a href={`https://sepolia.etherscan.io/tx/${status.hash}`} target="_blank" rel="noopener noreferrer">{shortHash(status.hash)}</a>
      : <span>{status.msg}</span>}
  </p>
)}

6. Guard deposit on allowance

deposit reverts if you haven’t approved enough — and a failed gas estimate surfaces as a confusing “gas limit too high”. Catch it before sending: if the allowance is too low, show a hint instead of a doomed transaction.
const needApprove = wei !== undefined && (allowance.data ?? 0n) < wei

// on Deposit click:
if (needApprove) {
  setShowApproveNote(true) // "Allowance kurang — Approve dulu"
  return                   // don't send a tx that will revert
}
After Approve succeeds, allowance refetches → needApprove becomes false → Deposit works.

The full flow

1

Connect

Connect MetaMask on Sepolia.
2

Mint

Click Mint to get IDRT (open faucet).
3

Approve

Enter an amount, click Approve.
4

Deposit

Click Deposit — tokens move into the Vault.
5

Withdraw

Click Withdraw to pull them back out.
That’s the full loop: a neo-brutalism dApp reading and writing real contracts on Sepolia. 🎉

Full Source

Copy every file in full from one page.