mirror of
https://github.com/arkorty/B.Tech-Project-III.git
synced 2026-04-19 20:51:49 +00:00
init
This commit is contained in:
210
negot8/backend/blockchain_web3/blockchain.py
Normal file
210
negot8/backend/blockchain_web3/blockchain.py
Normal file
@@ -0,0 +1,210 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user