@wauth/sdk
Version:
Web2 auth sdk for Arweave
1,133 lines • 77.4 kB
JavaScript
import { wauthLogger } from "./logger";
// Import HTMLSanitizer for safe DOM manipulation
export class HTMLSanitizer {
/**
* Escapes HTML entities to prevent XSS attacks
* @param text - The text to escape
* @returns Escaped text safe for innerHTML
*/
static escapeHTML(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Creates a safe HTML string with basic formatting
* @param text - The text content
* @param allowedTags - Array of allowed HTML tags (default: ['br', 'strong', 'em'])
* @returns Sanitized HTML string
*/
static sanitizeHTML(text, allowedTags = ['br', 'strong', 'em']) {
// First escape all HTML
let sanitized = this.escapeHTML(text);
// Then allow specific tags back in a controlled way
allowedTags.forEach(tag => {
const escapedOpenTag = `<${tag}>`;
const escapedCloseTag = `</${tag}>`;
const openTagRegex = new RegExp(escapedOpenTag, 'gi');
const closeTagRegex = new RegExp(escapedCloseTag, 'gi');
sanitized = sanitized.replace(openTagRegex, `<${tag}>`);
sanitized = sanitized.replace(closeTagRegex, `</${tag}>`);
});
return sanitized;
}
/**
* Safely sets innerHTML with sanitization
* @param element - The DOM element
* @param html - The HTML content to set
* @param allowedTags - Array of allowed HTML tags
*/
static safeSetInnerHTML(element, html, allowedTags) {
element.innerHTML = this.sanitizeHTML(html, allowedTags);
}
/**
* Creates a safe link element
* @param href - The URL (will be validated)
* @param text - The link text (will be escaped)
* @param target - Link target (default: '_blank')
* @returns HTMLAnchorElement
*/
static createSafeLink(href, text, target = '_blank') {
const link = document.createElement('a');
// Validate URL - only allow http/https
try {
const url = new URL(href);
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error('Invalid protocol');
}
link.href = url.toString();
}
catch {
// If URL is invalid, don't set href
link.href = '#';
wauthLogger.simple('warn', 'Invalid URL provided to createSafeLink', { href });
}
link.textContent = text; // textContent automatically escapes
link.target = target;
// Security attributes for external links
if (target === '_blank') {
link.rel = 'noopener noreferrer';
}
return link;
}
}
// Focus management for accessibility
let previouslyFocusedElement = null;
function trapFocus(modal) {
const focusableElements = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
const firstFocusableElement = focusableElements[0];
const lastFocusableElement = focusableElements[focusableElements.length - 1];
function handleTab(e) {
if (e.key !== 'Tab')
return;
if (e.shiftKey) {
if (document.activeElement === firstFocusableElement) {
lastFocusableElement.focus();
e.preventDefault();
}
}
else {
if (document.activeElement === lastFocusableElement) {
firstFocusableElement.focus();
e.preventDefault();
}
}
}
modal.addEventListener('keydown', handleTab);
return () => modal.removeEventListener('keydown', handleTab);
}
function setInitialFocus(modal) {
// Store the previously focused element
previouslyFocusedElement = document.activeElement;
// Focus on the first input field, or fallback to first button
const passwordInput = modal.querySelector('input[type="password"]');
const firstInput = modal.querySelector('input');
const firstButton = modal.querySelector('button');
const elementToFocus = passwordInput || firstInput || firstButton;
if (elementToFocus) {
// Use requestAnimationFrame to ensure DOM is ready
requestAnimationFrame(() => {
elementToFocus.focus();
});
}
}
function restoreFocus() {
if (previouslyFocusedElement) {
previouslyFocusedElement.focus();
previouslyFocusedElement = null;
}
}
export function createModalContainer() {
// Check for existing modal container
let div = document.getElementById("modal-container");
if (div) {
// If a container exists, remove it first
div.parentNode?.removeChild(div);
}
// Create new container
div = document.createElement("div");
div.id = "modal-container";
div.style.fontFamily = "'Inter', sans-serif";
div.style.position = "fixed";
div.style.top = "0";
div.style.left = "0";
div.style.width = "100vw";
div.style.height = "100vh";
div.style.backgroundColor = "rgba(0, 0, 0, 0.6)";
div.style.display = "flex";
div.style.flexDirection = "column";
div.style.justifyContent = "center";
div.style.alignItems = "center";
div.style.zIndex = "999999999"; // Extremely high z-index to override shadcn/radix components
div.style.backdropFilter = "blur(8px)";
div.style.color = "#fff";
div.style.animation = "fadeIn 0.3s ease-out";
div.style.pointerEvents = "all"; // Ensure all pointer events are captured
// Selective event blocking - only prevent interactions with background elements
const preventBackgroundInteraction = (e) => {
// Only prevent events if clicking directly on the background container
if (e.target === div) {
e.preventDefault();
e.stopPropagation();
// Add subtle shake animation to indicate modal can't be dismissed by clicking background
if (e.type === 'click') {
div.style.animation = 'none';
div.style.animation = 'modalShake 0.3s ease-in-out';
setTimeout(() => {
div.style.animation = 'fadeIn 0.3s ease-out';
}, 300);
}
}
};
// Only block background clicks and touches, allow other interactions
div.addEventListener('click', preventBackgroundInteraction, false);
div.addEventListener('touchstart', preventBackgroundInteraction, false);
// Block scroll on background to prevent page scrolling, but allow modal content scrolling
div.addEventListener('wheel', (e) => {
const target = e.target;
if (target === div || !target.closest('#modal-content')) {
e.preventDefault();
}
}, { passive: false });
// Prevent context menu only on background
div.addEventListener('contextmenu', (e) => {
if (e.target === div) {
e.preventDefault();
}
});
// Add fade-in animation
const style = document.createElement("style");
style.textContent = `
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes modalShake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
`;
if (!document.head.querySelector('style[data-wauth-fade-animations]')) {
style.setAttribute('data-wauth-fade-animations', 'true');
document.head.appendChild(style);
}
return div;
}
export function createModal(type, payload, onResult) {
let modal;
let cleanupFocus;
// Remove any existing modal content
const existingModal = document.getElementById("modal-content");
if (existingModal) {
existingModal.parentNode?.removeChild(existingModal);
}
// Create wrapper that handles cleanup AFTER the original callback
const wrappedOnResult = (result) => {
wauthLogger.simple('info', 'Modal result received', { proceed: result.proceed, hasPassword: !!result.password });
// Call original callback first
onResult(result);
// Then do cleanup after a longer delay to ensure modal removal completes
setTimeout(() => {
if (cleanupFocus) {
console.log("[modal-helper] Cleaning up focus management");
cleanupFocus();
}
restoreFocus();
}, 50); // Increased delay
};
if (type === "confirm-tx") {
modal = createConfirmTxModal(payload, wrappedOnResult);
}
else if (type === "password-new") {
modal = createPasswordNewModal(payload, wrappedOnResult);
}
else if (type === "password-existing") {
modal = createPasswordExistingModal(payload, wrappedOnResult);
}
else {
modal = document.createElement("div");
}
// Store setup function to be called after modal is added to DOM
modal._setupFocus = () => {
console.log("[modal-helper] Setting up focus management for modal type:", type);
setInitialFocus(modal);
const trapCleanup = trapFocus(modal);
// Handle escape key to close modal (for some modal types)
const handleEscape = (e) => {
if (e.key === 'Escape' && type !== 'password-existing' && type !== 'password-new') {
console.log("[modal-helper] Escape key pressed, closing modal");
// Only allow escape for transaction confirmations, not password modals
onResult({ proceed: false });
}
};
document.addEventListener('keydown', handleEscape);
// Store cleanup function
cleanupFocus = () => {
trapCleanup();
document.removeEventListener('keydown', handleEscape);
};
};
return modal;
}
export function createConfirmTxModal(payload, onResult) {
// Extract transaction/dataItem details
const tx = (payload.transaction || payload.dataItem);
const tags = tx.tags || [];
const actionTag = tags.find((tag) => tag.name === "Action");
const recipientTag = tags.find((tag) => tag.name === "Recipient");
const quantityTag = tags.find((tag) => tag.name === "Quantity");
const processId = tx.target || "-";
// Get the from address - try multiple fields
let from = "-";
if ('owner' in tx && typeof tx.owner === 'string') {
from = tx.owner;
}
else if ('from' in tx && typeof tx.from === 'string') {
from = tx.from;
}
else if ('id' in tx && typeof tx.id === 'string') {
from = tx.id;
}
// Helper function to format token quantity based on denomination
function formatTokenQuantity(rawQuantity, denomination) {
try {
const denom = parseInt(denomination.toString()) || 0;
if (denom === 0) {
return rawQuantity; // No denomination, return as-is
}
// Work with strings to avoid JavaScript's scientific notation
const quantity = rawQuantity.toString();
// Helper function to divide a number string by 10^n without scientific notation
function divideByPowerOf10(numStr, power) {
if (power === 0)
return numStr;
// Remove any existing decimal point and count digits after it
let wholePart = numStr.replace('.', '');
let decimalPlaces = numStr.includes('.') ? numStr.split('.')[1].length : 0;
// Total decimal places after division
const totalDecimalPlaces = decimalPlaces + power;
// If the number is shorter than the power, pad with zeros
if (wholePart.length <= power) {
const zerosNeeded = power - wholePart.length + 1;
wholePart = '0'.repeat(zerosNeeded) + wholePart;
}
// Insert decimal point
const insertPosition = wholePart.length - power;
let result = wholePart.slice(0, insertPosition) + '.' + wholePart.slice(insertPosition);
// Clean up the result
if (result.startsWith('.')) {
result = '0' + result;
}
// Remove trailing zeros but keep at least one decimal place for non-whole numbers
if (result.includes('.')) {
result = result.replace(/\.?0+$/, '');
if (!result.includes('.') && totalDecimalPlaces > 0) {
// This was a whole number after removing trailing zeros
return result;
}
if (result.endsWith('.')) {
result = result.slice(0, -1);
}
}
return result || '0';
}
const formattedQuantity = divideByPowerOf10(quantity, denom);
// Convert to number to check size for K/M formatting, but keep as string for display
const numValue = parseFloat(formattedQuantity);
// Apply K/M formatting only for large numbers
if (numValue >= 1000000) {
const millions = numValue / 1000000;
return millions.toFixed(2).replace(/\.?0+$/, '') + 'M';
}
else if (numValue >= 1000) {
const thousands = numValue / 1000;
return thousands.toFixed(2).replace(/\.?0+$/, '') + 'K';
}
else {
// For small numbers, return the decimal string as-is
return formattedQuantity;
}
}
catch (error) {
wauthLogger.simple('warn', 'Error formatting token quantity', error);
return rawQuantity; // Fallback to raw quantity
}
}
// Extract and format quantity
const rawQuantity = quantityTag?.value || "0";
const tokenDetails = payload.tokenDetails || {};
// Use token details if available, otherwise show loading state
const isLoading = !payload.tokenDetails && tx.target;
// Format the quantity using denomination if available
const formattedQuantity = tokenDetails.Denomination
? formatTokenQuantity(rawQuantity, tokenDetails.Denomination)
: rawQuantity;
const amount = isLoading ? "Loading..." : formattedQuantity;
const unit = isLoading ? "..." : (tokenDetails.Ticker || tokenDetails.Symbol || "TOKEN");
const tokenName = tokenDetails.Name || (isLoading ? "Loading..." : "Unknown Token");
const tokenSymbol = tokenDetails.Ticker || tokenDetails.Symbol || (isLoading ? "..." : "TOKEN");
const tokenLogo = tokenDetails.Logo ? `https://arweave.net/${tokenDetails.Logo}` : "";
// Helper function to truncate long strings
function truncateString(str, maxLength = 20) {
if (str.length <= maxLength)
return str;
return str.substring(0, 8) + "..." + str.substring(str.length - 8);
}
// Helper function to create a loading spinner
function createLoader() {
const loader = document.createElement("div");
loader.style.width = "64px";
loader.style.height = "64px";
loader.style.borderRadius = "16px";
loader.style.background = "linear-gradient(135deg, #2a2a2a 0%, #1a1a1a 100%)";
loader.style.display = "flex";
loader.style.alignItems = "center";
loader.style.justifyContent = "center";
loader.style.marginBottom = "8px";
loader.style.border = "2px solid #6c63ff";
loader.style.position = "relative";
loader.style.overflow = "hidden";
loader.style.boxShadow = "0 4px 12px rgba(108, 99, 255, 0.3)";
// Create spinning element
const spinner = document.createElement("div");
spinner.style.width = "24px";
spinner.style.height = "24px";
spinner.style.border = "3px solid rgba(108, 99, 255, 0.2)";
spinner.style.borderTop = "3px solid #6c63ff";
spinner.style.borderRadius = "50%";
spinner.style.animation = "spin 1s linear infinite";
// Add enhanced CSS animations
const style = document.createElement("style");
style.textContent = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes pulse {
0% { opacity: 0.6; transform: scale(1); }
50% { opacity: 0.3; transform: scale(1.05); }
100% { opacity: 0.6; transform: scale(1); }
}
@keyframes glow {
0% { box-shadow: 0 4px 12px rgba(108, 99, 255, 0.3); }
50% { box-shadow: 0 4px 20px rgba(108, 99, 255, 0.5); }
100% { box-shadow: 0 4px 12px rgba(108, 99, 255, 0.3); }
}
`;
if (!document.head.querySelector('style[data-wauth-animations]')) {
style.setAttribute('data-wauth-animations', 'true');
document.head.appendChild(style);
}
loader.appendChild(spinner);
return loader;
}
// Helper function to create powered by element
function createPoweredByElement() {
const powered = document.createElement("div");
powered.className = "wauth-powered";
// Use secure link creation instead of innerHTML
const poweredLink = HTMLSanitizer.createSafeLink("https://wauth_subspace.ar.io", "powered by wauth", "_blank");
powered.appendChild(poweredLink);
powered.style.position = "absolute";
powered.style.bottom = "15px";
powered.style.textAlign = "center";
powered.style.fontSize = "0.95rem";
powered.style.color = "#b3b3b3";
powered.style.opacity = "0.7";
powered.style.letterSpacing = "0.02em";
powered.style.left = "0";
powered.style.right = "0";
// Style the link directly
poweredLink.style.color = "inherit";
poweredLink.style.textDecoration = "inherit";
return powered;
}
// Modal card (content only, container handled by index.ts) - Responsive design
const modal = document.createElement("div");
modal.id = "modal-content";
modal.style.background = "linear-gradient(135deg, #2a2a2a 0%, #1e1e1e 100%)";
modal.style.padding = "clamp(10px, 4vw, 20px)"; // Responsive padding
modal.style.width = "min(400px, calc(100vw - 32px))"; // Responsive width with proper margins
modal.style.maxHeight = "calc(100vh - 32px)"; // Account for margins on both sides
modal.style.overflowY = "auto";
modal.style.borderRadius = "clamp(12px, 3vw, 20px)"; // Responsive border radius
modal.style.border = "1px solid rgba(255, 255, 255, 0.1)";
modal.style.boxShadow = "0 20px 40px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.05)";
modal.style.position = "relative";
modal.style.display = "flex";
modal.style.flexDirection = "column";
modal.style.gap = "clamp(8px, 2vw, 12px)"; // Responsive gap
modal.style.animation = "slideUp 0.4s ease-out";
modal.style.backdropFilter = "blur(20px)";
modal.setAttribute('role', 'dialog'); // Accessibility
modal.setAttribute('aria-modal', 'true'); // Accessibility
modal.setAttribute('aria-labelledby', 'modal-title'); // Link to title
modal.setAttribute('aria-describedby', 'modal-description'); // Link to description
// Add custom scrollbar styling
const scrollbarStyle = document.createElement("style");
scrollbarStyle.textContent = `
#modal-content::-webkit-scrollbar {
width: 6px;
}
#modal-content::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
#modal-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 3px;
}
#modal-content::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
`;
if (!document.head.querySelector('style[data-wauth-scrollbar]')) {
scrollbarStyle.setAttribute('data-wauth-scrollbar', 'true');
document.head.appendChild(scrollbarStyle);
}
// Header with enhanced styling
const header = document.createElement("div");
header.className = "modal-header";
header.style.display = "flex";
header.style.justifyContent = "space-between";
header.style.alignItems = "center";
header.style.margin = "-10px";
header.style.marginBottom = "8px";
header.style.padding = "12px";
header.style.paddingLeft = "16px";
header.style.borderBottom = "1px solid rgba(255, 255, 255, 0.1)";
const title = document.createElement("div");
title.className = "modal-title";
title.id = "modal-title"; // For aria-labelledby
title.textContent = "Transfer";
title.style.fontSize = "2rem";
title.style.fontWeight = "700";
title.style.letterSpacing = "-0.02em";
title.style.background = "linear-gradient(135deg, #ffffff 0%, #e0e0e0 100%)";
title.style.backgroundClip = "text";
title.style.webkitBackgroundClip = "text";
title.style.webkitTextFillColor = "transparent";
const appIcon = document.createElement("div");
appIcon.className = "modal-appicon";
appIcon.style.width = "40px";
appIcon.style.height = "40px";
appIcon.style.borderRadius = "12px";
appIcon.style.background = "linear-gradient(135deg, #6c63ff 0%, #8b7fff 100%)";
appIcon.style.display = "flex";
appIcon.style.alignItems = "center";
appIcon.style.justifyContent = "center";
appIcon.style.boxShadow = "0 4px 12px rgba(108, 99, 255, 0.4)";
appIcon.style.position = "relative";
appIcon.style.overflow = "hidden";
// Add favicon as background
const favicon = document.createElement("img");
favicon.src = `${window.location.origin}/favicon.ico`;
favicon.style.width = "24px";
favicon.style.height = "24px";
favicon.style.borderRadius = "6px";
favicon.style.filter = "brightness(1.2)";
favicon.onerror = () => {
// Fallback to a nice icon - use textContent for safety
appIcon.textContent = "₿";
appIcon.style.fontSize = "20px";
};
appIcon.appendChild(favicon);
header.appendChild(title);
header.appendChild(appIcon);
modal.appendChild(header);
// Enhanced description - SECURITY: Use safe HTML to prevent XSS
const desc = document.createElement("div");
desc.className = "modal-desc";
desc.id = "modal-description"; // For aria-describedby
// Create safe description with escaped hostname and proper line break
const hostname = HTMLSanitizer.escapeHTML(window.location.hostname);
const descText = `${hostname} wants to sign a transaction.`;
const reviewText = "Review the details below.";
desc.appendChild(document.createTextNode(descText));
desc.appendChild(document.createElement("br"));
desc.appendChild(document.createTextNode(reviewText));
desc.style.fontSize = "0.95rem";
desc.style.color = "rgba(255, 255, 255, 0.7)";
desc.style.lineHeight = "1.5";
desc.style.marginBottom = "8px";
desc.style.textAlign = "center";
modal.appendChild(desc);
// Enhanced center section
const center = document.createElement("div");
center.className = "modal-center";
center.style.display = "flex";
center.style.flexDirection = "column";
center.style.alignItems = "center";
center.style.margin = "10px";
center.style.padding = "10px";
center.style.background = "rgba(255, 255, 255, 0.02)";
center.style.borderRadius = "16px";
center.style.border = "1px solid rgba(255, 255, 255, 0.05)";
// Enhanced token logo or loader
if (isLoading) {
const loader = createLoader();
loader.style.animation = "glow 2s ease-in-out infinite";
center.appendChild(loader);
}
else {
const tokenLogoEl = document.createElement("img");
tokenLogoEl.className = "token-logo";
tokenLogoEl.src = tokenLogo;
tokenLogoEl.alt = `${tokenName} Logo`;
tokenLogoEl.style.width = "64px";
tokenLogoEl.style.height = "64px";
tokenLogoEl.style.borderRadius = "16px";
tokenLogoEl.style.background = "linear-gradient(135deg, #ffffff 0%, #f0f0f0 100%)";
tokenLogoEl.style.display = "flex";
tokenLogoEl.style.alignItems = "center";
tokenLogoEl.style.justifyContent = "center";
tokenLogoEl.style.marginBottom = "8px";
tokenLogoEl.style.border = "2px solid #6c63ff";
tokenLogoEl.style.boxShadow = "0 4px 12px rgba(108, 99, 255, 0.3)";
tokenLogoEl.style.transition = "transform 0.2s ease";
tokenLogoEl.onmouseover = () => {
tokenLogoEl.style.transform = "scale(1.05)";
};
tokenLogoEl.onmouseleave = () => {
tokenLogoEl.style.transform = "scale(1)";
};
// Handle image load errors - show loader instead
tokenLogoEl.onerror = () => {
const loader = createLoader();
center.replaceChild(loader, tokenLogoEl);
};
center.appendChild(tokenLogoEl);
}
const tokenAmount = document.createElement("div");
tokenAmount.className = "token-amount";
tokenAmount.textContent = `${amount} `;
// Dynamic font sizing based on amount length
let fontSize = "2.5rem";
const amountLength = amount.length;
if (amountLength > 15) {
fontSize = "1.6rem";
}
else if (amountLength > 12) {
fontSize = "1.8rem";
}
else if (amountLength > 9) {
fontSize = "2rem";
}
else if (amountLength > 6) {
fontSize = "2.2rem";
}
tokenAmount.style.fontSize = fontSize;
tokenAmount.style.fontWeight = "800";
tokenAmount.style.background = "linear-gradient(135deg, #ffffff 0%, #e0e0e0 100%)";
tokenAmount.style.backgroundClip = "text";
tokenAmount.style.webkitBackgroundClip = "text";
tokenAmount.style.webkitTextFillColor = "transparent";
tokenAmount.style.marginBottom = "4px";
tokenAmount.style.letterSpacing = "-0.02em";
tokenAmount.style.lineHeight = "1.1";
tokenAmount.style.textAlign = "center";
tokenAmount.style.wordBreak = "break-all";
tokenAmount.style.maxWidth = "100%";
const tokenUnit = document.createElement("span");
tokenUnit.className = "token-unit";
tokenUnit.textContent = unit;
// Adjust unit font size relative to amount font size
const unitFontSize = parseFloat(fontSize) * 0.52 + "rem"; // About 52% of amount font size
tokenUnit.style.fontSize = unitFontSize;
tokenUnit.style.color = "rgba(255, 255, 255, 0.6)";
tokenUnit.style.marginLeft = "6px";
tokenUnit.style.fontWeight = "600";
// Add loading animation to unit if loading
if (isLoading) {
tokenUnit.style.opacity = "0.6";
tokenUnit.style.animation = "pulse 2s infinite";
}
tokenAmount.appendChild(tokenUnit);
center.appendChild(tokenAmount);
modal.appendChild(center);
// Enhanced details section
const details = document.createElement("div");
details.className = "modal-details";
details.style.margin = "0 0 12px 0";
details.style.background = "rgba(255, 255, 255, 0.02)";
details.style.borderRadius = "12px";
details.style.padding = "14px";
details.style.border = "1px solid rgba(255, 255, 255, 0.05)";
// Helper function to create detail rows with enhanced styling
function createDetailRow(label, value, shouldTruncate = true) {
const row = document.createElement("div");
row.className = "modal-details-row";
row.style.display = "flex";
row.style.justifyContent = "space-between";
row.style.marginBottom = "12px";
row.style.fontSize = "0.95rem";
row.style.alignItems = "flex-start";
row.style.gap = "12px";
row.style.padding = "8px 0";
row.style.borderBottom = "1px solid rgba(255, 255, 255, 0.05)";
const labelSpan = document.createElement("span");
labelSpan.className = "modal-details-label";
labelSpan.textContent = label;
labelSpan.style.color = "rgba(255, 255, 255, 0.6)";
labelSpan.style.flexShrink = "0";
labelSpan.style.minWidth = "90px";
labelSpan.style.fontWeight = "500";
const valueSpan = document.createElement("span");
valueSpan.className = "modal-details-value";
valueSpan.textContent = shouldTruncate ? truncateString(value) : value;
valueSpan.style.color = "#ffffff";
valueSpan.style.fontFamily = "'JetBrains Mono', 'Courier New', monospace";
valueSpan.style.wordBreak = "break-all";
valueSpan.style.textAlign = "right";
valueSpan.style.fontSize = "0.9rem";
valueSpan.style.lineHeight = "1.4";
valueSpan.style.fontWeight = "500";
valueSpan.style.background = "rgba(255, 255, 255, 0.05)";
valueSpan.style.padding = "4px 8px";
valueSpan.style.borderRadius = "6px";
valueSpan.style.transition = "background 0.2s ease";
// Add tooltip and hover effects
if (shouldTruncate && value.length > 20) {
valueSpan.title = value;
valueSpan.style.cursor = "help";
valueSpan.onmouseover = () => {
valueSpan.style.background = "rgba(255, 255, 255, 0.08)";
};
valueSpan.onmouseleave = () => {
valueSpan.style.background = "rgba(255, 255, 255, 0.05)";
};
}
row.appendChild(labelSpan);
row.appendChild(valueSpan);
details.appendChild(row);
}
createDetailRow("Process ID", processId);
// createDetailRow("From", from)
// Add token name row if available or loading
if (tokenDetails.Name || isLoading) {
const tokenInfoText = isLoading ? "Loading token info..." : `${tokenName} (${tokenSymbol})`;
createDetailRow("Token", tokenInfoText, false);
}
// Enhanced tags section
const tagsDiv = document.createElement("div");
tagsDiv.className = "modal-tags";
tagsDiv.style.display = "flex";
tagsDiv.style.flexDirection = "column";
tagsDiv.style.gap = "4px";
tagsDiv.style.marginTop = "12px";
tagsDiv.style.background = "rgba(255, 255, 255, 0.02)";
tagsDiv.style.borderRadius = "12px";
tagsDiv.style.paddingLeft = "4px";
tagsDiv.style.paddingRight = "4px";
tagsDiv.style.border = "1px solid rgba(255, 255, 255, 0.05)";
tagsDiv.style.maxHeight = "200px";
tagsDiv.style.overflowY = "auto";
const tagsTitle = document.createElement("div");
tagsTitle.className = "modal-tags-title";
tagsTitle.textContent = "Transaction Tags";
tagsTitle.style.color = "rgba(255, 255, 255, 0.8)";
tagsTitle.style.fontSize = "0.95rem";
tagsTitle.style.fontWeight = "600";
tagsTitle.style.margin = "-4px";
tagsTitle.style.marginBottom = "6px";
tagsTitle.style.padding = "6px";
tagsTitle.style.borderBottom = "1px solid rgba(255, 255, 255, 0.1)";
tagsTitle.style.position = "sticky";
tagsTitle.style.top = "0";
tagsTitle.style.background = "rgba(255, 255, 255, 0.02)";
tagsTitle.style.backdropFilter = "blur(10px)";
tagsTitle.style.borderRadius = "12px 12px 0px 0px";
tagsDiv.appendChild(tagsTitle);
// Add tag rows with enhanced styling
tags.forEach((tag) => {
const tagRow = document.createElement("div");
tagRow.className = "modal-tag";
tagRow.style.display = "flex";
tagRow.style.justifyContent = "space-between";
tagRow.style.alignItems = "flex-start";
tagRow.style.marginBottom = "6px";
tagRow.style.fontSize = "0.85rem";
tagRow.style.gap = "10px";
tagRow.style.padding = "6px 10px";
tagRow.style.background = "rgba(255, 255, 255, 0.03)";
tagRow.style.borderRadius = "6px";
tagRow.style.border = "1px solid rgba(255, 255, 255, 0.05)";
tagRow.style.transition = "background 0.2s ease";
const tagLabel = document.createElement("span");
tagLabel.className = "modal-tag-label";
tagLabel.textContent = tag.name;
tagLabel.style.color = "rgba(255, 255, 255, 0.6)";
tagLabel.style.fontSize = "0.8rem";
tagLabel.style.flexShrink = "0";
tagLabel.style.minWidth = "70px";
tagLabel.style.fontWeight = "500";
const tagValue = document.createElement("span");
tagValue.className = "modal-tag-value";
tagValue.textContent = tag.value.length > 20 ? truncateString(tag.value) : tag.value;
tagValue.style.color = "#ffffff";
tagValue.style.wordBreak = "break-all";
tagValue.style.textAlign = "right";
tagValue.style.fontSize = "0.8rem";
tagValue.style.lineHeight = "1.3";
tagValue.style.fontFamily = "'JetBrains Mono', 'Courier New', monospace";
tagValue.style.fontWeight = "500";
// Add tooltip for long values
if (tag.value.length > 20) {
tagValue.title = tag.value;
tagValue.style.cursor = "help";
}
// Hover effect
tagRow.onmouseover = () => {
tagRow.style.background = "rgba(255, 255, 255, 0.05)";
};
tagRow.onmouseleave = () => {
tagRow.style.background = "rgba(255, 255, 255, 0.03)";
};
tagRow.appendChild(tagLabel);
tagRow.appendChild(tagValue);
tagsDiv.appendChild(tagRow);
});
details.appendChild(tagsDiv);
modal.appendChild(details);
// Enhanced actions section
const actions = document.createElement("div");
actions.className = "modal-actions";
actions.style.display = "flex";
actions.style.flexDirection = "column";
actions.style.gap = "10px";
actions.style.marginTop = "6px";
const signBtn = document.createElement("button");
signBtn.className = "modal-btn modal-btn-primary";
signBtn.textContent = "Sign Transaction";
signBtn.setAttribute('aria-describedby', 'modal-description'); // Accessibility
signBtn.setAttribute('aria-disabled', isLoading ? 'true' : 'false'); // Accessibility
signBtn.style.width = "100%";
signBtn.style.padding = "14px 0";
signBtn.style.border = "none";
signBtn.style.borderRadius = "12px";
signBtn.style.fontSize = "1rem";
signBtn.style.fontWeight = "600";
signBtn.style.cursor = isLoading ? "not-allowed" : "pointer";
signBtn.style.transition = "all 0.2s ease";
signBtn.style.position = "relative";
signBtn.style.overflow = "hidden";
signBtn.disabled = Boolean(isLoading);
// Apply different styles based on loading state
if (isLoading) {
signBtn.style.background = "rgba(108, 99, 255, 0.3)";
signBtn.style.color = "rgba(255, 255, 255, 0.5)";
signBtn.style.boxShadow = "none";
signBtn.setAttribute('aria-label', 'Loading token details, please wait'); // Better accessibility
// Add loading animation with better UX
signBtn.style.position = "relative";
const loadingSpinner = document.createElement("div");
loadingSpinner.style.width = "16px";
loadingSpinner.style.height = "16px";
loadingSpinner.style.border = "2px solid rgba(255, 255, 255, 0.3)";
loadingSpinner.style.borderTop = "2px solid rgba(255, 255, 255, 0.8)";
loadingSpinner.style.borderRadius = "50%";
loadingSpinner.style.animation = "spin 1s linear infinite";
loadingSpinner.style.display = "inline-block";
loadingSpinner.style.marginRight = "8px";
loadingSpinner.style.verticalAlign = "middle";
loadingSpinner.setAttribute('aria-hidden', 'true'); // Hide from screen readers
signBtn.textContent = "";
signBtn.appendChild(loadingSpinner);
signBtn.appendChild(document.createTextNode("Loading Token Details..."));
// Add timeout fallback for loading state
setTimeout(() => {
if (signBtn.disabled) {
signBtn.textContent = "Continue without token details";
signBtn.disabled = false;
signBtn.style.background = "rgba(108, 99, 255, 0.6)";
signBtn.style.color = "rgba(255, 255, 255, 0.8)";
signBtn.setAttribute('aria-label', 'Token details could not be loaded, but you can still proceed with the transaction');
}
}, 10000); // 10 second timeout
}
else {
signBtn.style.background = "linear-gradient(135deg, #6c63ff 0%, #8b7fff 100%)";
signBtn.style.color = "#fff";
signBtn.style.boxShadow = "0 4px 12px rgba(108, 99, 255, 0.4)";
signBtn.onmouseover = () => {
if (!signBtn.disabled) {
signBtn.style.background = "linear-gradient(135deg, #7f6fff 0%, #9c8fff 100%)";
signBtn.style.transform = "translateY(-2px)";
signBtn.style.boxShadow = "0 6px 20px rgba(108, 99, 255, 0.5)";
}
};
signBtn.onmouseleave = () => {
if (!signBtn.disabled) {
signBtn.style.background = "linear-gradient(135deg, #6c63ff 0%, #8b7fff 100%)";
signBtn.style.transform = "translateY(0)";
signBtn.style.boxShadow = "0 4px 12px rgba(108, 99, 255, 0.4)";
}
};
}
signBtn.onclick = () => {
if (!signBtn.disabled) {
onResult({ proceed: true });
}
};
actions.appendChild(signBtn);
const cancelBtn = document.createElement("button");
cancelBtn.className = "modal-btn modal-btn-secondary";
cancelBtn.textContent = "Cancel";
cancelBtn.style.width = "100%";
cancelBtn.style.padding = "12px 0";
cancelBtn.style.border = "1px solid rgba(255, 255, 255, 0.2)";
cancelBtn.style.borderRadius = "12px";
cancelBtn.style.fontSize = "1rem";
cancelBtn.style.fontWeight = "600";
cancelBtn.style.cursor = "pointer";
cancelBtn.style.transition = "all 0.2s ease";
cancelBtn.style.background = "rgba(255, 255, 255, 0.05)";
cancelBtn.style.color = "rgba(255, 255, 255, 0.8)";
cancelBtn.style.backdropFilter = "blur(10px)";
cancelBtn.onmouseover = () => {
cancelBtn.style.background = "rgba(255, 255, 255, 0.1)";
cancelBtn.style.color = "#ffffff";
cancelBtn.style.borderColor = "rgba(255, 255, 255, 0.3)";
};
cancelBtn.onmouseleave = () => {
cancelBtn.style.background = "rgba(255, 255, 255, 0.05)";
cancelBtn.style.color = "rgba(255, 255, 255, 0.8)";
cancelBtn.style.borderColor = "rgba(255, 255, 255, 0.2)";
};
cancelBtn.onclick = () => {
console.log("[modal-helper] Cancel button clicked");
onResult({ proceed: false });
};
cancelBtn.onmouseup = (e) => {
e.stopPropagation();
};
actions.appendChild(cancelBtn);
modal.appendChild(actions);
// Don't add powered by element to modal - it should be added to container
return modal;
}
export function createPasswordNewModal(payload, onResult) {
// Minimal Modal card (content only, container handled by index.ts)
const modal = document.createElement("div");
modal.id = "modal-content";
modal.style.background = "#1a1a1a";
modal.style.padding = "24px";
modal.style.width = "min(380px, calc(100vw - 32px))";
modal.style.maxHeight = "calc(100vh - 32px)";
modal.style.overflowY = "auto";
modal.style.borderRadius = "12px";
modal.style.border = "1px solid rgba(255, 255, 255, 0.1)";
modal.style.boxShadow = "0 8px 32px rgba(0, 0, 0, 0.5)";
modal.style.position = "relative";
modal.style.display = "flex";
modal.style.flexDirection = "column";
modal.style.gap = "16px";
modal.style.animation = "slideUp 0.3s ease-out";
modal.style.pointerEvents = "auto";
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
// Remove problematic event listeners that interfere with button clicks
// These were preventing proper button interactions
// Minimal Header
const header = document.createElement("div");
header.className = "modal-header";
header.style.textAlign = "center";
header.style.marginBottom = "12px";
const title = document.createElement("div");
title.className = "modal-title";
title.textContent = "Secure Your Wallet";
title.style.fontSize = "1.5rem";
title.style.fontWeight = "600";
title.style.color = "#ffffff";
title.style.marginBottom = "4px";
const subtitle = document.createElement("div");
subtitle.className = "modal-subtitle";
subtitle.textContent = "Choose how you'd like to secure your wallet";
subtitle.style.fontSize = "0.9rem";
subtitle.style.color = "rgba(255, 255, 255, 0.7)";
header.appendChild(title);
header.appendChild(subtitle);
modal.appendChild(header);
// Initial options container
const optionsContainer = document.createElement("div");
optionsContainer.className = "modal-options";
optionsContainer.style.display = "flex";
optionsContainer.style.flexDirection = "column";
optionsContainer.style.gap = "12px";
optionsContainer.style.margin = "0 0 16px 0";
// Encrypt with password option
const encryptBtn = document.createElement("button");
encryptBtn.type = "button";
encryptBtn.className = "modal-btn modal-btn-primary";
encryptBtn.textContent = "Encrypt wallet with password";
encryptBtn.style.width = "100%";
encryptBtn.style.padding = "14px 0";
encryptBtn.style.border = "none";
encryptBtn.style.borderRadius = "8px";
encryptBtn.style.fontSize = "1rem";
encryptBtn.style.fontWeight = "600";
encryptBtn.style.cursor = "pointer";
encryptBtn.style.transition = "all 0.2s ease";
encryptBtn.style.background = "#6c63ff";
encryptBtn.style.color = "#fff";
encryptBtn.onmouseover = () => {
encryptBtn.style.background = "#7f6fff";
};
encryptBtn.onmouseleave = () => {
encryptBtn.style.background = "#6c63ff";
};
// Use without password option
const noPasswordBtn = document.createElement("button");
noPasswordBtn.type = "button";
noPasswordBtn.className = "modal-btn modal-btn-secondary";
noPasswordBtn.textContent = "Don't use password";
noPasswordBtn.style.width = "100%";
noPasswordBtn.style.padding = "14px 0";
noPasswordBtn.style.border = "1px solid rgba(255, 255, 255, 0.2)";
noPasswordBtn.style.borderRadius = "8px";
noPasswordBtn.style.fontSize = "1rem";
noPasswordBtn.style.fontWeight = "600";
noPasswordBtn.style.cursor = "pointer";
noPasswordBtn.style.transition = "all 0.2s ease";
noPasswordBtn.style.background = "rgba(255, 255, 255, 0.05)";
noPasswordBtn.style.color = "rgba(255, 255, 255, 0.8)";
noPasswordBtn.onmouseover = () => {
noPasswordBtn.style.background = "rgba(255, 255, 255, 0.1)";
noPasswordBtn.style.color = "#ffffff";
noPasswordBtn.style.borderColor = "rgba(255, 255, 255, 0.3)";
};
noPasswordBtn.onmouseleave = () => {
noPasswordBtn.style.background = "rgba(255, 255, 255, 0.05)";
noPasswordBtn.style.color = "rgba(255, 255, 255, 0.8)";
noPasswordBtn.style.borderColor = "rgba(255, 255, 255, 0.2)";
};
// Event handlers for initial options
encryptBtn.onclick = () => {
// Hide initial options and show password form
optionsContainer.style.display = "none";
form.style.display = "flex";
warning.style.display = "block";
strengthContainer.style.display = "flex";
actions.style.display = "flex";
// Update title and subtitle
title.textContent = "Create Master Password";
subtitle.textContent = "Enter a strong password to encrypt your wallet";
// Focus on password input
setTimeout(() => {
passwordInput.focus();
}, 100);
};
noPasswordBtn.onclick = () => {
// Skip password and proceed with unencrypted wallet
onResult({ proceed: true, skipPassword: true });
};
optionsContainer.appendChild(encryptBtn);
optionsContainer.appendChild(noPasswordBtn);
modal.appendChild(optionsContainer);
// Password form container (initially hidden)
const form = document.createElement("form");
form.className = "modal-form";
form.action = "#";
form.method = "post";
form.style.display = "none"; // Initially hidden
form.style.flexDirection = "column";
form.style.gap = "12px";
form.autocomplete = "on";
form.onsubmit = (e) => e.preventDefault();
// Hidden username field to help password managers understand context
const hiddenUsernameInput = document.createElement("input");
hiddenUsernameInput.type = "text";
hiddenUsernameInput.name = "username";
hiddenUsernameInput.id = "username-new";
hiddenUsernameInput.autocomplete = "username";
hiddenUsernameInput.value = `wauth-${window.location.hostname}`; // Make it unique per domain
hiddenUsernameInput.readOnly = true;
hiddenUsernameInput.style.display = "none";
hiddenUsernameInput.style.position = "absolute";
hiddenUsernameInput.style.left = "-9999px";
hiddenUsernameInput.tabIndex = -1;
hiddenUsernameInput.setAttribute('aria-hidden', 'true');
form.appendChild(hiddenUsernameInput);
// Password input
const passwordContainer = document.createElement("div");
passwordContainer.style.display = "flex";
passwordContainer.style.flexDirection = "column";
passwordContainer.style.gap = "6px";
const passwordLabel = document.createElement("label");
passwordLabel.textContent = "Master Password";
passwordLabel.htmlFor = "new-password";
passwordLabel.style.fontSize = "0.9rem";
passwordLabel.style.color = "rgba(255, 255, 255, 0.8)";
passwordLabel.style.fontWeight = "500";
const passwordInputWrapper = document.createElement("div");
passwordInputWrapper.style.position = "relative";
passwordInputWrapper.style.display = "flex";
passwordInputWrapper.style.alignItems = "center";
const passwordInput = document.createElement("input");
passwordInput.type = "password";
passwordInput.name = "password";
passwordInput.id = "new-password";
passwordInput.autocomplete = "new-password";
passwordInput.placeholder = "Create a strong password";
passwordInput.required = true;
passwordInput.minLength = 8;
passwordInput.style.padding = "12px 16px";
passwordInput.style.paddingRight = "40px";
passwordInput.style.borderRadius = "8px";
passwordInput.style.border = "1px solid rgba(255, 255, 255, 0.2)";
passwordInput.style.background = "rgba(255, 255, 255, 0.05)";
passwordInput.style.color = "#ffffff";
passwordInput.style.fontSize = "0.95rem";
passwordInput.style.outline = "none";
passwordInput.style.transition = "all 0.2s ease";
passwordInput.style.width = "100%";
// Password visibility toggle
const toggleButton = document.createElement("button");
toggleButton.type = "button";
toggleButton.innerHTML = "👁";
toggleButton.style.position = "absolute";
toggleButton.style.right = "8px";
toggleButton.style.background = "none";
toggleButton.style.border = "none";
toggleButton.style.cursor = "pointer";
toggleButton.style.fontSize = "1rem";
toggleButton.style.opacity = "0.6";
toggleButton.style.padding = "4px";
toggleButton.style.borderRadius = "4px";
toggleButton.onmouseover = () => {
toggleButton.style.opacity = "1";
};
toggleButton.onmouseleave = () => {
toggleButton.style.opacity = "0.6";
};
let isPasswordVisible = false;
toggleButton.onclick = () => {
isPasswordVisible = !isPasswordVisible;
passwordInput.type = isPasswordVisible ? "text" : "password";
toggleButton.innerHTML = isPasswordVisible ? "🙈" : "👁";
};
passwordInput.onfocus = () => {
passwordInput.style.borderColor = "#6c63ff";
passwordInput.style.background = "rgba(255, 255, 255, 0.08)";
};
passwordInput.onblur = () => {
passwordInput.style.borderColor = "rgba(255, 255, 255, 0.2)";
passwordInput.style.background = "rgba(255, 255, 255, 0.05)";
};
passwordInputWrapper.appendChild(passwordInput);
passwordInputWrapper.appendChild(toggleButton);
passwordContainer.appendChild(passwordLabel);
passwordContainer.appendChild(passwordInputWrapper);
// Confirm password input
const confirmContainer = document.createElement("div");
confirmContainer.style.display = "flex";
confirmContainer.style.flexDirection = "column";
confirmContainer.style.gap = "6px";
const confirmLabel = document.createElement("label");
confirmLabel.textContent = "Confirm Password";
confirmLabel.htmlFor = "confirm-password";
confirmLabel.style.fontSize = "0.9rem";
confirmLabel.style.color = "rgba(255, 255, 255, 0.8)";
confirmLabel.style.fontWeight = "500";
const confirmInputWrapper = document.createElement("div");
confirmInputWrapper.style.position = "relative";
confirmInputWrapper.style.display = "flex";
confirmInputWrapper.style.alignItems = "center";
const confirmInput = document.createElement("input");
confirmInput.type = "password";
confirmInput.name = "confirmPassword";
confirmInput.id = "confirm-password";
confirmInput.autocomplete = "new-password";
confirmInput.placeholder = "Confirm your password";
confirmInp