Skip to content

Commit

Permalink
feat(frontend): add transfer and withdrawal functionality
Browse files Browse the repository at this point in the history
- Add current balance display from indexer
- Add transfer and withdrawal buttons
- Implement forced withdrawal flow with timelock
- Add Base and Base Sepolia chain support
- Update contract ABI with withdrawal functions
- Create dialog components for withdrawal actions
- Update useCompact hook with withdrawal methods
- Fix ESLint configuration
  • Loading branch information
0age committed Dec 6, 2024
1 parent dafc4cb commit 8db4563
Show file tree
Hide file tree
Showing 9 changed files with 805 additions and 134 deletions.
94 changes: 27 additions & 67 deletions frontend/eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,70 +1,30 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'

export default tseslint.config(
{
// Ignore node_modules and dist
ignores: ['node_modules/**/*', 'dist/**/*']
module.exports = {
root: true,
env: {
browser: true,
es2020: true,
},
{
// Base config for all files
files: ['**/*.{js,jsx,ts,tsx}'],
extends: [js.configs.recommended],
languageOptions: {
ecmaVersion: 2020,
globals: {
...globals.browser,
...globals.es2020,
},
parserOptions: {
ecmaFeatures: {
jsx: true
}
}
}
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'@typescript-eslint/no-explicit-any': 'warn',
},
{
// TypeScript-specific config
files: ['**/*.{ts,tsx}'],
extends: [...tseslint.configs.recommended],
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}],
// Allow console.error for error logging
'no-console': ['error', { allow: ['error'] }],
// Ensure return types are specified
'@typescript-eslint/explicit-function-return-type': ['error', {
allowExpressions: true,
allowTypedFunctionExpressions: true,
allowHigherOrderFunctions: true,
allowDirectConstAssertionInArrowFunctions: true,
allowConciseArrowFunctionExpressionsStartingWithVoid: true,
}],
},
settings: {
react: {
version: 'detect'
}
overrides: [
{
files: ['*.ts', '*.tsx'],
rules: {
'@typescript-eslint/explicit-function-return-type': 'off',
},
},
languageOptions: {
parserOptions: {
project: ['./tsconfig.json']
}
}
}
)
],
}
209 changes: 143 additions & 66 deletions frontend/src/components/BalanceDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { useAccount } from 'wagmi';
import { useBalances } from '../hooks/useBalances';
import { useState } from 'react'
import { useAccount } from 'wagmi'
import { useBalances } from '../hooks/useBalances'
import { useResourceLocks } from '../hooks/useResourceLocks'
import { formatUnits } from 'viem'
import { Transfer } from './Transfer'
import { InitiateForcedWithdrawalDialog } from './InitiateForcedWithdrawalDialog'
import { ForcedWithdrawalDialog } from './ForcedWithdrawalDialog'

// Utility function to format reset period
const formatResetPeriod = (seconds: number): string => {
Expand All @@ -10,12 +16,17 @@ const formatResetPeriod = (seconds: number): string => {
};

export function BalanceDisplay(): JSX.Element | null {
const { isConnected } = useAccount();
const { balances, error, isLoading } = useBalances();
const { isConnected } = useAccount()
const { balances, error, isLoading } = useBalances()
const { data: resourceLocksData, isLoading: resourceLocksLoading } = useResourceLocks()
const [isWithdrawalDialogOpen, setIsWithdrawalDialogOpen] = useState(false)
const [isExecuteDialogOpen, setIsExecuteDialogOpen] = useState(false)
const [selectedLockId, setSelectedLockId] = useState<string>('')
const [selectedLock, setSelectedLock] = useState<any>(null)

if (!isConnected) return null;

if (isLoading) {
if (isLoading || resourceLocksLoading) {
return (
<div className="flex justify-center items-center py-4">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#00ff00]"></div>
Expand Down Expand Up @@ -61,84 +72,150 @@ export function BalanceDisplay(): JSX.Element | null {
</div>

<div className="space-y-4">
{balances.map((balance) => (
<div
key={`${balance.chainId}-${balance.lockId}`}
className="p-4 bg-gray-800 rounded-lg"
>
{/* Header with Chain, Token Info, and Lock ID */}
<div className="flex justify-between items-baseline mb-4">
<div className="text-sm font-medium text-gray-300">
Chain {balance.chainId}
{balance.token && (
<span className="ml-2 text-gray-400">
{balance.token.name} ({balance.token.symbol})
{balances.map((balance) => {
// Find matching resource lock from indexer data
const resourceLock = resourceLocksData?.resourceLocks.items.find(
(item) => item.resourceLock.lockId === balance.lockId
)

return (
<div
key={`${balance.chainId}-${balance.lockId}`}
className="p-4 bg-gray-800 rounded-lg"
>
{/* Header with Chain, Token Info, and Lock ID */}
<div className="flex justify-between items-baseline mb-4">
<div className="text-sm font-medium text-gray-300">
Chain {balance.chainId}
{balance.token && (
<span className="ml-2 text-gray-400">
{balance.token.name} ({balance.token.symbol})
</span>
)}
</div>
<div className="text-xs text-gray-400">Lock ID: {balance.lockId}</div>
</div>

{/* Resource Lock Properties */}
<div className="flex gap-2 mb-4">
{balance.resourceLock?.resetPeriod && balance.resourceLock.resetPeriod > 0 && (
<span className="px-2 py-1 text-xs bg-[#00ff00]/10 text-[#00ff00] rounded">
Reset Period: {formatResetPeriod(balance.resourceLock.resetPeriod)}
</span>
)}
{balance.resourceLock?.isMultichain && (
<span className="px-2 py-1 text-xs bg-[#00ff00]/10 text-[#00ff00] rounded">
Multichain
</span>
)}
<span className={`px-2 py-1 text-xs rounded ${
balance.withdrawalStatus === 0
? 'bg-[#00ff00]/10 text-[#00ff00]'
: 'bg-orange-500/10 text-orange-500'
}`}>
{balance.withdrawalStatus === 0 ? 'Active' : 'Withdrawal Pending'}
</span>
</div>
<div className="text-xs text-gray-400">Lock ID: {balance.lockId}</div>
</div>

{/* Resource Lock Properties */}
<div className="flex gap-2 mb-4">
{balance.resourceLock?.resetPeriod && balance.resourceLock.resetPeriod > 0 && (
<span className="px-2 py-1 text-xs bg-[#00ff00]/10 text-[#00ff00] rounded">
Reset Period: {formatResetPeriod(balance.resourceLock.resetPeriod)}
</span>
)}
{balance.resourceLock?.isMultichain && (
<span className="px-2 py-1 text-xs bg-[#00ff00]/10 text-[#00ff00] rounded">
Multichain
</span>
)}
<span className={`px-2 py-1 text-xs rounded ${
balance.withdrawalStatus === 0
? 'bg-[#00ff00]/10 text-[#00ff00]'
: 'bg-orange-500/10 text-orange-500'
}`}>
{balance.withdrawalStatus === 0 ? 'Active' : 'Withdrawal Pending'}
</span>
</div>
{/* Balances Grid */}
<div className="grid grid-cols-12 gap-4">
{/* Left side - Current, Allocatable, and Allocated */}
<div className="col-span-8 grid grid-cols-3 gap-4 pr-4 border-r border-gray-700">
<div>
<div className="text-xs text-gray-400">Current</div>
<div className="mt-1 text-sm text-[#00ff00] font-mono">
{resourceLock && formatUnits(BigInt(resourceLock.balance), resourceLock.resourceLock.token.decimals)}
{balance.token?.symbol && (
<span className="ml-1 text-gray-400">{balance.token.symbol}</span>
)}
</div>
</div>

{/* Balances Grid */}
<div className="grid grid-cols-12 gap-4">
{/* Left side - Compact display of allocatable and allocated */}
<div className="col-span-7 grid grid-cols-2 gap-4 pr-4 border-r border-gray-700">
<div>
<div className="text-xs text-gray-400">Allocatable</div>
<div className="mt-1 text-sm text-[#00ff00] font-mono">
{balance.formattedAllocatableBalance || balance.allocatableBalance}
{balance.token?.symbol && (
<span className="ml-1 text-gray-400">{balance.token.symbol}</span>
)}
<div>
<div className="text-xs text-gray-400">Allocatable</div>
<div className="mt-1 text-sm text-[#00ff00] font-mono">
{balance.formattedAllocatableBalance || balance.allocatableBalance}
{balance.token?.symbol && (
<span className="ml-1 text-gray-400">{balance.token.symbol}</span>
)}
</div>
</div>

<div>
<div className="text-xs text-gray-400">Allocated</div>
<div className="mt-1 text-sm text-[#00ff00] font-mono">
{balance.formattedAllocatedBalance || balance.allocatedBalance}
{balance.token?.symbol && (
<span className="ml-1 text-gray-400">{balance.token.symbol}</span>
)}
</div>
</div>
</div>

<div>
<div className="text-xs text-gray-400">Allocated</div>
<div className="mt-1 text-sm text-[#00ff00] font-mono">
{balance.formattedAllocatedBalance || balance.allocatedBalance}
{/* Right side - Emphasized available to allocate */}
<div className="col-span-4 flex flex-col justify-center">
<div className="text-xs text-gray-400">Available to Allocate</div>
<div className="mt-1 text-lg font-bold text-[#00ff00] font-mono">
{balance.formattedAvailableBalance || balance.balanceAvailableToAllocate}
{balance.token?.symbol && (
<span className="ml-1 text-gray-400">{balance.token.symbol}</span>
<span className="ml-1 text-gray-400 text-sm">{balance.token.symbol}</span>
)}
</div>
</div>
</div>

{/* Right side - Emphasized available to allocate */}
<div className="col-span-5 flex flex-col justify-center">
<div className="text-xs text-gray-400">Available to Allocate</div>
<div className="mt-1 text-lg font-bold text-[#00ff00] font-mono">
{balance.formattedAvailableBalance || balance.balanceAvailableToAllocate}
{balance.token?.symbol && (
<span className="ml-1 text-gray-400 text-sm">{balance.token.symbol}</span>
)}
{/* Transfer and Withdrawal Actions */}
{resourceLock && (
<div className="mt-4 border-t border-gray-700 pt-4">
<Transfer
allocatorAddress={resourceLock.resourceLock.allocator.account}
chainId={balance.chainId}
resourceLockBalance={resourceLock.balance}
lockId={BigInt(balance.lockId)}
decimals={resourceLock.resourceLock.token.decimals}
tokenName={{
resourceLockName: resourceLock.resourceLock.token.name,
resourceLockSymbol: resourceLock.resourceLock.token.symbol,
tokenName: balance.token?.name || ''
}}
tokenSymbol={balance.token?.symbol || ''}
resetPeriod={balance.resourceLock?.resetPeriod || 0}
withdrawalStatus={balance.withdrawalStatus}
withdrawableAt={balance.withdrawableAt || '0'}
onForceWithdraw={() => {
setSelectedLockId(balance.lockId)
setIsWithdrawalDialogOpen(true)
}}
onDisableForceWithdraw={() => {
setSelectedLockId(balance.lockId)
setSelectedLock(null)
}}
/>
</div>
</div>
)}
</div>
</div>
))}
)
})}
</div>

{/* Withdrawal Dialogs */}
<InitiateForcedWithdrawalDialog
isOpen={isWithdrawalDialogOpen}
onClose={() => setIsWithdrawalDialogOpen(false)}
lockId={selectedLockId}
resetPeriod={parseInt(balances.find(b => b.lockId === selectedLockId)?.resourceLock?.resetPeriod?.toString() || "0")}
/>

<ForcedWithdrawalDialog
isOpen={isExecuteDialogOpen}
onClose={() => setIsExecuteDialogOpen(false)}
lockId={selectedLockId}
maxAmount={selectedLock?.balance || '0'}
decimals={selectedLock?.decimals || 18}
symbol={selectedLock?.symbol || ''}
tokenName={selectedLock?.tokenName || ''}
chainId={parseInt(selectedLock?.chainId || '1')}
/>
</div>
);
}
Loading

0 comments on commit 8db4563

Please sign in to comment.