Direct OP Withdrawal Claim button (#3123)

* Direct OP Withdrawal Claim button instead of NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL env

Resolves #3022

* [skip ci] fix envs preset
This commit is contained in:
tom goriunov
2025-11-18 07:53:48 +01:00
committed by GitHub
parent ff411308b8
commit 2205672dd5
20 changed files with 569 additions and 57 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ const parentChain: ParentChain | undefined = (() => {
return {
...envValue,
baseUrl: baseUrl ?? envValue?.baseUrl,
baseUrl: baseUrl || envValue?.baseUrl || '',
};
})();
+2
View File
@@ -9,6 +9,8 @@ NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
NEXT_PUBLIC_ROLLUP_PARENT_CHAIN={'id':11155111,'name':'Sepolia','baseUrl':'https://eth-sepolia.blockscout.com','rpcUrls':['https://0xrpc.io/sep'],'currency':{'name':'Ether','symbol':'ETH','decimals':18},'isTestnet':true}
# Instance ENVs
NEXT_PUBLIC_AD_BANNER_ENABLE_SPECIFY=true
NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS=['talentprotocol','drops','blockscoutbadges','gitpoap','efp','etherscore','webacy','humanpassport','trustblock','smartmuv','humanode','deepdao']
+38
View File
@@ -21,6 +21,13 @@ export const data: OptimisticL2WithdrawalsResponse = {
msg_nonce: 396,
msg_nonce_version: 1,
status: 'Ready to prove',
portal_contract_address_hash: null,
msg_sender_address_hash: null,
msg_target_address_hash: null,
msg_data: null,
msg_gas_limit: null,
msg_nonce_raw: null,
msg_value: null,
},
{
challenge_period_end: null,
@@ -31,6 +38,13 @@ export const data: OptimisticL2WithdrawalsResponse = {
msg_nonce: 391,
msg_nonce_version: 1,
status: 'Ready to prove',
portal_contract_address_hash: null,
msg_sender_address_hash: null,
msg_target_address_hash: null,
msg_data: null,
msg_gas_limit: null,
msg_nonce_raw: null,
msg_value: null,
},
{
challenge_period_end: '2022-11-11T12:50:02.000000Z',
@@ -41,6 +55,30 @@ export const data: OptimisticL2WithdrawalsResponse = {
msg_nonce: 390,
msg_nonce_version: 1,
status: 'Ready for relay',
portal_contract_address_hash: null,
msg_sender_address_hash: null,
msg_target_address_hash: null,
msg_data: null,
msg_gas_limit: null,
msg_nonce_raw: null,
msg_value: null,
},
{
challenge_period_end: '2022-11-13T12:50:02.000000Z',
from: null,
l1_transaction_hash: null,
l2_timestamp: null,
l2_transaction_hash: '0xe14b1f46838176702244a5343629bcecf728ca2d9881d47b4ce46e00c387d7aa',
msg_nonce: 388,
msg_nonce_version: 3,
status: 'Ready for relay',
portal_contract_address_hash: '0x67aab90c548b284be30b05c376001b4db90b87ba',
msg_sender_address_hash: '0x67aab90c548b284be30b05c376001b4db90b87ba',
msg_target_address_hash: '0x67aab90c548b284be30b05c376001b4db90b87ba',
msg_data: '0x01',
msg_gas_limit: '42',
msg_nonce_raw: '390',
msg_value: '1000000000000000000',
},
],
next_page_params: {
+1 -1
View File
@@ -18,7 +18,7 @@ export default fixture;
export const ENVS_MAP: Record<string, Array<[string, string]>> = {
optimisticRollup: [
[ 'NEXT_PUBLIC_ROLLUP_TYPE', 'optimistic' ],
[ 'NEXT_PUBLIC_ROLLUP_L1_BASE_URL', 'https://localhost:3101' ],
[ 'NEXT_PUBLIC_ROLLUP_PARENT_CHAIN', '{"id":11155111,"name":"Sepolia","baseUrl":"https://localhost:3101","rpcUrls":["https://localhost:3101"],"currency":{"name":"Ether","symbol":"ETH","decimals":18},"isTestnet":true}' ],
[ 'NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL', 'https://localhost:3102' ],
[ 'NEXT_PUBLIC_FAULT_PROOF_ENABLED', 'true' ],
[ 'NEXT_PUBLIC_ROLLUP_DA_CELESTIA_CELENIUM_URL', 'https://mocha.celenium.io/blob' ],
+7
View File
@@ -28,6 +28,13 @@ export const L2_WITHDRAWAL_ITEM: OptimisticL2WithdrawalsItem = {
msg_nonce: 2393,
msg_nonce_version: 1,
status: 'Ready to prove',
portal_contract_address_hash: null,
msg_sender_address_hash: null,
msg_target_address_hash: null,
msg_data: null,
msg_gas_limit: null,
msg_nonce_raw: null,
msg_value: null,
};
export const L2_TXN_BATCHES_ITEM: OptimisticL2TxnBatchesItem = {
+11 -1
View File
@@ -113,7 +113,17 @@ export type OptimismL2BatchBlocks = {
} | null;
};
export type OptimisticL2WithdrawalsItem = {
export interface OptimisticL2WithdrawalClaimInfo {
portal_contract_address_hash: string | null;
msg_sender_address_hash: string | null;
msg_target_address_hash: string | null;
msg_data: string | null;
msg_gas_limit: string | null;
msg_nonce_raw: string | null;
msg_value: string | null;
}
export interface OptimisticL2WithdrawalsItem extends OptimisticL2WithdrawalClaimInfo {
challenge_period_end: string | null;
from: AddressParam | null;
l1_transaction_hash: string | null;
+2 -2
View File
@@ -5,7 +5,7 @@ import type { DecodedInput } from './decodedInput';
import type { Fee } from './fee';
import type { ChainInfo, MessageStatus } from './interop';
import type { NovesTxTranslation } from './noves';
import type { OptimisticL2WithdrawalStatus } from './optimisticL2';
import type { OptimisticL2WithdrawalClaimInfo, OptimisticL2WithdrawalStatus } from './optimisticL2';
import type { ScrollL2BlockStatus } from './scrollL2';
import type { TokenInfo } from './token';
import type { TokenTransfer } from './tokenTransfer';
@@ -19,7 +19,7 @@ export type TransactionRevertReason = {
export type WrappedTransactionFields = 'decoded_input' | 'fee' | 'gas_limit' | 'gas_price' | 'hash' | 'max_fee_per_gas' |
'max_priority_fee_per_gas' | 'method' | 'nonce' | 'raw_input' | 'to' | 'type' | 'value';
export interface OpWithdrawal {
export interface OpWithdrawal extends OptimisticL2WithdrawalClaimInfo {
l1_transaction_hash: string;
nonce: number;
status: OptimisticL2WithdrawalStatus;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 76 KiB

@@ -1,28 +1,114 @@
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { OptimisticL2WithdrawalStatus } from 'types/api/optimisticL2';
import type { OpWithdrawal } from 'types/api/transaction';
import * as addressMock from 'mocks/address/address';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
import TxDetailsWithdrawalStatusOptimistic from './TxDetailsWithdrawalStatusOptimistic';
const statuses: Array<OptimisticL2WithdrawalStatus> = [
'Waiting for state root',
'Ready for relay',
'Relayed',
];
const TX_HASH = '0x7d93a59a228e97d084a635181c3053e324237d07566ec12287eae6da2bcf9456';
statuses.forEach((status) => {
test(`status="${ status }"`, async({ render, mockEnvs }) => {
const BASE_DATA: OpWithdrawal = {
l1_transaction_hash: TX_HASH,
nonce: 1,
status: 'Waiting for state root',
portal_contract_address_hash: null,
msg_sender_address_hash: null,
msg_target_address_hash: null,
msg_data: null,
msg_gas_limit: null,
msg_nonce_raw: null,
msg_value: null,
};
test('status=Waiting for state root', async({ render, mockEnvs }) => {
await mockEnvs(ENVS_MAP.optimisticRollup);
const data: OpWithdrawal = {
...BASE_DATA,
status: 'Waiting for state root',
};
const component = await render(
<Box p={ 2 }>
<TxDetailsWithdrawalStatusOptimistic
data={ data }
txHash={ TX_HASH }
from={ addressMock.withoutName }
/>
</Box>,
);
await expect(component).toHaveScreenshot();
});
test.describe('status=Ready for relay', () => {
test('with claim button', async({ render, mockEnvs }) => {
await mockEnvs(ENVS_MAP.optimisticRollup);
const data: OpWithdrawal = {
...BASE_DATA,
status: 'Ready for relay',
};
const component = await render(
<Box p={ 2 }>
<TxDetailsWithdrawalStatusOptimistic status={ status } l1TxHash="0x7d93a59a228e97d084a635181c3053e324237d07566ec12287eae6da2bcf9456"/>
<TxDetailsWithdrawalStatusOptimistic
data={ data }
txHash={ TX_HASH }
from={ addressMock.withoutName }
/>
</Box>,
);
await expect(component).toHaveScreenshot();
});
test('without claim button', async({ render, mockEnvs }) => {
await mockEnvs([
...ENVS_MAP.optimisticRollup,
[ 'NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL', '' ],
]);
const data: OpWithdrawal = {
...BASE_DATA,
status: 'Ready for relay',
};
const component = await render(
<Box p={ 2 }>
<TxDetailsWithdrawalStatusOptimistic
data={ data }
txHash={ TX_HASH }
from={ addressMock.withoutName }
/>
</Box>,
);
await expect(component).toHaveScreenshot();
});
});
test('status=Relayed', async({ render, mockEnvs }) => {
await mockEnvs(ENVS_MAP.optimisticRollup);
const data: OpWithdrawal = {
...BASE_DATA,
status: 'Relayed',
};
const component = await render(
<Box p={ 2 }>
<TxDetailsWithdrawalStatusOptimistic
data={ data }
txHash={ TX_HASH }
from={ addressMock.withoutName }
/>
</Box>,
);
await expect(component).toHaveScreenshot();
});
@@ -1,16 +1,20 @@
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import type { AddressParam } from 'types/api/addressParams';
import type { OptimisticL2WithdrawalStatus } from 'types/api/optimisticL2';
import type { OpWithdrawal, Transaction } from 'types/api/transaction';
import config from 'configs/app';
import { Button } from 'toolkit/chakra/button';
import { Link } from 'toolkit/chakra/link';
import { getResourceKey } from 'lib/api/useApiQuery';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps';
import OptimisticL2ClaimButton, { canClaimDirectlyGuard } from 'ui/withdrawals/optimisticL2/OptimisticL2ClaimButton';
interface Props {
status: OptimisticL2WithdrawalStatus | undefined;
l1TxHash: string | undefined;
data: OpWithdrawal;
from: AddressParam;
txHash: string;
}
const WITHDRAWAL_STATUS_STEPS: Array<OptimisticL2WithdrawalStatus> = [
@@ -39,26 +43,59 @@ const WITHDRAWAL_STATUS_ORDER_GAME: Array<OptimisticL2WithdrawalStatus> = [
const rollupFeature = config.features.rollup;
const TxDetailsWithdrawalStatusOptimistic = ({ status, l1TxHash }: Props) => {
if (!status || !rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
const TxDetailsWithdrawalStatusOptimistic = ({ data, from, txHash }: Props) => {
const queryClient = useQueryClient();
const handleClaimSuccess = React.useCallback((l1TxHash: string) => {
queryClient.setQueryData(
getResourceKey('general:tx', { pathParams: { hash: txHash } }),
(prevData: Transaction | undefined) => {
if (!prevData) {
return;
}
const newWithdrawals = prevData.op_withdrawals?.map((withdrawal) => {
if (withdrawal.nonce === data.nonce) {
return {
...withdrawal,
l1_transaction_hash: l1TxHash,
status: 'Relayed',
};
}
return withdrawal;
});
return {
...prevData,
op_withdrawals: newWithdrawals,
};
});
}, [ data.nonce, queryClient, txHash ]);
if (!data.status || !rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
return null;
}
const hasClaimButton = status === 'Ready for relay' && rollupFeature.L2WithdrawalUrl;
const canClaimDirectly = canClaimDirectlyGuard(data);
const hasRightSlot = data.status === 'Ready for relay' && (rollupFeature.L2WithdrawalUrl || canClaimDirectly);
const rightSlot = hasRightSlot ?
<OptimisticL2ClaimButton data={ data } from={ from } onSuccess={ handleClaimSuccess } source="tx"/> :
null;
const steps = (() => {
switch (status) {
switch (data.status) {
case 'Ready for relay':
return WITHDRAWAL_STATUS_STEPS.slice(0, -1);
return hasRightSlot ? WITHDRAWAL_STATUS_STEPS.slice(0, -1) : WITHDRAWAL_STATUS_STEPS;
case 'Proven':
return WITHDRAWAL_STATUS_ORDER_PROVEN;
case 'Waiting a game to resolve':
return WITHDRAWAL_STATUS_ORDER_GAME;
case 'Relayed': {
if (l1TxHash) {
if (data.l1_transaction_hash) {
return WITHDRAWAL_STATUS_STEPS.map((status) => {
return status === 'Relayed' ? {
content: <TxEntityL1 hash={ l1TxHash } truncation="constant" text="Relayed" noIcon noCopy/>,
content: <TxEntityL1 hash={ data.l1_transaction_hash } truncation="constant" text="Relayed" noIcon noCopy/>,
label: status,
} : status;
});
@@ -72,28 +109,11 @@ const TxDetailsWithdrawalStatusOptimistic = ({ status, l1TxHash }: Props) => {
}
})();
const rightSlot = hasClaimButton ? (
<Link
href={ rollupFeature.L2WithdrawalUrl }
external
noIcon
>
<Button
variant="outline"
size="sm"
>
Claim funds
</Button>
</Link>
) : null;
return (
<VerificationSteps
steps={ steps as unknown as Array<OptimisticL2WithdrawalStatus> }
currentStep={ status }
currentStep={ data.status }
rightSlot={ rightSlot }
my={ hasClaimButton ? '-6px' : 0 }
lineHeight={ hasClaimButton ? '32px' : undefined }
/>
);
};
+2 -5
View File
@@ -228,14 +228,11 @@ const TxInfo = ({ data, tacOperations, isLoading, socketStatus }: Props) => {
<Flex flexDir="column" rowGap={ 2 }>
{ data.op_withdrawals.map((withdrawal) => (
<Box key={ withdrawal.nonce }>
<Box mb={ 2 }>
<Box mb={ 2 } py={{ base: '5px', lg: 1 }}>
<span>Nonce: </span>
<chakra.span fontWeight={ 600 }>{ withdrawal.nonce }</chakra.span>
</Box>
<TxDetailsWithdrawalStatusOptimistic
status={ withdrawal.status }
l1TxHash={ withdrawal.l1_transaction_hash }
/>
<TxDetailsWithdrawalStatusOptimistic data={ withdrawal } txHash={ data.hash } from={ data.from }/>
</Box>
)) }
</Flex>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

@@ -0,0 +1,86 @@
import React from 'react';
import type { AddressParam } from 'types/api/addressParams';
import type { OptimisticL2WithdrawalClaimInfo } from 'types/api/optimisticL2';
import config from 'configs/app';
import { parentChain } from 'lib/web3/chains';
import { Button } from 'toolkit/chakra/button';
import { Link } from 'toolkit/chakra/link';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import OptimisticL2ClaimModal from './OptimisticL2ClaimModal';
const rollupFeature = config.features.rollup;
export const canClaimDirectlyGuard = (data: OptimisticL2WithdrawalClaimInfo) => {
return (
config.features.blockchainInteraction.isEnabled &&
Boolean(parentChain) &&
data.portal_contract_address_hash !== null &&
data.msg_sender_address_hash !== null &&
data.msg_target_address_hash !== null &&
data.msg_data !== null &&
data.msg_gas_limit !== null &&
data.msg_nonce_raw !== null &&
data.msg_value !== null
);
};
interface Props {
data: OptimisticL2WithdrawalClaimInfo;
from: AddressParam | null;
onSuccess: (txHash: string) => void;
source: 'list' | 'tx';
}
const OptimisticL2ClaimButton = ({ data, from, onSuccess, source }: Props) => {
const modal = useDisclosure();
if (canClaimDirectlyGuard(data)) {
return (
<>
{ modal.open && (
<OptimisticL2ClaimModal
data={ data }
onOpenChange={ modal.onOpenChange }
proofSubmitterAddress={ from?.hash }
onSuccess={ onSuccess }
/>
) }
<Button variant="outline" size="sm" onClick={ modal.onOpen }>Claim</Button>
</>
);
}
if (!rollupFeature.isEnabled || !rollupFeature.L2WithdrawalUrl) {
if (source === 'list') {
return 'Ready for relay';
}
return null;
}
if (source === 'list') {
return (
<Link
href={ rollupFeature.L2WithdrawalUrl }
external
>
Ready for relay
</Link>
);
}
return (
<Link
href={ rollupFeature.L2WithdrawalUrl }
external
noIcon
>
<Button variant="outline" size="sm">Claim</Button>
</Link>
);
};
export default React.memo(OptimisticL2ClaimButton);
@@ -0,0 +1,213 @@
import { chakra } from '@chakra-ui/react';
import React from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { FormProvider, useForm } from 'react-hook-form';
import type { Abi } from 'viem';
import { useSwitchChain, useWaitForTransactionReceipt, useWalletClient } from 'wagmi';
import type { OptimisticL2WithdrawalClaimInfo } from 'types/api/optimisticL2';
import config from 'configs/app';
import capitalizeFirstLetter from 'lib/capitalizeFirstLetter';
import getErrorMessage from 'lib/errors/getErrorMessage';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import getErrorProp from 'lib/errors/getErrorProp';
import useWeb3Wallet from 'lib/web3/useWallet';
import { Button } from 'toolkit/chakra/button';
import { DialogBody, DialogContent, DialogHeader, DialogRoot } from 'toolkit/chakra/dialog';
import { toaster } from 'toolkit/chakra/toaster';
import { FormFieldAddress } from 'toolkit/components/forms/fields/FormFieldAddress';
const rollupFeature = config.features.rollup;
const parentChain = rollupFeature.isEnabled ? rollupFeature.parentChain : undefined;
const FINALIZE_WITHDRAWAL_ABI = {
inputs: [
{
components: [
{ internalType: 'uint256', name: 'nonce', type: 'uint256' },
{ internalType: 'address', name: 'sender', type: 'address' },
{ internalType: 'address', name: 'target', type: 'address' },
{ internalType: 'uint256', name: 'value', type: 'uint256' },
{ internalType: 'uint256', name: 'gasLimit', type: 'uint256' },
{ internalType: 'bytes', name: 'data', type: 'bytes' },
],
internalType: 'struct Types.WithdrawalTransaction',
name: '_tx',
type: 'tuple',
},
{ internalType: 'address', name: '_proofSubmitter', type: 'address' },
],
name: 'finalizeWithdrawalTransactionExternalProof',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
};
interface FormFields {
address: string;
}
interface Props {
data: OptimisticL2WithdrawalClaimInfo;
onOpenChange: ({ open }: { open: boolean }) => void;
proofSubmitterAddress?: string;
onSuccess: (txHash: string) => void;
}
const OptimisticL2ClaimModal = ({ data, onOpenChange, proofSubmitterAddress, onSuccess }: Props) => {
const [ txHash, setTxHash ] = React.useState<`0x${ string }` | undefined>(undefined);
const { connect: connectWeb3Wallet, isConnected: isWeb3WalletConnected, isOpen: isWeb3WalletOpen } = useWeb3Wallet({ source: 'Smart contracts' });
const { switchChainAsync } = useSwitchChain();
const { data: walletClient } = useWalletClient({ chainId: parentChain?.id ? Number(parentChain.id) : undefined });
const { status: txStatus, error: txError, isLoading: isTxPending } = useWaitForTransactionReceipt({
hash: txHash,
chainId: parentChain?.id ? Number(parentChain.id) : undefined,
query: { enabled: Boolean(txHash) },
});
const formApi = useForm<FormFields>({
mode: 'onBlur',
defaultValues: {
address: proofSubmitterAddress,
},
});
const showErrorToast = React.useCallback((error: unknown) => {
const apiError = getErrorObjPayload<{ message: string }>(error);
const message = capitalizeFirstLetter(apiError?.message || getErrorProp(error, 'shortMessage') || getErrorMessage(error) || 'Something went wrong');
toaster.error({
title: 'Error',
description: message,
});
}, []);
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(formData) => {
try {
if (!rollupFeature.isEnabled || !parentChain) {
throw new Error('Feature is not enabled');
}
await switchChainAsync({ chainId: Number(parentChain.id) });
if (!walletClient) {
throw new Error('Wallet Client is not defined');
}
if (
data.portal_contract_address_hash === null ||
data.msg_sender_address_hash === null ||
data.msg_target_address_hash === null ||
data.msg_data === null ||
data.msg_gas_limit === null ||
data.msg_nonce_raw === null ||
data.msg_value === null
) {
throw new Error('Data is not valid');
}
const args = [
[
data.msg_nonce_raw,
data.msg_sender_address_hash,
data.msg_target_address_hash,
data.msg_value,
data.msg_gas_limit,
data.msg_data,
],
formData.address,
];
const hash = await walletClient.writeContract({
args,
abi: [ FINALIZE_WITHDRAWAL_ABI ] as Abi,
functionName: FINALIZE_WITHDRAWAL_ABI.name,
address: data.portal_contract_address_hash as `0x${ string }`,
});
setTxHash(hash);
} catch (error) {
showErrorToast(error);
}
}, [ walletClient, switchChainAsync, data, showErrorToast ]);
React.useEffect(() => {
if (!txHash) {
return;
}
switch (txStatus) {
case 'success': {
onOpenChange({ open: false });
onSuccess(txHash);
break;
}
case 'error': {
showErrorToast(txError);
break;
}
}
}, [ onSuccess, txStatus, showErrorToast, txError, txHash, onOpenChange ]);
return (
<DialogRoot
open
onOpenChange={ onOpenChange }
size={{ lgDown: 'full', lg: 'md' }}
trapFocus={ false }
preventScroll={ false }
modal={ false }
closeOnInteractOutside={ false }
>
<DialogContent>
<DialogHeader>
Claim your withdrawal
</DialogHeader>
<DialogBody>
<FormProvider { ...formApi }>
<chakra.form
noValidate
onSubmit={ formApi.handleSubmit(onFormSubmit) }
>
<p>Confirm the proof submitter address is correct to claim</p>
<FormFieldAddress<FormFields>
name="address"
required
placeholder="Address (0x...)"
bgColor="dialog.bg"
mt={ 6 }
/>
{ isWeb3WalletConnected ? (
<Button
mt={ 6 }
type="submit"
disabled={ formApi.formState.isSubmitting || isTxPending }
loading={ formApi.formState.isSubmitting || isTxPending }
loadingText="Claim"
>
Claim
</Button>
) : (
<Button
mt={ 6 }
onClick={ connectWeb3Wallet }
disabled={ isWeb3WalletOpen }
loading={ isWeb3WalletOpen }
loadingText="Connect wallet"
>
Connect wallet
</Button>
) }
</chakra.form>
</FormProvider>
</DialogBody>
</DialogContent>
</DialogRoot>
);
};
export default React.memo(OptimisticL2ClaimModal);
@@ -0,0 +1,56 @@
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import type { OptimisticL2WithdrawalsItem, OptimisticL2WithdrawalsResponse } from 'types/api/optimisticL2';
import { getResourceKey } from 'lib/api/useApiQuery';
import { Skeleton } from 'toolkit/chakra/skeleton';
import OptimisticL2ClaimButton from './OptimisticL2ClaimButton';
interface Props {
data: OptimisticL2WithdrawalsItem;
isLoading?: boolean;
}
const OptimisticL2WithdrawalsItemStatus = ({ data, isLoading }: Props) => {
const queryClient = useQueryClient();
const handleClaimSuccess = React.useCallback((l1TxHash: string) => {
queryClient.setQueriesData({
queryKey: getResourceKey('general:optimistic_l2_withdrawals'),
exact: false,
type: 'active',
},
(prevData: OptimisticL2WithdrawalsResponse | undefined) => {
if (!prevData) {
return;
}
const newItems = prevData.items.map((withdrawal) => {
if (`${ withdrawal.msg_nonce_version }-${ withdrawal.msg_nonce }` === `${ data.msg_nonce_version }-${ data.msg_nonce }`) {
return {
...withdrawal,
l1_transaction_hash: l1TxHash,
status: 'Relayed',
};
}
return withdrawal;
});
return {
...prevData,
items: newItems,
};
});
}, [ data.msg_nonce, data.msg_nonce_version, queryClient ]);
if (data.status !== 'Ready for relay') {
return <Skeleton loading={ isLoading } display="inline-block">{ data.status }</Skeleton>;
}
return <OptimisticL2ClaimButton data={ data } from={ data.from } onSuccess={ handleClaimSuccess } source="list"/>;
};
export default React.memo(OptimisticL2WithdrawalsItemStatus);
@@ -4,7 +4,6 @@ import type { OptimisticL2WithdrawalsItem } from 'types/api/optimisticL2';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
@@ -12,6 +11,8 @@ import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip';
import OptimisticL2WithdrawalsItemStatus from './OptimisticL2WithdrawalsItemStatus';
const rollupFeature = config.features.rollup;
type Props = { item: OptimisticL2WithdrawalsItem; isLoading?: boolean };
@@ -70,9 +71,7 @@ const OptimisticL2WithdrawalsListItem = ({ item, isLoading }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>Status</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ item.status === 'Ready for relay' && rollupFeature.L2WithdrawalUrl ?
<Link external href={ rollupFeature.L2WithdrawalUrl }>{ item.status }</Link> :
<Skeleton loading={ isLoading } display="inline-block">{ item.status }</Skeleton> }
<OptimisticL2WithdrawalsItemStatus data={ item } isLoading={ isLoading }/>
</ListItemMobileGrid.Value>
{ item.l1_transaction_hash && (
@@ -4,7 +4,6 @@ import type { OptimisticL2WithdrawalsItem } from 'types/api/optimisticL2';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { TableCell, TableRow } from 'toolkit/chakra/table';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
@@ -12,6 +11,8 @@ import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import TimeWithTooltip from 'ui/shared/time/TimeWithTooltip';
import OptimisticL2WithdrawalsItemStatus from './OptimisticL2WithdrawalsItemStatus';
const rollupFeature = config.features.rollup;
type Props = { item: OptimisticL2WithdrawalsItem; isLoading?: boolean };
@@ -55,10 +56,7 @@ const OptimisticL2WithdrawalsTableItem = ({ item, isLoading }: Props) => {
/>
</TableCell>
<TableCell verticalAlign="middle">
{ item.status === 'Ready for relay' && rollupFeature.L2WithdrawalUrl ?
<Link external href={ rollupFeature.L2WithdrawalUrl }>{ item.status }</Link> :
<Skeleton loading={ isLoading } display="inline-block">{ item.status }</Skeleton>
}
<OptimisticL2WithdrawalsItemStatus data={ item } isLoading={ isLoading }/>
</TableCell>
<TableCell verticalAlign="middle">
{ item.l1_transaction_hash ? (