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
@@ -23,7 +23,7 @@ const parentChain: ParentChain | undefined = (() => {
|
||||
|
||||
return {
|
||||
...envValue,
|
||||
baseUrl: baseUrl ?? envValue?.baseUrl,
|
||||
baseUrl: baseUrl || envValue?.baseUrl || '',
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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' ],
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 47 KiB |
|
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 }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
Before Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.6 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 ? (
|
||||
|
||||