Transaction

ca5e41b596790f347340bd37158d7e20e52f3064477b76754e4fdcd263601b74
Timestamp (utc)
2024-11-08 12:13:22
Fee Paid
0.00023222 BSV
(
0.05760702 BSV
-
0.05737480 BSV
)
Fee Rate
499.7 sat/KB
Version
1
Confirmations
65,974
Size Stats
46,466 B

2 Outputs

Total Output:
0.05737480 BSV
  • jM´´<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="https://unpkg.com/bsv@1.5.6/bsv.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script> <title>My Chain Photos From BSV Labs</title> <style> /* General Layout */ body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; } .container { max-width: 1200px; margin: 0 auto; padding: 20px; } /* Upload Section */ .upload-section { margin: 20px 0; text-align: center; } /* Album Layout */ #album-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; padding: 20px; } /* Photo Cards */ .photo { background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); cursor: pointer; transition: transform 0.2s; } .photo:hover { transform: translateY(-5px); box-shadow: 0 4px 8px rgba(0,0,0,0.15); } .photo img { width: 100%; height: 200px; object-fit: cover; } .photo-caption { padding: 10px; margin: 0; text-align: center; color: #333; } /* Modal Styles */ #photo-modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); z-index: 1000; justify-content: center; align-items: center; } .modal-content { background: white; padding: 20px; border-radius: 8px; max-width: 800px; width: 90%; max-height: 90vh; overflow-y: auto; } .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .modal-body { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } /* Photo Display in Modal */ #photo-frame { max-width: 100%; max-height: 60vh; object-fit: contain; } /* Tags */ .tag-container { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0; } .tag { background: #e9ecef; padding: 4px 8px; border-radius: 4px; font-size: 0.9em; } .tag[contenteditable="true"]:focus { outline: 2px solid #007bff; background: #fff; } /* Blockchain Info */ .deployment-info { margin-top: 10px; padding: 8px; background: #f8f9fa; border-radius: 4px; } .blockchain-status { color: #28a745; font-weight: bold; } .txid-info { margin: 5px 0; font-size: 0.9em; } .txid-info a { color: #007bff; text-decoration: none; } /* Buttons */ .button, .deploy-btn, .view-blockchain-btn { background: #007bff; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 1em; transition: background-color 0.2s; } .button:hover, .deploy-btn:hover, .view-blockchain-btn:hover { background: #0056b3; } .save-btn { background: #28a745; } .save-btn:hover { background: #218838; } .deploy-btn:disabled { background: #6c757d; cursor: not-allowed; } /* Status Messages */ .status-message { margin-top: 10px; padding: 8px; background: #f8f9fa; border-radius: 4px; } .status-message.error { background: #fff3f3; color: #dc3545; } /* Payment Section */ .payment-section { margin: 20px 0; padding: 20px; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .message { padding: 10px; margin: 10px 0; background: #f8f9fa; border-radius: 4px; } /* Payment Section */ .payment-section { margin: 20px 0; padding: 20px; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .message { padding: 10px; margin: 10px 0; background: #f8f9fa; border-radius: 4px; } .payment-section { background: #f8f9fa; padding: 15px; border-radius: 8px; margin-top: 15px; } .qr-container { background: white; padding: 10px; border-radius: 4px; display: inline-block; margin: 15px 0; } .payment-status { margin-top: 10px; padding: 10px; background: white; border-radius: 4px; } .success-message { color: #28a745; font-weight: bold; } .key-display { word-break: break-all; font-family: monospace; background: white; padding: 10px; border-radius: 4px; margin: 10px 0; } </style> </head> <body> <div class="container"> <!-- Upload Section --> <div class="upload-section"> <input type="file" id="photoInput" accept="image/*" style="display: none"> <button class="button" id="uploadButton">Upload Photo</button> <button class="button" onclick="exportIndexedDB()">Export Album</button> <button class="button" onclick="importIndexedDB()">Import Album</button> <input type="file" id="importFileInput" accept=".json" style="display: none;" onchange="handleFileImport(event)"> </div> <!-- Album Container --> <div id="album-container"></div> <!-- Modal --> <div id="photo-modal"> <div class="modal-content"> <!-- ... other modal content ... --> <div id="deployment-section" class="deployment-section"> <!-- This section will be populated by getDeploymentSection() --> </div> <!-- Payment Section --> <div id="paymentSection" class="payment-section" style="display: none;"> <h3>Payment Required</h3> <p>Cost: <span id="uploadCost">0.001</span> BSV</p> <div id="currentAddress" class="key-display"></div> <div id="qrCode"></div> <div id="paymentStatus" class="message">Waiting for payment...</div> </div> <!-- Status Display --> <div id="deployment-status" class="message"></div> <!-- Transaction Display --> <div id="txidDisplay" class="message"> <h3>Transaction IDs</h3> </div> <!-- Transaction History --> <div id="txidHistoryContent" class="message"></div> </div> </div> </div> <script> // Constants and Global Variables const XPUB = 'xpub661MyMwAqRbcG5pHG2zLFS2xJJN2hokNjM5nNsqWSSCnZeUEnuuHn4iTQ9TxTHQaewHBNiwSE1iiE7Q7g2XQ9LwxB71nHUUvFjYxCB6ZffR'; const FIXED_COST = 0.001; // BSV payment amount let addressIndex = parseInt(localStorage.getItem('addressIndex') || '0'); const hdPublicKey = bsv.HDPublicKey.fromString(XPUB); // Constants and Database Setup const DB_NAME = 'PhotoAlbumDB'; const DB_VERSION = 1; document.addEventListener('DOMContentLoaded', function() { // Initialize event listeners initializeEventListeners(); // Initial render renderPhotoAlbum(); // Setup event listeners function initializeEventListeners() { const uploadButton = document.getElementById('uploadButton'); const photoInput = document.getElementById('photoInput'); const closeModalBtn = document.getElementById('close-modal'); const photoModal = document.getElementById('photo-modal'); const addTagButton = document.getElementById('addTagButton'); const saveChangesButton = document.getElementById('saveChangesButton'); if (uploadButton) { uploadButton.addEventListener('click', () => { photoInput.click(); }); } if (photoInput) { photoInput.addEventListener('change', async (event) => { const files = event.target.files; for (const file of files) { await storeTemporaryPhotoWithTags(file); } }); } if (closeModalBtn) { closeModalBtn.addEventListener('click', closeModal); } if (photoModal) { window.addEventListener('click', (event) => { if (event.target === photoModal) { closeModal(); } }); } if (addTagButton) { addTagButton.addEventListener('click', () => { const currentPhotoId = photoModal.getAttribute('data-photo-id'); if (currentPhotoId) { addNewTag(currentPhotoId); } }); } if (saveChangesButton) { saveChangesButton.addEventListener('click', () => { const currentPhotoId = photoModal.getAttribute('data-photo-id'); if (currentPhotoId) { saveChanges(currentPhotoId); } }); } } }); // This function should be outside the DOMContentLoaded event listener async function handlePhotoUpload(event) { const files = Array.from(event.target.files); event.target.value = ''; // Clear input for (const file of files) { await storeTemporaryPhotoWithTags(file); } } // Database initialization async function openDatabase() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onerror = () => reject(request.error); request.onsuccess = () => resolve(request.result); request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains('photos')) { const store = db.createObjectStore('photos', { keyPath: 'id' }); store.createIndex('status', 'status'); store.createIndex('tags', 'tags', { multiEntry: true }); store.createIndex('dateUploaded', 'dateUploaded'); store.createIndex('txid', 'txid'); } }; }); } // 3. Modal functions function closeModal() { const modal = document.getElementById('photo-modal'); modal.style.display = "none"; } async function savePhotoToIndexedDB(photoData) { const db = await openDatabase(); const transaction = db.transaction('photos', 'readwrite'); const store = transaction.objectStore('photos'); return new Promise((resolve, reject) => { const request = store.put(photoData); request.onsuccess = () => resolve(); request.onerror = () => reject('Failed to save photo to IndexedDB'); }); } // Load and Display Functions async function loadPhotosFromIndexedDB(filter = {}) { const db = await openDatabase(); const transaction = db.transaction('photos', 'readonly'); const store = transaction.objectStore('photos'); return new Promise((resolve, reject) => { const request = store.getAll(); request.onsuccess = () => { let photos = request.result; // Apply filters if (filter.tags && filter.tags.length > 0) { photos = photos.filter(photo => filter.tags.every(tag => photo.tags.includes(tag)) ); } if (filter.status) { photos = photos.filter(photo => photo.status === filter.status); } // Sort by date photos.sort((a, b) => new Date(b.dateUploaded) - new Date(a.dateUploaded)); resolve(photos); }; request.onerror = () => reject('Failed to load photos from IndexedDB'); }); } // Enhanced renderPhotoAlbum with blockchain support async function renderPhotoAlbum() { const container = document.getElementById('album-container'); if (!container) return; container.innerHTML = ''; // Clear existing content try { const photos = await loadPhotosFromIndexedDB(); // Sort photos by date, newest first photos.sort((a, b) => new Date(b.dateUploaded) - new Date(a.dateUploaded)); photos.forEach(photo => { const photoElement = createPhotoElement(photo); container.appendChild(photoElement); }); } catch (error) { console.error('Error rendering photo album:', error); container.innerHTML = '<div class="error">Error loading photos</div>'; } } function createPhotoElement(photo) { const div = document.createElement('div'); div.className = 'photo'; div.setAttribute('data-photo-id', photo.id); const html = ` <img src="${photo.src}" alt="${photo.caption}"> <p class="photo-caption">${photo.caption}</p> <div class="photo-tags"> ${photo.tags.map(tag => `<span class="tag">${tag}</span>`).join('')} </div> ${photo.txid ? ` <div class="deployment-info"> <span class="blockchain-status">✓ On Blockchain</span> <div class="txid-info"> <a href="https://whatsonchain.com/tx/${photo.txid}" target="_blank">View Transaction</a> </div> </div> ` : ''} `; div.innerHTML = html; div.addEventListener('click', () => openPhotoModal(photo.id)); return div; } // Open photo modal async function openPhotoModal(photoId) { const modal = document.getElementById('photo-modal'); const modalContent = document.createElement('div'); modalContent.className = 'modal-content'; const photoData = await getPhotoData(photoId); if (!photoData) return; modalContent.innerHTML = ` <div class="modal-header"> <h2>${photoData.caption}</h2> <span class="close" onclick="closeModal()">&times;</span> </div> <div class="modal-body"> <div class="photo-display"> <img id="modal-image" src="${photoData.src}" alt="${photoData.caption}"> </div> <div class="photo-info"> <div class="info-section"> <label>Uploaded:</label> <span>${new Date(photoData.dateUploaded).toLocaleDateString()}</span> </div> <div class="info-section"> <label>Description:</label> <textarea id="photo-description">${photoData.description || ''}</textarea> </div> <div class="info-section"> <label>Tags:</label> <div id="tag-container" class="tag-container"> ${photoData.tags.map(tag => ` <span class="tag" contenteditable="true">${tag}</span> `).join('')} </div> <button onclick="addNewTag('${photoId}')" class="add-tag-btn">+ Add Tag</button> </div> ${getDeploymentSection(photoData)} </div> </div> <div class="modal-footer"> <button onclick="saveChanges('${photoId}')" class="save-btn">Save Changes</button> </div> `; modal.innerHTML = ''; modal.appendChild(modalContent); modal.style.display = 'flex'; // Setup deployment status updates if not deployed if (!photoData.txid) { setupDeploymentStatus(photoId); } } // Add status update function function updateDeployStatus(message) { const statusElement = document.getElementById('deploy-status'); if (statusElement) { statusElement.innerHTML = `<div class="status-message">${message}</div>`; } } function getDeploymentSection(photoData) { if (photoData.txid) { return ` <div class="deployment-section"> <div class="deployment-info"> <h3>✓ Deployed to Blockchain</h3> <p>Deploy TXID: <a href="https://whatsonchain.com/tx/${photoData.txid}" target="_blank"> ${photoData.txid.substring(0, 8)}...${photoData.txid.substring(photoData.txid.length - 8)} </a></p> <button onclick="viewBlockchainImage('${photoData.txid}')" class="view-blockchain-btn"> View Blockchain Version </button> </div> </div> `; } else { return ` <div class="deployment-section"> <button onclick="deployPhoto('${photoData.id}')" class="deploy-btn"> Deploy to Blockchain </button> <div id="deployment-status"></div> <div id="paymentSection" class="payment-section" style="display: none;"> <h3>Payment Required</h3> <p>Amount: ${FIXED_COST} BSV</p> <div id="currentAddress" class="key-display"></div> <div id="qrCode"></div> <div id="paymentStatus" class="message">Waiting for payment...</div> </div> </div> `; } } async function getPhotoData(photoId) { const db = await openDatabase(); const transaction = db.transaction('photos', 'readonly'); const store = transaction.objectStore('photos'); return new Promise((resolve, reject) => { const request = store.get(photoId); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } function addNewTag(photoId) { const container = document.getElementById('tag-container'); const tagElement = document.createElement('span'); tagElement.className = 'tag'; tagElement.contentEditable = "true"; tagElement.innerText = "new tag"; container.appendChild(tagElement); tagElement.focus(); } function applyTagFilter() { const tagInput = document.getElementById('tagFilter').value; const tags = tagInput.split(',').map(tag => tag.trim()).filter(tag => tag); renderPhotoAlbum({ tags }); } function filterByTag(tag) { document.getElementById('tagFilter').value = tag; applyTagFilter(); } function setupDeploymentStatus(photoId) { const statusElement = document.getElementById('deployment-status'); if (statusElement) { statusElement.innerHTML = `<div class="status-message">Ready for deployment</div>`; } } async function deployPhoto(photoId) { const statusElement = document.getElementById('deployment-status'); const deployButton = document.querySelector('.deploy-btn'); const paymentSection = document.getElementById('paymentSection'); // Ensure deploy button is disabled to prevent multiple clicks if (deployButton) { deployButton.disabled = true; } // Show the payment section for the QR code display if (paymentSection) { paymentSection.style.display = 'block'; } try { // Start the payment process and generate QR code const paymentTxid = await startPaymentProcess(); // If no payment is detected, throw an error if (!paymentTxid) { throw new Error('Payment not received'); } // Proceed with deployment only after payment is received const result = await createImageHtml(photoId, (message) => { if (statusElement) { statusElement.innerHTML = `<div class="status-message">${message}</div>`; } }); if (result.success) { // Record both payment and deployment TXIDs displayAndSaveTxids(paymentTxid, result.txid); await renderPhotoAlbum(); // Re-render the album with deployed photo data openPhotoModal(photoId); // Refresh modal with deployment info } } catch (error) { if (statusElement) { statusElement.innerHTML = `<div class="status-message error">${error.message}</div>`; } if (deployButton) { deployButton.disabled = false; // Re-enable the button if there's an error } } } async function saveChanges(photoId) { try { const description = document.getElementById('photo-description').value; const tags = Array.from(document.getElementById('tag-container').children) .map(tag => tag.textContent.trim()) .filter(tag => tag); const db = await openDatabase(); const tx = db.transaction('photos', 'readwrite'); const store = tx.objectStore('photos'); const photoData = await new Promise((resolve, reject) => { const request = store.get(photoId); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); if (photoData) { const updatedPhotoData = { ...photoData, description, tags }; await new Promise((resolve, reject) => { const updateRequest = store.put(updatedPhotoData); updateRequest.onsuccess = () => resolve(); updateRequest.onerror = () => reject(updateRequest.error); }); await renderPhotoAlbum(); closeModal(); } } catch (error) { console.error('Error saving changes:', error); alert('Failed to save changes'); } } async function aiTaggingModel(imageBase64) { // Basic image analysis without external API const tags = ['photo']; // Add time-based tags const hour = new Date().getHours(); if (hour >= 5 && hour < 12) tags.push('morning'); else if (hour >= 12 && hour < 17) tags.push('afternoon'); else if (hour >= 17 && hour < 20) tags.push('evening'); else tags.push('night'); // Add size-based tags const sizeInKB = Math.round(imageBase64.length * 0.75 / 1024); if (sizeInKB > 1000) tags.push('large-file'); else if (sizeInKB > 100) tags.push('medium-file'); else tags.push('small-file'); // Add format tag if (imageBase64.includes('image/jpeg')) tags.push('jpeg'); else if (imageBase64.includes('image/png')) tags.push('png'); else if (imageBase64.includes('image/gif')) tags.push('gif'); return tags; } // Process AI results into relevant tags function processTags(aiResult) { // This would depend on your chosen AI service's response format // Example processing for common AI image analysis results const tags = []; // Process objects detected if (aiResult.objects) { tags.push(...aiResult.objects.map(obj => obj.name.toLowerCase())); } // Process scene classification if (aiResult.scene) { tags.push(aiResult.scene.toLowerCase()); } // Process colors if (aiResult.colors) { tags.push(...aiResult.colors.map(color => color.toLowerCase())); } // Remove duplicates and filter out unwanted tags return [...new Set(tags)].filter(tag => tag.length > 2); } // Fallback function for basic tagging function generateBasicTags(imageBase64) { const tags = []; // Add basic file type tag if (imageBase64.includes('image/jpeg')) { tags.push('jpeg'); } else if (imageBase64.includes('image/png')) { tags.push('png'); } // Add basic size classification const sizeInBytes = Math.ceil((imageBase64.length * 3) / 4); if (sizeInBytes > 1000000) { tags.push('large-file'); } else if (sizeInBytes > 100000) { tags.push('medium-file'); } else { tags.push('small-file'); } // Add timestamp-based tags const hour = new Date().getHours(); if (hour >= 5 && hour < 12) { tags.push('morning'); } else if (hour >= 12 && hour < 17) { tags.push('afternoon'); } else if (hour >= 17 && hour < 20) { tags.push('evening'); } else { tags.push('night'); } return tags; } // Photo Upload and Storage async function handlePhotoUpload(event) { const files = event.target.files; for (const file of files) { await storeTemporaryPhotoWithTags(file); } } async function storeTemporaryPhotoWithTags(file) { if (!file || !file.type.startsWith('image/')) { console.error('Invalid file type'); return; } return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = async function(event) { try { const photoId = `temp-photo-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // Use basic tags if AI tagging fails let tags; try { tags = await aiTaggingModel(event.target.result); } catch (error) { console.warn('AI tagging failed, using basic tags:', error); tags = ['untagged']; // Default tag if AI fails } const photoData = { id: photoId, src: event.target.result, tags: tags, description: "", caption: file.name, dateUploaded: new Date().toISOString().split('T')[0], txid: null, status: 'temporary' }; await savePhotoToIndexedDB(photoData); await renderPhotoAlbum(); // Re-render after saving resolve(photoData); } catch (error) { reject(error); } }; reader.onerror = () => reject(reader.error); reader.readAsDataURL(file); }); } // Add blockchain image viewing functionality async function viewBlockchainImage(txid) { event.stopPropagation(); // Prevent modal from opening try { const response = await fetch(`https://api.whatsonchain.com/v1/bsv/main/tx/${txid}/hex`); if (!response.ok) { throw new Error('Failed to fetch blockchain data'); } const dataHex = await response.text(); const opReturnDataHex = extractOpReturnData(dataHex); if (!opReturnDataHex) { throw new Error('OP_RETURN data not found'); } const cleanedDataHex = removeAppendedData(opReturnDataHex); const contentBytes = hexToBytes(cleanedDataHex); const htmlString = new TextDecoder().decode(contentBytes); // Open in new window const newWindow = window.open(); newWindow.document.open(); newWindow.document.write(htmlString); newWindow.document.close(); } catch (error) { console.error('Error viewing blockchain image:', error); alert('Error viewing blockchain image: ' + error.message); } } // Blockchain Integration Functions async function fetchAndDisplayHtml(txid, targetElement = null) { if (!txid) { throw new Error('Transaction ID is required'); } try { const response = await fetch(`https://api.whatsonchain.com/v1/bsv/main/tx/${txid}/hex`); if (!response.ok) { throw new Error('Network response was not ok'); } const dataHex = await response.text(); const opReturnDataHex = extractOpReturnData(dataHex); if (!opReturnDataHex) { throw new Error('OP_RETURN data not found'); } const cleanedDataHex = removeAppendedData(opReturnDataHex); const contentBytes = hexToBytes(cleanedDataHex); const htmlString = new TextDecoder().decode(contentBytes); if (targetElement) { targetElement.innerHTML = htmlString; } else { const newWindow = window.open(); newWindow.document.open(); newWindow.document.write(htmlString); newWindow.document.close(); } return htmlString; } catch (error) { console.error('Error fetching content:', error); throw error; } } function extractOpReturnData(dataHex) { const marker = 'ffffffff'; const markerIndex = dataHex.indexOf(marker); if (markerIndex === -1) { console.error(`No '${marker}' marker found in the transaction data.`); return null; } const opReturnOpcode = '6a'; const opReturnIndex = dataHex.indexOf(opReturnOpcode, markerIndex + marker.length); if (opReturnIndex === -1) { console.error("No '6a' opcode found after marker"); return null; } let currentIndex = opReturnIndex + 2; let nextTwoChars = dataHex.substring(currentIndex, currentIndex + 2); let lengthHex = nextTwoChars; let dataLength = parseInt(lengthHex, 16) - 4; if (lengthHex === '4c') { lengthHex = dataHex.substring(currentIndex + 2, currentIndex + 4); dataLength = parseInt(lengthHex, 16) - 4; currentIndex += 4; } else if (lengthHex === '4d') { lengthHex = dataHex.substring(currentIndex + 2, currentIndex + 6); dataLength = parseInt(lengthHex, 16) - 4; currentIndex += 6; } else if (lengthHex === '4e') { lengthHex = dataHex.substring(currentIndex + 2, currentIndex + 10); dataLength = parseInt(lengthHex, 16) - 4; currentIndex += 10; } else { currentIndex += 2; } return dataHex.substring(currentIndex, currentIndex + dataLength * 2); } function removeAppendedData(dataHex) { return dataHex.substring(0, dataHex.length - 76); // Remove last 38 bytes } function hexToBytes(hex) { const bytes = []; for (let c = 0; c < hex.length; c += 2) { bytes.push(parseInt(hex.substr(c, 2), 16)); } return new Uint8Array(bytes); } async function createImageHtml(photoId, statusCallback) { if (!photoId) { throw new Error('Photo ID is required'); } try { statusCallback?.('Retrieving photo data...'); const db = await openDatabase(); const transaction = db.transaction('photos', 'readonly'); const store = transaction.objectStore('photos'); const photoData = await new Promise((resolve, reject) => { const request = store.get(photoId); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(new Error('Failed to retrieve photo from IndexedDB: ' + request.error)); }); if (!photoData) { throw new Error('Photo not found in database'); } statusCallback?.('Preparing HTML content...'); const htmlContent = `<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${photoData.caption}</title> <style> body { margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; background: #fff; } img { max-width: 100%; height: auto; display: block; } </style> </head> <body> <img src="${photoData.src}" alt="${photoData.caption}"> </body> </html>`; const timestamp = Date.now(); const randomId = Math.random().toString(36).substring(2, 15); const queueId = `${timestamp}_${randomId}`; statusCallback?.('Creating upload payload...'); const blob = new Blob([htmlContent], { type: 'text/html' }); const formData = new FormData(); formData.append('file', blob, `payload_${queueId}.dat`); statusCallback?.('Uploading to queue...'); const uploadResponse = await fetch('https://bsvhost.com/uploads', { method: 'POST', body: formData }); if (!uploadResponse.ok) { throw new Error(`Upload failed: ${uploadResponse.statusText}`); } const uploadResult = await uploadResponse.json(); if (!uploadResult.success) { throw new Error(uploadResult.message || 'Upload failed'); } statusCallback?.('File queued, waiting for deployment...'); // Poll for deployment status const maxAttempts = 30; let attempt = 0; while (attempt < maxAttempts) { const delay = Math.min(1000 * Math.pow(2, attempt), 10000); await new Promise(resolve => setTimeout(resolve, delay)); statusCallback?.(`Checking deployment status (attempt ${attempt + 1}/${maxAttempts})...`); const statusResponse = await fetch(`https://bsvhost.com/status/${queueId}`); if (!statusResponse.ok) { const errorText = await statusResponse.text(); console.error('Status response:', statusResponse.status, errorText); throw new Error(`Status check failed: ${statusResponse.status}`); } const status = await statusResponse.json(); switch(status.status) { case 'completed': statusCallback?.(`Deployment successful! TXID: ${status.txid}`); try { const updateTx = db.transaction('photos', 'readwrite'); const updateStore = updateTx.objectStore('photos'); // Update photo record const updatedPhotoData = { ...photoData, txid: status.txid, deployedAt: new Date().toISOString(), blockchainUrl: `https://whatsonchain.com/tx/${status.txid}` }; await new Promise((resolve, reject) => { const updateRequest = updateStore.put(updatedPhotoData); updateRequest.onsuccess = () => resolve(); updateRequest.onerror = () => reject(updateRequest.error); }); // Update UI await renderPhotoAlbum(); } catch (updateError) { console.error('Failed to update photo record:', updateError); } return { success: true, message: 'File deployed successfully', txid: status.txid }; case 'failed': throw new Error(`Deployment failed: ${status.error}`); case 'pending': statusCallback?.('Still in queue, please wait...'); break; default: console.log(`Unknown status: ${status.status}`); statusCallback?.('Checking deployment status...'); } attempt++; } throw new Error('Deployment timed out after 5 minutes'); } catch (error) { console.error('Error in createImageHtml:', error); statusCallback?.(`Error: ${error.message}`); throw error; } } function generateNewAddress() { const childPubKey = hdPublicKey.deriveChild(addressIndex++); localStorage.setItem('addressIndex', addressIndex); // Save index for next address return bsv.Address.fromPublicKey(childPubKey.publicKey).toString(); } // Start Payment Process and Display QR Code async function startPaymentProcess() { const paymentSection = document.getElementById('paymentSection'); const currentAddressElement = document.getElementById('currentAddress'); const qrContainer = document.getElementById('qrCode'); const paymentStatus = document.getElementById('paymentStatus'); // Generate a new payment address const paymentAddress = generateNewAddress(); // Check for each element before accessing its properties if (!paymentSection) { console.error("Element 'paymentSection' not found."); return; } if (!currentAddressElement) { console.error("Element 'currentAddress' not found."); return; } if (!qrContainer) { console.error("Element 'qrCode' not found."); return; } if (!paymentStatus) { console.error("Element 'paymentStatus' not found."); return; } // Display payment section paymentSection.style.display = 'block'; // Update address and status text currentAddressElement.textContent = `Payment Address: ${paymentAddress}`; paymentStatus.textContent = 'Waiting for payment...'; // Generate QR code for payment qrContainer.innerHTML = ''; // Clear any existing QR code try { new QRCode(qrContainer, { text: `bitcoin:${paymentAddress}?amount=${FIXED_COST}`, width: 128, height: 128, colorDark: "#000000", colorLight: "#ffffff", correctLevel: QRCode.CorrectLevel.H }); console.log('QR Code generated successfully.'); } catch (error) { console.error('Error generating QR code:', error); } // Placeholder for payment confirmation logic const paymentTxid = await checkPayment(paymentAddress); return paymentTxid; } // Function to poll the blockchain for payment confirmation async function checkPayment(address) { const paymentStatus = document.getElementById('paymentStatus'); let attempts = 0; const maxAttempts = 60; // 10 minutes return new Promise((resolve, reject) => { const checkInterval = setInterval(async () => { attempts++; if (attempts > maxAttempts) { clearInterval(checkInterval); reject(new Error('Payment timeout - please try again')); return; } try { const response = await fetch(`https://api.whatsonchain.com/v1/bsv/main/address/${address}/unspent`); const unspentOutputs = await response.json(); const payment = findMatchingPayment(unspentOutputs); if (payment) { clearInterval(checkInterval); paymentStatus.textContent = 'Payment received! Deploying files...'; resolve(payment.tx_hash); } else { paymentStatus.textContent = `Waiting for payment... (${attempts} of ${maxAttempts})`; } } catch (error) { paymentStatus.textContent = 'Error checking payment: ' + error.message; } }, 10000); // Check every 10 seconds }); } // Helper to find payment output meeting the required amount function findMatchingPayment(unspentOutputs) { const requiredAmount = FIXED_COST * 100000000; // Convert to satoshis return unspentOutputs.find(utxo => utxo.value >= requiredAmount); } // Check Payment Status with a Timeout Mechanism async function checkPayment(address) { const paymentStatus = document.getElementById('paymentStatus'); let attempts = 0; const maxAttempts = 60; // 10 minutes return new Promise((resolve, reject) => { const checkInterval = setInterval(async () => { attempts++; if (attempts > maxAttempts) { clearInterval(checkInterval); reject(new Error('Payment timeout - please try again')); return; } try { const response = await fetch(`https://api.whatsonchain.com/v1/bsv/main/address/${address}/unspent`); const unspentOutputs = await response.json(); const payment = findMatchingPayment(unspentOutputs); if (payment) { clearInterval(checkInterval); paymentStatus.textContent = 'Payment received! Deploying files...'; resolve(payment.tx_hash); } else { paymentStatus.textContent = `Waiting for payment... (${attempts} of ${maxAttempts})`; } } catch (error) { paymentStatus.textContent = 'Error checking payment: ' + error.message; } }, 10000); // Check every 10 seconds }); } function findMatchingPayment(unspentOutputs) { const requiredAmount = FIXED_COST * 100000000; // Convert to satoshis return unspentOutputs.find(utxo => utxo.value >= requiredAmount); } // Display and Save Payment and Deployment TXIDs function displayAndSaveTxids(paymentTxid, deployTxid) { // Display TXIDs to the user const txidDisplay = document.getElementById('txidDisplay'); txidDisplay.innerHTML = ` <div>Payment TXID: ${paymentTxid}</div> <div>Deploy TXID: ${deployTxid}</div> `; // Save TXIDs to Local Storage for history const history = JSON.parse(localStorage.getItem('txidHistory') || '[]'); history.push({ paymentTxid, deployTxid, timestamp: new Date().toISOString() }); localStorage.setItem('txidHistory', JSON.stringify(history)); updateTxidHistoryDisplay(); } // Function to Update TXID History Display function updateTxidHistoryDisplay() { const historyContent = document.getElementById('txidHistoryContent'); const history = JSON.parse(localStorage.getItem('txidHistory') || '[]'); historyContent.innerHTML = history.map(entry => ` <div> <div>Payment TXID: ${entry.paymentTxid}</div> <div>Deploy TXID: ${entry.deployTxid}</div> <div>Date: ${new Date(entry.timestamp).toLocaleString()}</div> </div> `).join(''); } // Export IndexedDB Data to JSON and allow download async function exportIndexedDB() { try { const db = await openDatabase(); const transaction = db.transaction('photos', 'readonly'); const store = transaction.objectStore('photos'); const allPhotos = await new Promise((resolve, reject) => { const request = store.getAll(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject('Failed to retrieve photos'); }); const blob = new Blob([JSON.stringify(allPhotos)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'photo_album_backup.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); alert('Album exported successfully!'); } catch (error) { console.error('Export failed:', error); alert('Failed to export album.'); } } // Import JSON data and restore IndexedDB function importIndexedDB() { const input = document.getElementById('importFileInput'); input.click(); // Trigger file input click } // Handle file input and restore data to IndexedDB function handleFileImport(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = async function(event) { try { const data = JSON.parse(event.target.result); if (!Array.isArray(data)) { throw new Error('Invalid file format'); } const db = await openDatabase(); const transaction = db.transaction('photos', 'readwrite'); const store = transaction.objectStore('photos'); for (const photo of data) { await new Promise((resolve, reject) => { const request = store.put(photo); request.onsuccess = () => resolve(); request.onerror = () => reject('Failed to import photo'); }); } alert('Album restored successfully!'); await renderPhotoAlbum(); // Refresh album display } catch (error) { console.error('Import failed:', error); alert('Failed to import album.'); } }; reader.onerror = () => alert('Failed to read file'); reader.readAsText(file); } // Optional: Download HTML file function downloadImageHtml(filename) { const htmlContent = sessionStorage.getItem('imageHtml'); if (htmlContent) { const blob = new Blob([htmlContent], { type: 'text/html' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename || 'image.html'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } } </script> </body> </html>
    https://whatsonchain.com/tx/ca5e41b596790f347340bd37158d7e20e52f3064477b76754e4fdcd263601b74