Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bridge-ui-v2): BridgeTx storage #14284

Merged
merged 4 commits into from
Jul 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/bridge-ui-v2/src/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ export const bridge = {
export const pendingTransaction = {
waitTimeout: 300000,
};

export const storageService = {
bridgeTxPrefix: 'bridge-tx',
customTokenPrefix: 'custom-token',
};
27 changes: 26 additions & 1 deletion packages/bridge-ui-v2/src/components/Bridge/Bridge.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand Down
15 changes: 9 additions & 6 deletions packages/bridge-ui-v2/src/components/Bridge/BridgeTabs.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
</script>

<div class="flex w-full ml-2">
<LinkButton class={classes} href="/" active={isERC20Bridge}>
<div class={classes}>
<LinkButton class="py-2 px-[20px]" href="/" active={isERC20Bridge}>
<span> {$t('nav.token')}</span>
</LinkButton>

<LinkButton class={classes} href="/nft" active={isNFTBridge}>
<LinkButton class="py-2 px-[20px]" href="/nft" active={isNFTBridge}>
<span> {$t('nav.nft')}</span>
</LinkButton>
</div>
17 changes: 10 additions & 7 deletions packages/bridge-ui-v2/src/components/Header/Header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
</script>

<!-- <DesktopOrLarger bind:is={isDesktopOrLarger} /> -->

<header
class="
sticky-top
Expand All @@ -26,15 +29,15 @@
">
<LogoWithText class="w-[77px] h-[20px] md:hidden" />

{#if isBridgePage}
<BridgeTabs />
{:else}
<div />
{/if}
<div class="flex justify-end md:f-between-center w-full">
{#if isBridgePage}
<BridgeTabs class="hidden md:flex" />
{/if}

<ConnectButton connected={$account?.isConnected} />
<ConnectButton connected={$account?.isConnected} />
</div>

<label for={drawerToggleId} class="md:hidden">
<label for={drawerToggleId} class="ml-[10px] md:hidden">
<Icon type="bars-menu" />
</label>
</header>
Original file line number Diff line number Diff line change
Expand Up @@ -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);
</script>

<a
{href}
target={external ? '_blank' : null}
class={classNames('p-3 rounded-full flex justify-start content-center', activeClass, $$props.class)}>
<a {href} target={external ? '_blank' : null} class={classes}>
<slot />
</a>
4 changes: 2 additions & 2 deletions packages/bridge-ui-v2/src/libs/bridge/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
175 changes: 175 additions & 0 deletions packages/bridge-ui-v2/src/libs/storage/BridgeTxService.ts
Original file line number Diff line number Diff line change
@@ -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<MessageStatus>;
}

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));
}
}
9 changes: 9 additions & 0 deletions packages/bridge-ui-v2/src/libs/storage/CustomTokenService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export class CustomTokenService {
private readonly storage: Storage;

constructor(storage: Storage) {
this.storage = storage;
}

// TODO
}
6 changes: 6 additions & 0 deletions packages/bridge-ui-v2/src/libs/storage/services.ts
Original file line number Diff line number Diff line change
@@ -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);
1 change: 0 additions & 1 deletion packages/bridge-ui-v2/src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script>
import { Bridge } from '$components/Bridge';
import { Page } from '$components/Page';
import bg from '$libs/assets/bg.svg';
</script>

<Page>
Expand Down