DEV Community

Swatantra goswami
Swatantra goswami

Posted on

Spark Protocol Staking

import { usdtTokenABI, usdtContractAddress, usdcTokenABI, aavePoolABI, usdcContractAddress, } from "../constant/constants"
import { SPARK_PROVIDER, POOL_ADDRESSES_PROVIDER_ABI } from "../constant/spark"

const RPC_URL = 'https://mainnet.infura.io/v3/b762a9db129e4b2ba94ca57f5e046f97'

const { ethers, JsonRpcProvider, formatUnits, parseUnits } = require('ethers');

const SPARK_VAULT_BY_TOKEN = {
    "0xdAC17F958D2ee523a2206206994597C13D831ec7": "0xe2e7a17dFf93280dec073C995595155283e3C372", // USDT → spUSDT
    "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": "0x28b3a8fb53b741a8fd78c0fb9a6b2393d896a43d", // USDC → spUSDC
};
export const useSpark = () => {
    const provider = new JsonRpcProvider(RPC_URL);
    let privateKey = "8220310fb6bf04bf68324337e2fa5c3811b8d2dbbee74018ba56e075d7b75b8d"
    const signer = new ethers.Wallet(privateKey, provider);
    const usdtInstance = new ethers.Contract(usdtContractAddress, usdtTokenABI, signer);
    const usdcInstance = new ethers.Contract(usdcContractAddress, usdcTokenABI, signer);

    async function getBalance() {
        try {
            const userAddress = await signer.getAddress();
            console.log("userAddress:", userAddress)
            const balance = await usdtInstance.balanceOf(userAddress);
            const formattedBalance = formatUnits(balance, 6);

            console.log("USDT balance:", formattedBalance);

            return formattedBalance;

        } catch (error) {
            console.error("Error fetching USDT balance:", error);
            return null;
        }
    }

    async function stake({ amount, tokenAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7" }) {
        try {
            const user = await signer.getAddress();

            const vaultAddress = SPARK_VAULT_BY_TOKEN[tokenAddress];
            if (!vaultAddress) throw new Error(`No Spark vault for token: ${tokenAddress}`);

            // const tokenInstance = new ethers.Contract(tokenAddress, usdtTokenABI, signer);
            const vault = new ethers.Contract(
                vaultAddress,
                [
                    "function deposit(uint256 assets, address receiver) returns (uint256 shares)"
                ],
                signer
            );

            const decimals = tokenAddress === usdtContractAddress ? 6 : 6; // both USDT & USDC are 6
            const amountInWei = parseUnits(amount.toString(), decimals);

            // Check & reset allowance (USDT requires reset to 0 before new approval)
            const allowance = await usdtInstance.allowance(user, vaultAddress);
            if (allowance > 0n) {
                console.log("Resetting allowance to 0...");
                const resetTx = await usdtInstance.approve(vaultAddress, 0n);
                await resetTx.wait();
            }

            console.log("Approving...");
            const approveTx = await usdtInstance.approve(vaultAddress, amountInWei);
            await approveTx.wait();

            console.log("Depositing into Spark savings vault...");
            const tx = await vault.deposit(amountInWei, user);
            const receipt = await tx.wait();

            console.log("Stake successful:", receipt.transactionHash);
            return true;

        } catch (error) {
            console.error("REAL ERROR:", error);
            return false;
        }
    }

    async function unStake({ amount, tokenAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7" }) {
        try {
            const user = await signer.getAddress();

            const vaultAddress = SPARK_VAULT_BY_TOKEN[tokenAddress];
            const vault = new ethers.Contract(
                vaultAddress,
                [
                    "function redeem(uint256 shares, address receiver, address owner) returns (uint256 assets)",
                    "function balanceOf(address) view returns (uint256)"
                ],
                signer
            );

            // Redeem all shares
            const shares = await vault.balanceOf(user);

            const tx = await vault.redeem(shares, user, user);
            const receipt = await tx.wait();

            console.log("Unstake successful:", receipt.transactionHash);
            return true;

        } catch (error) {
            console.error("Withdraw failed:", error);
            return false;
        }
    }

    async function getATokenBalance(tokenAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7") {
        try {
            const user = await signer.getAddress();

            const vaultAddress = SPARK_VAULT_BY_TOKEN[tokenAddress];
            if (!vaultAddress) throw new Error(`No Spark vault for token: ${tokenAddress}`);

            const vault = new ethers.Contract(
                vaultAddress,
                [
                    "function assetsOf(address owner) view returns (uint256)",
                    "function balanceOf(address owner) view returns (uint256)"
                ],
                provider
            );

            const [assetsInWei, sharesInWei] = await Promise.all([
                vault.assetsOf(user),
                vault.balanceOf(user),
            ]);

            const assets = formatUnits(assetsInWei, 6); // both USDT & USDC are 6 decimals
            const shares = formatUnits(sharesInWei, 18); // vault shares are 18 decimals

            console.log(`Staked balance: ${assets} (${shares} sp${tokenAddress === usdtContractAddress ? "USDT" : "USDC"} shares)`);

            return assets; // underlying token amount (what to show user)

        } catch (error) {
            console.error("Error fetching spToken balance:", error);
            return null;
        }
    }

    async function getSupplyAPY(tokenAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7") {
        try {
            const vaultAddress = SPARK_VAULT_BY_TOKEN[tokenAddress];
            if (!vaultAddress) throw new Error(`No Spark vault found for token: ${tokenAddress}`);

            const vault = new ethers.Contract(
                vaultAddress,
                ["function vsr() view returns (uint256)"],
                provider
            );

            const vsr = await vault.vsr();

            const RAY = 1e27;
            const SECONDS_PER_YEAR = 31536000;

            const vsrFloat = Number(vsr) / RAY;
            const supplyAPY = (Math.pow(vsrFloat, SECONDS_PER_YEAR) - 1) * 100;

            const result = supplyAPY.toFixed(2);
            console.log(`Spark Supply APY for ${tokenAddress}:`, result + "%");

            return result;

        } catch (error) {
            console.error("APY error:", error);
            return null;
        }
    }

    async function getRewards(tokenAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7") {
        try {
            const user = await signer.getAddress();

            const vaultAddress = SPARK_VAULT_BY_TOKEN[tokenAddress];
            if (!vaultAddress) throw new Error(`No Spark vault for token: ${tokenAddress}`);

            const vault = new ethers.Contract(
                vaultAddress,
                [
                    "function assetsOf(address owner) view returns (uint256)",
                    "event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares)",
                    "event Withdraw(address indexed caller, address indexed receiver, address indexed owner, uint256 assets, uint256 shares)"
                ],
                provider
            );

            const [depositEvents, withdrawEvents, currentAssetsInWei] = await Promise.all([
                vault.queryFilter(vault.filters.Deposit(null, user)),
                vault.queryFilter(vault.filters.Withdraw(null, null, user)),
                vault.assetsOf(user),
            ]);

            const totalDeposited = depositEvents.reduce((sum, e) => sum + e.args.assets, 0n);
            const totalWithdrawn = withdrawEvents.reduce((sum, e) => sum + e.args.assets, 0n);
            const principal = totalDeposited - totalWithdrawn;

            const rewardsInWei = currentAssetsInWei - principal;

            return {
                token: tokenAddress, // USDT or USDC — yield is paid in the same underlying asset
                amount: formatUnits(rewardsInWei > 0n ? rewardsInWei : 0n, 6)
            };

        } catch (error) {
            console.error("Error fetching rewards:", error);
            return null;
        }
    }

    async function claimRewards(tokenAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7") {
        try {
            const user = await signer.getAddress();

            const vaultAddress = SPARK_VAULT_BY_TOKEN[tokenAddress];
            if (!vaultAddress) throw new Error(`No Spark vault for token: ${tokenAddress}`);

            const vault = new ethers.Contract(
                vaultAddress,
                [
                    "function assetsOf(address owner) view returns (uint256)",
                    "function convertToShares(uint256 assets) view returns (uint256)",
                    "function redeem(uint256 shares, address receiver, address owner) returns (uint256 assets)",
                    "event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares)",
                    "event Withdraw(address indexed caller, address indexed receiver, address indexed owner, uint256 assets, uint256 shares)"
                ],
                signer
            );

            const [depositEvents, withdrawEvents, currentAssetsInWei] = await Promise.all([
                vault.queryFilter(vault.filters.Deposit(null, user)),
                vault.queryFilter(vault.filters.Withdraw(null, null, user)),
                vault.assetsOf(user),
            ]);

            const totalDeposited = depositEvents.reduce((sum, e) => sum + e.args.assets, 0n);
            const totalWithdrawn = withdrawEvents.reduce((sum, e) => sum + e.args.assets, 0n);
            const principal = totalDeposited - totalWithdrawn;
            const rewardsInWei = currentAssetsInWei - principal;

            if (rewardsInWei <= 0n) {
                console.log("No rewards to claim");
                return true;
            }

            // Convert reward assets → shares first, then redeem exact shares
            // redeem() rounds DOWN (safe), withdraw() rounds UP (causes insufficient-balance)
            const rewardShares = await vault.convertToShares(rewardsInWei);

            if (rewardShares <= 0n) {
                console.log("Reward shares too small to claim");
                return true;
            }

            console.log("Claiming rewards:", formatUnits(rewardsInWei, 6));
            const tx = await vault.redeem(rewardShares, user, user);
            const receipt = await tx.wait();

            console.log("Rewards claimed:", receipt.transactionHash);
            return true;

        } catch (error) {
            console.error("Error claiming rewards:", error);
            return false;
        }
    }



    return {
        stake,
        unStake,
        getBalance,
        getATokenBalance,
        getSupplyAPY,
        getRewards,
        claimRewards
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)