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
@@ -1,46 +1,59 @@
|
||||
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
|
||||
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
|
||||
@@ -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://[email protected]/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
|
||||
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
|
||||
@@ -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
|
||||
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` |
|
||||
|
||||
|
||||
|
||||
|
||||
### 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 :
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
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',
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/* 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: '',
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { type Chain } from 'viem';
|
||||
|
||||
import config from 'configs/app';
|
||||
|
||||
export const currentChain: Chain = {
|
||||
id: Number(config.chain.id),
|
||||
name: config.chain.name ?? '',
|
||||
nativeCurrency: {
|
||||
decimals: config.chain.currency.decimals,
|
||||
name: config.chain.currency.name ?? '',
|
||||
symbol: config.chain.currency.symbol ?? '',
|
||||
},
|
||||
rpcUrls: {
|
||||
'default': {
|
||||
http: config.chain.rpcUrls,
|
||||
},
|
||||
},
|
||||
blockExplorers: {
|
||||
'default': {
|
||||
name: 'Blockscout',
|
||||
url: config.app.baseUrl,
|
||||
},
|
||||
},
|
||||
testnet: config.chain.isTestnet,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
})();
|
||||
@@ -1,6 +1,6 @@
|
||||
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) {
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { type Chain } from 'viem';
|
||||
|
||||
import config from 'configs/app';
|
||||
|
||||
const currentChain = {
|
||||
id: Number(config.chain.id),
|
||||
name: config.chain.name ?? '',
|
||||
nativeCurrency: {
|
||||
decimals: config.chain.currency.decimals,
|
||||
name: config.chain.currency.name ?? '',
|
||||
symbol: config.chain.currency.symbol ?? '',
|
||||
},
|
||||
rpcUrls: {
|
||||
'default': {
|
||||
http: config.chain.rpcUrls,
|
||||
},
|
||||
},
|
||||
blockExplorers: {
|
||||
'default': {
|
||||
name: 'Blockscout',
|
||||
url: config.app.baseUrl,
|
||||
},
|
||||
},
|
||||
testnet: config.chain.isTestnet,
|
||||
} as const satisfies Chain;
|
||||
|
||||
export default currentChain;
|
||||
@@ -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();
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
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 }>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
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';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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 ? (
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
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();
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
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);
|
||||
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 67 KiB |
@@ -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;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
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) },
|
||||
});
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
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>
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
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) && (
|
||||
<>
|
||||
|
||||
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
@@ -0,0 +1,142 @@
|
||||
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);
|
||||
@@ -0,0 +1,43 @@
|
||||
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);
|
||||
@@ -0,0 +1,29 @@
|
||||
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);
|
||||
@@ -0,0 +1,72 @@
|
||||
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;
|
||||
@@ -0,0 +1,38 @@
|
||||
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);
|
||||
@@ -0,0 +1,50 @@
|
||||
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);
|
||||
@@ -0,0 +1,69 @@
|
||||
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);
|
||||