Add optional Chain 2138 frontend support

This commit is contained in:
defiQUG
2026-03-28 15:38:51 -07:00
parent 6deb072fa7
commit 1771db2190
31 changed files with 408 additions and 187 deletions

View File

@@ -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

View File

@@ -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
---

View File

@@ -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.

View File

@@ -57,7 +57,7 @@ const CONTRACT_FUNCTIONS = {
export default function FunctionPermissions() {
const { address } = useAccount()
const { addAuditLog } = useAdmin()
const [roles, setRoles] = useState<Role[]>(DEFAULT_ROLES)
const [roles] = useState<Role[]>(DEFAULT_ROLES)
const [permissions, setPermissions] = useState<FunctionPermission[]>([])
const [selectedContract, setSelectedContract] = useState<string>(CONTRACT_ADDRESSES.mainnet.MAINNET_TETHER)
const [selectedRole, setSelectedRole] = useState<string>('operator')

View File

@@ -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: {},
},
]

View File

@@ -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<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',
},
])
const [services, setServices] = useState<ServiceStatus[]>(INITIAL_SERVICES)
const checkServiceStatus = async (service: ServiceStatus): Promise<ServiceStatus> => {
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() {
<div>
<p className="text-white/70 mb-1">State Anchoring Service</p>
<p className="text-white/60 text-xs">
Monitors ChainID 138 blocks and submits state proofs to MainnetTether contract.
Monitors {chain138.name} blocks and submits state proofs to MainnetTether contract.
</p>
<p className="text-white/60 text-xs font-mono mt-1">
Contract: {CONTRACT_ADDRESSES.mainnet.MAINNET_TETHER}
@@ -183,7 +186,7 @@ export default function OffChainServices() {
<div>
<p className="text-white/70 mb-1">Transaction Mirroring Service</p>
<p className="text-white/60 text-xs">
Monitors ChainID 138 transactions and mirrors them to TransactionMirror contract.
Monitors {chain138.name} transactions and mirrors them to TransactionMirror contract.
</p>
<p className="text-white/60 text-xs font-mono mt-1">
Contract: {CONTRACT_ADDRESSES.mainnet.TRANSACTION_MIRROR}

View File

@@ -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 (
<div className="rounded-xl bg-white/5 p-6 text-white/80">
<p>Payment channel manager not deployed on this chain. Set PAYMENT_CHANNEL_MANAGER in config.</p>
<p>
Payment channel manager not deployed on this chain. Set `PAYMENT_CHANNEL_MANAGER`
in config for Ethereum Mainnet, {chain138.name}, or {chain2138Testnet.name}.
</p>
</div>
)
}

View File

@@ -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 (
<div className="rounded-xl bg-white/5 p-6 text-white/80">
<p>Payment channel manager not deployed on this chain. Set PAYMENT_CHANNEL_MANAGER in config for Mainnet (1) or Chain 138.</p>
<p>
Payment channel manager not deployed on this chain. Set `PAYMENT_CHANNEL_MANAGER`
in config for Ethereum Mainnet, {chain138.name}, or {chain2138Testnet.name}.
</p>
</div>
)
}
@@ -205,7 +210,7 @@ export default function PaymentChannels() {
<div className="mb-4 p-3 rounded-lg bg-amber-500/20 text-amber-200 text-sm">Contract is paused. Only close/finalize allowed.</div>
)}
<p className="text-white/70 text-sm mb-4">
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.
</p>
<div className="text-white/60 text-xs mb-4 space-y-1">
<p>Payment channels (above) = state channels for payments. For cross-chain or routed payments, general state channels, or channel networks:</p>
@@ -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 (
<div className="rounded-lg border border-white/20 p-4 bg-black/20">
<div className="flex flex-wrap items-center gap-2 text-sm">

View File

@@ -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 && (
<div className="bg-yellow-500/20 border border-yellow-500/50 rounded-lg p-4">
<p className="text-yellow-200 text-sm">
Click "Start Monitoring" to begin real-time monitoring of contract states and events.
Click &quot;Start Monitoring&quot; to begin real-time monitoring of contract states and events.
</p>
</div>
)}

View File

@@ -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 (
<div className="rounded-xl bg-white/5 p-6 text-white/80">
<p>Generic state channel manager not deployed on this chain. Set GENERIC_STATE_CHANNEL_MANAGER in config for Mainnet (1) or Chain 138.</p>
<p>
Generic state channel manager not deployed on this chain. Set `GENERIC_STATE_CHANNEL_MANAGER`
in config for Ethereum Mainnet, {chain138.name}, or {chain2138Testnet.name}.
</p>
</div>
)
}

View File

@@ -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<ChainMetadata[]>([]);
@@ -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'}
</span>
<button
onClick={() => toggleChain(chain.chainId, chain.chainIdentifier, chain.isActive)}
onClick={() => toggleChain(chain.isActive)}
className={`px-4 py-2 rounded-lg font-semibold transition-colors ${
chain.isActive
? 'bg-red-600 hover:bg-red-700 text-white'
@@ -156,7 +159,7 @@ export default function ChainManagementDashboard() {
<option value="ethereum">Ethereum Mainnet</option>
</select>
<button
onClick={() => toast.info('Deployment feature coming soon')}
onClick={() => toast('Deployment feature coming soon', { icon: '' })}
className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-semibold"
>
Deploy Chain Adapter

View File

@@ -33,6 +33,16 @@ interface BridgeButtonsProps {
recipientAddress?: string; // Defaults to connected wallet
}
function getErrorMessage(error: unknown): string {
if (typeof error === 'object' && error !== null) {
const maybeMessage = 'message' in error ? error.message : undefined
const maybeReason = 'reason' in error ? error.reason : undefined
if (typeof maybeMessage === 'string' && maybeMessage.length > 0) return maybeMessage
if (typeof maybeReason === 'string' && maybeReason.length > 0) return maybeReason
}
return 'Unknown error'
}
/** Fetches CCIP fee only when amount > 0 (bridge reverts on zero). Reports result to parent via onFee. */
function CalculateFeeFetcher({
bridgeContract,
@@ -59,7 +69,7 @@ function CalculateFeeFetcher({
/** Inner bridge form: only mounted when address is set so balance/allowance are never called with zero address. */
function BridgeButtonsConnected({
address,
destinationChainSelector,
destinationChainSelector = CHAIN_SELECTORS.ETHEREUM_MAINNET,
recipientAddress,
}: BridgeButtonsProps & { address: string }) {
const [amount, setAmount] = useState<string>('');
@@ -190,10 +200,9 @@ function BridgeButtonsConnected({
toast.success('ETH wrapped successfully!', { id: toastId });
setAmount('');
await handleRefreshBalances();
} catch (error: any) {
} catch (error: unknown) {
console.error('Wrap error:', error);
const errorMessage = error?.message || error?.reason || 'Unknown error';
toast.error(`Wrap failed: ${errorMessage}`, { id: toastId });
toast.error(`Wrap failed: ${getErrorMessage(error)}`, { id: toastId });
} finally {
setIsWrapping(false);
}
@@ -246,10 +255,9 @@ function BridgeButtonsConnected({
toast.success('All approvals successful!', { id: toastId });
await handleRefreshBalances();
} catch (error: any) {
} catch (error: unknown) {
console.error('Approve error:', error);
const errorMessage = error?.message || error?.reason || 'Unknown error';
toast.error(`Approval failed: ${errorMessage}`, { id: toastId });
toast.error(`Approval failed: ${getErrorMessage(error)}`, { id: toastId });
} finally {
setIsApproving(false);
}
@@ -328,10 +336,9 @@ function BridgeButtonsConnected({
);
setAmount('');
await handleRefreshBalances();
} catch (error: any) {
} catch (error: unknown) {
console.error('Bridge error:', error);
const errorMessage = error?.message || error?.reason || 'Unknown error';
toast.error(`Bridge failed: ${errorMessage}`, { id: toastId });
toast.error(`Bridge failed: ${getErrorMessage(error)}`, { id: toastId });
} finally {
setIsBridging(false);
}
@@ -364,7 +371,7 @@ function BridgeButtonsConnected({
recipient &&
ethers.utils.isAddress(recipient);
const destination = CCIP_DESTINATIONS.find((d) => d.selector === destinationChainSelector);
const destination = CCIP_DESTINATIONS.find((d) => d.selector === destinationChainSelector) ?? null;
return (
<div className="w-full">

View File

@@ -5,6 +5,7 @@
import { useState } from 'react';
import { BRIDGE_QUOTE_URL } from '../../config/bridge';
import { defaultFrontendChainId } from '../../config/networks';
interface QuoteResult {
transferId?: string;
@@ -19,7 +20,7 @@ interface QuoteResult {
export default function SwapBridgeSwapQuoteForm() {
const [sourceToken, setSourceToken] = useState('');
const [destinationToken, setDestinationToken] = useState('');
const [sourceChainId, setSourceChainId] = useState('138');
const [sourceChainId, setSourceChainId] = useState(String(defaultFrontendChainId));
const [destinationChainId, setDestinationChainId] = useState('137');
const [amount, setAmount] = useState('');
const [quote, setQuote] = useState<QuoteResult | null>(null);

View File

@@ -4,6 +4,7 @@
*/
import { useState, useMemo } from 'react';
import { defaultFrontendChain, chain2138TestnetEnabled } from '../../config/networks';
interface ThirdwebBridgeWidgetProps {
clientId: string;
@@ -15,7 +16,7 @@ interface ThirdwebBridgeWidgetProps {
export default function ThirdwebBridgeWidget({
clientId,
fromChain = 138,
fromChain = defaultFrontendChain.id,
toChain,
fromToken,
toToken
@@ -42,10 +43,16 @@ export default function ThirdwebBridgeWidget({
Bridge to EVM Chain
</h2>
<p className="text-gray-600">
Bridge or swap tokens from Chain 138 to supported EVM destinations using ThirdWeb
Bridge or swap tokens from {defaultFrontendChain.name} to supported EVM destinations using ThirdWeb
</p>
</div>
{chain2138TestnetEnabled && (
<p className="mb-4 text-sm text-blue-700 bg-blue-50 border border-blue-200 rounded-xl px-4 py-3">
Chain 2138 is enabled for frontend wallet flows. The Thirdweb embed will default to the configured frontend source chain.
</p>
)}
<div className="mb-6">
<label className="block text-sm font-semibold mb-3 text-gray-700">
Destination Chain

View File

@@ -14,11 +14,12 @@ import {
CHALLENGE_MANAGER_ABI,
ERC20_ABI,
} from '../../config/bridge';
import { chain138 } from '../../config/networks';
import { getTrustlessL2LockboxConfig, TRUSTLESS_L2_CHAIN_ID } from '../../config/trustlessL2';
const MAINNET_CHAIN_ID = 1;
const OUTPUT_ASSET_ETH = 0;
const OUTPUT_ASSET_WETH = 1;
const ZERO = '0x0000000000000000000000000000000000000000' as const;
const STABLECOIN_OPTIONS = [
{ value: 'USDT', label: 'USDT', address: TRUSTLESS.mainnet.USDT },
@@ -26,13 +27,16 @@ const STABLECOIN_OPTIONS = [
{ value: 'DAI', label: 'DAI', address: TRUSTLESS.mainnet.DAI },
] as const;
const LOCKBOX_ERC20_OPTIONS = [
{ value: 'WETH', label: 'WETH', address: TRUSTLESS.chain138.WETH, decimals: 18 },
{ value: 'cUSDT', label: 'cUSDT', address: TRUSTLESS.chain138.CUSDT, decimals: 6 },
{ value: 'cUSDC', label: 'cUSDC', address: TRUSTLESS.chain138.CUSDC, decimals: 6 },
].filter((o) => o.address !== '0x0000000000000000000000000000000000000000') as Array<{ value: string; label: string; address: `0x${string}`; decimals: number }>;
export default function TrustlessBridgeForm() {
const l2 = getTrustlessL2LockboxConfig();
const nativeSymbol = l2.viemChain.nativeCurrency.symbol;
const l2Label = l2.viemChain.name;
const LOCKBOX_ERC20_OPTIONS = [
{ value: 'WETH', label: 'WETH', address: l2.weth, decimals: 18 },
{ value: 'cUSDT', label: 'cUSDT', address: l2.cusdt, decimals: 6 },
{ value: 'cUSDC', label: 'cUSDC', address: l2.cusdc, decimals: 6 },
].filter((o) => o.address !== ZERO) as Array<{ value: string; label: string; address: `0x${string}`; decimals: number }>;
const { address } = useAccount();
const chainId = useChainId();
const switchChain = useSwitchChain();
@@ -49,17 +53,17 @@ export default function TrustlessBridgeForm() {
const [checkDepositId, setCheckDepositId] = useState('');
const [finalizeDepositId, setFinalizeDepositId] = useState('');
const lockboxAddress = TRUSTLESS.chain138.LOCKBOX_138;
const lockboxAddress = l2.lockbox;
const coordinatorAddress = TRUSTLESS.mainnet.DUAL_ROUTER_BRIDGE_SWAP_COORDINATOR;
const challengeManagerAddress = TRUSTLESS.mainnet.CHALLENGE_MANAGER;
const erc20TokenAddress = LOCKBOX_ERC20_OPTIONS.find((o) => o.value === erc20Token)?.address ?? TRUSTLESS.chain138.WETH;
const erc20TokenAddress = LOCKBOX_ERC20_OPTIONS.find((o) => o.value === erc20Token)?.address ?? l2.weth;
const { data: nonceData } = useReadContract({
address: address ? lockboxAddress : undefined,
abi: LOCKBOX_138_ABI,
functionName: 'getNonce',
args: address ? [address] : undefined,
chainId: chain138.id,
chainId: l2.chainId,
});
const { data: canSwapResult, refetch: refetchCanSwap } = useReadContract({
@@ -83,14 +87,16 @@ export default function TrustlessBridgeForm() {
abi: ERC20_ABI,
functionName: 'allowance',
args: address ? [address, lockboxAddress] : undefined,
chainId: chain138.id,
chainId: l2.chainId,
});
const { writeContract, data: hash, isPending, reset } = useWriteContract();
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash });
const onChain138 = chainId === chain138.id;
const onL2 = chainId === l2.chainId;
const onMainnet = chainId === MAINNET_CHAIN_ID;
const l2Misconfigured =
TRUSTLESS_L2_CHAIN_ID === 2138 && l2.chainId === 138;
const stablecoinAddress = STABLECOIN_OPTIONS.find((o) => o.value === stablecoin)?.address ?? TRUSTLESS.mainnet.USDT;
@@ -105,8 +111,8 @@ export default function TrustlessBridgeForm() {
return;
}
const nonceBytes = padHex(toHex(nonceData ?? 0n), { size: 32 }) as `0x${string}`;
if (!onChain138) {
switchChain?.switchChain({ chainId: chain138.id });
if (!onL2) {
switchChain?.switchChain({ chainId: l2.chainId });
return;
}
writeContract(
@@ -139,8 +145,8 @@ export default function TrustlessBridgeForm() {
toast.error('Amount must be > 0');
return;
}
if (!onChain138) {
switchChain?.switchChain({ chainId: chain138.id });
if (!onL2) {
switchChain?.switchChain({ chainId: l2.chainId });
return;
}
writeContract(
@@ -167,8 +173,8 @@ export default function TrustlessBridgeForm() {
return;
}
const nonceBytes = padHex(toHex(nonceData ?? 0n), { size: 32 }) as `0x${string}`;
if (!onChain138) {
switchChain?.switchChain({ chainId: chain138.id });
if (!onL2) {
switchChain?.switchChain({ chainId: l2.chainId });
return;
}
writeContract(
@@ -245,19 +251,31 @@ export default function TrustlessBridgeForm() {
return (
<div className="space-y-8 rounded-2xl bg-white/5 backdrop-blur border border-cyan-400/20 p-6">
<h2 className="text-2xl font-bold text-white">Trustless Bridge (Chain 138 Mainnet)</h2>
<h2 className="text-2xl font-bold text-white">Trustless Bridge ({l2Label} Mainnet)</h2>
{l2Misconfigured && (
<p className="text-amber-200/90 text-sm mb-2 rounded-lg border border-amber-400/40 bg-amber-500/10 p-3">
VITE_TRUSTLESS_L2_CHAIN_ID is 2138 but VITE_ENABLE_CHAIN2138 is off L2 fell back to {l2Label}. Enable the testnet chain or set L2 to 138.
</p>
)}
{l2.chainId === 2138 && lockboxAddress === ZERO && (
<p className="text-amber-200/90 text-sm mb-2 rounded-lg border border-amber-400/40 bg-amber-500/10 p-3">
Set VITE_LOCKBOX_2138 (and token addresses) after deploying the testnet lockbox.
</p>
)}
{/* Lockbox 138 deposit */}
{/* Lockbox L2 deposit */}
<section>
<h3 className="text-lg font-semibold text-cyan-300 mb-3">1. Deposit on Chain 138 (Lockbox)</h3>
<p className="text-white/70 text-sm mb-3">Deposit native ETH or ERC-20 (WETH, cUSDT, cUSDC) on Chain 138. Then relay and finalize claim on Mainnet before Bridge & Swap.</p>
{!onChain138 && (
<h3 className="text-lg font-semibold text-cyan-300 mb-3">1. Deposit on {l2Label} (Lockbox)</h3>
<p className="text-white/70 text-sm mb-3">
Deposit native {nativeSymbol} or ERC-20 (WETH, cUSDT, cUSDC) on {l2Label}. Then relay and finalize claim on Mainnet before Bridge & Swap.
</p>
{!onL2 && (
<button
type="button"
onClick={() => switchChain?.switchChain({ chainId: chain138.id })}
onClick={() => switchChain?.switchChain({ chainId: l2.chainId })}
className="mb-3 px-4 py-2 rounded-lg bg-cyan-500/20 text-cyan-300 border border-cyan-400/40"
>
Switch to Chain 138
Switch to {l2Label}
</button>
)}
<div className="grid gap-3 max-w-md">
@@ -265,13 +283,16 @@ export default function TrustlessBridgeForm() {
<span className="text-white/80">Deposit type:</span>
<label className="flex items-center gap-2 cursor-pointer">
<input type="radio" checked={depositType === 'native'} onChange={() => setDepositType('native')} className="rounded" />
<span>Native ETH</span>
<span>Native {nativeSymbol}</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input type="radio" checked={depositType === 'erc20'} onChange={() => setDepositType('erc20')} className="rounded" />
<span>ERC-20</span>
</label>
</div>
{depositType === 'erc20' && LOCKBOX_ERC20_OPTIONS.length === 0 && (
<p className="text-amber-200/90 text-sm">No ERC-20 tokens configured for this L2 (set non-zero addresses in env).</p>
)}
{depositType === 'erc20' && LOCKBOX_ERC20_OPTIONS.length > 0 && (
<div className="flex gap-4 items-center">
<label htmlFor="trustless-bridge-token" className="text-white/80">Token:</label>
@@ -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' && (
<button
type="button"
disabled={isPending || isConfirming || !address || !amount || !recipient || !onChain138}
disabled={isPending || isConfirming || !address || !amount || !recipient || !onL2 || lockboxAddress === ZERO}
onClick={handleDepositNative}
className="px-4 py-2 rounded-lg bg-cyan-500 text-white font-medium disabled:opacity-50"
>
{isPending || isConfirming ? 'Confirming...' : 'Deposit native ETH'}
{isPending || isConfirming ? 'Confirming...' : `Deposit native ${nativeSymbol}`}
</button>
)}
{depositType === 'erc20' && (
@@ -323,7 +344,7 @@ export default function TrustlessBridgeForm() {
{tokenAllowance !== undefined && BigInt(amount || 0) * 10n ** 18n > (tokenAllowance ?? 0n) && (
<button
type="button"
disabled={isPending || isConfirming || !address || !amount || !onChain138}
disabled={isPending || isConfirming || !address || !amount || !onL2 || lockboxAddress === ZERO}
onClick={handleApprove}
className="px-4 py-2 rounded-lg bg-cyan-500/80 text-white font-medium disabled:opacity-50"
>
@@ -332,7 +353,7 @@ export default function TrustlessBridgeForm() {
)}
<button
type="button"
disabled={isPending || isConfirming || !address || !amount || !recipient || !onChain138 || (tokenAllowance !== undefined && erc20AmountWei > (tokenAllowance ?? 0n))}
disabled={isPending || isConfirming || !address || !amount || !recipient || !onL2 || lockboxAddress === ZERO || (tokenAllowance !== undefined && erc20AmountWei > (tokenAllowance ?? 0n))}
onClick={handleDepositERC20}
className="px-4 py-2 rounded-lg bg-cyan-500 text-white font-medium disabled:opacity-50"
>

View File

@@ -2,8 +2,7 @@ import { Link, useLocation } from 'react-router-dom'
import { useRef } from 'react'
import WalletConnect from '../wallet/WalletConnect'
import WalletDisconnectNotice from '../wallet/WalletDisconnectNotice'
const EXPLORER_URL = 'https://explorer.d-bis.org'
import { defaultFrontendExplorerUrl } from '../../config/networks'
interface LayoutProps {
children: React.ReactNode
@@ -59,7 +58,7 @@ export default function Layout({ children }: LayoutProps) {
Wallets
</Link>
<a
href={EXPLORER_URL}
href={defaultFrontendExplorerUrl}
target="_blank"
rel="noopener noreferrer"
className="px-4 py-2 rounded-lg text-sm font-medium text-[#A0A0A0] hover:text-white hover:bg-white/5 transition-colors"

View File

@@ -1,8 +1,11 @@
import { useAccount, useConnect, useDisconnect, useChainId, useSwitchChain } from 'wagmi'
import { useEffect, useState, useRef } from 'react'
const CHAIN_138_ID = 138
const EXPLORER_URL = 'https://explorer.d-bis.org'
import {
defaultFrontendChainId,
defaultFrontendChainName,
defaultFrontendExplorerUrl,
frontendSourceChainIds,
} from '../../config/networks'
interface WalletConnectProps {
/** Callback before disconnect so we can treat it as user-initiated (no "disconnected" toast). */
@@ -21,7 +24,7 @@ export default function WalletConnect({ onBeforeDisconnect }: WalletConnectProps
const dropdownRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (isConnected && chainId !== CHAIN_138_ID) {
if (isConnected && !frontendSourceChainIds.includes(chainId)) {
setShowChainWarning(true)
} else {
setShowChainWarning(false)
@@ -40,7 +43,7 @@ export default function WalletConnect({ onBeforeDisconnect }: WalletConnectProps
const handleSwitchChain = async () => {
try {
await switchChain({ chainId: CHAIN_138_ID })
await switchChain({ chainId: defaultFrontendChainId })
} catch (error) {
console.error('Failed to switch chain:', error)
}
@@ -59,7 +62,7 @@ export default function WalletConnect({ onBeforeDisconnect }: WalletConnectProps
}
const viewOnExplorer = () => {
if (address) window.open(`${EXPLORER_URL}/address/${address}`, '_blank', 'noopener')
if (address) window.open(`${defaultFrontendExplorerUrl}/address/${address}`, '_blank', 'noopener')
setShowDropdown(false)
}
@@ -81,7 +84,7 @@ export default function WalletConnect({ onBeforeDisconnect }: WalletConnectProps
Switching...
</>
) : (
'Switch to Chain 138'
`Switch to ${defaultFrontendChainName}`
)}
</button>
)}

View File

@@ -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')
})
})

View File

@@ -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;
// ---------------------------------------------------------------------------

View File

@@ -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

View File

@@ -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

View File

@@ -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,
}
}

View File

@@ -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<number, ReturnType<typeof http>>,
})

View File

@@ -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<TabType>('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() {
<div className="bg-white/10 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 p-8 text-center">
<h1 className="text-3xl font-bold text-white mb-4">Admin Panel</h1>
<p className="text-white/80 mb-6">
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.
</p>
<div className="inline-block bg-red-500/20 border border-red-500/50 rounded-lg p-4">
<p className="text-red-200 text-sm">
Current network: Chain ID {chainId}. Use Mainnet (1) or Chain 138.
Current network: Chain ID {chainId}. Use Mainnet (1) or {chain138.name}.
</p>
</div>
</div>

View File

@@ -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<string | undefined>();
const [ccipDestinationSelector, setCcipDestinationSelector] = useState(CHAIN_SELECTORS.ETHEREUM_MAINNET);
const [ccipDestinationSelector, setCcipDestinationSelector] = useState<string>(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() {
<div className="mb-4 flex flex-wrap items-center gap-6">
<div className="flex items-center gap-2">
<span className="text-[#A0A0A0] font-medium">From</span>
<ChainIcon chainId={CHAIN_138_ID} name="Chain 138" size={28} />
<span className="text-white font-medium">Chain 138</span>
<ChainIcon chainId={chain138.id} name={chain138.name} size={28} />
<span className="text-white font-medium">{chain138.name}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-[#A0A0A0] font-medium">To</span>

View File

@@ -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() {
<div>
<h1 className="text-2xl font-bold text-white mb-2">Thirdweb Wallets</h1>
<p className="text-[#A0A0A0] text-sm">
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}.
</p>
</div>
<div className="flex flex-col items-center gap-6 p-6 bg-[#252830] rounded-xl border border-white/10">
<ConnectButton
client={client}
chain={defaultChain}
chain={defaultThirdwebChain}
wallets={wallets}
theme="dark"
connectButton={{
@@ -99,7 +84,7 @@ function WalletsDemoContent() {
<p className="text-[#A0A0A0]">Loading balance</p>
) : balance ? (
<p className="text-[#A0A0A0]">
Balance ({defaultChain.name}):{' '}
Balance ({defaultFrontendChain.name}):{' '}
<span className="text-white">
{balance.displayValue} {balance.symbol}
</span>

View File

@@ -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<string | null>
getEnsAddress: (args: { name: string }) => Promise<string | null>
}
const ENS_CACHE: Record<string, { name: string | null; timestamp: number }> = {}
const ADDRESS_CACHE: Record<string, { address: string | null; timestamp: number }> = {}
const CACHE_TTL = 3600000 // 1 hour
@@ -22,7 +27,10 @@ export async function resolveENS(address: string): Promise<string | null> {
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<string | null> {
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
}

View File

@@ -22,9 +22,13 @@ export const RATE_LIMITS: Record<string, RateLimitConfig> = {
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) {

View File

@@ -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'

View File

@@ -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" }]
}

View File

@@ -34,6 +34,12 @@ CHAIN2138_ETHERSCAN_API_KEY=<explorer-api-key-if-required>
# Fee token on 2138 (often native or LINK once deployed)
FEE_TOKEN=<native-or-link-address>
# 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=<lockbox-after-deploy>
# VITE_WETH_CHAIN2138=... VITE_CUSDT_CHAIN2138=... VITE_CUSDC_CHAIN2138=...
# thirdweb (optional)
THIRDWEB_PROJECT_NAME="DBIS ChainID 2138 Testnet"
THIRDWEB_CLIENT_ID=<your-thirdweb-client-id>