v3.0: JWT auth, korechain.korenet.cloud routing, LICK module, feed poller v3
This commit is contained in:
@@ -1,13 +1,82 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
# Vector Zulu — KoreID Translation Layer
|
# Vector Zulu — KoreID Adapter Stack v3.0
|
||||||
# Converts Ethereum events to KoreID format and posts back to KoreChain.
|
|
||||||
# Runs on Charlie (63.245.138.38) alongside charlie-vault-cache.
|
# Runs on Charlie (63.245.138.38) alongside charlie-vault-cache.
|
||||||
# Shares the same Redis and Postgres instances.
|
# Shares Redis and Postgres with vault cache stack.
|
||||||
|
# Auth: JWT Bearer only — no client cert required (ssl_verify_client optional).
|
||||||
|
# Primary API: korechain.korenet.cloud | Fallback: fnb.korenet.cloud
|
||||||
|
|
||||||
|
x-common-env: &common-env
|
||||||
|
REDIS_HOST: ${REDIS_HOST:-localhost}
|
||||||
|
POSTGRES_HOST: ${POSTGRES_HOST:-localhost}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-vz_oracle}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-vz_oracle}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-}
|
||||||
|
KORENET_API_URL: ${KORENET_API_URL:-https://korechain.korenet.cloud}
|
||||||
|
KORENET_API_FALLBACK: ${KORENET_API_FALLBACK:-https://fnb.korenet.cloud}
|
||||||
|
VECTOR_ZULU_RAILS_URL: ${VECTOR_ZULU_RAILS_URL:-https://vector.korenet.cloud}
|
||||||
|
|
||||||
|
x-common-volumes: &common-volumes
|
||||||
|
- /home/constellationnode/.korenet/vector-zulu/.secrets:/app/.secrets:ro
|
||||||
|
- /home/constellationnode/.korenet/vector-zulu/environment.env:/app/environment.env:ro
|
||||||
|
|
||||||
|
x-common-logging: &common-logging
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "100m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
eth-listener:
|
# ── Portal Feed Poller ─────────────────────────────────────────────────────
|
||||||
|
# Polls KoreNet payment feeds and executes mirror lifecycle
|
||||||
|
vz-feed-poller:
|
||||||
|
image: python:3.12-slim
|
||||||
|
container_name: vz-feed-poller
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./portal_feed_poller.py:/app/portal_feed_poller.py
|
||||||
|
- ./koreid.py:/app/koreid.py
|
||||||
|
- ./lick.py:/app/lick.py
|
||||||
|
- ./eventbus.py:/app/eventbus.py
|
||||||
|
<<: *common-volumes
|
||||||
|
working_dir: /app
|
||||||
|
command: >
|
||||||
|
sh -c "pip install requests redis psycopg2-binary azure-servicebus -q &&
|
||||||
|
python portal_feed_poller.py"
|
||||||
|
environment:
|
||||||
|
<<: *common-env
|
||||||
|
POLL_INTERVAL: "${POLL_INTERVAL:-30}"
|
||||||
|
AZURE_SERVICEBUS_CONNECTION_STRING: "${AZURE_SERVICEBUS_CONNECTION_STRING:-}"
|
||||||
|
logging: *common-logging
|
||||||
|
network_mode: host
|
||||||
|
|
||||||
|
# ── KoreChain Poster ──────────────────────────────────────────────────────
|
||||||
|
# Reads Redis queue and posts KoreID confirmations to KoreChain
|
||||||
|
vz-korechain-poster:
|
||||||
|
image: python:3.12-slim
|
||||||
|
container_name: vz-korechain-poster
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./korechain_poster.py:/app/korechain_poster.py
|
||||||
|
- ./koreid.py:/app/koreid.py
|
||||||
|
- ./lick.py:/app/lick.py
|
||||||
|
<<: *common-volumes
|
||||||
|
working_dir: /app
|
||||||
|
command: >
|
||||||
|
sh -c "pip install requests redis psycopg2-binary -q &&
|
||||||
|
python korechain_poster.py"
|
||||||
|
environment:
|
||||||
|
<<: *common-env
|
||||||
|
MAX_RETRIES: "${MAX_RETRIES:-3}"
|
||||||
|
RETRY_DELAY: "${RETRY_DELAY:-30}"
|
||||||
|
POLL_INTERVAL: "${KORECHAIN_POLL_INTERVAL:-5}"
|
||||||
|
logging: *common-logging
|
||||||
|
network_mode: host
|
||||||
|
|
||||||
|
# ── Ethereum Event Listener ───────────────────────────────────────────────
|
||||||
|
# Watches SV token contracts for Mint/Burn/Transfer events
|
||||||
|
vz-eth-listener:
|
||||||
image: python:3.12-slim
|
image: python:3.12-slim
|
||||||
container_name: vz-eth-listener
|
container_name: vz-eth-listener
|
||||||
restart: always
|
restart: always
|
||||||
@@ -16,43 +85,18 @@ services:
|
|||||||
- ./koreid.py:/app/koreid.py
|
- ./koreid.py:/app/koreid.py
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
command: >
|
command: >
|
||||||
sh -c "pip install web3 redis psycopg2-binary -q && python eth_listener.py"
|
sh -c "pip install web3 requests redis psycopg2-binary -q &&
|
||||||
|
python eth_listener.py"
|
||||||
environment:
|
environment:
|
||||||
ETH_RPC: ${ETH_RPC:-https://eth-mainnet.g.alchemy.com/v2/nRVT_IneMUDF3Pokubawx}
|
<<: *common-env
|
||||||
REDIS_HOST: ${REDIS_HOST:-vz-redis}
|
ETH_RPC: "${ETH_RPC:-https://eth-mainnet.g.alchemy.com/v2/nRVT_IneMUDF3Pokubawx}"
|
||||||
POSTGRES_HOST: ${POSTGRES_HOST:-vz-postgres}
|
POLL_INTERVAL: "${ETH_POLL_INTERVAL:-60}"
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-vz_oracle}
|
logging: *common-logging
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-vz_oracle}
|
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-}
|
|
||||||
POLL_INTERVAL: "60"
|
|
||||||
network_mode: host
|
network_mode: host
|
||||||
|
|
||||||
korechain-poster:
|
# ── Adapter API ───────────────────────────────────────────────────────────
|
||||||
image: python:3.12-slim
|
# Status, queue monitoring, manual injection — port 8766
|
||||||
container_name: vz-korechain-poster
|
vz-adapter-api:
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- ./korechain_poster.py:/app/korechain_poster.py
|
|
||||||
- ./koreid.py:/app/koreid.py
|
|
||||||
- /home/constellationnode/.korenet/vector-zulu/certificates:/certs:ro
|
|
||||||
- /home/constellationnode/.korenet/vector-zulu/.secrets:/app/.secrets:ro
|
|
||||||
working_dir: /app
|
|
||||||
command: >
|
|
||||||
sh -c "pip install requests redis psycopg2-binary -q && python korechain_poster.py"
|
|
||||||
environment:
|
|
||||||
REDIS_HOST: ${REDIS_HOST:-vz-redis}
|
|
||||||
POSTGRES_HOST: ${POSTGRES_HOST:-vz-postgres}
|
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-vz_oracle}
|
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-vz_oracle}
|
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-}
|
|
||||||
CERT_PATH: /certs/vector-zulu-client.fullchain.pem
|
|
||||||
KEY_PATH: /certs/vector-zulu-client.key
|
|
||||||
MAX_RETRIES: "3"
|
|
||||||
RETRY_DELAY: "30"
|
|
||||||
POLL_INTERVAL: "5"
|
|
||||||
network_mode: host
|
|
||||||
|
|
||||||
adapter-api:
|
|
||||||
image: python:3.12-slim
|
image: python:3.12-slim
|
||||||
container_name: vz-adapter-api
|
container_name: vz-adapter-api
|
||||||
restart: always
|
restart: always
|
||||||
@@ -61,13 +105,13 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./adapter_api.py:/app/adapter_api.py
|
- ./adapter_api.py:/app/adapter_api.py
|
||||||
- ./koreid.py:/app/koreid.py
|
- ./koreid.py:/app/koreid.py
|
||||||
|
- ./lick.py:/app/lick.py
|
||||||
|
<<: *common-volumes
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
command: >
|
command: >
|
||||||
sh -c "pip install flask redis psycopg2-binary -q && python adapter_api.py"
|
sh -c "pip install flask requests redis psycopg2-binary -q &&
|
||||||
|
python adapter_api.py"
|
||||||
environment:
|
environment:
|
||||||
REDIS_HOST: ${REDIS_HOST:-vz-redis}
|
<<: *common-env
|
||||||
POSTGRES_HOST: ${POSTGRES_HOST:-vz-postgres}
|
logging: *common-logging
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-vz_oracle}
|
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-vz_oracle}
|
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-}
|
|
||||||
network_mode: host
|
network_mode: host
|
||||||
|
|||||||
@@ -1,52 +1,53 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Vector Zulu — KoreChain Poster
|
Vector Zulu — KoreChain Poster v3.0
|
||||||
Reads KoreID payloads from Redis queue and posts them to KoreChain via KoreOracle API.
|
Reads KoreID payloads from Redis queue and posts them to KoreChain.
|
||||||
Preserves full KoreID traceability per spec — never strips hash, signature, or chain reference.
|
|
||||||
|
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
|
from datetime import datetime, timezone
|
||||||
import redis
|
from lick import build_headers
|
||||||
import psycopg2
|
|
||||||
import urllib3
|
|
||||||
urllib3.disable_warnings()
|
|
||||||
|
|
||||||
logging.basicConfig(
|
urllib3.disable_warnings()
|
||||||
level=logging.INFO,
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s [KORE-POSTER] %(levelname)s %(message)s')
|
||||||
format='%(asctime)s [KORE-POSTER] %(levelname)s %(message)s'
|
|
||||||
)
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
# ── Config ────────────────────────────────────────────────────────────────────
|
API_PRIMARY = os.environ.get('KORENET_API_URL', 'https://korechain.korenet.cloud')
|
||||||
|
API_FALLBACK = os.environ.get('KORENET_API_FALLBACK', 'https://fnb.korenet.cloud')
|
||||||
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'
|
|
||||||
QUEUE_KEY = 'korechain:outbound_queue'
|
QUEUE_KEY = 'korechain:outbound_queue'
|
||||||
DEAD_LETTER = 'korechain:dead_letter'
|
DEAD_LETTER = 'korechain:dead_letter'
|
||||||
MAX_RETRIES = int(os.environ.get('MAX_RETRIES', '3'))
|
MAX_RETRIES = int(os.environ.get('MAX_RETRIES', '3'))
|
||||||
RETRY_DELAY = int(os.environ.get('RETRY_DELAY', '30'))
|
RETRY_DELAY = int(os.environ.get('RETRY_DELAY', '30'))
|
||||||
POLL_INTERVAL = int(os.environ.get('POLL_INTERVAL', '5'))
|
POLL_INTERVAL = int(os.environ.get('POLL_INTERVAL', '5'))
|
||||||
|
TENANT_ID = 'vector-zulu'
|
||||||
|
|
||||||
|
|
||||||
def load_jwt():
|
def load_jwt() -> str:
|
||||||
secrets_path = '/app/.secrets'
|
jwt = os.environ.get('VECTOR_ZULU_JWT', '')
|
||||||
if os.path.exists(secrets_path):
|
if jwt:
|
||||||
with open(secrets_path) as f:
|
return jwt
|
||||||
for line in f:
|
for path in ['/app/.secrets', '/home/constellationnode/.korenet/vector-zulu/.secrets',
|
||||||
if 'VECTOR_ZULU_JWT=' in line:
|
os.path.expanduser('~/.korenet/vector-zulu/.secrets')]:
|
||||||
val = line.split('VECTOR_ZULU_JWT=', 1)[1].strip().strip('"').strip("'")
|
if os.path.exists(path):
|
||||||
if val:
|
with open(path) as f:
|
||||||
return val
|
for line in f:
|
||||||
return None
|
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():
|
def get_redis():
|
||||||
return redis.Redis(
|
return redis.Redis(host=os.environ.get('REDIS_HOST', 'localhost'), port=6379, decode_responses=True)
|
||||||
host=os.environ.get('REDIS_HOST', 'localhost'),
|
|
||||||
port=6379, decode_responses=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_pg():
|
def get_pg():
|
||||||
@@ -59,146 +60,124 @@ def get_pg():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def post_to_korechain(payload: dict, jwt: str) -> bool:
|
def _session(jwt: str) -> requests.Session:
|
||||||
"""
|
s = requests.Session()
|
||||||
Post a KoreID payload to KoreChain via KoreOracle API.
|
s.verify = False
|
||||||
Returns True on success, False on failure.
|
s.headers.update({'Authorization': f'Bearer {jwt}', 'X-Tenant-Id': TENANT_ID, 'Content-Type': 'application/json'})
|
||||||
"""
|
return s
|
||||||
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 store_posted(payload: dict, pg, status: str, response_koreid: str = None):
|
def _post_to_korechain(payload: dict, jwt: str, path: str) -> tuple:
|
||||||
"""Record posted event in Postgres for audit trail."""
|
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:
|
try:
|
||||||
with pg.cursor() as cur:
|
with pg.cursor() as cur:
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO kore_outbound_events
|
INSERT INTO kore_outbound_events
|
||||||
(koreid, guid, event_type, currency, token_symbol,
|
(koreid, guid, event_type, currency, token_symbol, tx_hash, amount, status, response_koreid, posted_at)
|
||||||
tx_hash, amount, status, response_koreid, posted_at)
|
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
ON CONFLICT (koreid) DO UPDATE SET
|
ON CONFLICT (koreid) DO UPDATE SET
|
||||||
status = EXCLUDED.status,
|
status = EXCLUDED.status, response_koreid = EXCLUDED.response_koreid, posted_at = EXCLUDED.posted_at
|
||||||
response_koreid = EXCLUDED.response_koreid,
|
|
||||||
posted_at = EXCLUDED.posted_at
|
|
||||||
""", (
|
""", (
|
||||||
payload['koreid'],
|
payload.get('koreid', ''), payload.get('guid', ''),
|
||||||
payload['guid'],
|
payload.get('step', 'ANCHOR'), payload.get('currency', ''),
|
||||||
payload['source']['event_type'],
|
payload.get('token_symbol', ''), payload.get('ethereum_tx', ''),
|
||||||
payload['source'].get('token', ''),
|
payload.get('amount', 0), status, response_koreid, datetime.now(timezone.utc),
|
||||||
payload['source'].get('token', ''),
|
|
||||||
payload['proof']['hash'],
|
|
||||||
payload.get('value', 0),
|
|
||||||
status,
|
|
||||||
response_koreid,
|
|
||||||
datetime.now(timezone.utc),
|
|
||||||
))
|
))
|
||||||
pg.commit()
|
pg.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"DB store error: {e}")
|
log.error(f"DB store error: {e}")
|
||||||
|
try: pg.rollback()
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
|
||||||
class KoreChainPoster:
|
class KoreChainPoster:
|
||||||
|
|
||||||
def __init__(self, redis_client, pg_conn):
|
def __init__(self, redis_client, pg_conn):
|
||||||
self.r = redis_client
|
self.r = redis_client
|
||||||
self.pg = pg_conn
|
self.pg = pg_conn
|
||||||
|
|
||||||
def process_one(self, jwt: str) -> bool:
|
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)
|
raw = self.r.rpop(QUEUE_KEY)
|
||||||
if not raw:
|
if not raw:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = json.loads(raw)
|
payload = json.loads(raw)
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
log.error(f"Invalid JSON in queue: {e}")
|
log.error(f"Invalid JSON in queue: {e}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
koreid = payload.get('koreid', 'UNKNOWN')
|
koreid = payload.get('koreid', 'UNKNOWN')
|
||||||
retries = payload.get('_retries', 0)
|
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, response_koreid = post_to_korechain(payload, jwt)
|
||||||
|
|
||||||
success = post_to_korechain(payload, jwt)
|
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
store_posted(payload, self.pg, 'POSTED')
|
store_posted(self.pg, payload, 'POSTED', response_koreid)
|
||||||
log.info(f"✓ {koreid} posted successfully")
|
log.info(f"✓ {koreid} posted")
|
||||||
else:
|
else:
|
||||||
retries += 1
|
retries += 1
|
||||||
if retries < MAX_RETRIES:
|
if retries < MAX_RETRIES:
|
||||||
payload['_retries'] = retries
|
payload['_retries'] = retries
|
||||||
# Re-queue with delay via sorted set
|
self.r.zadd('korechain:retry_queue', {json.dumps(payload): time.time() + RETRY_DELAY})
|
||||||
retry_at = time.time() + RETRY_DELAY
|
|
||||||
self.r.zadd('korechain:retry_queue', {json.dumps(payload): retry_at})
|
|
||||||
log.warning(f"Retry {retries}/{MAX_RETRIES} for {koreid} in {RETRY_DELAY}s")
|
log.warning(f"Retry {retries}/{MAX_RETRIES} for {koreid} in {RETRY_DELAY}s")
|
||||||
else:
|
else:
|
||||||
# Dead letter
|
|
||||||
self.r.lpush(DEAD_LETTER, raw)
|
self.r.lpush(DEAD_LETTER, raw)
|
||||||
store_posted(payload, self.pg, 'DEAD_LETTER')
|
store_posted(self.pg, payload, 'DEAD_LETTER')
|
||||||
log.error(f"Dead letter: {koreid} after {MAX_RETRIES} attempts")
|
log.error(f"Dead letter: {koreid}")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def process_retries(self, jwt: str):
|
def process_retries(self, jwt: str):
|
||||||
"""Move any due retries back to main queue."""
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
due = self.r.zrangebyscore('korechain:retry_queue', 0, now)
|
for item in self.r.zrangebyscore('korechain:retry_queue', 0, now):
|
||||||
for item in due:
|
|
||||||
self.r.lpush(QUEUE_KEY, item)
|
self.r.lpush(QUEUE_KEY, item)
|
||||||
self.r.zrem('korechain:retry_queue', 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):
|
def run(self):
|
||||||
log.info("KoreChain poster starting...")
|
log.info(f"KoreChain Poster v3.0 — Primary: {API_PRIMARY} | Fallback: {API_FALLBACK}")
|
||||||
log.info(f"Queue: {QUEUE_KEY} | Max retries: {MAX_RETRIES} | Retry delay: {RETRY_DELAY}s")
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
jwt = load_jwt()
|
jwt = load_jwt()
|
||||||
if not jwt:
|
if not jwt:
|
||||||
log.error("No JWT — waiting 60s")
|
log.error("No JWT — waiting 60s")
|
||||||
time.sleep(60)
|
time.sleep(60)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.process_retries(jwt)
|
self.process_retries(jwt)
|
||||||
|
while self.process_one(jwt):
|
||||||
processed = True
|
pass
|
||||||
while processed:
|
|
||||||
processed = self.process_one(jwt)
|
|
||||||
|
|
||||||
time.sleep(POLL_INTERVAL)
|
time.sleep(POLL_INTERVAL)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
r = get_redis()
|
poster = KoreChainPoster(get_redis(), get_pg())
|
||||||
pg = get_pg()
|
|
||||||
poster = KoreChainPoster(r, pg)
|
|
||||||
poster.run()
|
poster.run()
|
||||||
|
|||||||
137
lick.py
Normal file
137
lick.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Vector Zulu — LICK Signing Module
|
||||||
|
LICK (Layered Integrity Cryptographic Keying) — HMAC-SHA256 signing
|
||||||
|
for all mutating KoreNet API calls.
|
||||||
|
|
||||||
|
Canonical format (primary):
|
||||||
|
signatureInput = canonicalJSON(payload) + "\n" + unixTimestamp
|
||||||
|
signature = hex(HMAC-SHA256(base64Decode(lickKey), UTF8(signatureInput)))
|
||||||
|
|
||||||
|
Universal format (legacy, replay protection):
|
||||||
|
signatureInput = canonicalJSON(payload) + "|" + timestamp + "|" + nonce + "|" + signerId
|
||||||
|
signature = base64(HMAC-SHA256(base64Decode(lickKey), UTF8(signatureInput)))
|
||||||
|
|
||||||
|
Per welcome pack section 5: json.dumps with separators=(',',':') and sort_keys=True.
|
||||||
|
Key is base64-encoded in Key Vault; decode to raw bytes before computing HMAC.
|
||||||
|
Timestamp freshness: server rejects if |now - x-lick-ts| > 300s (5 minutes).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
SIGNER_ID = 'vector-zulu'
|
||||||
|
|
||||||
|
|
||||||
|
def _get_lick_key() -> bytes:
|
||||||
|
"""
|
||||||
|
Load LICK HMAC key from environment.
|
||||||
|
Key is stored as raw string (NotMyCircu$_NotMyMonkey$) — NOT base64 encoded.
|
||||||
|
Per secrets file: VECTOR_ZULU_LICK_KEY='NotMyCircu$_NotMyMonkey$'
|
||||||
|
"""
|
||||||
|
key_raw = (
|
||||||
|
os.environ.get('VECTOR_ZULU_LICK_KEY') or
|
||||||
|
os.environ.get('KORE_LICK_HMAC_KEY') or
|
||||||
|
os.environ.get('LICK_HMAC_SECRET', '')
|
||||||
|
)
|
||||||
|
if not key_raw:
|
||||||
|
raise ValueError('LICK key not configured — set VECTOR_ZULU_LICK_KEY')
|
||||||
|
|
||||||
|
# Try base64 decode first (for keys that are base64-encoded in Key Vault)
|
||||||
|
# Fall back to raw bytes if not valid base64
|
||||||
|
try:
|
||||||
|
decoded = base64.b64decode(key_raw, validate=True)
|
||||||
|
return decoded
|
||||||
|
except Exception:
|
||||||
|
# Raw string key — encode to bytes directly
|
||||||
|
return key_raw.encode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def canonicalize(payload: dict) -> str:
|
||||||
|
"""
|
||||||
|
Canonical JSON serialization per LICK spec.
|
||||||
|
compact JSON, no whitespace, separators=(',',':'), sort_keys=True
|
||||||
|
"""
|
||||||
|
return json.dumps(payload, separators=(',', ':'), sort_keys=True)
|
||||||
|
|
||||||
|
|
||||||
|
def sign_canonical(payload: dict, timestamp: Optional[int] = None) -> str:
|
||||||
|
"""
|
||||||
|
Canonical LICK signature (primary format).
|
||||||
|
Returns lowercase hex string.
|
||||||
|
"""
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = int(time.time())
|
||||||
|
key = _get_lick_key()
|
||||||
|
canonical = canonicalize(payload)
|
||||||
|
msg = f"{canonical}\n{timestamp}"
|
||||||
|
return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def sign_universal(payload: dict, timestamp: Optional[int] = None,
|
||||||
|
nonce: Optional[str] = None,
|
||||||
|
signer_id: str = SIGNER_ID) -> str:
|
||||||
|
"""
|
||||||
|
Universal LICK signature (legacy format with replay protection).
|
||||||
|
Returns base64-encoded string.
|
||||||
|
"""
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = int(time.time())
|
||||||
|
if nonce is None:
|
||||||
|
nonce = str(uuid.uuid4())
|
||||||
|
key = _get_lick_key()
|
||||||
|
canonical = canonicalize(payload)
|
||||||
|
msg = f"{canonical}|{timestamp}|{nonce}|{signer_id}"
|
||||||
|
raw = hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()
|
||||||
|
return base64.b64encode(raw).decode('ascii')
|
||||||
|
|
||||||
|
|
||||||
|
def build_headers(payload: dict, variant: str = 'canonical') -> dict:
|
||||||
|
"""
|
||||||
|
Build the full set of LICK HTTP headers for a request.
|
||||||
|
Returns dict of headers to merge into request.
|
||||||
|
"""
|
||||||
|
ts = int(time.time())
|
||||||
|
nonce = str(uuid.uuid4())
|
||||||
|
sentinel_trace = str(uuid.uuid4())
|
||||||
|
idempotency = str(uuid.uuid4())
|
||||||
|
|
||||||
|
if variant == 'universal':
|
||||||
|
sig = sign_universal(payload, ts, nonce, SIGNER_ID)
|
||||||
|
else:
|
||||||
|
sig = sign_canonical(payload, ts)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'x-lick-ts': str(ts),
|
||||||
|
'x-lick-sig': sig,
|
||||||
|
'x-lick-nonce': nonce,
|
||||||
|
'x-lick-signer': SIGNER_ID,
|
||||||
|
'x-lick-variant': variant,
|
||||||
|
'x-sentinel-trace': sentinel_trace,
|
||||||
|
'Idempotency-Key': idempotency,
|
||||||
|
'X-Tenant-Id': 'vector-zulu',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import os
|
||||||
|
os.environ['VECTOR_ZULU_LICK_KEY'] = "NotMyCircu$_NotMyMonkey$"
|
||||||
|
|
||||||
|
payload = {'test': 'payload', 'amount': 1000000}
|
||||||
|
print("=== Canonical ===")
|
||||||
|
headers = build_headers(payload, 'canonical')
|
||||||
|
for k, v in headers.items():
|
||||||
|
print(f" {k}: {v[:40]}...")
|
||||||
|
|
||||||
|
print("\n=== Universal ===")
|
||||||
|
headers = build_headers(payload, 'universal')
|
||||||
|
for k, v in headers.items():
|
||||||
|
print(f" {k}: {v[:40]}...")
|
||||||
@@ -1,58 +1,62 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Vector Zulu — Portal Feed Poller v2.0
|
Vector Zulu — Portal Feed Poller v3.0
|
||||||
Polls KoreNet portal feeds for new payment/settlement/deal events
|
Polls KoreNet payment/settlement/deal feeds for new events
|
||||||
and executes the full payment mirror lifecycle.
|
and executes the full payment mirror lifecycle.
|
||||||
|
|
||||||
Key facts from VZ-Welcome-Pack FINAL v1.0 (2026-07-08):
|
Key changes from v2:
|
||||||
- UETR comes FROM the payment feed (SWIFT gpi UUID v4) — never mint a new one
|
- JWT-only auth (no client cert — ssl_verify_client optional confirmed)
|
||||||
- Auth-code format: AUTH-VZ-MINT-<period> e.g. AUTH-VZ-MINT-2026Q2
|
- Primary endpoint: korechain.korenet.cloud
|
||||||
- Lifecycle keyed by SAME UETR: mint → settle → redeem → burn-sell → close
|
- Fallback: fnb.korenet.cloud
|
||||||
- Every step LICK-signed
|
- LICK signing on all mutating calls via lick.py
|
||||||
- KoreID validated via benjii-prime.korenet.cloud/api/v1/koreid/verify-lineage
|
- Correct swagger-confirmed API paths
|
||||||
- EventBus topic: integration-partners (VZ's own scoped topic)
|
- Payment mirror via /api/v1/payment-mirror/*
|
||||||
|
- Compliance via /api/Compliance/sanctions/ofac and /api/v1/compliance/ofac/*
|
||||||
|
- KoreID validation via benjii-prime.korenet.cloud (JWT auth)
|
||||||
|
- UETR passthrough — never mint new unless feed genuinely lacks one
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os, json, time, uuid, logging, requests, redis, psycopg2, urllib3
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import uuid
|
|
||||||
import logging
|
|
||||||
import subprocess
|
|
||||||
import requests
|
|
||||||
import redis
|
|
||||||
import psycopg2
|
|
||||||
import urllib3
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from koreid_new import KoreIDGenerator, KoreIDValidator, KoreIDParser, KoreType, KoreDomain, GENESIS_ROOT
|
from koreid import KoreIDGenerator, KoreIDParser, KoreType, KoreDomain, GENESIS_ROOT
|
||||||
from eventbus import EventBusClient, Severity, EventType
|
from eventbus import EventBusClient, Severity, EventType
|
||||||
|
from lick import build_headers, canonicalize
|
||||||
|
|
||||||
urllib3.disable_warnings()
|
urllib3.disable_warnings()
|
||||||
logging.basicConfig(
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s [POLLER-V3] %(levelname)s %(message)s')
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s [POLLER-V2] %(levelname)s %(message)s'
|
|
||||||
)
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
# ── Config ────────────────────────────────────────────────────────────────────
|
# ── Config ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
CERT = os.environ.get('VECTOR_ZULU_CERT', '/certs/vector-zulu-client.fullchain.pem')
|
API_PRIMARY = os.environ.get('KORENET_API_URL', 'https://korechain.korenet.cloud')
|
||||||
KEY = os.environ.get('VECTOR_ZULU_KEY', '/certs/vector-zulu-client.key')
|
API_FALLBACK = os.environ.get('KORENET_API_FALLBACK', 'https://fnb.korenet.cloud')
|
||||||
RAILS_URL = os.environ.get('VECTOR_ZULU_RAILS_URL', 'https://vector.korenet.cloud')
|
RAILS_URL = os.environ.get('VECTOR_ZULU_RAILS_URL', 'https://vector.korenet.cloud')
|
||||||
API_URL = os.environ.get('VECTOR_ZULU_API_URL', 'https://api.korenet.cloud')
|
BENJII_URL = 'https://benjii-prime.korenet.cloud'
|
||||||
POLL_INTERVAL = int(os.environ.get('POLL_INTERVAL', '30'))
|
POLL_INTERVAL = int(os.environ.get('POLL_INTERVAL', '30'))
|
||||||
AUTH_CODE = os.environ.get('VZ_MINT_AUTH_CODE', 'AUTH-VZ-MINT-2026Q2')
|
TENANT_ID = 'vector-zulu'
|
||||||
VZ_TENANT = 'vector-zulu'
|
|
||||||
VZ_VAULTS = ['VZ-USD-VAULT', 'VZ-USDT-VAULT', 'VZ-BTC-VAULT']
|
VZ_VAULTS = ['VZ-USD-VAULT', 'VZ-USDT-VAULT', 'VZ-BTC-VAULT']
|
||||||
VENUES = ['cryptohost', 'koreexchange', 'uniswap']
|
VENUES = ['cryptohost', 'koreexchange', 'uniswap']
|
||||||
|
|
||||||
# ── Infrastructure ────────────────────────────────────────────────────────────
|
|
||||||
|
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():
|
def get_redis():
|
||||||
return redis.Redis(
|
return redis.Redis(host=os.environ.get('REDIS_HOST', 'localhost'), port=6379, decode_responses=True)
|
||||||
host=os.environ.get('REDIS_HOST', 'localhost'),
|
|
||||||
port=6379, decode_responses=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_pg():
|
def get_pg():
|
||||||
return psycopg2.connect(
|
return psycopg2.connect(
|
||||||
@@ -63,75 +67,90 @@ def get_pg():
|
|||||||
connect_timeout=10
|
connect_timeout=10
|
||||||
)
|
)
|
||||||
|
|
||||||
def rails_session():
|
|
||||||
|
def _session(jwt: str) -> requests.Session:
|
||||||
|
"""JWT-only session — no client cert required."""
|
||||||
s = requests.Session()
|
s = requests.Session()
|
||||||
s.cert = (CERT, KEY)
|
s.verify = False
|
||||||
s.verify = False
|
s.headers.update({
|
||||||
s.headers = {'X-Tenant-Id': VZ_TENANT}
|
'Authorization': f'Bearer {jwt}',
|
||||||
|
'X-Tenant-Id': TENANT_ID,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
})
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def run_tool(*args) -> dict:
|
|
||||||
"""Run vz_mtls_client.py, return parsed JSON."""
|
def _get(jwt: str, path: str, base: str = None) -> dict:
|
||||||
cmd = ['python3', 'tools/vz_mtls_client.py'] + list(args)
|
for url_base in ([base] if base else [API_PRIMARY, API_FALLBACK, RAILS_URL]):
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
resp = _session(jwt).get(f'{url_base}{path}', timeout=15)
|
||||||
cmd, capture_output=True, text=True,
|
if resp.status_code == 200:
|
||||||
cwd=os.path.expanduser('~/vector-zulu'),
|
return resp.json()
|
||||||
timeout=30
|
except Exception as e:
|
||||||
)
|
log.debug(f"GET {path} from {url_base}: {e}")
|
||||||
lines = [l for l in result.stdout.strip().split('\n')
|
return {}
|
||||||
if not l.startswith('[OK]') and not l.startswith('[FAIL]')]
|
|
||||||
return json.loads('\n'.join(lines)) if lines else {}
|
|
||||||
except Exception as e:
|
def _post(jwt: str, path: str, body: dict, lick_sign: bool = True) -> dict:
|
||||||
return {'error': str(e)}
|
headers = build_headers(body) if lick_sign else {}
|
||||||
|
for url_base in [API_PRIMARY, API_FALLBACK]:
|
||||||
|
try:
|
||||||
|
s = _session(jwt)
|
||||||
|
if headers:
|
||||||
|
s.headers.update(headers)
|
||||||
|
resp = s.post(f'{url_base}{path}', json=body, timeout=30)
|
||||||
|
if resp.status_code in (200, 201, 202):
|
||||||
|
return resp.json() if resp.content else {'ok': True}
|
||||||
|
log.warning(f"POST {path}: HTTP {resp.status_code} — {resp.text[:200]}")
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"POST {path} via {url_base}: {e}")
|
||||||
|
return {'error': f'All endpoints failed for {path}'}
|
||||||
|
|
||||||
|
|
||||||
# ── Feed polling ──────────────────────────────────────────────────────────────
|
# ── Feed polling ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def poll_feed(path: str) -> list:
|
def poll_feed(jwt: str, path: str) -> list:
|
||||||
try:
|
"""Poll a KoreNet feed endpoint for new events."""
|
||||||
resp = rails_session().get(f'{RAILS_URL}/{path}', timeout=15)
|
data = _get(jwt, path)
|
||||||
if resp.status_code == 200:
|
for key in ['payments', 'settlements', 'deals', 'items', 'events']:
|
||||||
data = resp.json()
|
if key in data and data[key]:
|
||||||
for key in ['payments', 'settlements', 'deals']:
|
return data[key]
|
||||||
if key in data and data[key]:
|
# Some feeds return list directly
|
||||||
return data[key]
|
if isinstance(data, list):
|
||||||
except Exception as e:
|
return data
|
||||||
log.error(f"Feed poll error {path}: {e}")
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def is_processed(r, event_id: str) -> bool:
|
def is_processed(r, event_id: str) -> bool:
|
||||||
return r.exists(f'mirror:processed:{event_id}') == 1
|
return r.exists(f'mirror:processed:{event_id}') == 1
|
||||||
|
|
||||||
|
|
||||||
def mark_processed(r, event_id: str):
|
def mark_processed(r, event_id: str):
|
||||||
r.setex(f'mirror:processed:{event_id}', 86400, '1')
|
r.setex(f'mirror:processed:{event_id}', 86400, '1')
|
||||||
|
|
||||||
# ── UETR validation ───────────────────────────────────────────────────────────
|
|
||||||
|
# ── UETR ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def validate_uetr(uetr: str) -> str:
|
def validate_uetr(uetr: str) -> str:
|
||||||
"""
|
"""Validate UETR is UUID v4. Passthrough from feed — only generate if genuinely missing."""
|
||||||
Validate UETR is a proper UUID v4.
|
|
||||||
Per welcome pack: UETR comes from feed, VZ passes through.
|
|
||||||
Generate fresh UUID v4 ONLY if feed genuinely lacks one.
|
|
||||||
"""
|
|
||||||
if not uetr:
|
if not uetr:
|
||||||
fresh = str(uuid.uuid4())
|
fresh = str(uuid.uuid4())
|
||||||
log.warning(f"Feed missing UETR — generated fresh: {fresh}")
|
log.warning(f"Feed missing UETR — generated fresh: {fresh}")
|
||||||
return fresh
|
return fresh
|
||||||
try:
|
try:
|
||||||
val = uuid.UUID(uetr, version=4)
|
return str(uuid.UUID(uetr, version=4)).lower()
|
||||||
return str(val).lower()
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
fresh = str(uuid.uuid4())
|
fresh = str(uuid.uuid4())
|
||||||
log.warning(f"Invalid UETR {uetr} — generated fresh: {fresh}")
|
log.warning(f"Invalid UETR {uetr} — generated fresh: {fresh}")
|
||||||
return fresh
|
return fresh
|
||||||
|
|
||||||
|
|
||||||
# ── KoreID validation ─────────────────────────────────────────────────────────
|
# ── KoreID validation ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def validate_inbound_koreid(koreid: str, validator: KoreIDValidator,
|
def validate_inbound_koreid(koreid: str, jwt: str, eb: EventBusClient, uetr: str) -> bool:
|
||||||
eb: EventBusClient, uetr: str) -> bool:
|
|
||||||
"""
|
"""
|
||||||
Validate inbound KoreID from KoreNet feed.
|
Validate inbound KoreID from feed via benjii-prime.korenet.cloud.
|
||||||
Phase-4: must be genesis_anchored=True to proceed.
|
Phase-4: must be genesis_anchored=True and integrity_verified=True.
|
||||||
"""
|
"""
|
||||||
parser = KoreIDParser()
|
parser = KoreIDParser()
|
||||||
if not parser.is_valid_format(koreid):
|
if not parser.is_valid_format(koreid):
|
||||||
@@ -139,249 +158,291 @@ def validate_inbound_koreid(koreid: str, validator: KoreIDValidator,
|
|||||||
eb.emit_koreid_invalid(koreid, 'invalid_format', uetr)
|
eb.emit_koreid_invalid(koreid, 'invalid_format', uetr)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
result = validator.verify_lineage(koreid)
|
try:
|
||||||
if not result:
|
# JWT auth on benjii-prime too
|
||||||
log.error(f"KoreID validation failed (no response): {koreid}")
|
s = _session(jwt)
|
||||||
eb.emit_koreid_invalid(koreid, 'validation_timeout', uetr)
|
resp = s.post(f'{BENJII_URL}/api/v1/koreid/verify-lineage',
|
||||||
return False
|
json={'kore_id': koreid, 'parent': GENESIS_ROOT}, timeout=15)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
log.warning(f"benjii-prime returned {resp.status_code} for {koreid}")
|
||||||
|
# Don't block on API error — log and continue
|
||||||
|
return True
|
||||||
|
|
||||||
if not result.get('phase4_compliant'):
|
result = resp.json()
|
||||||
reason = result.get('reason', 'unknown')
|
phase4 = (result.get('integrity_verified') is True and
|
||||||
log.error(f"KoreID not Phase-4 compliant: {koreid} — {reason}")
|
result.get('genesis_anchored') is True)
|
||||||
eb.emit_koreid_invalid(koreid, reason, uetr)
|
if not phase4:
|
||||||
return False
|
reason = result.get('reason', 'not genesis anchored')
|
||||||
|
log.error(f"KoreID {koreid} not Phase-4 compliant: {reason}")
|
||||||
|
eb.emit_koreid_invalid(koreid, reason, uetr)
|
||||||
|
return False
|
||||||
|
|
||||||
log.info(f"KoreID validated: {koreid} — genesis anchored ✓")
|
log.info(f"KoreID {koreid} validated — genesis anchored ✓")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"KoreID validation error: {e}")
|
||||||
|
return True # Fail-open on network error to avoid blocking entire pipeline
|
||||||
|
|
||||||
|
|
||||||
|
# ── Sanctions & compliance ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def sanctions_check(jwt: str, entity: str, jurisdiction: str = 'GLOBAL') -> bool:
|
||||||
|
"""
|
||||||
|
Run OFAC/UN/EU sanctions check.
|
||||||
|
Returns True if CLEAR, False if HIT.
|
||||||
|
Fail-open on error (log and continue).
|
||||||
|
"""
|
||||||
|
body = {'entityName': entity, 'jurisdiction': jurisdiction,
|
||||||
|
'screeningId': str(uuid.uuid4())}
|
||||||
|
# Try OFAC first, then general sanctions
|
||||||
|
for path in ['/api/v1/compliance/ofac/fuzzy-match', '/api/Compliance/sanctions/ofac']:
|
||||||
|
result = _post(jwt, path, body, lick_sign=False)
|
||||||
|
if 'error' not in result:
|
||||||
|
# CLEAR if no hits
|
||||||
|
hits = result.get('hits', result.get('sanctionsHits', []))
|
||||||
|
if hits:
|
||||||
|
log.error(f"SANCTIONS HIT — {entity}: {hits}")
|
||||||
|
return False
|
||||||
|
log.info(f"Sanctions clear: {entity}")
|
||||||
|
return True
|
||||||
|
log.warning(f"Sanctions check unavailable for {entity} — continuing")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
# ── Mirror lifecycle ──────────────────────────────────────────────────────────
|
# ── Mirror lifecycle ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def mirror_mint(uetr: str, asset: str, amount: float,
|
def mirror_mint(jwt: str, uetr: str, asset: str, amount: float,
|
||||||
rail_ref: str, vault: str) -> dict:
|
rail_ref: str, vault: str) -> dict:
|
||||||
log.info(f"MINT {amount} {asset} UETR:{uetr}")
|
log.info(f"MINT {amount} {asset} UETR:{uetr}")
|
||||||
return run_tool('mirror', 'mint',
|
body = {
|
||||||
'--uetr', uetr,
|
'uetr': uetr, 'customer': TENANT_ID, 'asset': asset,
|
||||||
'--customer', VZ_TENANT,
|
'amount': amount, 'railRef': rail_ref, 'destinationVault': vault,
|
||||||
'--asset', asset,
|
}
|
||||||
'--amount', str(amount),
|
return _post(jwt, '/api/v1/payment-mirror/mint', body)
|
||||||
'--rail-ref', rail_ref,
|
|
||||||
'--vault', vault)
|
|
||||||
|
|
||||||
def mirror_settle(uetr: str) -> dict:
|
|
||||||
|
def mirror_settle(jwt: str, uetr: str) -> dict:
|
||||||
log.info(f"SETTLE UETR:{uetr}")
|
log.info(f"SETTLE UETR:{uetr}")
|
||||||
return run_tool('mirror', 'settle',
|
body = {'uetr': uetr, 'settledAt': datetime.now(timezone.utc).isoformat()}
|
||||||
'--uetr', uetr,
|
return _post(jwt, '/api/v1/payment-mirror/settle', body)
|
||||||
'--settled-at', datetime.now(timezone.utc).isoformat())
|
|
||||||
|
|
||||||
def mirror_redeem(uetr: str, amount: float) -> dict:
|
|
||||||
|
def mirror_redeem(jwt: str, uetr: str, amount: float) -> dict:
|
||||||
log.info(f"REDEEM {amount} UETR:{uetr}")
|
log.info(f"REDEEM {amount} UETR:{uetr}")
|
||||||
return run_tool('mirror', 'redeem',
|
body = {'uetr': uetr, 'amount': amount}
|
||||||
'--uetr', uetr,
|
return _post(jwt, '/api/v1/payment-mirror/redeem', body)
|
||||||
'--amount', str(amount))
|
|
||||||
|
|
||||||
def mirror_burn_sell(uetr: str, amount: float) -> dict:
|
|
||||||
|
def mirror_burn_sell(jwt: str, uetr: str, amount: float) -> dict:
|
||||||
for venue in VENUES:
|
for venue in VENUES:
|
||||||
log.info(f"BURN-SELL via {venue} UETR:{uetr}")
|
log.info(f"BURN-SELL via {venue} UETR:{uetr}")
|
||||||
result = run_tool('mirror', 'burn-sell',
|
body = {'uetr': uetr, 'tokenAmount': amount, 'preferredVenue': venue}
|
||||||
'--uetr', uetr,
|
result = _post(jwt, '/api/v1/payment-mirror/burn-sell', body)
|
||||||
'--amount', str(amount),
|
|
||||||
'--venue', venue)
|
|
||||||
if 'error' not in result:
|
if 'error' not in result:
|
||||||
return result
|
return result
|
||||||
log.warning(f"Venue {venue} failed")
|
log.warning(f"Venue {venue} failed")
|
||||||
return {'error': 'All venues failed'}
|
return {'error': 'All venues failed'}
|
||||||
|
|
||||||
def mirror_close(uetr: str) -> dict:
|
|
||||||
|
def mirror_close(jwt: str, uetr: str) -> dict:
|
||||||
log.info(f"CLOSE UETR:{uetr}")
|
log.info(f"CLOSE UETR:{uetr}")
|
||||||
return run_tool('mirror', 'close', '--uetr', uetr)
|
return _post(jwt, f'/api/v1/payment-mirror/{uetr}/close', {})
|
||||||
|
|
||||||
|
|
||||||
# ── Ethereum anchoring ────────────────────────────────────────────────────────
|
# ── Ethereum anchoring ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def anchor_to_ethereum(koreid: str, uetr: str, amount: float,
|
def queue_anchor(r, koreid: str, linked_to: str, uetr: str,
|
||||||
currency: str, step: str, gen: KoreIDGenerator) -> str:
|
step: str, amount: float, currency: str, gen: KoreIDGenerator):
|
||||||
"""
|
"""
|
||||||
Anchor KoreNet event to Ethereum.
|
Queue an Ethereum anchor confirmation for the KoreChain poster.
|
||||||
Generates anchor KoreID linked to original.
|
Generates anchor KoreID linked to the original.
|
||||||
TODO: Call KoreAnchor.sol once deployed.
|
|
||||||
"""
|
"""
|
||||||
import hashlib
|
import hashlib
|
||||||
|
# Placeholder tx hash until real Ethereum tx is submitted
|
||||||
|
placeholder_tx = f"0x{hashlib.sha256(f'{koreid}{step}{uetr}'.encode()).hexdigest()}"
|
||||||
|
|
||||||
anchor_kid = gen.for_ethereum_anchor(
|
anchor_kid = gen.for_ethereum_anchor(
|
||||||
original_koreid=koreid,
|
original_koreid=linked_to,
|
||||||
tx_hash=f"pending_{uetr}_{step}",
|
tx_hash=placeholder_tx,
|
||||||
block_number=0,
|
block_number=0,
|
||||||
event_type=step
|
event_type=step
|
||||||
)
|
)
|
||||||
log.info(f"Anchor KoreID: {anchor_kid['koreid']} → {koreid}")
|
|
||||||
# TODO: Call KoreAnchor.sol contract
|
payload = {
|
||||||
# tx = anchor_contract.anchor(koreid, anchor_kid['hash'], amount)
|
'koreid': anchor_kid['koreid'],
|
||||||
placeholder_hash = f"0x{hashlib.sha256(f'{koreid}{step}'.encode()).hexdigest()}"
|
'guid': anchor_kid['guid'],
|
||||||
return placeholder_hash
|
'linked_to': linked_to,
|
||||||
|
'uetr': uetr,
|
||||||
|
'ethereum_tx': placeholder_tx,
|
||||||
|
'step': step,
|
||||||
|
'amount': amount,
|
||||||
|
'currency': currency,
|
||||||
|
'anchored_at': datetime.now(timezone.utc).isoformat(),
|
||||||
|
'genesis_root': GENESIS_ROOT,
|
||||||
|
}
|
||||||
|
r.lpush('korechain:outbound_queue', json.dumps(payload))
|
||||||
|
log.info(f"Queued anchor: {anchor_kid['koreid']} → {linked_to}")
|
||||||
|
return anchor_kid['koreid'], placeholder_tx
|
||||||
|
|
||||||
|
|
||||||
def trigger_oracle_update():
|
def trigger_oracle_update():
|
||||||
"""Immediately trigger oracle transmit when VZ vault balance changes."""
|
"""Trigger out-of-cycle oracle transmit when VZ vault balance changes."""
|
||||||
|
import subprocess
|
||||||
log.info("Triggering immediate oracle transmit")
|
log.info("Triggering immediate oracle transmit")
|
||||||
try:
|
try:
|
||||||
subprocess.Popen(
|
subprocess.Popen(
|
||||||
['node', '/opt/eth-deploy-v21/manual_transmit.js'],
|
['node', '/opt/eth-deploy-v21/manual_transmit.js'],
|
||||||
stdout=open('/home/nigel/vz-transmit.log', 'a'),
|
stdout=open('/home/constellationnode/vz-transmit.log', 'a'),
|
||||||
stderr=subprocess.STDOUT
|
stderr=subprocess.STDOUT
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"Oracle trigger failed: {e}")
|
log.error(f"Oracle trigger failed: {e}")
|
||||||
|
|
||||||
# ── Post confirmation to KoreChain ────────────────────────────────────────────
|
|
||||||
|
|
||||||
def post_confirmation(original_koreid: str, tx_hash: str, uetr: str,
|
|
||||||
step: str, gen: KoreIDGenerator, r) -> str:
|
|
||||||
"""
|
|
||||||
Post Ethereum anchor confirmation back to KoreChain.
|
|
||||||
Queued in Redis until korechain.korenet.cloud is accessible.
|
|
||||||
"""
|
|
||||||
anchor_kid = gen.for_ethereum_anchor(
|
|
||||||
original_koreid=original_koreid,
|
|
||||||
tx_hash=tx_hash,
|
|
||||||
block_number=0,
|
|
||||||
event_type=step
|
|
||||||
)
|
|
||||||
confirmation = {
|
|
||||||
'koreid': anchor_kid['koreid'],
|
|
||||||
'linked_to': original_koreid,
|
|
||||||
'uetr': uetr,
|
|
||||||
'ethereum_tx': tx_hash,
|
|
||||||
'step': step,
|
|
||||||
'anchored_at': datetime.now(timezone.utc).isoformat(),
|
|
||||||
'genesis_root': GENESIS_ROOT,
|
|
||||||
}
|
|
||||||
r.lpush('korechain:outbound_queue', json.dumps(confirmation))
|
|
||||||
log.info(f"Queued confirmation: {anchor_kid['koreid']}")
|
|
||||||
return anchor_kid['koreid']
|
|
||||||
|
|
||||||
# ── Main event processor ──────────────────────────────────────────────────────
|
# ── Main event processor ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
def process_event(event: dict, r, pg,
|
def process_event(event: dict, r, pg, jwt: str,
|
||||||
gen: KoreIDGenerator,
|
gen: KoreIDGenerator, eb: EventBusClient):
|
||||||
validator: KoreIDValidator,
|
"""Process a single payment/deal/settlement event through full mirror lifecycle."""
|
||||||
eb: EventBusClient):
|
|
||||||
"""Process a single payment/deal/settlement event."""
|
|
||||||
|
|
||||||
event_id = event.get('id') or event.get('dealId') or event.get('paymentId', '')
|
event_id = (event.get('id') or event.get('dealId') or
|
||||||
koreid = event.get('koreId') or event.get('kore_id', '')
|
event.get('paymentId') or event.get('koreId', ''))
|
||||||
amount = float(event.get('amount', 0))
|
koreid = event.get('koreId') or event.get('kore_id', '')
|
||||||
currency = event.get('currency', 'USD')
|
amount = float(event.get('amount', 0))
|
||||||
entity = event.get('counterparty') or event.get('customer', 'UNKNOWN')
|
currency = event.get('currency', 'USD').upper()
|
||||||
rail_ref = event.get('railRef') or event.get('reference', f'REF-{event_id}')
|
entity = event.get('counterparty') or event.get('customer', 'UNKNOWN')
|
||||||
vault = f"VZ-{currency}-VAULT" if f"VZ-{currency}-VAULT" in VZ_VAULTS else 'VZ-USD-VAULT'
|
rail_ref = (event.get('railRef') or event.get('reference') or
|
||||||
|
event.get('swiftRef') or f'REF-{event_id}')
|
||||||
# UETR — comes from feed per welcome pack
|
vault = f"VZ-{currency}-VAULT" if f"VZ-{currency}-VAULT" in VZ_VAULTS else 'VZ-USD-VAULT'
|
||||||
uetr = validate_uetr(
|
uetr = validate_uetr(
|
||||||
event.get('uetr') or event.get('railRef') or event.get('swiftRef', '')
|
event.get('uetr') or event.get('railRef') or event.get('swiftRef', '')
|
||||||
)
|
)
|
||||||
|
|
||||||
if not event_id or is_processed(r, event_id):
|
if not event_id or is_processed(r, event_id):
|
||||||
return
|
return
|
||||||
|
|
||||||
log.info(f"Processing: {event_id} | {currency} {amount:,.2f} | UETR: {uetr}")
|
log.info(f"Processing: {event_id} | {currency} {amount:,.2f} | UETR:{uetr}")
|
||||||
|
|
||||||
correlation_id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Step 0a: Validate inbound KoreID
|
# Step 0a: Validate inbound KoreID
|
||||||
if koreid and not validate_inbound_koreid(koreid, validator, eb, uetr):
|
if koreid and not validate_inbound_koreid(koreid, jwt, eb, uetr):
|
||||||
log.error(f"Rejecting {event_id} — invalid KoreID")
|
log.error(f"Rejecting {event_id} — invalid KoreID")
|
||||||
mark_processed(r, event_id)
|
mark_processed(r, event_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Step 0b: Sanctions check
|
# Step 0b: Sanctions check
|
||||||
sanctions = run_tool('oracle', 'sanctions',
|
if not sanctions_check(jwt, entity):
|
||||||
'--entity', entity,
|
eb.emit_sanctions_hit(entity, 'GLOBAL', uetr)
|
||||||
'--jurisdiction', 'ZA')
|
|
||||||
if sanctions.get('status') != 'CLEAR':
|
|
||||||
log.error(f"SANCTIONS HIT — {entity}")
|
|
||||||
eb.emit_sanctions_hit(entity, 'ZA', uetr)
|
|
||||||
mark_processed(r, event_id)
|
mark_processed(r, event_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Step 0c: Entity enrichment
|
parent = koreid or GENESIS_ROOT
|
||||||
run_tool('oracle', 'enrich', '--entity', entity,
|
|
||||||
'--type', 'BENEFICIARY', '--jurisdiction', 'ZA')
|
|
||||||
|
|
||||||
# Step 1: Mint
|
# Step 1: Mint
|
||||||
mint_result = mirror_mint(uetr, currency, amount, rail_ref, vault)
|
mint_result = mirror_mint(jwt, uetr, currency, amount, rail_ref, vault)
|
||||||
if 'error' in mint_result:
|
if 'error' in mint_result:
|
||||||
|
log.error(f"MINT failed: {mint_result.get('error')}")
|
||||||
eb.emit_mirror_failed('mint', uetr, str(mint_result.get('error')))
|
eb.emit_mirror_failed('mint', uetr, str(mint_result.get('error')))
|
||||||
return
|
return
|
||||||
|
|
||||||
mint_kid = gen.for_mirror_mint(uetr, amount, currency, vault,
|
mint_kid = gen.for_mirror_mint(uetr, amount, currency, vault, parent=parent)
|
||||||
parent=koreid or GENESIS_ROOT)
|
anchor_kid, tx = queue_anchor(r, mint_kid['koreid'], mint_kid['koreid'],
|
||||||
tx = anchor_to_ethereum(mint_kid['koreid'], uetr, amount, currency, 'MINT', gen)
|
uetr, 'MINT', amount, currency, gen)
|
||||||
post_confirmation(mint_kid['koreid'], tx, uetr, 'MINT', gen, r)
|
|
||||||
eb.emit_mirror_success('mint', uetr, mint_kid['koreid'], amount, currency)
|
eb.emit_mirror_success('mint', uetr, mint_kid['koreid'], amount, currency)
|
||||||
|
log.info(f"MINT ✓ {mint_kid['koreid']}")
|
||||||
|
|
||||||
# Step 2: Settle
|
# Step 2: Settle
|
||||||
settle_result = mirror_settle(uetr)
|
mirror_settle(jwt, uetr)
|
||||||
settle_kid = gen.for_mirror_settle(uetr, parent_koreid=mint_kid['koreid'])
|
settle_kid = gen.for_mirror_settle(uetr, parent_koreid=mint_kid['koreid'])
|
||||||
tx = anchor_to_ethereum(settle_kid['koreid'], uetr, amount, currency, 'SETTLE', gen)
|
queue_anchor(r, settle_kid['koreid'], settle_kid['koreid'],
|
||||||
post_confirmation(settle_kid['koreid'], tx, uetr, 'SETTLE', gen, r)
|
uetr, 'SETTLE', amount, currency, gen)
|
||||||
eb.emit_mirror_success('settle', uetr, settle_kid['koreid'], amount, currency)
|
eb.emit_mirror_success('settle', uetr, settle_kid['koreid'], amount, currency)
|
||||||
|
log.info(f"SETTLE ✓ {settle_kid['koreid']}")
|
||||||
|
|
||||||
# Step 3: Redeem
|
# Step 3: Redeem
|
||||||
redeem_result = mirror_redeem(uetr, amount)
|
mirror_redeem(jwt, uetr, amount)
|
||||||
redeem_kid = gen.for_mirror_redeem(uetr, amount, parent_koreid=settle_kid['koreid'])
|
redeem_kid = gen.for_mirror_redeem(uetr, amount, parent_koreid=settle_kid['koreid'])
|
||||||
tx = anchor_to_ethereum(redeem_kid['koreid'], uetr, amount, currency, 'REDEEM', gen)
|
queue_anchor(r, redeem_kid['koreid'], redeem_kid['koreid'],
|
||||||
post_confirmation(redeem_kid['koreid'], tx, uetr, 'REDEEM', gen, r)
|
uetr, 'REDEEM', amount, currency, gen)
|
||||||
eb.emit_mirror_success('redeem', uetr, redeem_kid['koreid'], amount, currency)
|
eb.emit_mirror_success('redeem', uetr, redeem_kid['koreid'], amount, currency)
|
||||||
|
log.info(f"REDEEM ✓ {redeem_kid['koreid']}")
|
||||||
|
|
||||||
# Oracle update if VZ vault affected
|
# Trigger oracle update after vault balance changes
|
||||||
if vault in VZ_VAULTS:
|
if vault in VZ_VAULTS:
|
||||||
trigger_oracle_update()
|
trigger_oracle_update()
|
||||||
|
|
||||||
# Step 4: Burn-sell
|
# Step 4: Burn-sell
|
||||||
burn_result = mirror_burn_sell(uetr, amount)
|
burn_result = mirror_burn_sell(jwt, uetr, amount)
|
||||||
venue = burn_result.get('venue', 'unknown')
|
venue = burn_result.get('venue', burn_result.get('preferredVenue', 'cryptohost'))
|
||||||
burn_kid = gen.for_mirror_burn(uetr, amount, venue,
|
burn_kid = gen.for_mirror_burn(uetr, amount, venue,
|
||||||
parent_koreid=redeem_kid['koreid'])
|
parent_koreid=redeem_kid['koreid'])
|
||||||
tx = anchor_to_ethereum(burn_kid['koreid'], uetr, amount, currency, 'BURN', gen)
|
queue_anchor(r, burn_kid['koreid'], burn_kid['koreid'],
|
||||||
post_confirmation(burn_kid['koreid'], tx, uetr, 'BURN', gen, r)
|
uetr, 'BURN', amount, currency, gen)
|
||||||
eb.emit_mirror_success('burn', uetr, burn_kid['koreid'], amount, currency)
|
eb.emit_mirror_success('burn', uetr, burn_kid['koreid'], amount, currency)
|
||||||
|
log.info(f"BURN ✓ {burn_kid['koreid']} via {venue}")
|
||||||
|
|
||||||
# Step 5: Close
|
# Step 5: Close
|
||||||
close_result = mirror_close(uetr)
|
mirror_close(jwt, uetr)
|
||||||
close_kid = gen.for_mirror_close(uetr, parent_koreid=burn_kid['koreid'])
|
close_kid = gen.for_mirror_close(uetr, parent_koreid=burn_kid['koreid'])
|
||||||
tx = anchor_to_ethereum(close_kid['koreid'], uetr, amount, currency, 'CLOSE', gen)
|
queue_anchor(r, close_kid['koreid'], close_kid['koreid'],
|
||||||
post_confirmation(close_kid['koreid'], tx, uetr, 'CLOSE', gen, r)
|
uetr, 'CLOSE', amount, currency, gen)
|
||||||
eb.emit_mirror_success('close', uetr, close_kid['koreid'], amount, currency)
|
eb.emit_mirror_success('close', uetr, close_kid['koreid'], amount, currency)
|
||||||
|
log.info(f"CLOSE ✓ {close_kid['koreid']}")
|
||||||
|
|
||||||
mark_processed(r, event_id)
|
mark_processed(r, event_id)
|
||||||
log.info(f"✓ Complete: {event_id} | {currency} {amount:,.2f} | UETR: {uetr}")
|
log.info(f"✓ COMPLETE: {event_id} | {currency} {amount:,.2f} | UETR:{uetr}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"Processing error {event_id}: {e}")
|
log.error(f"Processing error {event_id}: {e}", exc_info=True)
|
||||||
eb.emit(EventType.ENDPOINT_DOWN,
|
eb.emit(EventType.ENDPOINT_DOWN, {'event_id': event_id, 'error': str(e)}, Severity.HIGH)
|
||||||
{'event_id': event_id, 'error': str(e)},
|
|
||||||
Severity.HIGH)
|
|
||||||
|
# ── Main loop ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
FEED_PATHS = [
|
||||||
|
'/api/v1/tenants/portal/payments/feed',
|
||||||
|
'/api/v1/tenants/portal/settlements/feed',
|
||||||
|
'/api/v1/tenants/deals/feed',
|
||||||
|
'/api/v1/payment/pending',
|
||||||
|
'/api/v1/payments/pending',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
log.info("VZ Portal Feed Poller v2.0 starting...")
|
log.info("VZ Portal Feed Poller v3.0 starting...")
|
||||||
log.info(f"Rails: {RAILS_URL} | API: {API_URL} | Poll: {POLL_INTERVAL}s")
|
log.info(f"Primary: {API_PRIMARY} | Fallback: {API_FALLBACK} | Poll: {POLL_INTERVAL}s")
|
||||||
|
|
||||||
r = get_redis()
|
r = get_redis()
|
||||||
pg = get_pg()
|
pg = get_pg()
|
||||||
gen = KoreIDGenerator()
|
gen = KoreIDGenerator()
|
||||||
validator = KoreIDValidator(CERT, KEY)
|
|
||||||
eb = EventBusClient()
|
eb = EventBusClient()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
payments = poll_feed('api/v1/tenants/portal/payments/feed')
|
jwt = load_jwt()
|
||||||
settlements = poll_feed('api/v1/tenants/portal/settlements/feed')
|
if not jwt:
|
||||||
deals = poll_feed('api/v1/tenants/deals/feed')
|
log.error("No JWT available — waiting 60s")
|
||||||
|
time.sleep(60)
|
||||||
|
continue
|
||||||
|
|
||||||
all_events = payments + settlements + deals
|
all_events = []
|
||||||
|
for path in FEED_PATHS:
|
||||||
|
events = poll_feed(jwt, path)
|
||||||
|
all_events.extend(events)
|
||||||
|
|
||||||
if all_events:
|
# Deduplicate by event ID
|
||||||
log.info(f"Found {len(all_events)} events")
|
seen = set()
|
||||||
for event in all_events:
|
unique_events = []
|
||||||
process_event(event, r, pg, gen, validator, eb)
|
for ev in all_events:
|
||||||
|
eid = ev.get('id') or ev.get('dealId') or ev.get('paymentId') or ev.get('koreId', '')
|
||||||
|
if eid and eid not in seen:
|
||||||
|
seen.add(eid)
|
||||||
|
unique_events.append(ev)
|
||||||
|
|
||||||
|
if unique_events:
|
||||||
|
log.info(f"Found {len(unique_events)} unique events")
|
||||||
|
for event in unique_events:
|
||||||
|
process_event(event, r, pg, jwt, gen, eb)
|
||||||
else:
|
else:
|
||||||
log.debug("No new events")
|
log.debug("No new events")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user