#!/usr/bin/env python3 """ Vector Zulu — Chain API Serves Amitis L1 + KoreAnchor V1.0 data to KoreNet adapter. Port 8767 """ import os, json, time, hmac, hashlib, uuid, logging, requests, urllib3 from datetime import datetime, timezone from flask import Flask, jsonify, request, abort import redis, psycopg2 urllib3.disable_warnings() logging.basicConfig(level=logging.INFO, format='%(asctime)s [CHAIN-API] %(levelname)s %(message)s') log = logging.getLogger(__name__) app = Flask(__name__) ALCHEMY_URL = os.environ.get('ALCHEMY_URL', 'https://eth-mainnet.g.alchemy.com/v2/nRVT_IneMUDF3Pokubawx') KORE_ANCHOR = '0x8AaA785c7dC116b834E3A1766866078051fC0313' VZ_POR = '0x508DA1733567d3361988CD554038301c0E61e260' LICK_KEY = os.environ.get('VECTOR_ZULU_LICK_KEY', 'NotMyCircu$_NotMyMonkey$').encode() FINALITY_BLOCKS = 12 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 alchemy_call(method, params): r = requests.post(ALCHEMY_URL, json={ 'jsonrpc':'2.0','method':method,'params':params,'id':1 }, timeout=10) d = r.json() if 'error' in d: raise Exception(d['error']['message']) return d['result'] def sign_response(payload): ts = int(time.time()) body = json.dumps(payload, separators=(',',':')).encode() sig = hmac.new(LICK_KEY, body + b'\n' + str(ts).encode(), hashlib.sha256).hexdigest() return { 'signedBy': 'vector-zulu-chain-api-v1', 'timestamp': datetime.now(timezone.utc).isoformat(), 'signature': sig, 'payloadHash': hashlib.sha256(body).hexdigest() } def current_block(): result = alchemy_call('eth_blockNumber', []) return int(result, 16) def get_tx_receipt(tx_hash): return alchemy_call('eth_getTransactionReceipt', [tx_hash]) def init_schema(): try: pg = get_pg() with pg.cursor() as cur: cur.execute(""" CREATE TABLE IF NOT EXISTS chain_webhooks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), url TEXT NOT NULL, event_types TEXT[] NOT NULL, signing_key TEXT NOT NULL, max_retries INT DEFAULT 3, timeout_seconds INT DEFAULT 30, active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ DEFAULT NOW() ); """) pg.commit() pg.close() log.info("Chain API schema ready") except Exception as e: log.error(f"Schema init error: {e}") # ── Routes ──────────────────────────────────────────────────── @app.route('/health') def health(): try: block = current_block() r = get_redis() last_poll = r.get('vault:last_successful_poll') return jsonify({ 'status': 'OPERATIONAL', 'network': 'ETHEREUM_MAINNET', 'blockHeight': block, 'timestamp': datetime.now(timezone.utc).isoformat(), 'vaultCacheLastPoll': last_poll, }) except Exception as e: return jsonify({'status': 'DEGRADED', 'error': str(e)}), 503 @app.route('/v1/time') def server_time(): ts = int(time.time()) body = str(ts).encode() sig = hmac.new(LICK_KEY, body, hashlib.sha256).hexdigest() return jsonify({ 'timestamp': datetime.now(timezone.utc).isoformat(), 'epoch': ts, 'signature': sig }) @app.route('/v1/transactions/') def get_transaction(tx_hash): try: receipt = get_tx_receipt(tx_hash) if not receipt: return jsonify({'code':'NOT_FOUND','message':'Transaction not found'}), 404 block_num = int(receipt['blockNumber'], 16) current = current_block() confirmations = current - block_num status = 'CONFIRMED' if int(receipt['status'],16) == 1 else 'FAILED' if confirmations >= FINALITY_BLOCKS: status = 'FINALIZED' payload = { 'txHash': tx_hash, 'status': status, 'blockNumber': block_num, 'confirmations': confirmations, 'finalized': confirmations >= FINALITY_BLOCKS, 'gasUsed': int(receipt['gasUsed'],16), 'network': 'ETHEREUM_MAINNET', 'etherscan': f'https://etherscan.io/tx/{tx_hash}', 'timestamp': datetime.now(timezone.utc).isoformat(), } payload['provenance'] = sign_response(payload) return jsonify(payload) except Exception as e: return jsonify({'code':'ERROR','message':str(e)}), 500 @app.route('/v1/transactions/koreid/') def get_transaction_by_koreid(kore_id): try: pg = get_pg() with pg.cursor() as cur: cur.execute(""" SELECT koreid, event_type, status, tx_hash, amount, currency, posted_at FROM kore_outbound_events WHERE koreid = %s """, (kore_id,)) row = cur.fetchone() pg.close() if not row: return jsonify({'code':'NOT_FOUND','message':'No record for KoreID'}), 404 koreid, event_type, status, tx_hash, amount, currency, posted_at = row payload = { 'koreId': koreid, 'eventType': event_type, 'status': status, 'txHash': tx_hash, 'amount': str(amount), 'currency': currency, 'postedAt': posted_at.isoformat() if posted_at else None, 'network': 'ETHEREUM_MAINNET', 'etherscan': f'https://etherscan.io/tx/{tx_hash}' if tx_hash else None, } payload['provenance'] = sign_response(payload) return jsonify(payload) except Exception as e: return jsonify({'code':'ERROR','message':str(e)}), 500 @app.route('/v1/transactions/uetr/') def get_transaction_by_uetr(uetr): try: pg = get_pg() with pg.cursor() as cur: cur.execute(""" SELECT koreid, event_type, status, tx_hash, amount, currency, posted_at FROM kore_outbound_events WHERE guid::text = %s OR koreid LIKE %s """, (uetr, f'%{uetr[-8:]}%')) row = cur.fetchone() pg.close() if not row: return jsonify({'code':'NOT_FOUND','message':'No record for UETR'}), 404 koreid, event_type, status, tx_hash, amount, currency, posted_at = row payload = { 'uetr': uetr, 'koreId': koreid, 'eventType': event_type, 'status': status, 'txHash': tx_hash, 'amount': str(amount), 'currency': currency, 'postedAt': posted_at.isoformat() if posted_at else None, } payload['provenance'] = sign_response(payload) return jsonify(payload) except Exception as e: return jsonify({'code':'ERROR','message':str(e)}), 500 @app.route('/v1/blocks/') def get_block(block_height): try: current = current_block() confirmations = current - block_height payload = { 'blockHeight': block_height, 'currentBlock': current, 'confirmations': max(0, confirmations), 'finalized': confirmations >= FINALITY_BLOCKS, 'network': 'ETHEREUM_MAINNET', 'timestamp': datetime.now(timezone.utc).isoformat(), } return jsonify(payload) except Exception as e: return jsonify({'code':'ERROR','message':str(e)}), 500 @app.route('/v1/webhooks', methods=['POST']) def register_webhook(): data = request.get_json() if not data or not data.get('url') or not data.get('eventTypes'): return jsonify({'code':'INVALID','message':'url and eventTypes required'}), 400 try: pg = get_pg() webhook_id = str(uuid.uuid4()) with pg.cursor() as cur: cur.execute(""" INSERT INTO chain_webhooks (id, url, event_types, signing_key, max_retries, timeout_seconds) VALUES (%s, %s, %s, %s, %s, %s) """, (webhook_id, data['url'], data['eventTypes'], data.get('signingKey', str(uuid.uuid4())), data.get('maxRetries', 3), data.get('timeoutSeconds', 30))) pg.commit() pg.close() return jsonify({'webhookId': webhook_id, 'status': 'REGISTERED'}), 201 except Exception as e: return jsonify({'code':'ERROR','message':str(e)}), 500 @app.route('/v1/webhooks/', methods=['DELETE']) def delete_webhook(webhook_id): try: pg = get_pg() with pg.cursor() as cur: cur.execute("UPDATE chain_webhooks SET active=false WHERE id=%s", (webhook_id,)) pg.commit() pg.close() return '', 204 except Exception as e: return jsonify({'code':'ERROR','message':str(e)}), 500 if __name__ == '__main__': init_schema() log.info("Chain API starting on port 8767") app.run(host='0.0.0.0', port=8767)