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
@@ -441,7 +441,7 @@ async fn close_bet_house_wins_down() {
}
#[tokio::test]
async fn close_bet_equal_price_stays_open() {
async fn close_bet_equal_price_refunds() {
let (mut ctx, admin) = setup().await;
initialize(&mut ctx, &admin).await;
let mint = create_token_env(&mut ctx, &admin).await;
@@ -449,21 +449,35 @@ async fn close_bet_equal_price_stays_open() {
set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await;
let before = read_token_balance(&mut ctx, &ata).await;
let payout = amount * MULTIPLIER_BPS / 10_000;
let equal_ix = make_close_ix(0, entry_price, admin.pubkey(), bettor.pubkey(), &ata);
run_close_tx(&mut ctx, &admin, &equal_ix).await.expect("equal close");
assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 0);
assert_eq!(read_token_balance(&mut ctx, &ata).await, before);
run_close_tx(
&mut ctx,
&admin,
&make_close_ix(0, entry_price, admin.pubkey(), bettor.pubkey(), &ata),
)
.await
.expect("equal close");
let exit_price = entry_price + 1_000;
let second_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey(), &ata);
run_close_tx(&mut ctx, &admin, &second_ix).await.expect("second close");
let b = read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await);
assert_eq!(b.status, 3, "expected status REFUNDED=3");
assert_eq!(b.exit_price, entry_price, "exit_price must be recorded as entry_price");
assert_eq!(read_token_balance(&mut ctx, &ata).await, before + amount, "bettor +amount");
assert_eq!(
read_token_balance(&mut ctx, &ata).await,
before + payout
read_token_balance(&mut ctx, &vault_pda()).await,
VAULT_TOKEN_SUPPLY,
"vault -amount"
);
assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 1);
// Повторный close по той же ставке → ошибка BetAlreadyFinalized.
// exit_price чуть другой, иначе banks дедуплицирует транзакцию
// (тот же signer + тот же blockhash + те же данные икса → Ok без перезапуска).
let res = run_close_tx(
&mut ctx,
&admin,
&make_close_ix(0, entry_price + 1, admin.pubkey(), bettor.pubkey(), &ata),
)
.await;
assert!(res.is_err(), "expected second close after refund to be rejected");
}
#[tokio::test]