super_ball/web/superball.html

1086 lines
38 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Superball Builder</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background: #ffffff;
min-height: 100vh;
color: #000000;
}
.section {
margin: 20px 0;
border: 1px solid #ddd;
padding: 15px;
}
.section h2 {
margin: 0 0 15px 0;
font-size: 18px;
}
input, textarea, button {
width: 100%;
padding: 8px;
margin: 5px 0;
border: 1px solid #ccc;
font-family: inherit;
}
button {
background: #f0f0f0;
cursor: pointer;
width: auto;
padding: 10px 20px;
}
button:hover {
background: #e0e0e0;
}
.json-display {
background: #f8f8f8;
border: 1px solid #ddd;
padding: 10px;
font-family: 'Courier New', monospace;
font-size: 12px;
white-space: pre-wrap;
word-wrap: break-word;
max-height: 400px;
overflow-y: auto;
margin: 10px 0;
}
.bounce-section {
background: #f9f9f9;
}
.hidden {
display: none;
}
.input-group {
margin: 10px 0;
}
label {
display: block;
font-weight: bold;
margin-bottom: 3px;
}
.small-input {
width: 200px;
}
#profile-picture {
width: 50px;
height: 50px;
border-radius: 25px;
}
/* Visualization Styles */
.visualization {
background: #f0f8ff;
border: 2px solid #4a90e2;
padding: 15px;
margin: 15px 0;
border-radius: 5px;
}
.timeline {
display: flex;
flex-direction: column;
gap: 15px;
}
.timeline-step {
display: flex;
align-items: center;
padding: 10px;
border: 1px solid #ddd;
border-radius: 3px;
background: white;
}
.step-time {
min-width: 80px;
font-weight: bold;
color: #666;
font-size: 12px;
}
.step-actor {
min-width: 100px;
font-weight: bold;
color: #2c5aa0;
}
.step-action {
flex: 1;
margin: 0 10px;
}
.step-size {
min-width: 80px;
text-align: right;
font-size: 12px;
color: #666;
}
.step-relays {
font-size: 11px;
color: #888;
font-style: italic;
}
/* Throw button hover effect */
.throw-button:hover {
background: #e8f4f8 !important;
}
</style>
</head>
<body>
<div id="login-section">
<!-- Login UI if needed -->
</div>
<div id="profile-section">
<h1>Superball Builder</h1>
<img id="profile-picture">
<div id="profile-name"></div>
<div id="profile-pubkey"></div>
<div id="profile-about"></div>
</div>
<div id="event-builder" class="hidden">
<!-- FINAL EVENT SECTION -->
<div class="section">
<h2>Final Event (What gets posted at the end)</h2>
<div class="input-group">
<label for="final-content">Message Content:</label>
<textarea id="final-content" rows="3" placeholder="Enter your message content..."></textarea>
</div>
<button onclick="createFinalEvent()">Create Event That Will Be Published Publicly</button>
<div id="final-event-display" class="json-display"></div>
</div>
<!-- BOUNCES SECTION -->
<div id="bounces-container">
<!-- Bounce sections will be added here dynamically -->
</div>
<div class="section">
<button onclick="addBounce()">Add Bounce</button>
<button onclick="resetBuilder()" style="background: #dc3545; color: white; margin-left: 10px;">Reset Builder</button>
</div>
</div>
<!-- Load the official nostr-tools bundle first -->
<script src="./nostr.bundle.js"></script>
<!-- Load NOSTR_LOGIN_LITE main library -->
<script src="./nostr-lite.js"></script>
<script>
// Global variables
let nlLite = null;
let userPubkey = null;
let relayUrl = 'wss://relay.laantungir.net';
let finalEvent = null;
let bounces = [];
let bounceCounter = 0;
// Initialize NOSTR_LOGIN_LITE
async function initializeApp() {
try {
await window.NOSTR_LOGIN_LITE.init({
theme: 'default',
darkMode: false,
methods: {
extension: true,
local: true,
seedphrase: true,
connect: true,
remote: true,
otp: true
},
floatingTab: {
enabled: true,
hPosition: .98,
vPosition: 0,
getUserInfo: true,
getUserRelay: ['wss://relay.laantungir.net'],
appearance: {
style: 'minimal',
theme: 'auto',
icon: '',
text: 'Login',
iconOnly: false
},
behavior: {
hideWhenAuthenticated: false,
showUserInfo: true,
autoSlide: false,
persistent: false
},
animation: {
slideDirection: 'right'
}
},
debug: true
});
nlLite = window.NOSTR_LOGIN_LITE;
console.log('SUCCESS', 'NOSTR_LOGIN_LITE initialized successfully');
window.addEventListener('nlMethodSelected', handleAuthEvent);
} catch (error) {
console.log('ERROR', `Initialization failed: ${error.message}`);
}
}
function handleAuthEvent(event) {
const { pubkey, method, error } = event.detail;
console.log('INFO', `Auth event received: method=${method}`);
if (method && pubkey) {
userPubkey = pubkey;
console.log('SUCCESS', `Login successful! Method: ${method}, Pubkey: ${pubkey}`);
loadUserProfile();
document.getElementById('event-builder').classList.remove('hidden');
} else if (error) {
console.log('ERROR', `Authentication error: ${error}`);
}
}
// Load user profile
async function loadUserProfile() {
if (!userPubkey) return;
console.log('INFO', `Loading profile for: ${userPubkey}`);
document.getElementById('profile-name').textContent = 'Loading profile...';
document.getElementById('profile-pubkey').textContent = userPubkey;
try {
const pool = new window.NostrTools.SimplePool();
const relays = [relayUrl, 'wss://relay.laantungir.net'];
const events = await pool.querySync(relays, {
kinds: [0],
authors: [userPubkey],
limit: 1
});
pool.close(relays);
if (events.length > 0) {
console.log('SUCCESS', 'Profile event received');
const profile = JSON.parse(events[0].content);
displayProfile(profile);
} else {
console.log('INFO', 'No profile found');
document.getElementById('profile-name').textContent = 'No profile found';
document.getElementById('profile-about').textContent = 'User has not set up a profile yet.';
}
} catch (error) {
console.log('ERROR', `Profile loading failed: ${error.message}`);
document.getElementById('profile-name').textContent = 'Error loading profile';
}
}
function displayProfile(profile) {
const name = profile.name || profile.display_name || profile.displayName || 'Anonymous User';
const about = profile.about || '';
const picture = profile.picture || '';
document.getElementById('profile-name').textContent = name;
if (picture) {
document.getElementById('profile-picture').src = picture;
}
console.log('SUCCESS', `Profile displayed: ${name}`);
}
// Create final event (kind 1)
async function createFinalEvent() {
const content = document.getElementById('final-content').value.trim();
if (!content) {
alert('Please enter message content');
return;
}
try {
// Create the final event (kind 1) - pure message, no relay info
const eventTemplate = {
kind: 1,
content: content,
tags: [],
created_at: Math.floor(Date.now() / 1000)
};
// Sign the event using window.nostr (NIP-07)
finalEvent = await window.nostr.signEvent(eventTemplate);
// Display the final event (clean JSON without _targetRelays)
document.getElementById('final-event-display').textContent = JSON.stringify(finalEvent, null, 2);
console.log('SUCCESS', 'Final event created and signed');
// Reset bounces when final event is recreated
bounces = [];
document.getElementById('bounces-container').innerHTML = '';
} catch (error) {
console.log('ERROR', `Failed to create final event: ${error.message}`);
alert(`Failed to create final event: ${error.message}`);
}
}
// Generate random audit tag (looks like pubkey)
function generateAuditTag() {
// Use the same method as generateSecretKey but get the pubkey
const randomKey = window.NostrTools.generateSecretKey();
return window.NostrTools.getPublicKey(randomKey);
}
// Generate random pubkey for testing (since we don't have real superballs yet)
function generateRandomPubkey(bounceId) {
const randomKey = window.NostrTools.generateSecretKey();
const randomPubkey = window.NostrTools.getPublicKey(randomKey);
document.getElementById(`superball-pubkey-${bounceId}`).value = randomPubkey;
console.log('Generated random test pubkey for bounce', bounceId, ':', randomPubkey);
}
// Decrypt and display bounce content for testing
function decryptBounce(bounceId) {
try {
// Find the bounce data
const bounce = bounces.find(b => b.id === bounceId);
if (!bounce) {
alert('Bounce data not found');
return;
}
// Get the routing event content (encrypted payload)
const routingEvent = bounce.routingEvent;
const encryptedContent = routingEvent.content;
// Get the ephemeral key used for encryption
const ephemeralKeyHex = bounce.ephemeralKey;
const superballPubkey = bounce.superballPubkey;
console.log('DEBUG: Decrypting bounce', bounceId);
console.log('DEBUG: Ephemeral key:', ephemeralKeyHex);
console.log('DEBUG: Superball pubkey:', superballPubkey);
console.log('DEBUG: Encrypted content length:', encryptedContent.length);
// Recreate conversation key
const conversationKey = window.NostrTools.nip44.v2.utils.getConversationKey(
ephemeralKeyHex,
superballPubkey
);
// Decrypt the content
const decryptedJson = window.NostrTools.nip44.v2.decrypt(
encryptedContent,
conversationKey
);
// Parse and display the decrypted payload
const decryptedPayload = JSON.parse(decryptedJson);
document.getElementById(`bounce-${bounceId}-decrypted-content`).textContent = JSON.stringify(decryptedPayload, null, 2);
document.getElementById(`bounce-${bounceId}-decrypted`).style.display = 'block';
console.log('SUCCESS: Bounce decrypted successfully');
console.log('Decrypted payload:', decryptedPayload);
} catch (error) {
console.log('ERROR', `Failed to decrypt bounce ${bounceId}: ${error.message}`);
alert(`Failed to decrypt bounce ${bounceId}: ${error.message}`);
}
}
// Add a new bounce section
function addBounce() {
if (!finalEvent && bounces.length === 0) {
alert('Please create the final event first');
return;
}
bounceCounter++;
const bounceId = bounceCounter;
const bounceSection = document.createElement('div');
bounceSection.className = 'section bounce-section';
bounceSection.id = `bounce-${bounceId}`;
bounceSection.innerHTML = `
<h2 id="bounce-${bounceId}-header">Bounce ${bounceId} (Kind 22222 Routing Event)</h2>
<div class="input-group">
<label for="superball-pubkey-${bounceId}">Superball Pubkey:</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="superball-pubkey-${bounceId}" placeholder="Superball's public key (64 hex chars)" value="03f857567fc96b47b68632457a818563c53e09aaf0028ac1081450afe3352e25" style="flex: 1;">
<button type="button" onclick="generateRandomPubkey(${bounceId})" style="width: auto; padding: 8px 12px;">Random</button>
</div>
</div>
<div class="input-group">
<label for="bounce-relays-${bounceId}">Target Relays (comma separated):</label>
<input type="text" id="bounce-relays-${bounceId}" placeholder="wss://relay1.com, wss://relay2.com" value="wss://relay.laantungir.net">
</div>
<div class="input-group">
<label for="delay-${bounceId}">Delay (seconds):</label>
<input type="number" id="delay-${bounceId}" class="small-input" value="30" min="1">
</div>
<div class="input-group">
<label for="padding-${bounceId}">Add Padding Bytes to This Event:</label>
<input type="number" id="padding-${bounceId}" class="small-input" placeholder="150" min="0" step="1">
</div>
<div class="input-group" id="daemon-padding-${bounceId}">
<label for="add-padding-bytes-${bounceId}">Instruct Next Daemon to Add Padding (bytes):</label>
<input type="number" id="add-padding-bytes-${bounceId}" class="small-input" placeholder="256" min="0" step="1">
<small style="color: #666; font-size: 12px;">Only used when forwarding to another Superball</small>
</div>
<div class="input-group">
<label for="payment-${bounceId}">Payment (optional):</label>
<input type="text" id="payment-${bounceId}" placeholder="eCash token or payment info">
</div>
<div class="input-group">
<label for="audit-tag-${bounceId}">Audit Tag (auto-generated):</label>
<input type="text" id="audit-tag-${bounceId}" readonly style="background: #f5f5f5;">
</div>
<button onclick="createBounce(${bounceId})" id="create-bounce-btn-${bounceId}">Create Bounce ${bounceId}</button>
<div id="bounce-${bounceId}-display" class="json-display"></div>
<div style="text-align: right; margin-top: 5px;">
<button onclick="decryptBounce(${bounceId})" id="decrypt-btn-${bounceId}" style="display:none; width:auto; padding:8px 15px;">Decrypt Content</button>
</div>
<div id="bounce-${bounceId}-decrypted" class="json-display" style="display:none;">
<h4>Decrypted Payload:</h4>
<div id="bounce-${bounceId}-decrypted-content"></div>
</div>
`;
document.getElementById('bounces-container').appendChild(bounceSection);
// Automatically generate and fill in the audit tag
const auditTag = generateAuditTag();
document.getElementById(`audit-tag-${bounceId}`).value = auditTag;
// Hide daemon padding field for final bounce (first one created)
if (bounces.length === 0) {
// This is the final bounce - hide daemon padding field since it makes no sense
const daemonPaddingDiv = document.getElementById(`daemon-padding-${bounceId}`);
if (daemonPaddingDiv) {
daemonPaddingDiv.style.display = 'none';
}
}
// Update bounce labels to reflect execution order
updateBounceLabels();
}
// Update bounce labels to reflect execution order (newest bounce is Bounce 1, oldest is Bounce N)
function updateBounceLabels() {
// Get all existing bounce sections
const bounceContainer = document.getElementById('bounces-container');
const bounceSections = bounceContainer.querySelectorAll('.bounce-section');
// Update labels in reverse order (newest first gets Bounce 1)
bounceSections.forEach((section, index) => {
const bounceId = section.id.replace('bounce-', '');
const executionOrder = bounceSections.length - index; // Reverse the index
// Update the header
const header = document.getElementById(`bounce-${bounceId}-header`);
if (header) {
header.textContent = `Bounce ${executionOrder} (Kind 22222 Routing Event)`;
}
// Update the create button text
const createBtn = document.getElementById(`create-bounce-btn-${bounceId}`);
if (createBtn) {
createBtn.textContent = `Create Bounce ${executionOrder}`;
}
});
console.log('INFO: Updated bounce labels for execution order');
}
// Update all timeline absolute times continuously
function updateAllTimelineTimes() {
// Recalculate and update absolute times in the visualization timeline
if (bounces.length === 0 || !finalEvent) {
return;
}
// Recalculate the current event flow with updated timestamps
const currentEventFlow = calculateEventFlow();
// Update each timeline step
const timelineSteps = document.querySelectorAll('.step-time-absolute');
timelineSteps.forEach((element, index) => {
if (currentEventFlow[index]) {
const newTime = formatAbsoluteTime(currentEventFlow[index].time);
element.textContent = newTime;
}
});
}
// Start continuous time updates
function startTimeUpdates() {
// Update every second
setInterval(updateAllTimelineTimes, 1000);
}
// Create a bounce event
async function createBounce(bounceId) {
const superballPubkey = document.getElementById(`superball-pubkey-${bounceId}`).value.trim();
const bounceRelays = document.getElementById(`bounce-relays-${bounceId}`).value.split(',').map(r => r.trim()).filter(r => r);
const delay = parseInt(document.getElementById(`delay-${bounceId}`).value);
const builderPadding = parseInt(document.getElementById(`padding-${bounceId}`).value) || 0;
const addPaddingBytes = parseInt(document.getElementById(`add-padding-bytes-${bounceId}`).value) || 0;
const payment = document.getElementById(`payment-${bounceId}`).value.trim();
// Debug the input values
console.log('DEBUG: Input values:');
console.log(' - superballPubkey:', superballPubkey);
console.log(' - bounceRelays:', bounceRelays);
console.log(' - delay:', delay);
console.log(' - builderPadding:', builderPadding);
console.log(' - addPaddingBytes:', addPaddingBytes);
console.log(' - payment:', `"${payment}"`);
if (!superballPubkey || superballPubkey.length !== 64) {
alert('Please enter a valid superball pubkey (64 hex characters)');
return;
}
if (!bounceRelays.length) {
alert('Please enter at least one bounce relay');
return;
}
if (!delay || delay < 1) {
alert('Please enter a valid delay (minimum 1 second)');
return;
}
try {
// Determine what event to wrap
let eventToWrap;
let targetRelays;
let isLastBounce = (bounces.length === 0);
if (isLastBounce) {
// This is the first bounce - wrap the final event
eventToWrap = finalEvent;
targetRelays = bounceRelays; // Use the bounce relays for final posting
} else {
// This bounce wraps the previous bounce
eventToWrap = bounces[bounces.length - 1].routingEvent;
targetRelays = bounceRelays;
}
// Use the pre-generated audit tag from the field
const auditTag = document.getElementById(`audit-tag-${bounceId}`).value;
// Create routing instructions
const routingInstructions = {
relays: targetRelays,
delay: delay,
audit: auditTag
};
// Add daemon instruction to add padding when forwarding (if specified and not final bounce)
if (!isLastBounce && addPaddingBytes > 0) {
routingInstructions.add_padding_bytes = addPaddingBytes;
console.log('DEBUG: Added add_padding_bytes instruction:', addPaddingBytes);
}
if (payment) {
routingInstructions.payment = payment;
}
console.log('DEBUG: Final routing instructions:', routingInstructions);
// Only add 'p' field if this isn't the last bounce
if (!isLastBounce) {
// Get the superball pubkey from the previous bounce (the next hop in the chain)
const prevBounce = bounces[bounces.length - 1];
routingInstructions.p = prevBounce.superballPubkey;
}
// Note: If this IS the last bounce (first one created), no 'p' field means final posting
// Add builder padding to routing instructions (if specified)
if (builderPadding > 0) {
// Generate counting padding string (0123456789012...)
let paddingString = '';
for (let i = 0; i < builderPadding; i++) {
paddingString += (i % 10).toString();
}
routingInstructions.padding = paddingString;
console.log('DEBUG: Added', builderPadding, 'bytes of builder padding to routing instructions');
}
// Create the payload to encrypt
const payload = {
event: eventToWrap,
routing: routingInstructions
};
// Generate ephemeral key for this routing event
const ephemeralKey = window.NostrTools.generateSecretKey();
const ephemeralPubkey = window.NostrTools.getPublicKey(ephemeralKey);
// Encrypt payload using NIP-44
// Convert ephemeral key to hex - try different API methods
let ephemeralKeyHex;
if (window.NostrTools.utils && window.NostrTools.utils.bytesToHex) {
ephemeralKeyHex = window.NostrTools.utils.bytesToHex(ephemeralKey);
} else if (window.NostrTools.bytesToHex) {
ephemeralKeyHex = window.NostrTools.bytesToHex(ephemeralKey);
} else {
// Fallback: convert Uint8Array to hex manually
ephemeralKeyHex = Array.from(ephemeralKey).map(b => b.toString(16).padStart(2, '0')).join('');
}
console.log('DEBUG: Ephemeral key hex:', ephemeralKeyHex);
console.log('DEBUG: Superball pubkey:', superballPubkey);
console.log('DEBUG: Payload size:', JSON.stringify(payload).length);
const conversationKey = window.NostrTools.nip44.v2.utils.getConversationKey(
ephemeralKeyHex,
superballPubkey
);
const encryptedContent = window.NostrTools.nip44.v2.encrypt(
JSON.stringify(payload),
conversationKey
);
console.log('DEBUG: Encryption successful, content length:', encryptedContent.length);
// Create routing event template
const routingEventTemplate = {
kind: 22222,
content: encryptedContent,
tags: [
['p', superballPubkey],
['p', auditTag] // Audit tag disguised as p tag
],
created_at: Math.floor(Date.now() / 1000)
};
// Sign the routing event with ephemeral key
const routingEvent = window.NostrTools.finalizeEvent(routingEventTemplate, ephemeralKey);
// Store the bounce
const bounce = {
id: bounceId,
superballPubkey: superballPubkey,
auditTag: auditTag,
routingEvent: routingEvent,
payload: payload,
ephemeralKey: ephemeralKeyHex
};
bounces.push(bounce);
// Display the bounce event
document.getElementById(`bounce-${bounceId}-display`).textContent = JSON.stringify(routingEvent, null, 2);
// Show the decrypt button now that bounce is created
document.getElementById(`decrypt-btn-${bounceId}`).style.display = 'inline-block';
// Generate and show visualization
generateVisualization();
console.log('SUCCESS', `Bounce ${bounceId} created successfully`);
// Update bounce labels after creation to reflect execution order
updateBounceLabels();
} catch (error) {
console.log('ERROR', `Failed to create bounce ${bounceId}: ${error.message}`);
alert(`Failed to create bounce ${bounceId}: ${error.message}`);
}
}
// Generate event flow visualization
function generateVisualization() {
if (bounces.length === 0 || !finalEvent) {
return;
}
// Remove existing visualization
const existingViz = document.querySelector('.visualization');
if (existingViz) {
existingViz.remove();
}
// Calculate event flow
const eventFlow = calculateEventFlow();
// Create visualization container
const vizContainer = document.createElement('div');
vizContainer.className = 'visualization';
vizContainer.innerHTML = `
<h2>Event Flow Visualization</h2>
<p>This shows how your message will travel through the Superball anonymity network:</p>
<div class="timeline">
${generateThrowButtonHtml()}
${generateTimelineHtml(eventFlow)}
</div>
`;
// Insert after bounces container
const bouncesContainer = document.getElementById('bounces-container');
bouncesContainer.parentNode.insertBefore(vizContainer, bouncesContainer.nextSibling);
}
// Generate the "Throw the Superball" button HTML
function generateThrowButtonHtml() {
return `
<div class="timeline-step throw-button" style="background: white; border: 2px solid #4a90e2; cursor: pointer;" onclick="throwSuperball()">
<div class="step-time">
<div class="step-time-relative">Ready</div>
<div class="step-time-absolute" style="font-size: 10px; color: #888;">Click to start</div>
</div>
<div class="step-actor" style="color: #2c5aa0; font-weight: bold;">Throw Superball</div>
<div class="step-action" style="font-weight: bold;">Publish your routing event to start the anonymity chain</div>
<div class="step-size">Click here</div>
</div>
`;
}
// Throw the Superball - publish the outermost routing event to start the chain
async function throwSuperball() {
if (!bounces.length || !finalEvent) {
alert('Please create your final event and at least one bounce before throwing the Superball');
return;
}
if (!userPubkey) {
alert('Please log in first');
return;
}
try {
// Get the last (outermost) bounce to publish - the most recently created bounce
const outermostBounce = bounces[bounces.length - 1];
const routingEvent = outermostBounce.routingEvent;
// Get relays to publish to - use the outermost bounce's target relays
const targetRelays = outermostBounce.payload.routing.relays;
if (!targetRelays || targetRelays.length === 0) {
alert('No target relays configured for the first bounce');
return;
}
console.log('INFO: Publishing Superball routing event to relays:', targetRelays);
console.log('DEBUG: Event to publish:', JSON.stringify(routingEvent, null, 2));
// Update the throw button to show it's in progress
const throwButton = document.querySelector('.throw-button');
const throwButtonText = throwButton.querySelector('.step-action');
const originalText = throwButtonText.textContent;
throwButtonText.textContent = 'Publishing routing event...';
throwButton.style.pointerEvents = 'none';
throwButton.style.opacity = '0.7';
// Create a pool for publishing
const pool = new window.NostrTools.SimplePool();
try {
// Use Promise.any() to publish to all relays - succeeds if ANY relay accepts
console.log('DEBUG: Using Promise.any() to publish to all target relays simultaneously');
await Promise.any(pool.publish(targetRelays, routingEvent));
pool.close(targetRelays);
// Success! Update the UI
throwButtonText.innerHTML = '<strong>Superball Thrown Successfully!</strong><br><small>Your message is now in the anonymity network</small>';
throwButton.style.background = '#d4edda';
throwButton.style.borderColor = '#28a745';
throwButton.onclick = null; // Disable further clicks
console.log('SUCCESS: Superball routing event published successfully to at least one relay');
} catch (aggregateError) {
pool.close(targetRelays);
console.log('ERROR: All relay publish attempts failed:', aggregateError.errors);
// Extract individual error messages
const errorMessages = aggregateError.errors.map((err, index) =>
`${targetRelays[index]}: ${err.message}`
).join(', ');
// Restore button state
throwButtonText.textContent = originalText;
throwButton.style.pointerEvents = '';
throwButton.style.opacity = '';
throw new Error('Failed to publish Superball to any relay: ' + errorMessages);
}
} catch (error) {
console.log('ERROR', `Failed to throw Superball: ${error.message}`);
// Restore button if it was modified
const throwButton = document.querySelector('.throw-button');
if (throwButton) {
const throwButtonText = throwButton.querySelector('.step-action');
if (throwButtonText && throwButtonText.textContent.includes('Publishing')) {
throwButtonText.textContent = 'Publish your routing event to start the anonymity chain';
throwButton.style.pointerEvents = '';
throwButton.style.opacity = '';
}
}
alert(`Failed to throw Superball: ${error.message}\n\nPlease check your internet connection and relay configuration, then try again.`);
}
}
// Calculate the complete event flow with timing and sizes
function calculateEventFlow() {
const flow = [];
const baseTime = Date.now();
// Get the user's name from the profile section
const userNameElement = document.getElementById('profile-name');
const userName = userNameElement && userNameElement.textContent && userNameElement.textContent !== 'Loading profile...' && userNameElement.textContent !== 'No profile found' && userNameElement.textContent !== 'Error loading profile'
? userNameElement.textContent
: 'User';
// Start with user sending the outermost bounce
let currentTime = baseTime;
// Work forward through bounces (in order they were created)
bounces.forEach((bounce, index) => {
const bounceNumber = index + 1;
const isFirst = (index === 0);
const isLast = (index === bounces.length - 1);
if (isFirst) {
// User sends the outermost routing event (first bounce created)
const routingEventSize = JSON.stringify(bounce.routingEvent).length;
const relays = getRelaysForBounce(bounceNumber);
// Step 1: User sends to relay
flow.push({
time: currentTime,
actor: userName,
action: `Publishes routing event`,
size: routingEventSize
});
// Step 2: Relay propagates (immediate)
currentTime += 2000; // 2 seconds for relay propagation
flow.push({
time: currentTime,
actor: `Relay (${relays.join(', ')})`,
action: `Event available for Superball ${bounceNumber}`,
size: routingEventSize
});
}
// Add the delay for this superball to process
const delayMs = getDelayForBounce(bounceNumber) * 1000;
currentTime += delayMs;
// Superball processes and forwards
const paddingAdjustment = getPaddingAdjustmentForBounce(bounceNumber);
if (isLast) {
// Last bounce - posts final event
const finalEventSize = JSON.stringify(finalEvent).length + paddingAdjustment;
const finalRelays = getRelaysForBounce(bounceNumber);
// Step 3: Superball decrypts and sends final message
flow.push({
time: currentTime,
actor: `Superball ${bounceNumber}`,
action: `Decrypts and publishes your original message`,
size: Math.max(finalEventSize, 0)
});
// Step 4: Final relay makes message public
currentTime += 2000; // 2 seconds for relay propagation
flow.push({
time: currentTime,
actor: `Relay (${finalRelays.join(', ')})`,
action: `Your message is now publicly visible`,
size: Math.max(finalEventSize, 0)
});
} else {
// Intermediate bounce - forwards to next superball
const nextBounce = bounces[index + 1];
const nextRoutingSize = JSON.stringify(nextBounce.routingEvent).length + paddingAdjustment;
const nextRelays = getRelaysForBounce(bounceNumber + 1); // Next superball's relays
// Step 3: Superball forwards to next relay
flow.push({
time: currentTime,
actor: `Superball ${bounceNumber}`,
action: `Forwards routing event to next hop`,
size: Math.max(nextRoutingSize, 0)
});
// Step 4: Relay propagates to next superball
currentTime += 2000; // 2 seconds for relay propagation
flow.push({
time: currentTime,
actor: `Relay (${nextRelays.join(', ')})`,
action: `Event available for Superball ${bounceNumber + 1}`,
size: Math.max(nextRoutingSize, 0)
});
}
});
return flow;
}
// Generate HTML for timeline visualization
function generateTimelineHtml(eventFlow) {
return eventFlow.map((step, index) => `
<div class="timeline-step" id="timeline-step-${index}">
<div class="step-time">
<div class="step-time-relative">${formatTime(step.time)}</div>
<div class="step-time-absolute" style="font-size: 10px; color: #888;">${formatAbsoluteTime(step.time)}</div>
</div>
<div class="step-actor">${step.actor}</div>
<div class="step-action">${step.action}</div>
<div class="step-size">${step.size} bytes</div>
</div>
`).join('');
}
// Format absolute time (HH:MM:SS)
function formatAbsoluteTime(timestamp) {
const date = new Date(timestamp);
return date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
// Helper functions for bounce data extraction
function getRelaysForBounce(bounceNumber) {
const bounceIndex = bounceNumber - 1;
if (bounceIndex < 0 || bounceIndex >= bounces.length) return [];
// Get relays from the bounce's routing instructions
const bounce = bounces[bounceIndex];
if (bounce.payload && bounce.payload.routing && bounce.payload.routing.relays) {
return bounce.payload.routing.relays;
}
return ['Unknown relay'];
}
function getDelayForBounce(bounceNumber) {
const bounceIndex = bounceNumber - 1;
if (bounceIndex < 0 || bounceIndex >= bounces.length) return 30;
const bounce = bounces[bounceIndex];
if (bounce.payload && bounce.payload.routing && bounce.payload.routing.delay) {
return bounce.payload.routing.delay;
}
return 30; // default delay
}
function getPaddingAdjustmentForBounce(bounceNumber) {
const bounceIndex = bounceNumber - 1;
if (bounceIndex < 0 || bounceIndex >= bounces.length) return 0;
const bounce = bounces[bounceIndex];
if (bounce.payload && bounce.payload.routing && bounce.payload.routing.padding) {
const padding = bounce.payload.routing.padding;
if (typeof padding === 'string' && padding.match(/^[+-]\d+$/)) {
const match = padding.match(/^([+-])(\d+)$/);
const operation = match[1];
const bytes = parseInt(match[2]);
return operation === '+' ? bytes : -bytes;
} else if (typeof padding === 'string') {
// Actual padding string - return its length
return padding.length;
}
}
return 0;
}
// Format timestamp for display
function formatTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diffSeconds = Math.floor((timestamp - now.getTime()) / 1000);
if (diffSeconds <= 0) {
return 'Now';
} else if (diffSeconds < 60) {
return `+${diffSeconds}s`;
} else if (diffSeconds < 3600) {
const minutes = Math.floor(diffSeconds / 60);
const seconds = diffSeconds % 60;
return `+${minutes}m ${seconds}s`;
} else {
const hours = Math.floor(diffSeconds / 3600);
const minutes = Math.floor((diffSeconds % 3600) / 60);
return `+${hours}h ${minutes}m`;
}
}
// Reset the builder to start over (keeps login session)
function resetBuilder() {
// Clear all global variables
finalEvent = null;
bounces = [];
bounceCounter = 0;
// Clear UI elements
document.getElementById('final-content').value = '';
document.getElementById('final-event-display').textContent = '';
document.getElementById('bounces-container').innerHTML = '';
// Remove visualization if it exists
const existingViz = document.querySelector('.visualization');
if (existingViz) {
existingViz.remove();
}
console.log('INFO: Builder reset successfully');
}
document.addEventListener('DOMContentLoaded', () => {
setTimeout(initializeApp, 100);
// Start the continuous time updates
startTimeUpdates();
});
</script>
</body>
</html>