Here's my Solana Program. It defines the logic for staking SPL tokens.
use anchor_lang::prelude::*;
use anchor_spl::{
token::{TokenAccount, Mint,Token, Transfer, transfer, Approve},
associated_token::{AssociatedToken, create, get_associated_token_address}
};
use std::mem::size_of;
// use solana_program::clock::Clock;
declare_id!("8eeWAxSis5uy5ox3ucNj2DouFsc7bfMvTnGx3wc1GaXG");
pub mod constants {
pub const VAULT_SEED: &[u8] = b"vault";
pub const STAKE_INFO_ACCOUNT: &[u8] = b"stake_info_account";
pub const TOKEN_SEED: &[u8] = b"token_seed";
}
#[program]
pub mod staking_solana {
use super::*;
pub fn initialize(_ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
pub fn stake(ctx: Context<Stake>, amt:u64) -> Result<()> {
let stake_info: &mut Account<'_, StakeInfo> = &mut ctx.accounts.stake_info_account;
if (stake_info.is_staked) {
return Err(ErrorCodes::IsStaked.into());
}
if (amt == 0) {
return Err(ErrorCodes::NoTokens.into());
}
// staking logic
let clock = &mut ctx.accounts.clock;
stake_info.stake_at_slot = clock.slot;
stake_info.is_staked = true;
// format the staking amount
// 10^decimals * amt
let stake_amt = amt.checked_mul(10u64.pow(ctx.accounts.mint.decimals as u32)).unwrap();
msg!("Transferring {} tokens from user_token_account ({}) to user_staking_account ({})",
stake_amt,
ctx.accounts.user_token_account.key(),
ctx.accounts.user_staking_ata.key()
);
// transfer stake amt to user_Stake_account from user_token_account
anchor_spl::token::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.user_token_account.to_account_info(),
to: ctx.accounts.user_staking_ata.to_account_info(),
authority: ctx.accounts.signer.to_account_info(),
}
),
stake_amt
)?;
msg!("balance of the user token account is {:?}", ctx.accounts.user_token_account.amount);
msg!("balance of the user stake account is {:?}", ctx.accounts.user_staking_ata.amount);
Ok(())
}
pub fn destake(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub signer : Signer<'info>,
#[account(
init_if_needed,
seeds = [constants::VAULT_SEED],
bump,
payer = signer,
token::mint = mint,
token::authority = token_vault_account,
)]
pub token_vault_account : Account<'info, TokenAccount>,
#[account()]
pub mint: Account<'info, Mint>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Stake<'info> {
#[account(mut)]
pub signer : Signer<'info>,
pub clock : Sysvar<'info, Clock>,
#[account(
init_if_needed,
seeds = [constants::STAKE_INFO_ACCOUNT, signer.key.as_ref()],
space = size_of::<StakeInfo>() + 8,
bump,
payer = signer,
)]
pub stake_info_account : Account<'info, StakeInfo>,
#[account(
init_if_needed,
seeds = [constants::TOKEN_SEED, signer.key.as_ref()],
bump,
payer = signer,
token::mint = mint,
token::authority = user_staking_account
)]
pub user_staking_account : Account<'info, TokenAccount>,
#[account(
init_if_needed,
payer = signer,
associated_token::mint = mint,
associated_token::authority = user_staking_account
)]
user_staking_ata : Account<'info, TokenAccount>,
#[account(
mut,
associated_token::mint = mint,
associated_token::authority = signer
)]
pub user_token_account : Account<'info, TokenAccount>,
#[account()]
pub mint: Account<'info, Mint>,
pub token_program: Program<'info, Token>,
pub associated_token_program : Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct StakeInfo {
pub total_staked: u64,
pub remaining_rewards: u64,
pub stake_at_slot: u64,
pub is_staked: bool,
}
#[error_code]
pub enum ErrorCodes{
#[msg("Tokens already staked")]
IsStaked,
#[msg("Tokens not staked")]
NotStaked,
#[msg("No Tokens To Stake")]
NoTokens
}
There are 5 different accounts involved illustrated below
The SPL tokens are supposed to be sent from User's Token Account to User STaking Info Acc.
Here's the TS test suite for this
it("stake", async () => {
// Create user account
const local_connection = new Connection("http://localhost:8899", {
commitment: "confirmed",
});
console.log("mint is:", mint);
const user_addr = Keypair.generate();
// creating ATA to the user (user is the owner of token account)
const user = await getOrCreateAssociatedTokenAccount(
local_connection,
payer.payer,
mint,
payer.publicKey // Correct owner of token account is the user
);
console.log("user token account is ", user.address);
// Mint some tokens to the user (e.g., 100 tokens)
await mintTo(
local_connection,
payer.payer,
mint,
user.address, // Correct user token account
payer.publicKey, // payer is the owner of the mint
1e11, // This mints 100 tokens (1e11 atomic units with 9 decimals)
[payer.payer]
);
// Get the user's initial token balance before staking
const initialUserBalance = await getTokenBalance(
local_connection,
mint,
payer.publicKey // Use the correct user token account
);
console.log("Initial User Token Balance:", initialUserBalance);
// Get the stake info account for the program
const [stake_info] = PublicKey.findProgramAddressSync(
[Buffer.from("stake_info_account"), payer.publicKey.toBuffer()],
program.programId
);
// Get the user_stake PDA (program-derived address)
const [stake_account] = PublicKey.findProgramAddressSync(
[Buffer.from("token_seed"), payer.publicKey.toBuffer()], // Ensure the correct user address is used for PDA derivation
program.programId
);
// Create an associated token account for the user_stake PDA (program owns the stake account)
const stake_acc_ata = await getOrCreateAssociatedTokenAccount(
local_connection,
payer.payer,
mint,
stake_account, // Stake account owned by the program's PDA
true // This ensures it creates the ATA for the program's PDA
);
console.log("stake ATA is ", stake_acc_ata.address);
console.log("user token account is ", user.address);
// Staking 1 token (1e9 in atomic units, considering 9 decimals)
const amountToStake = new anchor.BN(1); // This represents 1 token
// Execute the staking transaction
const tx = await program.methods
.stake(amountToStake) // Assuming 1 token is being staked
.signers([payer.payer])
.accountsPartial({
stakeInfoAccount: stake_info,
userStakingAccount: stake_account,
userTokenAccount: user.address, // Use the correct user token account
mint: mint,
signer: payer.publicKey, // The signer should be the user staking the tokens
})
.rpc();
console.log("Transaction signature:", tx);
// Get the user's token balance after staking
const token_amt = (await getAccount(local_connection, user.address)).amount;
console.log("Final User Token Balance:", token_amt);
// Check that the user's balance was reduced by the staked amount (1 token)
assert.equal(
BigInt(initialUserBalance) - token_amt,
BigInt(1e9), // 1 token in atomic units (with 9 decimals)
"User's token balance should decrease by 1 token"
);
// Get the staked account balance to verify the staked amount
const stakedAccountBalance = await getTokenBalance(
local_connection,
mint,
stake_acc_ata.address // Check the stake account's balance
);
console.log("Staked Account Balance:", stakedAccountBalance);
// Check that the staked account has the staked tokens (1 token)
assert.equal(
BigInt(stakedAccountBalance),
BigInt(1e9), // 1 token in atomic units (with 9 decimals)
"Staked account should have the correct token amount (1 token)"
);
});
There's NO issue in the function execution; just that after the tx when we check the balance of the user's staking info account's Token account; it returns a balance of 0.
I am a newbie in Solana dev, so forgive me for asking something that may be obvious!!