Files
vz-kore-adapter/bank_bridge.py

380 lines
15 KiB
Python

#!/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()