#!/usr/bin/env python3 """ Vector Zulu — Payment Mirror Adapter Polls KoreNet portal feeds for new payment events and executes the full payment mirror lifecycle via api.korenet.cloud. Full lifecycle per VZ runbook: INCOMING SWIFT MT103/PACS.008 → MINT mirror tokens → SETTLE T+0 → REDEEM fiat → BURN-SELL via venue routing → CLOSE tranche → ANCHOR to Ethereum (ISO 20022 proof) → POST confirmation KoreID back to KoreChain Dependencies: - vz_mtls_client.py (Bryan's official client) - koreid.py (KoreID generator) - Redis (event deduplication) - Postgres (audit trail) """ import os import json import time import logging import hashlib import subprocess import requests import redis import psycopg2 import urllib3 from datetime import datetime, timezone from koreid import KoreIDGenerator, wrap_ethereum_event urllib3.disable_warnings() logging.basicConfig( level=logging.INFO, format='%(asctime)s [MIRROR] %(levelname)s %(message)s' ) log = logging.getLogger(__name__) # ── Config ──────────────────────────────────────────────────────────────────── CERT = os.environ.get('VECTOR_ZULU_CERT', '/certs/vector-zulu-client.fullchain.pem') KEY = os.environ.get('VECTOR_ZULU_KEY', '/certs/vector-zulu-client.key') RAILS_URL = os.environ.get('VECTOR_ZULU_RAILS_URL', 'https://vector.korenet.cloud') API_URL = os.environ.get('VECTOR_ZULU_API_URL', 'https://api.korenet.cloud') POLL_INTERVAL = int(os.environ.get('POLL_INTERVAL', '30')) # Venue routing priority (per runbook) VENUES = ['cryptohost', 'koreexchange', 'uniswap'] # VZ vault IDs VZ_VAULTS = ['VZ-USD-VAULT', 'VZ-USDT-VAULT', 'VZ-BTC-VAULT'] # ── Infrastructure ──────────────────────────────────────────────────────────── def get_redis(): return redis.Redis( host=os.environ.get('REDIS_HOST', 'localhost'), port=6379, decode_responses=True ) def get_pg(): return psycopg2.connect( host=os.environ.get('POSTGRES_HOST', 'localhost'), dbname=os.environ.get('POSTGRES_DB', 'vz_oracle'), user=os.environ.get('POSTGRES_USER', 'vz_oracle'), password=os.environ.get('POSTGRES_PASSWORD', ''), connect_timeout=10 ) def session(): s = requests.Session() s.cert = (CERT, KEY) s.verify = False s.headers = {'X-Tenant-Id': 'vector-zulu'} return s def run_tool(*args) -> dict: """Run vz_mtls_client.py with given args, return parsed JSON.""" cmd = ['python3', 'tools/vz_mtls_client.py'] + list(args) try: result = subprocess.run( cmd, capture_output=True, text=True, cwd=os.path.expanduser('~/vector-zulu'), timeout=30 ) # Strip [OK]/[FAIL] prefix line lines = result.stdout.strip().split('\n') json_lines = [l for l in lines if not l.startswith('[OK]') and not l.startswith('[FAIL]')] return json.loads('\n'.join(json_lines)) except Exception as e: log.error(f"Tool error: {e}") return {'error': str(e)} # ── Feed polling ────────────────────────────────────────────────────────────── def poll_feed(path: str) -> list: """Poll a KoreNet portal feed, return new items.""" try: resp = session().get(f'{RAILS_URL}/{path}', timeout=15) if resp.status_code == 200: data = resp.json() # Return whichever list field is present for key in ['payments', 'settlements', 'deals']: if key in data and data[key]: return data[key] except Exception as e: log.error(f"Feed poll error {path}: {e}") return [] def is_processed(r, event_id: str) -> bool: """Check Redis deduplication cache.""" return r.exists(f'mirror:processed:{event_id}') == 1 def mark_processed(r, event_id: str): """Mark event as processed in Redis (24h TTL).""" r.setex(f'mirror:processed:{event_id}', 86400, '1') # ── Compliance checks ───────────────────────────────────────────────────────── def sanctions_check(entity: str, jurisdiction: str = 'ZA') -> bool: """ Run sanctions screening before any mirror operation. Returns True if CLEAR, False if HIT. """ result = run_tool('oracle', 'sanctions', '--entity', entity, '--jurisdiction', jurisdiction) status = result.get('status', 'UNKNOWN') if status == 'CLEAR': log.info(f"Sanctions CLEAR: {entity}") return True else: log.warning(f"Sanctions HIT: {entity} — {status}") return False def entity_enrich(entity: str, entity_type: str = 'BENEFICIARY', jurisdiction: str = 'ZA') -> dict: """Enrich entity data before mirror mint.""" return run_tool('oracle', 'enrich', '--entity', entity, '--type', entity_type, '--jurisdiction', jurisdiction) # ── Payment mirror lifecycle ────────────────────────────────────────────────── def mirror_mint(uetr: str, customer: str, asset: str, amount: float, rail_ref: str, vault: str) -> dict: """ Step 1: Mint mirror tokens against incoming SWIFT payment. """ log.info(f"MINT {amount} {asset} — UETR: {uetr}") return run_tool('mirror', 'mint', '--uetr', uetr, '--customer', customer, '--asset', asset, '--amount', str(amount), '--rail-ref', rail_ref, '--vault', vault) def mirror_settle(uetr: str) -> dict: """Step 2: Record T+0 settlement.""" log.info(f"SETTLE — UETR: {uetr}") settled_at = datetime.now(timezone.utc).isoformat() return run_tool('mirror', 'settle', '--uetr', uetr, '--settled-at', settled_at) def mirror_redeem(uetr: str, amount: float) -> dict: """Step 3: Redeem fiat from VZ vault.""" log.info(f"REDEEM {amount} — UETR: {uetr}") return run_tool('mirror', 'redeem', '--uetr', uetr, '--amount', str(amount)) def mirror_burn_sell(uetr: str, amount: float) -> dict: """ Step 4: Burn-sell via venue routing. Tries CryptoHost → KoreExchange → Uniswap per runbook. """ for venue in VENUES: log.info(f"BURN-SELL via {venue} — UETR: {uetr}") result = run_tool('mirror', 'burn-sell', '--uetr', uetr, '--amount', str(amount), '--venue', venue) if 'error' not in result: log.info(f"Burn-sell success via {venue}") return result log.warning(f"Venue {venue} failed, trying next") return {'error': 'All venues failed'} def mirror_close(uetr: str) -> dict: """Step 5: Close the tranche.""" log.info(f"CLOSE — UETR: {uetr}") return run_tool('mirror', 'close', '--uetr', uetr) # ── Ethereum anchoring ──────────────────────────────────────────────────────── def anchor_to_ethereum(kore_id: str, uetr: str, amount: float, currency: str, event_type: str) -> str: """ Anchor KoreNet event to Ethereum mainnet. Returns Ethereum TX hash. TODO: Deploy KoreAnchor.sol contract and call it here. For now, stores in oracle transmit log. """ payload = { 'koreId': kore_id, 'uetr': uetr, 'amount': amount, 'currency': currency, 'eventType': event_type, 'timestamp': datetime.now(timezone.utc).isoformat(), 'hash': hashlib.sha256( json.dumps({'koreId': kore_id, 'uetr': uetr, 'amount': amount}, sort_keys=True).encode() ).hexdigest() } log.info(f"Anchoring to Ethereum: {kore_id} — {event_type}") # TODO: Call KoreAnchor.sol contract # tx_hash = kore_anchor_contract.anchor(payload) # return tx_hash return f"0x{payload['hash'][:64]}" # placeholder until contract deployed def trigger_oracle_update(): """Immediately trigger oracle transmit when VZ vault balance changes.""" log.info("Triggering immediate oracle update") try: import subprocess subprocess.Popen( ['node', '/opt/eth-deploy-v21/manual_transmit.js'], stdout=open('/home/nigel/vz-transmit.log', 'a'), stderr=subprocess.STDOUT ) except Exception as e: log.error(f"Oracle update failed: {e}") # ── KoreID confirmation ─────────────────────────────────────────────────────── def post_confirmation(original_kore_id: str, tx_hash: str, uetr: str, event_type: str, pg) -> dict: """ Post Ethereum anchor confirmation back to KoreChain. Generates a confirmation KoreID linked to the original. TODO: POST to korechain.korenet.cloud/api/v1/anchor once G2 cert trusted. """ gen = KoreIDGenerator(pg) payload = gen.generate( event_type=f'{event_type}_CONFIRM', currency='USD', current_network='ETHEREUM', parent_network='KORECHAIN', ) confirmation = { 'koreid': payload['koreid'], 'linked_to': original_kore_id, 'uetr': uetr, 'ethereum_tx': tx_hash, 'event_type': f'{event_type}_CONFIRM', 'anchored_at': datetime.now(timezone.utc).isoformat(), 'proof': { 'hash': tx_hash, 'network': 'ETHEREUM_MAINNET', } } log.info(f"Confirmation KoreID: {payload['koreid']} → {original_kore_id}") # TODO: POST to korechain.korenet.cloud once G2 cert trusted # session().post('https://korechain.korenet.cloud/api/v1/anchor', json=confirmation) # Queue for when endpoint is available r = get_redis() r.lpush('korechain:outbound_queue', json.dumps(confirmation)) return confirmation # ── Main processing ─────────────────────────────────────────────────────────── def process_payment_event(event: dict, r, pg): """ Process a single payment/deal event from the KoreNet feed. Executes full mirror lifecycle. """ event_id = event.get('id') or event.get('dealId') or event.get('uetr', '') kore_id = event.get('koreId') or event.get('kore_id', '') amount = float(event.get('amount', 0)) currency = event.get('currency', 'USD') entity = event.get('counterparty') or event.get('customer', 'UNKNOWN') uetr = event.get('uetr') or event.get('railRef') or event_id rail_ref = event.get('railRef') or event.get('reference', f'REF-{event_id}') vault = f"VZ-{currency}-VAULT" if f"VZ-{currency}-VAULT" in VZ_VAULTS else 'VZ-USD-VAULT' if not event_id or is_processed(r, event_id): return log.info(f"Processing event: {event_id} | {currency} {amount:,.2f} | KoreID: {kore_id}") try: # Step 0: Compliance if not sanctions_check(entity): log.error(f"SANCTIONS HIT — {entity} — rejecting {event_id}") mark_processed(r, event_id) return # Step 1: Mint mint_result = mirror_mint(uetr, 'vector-zulu', currency, amount, rail_ref, vault) if 'error' in mint_result: log.error(f"Mint failed: {mint_result}") return # Anchor MINT to Ethereum tx_hash = anchor_to_ethereum(kore_id, uetr, amount, currency, 'MINT') post_confirmation(kore_id, tx_hash, uetr, 'MINT', pg) # Step 2: Settle settle_result = mirror_settle(uetr) tx_hash = anchor_to_ethereum(kore_id, uetr, amount, currency, 'SETTLE') post_confirmation(kore_id, tx_hash, uetr, 'SETTLE', pg) # Step 3: Redeem redeem_result = mirror_redeem(uetr, amount) tx_hash = anchor_to_ethereum(kore_id, uetr, amount, currency, 'REDEEM') post_confirmation(kore_id, tx_hash, uetr, 'REDEEM', pg) # Trigger oracle update if VZ vault affected if vault in VZ_VAULTS: trigger_oracle_update() # Step 4: Burn-sell burn_result = mirror_burn_sell(uetr, amount) tx_hash = anchor_to_ethereum(kore_id, uetr, amount, currency, 'BURN_SELL') post_confirmation(kore_id, tx_hash, uetr, 'BURN_SELL', pg) # Step 5: Close close_result = mirror_close(uetr) tx_hash = anchor_to_ethereum(kore_id, uetr, amount, currency, 'CLOSE') post_confirmation(kore_id, tx_hash, uetr, 'CLOSE', pg) mark_processed(r, event_id) log.info(f"✓ Complete: {event_id} | {currency} {amount:,.2f}") except Exception as e: log.error(f"Processing error {event_id}: {e}") def run(): log.info("Vector Zulu Payment Mirror Adapter starting...") log.info(f"Rails: {RAILS_URL} | API: {API_URL} | Poll: {POLL_INTERVAL}s") r = get_redis() pg = get_pg() while True: # Poll all three feeds payments = poll_feed('api/v1/tenants/portal/payments/feed') settlements = poll_feed('api/v1/tenants/portal/settlements/feed') deals = poll_feed('api/v1/tenants/deals/feed') all_events = payments + settlements + deals if all_events: log.info(f"Found {len(all_events)} events to process") for event in all_events: process_payment_event(event, r, pg) else: log.debug("No new events") time.sleep(POLL_INTERVAL) if __name__ == '__main__': run()