Add chain_api.py — KoreNet integration API
This commit is contained in:
258
chain_api.py
Normal file
258
chain_api.py
Normal file
@@ -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/<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)
|
||||
Reference in New Issue
Block a user