From 98127f4ad4f2db82ccf4997a676471ea9b2e4351 Mon Sep 17 00:00:00 2001 From: JA Date: Tue, 11 Aug 2026 04:42:07 +0000 Subject: [PATCH] =?UTF-8?q?Add=20chain=5Fapi.py=20=E2=80=94=20KoreNet=20in?= =?UTF-8?q?tegration=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chain_api.py | 258 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 chain_api.py diff --git a/chain_api.py b/chain_api.py new file mode 100644 index 0000000..dd68945 --- /dev/null +++ b/chain_api.py @@ -0,0 +1,258 @@ +#!/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)