mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 14:47:58 +00:00
4f683538d3
Add full internationalization across 127+ components and pages. 790+ translation keys in en, tr, kmr, ckb, ar, fa locales. Remove duplicate keys and delete unused .json locale files.
387 lines
14 KiB
TypeScript
387 lines
14 KiB
TypeScript
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<TransactionHistoryProps> = ({ isOpen, onClose }) => {
|
|
const { api, isApiReady, selectedAccount } = usePezkuwi();
|
|
const { toast } = useToast();
|
|
const { t } = useTranslation();
|
|
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
|
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 (
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
<DialogContent className="bg-gray-900 border-gray-800 max-w-3xl max-h-[80vh]">
|
|
<DialogHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<DialogTitle className="text-white">{t('txHistory.title')}</DialogTitle>
|
|
<DialogDescription className="text-gray-400">
|
|
{t('txHistory.description')}
|
|
</DialogDescription>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={fetchTransactions}
|
|
disabled={isLoading}
|
|
className="text-gray-400 hover:text-white"
|
|
>
|
|
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
|
|
</Button>
|
|
</div>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-3 overflow-y-auto max-h-[500px]">
|
|
{isLoading ? (
|
|
<div className="text-center py-12">
|
|
<RefreshCw className="w-12 h-12 text-gray-600 mx-auto mb-3 animate-spin" />
|
|
<p className="text-gray-400">{t('txHistory.loading')}</p>
|
|
</div>
|
|
) : transactions.length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<History className="w-12 h-12 text-gray-600 mx-auto mb-3" />
|
|
<p className="text-gray-500">{t('txHistory.noTx')}</p>
|
|
<p className="text-gray-600 text-sm mt-1">
|
|
{t('txHistory.noTxDesc')}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
transactions.map((tx) => (
|
|
<div
|
|
key={`${tx.blockNumber}-${tx.extrinsicIndex}`}
|
|
className="bg-gray-800/50 border border-gray-700 rounded-lg p-4 hover:bg-gray-800 transition-colors"
|
|
>
|
|
<div className="flex items-start justify-between mb-2">
|
|
<div className="flex items-center gap-2">
|
|
{isIncoming(tx) ? (
|
|
<div className="bg-green-500/20 p-2 rounded-lg">
|
|
<ArrowDownRight className="w-4 h-4 text-green-400" />
|
|
</div>
|
|
) : (
|
|
<div className="bg-yellow-500/20 p-2 rounded-lg">
|
|
<ArrowUpRight className="w-4 h-4 text-yellow-400" />
|
|
</div>
|
|
)}
|
|
<div>
|
|
<div className="text-white font-semibold">
|
|
{isIncoming(tx) ? t('txHistory.received') : t('txHistory.sent')}
|
|
</div>
|
|
<div className="text-xs text-gray-400">
|
|
{tx.section}.{tx.method}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="text-white font-mono">
|
|
{isIncoming(tx) ? '+' : '-'}{formatAmount(tx.amount || '0')}
|
|
</div>
|
|
<div className="text-xs text-gray-400">
|
|
Block #{tx.blockNumber}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
|
<div>
|
|
<span className="text-gray-500">{t('txHistory.from')}</span>
|
|
<div className="text-gray-300 font-mono">
|
|
{tx.from.slice(0, 8)}...{tx.from.slice(-6)}
|
|
</div>
|
|
</div>
|
|
{tx.to && (
|
|
<div>
|
|
<span className="text-gray-500">{t('txHistory.to')}</span>
|
|
<div className="text-gray-300 font-mono">
|
|
{tx.to.slice(0, 8)}...{tx.to.slice(-6)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between mt-2 pt-2 border-t border-gray-700">
|
|
<div className="text-xs text-gray-500">
|
|
{formatTimestamp(tx.timestamp)}
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="text-xs text-blue-400 hover:text-blue-300"
|
|
onClick={() => {
|
|
toast({
|
|
title: t('txHistory.txDetails'),
|
|
description: t('txHistory.blockExtrinsic', { block: tx.blockNumber, extrinsic: tx.extrinsicIndex }),
|
|
});
|
|
}}
|
|
>
|
|
{t('txHistory.viewDetails')}
|
|
<ExternalLink className="w-3 h-3 ml-1" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}; |