feat: миграция smart-updown на anchor 0.31.1 + solana 2.x (SBPFv3)
- anchor-lang 0.31.1 вместо 2.0-rc/anchor-next (git) - Address -> Pubkey (state/events/instructions) - убран #[discrim], добавлен #[derive(InitSpace)] + space=8+INIT_SPACE - тесты переписаны на solana-program-test 2.x (10 passed) - сборка SBPFv3 через cargo build-sbf --tools-version v1.57 - deploy успешен в нашу сеть (agave 4.2.2, SBPFv3)
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
# >>> cortexkit:magic-context
|
||||||
|
magic-context/
|
||||||
|
# <<< cortexkit:magic-context
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# ТЗ: перевод смарт-контракта smart-updown на Anchor 0.31.1 + Solana 2.x (SBPFv3)
|
||||||
|
|
||||||
|
Цель: контракт собирается под SBPFv3 и успешно деплоится в наш прод-блокчейн
|
||||||
|
(агave 4.2.2 / feature-set 565236538 = mainnet). Логика игры сохраняется 100%.
|
||||||
|
|
||||||
|
Рабочая ветка: `extract-sources`. Репо: ~/WORK/games/Smart-game (push в git.binom.pw).
|
||||||
|
|
||||||
|
## Что сделать
|
||||||
|
|
||||||
|
1. **Cargo.toml** (`program/programs/smart-updown/Cargo.toml`):
|
||||||
|
- `anchor-lang = "0.31.1"` (crates.io), убрать git-зависимость otter-sec/anchor branch anchor-next.
|
||||||
|
- Убрать `anchor-v2-testing` из dev-deps (git).
|
||||||
|
- `solana-clock`/`solana-pubkey` dev-deps → версии 2.x (совместимы с solana-program ^2).
|
||||||
|
- Оставить остальные зависимости как есть, если они совместимы с 0.31.1.
|
||||||
|
|
||||||
|
2. **Типы**: заменить `Address` на `Pubkey` во всех state/events/instructions
|
||||||
|
(state.rs: admin, relayer, bettor; events.rs: bet, bettor; update.rs: new_relayer).
|
||||||
|
Тип `Pubkey` — из `solana_program::pubkey::Pubkey` (доступен через anchor prelude).
|
||||||
|
|
||||||
|
3. **Убрать `#[discrim = N]`** со всех 4 инструкций в lib.rs (initialize/create_bet/close_bet/update).
|
||||||
|
Anchor 0.31 генерирует дискриминатор автоматически. **Перегенерить IDL** (idl/smart_updown.json, .ts;
|
||||||
|
relayer/idl/*) — дискримы могут измениться, клиенты должны использовать новый IDL.
|
||||||
|
|
||||||
|
4. **Математика payout (1.3x) — НЕ менять**: `(amount as u128).checked_mul(multiplier_bps as u128) / BPS_DIVISOR`
|
||||||
|
через checked_mul/checked_div, BPS_DIVISOR=10000. Никаких других формул/округлений.
|
||||||
|
|
||||||
|
5. **Логика НЕ менять**: seeds (`GLOBAL_SEED`=b"global", `BET_SEED`+id), статусы, paused-проверки, админ/релейер-проверки, события — как есть.
|
||||||
|
|
||||||
|
6. **API-адаптация под 0.31** (если что-то из 2.0-rc синтаксиса недоступно):
|
||||||
|
- `ctx.accounts.admin.address()` → использовать `.key()` или `*ctx.accounts.admin.key`
|
||||||
|
- `cpi_handle_mut()` → `to_account_info()` (классический CpiContext::cpi_accounts)
|
||||||
|
- `.view().lamports()` → `.to_account_info().lamports()`
|
||||||
|
- `address_eq` → сравнение через `==` на Pubkey
|
||||||
|
- аккаунты: `space` НЕ задавать явно (авто-вывод 8-байт дискриминатора) — не ломать авто-аллокацию.
|
||||||
|
|
||||||
|
7. **Тесты** (`program/programs/smart-updown/tests/test_smart_updown.rs`):
|
||||||
|
перевести на Rust API Anchor 0.31 (НЕ JS). Убрать `anchor_v2_testing`, `Address`.
|
||||||
|
9 тестов долube зелёные (`cargo test`). Не использовать хардкод-дискриминаторы —
|
||||||
|
использовать генерируемые данные из `smart_updown::instruction::*`.
|
||||||
|
|
||||||
|
## Критерии приёмки
|
||||||
|
|
||||||
|
- `anchor build` (агave 4.2.2 CI, `--arch v3`) → успешно, `.so` собран.
|
||||||
|
- `cargo test` → 9 тестов зелёные.
|
||||||
|
- IDL перегенерён, дискримы согласованы.
|
||||||
|
- Деплой в нашу сеть (http://192.168.76.181:8899, агave 4.2.2) успешен.
|
||||||
|
|
||||||
|
## Запрещено
|
||||||
|
|
||||||
|
- Менять игровую логику (правила, множитель, статусы, seeds, проверки).
|
||||||
|
- Ломать явные ошибки компиляции скрытием (`#[allow]`, unwrap без причины).
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# ТЗ: дописать тесты для smart-updown под Anchor 0.31.1 / Solana 2.x
|
||||||
|
|
||||||
|
Контракт уже переведён на anchor-lang 0.31.1 + solana 2.x и `.so` собран в SBPFv3.
|
||||||
|
Всё в `program/programs/smart-updown/`, ветка `extract-sources`.
|
||||||
|
|
||||||
|
## Текущее состояние тестов
|
||||||
|
|
||||||
|
Файл `tests/test_smart_updown.rs` сейчас содержит ТОЛЬКО первый тест
|
||||||
|
(`initialize_creates_global`) полностью + каркас (setup, read_account, helpers).
|
||||||
|
Остальные 8 тестов НАДО дописать по original-логике (см. ниже). Файл уже
|
||||||
|
переведён на `solana-program-test` + `tokio::test` + async `BanksClient`.
|
||||||
|
|
||||||
|
## Задача
|
||||||
|
|
||||||
|
Дописать в `tests/test_smart_updown.rs` все 8 недостающих тестов, сохранив
|
||||||
|
РОВНО их исходную игровую логику:
|
||||||
|
|
||||||
|
1. `create_bet_escrows_sol` — create_bet (UP, side=0): эскроу на global-pda,
|
||||||
|
read_bet: bettor, side=0, amount, entry_price, bet_time=1_700_000_000,
|
||||||
|
expire_time=+EXPIRY_SECONDS, status=0; counter становится 1.
|
||||||
|
2. `close_bet_user_wins_up` — close UP после expiry, exit>entry: bettor получает
|
||||||
|
payout=amount*MULTIPLIER_BPS/10000 (1.3x), статус=1, exit_price установлен.
|
||||||
|
Перед этим айрдроп на global-pda ≥10*amount (для выплаты из vault).
|
||||||
|
3. `close_bet_house_wins_down` — side=DOWN, exit>entry (проиграл): баланс не
|
||||||
|
меняется, статус=2.
|
||||||
|
4. `close_bet_equal_price_stays_open` — exit==entry: статус остаётся 0, баланс не меняется.
|
||||||
|
5. `close_bet_already_finalized` — повторный close после успешного: вторая
|
||||||
|
транзакция падает (err), статус остаётся 1.
|
||||||
|
6. `close_bet_before_expiry_rejected` — close до expiry: падает, статус 0.
|
||||||
|
7. `close_bet_wrong_relayer_rejected` — close не-релейером: падает, статус 0.
|
||||||
|
8. `close_bet_replay_rejected` — close после уже закрытого: падает.
|
||||||
|
|
||||||
|
Плюс тест `create_bet_rejects_out_of_range` (amount=1 < MIN_AMOUNT) — падает.
|
||||||
|
|
||||||
|
## Критичные API-условия (solana-program-test 2.x)
|
||||||
|
|
||||||
|
- `setup()` возвращает `(ProgramTestContext, Keypair)`; первый тест уже это делает.
|
||||||
|
- Airdrop любому аккаунту: `system_instruction::transfer(&ctx.payer.pubkey(), to, lamports)` в транзакции от payer.
|
||||||
|
- Системные часы: получить `Clock` через `ctx.banks_client.get_sysvar().await`, изменить `unix_timestamp`, затем `ctx.set_sysvar(&clk)`.
|
||||||
|
- Читать аккаунт: `ctx.banks_client.get_account(pubkey).await.unwrap().unwrap()` (Account), данные через `bytemuck::from_bytes::<Global/Bet>(&data[8..])`.
|
||||||
|
- Проверка баланса: `ctx.banks_client.get_balance(pubkey).await.unwrap()`.
|
||||||
|
- Все тесты `#[tokio::test] async fn`, всегда первая строка `let (mut ctx, admin) = setup().await;`.
|
||||||
|
|
||||||
|
## Как исполнять
|
||||||
|
|
||||||
|
Дописать тесты, затем ЗАПУСТИТЬ:
|
||||||
|
```
|
||||||
|
cd program && cargo test -p smart-updown 2>&1 | tail -40
|
||||||
|
```
|
||||||
|
Добиться, чтобы ВСЕ тесты прошли зелёными (а те, что ожидают ошибку, получали её).
|
||||||
|
Устранить ошибки компиляции (импорты, типы). НЕ менять игровую логику в src.
|
||||||
|
|
||||||
|
## Критерии
|
||||||
|
|
||||||
|
- `cargo test -p smart-updown` → все 9+ тестов green (ожидаемые ошибки — как задумано).
|
||||||
|
- Отчёт: какие тесты добавил, что чинит, какие остались красными (если есть).
|
||||||
Generated
+5362
-1175
File diff suppressed because it is too large
Load Diff
@@ -15,19 +15,20 @@ cpi = ["no-entrypoint"]
|
|||||||
no-entrypoint = []
|
no-entrypoint = []
|
||||||
no-log-ix-name = []
|
no-log-ix-name = []
|
||||||
idl-build = []
|
idl-build = []
|
||||||
profile = ["anchor-v2-testing/profile"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Once anchor-lang is published to crates.io, swap to: anchor-lang = "2.0.0-rc.1"
|
anchor-lang = "0.31.1"
|
||||||
anchor-lang = { git = "https://github.com/otter-sec/anchor.git", branch = "anchor-next" }
|
|
||||||
solana-program-log = { version = "1.1", features = ["macro"] }
|
|
||||||
wincode = { version = "0.5", features = ["derive"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anchor-v2-testing = { git = "https://github.com/otter-sec/anchor.git", branch = "anchor-next" }
|
anchor-lang = "0.31.1"
|
||||||
solana-clock = "3.0.1"
|
solana-program-test = "2.1"
|
||||||
solana-pubkey = "3.0.0"
|
solana-client = "2.1"
|
||||||
|
solana-pubkey = "2.1"
|
||||||
|
solana-signer = "2.1"
|
||||||
|
solana-keypair = "2.1"
|
||||||
|
solana-clock = "2.1"
|
||||||
|
solana-transaction = "2.1"
|
||||||
|
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
|
||||||
[lints.rust]
|
[lints.rust]
|
||||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }
|
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"))'] }
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use anchor_lang::prelude::*;
|
|||||||
|
|
||||||
#[event]
|
#[event]
|
||||||
pub struct BetCreated {
|
pub struct BetCreated {
|
||||||
pub bet: Address,
|
pub bet: Pubkey,
|
||||||
pub bettor: Address,
|
pub bettor: Pubkey,
|
||||||
pub side: u64,
|
pub side: u64,
|
||||||
pub amount: u64,
|
pub amount: u64,
|
||||||
pub bet_time: i64,
|
pub bet_time: i64,
|
||||||
@@ -13,7 +13,7 @@ pub struct BetCreated {
|
|||||||
|
|
||||||
#[event]
|
#[event]
|
||||||
pub struct BetClosed {
|
pub struct BetClosed {
|
||||||
pub bet: Address,
|
pub bet: Pubkey,
|
||||||
pub exit_price: u64,
|
pub exit_price: u64,
|
||||||
pub user_won: bool,
|
pub user_won: bool,
|
||||||
pub payout: u64,
|
pub payout: u64,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use anchor_lang::{prelude::*, Lamports};
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
constants::{
|
constants::{
|
||||||
@@ -12,33 +12,33 @@ use crate::{
|
|||||||
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
#[instruction(id: u64, exit_price: u64)]
|
#[instruction(id: u64, exit_price: u64)]
|
||||||
pub struct CloseBet {
|
pub struct CloseBet<'info> {
|
||||||
pub relayer: Signer,
|
pub relayer: Signer<'info>,
|
||||||
#[account(
|
#[account(
|
||||||
mut,
|
mut,
|
||||||
seeds = [BET_SEED, id.to_le_bytes()],
|
seeds = [BET_SEED, &id.to_le_bytes()],
|
||||||
bump
|
bump
|
||||||
)]
|
)]
|
||||||
pub bet: Account<Bet>,
|
pub bet: Account<'info, Bet>,
|
||||||
#[account(
|
#[account(
|
||||||
mut,
|
mut,
|
||||||
seeds = [GLOBAL_SEED],
|
seeds = [GLOBAL_SEED],
|
||||||
bump
|
bump
|
||||||
)]
|
)]
|
||||||
pub global: Account<Global>,
|
pub global: Account<'info, Global>,
|
||||||
/// CHECK: recipient must be the bet's bettor and is validated in the handler.
|
/// CHECK: recipient must be the bet's bettor and is validated in the handler.
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub bettor: SystemAccount,
|
pub bettor: SystemAccount<'info>,
|
||||||
pub clock: Sysvar<Clock>,
|
pub clock: Sysvar<'info, Clock>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handler(ctx: &mut Context<CloseBet>, id: u64, exit_price: u64) -> Result<()> {
|
pub fn handler(ctx: &mut Context<CloseBet>, id: u64, exit_price: u64) -> Result<()> {
|
||||||
require!(
|
require!(
|
||||||
anchor_lang::address_eq(ctx.accounts.relayer.address(), &ctx.accounts.global.relayer),
|
ctx.accounts.relayer.key() == ctx.accounts.global.relayer,
|
||||||
SmartUpdownError::NotRelayer
|
SmartUpdownError::NotRelayer
|
||||||
);
|
);
|
||||||
require!(
|
require!(
|
||||||
anchor_lang::address_eq(ctx.accounts.bettor.address(), &ctx.accounts.bet.bettor),
|
ctx.accounts.bettor.key() == ctx.accounts.bet.bettor,
|
||||||
SmartUpdownError::NotRelayer
|
SmartUpdownError::NotRelayer
|
||||||
);
|
);
|
||||||
require!(
|
require!(
|
||||||
@@ -59,24 +59,24 @@ pub fn handler(ctx: &mut Context<CloseBet>, id: u64, exit_price: u64) -> Result<
|
|||||||
let user_won = (side == SIDE_UP && exit_price > entry)
|
let user_won = (side == SIDE_UP && exit_price > entry)
|
||||||
|| (side == SIDE_DOWN && exit_price < entry);
|
|| (side == SIDE_DOWN && exit_price < entry);
|
||||||
|
|
||||||
let payout;
|
let payout;
|
||||||
if user_won {
|
if user_won {
|
||||||
let multiplier = ctx.accounts.global.multiplier_bps;
|
let multiplier = ctx.accounts.global.multiplier_bps;
|
||||||
let payout_amount =
|
let payout_amount =
|
||||||
(amount as u128)
|
(amount as u128)
|
||||||
.checked_mul(multiplier as u128)
|
.checked_mul(multiplier as u128)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.checked_div(BPS_DIVISOR as u128)
|
.checked_div(BPS_DIVISOR as u128)
|
||||||
.unwrap() as u64;
|
.unwrap() as u64;
|
||||||
require!(
|
require!(
|
||||||
ctx.accounts.global.view().lamports() >= payout_amount,
|
ctx.accounts.global.get_lamports() >= payout_amount,
|
||||||
SmartUpdownError::InsufficientVault
|
SmartUpdownError::InsufficientVault
|
||||||
);
|
);
|
||||||
payout = payout_amount;
|
payout = payout_amount;
|
||||||
ctx.accounts.global.sub_lamports(payout)?;
|
ctx.accounts.global.sub_lamports(payout)?;
|
||||||
ctx.accounts.bettor.add_lamports(payout)?;
|
ctx.accounts.bettor.add_lamports(payout)?;
|
||||||
ctx.accounts.bet.status = STATUS_PAYOUT_DONE;
|
ctx.accounts.bet.status = STATUS_PAYOUT_DONE;
|
||||||
} else {
|
} else {
|
||||||
payout = 0;
|
payout = 0;
|
||||||
ctx.accounts.bet.status = STATUS_HOUSE_WON;
|
ctx.accounts.bet.status = STATUS_HOUSE_WON;
|
||||||
}
|
}
|
||||||
@@ -84,7 +84,7 @@ pub fn handler(ctx: &mut Context<CloseBet>, id: u64, exit_price: u64) -> Result<
|
|||||||
ctx.accounts.bet.exit_price = exit_price;
|
ctx.accounts.bet.exit_price = exit_price;
|
||||||
|
|
||||||
emit!(BetClosed {
|
emit!(BetClosed {
|
||||||
bet: *ctx.accounts.bet.address(),
|
bet: ctx.accounts.bet.key(),
|
||||||
exit_price,
|
exit_price,
|
||||||
user_won,
|
user_won,
|
||||||
payout,
|
payout,
|
||||||
|
|||||||
@@ -9,24 +9,25 @@ use crate::{
|
|||||||
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
#[instruction(id: u64, side: u64, amount: u64, entry_price: u64)]
|
#[instruction(id: u64, side: u64, amount: u64, entry_price: u64)]
|
||||||
pub struct CreateBet {
|
pub struct CreateBet<'info> {
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub bettor: Signer,
|
pub bettor: Signer<'info>,
|
||||||
#[account(
|
#[account(
|
||||||
mut,
|
mut,
|
||||||
seeds = [GLOBAL_SEED],
|
seeds = [GLOBAL_SEED],
|
||||||
bump
|
bump
|
||||||
)]
|
)]
|
||||||
pub global: Account<Global>,
|
pub global: Account<'info, Global>,
|
||||||
#[account(
|
#[account(
|
||||||
init,
|
init,
|
||||||
payer = bettor,
|
payer = bettor,
|
||||||
seeds = [BET_SEED, id.to_le_bytes()],
|
space = 8 + Bet::INIT_SPACE,
|
||||||
|
seeds = [BET_SEED, &id.to_le_bytes()],
|
||||||
bump
|
bump
|
||||||
)]
|
)]
|
||||||
pub bet: Account<Bet>,
|
pub bet: Account<'info, Bet>,
|
||||||
pub clock: Sysvar<Clock>,
|
pub clock: Sysvar<'info, Clock>,
|
||||||
pub system_program: Program<System>,
|
pub system_program: Program<'info, System>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handler(
|
pub fn handler(
|
||||||
@@ -52,8 +53,8 @@ pub fn handler(
|
|||||||
);
|
);
|
||||||
|
|
||||||
let now = ctx.accounts.clock.unix_timestamp;
|
let now = ctx.accounts.clock.unix_timestamp;
|
||||||
let bettor_address = *ctx.accounts.bettor.address();
|
let bettor_address = ctx.accounts.bettor.key();
|
||||||
let bet_address = *ctx.accounts.bet.address();
|
let bet_address = ctx.accounts.bet.key();
|
||||||
let expiry_seconds = ctx.accounts.global.expiry_seconds;
|
let expiry_seconds = ctx.accounts.global.expiry_seconds;
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -71,12 +72,15 @@ pub fn handler(
|
|||||||
ctx.accounts.global.counter = ctx.accounts.global.counter.checked_add(1).unwrap();
|
ctx.accounts.global.counter = ctx.accounts.global.counter.checked_add(1).unwrap();
|
||||||
|
|
||||||
{
|
{
|
||||||
let cpi_accounts = system_program::Transfer {
|
let cpi_accounts = anchor_lang::system_program::Transfer {
|
||||||
from: ctx.accounts.bettor.cpi_handle_mut(),
|
from: ctx.accounts.bettor.to_account_info(),
|
||||||
to: ctx.accounts.global.cpi_handle_mut(),
|
to: ctx.accounts.global.to_account_info(),
|
||||||
};
|
};
|
||||||
let cpi_ctx = CpiContext::new(ctx.accounts.system_program.address(), cpi_accounts);
|
let cpi_ctx = CpiContext::new(
|
||||||
system_program::transfer(cpi_ctx, amount)?;
|
ctx.accounts.system_program.to_account_info(),
|
||||||
|
cpi_accounts,
|
||||||
|
);
|
||||||
|
anchor_lang::system_program::transfer(cpi_ctx, amount)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit!(BetCreated {
|
emit!(BetCreated {
|
||||||
|
|||||||
@@ -3,17 +3,18 @@ use anchor_lang::prelude::*;
|
|||||||
use crate::{constants::GLOBAL_SEED, state::Global};
|
use crate::{constants::GLOBAL_SEED, state::Global};
|
||||||
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
pub struct Initialize {
|
pub struct Initialize<'info> {
|
||||||
#[account(mut)]
|
#[account(mut)]
|
||||||
pub admin: Signer,
|
pub admin: Signer<'info>,
|
||||||
#[account(
|
#[account(
|
||||||
init,
|
init,
|
||||||
payer = admin,
|
payer = admin,
|
||||||
|
space = 8 + Global::INIT_SPACE,
|
||||||
seeds = [GLOBAL_SEED],
|
seeds = [GLOBAL_SEED],
|
||||||
bump
|
bump
|
||||||
)]
|
)]
|
||||||
pub global: Account<Global>,
|
pub global: Account<'info, Global>,
|
||||||
pub system_program: Program<System>,
|
pub system_program: Program<'info, System>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handler(
|
pub fn handler(
|
||||||
@@ -24,8 +25,8 @@ pub fn handler(
|
|||||||
expiry_seconds: i64,
|
expiry_seconds: i64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let global = &mut ctx.accounts.global;
|
let global = &mut ctx.accounts.global;
|
||||||
global.admin = *ctx.accounts.admin.address();
|
global.admin = ctx.accounts.admin.key();
|
||||||
global.relayer = *ctx.accounts.admin.address();
|
global.relayer = ctx.accounts.admin.key();
|
||||||
global.counter = 0;
|
global.counter = 0;
|
||||||
global.min_amount = min_amount;
|
global.min_amount = min_amount;
|
||||||
global.max_amount = max_amount;
|
global.max_amount = max_amount;
|
||||||
|
|||||||
@@ -7,19 +7,19 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
pub struct Update {
|
pub struct Update<'info> {
|
||||||
pub admin: Signer,
|
pub admin: Signer<'info>,
|
||||||
#[account(
|
#[account(
|
||||||
mut,
|
mut,
|
||||||
seeds = [GLOBAL_SEED],
|
seeds = [GLOBAL_SEED],
|
||||||
bump
|
bump
|
||||||
)]
|
)]
|
||||||
pub global: Account<Global>,
|
pub global: Account<'info, Global>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handler(
|
pub fn handler(
|
||||||
ctx: &mut Context<Update>,
|
ctx: &mut Context<Update>,
|
||||||
new_relayer: Address,
|
new_relayer: Pubkey,
|
||||||
min_amount: u64,
|
min_amount: u64,
|
||||||
max_amount: u64,
|
max_amount: u64,
|
||||||
multiplier_bps: u64,
|
multiplier_bps: u64,
|
||||||
@@ -27,7 +27,7 @@ pub fn handler(
|
|||||||
expiry_seconds: i64,
|
expiry_seconds: i64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
require!(
|
require!(
|
||||||
anchor_lang::address_eq(ctx.accounts.admin.address(), &ctx.accounts.global.admin),
|
ctx.accounts.admin.key() == ctx.accounts.global.admin,
|
||||||
SmartUpdownError::NotAdmin
|
SmartUpdownError::NotAdmin
|
||||||
);
|
);
|
||||||
let global = &mut ctx.accounts.global;
|
let global = &mut ctx.accounts.global;
|
||||||
|
|||||||
@@ -14,41 +14,37 @@ declare_id!("9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf");
|
|||||||
pub mod smart_updown {
|
pub mod smart_updown {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[discrim = 0]
|
|
||||||
pub fn initialize(
|
pub fn initialize(
|
||||||
ctx: &mut Context<Initialize>,
|
mut ctx: Context<Initialize>,
|
||||||
min_amount: u64,
|
min_amount: u64,
|
||||||
max_amount: u64,
|
max_amount: u64,
|
||||||
multiplier_bps: u64,
|
multiplier_bps: u64,
|
||||||
expiry_seconds: i64,
|
expiry_seconds: i64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
initialize::handler(ctx, min_amount, max_amount, multiplier_bps, expiry_seconds)
|
initialize::handler(&mut ctx, min_amount, max_amount, multiplier_bps, expiry_seconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[discrim = 1]
|
|
||||||
pub fn create_bet(
|
pub fn create_bet(
|
||||||
ctx: &mut Context<CreateBet>,
|
mut ctx: Context<CreateBet>,
|
||||||
id: u64,
|
id: u64,
|
||||||
side: u64,
|
side: u64,
|
||||||
amount: u64,
|
amount: u64,
|
||||||
entry_price: u64,
|
entry_price: u64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
create_bet::handler(ctx, id, side, amount, entry_price)
|
create_bet::handler(&mut ctx, id, side, amount, entry_price)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[discrim = 2]
|
|
||||||
pub fn close_bet(
|
pub fn close_bet(
|
||||||
ctx: &mut Context<CloseBet>,
|
mut ctx: Context<CloseBet>,
|
||||||
id: u64,
|
id: u64,
|
||||||
exit_price: u64,
|
exit_price: u64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
close_bet::handler(ctx, id, exit_price)
|
close_bet::handler(&mut ctx, id, exit_price)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[discrim = 3]
|
|
||||||
pub fn update(
|
pub fn update(
|
||||||
ctx: &mut Context<Update>,
|
mut ctx: Context<Update>,
|
||||||
new_relayer: Address,
|
new_relayer: Pubkey,
|
||||||
min_amount: u64,
|
min_amount: u64,
|
||||||
max_amount: u64,
|
max_amount: u64,
|
||||||
multiplier_bps: u64,
|
multiplier_bps: u64,
|
||||||
@@ -56,7 +52,7 @@ pub mod smart_updown {
|
|||||||
expiry_seconds: i64,
|
expiry_seconds: i64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
update::handler(
|
update::handler(
|
||||||
ctx,
|
&mut ctx,
|
||||||
new_relayer,
|
new_relayer,
|
||||||
min_amount,
|
min_amount,
|
||||||
max_amount,
|
max_amount,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
#[account]
|
#[account]
|
||||||
|
#[derive(InitSpace)]
|
||||||
pub struct Global {
|
pub struct Global {
|
||||||
pub counter: u64,
|
pub counter: u64,
|
||||||
pub min_amount: u64,
|
pub min_amount: u64,
|
||||||
@@ -8,11 +9,12 @@ pub struct Global {
|
|||||||
pub multiplier_bps: u64,
|
pub multiplier_bps: u64,
|
||||||
pub expiry_seconds: i64,
|
pub expiry_seconds: i64,
|
||||||
pub paused: u64,
|
pub paused: u64,
|
||||||
pub admin: Address,
|
pub admin: Pubkey,
|
||||||
pub relayer: Address,
|
pub relayer: Pubkey,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[account]
|
#[account]
|
||||||
|
#[derive(InitSpace)]
|
||||||
pub struct Bet {
|
pub struct Bet {
|
||||||
pub amount: u64,
|
pub amount: u64,
|
||||||
pub bet_time: i64,
|
pub bet_time: i64,
|
||||||
@@ -21,5 +23,5 @@ pub struct Bet {
|
|||||||
pub exit_price: u64,
|
pub exit_price: u64,
|
||||||
pub side: u64,
|
pub side: u64,
|
||||||
pub status: u64,
|
pub status: u64,
|
||||||
pub bettor: Address,
|
pub bettor: Pubkey,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
use {
|
use {
|
||||||
anchor_lang::{
|
anchor_lang::{
|
||||||
bytemuck, cpi::find_program_address, solana_program::instruction::Instruction, Id,
|
solana_program::{
|
||||||
InstructionData, ToAccountMetas,
|
instruction::Instruction,
|
||||||
|
pubkey::Pubkey,
|
||||||
|
system_instruction,
|
||||||
|
},
|
||||||
|
AccountDeserialize, InstructionData, ToAccountMetas,
|
||||||
},
|
},
|
||||||
anchor_v2_testing::{Keypair, LiteSVM, Message, Signer, VersionedMessage, VersionedTransaction},
|
|
||||||
smart_updown::state::{Bet, Global},
|
smart_updown::state::{Bet, Global},
|
||||||
|
solana_clock::Clock,
|
||||||
|
solana_keypair::Keypair,
|
||||||
|
solana_program_test::{BanksClient, ProgramTest, ProgramTestContext},
|
||||||
|
solana_signer::Signer,
|
||||||
|
solana_transaction::Transaction,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CLOCK_ADDR: &str = "SysvarC1ock11111111111111111111111111111111";
|
const CLOCK_ADDR: &str = "SysvarC1ock11111111111111111111111111111111";
|
||||||
@@ -12,40 +20,62 @@ const SYSTEM_ADDR: &str = "11111111111111111111111111111111";
|
|||||||
const MIN_AMOUNT: u64 = 1_000_000_000; // 1 SOL
|
const MIN_AMOUNT: u64 = 1_000_000_000; // 1 SOL
|
||||||
const MULTIPLIER_BPS: u64 = 13_000; // 1.3x
|
const MULTIPLIER_BPS: u64 = 13_000; // 1.3x
|
||||||
const EXPIRY_SECONDS: i64 = 15;
|
const EXPIRY_SECONDS: i64 = 15;
|
||||||
|
const START_TIME: i64 = 1_700_000_000;
|
||||||
|
|
||||||
type Addr = anchor_lang::prelude::Address;
|
fn pid() -> Pubkey {
|
||||||
|
|
||||||
fn pid() -> Addr {
|
|
||||||
smart_updown::id()
|
smart_updown::id()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn global_pda() -> Addr {
|
fn global_pda() -> Pubkey {
|
||||||
find_program_address(&[b"global"], &pid()).0
|
Pubkey::find_program_address(&[b"global"], &pid()).0
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bet_pda(id: u64) -> Addr {
|
fn bet_pda(id: u64) -> Pubkey {
|
||||||
find_program_address(&[b"bet", &id.to_le_bytes()], &pid()).0
|
Pubkey::find_program_address(&[b"bet", &id.to_le_bytes()], &pid()).0
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup() -> (LiteSVM, Keypair) {
|
async fn setup() -> (ProgramTestContext, Keypair) {
|
||||||
let mut svm = LiteSVM::new();
|
let mut pt = ProgramTest::default();
|
||||||
let bytes = include_bytes!("../../../target/deploy/smart_updown.so");
|
pt.add_program("smart_updown", pid(), None);
|
||||||
svm.add_program(pid(), bytes).expect("deploy program");
|
// Fund the deploy keypair separately in the test; the validator in CI funds it.
|
||||||
let payer = Keypair::new();
|
let context = pt.start_with_context().await;
|
||||||
svm.airdrop(&payer.pubkey(), 100_000_000_000).unwrap();
|
let admin = Keypair::new();
|
||||||
(svm, payer)
|
context.banks_client.process_transaction(Transaction::new_signed_with_payer(
|
||||||
|
&[system_instruction::transfer(
|
||||||
|
&context.payer.pubkey(),
|
||||||
|
&admin.pubkey(),
|
||||||
|
100_000_000_000,
|
||||||
|
)],
|
||||||
|
Some(&context.payer.pubkey()),
|
||||||
|
&[&context.payer],
|
||||||
|
context.last_blockhash,
|
||||||
|
)).await.unwrap();
|
||||||
|
(context, admin)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send(svm: &mut LiteSVM, signers: &[&Keypair], instruction: Instruction) -> Result<(), String> {
|
async fn set_clock(ctx: &mut ProgramTestContext, unix: i64) {
|
||||||
let blockhash = svm.latest_blockhash();
|
let mut clk: Clock = ctx.banks_client.get_sysvar().await.unwrap();
|
||||||
let msg = Message::new_with_blockhash(&[instruction], Some(&signers[0].pubkey()), &blockhash);
|
clk.unix_timestamp = unix;
|
||||||
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), signers).unwrap();
|
ctx.set_sysvar(&clk);
|
||||||
let res = svm.send_transaction(tx);
|
|
||||||
res.map(|_| ()).map_err(|e| format!("{:?}", e))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize(svm: &mut LiteSVM, admin: &Keypair) {
|
async fn read_account_data(
|
||||||
let ix = Instruction::new_with_bytes(
|
bank: &BanksClient,
|
||||||
|
addr: &Pubkey,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
bank.get_account(*addr).await.unwrap().expect("account exists").data
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_global_typed(data: &[u8]) -> Global {
|
||||||
|
Global::try_deserialize(&mut &data[..]).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_bet_typed(data: &[u8]) -> Bet {
|
||||||
|
Bet::try_deserialize(&mut &data[..]).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_initialize_ix(admin: Pubkey) -> Instruction {
|
||||||
|
Instruction::new_with_bytes(
|
||||||
pid(),
|
pid(),
|
||||||
&smart_updown::instruction::Initialize {
|
&smart_updown::instruction::Initialize {
|
||||||
min_amount: MIN_AMOUNT,
|
min_amount: MIN_AMOUNT,
|
||||||
@@ -55,37 +85,15 @@ fn initialize(svm: &mut LiteSVM, admin: &Keypair) {
|
|||||||
}
|
}
|
||||||
.data(),
|
.data(),
|
||||||
smart_updown::accounts::Initialize {
|
smart_updown::accounts::Initialize {
|
||||||
admin: admin.pubkey(),
|
admin,
|
||||||
global: global_pda(),
|
global: global_pda(),
|
||||||
system_program: SYSTEM_ADDR.parse().unwrap(),
|
system_program: SYSTEM_ADDR.parse().unwrap(),
|
||||||
}
|
}
|
||||||
.to_account_metas(None),
|
.to_account_metas(None),
|
||||||
);
|
)
|
||||||
send(svm, &[admin], ix).expect("initialize");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_clock(svm: &mut LiteSVM, unix: i64) {
|
fn make_create_ix(id: u64, side: u64, amount: u64, entry_price: u64, bettor: Pubkey) -> Instruction {
|
||||||
use solana_clock::Clock;
|
|
||||||
let mut clk = svm.get_sysvar::<Clock>();
|
|
||||||
clk.unix_timestamp = unix;
|
|
||||||
svm.set_sysvar(&clk);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fund_vault(svm: &mut LiteSVM, lamports: u64) {
|
|
||||||
svm.airdrop(&global_pda(), lamports).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_global(svm: &LiteSVM) -> Global {
|
|
||||||
let acc = svm.get_account(&global_pda()).expect("global account");
|
|
||||||
*bytemuck::from_bytes::<Global>(&acc.data[8..])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_bet(svm: &LiteSVM, id: u64) -> Bet {
|
|
||||||
let acc = svm.get_account(&bet_pda(id)).expect("bet account");
|
|
||||||
*bytemuck::from_bytes::<Bet>(&acc.data[8..])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_create_ix(id: u64, side: u64, amount: u64, entry_price: u64, bettor: Addr) -> Instruction {
|
|
||||||
Instruction::new_with_bytes(
|
Instruction::new_with_bytes(
|
||||||
pid(),
|
pid(),
|
||||||
&smart_updown::instruction::CreateBet {
|
&smart_updown::instruction::CreateBet {
|
||||||
@@ -106,7 +114,7 @@ fn make_create_ix(id: u64, side: u64, amount: u64, entry_price: u64, bettor: Add
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_close_ix(id: u64, exit_price: u64, relayer: Addr, bettor: Addr) -> Instruction {
|
fn make_close_ix(id: u64, exit_price: u64, relayer: Pubkey, bettor: Pubkey) -> Instruction {
|
||||||
Instruction::new_with_bytes(
|
Instruction::new_with_bytes(
|
||||||
pid(),
|
pid(),
|
||||||
&smart_updown::instruction::CloseBet { id, exit_price }.data(),
|
&smart_updown::instruction::CloseBet { id, exit_price }.data(),
|
||||||
@@ -121,22 +129,68 @@ fn make_close_ix(id: u64, exit_price: u64, relayer: Addr, bettor: Addr) -> Instr
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_up_bet(svm: &mut LiteSVM, admin: &Keypair, side: u64) -> (Keypair, u64, u64) {
|
async fn run_ix(
|
||||||
|
ctx: &mut ProgramTestContext,
|
||||||
|
signer: &Keypair,
|
||||||
|
ix: Instruction,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let tx = Transaction::new_signed_with_payer(
|
||||||
|
&[ix],
|
||||||
|
Some(&signer.pubkey()),
|
||||||
|
&[signer],
|
||||||
|
ctx.last_blockhash,
|
||||||
|
);
|
||||||
|
ctx.banks_client
|
||||||
|
.process_transaction(tx)
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|e| format!("{:?}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn initialize(ctx: &mut ProgramTestContext, admin: &Keypair) {
|
||||||
|
run_ix(ctx, admin, make_initialize_ix(admin.pubkey()))
|
||||||
|
.await
|
||||||
|
.expect("initialize");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn airdrop_to(ctx: &mut ProgramTestContext, to: &Pubkey, lamports: u64) {
|
||||||
|
let tx = Transaction::new_signed_with_payer(
|
||||||
|
&[system_instruction::transfer(
|
||||||
|
&ctx.payer.pubkey(),
|
||||||
|
to,
|
||||||
|
lamports,
|
||||||
|
)],
|
||||||
|
Some(&ctx.payer.pubkey()),
|
||||||
|
&[&ctx.payer],
|
||||||
|
ctx.last_blockhash,
|
||||||
|
);
|
||||||
|
ctx.banks_client.process_transaction(tx).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fund_vault(ctx: &mut ProgramTestContext, lamports: u64) {
|
||||||
|
airdrop_to(ctx, &global_pda(), lamports).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_up_bet(
|
||||||
|
ctx: &mut ProgramTestContext,
|
||||||
|
side: u64,
|
||||||
|
) -> (Keypair, u64, u64) {
|
||||||
let bettor = Keypair::new();
|
let bettor = Keypair::new();
|
||||||
svm.airdrop(&bettor.pubkey(), 100_000_000_000).unwrap();
|
airdrop_to(ctx, &bettor.pubkey(), 100_000_000_000).await;
|
||||||
set_clock(svm, 1_700_000_000);
|
set_clock(ctx, START_TIME).await;
|
||||||
let amount = MIN_AMOUNT;
|
let amount = MIN_AMOUNT;
|
||||||
let entry_price = 100_000_000u64;
|
let entry_price = 100_000_000u64;
|
||||||
let create_ix = make_create_ix(0, side, amount, entry_price, bettor.pubkey());
|
let create_ix = make_create_ix(0, side, amount, entry_price, bettor.pubkey());
|
||||||
send(svm, &[&bettor], create_ix).expect("create_bet");
|
run_ix(ctx, &bettor, create_ix).await.expect("create_bet");
|
||||||
(bettor, amount, entry_price)
|
(bettor, amount, entry_price)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn initialize_creates_global() {
|
async fn initialize_creates_global() {
|
||||||
let (mut svm, admin) = setup();
|
let (mut ctx, admin) = setup().await;
|
||||||
initialize(&mut svm, &admin);
|
initialize(&mut ctx, &admin).await;
|
||||||
let g = read_global(&svm);
|
let acc = read_account_data(&ctx.banks_client, &global_pda()).await;
|
||||||
|
let g = read_global_typed(&acc);
|
||||||
assert_eq!(g.admin, admin.pubkey());
|
assert_eq!(g.admin, admin.pubkey());
|
||||||
assert_eq!(g.relayer, admin.pubkey());
|
assert_eq!(g.relayer, admin.pubkey());
|
||||||
assert_eq!(g.counter, 0);
|
assert_eq!(g.counter, 0);
|
||||||
@@ -145,139 +199,173 @@ fn initialize_creates_global() {
|
|||||||
assert_eq!(g.paused, 0);
|
assert_eq!(g.paused, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn create_bet_escrows_sol() {
|
async fn create_bet_escrows_sol() {
|
||||||
let (mut svm, admin) = setup();
|
let (mut ctx, admin) = setup().await;
|
||||||
initialize(&mut svm, &admin);
|
initialize(&mut ctx, &admin).await;
|
||||||
let (bettor, amount, entry_price) = create_up_bet(&mut svm, &admin, 0);
|
let (bettor, amount, entry_price) = create_up_bet(&mut ctx, 0).await;
|
||||||
|
|
||||||
let vault_balance = svm.get_balance(&global_pda()).unwrap();
|
let vault_balance = ctx.banks_client.get_balance(global_pda()).await.unwrap();
|
||||||
assert!(vault_balance >= amount);
|
assert!(vault_balance >= amount);
|
||||||
let b = read_bet(&svm, 0);
|
let b = read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await);
|
||||||
assert_eq!(b.bettor, bettor.pubkey());
|
assert_eq!(b.bettor, bettor.pubkey());
|
||||||
assert_eq!(b.side, 0);
|
assert_eq!(b.side, 0);
|
||||||
assert_eq!(b.amount, amount);
|
assert_eq!(b.amount, amount);
|
||||||
assert_eq!(b.entry_price, entry_price);
|
assert_eq!(b.entry_price, entry_price);
|
||||||
assert_eq!(b.bet_time, 1_700_000_000);
|
assert_eq!(b.bet_time, START_TIME);
|
||||||
assert_eq!(b.expire_time, 1_700_000_000 + EXPIRY_SECONDS);
|
assert_eq!(b.expire_time, START_TIME + EXPIRY_SECONDS);
|
||||||
assert_eq!(b.status, 0);
|
assert_eq!(b.status, 0);
|
||||||
assert_eq!(read_global(&svm).counter, 1);
|
assert_eq!(read_global_typed(&read_account_data(&ctx.banks_client, &global_pda()).await).counter, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn close_bet_user_wins_up() {
|
async fn close_bet_user_wins_up() {
|
||||||
let (mut svm, admin) = setup();
|
let (mut ctx, admin) = setup().await;
|
||||||
initialize(&mut svm, &admin);
|
initialize(&mut ctx, &admin).await;
|
||||||
let (bettor, amount, entry_price) = create_up_bet(&mut svm, &admin, 0);
|
let (bettor, amount, entry_price) = create_up_bet(&mut ctx, 0).await;
|
||||||
|
|
||||||
set_clock(&mut svm, 1_700_000_000 + EXPIRY_SECONDS + 1);
|
set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await;
|
||||||
let exit_price = entry_price + 1_000;
|
let exit_price = entry_price + 1_000;
|
||||||
let before = svm.get_balance(&bettor.pubkey()).unwrap();
|
let before = ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap();
|
||||||
let payout = amount * MULTIPLIER_BPS / 10_000;
|
let payout = amount * MULTIPLIER_BPS / 10_000;
|
||||||
fund_vault(&mut svm, 10 * amount);
|
fund_vault(&mut ctx, 10 * amount).await;
|
||||||
|
|
||||||
let close_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey());
|
let close_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey());
|
||||||
send(&mut svm, &[&admin], close_ix).expect("close_bet");
|
run_ix(&mut ctx, &admin, close_ix).await.expect("close_bet");
|
||||||
|
|
||||||
assert_eq!(svm.get_balance(&bettor.pubkey()).unwrap(), before + payout);
|
assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before + payout);
|
||||||
let b = read_bet(&svm, 0);
|
let b = read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await);
|
||||||
assert_eq!(b.status, 1);
|
assert_eq!(b.status, 1);
|
||||||
assert_eq!(b.exit_price, exit_price);
|
assert_eq!(b.exit_price, exit_price);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn close_bet_house_wins_down() {
|
async fn close_bet_house_wins_down() {
|
||||||
let (mut svm, admin) = setup();
|
let (mut ctx, admin) = setup().await;
|
||||||
initialize(&mut svm, &admin);
|
initialize(&mut ctx, &admin).await;
|
||||||
let (bettor, _amount, entry_price) = create_up_bet(&mut svm, &admin, 1);
|
let (bettor, _amount, entry_price) = create_up_bet(&mut ctx, 1).await;
|
||||||
|
|
||||||
set_clock(&mut svm, 1_700_000_000 + EXPIRY_SECONDS + 1);
|
set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await;
|
||||||
let exit_price = entry_price + 1_000; // side=DOWN but price rose -> lose
|
let exit_price = entry_price + 1_000; // side=DOWN but price rose -> lose
|
||||||
let before = svm.get_balance(&bettor.pubkey()).unwrap();
|
let before = ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap();
|
||||||
|
|
||||||
let close_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey());
|
let close_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey());
|
||||||
send(&mut svm, &[&admin], close_ix).expect("close_bet");
|
run_ix(&mut ctx, &admin, close_ix).await.expect("close_bet");
|
||||||
|
|
||||||
assert_eq!(svm.get_balance(&bettor.pubkey()).unwrap(), before);
|
assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before);
|
||||||
assert_eq!(read_bet(&svm, 0).status, 2);
|
assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn close_bet_equal_price_stays_open() {
|
async fn close_bet_equal_price_stays_open() {
|
||||||
let (mut svm, admin) = setup();
|
let (mut ctx, admin) = setup().await;
|
||||||
initialize(&mut svm, &admin);
|
initialize(&mut ctx, &admin).await;
|
||||||
let (bettor, amount, entry_price) = create_up_bet(&mut svm, &admin, 0);
|
let (bettor, amount, entry_price) = create_up_bet(&mut ctx, 0).await;
|
||||||
|
|
||||||
set_clock(&mut svm, 1_700_000_000 + EXPIRY_SECONDS + 1);
|
set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await;
|
||||||
let before = svm.get_balance(&bettor.pubkey()).unwrap();
|
let before = ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap();
|
||||||
let payout = amount * MULTIPLIER_BPS / 10_000;
|
let payout = amount * MULTIPLIER_BPS / 10_000;
|
||||||
fund_vault(&mut svm, 10 * amount);
|
fund_vault(&mut ctx, 10 * amount).await;
|
||||||
|
|
||||||
let equal_ix = make_close_ix(0, entry_price, admin.pubkey(), bettor.pubkey());
|
let equal_ix = make_close_ix(0, entry_price, admin.pubkey(), bettor.pubkey());
|
||||||
send(&mut svm, &[&admin], equal_ix).expect("equal close");
|
run_ix(&mut ctx, &admin, equal_ix).await.expect("equal close");
|
||||||
assert_eq!(read_bet(&svm, 0).status, 0);
|
assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 0);
|
||||||
assert_eq!(svm.get_balance(&bettor.pubkey()).unwrap(), before);
|
assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before);
|
||||||
|
|
||||||
let exit_price = entry_price + 1_000;
|
let exit_price = entry_price + 1_000;
|
||||||
let second_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey());
|
let second_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey());
|
||||||
send(&mut svm, &[&admin], second_ix).expect("second close");
|
run_ix(&mut ctx, &admin, second_ix).await.expect("second close");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
svm.get_balance(&bettor.pubkey()).unwrap(),
|
ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(),
|
||||||
before + payout
|
before + payout
|
||||||
);
|
);
|
||||||
assert_eq!(read_bet(&svm, 0).status, 1);
|
assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn close_bet_before_expiry_rejected() {
|
async fn close_bet_already_finalized() {
|
||||||
let (mut svm, admin) = setup();
|
let (mut ctx, admin) = setup().await;
|
||||||
initialize(&mut svm, &admin);
|
initialize(&mut ctx, &admin).await;
|
||||||
let (bettor, _amount, entry_price) = create_up_bet(&mut svm, &admin, 0);
|
let (bettor, amount, entry_price) = create_up_bet(&mut ctx, 0).await;
|
||||||
|
|
||||||
|
set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await;
|
||||||
|
let win_price = entry_price + 500;
|
||||||
|
fund_vault(&mut ctx, 10 * amount).await;
|
||||||
|
run_ix(&mut ctx, &admin, make_close_ix(0, win_price, admin.pubkey(), bettor.pubkey()))
|
||||||
|
.await
|
||||||
|
.expect("first close");
|
||||||
|
|
||||||
|
let res = run_ix(
|
||||||
|
&mut ctx,
|
||||||
|
&admin,
|
||||||
|
make_close_ix(0, win_price + 1, admin.pubkey(), bettor.pubkey()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(res.is_err(), "expected close after finalization to be rejected");
|
||||||
|
let b = read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await);
|
||||||
|
assert_eq!(b.status, 1);
|
||||||
|
assert_eq!(b.exit_price, win_price);
|
||||||
|
assert_eq!(b.status, 1);
|
||||||
|
assert_eq!(b.exit_price, win_price);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn close_bet_before_expiry_rejected() {
|
||||||
|
let (mut ctx, admin) = setup().await;
|
||||||
|
initialize(&mut ctx, &admin).await;
|
||||||
|
let (bettor, _amount, entry_price) = create_up_bet(&mut ctx, 0).await;
|
||||||
|
|
||||||
let close_ix = make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey());
|
let close_ix = make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey());
|
||||||
let res = send(&mut svm, &[&admin], close_ix);
|
let res = run_ix(&mut ctx, &admin, close_ix).await;
|
||||||
assert!(res.is_err(), "expected premature close to be rejected");
|
assert!(res.is_err(), "expected premature close to be rejected");
|
||||||
assert_eq!(read_bet(&svm, 0).status, 0);
|
assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn close_bet_wrong_relayer_rejected() {
|
async fn close_bet_wrong_relayer_rejected() {
|
||||||
let (mut svm, admin) = setup();
|
let (mut ctx, admin) = setup().await;
|
||||||
initialize(&mut svm, &admin);
|
initialize(&mut ctx, &admin).await;
|
||||||
let (bettor, _amount, entry_price) = create_up_bet(&mut svm, &admin, 0);
|
let (bettor, _amount, entry_price) = create_up_bet(&mut ctx, 0).await;
|
||||||
let attacker = Keypair::new();
|
let attacker = Keypair::new();
|
||||||
svm.airdrop(&attacker.pubkey(), 100_000_000_000).unwrap();
|
airdrop_to(&mut ctx, &attacker.pubkey(), 100_000_000_000).await;
|
||||||
|
|
||||||
set_clock(&mut svm, 1_700_000_000 + EXPIRY_SECONDS + 1);
|
set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await;
|
||||||
let close_ix = make_close_ix(0, entry_price + 500, attacker.pubkey(), bettor.pubkey());
|
let close_ix = make_close_ix(0, entry_price + 500, attacker.pubkey(), bettor.pubkey());
|
||||||
let res = send(&mut svm, &[&attacker], close_ix);
|
let res = run_ix(&mut ctx, &attacker, close_ix).await;
|
||||||
assert!(res.is_err(), "expected wrong relayer to be rejected");
|
assert!(res.is_err(), "expected wrong relayer to be rejected");
|
||||||
assert_eq!(read_bet(&svm, 0).status, 0);
|
assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn close_bet_replay_rejected() {
|
async fn close_bet_replay_rejected() {
|
||||||
let (mut svm, admin) = setup();
|
let (mut ctx, admin) = setup().await;
|
||||||
initialize(&mut svm, &admin);
|
initialize(&mut ctx, &admin).await;
|
||||||
let (bettor, _amount, entry_price) = create_up_bet(&mut svm, &admin, 0);
|
let (bettor, _amount, entry_price) = create_up_bet(&mut ctx, 0).await;
|
||||||
|
|
||||||
set_clock(&mut svm, 1_700_000_000 + EXPIRY_SECONDS + 1);
|
set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await;
|
||||||
fund_vault(&mut svm, 10 * MIN_AMOUNT);
|
fund_vault(&mut ctx, 10 * MIN_AMOUNT).await;
|
||||||
let close_ix = make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey());
|
let close_ix = make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey());
|
||||||
send(&mut svm, &[&admin], close_ix).expect("first close");
|
run_ix(&mut ctx, &admin, close_ix).await.expect("first close");
|
||||||
let res = send(&mut svm, &[&admin], make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey()));
|
let before = ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap();
|
||||||
|
let res = run_ix(
|
||||||
|
&mut ctx,
|
||||||
|
&admin,
|
||||||
|
make_close_ix(0, entry_price + 501, admin.pubkey(), bettor.pubkey()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
assert!(res.is_err(), "expected replay close to be rejected");
|
assert!(res.is_err(), "expected replay close to be rejected");
|
||||||
|
assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn create_bet_rejects_out_of_range() {
|
async fn create_bet_rejects_out_of_range() {
|
||||||
let (mut svm, admin) = setup();
|
let (mut ctx, admin) = setup().await;
|
||||||
initialize(&mut svm, &admin);
|
initialize(&mut ctx, &admin).await;
|
||||||
let bettor = Keypair::new();
|
let bettor = Keypair::new();
|
||||||
svm.airdrop(&bettor.pubkey(), 100_000_000_000).unwrap();
|
airdrop_to(&mut ctx, &bettor.pubkey(), 100_000_000_000).await;
|
||||||
set_clock(&mut svm, 1_700_000_000);
|
set_clock(&mut ctx, START_TIME).await;
|
||||||
|
|
||||||
let ix = make_create_ix(0, 0, 1, 100_000_000u64, bettor.pubkey());
|
let ix = make_create_ix(0, 0, 1, 100_000_000u64, bettor.pubkey());
|
||||||
let res = send(&mut svm, &[&bettor], ix);
|
let res = run_ix(&mut ctx, &bettor, ix).await;
|
||||||
assert!(res.is_err(), "expected below-min bet to be rejected");
|
assert!(res.is_err(), "expected below-min bet to be rejected");
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user