From 44ebb89b980d87248b63070b921fceadea00999a Mon Sep 17 00:00:00 2001 From: Francisco Date: Wed, 26 Jul 2023 15:29:01 +0200 Subject: [PATCH 1/4] bridgetx storage --- packages/bridge-ui-v2/src/app.config.ts | 5 + .../src/libs/storage/BridgeTxService.ts | 153 ++++++++++++++++++ .../src/libs/storage/CustomTokenService.ts | 3 + 3 files changed, 161 insertions(+) create mode 100644 packages/bridge-ui-v2/src/libs/storage/BridgeTxService.ts create mode 100644 packages/bridge-ui-v2/src/libs/storage/CustomTokenService.ts 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/libs/storage/BridgeTxService.ts b/packages/bridge-ui-v2/src/libs/storage/BridgeTxService.ts new file mode 100644 index 00000000000..2dd45f39722 --- /dev/null +++ b/packages/bridge-ui-v2/src/libs/storage/BridgeTxService.ts @@ -0,0 +1,153 @@ +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.owner.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) { + 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(userAddress: Address) { + const txs = this._getTxFromStorage(userAddress); + + log('Bridge transactions from storage', txs); + + const enhancedTxPromises = txs.map(async (tx) => this._enhanceTx(tx, userAddress)); + + 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; + } +} 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..09647f8c144 --- /dev/null +++ b/packages/bridge-ui-v2/src/libs/storage/CustomTokenService.ts @@ -0,0 +1,3 @@ +export class CustomTokenService { + // TODO +} From cc2eacfd6fe25b3cf40fd7abcc75201435709870 Mon Sep 17 00:00:00 2001 From: Francisco Date: Wed, 26 Jul 2023 15:57:54 +0200 Subject: [PATCH 2/4] bridge tx service --- .../src/components/Bridge/Bridge.svelte | 27 ++++++++++++++++- .../bridge-ui-v2/src/libs/bridge/types.ts | 4 +-- .../src/libs/storage/BridgeTxService.ts | 30 ++++++++++++++++--- .../src/libs/storage/CustomTokenService.ts | 6 ++++ .../bridge-ui-v2/src/libs/storage/services.ts | 6 ++++ packages/bridge-ui-v2/src/routes/+page.svelte | 1 - 6 files changed, 66 insertions(+), 8 deletions(-) create mode 100644 packages/bridge-ui-v2/src/libs/storage/services.ts 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/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 index 2dd45f39722..69f6052eac8 100644 --- a/packages/bridge-ui-v2/src/libs/storage/BridgeTxService.ts +++ b/packages/bridge-ui-v2/src/libs/storage/BridgeTxService.ts @@ -65,7 +65,7 @@ export class BridgeTxService { private async _enhanceTx(tx: BridgeTransaction, address: Address, waitForTx = false) { // Filters out the transactions that are not from the current address - if (tx.owner.toLowerCase() !== address.toLowerCase()) return; + if (tx.from.toLowerCase() !== address.toLowerCase()) return; const bridgeTx: BridgeTransaction = { ...tx }; // prevent mutation @@ -77,6 +77,7 @@ export class BridgeTxService { 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), @@ -118,12 +119,12 @@ export class BridgeTxService { bridgeTx.status = status; } - async getAllTxByAddress(userAddress: Address) { - const txs = this._getTxFromStorage(userAddress); + async getAllTxByAddress(address: Address) { + const txs = this._getTxFromStorage(address); log('Bridge transactions from storage', txs); - const enhancedTxPromises = txs.map(async (tx) => this._enhanceTx(tx, userAddress)); + const enhancedTxPromises = txs.map(async (tx) => this._enhanceTx(tx, address)); const enhancedTxs = (await Promise.all(enhancedTxPromises)) // Removes undefined values @@ -150,4 +151,25 @@ export class BridgeTxService { 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 index 09647f8c144..0be6a88168a 100644 --- a/packages/bridge-ui-v2/src/libs/storage/CustomTokenService.ts +++ b/packages/bridge-ui-v2/src/libs/storage/CustomTokenService.ts @@ -1,3 +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 @@ From beea5ad2597b2efb2a7d199cb1cca28c9c73c95c Mon Sep 17 00:00:00 2001 From: Francisco Date: Wed, 26 Jul 2023 16:29:56 +0200 Subject: [PATCH 3/4] minor changes in the styling --- .../src/components/Bridge/BridgeTabs.svelte | 16 ++++++++++------ .../src/components/Header/Header.svelte | 17 ++++++++++------- .../src/components/LinkButton/LinkButton.svelte | 7 +++---- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/packages/bridge-ui-v2/src/components/Bridge/BridgeTabs.svelte b/packages/bridge-ui-v2/src/components/Bridge/BridgeTabs.svelte index 9541eb57ba7..8ecc0023461 100644 --- a/packages/bridge-ui-v2/src/components/Bridge/BridgeTabs.svelte +++ b/packages/bridge-ui-v2/src/components/Bridge/BridgeTabs.svelte @@ -3,17 +3,21 @@ 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'; + + // TODO: mobile first approach? by default all it's for `sm` + 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); - + From bb6ef3d1b191485d3720005a1a8457735d70d733 Mon Sep 17 00:00:00 2001 From: Francisco Date: Wed, 26 Jul 2023 16:30:21 +0200 Subject: [PATCH 4/4] minor change --- packages/bridge-ui-v2/src/components/Bridge/BridgeTabs.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bridge-ui-v2/src/components/Bridge/BridgeTabs.svelte b/packages/bridge-ui-v2/src/components/Bridge/BridgeTabs.svelte index 8ecc0023461..16707e241c1 100644 --- a/packages/bridge-ui-v2/src/components/Bridge/BridgeTabs.svelte +++ b/packages/bridge-ui-v2/src/components/Bridge/BridgeTabs.svelte @@ -5,7 +5,6 @@ import { LinkButton } from '$components/LinkButton'; import { classNames } from '$libs/util/classNames'; - // TODO: mobile first approach? by default all it's for `sm` let classes = classNames('space-x-2', $$props.class); $: isERC20Bridge = $page.route.id === '/';