feat: пульт на HTTP-шлюз — ставки/баланс без Phantom (gwBase, пресеты кошельков)
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# TASK — Усилить диагностику при connect() к Phantom
|
||||
|
||||
Репозиторий: `test-front`, файл `index.html`.
|
||||
|
||||
## Задача
|
||||
При клике «Подключить Phantom» (`connect()`, стока 103-108) Phantom бросает
|
||||
generic «Unexpected error». Нужно вытащить **настоящую** ошибку, чтобы понять
|
||||
причину (сеть/кластер/конфиг Phantom).
|
||||
|
||||
## Что сделать (только `index.html`, `connect()` и `getProvider`)
|
||||
|
||||
1. ОБЕРНУТЬ точку ошибки: вместо `log.err('connect: '+e.message)` логировать БОЛЬШЕ:
|
||||
- `e.message`
|
||||
- `JSON.stringify(e)` (если сериализуется) — обернуть в try
|
||||
- `e.toString()`
|
||||
- стек: `e.stack` если есть
|
||||
Разделить в лог как несколько строк / один блок.
|
||||
|
||||
2. В `getProvider`: вернуть не только `window.solana`, но и **версию Phantom** и
|
||||
**доступные методы** (чтобы понять, что Phantom умеет):
|
||||
- если `w.solana`: залогировать `typeof w.solana.connect`, `typeof w.solana.signTransaction`,
|
||||
`typeof w.solana.isConnected`, `w.solana` перечислить ключи (только названия).
|
||||
Это не сломает — просто диагностический вывод в лог при подключении.
|
||||
|
||||
3. НЕ менять логику (не «чинить») — только ЛОГИРОВАТЬ детали, чтобы прислать
|
||||
пользователю полную картину. Это диагностика.
|
||||
|
||||
## Гейт
|
||||
- JS-синтаксис валиден (`node --check` извлечённого script).
|
||||
- Страница открывается.
|
||||
|
||||
## НЕ делать
|
||||
- Не менять ставку/балансы/swap.
|
||||
- Не виполнять Phantom насильно.
|
||||
- Не коммитить.
|
||||
|
||||
## Результат
|
||||
Пользователь обновит страницу, нажмёт «Подключить», в логе увидит ПОЛНУЮ ошибку
|
||||
(не только «Unexpected error»), скопирует и пришлёт нам. Из неё станет ясна причина.
|
||||
@@ -0,0 +1,34 @@
|
||||
# TASK — Перевести тестовый фронт (test-front) на HTTP-шлюз (ставки/баланс без Phantom)
|
||||
|
||||
## Контекст
|
||||
`test-front/index.html` — статичный пульт на `testfront.binom.pw` (осн.) / `game2.binom.pw`.
|
||||
Сейчас он требует Phantom для баланса и ставки. Но поднят **HTTP-шлюз** (релейер, localnet):
|
||||
- на `http://127.0.0.1:8895` (через прокси наружу будет виден как `GW_BASE`).
|
||||
- Эндпоинты:
|
||||
- `GET /state` → `{counter,minAmount,maxAmount,multiplierBps,expirySeconds,paused,admin,relayer,vaultBalanceHuman,minAmountHuman,maxAmountHuman}`
|
||||
- `GET /balance/{address}` → `{sol,solLamports,tokenUnits,tokenHuman}`
|
||||
- `POST /bet {side, amountUnits, address?}` → `{tx,id,bettor,entryPrice}` (без address — генерит и регистрирует кошелёк сам; но пульт будет передавать введённый/пресет-адрес)
|
||||
- `POST /close {id}` → `{tx,id,exitPrice,status,statusName,bettor}` (statusName: payout_done/house_won/open)
|
||||
|
||||
## Задача (только `test-front/index.html`)
|
||||
Сделать пульт работающим БЕЗ Phantom — все операции через шлюз:
|
||||
|
||||
1. **Добавить конфиг-поле «Шлюз (API)»** — базовый URL шлюза (default пустой → фронт сам выведет, если пусто). Назвать `gwBase`.
|
||||
2. **Баланс**: введите адрес в поле → кнопка → `GET {gw}/balance/{address}` → показать фантики (`tokenHuman`) и SOL (`sol`). Убрать требование Phantom для балances.
|
||||
3. **Ставка**: поля side (UP/DOWN), amountHuman; адрес (введённый или пресет). Кнопка → `POST {gw}/bet {side, amountUnits, address}` → показать `{id, bettor, entryPrice}` в лог. Ставить вверх/вниз.
|
||||
4. **Закрыть**: кнопка (id из последней ставки или ввод) → `POST {gw}/close {id}` → показать `statusName` (выиграл/проиграл/открыт).
|
||||
5. **Обновить state**: показывать min/max/expiry/vault с `/state`.
|
||||
6. **CORS**: шлюз уже отдаёт `Access-Control-Allow-Origin: *` — fetch с любого домена пройдёт. Оставим.
|
||||
7. **Пресет-кошельки**: захардкодь в поле адреса пару известных: admin (источник фантиков) — `Huha5S9ySxHC9u7RXmeQbt8GZh1VXGzfjVPGdsYV2tKK` — и/или зарегистрированный bettor из keystore (выведи его адрес при необходимости). Чтобы сразу было.
|
||||
|
||||
## Ограничения
|
||||
- НЕ менять шлюз (`smart-updown-token/relayer/gateway.ts`) — только фронт.
|
||||
- НЕ коммитить.
|
||||
- amountUnits в токен-единицах (×10^decimals=6). Пользователь вводит в фантиках (до 6 знаков) — умножить на 1e6 на фронте.
|
||||
|
||||
## Гейт
|
||||
- JS-синтаксис валиден.
|
||||
- Страница открывается; при заполненном gwBase живо показывает state, делает bet/close.
|
||||
|
||||
## Отчёт
|
||||
Что сделал, как запускать, как пользоваться (какие поля/кнопки). Если шлюз недоступен с браузера — как проксировать наружу (совет, не реализация).
|
||||
+146
-169
@@ -2,7 +2,7 @@
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Binary Rocket — техтест экономики (фантики)</title>
|
||||
<title>Binary Rocket — техтест экономики через HTTP-шлюз (фантики, без Phantom)</title>
|
||||
<style>
|
||||
body{font-family:monospace;background:#111;color:#eee;margin:20px;font-size:14px}
|
||||
h1{font-size:16px}
|
||||
@@ -10,227 +10,204 @@
|
||||
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 :: tech test (фантики)</h1>
|
||||
<h1>Binary Rocket :: техтест через HTTP-шлюз (фантики, без Phantom)</h1>
|
||||
|
||||
<fieldset>
|
||||
<legend>Конфигурация</legend>
|
||||
<legend>Конфигурация шлюза</legend>
|
||||
<div class="row">
|
||||
<label>API base</label><input id="apiBase" size="42" value="https://smart-updown.binom.pw"/>
|
||||
<!-- RPC: по умолчанию public; на localnet — http://127.0.0.1:8890 -->
|
||||
<label>RPC</label><input id="rpcUrl" size="34" value="https://rpc.solanatracker.io/public"/>
|
||||
<label>Program</label><input id="programId" size="30" value="9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf"/>
|
||||
<!-- token mint вводится вручную; на localnet mint выдаётся вручную, e.g. 8zd15Zoy1t3MDeCTT2eDa7y8jB1fBW8ynFD91ubKt2t3 -->
|
||||
<label>Token mint</label><input id="tokenMint" size="30" value=""/>
|
||||
<label>Шлюз (API)</label><input id="gwBase" size="40" value=""/>
|
||||
<span class="hint">если пусто — фронт выведет и использует свой адрес (location.origin)</span>
|
||||
</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>
|
||||
<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>
|
||||
<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>
|
||||
</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 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 src="https://cdn.jsdelivr.net/npm/@solana/web3.js@1.98.0/lib/index.iife.js"></script>
|
||||
<script>
|
||||
// ---------- tech-test front: Binary Rocket economics ----------
|
||||
// ---------- 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 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);}
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);};
|
||||
const ADDR = () => document.getElementById('addr').value.trim();
|
||||
|
||||
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);}
|
||||
// gwBase: если пусто — фронт выводит и использует свой адрес (location.origin)
|
||||
function gwBase(){
|
||||
const v = document.getElementById('gwBase').value.trim().replace(/\/+$/,'');
|
||||
return v || location.origin;
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
// фантики (до 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;
|
||||
}
|
||||
|
||||
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);}
|
||||
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 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);}
|
||||
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 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));
|
||||
const minAmount = st.minAmountToken ?? st.minAmountLamports;
|
||||
if(minAmount != null && amt < minAmount) log.warn?.('сумма ниже min — контракт отклонит');
|
||||
// 2) build create_bet instruction (token: 8 аккаунтов строго по порядку IDL)
|
||||
const vaultPda=web3.PublicKey.findProgramAddressSync([new TextEncoder().encode('global'), new TextEncoder().encode('vault')], c.programId)[0];
|
||||
const vaultAta=web3.PublicKey.findProgramAddressSync([vaultPda.toBuffer(), web3.TokenProgram.programId.toBuffer(), ctx.mint.toBuffer()], web3.ASSOCIATED_TOKEN_PROGRAM_ID)[0];
|
||||
const bettorAta=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}, // 1 global
|
||||
{pubkey:ctx.betPda(st.nextBetId),isSigner:false,isWritable:true}, // 2 bet
|
||||
{pubkey:new web3.PublicKey('SysvarC1ock11111111111111111111111111111111'),isSigner:false,isWritable:false}, // 3 clock
|
||||
{pubkey:new web3.PublicKey('11111111111111111111111111111111'),isSigner:false,isWritable:false}, // 4 system_program
|
||||
{pubkey:bettorAta,isSigner:false,isWritable:true}, // 5 bettor_token
|
||||
{pubkey:vaultAta,isSigner:false,isWritable:true}, // 6 vault_token
|
||||
{pubkey:web3.TokenProgram.programId,isSigner:false,isWritable:false}, // 7 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);}
|
||||
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>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user