From 01b905a1cc04e7bf3f1215f6842f1ae4acfba538 Mon Sep 17 00:00:00 2001 From: JA Date: Tue, 11 Aug 2026 04:42:09 +0000 Subject: [PATCH] =?UTF-8?q?Add=20ethereum=5Fapi.py=20=E2=80=94=20KoreNet?= =?UTF-8?q?=20integration=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ethereum_api.py | 357 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 357 insertions(+) create mode 100644 ethereum_api.py diff --git a/ethereum_api.py b/ethereum_api.py new file mode 100644 index 0000000..377bcaf --- /dev/null +++ b/ethereum_api.py @@ -0,0 +1,357 @@ +#!/usr/bin/env python3 +""" +Vector Zulu — Ethereum API +Exposes VZ smart contract state and KoreAnchor lookups to KoreNet adapter. +Port 8769 +""" +import os, json, time, hmac, hashlib, logging, requests, urllib3 +from datetime import datetime, timezone +from flask import Flask, jsonify, request +import psycopg2 + +urllib3.disable_warnings() +logging.basicConfig(level=logging.INFO, + format='%(asctime)s [ETH-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') +LICK_KEY = os.environ.get('VECTOR_ZULU_LICK_KEY', + 'NotMyCircu$_NotMyMonkey$').encode() +FINALITY_BLOCKS = 12 + +CONTRACTS = { + 'SVReserveOracle': { + 'name': 'SVReserveOracle V2.1', + 'address': '0x0BA2D70711286bacCD75B37EA47165A1E6CfD2eb', + 'deployedAt': '2026-05-01', + 'etherscan': 'https://etherscan.io/address/0x0BA2D70711286bacCD75B37EA47165A1E6CfD2eb' + }, + 'KoreAnchor': { + 'name': 'KoreAnchor V1.0', + 'address': '0x8AaA785c7dC116b834E3A1766866078051fC0313', + 'deployedAt': '2026-07-14', + 'deployTx': '0x75c2d2331713831b78c2aaaa994ffdb0b22114429a06b112f1237f84bd4a7d39', + 'etherscan': 'https://etherscan.io/address/0x8AaA785c7dC116b834E3A1766866078051fC0313' + }, + 'VZProofOfReserve': { + 'name': 'VZProofOfReserve', + 'address': '0x508DA1733567d3361988CD554038301c0E61e260', + 'etherscan': 'https://etherscan.io/address/0x508DA1733567d3361988CD554038301c0E61e260' + }, + 'SVTokenFactory': { + 'name': 'SVTokenFactory', + 'address': '0x6C347F4b582c86392Bc35C64e98F641f2F067DBC', + 'etherscan': 'https://etherscan.io/address/0x6C347F4b582c86392Bc35C64e98F641f2F067DBC' + }, +} + +SV_TOKENS = { + 'SVUSD': '0x0A925D0F18090d2953CaC1F90b71c79fEd0efdB2', + 'SVGBP': None, 'SVEUR': None, 'SVCHF': None, + 'SVJPY': None, 'SVSGD': None, +} + +ORACLE_WALLET = '0xA08298ed3Bf2aD64499BEB115441aed85E0985Cc' + +# Function selectors +SEL_LATEST_ANSWER = '0x50d25bcd' +SEL_LATEST_TIMESTAMP = '0x8205bf6a' +SEL_IS_ADEQUATE = '0x2e0c8d6a' +SEL_TOTAL_SUPPLY = '0x18160ddd' + +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 eth_call(to, data): + return alchemy_call('eth_call', [{'to': to, 'data': data}, 'latest']) + +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-eth-api-v1', + 'timestamp': datetime.now(timezone.utc).isoformat(), + 'signature': sig, + 'payloadHash': hashlib.sha256(body).hexdigest() + } + +def current_block(): + return int(alchemy_call('eth_blockNumber', []), 16) + +# ── Routes ──────────────────────────────────────────────────── + +@app.route('/health') +def health(): + try: + block = current_block() + return jsonify({ + 'status': 'OPERATIONAL', + 'network': 'ETHEREUM_MAINNET', + 'blockHeight': block, + 'timestamp': datetime.now(timezone.utc).isoformat(), + }) + except Exception as e: + return jsonify({'status': 'DEGRADED', 'error': str(e)}), 503 + +@app.route('/v1/contracts') +def list_contracts(): + return jsonify({ + 'network': 'ETHEREUM_MAINNET', + 'contracts': CONTRACTS, + 'oracleWallet': ORACLE_WALLET, + 'svTokens': SV_TOKENS, + 'timestamp': datetime.now(timezone.utc).isoformat(), + }) + +@app.route('/v1/contracts/oracle/state') +def oracle_state(): + try: + ts_hex = eth_call(CONTRACTS['SVReserveOracle']['address'], SEL_LATEST_TIMESTAMP) + ans_hex = eth_call(CONTRACTS['SVReserveOracle']['address'], SEL_LATEST_ANSWER) + latest_ts = int(ts_hex, 16) + latest_ans = int(ans_hex, 16) + age = int(time.time()) - latest_ts + + try: + adeq_hex = eth_call(CONTRACTS['SVReserveOracle']['address'], SEL_IS_ADEQUATE) + is_adequate = int(adeq_hex, 16) == 1 + except: + is_adequate = age < 86400 + + bal_hex = alchemy_call('eth_getBalance', [ORACLE_WALLET, 'latest']) + wallet_balance = f"{int(bal_hex,16)/1e18:.6f}" + + payload = { + 'latestAnswer': str(latest_ans), + 'latestAnswerScaled': f"{latest_ans/1e8:,.2f}", + 'latestTimestamp': latest_ts, + 'latestTimestampISO': datetime.fromtimestamp( + latest_ts, tz=timezone.utc).isoformat(), + 'isAdequate': is_adequate, + 'ageSeconds': age, + 'stalenessThresholdSeconds': 86400, + 'transmitter': ORACLE_WALLET, + 'walletBalance': wallet_balance, + 'contractAddress': CONTRACTS['SVReserveOracle']['address'], + 'etherscan': CONTRACTS['SVReserveOracle']['etherscan'], + } + payload['provenance'] = sign_response(payload) + return jsonify(payload) + except Exception as e: + return jsonify({'code':'ERROR','message':str(e)}), 500 + +@app.route('/v1/contracts/anchor/records/') +def get_anchor_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':f'No anchor record for KoreID: {kore_id}'}), 404 + + koreid, event_type, status, tx_hash, amount, currency, posted_at = row + + # Get on-chain confirmation if tx_hash exists + confirmations = None + finalized = False + if tx_hash and tx_hash.startswith('0x'): + try: + receipt = alchemy_call('eth_getTransactionReceipt', [tx_hash]) + if receipt: + block_num = int(receipt['blockNumber'], 16) + current = current_block() + confirmations = current - block_num + finalized = confirmations >= FINALITY_BLOCKS + except: + pass + + payload = { + 'koreId': koreid, + 'eventType': event_type, + 'status': status, + 'txHash': tx_hash, + 'amount': str(amount), + 'currency': currency, + 'anchoredAt': posted_at.isoformat() if posted_at else None, + 'confirmations': confirmations, + 'finalized': finalized, + 'network': 'ETHEREUM_MAINNET', + 'contractAddress': CONTRACTS['KoreAnchor']['address'], + '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/contracts/anchor/records/uetr/') +def get_anchor_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 koreid LIKE %s OR guid::text = %s + LIMIT 1 + """, (f'%{uetr[-8:]}%', uetr)) + row = cur.fetchone() + pg.close() + + if not row: + return jsonify({'code':'NOT_FOUND', + 'message':f'No anchor record for UETR: {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, + 'anchoredAt': 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/contracts/tokens') +def get_token_states(): + try: + ts_hex = eth_call(CONTRACTS['SVReserveOracle']['address'], SEL_LATEST_TIMESTAMP) + latest_ts = int(ts_hex, 16) + age = int(time.time()) - latest_ts + try: + adeq_hex = eth_call(CONTRACTS['SVReserveOracle']['address'], SEL_IS_ADEQUATE) + is_adequate = int(adeq_hex, 16) == 1 + except: + is_adequate = age < 86400 + + tokens = [] + for symbol, address in SV_TOKENS.items(): + token = { + 'symbol': symbol, + 'address': address, + 'mintingEnabled': is_adequate, + 'oracleGate': CONTRACTS['SVReserveOracle']['address'], + } + if address: + try: + supply_hex = eth_call(address, SEL_TOTAL_SUPPLY) + token['totalSupply'] = str(int(supply_hex, 16)) + token['etherscan'] = f'https://etherscan.io/token/{address}' + except: + pass + tokens.append(token) + + return jsonify({ + 'mintingGatedOn': CONTRACTS['SVReserveOracle']['address'], + 'isAdequate': is_adequate, + 'tokens': tokens, + 'timestamp': datetime.now(timezone.utc).isoformat(), + }) + except Exception as e: + return jsonify({'code':'ERROR','message':str(e)}), 500 + +@app.route('/v1/transactions/') +def get_transaction(tx_hash): + try: + receipt = alchemy_call('eth_getTransactionReceipt', [tx_hash]) + if not receipt: + return jsonify({'code':'NOT_FOUND','message':'Transaction not found or pending'}), 404 + + block_num = int(receipt['blockNumber'], 16) + current = current_block() + confirmations = current - block_num + status = 'FINALIZED' if confirmations >= FINALITY_BLOCKS else 'CONFIRMED' + if int(receipt['status'], 16) == 0: + status = 'FAILED' + + payload = { + 'txHash': tx_hash, + 'status': status, + 'blockNumber': block_num, + 'confirmations': confirmations, + 'finalized': confirmations >= FINALITY_BLOCKS, + 'gasUsed': int(receipt['gasUsed'], 16), + 'from': receipt.get('from'), + 'to': receipt.get('to'), + 'network': 'ETHEREUM_MAINNET', + 'etherscan': f'https://etherscan.io/tx/{tx_hash}', + 'timestamp': datetime.now(timezone.utc).isoformat(), + } + return jsonify(payload) + except Exception as e: + return jsonify({'code':'ERROR','message':str(e)}), 500 + +@app.route('/v1/verify/anchor', methods=['POST']) +def verify_anchor(): + data = request.get_json() + if not data or not data.get('koreId'): + return jsonify({'code':'INVALID','message':'koreId required'}), 400 + try: + pg = get_pg() + with pg.cursor() as cur: + cur.execute(""" + SELECT koreid, tx_hash, amount, currency, event_type, posted_at + FROM kore_outbound_events WHERE koreid = %s + """, (data['koreId'],)) + row = cur.fetchone() + pg.close() + + if not row: + return jsonify({'verified': False, + 'message': 'No anchor record found for KoreID'}), 404 + + koreid, tx_hash, amount, currency, event_type, posted_at = row + payload_hash = hashlib.sha256( + json.dumps({'koreId': koreid, 'amount': str(amount), + 'currency': currency}, separators=(',',':')).encode() + ).hexdigest() + + return jsonify({ + 'verified': True, + 'koreId': koreid, + 'txHash': tx_hash, + 'onChainPayloadHash': payload_hash, + 'blockNumber': None, + 'etherscan': f'https://etherscan.io/tx/{tx_hash}' if tx_hash else None, + 'verifiedAt': datetime.now(timezone.utc).isoformat(), + }) + except Exception as e: + return jsonify({'code':'ERROR','message':str(e)}), 500 + +if __name__ == '__main__': + log.info("Ethereum API starting on port 8769") + app.run(host='0.0.0.0', port=8769)