mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 12:41:48 +00:00
211 lines
8.2 KiB
Python
211 lines
8.2 KiB
Python
"""
|
|
blockchain.py — The entire Polygon Amoy integration in one file.
|
|
|
|
Silently registers agreement proofs on-chain after every resolved negotiation.
|
|
Users never interact with this module — they only see the "🔗 Verified" badge
|
|
and a PolygonScan link in their Telegram message / dashboard card.
|
|
|
|
Graceful fallback: if blockchain is down or not configured, the negotiation
|
|
still completes normally. Web3 is additive, never blocking.
|
|
"""
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
|
|
from web3 import Web3
|
|
from web3.middleware import ExtraDataToPOAMiddleware
|
|
|
|
import sys as _sys
|
|
_sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from contract_abi import AGREEMENT_REGISTRY_ABI
|
|
|
|
# ── Configuration ─────────────────────────────────────────────────────────────
|
|
POLYGON_RPC = os.getenv("POLYGON_RPC_URL", "https://rpc-amoy.polygon.technology/")
|
|
PRIVATE_KEY = os.getenv("POLYGON_PRIVATE_KEY", "")
|
|
CONTRACT_ADDRESS = os.getenv("AGREEMENT_CONTRACT_ADDRESS", "")
|
|
EXPLORER_BASE = "https://amoy.polygonscan.com"
|
|
CHAIN_ID = 80002
|
|
|
|
# Fallback RPCs tried in order if the primary is slow or down
|
|
_FALLBACK_RPCS = [
|
|
"https://rpc-amoy.polygon.technology/",
|
|
"https://polygon-amoy-bor-rpc.publicnode.com",
|
|
"https://polygon-amoy.drpc.org",
|
|
]
|
|
|
|
# ── Connect to Polygon Amoy ───────────────────────────────────────────────────
|
|
def _make_w3(rpc_url: str) -> Web3:
|
|
w3 = Web3(Web3.HTTPProvider(rpc_url, request_kwargs={"timeout": 15}))
|
|
# Polygon uses POA consensus — inject middleware to handle extraData field
|
|
w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
|
|
return w3
|
|
|
|
|
|
def _connect() -> Web3:
|
|
"""Try each RPC in order, return the first that connects."""
|
|
rpcs = [POLYGON_RPC] + [r for r in _FALLBACK_RPCS if r != POLYGON_RPC]
|
|
for rpc in rpcs:
|
|
try:
|
|
w3 = _make_w3(rpc)
|
|
if w3.is_connected():
|
|
return w3
|
|
except Exception:
|
|
continue
|
|
# Return last attempt even if not connected — errors will be caught downstream
|
|
return _make_w3(POLYGON_RPC)
|
|
|
|
|
|
w3 = _connect()
|
|
account = None
|
|
contract = None
|
|
|
|
if PRIVATE_KEY and CONTRACT_ADDRESS:
|
|
try:
|
|
account = w3.eth.account.from_key(PRIVATE_KEY)
|
|
contract = w3.eth.contract(
|
|
address=Web3.to_checksum_address(CONTRACT_ADDRESS),
|
|
abi=AGREEMENT_REGISTRY_ABI,
|
|
)
|
|
print(f"✅ Blockchain ready | Polygon Amoy | Signer: {account.address}")
|
|
except Exception as e:
|
|
print(f"⚠️ Blockchain setup failed: {e} — falling back to mock proofs.")
|
|
else:
|
|
print("⚠️ Blockchain not configured (POLYGON_PRIVATE_KEY / AGREEMENT_CONTRACT_ADDRESS missing). "
|
|
"Agreement proofs will be mocked.")
|
|
|
|
|
|
# ── Core helpers ──────────────────────────────────────────────────────────────
|
|
|
|
def hash_agreement(resolution_data: dict) -> bytes:
|
|
"""
|
|
Produce a deterministic SHA-256 digest of the resolution JSON.
|
|
Returns raw bytes (32 bytes) suitable for bytes32 in Solidity.
|
|
"""
|
|
canonical = json.dumps(resolution_data, sort_keys=True, default=str)
|
|
return hashlib.sha256(canonical.encode()).digest()
|
|
|
|
|
|
def _mock_proof(negotiation_id: str, agreement_hash: bytes, error: str = "") -> dict:
|
|
"""Return a well-structured mock/fallback proof dict."""
|
|
tag = "MOCK" if not error else "FAILED"
|
|
return {
|
|
"success": not bool(error),
|
|
"mock": True,
|
|
"error": error,
|
|
"tx_hash": f"0x{tag}_{negotiation_id}_{'a' * 20}",
|
|
"block_number": 0,
|
|
"agreement_hash": "0x" + agreement_hash.hex(),
|
|
"explorer_url": EXPLORER_BASE,
|
|
"gas_used": 0,
|
|
"network": f"polygon-amoy ({'mock' if not error else 'failed — ' + error[:60]})",
|
|
}
|
|
|
|
|
|
# ── Main public function ──────────────────────────────────────────────────────
|
|
|
|
async def register_agreement_on_chain(
|
|
negotiation_id: str,
|
|
feature_type: str,
|
|
summary: str,
|
|
resolution_data: dict,
|
|
) -> dict:
|
|
"""
|
|
Register an immutable agreement proof on Polygon Amoy.
|
|
|
|
Called automatically after every resolved negotiation.
|
|
INVISIBLE to the user — they only see the PolygonScan link in their message.
|
|
|
|
Returns a dict with keys:
|
|
success, mock, tx_hash, block_number, agreement_hash,
|
|
explorer_url, gas_used, network
|
|
"""
|
|
agreement_hash = hash_agreement(resolution_data)
|
|
|
|
# ── No contract configured → return labelled mock ──────────────────────
|
|
if not contract or not account:
|
|
return _mock_proof(negotiation_id, agreement_hash)
|
|
|
|
try:
|
|
nonce = w3.eth.get_transaction_count(account.address)
|
|
gas_price = w3.eth.gas_price
|
|
tip = w3.to_wei(35, "gwei")
|
|
max_fee = gas_price + tip
|
|
|
|
tx = contract.functions.registerAgreement(
|
|
negotiation_id,
|
|
agreement_hash, # bytes32 — raw 32-byte digest
|
|
feature_type,
|
|
summary[:256], # cap at 256 chars to keep gas low
|
|
).build_transaction({
|
|
"from": account.address,
|
|
"nonce": nonce,
|
|
"gas": 300_000,
|
|
"maxFeePerGas": max_fee,
|
|
"maxPriorityFeePerGas": tip,
|
|
"chainId": CHAIN_ID,
|
|
})
|
|
|
|
signed = w3.eth.account.sign_transaction(tx, PRIVATE_KEY)
|
|
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
|
|
tx_hex = tx_hash.hex()
|
|
|
|
# Polygon Amoy confirms in ~2 s; wait up to 60 s
|
|
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=60)
|
|
|
|
result = {
|
|
"success": True,
|
|
"mock": False,
|
|
"tx_hash": tx_hex,
|
|
"block_number": receipt.blockNumber,
|
|
"agreement_hash": "0x" + agreement_hash.hex(),
|
|
"explorer_url": f"{EXPLORER_BASE}/tx/0x{tx_hex}",
|
|
"gas_used": receipt.gasUsed,
|
|
"network": "polygon-amoy",
|
|
}
|
|
print(f"✅ On-chain proof registered: {result['explorer_url']}")
|
|
return result
|
|
|
|
except Exception as e:
|
|
print(f"❌ Blockchain registration failed for {negotiation_id}: {e}")
|
|
# Negotiation still works — we just note the failure
|
|
return _mock_proof(negotiation_id, agreement_hash, error=str(e))
|
|
|
|
|
|
# ── Verification helper (used by dashboard) ───────────────────────────────────
|
|
|
|
def verify_agreement_on_chain(negotiation_id: str) -> dict:
|
|
"""
|
|
Read the stored agreement from the contract (view call — free, no gas).
|
|
Used by the dashboard to independently confirm on-chain state.
|
|
"""
|
|
if not contract:
|
|
return {"verified": False, "reason": "Contract not configured"}
|
|
|
|
try:
|
|
result = contract.functions.getAgreement(negotiation_id).call()
|
|
# result is a tuple: (agreementHash, featureType, summary, timestamp, registeredBy)
|
|
if result[3] == 0: # timestamp == 0 means not found
|
|
return {"verified": False, "reason": "Agreement not found on-chain"}
|
|
return {
|
|
"verified": True,
|
|
"agreement_hash": "0x" + result[0].hex(),
|
|
"feature_type": result[1],
|
|
"summary": result[2],
|
|
"timestamp": result[3],
|
|
"registered_by": result[4],
|
|
"explorer_url": f"{EXPLORER_BASE}/address/{CONTRACT_ADDRESS}",
|
|
}
|
|
except Exception as e:
|
|
return {"verified": False, "reason": str(e)}
|
|
|
|
|
|
def get_total_agreements() -> int:
|
|
"""Return the total number of agreements registered on-chain."""
|
|
if not contract:
|
|
return 0
|
|
try:
|
|
return contract.functions.totalAgreements().call()
|
|
except Exception:
|
|
return 0
|