#!/usr/bin/env python3 """ Vector Zulu — Bank Bridge H2H Submission Submits BankBridgeDocumentRequest to KoreNet's banking platform. Targets korebank.korenet.cloud (primary) with fnb.korenet.cloud fallback. Usage: python bank_bridge.py --dry-run # validate and print payload only python bank_bridge.py --register-nostro # register VZ nostro at ABN AMRO python bank_bridge.py --register-corridor # register VZ → ABN AMRO corridor python bank_bridge.py --submit # submit BankBridgeDocumentRequest python bank_bridge.py --status {uetr} # check instruction status """ import os, sys, json, time, uuid, hmac, hashlib, logging, argparse, requests, urllib3 from datetime import datetime, timezone urllib3.disable_warnings() logging.basicConfig(level=logging.INFO, format='%(asctime)s [BANK-BRIDGE] %(levelname)s %(message)s') log = logging.getLogger(__name__) # ── Config ──────────────────────────────────────────────────────────────────── # KoreNet banking platform — primary KOREBANK_PRIMARY = os.environ.get('KOREBANK_URL', 'https://korebank.korenet.cloud') # FNB node — fallback (confirmed accepting JWT for vault reads) KOREBANK_FALLBACK = os.environ.get('KOREBANK_FALLBACK', 'https://fnb.korenet.cloud') # VZ identity TENANT_ID = 'vector-zulu' OPERATOR_ID = 'KORE-ENT-EFA1FD85-9173' # VZ bank relationship — Wells Fargo / SM Legacy Trust (beneficiary) VZ_BENEFICIARY = { 'name': 'SM Legacy Trust', 'account': os.environ.get('WF_ACCOUNT', '8618159373'), 'bic': 'WFBIUS6S', 'routing': '121000248', 'bankName': 'Wells Fargo Bank N.A.', 'country': 'US', } # ABN AMRO — H2H counterparty (sandbox details pending Bryan) ABN_AMRO = { 'bic': os.environ.get('ABN_AMRO_BIC', 'ABNANL2A'), 'account': os.environ.get('ABN_AMRO_ACCOUNT', ''), # PENDING BRYAN 'bankName': 'ABN AMRO Bank N.V.', 'country': 'NL', 'environment': os.environ.get('ABN_AMRO_ENV', 'SANDBOX'), } # Active proposal PROPOSAL = { 'transferId': 'VZ-PROP-a77a1edcbe7d435eb1907aa0', 'koreId': 'KID-TRF-PAY-20260716-9cd6806e01a556a2', 'uetr': 'd13a4cb3-518d-4eeb-84d6-b117c198159d', 'amount': '1000.00', 'currency': 'USD', } # ── Auth ────────────────────────────────────────────────────────────────────── def load_secrets(): for path in ['/app/.secrets', os.path.expanduser('~/.korenet/vector-zulu/.secrets')]: if os.path.exists(path): jwt = lick_key = '' with open(path) as f: for line in f: if 'VECTOR_ZULU_JWT=' in line: jwt = line.split('=',1)[1].strip().strip('"').strip("'") if 'VECTOR_ZULU_LICK_KEY=' in line: lick_key = line.split('=',1)[1].strip().strip('"').strip("'") if jwt: return jwt, lick_key raise RuntimeError("Could not load secrets") def lick_headers(jwt, lick_key, body_bytes): KEY = lick_key.encode('utf-8') ts = int(time.time()) sig = hmac.new(KEY, body_bytes + b'\n' + str(ts).encode(), hashlib.sha256).hexdigest() return { 'Authorization': f'Bearer {jwt}', 'X-Tenant-Id': TENANT_ID, 'Content-Type': 'application/json', 'x-lick-ts': str(ts), 'x-lick-sig': sig, 'x-lick-nonce': str(uuid.uuid4()), 'x-lick-signer': TENANT_ID, 'x-lick-variant': 'canonical', 'Idempotency-Key': str(uuid.uuid4()), 'X-Correlation-ID': str(uuid.uuid4()), } def post(jwt, lick_key, path, payload, dry_run=False): body_bytes = json.dumps(payload, separators=(',',':')).encode('utf-8') headers = lick_headers(jwt, lick_key, body_bytes) if dry_run: log.info(f"[DRY RUN] POST {path}") log.info(f"Payload: {json.dumps(payload, indent=2)}") log.info(f"x-lick-sig: {headers['x-lick-sig']}") return {'dryRun': True, 'path': path, 'payload': payload} for base in [KOREBANK_PRIMARY, KOREBANK_FALLBACK]: try: r = requests.post(f'{base}{path}', data=body_bytes, headers=headers, verify=False, timeout=30) log.info(f"POST {base}{path} → HTTP {r.status_code}") if r.status_code in (200, 201, 202): return r.json() elif r.status_code == 401: log.warning(f"{base}: 401 — {r.text[:100]}") # Try fallback continue else: log.error(f"{base}: {r.status_code} — {r.text[:200]}") return {'error': r.text, 'status': r.status_code} except Exception as e: log.warning(f"{base}: connection error — {e}") continue return {'error': 'All endpoints failed'} def get(jwt, path): headers = { 'Authorization': f'Bearer {jwt}', 'X-Tenant-Id': TENANT_ID, } for base in [KOREBANK_PRIMARY, KOREBANK_FALLBACK]: try: r = requests.get(f'{base}{path}', headers=headers, verify=False, timeout=15) if r.status_code == 200: return r.json() log.warning(f"{base}{path}: {r.status_code} — {r.text[:100]}") except Exception as e: log.warning(f"{base}: {e}") return None # ── Operations ──────────────────────────────────────────────────────────────── def register_corridor(jwt, lick_key, dry_run=False): """Register VZ → ABN AMRO corridor on KoreNet.""" log.info("Registering VZ → ABN AMRO corridor...") payload = { 'source_country': 'KN', # St Kitts & Nevis — VZ domicile 'destination_country': 'NL', # Netherlands — ABN AMRO 'name': 'VZ-ABNAMRO-USD-CORRIDOR', 'supported_rails': ['SWIFT_GPI', 'H2H'], 'settlement_currencies': ['USD', 'GBP', 'EUR'], } # PENDING BRYAN: confirm correct endpoint path on korebank # Candidate paths based on swagger schemas found: for path in [ '/api/v1/corridor/register', '/api/corridor/register', '/api/v1/BankRegistration/corridor', '/api/BankRegistration/corridor', ]: result = post(jwt, lick_key, path, payload, dry_run) if dry_run or result.get('error') != 'All endpoints failed': return result return result def register_nostro(jwt, lick_key, dry_run=False): """Register VZ nostro account at ABN AMRO.""" log.info("Registering VZ nostro at ABN AMRO...") if not ABN_AMRO['account'] and not dry_run: log.error("ABN_AMRO_ACCOUNT not set — cannot register nostro") log.error("Set ABN_AMRO_ACCOUNT env var with sandbox account from Bryan") return {'error': 'ABN_AMRO_ACCOUNT not configured'} payload = { 'holderBic': 'KNCOZAJJ', # VZ BIC 'counterpartyBic': ABN_AMRO['bic'], 'currency': 'USD', 'balanceMinor': 0, # Initial balance — ABN AMRO will confirm actual } for path in [ '/api/v1/nostro/register', '/api/nostro/register', '/api/v1/BankRegistration/nostro', '/api/BankRegistration/nostro', ]: result = post(jwt, lick_key, path, payload, dry_run) if dry_run or result.get('error') != 'All endpoints failed': return result return result def register_bank_account(jwt, lick_key, dry_run=False): """Register VZ bank account (Wells Fargo beneficiary) with KoreNet.""" log.info("Registering VZ Wells Fargo beneficiary account...") payload = { 'accountNumber': VZ_BENEFICIARY['account'], 'authorizedSigners': [TENANT_ID], 'bankName': VZ_BENEFICIARY['bankName'], 'iban': None, 'jurisdiction': VZ_BENEFICIARY['country'], 'routingNumber': VZ_BENEFICIARY['routing'], 'swiftCode': VZ_BENEFICIARY['bic'], 'vaultId': 'VZ-USD-VAULT', } result = post(jwt, lick_key, '/api/BankRegistration/register-bank-account', payload, dry_run) return result def submit_bridge_document(jwt, lick_key, dry_run=False): """Submit BankBridgeDocumentRequest for H2H transfer.""" log.info("Submitting BankBridgeDocumentRequest...") if not ABN_AMRO['account'] and not dry_run: log.error("ABN_AMRO_ACCOUNT not set — cannot submit bridge document") log.error("Set ABN_AMRO_ACCOUNT env var with sandbox account from Bryan") return {'error': 'ABN_AMRO_ACCOUNT not configured'} payload = { # Identifiers 'bankBrand': 'ABN_AMRO', 'transactionId': PROPOSAL['transferId'], 'koreId': PROPOSAL['koreId'], 'uetr': PROPOSAL['uetr'], 'transactionReference': f"VZ-H2H-{int(time.time())}", 'transactionRef': PROPOSAL['transferId'], 'reference': PROPOSAL['transferId'], # Amount 'amount': float(PROPOSAL['amount']), 'currency': PROPOSAL['currency'], 'amountInWords': 'One Thousand United States Dollars', # Source (KoreNet/ABN AMRO side) 'sourceName': 'Vector Zulu LLC', 'sourceAccount': 'VZ-USD-VAULT', 'sourceSwift': 'KNCOZAJJ', 'senderName': 'Vector Zulu LLC', 'senderAccount': 'VZ-USD-VAULT', 'senderBic': 'KNCOZAJJ', # Beneficiary (Wells Fargo via ABN AMRO correspondent) 'beneficiaryName': VZ_BENEFICIARY['name'], 'beneficiaryAccount': VZ_BENEFICIARY['account'], 'beneficiarySwift': VZ_BENEFICIARY['bic'], 'beneficiaryCountry': VZ_BENEFICIARY['country'], 'receiverBic': VZ_BENEFICIARY['bic'], # Settlement 'settlementType': 'SWIFT_GPI', 'purpose': 'VZ H2H treasury transfer — first milestone validation', 'bankSide': 'DEBIT', # Balance placeholders — ABN AMRO will populate 'balanceBefore': 0, 'balanceAfter': 0, # Bank officer — PENDING BRYAN 'bankOfficer': os.environ.get('ABN_AMRO_OFFICER', ''), } # Try candidate submission paths for path in [ '/api/BankBridge/submit', '/api/v1/BankBridge/submit', '/api/BankRegistration/bridge/submit', '/api/v1/bank-bridge/submit', '/api/v1/payments/bridge', ]: result = post(jwt, lick_key, path, payload, dry_run) if dry_run or (result and result.get('error') != 'All endpoints failed'): return result return {'error': 'No valid submission endpoint found — ask Bryan for correct path'} def check_status(jwt, uetr): """Check instruction status by UETR.""" log.info(f"Checking status for UETR: {uetr}") for path in [ f'/api/v1/payment-mirror/{uetr}/status', f'/api/ull/status/{uetr}', f'/api/v1/rails/status/{uetr}', f'/api/BankBridge/status/{uetr}', ]: result = get(jwt, path) if result: log.info(f"Found status at {path}") return result return {'error': 'Status not found on any endpoint'} def health_check(jwt): """Check korebank health.""" for base in [KOREBANK_PRIMARY, KOREBANK_FALLBACK]: try: r = requests.get(f'{base}/health', timeout=8, verify=False) log.info(f"{base}/health: {r.status_code} — {r.text[:100]}") except Exception as e: log.warning(f"{base}: {e}") # ── CLI ─────────────────────────────────────────────────────────────────────── def main(): parser = argparse.ArgumentParser(description='VZ Bank Bridge H2H Submission') parser.add_argument('--dry-run', action='store_true', help='Print payload without submitting') parser.add_argument('--health', action='store_true', help='Check korebank health') parser.add_argument('--register-corridor', action='store_true', help='Register VZ → ABN AMRO corridor') parser.add_argument('--register-nostro', action='store_true', help='Register VZ nostro at ABN AMRO') parser.add_argument('--register-bank', action='store_true', help='Register VZ Wells Fargo beneficiary account') parser.add_argument('--submit', action='store_true', help='Submit BankBridgeDocumentRequest') parser.add_argument('--status', type=str, metavar='UETR', help='Check instruction status by UETR') parser.add_argument('--all', action='store_true', help='Run full registration sequence then submit') args = parser.parse_args() jwt, lick_key = load_secrets() log.info(f"Loaded JWT and LICK key") log.info(f"Primary: {KOREBANK_PRIMARY}") log.info(f"Fallback: {KOREBANK_FALLBACK}") if args.health: health_check(jwt) elif args.register_corridor: result = register_corridor(jwt, lick_key, args.dry_run) print(json.dumps(result, indent=2)) elif args.register_nostro: result = register_nostro(jwt, lick_key, args.dry_run) print(json.dumps(result, indent=2)) elif args.register_bank: result = register_bank_account(jwt, lick_key, args.dry_run) print(json.dumps(result, indent=2)) elif args.submit: result = submit_bridge_document(jwt, lick_key, args.dry_run) print(json.dumps(result, indent=2)) elif args.status: result = check_status(jwt, args.status) print(json.dumps(result, indent=2)) elif args.all: log.info("=== Full H2H registration sequence ===") log.info("Step 1: Register corridor") r1 = register_corridor(jwt, lick_key, args.dry_run) print(json.dumps(r1, indent=2)) log.info("Step 2: Register nostro") r2 = register_nostro(jwt, lick_key, args.dry_run) print(json.dumps(r2, indent=2)) log.info("Step 3: Register bank account") r3 = register_bank_account(jwt, lick_key, args.dry_run) print(json.dumps(r3, indent=2)) log.info("Step 4: Submit bridge document") r4 = submit_bridge_document(jwt, lick_key, args.dry_run) print(json.dumps(r4, indent=2)) else: parser.print_help() if __name__ == '__main__': main()