יצירת ממשק סטייקינג עם תגמולים בזמן אמת
משתמש מבקר באתר שלך, מחבר ארנק דרך MetaMask, ורואה APR של 18.5%. כפתור ה-'Stake' אפור—ההרשאה נמוכה מהסכום המבוקש. נקודת כאב טיפוסית ב-DeFi: אישור וסטייקינג הם שתי פעולות נפרדות ששוברות את חוויית המשתמש אם לא מטפלים באישור אוטומטי. אנו מאחדים אותן לזרימה אחת: ראשית בודקים הרשאה, שולחים עסקת אישור אם נדרש, ולאחר מכן מבצעים סטייקינג. התהליך כולל שתי עסקאות, אך המשתמש רואה רק אחת—אנו מסתירים את האישור מאחורי הקלעים או מבקשים ממנו לחתום על שתיהן בבת אחת. מונה תגמולים בזמן אמת שמתעדכן כל 100 אלפיות השנייה שומר על המשתמשים בדף: התגמולים גדלים מול עיניהם.
ממשק סטייקינג הוא טופס ולוח בקרה להפקדת טוקנים בחוזה שצובר תגמולים. המשתמש מפקיד טוקנים, רואה תגמולים מצטברים בזמן אמת, ויכול למשוך אותם ולבצע משיכה. מתחת למכסה המנוע: אישור + סטייקינג, חישוב תגמולים תקופתי, משיכה עם אפשרות לתקופות נעילה. אנו משתמשים בחוזה סטנדרטי בסגנון Synthetix שניתן להתאמה בקלות לכל ABI (ויקיפדיה).
בעיות טיפוסיות באישור וסטייקינג
הבעיה העיקרית היא שמשתמשים לא מבינים למה הם צריכים לאשר קודם ואז לבצע סטייקינג. אנו פותרים זאת באמצעות useStakeAction, שבודק אוטומטית allowance ומפעיל אישור לפני סטייקינג אם ההרשאה אינה מספקת. הבעיה השנייה היא שהמשתמש שוכח לחתום על האישור ב-MetaMask, מה שגורם לעסקה להיתקע. אנו מציגים סטטוס: 'נדרש אישור', 'מאשר...', 'מבצע סטייקינג...'. השלישית היא מגבלות גז: אישור על ERC-20 עולה כ-45k גז, סטייקינג כ-150k גז. אם יתרת הארנק נמוכה, העסקה עלולה להיכשל. אנו מזהירים לגבי דרישות יתרה מינימלית.
ערימת טכנולוגיות וארכיטקטורה
בליבה, אנו משתמשים ב-wagmi/viem לאינטראקציות עם החוזה. כל המצב נקרא דרך useReadContracts עם מרווח של 12 שניות. לאינטרפולציה מקומית של תגמולים אנו משתמשים ב-useEarnedRealtime. ממשק המשתמש בנוי עם Tailwind CSS; ללא ספריות רכיבים של צד שלישי.
ABI טיפוסי של חוזה סטייקינג:
const STAKING_ABI = parseAbi([
'function totalSupply() view returns (uint256)',
'function balanceOf(address account) view returns (uint256)',
'function earned(address account) view returns (uint256)',
'function rewardRate() view returns (uint256)',
'function rewardPerToken() view returns (uint256)',
'function periodFinish() view returns (uint256)',
'function lockPeriod() view returns (uint256)',
'function unlockTime(address) view returns (uint256)',
'function stake(uint256 amount) nonpayable',
'function withdraw(uint256 amount) nonpayable',
'function getReward() nonpayable',
'function exit() nonpayable',
]);חישוב APR/APY:
import { formatUnits } from 'viem';
export function calculateAPR(
rewardRate: bigint,
totalSupply: bigint,
stakingTokenPrice: number,
rewardTokenPrice: number,
stakingDecimals = 18,
rewardDecimals = 18,
): number {
if (totalSupply === 0n) return 0;
const rewardPerYear =
(parseFloat(formatUnits(rewardRate, rewardDecimals)) * 31_536_000) *
rewardTokenPrice;
const totalStakedUSD =
parseFloat(formatUnits(totalSupply, stakingDecimals)) * stakingTokenPrice;
return (rewardPerYear / totalStakedUSD) * 100;
}
export function aprToApy(apr: number, compoundsPerYear = 365): number {
return (
(Math.pow(1 + apr / 100 / compoundsPerYear, compoundsPerYear) - 1) * 100
);
}
Hook למצב סטייקינג:
import { useReadContracts } from 'wagmi';
import { erc20Abi, formatUnits } from 'viem';
const STAKING = process.env.NEXT_PUBLIC_STAKING_CONTRACT as `0x${string}`;
const STAKE_TOKEN = process.env.NEXT_PUBLIC_STAKE_TOKEN as `0x${string}`;
const REWARD_TOKEN = process.env.NEXT_PUBLIC_REWARD_TOKEN as `0x${string}`;
export function useStakingState() {
const { address } = useAccount();
const { data } = useReadContracts({
contracts: [
{ address: STAKING, abi: STAKING_ABI, functionName: 'totalSupply' },
{ address: STAKING, abi: STAKING_ABI, functionName: 'rewardRate' },
{ address: STAKING, abi: STAKING_ABI, functionName: 'periodFinish' },
{ address: STAKE_TOKEN, abi: erc20Abi, functionName: 'balanceOf', args: [address!] },
{ address: STAKE_TOKEN, abi: erc20Abi, functionName: 'allowance', args: [address!, STAKING] },
{ address: STAKING, abi: STAKING_ABI, functionName: 'balanceOf', args: [address!] },
{ address: STAKING, abi: STAKING_ABI, functionName: 'earned', args: [address!] },
],
query: {
enabled: !!address,
refetchInterval: 12_000,
},
});
const totalSupply = data?.[0].result as bigint ?? 0n;
const rewardRate = data?.[1].result as bigint ?? 0n;
const periodFinish = Number(data?.[2].result as bigint ?? 0n);
const walletBalance = data?.[3].result as bigint ?? 0n;
const allowance = data?.[4].result as bigint ?? 0n;
const stakedBalance = data?.[5].result as bigint ?? 0n;
const earned = data?.[6].result as bigint ?? 0n;
const isActive = periodFinish > Date.now() / 1000;
return {
totalSupply,
rewardRate,
walletBalance,
allowance,
stakedBalance,
earned,
isActive,
needsApprove: (amount: bigint) => allowance < amount,
};
}אישור + סטייקינג בזרימה אחת:
import { useWriteContract } from 'wagmi';
import { erc20Abi, parseUnits } from 'viem';
import { waitForTransactionReceipt } from '@wagmi/core';
import { config } from '@/lib/wagmi';
export function useStakeAction() {
const { writeContractAsync } = useWriteContract();
const [step, setStep] = useState<'idle' | 'approving' | 'staking' | 'done' | 'error'>('idle');
const [txHash, setTxHash] = useState<`0x${string}`>();
const { needsApprove } = useStakingState();
const stake = async (amount: string, decimals: number) => {
const amountWei = parseUnits(amount, decimals);
try {
if (needsApprove(amountWei)) {
setStep('approving');
const approveTx = await writeContractAsync({
address: STAKE_TOKEN,
abi: erc20Abi,
functionName: 'approve',
args: [STAKING, amountWei],
});
await waitForTransactionReceipt(config, { hash: approveTx });
}
setStep('staking');
const stakeTx = await writeContractAsync({
address: STAKING,
abi: STAKING_ABI,
functionName: 'stake',
args: [amountWei],
});
setTxHash(stakeTx);
setStep('done');
} catch (e) {
setStep('error');
throw e;
}
};
return { stake, step, txHash };
}מונה תגמולים בזמן אמת:
export function useEarnedRealtime(
earnedOnChain: bigint,
stakedBalance: bigint,
rewardPerToken: bigint,
lastUpdatedAt: number,
): bigint {
const [displayed, setDisplayed] = useState(earnedOnChain);
useEffect(() => {
if (stakedBalance === 0n) {
setDisplayed(earnedOnChain);
return;
}
const interval = setInterval(() => {
const elapsed = BigInt(Math.floor((Date.now() / 1000) - lastUpdatedAt));
const delta = (stakedBalance * rewardPerToken * elapsed) / BigInt(1e18);
setDisplayed(earnedOnChain + delta);
}, 100);
return () => clearInterval(interval);
}, [earnedOnChain, stakedBalance, rewardPerToken, lastUpdatedAt]);
return displayed;
} השוואה: שאילתת RPC ישירה מול אינטרפולציה מקומית
| פרמטר | שאילתה כל 12 שניות | אינטרפולציה מקומית כל 100 אלפיות השנייה |
|---|---|---|
| עדכוני תגמולים | קפיצות של ~0.06 טוקן/בלוק | עלייה חלקה של ~0.001 טוקן/שנייה |
| עומס RPC | ~360 בקשות/שעה | 0 בקשות נוספות |
| תפיסת המשתמש | לא יציב, אמון נמוך | צמיחה טבעית, מעורבות גבוהה |
| מכפיל חלקות | 1x | חלק פי 120 |
לדוגמה, אינטרפולציה מקומית חלקה פי 120 משאילתת RPC ישירה, ומספקת חוויית משתמש עדיפה בהרבה.
מדריך שלב אחר שלב ליישום ממשק סטייקינג
- קבלת ABI של החוזה: חלץ ABI מחוזה הסטייקינג שלך (בסגנון Synthetix).
- הקמת פרונטאנד: צור פרויקט Next.js עם wagmi ו-Tailwind CSS.
- קריאת מצב החוזה: השתמש ב-
const STAKING_ABI = parseAbi([ 'function totalSupply() view returns (uint256)', 'function balanceOf(address account) view returns (uint256)', 'function earned(address account) view returns (uint256)', 'function rewardRate() view returns (uint256)', 'function rewardPerToken() view returns (uint256)', 'function periodFinish() view returns (uint256)', 'function lockPeriod() view returns (uint256)', 'function unlockTime(address) view returns (uint256)', 'function stake(uint256 amount) nonpayable', 'function withdraw(uint256 amount) nonpayable', 'function getReward() nonpayable', 'function exit() nonpayable', ]);כדי לקבל יתרות, תגמולים ושיעורים. - יישום זרימת אישור+סטייקינג: השתמש ב-
import { formatUnits } from 'viem'; export function calculateAPR( rewardRate: bigint, totalSupply: bigint, stakingTokenPrice: number, rewardTokenPrice: number, stakingDecimals = 18, rewardDecimals = 18, ): number { if (totalSupply === 0n) return 0; const rewardPerYear = (parseFloat(formatUnits(rewardRate, rewardDecimals)) * 31_536_000) * rewardTokenPrice; const totalStakedUSD = parseFloat(formatUnits(totalSupply, stakingDecimals)) * stakingTokenPrice; return (rewardPerYear / totalStakedUSD) * 100; } export function aprToApy(apr: number, compoundsPerYear = 365): number { return (Math.pow(1 + apr / 100 / compoundsPerYear, compoundsPerYear) - 1) * 100; }כדי לשלוח עסקאות ולטפל באישורים אוטומטית. - הצגת תגמולים בזמן אמת: השתמש ב-
import { useReadContracts } from 'wagmi'; import { erc20Abi, formatUnits } from 'viem'; const STAKING = process.env.NEXT_PUBLIC_STAKING_CONTRACT as `0x${string}`; const STAKE_TOKEN = process.env.NEXT_PUBLIC_STAKE_TOKEN as `0x${string}`; const REWARD_TOKEN = process.env.NEXT_PUBLIC_REWARD_TOKEN as `0x${string}`; export function useStakingState() { const { address } = useAccount(); const { data } = useReadContracts({ contracts: [ { address: STAKING, abi: STAKING_ABI, functionName: 'totalSupply' }, { address: STAKING, abi: STAKING_ABI, functionName: 'rewardRate' }, { address: STAKING, abi: STAKING_ABI, functionName: 'periodFinish' }, { address: STAKE_TOKEN, abi: erc20Abi, functionName: 'balanceOf', args: [address!] }, { address: STAKE_TOKEN, abi: erc20Abi, functionName: 'allowance', args: [address!, STAKING] }, { address: STAKING, abi: STAKING_ABI, functionName: 'balanceOf', args: [address!] }, { address: STAKING, abi: STAKING_ABI, functionName: 'earned', args: [address!] }, ], query: { enabled: !!address, refetchInterval: 12_000, }, }); const totalSupply = data?.[0].result as bigint ?? 0n; const rewardRate = data?.[1].result as bigint ?? 0n; const periodFinish = Number(data?.[2].result as bigint ?? 0n); const walletBalance = data?.[3].result as bigint ?? 0n; const allowance = data?.[4].result as bigint ?? 0n; const stakedBalance = data?.[5].result as bigint ?? 0n; const earned = data?.[6].result as bigint ?? 0n; const isActive = periodFinish > Date.now() / 1000; return { totalSupply, rewardRate, walletBalance, allowance, stakedBalance, earned, isActive, needsApprove: (amount: bigint) => allowance < amount, }; }כדי לבצע אינטרפולציה של תגמולים בין שאילתות בלוקצ'יין. - בדיקה על רשת בדיקות: פריסה ל-Goerli או Sepolia, אימות פונקציונליות.
- פריסה לרשת הראשית: לאחר ביקורות ובדיקות, פריסה לייצור.
מה כלול בעבודה שלנו
| שלב | תוצר | משך (ימים) |
|---|---|---|
| ניתוח | מפרט חוזה סטייקינג, אישור ABI | 1–2 |
| עיצוב | תרחישי UX, עיצוב hooks ורכיבים | 1 |
| פיתוח | זרימת אישור-סטייקינג, לוח בקרה, מונה תגמולים, בדיקות | 2–3 |
| אינטגרציה | פריסה לרשת בדיקות, ניפוי באגים, אופטימיזציית גז | 1 |
| פריסה | פריסה ל-Vercel, הגדרת סביבה, תיעוד | 0.5 |
| תמיכה | שבועיים של ניטור ותיקוני באגים בחינם | — |
איך מחושב APY עם ריבית דריבית?
אם משתמשים תובעים תגמולים יומית ומפקידים אותם מחדש, התשואה האפקטיבית עולה על APR. אנו משתמשים בנוסחה הסטנדרטית: APY = (1 + APR/n)^n - 1, כאשר n הוא מספר תקופות הריבית בשנה. כברירת מחדל, n=365 (ריבית יומית). ב-APR של 18.5%, APY הופך לכ-20.3%. שני הערכים מוצגים בממשק.כמה זמן עד למסירה?
ממשק סטייקינג בסיסי עם חוזה סטנדרטי (אישור + סטייקינג + תביעה + משיכה), חישוב APR ומונה תגמולים בזמן אמת לוקח 3 עד 5 ימים. אם אתה צריך תקופות נעילה מותאמות אישית, בריכות מרובות או אינטגרציית אורקל (Chainlink, Pyth), לוח הזמנים מתארך ל-7–10 ימים. התמחור נקבע באופן אישי—צור קשר להערכה. על ידי בחירת הפתרון המוכן שלנו, אתה חוסך עד 50% בהשוואה לפיתוח מאפס (עלות טיפוסית $3,000–$5,000).
למה מונה התגמולים בזמן אמת חשוב ל-UX?
ללא אינטרפולציה, משתמשים רואים קפיצות תגמולים כל 12 שניות (מרווח בלוקים). זה פוגע באמון—התגמולים נראים לא אחידים. ה-import { useWriteContract } from 'wagmi'; import { erc20Abi, parseUnits } from 'viem'; import { waitForTransactionReceipt } from '@wagmi/core'; import { config } from '@/lib/wagmi'; export function useStakeAction() { const { writeContractAsync } = useWriteContract(); const [step, setStep] = useState<'idle' | 'approving' | 'staking' | 'done' | 'error'>('idle'); const [txHash, setTxHash] = useState<`0x${string}`>(); const { needsApprove } = useStakingState(); const stake = async (amount: string, decimals: number) => { const amountWei = parseUnits(amount, decimals); try { if (needsApprove(amountWei)) { setStep('approving'); const approveTx = await writeContractAsync({ address: STAKE_TOKEN, abi: erc20Abi, functionName: 'approve', args: [STAKING, amountWei], }); await waitForTransactionReceipt(config, { hash: approveTx }); } setStep('staking'); const stakeTx = await writeContractAsync({ address: STAKING, abi: STAKING_ABI, functionName: 'stake', args: [amountWei], }); setTxHash(stakeTx); setStep('done'); } catch (e) { setStep('error'); throw e; } }; return { stake, step, txHash }; } שלנו מבצע אקסטרפולציה של ערכים בין בקשות, ויוצר עלייה חלקה. השווה: אינטרפולציה מייצרת צמיחה של ~0.001 טוקן/שנייה לעומת קפיצות של ~0.06 טוקן/בלוק. התפיסה החזותית משתנה דרמטית—אינטרפולציה מקומית חלקה פי 120 משאילתת RPC ישירה. קבל ייעוץ ליישום זה עבור החוזה שלך.
מה אתה מקבל
אנו מספקים ממשק סטייקינג מוכן לשימוש עם קוד מקור, תיעוד פריסה ושבועיים של תמיכה. אנו מבטיחים תאימות ל-Ethereum, Polygon, BSC וכל רשת EVM. עם 5 שנות ניסיון בפיתוח חוזי DeFi ויותר מ-30 פרויקטים שנמסרו, צור קשר כדי לדון בפרויקט שלך.







