Files
vz-kore-adapter/chain_api.py

259 lines
9.3 KiB
Python

#!/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/<tx_hash>')
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/<kore_id>')
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/<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/<int:block_height>')
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/<webhook_id>', 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)