158 lines
5.4 KiB
TypeScript
158 lines
5.4 KiB
TypeScript
import { PublicKey } from "@solana/web3.js";
|
|
import { getMint } from "@solana/spl-token";
|
|
import { Program } from "@anchor-lang/core";
|
|
import { loadConfig, loadKeypair, SIDE_UP } from "./config";
|
|
import { getPrice } from "./priceSource";
|
|
import { nextBetRequest } from "./frontStub";
|
|
import {
|
|
initialize,
|
|
createBet,
|
|
closeBet,
|
|
readGlobal,
|
|
ensureVaultFunded,
|
|
ensureBettorTokenAccount,
|
|
fundBettor,
|
|
} from "./ops";
|
|
import { betPda } from "./pda";
|
|
import { makeConnection, makeProgram } from "./program";
|
|
|
|
import type { SmartUpdown } from "../idl/smart_updown";
|
|
|
|
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
const STATUS_OPEN = 0;
|
|
|
|
/** Человекочитаемо-формат: токен-юниты -> "N.NNNNNN" по decimals. */
|
|
function humanize(units: string | number | bigint, decimals: number): string {
|
|
const n = BigInt(units);
|
|
const sign = n < 0n ? "-" : "";
|
|
const abs = n < 0n ? -n : n;
|
|
const pow = 10n ** BigInt(decimals);
|
|
return `${sign}${abs / pow}.${(abs % pow).toString().padStart(decimals, "0")}`;
|
|
}
|
|
|
|
/** Закрывает ставку, повторяя при «equal price» (exit==entry) пока не зафиналится. */
|
|
async function settleBet(
|
|
program: Program<SmartUpdown>,
|
|
id: any,
|
|
entryPrice: number,
|
|
relayer: any,
|
|
bettor: PublicKey,
|
|
mint: PublicKey
|
|
): Promise<void> {
|
|
for (let attempt = 1; ; attempt++) {
|
|
const exitPrice = await getPrice();
|
|
if (exitPrice === entryPrice) {
|
|
console.log(`attempt ${attempt}: exit==entry (${exitPrice}), жду движение...`);
|
|
await sleep(2000);
|
|
continue;
|
|
}
|
|
const sig = await closeBet(program, id, exitPrice, relayer, bettor, mint);
|
|
console.log(`close_bet #${id} exit=${exitPrice} (attempt ${attempt}) tx=${sig}`);
|
|
|
|
const status = await getStatus(program, id);
|
|
if (status === STATUS_OPEN) {
|
|
console.log(`ставка ещё open (equal price), повторяю close...`);
|
|
await sleep(2000);
|
|
continue;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
async function getStatus(program: Program<SmartUpdown>, id: any): Promise<number> {
|
|
const bet = await program.account.bet.fetchNullable(betPda(program.programId, id));
|
|
return bet ? bet.status.toNumber() : -1;
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const cfg = loadConfig();
|
|
const admin = loadKeypair(cfg.adminKeypairPath);
|
|
const connection = makeConnection(cfg.rpcUrl);
|
|
const program = makeProgram(connection, admin);
|
|
|
|
console.log(`RPC: ${cfg.rpcUrl}`);
|
|
console.log(`Admin/relayer: ${admin.publicKey.toBase58()}`);
|
|
console.log(`Token mint: ${cfg.tokenMint.toBase58()}`);
|
|
|
|
// Decimals фантиков (см. state.rs = 6). Только для человекочитаемых логов.
|
|
let decimals = 6;
|
|
try {
|
|
decimals = (await getMint(connection, cfg.tokenMint)).decimals;
|
|
} catch {
|
|
// mint ещё не создан (плейсхолдер) — дефолт из ТЗ контракта
|
|
}
|
|
|
|
const global = await readGlobal(program, program.programId, cfg.tokenMint);
|
|
if (!global) {
|
|
if (!process.argv.includes("--init")) {
|
|
throw new Error(
|
|
"Контракт не инициализирован. Запусти с флагом --init (или init+run)."
|
|
);
|
|
}
|
|
await initialize(program, {
|
|
minAmount: cfg.minAmount,
|
|
maxAmount: cfg.maxAmount,
|
|
multiplierBps: cfg.multiplierBps,
|
|
expirySeconds: cfg.expirySeconds,
|
|
}, admin);
|
|
console.log("Контракт инициализирован.");
|
|
} else {
|
|
console.log(
|
|
`Global: counter=${global.counter} vault=${humanize(global.vaultBalance, decimals)} fant ` +
|
|
`expiry=${global.expirySeconds}s paused=${global.paused}`
|
|
);
|
|
}
|
|
|
|
if (cfg.allowAirdrop) {
|
|
await ensureVaultFunded(connection, program, admin, cfg);
|
|
}
|
|
|
|
const loops = process.argv.includes("--once") ? 1 : Infinity;
|
|
let n = 0;
|
|
|
|
while (n < loops) {
|
|
n++;
|
|
const req = nextBetRequest(cfg.minAmount, cfg.maxAmount);
|
|
await fundBettor(connection, req.bettor.publicKey);
|
|
// Беттору нужен токен-аккаунт (ATA) — без него create_bet не пройдёт.
|
|
// На 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, {
|
|
side: req.side,
|
|
amount: req.amount,
|
|
entryPrice,
|
|
});
|
|
|
|
console.log(
|
|
`ставка ${req.side === SIDE_UP ? "UP" : "DOWN"} на ${humanize(req.amount, decimals)} fant, жду ${cfg.expirySeconds}s...`
|
|
);
|
|
await sleep((cfg.expirySeconds + 1) * 1000);
|
|
|
|
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);
|
|
|
|
const outcome = await program.account.bet.fetchNullable(betPda(program.programId, id));
|
|
console.log(
|
|
outcome!.status.toNumber() === 1
|
|
? `>> результат #${id}: ПОЛЬЗОВАТЕЛЬ ВЫИГРАЛ (payout 1.3x)`
|
|
: `>> результат #${id}: ВЫИГРАЛ ДОМ (user lost)`
|
|
);
|
|
}
|
|
console.log("Готово.");
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|