380 lines
15 KiB
Python
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()
|