diff --git a/.gitignore b/.gitignore index fda654d..957acdc 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ # ключи игроков/секрет — НЕ коммитить (имеют SOL-баланс) deploy/player.json deploy/*player*.json +test-ledger/ +*.idl_build_err.log diff --git a/TASK-fund-bettor.md b/TASK-fund-bettor.md new file mode 100644 index 0000000..d2a9f58 --- /dev/null +++ b/TASK-fund-bettor.md @@ -0,0 +1,29 @@ +# TASK — Выдавать фантики бетторам на localnet (как SOL airdrop) + +Репозиторий: `smart-updown-token`, файл `relayer/src/index.ts` (+ возможно `ops.ts`). + +## Проблема (обнаружено на localnet E2E) +`createBet` падает: `Tokenkeg... Transfer: insufficient funds`. +Причина: беттор имеет SOL (ренту/комиссии, `fundBettor` airdrop'ит SOL), но НЕ имеет +наших фантиков в своём ATA. Контракт верно требует средства игрока. Для localnet +нужно наливать игроку фантики (по аналогии с SOL-airdrop), потому что swap ещё не подключён. + +## Что исправить (релейер) +1. В `relayer/src/ops.ts`: в `ensureBettorTokenAccount` (или рядом) при + `cfg.allowAirdrop === true` — после создания ATA наливать токен: + `mintTo(connection, admin, mint, ata, admin, startBalance)`. + - `startBalance` — например, `10_000_000_000` (10 тыс. фантиков, по модулю + `minAmount`..`maxAmount` достаточно). Логировать. + - Учесть, что `admin` — mint authority (на localnet таковым является админская keypair). +2. `relayer/src/index.ts` фандинг беттора: вызывать новую логику наливания токенов + (удобно чтобы работало через `ensureBettorTokenAccount`). + +## Гейт +- `npm run typecheck` — 0 ошибок. +- `npm test` — зелёное. +- На localnet: createBet проходит (беттор имеет фантики), closeBet выплачивает. + +## НЕ делать +- Не трогать контракт (`program/`). +- Не менять логику цен/ставок. +- Не коммитить. \ No newline at end of file diff --git a/TASK-vault-ata-fix.md b/TASK-vault-ata-fix.md new file mode 100644 index 0000000..d8c6e49 --- /dev/null +++ b/TASK-vault-ata-fix.md @@ -0,0 +1,47 @@ +# TASK — Багфикс: vault_token в релейере должен быть ATA, а не PDA + +Репозиторий: `smart-updown-token`, файл `relayer/src/ops.ts`. + +## Проблема (обнаружена на localnet E2E) +Контракт переведён на SPL-токен (Фаза 1). В инструкциях `create_bet`/`close_bet` +аккаунт `vault_token` — это **ATA-аккаунт токена, который принадлежит владельцу +vaultPda** (PDA `[b"global", b"vault"]`). Правильный адрес: +`getAssociatedTokenAddress(mint, vaultPda(programId))` (allowOwnerOffCurve по PDA-владельцу). + +Однако в релейере `createBet`/`closeBet` используется: +```ts +vaultToken: vaultPda(program.programId), +``` +Это передаёт адрес **самого PDA-владельца** (не токен-аккаунт) — контракт +отвергнет транзакцию (wrong account), а `ensureVaultFunded` зовёт +`getTokenAccountBalance(vaultToken)` где vaultToken = vaultPda → «could not find account». +Т.е. ваулт ATA даже не создаётся в нужном месте. + +## Что исправить (только `relayer/src/ops.ts`) + +1. **`ensureVaultFunded`**: + - Получать ATA ваулта: `const vaultAta = await getAssociatedTokenAddress(mint, vaultPda, true);` + (третий аргумент `true` — allowOwnerOffCurve, иначе TokenOwnerOffCurveError). + - Создавать/получать: `getOrCreateAssociatedTokenAccount(connection, admin, mint, vaultPda, true)`. + - `getTokenAccountBalance` звать по **ATA-адресу** (`vaultAta`), не по vaultPda. + +2. **`createBet`**: + - `vaultToken: await getAssociatedTokenAddress(mint, vaultPda(program.programId), true)` + +3. **`closeBet`**: + - `vaultToken: await getAssociatedTokenAddress(mint, vaultPda(program.programId), true)` + +## Гейты +- `npm run typecheck` — 0 ошибок. +- `npm run test` (check.ts) — зелёные проверки. +- (Если можно) быстрый локальный smoke: создать ваулт ATA, налить токен, create/close. + +## НЕ делать +- Не трогать `program/` (контракт), не менять IDL. +- Не трогать логику цен/ставок. +- Не коммитить. + +## Суть одным абзацем +`vault_token` в контракте — токен-счёт (ATA), всегда считаемый через +`getAssociatedTokenAddress(mint, vaultPda, true)`. Заменить все прямые +использования `vaultPda(...)` как адреса acкаунта на ATA-адрес. \ No newline at end of file diff --git a/TASK-vault-contract-fix.md b/TASK-vault-contract-fix.md new file mode 100644 index 0000000..bcb4443 --- /dev/null +++ b/TASK-vault-contract-fix.md @@ -0,0 +1,62 @@ +# TASK — Фикс: сделать vault_token обычным ATA-токен-счётом (убрать seeds = PDA) + +Репозиторий: `smart-updown-token`. Файлы: `program/programs/smart-updown/src/instructions/create_bet.rs` +и `.../close_bet.rs`. + +## Проблема (обнаружено на localnet E2E) +Токен-аккаунт ваулта объявлен так: +```rust +#[account( + mut, + seeds = [GLOBAL_SEED, VAULT_SEED], // ← требует адрес == PDA([global,vault]) +)] +pub vault_token: Account<'info, TokenAccount> +``` +Anchor проверяет, что **адрес `vault_token` == PDA([global,vault])**. Но казённый +(escrow) токен-счёт у нас — это ATA от владельца `vaultPda`: адрес +`getAssociatedTokenAddress(mint, vaultPda)`, который НЕ равен PDA. Отсюда +`Error Code: ConstraintSeeds` — перевод в ваулт невозможен. + +У `bettor_token` такой проблемы нет (там обычный `Account`, адрес = ATA игрока). + +## Что исправить + +### 1. `create_bet.rs` +У `vault_token` **убрать блок `seeds = [GLOBAL_SEED, VAULT_SEED]`** — оставить просто: +```rust +#[account(mut)] +pub vault_token: Account<'info, TokenAccount>, +``` +(оставить поле `bump` больше не нужно — удалить его из struct, если он был). +Это сделает адрес любого токен-счёта (ATA ваулта) приемлемым. + +### 2. `close_bet.rs` +То же самое: у `vault_token` убрать `seeds = [...]`. НО: раз адрес больше не фиксируется +seeds, передавать/подписывать перевод нужно от **владельца-пользователя (vaultPda)**. +В `close_bet` AUTHORITY перевода остаётся владелец аккаунта (поле `owner` токен-счёта). +Проверить: signer_seeds `[GLOBAL_SEED, VAULT_SEED, bump]` должны по-прежнему давать +vaultPda (это и есть владелец ATA). Так как сигнатура от PDA не меняется, seed-фразы +в `close_bet.rs` (для подписи CPI) СТАВЯТСЯ из `ctx.accounts.vault_token` owner. +Возможно понадобится отдельный аккаунт `vault_authority: PDA([global,vault])`, чтобы +передать в signer seeds — оценить и сделать аккуратно (стандартный Anchor escrow). + +### 3. Пересборка и проверка +- `anchor build` зелёный. +- IDL перегенерировать (`anchor idl build`), синхронизировать `relayer/idl/smart_updown.json`. +- `npm run typecheck` / `npm test` в `relayer/` — зелёные (если тесты проверяют seeds — обновить). + +## Важно (архитектурно) +`vault_token` — это ATA от владельца `vaultPda` (адрес `getAssociatedTokenAddress(mint, vaultPda)`). +Контракт НЕ должен требовать конкретного адреса — только владельца-подпись для withdraw. +Всё, что нужно — чтобы в `close_bet` authority (тот, кто подписывает перевод из ваулта) +был **vaultPda** (владелец ATA), а не адрес аккаунта. + +## НЕ делать +- Не трогать логику ставок/цен. +- Не трогать релейер (он уже передаёт правильный ATA-адрес). +- Не коммитить. + +## Проверка успеха +- `anchor build` + IDL обновлён. +- На localnet: передача `vault_token` = ATA (`getAssociatedTokenAddress(mint, vaultPda)`), + create_bet принимает ставку, close_bet выплачивает (signer — vaultPda). E2E проходит. \ No newline at end of file diff --git a/deploy/smart_updown.so b/deploy/smart_updown.so index 47bcdad..6947af2 100755 Binary files a/deploy/smart_updown.so and b/deploy/smart_updown.so differ diff --git a/idl/smart_updown.json b/idl/smart_updown.json index 6401805..7d9b88f 100644 --- a/idl/smart_updown.json +++ b/idl/smart_updown.json @@ -73,9 +73,22 @@ { "name": "vault_token", "docs": [ - "Program-owned token vault: PDA([GLOBAL_SEED, VAULT_SEED]); payouts are paid out from here." + "Token vault ATA (owner = PDA([GLOBAL_SEED, VAULT_SEED])); payouts are paid out from here." + ], + "writable": true + }, + { + "name": "bettor_token", + "docs": [ + "Bettor's SPL-token account; the payout is sent here." + ], + "writable": true + }, + { + "name": "vault_authority", + "docs": [ + "It only serves as the CPI signing authority for the vault ATA payout, so no owner/type check is needed." ], - "writable": true, "pda": { "seeds": [ { @@ -102,13 +115,6 @@ ] } }, - { - "name": "bettor_token", - "docs": [ - "Bettor's SPL-token account; the payout is sent here." - ], - "writable": true - }, { "name": "token_program", "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" @@ -200,34 +206,9 @@ { "name": "vault_token", "docs": [ - "Program-owned token vault: PDA([GLOBAL_SEED, VAULT_SEED]); bet amounts are escrowed here." + "Token vault ATA (owner = PDA([GLOBAL_SEED, VAULT_SEED])); bet amounts are escrowed here." ], - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108 - ] - }, - { - "kind": "const", - "value": [ - 118, - 97, - 117, - 108, - 116 - ] - } - ] - } + "writable": true }, { "name": "token_program", diff --git a/program/programs/smart-updown/src/instructions/close_bet.rs b/program/programs/smart-updown/src/instructions/close_bet.rs index 2ce4d3d..30bdba7 100644 --- a/program/programs/smart-updown/src/instructions/close_bet.rs +++ b/program/programs/smart-updown/src/instructions/close_bet.rs @@ -30,16 +30,16 @@ pub struct CloseBet<'info> { /// CHECK: recipient must be the bet's bettor and is validated in the handler. 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 - )] + /// Token vault ATA (owner = PDA([GLOBAL_SEED, VAULT_SEED])); payouts are paid out from here. + #[account(mut)] pub vault_token: Account<'info, TokenAccount>, /// Bettor's SPL-token account; the payout is sent here. #[account(mut)] pub bettor_token: Account<'info, TokenAccount>, + /// CHECK: vault owner PDA, address enforced by the seeds = [GLOBAL_SEED, VAULT_SEED] + bump above. + /// It only serves as the CPI signing authority for the vault ATA payout, so no owner/type check is needed. + #[account(seeds = [GLOBAL_SEED, VAULT_SEED], bump)] + pub vault_authority: UncheckedAccount<'info>, pub token_program: Program<'info, Token>, } @@ -86,16 +86,16 @@ let payout; payout = payout_amount; { // 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. + // The vault ATA is owned by the vault 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(), + authority: ctx.accounts.vault_authority.to_account_info(), }; let signer_seeds: &[&[u8]] = &[ GLOBAL_SEED, VAULT_SEED, - &[ctx.bumps.vault_token], + &[ctx.bumps.vault_authority], ]; let vault_signer_seeds = [signer_seeds]; let cpi_ctx = CpiContext::new_with_signer( diff --git a/program/programs/smart-updown/src/instructions/create_bet.rs b/program/programs/smart-updown/src/instructions/create_bet.rs index 58eea2b..460bd1b 100644 --- a/program/programs/smart-updown/src/instructions/create_bet.rs +++ b/program/programs/smart-updown/src/instructions/create_bet.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; use anchor_spl::token::{Token, TokenAccount}; use crate::{ - constants::{BET_SEED, GLOBAL_SEED, SIDE_DOWN, SIDE_UP, STATUS_OPEN, VAULT_SEED}, + constants::{BET_SEED, GLOBAL_SEED, SIDE_DOWN, SIDE_UP, STATUS_OPEN}, error::SmartUpdownError, events::BetCreated, state::{Bet, Global}, @@ -32,12 +32,8 @@ pub struct CreateBet<'info> { /// 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 - )] + /// Token vault ATA (owner = PDA([GLOBAL_SEED, VAULT_SEED])); bet amounts are escrowed here. + #[account(mut)] pub vault_token: Account<'info, TokenAccount>, pub token_program: Program<'info, Token>, } diff --git a/program/programs/smart-updown/tests/test_smart_updown.rs b/program/programs/smart-updown/tests/test_smart_updown.rs index c39ba74..89a434f 100644 --- a/program/programs/smart-updown/tests/test_smart_updown.rs +++ b/program/programs/smart-updown/tests/test_smart_updown.rs @@ -216,6 +216,7 @@ fn make_close_ix( clock: CLOCK_ADDR.parse().unwrap(), vault_token: vault_pda(), bettor_token: *bettor_ata, + vault_authority: vault_pda(), token_program: spl_token::ID, } .to_account_metas(None), diff --git a/relayer/idl/smart_updown.json b/relayer/idl/smart_updown.json index 6401805..7d9b88f 100644 --- a/relayer/idl/smart_updown.json +++ b/relayer/idl/smart_updown.json @@ -73,9 +73,22 @@ { "name": "vault_token", "docs": [ - "Program-owned token vault: PDA([GLOBAL_SEED, VAULT_SEED]); payouts are paid out from here." + "Token vault ATA (owner = PDA([GLOBAL_SEED, VAULT_SEED])); payouts are paid out from here." + ], + "writable": true + }, + { + "name": "bettor_token", + "docs": [ + "Bettor's SPL-token account; the payout is sent here." + ], + "writable": true + }, + { + "name": "vault_authority", + "docs": [ + "It only serves as the CPI signing authority for the vault ATA payout, so no owner/type check is needed." ], - "writable": true, "pda": { "seeds": [ { @@ -102,13 +115,6 @@ ] } }, - { - "name": "bettor_token", - "docs": [ - "Bettor's SPL-token account; the payout is sent here." - ], - "writable": true - }, { "name": "token_program", "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" @@ -200,34 +206,9 @@ { "name": "vault_token", "docs": [ - "Program-owned token vault: PDA([GLOBAL_SEED, VAULT_SEED]); bet amounts are escrowed here." + "Token vault ATA (owner = PDA([GLOBAL_SEED, VAULT_SEED])); bet amounts are escrowed here." ], - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108 - ] - }, - { - "kind": "const", - "value": [ - 118, - 97, - 117, - 108, - 116 - ] - } - ] - } + "writable": true }, { "name": "token_program", diff --git a/relayer/idl/smart_updown.ts b/relayer/idl/smart_updown.ts index a7d58b6..3731483 100644 --- a/relayer/idl/smart_updown.ts +++ b/relayer/idl/smart_updown.ts @@ -78,9 +78,22 @@ export type SmartUpdown = { { "name": "vaultToken", "docs": [ - "Program-owned token vault: PDA([GLOBAL_SEED, VAULT_SEED]); payouts are paid out from here." + "Token vault ATA (owner = PDA([GLOBAL_SEED, VAULT_SEED])); payouts are paid out from here." + ], + "writable": true + }, + { + "name": "bettorToken", + "docs": [ + "Bettor's SPL-token account; the payout is sent here." + ], + "writable": true + }, + { + "name": "vaultAuthority", + "docs": [ + "It only serves as the CPI signing authority for the vault ATA payout, so no owner/type check is needed." ], - "writable": true, "pda": { "seeds": [ { @@ -107,13 +120,6 @@ export type SmartUpdown = { ] } }, - { - "name": "bettorToken", - "docs": [ - "Bettor's SPL-token account; the payout is sent here." - ], - "writable": true - }, { "name": "tokenProgram", "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" @@ -205,34 +211,9 @@ export type SmartUpdown = { { "name": "vaultToken", "docs": [ - "Program-owned token vault: PDA([GLOBAL_SEED, VAULT_SEED]); bet amounts are escrowed here." + "Token vault ATA (owner = PDA([GLOBAL_SEED, VAULT_SEED]); bet amounts are escrowed here." ], - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 103, - 108, - 111, - 98, - 97, - 108 - ] - }, - { - "kind": "const", - "value": [ - 118, - 97, - 117, - 108, - 116 - ] - } - ] - } + "writable": true }, { "name": "tokenProgram", diff --git a/relayer/src/check.ts b/relayer/src/check.ts index 0405ee8..f1c0548 100644 --- a/relayer/src/check.ts +++ b/relayer/src/check.ts @@ -58,12 +58,16 @@ async function main(): Promise { "betPda(0) совпадает с седами bet из IDL" ); check( - vaultPda(programId).equals(pdaFromIdlSeeds(programId, (acct(createIx, "vault_token") as any).pda.seeds)), - "vaultPda совпадает с седами vault_token из IDL" + vaultPda(programId).equals(pdaFromIdlSeeds(programId, (acct(closeIx, "vault_authority") as any).pda.seeds)), + "vaultPda совпадает с седами vault_authority в close_bet" ); check( - vaultPda(programId).equals(pdaFromIdlSeeds(programId, (acct(closeIx, "vault_token") as any).pda.seeds)), - "vaultPda совпадает с седами vault_token в close_bet" + (acct(createIx, "vault_token") as any).pda === undefined, + "create_bet: vault_token — обычный ATA-счёт (без PDA/seeds)" + ); + check( + (acct(closeIx, "vault_token") as any).pda === undefined, + "close_bet: vault_token — обычный ATA-счёт (без PDA/seeds)" ); // 2. Сборка инструкций оффлайн и проверка ключей @@ -114,7 +118,7 @@ async function main(): Promise { check(!!(kByName(bettorToken)!.isWritable), "close_bet: bettor_token — writable"); check(!!(kByName(vaultToken)!.isWritable), "close_bet: vault_token — writable"); check(kByName(TOKEN_PROGRAM_ID) !== undefined && !kByName(TOKEN_PROGRAM_ID)!.isWritable, "close_bet: token_program присутствует"); - check(close.keys.length === 8, "close_bet: 8 аккаунтов (IDL-состав)"); + check(close.keys.length === 9, "close_bet: 9 аккаунтов (IDL-состав)"); if (failed > 0) { console.error(`\ncheck: ${failed} проверок ПРОВАЛЕНО`); diff --git a/relayer/src/config.ts b/relayer/src/config.ts index 242a936..85ec6be 100644 --- a/relayer/src/config.ts +++ b/relayer/src/config.ts @@ -30,6 +30,13 @@ export interface Config { /** Bet amount bounds in token base units (NOT lamports); must match the on-chain `initialize` values. */ minAmount: number; maxAmount: number; + /** + * How many "фантики" (token base units) the localnet relayer mints into each + * bettor's ATA on start (analog of the SOL airdrop). Must cover the max + * bet size so a freshly funded bettor can place a full-size bet. Only used + * when `allowAirdrop` is true (localnet). + */ + startBalance: number; multiplierBps: number; expirySeconds: number; } @@ -64,6 +71,9 @@ export function loadConfig(): Config { maxAmount: envNum("SMART_MAX_AMOUNT", 10_000_000_000), // 10 000 фантиков multiplierBps: envNum("SMART_MULTIPLIER_BPS", 13_000), // 1.3x expirySeconds: envNum("SMART_EXPIRY_SECONDS", 15), + // Стартовый баланс фантиков, который локальный релейер mintTo'ит беттору + // (аналог SOL-airdrop). По умолчанию равен maxAmount — хватает на максимум ставки. + startBalance: envNum("SMART_START_BALANCE", 10_000_000_000), }; } diff --git a/relayer/src/index.ts b/relayer/src/index.ts index fe4e7a4..e717ba6 100644 --- a/relayer/src/index.ts +++ b/relayer/src/index.ts @@ -94,7 +94,7 @@ async function main(): Promise { // mint ещё не создан (плейсхолдер) — дефолт из ТЗ контракта } - const global = await readGlobal(program, program.programId); + const global = await readGlobal(program, program.programId, cfg.tokenMint); if (!global) { if (!process.argv.includes("--init")) { throw new Error( @@ -127,7 +127,14 @@ async function main(): Promise { const req = nextBetRequest(cfg.minAmount, cfg.maxAmount); await fundBettor(connection, req.bettor.publicKey); // Беттору нужен токен-аккаунт (ATA) — без него create_bet не пройдёт. - await ensureBettorTokenAccount(connection, admin, cfg.tokenMint, req.bettor.publicKey); + // На localnet (allowAirdrop) после создания ATA наливает фантики (аналог SOL-airdrop). + await ensureBettorTokenAccount( + connection, + admin, + cfg.tokenMint, + req.bettor.publicKey, + cfg.allowAirdrop ? BigInt(cfg.startBalance) : undefined + ); const entryPrice = await getPrice(); await createBet(program, cfg.tokenMint, req.bettor, { @@ -141,7 +148,7 @@ async function main(): Promise { ); await sleep((cfg.expirySeconds + 1) * 1000); - const g0 = await readGlobal(program, program.programId); + const g0 = await readGlobal(program, program.programId, cfg.tokenMint); const id = g0!.counter.subn(1); await settleBet(program, id, entryPrice, admin, req.bettor.publicKey, cfg.tokenMint); diff --git a/relayer/src/ops.ts b/relayer/src/ops.ts index 24e9af0..7d57319 100644 --- a/relayer/src/ops.ts +++ b/relayer/src/ops.ts @@ -23,7 +23,8 @@ const BPS_DIVISOR = 10_000n; */ export async function readGlobal( program: Program, - programId: PublicKey + programId: PublicKey, + mint: PublicKey ): Promise<{ counter: BN; minAmount: BN; @@ -33,7 +34,7 @@ export async function readGlobal( paused: BN; admin: PublicKey; relayer: PublicKey; - /** Баланс vaultToken (PDA-ваула) в токен-юнитах; "0", если аккаунт ещё не создан. */ + /** Баланс vault ATA (owner = PDA-ваула) в токен-юнитах; "0", если аккаунт ещё не создан. */ vaultBalance: string; } | null> { const global = globalPda(programId); @@ -41,9 +42,8 @@ export async function readGlobal( if (!raw) return null; let vaultBalance = "0"; try { - const bal = await program.provider.connection.getTokenAccountBalance( - vaultPda(programId) - ); + const vaultAta = await getAssociatedTokenAddress(mint, vaultPda(programId), true); + const bal = await program.provider.connection.getTokenAccountBalance(vaultAta); vaultBalance = bal.value.amount || "0"; } catch { vaultBalance = "0"; @@ -94,7 +94,7 @@ export async function createBet( bettor: Keypair, args: { side: number; amount: number; entryPrice: number } ): Promise { - const global = await readGlobal(program, program.programId); + const global = await readGlobal(program, program.programId, mint); if (!global) throw new Error("Contract not initialized"); const id = global.counter; @@ -108,7 +108,7 @@ export async function createBet( .accountsPartial({ bettor: bettor.publicKey, bettorToken: await getAssociatedTokenAddress(mint, bettor.publicKey), - vaultToken: vaultPda(program.programId), + vaultToken: await getAssociatedTokenAddress(mint, vaultPda(program.programId), true), tokenProgram: TOKEN_PROGRAM_ID, }) .signers([bettor]) @@ -140,7 +140,7 @@ export async function closeBet( relayer: relayer.publicKey, bettor, bettorToken: await getAssociatedTokenAddress(mint, bettor), - vaultToken: vaultPda(program.programId), + vaultToken: await getAssociatedTokenAddress(mint, vaultPda(program.programId), true), tokenProgram: TOKEN_PROGRAM_ID, }) .signers([relayer]) @@ -151,12 +151,17 @@ export async function closeBet( * Создаёт ATA беттора, если ещё нет (рент платит админ; сам ATA не требует * подписи владельца). Без аккаунта createBet не пройдёт: контракт сам * его не инициализирует. + * + * При `startBalance` !== undefined (только localnet, `cfg.allowAirdrop === true`) + * после создания ATA доналивает беттору фантики до startBalance (аналог + * SOL-airdrop, swap ещё не подключён). `admin` — mint authority. */ export async function ensureBettorTokenAccount( connection: Connection, admin: Keypair, mint: PublicKey, - bettor: PublicKey + bettor: PublicKey, + startBalance?: bigint ): Promise { const ata = await getAssociatedTokenAddress(mint, bettor); try { @@ -165,13 +170,25 @@ export async function ensureBettorTokenAccount( await createAssociatedTokenAccount(connection, admin, mint, bettor); console.log(`bettor ATA created ${bettor.toBase58()} -> ${ata.toBase58()}`); } + if (startBalance !== undefined) { + const bal = await getAccount(connection, ata); + const current = BigInt(bal.amount); + const topUp = startBalance - current; + if (topUp > 0n) { + await mintTo(connection, admin, mint, ata, admin, topUp); + console.log( + `bettor funded ${bettor.toBase58()} +${topUp} fant base units (ATA total ${startBalance})` + ); + } + } } /** * Догружает ваулт (house bankroll), чтобы хватило на выплаты 1.3x. * Теперь это ТОКЕН, не SOL: на localnet mint-авторитетом выступает admin - * (см. state.rs: decimals = 6), так что наливается через mintTo в PDA-ваулт. - * На mainnet наливается off-chain переводом фантиков (вне этого CLI). + * (см. state.rs: decimals = 6), так что наливается через mintTo в ATA-ваулт + * (токен-счёт с owner = PDA([global, vault])). На mainnet наливается + * off-chain переводом фантиков (вне этого CLI). */ export async function ensureVaultFunded( connection: Connection, @@ -180,18 +197,19 @@ export async function ensureVaultFunded( cfg: Config ): Promise { const mint = cfg.tokenMint; - const vaultToken = vaultPda(program.programId); + const vault = vaultPda(program.programId); + const vaultAta = await getAssociatedTokenAddress(mint, vault, true); - // PDA-ваулт (ATA с owner = PDA([global, vault])) может не существовать — + // ATA-ваулт (owner = PDA([global, vault])) может не существовать — // контракт его не создаёт. ATA не требует подписи owner (allowOwnerOffCurve), // поэтому создаём её от имени admin. - await getOrCreateAssociatedTokenAccount(connection, admin, mint, vaultToken, true); + await getOrCreateAssociatedTokenAccount(connection, admin, mint, vault, true); - const bal = await connection.getTokenAccountBalance(vaultToken); + const bal = await connection.getTokenAccountBalance(vaultAta); const current = BigInt(bal.value.amount || "0"); const target = (BigInt(cfg.maxAmount) * BigInt(cfg.multiplierBps) / BPS_DIVISOR) * 2n; if (current >= target) return; const topUp = target - current; - await mintTo(connection, admin, mint, vaultToken, admin, topUp); + await mintTo(connection, admin, mint, vaultAta, admin, topUp); console.log(`vault funded +${topUp} token base units (target ${target})`); }