fix: vault как ATA + фандинг беттора; E2E на localnet ok

This commit is contained in:
Caffeine
2026-09-10 02:12:07 +03:00
parent fb9a7f4e8b
commit f937c8993d
15 changed files with 267 additions and 148 deletions
+2
View File
@@ -3,3 +3,5 @@
# ключи игроков/секрет — НЕ коммитить (имеют SOL-баланс)
deploy/player.json
deploy/*player*.json
test-ledger/
*.idl_build_err.log
+29
View File
@@ -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/`).
- Не менять логику цен/ставок.
- Не коммитить.
+47
View File
@@ -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-адрес.
+62
View File
@@ -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<TokenAccount>`, адрес = 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 проходит.
Binary file not shown.
+17 -36
View File
@@ -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",
@@ -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(
@@ -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>,
}
@@ -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),
+17 -36
View File
@@ -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",
+17 -36
View File
@@ -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",
+9 -5
View File
@@ -58,12 +58,16 @@ async function main(): Promise<void> {
"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<void> {
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} проверок ПРОВАЛЕНО`);
+10
View File
@@ -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),
};
}
+10 -3
View File
@@ -94,7 +94,7 @@ async function main(): Promise<void> {
// 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<void> {
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<void> {
);
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);
+34 -16
View File
@@ -23,7 +23,8 @@ const BPS_DIVISOR = 10_000n;
*/
export async function readGlobal(
program: Program<SmartUpdown>,
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<BN> {
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<void> {
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<void> {
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})`);
}