Staking Interface on Website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Development stages

Latest works

  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1171
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    831
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    879
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    453

Implementing a Staking Interface on a Website

A staking interface is a form and dashboard for depositing tokens into a contract with reward accrual. The user deposits tokens, sees accumulated rewards in real-time, can claim and withdraw. Under the hood — approve + stake, periodic reward calculation, unstaking with optional lock period.

Typical Staking Contract ABI

// Standard Synthetix-like staking
const STAKING_ABI = parseAbi([
  // View
  '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)',    // optional
  'function unlockTime(address) view returns (uint256)', // optional
  // Write
  'function stake(uint256 amount) nonpayable',
  'function withdraw(uint256 amount) nonpayable',
  'function getReward() nonpayable',
  'function exit() nonpayable', // withdraw all + getReward
]);

APR/APY Calculation

APR is calculated from rewardRate (tokens per second) and totalSupply (total staked):

import { formatUnits } from 'viem';

export function calculateAPR(
  rewardRate: bigint,        // reward tokens per second
  totalSupply: bigint,       // staked tokens
  stakingTokenPrice: number, // USD
  rewardTokenPrice: number,  // USD
  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;
}

// APY with compounding (if claiming once per day and restaking)
export function aprToApy(apr: number, compoundsPerYear = 365): number {
  return (Math.pow(1 + apr / 100 / compoundsPerYear, compoundsPerYear) - 1) * 100;
}

Staking State Hook

// hooks/useStakingState.ts
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: [
      // Global state
      { address: STAKING, abi: STAKING_ABI, functionName: 'totalSupply' },
      { address: STAKING, abi: STAKING_ABI, functionName: 'rewardRate' },
      { address: STAKING, abi: STAKING_ABI, functionName: 'periodFinish' },
      // User token balance
      { address: STAKE_TOKEN, abi: erc20Abi, functionName: 'balanceOf', args: [address!] },
      { address: STAKE_TOKEN, abi: erc20Abi, functionName: 'allowance', args: [address!, STAKING] },
      // User position
      { address: STAKING, abi: STAKING_ABI, functionName: 'balanceOf', args: [address!] },
      { address: STAKING, abi: STAKING_ABI, functionName: 'earned', args: [address!] },
    ],
    query: {
      enabled: !!address,
      refetchInterval: 12_000, // every block
    },
  });

  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,
    // Need approve?
    needsApprove: (amount: bigint) => allowance < amount,
  };
}

Approve + Stake in One Flow

// hooks/useStakeAction.ts
import { useWriteContract, useWaitForTransactionReceipt } 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 };
}

Real-Time Reward Counter

earned() updates on each call, but constant contract reads are expensive. Intermediate values are interpolated locally:

// hooks/useEarnedRealtime.ts
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));
      // Simplified extrapolation: earned + staked * rewardPerTokenPerSec * elapsed
      const delta = (stakedBalance * rewardPerToken * elapsed) / BigInt(1e18);
      setDisplayed(earnedOnChain + delta);
    }, 100);

    return () => clearInterval(interval);
  }, [earnedOnChain, stakedBalance, rewardPerToken, lastUpdatedAt]);

  return displayed;
}

The counter ticks every 100ms — visually smooth, no RPC load.

UI Component

export function StakingWidget() {
  const { totalSupply, rewardRate, walletBalance, stakedBalance, earned, isActive } = useStakingState();
  const { stake, step } = useStakeAction();
  const { withdraw } = useWithdrawAction();
  const { claimReward } = useClaimAction();
  const [stakeAmount, setStakeAmount] = useState('');

  const apr = useMemo(() => calculateAPR(rewardRate, totalSupply, stakingTokenPrice, rewardTokenPrice), [rewardRate, totalSupply]);

  return (
    <div className="space-y-6 rounded-2xl border border-white/10 bg-neutral-900 p-6">
      <div className="grid grid-cols-2 gap-4 sm:grid-cols-3">
        <Stat label="APR" value={`${apr.toFixed(1)}%`} highlight />
        <Stat label="Total Staked" value={`${formatUnits(totalSupply, 18)} TOKEN`} />
        <Stat label="Active" value={isActive ? 'Yes' : 'Ended'} />
      </div>

      <StakeForm amount={stakeAmount} onChange={setStakeAmount} max={walletBalance} onSubmit={stake} step={step} />
      <UserPosition staked={stakedBalance} earned={earned} onClaim={claimReward} onWithdraw={withdraw} />
    </div>
  );
}

Timeframe: staking interface with standard contract (approve + stake + claim + withdraw), APR calculation and real-time reward counter — 3–5 days.