/** * Application JavaScript pour le Dashboard Bitcoin Signet */ const API_BASE_URL = window.location.origin; // Utiliser le même origin pour le faucet (sera configuré via le proxy) // Si on est sur dashboard.certificator.4nkweb.com, utiliser faucet.certificator.4nkweb.com let FAUCET_API_URL; if (window.location.hostname.includes('dashboard.certificator.4nkweb.com')) { FAUCET_API_URL = window.location.origin.replace('dashboard.certificator.4nkweb.com', 'faucet.certificator.4nkweb.com'); } else if (window.location.hostname.includes('localhost') || window.location.hostname === '127.0.0.1') { FAUCET_API_URL = 'http://localhost:3021'; } else { FAUCET_API_URL = window.location.origin; } let selectedFile = null; let lastBlockHeight = null; let blockPollingInterval = null; let dataRefreshInterval = null; // Initialisation document.addEventListener('DOMContentLoaded', () => { loadData(); dataRefreshInterval = setInterval(loadData, 30000); // Rafraîchir toutes les 30 secondes // Démarrer le polling pour détecter les nouveaux blocs startBlockPolling(); }); // Nettoyer les intervalles lors du déchargement de la page window.addEventListener('beforeunload', () => { if (blockPollingInterval) { clearInterval(blockPollingInterval); blockPollingInterval = null; } if (dataRefreshInterval) { clearInterval(dataRefreshInterval); dataRefreshInterval = null; } }); /** * Démarrer le polling pour détecter les nouveaux blocs */ function startBlockPolling() { // Vérifier la hauteur du bloc toutes les 5 secondes blockPollingInterval = setInterval(async () => { try { const response = await fetch(`${API_BASE_URL}/api/blockchain/info`); if (!response.ok) return; const data = await response.json(); const currentHeight = data.blocks; // Si la hauteur a changé, actualiser les métriques if (lastBlockHeight !== null && currentHeight > lastBlockHeight) { console.log(`Nouveau bloc détecté: ${lastBlockHeight} -> ${currentHeight}`); // Actualiser uniquement les 4 métriques spécifiques await Promise.all([ loadAvgFee(), loadAvgTxAmount(), loadMiningDifficulty(), loadAvgBlockTime(), ]); } lastBlockHeight = currentHeight; } catch (error) { console.error('Error checking block height:', error); } }, 5000); // Vérifier toutes les 5 secondes } /** * Charge toutes les données de supervision * Optimisé pour charger d'abord les données critiques, puis les données moins critiques */ async function loadData() { try { // Phase 1 : Charger les données critiques rapidement (affichage immédiat) await Promise.all([ loadBlockchainInfo(), loadLatestBlock(), loadWalletBalance(), loadAnchorCount(), loadNetworkPeers(), loadMiningDifficulty(), ]); updateLastUpdateTime(); // Phase 2 : Charger les données moins critiques en arrière-plan (peuvent être plus lentes) // Ne pas attendre ces données pour mettre à jour l'heure Promise.all([ loadAvailableForAnchor(), loadAvgBlockTime(), loadAvgFee(), loadAvgTxAmount(), ]).catch((error) => { console.error('Error loading secondary data:', error); }); } catch (error) { console.error('Error loading data:', error); } } /** * Charge les informations de la blockchain */ async function loadBlockchainInfo() { try { const response = await fetch(`${API_BASE_URL}/api/blockchain/info`); const data = await response.json(); document.getElementById('block-height').textContent = data.blocks || 0; // Initialiser lastBlockHeight si ce n'est pas déjà fait if (lastBlockHeight === null && data.blocks !== undefined) { lastBlockHeight = data.blocks; } } catch (error) { console.error('Error loading blockchain info:', error); document.getElementById('block-height').textContent = 'Erreur'; } } /** * Charge les informations du dernier bloc */ async function loadLatestBlock() { try { const response = await fetch(`${API_BASE_URL}/api/blockchain/latest-block`); const data = await response.json(); if (data.time) { const date = new Date(data.time * 1000); document.getElementById('last-block-time').textContent = date.toLocaleString('fr-FR'); } else { document.getElementById('last-block-time').textContent = 'Aucun bloc'; } document.getElementById('last-block-tx-count').textContent = data.tx_count || 0; } catch (error) { console.error('Error loading latest block:', error); document.getElementById('last-block-time').textContent = 'Erreur'; document.getElementById('last-block-tx-count').textContent = 'Erreur'; } } /** * Charge le solde du wallet */ async function loadWalletBalance() { try { const response = await fetch(`${API_BASE_URL}/api/wallet/balance`); const data = await response.json(); document.getElementById('balance-mature').textContent = formatBTC(data.mature || 0); document.getElementById('balance-immature').textContent = formatBTC(data.immature || 0); } catch (error) { console.error('Error loading wallet balance:', error); document.getElementById('balance-mature').textContent = 'Erreur'; document.getElementById('balance-immature').textContent = 'Erreur'; } } /** * Charge le nombre d'ancrages */ async function loadAnchorCount() { const anchorCountValue = document.getElementById('anchor-count-value'); const anchorCountSpinner = document.getElementById('anchor-count-spinner'); if (!anchorCountValue || !anchorCountSpinner) { console.error('Elements anchor-count-value or anchor-count-spinner not found in DOM'); return; } // Afficher le spinner anchorCountSpinner.style.display = 'inline'; anchorCountValue.textContent = '...'; try { const response = await fetch(`${API_BASE_URL}/api/anchor/count`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); const count = data.count !== undefined ? data.count : 0; // Masquer le spinner et mettre à jour la valeur anchorCountSpinner.style.display = 'none'; if (count >= 0) { anchorCountValue.textContent = count.toLocaleString(); } else { anchorCountValue.textContent = '0'; } } catch (error) { console.error('Error loading anchor count:', error); // Masquer le spinner anchorCountSpinner.style.display = 'none'; // Ne pas réinitialiser à "Erreur" si on a déjà une valeur affichée // Garder la dernière valeur valide ou afficher "0" si c'est la première erreur const currentValue = anchorCountValue.textContent; if (currentValue === '-' || currentValue === 'Erreur' || currentValue === '...') { anchorCountValue.textContent = '0'; } } } /** * Charge la capacité d'ancrage restante * Utilise l'endpoint optimisé /api/utxo/count pour éviter de charger toute la liste */ async function loadAvailableForAnchor() { const availableForAnchorValue = document.getElementById('available-for-anchor-value'); const availableForAnchorSpinner = document.getElementById('available-for-anchor-spinner'); if (!availableForAnchorValue || !availableForAnchorSpinner) { console.error('Elements for available-for-anchor not found in DOM'); return; } // Afficher le spinner availableForAnchorSpinner.style.display = 'inline'; availableForAnchorValue.textContent = '...'; try { // Utiliser l'endpoint optimisé qui lit depuis la base de données const response = await fetch(`${API_BASE_URL}/api/utxo/count`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); const availableForAnchor = data.availableForAnchor || 0; // Masquer le spinner et mettre à jour la valeur availableForAnchorSpinner.style.display = 'none'; availableForAnchorValue.textContent = availableForAnchor.toLocaleString('fr-FR') + ' ancrages'; } catch (error) { console.error('Error loading available for anchor:', error); // Masquer le spinner availableForAnchorSpinner.style.display = 'none'; // Afficher une valeur par défaut en cas d'erreur const currentValue = availableForAnchorValue.textContent; if (currentValue === '-' || currentValue === 'Erreur' || currentValue === '...') { availableForAnchorValue.textContent = '0 ancrages'; } } } /** * Charge les informations sur les pairs */ async function loadNetworkPeers() { try { const response = await fetch(`${API_BASE_URL}/api/network/peers`); const data = await response.json(); document.getElementById('peer-count').textContent = data.connections || 0; } catch (error) { console.error('Error loading network peers:', error); document.getElementById('peer-count').textContent = 'Erreur'; } } /** * Charge la difficulté de minage */ async function loadMiningDifficulty() { try { const response = await fetch(`${API_BASE_URL}/api/mining/difficulty`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); if (data.difficulty !== undefined && data.difficulty !== null) { // Convertir en nombre si nécessaire (sécurité supplémentaire) const difficulty = typeof data.difficulty === 'string' ? parseFloat(data.difficulty) : Number(data.difficulty); if (!isNaN(difficulty)) { // Formater la difficulté avec séparateurs de milliers const formatted = formatDifficulty(difficulty); document.getElementById('mining-difficulty').textContent = formatted; } else { document.getElementById('mining-difficulty').textContent = '-'; } } else { document.getElementById('mining-difficulty').textContent = '-'; } } catch (error) { console.error('Error loading mining difficulty:', error); document.getElementById('mining-difficulty').textContent = 'Erreur'; } } /** * Charge le temps moyen entre blocs */ async function loadAvgBlockTime() { const avgBlockTimeValue = document.getElementById('avg-block-time-value'); const avgBlockTimeSpinner = document.getElementById('avg-block-time-spinner'); if (!avgBlockTimeValue || !avgBlockTimeSpinner) { console.error('Elements avg-block-time-value or avg-block-time-spinner not found in DOM'); return; } // Afficher le spinner avgBlockTimeSpinner.style.display = 'inline'; avgBlockTimeValue.textContent = '...'; try { const response = await fetch(`${API_BASE_URL}/api/mining/avg-block-time`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); // Masquer le spinner et mettre à jour la valeur avgBlockTimeSpinner.style.display = 'none'; if (data.formatted) { avgBlockTimeValue.textContent = data.formatted; } else if (data.timeAvgSeconds !== undefined) { avgBlockTimeValue.textContent = formatBlockTime(data.timeAvgSeconds); } else { avgBlockTimeValue.textContent = '-'; } } catch (error) { console.error('Error loading average block time:', error); // Masquer le spinner avgBlockTimeSpinner.style.display = 'none'; avgBlockTimeValue.textContent = 'Erreur'; } } /** * Formate la difficulté avec séparateurs de milliers */ function formatDifficulty(difficulty) { if (difficulty === 0) return '0'; if (difficulty < 1) return difficulty.toFixed(4); if (difficulty < 1000) return difficulty.toFixed(2); if (difficulty < 1000000) return (difficulty / 1000).toFixed(2) + ' K'; if (difficulty < 1000000000) return (difficulty / 1000000).toFixed(2) + ' M'; return (difficulty / 1000000000).toFixed(2) + ' G'; } /** * Formate le temps moyen entre blocs en format lisible */ function formatBlockTime(seconds) { if (seconds < 60) { return `${seconds}s`; } else if (seconds < 3600) { const minutes = Math.floor(seconds / 60); const secs = seconds % 60; return secs > 0 ? `${minutes}m ${secs}s` : `${minutes}m`; } else { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`; } } /** * Formate un montant en sats avec l'emoji ✅ */ function formatSats(sats) { if (sats === 0) return '0 ✅'; if (sats < 1000) return sats.toLocaleString('fr-FR') + ' ✅'; if (sats < 1000000) return (sats / 1000).toFixed(2) + ' K ✅'; if (sats < 1000000000) return (sats / 1000000).toFixed(2) + ' M ✅'; return (sats / 1000000000).toFixed(2) + ' G ✅'; } /** * Formate un montant en sats avec l'emoji ✅ (sans préfixes K/M/G) */ function formatSatsSimple(sats) { if (sats === 0) return '0 ✅'; return sats.toLocaleString('fr-FR') + ' ✅'; } /** * Charge les frais moyens */ async function loadAvgFee() { const avgFeeValue = document.getElementById('avg-fee-value'); const avgFeeSpinner = document.getElementById('avg-fee-spinner'); if (!avgFeeValue || !avgFeeSpinner) { console.error('Elements avg-fee-value or avg-fee-spinner not found in DOM'); return; } // Afficher le spinner avgFeeSpinner.style.display = 'inline'; avgFeeValue.textContent = '...'; try { const response = await fetch(`${API_BASE_URL}/api/transactions/avg-fee`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); // Masquer le spinner et mettre à jour la valeur avgFeeSpinner.style.display = 'none'; if (data.avgFee !== undefined && data.avgFee !== null) { const avgFee = Number(data.avgFee); if (!isNaN(avgFee) && avgFee >= 0) { const formatted = formatSatsSimple(avgFee); avgFeeValue.textContent = formatted; } else { avgFeeValue.textContent = '-'; } } else { avgFeeValue.textContent = '-'; } } catch (error) { console.error('Error loading average fee', error); // Masquer le spinner avgFeeSpinner.style.display = 'none'; avgFeeValue.textContent = 'Erreur'; } } /** * Charge le montant moyen des transactions */ async function loadAvgTxAmount() { const avgTxAmountValue = document.getElementById('avg-tx-amount-value'); const avgTxAmountSpinner = document.getElementById('avg-tx-amount-spinner'); if (!avgTxAmountValue || !avgTxAmountSpinner) { console.error('Elements avg-tx-amount-value or avg-tx-amount-spinner not found in DOM'); return; } // Afficher le spinner avgTxAmountSpinner.style.display = 'inline'; avgTxAmountValue.textContent = '...'; try { const response = await fetch(`${API_BASE_URL}/api/transactions/avg-amount`, { signal: AbortSignal.timeout(60000) // Timeout de 60 secondes }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); if (data.error) { throw new Error(data.error); } // Masquer le spinner et mettre à jour la valeur avgTxAmountSpinner.style.display = 'none'; if (data.avgAmount !== undefined && data.avgAmount !== null) { // Convertir en nombre si nécessaire const avgAmount = typeof data.avgAmount === 'string' ? parseFloat(data.avgAmount) : Number(data.avgAmount); if (!isNaN(avgAmount) && avgAmount >= 0) { const formatted = formatSatsSimple(avgAmount); avgTxAmountValue.textContent = formatted; } else { avgTxAmountValue.textContent = '-'; } } else { avgTxAmountValue.textContent = '-'; } } catch (error) { console.error('Error loading average transaction amount', error); // Masquer le spinner avgTxAmountSpinner.style.display = 'none'; avgTxAmountValue.textContent = 'Erreur'; } } /** * Formate un montant en BTC */ function formatBTC(btc) { if (btc === 0) return '0 🛡'; if (btc < 0.000001) return `${(btc * 100000000).toFixed(0)} sats`; // Arrondir sans décimales pour les balances Mature et Immature return `${Math.round(btc)} 🛡`; } /** * Met à jour l'heure de dernière mise à jour */ function updateLastUpdateTime() { const now = new Date(); document.getElementById('last-update').textContent = now.toLocaleString('fr-FR'); } /** * Change d'onglet */ function switchTab(tab, buttonElement) { // Désactiver tous les onglets document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); document.querySelectorAll('.tab-button').forEach(button => { button.classList.remove('active'); }); // Activer l'onglet sélectionné document.getElementById(`${tab}-tab`).classList.add('active'); // Activer le bouton correspondant if (buttonElement) { buttonElement.classList.add('active'); } } /** * Gère la sélection de fichier */ function handleFileSelect(event) { const file = event.target.files[0]; if (file) { selectedFile = file; const fileInfo = document.getElementById('file-info'); const maxSize = 100 * 1024 * 1024; // 100 MB en bytes const fileSize = file.size; const isOverLimit = fileSize > maxSize; let infoHtml = ` Fichier sélectionné : ${file.name}
Taille : ${formatFileSize(file.size)}
Type : ${file.type || 'Non spécifié'} `; if (isOverLimit) { infoHtml += `
⚠️ Fichier trop volumineux (limite : 100 MB)`; } fileInfo.innerHTML = infoHtml; } } /** * Formate la taille d'un fichier */ function formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; } /** * Génère le hash depuis le texte */ async function generateHashFromText() { const text = document.getElementById('anchor-text').value; if (!text.trim()) { showResult('anchor-result', 'error', 'Veuillez entrer un texte à ancrer.'); return; } try { const response = await fetch(`${API_BASE_URL}/api/hash/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ text }), }); const data = await response.json(); if (data.hash) { document.getElementById('anchor-hash').value = data.hash; showResult('anchor-result', 'success', `Hash généré avec succès : ${data.hash}`); } else { showResult('anchor-result', 'error', 'Erreur lors de la génération du hash.'); } } catch (error) { showResult('anchor-result', 'error', `Erreur : ${error.message}`); } } /** * Génère le hash depuis le fichier */ async function generateHashFromFile() { if (!selectedFile) { showResult('anchor-result', 'error', 'Veuillez sélectionner un fichier.'); return; } // Vérifier la taille du fichier (limite : 100 MB) const maxSize = 100 * 1024 * 1024; // 100 MB en bytes if (selectedFile.size > maxSize) { showResult('anchor-result', 'error', `Le fichier est trop volumineux (${formatFileSize(selectedFile.size)}). La limite est de 100 MB.`); return; } try { const reader = new FileReader(); await new Promise((resolve, reject) => { reader.onload = async (e) => { try { const arrayBuffer = e.target.result; // Convertir l'ArrayBuffer en base64 pour l'envoi au backend // Utiliser une boucle pour éviter les limites de taille des arguments de fonction const uint8Array = new Uint8Array(arrayBuffer); let binaryString = ''; for (let i = 0; i < uint8Array.length; i++) { binaryString += String.fromCharCode(uint8Array[i]); } const base64 = btoa(binaryString); const response = await fetch(`${API_BASE_URL}/api/hash/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ fileContent: base64, isBase64: true }), }); if (!response.ok) { const errorData = await response.json().catch(() => ({ error: `HTTP ${response.status}: ${response.statusText}` })); throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (data.hash) { document.getElementById('anchor-hash').value = data.hash; showResult('anchor-result', 'success', `Hash généré avec succès : ${data.hash}`); } else { showResult('anchor-result', 'error', data.error || 'Erreur lors de la génération du hash.'); } resolve(); } catch (error) { showResult('anchor-result', 'error', `Erreur : ${error.message}`); reject(error); } }; reader.onerror = (error) => { showResult('anchor-result', 'error', `Erreur lors de la lecture du fichier : ${error.message || 'Erreur inconnue'}`); reject(error); }; reader.readAsArrayBuffer(selectedFile); }); } catch (error) { showResult('anchor-result', 'error', `Erreur lors de la lecture du fichier : ${error.message}`); } } /** * Vérifie le hash */ async function verifyHash() { const apiKey = document.getElementById('anchor-api-key').value.trim(); const hash = document.getElementById('anchor-hash').value.trim(); if (!apiKey) { showResult('anchor-result', 'error', 'Veuillez entrer une clé API.'); return; } if (!hash || !/^[0-9a-fA-F]{64}$/.test(hash)) { showResult('anchor-result', 'error', 'Veuillez entrer un hash valide (64 caractères hexadécimaux).'); return; } try { showResult('anchor-result', 'info', 'Vérification du hash en cours...'); const response = await fetch(`${API_BASE_URL}/api/anchor/verify`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ hash, apiKey }), }); const data = await response.json(); if (response.ok && data.anchor_info) { const info = data.anchor_info; showResult('anchor-result', 'success', `Hash vérifié avec succès !
TXID : ${info.transaction_id || 'N/A'}
Hauteur du bloc : ${info.block_height !== null && info.block_height !== undefined ? info.block_height : 'Non confirmé'}
Confirmations : ${info.confirmations || 0}
Statut : ${info.confirmations > 0 ? 'Confirmé' : 'En attente'}`); } else { showResult('anchor-result', 'error', data.message || data.error || 'Hash non trouvé sur la blockchain.'); } } catch (error) { showResult('anchor-result', 'error', `Erreur : ${error.message}`); } } /** * Affiche/masque les options de filigrane */ function toggleWatermarkOptions() { const enabled = document.getElementById('watermark-enabled').checked; const optionsDiv = document.getElementById('watermark-options'); optionsDiv.style.display = enabled ? 'block' : 'none'; } /** * Ancre le document */ async function anchorDocument() { const apiKeyElement = document.getElementById('anchor-api-key'); if (!apiKeyElement) { showResult('anchor-result', 'error', 'Champ clé API introuvable.'); return; } const apiKey = apiKeyElement.value.trim(); const hash = document.getElementById('anchor-hash').value; const watermarkEnabled = document.getElementById('watermark-enabled').checked; if (!apiKey || apiKey.length === 0) { showResult('anchor-result', 'error', 'Veuillez entrer une clé API.'); return; } console.log('API Key récupérée:', apiKey ? apiKey.substring(0, 10) + '...' : 'vide'); if (!hash || !/^[0-9a-fA-F]{64}$/.test(hash)) { showResult('anchor-result', 'error', 'Veuillez générer un hash valide (64 caractères hexadécimaux).'); return; } try { showResult('anchor-result', 'info', 'Ancrage en cours...'); // Si le filigrane est activé, utiliser l'API filigrane if (watermarkEnabled) { await anchorWithWatermark(apiKey); } else { // Sinon, utiliser l'API d'ancrage classique const response = await fetch(`${API_BASE_URL}/api/anchor/test`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ hash, apiKey }), }); const data = await response.json(); if (response.ok && data.txid) { showResult('anchor-result', 'success', `Document ancré avec succès !
TXID : ${data.txid}
Statut : ${data.status}
Confirmations : ${data.confirmations || 0}`); // Recharger le nombre d'ancrages après un court délai setTimeout(loadAnchorCount, 2000); } else { showResult('anchor-result', 'error', data.message || data.error || 'Erreur lors de l\'ancrage.'); } } } catch (error) { showResult('anchor-result', 'error', `Erreur : ${error.message}`); } } /** * Ancre le document avec filigrane */ async function anchorWithWatermark(apiKey) { // Vérifier que la clé API est bien fournie if (!apiKey || apiKey.trim().length === 0) { showResult('anchor-result', 'error', 'Veuillez entrer une clé API.'); return; } // Utiliser le backend du dashboard comme proxy vers l'API filigrane const watermarkApiUrl = API_BASE_URL; const textContent = document.getElementById('anchor-text').value; const fileInput = document.getElementById('anchor-file'); const currentSelectedFile = selectedFile || (fileInput.files.length > 0 ? fileInput.files[0] : null); // Préparer les données du fichier let fileData = null; let fileName = null; let mimeType = null; if (currentSelectedFile) { const reader = new FileReader(); await new Promise((resolve, reject) => { reader.onload = (e) => { const arrayBuffer = e.target.result; const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer))); fileData = base64; fileName = currentSelectedFile.name; mimeType = currentSelectedFile.type; resolve(); }; reader.onerror = reject; reader.readAsArrayBuffer(currentSelectedFile); }); } // Préparer les options de filigrane const watermarkOptionsRaw = { enabled: true, text: document.getElementById('watermark-text').value.trim() || undefined, signature: document.getElementById('watermark-signature').value.trim() || undefined, depositor: document.getElementById('watermark-depositor').value.trim() || undefined, watermarkedFileName: document.getElementById('watermarked-filename').value.trim() || undefined, originalFileName: document.getElementById('original-filename').value.trim() || undefined, dateUTC: document.getElementById('watermark-date-utc').checked, dateLocal: document.getElementById('watermark-date-local').checked, blockNumber: document.getElementById('watermark-block-number').checked, blockHash: document.getElementById('watermark-block-hash').checked, documentHash: document.getElementById('watermark-document-hash').checked, }; // Nettoyer l'objet en supprimant les valeurs undefined const watermarkOptions = Object.fromEntries( Object.entries(watermarkOptionsRaw).filter(([_, value]) => value !== undefined) ); // S'assurer que enabled est toujours présent et true watermarkOptions.enabled = true; const requestBody = { apiKey, watermarkOptions, }; if (textContent) { requestBody.textContent = textContent; } else if (fileData) { requestBody.fileData = fileData; requestBody.fileName = fileName; requestBody.mimeType = mimeType; } else { showResult('anchor-result', 'error', 'Veuillez saisir un texte ou sélectionner un fichier.'); return; } try { const response = await fetch(`${watermarkApiUrl}/api/watermark/document`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), }); let data; try { data = await response.json(); } catch (jsonError) { const text = await response.text(); showResult('anchor-result', 'error', `Erreur de parsing de la réponse: ${text}`); return; } if (!response.ok) { const errorMessage = data.message || data.error || `Erreur ${response.status}: ${response.statusText}`; console.error('Watermark API error:', { status: response.status, statusText: response.statusText, data: data, requestBody: { hasApiKey: !!requestBody.apiKey, hasWatermarkOptions: !!requestBody.watermarkOptions, watermarkEnabled: requestBody.watermarkOptions?.enabled, hasFileData: !!requestBody.fileData, hasTextContent: !!requestBody.textContent, }, }); showResult('anchor-result', 'error', errorMessage); return; } if (data.success) { const mempoolBaseUrl = 'https://mempool.4nkweb.com/fr'; const originalTxidLink = data.original.txid ? `${data.original.txid}` : 'N/A'; const watermarkedTxidLink = data.watermarked.txid ? `${data.watermarked.txid}` : 'N/A'; let resultHtml = ` ✅ Documents ancrés avec succès !

Document original :
Hash SHA256 : ${data.original.hash || 'N/A'}
TXID : ${originalTxidLink}
Statut : ${data.original.status}
Confirmations : ${data.original.confirmations || 0}
Fichier : ${data.original.file.name}

Document filigrané :
Hash SHA256 : ${data.watermarked.hash || 'N/A'}
TXID : ${watermarkedTxidLink}
Statut : ${data.watermarked.status}
Confirmations : ${data.watermarked.confirmations || 0}
Fichier : ${data.watermarked.file.name}

`; if (data.certificate && data.certificate.data) { resultHtml += ` Certificat :
Fichier : ${data.certificate.name}

`; } if (data.merged && data.merged.data) { resultHtml += ` Document fusionné (filigrané + certificat) :
Fichier : ${data.merged.name}

`; } resultHtml += `

📥 Téléchargement automatique en cours...

`; showResult('anchor-result', 'success', resultHtml); // Télécharger automatiquement les 4 documents avec un délai entre chaque setTimeout(() => { downloadFile(data.original.file.name, data.original.file.data); }, 500); setTimeout(() => { downloadFile(data.watermarked.file.name, data.watermarked.file.data); }, 1000); if (data.certificate && data.certificate.data) { setTimeout(() => { downloadFile(data.certificate.name, data.certificate.data); }, 1500); } if (data.merged && data.merged.data) { setTimeout(() => { downloadFile(data.merged.name, data.merged.data); }, 2000); } // Recharger le nombre d'ancrages après un court délai setTimeout(loadAnchorCount, 2000); } else { showResult('anchor-result', 'error', data.message || data.error || 'Erreur lors de l\'ancrage avec filigrane.'); } } catch (error) { console.error('Error in anchorWithWatermark:', error); showResult('anchor-result', 'error', `Erreur lors de l'ancrage avec filigrane: ${error.message}`); } } /** * Télécharge un fichier depuis base64 */ function downloadFile(fileName, base64Data) { const byteCharacters = atob(base64Data); const byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); const blob = new Blob([byteArray], { type: 'application/pdf' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } /** * Demande des sats via le faucet */ async function requestFaucet() { const apiKey = document.getElementById('faucet-api-key').value.trim(); const address = document.getElementById('faucet-address').value.trim(); if (!apiKey) { showResult('faucet-result', 'error', 'Veuillez entrer une clé API.'); return; } if (!address) { showResult('faucet-result', 'error', 'Veuillez entrer une adresse Bitcoin.'); return; } // Validation basique de l'adresse const addressPattern = /^(tb1|bcrt1|2|3)[a-zA-HJ-NP-Z0-9]{25,62}$/; if (!addressPattern.test(address)) { showResult('faucet-result', 'error', 'Adresse Bitcoin invalide.'); return; } try { showResult('faucet-result', 'info', 'Demande en cours...'); const response = await fetch(`${FAUCET_API_URL}/api/faucet/request`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, }, body: JSON.stringify({ address }), }); const data = await response.json(); if (response.ok && data.txid) { showResult('faucet-result', 'success', `50 000 sats envoyés avec succès !
TXID : ${data.txid}
Montant : ${data.amount || '50000'} sats
Statut : ${data.status || 'En attente de confirmation'}`); } else { showResult('faucet-result', 'error', data.message || data.error || 'Erreur lors de la demande.'); } } catch (error) { showResult('faucet-result', 'error', `Erreur : ${error.message}`); } } /** * Affiche un résultat */ function showResult(elementId, type, message) { const element = document.getElementById(elementId); element.className = `result ${type}`; element.innerHTML = message; }