Commit b775d9ef authored by tom goriunov's avatar tom goriunov Committed by GitHub

Arbitrum messages claiming (#2533)

* txn withdrawals page stub

* add base page layout

* add ConnectWallet alert

* save search term in page query param

* add links to the new page

* add functionality to claim button

* combine "Connect wallet" and "Claim" buttons

* Refactor rollup configuration to introduce ParentChain structure and deprecate legacy environment variables

* handle txn claim

* allow to pass array of RPC urls for parent chain config

* display token value and completion tx hash

* fix csp

* write tests for arbitrum l2 txn withdrawals

* fix ts

* review fixes

* update screenshots and hide button if the txn is not confirmed yet
parent b88ebe49
import type { Feature } from './types';
import type { RollupType } from 'types/client/rollup';
import type { ParentChain, RollupType } from 'types/client/rollup';
import { ROLLUP_TYPES } from 'types/client/rollup';
import stripTrailingSlash from 'lib/stripTrailingSlash';
import { getEnvValue } from '../utils';
import { getEnvValue, parseEnvJson } from '../utils';
const type = (() => {
const envValue = getEnvValue('NEXT_PUBLIC_ROLLUP_TYPE');
return ROLLUP_TYPES.find((type) => type === envValue);
})();
const L1BaseUrl = getEnvValue('NEXT_PUBLIC_ROLLUP_L1_BASE_URL');
const L2WithdrawalUrl = getEnvValue('NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL');
const parentChain: ParentChain | undefined = (() => {
const envValue = parseEnvJson<ParentChain>(getEnvValue('NEXT_PUBLIC_ROLLUP_PARENT_CHAIN'));
const baseUrl = stripTrailingSlash(getEnvValue('NEXT_PUBLIC_ROLLUP_L1_BASE_URL') || '');
const chainName = getEnvValue('NEXT_PUBLIC_ROLLUP_PARENT_CHAIN_NAME');
if (!baseUrl && !envValue?.baseUrl) {
return;
}
return {
...envValue,
name: chainName ?? envValue?.name,
baseUrl: baseUrl ?? envValue?.baseUrl,
};
})();
const title = 'Rollup (L2) chain';
const config: Feature<{
type: RollupType;
L1BaseUrl: string;
homepage: { showLatestBlocks: boolean };
outputRootsEnabled: boolean;
L2WithdrawalUrl: string | undefined;
parentChainName: string | undefined;
parentChain: ParentChain;
DA: {
celestia: {
namespace: string | undefined;
};
};
}> = (() => {
if (type && L1BaseUrl) {
if (type && parentChain) {
return Object.freeze({
title,
isEnabled: true,
type,
L1BaseUrl: stripTrailingSlash(L1BaseUrl),
L2WithdrawalUrl: type === 'optimistic' ? L2WithdrawalUrl : undefined,
outputRootsEnabled: type === 'optimistic' && getEnvValue('NEXT_PUBLIC_ROLLUP_OUTPUT_ROOTS_ENABLED') === 'true',
parentChainName: type === 'arbitrum' ? getEnvValue('NEXT_PUBLIC_ROLLUP_PARENT_CHAIN_NAME') : undefined,
homepage: {
showLatestBlocks: getEnvValue('NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS') === 'true',
},
parentChain,
DA: {
celestia: {
namespace: type === 'arbitrum' ? getEnvValue('NEXT_PUBLIC_ROLLUP_DA_CELESTIA_NAMESPACE') : undefined,
......
......@@ -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={'baseUrl':'https://eth.blockscout.com','currency':{'name':'Ether','symbol':'ETH','decimals':18},'isTestnet':false,'id':1,'name':'Ethereum mainnet','rpcUrls':['https://eth.drpc.org']}
# Instance ENVs
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
......@@ -16,13 +18,27 @@ NEXT_PUBLIC_API_HOST=arbitrum.blockscout.com
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'swoop-exchange'},{'text':'Disperse','icon':'txn_batches_slim','dappId':'smol'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'},{'text':'Get gas','icon':'gas','dappId':'smol-refuel'}]
NEXT_PUBLIC_DEX_POOLS_ENABLED=true
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/arbitrum-one.json
NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x37c798810d49ba132b40efe7f4fdf6806a8fc58226bb5e185ddc91f896577abf
NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=true
NEXT_PUBLIC_HAS_USER_OPS=true
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(27, 74, 221, 1)
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(27, 74, 221, 1)']}
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_LOGOUT_URL=https://blockscout-arbitrum.us.auth0.com/v2/logout
NEXT_PUBLIC_MARKETPLACE_ENABLED=false
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=<p>Joined recent campaigns? Mint your Merit Badge <a href="https://badges.blockscout.com?utm_source=instance&utm_medium=arbitrum">here</a></p>
NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maikReal/974c47f86a3158c1a86b092ae2f044b3/raw/abcc7e02150cd85d4974503a0357162c0a2c35a9/merits-banner.html
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://swap.blockscout.com?utm_source=blockscout&utm_medium=arbitrum
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG=[{'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}]
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/pools']
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
......@@ -33,12 +49,15 @@ NEXT_PUBLIC_NETWORK_ID=42161
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/arbitrum-one-logo-light.svg
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/arbitrum-one-logo-dark.svg
NEXT_PUBLIC_NETWORK_NAME=Arbitrum One
NEXT_PUBLIC_NETWORK_RPC_URL=https://arbitrum.llamarpc.com
NEXT_PUBLIC_NETWORK_RPC_URL=https://arbitrum-one.publicnode.com
NEXT_PUBLIC_NETWORK_SHORT_NAME=Arbitrum One
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/arbitrum-one.png
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com
NEXT_PUBLIC_ROLLUP_PARENT_CHAIN_NAME=Ethereum
NEXT_PUBLIC_ROLLUP_TYPE=arbitrum
NEXT_PUBLIC_STATS_API_HOST=https://stats-arbitrum-one-nitro.k8s-prod-2.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
\ No newline at end of file
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
\ No newline at end of file
......@@ -41,7 +41,8 @@ NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/arbitrum-sepolia.png
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth-sepolia.blockscout.com
NEXT_PUBLIC_ROLLUP_TYPE=arbitrum
NEXT_PUBLIC_ROLLUP_PARENT_CHAIN={'baseUrl':'https://eth-sepolia.blockscout.com','currency':{'name':'Ether','symbol':'ETH','decimals':18},'isTestnet':true,'id':11155111,'name':'Sepolia','rpcUrls':['https://eth-sepolia.public.blastapi.io']}
NEXT_PUBLIC_SENTRY_DSN=https://fdcd971162e04694bf03564c5be3d291@o1222505.ingest.sentry.io/4503902500421632
NEXT_PUBLIC_STATS_API_HOST=https://stats-arbitrum-sepolia.k8s-prod-2.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
\ No newline at end of file
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
......@@ -157,6 +157,16 @@ function printDeprecationWarning(envsMap: Record<string, string>) {
console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗\n');
}
if (
envsMap.NEXT_PUBLIC_ROLLUP_PARENT_CHAIN_NAME ||
envsMap.NEXT_PUBLIC_ROLLUP_L1_BASE_URL
) {
console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗');
// eslint-disable-next-line max-len
console.warn('The NEXT_PUBLIC_ROLLUP_L1_BASE_URL and NEXT_PUBLIC_ROLLUP_PARENT_CHAIN_NAME variables are now deprecated and will be removed in the next release. Please migrate to the NEXT_PUBLIC_ROLLUP_PARENT_CHAIN variable.');
console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗\n');
}
if (
envsMap.NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR ||
envsMap.NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND
......
......@@ -70,6 +70,14 @@ const urlTest: yup.TestConfig = {
exclusive: true,
};
const getYupValidationErrorMessage = (error: unknown) =>
typeof error === 'object' &&
error !== null &&
'errors' in error &&
Array.isArray(error.errors) ?
error.errors.join(', ') :
'';
const marketplaceAppSchema: yup.ObjectSchema<MarketplaceAppOverview> = yup
.object({
id: yup.string().required(),
......@@ -270,6 +278,44 @@ const beaconChainSchema = yup
}),
});
const parentChainCurrencySchema = yup
.object()
.shape({
name: yup.string().required(),
symbol: yup.string().required(),
decimals: yup.number().required(),
});
const parentChainSchema = yup
.object()
.transform(replaceQuotes)
.json()
.shape({
id: yup.number(),
name: yup.string(),
baseUrl: yup.string().test(urlTest).required(),
rpcUrls: yup.array().of(yup.string().test(urlTest)),
currency: yup
.mixed()
.test(
'shape',
(ctx) => {
try {
parentChainCurrencySchema.validateSync(ctx.originalValue);
throw new Error('Unknown validation error');
} catch (error: unknown) {
const message = getYupValidationErrorMessage(error);
return 'in \"currency\" property ' + (message ? `${ message }` : '');
}
},
(data) => {
const isUndefined = data === undefined;
return isUndefined || parentChainCurrencySchema.isValidSync(data);
},
),
isTestnet: yup.boolean(),
});
const rollupSchema = yup
.object()
.shape({
......@@ -321,6 +367,34 @@ const rollupSchema = yup
value => value === undefined,
),
}),
NEXT_PUBLIC_ROLLUP_PARENT_CHAIN: yup
.mixed()
.when('NEXT_PUBLIC_ROLLUP_TYPE', {
is: (value: string) => value,
then: (schema) => {
return schema.test(
'shape',
(ctx) => {
try {
parentChainSchema.validateSync(ctx.originalValue);
throw new Error('Unknown validation error');
} catch (error: unknown) {
const message = getYupValidationErrorMessage(error);
return 'Invalid schema were provided for NEXT_PUBLIC_ROLLUP_TYPE' + (message ? `: ${ message }` : '');
}
},
(data) => {
const isUndefined = data === undefined;
return isUndefined || parentChainSchema.isValidSync(data);
}
)
},
otherwise: (schema) => schema.test(
'not-exist',
'NEXT_PUBLIC_ROLLUP_PARENT_CHAIN cannot not be used if NEXT_PUBLIC_ROLLUP_TYPE is not defined',
value => value === undefined,
),
}),
NEXT_PUBLIC_ROLLUP_DA_CELESTIA_NAMESPACE: yup
.string()
.min(60)
......@@ -662,7 +736,7 @@ const schema = yup
heroBannerSchema.validateSync(ctx.originalValue);
throw new Error('Unknown validation error');
} catch (error: unknown) {
const message = typeof error === 'object' && error !== null && 'errors' in error && Array.isArray(error.errors) ? error.errors.join(', ') : '';
const message = getYupValidationErrorMessage(error);
return 'Invalid schema were provided for NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG' + (message ? `: ${ message }` : '');
}
},
......
......@@ -2,4 +2,6 @@ NEXT_PUBLIC_ROLLUP_TYPE=arbitrum
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://example.com
NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS=true
NEXT_PUBLIC_ROLLUP_PARENT_CHAIN_NAME=DuckChain
NEXT_PUBLIC_ROLLUP_PARENT_CHAIN={'baseUrl':'https://explorer.duckchain.io','currency':{'name':'Quack','symbol':'QUACK','decimals':18},'isTestnet':true,'id':42,'name':'DuckChain','rpcUrls':['https://rpc.duckchain.io']}
NEXT_PUBLIC_ROLLUP_DA_CELESTIA_NAMESPACE=0x00000000000000000000000000000000000000ca1de12a9905be97beaf
\ No newline at end of file
......@@ -3,4 +3,5 @@ NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://example.com
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://example.com
NEXT_PUBLIC_FAULT_PROOF_ENABLED=true
NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS=true
NEXT_PUBLIC_ROLLUP_OUTPUT_ROOTS_ENABLED=false
\ No newline at end of file
NEXT_PUBLIC_ROLLUP_OUTPUT_ROOTS_ENABLED=false
NEXT_PUBLIC_ROLLUP_PARENT_CHAIN={'baseUrl':'https://explorer.duckchain.io'}
......@@ -450,15 +450,28 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_ROLLUP_TYPE | `'optimistic' \| 'arbitrum' \| 'shibarium' \| 'zkEvm' \| 'zkSync' \| 'scroll'` | Rollup chain type | Required | - | `'optimistic'` | v1.24.0+ |
| NEXT_PUBLIC_ROLLUP_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` | v1.24.0+ |
| NEXT_PUBLIC_ROLLUP_L1_BASE_URL | `string` | Blockscout base URL for L1 network. **DEPRECATED** _Use `NEXT_PUBLIC_ROLLUP_PARENT_CHAIN` instead_ | Required | - | `'http://eth-goerli.blockscout.com'` | v1.24.0+ |
| NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals (Optimistic stack only) | Required for `optimistic` rollups | - | `https://app.optimism.io/bridge/withdraw` | v1.24.0+ |
| NEXT_PUBLIC_FAULT_PROOF_ENABLED | `boolean` | Set to `true` for chains with fault proof system enabled (Optimistic stack only) | - | - | `true` | v1.31.0+ |
| NEXT_PUBLIC_HAS_MUD_FRAMEWORK | `boolean` | Set to `true` for instances that use MUD framework (Optimistic stack only) | - | - | `true` | v1.33.0+ |
| NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS | `boolean` | Set to `true` to display "Latest blocks" widget instead of "Latest batches" on the home page | - | - | `true` | v1.36.0+ |
| NEXT_PUBLIC_ROLLUP_OUTPUT_ROOTS_ENABLED | `boolean` | Enables "Output roots" page (Optimistic stack only) | - | `false` | `true` | v1.37.0+ |
| NEXT_PUBLIC_ROLLUP_PARENT_CHAIN_NAME | `string` | Set to customize L1 transaction status labels in the UI (e.g., "Sent to <chain-name>"). This setting is applicable only for Arbitrum-based chains. | - | - | `DuckChain` | v1.37.0+ |
| NEXT_PUBLIC_ROLLUP_PARENT_CHAIN_NAME | `string` | Set to customize L1 transaction status labels in the UI (e.g., "Sent to <chain-name>"). This setting is applicable only for Arbitrum-based chains. **DEPRECATED** _Use `NEXT_PUBLIC_ROLLUP_PARENT_CHAIN` instead_ | - | - | `DuckChain` | v1.37.0+ |
| NEXT_PUBLIC_ROLLUP_PARENT_CHAIN | `ParentChain`, see details [below](#parent-chain-configuration-properties) | Configuration parameters for the parent chain. | - | - | `{'baseUrl':'https://explorer.duckchain.io'}` | v1.38.0+ |
| NEXT_PUBLIC_ROLLUP_DA_CELESTIA_NAMESPACE | `string` | Hex-string for creating a link to the transaction batch on the Seleneium explorer. "0x"-format and 60 symbol length. Available only for Arbitrum roll-ups. | - | - | `0x00000000000000000000000000000000000000ca1de12a9905be97beaf` | v1.38.0+ |
#### Parent chain configuration properties
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| id | `number` | Chain id, see [https://chainlist.org](https://chainlist.org) for the reference. | - | - | `42` |
| name | `string` | Displayed name of the chain. Set to customize L1 transaction status labels in the UI (e.g., "Sent to <chain-name>"). Currently, this setting is applicable only for Arbitrum-based chains. | - | - | `DuckChain` |
| baseUrl | `string` | Base url of the chain explorer. | Required | - | `https://explorer.duckchain.io` |
| rpcUrls | `Array<string>` | Chain public RPC server urls, see [https://chainlist.org](https://chainlist.org) for the reference. | - | - | `['https://rpc.duckchain.io']` |
| currency | `{ name: string; symbol: string; decimals: number; }` | Chain currency config. | - | - | `{ name: Quack, symbol: QUA, decimals: 18 }` |
| isTestnet | `boolean` | Set to true if network is testnet. | - | - | `true` |
&nbsp;
### Export data to CSV file
......
......@@ -51,6 +51,8 @@ import type {
ArbitrumL2BatchBlocks,
ArbitrumL2TxnBatchesItem,
ArbitrumLatestDepositsResponse,
ArbitrumL2TxnWithdrawalsResponse,
ArbitrumL2MessageClaimResponse,
} from 'types/api/arbitrumL2';
import type { TxBlobs, Blob } from 'types/api/blobs';
import type {
......@@ -928,6 +930,18 @@ export const RESOURCES = {
filterFields: [],
},
arbitrum_l2_txn_withdrawals: {
path: '/api/v2/arbitrum/messages/withdrawals/:hash',
pathParams: [ 'hash' as const ],
filterFields: [],
},
arbitrum_l2_message_claim: {
path: '/api/v2/arbitrum/messages/claim/:id',
pathParams: [ 'id' as const ],
filterFields: [],
},
// zkEvm L2
zkevm_l2_deposits: {
path: '/api/v2/zkevm/deposits',
......@@ -1400,6 +1414,8 @@ Q extends 'arbitrum_l2_txn_batch' ? ArbitrumL2TxnBatch :
Q extends 'arbitrum_l2_txn_batch_celestia' ? ArbitrumL2TxnBatch :
Q extends 'arbitrum_l2_txn_batch_txs' ? ArbitrumL2BatchTxs :
Q extends 'arbitrum_l2_txn_batch_blocks' ? ArbitrumL2BatchBlocks :
Q extends 'arbitrum_l2_txn_withdrawals' ? ArbitrumL2TxnWithdrawalsResponse :
Q extends 'arbitrum_l2_message_claim' ? ArbitrumL2MessageClaimResponse :
Q extends 'zkevm_l2_deposits' ? ZkEvmL2DepositsResponse :
Q extends 'zkevm_l2_deposits_count' ? number :
Q extends 'zkevm_l2_withdrawals' ? ZkEvmL2WithdrawalsResponse :
......
import getErrorObj from './getErrorObj';
export default function getErrorProp<T extends unknown>(error: unknown, prop: string): T | undefined {
const errorObj = getErrorObj(error);
return errorObj && prop in errorObj ?
(errorObj[prop as keyof typeof errorObj] as T) :
undefined;
}
......@@ -246,6 +246,11 @@ export default function useNavItems(): ReturnType {
nextRoute: { pathname: '/public-tags/submit' as const },
isActive: pathname.startsWith('/public-tags/submit'),
},
rollupFeature.isEnabled && rollupFeature.type === 'arbitrum' && {
text: 'Txn withdrawals',
nextRoute: { pathname: '/txn-withdrawals' as const },
isActive: pathname.startsWith('/txn-withdrawals'),
},
...config.UI.navigation.otherLinks,
].filter(Boolean);
......
......@@ -36,6 +36,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/account/verified-addresses': 'Root page',
'/public-tags/submit': 'Regular page',
'/withdrawals': 'Root page',
'/txn-withdrawals': 'Root page',
'/visualize/sol2uml': 'Regular page',
'/csv-export': 'Regular page',
'/deposits': 'Root page',
......
......@@ -39,6 +39,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/account/verified-addresses': DEFAULT_TEMPLATE,
'/public-tags/submit': 'Propose a new public tag for your address, contract or set of contracts for your dApp. Our team will review and approve your submission. Public tags are incredible tool which helps users identify contracts and addresses.',
'/withdrawals': DEFAULT_TEMPLATE,
'/txn-withdrawals': DEFAULT_TEMPLATE,
'/visualize/sol2uml': DEFAULT_TEMPLATE,
'/csv-export': DEFAULT_TEMPLATE,
'/deposits': DEFAULT_TEMPLATE,
......
......@@ -36,6 +36,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/account/verified-addresses': '%network_name% - my verified addresses',
'/public-tags/submit': '%network_name% - public tag requests',
'/withdrawals': '%network_name% withdrawals - track on %network_name% explorer',
'/txn-withdrawals': '%network_name% L2 to L1 message relayer',
'/visualize/sol2uml': '%network_name% Solidity UML diagram',
'/csv-export': '%network_name% export data to CSV',
'/deposits': '%network_name% deposits (L1 > L2)',
......
......@@ -34,6 +34,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/account/verified-addresses': 'Verified addresses',
'/public-tags/submit': 'Submit public tag',
'/withdrawals': 'Withdrawals',
'/txn-withdrawals': 'Txn withdrawals',
'/visualize/sol2uml': 'Solidity UML diagram',
'/csv-export': 'Export data to CSV file',
'/deposits': 'Deposits (L1 > L2)',
......
......@@ -10,11 +10,13 @@ type Args = {
confirmation_transaction: ArbitrumL2TxData;
};
const parentChainName = rollupFeature.isEnabled ? rollupFeature.parentChain.name : undefined;
export const VERIFICATION_STEPS_MAP: Record<ArbitrumBatchStatus, string> = {
'Processed on rollup': 'Processed on rollup',
'Sent to base': rollupFeature.isEnabled && rollupFeature.parentChainName ? `Sent to ${ rollupFeature.parentChainName }` : 'Sent to parent chain',
'Confirmed on base': rollupFeature.isEnabled && rollupFeature.parentChainName ?
`Confirmed on ${ rollupFeature.parentChainName }` :
'Sent to base': parentChainName ? `Sent to ${ parentChainName }` : 'Sent to parent chain',
'Confirmed on base': parentChainName ?
`Confirmed on ${ parentChainName }` :
'Confirmed on parent chain',
};
......
/* eslint-disable max-len */
import type { ArbitrumMessageStatus } from 'types/api/transaction';
export const MESSAGE_DESCRIPTIONS: Record<ArbitrumMessageStatus, string> = {
'Syncing with base layer': 'The incoming message was discovered on the rollup, but the corresponding message on L1 has not yet been found',
'Settlement pending': 'The transaction with the message was included in a rollup block, but there is no batch on L1 containing the block yet',
'Waiting for confirmation': 'The rollup block with the transaction containing the message was included in a batch on L1, but it is still waiting for the expiration of the fraud proof countdown',
'Ready for relay': 'The rollup state was confirmed successfully, and the message can be executed—funds can be claimed on L1',
Relayed: '',
};
......@@ -2,7 +2,7 @@ import { type Chain } from 'viem';
import config from 'configs/app';
const currentChain = {
export const currentChain: Chain = {
id: Number(config.chain.id),
name: config.chain.name ?? '',
nativeCurrency: {
......@@ -22,6 +22,36 @@ const currentChain = {
},
},
testnet: config.chain.isTestnet,
} as const satisfies Chain;
};
export default currentChain;
export const parentChain: Chain | undefined = (() => {
const rollupFeature = config.features.rollup;
const parentChain = rollupFeature.isEnabled && rollupFeature.parentChain;
if (!parentChain) {
return;
}
if (!parentChain.id || !parentChain.name || !parentChain.rpcUrls || !parentChain.baseUrl || !parentChain.currency) {
return;
}
return {
id: parentChain.id,
name: parentChain.name,
nativeCurrency: parentChain.currency,
rpcUrls: {
'default': {
http: parentChain.rpcUrls,
},
},
blockExplorers: {
'default': {
name: 'Blockscout',
url: parentChain.baseUrl,
},
},
testnet: parentChain.isTestnet,
};
})();
import { createPublicClient, http } from 'viem';
import currentChain from './currentChain';
import { currentChain } from './chains';
export const publicClient = (() => {
if (currentChain.rpcUrls.default.http.filter(Boolean).length === 0) {
......
......@@ -7,9 +7,10 @@ import useAccount from 'lib/web3/useAccount';
interface Params {
source: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source'];
onConnect?: () => void;
}
export default function useWeb3Wallet({ source }: Params) {
export default function useWeb3Wallet({ source, onConnect }: Params) {
const { open: openModal } = useAppKit();
const { open: isOpen } = useAppKitState();
const { disconnect } = useDisconnect();
......@@ -35,9 +36,10 @@ export default function useWeb3Wallet({ source }: Params) {
mixpanel.userProfile.setOnce({
'With Connected Wallet': true,
});
onConnect?.();
}
isConnectionStarted.current = false;
}, [ source ]);
}, [ source, onConnect ]);
const handleDisconnect = React.useCallback(() => {
disconnect();
......
import { WagmiAdapter } from '@reown/appkit-adapter-wagmi';
import type { AppKitNetwork } from '@reown/appkit/networks';
import type { Chain } from 'viem';
import { fallback, http } from 'viem';
import { createConfig } from 'wagmi';
import config from 'configs/app';
import currentChain from 'lib/web3/currentChain';
import { currentChain, parentChain } from 'lib/web3/chains';
const feature = config.features.blockchainInteraction;
const chains = [ currentChain, parentChain ].filter(Boolean);
const wagmi = (() => {
const chains = [ currentChain ];
if (!feature.isEnabled) {
const wagmiConfig = createConfig({
chains: [ currentChain ],
chains: chains as [Chain, ...Array<Chain>],
transports: {
[currentChain.id]: fallback(
config.chain.rpcUrls
.map((url) => http(url))
.concat(http(`${ config.api.endpoint }/api/eth-rpc`)),
),
...(parentChain ? { [parentChain.id]: http(parentChain.rpcUrls.default.http[0]) } : {}),
},
ssr: true,
batch: { multicall: { wait: 100 } },
......@@ -27,10 +32,11 @@ const wagmi = (() => {
}
const wagmiAdapter = new WagmiAdapter({
networks: chains,
networks: chains as Array<AppKitNetwork>,
multiInjectedProviderDiscovery: true,
transports: {
[currentChain.id]: fallback(config.chain.rpcUrls.map((url) => http(url))),
...(parentChain ? { [parentChain.id]: http() } : {}),
},
projectId: feature.walletConnect.projectId,
ssr: true,
......
import type { ArbitrumL2TxnWithdrawalsItem } from 'types/api/arbitrumL2';
export const unclaimed: ArbitrumL2TxnWithdrawalsItem = {
arb_block_number: 115114348,
caller: '0x07e1e36fe70cd58a05c00812d573dc39a127ee6d',
callvalue: '21000000000000000000',
completion_transaction_hash: null,
data: '0x',
destination: '0x07e1e36fe70cd58a05c00812d573dc39a127ee6d',
eth_block_number: 7503173,
id: 59874,
l2_timestamp: 1737020350,
status: 'confirmed',
token: null,
};
export const claimed: ArbitrumL2TxnWithdrawalsItem = {
arb_block_number: 115114348,
caller: '0x07e1e36fe70cd58a05c00812d573dc39a127ee6d',
callvalue: '21000000000000000000',
completion_transaction_hash: '0x215382498438cb6532a5e5fb07d664bbf912187866591470d47c3cfbce2dc4a8',
data: '0x',
destination: '0x07e1e36fe70cd58a05c00812d573dc39a127ee6d',
eth_block_number: 7503173,
id: 59875,
l2_timestamp: 1737020350,
status: 'relayed',
token: {
address: '0x0000000000000000000000000000000000000000',
symbol: 'USDC',
name: 'USDC Token',
decimals: 6,
amount: '10000000000',
destination: '0x07e1e36fe70cd58a05c00812d573dc39a127ee6d',
},
};
......@@ -52,6 +52,7 @@ export function app(): CspDev.DirectiveDescriptor {
// chain RPC server
...config.chain.rpcUrls,
...(getFeaturePayload(config.features.rollup)?.parentChain?.rpcUrls ?? []),
'https://infragrid.v.network', // RPC providers
// github (spec for api-docs page)
......
......@@ -92,6 +92,16 @@ export const withdrawals: GetServerSideProps<Props> = async(context) => {
return base(context);
};
export const txnWithdrawals: GetServerSideProps<Props> = async(context) => {
if (!(rollupFeature.isEnabled && rollupFeature.type === 'arbitrum')) {
return {
notFound: true,
};
}
return base(context);
};
export const rollup: GetServerSideProps<Props> = async(context) => {
if (!config.features.rollup.isEnabled) {
return {
......
......@@ -65,6 +65,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/token-transfers">
| StaticRoute<"/tokens">
| DynamicRoute<"/tx/[hash]", { "hash": string }>
| StaticRoute<"/txn-withdrawals">
| StaticRoute<"/txs">
| DynamicRoute<"/txs/kettle/[hash]", { "hash": string }>
| DynamicRoute<"/validators/[id]", { "id": string }>
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
import config from 'configs/app';
const rollupFeature = config.features.rollup;
const Withdrawals = dynamic(() => {
if (rollupFeature.isEnabled && rollupFeature.type === 'arbitrum') {
return import('ui/pages/ArbitrumL2TxnWithdrawals');
}
throw new Error('Txn withdrawals feature is not enabled.');
}, { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/txn-withdrawals">
<Withdrawals/>
</PageNextJs>
);
};
export default Page;
export { txnWithdrawals as getServerSideProps } from 'nextjs/getServerSideProps';
......@@ -14,7 +14,7 @@ import { MarketplaceContext } from 'lib/contexts/marketplace';
import { RewardsContextProvider } from 'lib/contexts/rewards';
import { SettingsContextProvider } from 'lib/contexts/settings';
import { SocketProvider } from 'lib/socket/context';
import currentChain from 'lib/web3/currentChain';
import { currentChain } from 'lib/web3/chains';
import theme from 'theme/theme';
import { port as socketPort } from './utils/socket';
......
import type { ArbitrumL2TxnBatchesItem, ArbitrumL2TxnBatch, ArbitrumL2MessagesItem } from 'types/api/arbitrumL2';
import type { ArbitrumL2TxnBatchesItem, ArbitrumL2TxnBatch, ArbitrumL2MessagesItem, ArbitrumL2TxnWithdrawalsItem } from 'types/api/arbitrumL2';
import { ADDRESS_HASH } from './addressParams';
import { TX_HASH } from './tx';
......@@ -36,3 +36,17 @@ export const ARBITRUM_L2_TXN_BATCH: ArbitrumL2TxnBatch = {
batch_data_container: 'in_blob4844',
},
};
export const ARBITRUM_L2_TXN_WITHDRAWALS_ITEM: ArbitrumL2TxnWithdrawalsItem = {
arb_block_number: 70889261,
caller: '0x507f55d716340fc836ba52c1a8daebcfeedeef1a',
completion_transaction_hash: null,
callvalue: '100000000000000',
data: '0x',
destination: '0x507f55d716340fc836ba52c1a8daebcfeedeef1a',
eth_block_number: 6494128,
id: 43685,
l2_timestamp: 1723578569,
status: 'relayed',
token: null,
};
......@@ -12,6 +12,8 @@ export interface ArbitrumLatestDepositsResponse {
items: Array<ArbitrumLatestDepositsItem>;
}
export type ArbitrumL2MessageStatus = 'initiated' | 'sent' | 'confirmed' | 'relayed';
export type ArbitrumL2MessagesItem = {
completion_transaction_hash: string | null;
id: number;
......@@ -19,7 +21,7 @@ export type ArbitrumL2MessagesItem = {
origination_timestamp: string | null;
origination_transaction_block_number: number | null;
origination_transaction_hash: string;
status: 'initiated' | 'sent' | 'confirmed' | 'relayed';
status: ArbitrumL2MessageStatus;
};
export type ArbitrumL2MessagesResponse = {
......@@ -114,6 +116,36 @@ export type ArbitrumL2BatchBlocks = {
} | null;
};
export interface ArbitrumL2TxnWithdrawalsItem {
arb_block_number: number;
caller: string;
callvalue: string;
completion_transaction_hash: string | null;
data: string;
destination: string;
eth_block_number: number;
id: number;
l2_timestamp: number;
status: ArbitrumL2MessageStatus;
token: {
address: string;
amount: string | null;
destination: string | null;
name: string | null;
symbol: string | null;
decimals: number | null;
} | null;
}
export interface ArbitrumL2TxnWithdrawalsResponse {
items: Array<ArbitrumL2TxnWithdrawalsItem>;
}
export interface ArbitrumL2MessageClaimResponse {
calldata: string;
outbox_address: string;
}
export const ARBITRUM_L2_TX_BATCH_STATUSES = [
'Processed on rollup' as const,
'Sent to base' as const,
......
......@@ -121,11 +121,11 @@ type ArbitrumTransactionData = {
status: ArbitrumBatchStatus;
message_related_info: {
associated_l1_transaction: string | null;
message_status: ArbitrumMessageStatus;
message_status: ArbitrumTransactionMessageStatus;
};
};
export type ArbitrumMessageStatus = 'Relayed' | 'Syncing with base layer' | 'Waiting for confirmation' | 'Ready for relay' | 'Settlement pending';
export type ArbitrumTransactionMessageStatus = 'Relayed' | 'Syncing with base layer' | 'Waiting for confirmation' | 'Ready for relay' | 'Settlement pending';
export const ZKEVM_L2_TX_STATUSES = [ 'Confirmed by Sequencer', 'L1 Confirmed' ];
......
......@@ -10,3 +10,16 @@ export const ROLLUP_TYPES = [
] as const;
export type RollupType = ArrayElement<typeof ROLLUP_TYPES>;
export interface ParentChain {
id?: number;
name?: string;
baseUrl: string;
rpcUrls?: Array<string>;
currency?: {
name: string;
symbol: string;
decimals: number;
};
isTestnet?: boolean;
}
......@@ -9,12 +9,12 @@ import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import CustomAbiModal from 'ui/customAbi/CustomAbiModal/CustomAbiModal';
import Skeleton from 'ui/shared/chakra/Skeleton';
import ConnectWalletAlert from 'ui/shared/ConnectWalletAlert';
import RawDataSnippet from 'ui/shared/RawDataSnippet';
import AuthGuard from 'ui/snippets/auth/AuthGuard';
import useIsAuth from 'ui/snippets/auth/useIsAuth';
import ContractAbi from './ContractAbi';
import ContractConnectWallet from './ContractConnectWallet';
import ContractCustomAbiAlert from './ContractCustomAbiAlert';
import ContractMethodsContainer from './ContractMethodsContainer';
import ContractMethodsFilters from './ContractMethodsFilters';
......@@ -75,7 +75,7 @@ const ContractMethodsCustom = ({ isLoading: isLoadingProp }: Props) => {
{ currentInfo ? (
<>
<Flex flexDir="column" rowGap={ 2 }>
<ContractConnectWallet isLoading={ isLoading }/>
<ConnectWalletAlert isLoading={ isLoading }/>
<ContractCustomAbiAlert isLoading={ isLoading }/>
</Flex>
<RawDataSnippet
......
......@@ -6,11 +6,11 @@ import type { SmartContractMudSystemItem } from 'types/api/contract';
import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import ConnectWalletAlert from 'ui/shared/ConnectWalletAlert';
import type { Item } from '../ContractSourceAddressSelector';
import ContractSourceAddressSelector from '../ContractSourceAddressSelector';
import ContractAbi from './ContractAbi';
import ContractConnectWallet from './ContractConnectWallet';
import ContractMethodsContainer from './ContractMethodsContainer';
import ContractMethodsFilters from './ContractMethodsFilters';
import useMethodsFilters from './useMethodsFilters';
......@@ -47,7 +47,7 @@ const ContractMethodsMudSystem = ({ items }: Props) => {
return (
<Flex flexDir="column" rowGap={ 6 }>
<ContractConnectWallet/>
<ConnectWalletAlert/>
<div>
<ContractSourceAddressSelector
items={ items }
......
......@@ -7,10 +7,10 @@ import type { SmartContractProxyType } from 'types/api/contract';
import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import ConnectWalletAlert from 'ui/shared/ConnectWalletAlert';
import ContractSourceAddressSelector from '../ContractSourceAddressSelector';
import ContractAbi from './ContractAbi';
import ContractConnectWallet from './ContractConnectWallet';
import ContractMethodsContainer from './ContractMethodsContainer';
import ContractMethodsFilters from './ContractMethodsFilters';
import useMethodsFilters from './useMethodsFilters';
......@@ -43,7 +43,7 @@ const ContractMethodsProxy = ({ implementations, isLoading: isInitialLoading, pr
return (
<Flex flexDir="column" rowGap={ 6 }>
<ContractConnectWallet isLoading={ isInitialLoading }/>
<ConnectWalletAlert isLoading={ isInitialLoading }/>
<div>
<ContractSourceAddressSelector
items={ implementations }
......
......@@ -4,9 +4,9 @@ import React from 'react';
import type { Abi } from 'viem';
import getQueryParamString from 'lib/router/getQueryParamString';
import ConnectWalletAlert from 'ui/shared/ConnectWalletAlert';
import ContractAbi from './ContractAbi';
import ContractConnectWallet from './ContractConnectWallet';
import ContractMethodsContainer from './ContractMethodsContainer';
import ContractMethodsFilters from './ContractMethodsFilters';
import useMethodsFilters from './useMethodsFilters';
......@@ -29,7 +29,7 @@ const ContractMethodsRegular = ({ abi, isLoading }: Props) => {
return (
<Flex flexDir="column" rowGap={ 6 }>
<ContractConnectWallet isLoading={ isLoading }/>
<ConnectWalletAlert isLoading={ isLoading }/>
<ContractMethodsFilters
defaultMethodType={ filters.methodType }
defaultSearchTerm={ filters.searchTerm }
......
......@@ -3,12 +3,15 @@ import React from 'react';
import type { ArbitrumL2MessagesItem } from 'types/api/arbitrumL2';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import Skeleton from 'ui/shared/chakra/Skeleton';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import ArbitrumL2MessageStatus from 'ui/shared/statusTag/ArbitrumL2MessageStatus';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
......@@ -102,7 +105,9 @@ const ArbitrumL2MessagesListItem = ({ item, isLoading, direction }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>Status</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<ArbitrumL2MessageStatus status={ item.status } isLoading={ isLoading }/>
{ item.status === 'confirmed' && direction === 'from-rollup' ?
<LinkInternal href={ route({ pathname: '/txn-withdrawals', query: { q: item.origination_transaction_hash } }) }>Ready for relay</LinkInternal> :
<ArbitrumL2MessageStatus status={ item.status } isLoading={ isLoading }/> }
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 transaction</ListItemMobileGrid.Label>
......
......@@ -3,12 +3,15 @@ import React from 'react';
import type { ArbitrumL2MessagesItem } from 'types/api/arbitrumL2';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import Skeleton from 'ui/shared/chakra/Skeleton';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ArbitrumL2MessageStatus from 'ui/shared/statusTag/ArbitrumL2MessageStatus';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
......@@ -83,7 +86,9 @@ const ArbitrumL2MessagesTableItem = ({ item, direction, isLoading }: Props) => {
/>
</Td>
<Td verticalAlign="middle">
<ArbitrumL2MessageStatus status={ item.status } isLoading={ isLoading }/>
{ item.status === 'confirmed' && direction === 'from-rollup' ?
<LinkInternal href={ route({ pathname: '/txn-withdrawals', query: { q: item.origination_transaction_hash } }) }>Ready for relay</LinkInternal> :
<ArbitrumL2MessageStatus status={ item.status } isLoading={ isLoading }/> }
</Td>
<Td verticalAlign="middle">
{ l1TxHash ? (
......
import React from 'react';
import * as txnWithdrawalsMock from 'mocks/arbitrum/txnWithdrawals';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
import ArbitrumL2TxnWithdrawals from './ArbitrumL2TxnWithdrawals';
const TX_HASH = '0x215382498438cb6532a5e5fb07d664bbf912187866591470d47c3cfbce2dc4a8';
const hooksConfig = {
router: {
query: { q: TX_HASH },
},
};
test('base view +@mobile', async({ render, mockApiResponse, mockEnvs, mockTextAd }) => {
await mockTextAd();
await mockEnvs(ENVS_MAP.arbitrumRollup);
await mockApiResponse(
'arbitrum_l2_txn_withdrawals',
{ items: [ txnWithdrawalsMock.unclaimed, txnWithdrawalsMock.claimed ] },
{ pathParams: { hash: TX_HASH } },
);
const component = await render(<ArbitrumL2TxnWithdrawals/>, { hooksConfig });
await expect(component).toHaveScreenshot();
});
import { Box, chakra, Text } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ARBITRUM_L2_TXN_WITHDRAWALS_ITEM } from 'stubs/arbitrumL2';
import DataListDisplay from 'ui/shared/DataListDisplay';
import FilterInput from 'ui/shared/filters/FilterInput';
import FieldError from 'ui/shared/forms/components/FieldError';
import { TRANSACTION_HASH_REGEXP } from 'ui/shared/forms/validators/transaction';
import PageTitle from 'ui/shared/Page/PageTitle';
import ArbitrumL2TxnWithdrawalsList from 'ui/txnWithdrawals/arbitrumL2/ArbitrumL2TxnWithdrawalsList';
import ArbitrumL2TxnWithdrawalsTable from 'ui/txnWithdrawals/arbitrumL2/ArbitrumL2TxnWithdrawalsTable';
const ArbitrumL2TxnWithdrawals = () => {
const router = useRouter();
const [ searchTerm, setSearchTerm ] = React.useState(getQueryParamString(router.query.q) || undefined);
const [ error, setError ] = React.useState<string | null>(null);
const { data, isError, isPlaceholderData } = useApiQuery('arbitrum_l2_txn_withdrawals', {
pathParams: {
hash: searchTerm,
},
queryOptions: {
enabled: Boolean(searchTerm),
placeholderData: {
items: [ ARBITRUM_L2_TXN_WITHDRAWALS_ITEM ],
},
},
});
const handleSearchTermChange = React.useCallback(() => {
setError(null);
}, [ ]);
const handleSearchInputFocus = React.useCallback(() => {}, []);
const handleSearchInputBlur = React.useCallback((event: React.FocusEvent<HTMLInputElement>) => {
const { value } = event.target;
if (!value || TRANSACTION_HASH_REGEXP.test(value)) {
setSearchTerm(value);
setError(null);
router.push(
{ pathname: router.pathname, query: value ? { q: value } : undefined },
undefined,
{ shallow: true },
);
} else {
setError('Invalid transaction hash');
setSearchTerm(undefined);
}
}, [ router ]);
const handleSubmit = React.useCallback((event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.target as HTMLFormElement);
const searchTerm = formData.get('tx_hash') as string;
handleSearchInputBlur({ target: { value: searchTerm } } as React.FocusEvent<HTMLInputElement>);
}, [ handleSearchInputBlur ]);
const content = data?.items ? (
<>
<Box display={{ base: 'block', lg: 'none' }} mt={ 6 }>
<ArbitrumL2TxnWithdrawalsList data={ data.items } txHash={ searchTerm } isLoading={ isPlaceholderData }/>
</Box>
<Box display={{ base: 'none', lg: 'block' }} mt={ 6 }>
<ArbitrumL2TxnWithdrawalsTable data={ data.items } txHash={ searchTerm } isLoading={ isPlaceholderData }/>
</Box>
</>
) : null;
return (
<>
<PageTitle title="Transaction withdrawals" withTextAd/>
<Text>L2 to L1 message relayer: search for your L2 transaction to execute a manual withdrawal.</Text>
<chakra.form onSubmit={ handleSubmit } noValidate>
<FilterInput
name="tx_hash"
w={{ base: '100%', lg: '700px' }}
mt={ 6 }
size="xs"
placeholder="Search by transaction hash"
initialValue={ searchTerm }
onChange={ handleSearchTermChange }
onFocus={ handleSearchInputFocus }
onBlur={ handleSearchInputBlur }
/>
</chakra.form>
{ error && <FieldError message={ error }/> }
<DataListDisplay
mt={ 6 }
isError={ isError }
items={ searchTerm ? data?.items : undefined }
filterProps={{
emptyFilteredText: `Couldn${ apos }t find any withdrawals for your transaction.`,
hasActiveFilters: Boolean(searchTerm),
}}
content={ content }
/>
</>
);
};
export default React.memo(ArbitrumL2TxnWithdrawals);
......@@ -11,7 +11,7 @@ interface Props {
isLoading?: boolean;
}
const ContractConnectWallet = ({ isLoading }: Props) => {
const ConnectWalletAlert = ({ isLoading }: Props) => {
const web3Wallet = useWeb3Wallet({ source: 'Smart contracts' });
const isMobile = useIsMobile();
......@@ -62,4 +62,4 @@ const ContractConnectWallet = ({ isLoading }: Props) => {
const Fallback = () => null;
export default config.features.blockchainInteraction.isEnabled ? ContractConnectWallet : Fallback;
export default config.features.blockchainInteraction.isEnabled ? ConnectWalletAlert : Fallback;
......@@ -13,7 +13,7 @@ type FilterProps = {
type Props = {
isError: boolean;
items?: Array<unknown>;
emptyText: React.ReactNode;
emptyText?: React.ReactNode;
actionBar?: React.ReactNode;
showActionBarIfEmpty?: boolean;
content: React.ReactNode;
......
import { useColorMode } from '@chakra-ui/react';
import type { AppKitNetwork } from '@reown/appkit/networks';
import { createAppKit, useAppKitTheme } from '@reown/appkit/react';
import React from 'react';
import { WagmiProvider } from 'wagmi';
import config from 'configs/app';
import currentChain from 'lib/web3/currentChain';
import { currentChain, parentChain } from 'lib/web3/chains';
import wagmiConfig from 'lib/web3/wagmiConfig';
import colors from 'theme/foundations/colors';
import { BODY_TYPEFACE } from 'theme/foundations/typography';
......@@ -20,7 +21,7 @@ const init = () => {
createAppKit({
adapters: [ wagmiConfig.adapter ],
networks: [ currentChain ],
networks: [ currentChain, parentChain ].filter(Boolean) as [AppKitNetwork, ...Array<AppKitNetwork>],
metadata: {
name: `${ config.chain.name } explorer`,
description: `${ config.chain.name } explorer`,
......
......@@ -14,7 +14,7 @@ const AddressEntityL1 = (props: AddressEntity.EntityProps) => {
return null;
}
const defaultHref = rollupFeature.L1BaseUrl + route({
const defaultHref = rollupFeature.parentChain.baseUrl + route({
pathname: '/address/[hash]',
query: {
...props.query,
......
......@@ -14,7 +14,7 @@ const BlobEntityL1 = (props: BlobEntity.EntityProps) => {
return null;
}
const defaultHref = rollupFeature.L1BaseUrl + route({
const defaultHref = rollupFeature.parentChain.baseUrl + route({
pathname: '/blobs/[hash]',
query: { hash: props.hash },
});
......
......@@ -14,7 +14,7 @@ const BlockEntityL1 = (props: BlockEntity.EntityProps) => {
return null;
}
const defaultHref = rollupFeature.L1BaseUrl + route({
const defaultHref = rollupFeature.parentChain.baseUrl + route({
pathname: '/block/[height_or_hash]',
query: { height_or_hash: props.hash ?? String(props.number) },
});
......
import { chakra } from '@chakra-ui/react';
import React from 'react';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import * as TokenEntity from './TokenEntity';
const rollupFeature = config.features.rollup;
const TokenEntityL1 = (props: TokenEntity.EntityProps) => {
if (!rollupFeature.isEnabled) {
return null;
}
const defaultHref = rollupFeature.parentChain.baseUrl + route({
pathname: '/token/[hash]',
query: { hash: props.token.address },
});
return (
<TokenEntity.default { ...props } href={ props.href ?? defaultHref } isExternal/>
);
};
export default chakra(TokenEntityL1);
......@@ -14,7 +14,7 @@ const TxEntityL1 = (props: TxEntity.EntityProps) => {
return null;
}
const defaultHref = rollupFeature.L1BaseUrl + route({
const defaultHref = rollupFeature.parentChain.baseUrl + route({
pathname: '/tx/[hash]',
query: { hash: props.hash },
});
......
......@@ -8,6 +8,8 @@ import IconSvg from 'ui/shared/IconSvg';
type Props = {
onChange?: (searchTerm: string) => void;
onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
className?: string;
size?: 'xs' | 'sm' | 'md' | 'lg';
placeholder: string;
......@@ -17,7 +19,7 @@ type Props = {
name?: string;
};
const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialValue, isLoading, type, name }: Props) => {
const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialValue, isLoading, type, name, onFocus, onBlur }: Props) => {
const [ filterQuery, setFilterQuery ] = useState(initialValue || '');
const inputRef = React.useRef<HTMLInputElement>(null);
const iconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
......@@ -62,6 +64,8 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialVal
whiteSpace="nowrap"
type={ type }
name={ name }
onFocus={ onFocus }
onBlur={ onBlur }
/>
{ filterQuery ? (
......
......@@ -21,7 +21,7 @@ const ArbitrumL2MessageStatus = ({ status, isLoading }: Props) => {
break;
}
case 'confirmed': {
type = 'ok';
type = 'pending';
text = 'Ready for relay';
break;
}
......
......@@ -10,9 +10,10 @@ type Props = {
isLast: boolean;
isPassed: boolean;
isPending?: boolean;
noIcon?: boolean;
};
const VerificationStep = ({ step, isLast, isPassed, isPending }: Props) => {
const VerificationStep = ({ step, isLast, isPassed, isPending, noIcon }: Props) => {
let stepColor = 'text_secondary';
if (isPending) {
stepColor = 'yellow.500';
......@@ -22,7 +23,7 @@ const VerificationStep = ({ step, isLast, isPassed, isPending }: Props) => {
return (
<HStack gap={ 2 } color={ stepColor }>
<IconSvg name={ isPassed ? 'verification-steps/finalized' : 'verification-steps/unfinalized' } boxSize={ 5 }/>
{ !noIcon && <IconSvg name={ isPassed ? 'verification-steps/finalized' : 'verification-steps/unfinalized' } boxSize={ 5 }/> }
<Box color={ stepColor }>{ typeof step === 'string' ? step : step.content }</Box>
{ !isLast && <IconSvg name="arrows/east" boxSize={ 5 }/> }
</HStack>
......
......@@ -38,6 +38,7 @@ const VerificationSteps = ({ currentStep, currentStepPending, steps, isLoading,
isLast={ index === steps.length - 1 && !rightSlot }
isPassed={ index <= currentStepIndex }
isPending={ index === currentStepIndex && currentStepPending }
noIcon={ typeof step !== 'string' && index === currentStepIndex }
/>
)) }
{ rightSlot }
......
......@@ -48,10 +48,10 @@ const TxDetailsOther = ({ nonce, type, position, queueIndex }: Props) => {
]
.filter(Boolean)
.map((item, index) => (
<>
<React.Fragment key={ index }>
{ index !== 0 && <TextSeparator/> }
{ item }
</>
</React.Fragment>
))
}
</DetailsInfoItem.Value>
......
import { Text } from '@chakra-ui/react';
import React from 'react';
import type { ArbitrumTransactionMessageStatus, Transaction } from 'types/api/transaction';
import { route } from 'nextjs-routes';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import LinkInternal from 'ui/shared/links/LinkInternal';
import VerificationSteps from 'ui/shared/verificationSteps/VerificationSteps';
const WITHDRAWAL_STATUS_STEPS: Array<ArbitrumTransactionMessageStatus> = [
'Syncing with base layer',
'Settlement pending',
'Waiting for confirmation',
'Ready for relay',
'Relayed',
];
interface Props {
data: Transaction;
}
const TxDetailsWithdrawalStatusArbitrum = ({ data }: Props) => {
const steps = React.useMemo(() => {
if (!data.arbitrum?.message_related_info) {
return [];
}
switch (data.arbitrum.message_related_info.message_status) {
case 'Ready for relay':
case 'Relayed': {
const lastElementIndex = data.arbitrum.message_related_info.message_status === 'Relayed' ? Infinity : -1;
return WITHDRAWAL_STATUS_STEPS.slice(0, lastElementIndex).map((step, index, array) => {
if (index !== array.length - 1) {
return step;
}
return {
content: (
<LinkInternal
href={ route({ pathname: '/txn-withdrawals', query: { q: data.hash } }) }
>
{ step }
</LinkInternal>
),
label: step,
};
});
}
default:
return WITHDRAWAL_STATUS_STEPS;
}
}, [ data.arbitrum?.message_related_info, data.hash ]);
if (!data.arbitrum || !data.arbitrum?.contains_message || !data.arbitrum?.message_related_info) {
return null;
}
if (data.arbitrum.contains_message === 'outcoming') {
return (
<>
<DetailsInfoItem.Label
hint="Detailed status progress of the transaction"
>
Withdrawal status
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ data.arbitrum.message_related_info.message_status ? (
<VerificationSteps
steps={ steps as unknown as Array<ArbitrumTransactionMessageStatus> }
currentStep={ data.arbitrum.message_related_info.message_status }
/>
) : <Text variant="secondary">Could not determine</Text> }
</DetailsInfoItem.Value>
</>
);
}
return (
<>
<DetailsInfoItem.Label
hint="The hash of the transaction that originated the message from the base layer"
>
Originating L1 txn hash
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ data.arbitrum.message_related_info.associated_l1_transaction ?
<TxEntityL1 hash={ data.arbitrum.message_related_info.associated_l1_transaction }/> :
<Text variant="secondary">Waiting for confirmation</Text>
}
</DetailsInfoItem.Value>
</>
);
};
export default React.memo(TxDetailsWithdrawalStatusArbitrum);
......@@ -6,7 +6,7 @@ import type { OptimisticL2WithdrawalStatus } from 'types/api/optimisticL2';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
import TxDetailsWithdrawalStatus from './TxDetailsWithdrawalStatus';
import TxDetailsWithdrawalStatusOptimistic from './TxDetailsWithdrawalStatusOptimistic';
const statuses: Array<OptimisticL2WithdrawalStatus> = [
'Waiting for state root',
......@@ -19,7 +19,7 @@ statuses.forEach((status) => {
await mockEnvs(ENVS_MAP.optimisticRollup);
const component = await render(
<Box p={ 2 }>
<TxDetailsWithdrawalStatus status={ status } l1TxHash="0x7d93a59a228e97d084a635181c3053e324237d07566ec12287eae6da2bcf9456"/>
<TxDetailsWithdrawalStatusOptimistic status={ status } l1TxHash="0x7d93a59a228e97d084a635181c3053e324237d07566ec12287eae6da2bcf9456"/>
</Box>,
);
......
......@@ -38,7 +38,7 @@ const WITHDRAWAL_STATUS_ORDER_GAME: Array<OptimisticL2WithdrawalStatus> = [
const rollupFeature = config.features.rollup;
const TxDetailsWithdrawalStatus = ({ status, l1TxHash }: Props) => {
const TxDetailsWithdrawalStatusOptimistic = ({ status, l1TxHash }: Props) => {
if (!status || !rollupFeature.isEnabled || rollupFeature.type !== 'optimistic') {
return null;
}
......@@ -94,4 +94,4 @@ const TxDetailsWithdrawalStatus = ({ status, l1TxHash }: Props) => {
);
};
export default React.memo(TxDetailsWithdrawalStatus);
export default React.memo(TxDetailsWithdrawalStatusOptimistic);
......@@ -9,7 +9,6 @@ import {
Tooltip,
chakra,
useColorModeValue,
HStack,
} from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -26,7 +25,6 @@ import config from 'configs/app';
import { WEI, WEI_IN_GWEI } from 'lib/consts';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import * as arbitrum from 'lib/rollups/arbitrum';
import { MESSAGE_DESCRIPTIONS } from 'lib/tx/arbitrumMessageStatusDescription';
import getConfirmationDuration from 'lib/tx/getConfirmationDuration';
import { currencyUnits } from 'lib/units';
import Skeleton from 'ui/shared/chakra/Skeleton';
......@@ -42,7 +40,6 @@ import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import Hint from 'ui/shared/Hint';
import IconSvg from 'ui/shared/IconSvg';
import LogDecodedInputData from 'ui/shared/logs/LogDecodedInputData';
import RawInputData from 'ui/shared/RawInputData';
......@@ -58,12 +55,13 @@ import TxDetailsFeePerGas from 'ui/tx/details/TxDetailsFeePerGas';
import TxDetailsGasPrice from 'ui/tx/details/TxDetailsGasPrice';
import TxDetailsOther from 'ui/tx/details/TxDetailsOther';
import TxDetailsTokenTransfers from 'ui/tx/details/TxDetailsTokenTransfers';
import TxDetailsWithdrawalStatus from 'ui/tx/details/TxDetailsWithdrawalStatus';
import TxDetailsWithdrawalStatusOptimistic from 'ui/tx/details/TxDetailsWithdrawalStatusOptimistic';
import TxRevertReason from 'ui/tx/details/TxRevertReason';
import TxAllowedPeekers from 'ui/tx/TxAllowedPeekers';
import TxSocketAlert from 'ui/tx/TxSocketAlert';
import ZkSyncL2TxnBatchHashesInfo from 'ui/txnBatches/zkSyncL2/ZkSyncL2TxnBatchHashesInfo';
import TxDetailsWithdrawalStatusArbitrum from './TxDetailsWithdrawalStatusArbitrum';
import TxInfoScrollFees from './TxInfoScrollFees';
const rollupFeature = config.features.rollup;
......@@ -207,7 +205,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
<span>Nonce: </span>
<chakra.span fontWeight={ 600 }>{ withdrawal.nonce }</chakra.span>
</Box>
<TxDetailsWithdrawalStatus
<TxDetailsWithdrawalStatusOptimistic
status={ withdrawal.status }
l1TxHash={ withdrawal.l1_transaction_hash }
/>
......@@ -813,28 +811,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
<>
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>
{ data.arbitrum?.contains_message && data.arbitrum?.message_related_info && (
<>
<DetailsInfoItem.Label
hint={ data.arbitrum.contains_message === 'incoming' ?
'The hash of the transaction that originated the message from the base layer' :
'The hash of the transaction that completed the message on the base layer'
}
>
{ data.arbitrum.contains_message === 'incoming' ? 'Originating L1 txn hash' : 'Completion L1 txn hash' }
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ data.arbitrum.message_related_info.associated_l1_transaction ?
<TxEntityL1 hash={ data.arbitrum.message_related_info.associated_l1_transaction }/> : (
<HStack gap={ 2 }>
<Text color="text_secondary">{ data.arbitrum.message_related_info.message_status }</Text>
<Hint label={ MESSAGE_DESCRIPTIONS[data.arbitrum.message_related_info.message_status] }/>
</HStack>
)
}
</DetailsInfoItem.Value>
</>
) }
<TxDetailsWithdrawalStatusArbitrum data={ data }/>
{ (data.blob_gas_used || data.max_fee_per_blob_gas || data.blob_gas_price) && (
<>
......
import { Button } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import { useSendTransaction, useSwitchChain } from 'wagmi';
import type { ArbitrumL2MessageClaimResponse, ArbitrumL2TxnWithdrawalsResponse } from 'types/api/arbitrumL2';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import { getResourceKey } from 'lib/api/useApiQuery';
import capitalizeFirstLetter from 'lib/capitalizeFirstLetter';
import getErrorMessage from 'lib/errors/getErrorMessage';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import getErrorProp from 'lib/errors/getErrorProp';
import useToast from 'lib/hooks/useToast';
import useWallet from 'lib/web3/useWallet';
import Skeleton from 'ui/shared/chakra/Skeleton';
import ArbitrumL2TxnWithdrawalsClaimTx from './ArbitrumL2TxnWithdrawalsClaimTx';
const rollupFeature = config.features.rollup;
interface Props {
messageId: number;
txHash: string | undefined;
completionTxHash?: string;
isLoading?: boolean;
}
const ArbitrumL2TxnWithdrawalsClaimButton = ({ messageId, txHash, completionTxHash, isLoading: isDataLoading }: Props) => {
const [ isPending, setIsPending ] = React.useState(false);
const [ claimTxHash, setClaimTxHash ] = React.useState<string | undefined>(completionTxHash);
const apiFetch = useApiFetch();
const toast = useToast();
const { sendTransactionAsync } = useSendTransaction();
const { switchChainAsync } = useSwitchChain();
const queryClient = useQueryClient();
const sendClaimTx = React.useCallback(async() => {
if (!rollupFeature.isEnabled) {
return;
}
try {
setIsPending(true);
const response = await apiFetch<'arbitrum_l2_message_claim', ArbitrumL2MessageClaimResponse, ResourceError<unknown>>(
'arbitrum_l2_message_claim',
{ pathParams: { id: messageId.toString() },
});
if ('calldata' in response) {
await switchChainAsync({ chainId: Number(rollupFeature.parentChain.id) });
const hash = await sendTransactionAsync({
data: response.calldata as `0x${ string }`,
to: response.outbox_address as `0x${ string }`,
});
setClaimTxHash(hash);
}
} catch (error) {
const apiError = getErrorObjPayload<{ message: string }>(error);
const message = capitalizeFirstLetter(apiError?.message || getErrorProp(error, 'shortMessage') || getErrorMessage(error) || 'Something went wrong');
toast({
status: 'error',
title: 'Error',
description: message,
});
setIsPending(false);
}
}, [ apiFetch, messageId, sendTransactionAsync, toast, switchChainAsync ]);
const web3Wallet = useWallet({ source: 'Smart contracts', onConnect: sendClaimTx });
const handleClaimClick = React.useCallback(async() => {
if (!web3Wallet.address) {
web3Wallet.connect();
} else {
sendClaimTx();
}
}, [ sendClaimTx, web3Wallet ]);
const handleSuccess = React.useCallback(() => {
queryClient.setQueryData(
getResourceKey('arbitrum_l2_txn_withdrawals', { pathParams: { hash: txHash } }),
(prevData: ArbitrumL2TxnWithdrawalsResponse | undefined) => {
if (!prevData) {
return;
}
const newItems = prevData.items.map(item => item.id === messageId ? { ...item, status: 'relayed' } : item);
return {
...prevData,
items: newItems,
};
});
setIsPending(false);
}, [ messageId, queryClient, txHash ]);
const handleError = React.useCallback((error: Error) => {
toast({
status: 'error',
title: 'Error',
description: error.message,
});
setIsPending(false);
}, [ toast ]);
if (claimTxHash) {
return (
<ArbitrumL2TxnWithdrawalsClaimTx
isPending={ isPending }
hash={ claimTxHash }
onSuccess={ handleSuccess }
onError={ handleError }
/>
);
}
const isLoading = isPending || web3Wallet.isOpen;
return (
<Skeleton isLoaded={ !isDataLoading }>
<Button
size="sm"
variant="outline"
onClick={ handleClaimClick }
isLoading={ isLoading }
loadingText="Claim"
>
Claim
</Button>
</Skeleton>
);
};
export default React.memo(ArbitrumL2TxnWithdrawalsClaimButton);
import { HStack, Spinner } from '@chakra-ui/react';
import React from 'react';
import { useWaitForTransactionReceipt } from 'wagmi';
import config from 'configs/app';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
const rollupFeature = config.features.rollup;
interface Props {
isPending: boolean;
hash: string;
onSuccess: () => void;
onError: (error: Error) => void;
}
const ArbitrumL2TxnWithdrawalsClaimTx = ({ isPending, hash, onSuccess, onError }: Props) => {
const { status, error } = useWaitForTransactionReceipt({
hash: hash as `0x${ string }`,
chainId: rollupFeature.isEnabled ? Number(rollupFeature.parentChain.id) : undefined,
query: { enabled: isPending },
});
React.useEffect(() => {
switch (status) {
case 'success':
onSuccess();
break;
case 'error':
onError(error);
break;
}
}, [ status, error, onSuccess, onError ]);
return (
<HStack columnGap={ 2 } lineHeight="32px">
{ isPending && <Spinner size="sm"/> }
<TxEntityL1 hash={ hash } noIcon maxW="160px"/>
</HStack >
);
};
export default React.memo(ArbitrumL2TxnWithdrawalsClaimTx);
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { ArbitrumL2TxnWithdrawalsItem } from 'types/api/arbitrumL2';
import ArbitrumL2TxnWithdrawalsListItem from './ArbitrumL2TxnWithdrawalsListItem';
interface Props {
data: Array<ArbitrumL2TxnWithdrawalsItem>;
isLoading: boolean;
txHash: string | undefined;
}
const ArbitrumL2TxnWithdrawalsList = ({ data, isLoading, txHash }: Props) => {
return (
<Box>
{ data.map((item, index) => (
<ArbitrumL2TxnWithdrawalsListItem
key={ String(item.id) + (isLoading ? index : '') }
data={ item }
isLoading={ isLoading }
txHash={ txHash }
/>
)) }
</Box>
);
};
export default React.memo(ArbitrumL2TxnWithdrawalsList);
import React from 'react';
import type { ArbitrumL2TxnWithdrawalsItem } from 'types/api/arbitrumL2';
import config from 'configs/app';
import Skeleton from 'ui/shared/chakra/Skeleton';
import AddressEntityL1 from 'ui/shared/entities/address/AddressEntityL1';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import ArbitrumL2MessageStatus from 'ui/shared/statusTag/ArbitrumL2MessageStatus';
import ArbitrumL2TxnWithdrawalsClaimButton from './ArbitrumL2TxnWithdrawalsClaimButton';
import ArbitrumL2TxnWithdrawalsValue from './ArbitrumL2TxnWithdrawalsValue';
const rollupFeature = config.features.rollup;
interface Props {
data: ArbitrumL2TxnWithdrawalsItem;
isLoading: boolean;
txHash: string | undefined;
}
const ArbitrumL2TxnWithdrawalsListItem = ({ data, isLoading, txHash }: Props) => {
if (!rollupFeature.isEnabled || rollupFeature.type !== 'arbitrum') {
return null;
}
return (
<ListItemMobileGrid.Container gridTemplateColumns="110px auto">
<ListItemMobileGrid.Label isLoading={ isLoading }>Message #</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading }>{ data.id }</Skeleton>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Receiver</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<AddressEntityL1 address={{ hash: data.token?.destination || data.destination }} isLoading={ isLoading }/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Value</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading }>
<ArbitrumL2TxnWithdrawalsValue data={ data }/>
</Skeleton>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Status</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value
display="flex"
alignItems="center"
justifyContent="space-between"
flexWrap="wrap"
rowGap={ 2 }
columnGap={ 3 }
py={ 0 }
>
<ArbitrumL2MessageStatus status={ data.status } isLoading={ isLoading }/>
{ (data.status === 'confirmed' || data.status === 'relayed') && (
<ArbitrumL2TxnWithdrawalsClaimButton
messageId={ data.id }
txHash={ txHash }
completionTxHash={ data.completion_transaction_hash || undefined }
isLoading={ isLoading }
/>
) }
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container>
);
};
export default ArbitrumL2TxnWithdrawalsListItem;
import { Tbody, Tr, Thead, Table, Th } from '@chakra-ui/react';
import React from 'react';
import type { ArbitrumL2TxnWithdrawalsItem } from 'types/api/arbitrumL2';
import ArbitrumL2TxnWithdrawalsTableItem from './ArbitrumL2TxnWithdrawalsTableItem';
interface Props {
data: Array<ArbitrumL2TxnWithdrawalsItem>;
txHash: string | undefined;
isLoading: boolean;
}
const ArbitrumL2TxnWithdrawalsTable = ({ data, txHash, isLoading }: Props) => {
return (
<Table minW="900px">
<Thead>
<Tr>
<Th width="150px">Message #</Th>
<Th width="30%">Receiver</Th>
<Th width="30%" isNumeric>Value</Th>
<Th width="40%">Status</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item, index) => (
<ArbitrumL2TxnWithdrawalsTableItem
key={ String(item.id) + (isLoading ? index : '') }
data={ item }
isLoading={ isLoading }
txHash={ txHash }/>
)) }
</Tbody>
</Table>
);
};
export default React.memo(ArbitrumL2TxnWithdrawalsTable);
import { Tr, Td, Flex } from '@chakra-ui/react';
import React from 'react';
import type { ArbitrumL2TxnWithdrawalsItem } from 'types/api/arbitrumL2';
import Skeleton from 'ui/shared/chakra/Skeleton';
import AddressEntityL1 from 'ui/shared/entities/address/AddressEntityL1';
import ArbitrumL2MessageStatus from 'ui/shared/statusTag/ArbitrumL2MessageStatus';
import ArbitrumL2TxnWithdrawalsClaimButton from './ArbitrumL2TxnWithdrawalsClaimButton';
import ArbitrumL2TxnWithdrawalsValue from './ArbitrumL2TxnWithdrawalsValue';
interface Props {
txHash: string | undefined;
data: ArbitrumL2TxnWithdrawalsItem;
isLoading?: boolean;
}
const ArbitrumL2TxnWithdrawalsTableItem = ({ data, isLoading, txHash }: Props) => {
return (
<Tr>
<Td verticalAlign="middle">
<Skeleton isLoaded={ !isLoading }>{ data.id }</Skeleton>
</Td>
<Td verticalAlign="middle">
<AddressEntityL1 address={{ hash: data.token?.destination || data.destination }} isLoading={ isLoading }/>
</Td>
<Td verticalAlign="middle" isNumeric>
<Skeleton isLoaded={ !isLoading }>
<ArbitrumL2TxnWithdrawalsValue data={ data }/>
</Skeleton>
</Td>
<Td verticalAlign="middle">
<Flex alignItems="center" justifyContent="space-between" columnGap={ 8 }>
<ArbitrumL2MessageStatus status={ data.status } isLoading={ isLoading }/>
{ (data.status === 'confirmed' || data.status === 'relayed') && (
<ArbitrumL2TxnWithdrawalsClaimButton
messageId={ data.id }
txHash={ txHash }
completionTxHash={ data.completion_transaction_hash || undefined }
isLoading={ isLoading }
/>
) }
</Flex>
</Td>
</Tr>
);
};
export default React.memo(ArbitrumL2TxnWithdrawalsTableItem);
import { Flex } from '@chakra-ui/react';
import React from 'react';
import type { ArbitrumL2TxnWithdrawalsItem } from 'types/api/arbitrumL2';
import type { TokenInfo } from 'types/api/token';
import config from 'configs/app';
import getCurrencyValue from 'lib/getCurrencyValue';
import TokenEntityL1 from 'ui/shared/entities/token/TokenEntityL1';
interface Props {
data: ArbitrumL2TxnWithdrawalsItem;
}
const ArbitrumL2TxnWithdrawalsValue = ({ data }: Props) => {
const content = (() => {
if (data.token) {
const { valueStr } = data.token && getCurrencyValue({
value: data.token.amount ?? '0',
accuracy: 8,
decimals: String(data.token.decimals),
});
const formattedData: TokenInfo | null = {
...data.token,
decimals: String(data.token.decimals),
type: 'ERC-20',
holders: null,
exchange_rate: null,
total_supply: null,
circulating_market_cap: null,
icon_url: null,
};
return (
<>
{ valueStr }
<TokenEntityL1 token={ formattedData } noIcon noCopy onlySymbol/>
</>
);
}
if (data.callvalue && data.callvalue !== '0') {
const { valueStr } = getCurrencyValue({
value: data.callvalue,
accuracy: 8,
decimals: String(config.chain.currency.decimals),
});
return (
<>
<span>{ valueStr }</span>
<span>{ config.chain.currency.symbol }</span>
</>
);
}
return <span>-</span>;
})();
return (
<Flex alignItems="center" columnGap={ 1 } w="fit-content" ml={{ base: undefined, lg: 'auto' }} color="initial">
{ content }
</Flex>
);
};
export default React.memo(ArbitrumL2TxnWithdrawalsValue);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment