Skip to content

Commit

Permalink
WIP: transaction history page (#60)
Browse files Browse the repository at this point in the history
* feat: transaction history data fetching

* fix: jumpy bg image

* feat: listing transaction history
  • Loading branch information
yusufseyrek authored Sep 6, 2024
1 parent ffdfb82 commit d73bca7
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 21 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
]
},
"dependencies": {
"moment": "^2.30.1",
"notistack": "^3.0.1"
}
}
4 changes: 2 additions & 2 deletions src/chainConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const bridgeConfig = {
default_ae: 'ct_J3zBY8xxjsRr3QojETNw48Eb38fjvEuJKkQ6KzECvubvEcvCa',
wae: '0xCa781A1779c8f363f7F82BF6f4B406e5d54bAE1F',
default_eth: '0xAbaE76F98A84D1DC3E0af8ed68465631165d33B2',
aeAPI: 'https://mainnet.aeternity.io/mdw/v3',
aeAPI: 'https://mainnet.aeternity.io/mdw',
},
testnet: {
chainId: '0xaa36a7',
Expand All @@ -25,7 +25,7 @@ const bridgeConfig = {
default_ae: 'ct_22WVQXzVCkgQYDbPUTX1YRNnyUx7XUHQ9ZRkK9P7BwwdyqZaXH',
wae: '0xBC6e88A962662195e9bb8C17f8f396aCD7B7FE95',
default_eth: '0xd57aafdC9615835E1F75BcdBDE1c7B1Aa6e4cB10',
aeAPI: 'https://testnet.aeternity.io/mdw/v3',
aeAPI: 'https://testnet.aeternity.io/mdw',
},
};

Expand Down
78 changes: 78 additions & 0 deletions src/components/base/BridgeActionListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import moment from 'moment';
import { Box, Link, Typography } from '@mui/material';
import { BridgeAction } from 'src/hooks/useTransactionHistory';
import { Direction } from 'src/context/AppContext';
import AeternityIcon from './icons/aeternity';
import EthereumIcon from './icons/ethereum';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import getTxUrl from 'src/utils/getTxUrl';

interface Props {
item: BridgeAction;
}

const BridgeActionListItem = ({ item }: Props) => {
const date = moment(item.timestamp);
const fromNow = date.fromNow();

return (
<Box
flex={1}
flexDirection={'row'}
display={'flex'}
gap={2}
justifyContent={'space-around'}
flexWrap={'wrap'}
mt={1}
mb={1}
pt={1}
pb={1}
sx={{ '&:nth-child(even)': { backgroundColor: 'rgba(0, 0, 0, 0.05)' } }}
>
<Typography sx={{ color: 'black' }} component={'span'} variant="body1" width={120}>
<Link target="_blank" href={getTxUrl(item.direction, item.hash)}>
{fromNow}
</Link>
</Typography>

<Box display="flex" alignItems={'center'} gap={1}>
{item.direction === Direction.AeternityToEthereum ? (
<>
<AeternityIcon />
<Typography component="span" variant="body1">
æternity
</Typography>
<ArrowForwardIcon />
<EthereumIcon />
<Typography component="span" variant="body1">
Ethereum
</Typography>
</>
) : (
<>
<EthereumIcon />
<Typography component="span" variant="body1">
Ethereum
</Typography>
<ArrowForwardIcon />
<AeternityIcon />
<Typography component="span" variant="body1">
æternity
</Typography>
</>
)}
</Box>

<Box display={'flex'} gap={1} alignItems={'center'} width={120}>
<img width={28} height={28} src={item.tokenIcon} alt={item.tokenSymbol} />
<Typography component="span" variant="body1">
{item.amount}
</Typography>
<Typography component="span" variant="body1">
{item.tokenSymbol}
</Typography>
</Box>
</Box>
);
};
export default BridgeActionListItem;
3 changes: 3 additions & 0 deletions src/components/navigation/ConnectWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import useWalletContext from 'src/hooks/useWalletContext';

import EthereumIcon from 'src/components/base/icons/ethereum';
import AeternityIcon from 'src/components/base/icons/aeternity';
import { useSnackbar } from 'notistack';

const shortenAddress = (address: string | undefined) => {
if (!address) return '';
return `${address.slice(0, 6)}...${address.slice(-3)}`;
};

const WalletConnect = () => {
const { enqueueSnackbar } = useSnackbar();
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);

Expand Down Expand Up @@ -68,6 +70,7 @@ const WalletConnect = () => {

const handleCopyAddress = () => {
navigator.clipboard.writeText((connectedToEthereum ? ethereumAddress : aeternityAddress)!);
enqueueSnackbar('Copied to clipboard', { variant: 'success' });
handleClose();
};
const handleDisconnect = () => {
Expand Down
123 changes: 123 additions & 0 deletions src/hooks/useTransactionHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { useEffect, useState } from 'react';
import { Direction } from 'src/context/AppContext';
import Constants from 'src/constants';
import * as Ethereum from 'src/services/ethereum';
import BigNumber from 'bignumber.js';
import { Event } from 'ethers';

export interface BridgeAction {
direction: Direction;
hash: string;
toAddress: string;
tokenSymbol: string;
tokenIcon: string;
amount: number;
timestamp: number;
}

const useTransactionHistory = (direction: Direction, address?: string) => {
const [transactions, setTransactions] = useState<BridgeAction[]>([]);
const [loading, setLoading] = useState<boolean>(false);

useEffect(() => {
(async function () {
setLoading(true);

if (direction === Direction.AeternityToEthereum) {
setTransactions(await fetchAeternityTransactions(address));
} else if (direction === Direction.EthereumToAeternity) {
setTransactions(await fetchEthereumTransactions(address));
}

setLoading(false);
})();
}, [direction, address]);

return { transactions, loading };
};

const fetchEthereumTransactions = async (address?: string) => {
if (!address) {
return [];
}

const bridgeContract = new Ethereum.Contract(
Constants.ethereum.bridge_address,
Constants.ethereum.bridge_abi,
Ethereum.Provider,
);
const filter = bridgeContract.filters.BridgeOut();
const events = await bridgeContract.queryFilter(filter, 20141309, 'latest');

return await parseEthereumTransactions(events);
};

const parseEthereumTransactions = async (events: any): Promise<BridgeAction[]> => {
const actions = await Promise.all(
events.map(async (event: Event) => {
const token = Constants.assets.find(
(asset) => asset.ethAddress.toLowerCase() === event.args![0].toLowerCase(),
)!;
const toAddress = event.args![2];
const amount = new BigNumber(event.args![3].toString()).toNumber();

const block = await event.getBlock();
return {
amount: new BigNumber(amount).shiftedBy(-token.decimals).toNumber(),
toAddress,
tokenIcon: token.icon,
tokenSymbol: token.symbol,
hash: event.transactionHash,
timestamp: block.timestamp * 1000,
direction: Direction.EthereumToAeternity,
};
}),
);

return actions.reverse();
};

const fetchAeternityTransactions = async (address?: string) => {
if (!address) {
return [];
}

const transactionsStartUrl = `${Constants.aeAPI}/v3/transactions?account=${address}&contract_id=${Constants.aeternity.bridge_address}&entrypoint=bridge_out&limit=100`;
const fetchTransactions = async (url: string, prevData: any[] = []): Promise<any[]> => {
const _response = await fetch(url);
const response = await _response.json();

const aggregatedData = [...prevData, ...response.data];
if (response.next) {
return fetchTransactions(`${Constants.aeAPI}${response.next}`, aggregatedData);
} else {
return aggregatedData;
}
};

const transactions = await fetchTransactions(transactionsStartUrl);

return parseAeternityTransactions(transactions);
};

const parseAeternityTransactions = (transactions: any): BridgeAction[] => {
return transactions.map((transaction: any) => {
const token = Constants.assets.find(
(asset) => asset.ethAddress.toLowerCase() === transaction.tx.arguments[0].value[0].value.toLowerCase(),
)!;
const toAddress = transaction.tx.arguments[0].value[1].value;
const amount = transaction.tx.arguments[0].value[2].value;

return {
amount: new BigNumber(amount).shiftedBy(-token.decimals).toNumber(),
toAddress,
tokenIcon: token.icon,
tokenSymbol: token.symbol,
hash: transaction.hash,
timestamp: transaction.micro_time,
direction: Direction.AeternityToEthereum,
};
});
};

export default useTransactionHistory;
1 change: 1 addition & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ body {
-moz-osx-font-smoothing: grayscale;
background-image: url('./assets/images/aeternity-get-ae-coins-bg.svg');
background-size: cover;
background-attachment: fixed;
}

code {
Expand Down
14 changes: 3 additions & 11 deletions src/pages/Bridge.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import {
Alert,
Box,
Breadcrumbs,
Container,
Expand Down Expand Up @@ -39,6 +38,7 @@ import Spinner from 'src/components/base/Spinner';
import { useSnackbar } from 'notistack';
import BigNumber from 'bignumber.js';
import addTokenToEthereumWallet from 'src/utils/addTokenToEthereumWallet';
import getTxUrl from 'src/utils/getTxUrl';

const BRIDGE_TOKEN_ACTION_TYPE = 0;
const BRIDGE_ETH_ACTION_TYPE = 1;
Expand Down Expand Up @@ -75,12 +75,6 @@ interface BridgeAction {
bridgeTxHash: string;
}

const getTxUrl = (direction: Direction, hash: string) => {
return direction === Direction.AeternityToEthereum
? `${Constants.aeternity.explorer}/transactions/${hash}`
: `${Constants.ethereum.etherscan}/tx/${hash}`;
};

const checkEvmNetworkHasEnoughBalance = async (asset: Asset, normalizedAmount: number) => {
if (asset.symbol === 'WAE') return true;

Expand All @@ -107,7 +101,7 @@ const checkAeAccountHasEligibleBridgeUse = async (account: string) => {
const aeAPI = Constants.aeAPI;

const response = await fetch(
`${aeAPI}/transactions?account=${account}&contract_id=${bridge}&entrypoint=bridge_out&limit=1`,
`${aeAPI}/v3/transactions?account=${account}&contract_id=${bridge}&entrypoint=bridge_out&limit=1`,
).then((res) => res.json());

if (!response.data.length) {
Expand Down Expand Up @@ -609,9 +603,7 @@ const Bridge: React.FC = () => {

<Grid flexDirection={'row'} container justifyContent={'space-between'}>
<Grid>From:</Grid>
<Grid>
{isBridgeActionFromAeternity ? 'æternity to Ethereum' : 'Ethereum to æternity'}
</Grid>
<Grid>{isBridgeActionFromAeternity ? 'æternity to Ethereum' : 'Ethereum to æternity'}</Grid>
</Grid>

<Grid flexDirection={'row'} container justifyContent={'space-between'}>
Expand Down
21 changes: 16 additions & 5 deletions src/pages/TransactionHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,18 @@ import EthereumIcon from 'src/components/base/icons/ethereum';
import { Direction } from 'src/context/AppContext';
import useAppContext from 'src/hooks/useAppContext';
import useWalletContext from 'src/hooks/useWalletContext';
import useTransactionHistory from 'src/hooks/useTransactionHistory';
import BridgeActionListItem from 'src/components/base/BridgeActionListItem';
import { useSnackbar } from 'notistack';

const TransactionHistory = () => {
const { direction, updateDirection } = useAppContext();
const { aeternityAddress, ethereumAddress } = useWalletContext();
const connectedWalletAddress = direction === Direction.AeternityToEthereum ? aeternityAddress : ethereumAddress;

const { transactions } = useTransactionHistory(direction, connectedWalletAddress);
const { enqueueSnackbar } = useSnackbar();

return (
<Container sx={{ paddingY: 8 }}>
<Grid container direction="row" justifyContent="center" alignItems="flex-start">
Expand Down Expand Up @@ -58,18 +64,20 @@ const TransactionHistory = () => {
</Select>
</FormControl>
<TextField
sx={{ minWidth: { xs: 250, sm: 350, md: 600 }, marginBottom: 3 }}
sx={{ minWidth: { xs: 300, sm: 400, md: 450, lg: 600 }, marginBottom: 3 }}
label="Connected account"
value={connectedWalletAddress || 'Not connected'}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<ContentCopyIcon
sx={{ ':hover': { cursor: 'pointer' } }}
onClick={() =>
connectedWalletAddress &&
navigator.clipboard.writeText(connectedWalletAddress)
}
onClick={() => {
if (connectedWalletAddress) {
navigator.clipboard.writeText(connectedWalletAddress).then();
enqueueSnackbar('Copied to clipboard', { variant: 'success' });
}
}}
/>
</InputAdornment>
),
Expand All @@ -79,6 +87,9 @@ const TransactionHistory = () => {
disabled
/>
<Divider flexItem orientation="horizontal" />
{transactions.map((transaction, index) => (
<BridgeActionListItem key={index} item={transaction} />
))}
</Box>
</CardContent>
</Card>
Expand Down
10 changes: 10 additions & 0 deletions src/utils/getTxUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Constants from 'src/constants';
import { Direction } from 'src/context/AppContext';

const getTxUrl = (direction: Direction, hash: string) => {
return direction === Direction.AeternityToEthereum
? `${Constants.aeternity.explorer}/transactions/${hash}`
: `${Constants.ethereum.etherscan}/tx/${hash}`;
};

export default getTxUrl;
Loading

0 comments on commit d73bca7

Please sign in to comment.