feat(contract): возврат залога при ничьей (exit_price == entry_price)

Раньше при равенстве цен close_bet делал `return Ok(())` — ставка навсегда
оставалась в статусе open, а залог запирался в ваулте (замер: 33.6% окон).
Из-за правила «одна открытая ставка на адрес» игрок получал 409 пожизненно.

Теперь ничья возвращает залог целиком (без множителя) и финализирует ставку:
- constants.rs: STATUS_REFUNDED = 3 (0/1/2 не тронуты — зашиты в шлюз и в цепи)
- close_bet.rs: CPI-перевод amount из ваулта беттору под сидами ваулт-PDA,
  запись exit_price/status, событие BetClosed(status=3, payout=amount)
- settle.ts: STATUS_REFUNDED, statusName "refunded", статус 3 в условии финализации
- check.ts: проверка формы события refunded
- тест close_bet_equal_price_stays_open → close_bet_equal_price_refunds
  (+ повторное закрытие отклоняется)

Проверено: cargo test-sbf 10/10, npm run typecheck EXIT=0, две мутации
(снятие статуса, снятие перевода) валят именно новый тест.
Задеплоено: программа 9ALs… (хэш из цепи совпал побайтово), шлюз rev 15.
This commit is contained in:
Caffeine
2026-09-14 02:09:08 +03:00
parent a73e44de9a
commit 42dfa58582
6 changed files with 82 additions and 15 deletions
@@ -17,5 +17,8 @@ pub const SIDE_DOWN: u64 = 1;
pub const STATUS_OPEN: u64 = 0;
pub const STATUS_PAYOUT_DONE: u64 = 1;
pub const STATUS_HOUSE_WON: u64 = 2;
/// Возврат залога при ничьей (`exit_price == entry_price`).
/// Добавлен позже остальных — 0/1/2 уже зашиты в шлюз и в живые аккаунты цепи.
pub const STATUS_REFUNDED: u64 = 3;
pub const BPS_DIVISOR: u64 = 10_000;
@@ -4,7 +4,7 @@ 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, VAULT_SEED,
STATUS_PAYOUT_DONE, STATUS_OPEN, STATUS_REFUNDED, VAULT_SEED,
},
error::SmartUpdownError,
events::BetClosed,
@@ -64,6 +64,40 @@ pub fn handler(ctx: &mut Context<CloseBet>, id: u64, exit_price: u64) -> Result<
let amount = ctx.accounts.bet.amount;
if exit_price == entry {
require!(
ctx.accounts.vault_token.amount >= amount,
SmartUpdownError::InsufficientVault
);
{
// Возврат залога: переводим amount (БЕЗ множителя) из ваулта беттору.
// Подпись — сидами ваулт-PDA (как в ветке выигрыша ниже).
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_authority.to_account_info(),
};
let signer_seeds: &[&[u8]] = &[
GLOBAL_SEED,
VAULT_SEED,
&[ctx.bumps.vault_authority],
];
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, amount)?;
}
ctx.accounts.bet.exit_price = exit_price;
ctx.accounts.bet.status = STATUS_REFUNDED;
emit!(BetClosed {
bet: ctx.accounts.bet.key(),
exit_price,
user_won: false,
payout: amount,
status: STATUS_REFUNDED,
});
return Ok(());
}