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'; + +
- {#if isBridgePage} - - {:else} -
- {/if} +
+ {#if isBridgePage} +
-
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 @@