Skip to content

Commit

Permalink
add token details to balances
Browse files Browse the repository at this point in the history
  • Loading branch information
0age committed Dec 6, 2024
1 parent 3823ffd commit dafc4cb
Show file tree
Hide file tree
Showing 6 changed files with 486 additions and 75 deletions.
54 changes: 48 additions & 6 deletions frontend/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,31 @@ import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'

export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
// Ignore node_modules and dist
ignores: ['node_modules/**/*', 'dist/**/*']
},
{
// Base config for all files
files: ['**/*.{js,jsx,ts,tsx}'],
extends: [js.configs.recommended],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
globals: {
...globals.browser,
...globals.es2020,
},
parserOptions: {
ecmaFeatures: {
jsx: true
}
}
}
},
{
// TypeScript-specific config
files: ['**/*.{ts,tsx}'],
extends: [...tseslint.configs.recommended],
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
Expand All @@ -23,6 +40,31 @@ export default tseslint.config(
'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'
}
},
languageOptions: {
parserOptions: {
project: ['./tsconfig.json']
}
}
}
)
94 changes: 66 additions & 28 deletions frontend/src/components/BalanceDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { useAccount } from 'wagmi';
import { useBalances } from '../hooks/useBalances';

export function BalanceDisplay() {
// Utility function to format reset period
const formatResetPeriod = (seconds: number): string => {
if (seconds < 60) return `${seconds} seconds`;
if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours`;
return `${Math.floor(seconds / 86400)} days`;
};

export function BalanceDisplay(): JSX.Element | null {
const { isConnected } = useAccount();
const { balances, error, isLoading } = useBalances();

Expand Down Expand Up @@ -58,43 +66,73 @@ export function BalanceDisplay() {
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}</div>
<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>

<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<div className="text-xs text-gray-400">Allocatable Balance</div>
<div className="mt-1 text-[#00ff00] font-mono">
{balance.allocatableBalance}
</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>
<div className="text-xs text-gray-400">Allocated Balance</div>
<div className="mt-1 text-[#00ff00] font-mono">
{balance.allocatedBalance}
{/* 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>
</div>

<div>
<div className="text-xs text-gray-400">Available to Allocate</div>
<div className="mt-1 text-[#00ff00] font-mono">
{balance.balanceAvailableToAllocate}
<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">Withdrawal Status</div>
<div className="mt-1">
<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>
{/* 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>
)}
</div>
</div>
</div>
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/config/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Default to production URL
const DEFAULT_GRAPHQL_URL = 'https://the-compact-indexer-2.ponder-dev.com/'

interface Config {
graphqlUrl: string
}

export const config: Config = {
graphqlUrl: import.meta.env.VITE_GRAPHQL_INDEXER_URL || DEFAULT_GRAPHQL_URL,
}
152 changes: 111 additions & 41 deletions frontend/src/hooks/useBalances.ts
Original file line number Diff line number Diff line change
@@ -1,80 +1,150 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { useAccount } from 'wagmi';
import { useState, useEffect, useRef, useCallback } from 'react'
import { useAccount } from 'wagmi'
import { useResourceLocks } from './useResourceLocks'
import { formatUnits } from 'viem'

interface Token {
tokenAddress: string
name: string
symbol: string
decimals: number
}

interface ResourceLock {
resetPeriod: number
isMultichain: boolean
}

interface Balance {
chainId: string;
lockId: string;
allocatableBalance: string;
allocatedBalance: string;
balanceAvailableToAllocate: string;
withdrawalStatus: number;
chainId: string
lockId: string
allocatableBalance: string
allocatedBalance: string
balanceAvailableToAllocate: string
withdrawalStatus: number
// Token details from indexer
token?: Token
// Resource lock details from indexer
resourceLock?: ResourceLock
// Formatted balances using token decimals
formattedAllocatableBalance?: string
formattedAllocatedBalance?: string
formattedAvailableBalance?: string
}

interface UseBalancesResult {
balances: Balance[];
error: string | null;
isLoading: boolean;
balances: Balance[]
error: string | null
isLoading: boolean
}

interface ResourceLockItem {
resourceLock: {
lockId: string
token: Token
resetPeriod: number
isMultichain: boolean
}
}

export function useBalances(): UseBalancesResult {
const { address, isConnected } = useAccount();
const [balances, setBalances] = useState<Balance[]>([]);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const isFetchingRef = useRef(false);

const fetchBalances = useCallback(async () => {
if (!isConnected || !address || isFetchingRef.current) return;
const { address, isConnected } = useAccount()
const [balances, setBalances] = useState<Balance[]>([])
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const isFetchingRef = useRef(false)

// Get resource lock details from indexer
const { data: resourceLocksData, error: resourceLocksError, isLoading: resourceLocksLoading } = useResourceLocks()

const fetchBalances = useCallback(async (): Promise<void> => {
if (!isConnected || !address || isFetchingRef.current) return

isFetchingRef.current = true;
isFetchingRef.current = true

try {
const sessionId = localStorage.getItem(`session-${address}`);
const sessionId = localStorage.getItem(`session-${address}`)
if (!sessionId) {
throw new Error('No session ID found');
throw new Error('No session ID found')
}

const response = await fetch('/balances', {
headers: {
'x-session-id': sessionId
}
});
})

if (!response.ok) throw new Error('Failed to fetch balances.');
if (!response.ok) throw new Error('Failed to fetch balances.')

const data = await response.json();
const data = await response.json()

// Only update state if data has actually changed
setBalances(prevBalances => {
const newBalances = data.balances;
const hasChanged = JSON.stringify(prevBalances) !== JSON.stringify(newBalances);
return hasChanged ? newBalances : prevBalances;
});
const newBalances = data.balances.map((balance: Balance) => {
// Find matching resource lock from indexer data
const resourceLock = resourceLocksData?.resourceLocks.items.find(
(item: ResourceLockItem) => item.resourceLock.lockId === balance.lockId
)

if (resourceLock) {
const token = resourceLock.resourceLock.token
const decimals = token.decimals

return {
...balance,
token,
resourceLock: {
resetPeriod: resourceLock.resourceLock.resetPeriod,
isMultichain: resourceLock.resourceLock.isMultichain
},
formattedAllocatableBalance: formatUnits(BigInt(balance.allocatableBalance), decimals),
formattedAllocatedBalance: formatUnits(BigInt(balance.allocatedBalance), decimals),
formattedAvailableBalance: formatUnits(BigInt(balance.balanceAvailableToAllocate), decimals)
}
}

return balance
})

const hasChanged = JSON.stringify(prevBalances) !== JSON.stringify(newBalances)
return hasChanged ? newBalances : prevBalances
})

setError(null);
setError(null)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch balances');
setError(err instanceof Error ? err.message : 'Failed to fetch balances')
} finally {
isFetchingRef.current = false;
isFetchingRef.current = false
}
}, [isConnected, address]);
}, [isConnected, address, resourceLocksData])

useEffect(() => {
// Initial load should show loading state
if (isConnected && address) {
setIsLoading(true);
fetchBalances().finally(() => setIsLoading(false));
setIsLoading(true)
void fetchBalances().finally(() => setIsLoading(false))
}

// Set up polling interval
const intervalId = setInterval(fetchBalances, 5000);
const intervalId = setInterval(() => void fetchBalances(), 1000) // Poll every second

// Cleanup on unmount or address change
return () => {
clearInterval(intervalId);
isFetchingRef.current = false;
};
}, [fetchBalances, isConnected, address]);
clearInterval(intervalId)
isFetchingRef.current = false
}
}, [fetchBalances, isConnected, address])

// Set error from resource locks if present
useEffect(() => {
if (resourceLocksError) {
setError(resourceLocksError instanceof Error ? resourceLocksError.message : 'Failed to fetch resource locks')
}
}, [resourceLocksError])

return { balances, error, isLoading };
return {
balances,
error,
isLoading: isLoading || resourceLocksLoading
}
}
Loading

0 comments on commit dafc4cb

Please sign in to comment.