/**
* 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 !