Commit 7b9fa16f authored by timothe 2004's avatar timothe 2004

change front to angular

parent aeb203b3
Pipeline #3122 canceled with stages
# DApp de Vote # DApp de Vote
Cette application décentralisée (DApp) permet de gérer un système de vote transparent sur la blockchain Ethereum. Cette application décentralisée (DApp) permet de gérer un système de vote transparent sur la blockchain Ethereum. Elle implémente un processus de vote complet, depuis l'enregistrement des électeurs jusqu'au dépouillement des résultats.
## Fonctionnalités ## Fonctionnalités
- Inscription des électeurs sur liste blanche par l'administrateur - Inscription des électeurs sur liste blanche par l'administrateur
- Enregistrement des propositions par les électeurs inscrits - Enregistrement des propositions par les électeurs inscrits
- Vote pour les propositions préférées - Vote pour les propositions
- Délégation de vote à d'autres électeurs
- Comptabilisation automatique des votes - Comptabilisation automatique des votes
- Vérification des résultats accessible à tous - Vérification des résultats accessible à tous
- Période limitée pour l'enregistrement des propositions et le vote
## Technologies utilisées
- **Smart Contract**: Solidity 0.8.20, OpenZeppelin
- **Backend**: Hardhat (environnement de développement Ethereum)
- **Frontend**: Angular 17
- **Web3**: ethers.js v5.7
- **Styling**: Bootstrap 5
## Structure du Projet ## Structure du Projet
- `contracts/` : Contient les smart contracts Solidity - `contracts/` : Smart contracts Solidity
- `frontend/` : Application React pour l'interface utilisateur - `Voting.sol` : Contrat principal de la DApp
- `scripts/` : Scripts de déploiement et d'interaction avec le contrat
- `deploy.js` : Script pour déployer le contrat sur la blockchain
- `frontend-angular/` : Interface utilisateur Angular
- `test/` : Tests automatisés pour les smart contracts - `test/` : Tests automatisés pour les smart contracts
## Installation ## Prérequis
- Node.js v16+
- npm v8+
- MetaMask ou un autre portefeuille Ethereum installé dans votre navigateur
## Installation et démarrage
1. **Cloner le dépôt et installer les dépendances**
```bash ```bash
# Installer les dépendances git clone <url-du-repo>
cd voting-dapp
npm install npm install
```
2. **Compiler les smart contracts**
```bash
npm run compile
```
3. **Démarrer un nœud Ethereum local**
```bash
npm run node
```
Ce nœud local s'exécutera à l'adresse `http://127.0.0.1:8545`.
4. **Déployer le contrat sur le nœud local**
Dans un nouveau terminal, exécutez:
```bash
npm run deploy
```
Notez l'adresse du contrat déployé, vous en aurez besoin pour configurer le frontend.
5. **Configurer et démarrer le frontend Angular**
```bash
cd frontend-angular
npm install
```
Pour configurer l'adresse du contrat, ouvrez le fichier `src/app/services/contract.service.ts` et modifiez la ligne suivante avec l'adresse de votre contrat déployé:
```javascript
private contractAddress = '<ADRESSE_DU_CONTRAT_DÉPLOYÉ>';
```
# Démarrer le serveur de développement Puis démarrez l'application:
```bash
npm run dev npm run dev
``` ```
## Déploiement L'application sera accessible à l'adresse `http://localhost:4200`.
## Guide d'utilisation
### 1. Connexion à MetaMask
- Au démarrage de l'application, cliquez sur "Connecter votre portefeuille"
- Autorisez la connexion dans MetaMask
- Assurez-vous d'être sur le réseau local Hardhat (localhost:8545, Chain ID: 1337)
### 2. Processus de vote
Le processus de vote suit plusieurs étapes:
#### Phase 1: Enregistrement des électeurs
- Seul l'administrateur (le déployeur du contrat) peut enregistrer des électeurs
- Entrez l'adresse Ethereum de chaque électeur dans le formulaire
#### Phase 2: Enregistrement des propositions
- L'administrateur démarre cette phase en spécifiant une durée en minutes
- Les électeurs enregistrés peuvent soumettre des propositions
#### Phase 3: Fin de l'enregistrement des propositions
- L'administrateur termine cette phase manuellement ou la période expire automatiquement
#### Phase 4: Session de vote
- L'administrateur démarre la session en spécifiant une durée en minutes
- Les électeurs peuvent voter pour une proposition ou déléguer leur vote
#### Phase 5: Fin de la session de vote
- L'administrateur termine cette phase manuellement ou la période expire automatiquement
#### Phase 6: Dépouillement des votes
- L'administrateur déclenche le comptage des votes
- Les résultats sont visibles par tous les utilisateurs
## Tests
### Tests des smart contracts
```bash
npm test
```
Ces tests vérifient:
- L'enregistrement des électeurs
- Le cycle de vie du workflow
- La soumission de propositions
- Le processus de vote et de délégation
- Le dépouillement des résultats
### Tests manuels du frontend
1. **Test du rôle administrateur**
- Utilisez le compte qui a déployé le contrat
- Vérifiez que vous pouvez enregistrer des électeurs et gérer le workflow
2. **Test du rôle électeur**
- Connectez-vous avec un compte enregistré comme électeur
- Soumettez des propositions et votez
3. **Test de l'utilisateur non autorisé**
- Connectez-vous avec un compte non enregistré
- Vérifiez que vous ne pouvez ni soumettre de propositions ni voter
## Fonctionnalités supplémentaires
1. **Limite de temps pour les phases**
- Les phases d'enregistrement des propositions et de vote ont des durées configurables
- Le système verrouille automatiquement les actions après expiration
2. **Système de délégation de vote**
- Les électeurs peuvent déléguer leur vote à un autre électeur enregistré
- Vérification des boucles de délégation pour éviter les références circulaires
## Utilisation en production
Pour un déploiement en production, vous devrez:
1. Modifier le fichier `hardhat.config.js` pour spécifier un réseau public (Mainnet, Goerli, etc.)
2. Ajouter une clé privée sécurisée ou utiliser un gestionnaire de clés
3. Déployer le contrat sur le réseau choisi
4. Configurer le frontend avec la nouvelle adresse de contrat
5. Déployer le frontend sur un service d'hébergement
Lien vers le déploiement public : [à venir] ## Dépannage
## Démo ### Problèmes courants
Lien vers la vidéo de démo : [à venir] 1. **Erreur "No such contract"**
- Vérifiez que le contrat est bien déployé
- Assurez-vous que l'adresse du contrat dans le frontend est correcte
## Organisation du Projet 2. **Erreur "Cannot estimate gas"**
- La transaction peut échouer à cause des règles du contrat
- Vérifiez que vous êtes dans la bonne phase du workflow
Ce projet a été développé en suivant une architecture modulaire permettant de séparer clairement la logique de contrat de l'interface utilisateur. 3. **MetaMask n'affiche pas la confirmation**
- Assurez-vous d'être sur le bon réseau (localhost:8545)
- Redémarrez MetaMask si nécessaire
## Fonctionnalités Supplémentaires ### Support
1. Système de délégation de vote Pour toute question ou problème, veuillez ouvrir une issue sur le dépôt GitHub.
2. Possibilité de définir une date limite automatique pour les phases de vote \ No newline at end of file
\ No newline at end of file
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"voting-dapp-frontend-angular": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/voting-dapp-frontend-angular",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "voting-dapp-frontend-angular:build:production"
},
"development": {
"browserTarget": "voting-dapp-frontend-angular:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "voting-dapp-frontend-angular:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
}
}
}
}
}
}
\ No newline at end of file
{
"name": "voting-dapp-frontend-angular",
"version": "0.1.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"dev": "ng serve"
},
"private": true,
"dependencies": {
"@angular/animations": "^17.1.0",
"@angular/common": "^17.1.0",
"@angular/compiler": "^17.1.0",
"@angular/core": "^17.1.0",
"@angular/forms": "^17.1.0",
"@angular/platform-browser": "^17.1.0",
"@angular/platform-browser-dynamic": "^17.1.0",
"@angular/router": "^17.1.0",
"ethers": "^5.7.2",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.1.1",
"@angular/cli": "^17.1.1",
"@angular/compiler-cli": "^17.1.0",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.3.2"
}
}
\ No newline at end of file
<div class="container-fluid py-4">
<app-header [account]="account" [isOwner]="isOwner"></app-header>
<div class="container my-4">
<div class="row justify-content-center">
<div class="col-lg-10">
<!-- Alerte d'erreur -->
<div *ngIf="error" class="alert alert-danger alert-dismissible fade show" role="alert">
{{ error }}
<button type="button" class="btn-close" (click)="error = null"></button>
</div>
<!-- Chargement global -->
<div *ngIf="loading" class="text-center my-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
<p class="mt-2">Traitement en cours...</p>
</div>
<!-- Connexion au portefeuille si déconnecté -->
<div *ngIf="!isConnected" class="card">
<div class="card-body text-center p-5">
<app-connect-wallet></app-connect-wallet>
</div>
</div>
<!-- Contenu principal si connecté -->
<div *ngIf="isConnected" class="fade-in">
<!-- Statut du workflow -->
<app-workflow-status
[currentStatus]="currentStatus"
[isOwner]="isOwner">
</app-workflow-status>
<!-- Gestion des électeurs (admin uniquement, workflow 0) -->
<app-voter-management
*ngIf="isOwner && currentStatus === '0'"
[currentStatus]="currentStatus">
</app-voter-management>
<!-- Gestion des propositions (électeurs inscrits, workflow 1) -->
<app-proposal-management
*ngIf="isVoter && currentStatus === '1'"
[currentStatus]="currentStatus">
</app-proposal-management>
<!-- Session de vote (électeurs inscrits, workflow 3) -->
<app-voting-session
*ngIf="isVoter && (currentStatus === '3' || currentStatus === '5')"
[currentStatus]="currentStatus">
</app-voting-session>
<!-- Message si pas d'action disponible -->
<div *ngIf="!isOwner && !isVoter" class="card">
<div class="card-body text-center p-4">
<h4>Accès limité</h4>
<p>Votre adresse n'est pas enregistrée comme électeur pour cette session de vote.</p>
<p>Veuillez contacter l'administrateur pour être ajouté à la liste des électeurs.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
\ No newline at end of file
/* Styles spécifiques au composant App */
\ No newline at end of file
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ContractService } from './services/contract.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit, OnDestroy {
title = 'DApp de Vote';
isConnected = false;
account: string | null = null;
isOwner = false;
isVoter = false;
currentStatus = '0';
loading = false;
error: string | null = null;
private subscriptions: Subscription[] = [];
constructor(private contractService: ContractService) {}
ngOnInit(): void {
this.subscriptions.push(
this.contractService.account$.subscribe(account => {
this.account = account;
this.isConnected = !!account;
}),
this.contractService.isOwner$.subscribe(isOwner => {
this.isOwner = isOwner;
}),
this.contractService.isVoter$.subscribe(isVoter => {
this.isVoter = isVoter;
}),
this.contractService.currentStatus$.subscribe(status => {
this.currentStatus = status;
}),
this.contractService.loading$.subscribe(loading => {
this.loading = loading;
}),
this.contractService.error$.subscribe(error => {
this.error = error;
})
);
}
ngOnDestroy(): void {
// Désabonnement de tous les observables
this.subscriptions.forEach(sub => sub.unsubscribe());
}
}
\ No newline at end of file
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
import { HeaderComponent } from './components/header/header.component';
import { ConnectWalletComponent } from './components/connect-wallet/connect-wallet.component';
import { WorkflowStatusComponent } from './components/workflow-status/workflow-status.component';
import { VoterManagementComponent } from './components/voter-management/voter-management.component';
import { ProposalManagementComponent } from './components/proposal-management/proposal-management.component';
import { VotingSessionComponent } from './components/voting-session/voting-session.component';
@NgModule({
declarations: [
AppComponent,
HeaderComponent,
ConnectWalletComponent,
WorkflowStatusComponent,
VoterManagementComponent,
ProposalManagementComponent,
VotingSessionComponent
],
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
\ No newline at end of file
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss']
})
export class HeaderComponent {
@Input() account: string | null = null;
\ No newline at end of file
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { ethers } from 'ethers';
// ABI du contrat - à remplacer par le véritable ABI après compilation
const VOTING_ABI = [
// Fonctions et événements du contrat Voting
"function owner() view returns (address)",
"function workflowStatus() view returns (uint8)",
"function getVoter(address _address) view returns (bool isRegistered, bool hasVoted, uint votedProposalId)",
"function getProposal(uint _id) view returns (string description, uint voteCount)",
"function getProposalsCount() view returns (uint)",
"function registerVoter(address _voterAddress)",
"function startProposalsRegistration(uint _durationInMinutes)",
"function endProposalsRegistration()",
"function registerProposal(string calldata _description)",
"function startVotingSession(uint _durationInMinutes)",
"function endVotingSession()",
"function vote(uint _proposalId)",
"function delegateVoteTo(address _to)",
"function tallyVotes()",
"event VoterRegistered(address voterAddress)",
"event WorkflowStatusChange(uint8 previousStatus, uint8 newStatus)",
"event ProposalRegistered(uint proposalId)",
"event Voted(address voter, uint proposalId)"
];
@Injectable({
providedIn: 'root'
})
export class ContractService {
// Adresse déployée du contrat - à remplacer par l'adresse réelle après déploiement
private contractAddress = '';
// Provider et Signer ethers
private provider: ethers.providers.Web3Provider | null = null;
private signer: ethers.Signer | null = null;
private contract: ethers.Contract | null = null;
// Observables pour l'état du contrat
private accountSubject = new BehaviorSubject<string | null>(null);
account$ = this.accountSubject.asObservable();
private isOwnerSubject = new BehaviorSubject<boolean>(false);
isOwner$ = this.isOwnerSubject.asObservable();
private isVoterSubject = new BehaviorSubject<boolean>(false);
isVoter$ = this.isVoterSubject.asObservable();
private currentStatusSubject = new BehaviorSubject<string>('0');
currentStatus$ = this.currentStatusSubject.asObservable();
private loadingSubject = new BehaviorSubject<boolean>(false);
loading$ = this.loadingSubject.asObservable();
private errorSubject = new BehaviorSubject<string | null>(null);
error$ = this.errorSubject.asObservable();
constructor() {
// Vérifier si MetaMask est disponible
if (window.ethereum) {
// Écouter les changements de compte
window.ethereum.on('accountsChanged', (accounts: string[]) => {
if (accounts.length > 0) {
this.accountSubject.next(accounts[0]);
this.initializeContract();
} else {
this.resetState();
}
});
}
}
// Connexion au portefeuille MetaMask
async connectWallet(): Promise<void> {
try {
this.loadingSubject.next(true);
this.errorSubject.next(null);
if (!window.ethereum) {
throw new Error("MetaMask n'est pas installé. Veuillez installer l'extension MetaMask.");
}
// Demander l'accès aux comptes
const provider = new ethers.providers.Web3Provider(window.ethereum);
const accounts = await provider.send('eth_requestAccounts', []);
if (accounts.length === 0) {
throw new Error("Aucun compte n'a été autorisé.");
}
this.provider = provider;
this.signer = provider.getSigner();
this.accountSubject.next(accounts[0]);
await this.initializeContract();
this.loadingSubject.next(false);
} catch (error: any) {
this.errorSubject.next(error.message || "Erreur lors de la connexion au portefeuille");
this.loadingSubject.next(false);
}
}
// Initialiser le contrat et récupérer les données
private async initializeContract(): Promise<void> {
try {
if (!this.provider || !this.signer || !this.contractAddress) {
return;
}
// Créer une instance du contrat
this.contract = new ethers.Contract(this.contractAddress, VOTING_ABI, this.signer);
// Récupérer le propriétaire du contrat
const owner = await this.contract.owner();
const account = this.accountSubject.getValue();
this.isOwnerSubject.next(account?.toLowerCase() === owner.toLowerCase());
// Récupérer le statut actuel du workflow
const status = await this.contract.workflowStatus();
this.currentStatusSubject.next(status.toString());
// Vérifier si l'utilisateur est un électeur inscrit
try {
if (account) {
const voter = await this.contract.getVoter(account);
this.isVoterSubject.next(voter.isRegistered);
} else {
this.isVoterSubject.next(false);
}
} catch (error) {
// Si l'erreur est due au fait que l'utilisateur n'est pas un électeur
this.isVoterSubject.next(false);
}
// Configurer les écouteurs d'événements
this.setupEventListeners();
} catch (error: any) {
this.errorSubject.next(error.message || "Erreur lors de l'initialisation du contrat");
}
}
// Configurer les écouteurs d'événements pour le contrat
private setupEventListeners(): void {
if (!this.contract) return;
// Écouter les changements de statut du workflow
this.contract.on('WorkflowStatusChange', (previousStatus: number, newStatus: number) => {
this.currentStatusSubject.next(newStatus.toString());
});
// Écouter les enregistrements de votants
this.contract.on('VoterRegistered', async (voterAddress: string) => {
const account = this.accountSubject.getValue();
if (account?.toLowerCase() === voterAddress.toLowerCase()) {
this.isVoterSubject.next(true);
}
});
}
// Réinitialiser l'état quand l'utilisateur se déconnecte
private resetState(): void {
this.accountSubject.next(null);
this.isOwnerSubject.next(false);
this.isVoterSubject.next(false);
this.currentStatusSubject.next('0');
this.provider = null;
this.signer = null;
this.contract = null;
}
// Définir l'adresse du contrat (après déploiement)
setContractAddress(address: string): void {
this.contractAddress = address;
// Réinitialiser le contrat avec la nouvelle adresse
if (this.signer) {
this.initializeContract();
}
}
// Méthodes pour interagir avec le contrat
// Enregistrer un nouvel électeur
async registerVoter(voterAddress: string): Promise<void> {
try {
this.loadingSubject.next(true);
this.errorSubject.next(null);
if (!this.contract) {
throw new Error('Contrat non initialisé');
}
const tx = await this.contract.registerVoter(voterAddress);
await tx.wait();
this.loadingSubject.next(false);
} catch (error: any) {
this.errorSubject.next(error.message || "Erreur lors de l'enregistrement de l'électeur");
this.loadingSubject.next(false);
}
}
// Démarrer la session d'enregistrement des propositions
async startProposalsRegistration(durationInMinutes: number): Promise<void> {
try {
this.loadingSubject.next(true);
this.errorSubject.next(null);
if (!this.contract) {
throw new Error('Contrat non initialisé');
}
const tx = await this.contract.startProposalsRegistration(durationInMinutes);
await tx.wait();
this.loadingSubject.next(false);
} catch (error: any) {
this.errorSubject.next(error.message || "Erreur lors du démarrage de la session d'enregistrement");
this.loadingSubject.next(false);
}
}
// Terminer la session d'enregistrement des propositions
async endProposalsRegistration(): Promise<void> {
try {
this.loadingSubject.next(true);
this.errorSubject.next(null);
if (!this.contract) {
throw new Error('Contrat non initialisé');
}
const tx = await this.contract.endProposalsRegistration();
await tx.wait();
this.loadingSubject.next(false);
} catch (error: any) {
this.errorSubject.next(error.message || "Erreur lors de la clôture de la session d'enregistrement");
this.loadingSubject.next(false);
}
}
// Soumettre une proposition
async registerProposal(description: string): Promise<void> {
try {
this.loadingSubject.next(true);
this.errorSubject.next(null);
if (!this.contract) {
throw new Error('Contrat non initialisé');
}
const tx = await this.contract.registerProposal(description);
await tx.wait();
this.loadingSubject.next(false);
} catch (error: any) {
this.errorSubject.next(error.message || "Erreur lors de l'enregistrement de la proposition");
this.loadingSubject.next(false);
}
}
// Démarrer la session de vote
async startVotingSession(durationInMinutes: number): Promise<void> {
try {
this.loadingSubject.next(true);
this.errorSubject.next(null);
if (!this.contract) {
throw new Error('Contrat non initialisé');
}
const tx = await this.contract.startVotingSession(durationInMinutes);
await tx.wait();
this.loadingSubject.next(false);
} catch (error: any) {
this.errorSubject.next(error.message || "Erreur lors du démarrage de la session de vote");
this.loadingSubject.next(false);
}
}
// Terminer la session de vote
async endVotingSession(): Promise<void> {
try {
this.loadingSubject.next(true);
this.errorSubject.next(null);
if (!this.contract) {
throw new Error('Contrat non initialisé');
}
const tx = await this.contract.endVotingSession();
await tx.wait();
this.loadingSubject.next(false);
} catch (error: any) {
this.errorSubject.next(error.message || "Erreur lors de la clôture de la session de vote");
this.loadingSubject.next(false);
}
}
// Voter pour une proposition
async vote(proposalId: number): Promise<void> {
try {
this.loadingSubject.next(true);
this.errorSubject.next(null);
if (!this.contract) {
throw new Error('Contrat non initialisé');
}
const tx = await this.contract.vote(proposalId);
await tx.wait();
this.loadingSubject.next(false);
} catch (error: any) {
this.errorSubject.next(error.message || "Erreur lors du vote");
this.loadingSubject.next(false);
}
}
// Déléguer son vote
async delegateVote(delegateAddress: string): Promise<void> {
try {
this.loadingSubject.next(true);
this.errorSubject.next(null);
if (!this.contract) {
throw new Error('Contrat non initialisé');
}
const tx = await this.contract.delegateVoteTo(delegateAddress);
await tx.wait();
this.loadingSubject.next(false);
} catch (error: any) {
this.errorSubject.next(error.message || "Erreur lors de la délégation du vote");
this.loadingSubject.next(false);
}
}
// Comptabiliser les votes
async tallyVotes(): Promise<void> {
try {
this.loadingSubject.next(true);
this.errorSubject.next(null);
if (!this.contract) {
throw new Error('Contrat non initialisé');
}
const tx = await this.contract.tallyVotes();
await tx.wait();
this.loadingSubject.next(false);
} catch (error: any) {
this.errorSubject.next(error.message || "Erreur lors du comptage des votes");
this.loadingSubject.next(false);
}
}
// Récupérer les informations sur les propositions
async getProposals(): Promise<{ id: number; description: string; voteCount: string }[]> {
try {
if (!this.contract) {
throw new Error('Contrat non initialisé');
}
const count = await this.contract.getProposalsCount();
const proposals = [];
for (let i = 0; i < count; i++) {
const proposal = await this.contract.getProposal(i);
proposals.push({
id: i,
description: proposal.description,
voteCount: proposal.voteCount.toString()
});
}
return proposals;
} catch (error: any) {
this.errorSubject.next(error.message || "Erreur lors de la récupération des propositions");
return [];
}
}
// Récupérer les informations sur un électeur
async getVoterInfo(address: string): Promise<{ isRegistered: boolean; hasVoted: boolean; votedProposalId: string }> {
try {
if (!this.contract) {
throw new Error('Contrat non initialisé');
}
const voter = await this.contract.getVoter(address);
return {
isRegistered: voter.isRegistered,
hasVoted: voter.hasVoted,
votedProposalId: voter.votedProposalId.toString()
};
} catch (error) {
return {
isRegistered: false,
hasVoted: false,
votedProposalId: '0'
};
}
}
}
// Ajouter la définition de l'interface Window pour le provider Ethereum
declare global {
interface Window {
ethereum: any;
}
}
\ No newline at end of file
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>DApp de Vote - Système de vote décentralisé</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<app-root></app-root>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
\ No newline at end of file
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
\ No newline at end of file
/* Styles globaux */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
color: #333;
}
.gradient-custom {
background: linear-gradient(to right, #667eea, #764ba2);
}
.card {
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.card-header {
background-color: #f1f5fb;
border-bottom: 1px solid #e3e6f0;
padding: 15px 20px;
}
.btn-primary {
background-color: #667eea;
border-color: #667eea;
}
.btn-primary:hover {
background-color: #5a6acf;
border-color: #5a6acf;
}
.alert {
border-radius: 8px;
margin-bottom: 15px;
}
.form-control:focus {
border-color: #a496e9;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
.badge-success {
background-color: #28a745;
}
.badge-danger {
background-color: #dc3545;
}
.badge-warning {
background-color: #ffc107;
}
.badge-info {
background-color: #17a2b8;
}
/* Animation pour les transitions d'état */
.fade-in {
animation: fadeIn 0.5s;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Étiquettes de statut workflow */
.status-badge {
padding: 8px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
display: inline-block;
}
.spinner-border-sm {
width: 1rem;
height: 1rem;
margin-right: 5px;
}
\ No newline at end of file
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts"
],
"include": [
"src/**/*.d.ts"
]
}
\ No newline at end of file
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"lib": [
"ES2022",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
\ No newline at end of file
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}
\ No newline at end of file
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "hardhat test", "test": "hardhat test",
"dev": "cd frontend && npm run dev", "dev": "cd frontend-angular && npm run dev",
"compile": "hardhat compile", "compile": "hardhat compile",
"deploy": "hardhat run scripts/deploy.js --network localhost", "deploy": "hardhat run scripts/deploy.js --network localhost",
"node": "hardhat node" "node": "hardhat node"
......
@echo off
echo ===== Nettoyage de l'installation existante =====
cd frontend
rmdir /s /q node_modules
del package-lock.json
echo ===== Nettoyage du cache npm =====
npm cache clean --force
echo ===== Installation des packages avec option de compatibilité =====
npm install --legacy-peer-deps
echo ===== Installation spécifique des modules manquants =====
npm install schema-utils prompts webpack webpack-dev-server react-dev-utils --save-dev --legacy-peer-deps
echo ===== Vérification et installation des dépendances principales =====
npm install react react-dom ethers@5.7.2 web3 web3modal --legacy-peer-deps
echo ===== Réparation des modules cassés =====
npm rebuild
echo ===== Installation terminée =====
echo Pour démarrer l'application, exécutez: npm start
cd ..
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment