chore: sync submodule state (parent ref update)

Made-with: Cursor
This commit is contained in:
defiQUG
2026-03-02 12:14:09 -08:00
parent 50ab378da9
commit 5efe36b1e0
1100 changed files with 155024 additions and 8674 deletions

View File

@@ -3,9 +3,10 @@
# DO NOT commit .env or .env.local to version control
# Required Environment Variables
VITE_WALLETCONNECT_PROJECT_ID=your_walletconnect_project_id_here
# From cloud.reown.com (Reown / WalletConnect)
VITE_WALLETCONNECT_PROJECT_ID=your_reown_project_id_from_cloud_reown_com
VITE_THIRDWEB_CLIENT_ID=your_thirdweb_client_id_here
VITE_RPC_URL_138=http://192.168.11.250:8545
VITE_RPC_URL_138=https://rpc-http-pub.d-bis.org
# Optional Environment Variables
VITE_ETHERSCAN_API_KEY=YourApiKeyToken

View File

@@ -275,7 +275,7 @@ A comprehensive web-based admin panel (dApp) for managing the MainnetTether, Tra
- **Tailwind CSS**: Styling with utility-first approach
- **React Router**: Navigation
- **react-hot-toast**: Notifications
- **@safe-global/safe-core-sdk**: Gnosis Safe SDK integration
- **@safe-global/protocol-kit**: Gnosis Safe SDK integration (replaces deprecated safe-core-sdk)
- **@safe-global/api-kit**: Safe API client
- **ethers.js**: Additional Ethereum utilities (v5)
- **Web Crypto API**: Encryption for sensitive data storage

View File

@@ -39,9 +39,7 @@ This document outlines the comprehensive integration plan for enhancing the admi
- Display approval count vs threshold
**Dependencies Required:**
-**TODO: deps-1** - Install @safe-global/safe-core-sdk
-**TODO: deps-2** - Install @safe-global/safe-ethers-lib
-**TODO: deps-3** - Install @safe-global/safe-service-client
-**Migrated** - Replaced deprecated Safe packages with @safe-global/protocol-kit v1
---

View File

@@ -0,0 +1,2 @@
# Served by deploy-dapp-lxc.sh. CSP allows unsafe-eval for WalletConnect/Reown SDKs; tighten when deps allow.
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https: wss: http://192.168.11.221:8545 ws://192.168.11.221:8546 https://rpc-http-pub.d-bis.org wss://rpc-ws-pub.d-bis.org; frame-src 'self' https:; frame-ancestors 'self';" always;

View File

@@ -14,44 +14,43 @@
},
"dependencies": {
"@safe-global/api-kit": "^4.0.1",
"@safe-global/safe-core-sdk": "^3.3.5",
"@safe-global/safe-ethers-lib": "^1.9.4",
"@safe-global/safe-service-client": "^2.0.3",
"@tanstack/react-query": "^5.8.4",
"@safe-global/protocol-kit": "^1.3.0",
"@tanstack/react-query": "^5.90.21",
"@thirdweb-dev/react": "^4.9.4",
"@thirdweb-dev/sdk": "^4.0.99",
"@wagmi/core": "^3.2.2",
"@walletconnect/ethereum-provider": "^2.23.1",
"autoprefixer": "^10.4.16",
"@wagmi/core": "^3.3.4",
"@walletconnect/ethereum-provider": "^2.23.5",
"autoprefixer": "^10.4.24",
"ethers": "^5.8.0",
"postcss": "^8.4.32",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1",
"react-router-dom": "^6.20.0",
"tailwindcss": "^3.3.6",
"viem": "^2.0.0",
"wagmi": "^2.3.0"
"postcss": "^8.5.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hot-toast": "^2.6.0",
"react-router-dom": "^6.30.3",
"tailwindcss": "^3.4.19",
"thirdweb": "^5.29.6",
"viem": "^2.46.1",
"wagmi": "^2.19.5"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.1",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@vitejs/plugin-react": "^4.2.0",
"@vitest/ui": "^1.1.0",
"eslint": "^8.53.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^14.3.1",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^18.3.28",
"@types/react-dom": "^18.3.7",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react": "^4.7.0",
"@vitest/ui": "^1.6.1",
"eslint": "^8.57.1",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.26",
"identity-obj-proxy": "^3.0.0",
"jsdom": "^23.0.1",
"ts-jest": "^29.1.1",
"typescript": "^5.2.2",
"vite": "^5.0.0",
"jsdom": "^23.2.0",
"ts-jest": "^29.4.6",
"typescript": "^5.9.3",
"vite": "^5.4.21",
"vite-plugin-node-polyfills": "^0.24.0",
"vitest": "^1.1.0"
"vitest": "^1.6.1"
}
}

View File

@@ -10,6 +10,8 @@ import SwapPage from './pages/SwapPage'
import ReservePage from './pages/ReservePage'
import HistoryPage from './pages/HistoryPage'
import AdminPanel from './pages/AdminPanel'
import DocsPage from './pages/DocsPage'
import WalletsDemoPage from './pages/WalletsDemoPage'
import Layout from './components/layout/Layout'
import ToastProvider from './components/ui/ToastProvider'
@@ -53,6 +55,8 @@ function App() {
<Route path="/reserve" element={<ReservePage />} />
<Route path="/history" element={<HistoryPage />} />
<Route path="/admin" element={<AdminPanel />} />
<Route path="/docs" element={<DocsPage />} />
<Route path="/wallets" element={<WalletsDemoPage />} />
</Routes>
</Layout>
</AdminProvider>

View File

@@ -0,0 +1,134 @@
export const GENERIC_STATE_CHANNEL_MANAGER_ABI = [
{
inputs: [
{ name: '_admin', internalType: 'address', type: 'address' },
{ name: '_challengeWindowSeconds', internalType: 'uint256', type: 'uint256' },
],
stateMutability: 'nonpayable',
type: 'constructor',
},
{ inputs: [], name: 'admin', outputs: [{ name: '', internalType: 'address', type: 'address' }], stateMutability: 'view', type: 'function' },
{ inputs: [], name: 'paused', outputs: [{ name: '', internalType: 'bool', type: 'bool' }], stateMutability: 'view', type: 'function' },
{ inputs: [], name: 'challengeWindowSeconds', outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], stateMutability: 'view', type: 'function' },
{
inputs: [{ name: 'participantB', internalType: 'address', type: 'address' }],
name: 'openChannel',
outputs: [{ name: 'channelId', internalType: 'uint256', type: 'uint256' }],
stateMutability: 'payable',
type: 'function',
},
{
inputs: [{ name: 'channelId', internalType: 'uint256', type: 'uint256' }],
name: 'fundChannel',
outputs: [],
stateMutability: 'payable',
type: 'function',
},
{
inputs: [
{ name: 'channelId', internalType: 'uint256', type: 'uint256' },
{ name: 'stateHash', internalType: 'bytes32', type: 'bytes32' },
{ name: 'nonce', internalType: 'uint256', type: 'uint256' },
{ name: 'balanceA', internalType: 'uint256', type: 'uint256' },
{ name: 'balanceB', internalType: 'uint256', type: 'uint256' },
{ name: 'vA', internalType: 'uint8', type: 'uint8' },
{ name: 'rA', internalType: 'bytes32', type: 'bytes32' },
{ name: 'sA', internalType: 'bytes32', type: 'bytes32' },
{ name: 'vB', internalType: 'uint8', type: 'uint8' },
{ name: 'rB', internalType: 'bytes32', type: 'bytes32' },
{ name: 'sB', internalType: 'bytes32', type: 'bytes32' },
],
name: 'closeChannelCooperative',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ name: 'channelId', internalType: 'uint256', type: 'uint256' },
{ name: 'stateHash', internalType: 'bytes32', type: 'bytes32' },
{ name: 'nonce', internalType: 'uint256', type: 'uint256' },
{ name: 'balanceA', internalType: 'uint256', type: 'uint256' },
{ name: 'balanceB', internalType: 'uint256', type: 'uint256' },
{ name: 'vA', internalType: 'uint8', type: 'uint8' },
{ name: 'rA', internalType: 'bytes32', type: 'bytes32' },
{ name: 'sA', internalType: 'bytes32', type: 'bytes32' },
{ name: 'vB', internalType: 'uint8', type: 'uint8' },
{ name: 'rB', internalType: 'bytes32', type: 'bytes32' },
{ name: 'sB', internalType: 'bytes32', type: 'bytes32' },
],
name: 'submitClose',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ name: 'channelId', internalType: 'uint256', type: 'uint256' },
{ name: 'stateHash', internalType: 'bytes32', type: 'bytes32' },
{ name: 'nonce', internalType: 'uint256', type: 'uint256' },
{ name: 'balanceA', internalType: 'uint256', type: 'uint256' },
{ name: 'balanceB', internalType: 'uint256', type: 'uint256' },
{ name: 'vA', internalType: 'uint8', type: 'uint8' },
{ name: 'rA', internalType: 'bytes32', type: 'bytes32' },
{ name: 'sA', internalType: 'bytes32', type: 'bytes32' },
{ name: 'vB', internalType: 'uint8', type: 'uint8' },
{ name: 'rB', internalType: 'bytes32', type: 'bytes32' },
{ name: 'sB', internalType: 'bytes32', type: 'bytes32' },
],
name: 'challengeClose',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [{ name: 'channelId', internalType: 'uint256', type: 'uint256' }],
name: 'finalizeClose',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [{ name: 'channelId', internalType: 'uint256', type: 'uint256' }],
name: 'getChannel',
outputs: [
{
components: [
{ name: 'participantA', internalType: 'address', type: 'address' },
{ name: 'participantB', internalType: 'address', type: 'address' },
{ name: 'depositA', internalType: 'uint256', type: 'uint256' },
{ name: 'depositB', internalType: 'uint256', type: 'uint256' },
{ name: 'status', internalType: 'uint8', type: 'uint8' },
{ name: 'disputeNonce', internalType: 'uint256', type: 'uint256' },
{ name: 'disputeStateHash', internalType: 'bytes32', type: 'bytes32' },
{ name: 'disputeBalanceA', internalType: 'uint256', type: 'uint256' },
{ name: 'disputeBalanceB', internalType: 'uint256', type: 'uint256' },
{ name: 'disputeDeadline', internalType: 'uint256', type: 'uint256' },
],
internalType: 'struct IGenericStateChannelManager.Channel',
name: '',
type: 'tuple',
},
],
stateMutability: 'view',
type: 'function',
},
{ inputs: [], name: 'getChannelCount', outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], stateMutability: 'view', type: 'function' },
{
inputs: [
{ name: 'participantA', internalType: 'address', type: 'address' },
{ name: 'participantB', internalType: 'address', type: 'address' },
],
name: 'getChannelId',
outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [{ name: 'index', internalType: 'uint256', type: 'uint256' }],
name: 'getChannelIdByIndex',
outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
] as const

View File

@@ -0,0 +1,169 @@
export const PAYMENT_CHANNEL_MANAGER_ABI = [
{
inputs: [
{ name: '_admin', internalType: 'address', type: 'address' },
{ name: '_challengeWindowSeconds', internalType: 'uint256', type: 'uint256' },
],
stateMutability: 'nonpayable',
type: 'constructor',
},
{ inputs: [], name: 'admin', outputs: [{ name: '', internalType: 'address', type: 'address' }], stateMutability: 'view', type: 'function' },
{ inputs: [], name: 'paused', outputs: [{ name: '', internalType: 'bool', type: 'bool' }], stateMutability: 'view', type: 'function' },
{ inputs: [], name: 'challengeWindowSeconds', outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], stateMutability: 'view', type: 'function' },
{
inputs: [{ name: 'participantB', internalType: 'address', type: 'address' }],
name: 'openChannel',
outputs: [{ name: 'channelId', internalType: 'uint256', type: 'uint256' }],
stateMutability: 'payable',
type: 'function',
},
{
inputs: [{ name: 'channelId', internalType: 'uint256', type: 'uint256' }],
name: 'fundChannel',
outputs: [],
stateMutability: 'payable',
type: 'function',
},
{
inputs: [
{ name: 'channelId', internalType: 'uint256', type: 'uint256' },
{ name: 'nonce', internalType: 'uint256', type: 'uint256' },
{ name: 'balanceA', internalType: 'uint256', type: 'uint256' },
{ name: 'balanceB', internalType: 'uint256', type: 'uint256' },
{ name: 'vA', internalType: 'uint8', type: 'uint8' },
{ name: 'rA', internalType: 'bytes32', type: 'bytes32' },
{ name: 'sA', internalType: 'bytes32', type: 'bytes32' },
{ name: 'vB', internalType: 'uint8', type: 'uint8' },
{ name: 'rB', internalType: 'bytes32', type: 'bytes32' },
{ name: 'sB', internalType: 'bytes32', type: 'bytes32' },
],
name: 'closeChannelCooperative',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ name: 'channelId', internalType: 'uint256', type: 'uint256' },
{ name: 'nonce', internalType: 'uint256', type: 'uint256' },
{ name: 'balanceA', internalType: 'uint256', type: 'uint256' },
{ name: 'balanceB', internalType: 'uint256', type: 'uint256' },
{ name: 'vA', internalType: 'uint8', type: 'uint8' },
{ name: 'rA', internalType: 'bytes32', type: 'bytes32' },
{ name: 'sA', internalType: 'bytes32', type: 'bytes32' },
{ name: 'vB', internalType: 'uint8', type: 'uint8' },
{ name: 'rB', internalType: 'bytes32', type: 'bytes32' },
{ name: 'sB', internalType: 'bytes32', type: 'bytes32' },
],
name: 'submitClose',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ name: 'channelId', internalType: 'uint256', type: 'uint256' },
{ name: 'nonce', internalType: 'uint256', type: 'uint256' },
{ name: 'balanceA', internalType: 'uint256', type: 'uint256' },
{ name: 'balanceB', internalType: 'uint256', type: 'uint256' },
{ name: 'vA', internalType: 'uint8', type: 'uint8' },
{ name: 'rA', internalType: 'bytes32', type: 'bytes32' },
{ name: 'sA', internalType: 'bytes32', type: 'bytes32' },
{ name: 'vB', internalType: 'uint8', type: 'uint8' },
{ name: 'rB', internalType: 'bytes32', type: 'bytes32' },
{ name: 'sB', internalType: 'bytes32', type: 'bytes32' },
],
name: 'challengeClose',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [{ name: 'channelId', internalType: 'uint256', type: 'uint256' }],
name: 'finalizeClose',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [{ name: 'channelId', internalType: 'uint256', type: 'uint256' }],
name: 'getChannel',
outputs: [
{
components: [
{ name: 'participantA', internalType: 'address', type: 'address' },
{ name: 'participantB', internalType: 'address', type: 'address' },
{ name: 'depositA', internalType: 'uint256', type: 'uint256' },
{ name: 'depositB', internalType: 'uint256', type: 'uint256' },
{ name: 'status', internalType: 'uint8', type: 'uint8' },
{ name: 'disputeNonce', internalType: 'uint256', type: 'uint256' },
{ name: 'disputeBalanceA', internalType: 'uint256', type: 'uint256' },
{ name: 'disputeBalanceB', internalType: 'uint256', type: 'uint256' },
{ name: 'disputeDeadline', internalType: 'uint256', type: 'uint256' },
],
internalType: 'struct IPaymentChannelManager.Channel',
name: '',
type: 'tuple',
},
],
stateMutability: 'view',
type: 'function',
},
{ inputs: [], name: 'getChannelCount', outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], stateMutability: 'view', type: 'function' },
{
inputs: [
{ name: 'participantA', internalType: 'address', type: 'address' },
{ name: 'participantB', internalType: 'address', type: 'address' },
],
name: 'getChannelId',
outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [{ name: 'index', internalType: 'uint256', type: 'uint256' }],
name: 'getChannelIdByIndex',
outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{ inputs: [{ name: 'newAdmin', internalType: 'address', type: 'address' }], name: 'setAdmin', outputs: [], stateMutability: 'nonpayable', type: 'function' },
{ inputs: [{ name: 'newWindow', internalType: 'uint256', type: 'uint256' }], name: 'setChallengeWindow', outputs: [], stateMutability: 'nonpayable', type: 'function' },
{ inputs: [], name: 'pause', outputs: [], stateMutability: 'nonpayable', type: 'function' },
{ inputs: [], name: 'unpause', outputs: [], stateMutability: 'nonpayable', type: 'function' },
{
anonymous: false,
inputs: [
{ indexed: true, name: 'channelId', internalType: 'uint256', type: 'uint256' },
{ indexed: true, name: 'participantA', internalType: 'address', type: 'address' },
{ indexed: true, name: 'participantB', internalType: 'address', type: 'address' },
{ indexed: false, name: 'depositA', internalType: 'uint256', type: 'uint256' },
{ indexed: false, name: 'depositB', internalType: 'uint256', type: 'uint256' },
],
name: 'ChannelOpened',
type: 'event',
},
{
anonymous: false,
inputs: [
{ indexed: true, name: 'channelId', internalType: 'uint256', type: 'uint256' },
{ indexed: false, name: 'balanceA', internalType: 'uint256', type: 'uint256' },
{ indexed: false, name: 'balanceB', internalType: 'uint256', type: 'uint256' },
{ indexed: false, name: 'cooperative', internalType: 'bool', type: 'bool' },
],
name: 'ChannelClosed',
type: 'event',
},
{
anonymous: false,
inputs: [
{ indexed: true, name: 'channelId', internalType: 'uint256', type: 'uint256' },
{ indexed: false, name: 'nonce', internalType: 'uint256', type: 'uint256' },
{ indexed: false, name: 'balanceA', internalType: 'uint256', type: 'uint256' },
{ indexed: false, name: 'balanceB', internalType: 'uint256', type: 'uint256' },
{ indexed: false, name: 'newDeadline', internalType: 'uint256', type: 'uint256' },
],
name: 'ChallengeSubmitted',
type: 'event',
},
] as const

View File

@@ -18,13 +18,13 @@ export default function OffChainServices() {
name: 'State Anchoring Service',
status: 'unknown',
lastUpdate: null,
endpoint: 'http://192.168.11.250:8545', // Chain 138 RPC
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.250:8545',
endpoint: 'http://192.168.11.221:8545',
},
])

View File

@@ -0,0 +1,120 @@
import { useState } from 'react'
import { useChainId } from 'wagmi'
import { useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { CONTRACT_ADDRESSES } from '../../config/contracts'
import { PAYMENT_CHANNEL_MANAGER_ABI } from '../../abis/PaymentChannelManager'
import toast from 'react-hot-toast'
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
return undefined
}
export default function PaymentChannelAdmin() {
const chainId = useChainId()
const managerAddress = getManagerAddress(chainId)
const [newAdmin, setNewAdmin] = useState('')
const [newChallengeWindow, setNewChallengeWindow] = useState('')
const { data: admin } = useReadContract({
address: managerAddress,
abi: PAYMENT_CHANNEL_MANAGER_ABI,
functionName: 'admin',
})
const { data: paused } = useReadContract({
address: managerAddress,
abi: PAYMENT_CHANNEL_MANAGER_ABI,
functionName: 'paused',
})
const { data: challengeWindowSeconds } = useReadContract({
address: managerAddress,
abi: PAYMENT_CHANNEL_MANAGER_ABI,
functionName: 'challengeWindowSeconds',
})
const { data: channelCount } = useReadContract({
address: managerAddress,
abi: PAYMENT_CHANNEL_MANAGER_ABI,
functionName: 'getChannelCount',
})
const { writeContract, data: hash, isPending } = useWriteContract()
const { isLoading: isConfirming } = useWaitForTransactionReceipt({ hash })
const handlePause = () => {
if (!managerAddress) return
writeContract(
{ address: managerAddress, abi: PAYMENT_CHANNEL_MANAGER_ABI, functionName: 'pause' },
{ onSuccess: () => toast.success('Pause submitted'), onError: (e: unknown) => toast.error((e as Error).message) }
)
}
const handleUnpause = () => {
if (!managerAddress) return
writeContract(
{ address: managerAddress, abi: PAYMENT_CHANNEL_MANAGER_ABI, functionName: 'unpause' },
{ onSuccess: () => toast.success('Unpause submitted'), onError: (e: unknown) => toast.error((e as Error).message) }
)
}
const handleSetAdmin = () => {
if (!managerAddress || !newAdmin || !/^0x[a-fA-F0-9]{40}$/.test(newAdmin)) {
toast.error('Invalid admin address')
return
}
writeContract(
{ address: managerAddress, abi: PAYMENT_CHANNEL_MANAGER_ABI, functionName: 'setAdmin', args: [newAdmin as `0x${string}`] },
{ onSuccess: () => { toast.success('Set admin submitted'); setNewAdmin('') }, onError: (e: unknown) => toast.error((e as Error).message) }
)
}
const handleSetChallengeWindow = () => {
const sec = newChallengeWindow.trim()
if (!managerAddress || !sec || isNaN(Number(sec)) || Number(sec) <= 0) {
toast.error('Enter valid seconds (e.g. 86400 for 24h)')
return
}
writeContract(
{ address: managerAddress, abi: PAYMENT_CHANNEL_MANAGER_ABI, functionName: 'setChallengeWindow', args: [BigInt(sec)] },
{ onSuccess: () => { toast.success('Challenge window update submitted'); setNewChallengeWindow('') }, onError: (e: unknown) => toast.error((e as Error).message) }
)
}
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>
</div>
)
}
return (
<div className="space-y-6">
<div className="rounded-xl bg-white/5 p-6">
<h2 className="text-xl font-semibold text-white mb-4">Payment Channel Manager (Admin)</h2>
<dl className="grid gap-2 text-sm">
<div className="flex gap-2"><dt className="text-white/60">Admin:</dt><dd className="text-white font-mono">{admin ?? '—'}</dd></div>
<div className="flex gap-2"><dt className="text-white/60">Paused:</dt><dd className="text-white">{paused != null ? String(paused) : '—'}</dd></div>
<div className="flex gap-2"><dt className="text-white/60">Challenge window:</dt><dd className="text-white">{challengeWindowSeconds != null ? `${Number(challengeWindowSeconds)}s` : '—'}</dd></div>
<div className="flex gap-2"><dt className="text-white/60">Channels:</dt><dd className="text-white">{channelCount != null ? String(channelCount) : '—'}</dd></div>
</dl>
</div>
<div className="rounded-xl bg-white/5 p-6 grid gap-4 md:grid-cols-2">
<div>
<label className="block text-sm text-white/80 mb-1">Pause / Unpause</label>
<div className="flex gap-2">
<button onClick={handlePause} disabled={isPending || isConfirming || !!paused} className="rounded-lg bg-amber-600 px-4 py-2 text-white hover:bg-amber-700 disabled:opacity-50">Pause</button>
<button onClick={handleUnpause} disabled={isPending || isConfirming || !paused} className="rounded-lg bg-emerald-600 px-4 py-2 text-white hover:bg-emerald-700 disabled:opacity-50">Unpause</button>
</div>
</div>
<div>
<label className="block text-sm text-white/80 mb-1">Set admin</label>
<input type="text" placeholder="0x..." value={newAdmin} onChange={(e) => setNewAdmin(e.target.value)} className="w-full rounded-lg bg-black/30 border border-white/20 px-4 py-2 text-white placeholder-white/40 mb-2" />
<button onClick={handleSetAdmin} disabled={isPending || isConfirming} className="rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50">Set admin</button>
</div>
<div>
<label className="block text-sm text-white/80 mb-1">Challenge window (seconds, e.g. 86400 = 24h)</label>
<input type="text" placeholder="86400" value={newChallengeWindow} onChange={(e) => setNewChallengeWindow(e.target.value)} className="w-full rounded-lg bg-black/30 border border-white/20 px-4 py-2 text-white placeholder-white/40 mb-2" />
<button onClick={handleSetChallengeWindow} disabled={isPending || isConfirming} className="rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50">Set window</button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,405 @@
import { useState, useMemo } from 'react'
import { useAccount, useChainId } from 'wagmi'
import { useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { CONTRACT_ADDRESSES } from '../../config/contracts'
import { PAYMENT_CHANNEL_MANAGER_ABI } from '../../abis/PaymentChannelManager'
import toast from 'react-hot-toast'
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
return undefined
}
export default function PaymentChannels() {
const { address } = useAccount()
const chainId = useChainId()
const managerAddress = getManagerAddress(chainId)
const [counterparty, setCounterparty] = useState('')
const [openAmount, setOpenAmount] = useState('')
const [fundChannelId, setFundChannelId] = useState('')
const [fundAmount, setFundAmount] = useState('')
const [finalizeChannelId, setFinalizeChannelId] = useState('')
const [showAdvancedClose, setShowAdvancedClose] = useState(false)
const [closeChannelId, setCloseChannelId] = useState('')
const [closeNonce, setCloseNonce] = useState('')
const [closeBalanceA, setCloseBalanceA] = useState('')
const [closeBalanceB, setCloseBalanceB] = useState('')
const [closeVA, setCloseVA] = useState('')
const [closeRA, setCloseRA] = useState('')
const [closeSA, setCloseSA] = useState('')
const [closeVB, setCloseVB] = useState('')
const [closeRB, setCloseRB] = useState('')
const [closeSB, setCloseSB] = useState('')
const { data: channelCount = 0n } = useReadContract({
address: managerAddress,
abi: PAYMENT_CHANNEL_MANAGER_ABI,
functionName: 'getChannelCount',
})
const { data: paused } = useReadContract({
address: managerAddress,
abi: PAYMENT_CHANNEL_MANAGER_ABI,
functionName: 'paused',
})
const { writeContract, data: hash, isPending } = useWriteContract()
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash })
const channelIds = useMemo(() => {
const n = Number(channelCount)
return Array.from({ length: n }, (_, i) => i)
}, [channelCount])
const openChannel = () => {
if (!managerAddress || !counterparty || !openAmount) {
toast.error('Enter counterparty and amount')
return
}
const wei = BigInt(openAmount) * 10n ** 18n
if (wei <= 0n) {
toast.error('Amount must be > 0')
return
}
writeContract(
{
address: managerAddress,
abi: PAYMENT_CHANNEL_MANAGER_ABI,
functionName: 'openChannel',
args: [counterparty as `0x${string}`],
value: wei,
},
{
onSuccess: () => {
toast.success('Open channel tx submitted')
setCounterparty('')
setOpenAmount('')
},
onError: (e: unknown) => toast.error((e as Error).message),
}
)
}
const fundChannel = () => {
if (!managerAddress || fundChannelId === '' || !fundAmount) {
toast.error('Enter channel ID and amount')
return
}
const wei = BigInt(fundAmount) * 10n ** 18n
if (wei <= 0n) {
toast.error('Amount must be > 0')
return
}
writeContract(
{
address: managerAddress,
abi: PAYMENT_CHANNEL_MANAGER_ABI,
functionName: 'fundChannel',
args: [BigInt(fundChannelId)],
value: wei,
},
{
onSuccess: () => {
toast.success('Fund channel tx submitted')
setFundChannelId('')
setFundAmount('')
},
onError: (e: unknown) => toast.error((e as Error).message),
}
)
}
const doFinalizeClose = () => {
if (!managerAddress || finalizeChannelId === '') {
toast.error('Enter channel ID')
return
}
writeContract(
{
address: managerAddress,
abi: PAYMENT_CHANNEL_MANAGER_ABI,
functionName: 'finalizeClose',
args: [BigInt(finalizeChannelId)],
},
{
onSuccess: () => {
toast.success('Finalize close tx submitted')
setFinalizeChannelId('')
},
onError: (e: unknown) => toast.error((e as Error).message),
}
)
}
const doCooperativeClose = () => {
if (!managerAddress || !closeChannelId || !closeNonce || !closeBalanceA || !closeBalanceB) {
toast.error('Fill channel ID, nonce, balanceA, balanceB')
return
}
const balA = BigInt(closeBalanceA) * 10n ** 18n
const balB = BigInt(closeBalanceB) * 10n ** 18n
const vA = parseInt(closeVA || '0', 10)
const vB = parseInt(closeVB || '0', 10)
if (!closeRA || !closeSA || !closeRB || !closeSB || closeRA.length !== 66 || closeSA.length !== 66 || closeRB.length !== 66 || closeSB.length !== 66) {
toast.error('Enter v (0-255) and 0x-prefixed 32-byte hex for r and s (both parties)')
return
}
writeContract(
{
address: managerAddress,
abi: PAYMENT_CHANNEL_MANAGER_ABI,
functionName: 'closeChannelCooperative',
args: [BigInt(closeChannelId), BigInt(closeNonce), balA, balB, vA, closeRA as `0x${string}`, closeSA as `0x${string}`, vB, closeRB as `0x${string}`, closeSB as `0x${string}`],
},
{
onSuccess: () => { toast.success('Cooperative close submitted'); setCloseChannelId(''); setCloseNonce(''); setCloseBalanceA(''); setCloseBalanceB(''); setCloseVA(''); setCloseRA(''); setCloseSA(''); setCloseVB(''); setCloseRB(''); setCloseSB('') },
onError: (e: unknown) => toast.error((e as Error).message),
}
)
}
const doSubmitClose = () => {
if (!managerAddress || !closeChannelId || !closeNonce || !closeBalanceA || !closeBalanceB) {
toast.error('Fill channel ID, nonce, balanceA, balanceB')
return
}
const balA = BigInt(closeBalanceA) * 10n ** 18n
const balB = BigInt(closeBalanceB) * 10n ** 18n
const vA = parseInt(closeVA || '0', 10)
const vB = parseInt(closeVB || '0', 10)
if (!closeRA || !closeSA || !closeRB || !closeSB) {
toast.error('Enter v (0-255) and 0x-prefixed hex for r, s (both parties)')
return
}
writeContract(
{
address: managerAddress,
abi: PAYMENT_CHANNEL_MANAGER_ABI,
functionName: 'submitClose',
args: [BigInt(closeChannelId), BigInt(closeNonce), balA, balB, vA, closeRA as `0x${string}`, closeSA as `0x${string}`, vB, closeRB as `0x${string}`, closeSB as `0x${string}`],
},
{
onSuccess: () => { toast.success('Submit close tx submitted'); setCloseChannelId(''); setCloseNonce(''); setCloseBalanceA(''); setCloseBalanceB('') },
onError: (e: unknown) => toast.error((e as Error).message),
}
)
}
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>
</div>
)
}
return (
<div className="space-y-8">
<div className="rounded-xl bg-white/5 p-6">
<h2 className="text-xl font-semibold text-white mb-4">Payment Channels</h2>
{paused && (
<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.
</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>
<ul className="list-disc list-inside ml-2 space-y-0.5">
<li>
<a href="https://docs.connext.network/" target="_blank" rel="noopener noreferrer" className="text-blue-400 hover:underline">Connext</a>
{' '}(cross-chain, xcalls, routed payments)
</li>
<li>
<a href="https://docs.raiden.network/" target="_blank" rel="noopener noreferrer" className="text-blue-400 hover:underline">Raiden</a>
{' '}(payment channel network, path finding)
</li>
<li>
<a href="https://docs.statechannels.org/" target="_blank" rel="noopener noreferrer" className="text-blue-400 hover:underline">Statechannels.org</a>
{' '}(general state channel framework)
</li>
</ul>
<p>See docs/channels/EXTERNAL_PROTOCOL_INTEGRATION.md and docs/channels/STATE_CHANNELS_VS_PAYMENT_CHANNELS.md.</p>
</div>
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<label className="block text-sm text-white/80">Open channel (you deposit)</label>
<input
type="text"
placeholder="Counterparty address 0x..."
value={counterparty}
onChange={(e) => setCounterparty(e.target.value)}
className="w-full rounded-lg bg-black/30 border border-white/20 px-4 py-2 text-white placeholder-white/40"
/>
<input
type="text"
placeholder="Amount (ETH)"
value={openAmount}
onChange={(e) => setOpenAmount(e.target.value)}
className="w-full rounded-lg bg-black/30 border border-white/20 px-4 py-2 text-white placeholder-white/40"
/>
<button
onClick={openChannel}
disabled={isPending || isConfirming || !!paused}
className="rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
>
{isPending || isConfirming ? 'Confirming...' : 'Open channel'}
</button>
</div>
<div className="space-y-2">
<label className="block text-sm text-white/80">Fund channel (participant B)</label>
<input
type="text"
placeholder="Channel ID"
value={fundChannelId}
onChange={(e) => setFundChannelId(e.target.value)}
className="w-full rounded-lg bg-black/30 border border-white/20 px-4 py-2 text-white placeholder-white/40"
/>
<input
type="text"
placeholder="Amount (ETH)"
value={fundAmount}
onChange={(e) => setFundAmount(e.target.value)}
className="w-full rounded-lg bg-black/30 border border-white/20 px-4 py-2 text-white placeholder-white/40"
/>
<button
onClick={fundChannel}
disabled={isPending || isConfirming || !!paused}
className="rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
>
{isPending || isConfirming ? 'Confirming...' : 'Fund channel'}
</button>
</div>
</div>
<div className="mt-6 space-y-2">
<label className="block text-sm text-white/80">Finalize close (after challenge window)</label>
<input
type="text"
placeholder="Channel ID"
value={finalizeChannelId}
onChange={(e) => setFinalizeChannelId(e.target.value)}
className="w-full max-w-xs rounded-lg bg-black/30 border border-white/20 px-4 py-2 text-white placeholder-white/40"
/>
<button
onClick={doFinalizeClose}
disabled={isPending || isConfirming}
className="rounded-lg bg-emerald-600 px-4 py-2 text-white hover:bg-emerald-700 disabled:opacity-50"
>
{isPending || isConfirming ? 'Confirming...' : 'Finalize close'}
</button>
</div>
<div className="mt-6 border-t border-white/20 pt-4">
<button type="button" onClick={() => setShowAdvancedClose((x) => !x)} className="text-sm text-blue-400 hover:underline">
{showAdvancedClose ? 'Hide' : 'Show'} cooperative close / submit close
</button>
{showAdvancedClose && (
<div className="mt-3 p-4 rounded-lg bg-black/20 space-y-3 text-sm">
<p className="text-white/70">Both parties must sign the state off-chain: hash = keccak256(channelId, nonce, balanceA, balanceB); then sign with wallet (e.g. personal_sign). Use the same nonce and balances below. Enter v (27 or 28), r and s as 0x-prefixed 32-byte hex.</p>
<input type="text" placeholder="Channel ID" value={closeChannelId} onChange={(e) => setCloseChannelId(e.target.value)} className="w-full rounded bg-black/30 border border-white/20 px-3 py-2 text-white" />
<input type="text" placeholder="Nonce" value={closeNonce} onChange={(e) => setCloseNonce(e.target.value)} className="w-full rounded bg-black/30 border border-white/20 px-3 py-2 text-white" />
<input type="text" placeholder="Balance A (ETH)" value={closeBalanceA} onChange={(e) => setCloseBalanceA(e.target.value)} className="w-full rounded bg-black/30 border border-white/20 px-3 py-2 text-white" />
<input type="text" placeholder="Balance B (ETH)" value={closeBalanceB} onChange={(e) => setCloseBalanceB(e.target.value)} className="w-full rounded bg-black/30 border border-white/20 px-3 py-2 text-white" />
<div className="grid grid-cols-2 gap-2">
<span className="text-white/60">Party A sig: v</span>
<input type="text" placeholder="27 or 28" value={closeVA} onChange={(e) => setCloseVA(e.target.value)} className="rounded bg-black/30 border border-white/20 px-2 py-1 text-white" />
<span className="text-white/60">r (0x...)</span>
<input type="text" placeholder="0x..." value={closeRA} onChange={(e) => setCloseRA(e.target.value)} className="rounded bg-black/30 border border-white/20 px-2 py-1 text-white font-mono" />
<span className="text-white/60">s (0x...)</span>
<input type="text" placeholder="0x..." value={closeSA} onChange={(e) => setCloseSA(e.target.value)} className="rounded bg-black/30 border border-white/20 px-2 py-1 text-white font-mono" />
<span className="text-white/60">Party B sig: v</span>
<input type="text" placeholder="27 or 28" value={closeVB} onChange={(e) => setCloseVB(e.target.value)} className="rounded bg-black/30 border border-white/20 px-2 py-1 text-white" />
<span className="text-white/60">r (0x...)</span>
<input type="text" placeholder="0x..." value={closeRB} onChange={(e) => setCloseRB(e.target.value)} className="rounded bg-black/30 border border-white/20 px-2 py-1 text-white font-mono" />
<span className="text-white/60">s (0x...)</span>
<input type="text" placeholder="0x..." value={closeSB} onChange={(e) => setCloseSB(e.target.value)} className="rounded bg-black/30 border border-white/20 px-2 py-1 text-white font-mono" />
</div>
<div className="flex gap-2">
<button onClick={doCooperativeClose} disabled={isPending || isConfirming} className="rounded bg-blue-600 px-3 py-1.5 text-white text-sm disabled:opacity-50">Close cooperatively</button>
<button onClick={doSubmitClose} disabled={isPending || isConfirming} className="rounded bg-amber-600 px-3 py-1.5 text-white text-sm disabled:opacity-50">Submit close (start dispute)</button>
</div>
</div>
)}
</div>
</div>
<ChannelList managerAddress={managerAddress} channelIds={channelIds} currentUser={address} />
</div>
)
}
function ChannelList({
managerAddress,
channelIds,
currentUser,
}: {
managerAddress: `0x${string}`
channelIds: number[]
currentUser: string | undefined
}) {
return (
<div className="rounded-xl bg-white/5 p-6">
<h3 className="text-lg font-semibold text-white mb-4">Channels ({channelIds.length})</h3>
<div className="space-y-3">
{channelIds.length === 0 && <p className="text-white/60 text-sm">No channels yet.</p>}
{channelIds.map((idx) => (
<ChannelRow key={idx} managerAddress={managerAddress} index={idx} currentUser={currentUser} />
))}
</div>
</div>
)
}
function ChannelRow({
managerAddress,
index,
currentUser,
}: {
managerAddress: `0x${string}`
index: number
currentUser: string | undefined
}) {
const { data: channelId } = useReadContract({
address: managerAddress,
abi: PAYMENT_CHANNEL_MANAGER_ABI,
functionName: 'getChannelIdByIndex',
args: [BigInt(index)],
})
const { data: ch } = useReadContract({
address: managerAddress,
abi: PAYMENT_CHANNEL_MANAGER_ABI,
functionName: 'getChannel',
args: channelId !== undefined ? [channelId] : undefined,
})
if (ch == null || channelId == null) return null
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">
<span className="text-white/60">ID: {String(channelId)}</span>
<span className="text-white/80">A: {ch.participantA.slice(0, 10)}...</span>
<span className="text-white/80">B: {ch.participantB.slice(0, 10)}...</span>
<span className="text-white/80">Deposits: {(Number(ch.depositA) / 1e18).toFixed(4)} / {(Number(ch.depositB) / 1e18).toFixed(4)} ETH</span>
<span className={status === 1 ? 'text-green-400' : status === 2 ? 'text-amber-400' : 'text-white/60'}>
{statusLabel}
</span>
{isMine && <span className="text-blue-300">(yours)</span>}
</div>
{status === 2 && ch.disputeDeadline > 0n && (
<p className="text-amber-200 text-xs mt-1">
Dispute deadline: {new Date(Number(ch.disputeDeadline) * 1000).toISOString()}
</p>
)}
</div>
)
}

View File

@@ -0,0 +1,189 @@
import { useState, useMemo } from 'react'
import { useAccount, useChainId } from 'wagmi'
import { useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { CONTRACT_ADDRESSES } from '../../config/contracts'
import { GENERIC_STATE_CHANNEL_MANAGER_ABI } from '../../abis/GenericStateChannelManager'
import toast from 'react-hot-toast'
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
return undefined
}
export default function StateChannels() {
const { address } = useAccount()
const chainId = useChainId()
const managerAddress = getManagerAddress(chainId)
const [counterparty, setCounterparty] = useState('')
const [openAmount, setOpenAmount] = useState('')
const [fundChannelId, setFundChannelId] = useState('')
const [fundAmount, setFundAmount] = useState('')
const [finalizeChannelId, setFinalizeChannelId] = useState('')
const { data: channelCount = 0n } = useReadContract({
address: managerAddress,
abi: GENERIC_STATE_CHANNEL_MANAGER_ABI,
functionName: 'getChannelCount',
})
const { data: paused } = useReadContract({
address: managerAddress,
abi: GENERIC_STATE_CHANNEL_MANAGER_ABI,
functionName: 'paused',
})
const { writeContract, data: hash, isPending } = useWriteContract()
const { isLoading: isConfirming } = useWaitForTransactionReceipt({ hash })
const channelIds = useMemo(() => Array.from({ length: Number(channelCount) }, (_, i) => i), [channelCount])
const openChannel = () => {
if (!managerAddress || !counterparty || !openAmount) {
toast.error('Enter counterparty and amount')
return
}
const wei = BigInt(openAmount) * 10n ** 18n
if (wei <= 0n) {
toast.error('Amount must be > 0')
return
}
writeContract(
{
address: managerAddress,
abi: GENERIC_STATE_CHANNEL_MANAGER_ABI,
functionName: 'openChannel',
args: [counterparty as `0x${string}`],
value: wei,
},
{
onSuccess: () => { toast.success('Open channel tx submitted'); setCounterparty(''); setOpenAmount('') },
onError: (e: unknown) => toast.error((e as Error).message),
}
)
}
const fundChannel = () => {
if (!managerAddress || fundChannelId === '' || !fundAmount) {
toast.error('Enter channel ID and amount')
return
}
const wei = BigInt(fundAmount) * 10n ** 18n
if (wei <= 0n) { toast.error('Amount must be > 0'); return }
writeContract(
{
address: managerAddress,
abi: GENERIC_STATE_CHANNEL_MANAGER_ABI,
functionName: 'fundChannel',
args: [BigInt(fundChannelId)],
value: wei,
},
{
onSuccess: () => { toast.success('Fund channel tx submitted'); setFundChannelId(''); setFundAmount('') },
onError: (e: unknown) => toast.error((e as Error).message),
}
)
}
const doFinalizeClose = () => {
if (!managerAddress || finalizeChannelId === '') { toast.error('Enter channel ID'); return }
writeContract(
{
address: managerAddress,
abi: GENERIC_STATE_CHANNEL_MANAGER_ABI,
functionName: 'finalizeClose',
args: [BigInt(finalizeChannelId)],
},
{
onSuccess: () => { toast.success('Finalize close tx submitted'); setFinalizeChannelId('') },
onError: (e: unknown) => toast.error((e as Error).message),
}
)
}
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>
</div>
)
}
return (
<div className="space-y-8">
<div className="rounded-xl bg-white/5 p-6">
<h2 className="text-xl font-semibold text-white mb-4">State Channels (with stateHash)</h2>
{paused && <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">
Same as payment channels but settlement commits to a <code className="text-white/90">stateHash</code> (e.g. hash of game result or attestation). Open, fund, then close with stateHash + balances.
</p>
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<label className="block text-sm text-white/80">Open channel</label>
<input type="text" placeholder="Counterparty 0x..." value={counterparty} onChange={(e) => setCounterparty(e.target.value)} className="w-full rounded-lg bg-black/30 border border-white/20 px-4 py-2 text-white placeholder-white/40" />
<input type="text" placeholder="Amount (ETH)" value={openAmount} onChange={(e) => setOpenAmount(e.target.value)} className="w-full rounded-lg bg-black/30 border border-white/20 px-4 py-2 text-white placeholder-white/40" />
<button onClick={openChannel} disabled={!!(isPending || isConfirming || paused)} className="rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50">{isPending || isConfirming ? 'Confirming...' : 'Open channel'}</button>
</div>
<div className="space-y-2">
<label className="block text-sm text-white/80">Fund channel (participant B)</label>
<input type="text" placeholder="Channel ID" value={fundChannelId} onChange={(e) => setFundChannelId(e.target.value)} className="w-full rounded-lg bg-black/30 border border-white/20 px-4 py-2 text-white placeholder-white/40" />
<input type="text" placeholder="Amount (ETH)" value={fundAmount} onChange={(e) => setFundAmount(e.target.value)} className="w-full rounded-lg bg-black/30 border border-white/20 px-4 py-2 text-white placeholder-white/40" />
<button onClick={fundChannel} disabled={!!(isPending || isConfirming || paused)} className="rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50">{isPending || isConfirming ? 'Confirming...' : 'Fund channel'}</button>
</div>
</div>
<div className="mt-6 space-y-2">
<label className="block text-sm text-white/80">Finalize close (after challenge window)</label>
<input type="text" placeholder="Channel ID" value={finalizeChannelId} onChange={(e) => setFinalizeChannelId(e.target.value)} className="w-full max-w-xs rounded-lg bg-black/30 border border-white/20 px-4 py-2 text-white placeholder-white/40" />
<button onClick={doFinalizeClose} disabled={!!(isPending || isConfirming)} className="rounded-lg bg-emerald-600 px-4 py-2 text-white hover:bg-emerald-700 disabled:opacity-50">{isPending || isConfirming ? 'Confirming...' : 'Finalize close'}</button>
</div>
</div>
<StateChannelList managerAddress={managerAddress} channelIds={channelIds} currentUser={address} />
</div>
)
}
function StateChannelList({ managerAddress, channelIds, currentUser }: { managerAddress: `0x${string}`; channelIds: number[]; currentUser: string | undefined }) {
return (
<div className="rounded-xl bg-white/5 p-6">
<h3 className="text-lg font-semibold text-white mb-4">Channels ({channelIds.length})</h3>
{channelIds.length === 0 && <p className="text-white/60 text-sm">No channels yet.</p>}
{channelIds.map((idx) => (
<StateChannelRow key={idx} managerAddress={managerAddress} index={idx} currentUser={currentUser} />
))}
</div>
)
}
function StateChannelRow({ managerAddress, index, currentUser }: { managerAddress: `0x${string}`; index: number; currentUser: string | undefined }) {
const { data: channelId } = useReadContract({
address: managerAddress,
abi: GENERIC_STATE_CHANNEL_MANAGER_ABI,
functionName: 'getChannelIdByIndex',
args: [BigInt(index)],
})
const { data: ch } = useReadContract({
address: managerAddress,
abi: GENERIC_STATE_CHANNEL_MANAGER_ABI,
functionName: 'getChannel',
args: channelId !== undefined ? [channelId] : undefined,
})
if (ch == null || channelId == null) return null
const status = CHANNEL_STATUS[Number(ch.status)] ?? 'Unknown'
const isMine = currentUser && (ch.participantA.toLowerCase() === currentUser.toLowerCase() || ch.participantB.toLowerCase() === currentUser.toLowerCase())
return (
<div className="rounded-lg border border-white/20 p-4 bg-black/20 mb-2">
<div className="flex flex-wrap items-center gap-2 text-sm">
<span className="text-white/60">ID: {String(channelId)}</span>
<span className="text-white/80">A: {ch.participantA.slice(0, 10)}...</span>
<span className="text-white/80">B: {ch.participantB.slice(0, 10)}...</span>
<span className="text-white/80">Deposits: {(Number(ch.depositA) / 1e18).toFixed(4)} / {(Number(ch.depositB) / 1e18).toFixed(4)} ETH</span>
<span className="text-white/60">{status}</span>
{isMine && <span className="text-blue-300">(yours)</span>}
</div>
{Number(ch.status) === 2 && ch.disputeDeadline > 0n && (
<p className="text-amber-200 text-xs mt-1">Dispute deadline: {new Date(Number(ch.disputeDeadline) * 1000).toISOString()}</p>
)}
</div>
)
}

View File

@@ -4,12 +4,12 @@
import { useState } from 'react'
import { useAccount, usePublicClient, useWalletClient } from 'wagmi'
// Note: Safe SDK integration requires ethers.js v5 - needs adapter for viem/wagmi
// Note: Safe deployment uses @safe-global/protocol-kit (ethers v5). Needs adapter for viem/wagmi.
// For now, this component demonstrates the structure. Actual deployment would require:
// 1. Converting viem/wagmi to ethers.js providers
// 2. Creating EthersAdapter
// 3. Using SafeFactory to deploy
// import { SafeFactory, SafeAccountConfig } from '@safe-global/safe-core-sdk'
// 2. Creating EthersAdapter from @safe-global/protocol-kit
// 3. Using SafeFactory from protocol-kit to deploy
// import { SafeFactory, SafeAccountConfig, EthersAdapter } from '@safe-global/protocol-kit'
import { validateAddress } from '../../utils/security'
import toast from 'react-hot-toast'

View File

@@ -0,0 +1,168 @@
/**
* Chain Management Dashboard
* Admin UI for managing all supported chains, adapters, and deployments
*/
import { useState, useEffect } from 'react';
import { useAccount } from 'wagmi';
import { ethers } from 'ethers';
import toast from 'react-hot-toast';
interface ChainMetadata {
chainId: number;
chainIdentifier: string;
chainType: string;
adapter: string;
isActive: boolean;
explorerUrl: string;
minConfirmations: number;
avgBlockTime: number;
}
export default function ChainManagementDashboard() {
const { address, isConnected } = useAccount();
const [chains, setChains] = useState<ChainMetadata[]>([]);
const [loading, setLoading] = useState(true);
const [selectedChain, setSelectedChain] = useState<string>('');
useEffect(() => {
if (isConnected) {
loadChains();
}
}, [isConnected, address]);
const loadChains = async () => {
try {
setLoading(true);
// TODO: Connect to ChainRegistry contract
// const provider = new ethers.JsonRpcProvider(process.env.NEXT_PUBLIC_RPC_URL);
// const registry = new ethers.Contract(REGISTRY_ADDRESS, REGISTRY_ABI, provider);
// const [evmChainIds, evmChains] = await registry.getAllEVMChains();
// const [nonEvmIds, nonEvmChains] = await registry.getAllNonEVMChains();
// Mock data for now
setChains([
{
chainId: 138,
chainIdentifier: 'EVM-138',
chainType: 'EVM',
adapter: '0x...',
isActive: true,
explorerUrl: 'https://explorer.d-bis.org',
minConfirmations: 12,
avgBlockTime: 2
},
{
chainId: 50,
chainIdentifier: 'EVM-50',
chainType: 'XDC',
adapter: '0x...',
isActive: true,
explorerUrl: 'https://explorer.xdc.network',
minConfirmations: 12,
avgBlockTime: 2
}
]);
} catch (error: any) {
toast.error(`Failed to load chains: ${error.message}`);
} finally {
setLoading(false);
}
};
const toggleChain = async (chainId: number, chainIdentifier: string, 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}`);
}
};
if (!isConnected) {
return (
<div className="p-6 bg-white/10 rounded-xl">
<p className="text-white/70">Please connect your wallet to manage chains.</p>
</div>
);
}
return (
<div className="space-y-6">
<div className="bg-black/20 rounded-xl p-6 border border-white/10">
<h2 className="text-xl font-bold text-white mb-4">Chain Management</h2>
{loading ? (
<div className="text-white/70">Loading chains...</div>
) : (
<div className="space-y-4">
{chains.map((chain) => (
<div
key={chain.chainId || chain.chainIdentifier}
className="bg-white/5 rounded-lg p-4 flex items-center justify-between"
>
<div>
<h3 className="text-white font-semibold">
{chain.chainIdentifier} ({chain.chainType})
</h3>
<p className="text-white/70 text-sm">
Adapter: {chain.adapter.slice(0, 10)}...
</p>
<p className="text-white/70 text-sm">
Explorer: <a href={chain.explorerUrl} target="_blank" rel="noopener noreferrer" className="text-blue-400 hover:underline">{chain.explorerUrl}</a>
</p>
</div>
<div className="flex items-center gap-4">
<span className={`px-3 py-1 rounded-full text-sm ${
chain.isActive
? 'bg-green-500/20 text-green-200'
: 'bg-red-500/20 text-red-200'
}`}>
{chain.isActive ? 'Active' : 'Inactive'}
</span>
<button
onClick={() => toggleChain(chain.chainId, chain.chainIdentifier, chain.isActive)}
className={`px-4 py-2 rounded-lg font-semibold transition-colors ${
chain.isActive
? 'bg-red-600 hover:bg-red-700 text-white'
: 'bg-green-600 hover:bg-green-700 text-white'
}`}
>
{chain.isActive ? 'Disable' : 'Enable'}
</button>
</div>
</div>
))}
</div>
)}
</div>
<div className="bg-black/20 rounded-xl p-6 border border-white/10">
<h3 className="text-lg font-bold text-white mb-4">Add New Chain</h3>
<div className="space-y-4">
<select
value={selectedChain}
onChange={(e) => setSelectedChain(e.target.value)}
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
>
<option value="">Select chain to add...</option>
<option value="polygon">Polygon</option>
<option value="arbitrum">Arbitrum</option>
<option value="optimism">Optimism</option>
<option value="base">Base</option>
<option value="avalanche">Avalanche</option>
<option value="bsc">BSC</option>
<option value="ethereum">Ethereum Mainnet</option>
</select>
<button
onClick={() => toast.info('Deployment feature coming soon')}
className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-semibold"
>
Deploy Chain Adapter
</button>
</div>
</div>
</div>
);
}

View File

@@ -19,22 +19,49 @@ import {
BRIDGE_ABI,
WETH9_ABI,
ERC20_ABI,
CCIP_DESTINATIONS,
} from '../../config/bridge';
import CopyButton from '../ui/CopyButton';
import ConfirmationModal from '../ui/ConfirmationModal';
import Tooltip from '../ui/Tooltip';
import LoadingSkeleton from '../ui/LoadingSkeleton';
import ChainIcon from '../ui/ChainIcon';
import TokenIcon from '../ui/TokenIcon';
interface BridgeButtonsProps {
destinationChainSelector?: string; // Defaults to Ethereum Mainnet
recipientAddress?: string; // Defaults to connected wallet
}
export default function BridgeButtons({
destinationChainSelector = CHAIN_SELECTORS.ETHEREUM_MAINNET,
/** Fetches CCIP fee only when amount > 0 (bridge reverts on zero). Reports result to parent via onFee. */
function CalculateFeeFetcher({
bridgeContract,
destinationChainSelector,
amountWei,
onFee,
}: {
bridgeContract: NonNullable<ReturnType<typeof useContract>['contract']>;
destinationChainSelector: string;
amountWei: ethers.BigNumber;
onFee: (value: ethers.BigNumber | null) => void;
}) {
const { data } = useContractRead(bridgeContract, 'calculateFee', [
destinationChainSelector,
amountWei,
]);
useEffect(() => {
if (data != null) onFee(ethers.BigNumber.from(data.toString()));
return () => {};
}, [data, onFee]);
return null;
}
/** Inner bridge form: only mounted when address is set so balance/allowance are never called with zero address. */
function BridgeButtonsConnected({
address,
destinationChainSelector,
recipientAddress,
}: BridgeButtonsProps) {
const address = useAddress();
}: BridgeButtonsProps & { address: string }) {
const [amount, setAmount] = useState<string>('');
const [recipient, setRecipient] = useState<string>(recipientAddress || address || '');
const [isWrapping, setIsWrapping] = useState(false);
@@ -45,46 +72,42 @@ export default function BridgeButtons({
const [showBridgeModal, setShowBridgeModal] = useState(false);
const [amountError, setAmountError] = useState<string>('');
const [recipientError, setRecipientError] = useState<string>('');
const [ccipFee, setCcipFee] = useState<ethers.BigNumber | null>(null);
// Contracts
const { contract: weth9Contract } = useContract(CONTRACTS.WETH9, WETH9_ABI);
const { contract: bridgeContract } = useContract(CONTRACTS.WETH9_BRIDGE, BRIDGE_ABI);
const { contract: linkContract } = useContract(CONTRACTS.LINK_TOKEN, ERC20_ABI);
// Balances with refresh
const { data: ethBalance, refetch: refetchEthBalance } = useBalance();
// Only query when we have a real address (not zero address) to avoid revert errors
const { data: weth9Balance, refetch: refetchWeth9Balance } = useContractRead(
weth9Contract,
'balanceOf',
weth9Contract && address ? [address] : undefined
weth9Contract ? [address] : undefined
);
const { data: linkBalance, refetch: refetchLinkBalance } = useContractRead(
linkContract,
'balanceOf',
linkContract && address ? [address] : undefined
linkContract ? [address] : undefined
);
// Allowances
const { data: weth9Allowance, refetch: refetchWeth9Allowance } = useContractRead(
weth9Contract,
'allowance',
weth9Contract && bridgeContract && address ? [address, CONTRACTS.WETH9_BRIDGE] : undefined
weth9Contract && bridgeContract ? [address, CONTRACTS.WETH9_BRIDGE] : undefined
);
const { data: linkAllowance, refetch: refetchLinkAllowance } = useContractRead(
linkContract,
'allowance',
linkContract && bridgeContract && address ? [address, CONTRACTS.WETH9_BRIDGE] : undefined
linkContract && bridgeContract ? [address, CONTRACTS.WETH9_BRIDGE] : undefined
);
// Update recipient when address changes
const amountWei = amount && !isNaN(parseFloat(amount)) && parseFloat(amount) > 0
? ethers.utils.parseEther(amount)
: ethers.BigNumber.from(0);
useEffect(() => {
if (!recipientAddress && address) {
setRecipient(address);
}
if (!recipientAddress) setRecipient((r) => r || address);
}, [address, recipientAddress]);
// Validate amount on change
useEffect(() => {
if (!amount) {
setAmountError('');
@@ -100,31 +123,17 @@ export default function BridgeButtons({
}
}, [amount, ethBalance]);
// Validate recipient on change
useEffect(() => {
if (!recipient) {
setRecipientError('');
return;
}
if (!ethers.utils.isAddress(recipient)) {
setRecipientError('Invalid Ethereum address');
} else {
setRecipientError('');
}
setRecipientError(ethers.utils.isAddress(recipient) ? '' : 'Invalid Ethereum address');
}, [recipient]);
// Fee calculation
const amountWei = amount && !isNaN(parseFloat(amount)) && parseFloat(amount) > 0
? ethers.utils.parseEther(amount)
: ethers.BigNumber.from(0);
// Only calculate fee when we have a valid amount and bridge contract
// This prevents revert errors when amount is zero
const { data: ccipFee } = useContractRead(
bridgeContract,
'calculateFee',
bridgeContract && amountWei.gt(0) ? [destinationChainSelector, amountWei] : undefined
);
useEffect(() => {
if (!amountWei.gt(0)) setCcipFee(null);
}, [amountWei]);
// Write operations
const { mutateAsync: wrapETH } = useContractWrite(weth9Contract, 'deposit');
@@ -221,13 +230,12 @@ export default function BridgeButtons({
}
// Approve LINK for fees (if needed)
if (ccipFee && ethers.BigNumber.from(ccipFee.toString()).gt(0)) {
if (ccipFee?.gt(0)) {
const linkAllowanceBN = linkAllowance
? ethers.BigNumber.from(linkAllowance.toString())
: ethers.BigNumber.from(0);
const feeBN = ethers.BigNumber.from(ccipFee.toString());
if (linkAllowanceBN.lt(feeBN)) {
if (linkAllowanceBN.lt(ccipFee)) {
const linkTx = await approveLINK({
args: [CONTRACTS.WETH9_BRIDGE, maxApproval],
});
@@ -287,14 +295,13 @@ export default function BridgeButtons({
return;
}
if (ccipFee && ethers.BigNumber.from(ccipFee.toString()).gt(0)) {
if (ccipFee?.gt(0)) {
const linkBalanceBN = linkBalance
? ethers.BigNumber.from(linkBalance.toString())
: ethers.BigNumber.from(0);
const feeBN = ethers.BigNumber.from(ccipFee.toString());
if (linkBalanceBN.lt(feeBN)) {
const feeFormatted = ethers.utils.formatEther(feeBN);
if (linkBalanceBN.lt(ccipFee)) {
const feeFormatted = ethers.utils.formatEther(ccipFee);
toast.error(`Insufficient LINK for fees. Required: ${feeFormatted} LINK`);
return;
}
@@ -357,29 +364,36 @@ export default function BridgeButtons({
recipient &&
ethers.utils.isAddress(recipient);
const destination = CCIP_DESTINATIONS.find((d) => d.selector === destinationChainSelector);
return (
<div className="w-full max-w-4xl mx-auto">
<div className="bg-white/5 backdrop-blur-2xl rounded-3xl shadow-2xl border-2 border-cyan-400/30 p-8 md:p-10 animate-fadeIn relative overflow-hidden portal-glow portal-entrance">
{/* Portal ring effects */}
<div className="absolute inset-0 rounded-3xl border-4 border-cyan-400/20 shadow-[0_0_60px_rgba(34,211,238,0.4)] portal-ring pointer-events-none" />
<div className="absolute inset-4 rounded-3xl border-2 border-purple-400/15 shadow-[0_0_40px_rgba(168,85,247,0.3)] pointer-events-none" />
{/* Decorative gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-br from-cyan-500/10 via-purple-500/10 to-pink-500/10 pointer-events-none" />
<div className="absolute top-0 right-0 w-96 h-96 bg-cyan-500/10 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2 animate-pulse" />
<div className="absolute bottom-0 left-0 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl translate-y-1/2 -translate-x-1/2 animate-pulse" style={{ animationDelay: '1s' }} />
<div className="relative z-10">
<div className="w-full">
{bridgeContract && amountWei.gt(0) && (
<CalculateFeeFetcher
bridgeContract={bridgeContract}
destinationChainSelector={destinationChainSelector}
amountWei={amountWei}
onFee={setCcipFee}
/>
)}
<div className="relative">
<div className="mb-6">
<div className="flex items-center justify-between mb-2">
<h2 className="text-4xl font-black holographic-text drop-shadow-lg">
Bridge to Ethereum Mainnet
<h2 className="text-[20px] font-semibold text-white flex items-center gap-2">
Bridge to
{destination && (
<>
<ChainIcon chainId={destination.chainId} name={destination.name} size={24} />
<span>{destination.name}</span>
</>
)}
{!destination && <span>Ethereum Mainnet</span>}
</h2>
<Tooltip content="Refresh all balances and allowances">
<button
onClick={handleRefreshBalances}
disabled={!address}
className="p-2 text-cyan-300 hover:text-cyan-200 hover:bg-cyan-500/20 rounded-lg transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed border border-cyan-400/30 hover:border-cyan-400/60 hover:shadow-[0_0_15px_rgba(34,211,238,0.4)]"
className="p-2 text-teal-400 hover:text-teal-300 hover:bg-teal-500/20 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed border border-white/20"
aria-label="Refresh balances"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -388,16 +402,17 @@ export default function BridgeButtons({
</button>
</Tooltip>
</div>
<p className="text-white/80 text-lg font-medium mt-2">
Wrap ETH, approve tokens, and bridge WETH9 to Ethereum Mainnet via CCIP
<p className="text-[#A0A0A0] text-sm mt-1">
Wrap ETH, approve tokens, and bridge WETH9 via CCIP
</p>
</div>
<div className="mb-8">
<label className="block text-sm font-bold mb-4 text-white/90 uppercase tracking-wider">
Amount (ETH)
<label className="flex items-center gap-2 text-[13px] font-medium mb-2 text-[#A0A0A0]">
<TokenIcon symbol="ETH" size={20} />
<span>Amount (ETH)</span>
<Tooltip content="Enter the amount of ETH you want to bridge. This will be wrapped to WETH9 first.">
<span className="ml-2 text-white/60 cursor-help text-xs"></span>
<span className="text-white/60 cursor-help text-xs"></span>
</Tooltip>
</label>
<div className="relative">
@@ -407,10 +422,10 @@ export default function BridgeButtons({
min="0"
value={amount}
onChange={(e) => setAmount(e.target.value)}
className={`w-full p-5 border-2 rounded-2xl focus:ring-4 transition-all text-xl font-bold bg-black/20 backdrop-blur-xl shadow-xl text-cyan-100 placeholder:text-cyan-400/50 ${
className={`w-full min-h-[48px] pl-4 pr-24 py-3 border rounded-xl focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 focus:ring-offset-[#252830] transition-all text-lg bg-[#1a1d24] text-white placeholder:text-[#A0A0A0] hover:border-white/30 ${
amountError
? 'border-red-400 focus:border-red-500 focus:ring-red-500/30 focus:shadow-[0_0_20px_rgba(239,68,68,0.5)]'
: 'border-cyan-400/30 focus:border-cyan-400 focus:ring-cyan-400/20 focus:shadow-[0_0_20px_rgba(34,211,238,0.5)]'
? 'border-red-500 focus:border-red-500'
: 'border-white/20 focus:border-teal-500'
}`}
placeholder="0.0"
aria-invalid={!!amountError}
@@ -422,7 +437,7 @@ export default function BridgeButtons({
setAmount(ethBalance.displayValue);
setAmountError('');
}}
className="absolute right-4 top-1/2 -translate-y-1/2 px-4 py-2 text-sm font-bold bg-gradient-to-r from-cyan-500 to-purple-500 text-white rounded-xl hover:from-cyan-400 hover:to-purple-400 transition-all shadow-[0_0_15px_rgba(34,211,238,0.5)] hover:shadow-[0_0_25px_rgba(34,211,238,0.8)] hover:scale-105 border border-cyan-400/50"
className="absolute right-3 top-1/2 -translate-y-1/2 px-4 py-2 min-h-[36px] text-sm font-medium bg-teal-600 text-white rounded-lg hover:bg-teal-500 transition-colors"
>
MAX
</button>
@@ -439,7 +454,7 @@ export default function BridgeButtons({
</div>
<div className="mb-8">
<label className="block text-sm font-bold mb-4 text-white/90 uppercase tracking-wider">
<label htmlFor="bridge-recipient-address" className="block text-[13px] font-medium mb-2 text-[#A0A0A0]">
Recipient Address
<Tooltip content="The Ethereum address that will receive the bridged tokens on the destination chain.">
<span className="ml-2 text-white/60 cursor-help text-xs"></span>
@@ -447,13 +462,15 @@ export default function BridgeButtons({
</label>
<div className="relative">
<input
id="bridge-recipient-address"
name="recipient"
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
className={`w-full p-5 border-2 rounded-2xl focus:ring-4 transition-all font-mono text-base bg-black/20 backdrop-blur-xl shadow-xl text-cyan-100 placeholder:text-cyan-400/50 ${
className={`w-full min-h-[48px] p-4 border rounded-xl focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 focus:ring-offset-[#252830] transition-all font-mono text-base bg-[#1a1d24] text-white placeholder:text-[#A0A0A0] hover:border-white/30 ${
recipientError
? 'border-red-400 focus:border-red-500 focus:ring-red-500/30 focus:shadow-[0_0_20px_rgba(239,68,68,0.5)]'
: 'border-cyan-400/30 focus:border-cyan-400 focus:ring-cyan-400/20 focus:shadow-[0_0_20px_rgba(34,211,238,0.5)]'
? 'border-red-500 focus:border-red-500'
: 'border-white/20 focus:border-teal-500'
}`}
placeholder="0x..."
aria-invalid={!!recipientError}
@@ -466,7 +483,7 @@ export default function BridgeButtons({
setRecipient(address);
setRecipientError('');
}}
className="px-4 py-2 text-sm font-bold bg-gradient-to-r from-purple-500 to-pink-500 text-white rounded-xl hover:from-purple-400 hover:to-pink-400 transition-all shadow-[0_0_15px_rgba(168,85,247,0.5)] hover:shadow-[0_0_25px_rgba(236,72,153,0.8)] hover:scale-105 border border-purple-400/50"
className="px-4 py-2 text-sm font-medium bg-[#252830] text-white rounded-lg hover:bg-white/10 transition-colors border border-white/20"
>
Use my address
</button>
@@ -486,9 +503,9 @@ export default function BridgeButtons({
)}
</div>
<div className="mb-8 p-6 bg-gradient-to-br from-cyan-900/20 to-purple-900/20 backdrop-blur-xl rounded-2xl border-2 border-cyan-400/30 shadow-[inset_0_0_20px_rgba(34,211,238,0.1),0_0_30px_rgba(168,85,247,0.2)]">
<div className="mb-8 p-6 bg-[#1a1d24] rounded-xl border border-white/10">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-bold text-cyan-200 uppercase tracking-wider font-mono">Balances & Fees</h3>
<h3 className="text-[20px] font-semibold text-[#A0A0A0]">Balances & Fees</h3>
{address && (
<CopyButton text={address} className="text-xs">
<span className="text-xs">Copy Address</span>
@@ -496,48 +513,60 @@ export default function BridgeButtons({
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex justify-between items-center p-4 bg-gradient-to-br from-cyan-900/30 to-purple-900/20 backdrop-blur-sm rounded-xl border border-cyan-400/30 shadow-[0_0_15px_rgba(34,211,238,0.2)] font-mono">
<span className="font-semibold text-cyan-200">ETH Balance:</span>
<span className="font-black text-cyan-100 text-lg">
{ethBalance ? ethBalance.displayValue : <LoadingSkeleton />} <span className="text-cyan-400/60">ETH</span>
<div className="flex justify-between items-center p-4 bg-[#252830] rounded-lg border border-white/10 font-mono text-sm">
<span className="font-medium text-[#A0A0A0] flex items-center gap-2">
<TokenIcon symbol="ETH" size={20} />
ETH Balance:
</span>
<span className="text-white font-medium">
{ethBalance ? ethBalance.displayValue : <LoadingSkeleton />} <span className="text-[#A0A0A0]">ETH</span>
</span>
</div>
<div className="flex justify-between items-center p-4 bg-gradient-to-br from-cyan-900/30 to-purple-900/20 backdrop-blur-sm rounded-xl border border-cyan-400/30 shadow-[0_0_15px_rgba(34,211,238,0.2)] font-mono">
<span className="font-semibold text-cyan-200">WETH9 Balance:</span>
<span className="font-black text-cyan-100 text-lg">
<div className="flex justify-between items-center p-4 bg-[#252830] rounded-lg border border-white/10 font-mono text-sm">
<span className="font-medium text-[#A0A0A0] flex items-center gap-2">
<TokenIcon symbol="WETH9" size={20} />
WETH9 Balance:
</span>
<span className="text-white font-medium">
{address && weth9Balance !== undefined
? `${ethers.utils.formatEther(weth9Balance.toString())}`
: address
? <LoadingSkeleton />
: '0'} <span className="text-cyan-400/60">WETH9</span>
: '0'} <span className="text-[#A0A0A0]">WETH9</span>
</span>
</div>
<div className="flex justify-between items-center p-4 bg-gradient-to-br from-cyan-900/30 to-purple-900/20 backdrop-blur-sm rounded-xl border border-cyan-400/30 shadow-[0_0_15px_rgba(34,211,238,0.2)] font-mono">
<span className="font-semibold text-cyan-200">LINK Balance:</span>
<span className="font-black text-cyan-100 text-lg">
<div className="flex justify-between items-center p-4 bg-[#252830] rounded-lg border border-white/10 font-mono text-sm">
<span className="font-medium text-[#A0A0A0] flex items-center gap-2">
<TokenIcon symbol="LINK" size={20} />
LINK Balance:
</span>
<span className="text-white font-medium">
{address && linkBalance !== undefined
? `${ethers.utils.formatEther(linkBalance.toString())}`
: address
? <LoadingSkeleton />
: '0'} <span className="text-cyan-400/60">LINK</span>
: '0'} <span className="text-[#A0A0A0]">LINK</span>
</span>
</div>
{ccipFee && (
<div className="flex justify-between items-center p-4 bg-gradient-to-br from-purple-500/40 to-pink-500/30 backdrop-blur-sm rounded-xl border-2 border-purple-400/40 shadow-[0_0_25px_rgba(168,85,247,0.4)] font-mono">
<span className="font-semibold text-purple-200">CCIP Fee:</span>
<span className="font-black text-purple-100 text-lg">
{ethers.utils.formatEther(ccipFee.toString())} <span className="text-purple-300/80">LINK</span>
{ccipFee != null && ccipFee.gt(0) && (
<div className="flex justify-between items-center p-4 bg-[#252830] rounded-lg border border-teal-500/30 font-mono text-sm">
<span className="font-medium text-[#A0A0A0] flex items-center gap-2">
<TokenIcon symbol="LINK" size={20} />
CCIP Fee:
</span>
<span className="text-white font-medium">
{ethers.utils.formatEther(ccipFee)} <span className="text-[#A0A0A0]">LINK</span>
</span>
</div>
)}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 mb-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<button
onClick={() => setShowWrapModal(true)}
disabled={!needsWrapping || !address || isWrapping}
className="group relative px-8 py-5 bg-gradient-to-r from-cyan-500 via-blue-600 to-cyan-600 text-white rounded-2xl font-black text-lg hover:from-cyan-400 hover:via-blue-500 hover:to-cyan-500 disabled:from-gray-400 disabled:via-gray-500 disabled:to-gray-600 disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-300 shadow-[0_0_30px_rgba(34,211,238,0.5)] hover:shadow-[0_0_50px_rgba(34,211,238,0.8)] transform hover:scale-105 disabled:transform-none disabled:shadow-none border-2 border-cyan-400/50 energy-flow overflow-hidden"
className="px-6 py-4 bg-[#252830] text-white rounded-xl font-semibold hover:bg-white/10 disabled:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors border border-white/20"
aria-label="Wrap ETH to WETH9"
>
{isWrapping ? (
@@ -556,7 +585,7 @@ export default function BridgeButtons({
<button
onClick={() => setShowApproveModal(true)}
disabled={!needsApproval || !address || isApproving}
className="group relative px-8 py-5 bg-gradient-to-r from-emerald-400 via-cyan-500 to-teal-500 text-white rounded-2xl font-black text-lg hover:from-emerald-300 hover:via-cyan-400 hover:to-teal-400 disabled:from-gray-400 disabled:via-gray-500 disabled:to-gray-600 disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-300 shadow-[0_0_30px_rgba(16,185,129,0.5)] hover:shadow-[0_0_50px_rgba(34,211,238,0.8)] transform hover:scale-105 disabled:transform-none disabled:shadow-none border-2 border-emerald-400/50 energy-flow overflow-hidden"
className="px-6 py-4 bg-[#252830] text-white rounded-xl font-semibold hover:bg-white/10 disabled:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors border border-white/20"
aria-label="Approve tokens for bridge"
>
{isApproving ? (
@@ -571,12 +600,13 @@ export default function BridgeButtons({
'Approve'
)}
</button>
</div>
<div className="mb-8">
<button
onClick={() => setShowBridgeModal(true)}
disabled={!canBridge || !address || isBridging}
className="group relative px-8 py-5 bg-gradient-to-r from-purple-500 via-pink-500 to-cyan-500 text-white rounded-2xl font-black text-lg hover:from-purple-400 hover:via-pink-400 hover:to-cyan-400 disabled:from-gray-400 disabled:via-gray-500 disabled:to-gray-600 disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-300 shadow-[0_0_30px_rgba(168,85,247,0.5),0_0_30px_rgba(236,72,153,0.3)] hover:shadow-[0_0_50px_rgba(168,85,247,0.8),0_0_50px_rgba(34,211,238,0.6)] transform hover:scale-105 disabled:transform-none disabled:shadow-none border-2 border-purple-400/50 energy-flow overflow-hidden"
aria-label="Bridge tokens to destination chain"
className="w-full min-h-[56px] py-4 text-lg font-semibold bg-teal-600 text-white rounded-xl hover:bg-teal-500 disabled:bg-[#252830] disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 focus:ring-offset-[#252830] shadow-lg"
aria-label="Start bridge transfer"
>
{isBridging ? (
<span className="flex items-center justify-center gap-2">
@@ -587,20 +617,11 @@ export default function BridgeButtons({
Bridging...
</span>
) : (
'Bridge (CCIP Send)'
'Start Bridge Transfer'
)}
</button>
</div>
{!address && (
<div className="mt-8 p-5 bg-gradient-to-r from-yellow-500/20 to-orange-500/20 backdrop-blur-xl border-2 border-yellow-400/40 rounded-2xl text-base text-white font-semibold flex items-center gap-4 shadow-[0_0_25px_rgba(234,179,8,0.4)]">
<svg className="h-6 w-6 text-yellow-300 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span>Please connect your wallet to continue</span>
</div>
)}
{/* Confirmation Modals */}
<ConfirmationModal
isOpen={showWrapModal}
@@ -644,7 +665,40 @@ export default function BridgeButtons({
isLoading={isBridging}
/>
</div>
</div>
</div>
);
}
export default function BridgeButtons(props: BridgeButtonsProps) {
const address = useAddress();
if (!address) {
const destinationChainSelector = props.destinationChainSelector ?? CHAIN_SELECTORS.ETHEREUM_MAINNET;
const destination = CCIP_DESTINATIONS.find((d) => d.selector === destinationChainSelector);
return (
<div className="w-full">
<div className="mb-6">
<h2 className="text-[20px] font-semibold text-white flex items-center gap-2">
Bridge to
{destination && (
<>
<ChainIcon chainId={destination.chainId} name={destination.name} size={24} />
<span>{destination.name}</span>
</>
)}
{!destination && <span>Ethereum Mainnet</span>}
</h2>
<p className="text-[#A0A0A0] text-sm mt-1">
Wrap ETH, approve tokens, and bridge WETH9 via CCIP
</p>
</div>
<div className="mt-8 p-5 bg-amber-500/10 border border-amber-500/30 rounded-xl text-sm text-white font-medium flex items-center gap-4">
<svg className="h-6 w-6 text-yellow-300 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span>Please connect your wallet to continue</span>
</div>
</div>
);
}
return <BridgeButtonsConnected {...props} address={address} />;
}

View File

@@ -33,8 +33,10 @@ export default function BridgeForm() {
<h2 className="text-2xl font-bold">Bridge Transfer</h2>
<div>
<label className="block text-sm font-medium mb-2">Asset Type</label>
<label htmlFor="bridge-form-asset-type" className="block text-sm font-medium mb-2">Asset Type</label>
<select
id="bridge-form-asset-type"
name="assetType"
value={assetType}
onChange={(e) => setAssetType(e.target.value as 'ETH' | 'WETH')}
className="w-full p-2 border rounded"
@@ -45,8 +47,10 @@ export default function BridgeForm() {
</div>
<div>
<label className="block text-sm font-medium mb-2">Amount</label>
<label htmlFor="bridge-form-amount" className="block text-sm font-medium mb-2">Amount</label>
<input
id="bridge-form-amount"
name="amount"
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
@@ -57,8 +61,10 @@ export default function BridgeForm() {
</div>
<div>
<label className="block text-sm font-medium mb-2">Recipient Address</label>
<label htmlFor="bridge-form-recipient" className="block text-sm font-medium mb-2">Recipient Address</label>
<input
id="bridge-form-recipient"
name="recipient"
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}

View File

@@ -0,0 +1,142 @@
/**
* Get full path quote (swap+bridge+swap) when VITE_BRIDGE_QUOTE_URL is set.
* Calls POST /api/bridge/quote with sourceToken, destinationToken, sourceChainId, destinationChainId, amount.
*/
import { useState } from 'react';
import { BRIDGE_QUOTE_URL } from '../../config/bridge';
interface QuoteResult {
transferId?: string;
minReceived?: string;
sourceSwapQuote?: string;
destinationSwapQuote?: string;
totalFee?: string;
estimatedTime?: number;
routes?: unknown[];
}
export default function SwapBridgeSwapQuoteForm() {
const [sourceToken, setSourceToken] = useState('');
const [destinationToken, setDestinationToken] = useState('');
const [sourceChainId, setSourceChainId] = useState('138');
const [destinationChainId, setDestinationChainId] = useState('137');
const [amount, setAmount] = useState('');
const [quote, setQuote] = useState<QuoteResult | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
if (!BRIDGE_QUOTE_URL) return null;
const handleGetQuote = async () => {
if (!amount) {
setError('Enter amount');
return;
}
setError(null);
setQuote(null);
setLoading(true);
try {
const url = `${BRIDGE_QUOTE_URL.replace(/\/$/, '')}/api/bridge/quote`;
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sourceToken: sourceToken || undefined,
destinationToken: destinationToken || undefined,
sourceChainId: sourceChainId ? Number(sourceChainId) : undefined,
destinationChainId: Number(destinationChainId),
amount: amount,
token: sourceToken || '0x0000000000000000000000000000000000000000',
destinationAddress: '0x0000000000000000000000000000000000000000',
}),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || `Quote failed: ${res.status}`);
}
const data = await res.json();
setQuote(data);
} catch (e) {
setError(e instanceof Error ? e.message : 'Failed to get quote');
} finally {
setLoading(false);
}
};
return (
<div className="mb-6 p-4 rounded-xl bg-[#1a1d24] border border-white/10">
<h3 className="text-sm font-semibold text-[#A0A0A0] mb-3">Get full path quote (swap+bridge+swap)</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-3">
<label htmlFor="quote-source-token" className="sr-only">Source token address</label>
<input
id="quote-source-token"
name="sourceToken"
type="text"
placeholder="Source token address"
value={sourceToken}
onChange={(e) => setSourceToken(e.target.value)}
className="px-3 py-2 rounded-lg bg-[#252830] text-white border border-white/20 focus:ring-2 focus:ring-teal-500/50 placeholder:text-[#A0A0A0]"
/>
<label htmlFor="quote-destination-token" className="sr-only">Destination token address</label>
<input
id="quote-destination-token"
name="destinationToken"
type="text"
placeholder="Destination token address"
value={destinationToken}
onChange={(e) => setDestinationToken(e.target.value)}
className="px-3 py-2 rounded-lg bg-[#252830] text-white border border-white/20 focus:ring-2 focus:ring-teal-500/50 placeholder:text-[#A0A0A0]"
/>
<label htmlFor="quote-source-chain" className="sr-only">Source chain ID</label>
<input
id="quote-source-chain"
name="sourceChainId"
type="text"
placeholder="Source chain ID"
value={sourceChainId}
onChange={(e) => setSourceChainId(e.target.value)}
className="px-3 py-2 rounded-lg bg-[#252830] text-white border border-white/20 focus:ring-2 focus:ring-teal-500/50 placeholder:text-[#A0A0A0]"
/>
<label htmlFor="quote-destination-chain" className="sr-only">Destination chain ID</label>
<input
id="quote-destination-chain"
name="destinationChainId"
type="text"
placeholder="Destination chain ID"
value={destinationChainId}
onChange={(e) => setDestinationChainId(e.target.value)}
className="px-3 py-2 rounded-lg bg-[#252830] text-white border border-white/20 focus:ring-2 focus:ring-teal-500/50 placeholder:text-[#A0A0A0]"
/>
<label htmlFor="quote-amount" className="sr-only">Amount (wei or decimal)</label>
<input
id="quote-amount"
name="amount"
type="text"
placeholder="Amount (wei or decimal)"
value={amount}
onChange={(e) => setAmount(e.target.value)}
className="px-3 py-2 rounded-lg bg-[#252830] text-white border border-white/20 focus:ring-2 focus:ring-teal-500/50 placeholder:text-[#A0A0A0] md:col-span-2"
/>
</div>
<button
type="button"
onClick={handleGetQuote}
disabled={loading}
className="px-4 py-2 rounded-lg bg-teal-600 text-white font-medium hover:bg-teal-500 disabled:opacity-50"
>
{loading ? 'Getting quote...' : 'Get full path quote'}
</button>
{error && <p className="mt-2 text-red-400 text-sm">{error}</p>}
{quote && (
<div className="mt-4 p-3 rounded-lg bg-white/5 text-sm text-white/90">
<p><strong>Min received:</strong> {quote.minReceived ?? '—'}</p>
{quote.sourceSwapQuote != null && <p><strong>Source swap quote:</strong> {quote.sourceSwapQuote}</p>}
{quote.destinationSwapQuote != null && <p><strong>Destination swap quote:</strong> {quote.destinationSwapQuote}</p>}
{quote.totalFee != null && <p><strong>Total fee:</strong> {quote.totalFee}</p>}
{quote.estimatedTime != null && <p><strong>Estimated time:</strong> {quote.estimatedTime}s</p>}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,519 @@
/**
* Trustless bridge (Chain 138 <-> Ethereum): Lockbox deposit, claim status, bridgeAndSwap with router choice.
*/
import { useState } from 'react';
import { useAccount, useChainId, useSwitchChain } from 'wagmi';
import { useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
import { padHex, toHex } from 'viem';
import toast from 'react-hot-toast';
import {
TRUSTLESS,
LOCKBOX_138_ABI,
DUAL_ROUTER_COORDINATOR_ABI,
CHALLENGE_MANAGER_ABI,
ERC20_ABI,
} from '../../config/bridge';
import { chain138 } from '../../config/networks';
const MAINNET_CHAIN_ID = 1;
const OUTPUT_ASSET_ETH = 0;
const OUTPUT_ASSET_WETH = 1;
const STABLECOIN_OPTIONS = [
{ value: 'USDT', label: 'USDT', address: TRUSTLESS.mainnet.USDT },
{ value: 'USDC', label: 'USDC', address: TRUSTLESS.mainnet.USDC },
{ 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 { address } = useAccount();
const chainId = useChainId();
const switchChain = useSwitchChain();
const [depositId, setDepositId] = useState('');
const [recipient, setRecipient] = useState(address || '');
const [amount, setAmount] = useState('');
const [depositType, setDepositType] = useState<'native' | 'erc20'>('native');
const [erc20Token, setErc20Token] = useState(LOCKBOX_ERC20_OPTIONS[0]?.value ?? 'WETH');
const [outputAsset, setOutputAsset] = useState<0 | 1>(OUTPUT_ASSET_WETH);
const [stablecoin, setStablecoin] = useState<typeof STABLECOIN_OPTIONS[number]['value']>('USDT');
const [amountOutMin, setAmountOutMin] = useState('');
const [useEnhancedRouter, setUseEnhancedRouter] = useState(true);
const [checkDepositId, setCheckDepositId] = useState('');
const [finalizeDepositId, setFinalizeDepositId] = useState('');
const lockboxAddress = TRUSTLESS.chain138.LOCKBOX_138;
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 { data: nonceData } = useReadContract({
address: address ? lockboxAddress : undefined,
abi: LOCKBOX_138_ABI,
functionName: 'getNonce',
args: address ? [address] : undefined,
chainId: chain138.id,
});
const { data: canSwapResult, refetch: refetchCanSwap } = useReadContract({
address: checkDepositId ? coordinatorAddress : undefined,
abi: DUAL_ROUTER_COORDINATOR_ABI,
functionName: 'canSwap',
args: checkDepositId ? [BigInt(checkDepositId)] : undefined,
chainId: MAINNET_CHAIN_ID,
});
const { data: canFinalizeResult, refetch: refetchCanFinalize } = useReadContract({
address: finalizeDepositId ? challengeManagerAddress : undefined,
abi: CHALLENGE_MANAGER_ABI,
functionName: 'canFinalize',
args: finalizeDepositId ? [BigInt(finalizeDepositId)] : undefined,
chainId: MAINNET_CHAIN_ID,
});
const { data: tokenAllowance } = useReadContract({
address: depositType === 'erc20' && address ? erc20TokenAddress : undefined,
abi: ERC20_ABI,
functionName: 'allowance',
args: address ? [address, lockboxAddress] : undefined,
chainId: chain138.id,
});
const { writeContract, data: hash, isPending, reset } = useWriteContract();
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash });
const onChain138 = chainId === chain138.id;
const onMainnet = chainId === MAINNET_CHAIN_ID;
const stablecoinAddress = STABLECOIN_OPTIONS.find((o) => o.value === stablecoin)?.address ?? TRUSTLESS.mainnet.USDT;
const handleDepositNative = () => {
if (!address || !recipient || !amount) {
toast.error('Connect wallet and enter recipient and amount');
return;
}
const wei = BigInt(Math.floor(parseFloat(amount) * 1e18));
if (wei <= 0n) {
toast.error('Amount must be > 0');
return;
}
const nonceBytes = padHex(toHex(nonceData ?? 0n), { size: 32 }) as `0x${string}`;
if (!onChain138) {
switchChain?.switchChain({ chainId: chain138.id });
return;
}
writeContract(
{
address: lockboxAddress,
abi: LOCKBOX_138_ABI,
functionName: 'depositNative',
args: [recipient as `0x${string}`, nonceBytes],
value: wei,
},
{
onSuccess: () => toast.success('Deposit submitted. Switch to Mainnet to check claim and run Bridge & Swap.'),
onError: (e) => toast.error(e?.message ?? 'Deposit failed'),
}
);
};
const erc20Decimals = LOCKBOX_ERC20_OPTIONS.find((o) => o.value === erc20Token)?.decimals ?? 18;
const erc20AmountWei = (() => {
if (!amount) return 0n;
const d = erc20Decimals;
const [whole = '0', frac = ''] = amount.split('.');
const fracPadded = frac.slice(0, d).padEnd(d, '0');
return BigInt(whole) * 10n ** BigInt(d) + BigInt(fracPadded || '0');
})();
const handleApprove = () => {
if (!address || !amount || depositType !== 'erc20') return;
if (erc20AmountWei <= 0n) {
toast.error('Amount must be > 0');
return;
}
if (!onChain138) {
switchChain?.switchChain({ chainId: chain138.id });
return;
}
writeContract(
{
address: erc20TokenAddress,
abi: ERC20_ABI,
functionName: 'approve',
args: [lockboxAddress, erc20AmountWei],
},
{
onSuccess: () => toast.success('Approval submitted'),
onError: (e) => toast.error(e?.message ?? 'Approval failed'),
}
);
};
const handleDepositERC20 = () => {
if (!address || !recipient || !amount) {
toast.error('Connect wallet and enter recipient and amount');
return;
}
if (erc20AmountWei <= 0n) {
toast.error('Amount must be > 0');
return;
}
const nonceBytes = padHex(toHex(nonceData ?? 0n), { size: 32 }) as `0x${string}`;
if (!onChain138) {
switchChain?.switchChain({ chainId: chain138.id });
return;
}
writeContract(
{
address: lockboxAddress,
abi: LOCKBOX_138_ABI,
functionName: 'depositERC20',
args: [erc20TokenAddress, erc20AmountWei, recipient as `0x${string}`, nonceBytes],
},
{
onSuccess: () => toast.success('ERC-20 deposit submitted. Switch to Mainnet to finalize claim and run Bridge & Swap.'),
onError: (e) => toast.error(e?.message ?? 'Deposit failed'),
}
);
};
const handleFinalizeClaim = () => {
if (!finalizeDepositId || !onMainnet) {
if (!onMainnet) switchChain?.switchChain({ chainId: MAINNET_CHAIN_ID });
return;
}
writeContract(
{
address: challengeManagerAddress,
abi: CHALLENGE_MANAGER_ABI,
functionName: 'finalizeClaim',
args: [BigInt(finalizeDepositId)],
},
{
onSuccess: () => toast.success('Claim finalized'),
onError: (e) => toast.error(e?.message ?? 'Finalize failed'),
}
);
};
const handleBridgeAndSwap = () => {
if (!depositId || !recipient || !amountOutMin) {
toast.error('Enter deposit ID, recipient, and minimum out');
return;
}
if (!onMainnet) {
switchChain?.switchChain({ chainId: MAINNET_CHAIN_ID });
return;
}
const minOut = BigInt(amountOutMin);
const routeData = '0x' as `0x${string}`;
writeContract(
{
address: coordinatorAddress,
abi: DUAL_ROUTER_COORDINATOR_ABI,
functionName: 'bridgeAndSwap',
args: [
BigInt(depositId),
recipient as `0x${string}`,
outputAsset,
stablecoinAddress,
minOut,
routeData,
useEnhancedRouter,
],
value: outputAsset === OUTPUT_ASSET_ETH ? undefined : 0n,
},
{
onSuccess: () => toast.success('Bridge & Swap submitted'),
onError: (e) => toast.error(e?.message ?? 'Bridge & Swap failed'),
}
);
};
if (isSuccess && hash) {
toast.success('Transaction confirmed');
reset();
}
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>
{/* Lockbox 138 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 && (
<button
type="button"
onClick={() => switchChain?.switchChain({ chainId: chain138.id })}
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
</button>
)}
<div className="grid gap-3 max-w-md">
<div className="flex gap-4 items-center">
<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>
</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 && (
<div className="flex gap-4 items-center">
<label htmlFor="trustless-bridge-token" className="text-white/80">Token:</label>
<select
id="trustless-bridge-token"
name="erc20Token"
value={erc20Token}
onChange={(e) => setErc20Token(e.target.value)}
className="px-3 py-2 rounded-lg bg-white/10 text-white border border-cyan-400/30"
>
{LOCKBOX_ERC20_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</div>
)}
<label htmlFor="trustless-bridge-recipient" className="sr-only">Recipient (Mainnet address)</label>
<input
id="trustless-bridge-recipient"
name="recipient"
type="text"
placeholder="Recipient (Mainnet address)"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
className="w-full px-3 py-2 rounded-lg bg-white/10 text-white border border-cyan-400/30"
/>
<label htmlFor="trustless-bridge-amount" className="sr-only">Amount</label>
<input
id="trustless-bridge-amount"
name="amount"
type="text"
placeholder={depositType === 'native' ? 'Amount (ETH)' : '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"
/>
{depositType === 'native' && (
<button
type="button"
disabled={isPending || isConfirming || !address || !amount || !recipient || !onChain138}
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'}
</button>
)}
{depositType === 'erc20' && (
<>
{tokenAllowance !== undefined && BigInt(amount || 0) * 10n ** 18n > (tokenAllowance ?? 0n) && (
<button
type="button"
disabled={isPending || isConfirming || !address || !amount || !onChain138}
onClick={handleApprove}
className="px-4 py-2 rounded-lg bg-cyan-500/80 text-white font-medium disabled:opacity-50"
>
{isPending || isConfirming ? 'Confirming...' : 'Approve Lockbox'}
</button>
)}
<button
type="button"
disabled={isPending || isConfirming || !address || !amount || !recipient || !onChain138 || (tokenAllowance !== undefined && erc20AmountWei > (tokenAllowance ?? 0n))}
onClick={handleDepositERC20}
className="px-4 py-2 rounded-lg bg-cyan-500 text-white font-medium disabled:opacity-50"
>
{isPending || isConfirming ? 'Confirming...' : 'Deposit ERC-20'}
</button>
</>
)}
</div>
</section>
{/* Claim status / canSwap */}
<section>
<h3 className="text-lg font-semibold text-cyan-300 mb-3">2. Check if claim is ready (Mainnet)</h3>
<p className="text-white/70 text-sm mb-3">After claim is finalized on Mainnet, you can run Bridge & Swap.</p>
{!onMainnet && (
<button
type="button"
onClick={() => switchChain?.switchChain({ chainId: MAINNET_CHAIN_ID })}
className="mb-3 px-4 py-2 rounded-lg bg-cyan-500/20 text-cyan-300 border border-cyan-400/40"
>
Switch to Ethereum Mainnet
</button>
)}
<div className="flex gap-3 items-end flex-wrap">
<div>
<label className="block text-white/70 text-sm mb-1">Deposit ID</label>
<input
type="text"
placeholder="Deposit ID"
value={checkDepositId}
onChange={(e) => setCheckDepositId(e.target.value)}
className="w-40 px-3 py-2 rounded-lg bg-white/10 text-white border border-cyan-400/30"
/>
</div>
<button
type="button"
onClick={() => refetchCanSwap()}
className="px-4 py-2 rounded-lg bg-white/10 text-white border border-cyan-400/40"
>
Check canSwap
</button>
</div>
{checkDepositId && canSwapResult !== undefined && (
<p className="mt-2 text-white/90">
Can swap: {Array.isArray(canSwapResult) ? String(canSwapResult[0]) : String(canSwapResult)}
{Array.isArray(canSwapResult) && canSwapResult[1] ? `${canSwapResult[1]}` : ''}
</p>
)}
</section>
{/* Finalize claim (Mainnet) */}
<section>
<h3 className="text-lg font-semibold text-cyan-300 mb-3">2b. Finalize claim (Mainnet)</h3>
<p className="text-white/70 text-sm mb-3">After the challenge window expires, anyone can finalize the claim so the recipient can run Bridge & Swap.</p>
{!onMainnet && (
<button
type="button"
onClick={() => switchChain?.switchChain({ chainId: MAINNET_CHAIN_ID })}
className="mb-3 px-4 py-2 rounded-lg bg-cyan-500/20 text-cyan-300 border border-cyan-400/40"
>
Switch to Ethereum Mainnet
</button>
)}
<div className="flex gap-3 items-end flex-wrap">
<div>
<label className="block text-white/70 text-sm mb-1">Deposit ID</label>
<input
type="text"
placeholder="Deposit ID"
value={finalizeDepositId}
onChange={(e) => setFinalizeDepositId(e.target.value)}
className="w-40 px-3 py-2 rounded-lg bg-white/10 text-white border border-cyan-400/30"
/>
</div>
<button type="button" onClick={() => refetchCanFinalize()} className="px-4 py-2 rounded-lg bg-white/10 text-white border border-cyan-400/40">Check canFinalize</button>
<button
type="button"
disabled={isPending || isConfirming || !finalizeDepositId || !onMainnet}
onClick={handleFinalizeClaim}
className="px-4 py-2 rounded-lg bg-cyan-500 text-white font-medium disabled:opacity-50"
>
{isPending || isConfirming ? 'Confirming...' : 'Finalize claim'}
</button>
</div>
{finalizeDepositId && canFinalizeResult !== undefined && (
<p className="mt-2 text-white/90">
Can finalize: {Array.isArray(canFinalizeResult) ? String(canFinalizeResult[0]) : String(canFinalizeResult)}
{Array.isArray(canFinalizeResult) && canFinalizeResult[1] ? `${canFinalizeResult[1]}` : ''}
</p>
)}
</section>
{/* Bridge and swap with router choice */}
<section>
<h3 className="text-lg font-semibold text-cyan-300 mb-3">3. Bridge and swap (Mainnet)</h3>
<p className="text-white/70 text-sm mb-3">Uses DualRouter coordinator: choose basic (SwapRouter) or enhanced (EnhancedSwapRouter) router.</p>
{!onMainnet && (
<button
type="button"
onClick={() => switchChain?.switchChain({ chainId: MAINNET_CHAIN_ID })}
className="mb-3 px-4 py-2 rounded-lg bg-cyan-500/20 text-cyan-300 border border-cyan-400/40"
>
Switch to Ethereum Mainnet
</button>
)}
<div className="grid gap-3 max-w-md">
<input
type="text"
placeholder="Deposit ID"
value={depositId}
onChange={(e) => setDepositId(e.target.value)}
className="w-full px-3 py-2 rounded-lg bg-white/10 text-white border border-cyan-400/30"
/>
<input
type="text"
placeholder="Recipient"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
className="w-full px-3 py-2 rounded-lg bg-white/10 text-white border border-cyan-400/30"
/>
<div className="flex gap-4 items-center">
<label className="text-white/80">Output asset from bridge:</label>
<select
value={outputAsset}
onChange={(e) => setOutputAsset(Number(e.target.value) as 0 | 1)}
className="px-3 py-2 rounded-lg bg-white/10 text-white border border-cyan-400/30"
>
<option value={OUTPUT_ASSET_ETH}>ETH</option>
<option value={OUTPUT_ASSET_WETH}>WETH</option>
</select>
</div>
<div className="flex gap-4 items-center">
<label className="text-white/80">Stablecoin:</label>
<select
value={stablecoin}
onChange={(e) => setStablecoin(e.target.value as typeof stablecoin)}
className="px-3 py-2 rounded-lg bg-white/10 text-white border border-cyan-400/30"
>
{STABLECOIN_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</div>
<input
type="text"
placeholder="Amount out min (wei or units)"
value={amountOutMin}
onChange={(e) => setAmountOutMin(e.target.value)}
className="w-full px-3 py-2 rounded-lg bg-white/10 text-white border border-cyan-400/30"
/>
<div className="flex gap-4 items-center">
<label className="text-white/80">Router:</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
checked={!useEnhancedRouter}
onChange={() => setUseEnhancedRouter(false)}
className="rounded"
/>
<span>Basic (SwapRouter)</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
checked={useEnhancedRouter}
onChange={() => setUseEnhancedRouter(true)}
className="rounded"
/>
<span>Enhanced (EnhancedSwapRouter)</span>
</label>
</div>
<button
type="button"
disabled={isPending || isConfirming || !depositId || !recipient || !amountOutMin || !onMainnet}
onClick={handleBridgeAndSwap}
className="px-4 py-2 rounded-lg bg-cyan-500 text-white font-medium disabled:opacity-50"
>
{isPending || isConfirming ? 'Confirming...' : 'Bridge & Swap'}
</button>
</div>
</section>
</div>
);
}

View File

@@ -1,5 +1,9 @@
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'
interface LayoutProps {
children: React.ReactNode
@@ -7,99 +11,82 @@ interface LayoutProps {
export default function Layout({ children }: LayoutProps) {
const location = useLocation()
const userInitiatedDisconnectRef = useRef(false)
const isActive = (path: string) => location.pathname === path
return (
<div className="min-h-screen relative">
{/* Animated background overlay */}
<div className="fixed inset-0 bg-gradient-to-br from-indigo-900/20 via-purple-900/20 to-pink-900/20 pointer-events-none" />
<div
className="fixed inset-0 opacity-30 pointer-events-none portal-grid"
/>
{/* Floating portal particles */}
<div className="fixed inset-0 pointer-events-none overflow-hidden">
{[...Array(30)].map((_, i) => (
<div
key={i}
className="absolute w-1 h-1 bg-cyan-400 rounded-full floating-particle"
style={{
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
animationDelay: `${Math.random() * 4}s`,
animationDuration: `${3 + Math.random() * 4}s`
}}
/>
))}
</div>
<nav className="bg-white/10 backdrop-blur-xl shadow-2xl border-b border-white/20 sticky top-0 z-50">
<div className="min-h-screen relative bg-[#1a1d24] flex flex-col">
<WalletDisconnectNotice userInitiatedDisconnectRef={userInitiatedDisconnectRef} />
<nav className="bg-[#252830] border-b border-white/10 sticky top-0 z-50">
<div className="container mx-auto px-4 max-w-7xl">
<div className="flex justify-between items-center h-16">
<div className="flex items-center gap-8">
<Link to="/" className="text-2xl font-black bg-gradient-to-r from-white via-blue-100 to-purple-100 bg-clip-text text-transparent drop-shadow-lg">
🌉 Bridge DApp
<div className="flex justify-between items-center h-14 gap-4">
<div className="flex items-center gap-6 min-w-0">
<Link to="/" className="text-xl font-semibold text-white whitespace-nowrap flex items-center gap-2">
<span className="text-teal-400 font-bold">DBIS</span>
</Link>
<div className="flex gap-2">
<div className="hidden sm:flex gap-1">
<Link
to="/"
className={`px-5 py-2.5 rounded-xl font-semibold transition-all duration-300 ${
isActive('/')
? 'bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 text-white shadow-lg shadow-purple-500/50 scale-105'
: 'text-white/90 hover:text-white hover:bg-white/10 backdrop-blur-sm border border-white/20'
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
isActive('/') ? 'bg-teal-600 text-white' : 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
}`}
>
Bridge
</Link>
<Link
to="/swap"
className={`px-5 py-2.5 rounded-xl font-semibold transition-all duration-300 ${
isActive('/swap')
? 'bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 text-white shadow-lg shadow-purple-500/50 scale-105'
: 'text-white/90 hover:text-white hover:bg-white/10 backdrop-blur-sm border border-white/20'
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
isActive('/swap') ? 'bg-teal-600 text-white' : 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
}`}
>
Swap
</Link>
<Link
to="/reserve"
className={`px-5 py-2.5 rounded-xl font-semibold transition-all duration-300 ${
isActive('/reserve')
? 'bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 text-white shadow-lg shadow-purple-500/50 scale-105'
: 'text-white/90 hover:text-white hover:bg-white/10 backdrop-blur-sm border border-white/20'
}`}
>
Reserve
</Link>
<Link
to="/history"
className={`px-5 py-2.5 rounded-xl font-semibold transition-all duration-300 ${
isActive('/history')
? 'bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 text-white shadow-lg shadow-purple-500/50 scale-105'
: 'text-white/90 hover:text-white hover:bg-white/10 backdrop-blur-sm border border-white/20'
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
isActive('/history') ? 'bg-teal-600 text-white' : 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
}`}
>
History
</Link>
<Link
to="/admin"
className={`px-5 py-2.5 rounded-xl font-semibold transition-all duration-300 ${
isActive('/admin')
? 'bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 text-white shadow-lg shadow-purple-500/50 scale-105'
: 'text-white/90 hover:text-white hover:bg-white/10 backdrop-blur-sm border border-white/20'
to="/wallets"
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
isActive('/wallets') ? 'bg-teal-600 text-white' : 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
}`}
>
Admin
Wallets
</Link>
<a
href={EXPLORER_URL}
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"
>
Explorer
</a>
</div>
</div>
<WalletConnect />
<div className="flex-shrink-0 w-full sm:w-auto max-w-[200px] sm:max-w-none">
<WalletConnect onBeforeDisconnect={() => { userInitiatedDisconnectRef.current = true }} />
</div>
</div>
</div>
</nav>
<main>{children}</main>
<main className="flex-1">{children}</main>
<footer className="border-t border-white/10 py-6 mt-auto">
<div className="container mx-auto px-4 max-w-7xl">
<div className="flex flex-wrap justify-center items-center gap-x-6 gap-y-2 text-sm text-[#A0A0A0]">
<span> Contracts verified</span>
<span> Powered by CCIP</span>
<span> Secure cross-chain messaging</span>
<span> Trackable on Explorer</span>
<Link to="/wallets" className="hover:text-teal-400 transition-colors">Wallets</Link>
<Link to="/docs" className="hover:text-teal-400 transition-colors">Developers</Link>
</div>
</div>
</footer>
</div>
)
}

View File

@@ -0,0 +1,49 @@
import { getChainIconUrl } from '../../config/chainIcons';
interface ChainIconProps {
chainId: number
name?: string
size?: number
className?: string
}
export default function ChainIcon({ chainId, name, size = 24, className = '' }: ChainIconProps) {
const src = getChainIconUrl(chainId);
if (src) {
const fallbackChar = name ? name.charAt(0).toUpperCase() : chainId.toString().slice(0, 1);
return (
<span className={`relative inline-flex flex-shrink-0 ${className}`} style={{ width: size, height: size }}>
<img
src={src}
alt={name ? `${name} chain` : `Chain ${chainId}`}
width={size}
height={size}
className="rounded-full absolute inset-0 w-full h-full object-cover"
onError={(e) => {
e.currentTarget.style.display = 'none';
const fb = e.currentTarget.nextElementSibling as HTMLElement;
if (fb) fb.style.display = 'flex';
}}
/>
<span
className="rounded-full bg-[#252830] flex items-center justify-center text-xs font-medium text-[#A0A0A0] hidden"
style={{ width: size, height: size }}
aria-hidden
>
{fallbackChar}
</span>
</span>
);
}
return (
<span
className={`rounded-full bg-[#252830] flex items-center justify-center text-xs font-medium text-[#A0A0A0] flex-shrink-0 ${className}`}
style={{ width: size, height: size }}
title={name ?? `Chain ${chainId}`}
>
{name ? name.charAt(0).toUpperCase() : chainId.toString().slice(0, 1)}
</span>
);
}

View File

@@ -0,0 +1,49 @@
import { getTokenIconUrl } from '../../config/tokenIcons';
interface TokenIconProps {
symbol: string
logoURI?: string
size?: number
className?: string
}
export default function TokenIcon({ symbol, logoURI, size = 24, className = '' }: TokenIconProps) {
const src = logoURI || getTokenIconUrl(symbol);
if (src) {
const fallbackChar = symbol ? symbol.slice(0, 2).toUpperCase() : '?';
return (
<span className={`relative inline-flex flex-shrink-0 ${className}`} style={{ width: size, height: size }}>
<img
src={src}
alt={symbol}
width={size}
height={size}
className="rounded-full absolute inset-0 w-full h-full object-cover"
onError={(e) => {
e.currentTarget.style.display = 'none';
const fb = e.currentTarget.nextElementSibling as HTMLElement;
if (fb) fb.style.display = 'flex';
}}
/>
<span
className="rounded-full bg-[#252830] flex items-center justify-center text-xs font-medium text-[#A0A0A0] hidden"
style={{ width: size, height: size }}
aria-hidden
>
{fallbackChar}
</span>
</span>
);
}
return (
<span
className={`rounded-full bg-[#252830] flex items-center justify-center text-xs font-medium text-[#A0A0A0] flex-shrink-0 ${className}`}
style={{ width: size, height: size }}
title={symbol}
>
{symbol ? symbol.slice(0, 2).toUpperCase() : '?'}
</span>
);
}

View File

@@ -1,15 +1,24 @@
import { useAccount, useConnect, useDisconnect, useChainId, useSwitchChain } from 'wagmi'
import { useEffect, useState } from 'react'
import { useEffect, useState, useRef } from 'react'
const CHAIN_138_ID = 138
const EXPLORER_URL = 'https://explorer.d-bis.org'
export default function WalletConnect() {
interface WalletConnectProps {
/** Callback before disconnect so we can treat it as user-initiated (no "disconnected" toast). */
onBeforeDisconnect?: () => void
}
export default function WalletConnect({ onBeforeDisconnect }: WalletConnectProps) {
const { address, isConnected } = useAccount()
const { connect, connectors, isPending } = useConnect()
const { disconnect } = useDisconnect()
const chainId = useChainId()
const { switchChain, isPending: isSwitching } = useSwitchChain()
const [showChainWarning, setShowChainWarning] = useState(false)
const [showConnectModal, setShowConnectModal] = useState(false)
const [showDropdown, setShowDropdown] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (isConnected && chainId !== CHAIN_138_ID) {
@@ -19,6 +28,16 @@ export default function WalletConnect() {
}
}, [isConnected, chainId])
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setShowDropdown(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
const handleSwitchChain = async () => {
try {
await switchChain({ chainId: CHAIN_138_ID })
@@ -27,14 +46,31 @@ export default function WalletConnect() {
}
}
const handleConnect = (connector: (typeof connectors)[0]) => {
connect({ connector })
setShowConnectModal(false)
}
const copyAddress = () => {
if (address) {
navigator.clipboard.writeText(address)
setShowDropdown(false)
}
}
const viewOnExplorer = () => {
if (address) window.open(`${EXPLORER_URL}/address/${address}`, '_blank', 'noopener')
setShowDropdown(false)
}
if (isConnected) {
return (
<div className="flex items-center gap-3">
<div className="flex items-center gap-3 flex-shrink-0">
{showChainWarning && (
<button
onClick={handleSwitchChain}
disabled={isSwitching}
className="px-4 py-2 text-sm font-bold bg-gradient-to-r from-yellow-500 to-orange-500 text-white rounded-xl hover:from-yellow-600 hover:to-orange-600 disabled:opacity-50 transition-all duration-300 shadow-lg hover:shadow-xl flex items-center gap-2 border border-white/20"
className="px-4 py-2 text-sm font-medium bg-amber-500/20 text-amber-200 rounded-lg hover:bg-amber-500/30 border border-amber-500/40 disabled:opacity-50 transition-colors flex items-center gap-2"
>
{isSwitching ? (
<>
@@ -45,54 +81,85 @@ export default function WalletConnect() {
Switching...
</>
) : (
<>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
Switch to Chain 138
</>
'Switch to Chain 138'
)}
</button>
)}
<div className="flex items-center gap-2 px-4 py-2.5 bg-white/10 backdrop-blur-xl rounded-xl border border-white/20 shadow-lg">
<div className="h-2.5 w-2.5 bg-emerald-400 rounded-full animate-pulse shadow-lg shadow-emerald-400/50"></div>
<span className="text-sm font-bold text-white font-mono">
{address?.slice(0, 6)}...{address?.slice(-4)}
</span>
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setShowDropdown(!showDropdown)}
className="flex items-center gap-2 px-4 py-2.5 bg-[#1a1d24] rounded-xl border border-white/20 hover:border-white/30 transition-colors min-h-[44px] w-full md:w-auto"
aria-expanded={showDropdown}
aria-haspopup="true"
>
<div className="h-2 w-2 bg-emerald-400 rounded-full" />
<span className="text-sm font-medium text-white font-mono">
{address?.slice(0, 6)}...{address?.slice(-4)}
</span>
<svg className={`w-4 h-4 text-[#A0A0A0] transition-transform ${showDropdown ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{showDropdown && (
<div className="absolute right-0 mt-1 py-1 w-48 bg-[#252830] rounded-xl border border-white/10 shadow-xl z-50">
<button onClick={copyAddress} className="w-full px-4 py-2.5 text-left text-sm text-white hover:bg-white/5 transition-colors">
Copy address
</button>
<button onClick={viewOnExplorer} className="w-full px-4 py-2.5 text-left text-sm text-white hover:bg-white/5 transition-colors">
View on Explorer
</button>
<button
onClick={() => { onBeforeDisconnect?.(); disconnect(); setShowDropdown(false) }}
className="w-full px-4 py-2.5 text-left text-sm text-red-400 hover:bg-white/5 transition-colors"
>
Disconnect
</button>
</div>
)}
</div>
<button
onClick={() => disconnect()}
className="px-5 py-2.5 bg-gradient-to-r from-red-500 to-red-600 text-white rounded-xl hover:from-red-600 hover:to-red-700 font-bold transition-all duration-300 shadow-lg hover:shadow-xl transform hover:scale-105 border border-white/20"
>
Disconnect
</button>
</div>
)
}
return (
<div className="flex items-center gap-2">
{connectors.map((connector) => (
<button
key={connector.uid}
onClick={() => connect({ connector })}
disabled={isPending}
className="px-6 py-3 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 text-white rounded-xl hover:from-blue-600 hover:via-purple-600 hover:to-pink-600 font-bold disabled:opacity-50 transition-all duration-300 shadow-2xl hover:shadow-purple-500/50 transform hover:scale-105 disabled:transform-none border border-white/20"
>
{isPending ? (
<span className="flex items-center gap-2">
<svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Connecting...
</span>
) : (
`Connect ${connector.name}`
)}
</button>
))}
</div>
<>
<button
onClick={() => setShowConnectModal(true)}
className="px-6 py-3 bg-teal-600 text-white font-medium rounded-xl hover:bg-teal-500 transition-colors focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 focus:ring-offset-[#252830] min-h-[44px] w-full md:w-auto"
>
Connect Wallet
</button>
{showConnectModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60" onClick={() => setShowConnectModal(false)}>
<div className="bg-[#252830] rounded-2xl border border-white/10 shadow-2xl w-full max-w-md p-6" onClick={e => e.stopPropagation()}>
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold text-white">Connect Wallet</h2>
<button onClick={() => setShowConnectModal(false)} className="p-2 text-[#A0A0A0] hover:text-white rounded-lg hover:bg-white/5 transition-colors" aria-label="Close">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<div className="space-y-2">
{connectors.map((connector) => (
<button
key={connector.uid}
onClick={() => handleConnect(connector)}
disabled={isPending}
className="w-full px-4 py-3 rounded-xl bg-[#1a1d24] border border-white/10 text-white font-medium hover:bg-white/5 hover:border-white/20 disabled:opacity-50 transition-colors text-left flex items-center justify-between"
>
<span>{connector.name}</span>
{isPending && (
<svg className="animate-spin h-5 w-5 text-teal-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
)}
</button>
))}
</div>
</div>
</div>
)}
</>
)
}

View File

@@ -0,0 +1,29 @@
import { useAccount } from 'wagmi'
import { useEffect, useRef } from 'react'
import toast from 'react-hot-toast'
/**
* Shows a toast when the wallet disconnects unexpectedly (e.g. MetaMask extension
* "Lost connection to MetaMask background"). Does not show when the user clicked Disconnect.
*/
export default function WalletDisconnectNotice({
userInitiatedDisconnectRef,
}: {
userInitiatedDisconnectRef: React.MutableRefObject<boolean>
}) {
const { isConnected } = useAccount()
const wasConnectedRef = useRef(false)
useEffect(() => {
if (wasConnectedRef.current && !isConnected) {
if (userInitiatedDisconnectRef.current) {
userInitiatedDisconnectRef.current = false
} else {
toast.error('Wallet disconnected. Please reconnect or reload the page.', { duration: 6000 })
}
}
wasConnectedRef.current = isConnected
}, [isConnected, userInitiatedDisconnectRef])
return null
}

View File

@@ -1,128 +1,93 @@
/**
* @file bridge.ts
* @notice Bridge configuration constants
* Bridge config: CCIP (WETH9) and Trustless bridge addresses, chain selectors, ABIs.
* Chain 138 addresses from env or defaults; mainnet Trustless from env when deployed.
*/
// Chain 138 Configuration
export const CHAIN_138 = {
chainId: 138,
rpcUrl: import.meta.env.VITE_RPC_URL_138 || 'http://192.168.11.250:8545',
explorerUrl: 'https://explorer.d-bis.org',
};
// ---------------------------------------------------------------------------
// CCIP WETH9 bridge (Chain 138)
// ---------------------------------------------------------------------------
// Contract Addresses (Chain 138)
export const CONTRACTS = {
WETH9: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
WETH9_BRIDGE: '0x89dd12025bfCD38A168455A44B400e913ED33BE2',
LINK_TOKEN: '0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03', // Chain 138 LINK token address
CCIP_ROUTER: '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D',
WETH9: (import.meta.env.VITE_WETH9_CHAIN138 || '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2') as `0x${string}`,
WETH9_BRIDGE: (import.meta.env.VITE_CCIPWETH9_BRIDGE_CHAIN138 || '0x971cD9D156f193df8051E48043C476e53ECd4693') as `0x${string}`,
LINK_TOKEN: (import.meta.env.VITE_LINK_TOKEN_CHAIN138 || '0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03') as `0x${string}`,
} as const;
// Chain Selectors
export const CHAIN_SELECTORS = {
ETHEREUM_MAINNET: '5009297550715157269',
POLYGON: '4051577828743386545',
AVALANCHE: '6433500567565415381',
BASE: '15971525489660198786',
ARBITRUM: '4949039107694359620',
OPTIMISM: '3734403246176062136',
BSC: '11344663589394136015',
} as const;
// Bridge Function ABI
export const BRIDGE_ABI = [
{
inputs: [
{ internalType: 'uint64', name: 'destinationChainSelector', type: 'uint64' },
{ internalType: 'address', name: 'recipient', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
name: 'sendCrossChain',
outputs: [{ internalType: 'bytes32', name: 'messageId', type: 'bytes32' }],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ internalType: 'uint64', name: 'destinationChainSelector', type: 'uint64' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
name: 'calculateFee',
outputs: [{ internalType: 'uint256', name: 'fee', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
] as const;
export interface CCIPDestination {
selector: string;
name: string;
chainId: number;
}
// WETH9 ABI
export const WETH9_ABI = [
{
inputs: [],
name: 'deposit',
outputs: [],
stateMutability: 'payable',
type: 'function',
},
{
inputs: [
{ internalType: 'address', name: 'spender', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
name: 'approve',
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ internalType: 'address', name: 'account', type: 'address' },
],
name: 'balanceOf',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{ internalType: 'address', name: 'owner', type: 'address' },
{ internalType: 'address', name: 'spender', type: 'address' },
],
name: 'allowance',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
] as const;
export const CCIP_DESTINATIONS: CCIPDestination[] = [
{ selector: CHAIN_SELECTORS.ETHEREUM_MAINNET, name: 'Ethereum Mainnet', chainId: 1 },
];
export const BRIDGE_QUOTE_URL = import.meta.env.VITE_BRIDGE_QUOTE_URL || '';
// ---------------------------------------------------------------------------
// Trustless bridge (Lockbox 138 ↔ Mainnet)
// ---------------------------------------------------------------------------
const zero = '0x0000000000000000000000000000000000000000' as const;
export const TRUSTLESS = {
mainnet: {
INBOX_ETH: (import.meta.env.VITE_INBOX_ETH_MAINNET || zero) as `0x${string}`,
LIQUIDITY_POOL: (import.meta.env.VITE_LIQUIDITY_POOL_MAINNET || zero) as `0x${string}`,
BRIDGE_SWAP_COORDINATOR: (import.meta.env.VITE_BRIDGE_SWAP_COORDINATOR_MAINNET || zero) as `0x${string}`,
DUAL_ROUTER_BRIDGE_SWAP_COORDINATOR: (import.meta.env.VITE_DUAL_ROUTER_BRIDGE_SWAP_COORDINATOR_MAINNET || zero) as `0x${string}`,
CHALLENGE_MANAGER: (import.meta.env.VITE_CHALLENGE_MANAGER_MAINNET || zero) as `0x${string}`,
USDT: (import.meta.env.VITE_USDT_MAINNET || '0xdAC17F958D2ee523a2206206994597C13D831ec7') as `0x${string}`,
USDC: (import.meta.env.VITE_USDC_MAINNET || '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48') as `0x${string}`,
DAI: (import.meta.env.VITE_DAI_MAINNET || '0x6B175474E89094C44Da98b954Eedeac495271d0F') as `0x${string}`,
},
chain138: {
LOCKBOX_138: (import.meta.env.VITE_LOCKBOX_138 || '0xFce6f50B312B3D936Ea9693C5C9531CF92a3324c') as `0x${string}`,
WETH: (import.meta.env.VITE_WETH_CHAIN138 || '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2') as `0x${string}`,
CUSDT: (import.meta.env.VITE_CUSDT_CHAIN138 || zero) as `0x${string}`,
CUSDC: (import.meta.env.VITE_CUSDC_CHAIN138 || zero) as `0x${string}`,
},
} as const;
// ---------------------------------------------------------------------------
// ABIs (minimal for used functions)
// ---------------------------------------------------------------------------
// ERC20 ABI (for LINK token)
export const ERC20_ABI = [
{
inputs: [
{ internalType: 'address', name: 'spender', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
name: 'approve',
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{ internalType: 'address', name: 'account', type: 'address' },
],
name: 'balanceOf',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{ internalType: 'address', name: 'owner', type: 'address' },
{ internalType: 'address', name: 'spender', type: 'address' },
],
name: 'allowance',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
{ inputs: [{ name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }], name: 'allowance', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
{ inputs: [{ name: 'account', type: 'address' }], name: 'balanceOf', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
{ inputs: [{ name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }], name: 'approve', outputs: [{ type: 'bool' }], stateMutability: 'nonpayable', type: 'function' },
{ inputs: [{ name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }], name: 'transfer', outputs: [{ type: 'bool' }], stateMutability: 'nonpayable', type: 'function' },
] as const;
export const WETH9_ABI = [
...ERC20_ABI,
{ inputs: [], name: 'deposit', outputs: [], stateMutability: 'payable', type: 'function' },
{ inputs: [{ name: 'wad', type: 'uint256' }], name: 'withdraw', outputs: [], stateMutability: 'nonpayable', type: 'function' },
] as const;
export const BRIDGE_ABI = [
{ inputs: [{ name: 'destinationChainSelector', type: 'uint64' }, { name: 'amount', type: 'uint256' }], name: 'calculateFee', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
{ inputs: [{ name: 'destinationChainSelector', type: 'uint64' }, { name: 'recipient', type: 'address' }, { name: 'amount', type: 'uint256' }], name: 'sendCrossChain', outputs: [], stateMutability: 'payable', type: 'function' },
] as const;
export const LOCKBOX_138_ABI = [
{ inputs: [{ name: 'account', type: 'address' }], name: 'getNonce', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
{ inputs: [{ name: 'recipient', type: 'address' }, { name: 'nonce', type: 'bytes32' }], name: 'depositNative', outputs: [], stateMutability: 'payable', type: 'function' },
{ inputs: [{ name: 'token', type: 'address' }, { name: 'amount', type: 'uint256' }, { name: 'recipient', type: 'address' }, { name: 'nonce', type: 'bytes32' }], name: 'depositERC20', outputs: [], stateMutability: 'nonpayable', type: 'function' },
] as const;
export const DUAL_ROUTER_COORDINATOR_ABI = [
{ inputs: [{ name: 'depositId', type: 'uint256' }], name: 'canSwap', outputs: [{ type: 'bool' }], stateMutability: 'view', type: 'function' },
{ inputs: [{ name: 'depositId', type: 'uint256' }, { name: 'recipient', type: 'address' }, { name: 'outputAsset', type: 'uint8' }, { name: 'stablecoin', type: 'address' }, { name: 'amountOutMin', type: 'uint256' }, { name: 'routeData', type: 'bytes' }, { name: 'useEnhancedRouter', type: 'bool' }], name: 'bridgeAndSwap', outputs: [], stateMutability: 'payable', type: 'function' },
] as const;
export const CHALLENGE_MANAGER_ABI = [
{ inputs: [{ name: 'depositId', type: 'uint256' }], name: 'canFinalize', outputs: [{ type: 'bool' }], stateMutability: 'view', type: 'function' },
{ inputs: [{ name: 'depositId', type: 'uint256' }], name: 'finalizeClaim', outputs: [], stateMutability: 'nonpayable', type: 'function' },
] as const;

View File

@@ -0,0 +1,23 @@
/**
* Chain icon URLs for display in the DApp.
* Sources: Trust Wallet assets, chain-138.json, and public CDNs.
*/
const TW = 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains';
const ETH_DIAMOND = 'https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png';
export const CHAIN_ICON_URLS: Record<number, string> = {
1: `${TW}/ethereum/info/logo.png`,
10: `${TW}/optimism/info/logo.png`,
25: `${TW}/cronos/info/logo.png`,
56: `${TW}/smartchain/info/logo.png`,
100: `${TW}/xdai/info/logo.png`,
137: `${TW}/polygon/info/logo.png`,
138: ETH_DIAMOND, // Chain 138 (DBIS) - from chain-138.json
42161: `${TW}/arbitrum/info/logo.png`,
43114: `${TW}/avalanchec/info/logo.png`,
8453: `${TW}/base/info/logo.png`,
};
export function getChainIconUrl(chainId: number): string | undefined {
return CHAIN_ICON_URLS[chainId];
}

View File

@@ -1,20 +1,47 @@
import { mainnet } from 'wagmi/chains'
import type { Address } from 'viem'
import { TRUSTLESS } from './bridge'
// Contract addresses on Ethereum Mainnet
// 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
export const CONTRACT_ADDRESSES = {
mainnet: {
MAINNET_TETHER: '0x15DF1D5BFDD8Aa4b380445D4e3E9B38d34283619' as Address,
TRANSACTION_MIRROR: '0x4CF42c4F1dBa748601b8938be3E7ABD732E87cE9' as Address,
// TwoWayTokenBridge addresses (if deployed)
PAYMENT_CHANNEL_MANAGER: undefined as Address | undefined,
GENERIC_STATE_CHANNEL_MANAGER: undefined as Address | undefined,
TWOWAY_BRIDGE_L1: undefined as Address | undefined,
INBOX_ETH: TRUSTLESS.mainnet.INBOX_ETH as Address,
LIQUIDITY_POOL: TRUSTLESS.mainnet.LIQUIDITY_POOL as Address,
BRIDGE_SWAP_COORDINATOR: TRUSTLESS.mainnet.BRIDGE_SWAP_COORDINATOR as Address,
DUAL_ROUTER_BRIDGE_SWAP_COORDINATOR: TRUSTLESS.mainnet.DUAL_ROUTER_BRIDGE_SWAP_COORDINATOR as Address,
CHALLENGE_MANAGER: TRUSTLESS.mainnet.CHALLENGE_MANAGER as Address,
},
chain138: {
// TwoWayTokenBridge L2 would be on Chain 138 if deployed
TRANSACTION_MIRROR: '0xE362aa10D3Af1A16880A799b78D18F923403B55a' as Address,
PAYMENT_CHANNEL_MANAGER: undefined as Address | undefined,
GENERIC_STATE_CHANNEL_MANAGER: undefined as Address | undefined,
TWOWAY_BRIDGE_L2: undefined as Address | undefined,
LOCKBOX_138: TRUSTLESS.chain138.LOCKBOX_138 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' },
},
} as const
export const SUPPORTED_CHAINS = {
mainnet,
chain138,
} as const

View File

@@ -0,0 +1,165 @@
/**
* Shared network definitions for Wagmi, Viem, CowSwap, Jumper, and others.
* Single source of truth for chainId, RPC, explorers, native currency.
* Chainlist entries: pr-workspace/chains/_data/chains/eip155-*.json
*/
import { defineChain } from 'viem'
import {
mainnet,
base,
arbitrum,
polygon,
optimism,
} from 'wagmi/chains'
const rpcUrl138 = import.meta.env.VITE_RPC_URL_138 || 'https://rpc-http-pub.d-bis.org'
const rpcUrl651940 = import.meta.env.VITE_RPC_URL_651940 || 'https://mainnet-rpc.alltra.global'
const rpcUrl42793 = import.meta.env.VITE_RPC_URL_42793 || 'https://node.mainnet.etherlink.com'
/** Chain 138 - DeFi Oracle Meta Mainnet */
export const chain138 = defineChain({
id: 138,
name: 'DeFi Oracle Meta Mainnet',
network: 'chain138',
nativeCurrency: {
decimals: 18,
name: 'Ether',
symbol: 'ETH',
},
rpcUrls: {
default: { http: [rpcUrl138] },
public: { http: [rpcUrl138] },
},
blockExplorers: {
default: {
name: 'DBIS Explorer',
url: 'https://explorer.d-bis.org',
},
},
})
/** Chain 651940 - ALL Mainnet */
export const allMainnet = defineChain({
id: 651940,
name: 'ALL Mainnet',
network: 'all-mainnet',
nativeCurrency: {
decimals: 18,
name: 'ALL',
symbol: 'ALL',
},
rpcUrls: {
default: { http: [rpcUrl651940] },
public: { http: [rpcUrl651940] },
},
blockExplorers: {
default: {
name: 'Alltra Explorer',
url: 'https://alltra.global',
},
},
})
/** Chain 42793 - Etherlink Mainnet (Tezos EVM L2) */
export const etherlink = defineChain({
id: 42793,
name: 'Etherlink Mainnet',
network: 'etherlink-mainnet',
nativeCurrency: {
decimals: 18,
name: 'tez',
symbol: 'XTZ',
},
rpcUrls: {
default: { http: [rpcUrl42793] },
public: { http: [rpcUrl42793] },
},
blockExplorers: {
default: {
name: 'Etherlink Explorer',
url: 'https://explorer.etherlink.com',
},
},
})
const rpcUrlBsc = import.meta.env.VITE_BSC_RPC_URL || 'https://bsc-dataseed.binance.org'
const rpcUrlAvalanche = import.meta.env.VITE_AVALANCHE_RPC_URL || 'https://api.avax.network/ext/bc/C/rpc'
const rpcUrlCronos = import.meta.env.VITE_CRONOS_RPC_URL || 'https://evm.cronos.org'
const rpcUrlGnosis = import.meta.env.VITE_GNOSIS_RPC_URL || 'https://rpc.gnosischain.com'
/** BSC - Binance Smart Chain (56) */
export const bsc = defineChain({
id: 56,
name: 'BNB Smart Chain',
network: 'bsc',
nativeCurrency: { decimals: 18, name: 'BNB', symbol: 'BNB' },
rpcUrls: { default: { http: [rpcUrlBsc] }, public: { http: [rpcUrlBsc] } },
blockExplorers: { default: { name: 'BscScan', url: 'https://bscscan.com' } },
})
/** Avalanche C-Chain (43114) */
export const avalanche = defineChain({
id: 43114,
name: 'Avalanche C-Chain',
network: 'avalanche',
nativeCurrency: { decimals: 18, name: 'AVAX', symbol: 'AVAX' },
rpcUrls: { default: { http: [rpcUrlAvalanche] }, public: { http: [rpcUrlAvalanche] } },
blockExplorers: { default: { name: 'Snowtrace', url: 'https://snowtrace.io' } },
})
/** Cronos (25) */
export const cronos = defineChain({
id: 25,
name: 'Cronos',
network: 'cronos',
nativeCurrency: { decimals: 18, name: 'CRO', symbol: 'CRO' },
rpcUrls: { default: { http: [rpcUrlCronos] }, public: { http: [rpcUrlCronos] } },
blockExplorers: { default: { name: 'CronosScan', url: 'https://cronoscan.com' } },
})
/** Gnosis Chain (100) */
export const gnosis = defineChain({
id: 100,
name: 'Gnosis',
network: 'gnosis',
nativeCurrency: { decimals: 18, name: 'xDAI', symbol: 'xDAI' },
rpcUrls: { default: { http: [rpcUrlGnosis] }, public: { http: [rpcUrlGnosis] } },
blockExplorers: { default: { name: 'GnosisScan', url: 'https://gnosisscan.io' } },
})
/** All custom chains (Viem Chain type) - for use with Wagmi/Viem */
export const customChains = [chain138, allMainnet, etherlink, bsc, avalanche, cronos, gnosis] as const
/** Standard L2s from wagmi/chains (Ethereum, Base, Arbitrum, Polygon, Optimism) */
export const standardChains = [mainnet, base, arbitrum, polygon, optimism] as const
/** All networks: custom + standard. Use in Wagmi createConfig({ chains: allNetworks }) */
export const allNetworks = [...customChains, ...standardChains] as const
/** Chain IDs we support (for CowSwap, Jumper, UI filters) - all deployed contract networks */
export const supportedChainIds = [
chain138.id,
allMainnet.id,
etherlink.id,
mainnet.id,
base.id,
arbitrum.id,
polygon.id,
optimism.id,
bsc.id,
avalanche.id,
cronos.id,
gnosis.id,
] as const
/** Map chainId -> RPC URL for transports */
export const chainRpcUrls: Record<number, string> = {
[chain138.id]: rpcUrl138,
[allMainnet.id]: rpcUrl651940,
[etherlink.id]: rpcUrl42793,
[bsc.id]: rpcUrlBsc,
[avalanche.id]: rpcUrlAvalanche,
[cronos.id]: rpcUrlCronos,
[gnosis.id]: rpcUrlGnosis,
}

View File

@@ -0,0 +1,58 @@
/**
* Token icon URLs for display in the DApp (symbol or key → URL).
* Aligned with token-lists logoURI (IPFS-hosted via Pinata).
* Includes Chain 138, Cronos (25) ISO-4217 W tokens, and compliant stables.
*/
const IPFS = 'https://ipfs.io/ipfs';
const ETH_LOGO = `${IPFS}/Qma3FKtLce9MjgJgWbtyCxBiPjJ6xi8jGWUSKNS5Jc2ong`;
const WETH10_LOGO = `${IPFS}/QmanDFPHxnbKd6SSNzzXHf9GbpL9dLXSphxDZSPPYE6ds4`;
const LINK_LOGO = `${IPFS}/QmenWcmfNGfssz4HXvrRV912eZDiKqLTt6z2brRYuTGz9A`;
const CUSDT_LOGO = `${IPFS}/QmRfhPs9DcyFPpGjKwF6CCoVDWUHSxkQR34n9NK7JSbPCP`;
const CUSDC_LOGO = `${IPFS}/QmNPq4D5JXzurmi9jAhogVMzhAQRk1PZ1r9H3qQUV9gjDm`;
const ETHUSD_LOGO = `${IPFS}/QmPZuycjyJEe2otREuQ5HirvPJ8X6Yc6MBtwz1VhdD79pY`;
const EURW_LOGO = `${IPFS}/QmPh16PY241zNtePyeK7ep1uf1RcARV2ynGAuRU8U7sSqS`;
const GBPW_LOGO = `${IPFS}/QmT2nJ6WyhYBCsYJ6NfS1BPAqiGKkCEuMxiC8ye93Co1hF`;
const W_LOGO = `${IPFS}/Qmb9JmuD9ehaQtTLBBZmAoiAbvE53e3FMjkEty8rvbPf9K`;
export const TOKEN_ICON_URLS: Record<string, string> = {
ETH: ETH_LOGO,
WETH: ETH_LOGO,
WETH9: ETH_LOGO,
WETH10: WETH10_LOGO,
LINK: LINK_LOGO,
cUSDT: CUSDT_LOGO,
cUSDC: CUSDC_LOGO,
'ETH-USD': ETHUSD_LOGO,
USDW: CUSDC_LOGO,
EURW: EURW_LOGO,
GBPW: GBPW_LOGO,
AUDW: W_LOGO,
JPYW: W_LOGO,
CHFW: W_LOGO,
CADW: W_LOGO,
};
export const TOKEN_ICON_BY_ADDRESS: Record<string, string> = {
// Chain 138
'0x3304b747E565a97ec8AC220b0B6A1f6ffDB837e6': ETHUSD_LOGO,
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2': ETH_LOGO,
'0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9F': WETH10_LOGO,
'0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03': LINK_LOGO,
'0x93E66202A11B1772E55407B32B44e5Cd8eda7f22': CUSDT_LOGO,
'0xf22258f57794CC8E06237084b353Ab30fFfa640b': CUSDC_LOGO,
// Cronos (25) WETH9, LINK, ISO-4217 W tokens
'0x99B3511A2d315A497C8112C1fdd8D508d4B1E506': ETH_LOGO,
'0x8c80A01F461f297Df7F9DA3A4f740D7297C8Ac85': LINK_LOGO,
'0x948690147D2e50ffe50C5d38C14125aD6a9FA036': CUSDC_LOGO,
'0x58a8D8F78F1B65c06dAd7542eC46b299629A60dd': EURW_LOGO,
'0xFb4B6Cc81211F7d886950158294A44C312abCA29': GBPW_LOGO,
'0xf9f5D0ACD71C76F9476F10B3F3d3E201F0883C68': W_LOGO,
'0xeE17bB0322383fecCA2784fbE2d4CD7d02b1905B': W_LOGO,
'0xc9750828124D4c10e7a6f4B655cA8487bD3842EB': W_LOGO,
'0x328Cd365Bb35524297E68ED28c6fF2C9557d1363': W_LOGO,
};
export function getTokenIconUrl(symbol: string): string | undefined {
const key = symbol?.toUpperCase() || '';
return TOKEN_ICON_URLS[key] ?? TOKEN_ICON_URLS[symbol ?? ''];
}

View File

@@ -1,47 +1,39 @@
import { createConfig, http } from 'wagmi'
import { mainnet } from 'wagmi/chains'
import { defineChain } from 'viem'
import { metaMask, walletConnect, coinbaseWallet } from 'wagmi/connectors'
import {
allNetworks,
chainRpcUrls,
chain138,
allMainnet,
etherlink,
bsc,
avalanche,
cronos,
gnosis,
} from './networks'
const projectId = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID || ''
const rpcUrl138 = import.meta.env.VITE_RPC_URL_138 || 'http://192.168.11.250:8545'
// Chain 138 definition using viem's defineChain
const chain138 = defineChain({
id: 138,
name: 'DeFi Oracle Meta Mainnet',
network: 'chain138',
nativeCurrency: {
decimals: 18,
name: 'Ether',
symbol: 'ETH',
},
rpcUrls: {
default: {
http: [rpcUrl138],
},
public: {
http: [rpcUrl138],
},
},
blockExplorers: {
default: {
name: 'DBIS Explorer',
url: 'https://explorer.d-bis.org',
},
},
})
export const config = createConfig({
chains: [chain138, mainnet],
chains: allNetworks,
connectors: [
metaMask(),
walletConnect({ projectId }),
coinbaseWallet({ appName: 'Bridge DApp' }),
],
transports: {
[chain138.id]: http(rpcUrl138),
[mainnet.id]: http(),
[chain138.id]: http(chainRpcUrls[chain138.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()])
),
},
})

View File

@@ -1,9 +1,9 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
body {
margin: 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
@@ -11,9 +11,7 @@ body {
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: linear-gradient(135deg, #667eea 0%, #764ba2 25%, #f093fb 50%, #4facfe 75%, #00f2fe 100%);
background-size: 400% 400%;
animation: gradientShift 15s ease infinite;
background: #1a1d24;
min-height: 100vh;
}

View File

@@ -29,22 +29,27 @@ import TransactionQueuePriority from '../components/admin/TransactionQueuePriori
import HardwareWalletSupport from '../components/admin/HardwareWalletSupport'
import FunctionPermissions from '../components/admin/FunctionPermissions'
import RealtimeMonitor from '../components/admin/RealtimeMonitor'
import PaymentChannels from '../components/admin/PaymentChannels'
import PaymentChannelAdmin from '../components/admin/PaymentChannelAdmin'
import StateChannels from '../components/admin/StateChannels'
type TabType = 'dashboard' | 'mainnet-tether' | 'transaction-mirror' | 'two-way-bridge' | 'multisig' | 'queue' | 'impersonation' | 'emergency' | 'audit' | 'gas' | 'batch' | 'templates' | 'retry' | 'services' | 'preview' | 'roles' | 'timelock' | 'wallet' | 'multichain' | 'schedule' | 'balance' | 'owners' | 'backup' | 'priority' | 'hardware' | 'permissions' | 'realtime' | 'multisig' | 'queue' | 'impersonation' | 'emergency' | 'audit' | 'gas' | 'batch' | 'templates' | 'retry' | 'services' | 'preview' | 'roles' | 'timelock' | 'wallet' | 'multichain' | 'schedule' | 'balance' | 'owners' | 'backup' | 'priority'
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'
export default function AdminPanel() {
const { address, isConnected } = useAccount()
const chainId = useChainId()
const [activeTab, setActiveTab] = useState<TabType>('dashboard')
// Check if connected to mainnet
const isMainnet = chainId === 1
const isSupportedChain = chainId === 1 || chainId === 138
const tabs = [
{ id: 'dashboard' as TabType, label: 'Dashboard', icon: '📊' },
{ id: 'mainnet-tether' as TabType, label: 'Mainnet Tether', icon: '🔗' },
{ id: 'transaction-mirror' as TabType, label: 'Transaction Mirror', icon: '📋' },
{ id: 'two-way-bridge' as TabType, label: 'Two-Way Bridge', icon: '🌉' },
{ id: 'channels' as TabType, label: 'Channels', icon: '💸' },
{ id: 'state-channels' as TabType, label: 'State Channels', icon: '📜' },
{ id: 'channel-admin' as TabType, label: 'Channel Admin', icon: '⚙️' },
{ id: 'multisig' as TabType, label: 'Multi-Sig', icon: '👥' },
{ id: 'queue' as TabType, label: 'Queue', icon: '📝' },
{ id: 'impersonation' as TabType, label: 'Impersonation', icon: '🎭' },
@@ -86,17 +91,17 @@ export default function AdminPanel() {
)
}
if (!isMainnet) {
if (!isSupportedChain) {
return (
<div className="container mx-auto px-4 py-12 max-w-7xl">
<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 to access admin functions.
Please switch to Ethereum Mainnet (1) or Chain 138 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}. Please switch to Mainnet (Chain ID 1)
Current network: Chain ID {chainId}. Use Mainnet (1) or Chain 138.
</p>
</div>
</div>
@@ -139,6 +144,9 @@ export default function AdminPanel() {
{activeTab === 'mainnet-tether' && <MainnetTetherAdmin />}
{activeTab === 'transaction-mirror' && <TransactionMirrorAdmin />}
{activeTab === 'two-way-bridge' && <TwoWayBridgeAdmin />}
{activeTab === 'channels' && <PaymentChannels />}
{activeTab === 'state-channels' && <StateChannels />}
{activeTab === 'channel-admin' && <PaymentChannelAdmin />}
{activeTab === 'multisig' && <MultiSigAdmin />}
{activeTab === 'queue' && <TransactionQueue />}
{activeTab === 'impersonation' && <ImpersonationMode />}

View File

@@ -3,13 +3,19 @@ import ThirdwebBridgeWidget from '../components/bridge/ThirdwebBridgeWidget';
import BridgeButtons from '../components/bridge/BridgeButtons';
import XRPLBridgeForm, { XRPLBridgeData } from '../components/bridge/XRPLBridgeForm';
import TransferTracking from '../components/bridge/TransferTracking';
import { CHAIN_SELECTORS } from '../config/bridge';
import SwapBridgeSwapQuoteForm from '../components/bridge/SwapBridgeSwapQuoteForm';
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;
const THIRDWEB_CLIENT_ID = import.meta.env.VITE_THIRDWEB_CLIENT_ID || '542981292d51ec610388ba8985f027d7';
export default function BridgePage() {
const [activeTab, setActiveTab] = useState<'custom' | 'evm' | 'xrpl' | 'track'>('custom');
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 handleXRPLBridge = async (data: XRPLBridgeData) => {
try {
@@ -33,40 +39,46 @@ export default function BridgePage() {
return (
<div className="min-h-screen relative">
<div className="container mx-auto px-4 py-12 max-w-7xl relative z-10">
<div className="text-center mb-12">
<div className="inline-block mb-4">
<h1 className="text-5xl md:text-7xl font-black mb-4 holographic-text drop-shadow-2xl animate-fadeIn portal-entrance">
Interoperability Bridge
</h1>
<div className="h-1 w-32 bg-gradient-to-r from-cyan-400 via-purple-400 via-pink-400 to-cyan-400 mx-auto rounded-full shadow-[0_0_20px_rgba(34,211,238,0.6)]"></div>
</div>
<p className="text-white/90 text-xl md:text-2xl font-medium mt-6 drop-shadow-lg">
Seamlessly bridge assets across chains with CCIP
<div className="container mx-auto px-4 sm:px-6 py-8 sm:py-12 max-w-7xl relative z-10 w-full">
<div className="text-center mb-8">
<h1 className="text-[32px] font-bold text-white mb-2">
Interoperability Bridge
</h1>
<p className="text-[#A0A0A0] text-base">
Bridge assets between Chain 138 and Ethereum via CCIP
</p>
</div>
{/* Tabs */}
<div className="mb-10">
<nav className="flex flex-wrap gap-3 bg-white/5 backdrop-blur-2xl rounded-2xl p-3 shadow-2xl border-2 border-cyan-400/30 portal-glow" role="tablist">
{/* Tabs — text only, no icons */}
<div className="mb-6">
<nav className="flex flex-wrap gap-1 bg-[#252830] rounded-xl p-1.5 border border-white/10" role="tablist">
<button
onClick={() => setActiveTab('custom')}
role="tab"
aria-selected={activeTab === 'custom'}
aria-controls="custom-tabpanel"
id="custom-tab"
className={`flex-1 min-w-[140px] px-6 py-4 font-bold rounded-xl transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-cyan-400/50 relative overflow-hidden ${
className={`px-4 py-3 text-sm font-medium rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-teal-500/50 ${
activeTab === 'custom'
? 'bg-gradient-to-r from-cyan-500 via-purple-500 to-pink-500 text-white shadow-[0_0_30px_rgba(34,211,238,0.5),0_0_60px_rgba(168,85,247,0.3)] transform scale-105 border-2 border-cyan-400/50 energy-flow'
: 'text-white/80 hover:text-white hover:bg-white/10 backdrop-blur-sm border border-cyan-400/20 hover:border-cyan-400/40 hover:shadow-[0_0_20px_rgba(34,211,238,0.3)]'
? 'bg-teal-600 text-white'
: 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
}`}
>
<span className="flex items-center justify-center gap-2">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
Custom Bridge
</span>
Custom
</button>
<button
onClick={() => setActiveTab('trustless')}
role="tab"
aria-selected={activeTab === 'trustless'}
aria-controls="trustless-tabpanel"
id="trustless-tab"
className={`px-4 py-3 text-sm font-medium rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-teal-500/50 ${
activeTab === 'trustless'
? 'bg-teal-600 text-white'
: 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
}`}
>
Trustless
</button>
<button
onClick={() => setActiveTab('evm')}
@@ -74,18 +86,13 @@ export default function BridgePage() {
aria-selected={activeTab === 'evm'}
aria-controls="evm-tabpanel"
id="evm-tab"
className={`flex-1 min-w-[140px] px-6 py-4 font-bold rounded-xl transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-cyan-400/50 relative overflow-hidden ${
className={`px-4 py-3 text-sm font-medium rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-teal-500/50 ${
activeTab === 'evm'
? 'bg-gradient-to-r from-cyan-500 via-purple-500 to-pink-500 text-white shadow-[0_0_30px_rgba(34,211,238,0.5),0_0_60px_rgba(168,85,247,0.3)] transform scale-105 border-2 border-cyan-400/50 energy-flow'
: 'text-white/80 hover:text-white hover:bg-white/10 backdrop-blur-sm border border-cyan-400/20 hover:border-cyan-400/40 hover:shadow-[0_0_20px_rgba(34,211,238,0.3)]'
? 'bg-teal-600 text-white'
: 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
}`}
>
<span className="flex items-center justify-center gap-2">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
ThirdWeb Bridge
</span>
ThirdWeb
</button>
<button
onClick={() => setActiveTab('xrpl')}
@@ -93,18 +100,13 @@ export default function BridgePage() {
aria-selected={activeTab === 'xrpl'}
aria-controls="xrpl-tabpanel"
id="xrpl-tab"
className={`flex-1 min-w-[140px] px-6 py-4 font-bold rounded-xl transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-cyan-400/50 relative overflow-hidden ${
className={`px-4 py-3 text-sm font-medium rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-teal-500/50 ${
activeTab === 'xrpl'
? 'bg-gradient-to-r from-cyan-500 via-purple-500 to-pink-500 text-white shadow-[0_0_30px_rgba(34,211,238,0.5),0_0_60px_rgba(168,85,247,0.3)] transform scale-105 border-2 border-cyan-400/50 energy-flow'
: 'text-white/80 hover:text-white hover:bg-white/10 backdrop-blur-sm border border-cyan-400/20 hover:border-cyan-400/40 hover:shadow-[0_0_20px_rgba(34,211,238,0.3)]'
? 'bg-teal-600 text-white'
: 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
}`}
>
<span className="flex items-center justify-center gap-2">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
XRPL Bridge
</span>
XRPL
</button>
<button
onClick={() => setActiveTab('track')}
@@ -112,30 +114,55 @@ export default function BridgePage() {
aria-selected={activeTab === 'track'}
aria-controls="track-tabpanel"
id="track-tab"
className={`flex-1 min-w-[140px] px-6 py-4 font-bold rounded-xl transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-cyan-400/50 relative overflow-hidden ${
className={`px-4 py-3 text-sm font-medium rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-teal-500/50 ${
activeTab === 'track'
? 'bg-gradient-to-r from-cyan-500 via-purple-500 to-pink-500 text-white shadow-[0_0_30px_rgba(34,211,238,0.5),0_0_60px_rgba(168,85,247,0.3)] transform scale-105 border-2 border-cyan-400/50 energy-flow'
: 'text-white/80 hover:text-white hover:bg-white/10 backdrop-blur-sm border border-cyan-400/20 hover:border-cyan-400/40 hover:shadow-[0_0_20px_rgba(34,211,238,0.3)]'
? 'bg-teal-600 text-white'
: 'text-[#A0A0A0] hover:text-white hover:bg-white/5'
}`}
>
<span className="flex items-center justify-center gap-2">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Track Transfer
</span>
Track
</button>
</nav>
</div>
{/* Content */}
<div className="mt-6 animate-fadeIn">
{activeTab === 'trustless' && (
<div role="tabpanel" id="trustless-tabpanel" aria-labelledby="trustless-tab">
<TrustlessBridgeForm />
</div>
)}
{activeTab === 'custom' && (
<div role="tabpanel" id="custom-tabpanel" aria-labelledby="custom-tab">
<BridgeButtons
destinationChainSelector={CHAIN_SELECTORS.ETHEREUM_MAINNET}
recipientAddress={undefined}
/>
<div role="tabpanel" id="custom-tabpanel" aria-labelledby="custom-tab" className="w-full max-w-2xl mx-auto">
<div className="bg-[#252830] rounded-2xl border border-white/10 shadow-xl p-4 sm:p-6 md:p-8 w-full">
<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>
</div>
<div className="flex items-center gap-2">
<span className="text-[#A0A0A0] font-medium">To</span>
{CCIP_DESTINATIONS.map((d) => d.selector === ccipDestinationSelector && (
<ChainIcon key={d.chainId} chainId={d.chainId} name={d.name} size={28} />
))}
<select
value={ccipDestinationSelector}
onChange={(e) => setCcipDestinationSelector(e.target.value)}
className="px-3 py-2 rounded-lg bg-[#1a1d24] text-white border border-white/20 min-w-[200px] focus:ring-2 focus:ring-teal-500/50 focus:border-teal-500"
>
{CCIP_DESTINATIONS.map((d) => (
<option key={d.chainId} value={d.selector}>{d.name}</option>
))}
</select>
</div>
</div>
<SwapBridgeSwapQuoteForm />
<BridgeButtons
destinationChainSelector={ccipDestinationSelector}
recipientAddress={undefined}
/>
</div>
</div>
)}
{activeTab === 'evm' && (
@@ -154,6 +181,7 @@ export default function BridgePage() {
</div>
)}
</div>
</div>
</div>
);

View File

@@ -0,0 +1,19 @@
/**
* Developer / API documentation. Not shown in main UI.
*/
export default function DocsPage() {
return (
<div className="container mx-auto px-4 py-12 max-w-3xl">
<h1 className="text-2xl font-bold text-white mb-2">Developer reference</h1>
<p className="text-[#A0A0A0] text-sm mb-8">
For swap+bridge+swap (Dodoex PMM): use <code className="bg-[#252830] px-1.5 py-0.5 rounded text-teal-400">POST /api/bridge/quote</code> with{' '}
<code className="bg-[#252830] px-1.5 py-0.5 rounded text-teal-400">sourceToken</code>, <code className="bg-[#252830] px-1.5 py-0.5 rounded text-teal-400">destinationToken</code>,{' '}
<code className="bg-[#252830] px-1.5 py-0.5 rounded text-teal-400">sourceChainId</code>, <code className="bg-[#252830] px-1.5 py-0.5 rounded text-teal-400">destinationChainId</code>,{' '}
<code className="bg-[#252830] px-1.5 py-0.5 rounded text-teal-400">amount</code>. Response includes{' '}
<code className="bg-[#252830] px-1.5 py-0.5 rounded text-teal-400">sourceSwapQuote</code>,{' '}
<code className="bg-[#252830] px-1.5 py-0.5 rounded text-teal-400">destinationSwapQuote</code>,{' '}
<code className="bg-[#252830] px-1.5 py-0.5 rounded text-teal-400">minReceived</code>.
</p>
</div>
);
}

View File

@@ -0,0 +1,118 @@
/**
* Thirdweb Wallets (v5) integration demo.
* Shows ConnectButton with in-app wallet (email, social, passkey) + external wallets.
* See: https://portal.thirdweb.com/wallets, docs/04-configuration/THIRDWEB_WALLETS_INTEGRATION.md
*/
import { createThirdwebClient, defineChain } from 'thirdweb'
import { ThirdwebProvider, ConnectButton, useActiveAccount, useWalletBalance } from 'thirdweb/react'
import { inAppWallet } from 'thirdweb/wallets'
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 chain138 = defineChain({
id: 138,
name: 'DeFi Oracle Meta Mainnet',
rpc: rpcUrl138,
nativeCurrency: {
name: 'Ether',
symbol: 'ETH',
decimals: 18,
},
})
const wallets = [
inAppWallet({
auth: {
options: ['email', 'google', 'apple', 'passkey'],
},
metadata: {
name: 'DBIS Bridge',
image: {
src: 'https://explorer.d-bis.org/favicon.ico',
width: 32,
height: 32,
},
},
}),
]
function WalletsDemoContent() {
const account = useActiveAccount()
const { data: balance, isLoading: balanceLoading } = useWalletBalance({
client,
chain: chain138,
address: account?.address,
})
return (
<div className="max-w-2xl mx-auto p-6 space-y-8">
<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.).
</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={chain138}
wallets={wallets}
theme="dark"
connectButton={{
label: 'Connect wallet',
style: {
backgroundColor: '#0d9488',
color: 'white',
borderRadius: '0.75rem',
padding: '0.75rem 1.5rem',
},
}}
/>
{account && (
<div className="w-full space-y-2 text-sm">
<p className="text-[#A0A0A0]">
Address: <span className="font-mono text-white">{account.address}</span>
</p>
{balanceLoading ? (
<p className="text-[#A0A0A0]">Loading balance</p>
) : balance ? (
<p className="text-[#A0A0A0]">
Balance (Chain 138):{' '}
<span className="text-white">
{balance.displayValue} {balance.symbol}
</span>
</p>
) : null}
</div>
)}
</div>
<div className="text-sm text-[#A0A0A0] space-y-2">
<p>
This page uses thirdweb SDK v5 and the{' '}
<a
href="https://portal.thirdweb.com/wallets"
target="_blank"
rel="noopener noreferrer"
className="text-teal-400 hover:underline"
>
thirdweb Wallets
</a>{' '}
flow. The main Bridge still uses the existing connect (wagmi + external wallets). Full app
integration is documented in <code className="text-white/80">docs/04-configuration/THIRDWEB_WALLETS_INTEGRATION.md</code>.
</p>
</div>
</div>
)
}
export default function WalletsDemoPage() {
return (
<ThirdwebProvider>
<WalletsDemoContent />
</ThirdwebProvider>
)
}

View File

@@ -15,7 +15,7 @@ export default defineConfig({
process: true,
},
// Include specific polyfills
include: ['buffer', 'events', 'stream', 'util', 'crypto'],
include: ['buffer', 'events', 'stream', 'util', 'crypto', 'vm'],
// Exclude Node.js built-ins that shouldn't be polyfilled
exclude: ['https', 'http', 'url', 'path', 'fs', 'os', 'net', 'tls', 'zlib'],
}),
@@ -24,15 +24,24 @@ export default defineConfig({
alias: {
'@': path.resolve(__dirname, './src'),
},
// Dedupe to avoid "multiple instances" warnings from transitive deps (e.g. @emotion/react, Lit)
dedupe: [
'react',
'react-dom',
'@emotion/react',
'@emotion/styled',
'lit',
'lit-html',
'lit-element',
],
},
server: {
port: 3002,
},
optimizeDeps: {
// Exclude problematic packages from optimization
exclude: ['@safe-global/safe-core-sdk', 'https', 'http', 'url', 'stream', 'util', 'crypto', 'path', 'fs', 'os', 'net', 'tls', 'zlib'],
// Include Safe SDK dependencies
include: ['@safe-global/safe-ethers-lib', '@safe-global/safe-service-client'],
exclude: ['https', 'http', 'url', 'stream', 'util', 'crypto', 'path', 'fs', 'os', 'net', 'tls', 'zlib'],
include: ['@safe-global/protocol-kit'],
},
define: {
global: 'globalThis',
@@ -44,8 +53,9 @@ export default defineConfig({
// Add Content Security Policy meta tag via HTML plugin if needed
},
external: (id) => {
// Mark Node.js built-ins as external for browser
const nodeBuiltIns = ['https', 'http', 'url', 'stream', 'util', 'crypto', 'path', 'fs', 'os', 'net', 'tls', 'zlib']
// Do NOT externalise crypto, buffer, stream, util, events - vite-plugin-node-polyfills provides them for the browser.
// Externalise only Node built-ins we don't polyfill (bundling would fail or break in browser).
const nodeBuiltIns = ['path', 'fs', 'os', 'net', 'tls', 'zlib', 'https', 'http', 'url']
return nodeBuiltIns.includes(id)
},
},