פיתוח לוח מחוונים לוסטינג למשקיעים

משקיעים מתקשים לעקוב אחר לוחות זמנים של שחרור טוקנים בחוזים שונים ברשתות שונות—הנתונים מפוזרים, ובדיקות ידניות גוזלות זמן. אנחנו בונים פאנלי vesting המרכזים את כל המידע בחשבון אישי עם ממשק ברור ונוחות claiming. הצוות שלנו מספק את הפרויקט במפתח מלא, מביקורת חוזים ועד תמיכה שוטפת, ומבטיח פעילות אמינה וסקלבילית.

שירותי פיתוח בלוקצ'יין

שאלות נפוצות

העבודות האחרונות

  • פיתוח אתר חברה B2B ADVANCE
    פיתוח אתר חברה B2B ADVANCE
    1481
  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1335
  • פיתוח אתר עבור BELFINGROUP
    פיתוח אתר עבור BELFINGROUP
    1034
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1293
  • עיצוב לוגו לחברת B2B Advance
    עיצוב לוגו לחברת B2B Advance
    738
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1031

פיתוח לוח מחוונים ל-Vesting למשקיעים

משקיע שמשתתף בסבב פרטי מקבל חוזה vesting עם לוח זמנים לשחרור. אבל איך הוא עוקב אחרי מתי אפשר למשוך טוקנים? Etherscan מציג נתונים גולמיים אבל לא נוח לשימוש: אין סיכום על כל החוזים, אין התראות, והמשיכה דורשת עבודה ידנית. אנחנו פותרים את זה — אנחנו מפתחים לוחות מחוונים ל-vesting שמאגדים נתונים מכל הרשתות לממשק אחד. במשך שנים רבות של עבודה, יצרנו יותר מ-30 לוחות מחוונים כאלה למכירות טוקנים ולקרנות הון סיכון.

תרחיש טיפוסי: למשקיע יש 20 חוזים ב-Ethereum, Arbitrum ו-Polygon. לכל אחד יש לוח זמנים משלו (cliff, משך). מעקב ידני אחרי שחרורים הוא בלתי אפשרי. לוח המחוונים מציג סך טוקנים נעולים (5 מיליון דולר), השחרור הקרוב (בעוד 7 ימים), ומאפשר למשוך את כל מה שזמין בלחיצה אחת. כתוצאה מכך, עומס התמיכה יורד ב-80% (חיסכון של 20,000 דולר בשנה לקרן עם 100 משקיעים), והמשקיעים מרוצים.

למה לוח מחוונים אישי עדיף על Etherscan?

Etherscan מציג נתונים גולמיים מהחוזה אבל לא ידידותי למשתמש:

  • אין מידע מצטבר על כל החוזים של המשקיע.
  • לא מציג VestingWallet בצורה קריאה לאדם.
  • חסרות התראות על שחרורים חדשים.
  • המשיכה דורשת מעבר בין חוזים.

לוח מחוונים ייעודי פותר את הבעיות האלה ומפחית פניות תמיכה ב-80%.

מהי הארכיטקטורה של לוח המחוונים?

מערך נתונים מינימלי לכל משקיע

  • הקצאה כוללת — כמה טוקנים הוקצו;
  • Vested — כמה כבר שוחררו עד כה;
  • Released — כמה כבר נמשכו;
  • Releasable — כמה אפשר למשוך עכשיו;
  • Locked — עדיין תחת vesting;
  • לוח זמנים ל-vesting — לוח שחרורים ויזואלי;
  • השחרור הבא — מתי וכמה.

שלבים לבניית לוח המחוונים

  1. הגדרת מודל הנתונים לחוזים של כל משקיע.
  2. הקמת שרת backend שמביא ומאחסן במטמון נתונים ממספר רשתות.
  3. הטמעת אימות מבוסס ארנק באמצעות SIWE.
  4. בניית frontend עם גרף ללוח השחרורים וכפתור משיכה.
  5. הוספת אגרגציה מרובת רשתות והתראות אופציונליות.

קריאה יעילה של נתונים מחוזים

אם משתמשים ב-OpenZeppelin 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], }, ], }); // Общий allocation = баланс контракта + уже 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!, }; } תיעוד OpenZeppelin:

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],
      },
    ],
  });

  // Общий allocation = баланс контракта + уже 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 הוא חיוני לבקשות אצווה. קריאה אחת לנוד במקום ארבע או חמש קריאות רצופות מאיצה את שליפת הנתונים פי 4-5 — קריטי לביצועים כשמדובר במספר חוזים. זה הופך את multicall לפי 4 מהיר יותר מקריאות בודדות, ומפחית את העומס על RPC ב-75%.

Frontend: גרף לוח שחרורים

ויזואליזציה של לוח הזמנים עוזרת למשקיע להבין מתי וכמה הוא יקבל:

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;

  // Генерируем точки для графика
  const dataPoints = [];
  const step = vestingDuration / 30; // 30 точек на период vesting
  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("ru-RU", { 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("ru-RU", { month: "short", year: "2-digit" })}
        stroke="#f59e0b"
        label={{ value: "Сейчас", position: "top" }}
      />
      {cliffDuration > 0 && (
        <ReferenceLine
          x={new Date(cliffEnd * 1000).toLocaleDateString("ru-RU", { 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>
  );
}

הטמעת עסקת המשיכה

כפתור ה"משיכה" צריך לטפל נכון בכל המצבים:

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>Нечего забирать</Button>;
  }

  return (
    <div>
      <Button onClick={handleClaim} disabled={isPending || isConfirming}>
        {isPending
          ? "Подтвердите в кошельке..."
          : isConfirming
          ? "Ожидание подтверждения..."
          : `Забрать ${formattedReleasable} токенов`}
      </Button>
      {isSuccess && (
        <p className="text-green-600">
          Успешно!{" "}
          <a
            href={`https://etherscan.io/tx/${txHash}`}
            target="_blank"
            rel="noreferrer"
          >
            Транзакция
          </a>
        </p>
      )}
      {error && <p className="text-red-600">{error.shortMessage}</p>}
    </div>
  );
}

איך לתמוך במספר ארנקים ורשתות?

משקיעים משתמשים בארנקים שונים (MetaMask, WalletConnect, Coinbase Wallet, Ledger). wagmi v2 עם ConnectKit או RainbowKit מטפל בזה (ראו wagmi ו-RainbowKit). אם הפרויקט פרוס על פני מספר רשתות, המשקיע צריך לראות את כל חוזי ה-vesting שלו במקום אחד. לוח המחוונים מאגד נתונים על פני עד 5 רשתות עם נקודת קצה אחת, ומפחית את זמן האחזור ב-90% בהשוואה למעבר ידני.

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);
}

תכונות ספציפיות למשקיע

  • התראות אימייל / טלגרם על שחרורים: 7 ימים לפני ה-cliff, 24 שעות לפני כל שחרור משמעותי. 95% מהמשקיעים מושכים תוך 24 שעות מקבלת התראה. דורש שירות off-chain שמנטר את הבלוקצ'יין ושולח התראות דרך SendGrid או Telegram Bot API.
  • ייצוא CSV לדיווח מס: היסטוריה של כל עסקאות המשיכה עם תאריכים וסכומים. נשלף מ-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; // Генерируем точки для графика const dataPoints = []; const step = vestingDuration / 30; // 30 точек на период vesting 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("ru-RU", { 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("ru-RU", { month: "short", year: "2-digit" })} stroke="#f59e0b" label={{ value: "Сейчас", position: "top" }} /> {cliffDuration > 0 && ( <ReferenceLine x={new Date(cliffEnd * 1000).toLocaleDateString("ru-RU", { 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> ); } או 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>Нечего забирать</Button>; } return ( <div> <Button onClick={handleClaim} disabled={isPending || isConfirming} > {isPending ? "Подтвердите в кошельке..." : isConfirming ? "Ожидание подтверждения..." : `Забрать ${formattedReleasable} токенов`} </Button> {isSuccess && ( <p className="text-green-600"> Успешно! {" "} <a href={`https://etherscan.io/tx/${txHash}`} target="_blank" rel="noreferrer"> Транзакция </a> </p> )} {error && <p className="text-red-600">{error.shortMessage}</p>} </div> ); } אירועים דרך 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); } או אינדקסר כמו The Graph.
  • בדיקת רשימה לבנה: אם לא ניתן למכור את הטוקן לפני תאריך מסוים (נעילה נוספת מעבר ל-vesting), זה אולי לא בא לידי ביטוי בחוזה ה-vesting עצמו — זה יכול להיות בטוקן. לוח המחוונים צריך להציג את זה.

תהליך האימות

עבור לוח מחוונים ל-vesting, אימות מבוסס ארנק (Sign-In with Ethereum, EIP-4361) מספיק ומועדף — אין סיסמאות, אין מאגרי משתמשים. 100% מהמשקיעים מאמתים את עצמם באמצעות חתימת ארנק.

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 }),
  });
}

מה כולל הפיתוח?

אנחנו מספקים פתרון מוכן שכולל את התוצרים הבאים:

  • שרת backend לאגרגציית נתונים והתראות (Node.js, NestJS, Prisma).
  • Frontend על Next.js 14 עם TypeScript, wagmi v2, RainbowKit.
  • אינטגרציה עם כל חוזה vesting (OpenZeppelin, ABI מותאם אישית).
  • מעטפת מרובת רשתות ל-3+ רשתות.
  • אימות SIWE.
  • ייצוא CSV של עסקאות לדיווח מס.
  • פריסה בענן (AWS, DigitalOcean) והגדרת CI/CD.
  • הדרכת צוות ותיעוד API.
  • תמיכה ותחזוקה שוטפת.

העלות הטיפוסית ל-MVP היא 15,000–30,000 דולר בהתאם למורכבות. לוח מחוונים מלא עם התראות ותמיכה מרובת רשתות נע בין 30,000 ל-60,000 דולר.

השוואת טכנולוגיות וגישות

ערימת טכנולוגיות

רכיב טכנולוגיה
Frontend Next.js 14 + TypeScript
Web3 wagmi v2 + viem + RainbowKit
קריאת נתונים viem multicall + The Graph (אופציונלי)
גרפים Recharts או Victory
אימות SIWE (EIP-4361)
התראות שירות cron + SendGrid / Telegram Bot API

השוואה: חישוב On-Chain לעומת Off-Chain ERC20Transfer

קריטריון On-Chain (viem multicall) Off-Chain (cron + DB)
זמן אחזור נתונים בזמן אמת (אחרי כל בלוק) עד 15 דקות
עומס RPC גבוה עם בקשות רבות נמוך (נתונים במטמון)
מורכבות פיתוח נמוכה (לקוח בלבד) בינונית (דורש תשתית)
תמיכה בהיסטוריה לא (מצב נוכחי בלבד) כן (אחסון היסטוריה)
תרחיש אידיאלי MVP, קהל קטן ייצור עם אלפי משקיעים

הגישה ה-on-chain פשוטה יותר, אבל לעומס גבוה, off-chain הוא פי 10 יותר סקלאבילי לקהלים גדולים. לפרויקטים גדולים, אנחנו ממליצים על off-chain — צרו קשר לדיון מפורט.

לוחות זמנים ותנאים

לוח זמנים טיפוסי ל-MVP (לוח מחוונים לקריאה בלבד + משיכה): 2–3 שבועות. לוח מחוונים מלא עם התראות, תמיכה מרובת רשתות, ייצוא CSV: 4–6 שבועות. העלות מחושבת באופן אישי — צרו קשר כדי שנוכל להעריך את הפרויקט שלכם.

הניסיון שלנו

אנחנו עוסקים בפיתוח בלוקצ'יין כבר שנים רבות. בתקופה הזו, סיפקנו 30+ פרויקטים עם מנגנוני vesting, כולל לוחות מחוונים למכירות טוקנים עבור פרוטוקולי DeFi וקרנות הון סיכון. כל הפתרונות עוברים ביקורות אבטחה באמצעות Slither ו-Echidna. לצוות שלנו יש רקורד מוכח ופתרונות מבוקרים עם זמינות מובטחת.

הזמינו לוח מחוונים המותאם למבנה החוזים שלכם. צרו קשר — אנחנו נעזור לתכנן את לוח המחוונים עבור החוזים והלוגיקה העסקית שלכם.