From bf9d05f2f26ee0314ea3d4be2c2a3107a45a7c03 Mon Sep 17 00:00:00 2001 From: Caffeine Date: Thu, 10 Sep 2026 05:23:50 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20/faucet/{address}=20=D0=B2=20=D1=88?= =?UTF-8?q?=D0=BB=D1=8E=D0=B7=20=E2=80=94=20=D0=BD=D0=B0=D0=BB=D0=B8=D0=B2?= =?UTF-8?q?=20SOL+=D1=84=D0=B0=D0=BD=D1=82=D0=B8=D0=BA=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BF=D1=80=D0=BE=D0=B8=D0=B7=D0=B2=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=8B=D0=B9=20=D0=B0=D0=B4=D1=80=D0=B5=D1=81=20(lo?= =?UTF-8?q?calnet)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- relayer/src/gateway.ts | 76 +++++++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/relayer/src/gateway.ts b/relayer/src/gateway.ts index d1dc1c7..ad9ec0d 100644 --- a/relayer/src/gateway.ts +++ b/relayer/src/gateway.ts @@ -15,6 +15,9 @@ * Ответ: {tx, id, bettor, entryPrice} * POST /close {id} — close_bet по текущей цене (подпись relayer) * GET /balance/{address} — {sol, solLamports, tokenUnits, tokenHuman} + * GET /faucet/{address} — налив SOL + фантиков на произвольный адрес + * (только localnet, allowAirdrop=1; иначе 403). Ответ: + * {address, sol, solLamports, tokenUnits, tokenHuman} * * Запуск: npm run gateway (ts-node src/gateway.ts) * Env: GW_PORT (default 8895), GW_BETTOR_KEYSTORE, SMART_* (как у релейера). @@ -299,11 +302,17 @@ async function handleClose(ctx: Ctx, body: Record): Promise { - const pub = parsePubkey(address); - if (!pub) { - return { status: 400, body: { error: "address is not a valid Solana pubkey" } }; - } +/** Балансы адреса (SOL + фантики в ATA по нашему mint) — переиспользуется + * и /balance, и /faucet (реальные значения после налива). */ +async function readBalances( + ctx: Ctx, + pub: PublicKey +): Promise<{ + sol: number; + solLamports: number; + tokenUnits: string; + tokenHuman: string; +}> { const [lamports, tokenUnits] = await Promise.all([ ctx.connection.getBalance(pub), (async (): Promise => { @@ -316,14 +325,52 @@ async function handleBalance(ctx: Ctx, address: string): Promise { } })(), ]); + return { + sol: lamports / LAMPORTS_PER_SOL_NUM, + solLamports: lamports, + tokenUnits, + tokenHuman: humanize(tokenUnits, ctx.decimals), + }; +} + +async function handleBalance(ctx: Ctx, address: string): Promise { + const pub = parsePubkey(address); + if (!pub) { + return { status: 400, body: { error: "address is not a valid Solana pubkey" } }; + } + return { status: 200, body: await readBalances(ctx, pub) }; +} + +/** + * Налив фантиков/SOL на ПРОИЗВОЛЬНЫЙ адрес (Phantom и т.п.) — только localnet. + * Переиспользует готовые функции ops.ts без дублирования логики: + * - SOL (рента/комиссия) — тот же airdrop, что fundBettor; + * - фантики — mintTo в ATA адреса (до startBalance, дефолт = maxAmount), + * та же функция, которой /bet фандит ATA (ensureBettorTokenAccount). + * Ответ — адрес + реальные балансы после налива (readBalances, как в /balance). + */ +async function handleFaucet(ctx: Ctx, address: string): Promise { + if (!ctx.cfg.allowAirdrop) { + return { status: 403, body: { error: "faucet only on localnet" } }; + } + const pub = parsePubkey(address); + if (!pub) { + return { status: 400, body: { error: "address is not a valid Solana pubkey" } }; + } + // SOL под ренту/комиссию — тот же airdrop, что fundBettor. + await fundBettor(ctx.connection, pub); + // Фантики — mintTo в ATA адреса до startBalance (дефолт = maxAmount из Global). + await ensureBettorTokenAccount( + ctx.connection, + ctx.admin, + ctx.cfg.tokenMint, + pub, + BigInt(ctx.cfg.startBalance) + ); + const balances = await readBalances(ctx, pub); return { status: 200, - body: { - sol: lamports / LAMPORTS_PER_SOL_NUM, - solLamports: lamports, - tokenUnits, - tokenHuman: humanize(tokenUnits, ctx.decimals), - }, + body: { address: pub.toBase58(), ...balances }, }; } @@ -435,6 +482,9 @@ async function main(): Promise { } else if (m === "GET" && p.startsWith("/balance/")) { const address = decodeURIComponent(p.slice("/balance/".length)); result = await handleBalance(ctx, address); + } else if (m === "GET" && p.startsWith("/faucet/")) { + const address = decodeURIComponent(p.slice("/faucet/".length)); + result = await handleFaucet(ctx, address); } else { result = { status: 404, body: { error: "not found" } }; } @@ -461,7 +511,9 @@ async function main(): Promise { server.listen(port, "0.0.0.0", () => { console.log(`gateway: listening on 0.0.0.0:${port} (RPC ${cfg.rpcUrl}, mint ${cfg.tokenMint.toBase58()})`); - console.log(`gateway: endpoints: GET /state, POST /bet, POST /close, GET /balance/{address}`); + console.log( + `gateway: endpoints: GET /state, POST /bet, POST /close, GET /balance/{address}, GET /faucet/{address}` + ); }); }