USER
que fait ce code server.js // server.js
const express = require('express');
const ganache = require('ganache');
const solc = require('solc');
const { ethers } = require('ethers');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const app = express();
const apiPort = 3000;
const ganachePort = 8545;
const JWT_SECRET = 'change_cette_cle_secrete_en_prod'; // Change en prod
const TOKEN_EXPIRATION = '1h';
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
// --- Base de données SQLite ---
const db = new sqlite3.Database('./blockchain.db', err => {
if (err) console.error('Erreur DB:', err);
else console.log('DB SQLite connectée.');
});
db.serialize(() => {
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
passwordHash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
db.run(`
CREATE TABLE IF NOT EXISTS deployedContracts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
address TEXT UNIQUE,
abi TEXT,
blockNumber INTEGER
)`);
db.run(`
CREATE TABLE IF NOT EXISTS transactionsHistory (
txHash TEXT PRIMARY KEY,
txJson TEXT
)`);
});
// --- Fonctions DB ---
function insertUser(username, passwordHash, role = 'user') {
return new Promise((resolve, reject) => {
db.run(`INSERT INTO users (username, passwordHash, role) VALUES (?, ?, ?)`,
[username, passwordHash, role],
function(err) {
if(err) reject(err);
else resolve(this.lastID);
});
});
}
function getUserByUsername(username) {
return new Promise((resolve, reject) => {
db.get(`SELECT * FROM users WHERE username = ?`, [username], (err, row) => {
if(err) reject(err);
else resolve(row);
});
});
}
function insertContract(c) {
return new Promise((resolve, reject) => {
db.run(
`INSERT OR IGNORE INTO deployedContracts (name, address, abi, blockNumber) VALUES (?, ?, ?, ?)`,
[c.name, c.address, JSON.stringify(c.abi), c.blockNumber],
function(err) {
if(err) reject(err);
else resolve(this.lastID);
});
});
}
function getAllContracts() {
return new Promise((resolve, reject) => {
db.all(`SELECT name, address, abi, blockNumber FROM deployedContracts`, [], (err, rows) => {
if (err) reject(err);
else resolve(rows.map(r => ({
name: r.name,
address: r.address,
abi: JSON.parse(r.abi),
blockNumber: r.blockNumber
})));
});
});
}
function insertTransactions(txs) {
return new Promise((resolve, reject) => {
const stmt = db.prepare(`INSERT OR IGNORE INTO transactionsHistory (txHash, txJson) VALUES (?, ?)`);
txs.forEach(tx => {
stmt.run(tx.hash, JSON.stringify(tx));
});
stmt.finalize(err => {
if(err) reject(err);
else resolve();
});
});
}
function getAllTransactions() {
return new Promise((resolve, reject) => {
db.all(`SELECT txJson FROM transactionsHistory ORDER BY rowid DESC LIMIT 100`, [], (err, rows) => {
if (err) reject(err);
else resolve(rows.map(r => JSON.parse(r.txJson)));
});
});
}
// --- Middleware Auth JWT ---
function authMiddleware(req, res, next) {
if (req.path === '/login' || req.path === '/signup' || req.path.startsWith('/static') || req.path.startsWith('/api/signup') || req.path.startsWith('/api/login')) return next();
const authHeader = req.headers['authorization'];
if (!authHeader || !authHeader.startsWith('Bearer '))
return res.status(401).json({ error: 'Token manquant' });
const token = authHeader.slice(7);
jwt.verify(token, JWT_SECRET, (err, decoded) => {
if (err) return res.status(403).json({ error: 'Token invalide ou expiré' });
req.user = decoded;
next();
});
}
function adminOnly(req, res, next) {
if (req.user?.role !== 'admin') return res.status(403).json({ error: 'Accès réservé aux admins' });
next();
}
app.use(authMiddleware);
// --- Routes API ---
// Signup
app.post('/api/signup', async (req, res) => {
try {
const { username, password, role } = req.body;
if (!username || !password)
return res.status(400).json({ error: 'username et password requis' });
if (!/^[a-zA-Z0-9]{3,20}$/.test(username))
return res.status(400).json({ error: 'Nom utilisateur invalide (3-20 caractères alphanumériques)' });
if (password.length < 6)
return res.status(400).json({ error: 'Mot de passe trop court (minimum 6 caractères)' });
const existing = await getUserByUsername(username);
if (existing)
return res.status(409).json({ error: 'Utilisateur déjà existant' });
const passwordHash = await bcrypt.hash(password, 10);
const userRole = role === 'admin' ? 'admin' : 'user';
await insertUser(username, passwordHash, userRole);
res.json({ message: 'Utilisateur créé' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Login
app.post('/api/login', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password)
return res.status(400).json({ error: 'username et password requis' });
const user = await getUserByUsername(username);
if (!user) return res.status(401).json({ error: 'Utilisateur inconnu' });
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) return res.status(401).json({ error: 'Mot de passe invalide' });
const token = jwt.sign({ username: user.username, role: user.role }, JWT_SECRET, { expiresIn: TOKEN_EXPIRATION });
res.json({ token });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// --- Ganache Setup ---
const ganacheServer = ganache.server({
logging: { quiet: true },
wallet: { totalAccounts: 10, defaultBalance: 100 }
});
ganacheServer.listen(ganachePort, () => {
console.log(`Ganache server listening on port ${ganachePort}`);
});
const provider = new ethers.providers.JsonRpcProvider(`http://localhost:${ganachePort}`);
// --- Mettre à jour historique des transactions en DB ---
async function updateTransactionHistory() {
try {
const latestBlockNum = await provider.getBlockNumber();
const txs = [];
for (let i = latestBlockNum; i > latestBlockNum - 20 && i >= 0; i--) {
const block = await provider.getBlockWithTransactions(i);
if (block && block.transactions) txs.push(...block.transactions);
}
await insertTransactions(txs);
} catch (err) {
console.error('Erreur updateTransactionHistory:', err);
}
}
async function autoUpdateTransactionHistory() {
await updateTransactionHistory();
setTimeout(autoUpdateTransactionHistory, 15000);
}
autoUpdateTransactionHistory();
// --- API Blockchain ---
// Comptes + soldes
app.get('/api/accounts', async (req, res) => {
try {
const accounts = await provider.listAccounts();
const balances = {};
for (const addr of accounts) {
const bal = await provider.getBalance(addr);
balances[addr] = Number(ethers.utils.formatEther(bal));
}
res.json({ accounts, balances });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Envoyer transaction
app.post('/api/sendTransaction', async (req, res) => {
try {
const { from, to, value } = req.body;
if (!from || !to || !value) return res.status(400).json({ error: 'from, to et value requis' });
const signer = provider.getSigner(from);
const txResponse = await signer.sendTransaction({
to,
value: ethers.utils.parseEther(value.toString()),
gasLimit: 21000
});
await txResponse.wait();
await updateTransactionHistory(); // mise à jour DB
res.json({ txHash: txResponse.hash });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Blocs paginés
app.get('/api/blocks', async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 5;
const latestBlockNum = await provider.getBlockNumber();
const blocks = [];
const startBlock = latestBlockNum - (page - 1) * limit;
for (let i = startBlock; i > startBlock - limit && i >= 0; i--) {
const block = await provider.getBlockWithTransactions(i);
if (block) blocks.push(block);
}
res.json({ blocks, page, limit });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Transactions en historique
app.get('/api/transactions', async (req, res) => {
try {
const txs = await getAllTransactions();
res.json({ transactions: txs });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Déployer contrat Solidity (ADMIN ONLY)
app.post('/api/contracts/deploy', adminOnly, async (req, res) => {
try {
const { code, contractName } = req.body;
if (!code || !contractName)
return res.status(400).json({ error: 'code et contractName requis' });
const input = {
language: 'Solidity',
sources: { 'Contract.sol': { content: code } },
settings: { outputSelection: { '*': { '*': ['abi', 'evm.bytecode'] } } }
};
const output = JSON.parse(solc.compile(JSON.stringify(input)));
if (output.errors) {
const warnings = output.errors.filter(e => e.severity === 'warning');
warnings.forEach(w => console.warn('Warning:', w.formattedMessage));
const errors = output.errors.filter(e => e.severity === 'error');
if (errors.length) {
const msg = errors.map(e => e.formattedMessage).join('\n');
return res.status(400).json({ error: msg });
}
}
const compiled = output.contracts['Contract.sol'][contractName];
if (!compiled)
return res.status(400).json({ error: `Contrat ${contractName} introuvable.` });
const abi = compiled.abi;
const bytecode = compiled.evm.bytecode.object;
const signer = provider.getSigner(0);
const factory = new ethers.ContractFactory(abi, bytecode, signer);
const contract = await factory.deploy();
await contract.deployed();
const blockNumber = await provider.getBlockNumber();
await insertContract({
name: contractName,
address: contract.address,
abi,
blockNumber,
});
res.json({ address: contract.address });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Récupérer contrats déployés
app.get('/api/contracts', async (req, res) => {
try {
const contracts = await getAllContracts();
res.json({ contracts });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Recevoir détails receipt (reçu) d'une tx
app.get('/api/txreceipt/:txHash', async (req, res) => {
try {
const { txHash } = req.params;
if (!txHash) return res.status(400).json({ error: 'txHash obligatoire' });
const receipt = await provider.getTransactionReceipt(txHash);
res.json(receipt);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.listen(apiPort, () => {
console.log(`Serveur API démarré sur http://localhost:${apiPort}`);
}); index.html <!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ganache Web UI avancé - Modèle Onglets</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background: #f9f9f9;}
header {
background: #2a3f54; color: white; padding: 1em; text-align: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
nav {
display: flex;
background: #34495e;
border-bottom: 2px solid #16a085;
}
nav button {
flex: 1;
background: #34495e;
border: none;
padding: 1em;
color: white;
cursor: pointer;
font-weight: bold;
font-size: 1em;
transition: background 0.3s;
}
nav button:hover,
nav button.active {
background: #16a085;
outline: none;
}
main {
padding: 1em 1.5em;
max-width: 1000px;
margin: auto;
}
section {
display: none;
}
section.active {
display: block;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 0.75em;
}
th, td {
border: 1px solid #ccc;
padding: 0.5em 0.8em;
text-align: left;
}
th {
background: #ecf0f1;
}
#searchTx {
float: right;
margin-bottom: 0.75em;
width: 300px;
padding: 0.3em 0.5em;
border: 1px solid #ccc;
border-radius: 4px;
}
label {
display: block;
margin: 10px 0 6px;
}
input[type="text"], input[type="number"], textarea, select {
width: 100%;
padding: 0.5em;
box-sizing: border-box;
border-radius: 4px;
border: 1px solid #ccc;
font-size: 1em;
}
textarea {
font-family: monospace;
}
button[type=submit], button {
background: #16a085;
color: white;
border: none;
padding: 0.6em 1.2em;
border-radius: 6px;
cursor: pointer;
font-size: 1em;
margin-top: 0.8em;
transition: background 0.3s;
}
button[type=submit]:hover, button:hover {
background: #138d75;
}
p#deployResult, p#sendTxResult, p#functionResult {
margin-top: 0.5em;
font-weight: bold;
}
/* Clear floats */
.clearfix::after {
content: "";
clear: both;
display: table;
}
/* Modal */
#modal {
display: none;
position: fixed;
z-index: 1000;
padding-top: 100px;
left: 0; top: 0; width: 100%; height: 100%;
overflow: auto; background-color: rgba(0,0,0,0.4);
}
#modalContent {
background-color: #fefefe;
margin: auto;
padding: 1em;
border: 1px solid #888;
width: 90%;
max-width: 600px;
border-radius: 8px;
position: relative;
white-space: pre-wrap;
font-family: monospace;
max-height: 60vh;
overflow-y: auto;
}
#closeModal {
color: #aaa;
position: absolute;
right: 10px;
top: 10px;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
#closeModal:hover {
color: black;
}
/* Pagination buttons */
#prevPageBtn, #nextPageBtn {
background: #16a085;
color: white;
padding: 0.4em 1em;
border: none;
border-radius: 5px;
cursor: pointer;
margin: 0 1em;
font-size: 1em;
}
#prevPageBtn:hover, #nextPageBtn:hover {
background: #138d75;
}
#pageNumber {
font-weight: bold;
font-size: 1.1em;
}
</style>
</head>
<body>
<header>
<h1>Ganache Web UI avancé</h1>
</header>
<nav aria-label="Navigation des sections">
<button class="tabBtn active" type="button" data-target="accounts" aria-selected="true" role="tab" tabindex="0">Comptes</button>
<button class="tabBtn" type="button" data-target="transactions" aria-selected="false" role="tab" tabindex="-1">Transactions</button>
<button class="tabBtn" type="button" data-target="blocks" aria-selected="false" role="tab" tabindex="-1">Blocs</button>
<button class="tabBtn" type="button" data-target="contracts" aria-selected="false" role="tab" tabindex="-1">Contrats</button>
<button class="tabBtn" type="button" data-target="deploy-contract" aria-selected="false" role="tab" tabindex="-1">Déployer contrat</button>
<button class="tabBtn" type="button" data-target="send-tx" aria-selected="false" role="tab" tabindex="-1">Envoyer TX</button>
<button class="tabBtn" type="button" data-target="interact-contract" aria-selected="false" role="tab" tabindex="-1">Interagir contrat</button>
</nav>
<main>
<section id="accounts" role="tabpanel" aria-labelledby="tab-accounts" class="active">
<h2 class="section-title">Comptes</h2>
<table id="accounts-table" aria-describedby="accounts-desc">
<thead><tr><th>Adresse</th><th>Solde (ETH)</th></tr></thead>
<tbody></tbody>
</table>
</section>
<section id="transactions" role="tabpanel" aria-labelledby="tab-transactions" hidden>
<h2 class="section-title clearfix">
Transactions
<input id="searchTx" placeholder="Rechercher hash ou adresse..." aria-label="Rechercher transactions" />
</h2>
<table id="transactions-table" aria-describedby="transactions-desc">
<thead><tr><th>Hash</th><th>De</th><th>À</th><th>Valeur (ETH)</th><th>Voir logs</th></tr></thead>
<tbody></tbody>
</table>
</section>
<section id="blocks" role="tabpanel" aria-labelledby="tab-blocks" hidden>
<h2 class="section-title">Blocs</h2>
<table id="blocks-table" aria-describedby="blocks-desc">
<thead><tr><th>Numéro</th><th>Mineur</th><th>Timestamp</th><th>Tx count</th></tr></thead>
<tbody></tbody>
</table>
<div style="text-align:center; margin-top: 1em;">
<button id="prevPageBtn" type="button">Précédent</button>
<span id="pageNumber">1</span>
<button id="nextPageBtn" type="button">Suivant</button>
</div>
</section>
<section id="contracts" role="tabpanel" aria-labelledby="tab-contracts" hidden>
<h2 class="section-title">Contrats</h2>
<table id="contracts-table" aria-describedby="contracts-desc">
<thead><tr><th>Nom</th><th>Adresse</th><th>Bloc déploiement</th></tr></thead>
<tbody></tbody>
</table>
</section>
<section id="deploy-contract" role="tabpanel" aria-labelledby="tab-deploy-contract" hidden>
<h2 class="section-title">Déployer un contrat</h2>
<form id="deployForm">
<label for="contractName">Nom du contrat:</label>
<input type="text" id="contractName" required />
<label for="contractCode">Code Solidity:</label>
<textarea id="contractCode" rows="10" required>pragma solidity ^0.8.0;
contract SimpleStorage {
uint256 storedData;
event DataStored(uint256 data);
function set(uint256 x) public {
storedData = x;
emit DataStored(x);
}
function get() public view returns (uint256) {
return storedData;
}
}</textarea>
<button type="submit">Déployer</button>
</form>
<p id="deployResult"></p>
</section>
<section id="send-tx" role="tabpanel" aria-labelledby="tab-send-tx" hidden>
<h2 class="section-title">Envoyer une transaction</h2>
<form id="sendTxForm">
<label for="txFrom">De:</label>
<select id="txFrom" required></select>
<label for="txTo">À:</label>
<input type="text" id="txTo" placeholder="Adresse destination" required/>
<label for="txValue">Valeur (ETH):</label>
<input type="number" id="txValue" min="0" step="0.0001" required/>
<button type="submit">Envoyer</button>
</form>
<p id="sendTxResult"></p>
</section>
<section id="interact-contract" role="tabpanel" aria-labelledby="tab-interact-contract" hidden>
<h2>Interagir avec un contrat déployé</h2>
<label for="contractSelect">Choisir contrat:</label>
<select id="contractSelect"></select>
<label for="functionSelect">Fonction:</label>
<select id="functionSelect"></select>
<div id="functionArgs" style="display:none;"></div>
<button id="callFunctionBtn">Appeler</button>
<p id="functionResult"></p>
</section>
</main>
<!-- Modal logs -->
<div id="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle" aria-describedby="modalDesc">
<div id="modalContent">
<span id="closeModal" aria-label="Fermer la fenêtre">×</span>
<pre id="modalText"></pre>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/ethers@5.7.2/dist/ethers.min.js"></script>
<script>
// Token API utilisé dans chaque requête
const API_TOKEN = 'mon-token-secret';
const authHeaders = {
'Authorization': `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json',
};
// Gestion des onglets
const tabs = document.querySelectorAll('nav button.tabBtn');
const sections = document.querySelectorAll('main > section');
function activateTab(tab) {
tabs.forEach(t => {
t.classList.remove('active');
t.setAttribute('aria-selected', 'false');
t.setAttribute('tabindex', '-1');
});
sections.forEach(s => {
s.hidden = true;
s.classList.remove('active');
});
tab.classList.add('active');
tab.setAttribute('aria-selected', 'true');
tab.removeAttribute('tabindex');
const target = tab.dataset.target;
const section = document.getElementById(target);
if (section) {
section.hidden = false;
section.classList.add('active');
}
}
tabs.forEach(tab => {
tab.addEventListener('click', () => activateTab(tab));
tab.addEventListener('keydown', e => {
if (e.key === 'ArrowRight') {
e.preventDefault();
let next = tab.nextElementSibling || tabs[0];
next.focus();
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
let prev = tab.previousElementSibling || tabs[tabs.length - 1];
prev.focus();
}
});
});
activateTab(document.querySelector('nav button.tabBtn.active'));
let currentBlockPage = 1;
const blocksPerPage = 5;
let transactions = [];
// Modal
const modal = document.getElementById('modal');
const modalText = document.getElementById('modalText');
const closeModal = document.getElementById('closeModal');
closeModal.onclick = () => (modal.style.display = 'none');
window.onclick = e => {
if (e.target == modal) modal.style.display = 'none';
};
// --- Chargement Comptes ---
async function loadAccounts() {
const tbody = document.querySelector('#accounts-table tbody');
tbody.innerHTML = '';
try {
const resp = await fetch('/api/accounts', { headers: authHeaders });
const data = await resp.json();
data.accounts.forEach(addr => {
const tr = document.createElement('tr');
tr.innerHTML = `<td>${addr}</td><td>${data.balances[addr].toFixed(4)}</td>`;
tbody.appendChild(tr);
});
// Update select for "from" in send tx form
const txFromSelect = document.getElementById('txFrom');
const currentValue = txFromSelect.value;
txFromSelect.innerHTML = '';
data.accounts.forEach(addr => {
const opt = document.createElement('option');
opt.value = addr;
opt.textContent = addr;
txFromSelect.appendChild(opt);
});
if (data.accounts.includes(currentValue)) {
txFromSelect.value = currentValue;
}
} catch (e) {
tbody.innerHTML = '<tr><td colspan="2">Erreur chargement comptes</td></tr>';
console.error(e);
}
}
// --- Chargement Transactions ---
async function loadTransactions() {
const tbody = document.querySelector('#transactions-table tbody');
tbody.innerHTML = '';
try {
const resp = await fetch('/api/transactions', { headers: authHeaders });
const data = await resp.json();
transactions = data.transactions || [];
displayTransactions(transactions);
} catch (e) {
tbody.innerHTML = '<tr><td colspan="5">Erreur chargement transactions</td></tr>';
console.error(e);
}
}
function displayTransactions(txList) {
const tbody = document.querySelector('#transactions-table tbody');
tbody.innerHTML = '';
if (!txList.length) {
tbody.innerHTML = '<tr><td colspan="5">Aucune transaction trouvée</td></tr>';
return;
}
txList.forEach(tx => {
const valueEth = ethers.utils.formatEther(tx.value);
const from = tx.from ?? 'N/A';
const to = tx.to ?? 'Création contrat';
const tr = document.createElement('tr');
tr.innerHTML = `
<td style="font-family: monospace;">${tx.hash}</td>
<td>${from}</td>
<td>${to}</td>
<td>${valueEth}</td>
<td><button data-hash="${tx.hash}" type="button">Voir logs</button></td>
`;
tbody.appendChild(tr);
});
}
document.getElementById('searchTx').addEventListener('input', (e) => {
const search = e.target.value.toLowerCase();
const filtered = transactions.filter(tx =>
tx.hash.toLowerCase().includes(search) ||
(tx.from && tx.from.toLowerCase().includes(search)) ||
(tx.to && tx.to.toLowerCase().includes(search))
);
displayTransactions(filtered);
});
// Modal logs on click "Voir logs"
document.querySelector('#transactions-table tbody').addEventListener('click', async (e) => {
if (e.target.tagName === 'BUTTON') {
const hash = e.target.getAttribute('data-hash');
try {
const resp = await fetch(`/api/txreceipt/${hash}`, { headers: authHeaders });
const receipt = await resp.json();
if (!receipt) {
modalText.textContent = 'Aucun receipt trouvé pour cette transaction.';
} else {
const contractsResp = await fetch('/api/contracts', { headers: authHeaders });
const contractsData = await contractsResp.json();
const knownABIs = contractsData.contracts.map(c => ({
address: c.address.toLowerCase(),
abi: c.abi
}));
function decodeLogs(logs) {
let text = '';
for (const log of logs) {
const c = knownABIs.find(c => c.address === log.address.toLowerCase());
if (c) {
const iface = new ethers.utils.Interface(c.abi);
try {
const parsedLog = iface.parseLog(log);
text += `Événement ${parsedLog.name} :\n`;
parsedLog.eventFragment.inputs.forEach((input, i) => {
text += ` ${input.name} : ${parsedLog.args[i]}\n`;
});
} catch {
text += `Log non décodable à ${log.address}\n`;
}
} else {
text += `Log à une adresse inconnue ${log.address}\n`;
}
text += '\n';
}
return text || 'Aucun log décodable.';
}
modalText.textContent = decodeLogs(receipt.logs || []);
}
modal.style.display = 'block';
} catch (err) {
modalText.textContent = 'Erreur récupération logs : ' + err.message;
modal.style.display = 'block';
}
}
});
// --- Chargement Blocs ---
async function loadBlocks(page = 1) {
const tbody = document.querySelector('#blocks-table tbody');
const pageNumSpan = document.getElementById('pageNumber');
tbody.innerHTML = '';
try {
const resp = await fetch(`/api/blocks?page=${page}&limit=${blocksPerPage}`, {headers: authHeaders});
const data = await resp.json();
if (!data.blocks || data.blocks.length === 0) {
tbody.innerHTML = '<tr><td colspan="4">Aucun bloc trouvé.</td></tr>';
return;
}
data.blocks.forEach(block => {
const ts = new Date(block.timestamp * 1000).toLocaleString();
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${block.number}</td>
<td>${block.miner}</td>
<td>${ts}</td>
<td>${block.transactions.length}</td>
`;
tbody.appendChild(tr);
});
pageNumSpan.textContent = page;
currentBlockPage = page;
} catch (e) {
tbody.innerHTML = '<tr><td colspan="4">Erreur chargement blocs</td></tr>';
console.error(e);
}
}
document.getElementById('prevPageBtn').addEventListener('click', () => {
if (currentBlockPage > 1) loadBlocks(currentBlockPage - 1);
});
document.getElementById('nextPageBtn').addEventListener('click', () => {
loadBlocks(currentBlockPage + 1);
});
// --- Chargement Contrats ---
async function loadContracts() {
const tbody = document.querySelector('#contracts-table tbody');
tbody.innerHTML = '';
try {
const resp = await fetch('/api/contracts', { headers: authHeaders });
const data = await resp.json();
if (!data.contracts || !data.contracts.length) {
tbody.innerHTML = '<tr><td colspan="3">Aucun contrat déployé.</td></tr>';
} else {
data.contracts.forEach(c => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${c.name}</td>
<td style="font-family: monospace;">${c.address}</td>
<td>${c.blockNumber}</td>
`;
tbody.appendChild(tr);
});
}
// Pour la section interagir contrat - alimentation dropdown
const contractSelect = document.getElementById('contractSelect');
contractSelect.innerHTML = '';
if (data.contracts && data.contracts.length) {
data.contracts.forEach(c => {
const opt = document.createElement('option');
opt.value = c.address;
opt.textContent = `${c.name} (${c.address.slice(0, 6)}...)`;
contractSelect.appendChild(opt);
});
// Déclencher chargement fonctions du premier contrat si présent
contractSelect.dispatchEvent(new Event('change'));
} else {
contractSelect.innerHTML = '<option>Aucun contrat déployé</option>';
}
} catch (e) {
tbody.innerHTML = '<tr><td colspan="3">Erreur chargement contrats</td></tr>';
console.error(e);
}
}
// --- Déploiement contrat formulaire ---
const deployForm = document.getElementById('deployForm');
const deployResult = document.getElementById('deployResult');
deployForm.addEventListener('submit', async (e) => {
e.preventDefault();
deployResult.style.color = 'black';
deployResult.textContent = 'Déploiement en cours...';
const code = document.getElementById('contractCode').value;
const contractName = document.getElementById('contractName').value.trim();
if (!code || !contractName) {
deployResult.style.color = 'red';
deployResult.textContent = 'Nom et code requis.';
return;
}
try {
const resp = await fetch('/api/contracts/deploy', {
method: 'POST',
headers: authHeaders,
body: JSON.stringify({ code, contractName }),
});
const data = await resp.json();
if (data.error) {
deployResult.style.color = 'red';
deployResult.textContent = data.error;
} else {
deployResult.style.color = 'green';
deployResult.textContent = `Contrat déployé à l'adresse ${data.address}`;
deployForm.reset();
await loadContracts();
}
} catch (e) {
deployResult.style.color = 'red';
deployResult.textContent = 'Erreur serveur : ' + e.message;
}
});
// --- Envoi transaction complet ---
const sendTxForm = document.getElementById('sendTxForm');
const sendTxResult = document.getElementById('sendTxResult');
sendTxForm.addEventListener('submit', async e => {
e.preventDefault();
sendTxResult.style.color = 'black';
sendTxResult.textContent = 'Envoi en cours...';
const from = document.getElementById('txFrom').value;
const to = document.getElementById('txTo').value.trim();
const value = parseFloat(document.getElementById('txValue').value);
if (!from || !to || isNaN(value) || value <= 0) {
sendTxResult.style.color = 'red';
sendTxResult.textContent = 'Veuillez remplir tous les champs correctement.';
return;
}
try {
const resp = await fetch('/api/sendTransaction', {
method: 'POST',
headers: authHeaders,
body: JSON.stringify({ from, to, value }),
});
const data = await resp.json();
if (data.error) {
sendTxResult.style.color = 'red';
sendTxResult.textContent = 'Erreur : ' + data.error;
} else {
sendTxResult.style.color = 'green';
sendTxResult.textContent = 'Transaction envoyée ! Hash: ' + data.txHash;
sendTxForm.reset();
await loadAccounts();
await loadTransactions();
await loadBlocks(currentBlockPage);
}
} catch (e) {
sendTxResult.style.color = 'red';
sendTxResult.textContent = 'Erreur serveur : ' + e.message;
}
});
// --- Interaction dynamique complet avec contrat ---
const functionSelect = document.getElementById('functionSelect');
const functionArgsDiv = document.getElementById('functionArgs');
const callFunctionBtn = document.getElementById('callFunctionBtn');
const functionResult = document.getElementById('functionResult');
let currentABI = null;
contractSelect.addEventListener('change', async () => {
functionSelect.innerHTML = '';
functionArgsDiv.innerHTML = '';
functionResult.textContent = '';
const addr = contractSelect.value;
if (!addr) return;
try {
const resp = await fetch('/api/contracts', { headers: authHeaders });
const data = await resp.json();
const contractInfo = data.contracts.find(c => c.address === addr);
if (!contractInfo) throw new Error('Contrat introuvable');
currentABI = contractInfo.abi;
const iface = new ethers.utils.Interface(currentABI);
const functions = Object.values(iface.fragments).filter(frag => frag.type === 'function');
functions.forEach(f => {
const opt = document.createElement('option');
opt.value = f.name;
opt.textContent = `${f.name}(${f.inputs.map(inp => inp.type).join(', ')}) [${f.stateMutability}]`;
functionSelect.appendChild(opt);
});
if (functions.length > 0) {
loadFunctionInputs(functionSelect.value);
}
} catch (e) {
functionResult.style.color = 'red';
functionResult.textContent = 'Erreur chargement fonctions : ' + e.message;
}
});
functionSelect.addEventListener('change', (e) => {
loadFunctionInputs(e.target.value);
});
function loadFunctionInputs(fnName) {
functionArgsDiv.innerHTML = '';
if (!currentABI) return;
try {
const iface = new ethers.utils.Interface(currentABI);
const fnFragment = iface.getFunction(fnName);
if (!fnFragment) return;
if (fnFragment.inputs.length === 0) {
functionArgsDiv.style.display = 'none';
return;
}
functionArgsDiv.style.display = 'block';
fnFragment.inputs.forEach((input, i) => {
const label = document.createElement('label');
label.textContent = `${input.name} (${input.type}) :`;
label.htmlFor = `arg_${i}`;
const inputEl = document.createElement('input');
inputEl.id = `arg_${i}`;
inputEl.name = `arg_${i}`;
inputEl.type = 'text';
functionArgsDiv.appendChild(label);
functionArgsDiv.appendChild(inputEl);
});
} catch(e) {
functionArgsDiv.style.display = 'none';
}
}
callFunctionBtn.addEventListener('click', async () => {
const addr = contractSelect.value;
const fnName = functionSelect.value;
functionResult.style.color = 'black';
functionResult.textContent = 'Chargement...';
try {
const resp = await fetch('/api/contracts', { headers: authHeaders });
const data = await resp.json();
const contractInfo = data.contracts.find(c => c.address === addr);
if (!contractInfo) throw new Error('Contrat introuvable');
const provider = new ethers.providers.JsonRpcProvider('http://localhost:8545');
const signer = provider.getSigner();
const contract = new ethers.Contract(addr, contractInfo.abi, signer);
const iface = new ethers.utils.Interface(contractInfo.abi);
const fnFragment = iface.getFunction(fnName);
const args = [];
for (let i = 0; i < fnFragment.inputs.length; i++) {
const inputEl = document.getElementById(`arg_${i}`);
if (!inputEl) throw new Error('Argument manquant');
const val = inputEl.value.trim();
if (fnFragment.inputs[i].type.startsWith('uint') || fnFragment.inputs[i].type.startsWith('int')) {
args.push(val === '' ? 0 : ethers.BigNumber.from(val));
} else if (fnFragment.inputs[i].type === 'address') {
args.push(val);
} else if (fnFragment.inputs[i].type === 'bool') {
args.push(val.toLowerCase() === 'true' || val === '1');
} else if (fnFragment.inputs[i].type === 'string') {
args.push(val);
} else {
args.push(val);
}
}
let result;
if (fnFragment.stateMutability === 'view' || fnFragment.stateMutability === 'pure') {
result = await contract[fnName](...args);
functionResult.style.color = 'green';
functionResult.textContent = `Résultat: ${result.toString()}`;
} else {
const tx = await contract[fnName](...args);
await tx.wait();
functionResult.style.color = 'green';
functionResult.textContent = `Transaction minée, hash: ${tx.hash}`;
}
} catch (err) {
functionResult.style.color = 'red';
functionResult.textContent = 'Erreur: ' + err.message;
}
});
// --- Chargement initial ---
async function autoReload() {
await Promise.all([
loadAccounts(),
loadTransactions(),
loadBlocks(currentBlockPage),
loadContracts()
]);
}
autoReload();
setInterval(autoReload, 15000);
</script>
</body>
</html> ASSISTANT
Ce code met en place une **application web complète** pour interagir avec une blockchain Ethereum locale simulée avec Ganache, avec une interface utilisateur riche et plusieurs fonctionnalités.
---
## Vue d'ensemble
### Backend (`server.js`)
- Utilise **Express.js** pour créer une API REST sur le port 3000.
- Lance un **serveur Ganache** (blockchain Ethereum locale simulée) sur le port 8545.
- Utilise **SQLite** pour stocker :
- les utilisateurs (avec gestion des rôles et mot de passe hashé),
- les contrats déployés,
- l'historique des transactions.
- Authentification avec **JWT** et **bcrypt** pour la sécurité des utilisateurs.
- Compilation et déploiement dynamique de contrats Solidity via `solc` et `ethers.js`.
- Fonctionnalités exposées via l'API :
- Inscription et connexion d'utilisateurs avec rôles (admin/user).
- Lecture des comptes et de leurs soldes Ethereum.
- Envoi de transactions Ethereum.
- Pagination et lecture des blocs.
- Historique des transactions.
- Déploiement de contrats uniquement par les admins.
- Liste des contrats déployés.
- Récupération des receipts de transactions et décodage des logs.
---
### Frontend (`index.html` + script JavaScript)
- Interface web en HTML/CSS/JS qui se connecte à l'API backend.
- Utilise **fetch avec Token JWT** (ici un token fixe `mon-token-secret` à remplacer pour la prod).
- Navigation par onglets (Comptes, Transactions, Blocs, Contrats, Déployer contrat, Envoyer transaction, Interagir contrat).
- Affichage des listes dynamiques :
- Comptes et leurs soldes ETH.
- Transactions (avec recherche).
- Blocs paginés.
- Contrats déployés.
- Permet de déployer un contrat Solidity directement depuis une zone de texte (avec exemple de contrat SimpleStorage).
- Permet d’envoyer des transactions ETH d’un compte à un autre.
- Permet d’interagir avec les fonctions des contrats déployés (appels en lecture ou modification avec gestion des arguments).
- Affiche les logs des transactions dans une modale avec décodage des événements.
---
## Fonctionnement détaillé
### 1. Backend
- **Ganache** simule un environnement blockchain local avec 10 comptes préfinancés.
- La **base SQLite** contient trois tables : utilisateurs, contrats déployés, historique des transactions.
- Middleware JWT protège les routes, seuls `/api/login` et `/api/signup` sont accessibles publiquement.
- Lorsqu’un contrat est déployé (route POST `/api/contracts/deploy`) :
- Le code Solidity fourni est compilé par `solc`.
- Est déployé sur Ganache via `ethers.js`.
- Stocké dans la base avec son adresse, ABI, blockNumber.
- Transactions envoyées via `/api/sendTransaction` sont minées sur Ganache.
- L’historique des transactions des 20 derniers blocs est régulièrement mis à jour dans la base.
- Les admins ont des droits supplémentaires comme le déploiement de contrats.
### 2. Frontend
- Charge dynamiquement les différentes données via l’API (comptes, transactions, blocs, contrats).
- Implémente les interactions utilisateurs :
- Choix d’un compte depuis lequel envoyer une transaction.
- Formulaire de déploiement d’un smart contract avec code éditable.
- Liste déroulante pour sélectionner un contrat déployé et ses fonctions.
- Formulaire dynamique pour saisir arguments des fonctions.
- Affiche les résultats (hash des tx, résultats de fonctions, erreurs).
- Offre une navigation claire par onglets avec gestion accessible (ARIA).
- Affiche les logs d'événements décodés dans les transactions.
- Rafraîchissement automatique des données toutes les 15 secondes.
---
## En résumé
**Ce projet est une application web complète permettant :**
- de simuler une blockchain Ethereum locale via Ganache,
- de s’inscrire / se connecter en tant qu’utilisateur avec rôles,
- de visualiser les comptes, soldes, transactions, blocs,
- de déployer, lister et interagir avec des contrats Solidity,
- d’envoyer des transactions entre comptes,
- et de stocker toutes ces informations dans une base SQLite locale.
L'ensemble fournit une interface web intuitive et un serveur API sécurisé pour manipuler cette blockchain locale pour des fins de développement, test, apprentissage ou démonstration.
---
### Remarque importante :
- Le JWT secret doit impérativement être changé avant mise en production.
- Le frontend utilise un token (`mon-token-secret`) codé en dur; la gestion authentification réelle est à implémenter pour correspondre avec la backend.
- Le déploiement de contrats et autres opérations blockchain sont faits en local uniquement (Ganache).
- Le projet peut être étendu pour intégrer Metamask, une vraie blockchain (testnet/mainnet), etc.
---
Si tu veux, je peux aussi t'aider à comprendre ou modifier certaines parties, ou à sécuriser le système, ou ajouter des fonctionnalités spécifiques.