11import React , { useMemo } from 'react' ;
2- import { Address } from 'viem' ;
2+ import { Address , Hex } from 'viem' ;
33import { Box , Divider } from '@mui/material' ;
44import { 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' ;
610import { ABIContext , type Contracts } from '../Decode' ;
711import Events , { group , type Entry } from '../Events' ;
12+ import type { Call , SafeResponse } from '../types' ;
813
914type 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+
1434export 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