From 1771db2190343b223888e14f8155217a10ea3f4a Mon Sep 17 00:00:00 2001 From: defiQUG Date: Sat, 28 Mar 2026 15:38:51 -0700 Subject: [PATCH] Add optional Chain 2138 frontend support --- frontend-dapp/.env.example | 9 ++ frontend-dapp/QUICK_START.md | 10 +++ frontend-dapp/README.md | 16 +++- .../components/admin/FunctionPermissions.tsx | 2 +- .../src/components/admin/MultiChainAdmin.tsx | 5 +- .../src/components/admin/OffChainServices.tsx | 39 +++++---- .../components/admin/PaymentChannelAdmin.tsx | 7 +- .../src/components/admin/PaymentChannels.tsx | 12 ++- .../src/components/admin/RealtimeMonitor.tsx | 30 +++++-- .../src/components/admin/StateChannels.tsx | 7 +- .../ChainManagementDashboard.tsx | 19 +++-- .../src/components/bridge/BridgeButtons.tsx | 29 ++++--- .../bridge/SwapBridgeSwapQuoteForm.tsx | 3 +- .../bridge/ThirdwebBridgeWidget.tsx | 11 ++- .../components/bridge/TrustlessBridgeForm.tsx | 83 ++++++++++++------- .../src/components/layout/Layout.tsx | 5 +- .../src/components/wallet/WalletConnect.tsx | 17 ++-- .../src/config/__tests__/networks.test.ts | 32 +++++++ frontend-dapp/src/config/bridge.ts | 7 ++ frontend-dapp/src/config/contracts.ts | 23 +++-- frontend-dapp/src/config/networks.ts | 22 +++++ frontend-dapp/src/config/trustlessL2.ts | 55 ++++++++++++ frontend-dapp/src/config/wagmi.ts | 35 ++------ frontend-dapp/src/pages/AdminPanel.tsx | 7 +- frontend-dapp/src/pages/BridgePage.tsx | 13 ++- frontend-dapp/src/pages/WalletsDemoPage.tsx | 51 ++++-------- frontend-dapp/src/utils/ens.ts | 15 +++- frontend-dapp/src/utils/rateLimiter.ts | 8 +- frontend-dapp/src/vite-env.d.ts | 7 ++ frontend-dapp/tsconfig.json | 10 ++- .../phase1/config/env.chain2138.example | 6 ++ 31 files changed, 408 insertions(+), 187 deletions(-) create mode 100644 frontend-dapp/src/config/__tests__/networks.test.ts create mode 100644 frontend-dapp/src/config/trustlessL2.ts diff --git a/frontend-dapp/.env.example b/frontend-dapp/.env.example index 80269fd..0e4cc28 100644 --- a/frontend-dapp/.env.example +++ b/frontend-dapp/.env.example @@ -12,6 +12,15 @@ VITE_RPC_URL_138=https://rpc-http-pub.d-bis.org # VITE_ENABLE_CHAIN2138=true # VITE_RPC_URL_2138=https://rpc.public-2138.defi-oracle.io # VITE_EXPLORER_URL_2138=https://public-2138.defi-oracle.io +# Trustless bridge: L2 lockbox side β€” default 138; use 2138 for testnet (requires ENABLE_CHAIN2138) +# VITE_TRUSTLESS_L2_CHAIN_ID=138 +# VITE_LOCKBOX_2138=0x... +# VITE_WETH_CHAIN2138=0x... +# VITE_CUSDT_CHAIN2138=0x... +# VITE_CUSDC_CHAIN2138=0x... +# VITE_TRANSACTION_MIRROR_CHAIN2138=0x... +# Optional: set the preferred wallet/switch target to 2138 when the flag above is enabled +# VITE_DEFAULT_FRONTEND_CHAIN_ID=2138 # Optional Environment Variables VITE_ETHERSCAN_API_KEY=YourApiKeyToken diff --git a/frontend-dapp/QUICK_START.md b/frontend-dapp/QUICK_START.md index 3234a2d..53c3bbe 100644 --- a/frontend-dapp/QUICK_START.md +++ b/frontend-dapp/QUICK_START.md @@ -22,6 +22,10 @@ cp .env.example .env.local # - VITE_WALLETCONNECT_PROJECT_ID (get from https://cloud.walletconnect.com) # - VITE_THIRDWEB_CLIENT_ID (get from https://thirdweb.com/dashboard) # - VITE_RPC_URL_138 (your Chain 138 RPC endpoint) +# Optional: +# - VITE_ENABLE_CHAIN2138=true +# - VITE_RPC_URL_2138 / VITE_EXPLORER_URL_2138 +# - VITE_DEFAULT_FRONTEND_CHAIN_ID=2138 ``` ### Step 3: Start Development Server @@ -39,6 +43,11 @@ Minimum required variables for development: VITE_WALLETCONNECT_PROJECT_ID=your_project_id VITE_THIRDWEB_CLIENT_ID=your_client_id VITE_RPC_URL_138=http://192.168.11.250:8545 +# Optional: +# VITE_ENABLE_CHAIN2138=true +# VITE_RPC_URL_2138=https://rpc.public-2138.defi-oracle.io +# VITE_EXPLORER_URL_2138=https://public-2138.defi-oracle.io +# VITE_DEFAULT_FRONTEND_CHAIN_ID=2138 ``` ## 🎯 First Time Setup Checklist @@ -75,6 +84,7 @@ VITE_RPC_URL_138=http://192.168.11.250:8545 ### Build Errors - Clear cache: `rm -rf node_modules/.vite` - Reinstall: `rm -rf node_modules && pnpm install` +- Run `pnpm run build:check` to verify both typecheck and production build --- diff --git a/frontend-dapp/README.md b/frontend-dapp/README.md index eec110b..1aa64db 100644 --- a/frontend-dapp/README.md +++ b/frontend-dapp/README.md @@ -23,14 +23,24 @@ The app will be available at `http://localhost:3002` - DEX swap interface - Reserve status and peg monitoring - Transaction history +- Optional Chain 2138 frontend wallet/network support behind env flags ## Environment Variables -Create a `.env` file: +Copy `.env.example` to `.env.local` and set the values you need: ``` VITE_WALLETCONNECT_PROJECT_ID=your_project_id -VITE_BRIDGE_CONTRACT_ADDRESS=0x... -VITE_RESERVE_CONTRACT_ADDRESS=0x... +VITE_THIRDWEB_CLIENT_ID=your_client_id +VITE_RPC_URL_138=https://rpc-http-pub.d-bis.org +# Optional Chain 2138 frontend support +# VITE_ENABLE_CHAIN2138=true +# VITE_RPC_URL_2138=https://rpc.public-2138.defi-oracle.io +# VITE_EXPLORER_URL_2138=https://public-2138.defi-oracle.io +# VITE_DEFAULT_FRONTEND_CHAIN_ID=2138 ``` +Notes: +- The shared network source of truth lives in `src/config/networks.ts`. +- `VITE_ENABLE_CHAIN2138` only enables optional frontend wallet/network flows. +- Trustless bridge and Chain 138-specific operational flows remain pinned to Chain 138 unless explicitly expanded. diff --git a/frontend-dapp/src/components/admin/FunctionPermissions.tsx b/frontend-dapp/src/components/admin/FunctionPermissions.tsx index 55e9e35..b33af3d 100644 --- a/frontend-dapp/src/components/admin/FunctionPermissions.tsx +++ b/frontend-dapp/src/components/admin/FunctionPermissions.tsx @@ -57,7 +57,7 @@ const CONTRACT_FUNCTIONS = { export default function FunctionPermissions() { const { address } = useAccount() const { addAuditLog } = useAdmin() - const [roles, setRoles] = useState(DEFAULT_ROLES) + const [roles] = useState(DEFAULT_ROLES) const [permissions, setPermissions] = useState([]) const [selectedContract, setSelectedContract] = useState(CONTRACT_ADDRESSES.mainnet.MAINNET_TETHER) const [selectedRole, setSelectedRole] = useState('operator') diff --git a/frontend-dapp/src/components/admin/MultiChainAdmin.tsx b/frontend-dapp/src/components/admin/MultiChainAdmin.tsx index be458d7..119ab95 100644 --- a/frontend-dapp/src/components/admin/MultiChainAdmin.tsx +++ b/frontend-dapp/src/components/admin/MultiChainAdmin.tsx @@ -6,6 +6,7 @@ import { useState } from 'react' import { useChainId, useSwitchChain } from 'wagmi' import { CONTRACT_ADDRESSES } from '../../config/contracts' import toast from 'react-hot-toast' +import { chain138 } from '../../config/networks' interface ChainConfig { chainId: number @@ -26,8 +27,8 @@ const CHAIN_CONFIGS: ChainConfig[] = [ }, }, { - chainId: 138, - name: 'Chain 138', + chainId: chain138.id, + name: chain138.name, contractAddresses: {}, }, ] diff --git a/frontend-dapp/src/components/admin/OffChainServices.tsx b/frontend-dapp/src/components/admin/OffChainServices.tsx index e7e72f8..ffa2298 100644 --- a/frontend-dapp/src/components/admin/OffChainServices.tsx +++ b/frontend-dapp/src/components/admin/OffChainServices.tsx @@ -4,6 +4,7 @@ import { useState, useEffect } from 'react' import { CONTRACT_ADDRESSES } from '../../config/contracts' +import { chain138 } from '../../config/networks' interface ServiceStatus { name: string @@ -12,21 +13,23 @@ interface ServiceStatus { endpoint?: string } +const INITIAL_SERVICES: ServiceStatus[] = [ + { + name: 'State Anchoring Service', + status: 'unknown', + lastUpdate: null, + endpoint: 'http://192.168.11.221:8545', // Chain 138 RPC (VMID 2201, public/monitoring) + }, + { + name: 'Transaction Mirroring Service', + status: 'unknown', + lastUpdate: null, + endpoint: 'http://192.168.11.221:8545', + }, +] + export default function OffChainServices() { - const [services, setServices] = useState([ - { - name: 'State Anchoring Service', - status: 'unknown', - lastUpdate: null, - endpoint: 'http://192.168.11.221:8545', // Chain 138 RPC (VMID 2201, public/monitoring) - }, - { - name: 'Transaction Mirroring Service', - status: 'unknown', - lastUpdate: null, - endpoint: 'http://192.168.11.221:8545', - }, - ]) + const [services, setServices] = useState(INITIAL_SERVICES) const checkServiceStatus = async (service: ServiceStatus): Promise => { if (!service.endpoint) { @@ -86,7 +89,7 @@ export default function OffChainServices() { status: 'stopped' as 'running' | 'stopped' | 'unknown', lastUpdate: Date.now(), } - } catch (error: any) { + } catch (error: unknown) { // Timeout or network error console.error(`Health check failed for ${service.name}:`, error) return { @@ -99,7 +102,7 @@ export default function OffChainServices() { useEffect(() => { const checkAllServices = async () => { - const updated = await Promise.all(services.map(checkServiceStatus)) + const updated = await Promise.all(INITIAL_SERVICES.map(checkServiceStatus)) setServices(updated) } @@ -174,7 +177,7 @@ export default function OffChainServices() {

State Anchoring Service

- Monitors ChainID 138 blocks and submits state proofs to MainnetTether contract. + Monitors {chain138.name} blocks and submits state proofs to MainnetTether contract.

Contract: {CONTRACT_ADDRESSES.mainnet.MAINNET_TETHER} @@ -183,7 +186,7 @@ export default function OffChainServices() {

Transaction Mirroring Service

- Monitors ChainID 138 transactions and mirrors them to TransactionMirror contract. + Monitors {chain138.name} transactions and mirrors them to TransactionMirror contract.

Contract: {CONTRACT_ADDRESSES.mainnet.TRANSACTION_MIRROR} diff --git a/frontend-dapp/src/components/admin/PaymentChannelAdmin.tsx b/frontend-dapp/src/components/admin/PaymentChannelAdmin.tsx index 545773f..0d0ff86 100644 --- a/frontend-dapp/src/components/admin/PaymentChannelAdmin.tsx +++ b/frontend-dapp/src/components/admin/PaymentChannelAdmin.tsx @@ -4,10 +4,12 @@ import { useReadContract, useWriteContract, useWaitForTransactionReceipt } from import { CONTRACT_ADDRESSES } from '../../config/contracts' import { PAYMENT_CHANNEL_MANAGER_ABI } from '../../abis/PaymentChannelManager' import toast from 'react-hot-toast' +import { chain138, chain2138Testnet } from '../../config/networks' function getManagerAddress(chainId: number): `0x${string}` | undefined { if (chainId === 1) return CONTRACT_ADDRESSES.mainnet.PAYMENT_CHANNEL_MANAGER as `0x${string}` | undefined if (chainId === 138) return CONTRACT_ADDRESSES.chain138.PAYMENT_CHANNEL_MANAGER as `0x${string}` | undefined + if (chainId === 2138) return CONTRACT_ADDRESSES.chain2138.PAYMENT_CHANNEL_MANAGER as `0x${string}` | undefined return undefined } @@ -80,7 +82,10 @@ export default function PaymentChannelAdmin() { if (managerAddress == null) { return (

-

Payment channel manager not deployed on this chain. Set PAYMENT_CHANNEL_MANAGER in config.

+

+ Payment channel manager not deployed on this chain. Set `PAYMENT_CHANNEL_MANAGER` + in config for Ethereum Mainnet, {chain138.name}, or {chain2138Testnet.name}. +

) } diff --git a/frontend-dapp/src/components/admin/PaymentChannels.tsx b/frontend-dapp/src/components/admin/PaymentChannels.tsx index 4ef31f8..6afa63a 100644 --- a/frontend-dapp/src/components/admin/PaymentChannels.tsx +++ b/frontend-dapp/src/components/admin/PaymentChannels.tsx @@ -4,12 +4,14 @@ import { useReadContract, useWriteContract, useWaitForTransactionReceipt } from import { CONTRACT_ADDRESSES } from '../../config/contracts' import { PAYMENT_CHANNEL_MANAGER_ABI } from '../../abis/PaymentChannelManager' import toast from 'react-hot-toast' +import { chain138, chain2138Testnet } from '../../config/networks' const CHANNEL_STATUS = ['None', 'Open', 'Dispute', 'Closed'] as const function getManagerAddress(chainId: number): `0x${string}` | undefined { if (chainId === 1) return CONTRACT_ADDRESSES.mainnet.PAYMENT_CHANNEL_MANAGER as `0x${string}` | undefined if (chainId === 138) return CONTRACT_ADDRESSES.chain138.PAYMENT_CHANNEL_MANAGER as `0x${string}` | undefined + if (chainId === 2138) return CONTRACT_ADDRESSES.chain2138.PAYMENT_CHANNEL_MANAGER as `0x${string}` | undefined return undefined } @@ -48,7 +50,7 @@ export default function PaymentChannels() { }) const { writeContract, data: hash, isPending } = useWriteContract() - const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash }) + const { isLoading: isConfirming } = useWaitForTransactionReceipt({ hash }) const channelIds = useMemo(() => { const n = Number(channelCount) @@ -192,7 +194,10 @@ export default function PaymentChannels() { if (managerAddress == null) { return (
-

Payment channel manager not deployed on this chain. Set PAYMENT_CHANNEL_MANAGER in config for Mainnet (1) or Chain 138.

+

+ Payment channel manager not deployed on this chain. Set `PAYMENT_CHANNEL_MANAGER` + in config for Ethereum Mainnet, {chain138.name}, or {chain2138Testnet.name}. +

) } @@ -205,7 +210,7 @@ export default function PaymentChannels() {
Contract is paused. Only close/finalize allowed.
)}

- Open a channel with a counterparty, fund it (optional second side), then close cooperatively or via dispute window. On Chain 138, channel txs are mirrored to Mainnet. + Open a channel with a counterparty, fund it (optional second side), then close cooperatively or via dispute window. On {chain138.name}, channel transactions are mirrored to Mainnet.

Payment channels (above) = state channels for payments. For cross-chain or routed payments, general state channels, or channel networks:

@@ -382,7 +387,6 @@ function ChannelRow({ const status = Number(ch.status) const statusLabel = CHANNEL_STATUS[status] ?? 'Unknown' const isMine = currentUser && (ch.participantA.toLowerCase() === currentUser.toLowerCase() || ch.participantB.toLowerCase() === currentUser.toLowerCase()) - const total = ch.depositA + ch.depositB return (
diff --git a/frontend-dapp/src/components/admin/RealtimeMonitor.tsx b/frontend-dapp/src/components/admin/RealtimeMonitor.tsx index 23f3ca9..5330a1b 100644 --- a/frontend-dapp/src/components/admin/RealtimeMonitor.tsx +++ b/frontend-dapp/src/components/admin/RealtimeMonitor.tsx @@ -26,12 +26,16 @@ export default function RealtimeMonitor() { useEffect(() => { if (monitoring && publicClient) { const monitor = getRealtimeMonitor(wsUrl || undefined) + let isMounted = true + let stopMainnetTether: (() => void) | null = null + let stopTransactionMirror: (() => void) | null = null // Start monitoring MainnetTether - const stopMainnetTether = monitorContractState( + monitorContractState( CONTRACT_ADDRESSES.mainnet.MAINNET_TETHER, publicClient, (state) => { + if (!isMounted) return setContractStates((prev) => ({ ...prev, [CONTRACT_ADDRESSES.mainnet.MAINNET_TETHER]: { @@ -41,13 +45,18 @@ export default function RealtimeMonitor() { }, })) } - ) + ).then((unsubscribe) => { + stopMainnetTether = unsubscribe + }).catch(() => { + stopMainnetTether = null + }) // Start monitoring TransactionMirror - const stopTransactionMirror = monitorContractState( + monitorContractState( CONTRACT_ADDRESSES.mainnet.TRANSACTION_MIRROR, publicClient, (state) => { + if (!isMounted) return setContractStates((prev) => ({ ...prev, [CONTRACT_ADDRESSES.mainnet.TRANSACTION_MIRROR]: { @@ -57,7 +66,11 @@ export default function RealtimeMonitor() { }, })) } - ) + ).then((unsubscribe) => { + stopTransactionMirror = unsubscribe + }).catch(() => { + stopTransactionMirror = null + }) // Subscribe to events (async handling) let unsubscribeBlocks: (() => void) | null = null @@ -79,9 +92,10 @@ export default function RealtimeMonitor() { } return () => { - stopMainnetTether() - stopTransactionMirror() - if (unsubscribeBlocks) unsubscribeBlocks() + isMounted = false + stopMainnetTether?.() + stopTransactionMirror?.() + unsubscribeBlocks?.() } } }, [monitoring, publicClient, chainId, wsUrl]) @@ -222,7 +236,7 @@ export default function RealtimeMonitor() { {!monitoring && (

- Click "Start Monitoring" to begin real-time monitoring of contract states and events. + Click "Start Monitoring" to begin real-time monitoring of contract states and events.

)} diff --git a/frontend-dapp/src/components/admin/StateChannels.tsx b/frontend-dapp/src/components/admin/StateChannels.tsx index 0aed262..4cf2e5b 100644 --- a/frontend-dapp/src/components/admin/StateChannels.tsx +++ b/frontend-dapp/src/components/admin/StateChannels.tsx @@ -4,12 +4,14 @@ import { useReadContract, useWriteContract, useWaitForTransactionReceipt } from import { CONTRACT_ADDRESSES } from '../../config/contracts' import { GENERIC_STATE_CHANNEL_MANAGER_ABI } from '../../abis/GenericStateChannelManager' import toast from 'react-hot-toast' +import { chain138, chain2138Testnet } from '../../config/networks' const CHANNEL_STATUS = ['None', 'Open', 'Dispute', 'Closed'] as const function getManagerAddress(chainId: number): `0x${string}` | undefined { if (chainId === 1) return CONTRACT_ADDRESSES.mainnet.GENERIC_STATE_CHANNEL_MANAGER as `0x${string}` | undefined if (chainId === 138) return CONTRACT_ADDRESSES.chain138.GENERIC_STATE_CHANNEL_MANAGER as `0x${string}` | undefined + if (chainId === 2138) return CONTRACT_ADDRESSES.chain2138.GENERIC_STATE_CHANNEL_MANAGER as `0x${string}` | undefined return undefined } @@ -105,7 +107,10 @@ export default function StateChannels() { if (managerAddress == null) { return (
-

Generic state channel manager not deployed on this chain. Set GENERIC_STATE_CHANNEL_MANAGER in config for Mainnet (1) or Chain 138.

+

+ Generic state channel manager not deployed on this chain. Set `GENERIC_STATE_CHANNEL_MANAGER` + in config for Ethereum Mainnet, {chain138.name}, or {chain2138Testnet.name}. +

) } diff --git a/frontend-dapp/src/components/admin/chain-management/ChainManagementDashboard.tsx b/frontend-dapp/src/components/admin/chain-management/ChainManagementDashboard.tsx index 0175eb1..77414d6 100644 --- a/frontend-dapp/src/components/admin/chain-management/ChainManagementDashboard.tsx +++ b/frontend-dapp/src/components/admin/chain-management/ChainManagementDashboard.tsx @@ -5,7 +5,6 @@ import { useState, useEffect } from 'react'; import { useAccount } from 'wagmi'; -import { ethers } from 'ethers'; import toast from 'react-hot-toast'; interface ChainMetadata { @@ -19,6 +18,10 @@ interface ChainMetadata { avgBlockTime: number; } +function getErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : 'Unknown error'; +} + export default function ChainManagementDashboard() { const { address, isConnected } = useAccount(); const [chains, setChains] = useState([]); @@ -63,20 +66,20 @@ export default function ChainManagementDashboard() { avgBlockTime: 2 } ]); - } catch (error: any) { - toast.error(`Failed to load chains: ${error.message}`); + } catch (error: unknown) { + toast.error(`Failed to load chains: ${getErrorMessage(error)}`); } finally { setLoading(false); } }; - const toggleChain = async (chainId: number, chainIdentifier: string, currentStatus: boolean) => { + const toggleChain = async (currentStatus: boolean) => { try { // TODO: Call ChainRegistry.setChainActive() toast.success(`Chain ${currentStatus ? 'disabled' : 'enabled'}`); loadChains(); - } catch (error: any) { - toast.error(`Failed to toggle chain: ${error.message}`); + } catch (error: unknown) { + toast.error(`Failed to toggle chain: ${getErrorMessage(error)}`); } }; @@ -122,7 +125,7 @@ export default function ChainManagementDashboard() { {chain.isActive ? 'Active' : 'Inactive'} )}
@@ -265,13 +283,16 @@ export default function TrustlessBridgeForm() { Deposit type:
+ {depositType === 'erc20' && LOCKBOX_ERC20_OPTIONS.length === 0 && ( +

No ERC-20 tokens configured for this L2 (set non-zero addresses in env).

+ )} {depositType === 'erc20' && LOCKBOX_ERC20_OPTIONS.length > 0 && (
@@ -303,7 +324,7 @@ export default function TrustlessBridgeForm() { id="trustless-bridge-amount" name="amount" type="text" - placeholder={depositType === 'native' ? 'Amount (ETH)' : 'Amount (token units)'} + placeholder={depositType === 'native' ? `Amount (${nativeSymbol})` : 'Amount (token units)'} value={amount} onChange={(e) => setAmount(e.target.value)} className="w-full px-3 py-2 rounded-lg bg-white/10 text-white border border-cyan-400/30" @@ -311,11 +332,11 @@ export default function TrustlessBridgeForm() { {depositType === 'native' && ( )} {depositType === 'erc20' && ( @@ -323,7 +344,7 @@ export default function TrustlessBridgeForm() { {tokenAllowance !== undefined && BigInt(amount || 0) * 10n ** 18n > (tokenAllowance ?? 0n) && ( )} diff --git a/frontend-dapp/src/config/__tests__/networks.test.ts b/frontend-dapp/src/config/__tests__/networks.test.ts new file mode 100644 index 0000000..9046a4d --- /dev/null +++ b/frontend-dapp/src/config/__tests__/networks.test.ts @@ -0,0 +1,32 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +const loadNetworksModule = async () => import('../networks') + +describe('frontend network config', () => { + afterEach(() => { + vi.unstubAllEnvs() + vi.resetModules() + }) + + it('keeps Chain 2138 disabled by default', async () => { + const networks = await loadNetworksModule() + + expect(networks.chain2138TestnetEnabled).toBe(false) + expect(networks.frontendSourceChainIds).toEqual([138]) + expect(networks.defaultFrontendChainId).toBe(138) + }) + + it('enables Chain 2138 and promotes it to the default frontend chain when configured', async () => { + vi.stubEnv('VITE_ENABLE_CHAIN2138', 'true') + vi.stubEnv('VITE_DEFAULT_FRONTEND_CHAIN_ID', '2138') + vi.stubEnv('VITE_RPC_URL_2138', 'https://rpc.public-2138.defi-oracle.io') + vi.stubEnv('VITE_EXPLORER_URL_2138', 'https://public-2138.defi-oracle.io') + + const networks = await loadNetworksModule() + + expect(networks.chain2138TestnetEnabled).toBe(true) + expect(networks.frontendSourceChainIds).toEqual([138, 2138]) + expect(networks.defaultFrontendChainId).toBe(2138) + expect(networks.defaultFrontendChainName).toBe('Defi Oracle Meta Testnet') + }) +}) diff --git a/frontend-dapp/src/config/bridge.ts b/frontend-dapp/src/config/bridge.ts index 84d1280..4c40c76 100644 --- a/frontend-dapp/src/config/bridge.ts +++ b/frontend-dapp/src/config/bridge.ts @@ -52,6 +52,13 @@ export const TRUSTLESS = { CUSDT: (import.meta.env.VITE_CUSDT_CHAIN138 || zero) as `0x${string}`, CUSDC: (import.meta.env.VITE_CUSDC_CHAIN138 || zero) as `0x${string}`, }, + /** Testnet 2138 β€” set addresses after deploy; used when VITE_TRUSTLESS_L2_CHAIN_ID=2138 */ + chain2138: { + LOCKBOX_2138: (import.meta.env.VITE_LOCKBOX_2138 || zero) as `0x${string}`, + WETH: (import.meta.env.VITE_WETH_CHAIN2138 || zero) as `0x${string}`, + CUSDT: (import.meta.env.VITE_CUSDT_CHAIN2138 || zero) as `0x${string}`, + CUSDC: (import.meta.env.VITE_CUSDC_CHAIN2138 || zero) as `0x${string}`, + }, } as const; // --------------------------------------------------------------------------- diff --git a/frontend-dapp/src/config/contracts.ts b/frontend-dapp/src/config/contracts.ts index 4a144fb..dd01482 100644 --- a/frontend-dapp/src/config/contracts.ts +++ b/frontend-dapp/src/config/contracts.ts @@ -1,6 +1,7 @@ import { mainnet } from 'wagmi/chains' import type { Address } from 'viem' import { TRUSTLESS } from './bridge' +import { chain138 as chain138Network, chain2138Testnet } from './networks' // Contract addresses on Ethereum Mainnet and Chain 138 // Trustless bridge (Lockbox, Inbox, LP, Coordinators, ChallengeManager) β€” use TRUSTLESS from bridge.ts as single source of truth @@ -24,24 +25,28 @@ export const CONTRACT_ADDRESSES = { TWOWAY_BRIDGE_L2: undefined as Address | undefined, LOCKBOX_138: TRUSTLESS.chain138.LOCKBOX_138 as Address, }, + chain2138: { + TRANSACTION_MIRROR: (import.meta.env.VITE_TRANSACTION_MIRROR_CHAIN2138 || undefined) as Address | undefined, + PAYMENT_CHANNEL_MANAGER: undefined as Address | undefined, + GENERIC_STATE_CHANNEL_MANAGER: undefined as Address | undefined, + TWOWAY_BRIDGE_L2: undefined as Address | undefined, + LOCKBOX_2138: TRUSTLESS.chain2138.LOCKBOX_2138 as Address, + }, } as const export { TRUSTLESS } from './bridge' // Chain 138 for wagmi (custom chain) export const chain138 = { - id: 138, - name: 'Chain 138', - nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, - rpcUrls: { - default: { http: ['https://rpc.chain138.example'] }, - }, - blockExplorers: { - default: { name: 'Explorer', url: 'https://explorer.chain138.example' }, - }, + ...chain138Network, +} as const + +export const chain2138 = { + ...chain2138Testnet, } as const export const SUPPORTED_CHAINS = { mainnet, chain138, + chain2138, } as const diff --git a/frontend-dapp/src/config/networks.ts b/frontend-dapp/src/config/networks.ts index 35560f9..25349ae 100644 --- a/frontend-dapp/src/config/networks.ts +++ b/frontend-dapp/src/config/networks.ts @@ -26,6 +26,9 @@ const rpcUrl2138 = import.meta.env.VITE_RPC_URL_2138 || 'https://rpc.public-2138.defi-oracle.io' const explorerUrl2138 = import.meta.env.VITE_EXPLORER_URL_2138 || 'https://public-2138.defi-oracle.io' +const configuredDefaultFrontendChainId = Number( + import.meta.env.VITE_DEFAULT_FRONTEND_CHAIN_ID || 138 +) /** Chain 2138 - Defi Oracle Meta Testnet (optional; enable with VITE_ENABLE_CHAIN2138) */ export const chain2138Testnet = defineChain({ @@ -186,6 +189,25 @@ export const customChains = chain2138TestnetEnabled ? customChainsWith2138 : customChainsWithout2138 +/** Source chains the frontend can intentionally target for wallet UX. */ +export const frontendSourceChains = chain2138TestnetEnabled + ? [chain138, chain2138Testnet] as const + : [chain138] as const + +/** Shared source-chain IDs for connect/switch prompts. */ +export const frontendSourceChainIds: number[] = frontendSourceChains.map((chain) => chain.id) + +const configuredFrontendChain = + chain2138TestnetEnabled && configuredDefaultFrontendChainId === chain2138Testnet.id + ? chain2138Testnet + : chain138 + +/** Default chain the wallet UX should prompt for. */ +export const defaultFrontendChain = configuredFrontendChain +export const defaultFrontendChainId = defaultFrontendChain.id +export const defaultFrontendChainName = defaultFrontendChain.name +export const defaultFrontendExplorerUrl = defaultFrontendChain.blockExplorers.default.url + /** Standard L2s from wagmi/chains (Ethereum, Base, Arbitrum, Polygon, Optimism) */ export const standardChains = [mainnet, base, arbitrum, polygon, optimism] as const diff --git a/frontend-dapp/src/config/trustlessL2.ts b/frontend-dapp/src/config/trustlessL2.ts new file mode 100644 index 0000000..70890b6 --- /dev/null +++ b/frontend-dapp/src/config/trustlessL2.ts @@ -0,0 +1,55 @@ +/** + * Trustless bridge β€œL2” side: Chain 138 (mainnet) or Chain 2138 (testnet). + * Set VITE_TRUSTLESS_L2_CHAIN_ID=2138 and deploy addresses via VITE_LOCKBOX_2138, etc. + * When using 2138, enable VITE_ENABLE_CHAIN2138 so Wagmi lists the chain. + */ + +import type { Chain } from 'viem' +import { TRUSTLESS } from './bridge' +import { chain138, chain2138Testnet, chain2138TestnetEnabled } from './networks' + +const raw = import.meta.env.VITE_TRUSTLESS_L2_CHAIN_ID +const parsed = raw === undefined || raw === '' ? 138 : Number(raw) +export const TRUSTLESS_L2_CHAIN_ID: 138 | 2138 = parsed === 2138 ? 2138 : 138 + +export type TrustlessL2LockboxConfig = { + chainId: 138 | 2138 + viemChain: Chain + lockbox: `0x${string}` + weth: `0x${string}` + cusdt: `0x${string}` + cusdc: `0x${string}` +} + +/** Resolves L2 chain + lockbox token addresses for TrustlessBridgeForm. */ +export function getTrustlessL2LockboxConfig(): TrustlessL2LockboxConfig { + if (TRUSTLESS_L2_CHAIN_ID === 2138) { + if (!chain2138TestnetEnabled) { + return { + chainId: 138, + viemChain: chain138, + lockbox: TRUSTLESS.chain138.LOCKBOX_138, + weth: TRUSTLESS.chain138.WETH, + cusdt: TRUSTLESS.chain138.CUSDT, + cusdc: TRUSTLESS.chain138.CUSDC, + } + } + const c = TRUSTLESS.chain2138 + return { + chainId: 2138, + viemChain: chain2138Testnet, + lockbox: c.LOCKBOX_2138, + weth: c.WETH, + cusdt: c.CUSDT, + cusdc: c.CUSDC, + } + } + return { + chainId: 138, + viemChain: chain138, + lockbox: TRUSTLESS.chain138.LOCKBOX_138, + weth: TRUSTLESS.chain138.WETH, + cusdt: TRUSTLESS.chain138.CUSDT, + cusdc: TRUSTLESS.chain138.CUSDC, + } +} diff --git a/frontend-dapp/src/config/wagmi.ts b/frontend-dapp/src/config/wagmi.ts index 39b26d6..e8db9f2 100644 --- a/frontend-dapp/src/config/wagmi.ts +++ b/frontend-dapp/src/config/wagmi.ts @@ -3,19 +3,17 @@ import { metaMask, walletConnect, coinbaseWallet } from 'wagmi/connectors' import { allNetworks, chainRpcUrls, - chain138, - chain2138Testnet, - chain2138TestnetEnabled, - allMainnet, - etherlink, - bsc, - avalanche, - cronos, - gnosis, } from './networks' const projectId = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID || '' +const transports = Object.fromEntries( + allNetworks.map((chain) => { + const rpcUrl = chainRpcUrls[chain.id] + return [chain.id, rpcUrl ? http(rpcUrl) : http()] + }) +) + export const config = createConfig({ chains: allNetworks, connectors: [ @@ -23,22 +21,5 @@ export const config = createConfig({ walletConnect({ projectId }), coinbaseWallet({ appName: 'Bridge DApp' }), ], - transports: { - [chain138.id]: http(chainRpcUrls[chain138.id]), - ...(chain2138TestnetEnabled - ? { [chain2138Testnet.id]: http(chainRpcUrls[chain2138Testnet.id]) } - : {}), - [allMainnet.id]: http(chainRpcUrls[allMainnet.id]), - [etherlink.id]: http(chainRpcUrls[etherlink.id]), - [bsc.id]: http(chainRpcUrls[bsc.id]), - [avalanche.id]: http(chainRpcUrls[avalanche.id]), - [cronos.id]: http(chainRpcUrls[cronos.id]), - [gnosis.id]: http(chainRpcUrls[gnosis.id]), - // Standard chains use default public RPC when no override - ...Object.fromEntries( - allNetworks - .filter((c) => !(c.id in chainRpcUrls)) - .map((c) => [c.id, http()]) - ), - }, + transports: transports as Record>, }) diff --git a/frontend-dapp/src/pages/AdminPanel.tsx b/frontend-dapp/src/pages/AdminPanel.tsx index e03d7bc..8c62430 100644 --- a/frontend-dapp/src/pages/AdminPanel.tsx +++ b/frontend-dapp/src/pages/AdminPanel.tsx @@ -32,6 +32,7 @@ import RealtimeMonitor from '../components/admin/RealtimeMonitor' import PaymentChannels from '../components/admin/PaymentChannels' import PaymentChannelAdmin from '../components/admin/PaymentChannelAdmin' import StateChannels from '../components/admin/StateChannels' +import { chain138 } from '../config/networks' type TabType = 'dashboard' | 'mainnet-tether' | 'transaction-mirror' | 'two-way-bridge' | 'channels' | 'state-channels' | 'channel-admin' | 'multisig' | 'queue' | 'impersonation' | 'emergency' | 'audit' | 'gas' | 'batch' | 'templates' | 'retry' | 'services' | 'preview' | 'roles' | 'timelock' | 'wallet' | 'multichain' | 'schedule' | 'balance' | 'owners' | 'backup' | 'priority' | 'hardware' | 'permissions' | 'realtime' @@ -40,7 +41,7 @@ export default function AdminPanel() { const chainId = useChainId() const [activeTab, setActiveTab] = useState('dashboard') - const isSupportedChain = chainId === 1 || chainId === 138 + const isSupportedChain = chainId === 1 || chainId === chain138.id const tabs = [ { id: 'dashboard' as TabType, label: 'Dashboard', icon: 'πŸ“Š' }, @@ -97,11 +98,11 @@ export default function AdminPanel() {

Admin Panel

- Please switch to Ethereum Mainnet (1) or Chain 138 to access admin functions. + Please switch to Ethereum Mainnet (1) or {chain138.name} to access admin functions.

- Current network: Chain ID {chainId}. Use Mainnet (1) or Chain 138. + Current network: Chain ID {chainId}. Use Mainnet (1) or {chain138.name}.

diff --git a/frontend-dapp/src/pages/BridgePage.tsx b/frontend-dapp/src/pages/BridgePage.tsx index 547aeb7..936ba20 100644 --- a/frontend-dapp/src/pages/BridgePage.tsx +++ b/frontend-dapp/src/pages/BridgePage.tsx @@ -7,15 +7,14 @@ import SwapBridgeSwapQuoteForm from '../components/bridge/SwapBridgeSwapQuoteFor import TrustlessBridgeForm from '../components/bridge/TrustlessBridgeForm'; import ChainIcon from '../components/ui/ChainIcon'; import { CCIP_DESTINATIONS, CHAIN_SELECTORS } from '../config/bridge'; - -const CHAIN_138_ID = 138; +import { chain138 } from '../config/networks'; const THIRDWEB_CLIENT_ID = import.meta.env.VITE_THIRDWEB_CLIENT_ID || '542981292d51ec610388ba8985f027d7'; export default function BridgePage() { const [activeTab, setActiveTab] = useState<'custom' | 'trustless' | 'evm' | 'xrpl' | 'track'>('custom'); const [transferId, setTransferId] = useState(); - const [ccipDestinationSelector, setCcipDestinationSelector] = useState(CHAIN_SELECTORS.ETHEREUM_MAINNET); + const [ccipDestinationSelector, setCcipDestinationSelector] = useState(CHAIN_SELECTORS.ETHEREUM_MAINNET); const handleXRPLBridge = async (data: XRPLBridgeData) => { try { @@ -32,8 +31,8 @@ export default function BridgePage() { const result = await response.json(); setTransferId(result.transferId); setActiveTab('track'); - } catch (error: any) { - throw new Error(error.message || 'Bridge initiation failed'); + } catch (error: unknown) { + throw new Error(error instanceof Error ? error.message : 'Bridge initiation failed'); } }; @@ -138,8 +137,8 @@ export default function BridgePage() {
From - - Chain 138 + + {chain138.name}
To diff --git a/frontend-dapp/src/pages/WalletsDemoPage.tsx b/frontend-dapp/src/pages/WalletsDemoPage.tsx index 38dbe60..3b47005 100644 --- a/frontend-dapp/src/pages/WalletsDemoPage.tsx +++ b/frontend-dapp/src/pages/WalletsDemoPage.tsx @@ -6,41 +6,26 @@ import { createThirdwebClient, defineChain } from 'thirdweb' import { ThirdwebProvider, ConnectButton, useActiveAccount, useWalletBalance } from 'thirdweb/react' import { inAppWallet } from 'thirdweb/wallets' +import { + defaultFrontendChain, + defaultFrontendChainId, + defaultFrontendExplorerUrl, + chainRpcUrls, +} from '../config/networks' const clientId = import.meta.env.VITE_THIRDWEB_CLIENT_ID || '542981292d51ec610388ba8985f027d7' const client = createThirdwebClient({ clientId }) - -const rpcUrl138 = import.meta.env.VITE_RPC_URL_138 || 'https://rpc-http-pub.d-bis.org' -const rpcUrl651940 = import.meta.env.VITE_CHAIN_651940_RPC || import.meta.env.VITE_RPC_URL_651940 || 'https://mainnet-rpc.alltra.global' - -/** Chain 138 β€” hub (DeFi Oracle Meta Mainnet) */ -const chain138 = defineChain({ - id: 138, - name: 'DeFi Oracle Meta Mainnet', - rpc: rpcUrl138, +const defaultThirdwebChain = defineChain({ + id: defaultFrontendChain.id, + name: defaultFrontendChain.name, + rpc: chainRpcUrls[defaultFrontendChainId] || defaultFrontendExplorerUrl, nativeCurrency: { - name: 'Ether', - symbol: 'ETH', - decimals: 18, + name: defaultFrontendChain.nativeCurrency.name, + symbol: defaultFrontendChain.nativeCurrency.symbol, + decimals: defaultFrontendChain.nativeCurrency.decimals, }, }) -/** Chain 651940 β€” ALL Mainnet (Alltra); Alltra-native services + payments */ -const chain651940 = defineChain({ - id: 651940, - name: 'ALL Mainnet', - rpc: rpcUrl651940, - nativeCurrency: { - name: 'Ether', - symbol: 'ETH', - decimals: 18, - }, - blockExplorers: [{ name: 'Alltra', url: 'https://alltra.global' }], -}) - -/** Default chain for this page; can be switched to 651940 for Alltra flows */ -const defaultChain = chain138 - const wallets = [ inAppWallet({ auth: { @@ -49,7 +34,7 @@ const wallets = [ metadata: { name: 'DBIS Bridge', image: { - src: 'https://explorer.d-bis.org/favicon.ico', + src: `${defaultFrontendExplorerUrl}/favicon.ico`, width: 32, height: 32, }, @@ -61,7 +46,7 @@ function WalletsDemoContent() { const account = useActiveAccount() const { data: balance, isLoading: balanceLoading } = useWalletBalance({ client, - chain: defaultChain, + chain: defaultThirdwebChain, address: account?.address, }) @@ -70,14 +55,14 @@ function WalletsDemoContent() {

Thirdweb Wallets

- Connect with email, Google, Apple, passkey, or an external wallet (MetaMask, WalletConnect, etc.). + Connect with email, Google, Apple, passkey, or an external wallet (MetaMask, WalletConnect, etc.) on {defaultFrontendChain.name}.

Loading balance…

) : balance ? (

- Balance ({defaultChain.name}):{' '} + Balance ({defaultFrontendChain.name}):{' '} {balance.displayValue} {balance.symbol} diff --git a/frontend-dapp/src/utils/ens.ts b/frontend-dapp/src/utils/ens.ts index c05a809..dc5c4ca 100644 --- a/frontend-dapp/src/utils/ens.ts +++ b/frontend-dapp/src/utils/ens.ts @@ -7,6 +7,11 @@ import { getPublicClient } from '@wagmi/core' import { config } from '../config/wagmi' import { mainnet } from 'wagmi/chains' +type EnsPublicClient = { + getEnsName: (args: { address: `0x${string}` }) => Promise + getEnsAddress: (args: { name: string }) => Promise +} + const ENS_CACHE: Record = {} const ADDRESS_CACHE: Record = {} const CACHE_TTL = 3600000 // 1 hour @@ -22,7 +27,10 @@ export async function resolveENS(address: string): Promise { try { // Only resolve on mainnet - const publicClient = getPublicClient(config, { chainId: mainnet.id }) as any + const publicClient = getPublicClient( + config as never, + { chainId: mainnet.id } as never + ) as EnsPublicClient | undefined if (!publicClient) { return null } @@ -57,7 +65,10 @@ export async function resolveAddress(name: string): Promise { try { // Only resolve on mainnet - const publicClient = getPublicClient(config, { chainId: mainnet.id }) as any + const publicClient = getPublicClient( + config as never, + { chainId: mainnet.id } as never + ) as EnsPublicClient | undefined if (!publicClient) { return null } diff --git a/frontend-dapp/src/utils/rateLimiter.ts b/frontend-dapp/src/utils/rateLimiter.ts index e873217..e54147b 100644 --- a/frontend-dapp/src/utils/rateLimiter.ts +++ b/frontend-dapp/src/utils/rateLimiter.ts @@ -22,9 +22,13 @@ export const RATE_LIMITS: Record = { export function checkRateLimit( identifier: string, - action: string = 'default' + actionOrMaxRequests: string | number = 'default', + windowMs?: number ): { allowed: boolean; remaining?: number; resetAt?: number } { - const config = RATE_LIMITS[action] || RATE_LIMITS.default + const config = + typeof actionOrMaxRequests === 'number' + ? { maxRequests: actionOrMaxRequests, windowMs: windowMs ?? RATE_LIMITS.default.windowMs } + : RATE_LIMITS[actionOrMaxRequests] || RATE_LIMITS.default const allowed = rateLimiter.checkLimit(identifier, config.maxRequests) if (!allowed) { diff --git a/frontend-dapp/src/vite-env.d.ts b/frontend-dapp/src/vite-env.d.ts index 08b686a..ce64f0c 100644 --- a/frontend-dapp/src/vite-env.d.ts +++ b/frontend-dapp/src/vite-env.d.ts @@ -4,6 +4,13 @@ interface ImportMetaEnv { readonly VITE_ENABLE_CHAIN2138?: string readonly VITE_RPC_URL_2138?: string readonly VITE_EXPLORER_URL_2138?: string + readonly VITE_DEFAULT_FRONTEND_CHAIN_ID?: string + readonly VITE_TRUSTLESS_L2_CHAIN_ID?: string + readonly VITE_LOCKBOX_2138?: string + readonly VITE_WETH_CHAIN2138?: string + readonly VITE_CUSDT_CHAIN2138?: string + readonly VITE_CUSDC_CHAIN2138?: string + readonly VITE_TRANSACTION_MIRROR_CHAIN2138?: string } import { EventEmitter } from 'events' diff --git a/frontend-dapp/tsconfig.json b/frontend-dapp/tsconfig.json index 5128ea2..1c22d29 100644 --- a/frontend-dapp/tsconfig.json +++ b/frontend-dapp/tsconfig.json @@ -17,10 +17,14 @@ "noFallthroughCasesInSwitch": true, "baseUrl": ".", "paths": { - "@/*": ["./src/*"] - } + "@/*": ["./src/*"], + "react": ["./node_modules/@types/react"], + "react/jsx-runtime": ["./node_modules/@types/react/jsx-runtime"], + "react/jsx-dev-runtime": ["./node_modules/@types/react/jsx-dev-runtime"], + "react-dom": ["./node_modules/@types/react-dom"] + }, + "types": ["vite/client"] }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] } - diff --git a/terraform/phases/phase1/config/env.chain2138.example b/terraform/phases/phase1/config/env.chain2138.example index 40be968..1ae87ae 100644 --- a/terraform/phases/phase1/config/env.chain2138.example +++ b/terraform/phases/phase1/config/env.chain2138.example @@ -34,6 +34,12 @@ CHAIN2138_ETHERSCAN_API_KEY= # Fee token on 2138 (often native or LINK once deployed) FEE_TOKEN= +# Vite DApp (smom-dbis-138/frontend-dapp/.env*) β€” not read by Terraform; copy as needed +# VITE_ENABLE_CHAIN2138=true +# VITE_TRUSTLESS_L2_CHAIN_ID=2138 +# VITE_LOCKBOX_2138= +# VITE_WETH_CHAIN2138=... VITE_CUSDT_CHAIN2138=... VITE_CUSDC_CHAIN2138=... + # thirdweb (optional) THIRDWEB_PROJECT_NAME="DBIS ChainID 2138 Testnet" THIRDWEB_CLIENT_ID=