Files
OpenLearnX/backend/web3_service.py
T
2025-07-25 11:10:44 +05:30

284 lines
11 KiB
Python

from web3 import Web3
from eth_account.messages import encode_defunct
import json
import secrets
import time
from typing import Optional, Dict, Any
from pathlib import Path
class Web3Service:
def __init__(self, provider_url: str, contract_address: Optional[str] = None):
self.w3 = Web3(Web3.HTTPProvider(provider_url))
self.contract_address = contract_address
self.contract = None
if contract_address:
self.load_contract()
def load_contract(self):
"""Load the smart contract ABI and create contract instance"""
try:
# Updated path to match Foundry's output structure
contract_path = Path('out/CertificateNFT.sol/CertificateNFT.json')
if not contract_path.exists():
print(f"Contract JSON not found at {contract_path}")
return
with open(contract_path, 'r') as f:
contract_data = json.load(f)
abi = contract_data['abi']
self.contract = self.w3.eth.contract(
address=self.contract_address,
abi=abi
)
print(f"Contract loaded successfully at {self.contract_address}")
except Exception as e:
print(f"Failed to load contract: {e}")
def generate_nonce(self) -> str:
"""Generate a random nonce for signature verification"""
return secrets.token_hex(16)
def verify_signature(self, address: str, message: str, signature: str) -> bool:
"""Verify MetaMask signature"""
try:
# Create the message that was signed
message_hash = encode_defunct(text=message)
# Recover the address from signature
recovered_address = self.w3.eth.account.recover_message(
message_hash,
signature=signature
)
# Compare addresses (case insensitive)
return recovered_address.lower() == address.lower()
except Exception as e:
print(f"Signature verification failed: {e}")
return False
def mint_certificate(self, to_address: str, token_uri: str, private_key: str) -> Optional[str]:
"""Mint an NFT certificate using the simple mintCertificate function"""
if not self.contract:
raise Exception("Contract not loaded")
try:
# Get account from private key
account = self.w3.eth.account.from_key(private_key)
# Build transaction
transaction = self.contract.functions.mintCertificate(
to_address,
token_uri
).build_transaction({
'from': account.address,
'nonce': self.w3.eth.get_transaction_count(account.address),
'gas': 500000, # Increased gas limit
'gasPrice': self.w3.to_wei('20', 'gwei')
})
# Sign and send transaction
signed_txn = self.w3.eth.account.sign_transaction(transaction, private_key)
tx_hash = self.w3.eth.send_raw_transaction(signed_txn.rawTransaction)
# Wait for transaction receipt
receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
if receipt.status == 1:
print(f"Certificate minted successfully. TX: {receipt.transactionHash.hex()}")
return receipt.transactionHash.hex()
else:
print(f"Transaction failed. Status: {receipt.status}")
return None
except Exception as e:
print(f"Minting failed: {e}")
return None
def mint_certificate_with_details(self, to_address: str, token_uri: str,
subject: str, student_name: str, score: int,
private_key: str) -> Optional[str]:
"""Mint an NFT certificate with detailed information"""
if not self.contract:
raise Exception("Contract not loaded")
try:
# Get account from private key
account = self.w3.eth.account.from_key(private_key)
# Build transaction with detailed function
transaction = self.contract.functions.mintCertificateWithDetails(
to_address,
token_uri,
subject,
student_name,
score
).build_transaction({
'from': account.address,
'nonce': self.w3.eth.get_transaction_count(account.address),
'gas': 600000, # Higher gas for detailed function
'gasPrice': self.w3.to_wei('20', 'gwei')
})
# Sign and send transaction
signed_txn = self.w3.eth.account.sign_transaction(transaction, private_key)
tx_hash = self.w3.eth.send_raw_transaction(signed_txn.rawTransaction)
# Wait for transaction receipt
receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
if receipt.status == 1:
print(f"Detailed certificate minted successfully. TX: {receipt.transactionHash.hex()}")
return receipt.transactionHash.hex()
else:
print(f"Transaction failed. Status: {receipt.status}")
return None
except Exception as e:
print(f"Detailed minting failed: {e}")
return None
def get_certificate_details(self, token_id: int) -> Optional[Dict]:
"""Get certificate details by token ID"""
if not self.contract:
return None
try:
# Get certificate struct data
certificate = self.contract.functions.getCertificate(token_id).call()
# Get owner and token URI
owner = self.contract.functions.ownerOf(token_id).call()
token_uri = self.contract.functions.tokenURI(token_id).call()
return {
'token_id': token_id,
'owner': owner,
'token_uri': token_uri,
'subject': certificate[0],
'student_name': certificate[1],
'score': certificate[2],
'timestamp': certificate[3],
'verified': certificate[4]
}
except Exception as e:
print(f"Failed to get certificate details: {e}")
return None
def get_user_certificates(self, user_address: str) -> Optional[list]:
"""Get all certificate token IDs for a user"""
if not self.contract:
return None
try:
token_ids = self.contract.functions.getUserCertificates(user_address).call()
return token_ids
except Exception as e:
print(f"Failed to get user certificates: {e}")
return None
def verify_certificate(self, token_id: int) -> bool:
"""Verify if a certificate is valid"""
if not self.contract:
return False
try:
is_verified = self.contract.functions.verifyCertificate(token_id).call()
return is_verified
except Exception as e:
print(f"Failed to verify certificate: {e}")
return False
def get_total_supply(self) -> int:
"""Get total number of certificates minted"""
if not self.contract:
return 0
try:
total = self.contract.functions.totalSupply().call()
return total
except Exception as e:
print(f"Failed to get total supply: {e}")
return 0
def get_latest_token_id(self) -> int:
"""Get the latest token ID (useful for getting newly minted certificate)"""
return self.get_total_supply()
def get_transaction_receipt(self, tx_hash: str) -> Optional[Dict]:
"""Get transaction receipt for a given hash"""
try:
receipt = self.w3.eth.get_transaction_receipt(tx_hash)
return {
'transaction_hash': receipt.transactionHash.hex(),
'block_number': receipt.blockNumber,
'gas_used': receipt.gasUsed,
'status': receipt.status,
'contract_address': receipt.contractAddress
}
except Exception as e:
print(f"Failed to get transaction receipt: {e}")
return None
def is_connected(self) -> bool:
"""Check if connected to blockchain"""
try:
return self.w3.is_connected()
except:
return False
def get_balance(self, address: str) -> float:
"""Get ETH balance for an address"""
try:
balance_wei = self.w3.eth.get_balance(address)
return self.w3.from_wei(balance_wei, 'ether')
except Exception as e:
print(f"Failed to get balance: {e}")
return 0.0
def get_gas_price(self) -> int:
"""Get current gas price"""
try:
return self.w3.eth.gas_price
except Exception as e:
print(f"Failed to get gas price: {e}")
return self.w3.to_wei('20', 'gwei') # Default fallback
def estimate_gas(self, to_address: str, token_uri: str, account_address: str) -> int:
"""Estimate gas for certificate minting"""
if not self.contract:
return 500000 # Default estimate
try:
gas_estimate = self.contract.functions.mintCertificate(
to_address,
token_uri
).estimate_gas({'from': account_address})
# Add 20% buffer
return int(gas_estimate * 1.2)
except Exception as e:
print(f"Failed to estimate gas: {e}")
return 500000 # Default fallback
def get_contract_info(self) -> Dict:
"""Get basic contract information"""
if not self.contract:
return {}
try:
return {
'address': self.contract_address,
'total_certificates': self.get_total_supply(),
'is_connected': self.is_connected(),
'network_id': self.w3.eth.chain_id,
'latest_block': self.w3.eth.block_number
}
except Exception as e:
print(f"Failed to get contract info: {e}")
return {}