fix: vault как ATA + фандинг беттора; E2E на localnet ok
This commit is contained in:
@@ -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
@@ -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",
|
||||
|
||||
@@ -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} проверок ПРОВАЛЕНО`);
|
||||
|
||||
@@ -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
@@ -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
@@ -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})`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user