From abdd8881e2994c3bcbfadcf0688a7e7df86bce72 Mon Sep 17 00:00:00 2001 From: defiQUG Date: Sun, 5 Jul 2026 16:48:36 -0700 Subject: [PATCH] Add Z Bank mainnet payment presets --- .../central-bank/CentralBankDashboard.tsx | 54 +++-- .../ZBankMainnetTransferPanel.tsx | 140 +++++++++++++ .../central-bank/useZBankSettlement.ts | 188 ++++++++++++++++++ .../central-bank/zbank-chain-assets.ts | 48 +++++ packages/settlement-core/src/types.ts | 2 + .../src/adapters/token-aggregation.ts | 16 +- .../dbis-exchange/src/api/routes/exchange.ts | 34 +++- .../src/api/routes/settlement.ts | 2 + .../src/workflows/external-transfer.ts | 2 + 9 files changed, 464 insertions(+), 22 deletions(-) create mode 100644 frontend-dapp/src/features/central-bank/ZBankMainnetTransferPanel.tsx create mode 100644 frontend-dapp/src/features/central-bank/useZBankSettlement.ts create mode 100644 frontend-dapp/src/features/central-bank/zbank-chain-assets.ts diff --git a/frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx b/frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx index d60bdc5..2629546 100644 --- a/frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx +++ b/frontend-dapp/src/features/central-bank/CentralBankDashboard.tsx @@ -1,8 +1,10 @@ +import { useState } from 'react'; import { Link } from 'react-router-dom'; import { useCentralBank } from './useCentralBank'; import ChainMatrixPanel from './ChainMatrixPanel'; +import ZBankMainnetTransferPanel from './ZBankMainnetTransferPanel'; import FineractLedgerBanner from '../omnl-dashboard/FineractLedgerBanner'; @@ -23,6 +25,7 @@ import { formatUsd } from '../../utils/formatMoney'; export default function CentralBankDashboard() { const cb = useCentralBank(); + const [activeTab, setActiveTab] = useState<'overview' | 'payments'>('overview'); const office = cb.office as { @@ -80,6 +83,26 @@ export default function CentralBankDashboard() { +
+ {([ + ['overview', 'Overview'], + ['payments', 'Payments'], + ] as const).map(([value, label]) => ( + + ))} +
+ {cb.error && ( @@ -102,19 +125,17 @@ export default function CentralBankDashboard() { - + ) : ( + <> + - moneySupply={cb.moneySupply} - - loading={cb.loading} - - officeName={office?.name} - - /> - - - -
+

@@ -146,11 +167,11 @@ export default function CentralBankDashboard() { -

+
-
+
@@ -312,11 +333,12 @@ export default function CentralBankDashboard() { -
+
+ + )}
); } - diff --git a/frontend-dapp/src/features/central-bank/ZBankMainnetTransferPanel.tsx b/frontend-dapp/src/features/central-bank/ZBankMainnetTransferPanel.tsx new file mode 100644 index 0000000..f4be742 --- /dev/null +++ b/frontend-dapp/src/features/central-bank/ZBankMainnetTransferPanel.tsx @@ -0,0 +1,140 @@ +import NasaIcon from '../../components/icons/NasaIcon'; + +import { useZBankSettlement } from './useZBankSettlement'; + +export default function ZBankMainnetTransferPanel() { + const settlement = useZBankSettlement(); + + const actionByPreset = { + 'eth-mainnet': settlement.sendEthToMainnet, + 'usdt-mainnet': settlement.sendUsdtToMainnet, + } as const; + + return ( +
+
+
+

+ + Payments → Send to Ethereum mainnet +

+

+ One-click settlement, mint, and bridge presets for Ethereum mainnet. +

+
+ {settlement.status} +
+ +
+ + + +
+ +
+ {settlement.presets.map((preset) => ( +
+
+
+

{preset.buttonLabel}

+

+ {preset.bridgeRail} · {preset.tokenLineId} · targetChainId: {preset.destinationChainId} +

+
+ {preset.amountLabel} +
+ + + + +
+ ))} +
+ + {settlement.error && ( +

+ + {settlement.error} +

+ )} + +
+

Result

+ {settlement.result ? ( +
+
+
Settlement phase
+
{settlement.result.settlement?.phase ?? '—'}
+
+
+
Settlement ID
+
{settlement.result.settlement?.settlementId ?? '—'}
+
+
+
Mint tx
+
{settlement.result.settlement?.chainTxHash ?? '—'}
+
+
+
Bridge tx
+
{settlement.result.bridgeTxHash ?? '—'}
+
+
+
Route
+
+ {settlement.result.executionPlan?.plannerResponse?.selectedRouteReason ?? + settlement.result.executionPlan?.plannerResponse?.routePlan?.legs + ?.map((leg) => leg.provider) + .filter(Boolean) + .join(' → ') ?? + '—'} +
+
+
+ ) : ( +

No transfer submitted yet.

+ )} +
+
+ ); +} diff --git a/frontend-dapp/src/features/central-bank/useZBankSettlement.ts b/frontend-dapp/src/features/central-bank/useZBankSettlement.ts new file mode 100644 index 0000000..dcfca10 --- /dev/null +++ b/frontend-dapp/src/features/central-bank/useZBankSettlement.ts @@ -0,0 +1,188 @@ +import { useEffect, useState } from 'react'; +import { useAccount, useSendTransaction } from 'wagmi'; + +import { omnlApiHeaders, parseAmountToWei, exchangeApi, settlementApi } from '../../config/dex'; + +import { ZBANK_MAINNET_PRESETS, type ZBankMainnetPreset } from './zbank-chain-assets'; + +type SettlementRecord = { + settlementId?: string; + phase?: string; + chainTxHash?: string; + errors?: string[]; +}; + +type ExecutionPlan = { + error?: string; + execution?: { + contractAddress: string; + encodedCalldata: string; + }; + plannerResponse?: { + selectedRouteReason?: string; + routePlan?: { + legs?: Array<{ provider?: string }>; + }; + }; +}; + +type TransferResult = { + presetId: ZBankMainnetPreset['id']; + amount: string; + recipientWallet: string; + creditorIban: string; + settlement?: SettlementRecord; + executionPlan?: ExecutionPlan; + bridgeTxHash?: string; +}; + +export function useZBankSettlement() { + const { address, isConnected } = useAccount(); + const { sendTransactionAsync } = useSendTransaction(); + + const [mainnetWallet, setMainnetWallet] = useState(''); + const [creditorIban, setCreditorIban] = useState(''); + const [amounts, setAmounts] = useState>({ + 'eth-mainnet': '', + 'usdt-mainnet': '', + }); + const [loadingPreset, setLoadingPreset] = useState(null); + const [status, setStatus] = useState('Idle'); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + if (address && !mainnetWallet) { + setMainnetWallet(address); + } + }, [address, mainnetWallet]); + + async function postJson(url: string, body: Record): Promise { + const res = await fetch(url, { + method: 'POST', + headers: omnlApiHeaders(), + body: JSON.stringify(body), + }); + const data = (await res.json()) as T & { error?: string }; + if (!res.ok || data.error) { + throw new Error(data.error || `Request failed: ${res.status}`); + } + return data; + } + + async function runPreset(preset: ZBankMainnetPreset) { + const amount = amounts[preset.id]?.trim(); + if (!isConnected || !address) { + setError('Connect wallet before sending to mainnet.'); + return; + } + if (!mainnetWallet.trim()) { + setError('Ethereum mainnet wallet is required.'); + return; + } + if (!creditorIban.trim()) { + setError('Z Bank IBAN is required.'); + return; + } + if (!amount) { + setError(`${preset.amountLabel} amount is required.`); + return; + } + + setLoadingPreset(preset.id); + setStatus('Posting settlement'); + setError(null); + setResult(null); + + try { + const settlement = await postJson(settlementApi('/convert/fiat-to-crypto'), { + idempotencyKey: `ZB-${preset.id}-${Date.now()}`, + valueDate: new Date().toISOString().slice(0, 10), + currency: preset.currency, + amount, + creditorIban: creditorIban.trim(), + beneficiaryName: 'Z Bank Mainnet Transfer', + remittanceInfo: `${preset.buttonLabel} → ${mainnetWallet.trim()}`, + convertToCrypto: { + enabled: true, + targetChainId: preset.destinationChainId, + tokenLineId: preset.tokenLineId, + recipientAddress: address, + symbol: preset.sourceSymbol, + tokenAddress: preset.sourceTokenAddress, + }, + }); + + setResult({ + presetId: preset.id, + amount, + recipientWallet: mainnetWallet.trim(), + creditorIban: creditorIban.trim(), + settlement, + }); + + setStatus('Building bridge execution'); + const executionPlan = await postJson(exchangeApi('/execute-plan'), { + tokenIn: preset.sourceTokenAddress, + tokenOut: preset.destinationTokenAddress, + amountIn: parseAmountToWei(amount, preset.sourceDecimals), + recipient: mainnetWallet.trim(), + destinationChainId: preset.destinationChainId, + allowBridge: true, + preferredBridges: [preset.preferredBridgeLabel], + }); + + if (!executionPlan.execution) { + throw new Error(executionPlan.error || 'Bridge execution plan missing calldata.'); + } + + setResult((current) => + current + ? { + ...current, + executionPlan, + } + : current, + ); + + setStatus('Submitting bridge transaction'); + const bridgeTxHash = await sendTransactionAsync({ + to: executionPlan.execution.contractAddress as `0x${string}`, + data: executionPlan.execution.encodedCalldata as `0x${string}`, + }); + + setResult((current) => + current + ? { + ...current, + bridgeTxHash, + } + : current, + ); + setStatus('Submitted'); + } catch (err) { + setError(err instanceof Error ? err.message : 'Mainnet transfer failed'); + setStatus('Failed'); + } finally { + setLoadingPreset(null); + } + } + + return { + address, + isConnected, + mainnetWallet, + setMainnetWallet, + creditorIban, + setCreditorIban, + amounts, + setAmounts, + loadingPreset, + status, + result, + error, + presets: ZBANK_MAINNET_PRESETS, + sendEthToMainnet: () => runPreset(ZBANK_MAINNET_PRESETS[0]), + sendUsdtToMainnet: () => runPreset(ZBANK_MAINNET_PRESETS[1]), + }; +} diff --git a/frontend-dapp/src/features/central-bank/zbank-chain-assets.ts b/frontend-dapp/src/features/central-bank/zbank-chain-assets.ts new file mode 100644 index 0000000..446c930 --- /dev/null +++ b/frontend-dapp/src/features/central-bank/zbank-chain-assets.ts @@ -0,0 +1,48 @@ +export type ZBankMainnetPreset = { + id: 'eth-mainnet' | 'usdt-mainnet'; + buttonLabel: string; + amountLabel: string; + sourceSymbol: string; + sourceTokenAddress: `0x${string}`; + sourceDecimals: number; + destinationSymbol: string; + destinationTokenAddress: `0x${string}`; + destinationChainId: 1; + bridgeRail: string; + preferredBridgeLabel: string; + tokenLineId: string; + currency: string; +}; + +export const ZBANK_MAINNET_PRESETS: ZBankMainnetPreset[] = [ + { + id: 'eth-mainnet', + buttonLabel: 'Send ETH → Mainnet', + amountLabel: 'ETH', + sourceSymbol: 'WETH', + sourceTokenAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + sourceDecimals: 18, + destinationSymbol: 'WETH', + destinationTokenAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + destinationChainId: 1, + bridgeRail: 'ccip-weth9', + preferredBridgeLabel: 'CCIPWETH9Bridge', + tokenLineId: 'WETH-M2', + currency: 'USD', + }, + { + id: 'usdt-mainnet', + buttonLabel: 'Send USDT → Mainnet', + amountLabel: 'USDT', + sourceSymbol: 'USDT', + sourceTokenAddress: '0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1', + sourceDecimals: 6, + destinationSymbol: 'USDT', + destinationTokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + destinationChainId: 1, + bridgeRail: 'trustless-lockbox', + preferredBridgeLabel: 'GRUTransport', + tokenLineId: 'USD-M2', + currency: 'USD', + }, +]; diff --git a/packages/settlement-core/src/types.ts b/packages/settlement-core/src/types.ts index 488c4a7..ed6d535 100644 --- a/packages/settlement-core/src/types.ts +++ b/packages/settlement-core/src/types.ts @@ -57,6 +57,8 @@ export type ExternalTransferRequest = { targetChainId: number; tokenLineId: string; recipientAddress: string; + symbol?: string; + tokenAddress?: string; }; }; diff --git a/services/dbis-exchange/src/adapters/token-aggregation.ts b/services/dbis-exchange/src/adapters/token-aggregation.ts index 187fa5f..b1b4dc7 100644 --- a/services/dbis-exchange/src/adapters/token-aggregation.ts +++ b/services/dbis-exchange/src/adapters/token-aggregation.ts @@ -26,6 +26,8 @@ export async function fetchRoutePlan(body: { amountIn: string; recipient?: string; maxSlippageBps?: number; + allowBridge?: boolean; + preferredBridges?: string[]; }) { const { data } = await axios.post( `${tokenAggUrl()}/api/v2/routes/plan`, @@ -36,7 +38,11 @@ export async function fetchRoutePlan(body: { tokenOut: body.tokenOut, amountIn: body.amountIn, recipient: body.recipient, - constraints: { maxSlippageBps: body.maxSlippageBps ?? 50, allowBridge: false }, + constraints: { + maxSlippageBps: body.maxSlippageBps ?? 50, + allowBridge: body.allowBridge ?? false, + preferredBridges: body.preferredBridges, + }, }, { timeout: 120000 }, ); @@ -51,6 +57,8 @@ export async function fetchExecutionPlan(body: { amountIn: string; recipient: string; maxSlippageBps?: number; + allowBridge?: boolean; + preferredBridges?: string[]; }) { const { data } = await axios.post( `${tokenAggUrl()}/api/v2/routes/internal-execution-plan`, @@ -61,7 +69,11 @@ export async function fetchExecutionPlan(body: { tokenOut: body.tokenOut, amountIn: body.amountIn, recipient: body.recipient, - constraints: { maxSlippageBps: body.maxSlippageBps ?? 50, allowBridge: false }, + constraints: { + maxSlippageBps: body.maxSlippageBps ?? 50, + allowBridge: body.allowBridge ?? false, + preferredBridges: body.preferredBridges, + }, }, { timeout: 120000 }, ); diff --git a/services/dbis-exchange/src/api/routes/exchange.ts b/services/dbis-exchange/src/api/routes/exchange.ts index 51d1f97..c8756e1 100644 --- a/services/dbis-exchange/src/api/routes/exchange.ts +++ b/services/dbis-exchange/src/api/routes/exchange.ts @@ -106,19 +106,32 @@ export function createExchangeRouter(): Router { router.post('/plan', async (req, res) => { if (!requireTrade(req, res)) return; try { - const { tokenIn, tokenOut, amountIn, recipient, maxSlippageBps } = req.body as Record; + const { + tokenIn, + tokenOut, + amountIn, + recipient, + maxSlippageBps, + destinationChainId, + allowBridge, + preferredBridges, + } = req.body as Record; if (!tokenIn || !tokenOut || !amountIn) { res.status(400).json({ error: 'tokenIn, tokenOut, amountIn required' }); return; } const plan = await fetchRoutePlan({ sourceChainId: cfg.chainId, - destinationChainId: cfg.chainId, + destinationChainId: destinationChainId ? Number(destinationChainId) : cfg.chainId, tokenIn: String(tokenIn), tokenOut: String(tokenOut), amountIn: String(amountIn), recipient: recipient ? String(recipient) : undefined, maxSlippageBps: maxSlippageBps ? Number(maxSlippageBps) : cfg.defaultSlippageBps, + allowBridge: typeof allowBridge === 'boolean' ? allowBridge : undefined, + preferredBridges: Array.isArray(preferredBridges) + ? preferredBridges.map((value) => String(value)) + : undefined, }); res.json(plan); } catch (e) { @@ -129,19 +142,32 @@ export function createExchangeRouter(): Router { router.post('/execute-plan', async (req, res) => { if (!requireTrade(req, res)) return; try { - const { tokenIn, tokenOut, amountIn, recipient, maxSlippageBps } = req.body as Record; + const { + tokenIn, + tokenOut, + amountIn, + recipient, + maxSlippageBps, + destinationChainId, + allowBridge, + preferredBridges, + } = req.body as Record; if (!tokenIn || !tokenOut || !amountIn || !recipient) { res.status(400).json({ error: 'tokenIn, tokenOut, amountIn, recipient required' }); return; } const plan = await fetchExecutionPlan({ sourceChainId: cfg.chainId, - destinationChainId: cfg.chainId, + destinationChainId: destinationChainId ? Number(destinationChainId) : cfg.chainId, tokenIn: String(tokenIn), tokenOut: String(tokenOut), amountIn: String(amountIn), recipient: String(recipient), maxSlippageBps: maxSlippageBps ? Number(maxSlippageBps) : cfg.defaultSlippageBps, + allowBridge: typeof allowBridge === 'boolean' ? allowBridge : undefined, + preferredBridges: Array.isArray(preferredBridges) + ? preferredBridges.map((value) => String(value)) + : undefined, }); res.json(plan); } catch (e) { diff --git a/services/settlement-middleware/src/api/routes/settlement.ts b/services/settlement-middleware/src/api/routes/settlement.ts index b410a74..e0e3cac 100644 --- a/services/settlement-middleware/src/api/routes/settlement.ts +++ b/services/settlement-middleware/src/api/routes/settlement.ts @@ -224,6 +224,8 @@ export function createSettlementRouter(): Router { targetChainId: body.convertToCrypto?.targetChainId ?? 138, tokenLineId: body.convertToCrypto?.tokenLineId ?? `${body.currency ?? 'USD'}-M2`, recipientAddress: body.convertToCrypto?.recipientAddress ?? '', + symbol: body.convertToCrypto?.symbol, + tokenAddress: body.convertToCrypto?.tokenAddress, }, moneyLayers: ['M2', 'M3'], }); diff --git a/services/settlement-middleware/src/workflows/external-transfer.ts b/services/settlement-middleware/src/workflows/external-transfer.ts index dd5f437..4dd0043 100644 --- a/services/settlement-middleware/src/workflows/external-transfer.ts +++ b/services/settlement-middleware/src/workflows/external-transfer.ts @@ -160,6 +160,8 @@ export async function processExternalTransfer( recipient: req.convertToCrypto.recipientAddress, settlementRef: record.settlementId, dryRun: !cfg.production.allowChainMintExecute, + symbol: req.convertToCrypto.symbol, + tokenAddress: req.convertToCrypto.tokenAddress, }); advance('CHAIN_MINT_REQUESTED', { chainTxHash: mint.txHash }); } else {