/* eslint-disable @typescript-eslint/naming-convention */ import { useCallback, useContext, useEffect, useMemo, useState, } from 'react'; import { Box } from '@biom3/react'; import { ChainId, WrappedBrowserProvider, TokenFilterTypes, TokenInfo, WalletProviderRdns, ChainSlug, Checkout, ThemeOverrides, } from '@imtbl/checkout-sdk'; import { useTranslation } from 'react-i18next'; import { JsonRpcProvider } from 'ethers'; import { TokenBridge } from '@imtbl/bridge-sdk'; import { HeaderNavigation } from '../Header/HeaderNavigation'; import { SimpleLayout } from '../SimpleLayout/SimpleLayout'; import { FooterLogo } from '../Footer/FooterLogo'; import { EventTargetContext } from '../../context/event-target-context/EventTargetContext'; import { connectToProvider, isPassportProvider, } from '../../lib/provider'; import { DEFAULT_TRANSACTIONS_RETRY_POLICY, } from '../../lib'; import { Transaction, TransactionType } from '../../lib/clients'; import { retry } from '../../lib/retry'; import { getChainSlugById } from '../../lib/chains'; import { CryptoFiatActions, CryptoFiatContext, } from '../../context/crypto-fiat-context/CryptoFiatContext'; import { BridgeActions, BridgeContext, } from '../../widgets/bridge/context/BridgeContext'; import { WalletDrawer } from '../WalletDrawer/WalletDrawer'; import { sendBridgeWidgetCloseEvent } from '../../widgets/bridge/BridgeWidgetEvents'; import { Shimmer } from './Shimmer'; import { supportBoxContainerStyle, transactionsContainerStyle, transactionsListContainerStyle, } from './TransactionsStyles'; import { EmptyStateNotConnected } from './EmptyStateNotConnected'; import { SupportMessage } from './SupportMessage'; import { KnownNetworkMap } from './transactionsType'; import { TransactionList } from './TransactionList'; import { NoTransactions } from './NoTransactions'; import { useInjectedProviders } from '../../lib/hooks/useInjectedProviders'; import { WalletChangeEvent } from '../WalletDrawer/WalletDrawerEvents'; type TransactionsProps = { defaultTokenImage: string; onBackButtonClick: () => void; themeOverrides: ThemeOverrides; }; const getTransactionsDetails = async (tokenBridge: TokenBridge, checkout: Checkout, recipient: string) => { const pendingWithdrawals = await tokenBridge.getPendingWithdrawals({ recipient }); const transactions = (await Promise.all(pendingWithdrawals.pending.map(async (withdrawal, index) => { const tokenMapping = await tokenBridge.getTokenMapping({ rootToken: withdrawal.token, rootChainId: checkout.config.l1ChainId.toString(), childChainId: checkout.config.l2ChainId.toString(), }); if (!tokenMapping.childToken) return null; return { tx_type: TransactionType.BRIDGE, details: { from_address: withdrawal.withdrawer, from_chain: getChainSlugById(checkout.config.l2ChainId), from_token_address: tokenMapping.childToken, to_address: withdrawal.recipient, to_chain: getChainSlugById(checkout.config.l1ChainId), to_token_address: withdrawal.token, amount: withdrawal.amount.toString(), current_status: { status: 'withdrawal_pending', index, withdrawal_ready_at: new Date(withdrawal.timeoutEnd * 1000).toISOString(), }, }, created_at: new Date(withdrawal.timeoutStart * 1000).toISOString(), }; }))).filter((tx) => tx !== null); return { result: transactions }; }; export function Transactions({ defaultTokenImage, onBackButtonClick, themeOverrides, }: TransactionsProps) { const { eventTargetState: { eventTarget }, } = useContext(EventTargetContext); const { cryptoFiatDispatch } = useContext(CryptoFiatContext); const { bridgeDispatch, bridgeState: { checkout, from, tokenBridge }, } = useContext(BridgeContext); const { t } = useTranslation(); const [loading, setLoading] = useState(true); const [knownTokenMap, setKnownTokenMap] = useState< KnownNetworkMap | undefined >(undefined); const [txs, setTxs] = useState([]); const [showWalletDrawer, setShowWalletDrawer] = useState(false); const isPassport = isPassportProvider(from?.browserProvider); // Fetch the tokens for the root chain using the allowed tokens. // In case this list does not have all the tokens, there is logic // built into the component to fetch the // the missing data. const rootChainTokensHashmap = useCallback(async () => { if (!checkout) return {}; const rootChainId = checkout.config.l1ChainId; try { const tokens = ( await checkout.getTokenAllowList({ type: TokenFilterTypes.BRIDGE, chainId: rootChainId, }) ).tokens ?? []; return tokens.reduce((out, current) => { // eslint-disable-next-line no-param-reassign out[current.address!.toLowerCase()] = current; return out; }, {}); } catch (e) { // eslint-disable-next-line no-console console.error(e); return []; } }, [checkout]); // Fetch the tokens for the root chain using the user balances tokens. // In case this list does not have all the tokens, there is logic // built into the component to fetch the // the missing data. const childChainTokensHashmap = useCallback(async () => { if (!from?.browserProvider) return {}; if (!from?.walletAddress) return {}; const childChainId = checkout.config.l2ChainId; try { const data = await checkout.getAllBalances({ provider: from?.browserProvider, walletAddress: from?.walletAddress, chainId: childChainId, }); return data.balances.reduce((out, current) => { // eslint-disable-next-line no-param-reassign out[current.token.address!.toLowerCase()] = current.token; return out; }, {}); } catch (e) { // eslint-disable-next-line no-console console.error(e); return []; } }, [checkout, from]); const getTokensDetails = useCallback(async (tokensWithChainSlug: { [p: string]: string; }) => { const rootChainName = getChainSlugById(checkout.config.l1ChainId); const childChainName = getChainSlugById(checkout.config.l2ChainId); const [rootData, childData] = await Promise.all([ rootChainTokensHashmap(), childChainTokensHashmap(), ]); // Fetch the data for the missing tokens: tokensWithChainSlug const missingTokens: { [k: string]: string } = {}; Object.entries(tokensWithChainSlug).forEach(([key, value]) => { if ( (tokensWithChainSlug[key] === rootChainName && !rootData[key]) || (tokensWithChainSlug[key] === childChainName && !childData[key]) ) missingTokens[key] = value; }); // Root provider is always L1 const rootProvider = new JsonRpcProvider( checkout.config.networkMap.get(checkout.config.l1ChainId)?.rpcUrls[0], ); // Child provider is always L2 const childProvider = new JsonRpcProvider( checkout.config.networkMap.get(checkout.config.l2ChainId)?.rpcUrls[0], ); const rootTokenInfoPromises: Promise[] = []; const childTokenInfoPromises: Promise[] = []; Object.entries(missingTokens).forEach(([tokenAddress, chainName]) => { if (chainName === rootChainName) { // Root provider rootTokenInfoPromises.push( checkout.getTokenInfo({ provider: rootProvider, tokenAddress, }), ); } else { // child provider childTokenInfoPromises.push( checkout.getTokenInfo({ provider: childProvider, tokenAddress, }), ); } }); const rootTokenInfo = await Promise.allSettled(rootTokenInfoPromises); const childTokenInfo = await Promise.allSettled(childTokenInfoPromises); ( rootTokenInfo.filter( (result) => result.status === 'fulfilled', ) as PromiseFulfilledResult[] ).forEach((result) => { const resp = result; rootData[resp.value.address!.toLowerCase()] = resp.value; }); ( childTokenInfo.filter( (result) => result.status === 'fulfilled', ) as PromiseFulfilledResult[] ).forEach((result) => { const resp = result; childData[resp.value.address!.toLowerCase()] = resp.value; }); const allTokenSymbols: string[] = []; Object.values(rootData).forEach((token) => allTokenSymbols.push((token as TokenInfo).symbol.toLowerCase())); Object.values(childData).forEach((token) => allTokenSymbols.push((token as TokenInfo).symbol.toLowerCase())); cryptoFiatDispatch({ payload: { type: CryptoFiatActions.SET_TOKEN_SYMBOLS, tokenSymbols: allTokenSymbols, }, }); return { [rootChainName]: rootData, [childChainName]: childData }; }, [checkout, cryptoFiatDispatch, rootChainTokensHashmap, childChainTokensHashmap]); const handleWalletChange = useCallback( async (event: WalletChangeEvent) => { try { let changeAccount = false; if (event.providerDetail.info.rdns === WalletProviderRdns.METAMASK) { changeAccount = true; } const browserProvider = new WrappedBrowserProvider(event.provider); const connectedProvider = await connectToProvider(checkout, browserProvider, changeAccount); const network = await connectedProvider.getNetwork(); const address = await (await connectedProvider.getSigner()).getAddress(); setTxs([]); bridgeDispatch({ payload: { type: BridgeActions.SET_WALLETS_AND_NETWORKS, from: { browserProvider: connectedProvider, walletProviderInfo: { ...event.providerDetail.info, }, walletAddress: address.toLowerCase(), network: network.chainId as unknown as ChainId, }, to: null, }, }); } catch (error) { // eslint-disable-next-line no-console console.error(error); } finally { setShowWalletDrawer(false); } }, [checkout, bridgeDispatch], ); const handleBackButtonClick = () => { if (from) { bridgeDispatch({ payload: { type: BridgeActions.SET_WALLETS_AND_NETWORKS, from: { browserProvider: from?.browserProvider, walletAddress: from?.walletAddress, walletProviderInfo: from?.walletProviderInfo, network: from?.network, }, to: null, }, }); bridgeDispatch({ payload: { type: BridgeActions.SET_TOKEN_AND_AMOUNT, token: null, amount: '', }, }); } onBackButtonClick(); }; const fetchData = useCallback(async () => { if (!from?.walletAddress) return undefined; if (!tokenBridge) return undefined; const localTxs = await getTransactionsDetails(tokenBridge, checkout, from.walletAddress); const tokensWithChainSlug = localTxs.result.reduce>((acc, tx) => ({ ...acc, [tx.details.from_token_address]: tx.details.from_chain }), {}); return { tokens: await getTokensDetails(tokensWithChainSlug), transactions: localTxs.result, }; }, [from, tokenBridge, checkout, getTokensDetails]); const { providers } = useInjectedProviders({ checkout }); const walletOptions = useMemo(() => providers, [providers]); // Fetch all the data at once useEffect(() => { (async () => { setLoading(true); const data = await retry(fetchData, DEFAULT_TRANSACTIONS_RETRY_POLICY); if (!data) { setLoading(false); return; } const knownTxs = data.transactions.filter((txn) => { const tokens = data.tokens[txn.details.from_chain]; if (!tokens) return false; const token = tokens[txn.details.from_token_address.toLowerCase()]; if (!token) return false; return true; }); setKnownTokenMap(data.tokens); setTxs(knownTxs); setLoading(false); })(); }, [from, checkout, fetchData]); return ( sendBridgeWidgetCloseEvent(eventTarget)} /> )} footer={} > {!from?.browserProvider && ( setShowWalletDrawer(true)} /> )} {from?.browserProvider && loading && } {from?.browserProvider && !loading && txs.length > 0 && knownTokenMap && ( setShowWalletDrawer(true)} /> )} {from?.browserProvider && !loading && txs.length === 0 && ( setShowWalletDrawer(true)} /> )} {from?.browserProvider && txs.length > 0 && ( )} { setShowWalletDrawer(show); }} onWalletChange={handleWalletChange} drawerBackground={themeOverrides.drawerBackground} /> ); }