super_ball/web/superball-builder.html

1086 lines
38 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🏀 Superball Event 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 Event 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>