Files
test-front/index.html
T

229 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8"/>
<title>Binary Rocket — техтест экономики (фантики)</title>
<style>
body{font-family:monospace;background:#111;color:#eee;margin:20px;font-size:14px}
h1{font-size:16px}
.row{display:flex;gap:8px;flex-wrap:wrap;margin:8px 0;align-items:center}
button{background:#333;color:#fff;border:1px solid #555;padding:6px 10px;cursor:pointer;font-family:monospace}
button:disabled{opacity:.4;cursor:not-allowed}
input,select{background:#222;color:#eee;border:1px solid #555;padding:5px;font-family:monospace}
label{margin-right:2px}
#log{background:#000;border:1px solid #444;padding:10px;height:320px;overflow:auto;white-space:pre-wrap;font-size:12px;line-height:1.4}
.ok{color:#7f7} .err{color:#f77} .info{color:#88f}
fieldset{border:1px solid #444;margin:10px 0;padding:10px}
legend{font-size:13px;color:#aaa}
.bal{font-size:15px;font-weight:bold}
</style>
</head>
<body>
<h1>Binary Rocket :: tech test (фантики)</h1>
<fieldset>
<legend>Конфигурация</legend>
<div class="row">
<label>API base</label><input id="apiBase" size="42" value="https://smart-updown.binom.pw"/>
<label>RPC</label><input id="rpcUrl" size="34" value="https://rpc.solanatracker.io/public"/>
<label>Program</label><input id="programId" size="30" value="FMMDsmUKh5e4kYsofVA2jtzHGuY2kCfoh2NmMWSZr2f"/>
<label>Token mint</label><input id="tokenMint" size="30" value=""/>
</div>
<div class="row">
<button onclick="connect()">Подключить Phantom</button>
<span id="walletInfo">—</span>
<button onclick="refreshBalances()">Обновить баланс</button>
<button onclick="copyLog()">Копировать лог</button>
<button onclick="log.clear()">Очистить лог</button>
</div>
</fieldset>
<fieldset>
<legend>Валюта / балансы</legend>
<div class="row bal">
<span>Фантики: <span id="balTok">—</span></span>
<span style="margin-left:18px">SOL: <span id="balSol">—</span></span>
</div>
</fieldset>
<fieldset>
<legend>Пополнить / Вывести (swap pump.fun)</legend>
<div class="row">
<label>SOL (пополнить)</label><input id="depSol" size="10" value="0.1"/>
<button onclick="deposit()">Пополнить фантики</button>
</div>
<div class="row">
<label>Фантики (вывести)</label><input id="wdTok" size="10" value="100000"/>
<button onclick="withdraw()">Вывести в SOL</button>
</div>
</fieldset>
<fieldset>
<legend>Ставка (через контракт, валюта = фантики, исход = цена SOL)</legend>
<div class="row">
<label>Сумма (фантики)</label><input id="betAmt" size="12" value="100000"/>
<label>Сторона</label>
<select id="betSide"><option value="UP">UP (вверх)</option><option value="DOWN">DOWN (вниз)</option></select>
<button onclick="placeBet()">Сделать ставку</button>
</div>
<div id="stateInfo" class="info" style="margin-top:6px">Состояние ещё не прочитано.</div>
</fieldset>
<fieldset>
<legend>Лог</legend>
<div id="log"></div>
</fieldset>
<script src="https://cdn.jsdelivr.net/npm/@solana/web3.js@1.98.0/lib/index.iife.js"></script>
<script>
// ---------- tech-test front: Binary Rocket economics ----------
const log = (() => {
const el = () => document.getElementById('log');
const line = (cls, text) => { const e=el(); const d=document.createElement('div'); d.className=cls; d.textContent='['+new Date().toLocaleTimeString()+'] '+text; e.appendChild(d); e.scrollTop=e.scrollHeight; };
return { ok:(t)=>line('ok',t), err:(t)=>line('err',t), info:(t)=>line('info',t), clear:()=>el().textContent='' };
})();
const cfg = () => ({
apiBase: document.getElementById('apiBase').value.replace(/\/+$/,''),
rpcUrl: document.getElementById('rpcUrl').value,
programId:new web3.PublicKey(document.getElementById('programId').value),
tokenMint:document.getElementById('tokenMint').value.trim(),
});
const NATIVE_MINT='So11111111111111111111111111111111111111112';
let provider=null, pubkey=null, cache={}; // cache: programId, mint, decimals, conn, globalPda, betPda
const logUsr=(m)=>{document.getElementById('walletInfo').textContent=m;};
const getProvider=()=> new Promise((res,rej)=>{
const w=window; if(w.solana&&w.solana.isPhantom) return res(w.solana);
const dl=Date.now()+10000; const t=setInterval(()=>{ if(w.solana&&w.solana.isPhantom){clearInterval(t);res(w.solana);} else if(Date.now()>dl){clearInterval(t);rej(new Error('Phantom не найден'));} },150);
});
async function connect(){
try{ provider=await getProvider(); await provider.connect(); pubkey=provider.publicKey;
const key=pubkey?String(pubkey.toString()):''; logUsr(key? 'Подключён: '+key : 'нет ключа'); log.ok('Кошелёк подключён: '+key);
if(cfg().tokenMint) await refreshBalances();
}catch(e){log.err('connect: '+e.message);}
}
// returns {conn, mint, decimals, globalPda, betPda}
async function buildCtx(){
if(cache.conn) return cache;
const c=cfg();
cache.conn=new web3.Connection(c.rpcUrl,'confirmed');
cache.mint=new web3.PublicKey(c.tokenMint);
const acc=await cache.conn.getAccountInfo(cache.mint);
if(!acc) throw new Error('Не найден mint-аккаунт токена: '+c.tokenMint);
cache.decimals=ccna(acc);// TODO: читать из mint(метаданные/еxtensions) — для pump Token-2022 decimals в account data
if(!cache.decimals) cache.decimals=6; // pump default
cache.globalPda=web3.PublicKey.findProgramAddressSync([new TextEncoder().encode('global')], c.programId)[0];
cache.betPda=(id)=>web3.PublicKey.findProgramAddressSync([new TextEncoder().encode('bet'), leU64(id)], c.programId)[0];
return cache;
}
function ccna(acc){ // parse decimals from mint account data — SPL/Token22: u8(0..) mint_authority(32) supply(8) options(1) decimals(1)
try{ const d=acc.data.subarray(44,45).readUInt8(0); return d; }catch(e){ return 6; } }
function leU64(v){ const b=new Uint8Array(8); let n=BigInt(String(v)); for(let i=0;i<8;i++){b[i]=Number(n&BigInt(0xff)); n>>=BigInt(8);} return b; }
const aBuf=(bytes)=>{let x='';for(let i=0;i<bytes.length;i++)x+=String.fromCharCode(bytes[i]);return btoa(x);};
async function refreshBalances(){
try{ if(!pubkey){log.err('Сначала подключи кошелёк');return;}
const c=cfg(); if(!c.tokenMint){log.err('Укажи token mint');return;}
const ctx=await buildCtx();
// SOL balance
const sol=await ctx.conn.getBalance(pubkey);
document.getElementById('balSol').textContent=(sol/1e9).toFixed(6)+' SOL';
// token balance via ATA
const ata=web3.PublicKey.findProgramAddressSync([pubkey.toBuffer(), web3.TokenProgram.programId.toBuffer(), ctx.mint.toBuffer()], web3.ASSOCIATED_TOKEN_PROGRAM_ID)[0];
let tok='0';
try{ const ai=await ctx.conn.getAccountInfo(ata); if(ai){ const raw=ai.data; /* u64 at offset 64 */ const v=raw.readBigUInt64LE(64); tok=Number(v)/Math.pow(10,ctx.decimals); } }
catch(e){ log.warn?.('token bal: '+e.message); }
document.getElementById('balTok').textContent=tok.toLocaleString('ru');
log.info('Балансы обновлены: SOL='+sol+' lamports, токен='+tok);
}catch(e){log.err('refreshBalances: '+e.message);}
}
async function http(method, path, body){
const c=cfg(); const url=c.apiBase+path;
const r=await fetch(url,{method,headers:{'Content-Type':'application/json'},body:body?JSON.stringify(body):undefined});
const text=await r.text(); let j=null; try{ j=JSON.parse(text);}catch(e){}
if(!r.ok) throw new Error('HTTP '+r.status+' '+text);
return j;
}
async function swap(inputMint, outputMint, amount){ // returns base64 tx
if(!pubkey) throw new Error('no wallet');
const j=await http('POST','/api/pump/swap',{inputMint,outputMint,amount,user:String(pubkey.toString()),slippagePct:2,frontRunningProtection:false,encoding:'base64'});
return j.transaction;
}
async function signSend(txBase64){
const tx=web3.VersionedTransaction.deserialize(base64ToBytes(txBase64));
const signed=await provider.signTransaction(tx);
const c=cfg(); const sig=await c.rpc.sendRawTransaction(signed.serialize());
return sig;
}
function base64ToBytes(b64){ const bin=atob(b64); const u=new Uint8Array(bin.length); for(let i=0;i<bin.length;i++)u[i]=bin.charCodeAt(i); return u.buffer; }
async function deposit(){
try{ if(!pubkey){log.err('нет кошелька');return;} const c=cfg(); if(!c.tokenMint){log.err('нет token mint');return;}
const sol=parseFloat(document.getElementById('depSol').value); if(!(sol>0)){log.err('некорр SOL');return;}
const lamports=Math.round(sol*1e9); log.info('Пополнение: '+sol+' SOL → фантики...');
const txBase64=await swap(NATIVE_MINT, c.tokenMint, String(lamports));
const sig=await signSend(txBase64); log.ok('SWAP отправлен, sig='+sig);
await new Promise(r=>setTimeout(r,3000)); await refreshBalances();
}catch(e){log.err('deposit: '+e.message);}
}
async function withdraw(){
try{ if(!pubkey){log.err('нет кошелька');return;} const c=cfg(); if(!c.tokenMint){log.err('нет token mint');return;}
const ctx=await buildCtx();
const amt=document.getElementById('wdTok').value; const units=BigInt(amt)*BigInt(Math.pow(10,ctx.decimals));
log.info('Вывод: '+amt+' фантиков → SOL...');
const txBase64=await swap(c.tokenMint, NATIVE_MINT, String(units));
const sig=await signSend(txBase64); log.ok('SWAP отправлен, sig='+sig);
await new Promise(r=>setTimeout(r,3000)); await refreshBalances();
}catch(e){log.err('withdraw: '+e.message);}
}
let CREATE_BET_DISC=[0xc5,0x2a,0x99,0x02,0x3b,0x3f,0x8f,0xf6];
async function placeBet(){
try{ if(!pubkey){log.err('нет кошелька');return;} const c=cfg(); if(!c.tokenMint){log.err('нет token mint');return;}
// 1) state
log.info('Читаем /api/state...');
const st=await http('GET','/api/state'); document.getElementById('stateInfo').textContent=JSON.stringify(st);
const ctx=await buildCtx();
const side=document.getElementById('betSide').value==='UP'?0:1;
const amt=BigInt(document.getElementById('betAmt').value)*BigInt(Math.pow(10,ctx.decimals));
if(amt<st.minAmountLamports) log.warn?.('сумма ниже min — контракт отклонит');
// 2) build create_bet instruction (TOKEN variant — TODO: подтвердить ключи под токен-программу)
const ata=web3.PublicKey.findProgramAddressSync([pubkey.toBuffer(), web3.TokenProgram.programId.toBuffer(), ctx.mint.toBuffer()], web3.ASSOCIATED_TOKEN_PROGRAM_ID)[0];
const keys=[
{pubkey,isSigner:true,isWritable:true}, // 0 bettor
{pubkey:ctx.globalPda,isSigner:false,isWritable:true},
{pubkey:ctx.betPda(st.nextBetId),isSigner:false,isWritable:true},
{pubkey:new web3.PublicKey('SysvarC1ock11111111111111111111111111111111'),isSigner:false,isWritable:false},
{pubkey:new web3.PublicKey('11111111111111111111111111111111'),isSigner:false,isWritable:false},
// NOTE: при переводе на токен эти ключи изменятся (mint, ata ваулт, token program) — заглушка
];
const data=new Uint8Array(8+4*8); data.set(CREATE_BET_DISC,0); data.set(leU64(st.nextBetId),8); data.set(leU64(side),16); data.set(leU64(amt),24); data.set(leU64(st.entryPrice),32);
const ix=new web3.TransactionInstruction({programId:c.programId,keys,data});
const blockhash=await ctx.conn.getLatestBlockhash().then(r=>r.blockhash);
const tx=new web3.Transaction({feePayer:pubkey,recentBlockhash:blockhash}); tx.add(ix);
log.info('Подтверди ставку в Phantom (сумма '+amt+' units)...');
const signed=await provider.signTransaction(tx);
const signedB64=aBuf(signed.serialize());
log.info('POST /api/bet...');
const created=await http('POST','/api/bet',{side:side===0?'UP':'DOWN',amount:amt.toString(),entryPrice:st.entryPrice,betId:st.nextBetId,signedTx:signedB64});
log.ok('Ставка принята id='+created.id+' status='+created.status);
// 3) poll
for(let i=0;i<40;i++){ await new Promise(r=>setTimeout(r,1500));
const r=await http('GET','/api/bet/'+created.id);
if(r.status==='SETTLED'||r.status==='FAILED'){ log.ok('Раунд закрыт: '+r.status+' userWon='+r.userWon+' payout='+r.payout); await refreshBalances(); return; }
log.info('poll '+i+': '+r.status); }
log.err('poll таймаут');
}catch(e){log.err('placeBet: '+e.message);}
}
function copyLog(){ const t=document.getElementById('log').innerText; navigator.clipboard?.writeText(t).then(()=>log.ok('лог скопирован')).catch(()=>{}); }
</script>
</body>
</html>