feat: migrate smart-updown contract to SPL-token (фантики)

- create_bet: escrow фантиков с bettor на vault (PDA[global,vault]) через anchor_spl::token
- close_bet: выплата фантиков из vault на bettor (CPI с signer_seeds vault)
- state/constants: comments про единицы (token units decimals=6), + VAULT_SEED
- IDL регенерирован (idl/ + relayer/idl/) с аккаунтами bettor_token/vault_token/token_program
- тесты обновлены под токен (10/10 + doctest)
This commit is contained in:
Caffeine
2026-09-09 22:00:41 +03:00
parent ff2289e68d
commit 45d64d45a7
10 changed files with 1106 additions and 120 deletions
@@ -6,6 +6,11 @@ pub const GLOBAL_SEED: &[u8] = b"global";
#[constant]
pub const BET_SEED: &[u8] = b"bet";
/// Vault PDA seed: the token vault lives at PDA([GLOBAL_SEED, VAULT_SEED]).
/// It cannot reuse GLOBAL_SEED alone — that PDA address is the `global` state account itself.
#[constant]
pub const VAULT_SEED: &[u8] = b"vault";
pub const SIDE_UP: u64 = 0;
pub const SIDE_DOWN: u64 = 1;
@@ -1,9 +1,10 @@
use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount};
use crate::{
constants::{
BPS_DIVISOR, BET_SEED, GLOBAL_SEED, SIDE_DOWN, SIDE_UP, STATUS_HOUSE_WON,
STATUS_PAYOUT_DONE, STATUS_OPEN,
STATUS_PAYOUT_DONE, STATUS_OPEN, VAULT_SEED,
},
error::SmartUpdownError,
events::BetClosed,
@@ -27,9 +28,19 @@ pub struct CloseBet<'info> {
)]
pub global: Account<'info, Global>,
/// CHECK: recipient must be the bet's bettor and is validated in the handler.
#[account(mut)]
pub bettor: SystemAccount<'info>,
pub clock: Sysvar<'info, Clock>,
/// Program-owned token vault: PDA([GLOBAL_SEED, VAULT_SEED]); payouts are paid out from here.
#[account(
mut,
seeds = [GLOBAL_SEED, VAULT_SEED],
bump
)]
pub vault_token: Account<'info, TokenAccount>,
/// Bettor's SPL-token account; the payout is sent here.
#[account(mut)]
pub bettor_token: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
}
pub fn handler(ctx: &mut Context<CloseBet>, id: u64, exit_price: u64) -> Result<()> {
@@ -69,17 +80,36 @@ let payout;
.checked_div(BPS_DIVISOR as u128)
.unwrap() as u64;
require!(
ctx.accounts.global.get_lamports() >= payout_amount,
ctx.accounts.vault_token.amount >= payout_amount,
SmartUpdownError::InsufficientVault
);
payout = payout_amount;
ctx.accounts.global.sub_lamports(payout)?;
ctx.accounts.bettor.add_lamports(payout)?;
{
// Pay the bettor: transfer the payout from the vault to the bettor's token account.
// The vault is owned by the `global` PDA, so the CPI is signed with its seeds.
let cpi_accounts = anchor_spl::token::Transfer {
from: ctx.accounts.vault_token.to_account_info(),
to: ctx.accounts.bettor_token.to_account_info(),
authority: ctx.accounts.vault_token.to_account_info(),
};
let signer_seeds: &[&[u8]] = &[
GLOBAL_SEED,
VAULT_SEED,
&[ctx.bumps.vault_token],
];
let vault_signer_seeds = [signer_seeds];
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
&vault_signer_seeds,
);
anchor_spl::token::transfer(cpi_ctx, payout)?;
}
ctx.accounts.bet.status = STATUS_PAYOUT_DONE;
} else {
payout = 0;
ctx.accounts.bet.status = STATUS_HOUSE_WON;
}
payout = 0;
ctx.accounts.bet.status = STATUS_HOUSE_WON;
}
ctx.accounts.bet.exit_price = exit_price;
@@ -1,7 +1,8 @@
use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount};
use crate::{
constants::{BET_SEED, GLOBAL_SEED, SIDE_DOWN, SIDE_UP, STATUS_OPEN},
constants::{BET_SEED, GLOBAL_SEED, SIDE_DOWN, SIDE_UP, STATUS_OPEN, VAULT_SEED},
error::SmartUpdownError,
events::BetCreated,
state::{Bet, Global},
@@ -28,6 +29,17 @@ pub struct CreateBet<'info> {
pub bet: Account<'info, Bet>,
pub clock: Sysvar<'info, Clock>,
pub system_program: Program<'info, System>,
/// Bettor's SPL-token account (our "fanti" mint); bet amount is pulled from here.
#[account(mut)]
pub bettor_token: Account<'info, TokenAccount>,
/// Program-owned token vault: PDA([GLOBAL_SEED, VAULT_SEED]); bet amounts are escrowed here.
#[account(
mut,
seeds = [GLOBAL_SEED, VAULT_SEED],
bump
)]
pub vault_token: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
}
pub fn handler(
@@ -72,15 +84,18 @@ pub fn handler(
ctx.accounts.global.counter = ctx.accounts.global.counter.checked_add(1).unwrap();
{
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.bettor.to_account_info(),
to: ctx.accounts.global.to_account_info(),
// Escrow: move the bet amount (token units) from the bettor's account to the vault.
// The SPL token program enforces that `bettor_token` is owned by `bettor` (authority).
let cpi_accounts = anchor_spl::token::Transfer {
from: ctx.accounts.bettor_token.to_account_info(),
to: ctx.accounts.vault_token.to_account_info(),
authority: ctx.accounts.bettor.to_account_info(),
};
let cpi_ctx = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
);
anchor_lang::system_program::transfer(cpi_ctx, amount)?;
anchor_spl::token::transfer(cpi_ctx, amount)?;
}
emit!(BetCreated {
@@ -1,5 +1,10 @@
use anchor_lang::prelude::*;
/// Global game state.
///
/// All money-related fields are denominated in **SPL-token base units**
/// (our "fanti" token, decimals = 6), not in lamports:
/// `min_amount`/`max_amount` bound a bet amount in token units.
#[account]
#[derive(InitSpace)]
pub struct Global {
@@ -13,6 +18,7 @@ pub struct Global {
pub relayer: Pubkey,
}
/// Open bet. `amount` is in token base units (decimals = 6), not lamports.
#[account]
#[derive(InitSpace)]
pub struct Bet {