Files
test-front/index.html
T

214 lines
10 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 — техтест экономики через HTTP-шлюз (фантики, без Phantom)</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}
input::placeholder{color:#555}
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}
.hint{color:#666;font-size:12px}
</style>
</head>
<body>
<h1>Binary Rocket :: техтест через HTTP-шлюз (фантики, без Phantom)</h1>
<fieldset>
<legend>Конфигурация шлюза</legend>
<div class="row">
<label>Шлюз (API)</label><input id="gwBase" size="40" value=""/>
<span class="hint">если пусто — фронт выведет и использует свой адрес (location.origin)</span>
</div>
<div class="row">
<label>Адрес</label><input id="addr" size="46" value="6Q1koEYH3R2CdV13mwcPQTXbDjQhbyzY6N82f55o13s6"/>
<button onclick="fillPreset('admin')">admin</button>
<button onclick="fillPreset('bettor-1')">bettor-1</button>
<button onclick="fillPreset('bettor-2')">bettor-2</button>
</div>
<div class="hint">admin — источник фантиков; bettor-N — custodial-кошельки, зарегистрированные в keystore шлюза</div>
</fieldset>
<fieldset>
<legend>Состояние / баланс</legend>
<div class="row">
<button onclick="gwState()">Обновить state</button>
<button onclick="gwBalance()">Проверить баланс</button>
<button onclick="copyLog()">Копировать лог</button>
<button onclick="log.clear()">Очистить лог</button>
</div>
<div class="row bal">
<span>Фантики: <span id="balTok">—</span></span>
<span style="margin-left:18px">SOL: <span id="balSol">—</span></span>
</div>
<div class="hint">баланс показывается для адреса из поля «Адрес»</div>
<div id="stateInfo" class="info" style="margin-top:6px">Состояние ещё не прочитано.</div>
</fieldset>
<fieldset>
<legend>Ставка (POST /bet, без Phantom)</legend>
<div class="row">
<label>Сумма (фантики)</label><input id="betAmt" size="14" value="1000"/>
<label>Сторона</label>
<select id="betSide">
<option value="UP">UP (вверх)</option>
<option value="DOWN">DOWN (вниз)</option>
</select>
<button onclick="gwBet()">Сделать ставку</button>
</div>
<div class="hint">сумма ×1e6 → units (decimals=6); адрес берётся из поля «Адрес» (пустой адрес — шлюз сам сгенерит и зарегистрирует кошелёк)</div>
<div class="hint" id="betLimits">лимиты ставок неизвестны (обновите state)</div>
</fieldset>
<fieldset>
<legend>Закрытие (POST /close)</legend>
<div class="row">
<label>id ставки</label><input id="closeId" size="12" value=""/>
<button onclick="gwClose()">Закрыть ставку</button>
<span class="hint">id заполнится после ставки, либо введите вручную</span>
</div>
</fieldset>
<fieldset>
<legend>Лог</legend>
<div id="log"></div>
</fieldset>
<script>
// ---------- tech-test front: Binary Rocket economics via HTTP gateway (без Phantom) ----------
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 PRESETS = {
'admin': 'Huha5S9ySxHC9u7RXmeQbt8GZh1VXGzfjVPGdsYV2tKK',
'bettor-1': '6Q1koEYH3R2CdV13mwcPQTXbDjQhbyzY6N82f55o13s6',
'bettor-2': '6v2HXsowRnwrX82K7kCAKzkCpFA4gLqk1DAmRQvA96WA'
};
function fillPreset(name){
document.getElementById('addr').value = PRESETS[name] || '';
log.info('Пресет "'+name+'" → '+document.getElementById('addr').value);
}
const ADDR = () => document.getElementById('addr').value.trim();
// gwBase: если пусто — фронт выводит и использует свой адрес (location.origin)
function gwBase(){
const v = document.getElementById('gwBase').value.trim().replace(/\/+$/,'');
return v || location.origin;
}
async function gwFetch(path, body){
const url = gwBase() + path;
const opts = { method: body ? 'POST' : 'GET', headers: {'Content-Type':'application/json'} };
if(body) opts.body = JSON.stringify(body);
const r = await fetch(url, opts);
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||r.statusText));
return j;
}
// фантики (до 6 знаков) → токен-юниты (×1e6), через строку — без float-погрешности
function humanToUnits(human, dec){
dec = dec || 6;
const s = String(human==null?'':human).trim().replace(',', '.');
if(s===''||!/^\d*(\.\d*)?$/.test(s)) return null;
const dot = s.indexOf('.');
const intPart = dot===-1 ? s : s.slice(0,dot);
const frac = dot===-1 ? '' : s.slice(dot+1);
const digits = (intPart||'0') + frac.padEnd(dec,'0').slice(0,dec);
const clean = digits.replace(/^0+(?=\d)/,'');
return /^\d+$/.test(clean) ? BigInt(clean) : null;
}
let lastState = null;
async function gwState(){
try{
const j = await gwFetch('/state');
lastState = j;
document.getElementById('stateInfo').textContent =
'counter='+j.counter+
' | min='+j.minAmountHuman+' max='+j.maxAmountHuman+' фантик'+
' | x='+(Number(j.multiplierBps)/100)+
' | expiry='+j.expirySeconds+'s'+
' | paused='+j.paused+
' | vault='+j.vaultBalanceHuman+' фантик'+
' | admin='+j.admin+
' | relayer='+j.relayer;
document.getElementById('betLimits').textContent = 'лимиты шлюза: '+j.minAmountHuman+'–'+j.maxAmountHuman+' фантик';
log.ok('State: counter='+j.counter+', vault='+j.vaultBalanceHuman+' фантик, min='+j.minAmountHuman+', max='+j.maxAmountHuman);
}catch(e){ log.err('state: '+e.message); }
}
async function gwBalance(){
try{
const a = ADDR();
if(!a){ log.err('balance: введите адрес в поле «Адрес»'); return; }
const j = await gwFetch('/balance/'+encodeURIComponent(a));
document.getElementById('balTok').textContent = j.tokenHuman;
document.getElementById('balSol').textContent = Number(j.sol).toFixed(6)+' SOL';
log.ok('Баланс '+a+': фантики='+j.tokenHuman+', SOL='+j.sol+', tokenUnits='+j.tokenUnits);
}catch(e){ log.err('balance: '+e.message); }
}
let lastBetId = '';
async function gwBet(){
try{
const side = document.getElementById('betSide').value;
const units = humanToUnits(document.getElementById('betAmt').value);
if(units===null){ log.err('bet: некорректная сумма (фантики, до 6 знаков, напр 1000 или 1.5)'); return; }
if(units===0n){ log.err('bet: сумма должна быть больше нуля'); return; }
if(lastState){
const min = BigInt(lastState.minAmount), max = BigInt(lastState.maxAmount);
if(units<min||units>max) log.info('внимание: сумма вне лимитов '+lastState.minAmountHuman+'–'+lastState.maxAmountHuman+' (шлюз отклонит)');
}
const address = ADDR();
log.info('POST /bet side='+side+', amountUnits='+units.toString()+(address?' (address='+address+')':' (адрес пуст — шлюз сгенерит новый кошелёк)'));
const j = await gwFetch('/bet', {side:side, amountUnits:units.toString(), address:address});
lastBetId = String(j.id);
document.getElementById('closeId').value = lastBetId;
log.ok('Ставка принята: id='+j.id+', bettor='+j.bettor+', entryPrice='+j.entryPrice+(j.tx?' tx='+j.tx:''));
if(!address) log.info('кошелёк шлюза: '+j.bettor+' (скопируйте в поле «Адрес» для дальнейших ставок)');
}catch(e){ log.err('bet: '+e.message); }
}
async function gwClose(){
try{
const id = document.getElementById('closeId').value.trim();
if(!id){ log.err('close: нет id ставки (сначала сделайте ставку или введите id вручную)'); return; }
const j = await gwFetch('/close', {id:id});
const label = {payout_done:'ВЫИГРАЛ (payout_done)', house_won:'ПРОИГРАЛ (house_won)', open:'открыт (open)'}[j.statusName] || ('status='+j.statusName);
const detail = 'exitPrice='+j.exitPrice+(j.note?' — '+j.note:'');
if(j.statusName==='payout_done') log.ok('Закрыт #'+id+': '+label+', '+detail);
else if(j.statusName==='house_won') log.err('Закрыт #'+id+': '+label+', '+detail);
else log.info('Закрыт #'+id+': '+label+', '+detail);
}catch(e){ log.err('close: '+e.message); }
}
function copyLog(){ const t=document.getElementById('log').innerText; navigator.clipboard?.writeText(t).then(()=>log.ok('лог скопирован')).catch(()=>{}); }
(function init(){
const el = document.getElementById('gwBase');
if(!el.value.trim()){
el.placeholder = location.origin;
log.info('Шлюз (gwBase) не указан — используется адрес страницы: '+location.origin);
}
gwState();
gwBalance();
})();
</script>
</body>
</html>