diff --git a/packages/bridge-ui-v2/src/app.config.ts b/packages/bridge-ui-v2/src/app.config.ts
index 64e9bd3fb03..d3032b74a9f 100644
--- a/packages/bridge-ui-v2/src/app.config.ts
+++ b/packages/bridge-ui-v2/src/app.config.ts
@@ -17,3 +17,8 @@ export const bridge = {
export const pendingTransaction = {
waitTimeout: 300000,
};
+
+export const storageService = {
+ bridgeTxPrefix: 'bridge-tx',
+ customTokenPrefix: 'custom-token',
+};
diff --git a/packages/bridge-ui-v2/src/components/Bridge/Bridge.svelte b/packages/bridge-ui-v2/src/components/Bridge/Bridge.svelte
index cc7cd6aac49..3025af946d3 100644
--- a/packages/bridge-ui-v2/src/components/Bridge/Bridge.svelte
+++ b/packages/bridge-ui-v2/src/components/Bridge/Bridge.svelte
@@ -10,10 +10,17 @@
import { OnNetwork } from '$components/OnNetwork';
import { TokenDropdown } from '$components/TokenDropdown';
import { PUBLIC_L1_EXPLORER_URL } from '$env/static/public';
- import { type BridgeArgs, bridges, type ERC20BridgeArgs, type ETHBridgeArgs } from '$libs/bridge';
+ import {
+ type BridgeArgs,
+ bridges,
+ type BridgeTransaction,
+ type ERC20BridgeArgs,
+ type ETHBridgeArgs,
+ } from '$libs/bridge';
import type { ERC20Bridge } from '$libs/bridge/ERC20Bridge';
import { chainContractsMap, chains } from '$libs/chain';
import { ApproveError, NoAllowanceRequiredError, SendERC20Error, SendMessageError } from '$libs/error';
+ import { bridgeTxService } from '$libs/storage/services';
import { ETHToken, getAddress, isDeployedCrossChain, tokens, TokenType } from '$libs/token';
import { getConnectedWallet } from '$libs/util/getConnectedWallet';
import { type Account, account } from '$stores/account';
@@ -199,6 +206,24 @@
}),
);
+ // Let's add it to the user's localStorage
+ const bridgeTx = {
+ hash: txHash,
+ from: $account.address,
+ amount: $enteredAmount,
+ symbol: $selectedToken.symbol,
+ decimals: $selectedToken.decimals,
+ srcChainId: BigInt($network.id),
+ destChainId: BigInt($destNetwork.id),
+
+ // TODO: do we need something else? we can have
+ // access to the Transaction object:
+ // TransactionLegacy, TransactionEIP2930 and
+ // TransactionEIP1559
+ } as BridgeTransaction;
+
+ bridgeTxService.addTxByAddress($account.address, bridgeTx);
+
// Reset the form
amountComponent.clearAmount();
recipientComponent.clearRecipient();
diff --git a/packages/bridge-ui-v2/src/components/Bridge/BridgeTabs.svelte b/packages/bridge-ui-v2/src/components/Bridge/BridgeTabs.svelte
index 9541eb57ba7..16707e241c1 100644
--- a/packages/bridge-ui-v2/src/components/Bridge/BridgeTabs.svelte
+++ b/packages/bridge-ui-v2/src/components/Bridge/BridgeTabs.svelte
@@ -3,17 +3,20 @@
import { page } from '$app/stores';
import { LinkButton } from '$components/LinkButton';
- $: isERC20Bridge = $page.route.id === '/' ? true : false;
- $: isNFTBridge = $page.route.id === '/nft' ? true : false;
- $: classes = `flex-basis-0 mr-2 p-3 rounded-full flex justify-center items-center w-auto sm:w-20 2xl:w-32`;
+ import { classNames } from '$libs/util/classNames';
+
+ let classes = classNames('space-x-2', $$props.class);
+
+ $: isERC20Bridge = $page.route.id === '/';
+ $: isNFTBridge = $page.route.id === '/nft';
-
-
+
+
{$t('nav.token')}
-
+
{$t('nav.nft')}
diff --git a/packages/bridge-ui-v2/src/components/Header/Header.svelte b/packages/bridge-ui-v2/src/components/Header/Header.svelte
index 7887db40fcc..9ffee545928 100644
--- a/packages/bridge-ui-v2/src/components/Header/Header.svelte
+++ b/packages/bridge-ui-v2/src/components/Header/Header.svelte
@@ -6,9 +6,12 @@
import { LogoWithText } from '$components/Logo';
import { drawerToggleId } from '$components/SideNavigation';
import { account } from '$stores/account';
+
$: isBridgePage = $page.route.id === '/' || $page.route.id === '/nft';
+
+
diff --git a/packages/bridge-ui-v2/src/components/LinkButton/LinkButton.svelte b/packages/bridge-ui-v2/src/components/LinkButton/LinkButton.svelte
index a18dff88fca..75fa94bc8c8 100644
--- a/packages/bridge-ui-v2/src/components/LinkButton/LinkButton.svelte
+++ b/packages/bridge-ui-v2/src/components/LinkButton/LinkButton.svelte
@@ -8,11 +8,10 @@
$: activeClass = active
? 'body-bold bg-primary-interactive hover:bg-primary-interactive-hover'
: 'body-regular hover:bg-secondary-interactive-hover';
+
+ $: classes = classNames('p-3 rounded-full flex justify-start content-center', activeClass, $$props.class);
-
+
diff --git a/packages/bridge-ui-v2/src/libs/bridge/types.ts b/packages/bridge-ui-v2/src/libs/bridge/types.ts
index a034b59255f..498017ce277 100644
--- a/packages/bridge-ui-v2/src/libs/bridge/types.ts
+++ b/packages/bridge-ui-v2/src/libs/bridge/types.ts
@@ -44,13 +44,13 @@ export type Message = {
export type BridgeTransaction = {
hash: Hash;
- owner: Address;
- status: MessageStatus;
+ from: Address;
amount: bigint;
symbol: string;
decimals: number;
srcChainId: ChainID;
destChainId: ChainID;
+ status?: MessageStatus;
receipt?: TransactionReceipt;
msgHash?: Hash;
message?: Message;
diff --git a/packages/bridge-ui-v2/src/libs/storage/BridgeTxService.ts b/packages/bridge-ui-v2/src/libs/storage/BridgeTxService.ts
new file mode 100644
index 00000000000..69f6052eac8
--- /dev/null
+++ b/packages/bridge-ui-v2/src/libs/storage/BridgeTxService.ts
@@ -0,0 +1,175 @@
+import { getContract, waitForTransaction } from '@wagmi/core';
+import type { Address, Hash, TransactionReceipt } from 'viem';
+
+import { bridgeABI } from '$abi';
+import { pendingTransaction, storageService } from '$config';
+import { type BridgeTransaction, MessageStatus } from '$libs/bridge';
+import { chainContractsMap, isSupportedChain } from '$libs/chain';
+import { jsonParseWithDefault } from '$libs/util/jsonParseWithDefault';
+import { getLogger } from '$libs/util/logger';
+import { publicClient } from '$libs/wagmi';
+
+const log = getLogger('storage:BridgeTxService');
+
+export class BridgeTxService {
+ private readonly storage: Storage;
+
+ private static async _getTransactionReceipt(chainId: number, hash: Hash) {
+ const client = publicClient({ chainId });
+ return client.getTransactionReceipt({ hash });
+ }
+
+ private static async _getBridgeMessageSent(userAddress: Address, chainId: number, blockNumber: number) {
+ // Gets the event MessageSent from the bridge contract
+ // in the block where the transaction was mined, and find
+ // our event MessageSent whose owner is the address passed in
+
+ const bridgeAddress = chainContractsMap[chainId].bridgeAddress;
+ const client = publicClient({ chainId });
+
+ const filter = await client.createContractEventFilter({
+ abi: bridgeABI,
+ address: bridgeAddress,
+ eventName: 'MessageSent',
+ fromBlock: BigInt(blockNumber),
+ toBlock: BigInt(blockNumber),
+ });
+
+ const messageSentEvents = await client.getFilterLogs({ filter });
+
+ // Filter out those events that are not from the current address
+ return messageSentEvents.find(({ args }) => args.message?.owner.toLowerCase() === userAddress.toLowerCase());
+ }
+
+ private static _getBridgeMessageStatus(msgHash: Hash, chainId: number) {
+ const bridgeAddress = chainContractsMap[chainId].bridgeAddress;
+
+ const bridgeContract = getContract({
+ chainId,
+ abi: bridgeABI,
+ address: bridgeAddress,
+ });
+
+ return bridgeContract.read.getMessageStatus([msgHash]) as Promise;
+ }
+
+ constructor(storage: Storage) {
+ this.storage = storage;
+ }
+
+ private _getTxFromStorage(address: Address) {
+ const key = `${storageService.bridgeTxPrefix}:${address}`;
+ const txs = jsonParseWithDefault(this.storage.getItem(key), []) as BridgeTransaction[];
+ return txs;
+ }
+
+ private async _enhanceTx(tx: BridgeTransaction, address: Address, waitForTx = false) {
+ // Filters out the transactions that are not from the current address
+ if (tx.from.toLowerCase() !== address.toLowerCase()) return;
+
+ const bridgeTx: BridgeTransaction = { ...tx }; // prevent mutation
+
+ const { destChainId, srcChainId, hash } = bridgeTx;
+
+ // Ignore transactions from chains not supported by the bridge
+ if (isSupportedChain(srcChainId)) return;
+
+ let receipt: TransactionReceipt | null = null;
+
+ if (waitForTx) {
+ // We might want to wait for the transaction to be mined
+ receipt = await waitForTransaction({
+ hash,
+ chainId: Number(srcChainId),
+ timeout: pendingTransaction.waitTimeout,
+ });
+ } else {
+ // Returns the transaction receipt for hash or null
+ // if the transaction has not been mined.
+ receipt = await BridgeTxService._getTransactionReceipt(Number(srcChainId), hash);
+ }
+
+ if (!receipt) {
+ return bridgeTx;
+ }
+
+ // We have receipt
+ bridgeTx.receipt = receipt;
+
+ const messageSentEvent = await BridgeTxService._getBridgeMessageSent(
+ address,
+ Number(srcChainId),
+ Number(receipt.blockNumber),
+ );
+
+ if (!messageSentEvent?.args?.msgHash || !messageSentEvent?.args?.message) {
+ // No message yet, so we can't get more info from this transaction
+ return bridgeTx;
+ }
+
+ const { msgHash, message } = messageSentEvent.args;
+
+ // Let's add this new info to the transaction in case something else
+ // fails, such as the filter for ERC20Sent events
+ bridgeTx.msgHash = msgHash;
+ bridgeTx.message = message;
+
+ const status = await BridgeTxService._getBridgeMessageStatus(msgHash, Number(destChainId));
+
+ bridgeTx.status = status;
+ }
+
+ async getAllTxByAddress(address: Address) {
+ const txs = this._getTxFromStorage(address);
+
+ log('Bridge transactions from storage', txs);
+
+ const enhancedTxPromises = txs.map(async (tx) => this._enhanceTx(tx, address));
+
+ const enhancedTxs = (await Promise.all(enhancedTxPromises))
+ // Removes undefined values
+ .filter((tx) => Boolean(tx)) as BridgeTransaction[];
+
+ // Place new transactions at the top of the list
+ enhancedTxs.sort((tx) => (tx.status === MessageStatus.NEW ? -1 : 1));
+
+ log('Enhanced transactions', [...enhancedTxs]);
+
+ return enhancedTxs;
+ }
+
+ async getTxByHash(hash: Hash, address: Address) {
+ const txs = this._getTxFromStorage(address);
+
+ const tx = txs.find((tx) => tx.hash === hash) as BridgeTransaction;
+
+ log('Transaction from storage', { ...tx });
+
+ const enhancedTx = await this._enhanceTx(tx, address, true);
+
+ log('Enhanced transaction', enhancedTx);
+
+ return enhancedTx;
+ }
+
+ addTxByAddress(address: Address, tx: BridgeTransaction) {
+ const txs = this._getTxFromStorage(address);
+
+ txs.unshift(tx);
+
+ log('Adding transaction to storage', tx);
+
+ const key = `${storageService.bridgeTxPrefix}:${address}`;
+ this.storage.setItem(
+ key,
+ // We need to serialize the BigInts as strings
+ JSON.stringify(txs, (_, value) => (typeof value === 'bigint' ? value.toString() : value)),
+ );
+ }
+
+ updateByAddress(address: Address, txs: BridgeTransaction[]) {
+ log('Updating storage with transactions', txs);
+ const key = `${storageService.bridgeTxPrefix}:${address}`;
+ this.storage.setItem(key, JSON.stringify(txs));
+ }
+}
diff --git a/packages/bridge-ui-v2/src/libs/storage/CustomTokenService.ts b/packages/bridge-ui-v2/src/libs/storage/CustomTokenService.ts
new file mode 100644
index 00000000000..0be6a88168a
--- /dev/null
+++ b/packages/bridge-ui-v2/src/libs/storage/CustomTokenService.ts
@@ -0,0 +1,9 @@
+export class CustomTokenService {
+ private readonly storage: Storage;
+
+ constructor(storage: Storage) {
+ this.storage = storage;
+ }
+
+ // TODO
+}
diff --git a/packages/bridge-ui-v2/src/libs/storage/services.ts b/packages/bridge-ui-v2/src/libs/storage/services.ts
new file mode 100644
index 00000000000..15ea11c1a4c
--- /dev/null
+++ b/packages/bridge-ui-v2/src/libs/storage/services.ts
@@ -0,0 +1,6 @@
+import { BridgeTxService } from './BridgeTxService';
+import { CustomTokenService } from './CustomTokenService';
+
+export const bridgeTxService = new BridgeTxService(globalThis.localStorage);
+
+export const customTokenService = new CustomTokenService(globalThis.localStorage);
diff --git a/packages/bridge-ui-v2/src/routes/+page.svelte b/packages/bridge-ui-v2/src/routes/+page.svelte
index cfc543f5696..e57f4ce38b7 100644
--- a/packages/bridge-ui-v2/src/routes/+page.svelte
+++ b/packages/bridge-ui-v2/src/routes/+page.svelte
@@ -1,7 +1,6 @@