Investor Vesting Panel Development

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Investor Vesting Panel Development
Medium
~3-5 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Investor Vesting Panel Development

Task: investor bought tokens in private round, has vesting contract on blockchain, needs convenient interface to see unlocked amount, locked amount, claim available tokens. Sounds simple—until you face dozens of contracts on multiple networks, multi-sig investor wallets, and wallet-agnostic requirements.

Panel Architecture

What Should Display

Minimum for each investor:

  • Total allocation — total tokens allocated
  • Vested — unlocked by now
  • Released — already claimed
  • Releasable — can claim right now
  • Locked — still under vesting
  • Vesting schedule — unlock timeline (visual)
  • Next unlock — when next release and how much

Read from Contracts

If using OpenZeppelin VestingWallet:

import { createPublicClient, http, parseAbi } from "viem";

const VESTING_ABI = parseAbi([
  "function beneficiary() view returns (address)",
  "function start() view returns (uint64)",
  "function duration() view returns (uint64)",
  "function released(address token) view returns (uint256)",
  "function releasable(address token) view returns (uint256)",
  "function vestedAmount(address token, uint64 timestamp) view returns (uint256)",
]);

async function getVestingData(
  vestingAddress: `0x${string}`,
  tokenAddress: `0x${string}`,
  client: PublicClient
) {
  const [start, duration, released, releasable] = await client.multicall({
    contracts: [
      { address: vestingAddress, abi: VESTING_ABI, functionName: "start" },
      { address: vestingAddress, abi: VESTING_ABI, functionName: "duration" },
      {
        address: vestingAddress,
        abi: VESTING_ABI,
        functionName: "released",
        args: [tokenAddress],
      },
      {
        address: vestingAddress,
        abi: VESTING_ABI,
        functionName: "releasable",
        args: [tokenAddress],
      },
    ],
  });
  
  // Total allocation = contract balance + already released
  const balance = await client.readContract({
    address: tokenAddress,
    abi: parseAbi(["function balanceOf(address) view returns (uint256)"]),
    functionName: "balanceOf",
    args: [vestingAddress],
  });
  
  const totalAllocation = balance.result! + released.result!;
  
  return {
    start: Number(start.result),
    duration: Number(duration.result),
    released: released.result!,
    releasable: releasable.result!,
    totalAllocation,
    locked: totalAllocation - released.result! - releasable.result!,
  };
}

multicall is mandatory—batch requests. One node call instead of four-five sequential—critical for performance with multiple contracts.

Frontend: Vesting Chart Component

Visualization helps investor understand when and how much they'll receive:

import { LineChart, Line, XAxis, YAxis, Tooltip, ReferenceLine } from "recharts";
import { formatUnits } from "viem";

function VestingChart({ start, cliffDuration, vestingDuration, totalAllocation, decimals }) {
  const cliffEnd = start + cliffDuration;
  const vestingEnd = cliffEnd + vestingDuration;
  const now = Date.now() / 1000;
  
  // Generate chart points
  const dataPoints = [];
  const step = vestingDuration / 30; // 30 points
  
  for (let t = start; t <= vestingEnd; t += step) {
    let vested = 0;
    if (t >= cliffEnd) {
      const elapsed = Math.min(t - cliffEnd, vestingDuration);
      vested = Number(formatUnits(
        BigInt(Math.floor(Number(totalAllocation) * elapsed / vestingDuration)),
        decimals
      ));
    }
    dataPoints.push({
      date: new Date(t * 1000).toLocaleDateString("en-US", { month: "short", year: "2-digit" }),
      vested,
    });
  }
  
  return (
    <LineChart width={600} height={300} data={dataPoints}>
      <XAxis dataKey="date" tick={{ fontSize: 11 }} />
      <YAxis tickFormatter={(v) => `${(v / 1000).toFixed(0)}k`} />
      <Tooltip
        formatter={(value) => [`${Number(value).toLocaleString()} tokens`, "Vested"]}
      />
      <ReferenceLine
        x={new Date(now * 1000).toLocaleDateString("en-US", { month: "short", year: "2-digit" })}
        stroke="#f59e0b"
        label={{ value: "Now", position: "top" }}
      />
      {cliffDuration > 0 && (
        <ReferenceLine
          x={new Date(cliffEnd * 1000).toLocaleDateString("en-US", { month: "short", year: "2-digit" })}
          stroke="#6366f1"
          strokeDasharray="4 4"
          label={{ value: "Cliff", position: "top" }}
        />
      )}
      <Line type="monotone" dataKey="vested" stroke="#10b981" strokeWidth={2} dot={false} />
    </LineChart>
  );
}

Claim Transaction

Claim button must handle all states:

import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";

function ClaimButton({ vestingAddress, tokenAddress, releasable, decimals }) {
  const { writeContract, data: txHash, isPending, error } = useWriteContract();
  const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
    hash: txHash,
  });
  
  const handleClaim = () => {
    writeContract({
      address: vestingAddress,
      abi: VESTING_ABI,
      functionName: "release",
      args: [tokenAddress],
    });
  };
  
  const formattedReleasable = Number(formatUnits(releasable, decimals)).toLocaleString();
  
  if (releasable === 0n) {
    return <Button disabled>Nothing to claim</Button>;
  }
  
  return (
    <div>
      <Button
        onClick={handleClaim}
        disabled={isPending || isConfirming}
      >
        {isPending ? "Confirm in wallet..." :
         isConfirming ? "Confirming..." :
         `Claim ${formattedReleasable} tokens`}
      </Button>
      {isSuccess && (
        <p className="text-green-600">
          Success! {" "}
          <a href={`https://etherscan.io/tx/${txHash}`} target="_blank">
            Transaction
          </a>
        </p>
      )}
      {error && <p className="text-red-600">{error.shortMessage}</p>}
    </div>
  );
}

Multi-Wallet and Multi-Chain Support

Investors use different wallets (MetaMask, WalletConnect, Coinbase, Ledger). wagmi v2 with ConnectKit/RainbowKit handles this.

For multi-chain deployment, investor sees all vesting contracts in one place:

const NETWORKS = [
  { chainId: 1, name: "Ethereum", client: mainnetClient },
  { chainId: 42161, name: "Arbitrum", client: arbitrumClient },
];

async function getAllVestings(investorAddress: string) {
  const results = await Promise.all(
    NETWORKS.map(async (network) => {
      const vestingAddress = VESTING_CONTRACTS[network.chainId]?.[investorAddress];
      if (!vestingAddress) return null;
      
      const data = await getVestingData(vestingAddress, TOKEN_ADDRESS, network.client);
      return { ...data, network: network.name, chainId: network.chainId, vestingAddress };
    })
  );
  
  return results.filter(Boolean);
}

Investor-Specific Features

Email/Telegram notifications on unlocks: 7 days before cliff, 24 hours before each significant unlock. Requires off-chain service monitoring blockchain and sending notifications.

CSV export for tax reporting: history of all claim transactions with dates and amounts. From ERC20Transfer or TokensReleased events via getLogs or indexer (The Graph).

Whitelist check: if token can't be sold until date (additional lock-up beyond vesting), this may not be in vesting contract—may be in token itself. Panel should display this.

Authentication

For vesting panel, wallet auth (Sign-In with Ethereum, EIP-4361) is sufficient and preferred—no passwords, no user databases.

import { SiweMessage } from "siwe";

async function signIn(address: string, chainId: number) {
  const nonce = await fetch("/api/nonce").then((r) => r.text());
  
  const message = new SiweMessage({
    domain: window.location.host,
    address,
    statement: "Sign in to view your vesting schedule",
    uri: window.location.origin,
    version: "1",
    chainId,
    nonce,
  });
  
  const signature = await walletClient.signMessage({
    message: message.prepareMessage(),
  });
  
  await fetch("/api/verify", {
    method: "POST",
    body: JSON.stringify({ message, signature }),
  });
}

Stack

Component Technology
Frontend Next.js 14 + TypeScript
Web3 wagmi v2 + viem + RainbowKit
Data reading viem multicall + The Graph (optional)
Charts Recharts or Victory
Auth SIWE (EIP-4361)
Notifications cron-service + SendGrid / Telegram Bot API

MVP timeline (read-only dashboard + claim): 2–3 weeks. Full panel with notifications, multi-chain, CSV export: 4–6 weeks.