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): relayer component #17777

Merged
merged 10 commits into from
Jul 18, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
placeholder="0x1B77..."
bind:value={ethereumAddress}
on:input={validateAddress}
class="w-full input-box withValdiation py-6 pr-16 px-[26px] font-bold placeholder:text-tertiary-content {classes}" />
class="w-full input-box withValdiation py-6 pr-16 px-[26px] font-bold placeholder:text-tertiary-content {classes} !border-primary-border-dark" />
{#if ethereumAddress}
<button class="absolute right-6 uppercase body-bold text-secondary-content" on:click={clearAddress}>
<Icon type="x-close-circle" fillClass="fill-primary-icon" size={24} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
} from '$components/NotificationToast/NotificationToast.svelte';
import OnAccount from '$components/OnAccount/OnAccount.svelte';
import type { BridgeTransaction } from '$libs/bridge/types';
import { closeOnEscapeOrOutsideClick } from '$libs/customActions';
import {
InsufficientBalanceError,
InvalidProofError,
Expand Down Expand Up @@ -180,7 +181,11 @@
}
</script>

<dialog id={dialogId} class="modal {isDesktopOrLarger ? '' : 'modal-bottom'}" class:modal-open={dialogOpen}>
<dialog
id={dialogId}
class="modal {isDesktopOrLarger ? '' : 'modal-bottom'}"
class:modal-open={dialogOpen}
use:closeOnEscapeOrOutsideClick={{ enabled: dialogOpen, callback: closeDialog, uuid: dialogId }}>
<div class="modal-box relative w-full bg-neutral-background absolute md:min-h-[600px]">
<div class="w-full f-between-center">
<CloseButton onClick={closeDialog} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@

$: successFullPreChecks = correctChain && hasEnoughEth && hasEnoughQuota;

$: if (!checkingPrerequisites && successFullPreChecks && $account && !differentRecipient) {
$: if (!checkingPrerequisites && successFullPreChecks && $account && !onlyDestOwnerCanClaimWarning) {
hideContinueButton = false;
canContinue = true;
} else {
Expand All @@ -103,15 +103,15 @@

$: hasPaidProcessingFee = tx.processingFee > 0;

$: differentRecipient = false;
$: onlyDestOwnerCanClaimWarning = false;
$: if (tx.message?.to && $account?.address && tx.message.destOwner) {
if (
getAddress(tx.message.to) === getAddress($account.address) ||
getAddress($account.address) === getAddress(tx.message.destOwner)
) {
differentRecipient = false;
const destOwnerMustClaim = tx.message.gasLimit === 0; // If gasLimit is 0, the destOwner must claim
const isDestOwner = getAddress($account.address) === getAddress(tx.message.destOwner);

if (destOwnerMustClaim && !isDestOwner) {
onlyDestOwnerCanClaimWarning = true;
} else {
differentRecipient = true;
onlyDestOwnerCanClaimWarning = false;
}
}

Expand All @@ -125,21 +125,23 @@
<div class="font-bold text-primary-content">{$t('transactions.claim.steps.pre_check.title')}</div>
</div>
<div class="min-h-[150px] grid content-between">
{#if differentRecipient}
{#if onlyDestOwnerCanClaimWarning}
<div class="f-between-center">
<div class="f-row gap-1">
<div class="f-col">
<Alert type="info"
>{$t('transactions.claim.steps.pre_check.different_recipient')}
>{$t('transactions.claim.steps.pre_check.only_destowner_can_claim')}
<div class="h-sep" />
<span class="font-bold">{$t('common.recipient')}: </span>{shortenAddress(tx.message?.destOwner, 6, 4)}
<span class="font-bold">{$t('common.owner.destination')}: </span>{shortenAddress(
tx.message?.destOwner,
6,
4,
)}
</Alert>
</div>
</div>
{#if checkingPrerequisites}
<Spinner />
{:else}
<Icon type="x-close-circle" fillClass="fill-negative-sentiment" />
{/if}
</div>
{:else}
Expand Down Expand Up @@ -208,7 +210,7 @@
</div>
{/if}
</div>
{#if !canContinue && !correctChain && !differentRecipient}
{#if !canContinue && !correctChain && !onlyDestOwnerCanClaimWarning}
<div class="h-sep" />
<div class="f-col space-y-[16px]">
<ActionButton
Expand Down
111 changes: 111 additions & 0 deletions packages/bridge-ui/src/components/Relayer/Relayer.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<script lang="ts">
import { t } from 'svelte-i18n';

import AddressInput from '$components/Bridge/SharedBridgeComponents/AddressInput/AddressInput.svelte';
import { AddressInputState } from '$components/Bridge/SharedBridgeComponents/AddressInput/state';
import ActionButton from '$components/Button/ActionButton.svelte';
import Card from '$components/Card/Card.svelte';
import OnAccount from '$components/OnAccount/OnAccount.svelte';
import Transaction from '$components/Transactions/Transaction.svelte';
import { type BridgeTransaction, fetchTransactions, MessageStatus } from '$libs/bridge';
import { getLogger } from '$libs/util/logger';
import { type Account, account } from '$stores/account';

const log = getLogger('RelayerComponent');

let transactions: BridgeTransaction[] = [];
let fetching = false;
let addressState = AddressInputState.DEFAULT;

const onAccountChange = async (newAccount: Account, oldAccount?: Account) => {
// We want to make sure that we are connected and only
// fetch if the account has changed
if (newAccount.address && newAccount.address !== oldAccount?.address) {
reset();
}
};
const reset = () => {
log('reset');
transactions = [];
fetching = false;
addressState = AddressInputState.DEFAULT;
transactionsToShow = [];
addressToSearch = undefined;
searchDisabled = true;
};

const fetchTxForAddress = async () => {
log('fetchTxForAddress');
fetching = true;
if (addressToSearch) {
const { mergedTransactions } = await fetchTransactions(addressToSearch);
log('mergedTransactions', mergedTransactions);
if (mergedTransactions.length > 0) {
transactions = mergedTransactions;
}
}
fetching = false;
};

const handleTransactionRemoved = (event: CustomEvent<{ transaction: BridgeTransaction }>) => {
log('handleTransactionRemoved', event.detail.transaction);
transactions = transactions.filter((tx) => tx !== event.detail.transaction);
};

$: inputDisabled = fetching || !$account?.isConnected;

$: addressToSearch = undefined;
$: searchDisabled = fetching || !addressToSearch || addressState !== AddressInputState.VALID || inputDisabled;

$: transactionsToShow = transactions.filter((tx) => {
const gasLimitZero = tx.message?.gasLimit === 0;
const userIsRecipientOrDestOwner =
tx.message?.to === $account?.address || tx.message?.destOwner === $account?.address;
if (tx.status === MessageStatus.NEW) {
if (gasLimitZero && userIsRecipientOrDestOwner) {
return tx;
} else if (!gasLimitZero) {
return tx;
} else if (gasLimitZero && !userIsRecipientOrDestOwner) {
console.warn('gaslimit set to zero, not claimable by connected wallet', tx);
}
}
});
</script>

<Card
title={$t('relayer_component.title')}
class="container f-col md:w-[768px]"
text={$t('relayer_component.description')}>
<div class="f-col space-y-[35px]">
<span class="mt-[30px]">{$t('relayer_component.step1.title')}</span>

<AddressInput
labelText={$t('relayer_component.address_input_label')}
isDisabled={inputDisabled}
bind:ethereumAddress={addressToSearch}
bind:state={addressState} />

<div class="h-sep" />
<span>{$t('relayer_component.step2.title')}</span>
<ActionButton
on:click={fetchTxForAddress}
priority="primary"
class="w-full"
label="Search"
loading={fetching}
disabled={searchDisabled}>Search transactions</ActionButton>
{#if transactionsToShow.length === 0}
<div class="text-center">{$t('relayer_component.no_tx_found')}</div>
{:else}
<div class="h-sep" />
{/if}
</div>
{#each transactionsToShow as tx}
{#if tx.status === MessageStatus.NEW}
<Transaction item={tx} {handleTransactionRemoved} bind:bridgeTxStatus={tx.status} />
{/if}
{/each}
</Card>

<OnAccount change={onAccountChange} />
1 change: 1 addition & 0 deletions packages/bridge-ui/src/components/Relayer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Relayer } from './Relayer.svelte';
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import { tick } from 'svelte';
import { t } from 'svelte-i18n';

import IconFlipper from '$components/Icon/IconFlipper.svelte';
Expand Down Expand Up @@ -33,13 +34,14 @@
flipped = !flipped;
};

const select = (option: (typeof options)[0]) => {
const select = async (option: (typeof options)[0]) => {
selectedStatus = option.value;
await tick();
closeMenu();
};

$: menuClasses = classNames(
'menu absolute right-0 w-[210px] p-3 mt-2 rounded-[10px] bg-neutral-background z-10 box-shadow-small',
'menu absolute right-0 w-[210px] p-3 mt-2 rounded-[10px] bg-neutral-background z-10 box-shadow-small',
menuOpen ? 'visible opacity-100' : 'invisible opacity-0',
);
</script>
Expand All @@ -66,9 +68,10 @@
</button>
{#if menuOpen}
<ul
id={uuid}
role="listbox"
class={menuClasses}
use:closeOnEscapeOrOutsideClick={{ enabled: menuOpen, callback: () => closeMenu, uuid: uuid }}>
use:closeOnEscapeOrOutsideClick={{ enabled: menuOpen, callback: closeMenu, uuid }}>
{#each options as option (option.value)}
<li
role="option"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
function onStatusChange(status: MessageStatus) {
// Keeping model and UI in sync
bridgeTxStatus = bridgeTx.msgStatus = status;
dispatch('statusChange', status);
}

async function handleRetryClick() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
let isDesktopOrLarger = false;
let detailsOpen = false;

let bridgeTxStatus: Maybe<MessageStatus>;
export let bridgeTxStatus: Maybe<MessageStatus>;

let attrs = isDesktopOrLarger ? {} : { role: 'button' };

Expand Down Expand Up @@ -91,6 +91,10 @@
bridgeTxStatus = item.msgStatus;
}

const handleStatusChange = (event: CustomEvent<MessageStatus>) => {
bridgeTxStatus = event.detail;
};

$: {
if (item.tokenType === TokenType.ERC721 || item.tokenType === TokenType.ERC1155) {
// for NFTs we need to fetch more information about the transaction
Expand Down Expand Up @@ -252,7 +256,8 @@
on:transactionRemoved={handleTransactionRemoved}
bind:bridgeTxStatus
on:openModal={handleOpenModal}
on:insufficientFunds={handleInsufficientFunds} />
on:insufficientFunds={handleInsufficientFunds}
on:statusChange={handleStatusChange} />
</div>
<div class="hidden md:flex w-1/5 py-2 flex flex-col justify-center">
<a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,8 @@
class="flex flex-col items-center"
style={isBlurred ? `filter: blur(5px); transition: filter ${transitionTime / 1000}s ease-in-out` : ''}>
{#each transactionsToShow as item (item.srcTxHash)}
<Transaction {item} {handleTransactionRemoved} />
{@const status = item.msgStatus}
<Transaction {item} {handleTransactionRemoved} bridgeTxStatus={status} />
<div class="h-sep !my-0 {isDesktopOrLarger ? 'display-inline' : 'hidden'}" />
{/each}
</div>
Expand Down
17 changes: 16 additions & 1 deletion packages/bridge-ui/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,9 @@
"name": "Name",
"not_available_short": "N/A",
"ok": "Okay",
"owner": {
"destination": "Destination owner"
},
"recipient": "Recipient",
"reset_to_wallet": "Reset to current address",
"review": "Review",
Expand Down Expand Up @@ -436,6 +439,18 @@
"tooltip": "Defaults to your address. You can add a custom recipient address as well.",
"tooltip_title": "What is Custom Recipient?"
},
"relayer_component": {
"address_input_label": "Enter the recipient address",
"description": "This component allows you to manually claim any claimable transaction",
"no_tx_found": "No claimable transactions found",
"step1": {
"title": "Step 1: Select the recipient"
},
"step2": {
"title": "Step 2: Search the transaction you want"
},
"title": "Relayer Component"
},
"switch_modal": {
"description": "Your current network is not supported. Please select one of the following chains to proceed:",
"title": "Incorrect network detected"
Expand Down Expand Up @@ -501,8 +516,8 @@
},
"pre_check": {
"chain_check": "Connected to the correct chain",
"different_recipient": "The recipient of this transaction does not match the sender or the currently connected wallet. To manually claim, please connect to the recipient's wallet.",
"funds_check": "Sufficient funds to claim",
"only_destowner_can_claim": "This transaction can only be claimed by the destination owner. Please connect to the correct wallet to claim this transaction.",
"quota_check": "Sufficient daily quota",
"ready": "You can continue with the claim process!",
"step": "Claim step",
Expand Down
4 changes: 3 additions & 1 deletion packages/bridge-ui/src/libs/bridge/Bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ export abstract class Bridge {
const userAddress = wallet.account.address;
// Are we the owner of the message, either src or dest?
if (getAddress(srcOwner) !== getAddress(userAddress) && getAddress(destOwner) !== getAddress(userAddress)) {
throw new WrongOwnerError('user cannot process this as it is not their message');
if (bridgeTx.message?.gasLimit === 0) {
throw new WrongOwnerError('user cannot process this as it is not their message');
}
}

const destBridgeAddress = routingContractsMap[destChainId][srcChainId].bridgeAddress;
Expand Down
14 changes: 9 additions & 5 deletions packages/bridge-ui/src/libs/bridge/fetchTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,20 @@ import { type BridgeTransaction, MessageStatus } from './types';
const log = getLogger('bridge:fetchTransactions');
let error: Error;

export async function fetchTransactions(userAddress: Address) {
export async function fetchTransactions(userAddress: Address, chainId?: number) {
// Transactions from local storage
const localTxs: BridgeTransaction[] = await bridgeTxService.getAllTxByAddress(userAddress);

// Get all transactions from all relayers
const relayerTxPromises: Promise<BridgeTransaction[]>[] = relayerApiServices.map(async (relayerApiService) => {
const { txs } = await relayerApiService.getAllBridgeTransactionByAddress(userAddress, {
page: 0,
size: 100,
});
const { txs } = await relayerApiService.getAllBridgeTransactionByAddress(
userAddress,
{
page: 0,
size: 100,
},
chainId,
);
log(`fetched ${txs?.length ?? 0} transactions from relayer`, txs);
return txs;
});
Expand Down
1 change: 1 addition & 0 deletions packages/bridge-ui/src/libs/bridge/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export type BridgeTransaction = {
status?: MessageStatus;
receipt?: TransactionReceipt;
canonicalTokenAddress?: Address;
claimedBy?: Address;
};

interface BaseBridgeTransferOp {
Expand Down
Loading