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()">×</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