deviant/scripts/eth-sweep.html
walerikus 35076606c7 chore: checkpoint in-flight work across shell, contracts, and docs
Snapshot of the shared working tree's outstanding changes (multiple
concurrent workstreams), excluding generated runtime state files
(.football-tote-state.json, .tote-platform-state.json, .usdm-historical-chart.html).

Spans: wallet shell (connect/, keystore MultiKeyStore+vault, screens,
currency picker, sign-message), verify artifacts, new Potidaea/DRA/tote
contracts + regtest suites, cross-chain/T6 + WalletConnect Parameters,
a telegram-orderbook subproject, design mockups, and docs. See
shells/app/WALLET-COORDINATION.md for the per-file ownership split.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 02:02:05 +02:00

398 lines
15 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="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ETH Sweep</title>
<script src="https://cdn.jsdelivr.net/npm/ethers@6/dist/ethers.umd.min.js"></script>
<style>
:root {
--bg: #0d0d12;
--surface: #161621;
--border: #2a2a3d;
--text: #e2e2f0;
--muted: #888899;
--accent: #7c5cfc;
--ok: #4caf8a;
--err: #f05a4a;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font: 14px/1.6 'SF Mono', 'Consolas', monospace;
min-height: 100vh;
display: flex;
justify-content: center;
padding: 32px 16px 64px;
}
.card { width: 100%; max-width: 560px; }
h1 { font-size: 20px; margin-bottom: 4px; }
h1 span { color: var(--accent); }
.subtitle { color: var(--muted); font-size: 12px; margin-bottom: 28px; }
h2 {
font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em;
color: var(--muted); margin: 24px 0 8px;
}
label { display: block; font-size: 12px; color: var(--muted); margin-bottom: 4px; }
input {
width: 100%;
background: var(--surface);
border: 1px solid var(--border);
color: var(--text);
padding: 10px 12px;
border-radius: 6px;
font: inherit;
font-size: 13px;
outline: none;
transition: border-color 0.15s;
}
input:focus { border-color: var(--accent); }
.row { display: flex; gap: 8px; align-items: stretch; }
.row input { flex: 1; min-width: 0; }
.icon-btn {
flex-shrink: 0;
background: var(--surface);
border: 1px solid var(--border);
color: var(--muted);
padding: 0 12px;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
}
.icon-btn:hover { border-color: var(--accent); color: var(--text); }
.seg {
display: flex;
border: 1px solid var(--border);
border-radius: 6px;
overflow: hidden;
}
.seg button {
flex: 1;
padding: 9px 8px;
background: var(--surface);
border: none;
border-right: 1px solid var(--border);
color: var(--muted);
cursor: pointer;
font: inherit;
font-size: 13px;
transition: background 0.15s, color 0.15s;
}
.seg button:last-child { border-right: none; }
.seg button.active { background: var(--accent); color: #fff; }
.seg button:not(.active):hover { background: rgba(124,92,252,0.12); color: var(--text); }
.plan {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px 16px;
margin-top: 16px;
}
.plan-row { display: flex; justify-content: space-between; padding: 4px 0; font-size: 13px; }
.plan-row .l { color: var(--muted); }
.plan-row.total { border-top: 1px solid var(--border); margin-top: 8px; padding-top: 12px; font-weight: 600; }
.plan-dest { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border); font-size: 12px; color: var(--muted); word-break: break-all; }
.plan-dest span { color: var(--text); }
.btn {
display: block; width: 100%; padding: 12px; border: none; border-radius: 6px;
font: inherit; font-size: 14px; cursor: pointer; margin-top: 12px; transition: opacity 0.15s;
}
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.btn:not(:disabled):hover { opacity: 0.88; }
.btn-primary { background: var(--accent); color: #fff; }
.btn-secondary { background: var(--surface); color: var(--text); border: 1px solid var(--border); }
.result {
margin-top: 16px; padding: 12px 16px; border-radius: 6px; border: 1px solid;
font-size: 13px; line-height: 1.7; word-break: break-all;
}
.result.ok { border-color: var(--ok); background: rgba(76,175,138,0.08); color: var(--ok); }
.result.err { border-color: var(--err); background: rgba(240,90,74,0.08); color: var(--err); }
.result a { color: inherit; }
.result code { display: block; font: inherit; opacity: 0.85; margin: 4px 0; }
.derived { font-size: 12px; color: var(--muted); margin-top: 6px; word-break: break-all; }
.derived .addr { color: var(--ok); }
.hint { font-size: 11px; color: var(--muted); margin-top: 4px; }
.mt8 { margin-top: 8px; }
hr { border: none; border-top: 1px solid var(--border); margin: 28px 0 16px; }
.hidden { display: none !important; }
</style>
</head>
<body>
<div class="card">
<h1>ETH <span>Sweep</span></h1>
<p class="subtitle">Runs entirely in your browser — private keys are never transmitted.</p>
<!-- Network -->
<h2>Network</h2>
<div class="seg" id="netSeg">
<button class="active" data-net="mainnet">Mainnet</button>
<button data-net="sepolia">Sepolia</button>
<button data-net="custom">Custom RPC</button>
</div>
<div id="customRpcWrap" class="mt8 hidden">
<label>RPC URL</label>
<input id="customRpc" type="url" placeholder="https://rpc.example.com" />
</div>
<!-- Source wallet -->
<h2>Source wallet</h2>
<div class="seg" id="modeSeg">
<button class="active" data-mode="key">Private key</button>
<button data-mode="seed">Seed phrase</button>
</div>
<div id="keySection" class="mt8">
<label>Private key (hex)</label>
<div class="row">
<input id="privateKey" type="password" placeholder="0x… or 64 hex chars" autocomplete="off" spellcheck="false" />
<button class="icon-btn" id="revealKey" aria-label="Show/hide">👁</button>
</div>
</div>
<div id="seedSection" class="mt8 hidden">
<label>Seed phrase (BIP-39)</label>
<div class="row">
<input id="mnemonic" type="password" placeholder="word1 word2 … word12" autocomplete="off" spellcheck="false" />
<button class="icon-btn" id="revealSeed" aria-label="Show/hide">👁</button>
</div>
<div class="mt8">
<label>Derivation path</label>
<input id="derivPath" type="text" value="m/44'/60'/0'/0/0" />
<p class="hint">Default = MetaMask / Ledger Live. Change index at the end for other accounts (…/0/1, …/0/2 …).</p>
</div>
</div>
<div id="derivedAddr" class="derived hidden"></div>
<!-- Destination -->
<h2>Destination</h2>
<label>Address</label>
<input id="destination" type="text" placeholder="0x…" autocomplete="off" spellcheck="false" />
<!-- Actions -->
<button class="btn btn-secondary" id="checkBtn">Check balance</button>
<div id="planWrap" class="hidden"></div>
<button class="btn btn-primary hidden" id="sweepBtn">Sweep</button>
<div id="result" class="hidden result"></div>
<hr />
<p class="hint">Only plain ETH is swept. ERC-20 tokens require a separate transfer per contract — not yet supported here.</p>
</div>
<script>
(function () {
'use strict';
const NETS = {
mainnet: { rpc: 'https://eth.llamarpc.com', name: 'Mainnet', explorer: 'https://etherscan.io/tx/' },
sepolia: { rpc: 'https://rpc2.sepolia.org', name: 'Sepolia', explorer: 'https://sepolia.etherscan.io/tx/' },
custom: { rpc: null, name: 'Custom', explorer: null },
};
let net = 'mainnet';
let mode = 'key';
let cachedPlan = null;
// ── Segment controls ─────────────────────────────────────────────────────
document.querySelectorAll('#netSeg button').forEach(btn =>
btn.addEventListener('click', () => {
activate('#netSeg', btn);
net = btn.dataset.net;
toggle('customRpcWrap', net === 'custom');
clearPlan();
})
);
document.querySelectorAll('#modeSeg button').forEach(btn =>
btn.addEventListener('click', () => {
activate('#modeSeg', btn);
mode = btn.dataset.mode;
toggle('keySection', mode === 'key');
toggle('seedSection', mode === 'seed');
hide('derivedAddr');
clearPlan();
})
);
function activate(sel, active) {
document.querySelectorAll(sel + ' button').forEach(b => b.classList.remove('active'));
active.classList.add('active');
}
// ── Reveal toggles ───────────────────────────────────────────────────────
['revealKey:privateKey', 'revealSeed:mnemonic'].forEach(pair => {
const [btnId, inputId] = pair.split(':');
document.getElementById(btnId).addEventListener('click', () => {
const inp = document.getElementById(inputId);
inp.type = inp.type === 'password' ? 'text' : 'password';
document.getElementById(btnId).textContent = inp.type === 'password' ? '👁' : '🙈';
});
});
// ── Live address preview ─────────────────────────────────────────────────
['privateKey', 'mnemonic', 'derivPath'].forEach(id =>
document.getElementById(id).addEventListener('input', async () => {
const div = document.getElementById('derivedAddr');
try {
const w = await buildWallet(null);
div.innerHTML = 'Derived address: <span class="addr">' + w.address + '</span>';
div.classList.remove('hidden');
} catch {
div.classList.add('hidden');
}
})
);
// ── Wallet / provider construction ───────────────────────────────────────
function getProvider() {
const rpc = net === 'custom'
? document.getElementById('customRpc').value.trim()
: NETS[net].rpc;
if (!rpc) throw new Error('Enter a custom RPC URL');
return new ethers.JsonRpcProvider(rpc);
}
async function buildWallet(provider) {
if (mode === 'key') {
let raw = document.getElementById('privateKey').value.trim();
if (!raw) throw new Error('Enter a private key');
if (!raw.startsWith('0x')) raw = '0x' + raw;
const w = new ethers.Wallet(raw);
return provider ? w.connect(provider) : w;
} else {
const phrase = document.getElementById('mnemonic').value.trim();
if (!phrase) throw new Error('Enter a seed phrase');
const path = document.getElementById('derivPath').value.trim() || "m/44'/60'/0'/0/0";
// HDNodeWallet.fromPhrase(phrase, password, path) derives the full path.
const w = ethers.HDNodeWallet.fromPhrase(phrase, '', path);
return provider ? w.connect(provider) : w;
}
}
// ── Check balance ─────────────────────────────────────────────────────────
document.getElementById('checkBtn').addEventListener('click', async () => {
clearPlan();
const btn = document.getElementById('checkBtn');
busy(btn, 'Checking…');
try {
const provider = getProvider();
const wallet = await buildWallet(provider);
// Show derived address
const addrDiv = document.getElementById('derivedAddr');
addrDiv.innerHTML = 'Derived address: <span class="addr">' + wallet.address + '</span>';
addrDiv.classList.remove('hidden');
// Parallel fetch: balance + fee data
const [balance, feeData] = await Promise.all([
provider.getBalance(wallet.address),
provider.getFeeData(),
]);
if (balance === 0n) { showErr('No ETH at ' + wallet.address); return; }
const dest = document.getElementById('destination').value.trim();
if (!dest) { showErr('Enter a destination address'); return; }
if (!ethers.isAddress(dest)) { showErr('Invalid destination: ' + dest); return; }
if (dest.toLowerCase() === wallet.address.toLowerCase()) {
showErr('Source and destination are the same address'); return;
}
// Gas: simple ETH transfer always uses exactly 21 000 gas units.
const GAS = 21_000n;
const isEip1559 = !!feeData.maxFeePerGas;
const gasCost = GAS * (isEip1559 ? feeData.maxFeePerGas : feeData.gasPrice);
const txExtras = isEip1559
? { maxFeePerGas: feeData.maxFeePerGas, maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? 1_000_000_000n }
: { gasPrice: feeData.gasPrice };
const netAmt = balance - gasCost;
if (netAmt <= 0n) {
showErr('Balance (' + fmt(balance) + ') is less than gas estimate (' + fmt(gasCost) + ')');
return;
}
cachedPlan = { wallet, balance, gasCost, netAmt, txExtras, dest, GAS };
document.getElementById('planWrap').innerHTML =
'<div class="plan">' +
'<div class="plan-row"><span class="l">Balance</span><span>' + fmt(balance) + '</span></div>' +
'<div class="plan-row"><span class="l">Gas (21 000 units)</span><span>−' + fmt(gasCost) + '</span></div>' +
'<div class="plan-row total"><span class="l">Net sweep</span><span>' + fmt(netAmt) + '</span></div>' +
'<div class="plan-dest">→ <span>' + dest + '</span></div>' +
'</div>';
show('planWrap');
show('sweepBtn');
} catch (e) {
showErr(e.message || String(e));
} finally {
idle(btn, 'Check balance');
}
});
// ── Sweep ─────────────────────────────────────────────────────────────────
document.getElementById('sweepBtn').addEventListener('click', async () => {
if (!cachedPlan) return;
const btn = document.getElementById('sweepBtn');
busy(btn, 'Broadcasting…');
try {
const { wallet, netAmt, txExtras, dest, GAS } = cachedPlan;
const tx = await wallet.sendTransaction({ to: dest, value: netAmt, gasLimit: GAS, ...txExtras });
const netCfg = NETS[net];
const explorerLink = netCfg?.explorer
? '<br><a href="' + netCfg.explorer + tx.hash + '" target="_blank" rel="noopener noreferrer">View on ' +
(net === 'mainnet' ? 'Etherscan' : netCfg.name + ' Etherscan') + ' ↗</a>'
: '';
const div = document.getElementById('result');
div.className = 'result ok';
div.innerHTML = '✓ Transaction sent<br><code>' + tx.hash + '</code>' + explorerLink;
div.classList.remove('hidden');
cachedPlan = null;
hide('sweepBtn');
} catch (e) {
showErr(e.message || String(e));
} finally {
idle(btn, 'Sweep');
}
});
// ── Helpers ───────────────────────────────────────────────────────────────
function fmt(wei) { return parseFloat(ethers.formatEther(wei)).toFixed(8) + ' ETH'; }
function showErr(msg) {
const div = document.getElementById('result');
div.className = 'result err';
div.textContent = msg;
div.classList.remove('hidden');
}
function clearPlan() {
cachedPlan = null;
hide('planWrap');
hide('sweepBtn');
hide('result');
}
function show(id) { document.getElementById(id).classList.remove('hidden'); }
function hide(id) { document.getElementById(id).classList.add('hidden'); }
function toggle(id, show) { document.getElementById(id).classList.toggle('hidden', !show); }
function busy(btn, label) { btn.textContent = label; btn.disabled = true; }
function idle(btn, label) { btn.textContent = label; btn.disabled = false; }
})();
</script>
</body>
</html>