diff --git a/src/App.tsx b/src/App.tsx index cdca9588d..111fced96 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 2a3203ca7..915bd767c 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 fc9847d96..4ef33ad08 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 349b0eb5e..46e434bfd 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 2502d64a3..861cd9298 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 15149a767..13240f770 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;