Add bank_bridge.py — H2H BankBridgeDocumentRequest submission
This commit is contained in:
379
bank_bridge.py
Normal file
379
bank_bridge.py
Normal file
@@ -0,0 +1,379 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user