|
| 1 | +from __future__ import annotations |
| 2 | +#!/usr/bin/env python3 |
| 3 | +""" |
| 4 | +01 — Contract Conversion: End-to-End Pipeline |
| 5 | +
|
| 6 | +Demonstrates the full 0pnMatrx contract conversion flow on Base Sepolia: |
| 7 | +
|
| 8 | + 1. Takes a plain English rental agreement description |
| 9 | + 2. Converts it to optimised Solidity via ContractConversionService |
| 10 | + 3. Runs Glasswing (Morpheus) security audit |
| 11 | + 4. Deploys the compiled contract to Base Sepolia |
| 12 | + 5. Creates an EAS attestation for the deployment |
| 13 | +
|
| 14 | +This is the core value proposition of 0pnMatrx: describe a contract in |
| 15 | +plain English and the platform handles parsing, generation, auditing, |
| 16 | +compilation, deployment, and attestation. |
| 17 | +
|
| 18 | +Usage: |
| 19 | + python examples/01_contract_conversion.py |
| 20 | +""" |
| 21 | + |
| 22 | +import asyncio |
| 23 | +import json |
| 24 | +import os |
| 25 | +import sys |
| 26 | +import time |
| 27 | + |
| 28 | +# Ensure repo root is importable |
| 29 | +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| 30 | +sys.path.insert(0, ROOT) |
| 31 | + |
| 32 | +from runtime.blockchain.services.service_dispatcher import ServiceDispatcher |
| 33 | + |
| 34 | +# ── Colours ────────────────────────────────────────────────────────── |
| 35 | +CYAN = "\033[96m"; GREEN = "\033[92m"; YELLOW = "\033[93m" |
| 36 | +RED = "\033[91m"; BOLD = "\033[1m"; DIM = "\033[2m"; RESET = "\033[0m" |
| 37 | + |
| 38 | +def step(n, text): print(f"\n{CYAN}{BOLD}[Step {n}]{RESET} {text}") |
| 39 | +def ok(text): print(f" {GREEN}+{RESET} {text}") |
| 40 | +def warn(text): print(f" {YELLOW}!{RESET} {text}") |
| 41 | +def fail(text): print(f" {RED}x{RESET} {text}") |
| 42 | + |
| 43 | + |
| 44 | +def load_config() -> dict: |
| 45 | + config_path = os.path.join(ROOT, "openmatrix.config.json") |
| 46 | + if not os.path.exists(config_path): |
| 47 | + fail(f"Config not found: {config_path}") |
| 48 | + fail("Copy openmatrix.config.json.example to openmatrix.config.json and fill in your credentials.") |
| 49 | + sys.exit(1) |
| 50 | + with open(config_path) as f: |
| 51 | + return json.load(f) |
| 52 | + |
| 53 | + |
| 54 | +# ── The rental agreement we want to convert ───────────────────────── |
| 55 | + |
| 56 | +RENTAL_AGREEMENT_PSEUDOCODE = """\ |
| 57 | +contract RentalAgreement |
| 58 | +
|
| 59 | + state landlord: address |
| 60 | + state tenant: address |
| 61 | + state monthlyRent: uint256 |
| 62 | + state deposit: uint256 |
| 63 | + state leaseStart: uint256 |
| 64 | + state leaseEnd: uint256 |
| 65 | + state isActive: bool |
| 66 | + state rentPaid: map |
| 67 | +
|
| 68 | + event LeaseCreated(address indexed landlord, address indexed tenant, uint256 rent) |
| 69 | + event RentPaid(address indexed tenant, uint256 month, uint256 amount) |
| 70 | + event DepositReturned(address indexed tenant, uint256 amount) |
| 71 | + event LeaseTerminated(address indexed by, uint256 timestamp) |
| 72 | +
|
| 73 | + function constructor(tenantAddr: address, rent: uint256, durationMonths: uint256) |
| 74 | + landlord = msg.sender |
| 75 | + tenant = tenantAddr |
| 76 | + monthlyRent = rent |
| 77 | + deposit = rent * 2 |
| 78 | + leaseStart = block.timestamp |
| 79 | + leaseEnd = block.timestamp + (durationMonths * 30 days) |
| 80 | + isActive = true |
| 81 | + emit LeaseCreated(landlord, tenant, rent) |
| 82 | +
|
| 83 | + payable function payRent(month: uint256) |
| 84 | + require(msg.sender == tenant, "Only tenant can pay rent") |
| 85 | + require(isActive, "Lease not active") |
| 86 | + require(msg.value == monthlyRent, "Must pay exact rent") |
| 87 | + require(!rentPaid[month], "Already paid") |
| 88 | + rentPaid[month] = true |
| 89 | + payable(landlord).transfer(msg.value) |
| 90 | + emit RentPaid(tenant, month, msg.value) |
| 91 | +
|
| 92 | + function terminateLease() |
| 93 | + require(msg.sender == landlord || msg.sender == tenant, "Unauthorized") |
| 94 | + require(isActive, "Already terminated") |
| 95 | + isActive = false |
| 96 | + emit LeaseTerminated(msg.sender, block.timestamp) |
| 97 | +
|
| 98 | + function returnDeposit() |
| 99 | + require(msg.sender == landlord, "Only landlord") |
| 100 | + require(!isActive, "Lease still active") |
| 101 | + payable(tenant).transfer(deposit) |
| 102 | + emit DepositReturned(tenant, deposit) |
| 103 | +
|
| 104 | + view function getLeaseInfo() -> (address, address, uint256, uint256, bool) |
| 105 | + return (landlord, tenant, monthlyRent, leaseEnd, isActive) |
| 106 | +""" |
| 107 | + |
| 108 | + |
| 109 | +async def main(): |
| 110 | + print(f""" |
| 111 | +{CYAN}{BOLD}{'=' * 60} |
| 112 | + 0pnMatrx Example 01: Contract Conversion Pipeline |
| 113 | +{'=' * 60}{RESET} |
| 114 | +""") |
| 115 | + |
| 116 | + config = load_config() |
| 117 | + dispatcher = ServiceDispatcher(config) |
| 118 | + |
| 119 | + # ── Step 1: Show the input ────────────────────────────────────── |
| 120 | + step(1, "Input: Plain English Rental Agreement (pseudocode)") |
| 121 | + print(f"{DIM}") |
| 122 | + for line in RENTAL_AGREEMENT_PSEUDOCODE.strip().splitlines(): |
| 123 | + print(f" {line}") |
| 124 | + print(f"{RESET}") |
| 125 | + |
| 126 | + # ── Step 2: Estimate conversion cost ──────────────────────────── |
| 127 | + step(2, "Estimating conversion cost...") |
| 128 | + try: |
| 129 | + estimate_result = await dispatcher.execute( |
| 130 | + action="estimate_contract_cost", |
| 131 | + params={"source_code": RENTAL_AGREEMENT_PSEUDOCODE}, |
| 132 | + ) |
| 133 | + estimate = json.loads(estimate_result) |
| 134 | + if estimate.get("status") == "ok": |
| 135 | + est = estimate["result"] |
| 136 | + ok(f"Tier: {est.get('tier', 'N/A')}") |
| 137 | + ok(f"Estimated fee: {est.get('fee_display', 'N/A')}") |
| 138 | + ok(f"Lines: {est.get('line_count', 'N/A')}") |
| 139 | + ok(f"Complexity: {est.get('complexity_score', 'N/A')}") |
| 140 | + else: |
| 141 | + warn(f"Estimate returned: {estimate.get('error', 'unknown error')}") |
| 142 | + except Exception as e: |
| 143 | + warn(f"Cost estimation failed (non-critical): {e}") |
| 144 | + |
| 145 | + # ── Step 3: Convert to Solidity ───────────────────────────────── |
| 146 | + step(3, "Converting pseudocode to optimised Solidity via ContractConversionService...") |
| 147 | + t0 = time.monotonic() |
| 148 | + try: |
| 149 | + convert_result = await dispatcher.execute( |
| 150 | + action="convert_contract", |
| 151 | + params={ |
| 152 | + "source_code": RENTAL_AGREEMENT_PSEUDOCODE, |
| 153 | + "source_lang": "pseudocode", |
| 154 | + "target_chain": "base", |
| 155 | + }, |
| 156 | + ) |
| 157 | + elapsed = (time.monotonic() - t0) * 1000 |
| 158 | + result = json.loads(convert_result) |
| 159 | + |
| 160 | + if result.get("status") != "ok": |
| 161 | + fail(f"Conversion failed: {result.get('error', 'unknown')}") |
| 162 | + sys.exit(1) |
| 163 | + |
| 164 | + conv = result["result"] |
| 165 | + ok(f"Contract name: {conv.get('contract_name', 'N/A')}") |
| 166 | + ok(f"Target chain: {conv.get('target_chain', 'N/A')}") |
| 167 | + ok(f"Conversion time: {conv.get('conversion_time_ms', elapsed):.1f}ms") |
| 168 | + ok(f"Tier: {conv.get('tier', {}).get('tier', 'N/A')}") |
| 169 | + |
| 170 | + # Show audit results |
| 171 | + audit = conv.get("audit", {}) |
| 172 | + audit_passed = conv.get("audit_passed", None) |
| 173 | + if audit_passed is True: |
| 174 | + ok(f"Security audit: PASSED") |
| 175 | + elif audit_passed is False: |
| 176 | + warn(f"Security audit: FAILED — {audit.get('summary', 'see details')}") |
| 177 | + else: |
| 178 | + ok("Security audit: completed") |
| 179 | + |
| 180 | + if audit.get("findings"): |
| 181 | + for finding in audit["findings"][:3]: |
| 182 | + severity = finding.get("severity", "info") |
| 183 | + msg = finding.get("message", finding.get("description", "")) |
| 184 | + print(f" {DIM}[{severity}] {msg}{RESET}") |
| 185 | + |
| 186 | + # Show generated Solidity |
| 187 | + generated = conv.get("generated_source", "") |
| 188 | + if generated: |
| 189 | + print(f"\n{DIM}{'=' * 50}") |
| 190 | + print("Generated Solidity:") |
| 191 | + print(f"{'=' * 50}{RESET}") |
| 192 | + for i, line in enumerate(generated.splitlines()[:40], 1): |
| 193 | + print(f" {DIM}{i:3d}{RESET} {line}") |
| 194 | + if len(generated.splitlines()) > 40: |
| 195 | + print(f" {DIM}... ({len(generated.splitlines()) - 40} more lines){RESET}") |
| 196 | + print(f"{DIM}{'=' * 50}{RESET}") |
| 197 | + |
| 198 | + except Exception as e: |
| 199 | + fail(f"Conversion failed: {e}") |
| 200 | + fail("Make sure all dependencies are installed: pip install web3 eth-account py-solc-x") |
| 201 | + sys.exit(1) |
| 202 | + |
| 203 | + # ── Step 4: Deploy to Base Sepolia ────────────────────────────── |
| 204 | + step(4, "Deploying to Base Sepolia...") |
| 205 | + bc = config.get("blockchain", {}) |
| 206 | + rpc_url = bc.get("rpc_url", "") |
| 207 | + private_key = bc.get("demo_wallet_private_key", "") |
| 208 | + |
| 209 | + if not rpc_url or rpc_url.startswith("YOUR_"): |
| 210 | + warn("Skipping deployment: blockchain.rpc_url not configured.") |
| 211 | + warn("Set blockchain.rpc_url in openmatrix.config.json to deploy.") |
| 212 | + elif not private_key or private_key.startswith("YOUR_"): |
| 213 | + warn("Skipping deployment: blockchain.demo_wallet_private_key not configured.") |
| 214 | + warn("Set blockchain.demo_wallet_private_key in openmatrix.config.json to deploy.") |
| 215 | + else: |
| 216 | + try: |
| 217 | + deploy_result = await dispatcher.execute( |
| 218 | + action="deploy_contract", |
| 219 | + params={ |
| 220 | + "source_code": generated, |
| 221 | + "source_lang": "solidity", |
| 222 | + "target_chain": "base", |
| 223 | + }, |
| 224 | + ) |
| 225 | + deploy = json.loads(deploy_result) |
| 226 | + if deploy.get("status") == "ok": |
| 227 | + dep = deploy["result"] |
| 228 | + ok(f"Contract deployed at: {dep.get('contract_address', 'N/A')}") |
| 229 | + ok(f"Tx hash: {dep.get('tx_hash', 'N/A')}") |
| 230 | + ok(f"Block: {dep.get('block_number', 'N/A')}") |
| 231 | + ok(f"Gas used: {dep.get('gas_used', 'N/A')}") |
| 232 | + ok(f"Gas paid by: {dep.get('gas_paid_by', 'platform')}") |
| 233 | + |
| 234 | + contract_address = dep.get("contract_address", "") |
| 235 | + |
| 236 | + # ── Step 5: Create EAS attestation ────────────────── |
| 237 | + step(5, "Creating EAS attestation for deployment...") |
| 238 | + try: |
| 239 | + attest_result = await dispatcher.execute( |
| 240 | + action="create_attestation", |
| 241 | + params={ |
| 242 | + "schema_name": "contract_deployment", |
| 243 | + "data": { |
| 244 | + "contract_address": contract_address, |
| 245 | + "contract_name": conv.get("contract_name", "RentalAgreement"), |
| 246 | + "deployer": bc.get("demo_wallet_address", ""), |
| 247 | + "chain": "base-sepolia", |
| 248 | + "audit_passed": audit_passed, |
| 249 | + }, |
| 250 | + "recipient": bc.get("demo_wallet_address", "0x0"), |
| 251 | + }, |
| 252 | + ) |
| 253 | + attest = json.loads(attest_result) |
| 254 | + if attest.get("status") == "ok": |
| 255 | + att = attest["result"] |
| 256 | + ok(f"Attestation created: {att.get('uid', att.get('attestation_tx', 'N/A'))}") |
| 257 | + ok(f"Attestation status: {att.get('status', 'ok')}") |
| 258 | + else: |
| 259 | + warn(f"Attestation: {attest.get('error', 'failed')}") |
| 260 | + except Exception as e: |
| 261 | + warn(f"EAS attestation failed (non-critical): {e}") |
| 262 | + |
| 263 | + # ── Summary ───────────────────────────────────────── |
| 264 | + explorer = "https://sepolia.basescan.org" |
| 265 | + print(f""" |
| 266 | +{GREEN}{BOLD}{'=' * 60} |
| 267 | + PIPELINE COMPLETE |
| 268 | +{'=' * 60}{RESET} |
| 269 | +
|
| 270 | + {BOLD}Input:{RESET} Plain English rental agreement (pseudocode) |
| 271 | + {BOLD}Output:{RESET} Deployed + Attested Solidity contract |
| 272 | + {BOLD}Contract:{RESET} {contract_address} |
| 273 | + {BOLD}Explorer:{RESET} {explorer}/address/{contract_address} |
| 274 | + {BOLD}Network:{RESET} Base Sepolia (chain 84532) |
| 275 | + {BOLD}Audit:{RESET} {"PASSED" if audit_passed else "COMPLETED"} |
| 276 | +
|
| 277 | +{GREEN}{'=' * 60}{RESET} |
| 278 | +""") |
| 279 | + else: |
| 280 | + warn(f"Deployment returned: {deploy.get('error', 'unknown')}") |
| 281 | + warn("This may be expected if your wallet lacks Base Sepolia ETH.") |
| 282 | + warn("Get test ETH: https://www.alchemy.com/faucets/base-sepolia") |
| 283 | + except Exception as e: |
| 284 | + warn(f"Deployment failed: {e}") |
| 285 | + warn("Ensure you have Base Sepolia ETH in your demo wallet.") |
| 286 | + |
| 287 | + print(f"\n{DIM}Pipeline complete. This example demonstrated the full") |
| 288 | + print(f"contract conversion flow: pseudocode -> Solidity -> audit -> deploy -> attest.{RESET}\n") |
| 289 | + |
| 290 | + |
| 291 | +if __name__ == "__main__": |
| 292 | + asyncio.run(main()) |
0 commit comments