214 lines
10 KiB
HTML
214 lines
10 KiB
HTML
<!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>
|