import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { usePezkuwi } from '@/contexts/PezkuwiContext'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { History, ExternalLink, ArrowUpRight, ArrowDownRight, RefreshCw } from 'lucide-react'; import { useToast } from '@/hooks/use-toast'; interface TransactionHistoryProps { isOpen: boolean; onClose: () => void; } interface Transaction { blockNumber: number; extrinsicIndex: number; hash: string; method: string; section: string; from: string; to?: string; amount?: string; success: boolean; timestamp?: number; } export const TransactionHistory: React.FC = ({ isOpen, onClose }) => { const { api, isApiReady, selectedAccount } = usePezkuwi(); const { toast } = useToast(); const { t } = useTranslation(); const [transactions, setTransactions] = useState([]); const [isLoading, setIsLoading] = useState(false); const fetchTransactions = async () => { if (!api || !isApiReady || !selectedAccount) return; setIsLoading(true); try { if (import.meta.env.DEV) console.log('Fetching transactions...'); const currentBlock = await api.rpc.chain.getBlock(); const currentBlockNumber = currentBlock.block.header.number.toNumber(); if (import.meta.env.DEV) console.log('Current block number:', currentBlockNumber); const txList: Transaction[] = []; const blocksToCheck = Math.min(200, currentBlockNumber); for (let i = 0; i < blocksToCheck && txList.length < 20; i++) { const blockNumber = currentBlockNumber - i; try { const blockHash = await api.rpc.chain.getBlockHash(blockNumber); const block = await api.rpc.chain.getBlock(blockHash); // Try to get timestamp, but don't fail if state is pruned let timestamp = 0; try { const ts = await api.query.timestamp.now.at(blockHash); timestamp = ts.toNumber(); } catch { // State pruned, use current time as fallback timestamp = Date.now(); } if (import.meta.env.DEV) console.log(`Block #${blockNumber}: ${block.block.extrinsics.length} extrinsics`); // Check each extrinsic in the block block.block.extrinsics.forEach((extrinsic, index) => { // Skip unsigned extrinsics (system calls) if (!extrinsic.isSigned) { return; } const { method, signer } = extrinsic; if (import.meta.env.DEV) console.log(` Extrinsic #${index}: ${method.section}.${method.method}, signer: ${signer.toString()}`); // Check if transaction involves our account const fromAddress = signer.toString(); const isFromOurAccount = fromAddress === selectedAccount.address; // Only track transactions from this account if (!isFromOurAccount) { return; } // Parse balances.transfer or balances.transferKeepAlive if (method.section === 'balances' && (method.method === 'transfer' || method.method === 'transferKeepAlive')) { const [dest, value] = method.args; txList.push({ blockNumber, extrinsicIndex: index, hash: extrinsic.hash.toHex(), method: method.method, section: method.section, from: fromAddress, to: dest.toString(), amount: value.toString(), success: true, timestamp: timestamp, }); } // Parse assets.transfer (PEZ, USDT, etc.) else if (method.section === 'assets' && method.method === 'transfer') { const [assetId, dest, value] = method.args; txList.push({ blockNumber, extrinsicIndex: index, hash: extrinsic.hash.toHex(), method: `${method.method} (Asset ${assetId.toString()})`, section: method.section, from: fromAddress, to: dest.toString(), amount: value.toString(), success: true, timestamp: timestamp, }); } // Parse staking operations else if (method.section === 'staking') { if (method.method === 'bond' || method.method === 'bondExtra') { const value = method.args[method.method === 'bond' ? 1 : 0]; txList.push({ blockNumber, extrinsicIndex: index, hash: extrinsic.hash.toHex(), method: method.method, section: method.section, from: fromAddress, amount: value.toString(), success: true, timestamp: timestamp, }); } else if (method.method === 'unbond') { const [value] = method.args; txList.push({ blockNumber, extrinsicIndex: index, hash: extrinsic.hash.toHex(), method: method.method, section: method.section, from: fromAddress, amount: value.toString(), success: true, timestamp: timestamp, }); } else if (method.method === 'nominate' || method.method === 'withdrawUnbonded' || method.method === 'chill') { txList.push({ blockNumber, extrinsicIndex: index, hash: extrinsic.hash.toHex(), method: method.method, section: method.section, from: fromAddress, success: true, timestamp: timestamp, }); } } // Parse DEX operations else if (method.section === 'dex') { if (method.method === 'swap') { const [, amountIn] = method.args; txList.push({ blockNumber, extrinsicIndex: index, hash: extrinsic.hash.toHex(), method: method.method, section: method.section, from: fromAddress, amount: amountIn.toString(), success: true, timestamp: timestamp, }); } else if (method.method === 'addLiquidity' || method.method === 'removeLiquidity') { txList.push({ blockNumber, extrinsicIndex: index, hash: extrinsic.hash.toHex(), method: method.method, section: method.section, from: fromAddress, success: true, timestamp: timestamp, }); } } // Parse stakingScore operations else if (method.section === 'stakingScore' && method.method === 'startTracking') { txList.push({ blockNumber, extrinsicIndex: index, hash: extrinsic.hash.toHex(), method: method.method, section: method.section, from: fromAddress, success: true, timestamp: timestamp, }); } // Parse pezRewards operations else if (method.section === 'pezRewards' && (method.method === 'claimReward' || method.method === 'recordTrustScore')) { txList.push({ blockNumber, extrinsicIndex: index, hash: extrinsic.hash.toHex(), method: method.method, section: method.section, from: fromAddress, success: true, timestamp: timestamp, }); } }); } catch (blockError) { if (import.meta.env.DEV) console.warn(`Error processing block #${blockNumber}:`, blockError); // Continue to next block } } if (import.meta.env.DEV) console.log('Found transactions:', txList.length); setTransactions(txList); } catch { if (import.meta.env.DEV) console.error('Failed to fetch transactions:', error); toast({ title: t('transfer.error'), description: t('txHistory.fetchError'), variant: "destructive", }); } finally { setIsLoading(false); } }; useEffect(() => { if (isOpen) { fetchTransactions(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOpen, api, isApiReady, selectedAccount]); const formatAmount = (amount: string, decimals: number = 12) => { const value = parseInt(amount) / Math.pow(10, decimals); return value.toFixed(4); }; const formatTimestamp = (timestamp?: number) => { if (!timestamp) return 'Unknown'; const date = new Date(timestamp); return date.toLocaleString(); }; const isIncoming = (tx: Transaction) => { return tx.to === selectedAccount?.address; }; return (
{t('txHistory.title')} {t('txHistory.description')}
{isLoading ? (

{t('txHistory.loading')}

) : transactions.length === 0 ? (

{t('txHistory.noTx')}

{t('txHistory.noTxDesc')}

) : ( transactions.map((tx) => (
{isIncoming(tx) ? (
) : (
)}
{isIncoming(tx) ? t('txHistory.received') : t('txHistory.sent')}
{tx.section}.{tx.method}
{isIncoming(tx) ? '+' : '-'}{formatAmount(tx.amount || '0')}
Block #{tx.blockNumber}
{t('txHistory.from')}
{tx.from.slice(0, 8)}...{tx.from.slice(-6)}
{tx.to && (
{t('txHistory.to')}
{tx.to.slice(0, 8)}...{tx.to.slice(-6)}
)}
{formatTimestamp(tx.timestamp)}
)) )}
); };