v3.0: JWT auth, korechain.korenet.cloud routing, LICK module, feed poller v3
This commit is contained in:
@@ -1,52 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Vector Zulu — KoreChain Poster
|
||||
Reads KoreID payloads from Redis queue and posts them to KoreChain via KoreOracle API.
|
||||
Preserves full KoreID traceability per spec — never strips hash, signature, or chain reference.
|
||||
Vector Zulu — KoreChain Poster v3.0
|
||||
Reads KoreID payloads from Redis queue and posts them to KoreChain.
|
||||
|
||||
Key changes from v2:
|
||||
- Primary endpoint: korechain.korenet.cloud (not api.korenet.cloud)
|
||||
- Auth: JWT Bearer only — no client cert required (ssl_verify_client optional)
|
||||
- LICK signing on all mutating calls
|
||||
- Uses /api/v1/korechain/anchor for Ethereum anchor confirmations
|
||||
- Uses /api/korechain/v1/broadcast for broadcast events
|
||||
- Retry with dead-letter after MAX_RETRIES
|
||||
"""
|
||||
|
||||
import os, json, time, logging, requests
|
||||
import os, json, time, logging, requests, redis, psycopg2, urllib3
|
||||
from datetime import datetime, timezone
|
||||
import redis
|
||||
import psycopg2
|
||||
import urllib3
|
||||
urllib3.disable_warnings()
|
||||
from lick import build_headers
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s [KORE-POSTER] %(levelname)s %(message)s'
|
||||
)
|
||||
urllib3.disable_warnings()
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s [KORE-POSTER] %(levelname)s %(message)s')
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# ── Config ────────────────────────────────────────────────────────────────────
|
||||
|
||||
CERT = os.environ.get('CERT_PATH', '/certs/vector-zulu-client.fullchain.pem')
|
||||
KEY = os.environ.get('KEY_PATH', '/certs/vector-zulu-client.key')
|
||||
API_BASE = 'https://api.korenet.cloud'
|
||||
API_PRIMARY = os.environ.get('KORENET_API_URL', 'https://korechain.korenet.cloud')
|
||||
API_FALLBACK = os.environ.get('KORENET_API_FALLBACK', 'https://fnb.korenet.cloud')
|
||||
QUEUE_KEY = 'korechain:outbound_queue'
|
||||
DEAD_LETTER = 'korechain:dead_letter'
|
||||
MAX_RETRIES = int(os.environ.get('MAX_RETRIES', '3'))
|
||||
RETRY_DELAY = int(os.environ.get('RETRY_DELAY', '30'))
|
||||
POLL_INTERVAL = int(os.environ.get('POLL_INTERVAL', '5'))
|
||||
TENANT_ID = 'vector-zulu'
|
||||
|
||||
|
||||
def load_jwt():
|
||||
secrets_path = '/app/.secrets'
|
||||
if os.path.exists(secrets_path):
|
||||
with open(secrets_path) as f:
|
||||
for line in f:
|
||||
if 'VECTOR_ZULU_JWT=' in line:
|
||||
val = line.split('VECTOR_ZULU_JWT=', 1)[1].strip().strip('"').strip("'")
|
||||
if val:
|
||||
return val
|
||||
return None
|
||||
def load_jwt() -> str:
|
||||
jwt = os.environ.get('VECTOR_ZULU_JWT', '')
|
||||
if jwt:
|
||||
return jwt
|
||||
for path in ['/app/.secrets', '/home/constellationnode/.korenet/vector-zulu/.secrets',
|
||||
os.path.expanduser('~/.korenet/vector-zulu/.secrets')]:
|
||||
if os.path.exists(path):
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
if 'VECTOR_ZULU_JWT=' in line:
|
||||
val = line.split('VECTOR_ZULU_JWT=', 1)[1].strip().strip('"').strip("'")
|
||||
if val:
|
||||
return val
|
||||
return ''
|
||||
|
||||
|
||||
def get_redis():
|
||||
return redis.Redis(
|
||||
host=os.environ.get('REDIS_HOST', 'localhost'),
|
||||
port=6379, decode_responses=True
|
||||
)
|
||||
return redis.Redis(host=os.environ.get('REDIS_HOST', 'localhost'), port=6379, decode_responses=True)
|
||||
|
||||
|
||||
def get_pg():
|
||||
@@ -59,146 +60,124 @@ def get_pg():
|
||||
)
|
||||
|
||||
|
||||
def post_to_korechain(payload: dict, jwt: str) -> bool:
|
||||
"""
|
||||
Post a KoreID payload to KoreChain via KoreOracle API.
|
||||
Returns True on success, False on failure.
|
||||
"""
|
||||
url = f'{API_BASE}/api/external/event'
|
||||
|
||||
headers = {
|
||||
'Authorization': f'Bearer {jwt}',
|
||||
'X-Tenant-Id': 'vector-zulu',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
try:
|
||||
resp = requests.post(
|
||||
url,
|
||||
cert=(CERT, KEY),
|
||||
verify=False,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if resp.status_code in (200, 201, 202):
|
||||
log.info(f"Posted KoreID {payload['koreid']} → HTTP {resp.status_code}")
|
||||
return True
|
||||
else:
|
||||
log.warning(f"KoreChain rejected {payload['koreid']}: HTTP {resp.status_code} — {resp.text[:200]}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Post error for {payload.get('koreid','?')}: {e}")
|
||||
return False
|
||||
def _session(jwt: str) -> requests.Session:
|
||||
s = requests.Session()
|
||||
s.verify = False
|
||||
s.headers.update({'Authorization': f'Bearer {jwt}', 'X-Tenant-Id': TENANT_ID, 'Content-Type': 'application/json'})
|
||||
return s
|
||||
|
||||
|
||||
def store_posted(payload: dict, pg, status: str, response_koreid: str = None):
|
||||
"""Record posted event in Postgres for audit trail."""
|
||||
def _post_to_korechain(payload: dict, jwt: str, path: str) -> tuple:
|
||||
body = {**payload, 'tenantId': TENANT_ID, 'source': 'vector-zulu',
|
||||
'timestamp': datetime.now(timezone.utc).isoformat()}
|
||||
lick = build_headers(body)
|
||||
session = _session(jwt)
|
||||
session.headers.update(lick)
|
||||
for base_url in [API_PRIMARY, API_FALLBACK]:
|
||||
url = f'{base_url}{path}'
|
||||
try:
|
||||
resp = session.post(url, json=body, timeout=30)
|
||||
if resp.status_code in (200, 201, 202):
|
||||
data = resp.json() if resp.content else {}
|
||||
response_koreid = data.get('koreId') or data.get('koreid') or ''
|
||||
log.info(f"✓ Posted {payload.get('koreid')} via {base_url} → {resp.status_code}")
|
||||
return True, response_koreid
|
||||
else:
|
||||
log.warning(f"Post {payload.get('koreid')} via {base_url}: HTTP {resp.status_code}")
|
||||
except Exception as e:
|
||||
log.error(f"Post error via {base_url}: {e}")
|
||||
return False, ''
|
||||
|
||||
|
||||
def post_to_korechain(payload: dict, jwt: str) -> tuple:
|
||||
step = payload.get('step', 'ANCHOR')
|
||||
if step in ('MINT', 'SETTLE', 'REDEEM', 'BURN', 'CLOSE'):
|
||||
path = '/api/korechain/v1/broadcast'
|
||||
else:
|
||||
path = '/api/v1/korechain/anchor'
|
||||
return _post_to_korechain(payload, jwt, path)
|
||||
|
||||
|
||||
def store_posted(pg, payload: dict, status: str, response_koreid: str = ''):
|
||||
try:
|
||||
with pg.cursor() as cur:
|
||||
cur.execute("""
|
||||
INSERT INTO kore_outbound_events
|
||||
(koreid, guid, event_type, currency, token_symbol,
|
||||
tx_hash, amount, status, response_koreid, posted_at)
|
||||
(koreid, guid, event_type, currency, token_symbol, tx_hash, amount, status, response_koreid, posted_at)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
ON CONFLICT (koreid) DO UPDATE SET
|
||||
status = EXCLUDED.status,
|
||||
response_koreid = EXCLUDED.response_koreid,
|
||||
posted_at = EXCLUDED.posted_at
|
||||
status = EXCLUDED.status, response_koreid = EXCLUDED.response_koreid, posted_at = EXCLUDED.posted_at
|
||||
""", (
|
||||
payload['koreid'],
|
||||
payload['guid'],
|
||||
payload['source']['event_type'],
|
||||
payload['source'].get('token', ''),
|
||||
payload['source'].get('token', ''),
|
||||
payload['proof']['hash'],
|
||||
payload.get('value', 0),
|
||||
status,
|
||||
response_koreid,
|
||||
datetime.now(timezone.utc),
|
||||
payload.get('koreid', ''), payload.get('guid', ''),
|
||||
payload.get('step', 'ANCHOR'), payload.get('currency', ''),
|
||||
payload.get('token_symbol', ''), payload.get('ethereum_tx', ''),
|
||||
payload.get('amount', 0), status, response_koreid, datetime.now(timezone.utc),
|
||||
))
|
||||
pg.commit()
|
||||
except Exception as e:
|
||||
log.error(f"DB store error: {e}")
|
||||
try: pg.rollback()
|
||||
except: pass
|
||||
|
||||
|
||||
class KoreChainPoster:
|
||||
|
||||
def __init__(self, redis_client, pg_conn):
|
||||
self.r = redis_client
|
||||
self.r = redis_client
|
||||
self.pg = pg_conn
|
||||
|
||||
def process_one(self, jwt: str) -> bool:
|
||||
"""Process one item from the queue. Returns True if item was processed."""
|
||||
raw = self.r.rpop(QUEUE_KEY)
|
||||
if not raw:
|
||||
return False
|
||||
|
||||
try:
|
||||
payload = json.loads(raw)
|
||||
except json.JSONDecodeError as e:
|
||||
log.error(f"Invalid JSON in queue: {e}")
|
||||
return True
|
||||
|
||||
koreid = payload.get('koreid', 'UNKNOWN')
|
||||
retries = payload.get('_retries', 0)
|
||||
koreid = payload.get('koreid', 'UNKNOWN')
|
||||
retries = payload.get('_retries', 0)
|
||||
log.info(f"Processing {koreid} step={payload.get('step','ANCHOR')} (attempt {retries+1}/{MAX_RETRIES})")
|
||||
|
||||
log.info(f"Processing {koreid} (attempt {retries + 1}/{MAX_RETRIES})")
|
||||
|
||||
success = post_to_korechain(payload, jwt)
|
||||
success, response_koreid = post_to_korechain(payload, jwt)
|
||||
|
||||
if success:
|
||||
store_posted(payload, self.pg, 'POSTED')
|
||||
log.info(f"✓ {koreid} posted successfully")
|
||||
store_posted(self.pg, payload, 'POSTED', response_koreid)
|
||||
log.info(f"✓ {koreid} posted")
|
||||
else:
|
||||
retries += 1
|
||||
if retries < MAX_RETRIES:
|
||||
payload['_retries'] = retries
|
||||
# Re-queue with delay via sorted set
|
||||
retry_at = time.time() + RETRY_DELAY
|
||||
self.r.zadd('korechain:retry_queue', {json.dumps(payload): retry_at})
|
||||
self.r.zadd('korechain:retry_queue', {json.dumps(payload): time.time() + RETRY_DELAY})
|
||||
log.warning(f"Retry {retries}/{MAX_RETRIES} for {koreid} in {RETRY_DELAY}s")
|
||||
else:
|
||||
# Dead letter
|
||||
self.r.lpush(DEAD_LETTER, raw)
|
||||
store_posted(payload, self.pg, 'DEAD_LETTER')
|
||||
log.error(f"Dead letter: {koreid} after {MAX_RETRIES} attempts")
|
||||
|
||||
store_posted(self.pg, payload, 'DEAD_LETTER')
|
||||
log.error(f"Dead letter: {koreid}")
|
||||
return True
|
||||
|
||||
def process_retries(self, jwt: str):
|
||||
"""Move any due retries back to main queue."""
|
||||
now = time.time()
|
||||
due = self.r.zrangebyscore('korechain:retry_queue', 0, now)
|
||||
for item in due:
|
||||
for item in self.r.zrangebyscore('korechain:retry_queue', 0, now):
|
||||
self.r.lpush(QUEUE_KEY, item)
|
||||
self.r.zrem('korechain:retry_queue', item)
|
||||
log.info("Moved retry back to main queue")
|
||||
log.info("Moved retry to main queue")
|
||||
|
||||
def run(self):
|
||||
log.info("KoreChain poster starting...")
|
||||
log.info(f"Queue: {QUEUE_KEY} | Max retries: {MAX_RETRIES} | Retry delay: {RETRY_DELAY}s")
|
||||
|
||||
log.info(f"KoreChain Poster v3.0 — Primary: {API_PRIMARY} | Fallback: {API_FALLBACK}")
|
||||
while True:
|
||||
jwt = load_jwt()
|
||||
if not jwt:
|
||||
log.error("No JWT — waiting 60s")
|
||||
time.sleep(60)
|
||||
continue
|
||||
|
||||
self.process_retries(jwt)
|
||||
|
||||
processed = True
|
||||
while processed:
|
||||
processed = self.process_one(jwt)
|
||||
|
||||
while self.process_one(jwt):
|
||||
pass
|
||||
time.sleep(POLL_INTERVAL)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
r = get_redis()
|
||||
pg = get_pg()
|
||||
poster = KoreChainPoster(r, pg)
|
||||
poster = KoreChainPoster(get_redis(), get_pg())
|
||||
poster.run()
|
||||
|
||||
Reference in New Issue
Block a user