Skip to content

Commit 865f2ea

Browse files
committed
🚧 risk feed
1 parent 4f3e7d5 commit 865f2ea

10 files changed

Lines changed: 219 additions & 307 deletions

File tree

components/RiskFeed/Decode/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy';
55
import { useTranslation } from 'react-i18next';
66
import Link from 'next/link';
77

8-
import { DataDecoded } from '../api';
8+
import { DataDecoded } from '../types';
99
import useEtherscanLink from 'hooks/useEtherscanLink';
1010
import { formatWallet, formatHex } from 'utils/utils';
1111

components/RiskFeed/Events/index.tsx

Lines changed: 17 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,20 @@ import {
1212
} from '@mui/material';
1313
import ExpandMoreIcon from '@mui/icons-material/ExpandMoreRounded';
1414
import { useTranslation } from 'react-i18next';
15-
import { getAddress } from 'viem';
1615
import Link from 'next/link';
16+
import { useQuery } from '@tanstack/react-query';
1717

18-
import { type SafeResponse, type Transaction, useTransaction, type Call } from '../api';
1918
import Decode, { DecodeCall } from '../Decode';
19+
import type { Call, SafeResponse, SafeTransaction, Transaction } from '../types';
2020

21+
import { defaultChain } from 'utils/client';
2122
import { formatTx, formatWallet } from 'utils/utils';
2223
import parseTimestamp from 'utils/parseTimestamp';
2324
import useEtherscanLink from 'hooks/useEtherscanLink';
2425
import Pill from 'components/common/Pill';
2526

27+
const base = `https://safe-client.safe.global/v1/chains/${defaultChain.id}`;
28+
2629
export type Entry = {
2730
schedule?: Transaction;
2831
events?: Call[];
@@ -362,7 +365,14 @@ function EventSummary({
362365
inner?: boolean;
363366
}) {
364367
const { t } = useTranslation();
365-
const { data, isLoading } = useTransaction(tx.id);
368+
const { data, isLoading } = useQuery({
369+
queryKey: ['safe', 'transaction', tx.id],
370+
queryFn: (): Promise<SafeTransaction> =>
371+
fetch(`${base}/transactions/${tx.id}`).then((response) => {
372+
if (!response.ok) throw new Error(`safe ${response.status}`);
373+
return response.json();
374+
}),
375+
});
366376

367377
const { breakpoints } = useTheme();
368378
const isMobile = useMediaQuery(breakpoints.down('sm'));
@@ -457,47 +467,20 @@ function EventSummary({
457467

458468
function EventSummaryCall({ calls }: { calls: Call[] }) {
459469
const { t } = useTranslation();
460-
461-
const { breakpoints } = useTheme();
462-
const isMobile = useMediaQuery(breakpoints.down('sm'));
463-
464-
const { address } = useEtherscanLink();
465-
466-
const format = (value: string) => {
467-
const addr = getAddress(value);
468-
return isMobile ? formatWallet(addr) : addr;
469-
};
470-
471470
const call = calls[0];
472471

473472
return (
474473
<Box display="flex" flexDirection="column">
475474
<Row title={t('ID')}>
476475
<Value>{call.id}</Value>
477476
</Row>
478-
<Row title={t('Scheduler')}>
479-
<Value>
480-
<Link href={address(call.scheduler)} target="_blank" rel="noopener noreferrer">
481-
{format(call.scheduler)}
482-
</Link>
483-
</Value>
484-
</Row>
485477
<Row title={t('Scheduled At')}>
486478
<Value>{parseTimestamp(call.scheduledAt, 'YYYY-MM-DD HH:mm:ss')}</Value>
487479
</Row>
488-
{call.executedAt && call.executor && (
489-
<>
490-
<Row title={t('Executor')}>
491-
<Value>
492-
<Link href={address(call.executor)} target="_blank" rel="noopener noreferrer">
493-
{format(call.executor)}
494-
</Link>
495-
</Value>
496-
</Row>
497-
<Row title={t('Executed At')}>
498-
<Value>{parseTimestamp(call.executedAt, 'YYYY-MM-DD HH:mm:ss')}</Value>
499-
</Row>
500-
</>
480+
{call.executedAt && (
481+
<Row title={t('Executed At')}>
482+
<Value>{parseTimestamp(call.executedAt, 'YYYY-MM-DD HH:mm:ss')}</Value>
483+
</Row>
501484
)}
502485
<Row title={calls.length === 1 ? t('Action') : t('Actions')}>
503486
<Box display="flex" flexDirection="column" gap={2}>

components/RiskFeed/Feed/index.tsx

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,100 @@
11
import React, { useMemo } from 'react';
2-
import { Address } from 'viem';
2+
import { Address, Hex } from 'viem';
33
import { Box, Divider } from '@mui/material';
44
import { useTranslation } from 'react-i18next';
5-
import { useHistory, useQueued, useTimelockControllerEvents } from '../api';
5+
import { useQuery } from '@tanstack/react-query';
6+
import { useContractEvents } from 'wagmi';
7+
8+
import { timelockControllerAbi, timelockControllerAddress, timelockControllerBlock } from 'generated/wagmi';
9+
import { defaultChain } from 'utils/client';
610
import { ABIContext, type Contracts } from '../Decode';
711
import Events, { group, type Entry } from '../Events';
12+
import type { Call, SafeResponse } from '../types';
813

914
type Props = {
1015
multisig: Address;
1116
contracts: Contracts;
1217
};
1318

19+
const base = `https://safe-client.safe.global/v1/chains/${defaultChain.id}`;
20+
21+
function useSafeTransactions(multisig: Address, kind: 'queued' | 'history') {
22+
return useQuery({
23+
queryKey: ['safe', kind, multisig],
24+
queryFn: (): Promise<SafeResponse> =>
25+
fetch(`${base}/safes/${multisig}/transactions/${kind}?cursor=${encodeURIComponent('limit=40&offset=0')}`).then(
26+
(response) => {
27+
if (!response.ok) throw new Error(`safe ${response.status}`);
28+
return response.json();
29+
},
30+
),
31+
});
32+
}
33+
1434
export default React.memo(function Feed({ contracts, multisig }: Props) {
1535
const { t } = useTranslation();
16-
const { data: queued, isLoading: queuedIsLoading } = useQueued(multisig);
17-
const { data: history, isLoading: historyIsLoading } = useHistory(multisig);
36+
const { data: queued, isLoading: queuedIsLoading } = useSafeTransactions(multisig, 'queued');
37+
const { data: history, isLoading: historyIsLoading } = useSafeTransactions(multisig, 'history');
38+
39+
const address = timelockControllerAddress[defaultChain.id as keyof typeof timelockControllerAddress];
40+
const fromBlock = timelockControllerBlock[defaultChain.id as keyof typeof timelockControllerBlock];
41+
const enabled = Boolean(address);
42+
const events = {
43+
abi: timelockControllerAbi,
44+
address,
45+
fromBlock,
46+
strict: true,
47+
chainId: defaultChain.id,
48+
query: { enabled },
49+
} as const;
50+
const { data: scheduledLogs } = useContractEvents({ ...events, eventName: 'CallScheduled' });
51+
const { data: executedLogs } = useContractEvents({ ...events, eventName: 'CallExecuted' });
52+
const { data: cancelledLogs } = useContractEvents({ ...events, eventName: 'Cancelled' });
1853

19-
const { data: calls, isLoading: callsLoading } = useTimelockControllerEvents();
54+
const calls = useMemo(() => {
55+
if (!scheduledLogs || !executedLogs || !cancelledLogs) return undefined;
56+
const executed = executedLogs.reduce(
57+
(state, log) => state.set(log.args.id, Number(log.blockTimestamp)),
58+
new Map<Hex, number>(),
59+
);
60+
const cancelled = cancelledLogs.reduce(
61+
(state, log) => state.set(log.args.id, Number(log.blockTimestamp)),
62+
new Map<Hex, number>(),
63+
);
64+
const grouped = new Map<Hex, Call>();
65+
for (const log of scheduledLogs) {
66+
const operation = { index: Number(log.args.index), target: log.args.target, data: log.args.data };
67+
const call = grouped.get(log.args.id);
68+
if (call) {
69+
call.operations.push(operation);
70+
} else {
71+
grouped.set(log.args.id, {
72+
id: log.args.id,
73+
operations: [operation],
74+
scheduledAt: Number(log.blockTimestamp),
75+
executedAt: executed.get(log.args.id) ?? null,
76+
cancelledAt: cancelled.get(log.args.id) ?? null,
77+
});
78+
}
79+
}
80+
return [...grouped.values()]
81+
.sort((x, y) => y.scheduledAt - x.scheduledAt)
82+
.slice(0, 150)
83+
.reduce(
84+
(state, call) => {
85+
(call.cancelledAt ? state.cancelled : call.executedAt ? state.executed : state.scheduled).push(call);
86+
return state;
87+
},
88+
{ scheduled: [] as Call[], executed: [] as Call[], cancelled: [] as Call[] },
89+
);
90+
}, [scheduledLogs, executedLogs, cancelledLogs]);
2091

2192
const [scheduled, executed] = useMemo<[Entry[], Entry[]]>(() => {
2293
if (!queued || !history || !calls) return [[], []];
23-
const _queued = group(queued, calls.scheduled);
24-
const _history = group(history, calls.executed);
25-
const _scheduled = _queued;
94+
const _scheduled = group(queued, calls.scheduled);
2695
const _executed: Entry[] = [];
2796

28-
for (const entry of _history) {
97+
for (const entry of group(history, calls.executed)) {
2998
if (entry.schedule && !entry.execution) {
3099
_scheduled.push(entry);
31100
} else {
@@ -43,14 +112,14 @@ export default React.memo(function Feed({ contracts, multisig }: Props) {
43112
title={t('Scheduled Transactions')}
44113
empty={t('No transactions queued at the moment.')}
45114
data={scheduled}
46-
isLoading={queuedIsLoading || callsLoading}
115+
isLoading={queuedIsLoading || (enabled && !calls)}
47116
/>
48117
<Divider />
49118
<Events
50119
title={t('Executed Transactions')}
51120
empty={t('No transactions executed at the moment.')}
52121
data={executed}
53-
isLoading={historyIsLoading || callsLoading}
122+
isLoading={historyIsLoading || (enabled && !calls)}
54123
/>
55124
</Box>
56125
</ABIContext.Provider>

0 commit comments

Comments
 (0)