Web3 — Transaction Confirm / Signing Sheet
A glassy, wallet-style signing request sheet that slides up to confirm a swap, send, or token approval. Shows from/to addresses, a network badge, an animated swap visual, a fee and estimated-total breakdown, and an expandable panel revealing raw calldata, nonce and gas. Risk warnings flag dangerous approvals. Confirm spins through signing into a success state with a mock tx hash and explorer link; reject, Esc, and overlay clicks dismiss it with a full focus trap.
MCP
الكود
:root {
--bg: #0a0b0f;
--surface: #13151c;
--surface-2: #1b1e27;
--elevated: #23262f;
--text: #e9ecf2;
--muted: #8a90a2;
--line: rgba(255, 255, 255, 0.08);
--line-2: rgba(255, 255, 255, 0.16);
--accent: #7c5cff;
--accent-2: #00e0c6;
--accent-glow: rgba(124, 92, 255, 0.45);
--pos: #26d07c;
--neg: #ff4d6d;
--warn: #ffb347;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--r-pill: 999px;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(1100px 700px at 12% -10%, rgba(124, 92, 255, 0.18), transparent 60%),
radial-gradient(900px 600px at 100% 0%, rgba(0, 224, 198, 0.12), transparent 55%),
var(--bg);
color: var(--text);
font-family: "Space Grotesk", system-ui, sans-serif;
line-height: 1.5;
}
.mono {
font-family: "JetBrains Mono", ui-monospace, monospace;
font-variant-numeric: tabular-nums;
letter-spacing: -0.01em;
}
.muted {
color: var(--muted);
}
button {
font-family: inherit;
cursor: pointer;
}
:where(button, a, [tabindex]):focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
border-radius: var(--r-sm);
}
/* ===== Page / launcher ===== */
.page {
min-height: 100vh;
display: grid;
place-items: center;
padding: 32px 18px 64px;
}
.page__hero {
width: 100%;
max-width: 460px;
text-align: center;
}
.brand {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--muted);
}
.brand__dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--accent-2);
box-shadow: 0 0 12px var(--accent-2);
}
.page__hero h1 {
margin: 14px 0 6px;
font-size: clamp(1.7rem, 6vw, 2.3rem);
font-weight: 700;
letter-spacing: -0.02em;
background: linear-gradient(120deg, #fff, var(--accent-2));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.page__hero > p {
margin: 0 auto 26px;
max-width: 340px;
color: var(--muted);
font-size: 0.95rem;
}
.actions {
display: grid;
gap: 12px;
text-align: left;
}
.launch {
display: flex;
align-items: center;
gap: 14px;
padding: 16px;
border: 1px solid var(--line);
border-radius: var(--r-lg);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.01));
backdrop-filter: blur(8px);
color: var(--text);
text-align: left;
transition: transform 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease;
}
.launch:hover {
transform: translateY(-2px);
border-color: var(--line-2);
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.4);
}
.launch:active {
transform: translateY(0);
}
.launch__icon {
display: grid;
place-items: center;
width: 44px;
height: 44px;
flex: none;
border-radius: 14px;
font-size: 1.15rem;
font-weight: 700;
color: #fff;
background: linear-gradient(140deg, var(--accent), #5a3df0);
box-shadow: 0 8px 22px var(--accent-glow);
}
.launch--send .launch__icon {
background: linear-gradient(140deg, var(--accent-2), #08a896);
box-shadow: 0 8px 22px rgba(0, 224, 198, 0.4);
}
.launch--approve .launch__icon {
background: linear-gradient(140deg, var(--warn), #e8893a);
box-shadow: 0 8px 22px rgba(255, 179, 71, 0.4);
}
.launch__body {
display: grid;
}
.launch__body strong {
font-weight: 600;
}
.launch__body small {
color: var(--muted);
font-size: 0.82rem;
}
/* ===== Overlay & sheet ===== */
.overlay {
position: fixed;
inset: 0;
z-index: 50;
display: flex;
align-items: flex-end;
justify-content: center;
padding: 0;
background: rgba(4, 5, 8, 0.62);
backdrop-filter: blur(6px);
opacity: 0;
transition: opacity 0.24s ease;
}
.overlay.is-open {
opacity: 1;
}
.sheet {
position: relative;
width: 100%;
max-width: 440px;
max-height: min(92vh, 760px);
overflow-y: auto;
padding: 10px 20px 20px;
border: 1px solid var(--line-2);
border-bottom: none;
border-radius: var(--r-lg) var(--r-lg) 0 0;
background:
linear-gradient(180deg, rgba(124, 92, 255, 0.1), transparent 120px),
var(--surface);
box-shadow: 0 -24px 60px rgba(0, 0, 0, 0.5);
transform: translateY(18px);
opacity: 0;
transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.24s ease;
}
@media (min-width: 560px) {
.overlay {
align-items: center;
padding: 24px;
}
.sheet {
border: 1px solid var(--line-2);
border-radius: var(--r-lg);
}
}
.overlay.is-open .sheet {
transform: translateY(0);
opacity: 1;
}
.sheet__grab {
width: 44px;
height: 5px;
margin: 0 auto 10px;
border-radius: var(--r-pill);
background: var(--line-2);
}
@media (min-width: 560px) {
.sheet__grab {
display: none;
}
}
.sheet__head {
display: flex;
align-items: flex-start;
gap: 10px;
padding-bottom: 16px;
}
.origin {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
flex: 1;
}
.origin__avatar {
display: grid;
place-items: center;
width: 36px;
height: 36px;
flex: none;
border-radius: 11px;
background: var(--elevated);
border: 1px solid var(--line);
color: var(--accent-2);
font-size: 1.1rem;
}
.origin__meta {
display: grid;
line-height: 1.25;
}
.origin__meta strong {
font-size: 0.95rem;
font-weight: 600;
}
.origin__url {
font-size: 0.78rem;
color: var(--accent-2);
}
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
font-size: 0.72rem;
font-weight: 600;
border-radius: var(--r-pill);
border: 1px solid var(--line);
background: var(--surface-2);
}
.badge--net {
color: var(--text);
}
.badge__dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--accent-2);
box-shadow: 0 0 8px var(--accent-2);
}
.iconbtn {
display: grid;
place-items: center;
width: 34px;
height: 34px;
flex: none;
border: 1px solid var(--line);
border-radius: 50%;
background: var(--surface-2);
color: var(--muted);
font-size: 0.8rem;
transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.iconbtn:hover {
color: var(--text);
border-color: var(--line-2);
background: var(--elevated);
}
/* ===== Hero card ===== */
.hero-card {
position: relative;
padding: 20px;
border-radius: var(--r-md);
background: var(--surface-2);
overflow: hidden;
}
.hero-card::before {
content: "";
position: absolute;
inset: 0;
padding: 1px;
border-radius: inherit;
background: linear-gradient(130deg, var(--accent), var(--accent-2));
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
opacity: 0.6;
pointer-events: none;
}
.hero-card__label {
margin: 0 0 14px;
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
}
.swap-visual {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.tokleg {
display: grid;
justify-items: flex-start;
gap: 3px;
min-width: 0;
}
.tokleg--to {
justify-items: flex-end;
text-align: right;
}
.tok {
display: grid;
place-items: center;
width: 40px;
height: 40px;
border-radius: 50%;
font-size: 1.1rem;
font-weight: 700;
color: #0a0b0f;
margin-bottom: 4px;
}
.tok--eth {
background: linear-gradient(150deg, #b9b2ff, #7c5cff);
color: #fff;
}
.tok--usdc {
background: linear-gradient(150deg, #4fd6ff, #2a7de1);
color: #fff;
}
.tok--nova {
background: linear-gradient(150deg, var(--accent-2), #08a896);
}
.tokleg__amt {
font-size: 1.05rem;
font-weight: 700;
}
.tokleg__sub {
font-size: 0.78rem;
color: var(--muted);
}
.swap-visual__arrow {
font-size: 1.3rem;
color: var(--accent);
flex: none;
}
/* ===== Risk banner ===== */
.risk {
display: flex;
gap: 10px;
margin-top: 12px;
padding: 12px 14px;
border-radius: var(--r-md);
border: 1px solid rgba(255, 77, 109, 0.35);
background: rgba(255, 77, 109, 0.1);
}
.risk__icon {
flex: none;
color: var(--neg);
font-size: 1rem;
}
.risk__body {
display: grid;
gap: 2px;
font-size: 0.84rem;
}
.risk__body strong {
color: var(--neg);
font-weight: 600;
}
.risk__body span {
color: var(--muted);
}
/* ===== Rows ===== */
.rows {
list-style: none;
margin: 16px 0 0;
padding: 6px 14px;
border-radius: var(--r-md);
background: var(--surface-2);
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 11px 0;
border-bottom: 1px solid var(--line);
font-size: 0.9rem;
}
.row:last-child {
border-bottom: none;
}
.row__k {
color: var(--muted);
flex: none;
}
.row__v {
text-align: right;
word-break: break-all;
}
.row--total .row__k {
color: var(--text);
font-weight: 600;
}
.row--total .row__v {
color: var(--accent-2);
font-weight: 700;
}
/* ===== Details expander ===== */
.details {
margin-top: 14px;
border: 1px solid var(--line);
border-radius: var(--r-md);
overflow: hidden;
}
.details__toggle {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 13px 14px;
border: none;
background: var(--surface-2);
color: var(--text);
font-size: 0.86rem;
font-weight: 500;
transition: background 0.15s;
}
.details__toggle:hover {
background: var(--elevated);
}
.details__chev {
transition: transform 0.2s ease;
color: var(--muted);
}
.details__toggle[aria-expanded="true"] .details__chev {
transform: rotate(180deg);
}
.details__panel {
padding: 14px;
border-top: 1px solid var(--line);
background: rgba(0, 0, 0, 0.18);
}
.data-grid {
margin: 0 0 14px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.data-grid > div {
display: grid;
gap: 2px;
}
.data-grid dt {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
}
.data-grid dd {
margin: 0;
font-size: 0.88rem;
}
.data-grid__label {
margin: 0 0 6px;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
}
.calldata {
margin: 0;
padding: 12px;
border-radius: var(--r-sm);
background: #0c0d12;
border: 1px solid var(--line);
font-size: 0.74rem;
line-height: 1.6;
color: var(--accent-2);
white-space: pre-wrap;
word-break: break-all;
overflow-x: auto;
}
.copybtn {
margin-top: 10px;
padding: 8px 12px;
border: 1px solid var(--line-2);
border-radius: var(--r-pill);
background: transparent;
color: var(--text);
font-size: 0.8rem;
transition: background 0.15s, border-color 0.15s;
}
.copybtn:hover {
background: var(--elevated);
border-color: var(--accent);
}
/* ===== Center states (signing / success) ===== */
.state--center {
display: grid;
justify-items: center;
gap: 8px;
padding: 38px 10px 26px;
text-align: center;
}
.state--center strong {
font-size: 1.1rem;
font-weight: 600;
}
.spinner {
width: 54px;
height: 54px;
margin-bottom: 8px;
border-radius: 50%;
border: 3px solid var(--line-2);
border-top-color: var(--accent);
border-right-color: var(--accent-2);
animation: spin 0.9s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.check {
width: 60px;
height: 60px;
margin-bottom: 8px;
display: grid;
place-items: center;
border-radius: 50%;
background: rgba(38, 208, 124, 0.14);
border: 1px solid rgba(38, 208, 124, 0.4);
}
.check svg {
width: 34px;
height: 34px;
fill: none;
stroke: var(--pos);
stroke-width: 4;
stroke-linecap: round;
stroke-linejoin: round;
}
.check svg path {
stroke-dasharray: 48;
stroke-dashoffset: 48;
animation: draw 0.5s 0.1s ease forwards;
}
@keyframes draw {
to {
stroke-dashoffset: 0;
}
}
.hash {
display: inline-flex;
align-items: center;
gap: 8px;
margin-top: 10px;
padding: 9px 14px;
border-radius: var(--r-pill);
border: 1px solid var(--line);
background: var(--surface-2);
color: var(--accent-2);
text-decoration: none;
font-size: 0.85rem;
transition: border-color 0.15s, background 0.15s;
}
.hash:hover {
border-color: var(--accent-2);
background: var(--elevated);
}
.hash__ext {
color: var(--muted);
}
/* ===== Footer ===== */
.sheet__foot {
display: grid;
grid-template-columns: 1fr 1.4fr;
gap: 10px;
margin-top: 18px;
padding-top: 16px;
border-top: 1px solid var(--line);
}
.btn {
padding: 14px 16px;
border-radius: var(--r-md);
border: 1px solid transparent;
font-size: 0.95rem;
font-weight: 600;
transition: transform 0.14s ease, box-shadow 0.16s ease, background 0.16s ease, opacity 0.16s;
}
.btn:active {
transform: translateY(1px);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn--ghost {
background: var(--surface-2);
border-color: var(--line-2);
color: var(--text);
}
.btn--ghost:hover:not(:disabled) {
background: var(--elevated);
border-color: var(--neg);
color: var(--neg);
}
.btn--primary {
background: linear-gradient(120deg, var(--accent), #5a3df0);
color: #fff;
box-shadow: 0 10px 26px var(--accent-glow);
}
.btn--primary:hover:not(:disabled) {
box-shadow: 0 14px 34px var(--accent-glow);
}
.btn--primary.is-danger {
background: linear-gradient(120deg, var(--neg), #d6344f);
box-shadow: 0 10px 26px rgba(255, 77, 109, 0.4);
}
.btn--full {
grid-column: 1 / -1;
}
/* ===== Toast ===== */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
z-index: 80;
transform: translate(-50%, 18px);
padding: 11px 18px;
border-radius: var(--r-pill);
background: var(--elevated);
border: 1px solid var(--line-2);
color: var(--text);
font-size: 0.85rem;
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.5);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
}
.toast.is-show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ===== Small screens ===== */
@media (max-width: 520px) {
.sheet {
padding: 10px 16px 18px;
}
.swap-visual {
flex-direction: column;
align-items: stretch;
gap: 6px;
}
.tokleg,
.tokleg--to {
justify-items: center;
text-align: center;
}
.swap-visual__arrow {
transform: rotate(90deg);
}
.data-grid {
grid-template-columns: 1fr;
}
.sheet__foot {
grid-template-columns: 1fr 1fr;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}(function () {
"use strict";
/* ---- Mock signing requests (fictional, UI only) ---- */
var REQUESTS = {
swap: {
title: "Swap request",
label: "You are about to swap",
net: "Lumen Chain",
from: { icon: "Ξ", cls: "tok--eth", amt: "1.5 ETH", fiat: "$4,218.00" },
to: { icon: "$", cls: "tok--usdc", amt: "4,210 USDC", fiat: "$4,210.00" },
arrow: "→",
fromAddr: "0x4e9a…1b07",
toAddr: "0x7a3f…c41d",
fee: "0.00214 ETH · $6.02",
total: "$4,224.02",
confirmLabel: "Confirm",
danger: false,
},
send: {
title: "Send request",
label: "You are about to send",
net: "Nova Network",
from: { icon: "◆", cls: "tok--nova", amt: "250 NOVA", fiat: "$1,062.50" },
to: { icon: "↑", cls: "tok--usdc", amt: "0x7a3f…c41d", fiat: "Recipient" },
arrow: "→",
fromAddr: "0x4e9a…1b07",
toAddr: "0x7a3f…c41d",
fee: "0.0008 NOVA · $0.34",
total: "250.0008 NOVA · $1,062.84",
confirmLabel: "Confirm send",
danger: false,
},
approve: {
title: "Token approval",
label: "You are granting spend access",
net: "Lumen Chain",
from: { icon: "$", cls: "tok--usdc", amt: "USDC", fiat: "Unlimited" },
to: { icon: "◎", cls: "tok--nova", amt: "NovaSwap", fiat: "Spender" },
arrow: "→",
fromAddr: "0x4e9a…1b07",
toAddr: "0x9d12…ab44",
fee: "0.00061 ETH · $1.71",
total: "$1.71",
confirmLabel: "Approve anyway",
danger: true,
risk: {
title: "Unlimited approval",
text: "This contract can spend any amount of USDC until you revoke it.",
},
},
};
/* ---- Elements ---- */
var overlay = document.getElementById("overlay");
var sheet = document.getElementById("sheet");
var toastEl = document.getElementById("toast");
var stateReview = document.getElementById("stateReview");
var stateSigning = document.getElementById("stateSigning");
var stateSuccess = document.getElementById("stateSuccess");
var sheetFoot = document.getElementById("sheetFoot");
var confirmBtn = document.getElementById("confirmBtn");
var rejectBtn = document.getElementById("rejectBtn");
var closeBtn = document.getElementById("closeBtn");
var detailsToggle = document.getElementById("detailsToggle");
var detailsPanel = document.getElementById("detailsPanel");
var lastFocus = null;
var signTimer = null;
/* ---- Toast helper ---- */
var toastTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2200);
}
function setText(sel, value) {
var el = sheet.querySelector(sel);
if (el) el.textContent = value;
}
function randHash() {
var hex = "0123456789abcdef";
var s = "";
for (var i = 0; i < 4; i++) s += hex[Math.floor(Math.random() * 16)];
var e = "";
for (var j = 0; j < 4; j++) e += hex[Math.floor(Math.random() * 16)];
return "0x" + s + "…" + e;
}
/* ---- Populate sheet from a request ---- */
function populate(req) {
setText("[data-net]", req.net);
setText("[data-net2]", req.net);
setText("#sheetTitle", req.title);
setText("#sheetSummary", req.label);
var fromIcon = sheet.querySelector("[data-from-icon]");
var toIcon = sheet.querySelector("[data-to-icon]");
fromIcon.textContent = req.from.icon;
fromIcon.className = "tok " + req.from.cls;
toIcon.textContent = req.to.icon;
toIcon.className = "tok " + req.to.cls;
setText("[data-from-amt]", req.from.amt);
setText("[data-from-fiat]", req.from.fiat);
setText("[data-to-amt]", req.to.amt);
setText("[data-to-fiat]", req.to.fiat);
sheet.querySelector(".swap-visual__arrow").textContent = req.arrow;
setText("[data-from-addr]", req.fromAddr);
setText("[data-to-addr]", req.toAddr);
setText("[data-fee]", req.fee);
setText("[data-total]", req.total);
var risk = document.getElementById("riskBanner");
if (req.risk) {
setText("[data-risk-title]", req.risk.title);
setText("[data-risk-text]", req.risk.text);
risk.hidden = false;
} else {
risk.hidden = true;
}
confirmBtn.textContent = req.confirmLabel;
confirmBtn.classList.toggle("is-danger", !!req.danger);
}
/* ---- State machine ---- */
function showState(name) {
stateReview.hidden = name !== "review";
stateSigning.hidden = name !== "signing";
stateSuccess.hidden = name !== "success";
if (name === "review") {
sheetFoot.hidden = false;
rejectBtn.textContent = "Reject";
rejectBtn.classList.remove("btn--full");
confirmBtn.style.display = "";
} else if (name === "signing") {
sheetFoot.hidden = true;
} else if (name === "success") {
sheetFoot.hidden = false;
confirmBtn.style.display = "none";
rejectBtn.textContent = "Done";
rejectBtn.classList.add("btn--full");
}
}
/* ---- Open / close ---- */
function openSheet(key) {
var req = REQUESTS[key];
if (!req) return;
lastFocus = document.activeElement;
populate(req);
collapseDetails();
showState("review");
overlay.hidden = false;
/* force reflow so transition runs */
void overlay.offsetWidth;
overlay.classList.add("is-open");
document.body.style.overflow = "hidden";
document.addEventListener("keydown", onKeydown, true);
setTimeout(function () {
confirmBtn.focus();
}, 60);
}
function closeSheet() {
clearTimeout(signTimer);
overlay.classList.remove("is-open");
document.body.style.overflow = "";
document.removeEventListener("keydown", onKeydown, true);
setTimeout(function () {
overlay.hidden = true;
}, 260);
if (lastFocus && lastFocus.focus) lastFocus.focus();
}
/* ---- Sign flow ---- */
function confirmTx() {
showState("signing");
clearTimeout(signTimer);
signTimer = setTimeout(function () {
var h = randHash();
setText("#txHash", h);
showState("success");
toast("Transaction submitted");
setTimeout(function () {
var done = document.getElementById("explorerLink");
if (done) done.focus();
}, 60);
}, 1900);
}
function rejectTx() {
if (!stateSuccess.hidden) {
closeSheet();
return;
}
toast("Request rejected");
closeSheet();
}
/* ---- Details expander ---- */
function collapseDetails() {
detailsToggle.setAttribute("aria-expanded", "false");
detailsPanel.hidden = true;
}
function toggleDetails() {
var open = detailsToggle.getAttribute("aria-expanded") === "true";
detailsToggle.setAttribute("aria-expanded", String(!open));
detailsPanel.hidden = open;
}
/* ---- Focus trap + Esc ---- */
function focusables() {
return Array.prototype.filter.call(
sheet.querySelectorAll(
'button:not([disabled]):not([style*="display: none"]), a[href], [tabindex]:not([tabindex="-1"])'
),
function (el) {
return el.offsetParent !== null;
}
);
}
function onKeydown(e) {
if (e.key === "Escape") {
e.preventDefault();
rejectTx();
return;
}
if (e.key === "Tab") {
var items = focusables();
if (!items.length) return;
var first = items[0];
var last = items[items.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
}
/* ---- Copy calldata ---- */
function copyCalldata() {
var pre = document.getElementById("calldata");
var text = pre ? pre.textContent.replace(/\s+/g, "") : "";
var done = function () {
toast("Calldata copied");
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done, done);
} else {
done();
}
}
/* ---- Wire events ---- */
Array.prototype.forEach.call(document.querySelectorAll("[data-open]"), function (btn) {
btn.addEventListener("click", function () {
openSheet(btn.getAttribute("data-open"));
});
});
confirmBtn.addEventListener("click", confirmTx);
rejectBtn.addEventListener("click", rejectTx);
closeBtn.addEventListener("click", function () {
rejectTx();
});
detailsToggle.addEventListener("click", toggleDetails);
document.getElementById("copyData").addEventListener("click", copyCalldata);
overlay.addEventListener("click", function (e) {
if (e.target === overlay) rejectTx();
});
var explorer = document.getElementById("explorerLink");
explorer.addEventListener("click", function (e) {
e.preventDefault();
toast("Opening block explorer (demo)");
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Web3 — Transaction Confirm / Signing Sheet</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Space+Grotesk:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="page">
<div class="page__hero">
<span class="brand">
<span class="brand__dot" aria-hidden="true"></span>
Lumen Wallet
</span>
<h1>Transaction Confirm</h1>
<p>A wallet-style signing request sheet. Tap a request below to review and sign.</p>
<div class="actions">
<button class="launch launch--swap" type="button" data-open="swap">
<span class="launch__icon" aria-hidden="true">⇄</span>
<span class="launch__body">
<strong>Swap request</strong>
<small>1.5 ETH → 4,210 USDC</small>
</span>
</button>
<button class="launch launch--send" type="button" data-open="send">
<span class="launch__icon" aria-hidden="true">↑</span>
<span class="launch__body">
<strong>Send request</strong>
<small>250 NOVA → 0x7a3f…c41d</small>
</span>
</button>
<button class="launch launch--approve" type="button" data-open="approve">
<span class="launch__icon" aria-hidden="true">!</span>
<span class="launch__body">
<strong>Token approval</strong>
<small>Unlimited USDC spend · flagged</small>
</span>
</button>
</div>
</div>
</main>
<!-- ====== Signing sheet ====== -->
<div class="overlay" id="overlay" hidden>
<div
class="sheet"
id="sheet"
role="dialog"
aria-modal="true"
aria-labelledby="sheetTitle"
aria-describedby="sheetSummary"
>
<div class="sheet__grab" aria-hidden="true"></div>
<header class="sheet__head">
<div class="origin">
<span class="origin__avatar" aria-hidden="true">◎</span>
<div class="origin__meta">
<strong id="sheetTitle">Signature request</strong>
<span class="origin__url">app.novaswap.fi</span>
</div>
<span class="badge badge--net" id="netBadge">
<span class="badge__dot" aria-hidden="true"></span>
<span data-net>Lumen Chain</span>
</span>
</div>
<button class="iconbtn" id="closeBtn" type="button" aria-label="Close request">✕</button>
</header>
<!-- DEFAULT (review) state -->
<section class="state" id="stateReview" data-state="review">
<div class="hero-card" id="heroCard">
<p class="hero-card__label" id="sheetSummary">You are about to</p>
<div class="swap-visual" data-action-visual>
<div class="tokleg tokleg--from">
<span class="tok tok--eth" data-from-icon aria-hidden="true">Ξ</span>
<span class="tokleg__amt mono" data-from-amt>1.5 ETH</span>
<span class="tokleg__sub mono" data-from-fiat>$4,218.00</span>
</div>
<span class="swap-visual__arrow" aria-hidden="true">→</span>
<div class="tokleg tokleg--to">
<span class="tok tok--usdc" data-to-icon aria-hidden="true">$</span>
<span class="tokleg__amt mono" data-to-amt>4,210 USDC</span>
<span class="tokleg__sub mono" data-to-fiat>$4,210.00</span>
</div>
</div>
</div>
<!-- risk banner (toggled per request) -->
<div class="risk" id="riskBanner" hidden>
<span class="risk__icon" aria-hidden="true">⚠</span>
<div class="risk__body">
<strong data-risk-title>Unlimited approval</strong>
<span data-risk-text>This contract can spend any amount of USDC until revoked.</span>
</div>
</div>
<ul class="rows" role="list">
<li class="row">
<span class="row__k">From</span>
<span class="row__v mono" data-from-addr>0x4e9a…1b07</span>
</li>
<li class="row">
<span class="row__k">Interacting with</span>
<span class="row__v mono" data-to-addr>0x7a3f…c41d</span>
</li>
<li class="row">
<span class="row__k">Network fee</span>
<span class="row__v mono" data-fee>0.00214 ETH · $6.02</span>
</li>
<li class="row row--total">
<span class="row__k">Estimated total</span>
<span class="row__v mono" data-total>$4,224.02</span>
</li>
</ul>
<!-- details expander -->
<div class="details">
<button
class="details__toggle"
id="detailsToggle"
type="button"
aria-expanded="false"
aria-controls="detailsPanel"
>
<span>Advanced details & raw data</span>
<span class="details__chev" aria-hidden="true">⌄</span>
</button>
<div class="details__panel" id="detailsPanel" hidden>
<dl class="data-grid">
<div><dt>Nonce</dt><dd class="mono">142</dd></div>
<div><dt>Gas limit</dt><dd class="mono">210,000</dd></div>
<div><dt>Max fee</dt><dd class="mono">18.4 gwei</dd></div>
<div><dt>Priority</dt><dd class="mono">1.5 gwei</dd></div>
</dl>
<p class="data-grid__label">Calldata</p>
<pre class="calldata mono" id="calldata">0x38ed1739000000000000000000000000000000000000000000000000014d1120d7b16000
0000000000000000000000000000000000000000000000000000000fa3e5a6a0
0000000000000000000000000000000000000000000000000000000000000080
0000000000000000000000004e9a3c0f8d2b71a5e6c9f10d3a82b41c7e0f51b07</pre>
<button class="copybtn" type="button" id="copyData" data-copy="calldata">Copy calldata</button>
</div>
</div>
</section>
<!-- SIGNING state -->
<section class="state state--center" id="stateSigning" data-state="signing" hidden>
<div class="spinner" aria-hidden="true"></div>
<strong>Awaiting signature…</strong>
<span class="muted">Broadcasting to <span data-net2>Lumen Chain</span></span>
</section>
<!-- SUCCESS state -->
<section class="state state--center" id="stateSuccess" data-state="success" hidden>
<div class="check" aria-hidden="true">
<svg viewBox="0 0 52 52"><path d="M14 27 l8 8 l16 -18" /></svg>
</div>
<strong>Transaction submitted</strong>
<span class="muted">It may take a moment to confirm on-chain.</span>
<a class="hash" id="explorerLink" href="#" data-noop>
<span class="mono" id="txHash">0x9c41…7e02</span>
<span class="hash__ext" aria-hidden="true">↗</span>
</a>
</section>
<!-- footer / actions -->
<footer class="sheet__foot" id="sheetFoot">
<button class="btn btn--ghost" id="rejectBtn" type="button">Reject</button>
<button class="btn btn--primary" id="confirmBtn" type="button">Confirm</button>
</footer>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Transaction Confirm / Signing Sheet
A wallet-popup style confirmation sheet for signing on-chain transactions. It mimics the moment a dApp asks your wallet to sign: the request slides up from the bottom (centering as a modal on wider screens) with the requesting origin, a network badge, and a gradient-bordered hero card that states exactly what you are about to do — for example Swap 1.5 ETH → 4,210 USDC. All addresses, amounts, hashes and gas values use a monospace font and truncated 0x1234…ab9f formatting for trust and legibility.
Three sample requests are wired up: a swap, a send, and a flagged token approval that surfaces a red risk banner (“unlimited approval”). Each populates the same sheet with its own tokens, fee line, and estimated total. An Advanced details expander reveals the nonce, gas limit, max/priority fee, and the raw calldata in a scrollable code block with a copy button.
Pressing Confirm moves through a signing spinner into an animated success state showing a freshly generated mock tx hash and a block-explorer link; Reject, the close button, the Esc key, and clicking the dimmed overlay all dismiss the request. The dialog uses role="dialog" with aria-modal, a focus trap, restored focus on close, focus-visible rings, and a prefers-reduced-motion fallback.
UI-only simulation — no real wallet, RPC, or on-chain calls. Mock data, fictional tokens.