From eba18f434e7864b01c7ac303fcaa859d5646b717 Mon Sep 17 00:00:00 2001 From: Carlos Andres Perez Ubeda Date: Mon, 20 Jan 2025 21:26:31 -0600 Subject: [PATCH 1/4] refactor(profile-header): remove unused GeoAddress constant - Removed an unused `GeoAddress` constant from `profile-header.tsx`. - Cleanup improves code readability and eliminates redundant code. --- src/sections/user/profile-header.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/sections/user/profile-header.tsx b/src/sections/user/profile-header.tsx index d877a4d2..bb598af0 100644 --- a/src/sections/user/profile-header.tsx +++ b/src/sections/user/profile-header.tsx @@ -56,8 +56,6 @@ import BadgeVerified from "@src/components/user-item/BadgeVerified.tsx"; const urlToShare = 'https://app.watchit.movie/profileId'; const urlAttestationBase = 'https://polygon-amoy.easscan.org/attestation/view/'; -// const GeoAddress = '0xEFBBD14082cF2FbCf5Badc7ee619F0f4e36D0A5B' - const shareLinks = [ { icon: 'mingcute:social-x-line', From cadae73977b165d68eeb073f405914860ea211b0 Mon Sep 17 00:00:00 2001 From: Carlos Andres Perez Ubeda Date: Tue, 21 Jan 2025 19:30:29 -0600 Subject: [PATCH 2/4] refactor(app): extract event handling and notification logic - Moved event handling and notification logic from `src/routes/sections/index.tsx` to `src/App.tsx`. - Created `AppContent` component in `src/App.tsx` for better encapsulation. - Replaced redundant logic in `Router` with streamlined routing logic, reducing complexity. - Simplified imports and removed unused constants and hooks in `src/routes/sections/index.tsx`. This improves maintainability by centralizing event watch and notification logic in a dedicated component. --- src/App.tsx | 115 ++++++++++++++++++++++++++++++++-- src/routes/sections/index.tsx | 89 +------------------------- 2 files changed, 111 insertions(+), 93 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 844e0cd6..cdca9588 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -45,8 +45,17 @@ import { AuthProvider } from '@src/auth/context/web3Auth'; import { ResponsiveOverlay } from '@src/components/responsive-overlay'; import { Buffer } from 'buffer'; -import { Provider } from 'react-redux'; +import {Provider, useDispatch, useSelector} from 'react-redux'; import { MetaMaskProvider } from '@metamask/sdk-react'; +import {useNotifications} from "@src/hooks/use-notifications.ts"; +import {useSnackbar} from "notistack"; +import {useEffect} from "react"; +import {setGlobalNotifier} from "@notifications/internal-notifications.ts"; +import {publicClientWebSocket} from "@src/clients/viem/publicClient.ts"; +import {GLOBAL_CONSTANTS} from "@src/config-global.ts"; +import LedgerVaultAbi from "@src/config/abi/LedgerVault.json"; +import {setBlockchainEvents} from "@redux/blockchain-events"; +import {subscribeToNotifications} from "@src/utils/subscribe-notifications-supabase.ts"; window.Buffer = Buffer; @@ -103,10 +112,7 @@ export default function App() { - - - - + @@ -117,3 +123,102 @@ export default function App() { ); } + + +interface EventArgs { + recipient?: string; + origin?: string; +} +const AppContent = () => { + const dispatch = useDispatch(); + const sessionData = useSelector((state: any) => state.auth.session); + const { getNotifications } = useNotifications(); + const { enqueueSnackbar } = useSnackbar(); + + useEffect(() => { + // Set the global reference so we can call notify(...) anywhere. + setGlobalNotifier(enqueueSnackbar); + }, [enqueueSnackbar]); + + const watchEvent = (eventName: string, args: EventArgs, logText: string) => { + const results = publicClientWebSocket.watchContractEvent({ + address: GLOBAL_CONSTANTS.LEDGER_VAULT_ADDRESS, + abi: LedgerVaultAbi.abi, + eventName, + args, + onLogs: (logs) => { + console.log(logText, logs); + dispatch(setBlockchainEvents(logs)); + }, + }); + + console.log('Watching', eventName, 'events'); + console.log('Results', results); + + return results; + }; + +/** + * Deposit Deposited Verde Fuente, monto, TX. + * Transfer From Transfer from Verde Origen, monto, TX. + * Transfer To Transfer to Rojo Destino, monto, TX. + * Withdraw To Withdraw to Rojo Destino, monto, TX. + * Reserve To Reserved Amarillo Propósito, monto, TX. + * Collect From Collected Verde Origen, monto, TX. + * Locked Locked Azul Propósito, monto, TX. + * Release Released Verde Destino (si aplica), monto, TX. + * Claim Claimed Verde Propósito/acuerdo, monto, TX. +*/ + + useEffect(() => { + if (!sessionData?.address) return; + + // @TODO Add the rest of the events + // Analiazed taking care of the user as the recipient or origin + const newEvents = [ + { name: 'FundsDeposited', args: { recipient: sessionData?.address }, logText: 'Deposited' }, + { name: 'FundsTransferFrom', args: { origin: sessionData?.address }, logText: 'Transfer from' }, + { name: 'FundsTransferTo', args: { recipient: sessionData?.address }, logText: 'Transfer to' }, + { name: 'FundsWithdrawTo', args: { recipient: sessionData?.address }, logText: 'Withdraw to' }, + { name: 'FundsReserved', args: { recipient: sessionData?.address }, logText: 'Reserved' }, + { name: 'FundsCollected', args: { origin: sessionData?.address }, logText: 'Collected' }, + { name: 'FundsLocked', args: { recipient: sessionData?.address }, logText: 'Locked' }, + { name: 'FundsReleased', args: { origin: sessionData?.address }, logText: 'Released' }, + { name: 'FundsClaimed', args: { recipient: sessionData?.address }, logText: 'Claimed' }, + ] + + const events = [ + { name: 'FundsDeposited', args: { recipient: sessionData?.address }, logText: 'New deposit (user as recipient):' }, + { name: 'FundsWithdrawn', args: { origin: sessionData?.address }, logText: 'New withdraw (user as origin):' }, + { name: 'FundsTransferred', args: { origin: sessionData?.address }, logText: 'New transfer from me:' }, + { name: 'FundsTransferred', args: { recipient: sessionData?.address }, logText: 'New transfer to me:' }, + ]; + + + const unwatchers = events.map(event => watchEvent(event.name, event.args, event.logText)); + + return () => { + unwatchers.forEach(unwatch => unwatch()); + }; + }, [sessionData?.address]); + + + useEffect(() => { + if (sessionData?.profile?.id) { + // Subscribe to notifications channel + subscribeToNotifications(sessionData?.profile?.id, dispatch, ['notifications']); + + // Set the notifications in first render + getNotifications(sessionData?.profile?.id).then(() => {}); + } + }, [sessionData?.profile?.id, dispatch]); + + return ( + <> + + + + + + ) +} diff --git a/src/routes/sections/index.tsx b/src/routes/sections/index.tsx index 5fd497ef..de09be1d 100644 --- a/src/routes/sections/index.tsx +++ b/src/routes/sections/index.tsx @@ -1,96 +1,9 @@ import { Navigate, useRoutes } from 'react-router-dom'; -import { GLOBAL_CONSTANTS, PATH_AFTER_LOGIN } from '@src/config-global'; +import { PATH_AFTER_LOGIN } from '@src/config-global'; import { dashboardRoutes } from './dashboard'; import NotFoundPage from '../../pages/404'; -import { useEffect } from 'react'; -import { subscribeToNotifications } from '@src/utils/subscribe-notifications-supabase.ts'; -import { useDispatch, useSelector } from 'react-redux'; -import { useNotifications } from '@src/hooks/use-notifications.ts'; -import { publicClientWebSocket } from '@src/clients/viem/publicClient.ts'; -import LedgerVaultAbi from '@src/config/abi/LedgerVault.json'; -import { setBlockchainEvents } from '@redux/blockchain-events'; -import { setGlobalNotifier } from '@notifications/internal-notifications.ts'; -import { useSnackbar } from 'notistack'; - export default function Router() { - const dispatch = useDispatch(); - const sessionData = useSelector((state: any) => state.auth.session); - const { getNotifications } = useNotifications(); - const { enqueueSnackbar } = useSnackbar(); - - useEffect(() => { - // Set the global reference so we can call notify(...) anywhere. - setGlobalNotifier(enqueueSnackbar); - }, [enqueueSnackbar]); - - useEffect(() => { - if (!sessionData?.address) return; - - // FundsDeposited (when i am the recipient) - const unwatchDeposit = publicClientWebSocket.watchContractEvent({ - address: GLOBAL_CONSTANTS.LEDGER_VAULT_ADDRESS, - abi: LedgerVaultAbi.abi, - eventName: 'FundsDeposited', - args: { recipient: sessionData?.address }, - onLogs: (logs) => { - console.log('New deposit (user as recipient):', logs); - dispatch(setBlockchainEvents(logs)); - }, - }); - - // FundsWithdrawn (when i am the origin) - const unwatchWithdraw = publicClientWebSocket.watchContractEvent({ - address: GLOBAL_CONSTANTS.LEDGER_VAULT_ADDRESS, - abi: LedgerVaultAbi.abi, - eventName: 'FundsWithdrawn', - args: { origin: sessionData?.address }, - onLogs: (logs) => { - console.log('New withdraw (user as origin):', logs); - dispatch(setBlockchainEvents(logs)); - }, - }); - - // FundsTransferred (when I send) - const unwatchTransferFrom = publicClientWebSocket.watchContractEvent({ - address: GLOBAL_CONSTANTS.LEDGER_VAULT_ADDRESS, - abi: LedgerVaultAbi.abi, - eventName: 'FundsTransferred', - args: { origin: sessionData?.address }, - onLogs: (logs) => { - console.log('New transfer from me:', logs); - dispatch(setBlockchainEvents(logs)); - }, - }); - - // FundsTransferred (when I receive) - const unwatchTransferTo = publicClientWebSocket.watchContractEvent({ - address: GLOBAL_CONSTANTS.LEDGER_VAULT_ADDRESS, - abi: LedgerVaultAbi.abi, - eventName: 'FundsTransferred', - args: { recipient: sessionData?.address }, - onLogs: (logs) => { - console.log('New transfer to me:', logs); - dispatch(setBlockchainEvents(logs)); - }, - }); - - return () => { - unwatchDeposit(); - unwatchWithdraw(); - unwatchTransferFrom(); - unwatchTransferTo(); - }; - }, [sessionData?.address]); - - useEffect(() => { - if (sessionData?.profile?.id) { - // Subscribe to notifications channel - subscribeToNotifications(sessionData?.profile?.id, dispatch, ['notifications']); - // Set the notifications in first render - getNotifications(sessionData?.profile?.id).then(() => {}); - } - }, [sessionData?.profile?.id, dispatch]); return useRoutes([ { From dbd27679a115632b4fa74c2b2863fb4208ca5229 Mon Sep 17 00:00:00 2001 From: jadapema Date: Wed, 22 Jan 2025 13:48:40 -0600 Subject: [PATCH 3/4] feat: added new events to transactions table ('locked', 'claimed', 'reserved', 'collected', 'released') --- src/App.tsx | 32 +-- .../use-get-smart-wallet-transactions.ts | 232 +++++++++--------- .../finance-transactions-history.tsx | 68 ++++- .../finance-transactions-table-row.tsx | 25 +- src/types/transaction.ts | 2 - .../finance-graphs/groupedTransactions.ts | 104 +++++++- 6 files changed, 290 insertions(+), 173 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index cdca9588..111fced9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -158,43 +158,21 @@ const AppContent = () => { return results; }; -/** - * Deposit Deposited Verde Fuente, monto, TX. - * Transfer From Transfer from Verde Origen, monto, TX. - * Transfer To Transfer to Rojo Destino, monto, TX. - * Withdraw To Withdraw to Rojo Destino, monto, TX. - * Reserve To Reserved Amarillo Propósito, monto, TX. - * Collect From Collected Verde Origen, monto, TX. - * Locked Locked Azul Propósito, monto, TX. - * Release Released Verde Destino (si aplica), monto, TX. - * Claim Claimed Verde Propósito/acuerdo, monto, TX. -*/ - useEffect(() => { if (!sessionData?.address) return; - // @TODO Add the rest of the events - // Analiazed taking care of the user as the recipient or origin - const newEvents = [ - { name: 'FundsDeposited', args: { recipient: sessionData?.address }, logText: 'Deposited' }, - { name: 'FundsTransferFrom', args: { origin: sessionData?.address }, logText: 'Transfer from' }, - { name: 'FundsTransferTo', args: { recipient: sessionData?.address }, logText: 'Transfer to' }, - { name: 'FundsWithdrawTo', args: { recipient: sessionData?.address }, logText: 'Withdraw to' }, - { name: 'FundsReserved', args: { recipient: sessionData?.address }, logText: 'Reserved' }, - { name: 'FundsCollected', args: { origin: sessionData?.address }, logText: 'Collected' }, - { name: 'FundsLocked', args: { recipient: sessionData?.address }, logText: 'Locked' }, - { name: 'FundsReleased', args: { origin: sessionData?.address }, logText: 'Released' }, - { name: 'FundsClaimed', args: { recipient: sessionData?.address }, logText: 'Claimed' }, - ] - const events = [ { name: 'FundsDeposited', args: { recipient: sessionData?.address }, logText: 'New deposit (user as recipient):' }, { name: 'FundsWithdrawn', args: { origin: sessionData?.address }, logText: 'New withdraw (user as origin):' }, { name: 'FundsTransferred', args: { origin: sessionData?.address }, logText: 'New transfer from me:' }, { name: 'FundsTransferred', args: { recipient: sessionData?.address }, logText: 'New transfer to me:' }, + { name: 'FundsLocked', args: { account: sessionData?.address }, logText: 'New funds locked:' }, + { name: 'FundsClaimed', args: { claimer: sessionData?.address }, logText: 'New funds claimed:' }, + { name: 'FundsReserved', args: { from: sessionData?.address }, logText: 'New funds reserved:' }, + { name: 'FundsCollected', args: { from: sessionData?.address }, logText: 'New funds collected:' }, + { name: 'FundsReleased', args: { to: sessionData?.address }, logText: 'New funds released:' }, ]; - const unwatchers = events.map(event => watchEvent(event.name, event.args, event.logText)); return () => { diff --git a/src/hooks/use-get-smart-wallet-transactions.ts b/src/hooks/use-get-smart-wallet-transactions.ts index 2a3203ca..915bd767 100644 --- a/src/hooks/use-get-smart-wallet-transactions.ts +++ b/src/hooks/use-get-smart-wallet-transactions.ts @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; -import { parseAbiItem, formatUnits, Address } from 'viem'; import { useDispatch, useSelector } from 'react-redux'; +import { parseAbiItem, formatUnits, Address } from 'viem'; import { publicClient } from '@src/clients/viem/publicClient.ts'; import { GLOBAL_CONSTANTS } from '@src/config-global.ts'; import LedgerVaultAbi from '@src/config/abi/LedgerVault.json'; @@ -29,17 +29,96 @@ export type TransactionLog = { transactionIndex: number; }; -const useGetSmartWalletTransactions = () => { +type EventConfig = { + eventName: string; + args: Record; + getEventType: (log: any, userAddress: string) => string; +}; + +export const useGetSmartWalletTransactions = () => { const dispatch = useDispatch(); const sessionData = useSelector((state: any) => state.auth.session); const blockchainEvents = useSelector((state: any) => state.blockchainEvents.events); const transactions = useSelector((state: any) => state.transactions.transactions); - // Local states for loading and error const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - // Function to fetch historical logs + const eventConfigs: EventConfig[] = [ + { + eventName: 'FundsTransferred', + args: { recipient: sessionData?.address || '' }, + getEventType: (log, userAddress) => + log.args.origin === userAddress ? 'transferTo' : 'transferFrom', + }, + { + eventName: 'FundsTransferred', + args: { origin: sessionData?.address || '' }, + getEventType: (log, userAddress) => + log.args.origin === userAddress ? 'transferTo' : 'transferFrom', + }, + { + eventName: 'FundsDeposited', + args: { recipient: sessionData?.address || '' }, + getEventType: () => 'deposit', + }, + { + eventName: 'FundsWithdrawn', + args: { origin: sessionData?.address || '' }, + getEventType: () => 'withdraw', + }, + { + eventName: 'FundsLocked', + args: { account: sessionData?.address || '' }, + getEventType: () => 'locked', + }, + { + eventName: 'FundsClaimed', + args: { claimer: sessionData?.address || '' }, + getEventType: () => 'claimed', + }, + { + eventName: 'FundsReserved', + args: { from: sessionData?.address || '' }, + getEventType: () => 'reserved', + }, + { + eventName: 'FundsCollected', + args: { from: sessionData?.address || '' }, + getEventType: () => 'collected', + }, + { + eventName: 'FundsReleased', + args: { to: sessionData?.address || '' }, + getEventType: () => 'released', + }, + ]; + + const createEventSignature = (event: any): string => { + if (!event || !event.name || !event.inputs) { + throw new Error('Invalid event in ABI'); + } + const inputs = event.inputs + .map((input: any) => `${input.type}${input.indexed ? ' indexed' : ''} ${input.name}`) + .join(', '); + return `event ${event.name}(${inputs})`; + }; + + const uniqueEventNames = Array.from( + new Set(eventConfigs.map((config) => config.eventName)) + ); + + const parsedAbis = uniqueEventNames.reduce((acc, eventName) => { + const eventAbi = LedgerVaultAbi.abi.find( + (item: any) => item.type === 'event' && item.name === eventName + ); + if (!eventAbi) { + throw new Error(`The event is not fund ${eventName} on the ABI`); + } + acc[eventName] = parseAbiItem(createEventSignature(eventAbi)); + return acc; + }, {} as Record>); + const fetchLogs = async () => { if (!sessionData?.address) { setLoading(false); @@ -50,102 +129,45 @@ const useGetSmartWalletTransactions = () => { setLoading(true); setError(null); - // Define ABI for events to monitor - const eventsAbi = { - FundsTransferred: parseAbiItem( - createEventSignature( - LedgerVaultAbi.abi.find( - (item: any) => item.type === 'event' && item.name === 'FundsTransferred' - ) - ) - ), - FundsDeposited: parseAbiItem( - createEventSignature( - LedgerVaultAbi.abi.find( - (item: any) => item.type === 'event' && item.name === 'FundsDeposited' - ) - ) - ), - FundsWithdrawn: parseAbiItem( - createEventSignature( - LedgerVaultAbi.abi.find( - (item: any) => item.type === 'event' && item.name === 'FundsWithdrawn' - ) - ) - ), - }; - - // Fetch logs for each event - const [transfersToMe, transfersFromMe, deposits, withdraws] = await Promise.all([ - publicClient.getLogs({ - address: GLOBAL_CONSTANTS.LEDGER_VAULT_ADDRESS as Address, - event: eventsAbi.FundsTransferred as any, - args: { recipient: sessionData.address }, - fromBlock: 0n, - toBlock: 'latest', - }), - publicClient.getLogs({ - address: GLOBAL_CONSTANTS.LEDGER_VAULT_ADDRESS as Address, - event: eventsAbi.FundsTransferred as any, - args: { origin: sessionData.address }, - fromBlock: 0n, - toBlock: 'latest', - }), - publicClient.getLogs({ + const promises = eventConfigs.map(({ eventName, args }) => { + return publicClient.getLogs({ address: GLOBAL_CONSTANTS.LEDGER_VAULT_ADDRESS as Address, - event: eventsAbi.FundsDeposited as any, - args: { recipient: sessionData.address }, + event: parsedAbis[eventName] as any, + args, fromBlock: 0n, toBlock: 'latest', - }), - publicClient.getLogs({ - address: GLOBAL_CONSTANTS.LEDGER_VAULT_ADDRESS as Address, - event: eventsAbi.FundsWithdrawn as any, - args: { origin: sessionData.address }, - fromBlock: 0n, - toBlock: 'latest', - }), - ]); + }); + }); + + const results = await Promise.all(promises); - const allLogs = [...transfersToMe, ...transfersFromMe, ...deposits, ...withdraws]; + const allLogs = results.flat(); - // Add timestamps and format details for each log const logsWithDetails = await Promise.all( allLogs.map(async (log: any) => { const block = await publicClient.getBlock({ blockNumber: log.blockNumber }); - // Determine the event type - const event = (() => { - switch (log.eventName) { - case 'FundsTransferred': - return log.args.origin === sessionData.address ? 'transferTo' : 'transferFrom'; - case 'FundsDeposited': - return 'deposit'; - case 'FundsWithdrawn': - return 'withdraw'; - default: - return 'unknown'; - } - })(); + const foundConfig = eventConfigs.find((c) => c.eventName === log.eventName); + const eventType = foundConfig + ? foundConfig.getEventType(log, sessionData?.address) + : 'unknown'; return { ...log, - timestamp: block.timestamp, // UNIX timestamp of the block - readableDate: new Date(Number(block.timestamp) * 1000).toLocaleString(), // Human-readable date - formattedAmount: log.args.amount ? formatUnits(log.args.amount, 18) : '0', // Convert amount from wei to ether - event, + timestamp: block.timestamp, + readableDate: new Date(Number(block.timestamp) * 1000).toLocaleString(), + formattedAmount: log.args.amount ? formatUnits(log.args.amount, 18) : '0', + event: eventType, }; }) ); - // Sort logs by block number and transaction index const sortedLogs = logsWithDetails.sort((a, b) => { const blockDifference = Number(b.blockNumber) - Number(a.blockNumber); if (blockDifference !== 0) return blockDifference; return Number(b.transactionIndex) - Number(a.transactionIndex); }); - // Dispatch the setTransactions action to store the logs in Redux dispatch(setTransactions(sortedLogs)); } catch (err) { console.error('Error fetching logs:', err); @@ -155,76 +177,48 @@ const useGetSmartWalletTransactions = () => { } }; - // Helper function to create event signatures - const createEventSignature = (event: any): string => { - if (!event || !event.name || !event.inputs) { - throw new Error('Invalid event in ABI'); - } - const inputs = event.inputs - .map((input: any) => `${input.type}${input.indexed ? ' indexed' : ''} ${input.name}`) - .join(', '); - return `event ${event.name}(${inputs})`; - }; - - // Effect to fetch historical logs when the address changes useEffect(() => { fetchLogs(); - - // Disable loader if data is already available if (transactions.length) { setLoading(false); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [sessionData?.address]); - // Effect to handle real-time events from blockchainEvents useEffect(() => { - if (!blockchainEvents || blockchainEvents.length === 0) return; + if (!blockchainEvents?.length) return; - // Iterate over new blockchain events blockchainEvents.forEach(async (log: any) => { - // Process only relevant events if ( log.address !== GLOBAL_CONSTANTS.LEDGER_VAULT_ADDRESS || - !['FundsTransferred', 'FundsDeposited', 'FundsWithdrawn'].includes(log.eventName) + !uniqueEventNames.includes(log.eventName) ) { return; } try { const block = await publicClient.getBlock({ blockNumber: log.blockNumber }); + const foundConfig = eventConfigs.find((c) => c.eventName === log.eventName); + const eventType = foundConfig + ? foundConfig.getEventType(log, sessionData?.address) + : 'unknown'; - // Determine the event type - const event = (() => { - switch (log.eventName) { - case 'FundsTransferred': - return log.args.origin === sessionData.address ? 'transferTo' : 'transferFrom'; - case 'FundsDeposited': - return 'deposit'; - case 'FundsWithdrawn': - return 'withdraw'; - default: - return 'unknown'; - } - })(); - - // Create a formatted transaction log const formattedLog = { ...log, - timestamp: block.timestamp, // UNIX timestamp of the block - readableDate: new Date(Number(block.timestamp) * 1000).toLocaleString(), // Human-readable date - formattedAmount: log.args.amount ? formatUnits(log.args.amount, 18) : '0', // Convert amount from wei to ether - event, + timestamp: block.timestamp, + readableDate: new Date(Number(block.timestamp) * 1000).toLocaleString(), + formattedAmount: log.args.amount ? formatUnits(log.args.amount, 18) : '0', + event: eventType, }; - // Dispatch the addTransaction action to add the new log to Redux dispatch(addTransaction(formattedLog)); } catch (err) { console.error('Error processing real-time log:', err); } }); - }, [blockchainEvents, sessionData?.address, dispatch]); + }, [blockchainEvents, sessionData?.address, dispatch, uniqueEventNames, eventConfigs]); return { transactions, loading, error }; -}; +} export default useGetSmartWalletTransactions; diff --git a/src/sections/finance/components/finance-transactions-history.tsx b/src/sections/finance/components/finance-transactions-history.tsx index fc9847d9..4ef33ad0 100644 --- a/src/sections/finance/components/finance-transactions-history.tsx +++ b/src/sections/finance/components/finance-transactions-history.tsx @@ -30,7 +30,11 @@ import useGetSmartWalletTransactions from '@src/hooks/use-get-smart-wallet-trans import { processTransactionData } from '@src/utils/finance-graphs/groupedTransactions'; import FinanceOverlayLoader from '@src/sections/finance/components/finance-overlay-loader.tsx'; -const STATUS_OPTIONS = [{ value: 'all', label: 'All' }, ...TRANSACTIONS_TYPES.slice(0, -2)]; +const STATUS_OPTIONS = [ + { value: 'all', label: 'All' }, + ...TRANSACTIONS_TYPES, + { value: 'other', label: 'Other' } +]; const TABLE_HEAD = [ { id: 'name', label: 'Transaction Info', width: 20 }, @@ -88,6 +92,10 @@ export default function FinanceTransactionsHistory() { [handleFilters] ); + const removeDuplicatesById = (array: any[]) => { + return Array.from(new Map(array.map((item) => [item.id, item])).values()); + } + return ( <> - {tab.value === 'all' && transactionData.length} + {tab.value === 'all' && + removeDuplicatesById(transactionData).length + } {tab.value === 'transferFrom' && - transactionData.filter( - (t) => - t.type.toLowerCase() === 'transferto' || t.type.toLowerCase() === 'withdraw' - ).length} + removeDuplicatesById( + transactionData.filter( + (t) => + t.type.toLowerCase() === 'transferto' || + t.type.toLowerCase() === 'withdraw' + ) + ).length + } {tab.value === 'transferTo' && - transactionData.filter( - (t) => - t.type.toLowerCase() === 'transferfrom' || - t.type.toLowerCase() === 'deposit' - ).length} + removeDuplicatesById( + transactionData.filter( + (t) => + t.type.toLowerCase() === 'transferfrom' || + t.type.toLowerCase() === 'deposit' + ) + ).length + } + {tab.value === 'other' && + removeDuplicatesById( + transactionData.filter( + (t) => + t.type.toLowerCase() === 'locked' || + t.type.toLowerCase() === 'claimed' || + t.type.toLowerCase() === 'reserved' || + t.type.toLowerCase() === 'collected' || + t.type.toLowerCase() === 'released' + ) + ).length + } } /> @@ -224,7 +254,23 @@ function applyFilter({ (t) => t.type.toLowerCase() === 'transferfrom' || t.type.toLowerCase() === 'deposit' ); } + + if (status === 'other') { + filteredData = filteredData.filter( + (t) => + t.type.toLowerCase() === 'locked' || + t.type.toLowerCase() === 'claimed' || + t.type.toLowerCase() === 'reserved' || + t.type.toLowerCase() === 'collected' || + t.type.toLowerCase() === 'released' + ); + } } + // delete duplicated items + filteredData = Array.from( + new Map(filteredData.map((item) => [item.id, item])).values() + ); + return filteredData; } diff --git a/src/sections/finance/components/finance-transactions-table-row.tsx b/src/sections/finance/components/finance-transactions-table-row.tsx index 349b0eb5..46e434bf 100644 --- a/src/sections/finance/components/finance-transactions-table-row.tsx +++ b/src/sections/finance/components/finance-transactions-table-row.tsx @@ -22,16 +22,37 @@ type Props = { // ---------------------------------------------------------------------- const urlTxBase = 'https://www.oklink.com/es-la/amoy/tx/'; +const COLORS = { + success: '#00AB55', + danger: '#FF4842', + warning: '#dc9f00', + info: '#3a7dd5', +} + +const TX_COLORS: any = { + 'transferTo': COLORS.danger, + 'transferFrom': COLORS.success, + 'deposit': COLORS.success, + 'withdraw': COLORS.danger, + 'locked': COLORS.info, + 'claimed': COLORS.success, + 'reserved': COLORS.warning, + 'collected': COLORS.success, + 'released': COLORS.success, +} // ---------------------------------------------------------------------- export default function FinanceTransactionTableRow({ row, selected }: Props) { - const { date, name, amount, avatarUrl, message, category, id } = row; + const { date, name, amount, avatarUrl, message, category, id, type } = row; const dateObject = new Date(Number(date) * 1000); const dateLbl = format(dateObject, 'dd/MM/yyyy'); const timeLbl = format(dateObject, 'p'); + console.log('row') + console.log(row) + const renderPrimary = ( @@ -61,7 +82,7 @@ export default function FinanceTransactionTableRow({ row, selected }: Props) { - + {category === 'income' ? '' : '-'} {amount} MMC diff --git a/src/types/transaction.ts b/src/types/transaction.ts index 2502d64a..861cd929 100644 --- a/src/types/transaction.ts +++ b/src/types/transaction.ts @@ -7,6 +7,4 @@ export type IOrderTableFilters = { export const TRANSACTIONS_TYPES = [ { value: 'transferTo', label: 'Income' }, { value: 'transferFrom', label: 'Outcomes' }, - { value: 'deposit', label: 'Deposit' }, - { value: 'withdraw', label: 'Withdraw' }, ]; diff --git a/src/utils/finance-graphs/groupedTransactions.ts b/src/utils/finance-graphs/groupedTransactions.ts index 15149a76..13240f77 100644 --- a/src/utils/finance-graphs/groupedTransactions.ts +++ b/src/utils/finance-graphs/groupedTransactions.ts @@ -115,19 +115,79 @@ export type ProcessedTransactionData = { amount: string | null; }; +type EventName = + | 'transferFrom' + | 'transferTo' + | 'deposit' + | 'withdraw' + | 'locked' + | 'claimed' + | 'reserved' + | 'collected' + | 'released'; + +type EventConfig = { + getName: (args: any) => string; + getAvatarUrl: (args: any) => string; +}; + +const eventConfig: Record = { + transferFrom: { + getName: (args) => args.origin, + getAvatarUrl: (args) => dicebear(args.origin), + }, + transferTo: { + getName: (args) => args.recipient, + getAvatarUrl: (args) => dicebear(args.recipient), + }, + deposit: { + getName: (args) => args.recipient, + getAvatarUrl: (args) => dicebear(args.recipient), + }, + withdraw: { + getName: (args) => args.origin, + getAvatarUrl: (args) => dicebear(args.origin), + }, + locked: { + getName: (args) => args.account, + getAvatarUrl: (args) => dicebear(args.account), + }, + claimed: { + getName: (args) => args.claimer, + getAvatarUrl: (args) => dicebear(args.claimer), + }, + reserved: { + getName: (args) => args.from, + getAvatarUrl: (args) => dicebear(args.from), + }, + collected: { + getName: (args) => args.from, + getAvatarUrl: (args) => dicebear(args.from), + }, + released: { + getName: (args) => args.to, + getAvatarUrl: (args) => dicebear(args.to), + }, +}; + export const processTransactionData = (data: TransactionLog[]): ProcessedTransactionData[] => { - return data?.map((transaction, _index) => ({ - id: transaction.transactionHash, - name: - transaction.event === 'transferFrom' ? transaction.args.origin : transaction.args.recipient, - avatarUrl: dicebear(transaction.event === 'transferFrom' ? transaction.args.origin : transaction.args.recipient), - type: transaction.event, - message: parseTransactionTypeLabel(transaction.event), - category: parseTransactionType(transaction.event), - date: transaction.timestamp, - status: 'completed', - amount: transaction.formattedAmount, - })); + return data.map((transaction) => { + const config = eventConfig[transaction.event as EventName]; + const name = config ? config.getName(transaction.args) : 'Unknown'; + const avatarUrl = config ? config.getAvatarUrl(transaction.args) : dicebear('default'); + + return { + id: transaction.transactionHash, + name, + avatarUrl, + type: transaction.event, + message: parseTransactionTypeLabel(transaction.event), + category: parseTransactionType(transaction.event), + date: transaction.timestamp, + status: 'completed', + amount: transaction.formattedAmount, + }; + }); }; const parseTransactionTypeLabel = (type: string): string => { @@ -140,6 +200,16 @@ const parseTransactionTypeLabel = (type: string): string => { return 'Deposited'; case 'withdraw': return 'Withdraw'; + case 'locked': + return 'Locked'; + case 'claimed': + return 'Claimed'; + case 'reserved': + return 'Reserved'; + case 'collected': + return 'Collected'; + case 'released': + return 'Released'; default: return type; @@ -157,6 +227,16 @@ const parseTransactionType = (type: string): string => { return 'income'; case 'withdraw': return 'outcome'; + case 'locked': + return 'other'; + case 'claimed': + return 'other'; + case 'reserved': + return 'other'; + case 'collected': + return 'other'; + case 'released': + return 'other'; default: return type; From b9e81e5afa610d054643c28f560c240ab465b39d Mon Sep 17 00:00:00 2001 From: jadapema Date: Wed, 22 Jan 2025 13:53:08 -0600 Subject: [PATCH 4/4] doc: added docs to useGetSmartWalletTransactions --- .../use-get-smart-wallet-transactions.ts | 61 +++++++++++++++++-- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/src/hooks/use-get-smart-wallet-transactions.ts b/src/hooks/use-get-smart-wallet-transactions.ts index 915bd767..6de24ee9 100644 --- a/src/hooks/use-get-smart-wallet-transactions.ts +++ b/src/hooks/use-get-smart-wallet-transactions.ts @@ -6,6 +6,9 @@ import { GLOBAL_CONSTANTS } from '@src/config-global.ts'; import LedgerVaultAbi from '@src/config/abi/LedgerVault.json'; import { addTransaction, setTransactions } from '@redux/transactions'; +/** + * Type definition for a transaction log, including event data and relevant block/transaction metadata. + */ export type TransactionLog = { address: string; args: { @@ -29,13 +32,23 @@ export type TransactionLog = { transactionIndex: number; }; +/** + * Configuration object for each event: + * - eventName: Name of the event in the smart contract ABI. + * - args: Address-related arguments used to filter logs (e.g., recipient, origin). + * - getEventType: Function to determine a custom "event type" (e.g., transferTo, transferFrom) based on the log contents and the user's address. + */ type EventConfig = { eventName: string; args: Record; getEventType: (log: any, userAddress: string) => string; }; -export const useGetSmartWalletTransactions = () => { +/** + * Hook to retrieve smart wallet transactions by querying logs from the LedgerVault contract. + * It also manages live updates when new events are detected in real time. + */ +export default function useGetSmartWalletTransactions() { const dispatch = useDispatch(); const sessionData = useSelector((state: any) => state.auth.session); const blockchainEvents = useSelector((state: any) => state.blockchainEvents.events); @@ -44,6 +57,13 @@ export const useGetSmartWalletTransactions = () => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + /** + * We define all event configurations that we want to capture. + * Each configuration includes: + * - The event name. + * - An object "args" that indicates which fields in the log must match the user's address. + * - A function to map the raw event to a custom "event type" (transferFrom, transferTo, deposit, etc.). + */ const eventConfigs: EventConfig[] = [ { eventName: 'FundsTransferred', @@ -94,6 +114,10 @@ export const useGetSmartWalletTransactions = () => { }, ]; + /** + * Helper function to create the event signature needed by viem's parseAbiItem(). + * For example, an event signature looks like "event FundsTransferred(address indexed origin, address indexed recipient, ...)". + */ const createEventSignature = (event: any): string => { if (!event || !event.name || !event.inputs) { throw new Error('Invalid event in ABI'); @@ -104,8 +128,13 @@ export const useGetSmartWalletTransactions = () => { return `event ${event.name}(${inputs})`; }; + /** + * Generate a dictionary (object) of parsed ABIs based on all unique event names in the eventConfigs. + * a) Extract unique event names (e.g. FundsTransferred, FundsDeposited, etc.). + * b) Find those events in the LedgerVaultAbi and parse them with parseAbiItem(). + */ const uniqueEventNames = Array.from( - new Set(eventConfigs.map((config) => config.eventName)) + new Set(eventConfigs.map((config) => config.eventName)) // Removes duplicates ); const parsedAbis = uniqueEventNames.reduce((acc, eventName) => { @@ -113,12 +142,16 @@ export const useGetSmartWalletTransactions = () => { (item: any) => item.type === 'event' && item.name === eventName ); if (!eventAbi) { - throw new Error(`The event is not fund ${eventName} on the ABI`); + throw new Error(`No definition found for event ${eventName} in the ABI`); } acc[eventName] = parseAbiItem(createEventSignature(eventAbi)); return acc; }, {} as Record>); + /** + * Function to fetch historical logs from the LedgerVault contract, using the user's address as a filter. + * The logs are then sorted, processed, and stored in Redux. + */ const fetchLogs = async () => { if (!sessionData?.address) { setLoading(false); @@ -129,6 +162,7 @@ export const useGetSmartWalletTransactions = () => { setLoading(true); setError(null); + // a) Build an array of promises, one for each eventConfig, calling publicClient.getLogs. const promises = eventConfigs.map(({ eventName, args }) => { return publicClient.getLogs({ address: GLOBAL_CONSTANTS.LEDGER_VAULT_ADDRESS as Address, @@ -139,14 +173,18 @@ export const useGetSmartWalletTransactions = () => { }); }); + // b) Execute all the promises in parallel. const results = await Promise.all(promises); + // c) Flatten the array of arrays of logs into one array. const allLogs = results.flat(); + // d) Fetch block timestamps for each log and map them to a structured format. const logsWithDetails = await Promise.all( allLogs.map(async (log: any) => { const block = await publicClient.getBlock({ blockNumber: log.blockNumber }); + // Find the event config to determine the custom "eventType". const foundConfig = eventConfigs.find((c) => c.eventName === log.eventName); const eventType = foundConfig ? foundConfig.getEventType(log, sessionData?.address) @@ -162,21 +200,27 @@ export const useGetSmartWalletTransactions = () => { }) ); + // e) Sort logs by blockNumber descending, then by transactionIndex descending. const sortedLogs = logsWithDetails.sort((a, b) => { const blockDifference = Number(b.blockNumber) - Number(a.blockNumber); if (blockDifference !== 0) return blockDifference; return Number(b.transactionIndex) - Number(a.transactionIndex); }); + // Finally, update Redux state with the sorted logs. dispatch(setTransactions(sortedLogs)); } catch (err) { console.error('Error fetching logs:', err); - setError(err instanceof Error ? err.message : 'An unknown error occurred'); + setError(err instanceof Error ? err.message : 'Unknown error'); } finally { setLoading(false); } }; + /** + * useEffect hook that fires when the user's address changes, triggering the fetchLogs function. + * If there's already a list of transactions, we can stop showing the loader. + */ useEffect(() => { fetchLogs(); if (transactions.length) { @@ -185,10 +229,17 @@ export const useGetSmartWalletTransactions = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [sessionData?.address]); + /** + * Real-time events handling: + * When a new event is picked up in `blockchainEvents`, we check if it's from the LedgerVault contract and + * if it's one of the recognized event names. If yes, we process it similarly (fetch block info, add extra fields) + * and then dispatch addTransaction to Redux. + */ useEffect(() => { if (!blockchainEvents?.length) return; blockchainEvents.forEach(async (log: any) => { + // Filter out logs not from our contract or event names not in use. if ( log.address !== GLOBAL_CONSTANTS.LEDGER_VAULT_ADDRESS || !uniqueEventNames.includes(log.eventName) @@ -220,5 +271,3 @@ export const useGetSmartWalletTransactions = () => { return { transactions, loading, error }; } - -export default useGetSmartWalletTransactions;