The POST /api/votes endpoint (used by frontend) was recording votes
in the database but NOT submitting them to the PoA blockchain. This
caused votes to appear in database but not on the blockchain.
Changes:
- Add vote submission to PoA validators in the simple endpoint
- Add fallback to local blockchain if PoA validators unreachable
- Include blockchain status in API response
- Use ballot hash as vote data for blockchain submission
This ensures votes are now submitted to the PoA blockchain when the
frontend votes, and users can see their votes on the blockchain.
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
684 lines
22 KiB
Python
684 lines
22 KiB
Python
"""
|
|
Routes pour le vote et les bulletins.
|
|
"""
|
|
|
|
import logging
|
|
from fastapi import APIRouter, HTTPException, status, Depends, Request, Query
|
|
from sqlalchemy.orm import Session
|
|
from datetime import datetime, timezone
|
|
import base64
|
|
import uuid
|
|
from .. import schemas, services
|
|
from ..dependencies import get_db, get_current_voter
|
|
from ..models import Voter
|
|
from ..crypto.hashing import SecureHash
|
|
from ..blockchain import BlockchainManager
|
|
from ..blockchain_client import BlockchainClient, get_blockchain_client_sync
|
|
import asyncio
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/votes", tags=["votes"])
|
|
|
|
# Global blockchain manager instance (fallback for in-memory blockchain)
|
|
blockchain_manager = BlockchainManager()
|
|
|
|
# Global blockchain client instance for PoA validators
|
|
blockchain_client: BlockchainClient = None
|
|
|
|
|
|
async def init_blockchain_client():
|
|
"""Initialize the blockchain client on startup"""
|
|
global blockchain_client
|
|
if blockchain_client is None:
|
|
blockchain_client = BlockchainClient()
|
|
await blockchain_client.refresh_validator_status()
|
|
|
|
|
|
def get_blockchain_client() -> BlockchainClient:
|
|
"""Get the blockchain client instance"""
|
|
global blockchain_client
|
|
if blockchain_client is None:
|
|
blockchain_client = BlockchainClient()
|
|
return blockchain_client
|
|
|
|
|
|
@router.post("")
|
|
async def submit_simple_vote(
|
|
vote_data: dict,
|
|
current_voter: Voter = Depends(get_current_voter),
|
|
db: Session = Depends(get_db),
|
|
request: Request = None
|
|
):
|
|
"""
|
|
Soumettre un vote simple avec just élection_id et nom du candidat.
|
|
Interface simplifiée pour l'application web.
|
|
"""
|
|
from .. import models
|
|
|
|
election_id = vote_data.get('election_id')
|
|
candidate_name = vote_data.get('choix')
|
|
|
|
if not election_id or not candidate_name:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="election_id and choix are required"
|
|
)
|
|
|
|
# Vérifier que l'électeur n'a pas déjà voté
|
|
if services.VoteService.has_voter_voted(db, current_voter.id, election_id):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Voter has already voted in this election"
|
|
)
|
|
|
|
# Vérifier que l'élection existe
|
|
election = services.ElectionService.get_election(db, election_id)
|
|
if not election:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Election not found"
|
|
)
|
|
|
|
# Trouver le candidat par nom
|
|
candidate = db.query(models.Candidate).filter(
|
|
models.Candidate.name == candidate_name,
|
|
models.Candidate.election_id == election_id
|
|
).first()
|
|
|
|
if not candidate:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Candidate not found"
|
|
)
|
|
|
|
# Enregistrer le vote (sans chiffrement pour l'MVP)
|
|
import time
|
|
from ..crypto.hashing import SecureHash
|
|
|
|
ballot_hash = SecureHash.hash_bulletin(
|
|
vote_id=current_voter.id,
|
|
candidate_id=candidate.id,
|
|
timestamp=int(time.time())
|
|
)
|
|
|
|
vote = services.VoteService.record_vote(
|
|
db=db,
|
|
voter_id=current_voter.id,
|
|
election_id=election_id,
|
|
candidate_id=candidate.id,
|
|
encrypted_vote=b"", # Empty for MVP
|
|
ballot_hash=ballot_hash,
|
|
ip_address=request.client.host if request else None
|
|
)
|
|
|
|
# Generate transaction ID for blockchain
|
|
import uuid
|
|
transaction_id = f"tx-{uuid.uuid4().hex[:12]}"
|
|
|
|
# Submit vote to PoA blockchain
|
|
blockchain_client = get_blockchain_client()
|
|
await blockchain_client.refresh_validator_status()
|
|
|
|
blockchain_response = {
|
|
"status": "pending"
|
|
}
|
|
|
|
try:
|
|
async with BlockchainClient() as poa_client:
|
|
# Submit vote to PoA network
|
|
submission_result = await poa_client.submit_vote(
|
|
voter_id=current_voter.id,
|
|
election_id=election_id,
|
|
encrypted_vote=ballot_hash, # Use ballot hash as data
|
|
transaction_id=transaction_id
|
|
)
|
|
blockchain_response = {
|
|
"status": "submitted",
|
|
"transaction_id": transaction_id,
|
|
"block_hash": submission_result.get("block_hash"),
|
|
"validator": submission_result.get("validator")
|
|
}
|
|
logger.info(
|
|
f"Vote submitted to PoA: voter={current_voter.id}, "
|
|
f"election={election_id}, tx={transaction_id}"
|
|
)
|
|
except Exception as e:
|
|
# Fallback: Record in local blockchain
|
|
logger.warning(f"PoA submission failed: {e}. Falling back to local blockchain.")
|
|
try:
|
|
blockchain = blockchain_manager.get_or_create_blockchain(election_id)
|
|
block = blockchain.add_block(
|
|
encrypted_vote=ballot_hash,
|
|
transaction_id=transaction_id
|
|
)
|
|
blockchain_response = {
|
|
"status": "submitted_fallback",
|
|
"transaction_id": transaction_id,
|
|
"block_index": block.index,
|
|
"warning": "Vote recorded in local blockchain (PoA validators unreachable)"
|
|
}
|
|
except Exception as fallback_error:
|
|
logger.error(f"Fallback blockchain also failed: {fallback_error}")
|
|
blockchain_response = {
|
|
"status": "database_only",
|
|
"transaction_id": transaction_id,
|
|
"warning": "Vote recorded in database but blockchain submission failed"
|
|
}
|
|
|
|
# Mark voter as having voted
|
|
services.VoterService.mark_as_voted(db, current_voter.id)
|
|
|
|
return {
|
|
"message": "Vote recorded successfully",
|
|
"id": vote.id,
|
|
"ballot_hash": ballot_hash,
|
|
"timestamp": vote.timestamp,
|
|
"blockchain": blockchain_response
|
|
}
|
|
|
|
|
|
|
|
@router.post("/submit")
|
|
async def submit_vote(
|
|
vote_bulletin: schemas.VoteBulletin,
|
|
current_voter: Voter = Depends(get_current_voter),
|
|
db: Session = Depends(get_db),
|
|
request: Request = None
|
|
):
|
|
"""
|
|
Soumettre un vote chiffré via PoA blockchain.
|
|
|
|
Le vote doit être:
|
|
- Chiffré avec ElGamal
|
|
- Accompagné d'une preuve ZK de validité
|
|
|
|
Le vote est enregistré dans la PoA blockchain pour l'immuabilité.
|
|
"""
|
|
|
|
# Vérifier que l'électeur n'a pas déjà voté
|
|
if services.VoteService.has_voter_voted(
|
|
db,
|
|
current_voter.id,
|
|
vote_bulletin.election_id
|
|
):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Voter has already voted in this election"
|
|
)
|
|
|
|
# Vérifier que l'élection existe
|
|
election = services.ElectionService.get_election(
|
|
db,
|
|
vote_bulletin.election_id
|
|
)
|
|
if not election:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Election not found"
|
|
)
|
|
|
|
# Vérifier que le candidat existe
|
|
from ..models import Candidate
|
|
candidate = db.query(Candidate).filter(
|
|
Candidate.id == vote_bulletin.candidate_id,
|
|
Candidate.election_id == vote_bulletin.election_id
|
|
).first()
|
|
if not candidate:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Candidate not found"
|
|
)
|
|
|
|
# Décoder le vote chiffré
|
|
try:
|
|
encrypted_vote_bytes = base64.b64decode(vote_bulletin.encrypted_vote)
|
|
except Exception:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Invalid encrypted vote format"
|
|
)
|
|
|
|
# Générer le hash du bulletin
|
|
import time
|
|
ballot_hash = SecureHash.hash_bulletin(
|
|
vote_id=current_voter.id,
|
|
candidate_id=vote_bulletin.candidate_id,
|
|
timestamp=int(time.time())
|
|
)
|
|
|
|
# Générer ID unique pour la blockchain (anonyme)
|
|
transaction_id = f"tx-{uuid.uuid4().hex[:12]}"
|
|
|
|
# Enregistrer le vote en base de données
|
|
vote = services.VoteService.record_vote(
|
|
db=db,
|
|
voter_id=current_voter.id,
|
|
election_id=vote_bulletin.election_id,
|
|
candidate_id=vote_bulletin.candidate_id,
|
|
encrypted_vote=encrypted_vote_bytes,
|
|
ballot_hash=ballot_hash,
|
|
ip_address=request.client.host if request else None
|
|
)
|
|
|
|
# Soumettre le vote aux validateurs PoA
|
|
blockchain_client = get_blockchain_client()
|
|
await blockchain_client.refresh_validator_status()
|
|
|
|
try:
|
|
async with BlockchainClient() as poa_client:
|
|
# Soumettre le vote au réseau PoA
|
|
submission_result = await poa_client.submit_vote(
|
|
voter_id=current_voter.id,
|
|
election_id=vote_bulletin.election_id,
|
|
encrypted_vote=vote_bulletin.encrypted_vote,
|
|
transaction_id=transaction_id
|
|
)
|
|
|
|
# Marquer l'électeur comme ayant voté
|
|
services.VoterService.mark_as_voted(db, current_voter.id)
|
|
|
|
logger.info(
|
|
f"Vote submitted to PoA: voter={current_voter.id}, "
|
|
f"election={vote_bulletin.election_id}, tx={transaction_id}"
|
|
)
|
|
|
|
return {
|
|
"id": vote.id,
|
|
"transaction_id": transaction_id,
|
|
"block_hash": submission_result.get("block_hash"),
|
|
"ballot_hash": ballot_hash,
|
|
"timestamp": vote.timestamp,
|
|
"status": "submitted",
|
|
"validator": submission_result.get("validator")
|
|
}
|
|
|
|
except Exception as e:
|
|
# Fallback: Try to record in local blockchain
|
|
logger.warning(f"PoA submission failed: {e}. Falling back to local blockchain.")
|
|
|
|
try:
|
|
blockchain = blockchain_manager.get_or_create_blockchain(vote_bulletin.election_id)
|
|
block = blockchain.add_block(
|
|
encrypted_vote=vote_bulletin.encrypted_vote,
|
|
transaction_id=transaction_id
|
|
)
|
|
|
|
services.VoterService.mark_as_voted(db, current_voter.id)
|
|
|
|
return {
|
|
"id": vote.id,
|
|
"transaction_id": transaction_id,
|
|
"block_index": block.index,
|
|
"ballot_hash": ballot_hash,
|
|
"timestamp": vote.timestamp,
|
|
"warning": "Vote recorded in local blockchain (PoA validators unreachable)"
|
|
}
|
|
except Exception as fallback_error:
|
|
logger.error(f"Fallback blockchain also failed: {fallback_error}")
|
|
services.VoterService.mark_as_voted(db, current_voter.id)
|
|
|
|
return {
|
|
"id": vote.id,
|
|
"transaction_id": transaction_id,
|
|
"ballot_hash": ballot_hash,
|
|
"timestamp": vote.timestamp,
|
|
"warning": "Vote recorded in database but blockchain submission failed"
|
|
}
|
|
|
|
|
|
@router.get("/status")
|
|
def get_vote_status(
|
|
election_id: int,
|
|
current_voter: Voter = Depends(get_current_voter),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Vérifier si l'électeur a déjà voté pour une élection"""
|
|
|
|
has_voted = services.VoteService.has_voter_voted(
|
|
db,
|
|
current_voter.id,
|
|
election_id
|
|
)
|
|
|
|
return {"has_voted": has_voted}
|
|
|
|
|
|
@router.get("/history", response_model=list)
|
|
def get_voter_history(
|
|
current_voter: Voter = Depends(get_current_voter),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Récupérer l'historique des votes de l'électeur actuel"""
|
|
from .. import models
|
|
from datetime import datetime
|
|
|
|
votes = db.query(models.Vote).filter(
|
|
models.Vote.voter_id == current_voter.id
|
|
).all()
|
|
|
|
# Retourner la structure avec infos des élections
|
|
history = []
|
|
for vote in votes:
|
|
election = db.query(models.Election).filter(
|
|
models.Election.id == vote.election_id
|
|
).first()
|
|
candidate = db.query(models.Candidate).filter(
|
|
models.Candidate.id == vote.candidate_id
|
|
).first()
|
|
|
|
if election:
|
|
# Déterminer le statut de l'élection
|
|
if election.start_date > datetime.now(timezone.utc):
|
|
status_val = "upcoming"
|
|
elif election.end_date < datetime.now(timezone.utc):
|
|
status_val = "closed"
|
|
else:
|
|
status_val = "active"
|
|
|
|
history.append({
|
|
"vote_id": vote.id,
|
|
"election_id": election.id,
|
|
"election_name": election.name,
|
|
"candidate_name": candidate.name if candidate else "Unknown",
|
|
"vote_date": vote.timestamp,
|
|
"election_status": status_val
|
|
})
|
|
|
|
return history
|
|
|
|
|
|
@router.post("/setup")
|
|
async def setup_election(
|
|
election_id: int,
|
|
current_voter: Voter = Depends(get_current_voter),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Initialiser une élection avec les clés cryptographiques.
|
|
|
|
Crée une blockchain pour l'élection et génère les clés publiques
|
|
pour le chiffrement ElGamal côté client.
|
|
"""
|
|
from .. import models
|
|
from ..crypto.encryption import ElGamal
|
|
|
|
# Vérifier que l'élection existe
|
|
election = services.ElectionService.get_election(db, election_id)
|
|
if not election:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Election not found"
|
|
)
|
|
|
|
# Générer ou récupérer la blockchain pour cette élection
|
|
blockchain = blockchain_manager.get_or_create_blockchain(election_id)
|
|
|
|
# Générer les clés ElGamal si nécessaire
|
|
if not election.public_key:
|
|
elgamal = ElGamal()
|
|
election.public_key = elgamal.public_key_bytes
|
|
db.commit()
|
|
|
|
return {
|
|
"status": "initialized",
|
|
"election_id": election_id,
|
|
"public_keys": {
|
|
"elgamal_pubkey": base64.b64encode(election.public_key).decode() if election.public_key else None
|
|
},
|
|
"blockchain_blocks": blockchain.get_block_count()
|
|
}
|
|
|
|
|
|
@router.get("/public-keys")
|
|
async def get_public_keys(
|
|
election_id: int = Query(...),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Récupérer les clés publiques pour le chiffrement côté client.
|
|
|
|
Accessible sans authentification pour permettre le chiffrement avant
|
|
la connexion (si applicable).
|
|
"""
|
|
from .. import models
|
|
|
|
# Vérifier que l'élection existe
|
|
election = db.query(models.Election).filter(
|
|
models.Election.id == election_id
|
|
).first()
|
|
|
|
if not election:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Election not found"
|
|
)
|
|
|
|
if not election.public_key:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Election keys not initialized. Call /setup first."
|
|
)
|
|
|
|
return {
|
|
"elgamal_pubkey": base64.b64encode(election.public_key).decode()
|
|
}
|
|
|
|
|
|
@router.get("/blockchain")
|
|
async def get_blockchain(
|
|
election_id: int = Query(...),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Récupérer l'état complet de la blockchain pour une élection.
|
|
|
|
Retourne tous les blocs et l'état de vérification.
|
|
"""
|
|
# Vérifier que l'élection existe
|
|
election = services.ElectionService.get_election(db, election_id)
|
|
if not election:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Election not found"
|
|
)
|
|
|
|
blockchain = blockchain_manager.get_or_create_blockchain(election_id)
|
|
return blockchain.get_blockchain_data()
|
|
|
|
|
|
@router.get("/results")
|
|
async def get_results(
|
|
election_id: int = Query(...),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Obtenir les résultats comptabilisés d'une élection.
|
|
|
|
Requête d'abord aux validateurs PoA, puis fallback sur blockchain locale.
|
|
"""
|
|
from .. import models
|
|
|
|
# Vérifier que l'élection existe
|
|
election = db.query(models.Election).filter(
|
|
models.Election.id == election_id
|
|
).first()
|
|
|
|
if not election:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Election not found"
|
|
)
|
|
|
|
# Essayer d'obtenir les résultats du réseau PoA en premier
|
|
try:
|
|
async with BlockchainClient() as poa_client:
|
|
poa_results = await poa_client.get_election_results(election_id)
|
|
|
|
if poa_results:
|
|
logger.info(f"Retrieved results from PoA validators for election {election_id}")
|
|
return poa_results
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Failed to get results from PoA: {e}")
|
|
|
|
# Fallback: Utiliser la blockchain locale
|
|
logger.info(f"Falling back to local blockchain for election {election_id}")
|
|
|
|
# Compter les votes par candidat (simple pour MVP)
|
|
votes = db.query(models.Vote).filter(
|
|
models.Vote.election_id == election_id
|
|
).all()
|
|
|
|
# Grouper par candidat
|
|
vote_counts = {}
|
|
for vote in votes:
|
|
candidate = db.query(models.Candidate).filter(
|
|
models.Candidate.id == vote.candidate_id
|
|
).first()
|
|
|
|
if candidate:
|
|
if candidate.name not in vote_counts:
|
|
vote_counts[candidate.name] = 0
|
|
vote_counts[candidate.name] += 1
|
|
|
|
# Obtenir la blockchain
|
|
blockchain = blockchain_manager.get_or_create_blockchain(election_id)
|
|
|
|
total_votes = blockchain.get_vote_count()
|
|
|
|
results = []
|
|
for candidate_name, count in vote_counts.items():
|
|
percentage = (count / total_votes * 100) if total_votes > 0 else 0
|
|
results.append({
|
|
"candidate_name": candidate_name,
|
|
"vote_count": count,
|
|
"percentage": round(percentage, 2)
|
|
})
|
|
|
|
return {
|
|
"election_id": election_id,
|
|
"election_name": election.name,
|
|
"total_votes": total_votes,
|
|
"results": sorted(results, key=lambda x: x["vote_count"], reverse=True),
|
|
"verification": {
|
|
"chain_valid": blockchain.verify_chain_integrity(),
|
|
"timestamp": datetime.now(timezone.utc).isoformat()
|
|
}
|
|
}
|
|
|
|
|
|
@router.post("/verify-blockchain")
|
|
async def verify_blockchain(
|
|
election_id: int,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Vérifier l'intégrité de la blockchain pour une élection.
|
|
|
|
Requête d'abord aux validateurs PoA, puis fallback sur blockchain locale.
|
|
Vérifie:
|
|
- La chaîne de hachage (chaque bloc lie au précédent)
|
|
- Les signatures des blocs
|
|
- L'absence de modification
|
|
"""
|
|
# Vérifier que l'élection existe
|
|
election = services.ElectionService.get_election(db, election_id)
|
|
if not election:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Election not found"
|
|
)
|
|
|
|
# Essayer de vérifier sur les validateurs PoA en premier
|
|
try:
|
|
async with BlockchainClient() as poa_client:
|
|
is_valid = await poa_client.verify_blockchain_integrity(election_id)
|
|
|
|
if is_valid is not None:
|
|
blockchain_state = await poa_client.get_blockchain_state(election_id)
|
|
|
|
logger.info(f"Blockchain verification from PoA validators for election {election_id}: {is_valid}")
|
|
|
|
return {
|
|
"election_id": election_id,
|
|
"chain_valid": is_valid,
|
|
"total_blocks": blockchain_state.get("verification", {}).get("total_blocks", 0) if blockchain_state else 0,
|
|
"total_votes": blockchain_state.get("verification", {}).get("total_votes", 0) if blockchain_state else 0,
|
|
"status": "valid" if is_valid else "invalid",
|
|
"source": "poa_validators"
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Failed to verify blockchain on PoA: {e}")
|
|
|
|
# Fallback: Vérifier sur la blockchain locale
|
|
logger.info(f"Falling back to local blockchain verification for election {election_id}")
|
|
|
|
blockchain = blockchain_manager.get_or_create_blockchain(election_id)
|
|
is_valid = blockchain.verify_chain_integrity()
|
|
|
|
return {
|
|
"election_id": election_id,
|
|
"chain_valid": is_valid,
|
|
"total_blocks": blockchain.get_block_count(),
|
|
"total_votes": blockchain.get_vote_count(),
|
|
"status": "valid" if is_valid else "invalid",
|
|
"source": "local_blockchain"
|
|
}
|
|
|
|
|
|
@router.get("/transaction-status")
|
|
async def get_transaction_status(
|
|
transaction_id: str = Query(...),
|
|
election_id: int = Query(...),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Check the confirmation status of a vote on the PoA blockchain.
|
|
|
|
Returns:
|
|
- status: "pending" or "confirmed"
|
|
- confirmed: boolean
|
|
- block_number: block where vote is confirmed (if confirmed)
|
|
- block_hash: hash of the block (if confirmed)
|
|
"""
|
|
# Vérifier que l'élection existe
|
|
election = services.ElectionService.get_election(db, election_id)
|
|
if not election:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Election not found"
|
|
)
|
|
|
|
# Essayer de vérifier le statut sur PoA en premier
|
|
try:
|
|
async with BlockchainClient() as poa_client:
|
|
status_info = await poa_client.get_vote_confirmation_status(
|
|
transaction_id,
|
|
election_id
|
|
)
|
|
|
|
if status_info:
|
|
logger.info(f"Transaction status from PoA: {transaction_id} = {status_info['status']}")
|
|
return {
|
|
**status_info,
|
|
"source": "poa_validators"
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Failed to get transaction status from PoA: {e}")
|
|
|
|
# Fallback: Check local blockchain
|
|
logger.debug(f"Falling back to local blockchain for transaction {transaction_id}")
|
|
|
|
return {
|
|
"status": "unknown",
|
|
"confirmed": False,
|
|
"transaction_id": transaction_id,
|
|
"source": "local_fallback"
|
|
}
|
|
|
|
|
|
from datetime import datetime
|