import os import json from web3 import Web3 from datetime import datetime from dotenv import load_dotenv from config import config # Load environment variables load_dotenv() class BlockchainManager: """ Manages actual blockchain interactions for Sustainability Credits. Supports Sepolia Testnet (Ethereum) out of the box. """ def __init__(self): self.rpc_url = os.getenv('BLOCKCHAIN_RPC_URL') self.private_key = os.getenv('BLOCKCHAIN_PRIVATE_KEY') self.contract_address = os.getenv('CONTRACT_ADDRESS') or os.getenv('VITE_CONTRACT_ADDRESS') if self.rpc_url: self.w3 = Web3(Web3.HTTPProvider(self.rpc_url)) else: self.w3 = None if self.w3 and self.w3.is_connected(): print(f"๐Ÿ”— Connected to Blockchain: {self.rpc_url}") # Load account from private key if self.private_key: try: self.account = self.w3.eth.account.from_key(self.private_key) self.address = self.account.address print(f"๐Ÿฆ Wallet Node Address: {self.address}") except Exception as e: print(f"โš ๏ธ Failed to load wallet from private key: {e}") # Load Contract ABI (Standard ERC20 + Custom awardCredits) self.abi = [ { "inputs": [ {"internalType": "address", "name": "student", "type": "address"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}, {"internalType": "string", "name": "actionType", "type": "string"}, {"internalType": "string", "name": "roomId", "type": "string"} ], "name": "awardCredits", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ {"internalType": "address", "name": "account", "type": "address"} ], "name": "balanceOf", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "decimals", "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], "stateMutability": "view", "type": "function" }, { "inputs": [ {"internalType": "address", "name": "to", "type": "address"}, {"internalType": "uint256", "name": "value", "type": "uint256"} ], "name": "transfer", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "nonpayable", "type": "function" } ] self.contract = None self._decimals = 18 # Default if self.contract_address: try: self.contract = self.w3.eth.contract(address=Web3.to_checksum_address(self.contract_address), abi=self.abi) self._decimals = self.contract.functions.decimals().call() print(f"๐Ÿ“„ Contract Initialized at {self.contract_address}") except Exception as e: print(f"โš ๏ธ Could not fully initialize contract: {e}") else: self.w3 = None self.contract = None print("โ„น๏ธ No Blockchain RPC found or connection failed. Running in Ledger-Simulation mode.") @property def is_connected(self): """Check live connection status""" if self.w3: return self.w3.is_connected() return False def mint_credits(self, target_address, amount, action_type="verified_action", room_id="CS_LAB"): """ Actually mints/awards tokens on the blockchain. Returns the transaction hash. """ if not self.is_connected or not self.contract or not self.private_key: if config.is_production(): raise ConnectionError( "โŒ BLOCKCHAIN ERROR: Cannot mint credits in Production mode without active RPC and Wallet Node.\n" "Simulation is strictly prohibited in Production to maintain user trust." ) # Fallback to simulation only in dev/sandbox tx_hash = f"0x_sim_{datetime.now().timestamp()}" print(f"๐Ÿงช [SIMULATION] Awarded {amount} CSC to {target_address} ({action_type})") return tx_hash try: # Build transaction nonce = self.w3.eth.get_transaction_count(self.address) # Simple award function call (converts to base units using decimals) tx = self.contract.functions.awardCredits( Web3.to_checksum_address(target_address), int(amount * (10 ** self._decimals)), action_type, room_id ).build_transaction({ 'from': self.address, 'nonce': nonce, 'gas': 200000, 'gasPrice': self.w3.eth.gas_price }) # Sign and send signed_tx = self.w3.eth.account.sign_transaction(tx, self.private_key) tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction) print(f"๐Ÿš€ Blockchain Transaction Sent: {tx_hash.hex()}") return tx_hash.hex() except Exception as e: print(f"โŒ Blockchain Transaction Failed: {e}") return None def get_wallet_balance(self, address): """Get balance of specific wallet in CSC tokens""" if not self.is_connected or not self.contract: return 0.0 try: raw_balance = self.contract.functions.balanceOf(Web3.to_checksum_address(address)).call() return raw_balance / (10 ** self._decimals) except Exception as e: print(f"โš ๏ธ Balance check failed: {e}") return 0.0 def transfer_credits(self, to_address, amount): """Perform a peer-to-peer transfer of credits on-chain""" if not self.is_connected or not self.contract or not self.private_key: return None try: nonce = self.w3.eth.get_transaction_count(self.address) value = int(amount * (10 ** self._decimals)) tx = self.contract.functions.transfer( Web3.to_checksum_address(to_address), value ).build_transaction({ 'from': self.address, 'nonce': nonce, 'gas': 100000, 'gasPrice': self.w3.eth.gas_price }) signed_tx = self.w3.eth.account.sign_transaction(tx, self.private_key) tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction) return tx_hash.hex() except Exception as e: print(f"โŒ Transfer failed: {e}") return None if __name__ == "__main__": # Test simulation mode bm = BlockchainManager() bm.mint_credits("0x1234567890123456789012345678901234567890", 50)