<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>dryrun ยท gate</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: #0b0e1a; color: #e2e8f0; font-family: monospace; font-size: 14px; padding: 24px; }
h1 { color: #7dd3fc; margin-bottom: 4px; font-size: 1.1em; letter-spacing: 0.05em; }
#addr { color: #475569; font-size: 0.82em; margin-bottom: 20px; min-height: 1.2em; }
#list { display: flex; flex-direction: column; gap: 12px; }
.card { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 14px 16px; }
.hash { color: #94a3b8; font-size: 0.85em; margin-bottom: 4px; word-break: break-all; }
.from { color: #64748b; font-size: 0.78em; margin-bottom: 8px; }
.status { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.8em; font-weight: 600; margin-bottom: 8px; }
.status.pending { background: rgba(251,191,36,0.15); color: #fbbf24; }
.status.done { background: rgba(74,222,128,0.15); color: #4ade80; }
.status.failed { background: rgba(248,113,113,0.15); color: #f87171; }
.meta { color: #64748b; font-size: 0.82em; margin-bottom: 10px; }
.meta span { margin-right: 16px; }
.meta .ok { color: #4ade80; }
.meta .bad { color: #f87171; }
.actions { display: flex; gap: 8px; }
.btn { background: rgba(56,189,248,0.12); border: 1px solid rgba(56,189,248,0.3); color: #7dd3fc;
padding: 6px 16px; border-radius: 6px; cursor: pointer; font-family: monospace; font-size: 0.85em; }
.btn:hover { background: rgba(56,189,248,0.22); }
.btn.danger { background: rgba(248,113,113,0.10); border-color: rgba(248,113,113,0.3); color: #f87171; }
.btn.danger:hover { background: rgba(248,113,113,0.20); }
.empty { color: #475569; font-style: italic; }
#connect { margin-bottom: 20px; }
.traces { margin-bottom: 10px; font-size: 0.8em; display: flex; flex-direction: column; gap: 2px; }
.trace-line { color: #94a3b8; padding: 2px 0; word-break: break-all; }
.trace-line.reverted { color: #475569; text-decoration: line-through; }
.trace-line .tkey { color: #7dd3fc; margin-right: 6px; }
.trace-line .tok { color: #4ade80; }
.trace-line .tbad { color: #f87171; }
</style>
</head>
<body>
<h1>dryrun ยท gate</h1>
<div id="addr"></div>
<div id="connect"><button class="btn" onclick="connect()">Connect Wallet</button></div>
<div id="list"></div>
<script>
let token = localStorage.getItem('dryrun_token');
let address = localStorage.getItem('dryrun_address');
function authHeaders() {
return token ? { 'Authorization': 'Bearer ' + token } : {};
}
async function connect() {
if (!window.ethereum) { alert('No wallet detected.'); return; }
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
const account = accounts[0];
const chainId = parseInt(await window.ethereum.request({ method: 'eth_chainId' }), 16);
const { nonce } = await fetch('/auth/challenge').then(r => r.json());
const now = new Date();
const exp = new Date(now.getTime() + 60_000);
const message = [
`${location.host} wants you to sign in with your Ethereum account:`,
account,
'',
'Sign in to yevm-gate',
'',
`URI: ${location.origin}`,
'Version: 1',
`Chain ID: ${chainId}`,
`Nonce: ${nonce}`,
`Issued At: ${now.toISOString()}`,
`Expiration Time: ${exp.toISOString()}`,
].join('\n');
const msgHex = '0x' + Array.from(new TextEncoder().encode(message), b => b.toString(16).padStart(2, '0')).join('');
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [msgHex, account],
});
const res = await fetch('/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, signature }),
});
if (!res.ok) { alert('Auth failed: ' + (await res.json()).error); return; }
const data = await res.json();
token = data.token;
address = data.address;
localStorage.setItem('dryrun_token', token);
localStorage.setItem('dryrun_address', address);
updateHeader();
load();
}
function updateHeader() {
const el = document.getElementById('addr');
const connect = document.getElementById('connect');
if (address) {
el.textContent = address;
connect.style.display = 'none';
} else {
el.textContent = '';
connect.style.display = '';
}
}
async function load() {
if (!token) {
document.getElementById('list').innerHTML = '<p class="empty">Connect wallet to view transactions.</p>';
return;
}
const res = await fetch('/api/txs', { headers: authHeaders() });
if (res.status === 401) {
token = null; address = null;
localStorage.removeItem('dryrun_token');
localStorage.removeItem('dryrun_address');
updateHeader();
load();
return;
}
const list = await res.json();
const el = document.getElementById('list');
if (!list.length) {
el.innerHTML = '<p class="empty">No pending transactions.</p>';
return;
}
el.innerHTML = list.map(({ hash, from, sim }) => {
const meta = sim.status === 'done'
? `<span>gas: ${sim.gasUsed}</span><span class="${sim.success ? 'ok' : 'bad'}">${sim.success ? 'success' : 'reverted'}</span>`
: sim.status === 'failed'
? `<span class="bad">${sim.error}</span>`
: '<span>simulatingโฆ</span>';
const fromLine = from && from.toLowerCase() !== (address || '').toLowerCase()
? `<div class="from">from: ${from}</div>` : '';
const actions = sim.status !== 'pending' ? `
<div class="actions">
<button class="btn" onclick="submit('${hash}')">Submit</button>
<button class="btn danger" onclick="reject('${hash}')">Reject</button>
</div>` : '';
return `<div class="card">
<div class="hash">${hash}</div>
${fromLine}
<div class="status ${sim.status}">${sim.status.toUpperCase()}</div>
<div class="meta">${meta}</div>
${renderTraces(sim.traces)}
${actions}
</div>`;
}).join('');
}
function fmtInt(hex) {
try { return BigInt(hex).toString(); } catch { return hex; }
}
function weiToEth(wei) {
const E18 = BigInt('1000000000000000000');
const whole = wei / E18;
const frac = (wei % E18).toString().padStart(18, '0').replace(/0+$/, '');
return whole.toString() + (frac ? '.' + frac : '') + ' ETH';
}
function fmtEth(hex) {
try {
const wei = BigInt(hex);
return weiToEth(wei);
} catch { return hex; }
}
function fmtEthDiff(hexOld, hexNew) {
try {
const diff = BigInt(hexNew) - BigInt(hexOld);
if (diff === 0n) return null;
const cls = diff > 0n ? 'tok' : 'tbad';
const abs = diff < 0n ? -diff : diff;
return `<span class="${cls}">${diff > 0n ? '+' : '-'}${weiToEth(abs)}</span>`;
} catch { return null; }
}
function fmtHex(hex) {
if (typeof hex !== 'string') return hex;
const stripped = hex.replace(/^0x0*/, '');
return stripped === '' ? '0' : '0x' + stripped;
}
function renderTrace(t) {
const e = t.event;
const cls = t.reverted ? 'trace-line reverted' : 'trace-line';
let body = '';
if (e.Move) {
const [from, to, val] = e.Move;
body = `<span class="tkey">move</span>${from} โ ${to}: <span class="tok">${fmtEth(val)}</span>`;
} else if (e.Fee) {
const [sender, , , gas] = e.Fee;
body = `<span class="tkey">fee</span>${sender}: ${gas} gas`;
} else if (e.Put) {
const [target, next] = e.Put;
if (target.Value) {
const diff = fmtEthDiff(target.Value.val, next);
body = `<span class="tkey">balance</span>${target.Value.acc}: ${fmtEth(target.Value.val)} โ ${fmtEth(next)}${diff ? ' (' + diff + ')' : ''}`;
} else if (target.Store) {
body = `<span class="tkey">store</span>${target.Store.acc}[${fmtHex(target.Store.key)}]: ${fmtHex(target.Store.val)} โ <span class="tok">${fmtHex(next)}</span>`;
} else if (target.Nonce) {
body = `<span class="tkey">nonce</span>${target.Nonce.acc}: ${fmtInt(target.Nonce.val)} โ <span class="tok">${fmtInt(next)}</span>`;
} else {
body = `<span class="tkey">put</span>${JSON.stringify(target)}`;
}
} else if (e.Log) {
const [topics] = e.Log;
body = `<span class="tkey">log</span>${topics.join(' ')}`;
} else if (e.Create) {
body = `<span class="tkey">create</span><span class="tok">${e.Create}</span>`;
} else if (e.Delete) {
body = `<span class="tkey">delete</span><span class="tbad">${e.Delete}</span>`;
} else {
return '';
}
return `<div class="${cls}">${body}</div>`;
}
function renderTraces(traces) {
if (!traces || !traces.length) return '';
const lines = traces.map(renderTrace).filter(Boolean).join('');
return lines ? `<div class="traces">${lines}</div>` : '';
}
async function submit(hash) {
await fetch('/api/txs/' + hash + '/submit', { method: 'POST', headers: authHeaders() });
load();
}
async function reject(hash) {
await fetch('/api/txs/' + hash + '/reject', { method: 'POST', headers: authHeaders() });
load();
}
updateHeader();
load();
setInterval(load, 2000);
</script>
</body>
</html>