Skip to content

Commit 4c387f9

Browse files
committed
feat: improve pending transactions card
1 parent 9a88c73 commit 4c387f9

File tree

3 files changed

+144
-76
lines changed

3 files changed

+144
-76
lines changed

components/Dashboard/PendingTransactions.tsx

Lines changed: 68 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -5,62 +5,21 @@ import { Button } from '@/components/ui/button';
55
import { Badge } from '@/components/ui/badge';
66
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
77
import { Clock, ArrowUpRight, ArrowDownLeft, ExternalLink, CheckCircle, XCircle } from 'lucide-react';
8-
9-
interface PendingTransaction {
10-
id: string;
11-
type: 'send' | 'receive' | 'swap';
12-
amount: string;
13-
token: string;
14-
status: 'pending' | 'confirming' | 'failed';
15-
timestamp: string;
16-
from?: string;
17-
to?: string;
18-
hash?: string;
19-
}
8+
import { SafeMultisigTransactionResponse } from '@safe-global/types-kit';
9+
import { usePendingTransactions } from '@/hooks/safe/usePendingTransactions';
2010

2111
interface PendingTransactionsProps {
22-
transactions?: PendingTransaction[];
2312
onViewAll?: () => void;
2413
}
2514

26-
const mockTransactions: PendingTransaction[] = [
27-
{
28-
id: '1',
29-
type: 'send',
30-
amount: '0.5',
31-
token: 'ETH',
32-
status: 'pending',
33-
timestamp: '2 min ago',
34-
to: '0x1234...5678',
35-
hash: '0xabcd...efgh',
36-
},
37-
{
38-
id: '2',
39-
type: 'swap',
40-
amount: '1000',
41-
token: 'USDC',
42-
status: 'confirming',
43-
timestamp: '5 min ago',
44-
hash: '0x9876...5432',
45-
},
46-
{
47-
id: '3',
48-
type: 'receive',
49-
amount: '2.5',
50-
token: 'ETH',
51-
status: 'pending',
52-
timestamp: '10 min ago',
53-
from: '0x5678...1234',
54-
hash: '0xdcba...hgfe',
55-
},
56-
];
15+
export function PendingTransactions({ onViewAll }: PendingTransactionsProps) {
16+
const { pendingTransactions, loading, totalCount, error } = usePendingTransactions();
5717

58-
export function PendingTransactions({ transactions = mockTransactions, onViewAll }: PendingTransactionsProps) {
5918
const getStatusIcon = (status: string) => {
6019
switch (status) {
61-
case 'confirming':
20+
case 'AWAITING_EXECUTION':
6221
return <CheckCircle className='h-4 w-4 text-green-500' />;
63-
case 'failed':
22+
case 'FAILED':
6423
return <XCircle className='h-4 w-4 text-red-500' />;
6524
default:
6625
return <Clock className='h-4 w-4 text-yellow-500' />;
@@ -69,44 +28,59 @@ export function PendingTransactions({ transactions = mockTransactions, onViewAll
6928

7029
const getStatusBadge = (status: string) => {
7130
switch (status) {
72-
case 'confirming':
31+
case 'AWAITING_EXECUTION':
7332
return (
7433
<Badge variant='default' className='bg-green-500'>
75-
Confirming
34+
Ready to Execute
7635
</Badge>
7736
);
78-
case 'failed':
37+
case 'FAILED':
7938
return <Badge variant='destructive'>Failed</Badge>;
39+
case 'AWAITING_CONFIRMATIONS':
40+
return <Badge variant='secondary'>Awaiting Confirmations</Badge>;
8041
default:
8142
return <Badge variant='secondary'>Pending</Badge>;
8243
}
8344
};
8445

85-
const getTypeIcon = (type: string) => {
86-
switch (type) {
87-
case 'send':
88-
return <ArrowUpRight className='h-4 w-4 text-red-500' />;
89-
case 'receive':
90-
return <ArrowDownLeft className='h-4 w-4 text-green-500' />;
91-
case 'swap':
92-
return <ArrowUpRight className='h-4 w-4 text-blue-500' />;
93-
default:
94-
return <ArrowUpRight className='h-4 w-4' />;
46+
const getTypeIcon = (tx: SafeMultisigTransactionResponse) => {
47+
// Determine transaction type based on data and value
48+
if (tx.value && tx.value !== '0') {
49+
return <ArrowUpRight className='h-4 w-4 text-red-500' />;
9550
}
51+
if (tx.data && tx.data !== '0x') {
52+
return <ArrowUpRight className='h-4 w-4 text-blue-500' />;
53+
}
54+
return <ArrowUpRight className='h-4 w-4' />;
55+
};
56+
57+
const formatAmount = (value: string) => {
58+
if (!value || value === '0') return '0';
59+
// Convert from wei to ether (simplified)
60+
const etherValue = parseFloat(value) / Math.pow(10, 18);
61+
return etherValue.toFixed(6);
62+
};
63+
64+
const formatTimestamp = (timestamp: string) => {
65+
const date = new Date(timestamp);
66+
const now = new Date();
67+
const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1000 * 60));
68+
69+
if (diffInMinutes < 1) return 'Just now';
70+
if (diffInMinutes < 60) return `${diffInMinutes} min ago`;
71+
if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)} hours ago`;
72+
return `${Math.floor(diffInMinutes / 1440)} days ago`;
9673
};
9774

9875
return (
9976
<Card>
10077
<CardHeader className='pb-4'>
10178
<div className='flex items-center justify-between'>
10279
<div className='flex items-center gap-3'>
103-
<div className='w-8 h-8 bg-yellow-500/10 rounded-lg flex items-center justify-center'>
104-
<Clock className='h-4 w-4 text-yellow-500' />
105-
</div>
10680
<div>
10781
<CardTitle className='text-lg'>Pending Transactions</CardTitle>
10882
<p className='text-sm text-muted-foreground'>
109-
{transactions.length} transaction{transactions.length !== 1 ? 's' : ''} waiting
83+
{loading ? 'Loading...' : `${totalCount} transaction${totalCount !== 1 ? 's' : ''} waiting`}
11084
</p>
11185
</div>
11286
</div>
@@ -118,36 +92,54 @@ export function PendingTransactions({ transactions = mockTransactions, onViewAll
11892
</div>
11993
</CardHeader>
12094
<CardContent className='space-y-4'>
121-
{transactions.length === 0 ? (
95+
{loading ? (
96+
<div className='space-y-3'>
97+
{[1, 2, 3].map((i) => (
98+
<div key={i} className='flex items-center justify-between p-3 rounded-lg border'>
99+
<div className='flex items-center gap-3'>
100+
<div className='h-8 w-8 bg-muted rounded-full animate-pulse' />
101+
<div className='space-y-2'>
102+
<div className='h-4 w-24 bg-muted rounded animate-pulse' />
103+
<div className='h-3 w-16 bg-muted rounded animate-pulse' />
104+
</div>
105+
</div>
106+
<div className='h-6 w-20 bg-muted rounded animate-pulse' />
107+
</div>
108+
))}
109+
</div>
110+
) : error ? (
111+
<div className='text-center py-8'>
112+
<Clock className='h-12 w-12 text-muted-foreground mx-auto mb-4' />
113+
<p className='text-muted-foreground'>Failed to load transactions</p>
114+
</div>
115+
) : pendingTransactions.length === 0 ? (
122116
<div className='text-center py-8'>
123117
<Clock className='h-12 w-12 text-muted-foreground mx-auto mb-4' />
124118
<p className='text-muted-foreground'>No pending transactions</p>
125119
</div>
126120
) : (
127121
<div className='space-y-3'>
128-
{transactions.map((tx) => (
129-
<div key={tx.id} className='flex items-center justify-between p-3 rounded-lg border'>
122+
{pendingTransactions.slice(0, 5).map((tx) => (
123+
<div key={tx.safeTxHash} className='flex items-center justify-between p-3 rounded-lg border'>
130124
<div className='flex items-center gap-3'>
131125
<Avatar className='h-8 w-8'>
132-
<AvatarFallback className='text-xs'>{getTypeIcon(tx.type)}</AvatarFallback>
126+
<AvatarFallback className='text-xs'>{getTypeIcon(tx)}</AvatarFallback>
133127
</Avatar>
134128
<div className='flex-1'>
135129
<div className='flex items-center gap-2'>
136-
<span className='font-medium'>
137-
{tx.amount} {tx.token}
138-
</span>
139-
{getStatusIcon(tx.status)}
130+
<span className='font-medium'>{formatAmount(tx.value)} ETH</span>
131+
{getStatusIcon(tx.executionDate ? 'AWAITING_EXECUTION' : 'AWAITING_CONFIRMATIONS')}
140132
</div>
141133
<div className='flex items-center gap-2 text-sm text-muted-foreground'>
142-
<span className='capitalize'>{tx.type}</span>
134+
<span>Nonce: {tx.nonce}</span>
143135
<span></span>
144-
<span>{tx.timestamp}</span>
136+
<span>{formatTimestamp(tx.submissionDate || new Date().toISOString())}</span>
145137
</div>
146138
</div>
147139
</div>
148140
<div className='flex items-center gap-2'>
149-
{getStatusBadge(tx.status)}
150-
{tx.hash && (
141+
{getStatusBadge(tx.executionDate ? 'AWAITING_EXECUTION' : 'AWAITING_CONFIRMATIONS')}
142+
{tx.safeTxHash && (
151143
<Button variant='ghost' size='sm' className='h-8 w-8 p-0'>
152144
<ExternalLink className='h-3 w-3' />
153145
</Button>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { useState, useEffect, useCallback } from 'react';
2+
import { useSafeApiKit, useSelectedSafeAddress } from '@/providers/SafeProvider';
3+
import { SafeMultisigTransactionResponse } from '@safe-global/types-kit';
4+
5+
interface UsePendingTransactionsReturn {
6+
pendingTransactions: SafeMultisigTransactionResponse[];
7+
loading: boolean;
8+
totalCount: number;
9+
fetchPendingTransactions: () => Promise<void>;
10+
error: string | null;
11+
}
12+
13+
export function usePendingTransactions(): UsePendingTransactionsReturn {
14+
const { selectedSafeAddress } = useSelectedSafeAddress();
15+
const safeApiKit = useSafeApiKit();
16+
17+
const [pendingTransactions, setPendingTransactions] = useState<SafeMultisigTransactionResponse[]>([]);
18+
const [loading, setLoading] = useState(false);
19+
const [totalCount, setTotalCount] = useState<number>(0);
20+
const [error, setError] = useState<string | null>(null);
21+
22+
const fetchPendingTransactions = useCallback(async () => {
23+
if (!selectedSafeAddress || !safeApiKit) {
24+
setError('No Safe selected or API kit not available');
25+
return;
26+
}
27+
28+
setLoading(true);
29+
setError(null);
30+
31+
try {
32+
// Get pending transactions from Safe API
33+
const response = await safeApiKit.getPendingTransactions(selectedSafeAddress, {
34+
ordering: 'created',
35+
});
36+
37+
if (response && response.results) {
38+
setPendingTransactions(response.results);
39+
setTotalCount(response.count);
40+
} else {
41+
setPendingTransactions([]);
42+
setTotalCount(0);
43+
}
44+
} catch (error) {
45+
console.error('Error fetching pending transactions:', error);
46+
setError(error instanceof Error ? error.message : 'Failed to fetch pending transactions');
47+
setPendingTransactions([]);
48+
setTotalCount(0);
49+
} finally {
50+
setLoading(false);
51+
}
52+
}, [selectedSafeAddress, safeApiKit]);
53+
54+
useEffect(() => {
55+
fetchPendingTransactions();
56+
}, [fetchPendingTransactions]);
57+
58+
return {
59+
pendingTransactions,
60+
loading,
61+
totalCount,
62+
fetchPendingTransactions,
63+
error,
64+
};
65+
}

providers/SafeProvider.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,17 @@ export function useCurrentSafeClient() {
129129
return context.safeClient;
130130
}
131131

132+
export function useSafeApiKit() {
133+
const context = useContext(SafeContext);
134+
if (context === undefined) {
135+
throw new Error('useSafeApiKit must be used within a SafeProvider');
136+
}
137+
if (!context.safeClient) {
138+
throw new Error('Safe client not found');
139+
}
140+
return context.safeClient.apiKit;
141+
}
142+
132143
export function useExtendedSafeApiKit() {
133144
const context = useContext(SafeContext);
134145
if (context === undefined) {

0 commit comments

Comments
 (0)