Commit 0eabf794 authored by tom's avatar tom

Merge branch 'main' into marketplace-config

parents c2a7ba08 fc65cfce
......@@ -39,7 +39,7 @@ NEXT_PUBLIC_HOMEPAGE_CHARTS=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_CHARTS__
NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT__
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER__
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME__
NEXT_PUBLIC_AD_DOMAIN_WITH_AD=__PLACEHOLDER_FOR_NEXT_PUBLIC_DOMAIN_WITH_AD__
NEXT_PUBLIC_AD_DOMAIN_WITH_AD=__PLACEHOLDER_FOR_NEXT_PUBLIC_AD_DOMAIN_WITH_AD__
NEXT_PUBLIC_AD_ADBUTLER_ON=__PLACEHOLDER_FOR_NEXT_PUBLIC_AD_ADBUTLER_ON__
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=__PLACEHOLDER_FOR_NEXT_PUBLIC_GRAPHIQL_TRANSACTION__
......
......@@ -28,7 +28,7 @@ jobs:
uses: actions/checkout@v3
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4
uses: rlespinasse/github-slug-action@v4.4.1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
......
......@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v3
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4
uses: rlespinasse/github-slug-action@v4.4.1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
......
......@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v3
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4
uses: rlespinasse/github-slug-action@v4.4.1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
......
......@@ -365,6 +365,7 @@
"options": [
"",
"--update-snapshots",
"--ui",
],
"default": ""
},
......
This diff is collapsed.
This diff is collapsed.
......@@ -16,7 +16,9 @@ NEXT_PUBLIC_NETWORK_SMALL_LOGO=
NEXT_PUBLIC_NETWORK_RPC_URL=https://core.poa.network
NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace/eth-goerli.json
NEXT_PUBLIC_IS_L2_NETWORK=false
# api config
NEXT_PUBLIC_API_HOST=blockscout.com
NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.blockscout.com
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
......@@ -7,6 +7,49 @@ const config: NextjsOptions = {
// We recommend adjusting this value in production, or using tracesSampler
// for finer control
tracesSampleRate: 1.0,
// error filtering settings
// were taken from here - https://docs.sentry.io/platforms/node/guides/azure-functions/configuration/filtering/#decluttering-sentry
ignoreErrors: [
// Random plugins/extensions
'top.GLOBALS',
// See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error.html
'originalCreateNotification',
'canvas.contentDocument',
'MyApp_RemoveAllHighlights',
'http://tt.epicplay.com',
'Can\'t find variable: ZiteReader',
'jigsaw is not defined',
'ComboSearch is not defined',
'http://loading.retry.widdit.com/',
'atomicFindClose',
// Facebook borked
'fb_xd_fragment',
// ISP "optimizing" proxy - `Cache-Control: no-transform` seems to reduce this. (thanks @acdha)
// See http://stackoverflow.com/questions/4113268/how-to-stop-javascript-injection-from-vodafone-proxy
'bmi_SafeAddOnload',
'EBCallBackMessageReceived',
// See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
'conduitPage',
// Generic error code from errors outside the security sandbox
'Script error.',
],
denyUrls: [
// Facebook flakiness
/graph\.facebook\.com/i,
// Facebook blocked
/connect\.facebook\.net\/en_US\/all\.js/i,
// Woopra flakiness
/eatdifferent\.com\.woopra-ns\.com/i,
/static\.woopra\.com\/js\/woopra\.js/i,
// Chrome extensions
/extensions\//i,
/^chrome:\/\//i,
// Other plugins
/127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
/webappstoolbarba\.texthelp\.com\//i,
/metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
],
};
export default config;
......@@ -9,6 +9,49 @@ export const config: Sentry.BrowserOptions = {
// We recommend adjusting this value in production, or using tracesSampler
// for finer control
tracesSampleRate: 1.0,
// error filtering settings
// were taken from here - https://docs.sentry.io/platforms/node/guides/azure-functions/configuration/filtering/#decluttering-sentry
ignoreErrors: [
// Random plugins/extensions
'top.GLOBALS',
// See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error.html
'originalCreateNotification',
'canvas.contentDocument',
'MyApp_RemoveAllHighlights',
'http://tt.epicplay.com',
'Can\'t find variable: ZiteReader',
'jigsaw is not defined',
'ComboSearch is not defined',
'http://loading.retry.widdit.com/',
'atomicFindClose',
// Facebook borked
'fb_xd_fragment',
// ISP "optimizing" proxy - `Cache-Control: no-transform` seems to reduce this. (thanks @acdha)
// See http://stackoverflow.com/questions/4113268/how-to-stop-javascript-injection-from-vodafone-proxy
'bmi_SafeAddOnload',
'EBCallBackMessageReceived',
// See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
'conduitPage',
// Generic error code from errors outside the security sandbox
'Script error.',
],
denyUrls: [
// Facebook flakiness
/graph\.facebook\.com/i,
// Facebook blocked
/connect\.facebook\.net\/en_US\/all\.js/i,
// Woopra flakiness
/eatdifferent\.com\.woopra-ns\.com/i,
/static\.woopra\.com\/js\/woopra\.js/i,
// Chrome extensions
/extensions\//i,
/^chrome:\/\//i,
// Other plugins
/127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
/webappstoolbarba\.texthelp\.com\//i,
/metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
],
};
export function configureScope(scope: Sentry.Scope) {
......
export const data = [
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
miner: 'KuCoin Pool',
after: {
balance: '0.012192910371186045',
nonce: '4',
},
before: {
balance: '0.008350264867549483',
nonce: '5',
},
diff: '0.003842645503636562',
storage: [
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
before: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
after: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
},
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
before: '0x730bc43aac5a6cf94a72f69a42adfa114fe119b5',
after: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
},
],
},
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
miner: 'KuCoin Pool',
after: {
balance: '0.012192910371186045',
nonce: '4',
},
before: {
balance: '0.008350264867549483',
nonce: '5',
},
diff: '0.003842645503636562',
storage: [
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
before: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
after: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
},
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
before: '0x730bc43aac5a6cf94a72f69a42adfa114fe119b5',
after: '0x000000000000000000000000730bc43aac5a6cf94a72f69a42adfa114fe119b5',
},
],
},
{
address: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
miner: 'KuCoin Pool',
after: {
balance: '0.012192910371186045',
},
before: {
balance: '0.008350264867549483',
},
diff: '-0.003842645503636562',
},
];
export type TTxState = Array<TTxStateItem>;
export type TTxStateItem = {
address: string;
miner: string;
after: {
balance: string;
nonce?: string;
};
before: {
balance: string;
nonce?: string;
};
diff: string;
storage?: Array<TTxStateItemStorage>;
}
export type TTxStateItemStorage = {
address: string;
before: string;
after: string;
}
......@@ -5,7 +5,7 @@ blockscout:
app: blockscout
enabled: true
image:
_default: &image blockscout/blockscout-optimism-l2-advanced:5.1.2-prerelease-84e22ed2
_default: &image blockscout/blockscout-optimism-l2-advanced:5.1.2-prerelease-465ba09e
replicas:
app: 1
# init container
......@@ -83,9 +83,9 @@ blockscout:
_default: prod
ECTO_USE_SSL:
_default: 'false'
ENABLE_RUST_VERIFICATION_SERVICE:
MICROSERVICE_SC_VERIFIER_ENABLED:
_default: 'true'
RUST_VERIFICATION_SERVICE_URL:
MICROSERVICE_SC_VERIFIER_URL:
_default: http://sc-verifier-svc:8043
ACCOUNT_ENABLED:
_default: 'true'
......
......@@ -33,7 +33,7 @@ blockscout:
ACCOUNT_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:xasZDDg1IuGbKSTm9Opjh43RcqDSzE3dMdmynP0gejo=,iv:TTRqPOkwMxAQIpmc7DklBoPdZzn3FsAprxwXtHY/KSk=,tag:ZI7zQ5zyjjYOqjzy2DyY7Q==,type:str]
ACCOUNT_AUTH0_CLIENT_SECRET:
_default: ENC[AES256_GCM,data:iYnptgxESZs216/m0ArCciXfirPTxVjt5urKTATyYv3uadokC54ofzMZagG/ZVvNY48+Uxh4YGzwySyLOOM0SA==,iv:UQoZf3tesEjAJ7E5YruoZmVclsYfS5EmSo4z90wfHfE=,tag:er+hARrkCrjoQugqvRr4ow==,type:str]
_default: ENC[AES256_GCM,data:jr/MLQ1Rq1xjBVAb/fZFbvak23hEcmRkh5nnG4WNCcORR+xEWhEv7ncP87ClpsfxedDN5pOcFrdUkF7u80OkKA==,iv:TIxP5v9K7mcApYmSKRByKNPwYw0djRWHojYfHvSWqZM=,tag:LSLmwapH044CEzOvlce9Og==,type:str]
ACCOUNT_AUTH0_CALLBACK_URL:
_default: ENC[AES256_GCM,data:GhyNTVlRvDpfW6swKExjVy65I3S+q6ITJWH8xp5TXuudp7hAtI4ujkYmXaa7D1BCaineTLhQ8UgHXFAgodOlrsGP1g41LIh6T2SGhrXTGGLbFw==,iv:ZflinC/h35jkrVAx0x7Z7U9mRridl85f7f6nfoc0Xts=,tag:40zL2KM9uPg/lRYv5JMxqw==,type:str]
ACCOUNT_AUTH0_LOGOUT_RETURN_URL:
......@@ -144,8 +144,8 @@ sops:
azure_kv: []
hc_vault: []
age: []
lastmodified: "2023-03-08T15:55:49Z"
mac: ENC[AES256_GCM,data:nl/CflZ5t09n3swP1vpR2rVjLVIW+H10/FCImDgeWuwt7F+0Whko3/UrPMypdfJeqiuCjWCmuynqjYqqZn2zjKabvc9bdo/RiQCyrdm7XIA+REA9XnKPnghYjkJYNm5kOLct5zif9Rq4wTfPOIpMRKnYvXrpnmiz5tE/lFXbYMw=,iv:mifwaplxQSJb5Q1CPWgR3hT5bTHTodNAoBEDKtdVLHI=,tag:rCAgVJxV52JV8O4CEZuLRg==,type:str]
lastmodified: "2023-04-18T09:26:42Z"
mac: ENC[AES256_GCM,data:4MdP3Fi3SDxyXjhyI7jogYdigziLkp7oh4M6BthtT2FuUu+XAGnONAdccxULJfXtKS6ue27BtV/pKWhdKeOqKIN23BJERqa4cFgYlyd85XAyL400/fEKEO4nCe2c9VIqNRUQls28/WkRjzRjvzQMMTIRIAJvfrJUYRpIabXWqAo=,iv:Z4shgbH8uJNuoJvmyyQjQ+fZsXuSC3nIGYb6GYO/nac=,tag:IDZvpGiZH41IwAgSidF3xQ==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
......
......@@ -108,9 +108,9 @@ blockscout:
_default: 'true'
CHAIN_ID:
_default: 5
ENABLE_RUST_VERIFICATION_SERVICE:
MICROSERVICE_SC_VERIFIER_ENABLED:
_default: 'true'
RUST_VERIFICATION_SERVICE_URL:
MICROSERVICE_SC_VERIFIER_URL:
_default: http://eth-bytecode-db-svc.eth-bytecode-db-testing.svc.cluster.local:80
INDEXER_MEMORY_LIMIT:
_default: 5
......
This diff is collapsed.
......@@ -18,13 +18,13 @@ import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } f
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig } from 'types/api/contract';
import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts';
import type { DepositsResponse } from 'types/api/deposits';
import type { DepositsResponse, DepositsItem } from 'types/api/deposits';
import type { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
import type { OutputRootsResponse } from 'types/api/outputRoots';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchResult, SearchResultFilters } from 'types/api/search';
import type { SearchRedirectResult, SearchResult, SearchResultFilters } from 'types/api/search';
import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats';
import type {
TokenCounters,
......@@ -39,9 +39,10 @@ import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/toke
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
import type { TxnBatchesResponse } from 'types/api/txnBatches';
import type { TTxsFilters } from 'types/api/txsFilters';
import type { TxStateChanges } from 'types/api/txStateChanges';
import type { VisualizedContract } from 'types/api/visualization';
import type { WithdrawalsResponse } from 'types/api/withdrawals';
import type ArrayElement from 'types/utils/ArrayElement';
import type { ArrayElement } from 'types/utils';
import appConfig from 'configs/app/config';
......@@ -162,6 +163,10 @@ export const RESOURCES = {
path: '/api/v2/transactions/:hash/raw-trace',
pathParams: [ 'hash' as const ],
},
tx_state_changes: {
path: '/api/v2/transactions/:hash/state-changes',
pathParams: [ 'hash' as const ],
},
// ADDRESSES
addresses: {
......@@ -334,6 +339,9 @@ export const RESOURCES = {
homepage_blocks: {
path: '/api/v2/main-page/blocks',
},
homepage_deposits: {
path: '/api/v2/main-page/optimism-deposits',
},
homepage_txs: {
path: '/api/v2/main-page/transactions',
},
......@@ -484,6 +492,7 @@ Q extends 'homepage_chart_txs' ? ChartTransactionResponse :
Q extends 'homepage_chart_market' ? ChartMarketResponse :
Q extends 'homepage_blocks' ? Array<Block> :
Q extends 'homepage_txs' ? Array<Transaction> :
Q extends 'homepage_deposits' ? Array<DepositsItem> :
Q extends 'homepage_indexing_status' ? IndexingStatus :
Q extends 'stats_counters' ? Counters :
Q extends 'stats_lines' ? StatsCharts :
......@@ -498,6 +507,7 @@ Q extends 'tx_internal_txs' ? InternalTransactionsResponse :
Q extends 'tx_logs' ? LogsResponseTx :
Q extends 'tx_token_transfers' ? TokenTransferResponse :
Q extends 'tx_raw_trace' ? RawTracesResponse :
Q extends 'tx_state_changes' ? TxStateChanges :
Q extends 'addresses' ? AddressesResponse :
Q extends 'address' ? Address :
Q extends 'address_counters' ? AddressCounters :
......@@ -519,6 +529,7 @@ Q extends 'token_instance_transfers' ? TokenInstanceTransferResponse :
Q extends 'token_inventory' ? TokenInventoryResponse :
Q extends 'tokens' ? TokensResponse :
Q extends 'search' ? SearchResult :
Q extends 'search_check_redirect' ? SearchRedirectResult :
Q extends 'contract' ? SmartContract :
Q extends 'contract_methods_read' ? Array<SmartContractReadMethod> :
Q extends 'contract_methods_read_proxy' ? Array<SmartContractReadMethod> :
......
......@@ -15,3 +15,5 @@ export const YEAR = 365 * DAY;
export const Kb = 1_000;
export const Mb = 1_000 * Kb;
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
......@@ -15,7 +15,10 @@ const REPORT_URI = process.env.SENTRY_CSP_REPORT_URI;
export function app(): CspDev.DirectiveDescriptor {
return {
'default-src': [
KEY_WORDS.NONE,
// KEY_WORDS.NONE,
// https://bugzilla.mozilla.org/show_bug.cgi?id=1242902
// need 'self' here to avoid an error with prefetch nextjs chunks in firefox
KEY_WORDS.SELF,
],
'connect-src': [
......
......@@ -9,18 +9,21 @@ export function googleAnalytics(): CspDev.DirectiveDescriptor {
return {
'connect-src': [
'*.google-analytics.com',
'*.analytics.google.com',
'https://www.googletagmanager.com',
'https://www.google-analytics.com',
'https://stats.g.doubleclick.net',
],
'script-src': [
// inline script hash, see ui/shared/GoogleAnalytics.tsx
'\'sha256-NTmEg2dBnojQfTYrYJEmp3nG7V66756qPbQMCIBrctk=\'',
'https://www.googletagmanager.com',
'https://www.google-analytics.com',
'*.google-analytics.com',
'*.analytics.google.com',
],
'img-src': [
'https://www.google-analytics.com',
'*.google-analytics.com',
'*.analytics.google.com',
],
};
}
export default function getErrorCause(error: Error | undefined): Record<string, unknown> | undefined {
return (
error && 'cause' in error &&
typeof error.cause === 'object' && error.cause !== null &&
error.cause as Record<string, unknown>
) ||
undefined;
}
import getErrorCause from './getErrorCause';
export default function getErrorStatusCode(error: Error | undefined): number | undefined {
return (
error && 'cause' in error &&
typeof error.cause === 'object' && error.cause !== null &&
'status' in error.cause && typeof error.cause.status === 'number' &&
error.cause.status
) ||
undefined;
const cause = getErrorCause(error);
return cause && 'status' in cause && typeof cause.status === 'number' ? cause.status : undefined;
}
import type { ResourceError } from 'lib/api/resources';
import getErrorCause from './getErrorCause';
export default function getResourceErrorPayload<Payload = Record<string, unknown>>(error: Error | undefined): ResourceError<Payload>['payload'] | undefined {
const cause = getErrorCause(error);
return cause && 'payload' in cause ? cause.payload as ResourceError<Payload>['payload'] : undefined;
}
const old = Number.prototype.toLocaleString;
Number.prototype.toLocaleString = function(locale, ...args) {
return old.call(this, 'en', ...args);
};
export {};
......@@ -3,6 +3,7 @@ import type { Channel } from 'phoenix';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import type { NewBlockSocketResponse } from 'types/api/block';
import type { SmartContractVerificationResponse } from 'types/api/contract';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import type { Transaction } from 'types/api/transaction';
......@@ -10,8 +11,10 @@ export type SocketMessageParams = SocketMessage.NewBlock |
SocketMessage.BlocksIndexStatus |
SocketMessage.InternalTxsIndexStatus |
SocketMessage.TxStatusUpdate |
SocketMessage.TxRawTrace |
SocketMessage.NewTx |
SocketMessage.NewPendingTx |
SocketMessage.NewDeposits |
SocketMessage.AddressBalance |
SocketMessage.AddressCurrentCoinBalance |
SocketMessage.AddressTokenBalance |
......@@ -19,7 +22,9 @@ SocketMessage.AddressCoinBalance |
SocketMessage.AddressTxs |
SocketMessage.AddressTxsPending |
SocketMessage.AddressTokenTransfer |
SocketMessage.AddressChangedBytecode |
SocketMessage.TokenTransfers |
SocketMessage.TokenTotalSupply |
SocketMessage.ContractVerification |
SocketMessage.Unknown;
......@@ -35,8 +40,10 @@ export namespace SocketMessage {
export type BlocksIndexStatus = SocketMessageParamsGeneric<'block_index_status', {finished: boolean; ratio: string}>;
export type InternalTxsIndexStatus = SocketMessageParamsGeneric<'internal_txs_index_status', {finished: boolean; ratio: string}>;
export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>;
export type TxRawTrace = SocketMessageParamsGeneric<'raw_trace', RawTracesResponse>;
export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>;
export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>;
export type NewDeposits = SocketMessageParamsGeneric<'deposits', { deposits: number }>;
export type AddressBalance = SocketMessageParamsGeneric<'balance', { balance: string; block_number: number; exchange_rate: string }>;
export type AddressCurrentCoinBalance =
SocketMessageParamsGeneric<'current_coin_balance', { coin_balance: string; block_number: number; exchange_rate: string }>;
......@@ -45,7 +52,9 @@ export namespace SocketMessage {
export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>;
export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>;
export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>;
export type AddressChangedBytecode = SocketMessageParamsGeneric<'changed_bytecode', Record<string, never>>;
export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>;
export type TokenTotalSupply = SocketMessageParamsGeneric<'total_supply', {total_supply: number }>;
export type ContractVerification = SocketMessageParamsGeneric<'verification_result', SmartContractVerificationResponse>;
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
}
......@@ -6,12 +6,12 @@ export default function getConfirmationString(durations: Array<number>) {
const [ lower, upper ] = durations.map((time) => time / 1_000);
if (!upper) {
return `Confirmed within ${ lower } secs`;
return `Confirmed within ${ lower.toLocaleString() } secs`;
}
if (lower === 0) {
return `Confirmed within <= ${ upper } secs`;
return `Confirmed within <= ${ upper.toLocaleString() } secs`;
}
return `Confirmed within ${ lower } - ${ upper } secs`;
return `Confirmed within ${ lower.toLocaleString() } - ${ upper.toLocaleString() } secs`;
}
......@@ -36,6 +36,7 @@ export const token: Address = {
name: null,
private_tags: [],
watchlist_names: [],
watchlist_address_id: null,
public_tags: [],
token: tokenInfo,
block_number_balance_updated_at: 8201413,
......@@ -84,6 +85,7 @@ export const contract: Address = {
public_tags: [ privateTag ],
token: null,
watchlist_names: [ watchlistName ],
watchlist_address_id: 42,
};
export const validator: Address = {
......@@ -113,4 +115,5 @@ export const validator: Address = {
public_tags: [],
token: null,
watchlist_names: [],
watchlist_address_id: null,
};
......@@ -8,7 +8,12 @@ export const verified: Partial<SmartContract> = {
constructor_args: 'constructor_args',
creation_bytecode: 'creation_bytecode',
deployed_bytecode: 'deployed_bytecode',
compiler_settings: 'compiler_settings',
compiler_settings: {
evmVersion: 'london',
remappings: [
'@openzeppelin/=node_modules/@openzeppelin/',
],
},
evm_version: 'default',
is_verified: true,
name: 'WPOA',
......
......@@ -85,7 +85,7 @@ export const erc721: TokenTransfer = {
method: 'updateSmartAsset',
};
export const erc1155: TokenTransfer = {
export const erc1155A: TokenTransfer = {
from: {
hash: '0x0000000000000000000000000000000000000000',
implementation_name: null,
......@@ -128,26 +128,44 @@ export const erc1155: TokenTransfer = {
log_index: '1',
};
export const erc1155multiple: TokenTransfer = {
...erc1155,
export const erc1155B: TokenTransfer = {
...erc1155A,
token: {
...erc1155.token,
...erc1155A.token,
name: 'SastanaNFT',
symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY',
},
total: [
{ token_id: '12345678', value: '100000000000000000000', decimals: null },
{ token_id: '483200961027732618117991942553110860267520', value: '200000000000000000000', decimals: null },
{ token_id: '456', value: '42', decimals: null },
],
total: { token_id: '12345678', value: '100000000000000000000', decimals: null },
};
export const erc1155C: TokenTransfer = {
...erc1155A,
token: {
...erc1155A.token,
name: 'SastanaNFT',
symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY',
},
total: { token_id: '483200961027732618117991942553110860267520', value: '200000000000000000000', decimals: null },
};
export const erc1155D: TokenTransfer = {
...erc1155A,
token: {
...erc1155A.token,
name: 'SastanaNFT',
symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY',
},
total: { token_id: '456', value: '42', decimals: null },
};
export const mixTokens: TokenTransferResponse = {
items: [
erc20,
erc721,
erc1155,
erc1155multiple,
erc1155A,
erc1155B,
erc1155C,
erc1155D,
],
next_page_params: null,
};
......@@ -39,5 +39,11 @@ export const withIndexedFields: DecodedInput = {
type: 'uint256',
value: '31567373703130350',
},
{
indexed: true,
name: 'inputArray',
type: 'uint256[2][2]',
value: [ [ '1', '1' ], [ '1', '1' ] ],
},
],
};
import type { TxStateChange } from 'types/api/txStateChanges';
export const mintToken: TxStateChange = {
address: {
hash: '0x0000000000000000000000000000000000000000',
implementation_name: null,
is_contract: false,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
balance_after: null,
balance_before: null,
change: [
{
direction: 'from',
total: {
token_id: '15077554365819457090226168288698582604878106156134383525616269766016907608065',
},
},
],
is_miner: false,
token: {
address: '0x8977EA6C55e878125d1bF3433EBf72138B7a4543',
decimals: null,
exchange_rate: null,
holders: '9191',
name: 'ParaSpace Derivative Token MOONBIRD',
symbol: 'nMOONBIRD',
total_supply: '10645',
type: 'ERC-721',
},
type: 'token',
};
export const receiveMintedToken: TxStateChange = {
address: {
hash: '0xC8F71D0ae51AfBdB009E2eC1Ea8CC9Ee204A42B5',
implementation_name: null,
is_contract: false,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
balance_after: '1',
balance_before: '0',
change: [
{
direction: 'to',
total: {
token_id: '15077554365819457090226168288698582604878106156134383525616269766016907608065',
},
},
],
is_miner: false,
token: {
address: '0x8977EA6C55e878125d1bF3433EBf72138B7a4543',
decimals: null,
exchange_rate: null,
holders: '9191',
name: 'ParaSpace Derivative Token MOONBIRD',
symbol: 'nMOONBIRD',
total_supply: '10645',
type: 'ERC-721',
},
type: 'token',
};
export const receiveCoin: TxStateChange = {
address: {
hash: '0x8dC847Af872947Ac18d5d63fA646EB65d4D99560',
implementation_name: null,
is_contract: false,
is_verified: null,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
balance_after: '443787514723917012805',
balance_before: '443787484997510408745',
change: '29726406604060',
is_miner: true,
token: null,
type: 'coin',
};
export const sendCoin: TxStateChange = {
address: {
hash: '0xC8F71D0ae51AfBdB009E2eC1Ea8CC9Ee204A42B5',
implementation_name: null,
is_contract: false,
is_verified: null,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
balance_after: '828282622733717191',
balance_before: '832127467556437753',
change: '-3844844822720562',
is_miner: false,
token: null,
type: 'coin',
};
export const baseResponse = [
mintToken,
receiveMintedToken,
sendCoin,
receiveCoin,
];
......@@ -100,8 +100,10 @@ export const withTokenTransfer: Transaction = {
token_transfers: [
tokenTransferMock.erc20,
tokenTransferMock.erc721,
tokenTransferMock.erc1155,
tokenTransferMock.erc1155multiple,
tokenTransferMock.erc1155A,
tokenTransferMock.erc1155B,
tokenTransferMock.erc1155C,
tokenTransferMock.erc1155D,
],
tx_types: [
'token_transfer',
......@@ -240,3 +242,11 @@ export const withActionsUniswap: Transaction = {
},
],
};
export const l2tx: Transaction = {
...base,
l1_gas_price: '82702201886',
l1_fee_scalar: '1.0',
l1_gas_used: '17060',
l1_fee: '1584574188135760',
};
......@@ -16,6 +16,7 @@
"build": "next build",
"build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse HEAD) -t blockscout ./",
"start": "next start",
"start:docker:local": "docker run -p 3000:3000 --env-file .env.local blockscout",
"start:docker:poa_core": "docker run -p 3000:3000 --env-file ./configs/envs/.env.common --env-file ./configs/envs/.env.poa_core --env-file ./configs/envs/.env.secrets blockscout",
"lint:eslint": "./node_modules/.bin/eslint . --ext .js,.jsx,.ts,.tsx",
"lint:eslint:fix": "./node_modules/.bin/eslint . --ext .js,.jsx,.ts,.tsx --fix",
......@@ -24,7 +25,7 @@
"format-svg": "./node_modules/.bin/svgo -r ./icons",
"test:pw": "./playwright/make-envs-script.sh && NODE_OPTIONS=\"--max-old-space-size=4096\" playwright test -c playwright-ct.config.ts",
"test:pw:local": "export NODE_PATH=$(pwd)/node_modules && rm -rf ./playwright/.cache && yarn test:pw",
"test:pw:docker": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.31.0-focal ./playwright/run-tests.sh",
"test:pw:docker": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.32.0-focal ./playwright/run-tests.sh",
"test:jest": "jest",
"test:jest:watch": "jest --watch"
},
......@@ -57,7 +58,7 @@
"js-cookie": "^3.0.1",
"lodash": "^4.0.0",
"monaco-editor": "^0.34.1",
"next": "12.2.5",
"next": "13.3.0",
"nextjs-routes": "^1.0.8",
"node-fetch": "^3.2.9",
"papaparse": "^5.3.2",
......@@ -78,7 +79,7 @@
"wagmi": "^0.10.6"
},
"devDependencies": {
"@playwright/experimental-ct-react": "1.31.0",
"@playwright/experimental-ct-react": "1.32.3",
"@svgr/webpack": "^6.5.1",
"@testing-library/react": "^13.4.0",
"@total-typescript/ts-reset": "^0.3.7",
......@@ -99,7 +100,7 @@
"css-loader": "^6.7.3",
"dotenv-cli": "^6.0.0",
"eslint": "^8.32.0",
"eslint-config-next": "^12.3.0",
"eslint-config-next": "13.3.0",
"eslint-plugin-es5": "^1.5.0",
"eslint-plugin-import-helpers": "^1.2.1",
"eslint-plugin-jest": "^27.1.6",
......@@ -112,7 +113,7 @@
"lint-staged": ">=10",
"mockdate": "^3.0.5",
"next-transpile-modules": "^10.0.0",
"playwright": "1.31.0",
"playwright": "1.32.3",
"style-loader": "^3.3.1",
"svgo": "^2.8.0",
"ts-jest": "^29.0.3",
......
......@@ -17,6 +17,8 @@ import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary';
import GoogleAnalytics from 'ui/shared/GoogleAnalytics';
import 'lib/setLocale';
function MyApp({ Component, pageProps }: AppProps) {
useConfigSentry();
const [ queryClient ] = useState(() => new QueryClient({
......
......@@ -5,6 +5,7 @@ import React from 'react';
import getSeo from 'lib/next/block/getSeo';
import Block from 'ui/pages/Block';
import Page from 'ui/shared/Page/Page';
const BlockPage: NextPage<RoutedQuery<'/block/[height]'>> = ({ height }: RoutedQuery<'/block/[height]'>) => {
const { title, description } = getSeo({ height });
......@@ -14,7 +15,9 @@ const BlockPage: NextPage<RoutedQuery<'/block/[height]'>> = ({ height }: RoutedQ
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<Page>
<Block/>
</Page>
</>
);
};
......
import type { NextPage } from 'next';
import type { GetServerSideProps, NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import appConfig from 'configs/app/config';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import type { Props } from 'lib/next/getServerSideProps';
import { getServerSideProps as getServerSidePropsBase } from 'lib/next/getServerSideProps';
import CsvExport from 'ui/pages/CsvExport';
const CsvExportPage: NextPage = () => {
......@@ -19,4 +22,12 @@ const CsvExportPage: NextPage = () => {
export default CsvExportPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
export const getServerSideProps: GetServerSideProps<Props> = async(args) => {
if (!appConfig.reCaptcha.siteKey) {
return {
notFound: true,
};
}
return getServerSidePropsBase(args);
};
import type { GetServerSideProps, NextPage } from 'next';
import type { NextPage } from 'next';
import Head from 'next/head';
import { route } from 'nextjs-routes';
import React from 'react';
import type { SearchRedirectResult } from 'types/api/search';
import buildUrlNode from 'lib/api/buildUrlNode';
import fetchFactory from 'lib/api/nodeFetch';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import type { Props } from 'lib/next/getServerSideProps';
import { getServerSideProps as getServerSidePropsBase } from 'lib/next/getServerSideProps';
import * as serverTiming from 'lib/next/serverTiming';
import SearchResults from 'ui/pages/SearchResults';
const SearchResultsPage: NextPage = () => {
......@@ -27,53 +19,4 @@ const SearchResultsPage: NextPage = () => {
export default SearchResultsPage;
export const getServerSideProps: GetServerSideProps<Props> = async({ req, res, resolvedUrl, query }) => {
const start = Date.now();
try {
const q = String(query.q);
const url = buildUrlNode('search_check_redirect', undefined, { q });
const redirectsResponse = await fetchFactory(req)(url, {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:next-line timeout property exist for AbortSignal since Node.js 17 - https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
// but @types/node has not updated their types yet, see issue https://github.com/DefinitelyTyped/DefinitelyTyped/issues/60868
signal: AbortSignal.timeout(1_000),
});
const payload = await redirectsResponse.json() as SearchRedirectResult;
if (!payload || typeof payload !== 'object' || !payload.redirect) {
throw Error();
}
const redirectUrl = (() => {
switch (payload.type) {
case 'block': {
return route({ pathname: '/block/[height]', query: { height: q } });
}
case 'address': {
return route({ pathname: '/address/[hash]', query: { hash: payload.parameter || q } });
}
case 'transaction': {
return route({ pathname: '/tx/[hash]', query: { hash: q } });
}
}
})();
if (!redirectUrl) {
throw Error();
}
return {
redirect: {
destination: redirectUrl,
permanent: false,
},
};
} catch (error) {}
const end = Date.now();
serverTiming.appendValue(res, 'query.search.check-redirect', end - start);
return getServerSidePropsBase({ req, res, resolvedUrl, query });
};
export { getServerSideProps } from 'lib/next/getServerSideProps';
......@@ -61,6 +61,8 @@ export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transacti
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'new_block', payload: NewBlockSocketResponse): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'verification_result', payload: SmartContractVerificationResponse): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'total_supply', payload: { total_supply: number}): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'changed_bytecode', payload: Record<string, never>): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: string, payload: unknown): void {
socket.send(JSON.stringify([
...channel,
......
......@@ -4,6 +4,10 @@ const semanticTokens = {
'default': 'blackAlpha.200',
_dark: 'whiteAlpha.200',
},
text: {
'default': 'blackAlpha.800',
_dark: 'whiteAlpha.800',
},
text_secondary: {
'default': 'gray.500',
_dark: 'gray.400',
......
......@@ -32,6 +32,7 @@ export interface Address {
private_tags: Array<AddressTag> | null;
public_tags: Array<AddressTag> | null;
token: TokenInfo | null;
watchlist_address_id: number | null;
watchlist_names: Array<WatchlistName> | null;
}
......
......@@ -30,7 +30,10 @@ export interface SmartContract {
file_path: string;
additional_sources: Array<{ file_path: string; source_code: string }>;
external_libraries: Array<SmartContractExternalLibrary> | null;
compiler_settings: unknown;
compiler_settings?: {
evmVersion?: string;
remappings?: Array<string>;
};
verified_twin_address_hash: string | null;
minimal_proxy_address_hash: string | null;
}
......@@ -124,6 +127,7 @@ export interface SmartContractVerificationConfigRaw {
verification_options: Array<string>;
vyper_compiler_versions: Array<string>;
vyper_evm_versions: Array<string>;
is_rust_verifier_microservice_enabled: boolean;
}
export interface SmartContractVerificationConfig extends SmartContractVerificationConfigRaw {
......
......@@ -7,6 +7,6 @@ export interface DecodedInput {
export interface DecodedInputParams {
name: string;
type: string;
value: string;
value: string | Array<unknown> | Record<string, unknown>;
indexed?: boolean;
}
......@@ -2,9 +2,9 @@ import type { AddressParam } from './addressParams';
export type TokenType = 'ERC-20' | 'ERC-721' | 'ERC-1155';
export interface TokenInfo {
export interface TokenInfo<T extends TokenType = TokenType> {
address: string;
type: TokenType;
type: T;
symbol: string | null;
name: string | null;
decimals: string | null;
......@@ -18,8 +18,6 @@ export interface TokenCounters {
transfers_count: string;
}
export type TokenInfoGeneric<Type extends TokenType> = Omit<TokenInfo, 'type'> & { type: Type };
export interface TokenHolders {
items: Array<TokenHolder>;
next_page_params: TokenHoldersPagination;
......@@ -43,7 +41,7 @@ export interface TokenInstance {
animation_url: string | null;
external_app_url: string | null;
metadata: Record<string, unknown> | null;
owner: AddressParam;
owner: AddressParam | null;
token: TokenInfo;
}
......
import type { AddressParam } from './addressParams';
import type { TokenInfoGeneric, TokenType } from './token';
import type { TokenInfo, TokenType } from './token';
export type Erc20TotalPayload = {
decimals: string | null;
......@@ -18,19 +18,21 @@ export type Erc1155TotalPayload = {
export type TokenTransfer = (
{
token: TokenInfoGeneric<'ERC-20'>;
token: TokenInfo<'ERC-20'>;
total: Erc20TotalPayload;
} |
{
token: TokenInfoGeneric<'ERC-721'>;
token: TokenInfo<'ERC-721'>;
total: Erc721TotalPayload;
} |
{
token: TokenInfoGeneric<'ERC-1155'>;
total: Erc1155TotalPayload | Array<Erc1155TotalPayload>;
token: TokenInfo<'ERC-1155'>;
total: Erc1155TotalPayload;
}
) & TokenTransferBase
export type TokenTotal = Erc20TotalPayload | Erc721TotalPayload | Erc1155TotalPayload;
interface TokenTransferBase {
type: 'token_transfer' | 'token_burning' | 'token_spawning' | 'token_minting';
tx_hash: string;
......
......@@ -9,16 +9,9 @@ export type TransactionRevertReason = {
raw: string;
} | DecodedInput;
export type Transaction = (
{
to: AddressParam;
created_contract: null;
} |
{
to: null;
created_contract: AddressParam;
}
) & {
export type Transaction = {
to: AddressParam | null;
created_contract: AddressParam | null;
hash: string;
result: string;
confirmations: number;
......@@ -50,6 +43,10 @@ export type Transaction = (
tx_types: Array<TransactionType>;
tx_tag: string | null;
actions: Array<TxAction>;
l1_fee?: string;
l1_fee_scalar?: string;
l1_gas_price?: string;
l1_gas_used?: string;
}
export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending;
......
import type { AddressParam } from './addressParams';
import type { TokenInfo } from './token';
import type { Erc1155TotalPayload, Erc721TotalPayload } from './tokenTransfer';
export type TxStateChange = (TxStateChangeCoin | TxStateChangeToken) & {
address: AddressParam;
is_miner: boolean;
balance_before: string | null;
balance_after: string | null;
}
export interface TxStateChangeCoin {
type: 'coin';
change: string;
token: null;
}
export type TxStateChangeToken = TxStateChangeTokenErc20 | TxStateChangeTokenErc721 | TxStateChangeTokenErc1155;
type ChangeDirection = 'from' | 'to';
export interface TxStateChangeTokenErc20 {
type: 'token';
token: TokenInfo<'ERC-20'>;
change: string;
}
export interface TxStateChangeTokenErc721 {
type: 'token';
token: TokenInfo<'ERC-721'>;
change: Array<{
direction: ChangeDirection;
total: Erc721TotalPayload;
}>;
}
export type TxStateChangeTokenErc1155 = TxStateChangeTokenErc1155Single | TxStateChangeTokenErc1155Batch;
export interface TxStateChangeTokenErc1155Single {
type: 'token';
token: TokenInfo<'ERC-1155'>;
change: Array<{
direction: ChangeDirection;
total: Erc1155TotalPayload;
}>;
}
export interface TxStateChangeTokenErc1155Batch {
type: 'token';
token: TokenInfo<'ERC-1155'>;
change: Array<{
direction: ChangeDirection;
total: Array<Erc1155TotalPayload>;
}>;
}
export type TxStateChanges = Array<TxStateChange>;
export type ArrayElement<ArrayType extends Array<unknown>> =
ArrayType extends Array<(infer ElementType)> ? ElementType : never;
export type ExcludeNull<T> = T extends null ? never : T;
export type KeysOfObjectOrNull<T> = keyof ExcludeNull<T>;
type ArrayElement<ArrayType extends Array<unknown>> =
ArrayType extends Array<(infer ElementType)> ? ElementType : never;
export default ArrayElement;
export type ExcludeNull<T> = T extends null ? never : T;
import type { ExcludeNull } from './ExcludeNull';
export type KeysOfObjectOrNull<T> = keyof ExcludeNull<T>;
......@@ -79,7 +79,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
<Th width="17%">Block</Th>
<Th width="17%">Age</Th>
<Th width="16%">Txn</Th>
<Th width="25%">GasUsed</Th>
<Th width="25%">Gas used</Th>
<Th width="25%" isNumeric>Reward { appConfig.network.currency.symbol }</Th>
</Tr>
</Thead>
......
......@@ -4,6 +4,7 @@ import React from 'react';
import type { CsvExportType } from 'types/client/address';
import appConfig from 'configs/app/config';
import svgFileIcon from 'icons/files/csv.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import LinkInternal from 'ui/shared/LinkInternal';
......@@ -17,6 +18,10 @@ interface Props {
const AddressCsvExportLink = ({ className, address, type }: Props) => {
const isMobile = useIsMobile();
if (!appConfig.reCaptcha.siteKey) {
return null;
}
return (
<Tooltip isDisabled={ !isMobile } label="Download CSV">
<LinkInternal
......
......@@ -56,6 +56,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
implementation_name: null,
implementation_address: null,
token: null,
watchlist_address_id: null,
watchlist_names: null,
creation_tx_hash: null,
block_number_balance_updated_at: null,
......
......@@ -2,7 +2,7 @@ import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { erc1155 } from 'mocks/tokens/tokenTransfer';
import { erc1155A } from 'mocks/tokens/tokenTransfer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
......@@ -20,7 +20,7 @@ const hooksConfig = {
test('with token filter and pagination +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ erc1155 ], next_page_params: { block_number: 1 } }),
body: JSON.stringify({ items: [ erc1155A ], next_page_params: { block_number: 1 } }),
}));
const component = await mount(
......@@ -37,7 +37,7 @@ test('with token filter and pagination +@mobile', async({ mount, page }) => {
test('with token filter and no pagination +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ erc1155 ] }),
body: JSON.stringify({ items: [ erc1155A ] }),
}));
const component = await mount(
......
......@@ -26,7 +26,6 @@ import HashStringShorten from 'ui/shared/HashStringShorten';
import Pagination from 'ui/shared/Pagination';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TokenLogo from 'ui/shared/TokenLogo';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
......@@ -161,12 +160,11 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0);
const isActionBarHidden = !tokenFilter && !numActiveFilters && !data?.items.length && !currentAddress;
const items = data?.items?.reduce(flattenTotal, []);
const content = items ? (
const content = data?.items ? (
<>
<Hide below="lg" ssr={ false }>
<TokenTransferTable
data={ items }
data={ data?.items }
baseAddress={ currentAddress }
showTxInfo
top={ isActionBarHidden ? 0 : 80 }
......@@ -187,7 +185,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
/>
) }
<TokenTransferList
data={ items }
data={ data?.items }
baseAddress={ currentAddress }
showTxInfo
enableTimeIncrement
......
import { test, expect } from '@playwright/experimental-ct-react';
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as contractMock from 'mocks/contract/info';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
......@@ -15,6 +16,14 @@ const hooksConfig = {
},
};
const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
// FIXME
// test cases which use socket cannot run in parallel since the socket server always run on the same port
test.describe.configure({ mode: 'serial' });
test('verified with changed byte code +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
......@@ -24,11 +33,32 @@ test('verified with changed byte code +@mobile +@dark-mode', async({ mount, page
const component = await mount(
<TestApp>
<ContractCode addressHash={ addressHash } noSocket/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('verified with changed byte code socket', async({ mount, page, createSocket }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMock.verified),
}));
await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort());
const component = await mount(
<TestApp withSocket>
<ContractCode addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:' + addressHash.toLowerCase());
socketServer.sendMessage(socket, channel, 'changed_bytecode', {});
await expect(component).toHaveScreenshot();
});
......@@ -41,7 +71,7 @@ test('verified with multiple sources +@mobile', async({ mount, page }) => {
await mount(
<TestApp>
<ContractCode addressHash={ addressHash }/>
<ContractCode addressHash={ addressHash } noSocket/>
</TestApp>,
{ hooksConfig },
);
......@@ -60,7 +90,7 @@ test('verified via sourcify', async({ mount, page }) => {
await mount(
<TestApp>
<ContractCode addressHash={ addressHash }/>
<ContractCode addressHash={ addressHash } noSocket/>
</TestApp>,
{ hooksConfig },
);
......@@ -77,7 +107,7 @@ test('self destructed', async({ mount, page }) => {
await mount(
<TestApp>
<ContractCode addressHash={ addressHash }/>
<ContractCode addressHash={ addressHash } noSocket/>
</TestApp>,
{ hooksConfig },
);
......@@ -95,7 +125,7 @@ test('with twin address alert +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractCode addressHash={ addressHash }/>
<ContractCode addressHash={ addressHash } noSocket/>
</TestApp>,
{ hooksConfig },
);
......@@ -112,7 +142,7 @@ test('with proxy address alert +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractCode addressHash={ addressHash }/>
<ContractCode addressHash={ addressHash } noSocket/>
</TestApp>,
{ hooksConfig },
);
......@@ -129,7 +159,7 @@ test('non verified', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ContractCode addressHash={ addressHash }/>
<ContractCode addressHash={ addressHash } noSocket/>
</TestApp>,
{ hooksConfig },
);
......
......@@ -2,8 +2,12 @@ import { Flex, Skeleton, Button, Grid, GridItem, Text, Alert, Link, chakra, Box
import { route } from 'nextjs-routes';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import useApiQuery from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
......@@ -16,6 +20,8 @@ import ContractSourceCode from './ContractSourceCode';
type Props = {
addressHash?: string;
// prop for pw tests only
noSocket?: boolean;
}
const InfoItem = chakra(({ label, value, className }: { label: string; value: string; className?: string }) => (
......@@ -25,15 +31,33 @@ const InfoItem = chakra(({ label, value, className }: { label: string; value: st
</GridItem>
));
const ContractCode = ({ addressHash }: Props) => {
const ContractCode = ({ addressHash, noSocket }: Props) => {
const [ isSocketOpen, setIsSocketOpen ] = React.useState(false);
const [ isChangedBytecodeSocket, setIsChangedBytecodeSocket ] = React.useState<boolean>();
const { data, isLoading, isError } = useApiQuery('contract', {
pathParams: { hash: addressHash },
queryOptions: {
enabled: Boolean(addressHash),
enabled: Boolean(addressHash) && (noSocket || isSocketOpen),
refetchOnMount: false,
},
});
const handleChangedBytecodeMessage: SocketMessage.AddressChangedBytecode['handler'] = React.useCallback(() => {
setIsChangedBytecodeSocket(true);
}, [ ]);
const channel = useSocketChannel({
topic: `addresses:${ addressHash?.toLowerCase() }`,
isDisabled: !addressHash,
onJoin: () => setIsSocketOpen(true),
});
useSocketMessage({
channel,
event: 'changed_bytecode',
handler: handleChangedBytecodeMessage,
});
if (isError) {
return <DataFetchAlert/>;
}
......@@ -117,7 +141,7 @@ const ContractCode = ({ addressHash }: Props) => {
{ data.sourcify_repo_url && <LinkExternal href={ data.sourcify_repo_url } fontSize="md">View contract in Sourcify repository</LinkExternal> }
</Alert>
) }
{ data.is_changed_bytecode && (
{ (data.is_changed_bytecode || isChangedBytecodeSocket) && (
<Alert status="warning">
Warning! Contract bytecode has been changed and does not match the verified one. Therefore, interaction with this smart contract may be risky.
</Alert>
......@@ -177,6 +201,7 @@ const ContractCode = ({ addressHash }: Props) => {
isViper={ Boolean(data.is_vyper_contract) }
filePath={ data.file_path }
additionalSource={ data.additional_sources }
remappings={ data.compiler_settings?.remappings }
/>
) }
{ Boolean(data.compiler_settings) && (
......
......@@ -20,7 +20,7 @@ interface ResultComponentProps<T extends SmartContractMethod> {
interface Props<T extends SmartContractMethod> {
data: T;
onSubmit: (data: T, args: Array<string | Array<string>>) => Promise<ContractMethodCallResult<T>>;
onSubmit: (data: T, args: Array<string | Array<unknown>>) => Promise<ContractMethodCallResult<T>>;
ResultComponent: (props: ResultComponentProps<T>) => JSX.Element | null;
isWrite?: boolean;
}
......@@ -44,13 +44,23 @@ const sortFields = (data: Array<SmartContractMethodInput>) => ([ a ]: [string, s
};
const castFieldValue = (data: Array<SmartContractMethodInput>) => ([ key, value ]: [ string, string ], index: number) => {
if (data[index].type.includes('[]')) {
if (data[index].type.includes('[')) {
return [ key, parseArrayValue(value) ];
}
return [ key, value ];
};
const parseArrayValue = (value: string) => value.replace(/(\[|\])|\s/g, '').split(',');
const parseArrayValue = (value: string) => {
try {
const parsedResult = JSON.parse(value);
if (Array.isArray(parsedResult)) {
return parsedResult;
}
throw new Error('Not an array');
} catch (error) {
return '';
}
};
const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit, ResultComponent, isWrite }: Props<T>) => {
......
......@@ -64,7 +64,7 @@ const ContractMethodField = ({ control, name, valueType, placeholder, setValue,
/>
<InputRightElement w="auto" right={ 1 }>
{ field.value && <InputClearButton onClick={ handleClear } isDisabled={ isDisabled }/> }
{ hasZerosControl && <ContractMethodFieldZeroes onClick={ handleAddZeroesClick }/> }
{ hasZerosControl && <ContractMethodFieldZeroes onClick={ handleAddZeroesClick } isDisabled={ isDisabled }/> }
</InputRightElement>
</InputGroup>
</FormControl>
......
......@@ -20,9 +20,10 @@ import { times } from 'lib/html-entities';
interface Props {
onClick: (power: number) => void;
isDisabled?: boolean;
}
const ContractMethodFieldZeroes = ({ onClick }: Props) => {
const ContractMethodFieldZeroes = ({ onClick, isDisabled }: Props) => {
const [ selectedOption, setSelectedOption ] = React.useState<number | undefined>(18);
const [ customValue, setCustomValue ] = React.useState<number>();
const { isOpen, onToggle, onClose } = useDisclosure();
......@@ -60,6 +61,7 @@ const ContractMethodFieldZeroes = ({ onClick }: Props) => {
colorScheme="gray"
display="inline"
onClick={ handleButtonClick }
isDisabled={ isDisabled }
>
{ times }
<chakra.span>10</chakra.span>
......@@ -76,6 +78,7 @@ const ContractMethodFieldZeroes = ({ onClick }: Props) => {
ml={ 1 }
p={ 0 }
onClick={ onToggle }
isDisabled={ isDisabled }
>
<Icon as={ iconEastMini } transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } boxSize={ 6 }/>
</Button>
......
......@@ -38,7 +38,7 @@ const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => {
},
});
const handleMethodFormSubmit = React.useCallback(async(item: SmartContractReadMethod, args: Array<string | Array<string>>) => {
const handleMethodFormSubmit = React.useCallback(async(item: SmartContractReadMethod, args: Array<string | Array<unknown>>) => {
return apiFetch<'contract_method_query', SmartContractQueryMethodRead>('contract_method_query', {
pathParams: { hash: addressHash },
queryParams: {
......
......@@ -16,9 +16,10 @@ interface Props {
isViper: boolean;
filePath?: string;
additionalSource?: SmartContract['additional_sources'];
remappings?: Array<string>;
}
const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, additionalSource }: Props) => {
const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, additionalSource, remappings }: Props) => {
const heading = (
<Text fontWeight={ 500 }>
<span>Contract source code</span>
......@@ -56,7 +57,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
{ diagramLink }
{ copyToClipboard }
</Flex>
<CodeEditor data={ editorData }/>
<CodeEditor data={ editorData } remappings={ remappings }/>
</section>
);
};
......
import _capitalize from 'lodash/capitalize';
import React from 'react';
import { useAccount, useSigner } from 'wagmi';
import { useAccount, useSigner, useNetwork, useSwitchNetwork } from 'wagmi';
import type { SmartContractWriteMethod } from 'types/api/contract';
......@@ -16,7 +15,7 @@ import ContractCustomAbiAlert from './ContractCustomAbiAlert';
import ContractImplementationAddress from './ContractImplementationAddress';
import ContractMethodCallable from './ContractMethodCallable';
import ContractWriteResult from './ContractWriteResult';
import { getNativeCoinValue, isExtendedError } from './utils';
import { getNativeCoinValue } from './utils';
interface Props {
addressHash?: string;
......@@ -27,6 +26,8 @@ interface Props {
const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
const { data: signer } = useSigner();
const { isConnected } = useAccount();
const { chain } = useNetwork();
const { switchNetworkAsync } = useSwitchNetwork();
const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_write_proxy' : 'contract_methods_write', {
pathParams: { hash: addressHash },
......@@ -51,12 +52,15 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
return contract;
})();
const handleMethodFormSubmit = React.useCallback(async(item: SmartContractWriteMethod, args: Array<string | Array<string>>) => {
const handleMethodFormSubmit = React.useCallback(async(item: SmartContractWriteMethod, args: Array<string | Array<unknown>>) => {
if (!isConnected) {
throw new Error('Wallet is not connected');
}
try {
if (chain?.id && String(chain.id) !== config.network.id) {
await switchNetworkAsync?.(Number(config.network.id));
}
if (!_contract) {
throw new Error('Something went wrong. Try again later.');
}
......@@ -80,26 +84,7 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
});
return { hash: result.hash as string };
} catch (error) {
if (isExtendedError(error)) {
if ('reason' in error && error.reason === 'underlying network changed') {
if ('detectedNetwork' in error && typeof error.detectedNetwork === 'object' && error.detectedNetwork !== null) {
const networkName = error.detectedNetwork.name;
if (networkName) {
throw new Error(
// eslint-disable-next-line max-len
`You connected to ${ _capitalize(networkName) } chain in the wallet, but the current instance of Blockscout is for ${ config.network.name } chain`,
);
}
}
throw new Error('Wrong network detected, please make sure you are switched to the correct network, and try again');
}
}
throw error;
}
}, [ _contract, addressHash, isConnected, signer ]);
}, [ _contract, addressHash, chain, isConnected, signer, switchNetworkAsync ]);
const renderContent = React.useCallback((item: SmartContractWriteMethod, index: number, id: number) => {
return (
......
......@@ -2,13 +2,18 @@ import BigNumber from 'bignumber.js';
import config from 'configs/app/config';
export const getNativeCoinValue = (value: string | Array<string>) => {
export const getNativeCoinValue = (value: string | Array<unknown>) => {
const _value = Array.isArray(value) ? value[0] : value;
if (typeof _value !== 'string') {
return '0';
}
return BigNumber(_value).times(10 ** config.network.currency.decimals).toString();
};
export const addZeroesAllowed = (valueType: string) => {
if (valueType.includes('[]')) {
if (valueType.includes('[')) {
return false;
}
......
......@@ -8,7 +8,7 @@ import type { UserInfo } from 'types/api/account';
import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import { resourceKey } from 'lib/api/resources';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import { getResourceKey } from 'lib/api/useApiQuery';
import useLoginUrl from 'lib/hooks/useLoginUrl';
import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
import WatchlistAddModal from 'ui/watchlist/AddressModal/AddressModal';
......@@ -17,10 +17,10 @@ import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal';
interface Props {
className?: string;
hash: string;
isAdded: boolean;
watchListId: number | null;
}
const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => {
const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
const addModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
const queryClient = useQueryClient();
......@@ -30,15 +30,13 @@ const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => {
const isAuth = Boolean(profileData);
const loginUrl = useLoginUrl();
const watchListQuery = useApiQuery('watchlist', { queryOptions: { enabled: isAdded } });
const handleClick = React.useCallback(() => {
if (!isAuth) {
window.location.assign(loginUrl);
return;
}
isAdded ? deleteModalProps.onOpen() : addModalProps.onOpen();
}, [ addModalProps, deleteModalProps, isAdded, isAuth, loginUrl ]);
watchListId ? deleteModalProps.onOpen() : addModalProps.onOpen();
}, [ addModalProps, deleteModalProps, watchListId, isAuth, loginUrl ]);
const handleAddOrDeleteSuccess = React.useCallback(async() => {
const queryKey = getResourceKey('address', { pathParams: { hash: router.query.hash?.toString() } });
......@@ -57,18 +55,15 @@ const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => {
const formData = React.useMemo(() => {
return {
address_hash: hash,
// FIXME temporary solution
// there is no endpoint in api what can return watchlist address info by its hash
// so we look up in the whole watchlist and hope we can find a necessary item
id: watchListQuery.data?.find((address) => address.address?.hash === hash)?.id || '',
id: String(watchListId),
};
}, [ hash, watchListQuery.data ]);
}, [ hash, watchListId ]);
return (
<>
<Tooltip label={ `${ isAdded ? 'Remove address from Watch list' : 'Add address to Watch list' }` }>
<Tooltip label={ `${ watchListId ? 'Remove address from Watch list' : 'Add address to Watch list' }` }>
<IconButton
isActive={ isAdded }
isActive={ Boolean(watchListId) }
className={ className }
aria-label="edit"
variant="outline"
......@@ -76,7 +71,7 @@ const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => {
pl="6px"
pr="6px"
onClick={ handleClick }
icon={ <Icon as={ isAdded ? starFilledIcon : starOutlineIcon } boxSize={ 5 }/> }
icon={ <Icon as={ watchListId ? starFilledIcon : starOutlineIcon } boxSize={ 5 }/> }
onFocusCapture={ usePreventFocusAfterModalClosing() }
/>
</Tooltip>
......
......@@ -83,7 +83,7 @@ const TokenSelect = ({ onClick }: Props) => {
}
<Tooltip label="Show all tokens">
<Box>
<NextLink href={{ pathname: '/address/[hash]', query: { hash: addressHash, tab: 'tokens' } }} passHref>
<NextLink href={{ pathname: '/address/[hash]', query: { hash: addressHash, tab: 'tokens' } }} passHref legacyBehavior>
<IconButton
aria-label="Show all tokens"
variant="outline"
......
......@@ -55,7 +55,7 @@ const AddressesListItem = ({
) }
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Txn count</Text>
<Text fontSize="sm" variant="secondary">{ Number(item.tx_count).toLocaleString('en') }</Text>
<Text fontSize="sm" variant="secondary">{ Number(item.tx_count).toLocaleString() }</Text>
</HStack>
</ListItemMobile>
);
......
......@@ -60,7 +60,7 @@ const AddressesTableItem = ({
</Td>
) }
<Td isNumeric>
<Text lineHeight="24px">{ Number(item.tx_count).toLocaleString('en') }</Text>
<Text lineHeight="24px">{ Number(item.tx_count).toLocaleString() }</Text>
</Td>
</Tr>
);
......
......@@ -15,7 +15,7 @@ const AppLink = ({ url, external, id, title }: Props) => {
{ title }
</LinkOverlay>
) : (
<NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref>
<NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref legacyBehavior>
<LinkOverlay>
{ title }
</LinkOverlay>
......
......@@ -27,7 +27,7 @@ const AppModalLink = ({ url, external, id }: Props) => {
{ ...buttonProps }
>Launch app</Button>
) : (
<NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref>
<NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref legacyBehavior>
<Button
as="a"
{ ...buttonProps }
......
......@@ -112,7 +112,7 @@ const BlockDetails = ({ query }: Props) => {
title="Size"
hint="Size of the block in bytes"
>
{ data.size.toLocaleString('en') }
{ data.size.toLocaleString() }
</DetailsInfoItem>
<DetailsInfoItem
title="Timestamp"
......@@ -141,7 +141,7 @@ const BlockDetails = ({ query }: Props) => {
{ /* api doesn't return the block processing time yet */ }
{ /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ }
</DetailsInfoItem>
{ !totalReward.isEqualTo(ZERO) && (
{ !appConfig.L2.isL2Network && !totalReward.isEqualTo(ZERO) && (
<DetailsInfoItem
title="Block reward"
hint={
......
......@@ -48,7 +48,7 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
</Flex>
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Size</Text>
<Text variant="secondary">{ data.size.toLocaleString('en') } bytes</Text>
<Text variant="secondary">{ data.size.toLocaleString() } bytes</Text>
</Flex>
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>{ capitalize(getNetworkValidatorTitle()) }</Text>
......@@ -77,10 +77,13 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
) }
</Flex>
</Box>
{ !appConfig.L2.isL2Network && (
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Reward { appConfig.network.currency.symbol }</Text>
<Text variant="secondary">{ totalReward.toFixed() }</Text>
</Flex>
) }
{ !appConfig.L2.isL2Network && (
<Box>
<Text fontWeight={ 500 }>Burnt fees</Text>
<Flex columnGap={ 4 } mt={ 2 }>
......@@ -91,6 +94,7 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<Utilization ml={ 4 } value={ burntFees.div(txFees).toNumber() }/>
</Flex>
</Box>
) }
</ListItemMobile>
);
};
......
......@@ -24,11 +24,11 @@ const BlocksTable = ({ data, top, page }: Props) => {
<Tr>
<Th width="125px">Block</Th>
<Th width="120px">Size, bytes</Th>
<Th width="21%" minW="144px">{ capitalize(getNetworkValidatorTitle()) }</Th>
<Th width={ appConfig.L2.isL2Network ? '37%' : '21%' } minW="144px">{ capitalize(getNetworkValidatorTitle()) }</Th>
<Th width="64px" isNumeric>Txn</Th>
<Th width="35%">Gas used</Th>
<Th width="22%">Reward { appConfig.network.currency.symbol }</Th>
<Th width="22%">Burnt fees { appConfig.network.currency.symbol }</Th>
<Th width={ appConfig.L2.isL2Network ? '63%' : '35%' }>Gas used</Th>
{ !appConfig.L2.isL2Network && <Th width="22%">Reward { appConfig.network.currency.symbol }</Th> }
{ !appConfig.L2.isL2Network && <Th width="22%">Burnt fees { appConfig.network.currency.symbol }</Th> }
</Tr>
</Thead>
<Tbody>
......
......@@ -6,6 +6,7 @@ import React from 'react';
import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config';
import flameIcon from 'icons/flame.svg';
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import { WEI } from 'lib/consts';
......@@ -28,7 +29,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
const txFees = BigNumber(data.tx_fees || 0);
const separatorColor = useColorModeValue('gray.200', 'gray.700');
const burntFeesIconColor = useColorModeValue('gray.500', 'inherit');
return (
<Tr
as={ motion.tr }
......@@ -52,7 +53,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
</Flex>
<BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement }/>
</Td>
<Td fontSize="sm">{ data.size.toLocaleString('en') }</Td>
<Td fontSize="sm">{ data.size.toLocaleString() }</Td>
<Td fontSize="sm">
<AddressLink type="address" alias={ data.miner.name } hash={ data.miner.hash } truncation="constant" display="inline-flex" maxW="100%"/>
</Td>
......@@ -63,6 +64,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
</LinkInternal>
) : data.tx_count }
</Td>
{ !appConfig.L2.isL2Network && (
<Td fontSize="sm">
<Box>{ BigNumber(data.gas_used || 0).toFormat() }</Box>
<Flex mt={ 2 }>
......@@ -79,10 +81,12 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
) }
</Flex>
</Td>
) }
<Td fontSize="sm">{ totalReward.toFixed(8) }</Td>
{ !appConfig.L2.isL2Network && (
<Td fontSize="sm">
<Flex alignItems="center" columnGap={ 1 }>
<Icon as={ flameIcon } boxSize={ 5 } color={ useColorModeValue('gray.500', 'inherit') }/>
<Icon as={ flameIcon } boxSize={ 5 } color={ burntFeesIconColor }/>
{ burntFees.dividedBy(WEI).toFixed(8) }
</Flex>
<Tooltip label="Burnt fees / Txn fees * 100%">
......@@ -91,6 +95,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
</Box>
</Tooltip>
</Td>
) }
</Tr>
);
};
......
......@@ -16,6 +16,7 @@ const hooksConfig = {
const hash = '0x2F99338637F027CFB7494E46B49987457beCC6E3';
const formConfig: SmartContractVerificationConfig = {
is_rust_verifier_microservice_enabled: true,
solidity_compiler_versions: [
'v0.8.17+commit.8df45f5f',
'v0.8.16+commit.07a7930e',
......
......@@ -22,15 +22,6 @@ import ContractVerificationVyperContract from './methods/ContractVerificationVyp
import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile';
import { prepareRequestBody, formatSocketErrors, getDefaultValues } from './utils';
const METHOD_COMPONENTS = {
'flattened-code': <ContractVerificationFlattenSourceCode/>,
'standard-input': <ContractVerificationStandardInput/>,
sourcify: <ContractVerificationSourcify/>,
'multi-part': <ContractVerificationMultiPartFile/>,
'vyper-code': <ContractVerificationVyperContract/>,
'vyper-multi-part': <ContractVerificationVyperMultiPartFile/>,
};
interface Props {
method?: SmartContractVerificationMethod;
config: SmartContractVerificationConfig;
......@@ -122,8 +113,18 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
handler: handleNewSocketMessage,
});
const methods = React.useMemo(() => {
return {
'flattened-code': <ContractVerificationFlattenSourceCode config={ config }/>,
'standard-input': <ContractVerificationStandardInput/>,
sourcify: <ContractVerificationSourcify/>,
'multi-part': <ContractVerificationMultiPartFile/>,
'vyper-code': <ContractVerificationVyperContract config={ config }/>,
'vyper-multi-part': <ContractVerificationVyperMultiPartFile/>,
};
}, [ config ]);
const method = watch('method');
const content = METHOD_COMPONENTS[method?.value] || null;
const content = methods[method?.value] || null;
const methodValue = method?.value;
useUpdateEffect(() => {
......
......@@ -11,9 +11,10 @@ import ContractVerificationFormRow from '../ContractVerificationFormRow';
interface Props {
hint?: string;
isReadOnly?: boolean;
}
const ContractVerificationFieldName = ({ hint }: Props) => {
const ContractVerificationFieldName = ({ hint, isReadOnly }: Props) => {
const { formState, control } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'name'>}) => {
......@@ -26,13 +27,13 @@ const ContractVerificationFieldName = ({ hint }: Props) => {
required
isInvalid={ Boolean(error) }
maxLength={ 255 }
isDisabled={ formState.isSubmitting }
isDisabled={ formState.isSubmitting || isReadOnly }
autoComplete="off"
/>
<InputPlaceholder text="Contract name" error={ error }/>
</FormControl>
);
}, [ formState.errors, formState.isSubmitting ]);
}, [ formState.errors, formState.isSubmitting, isReadOnly ]);
return (
<ContractVerificationFormRow>
......
import React from 'react';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldAutodetectArgs from '../fields/ContractVerificationFieldAutodetectArgs';
import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode';
......@@ -10,16 +12,16 @@ import ContractVerificationFieldLibraries from '../fields/ContractVerificationFi
import ContractVerificationFieldName from '../fields/ContractVerificationFieldName';
import ContractVerificationFieldOptimization from '../fields/ContractVerificationFieldOptimization';
const ContractVerificationFlattenSourceCode = () => {
const ContractVerificationFlattenSourceCode = ({ config }: { config: SmartContractVerificationConfig }) => {
return (
<ContractVerificationMethod title="Contract verification via Solidity (flattened source code)">
<ContractVerificationFieldName/>
<ContractVerificationFieldIsYul/>
{ !config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldName/> }
{ config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldIsYul/> }
<ContractVerificationFieldCompiler/>
<ContractVerificationFieldEvmVersion/>
<ContractVerificationFieldOptimization/>
<ContractVerificationFieldCode/>
<ContractVerificationFieldAutodetectArgs/>
{ !config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldAutodetectArgs/> }
<ContractVerificationFieldLibraries/>
</ContractVerificationMethod>
);
......
import React from 'react';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode';
import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler';
import ContractVerificationFieldConstructorArgs from '../fields/ContractVerificationFieldConstructorArgs';
import ContractVerificationFieldEvmVersion from '../fields/ContractVerificationFieldEvmVersion';
import ContractVerificationFieldName from '../fields/ContractVerificationFieldName';
const ContractVerificationVyperContract = () => {
const ContractVerificationVyperContract = ({ config }: { config: SmartContractVerificationConfig }) => {
return (
<ContractVerificationMethod title="Contract verification via Vyper (contract)">
<ContractVerificationFieldName hint="Must match the name specified in the code."/>
<ContractVerificationFieldName hint="Must match the name specified in the code." isReadOnly/>
<ContractVerificationFieldCompiler isVyper/>
{ config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldEvmVersion isVyper/> }
<ContractVerificationFieldCode isVyper/>
<ContractVerificationFieldConstructorArgs/>
{ !config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldConstructorArgs/> }
</ContractVerificationMethod>
);
};
......
......@@ -14,7 +14,7 @@ interface MethodOption {
export interface FormFieldsFlattenSourceCode {
method: MethodOption;
is_yul: boolean;
name: string;
name: string | undefined;
compiler: Option | null;
evm_version: Option | null;
is_optimization_enabled: boolean;
......@@ -53,9 +53,10 @@ export interface FormFieldsMultiPartFile {
export interface FormFieldsVyperContract {
method: MethodOption;
name: string;
evm_version: Option | null;
compiler: Option | null;
code: string;
constructor_args: string;
constructor_args: string | undefined;
}
export interface FormFieldsVyperMultiPartFile {
......
......@@ -84,8 +84,9 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
value: 'vyper-code' as const,
label: METHOD_LABELS['vyper-code'],
},
name: '',
name: 'Vyper_contract',
compiler: null,
evm_version: null,
code: '',
constructor_args: '',
},
......@@ -113,6 +114,13 @@ export function getDefaultValues(method: SmartContractVerificationMethod, config
}
}
if (config.is_rust_verifier_microservice_enabled) {
if (method === 'flattened-code') {
'name' in defaultValues && (defaultValues.name = undefined);
'autodetect_constructor_args' in defaultValues && (defaultValues.autodetect_constructor_args = false);
}
}
return defaultValues;
}
......@@ -145,7 +153,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
is_optimization_enabled: _data.is_optimization_enabled,
is_yul_contract: _data.is_yul,
optimization_runs: _data.optimization_runs,
contract_name: _data.name,
contract_name: _data.name || undefined,
libraries: reduceLibrariesArray(_data.libraries),
evm_version: _data.evm_version?.value,
autodetect_constructor_args: _data.autodetect_constructor_args,
......@@ -196,6 +204,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
return {
compiler_version: _data.compiler?.value,
evm_version: _data.evm_version?.value,
source_code: _data.code,
contract_name: _data.name,
constructor_args: _data.constructor_args,
......
......@@ -20,10 +20,11 @@ type Props = { item: DepositsItem };
const DepositsListItem = ({ item }: Props) => {
const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
const items = [
{
name: 'L1 block No',
value: (
return (
<ListItemMobileGrid.Container>
<ListItemMobileGrid.Label>L1 block No</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height]', query: { height: item.l1_block_number.toString() } }) }
fontWeight={ 600 }
......@@ -32,11 +33,10 @@ const DepositsListItem = ({ item }: Props) => {
<Icon as={ blockIcon } boxSize={ 6 } mr={ 1 }/>
{ item.l1_block_number }
</LinkExternal>
),
},
{
name: 'L2 txn hash',
value: (
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>L2 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<LinkInternal
href={ route({ pathname: '/tx/[hash]', query: { hash: item.l2_tx_hash } }) }
display="flex"
......@@ -48,15 +48,13 @@ const DepositsListItem = ({ item }: Props) => {
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l2_tx_hash }/></Box>
</LinkInternal>
),
},
{
name: 'Age',
value: timeAgo,
},
{
name: 'L1 txn hash',
value: (
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>{ timeAgo }</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>L1 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
maxW="100%"
......@@ -66,11 +64,10 @@ const DepositsListItem = ({ item }: Props) => {
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box>
</LinkExternal>
),
},
{
name: 'L1 txn origin',
value: (
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>L1 txn origin</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/address/[hash]', query: { hash: item.l1_tx_origin } }) }
maxW="100%"
......@@ -80,15 +77,13 @@ const DepositsListItem = ({ item }: Props) => {
<AddressIcon address={{ hash: item.l1_tx_origin, is_contract: false, implementation_name: '' }} mr={ 2 }/>
<Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_origin }/></Box>
</LinkExternal>
),
},
{
name: 'Gas limit',
value: BigNumber(item.l2_tx_gas_limit).toFormat(),
},
];
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>Gas limit</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>{ BigNumber(item.l2_tx_gas_limit).toFormat() }</ListItemMobileGrid.Value>
return <ListItemMobileGrid items={ items } gridTemplateColumns="92px auto"/>;
</ListItemMobileGrid.Container>
);
};
export default DepositsListItem;
......@@ -7,6 +7,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities';
......@@ -17,12 +18,20 @@ import LinkInternal from 'ui/shared/LinkInternal';
import LatestBlocksItem from './LatestBlocksItem';
import LatestBlocksItemSkeleton from './LatestBlocksItemSkeleton';
const BLOCK_HEIGHT = 166;
const BLOCK_HEIGHT_L1 = 166;
const BLOCK_HEIGHT_L2 = 112;
const BLOCK_MARGIN = 12;
const LatestBlocks = () => {
const blockHeight = appConfig.L2.isL2Network ? BLOCK_HEIGHT_L2 : BLOCK_HEIGHT_L1;
const isMobile = useIsMobile();
const blocksMaxCount = isMobile ? 2 : 3;
// const blocksMaxCount = isMobile ? 2 : 3;
let blocksMaxCount: number;
if (appConfig.L2.isL2Network) {
blocksMaxCount = isMobile ? 4 : 5;
} else {
blocksMaxCount = isMobile ? 2 : 3;
}
const { data, isLoading, isError } = useApiQuery('homepage_blocks');
const queryClient = useQueryClient();
......@@ -60,7 +69,7 @@ const LatestBlocks = () => {
<VStack
spacing={ `${ BLOCK_MARGIN }px` }
mb={ 6 }
height={ `${ BLOCK_HEIGHT * blocksMaxCount + BLOCK_MARGIN * (blocksMaxCount - 1) }px` }
height={ `${ blockHeight * blocksMaxCount + BLOCK_MARGIN * (blocksMaxCount - 1) }px` }
overflow="hidden"
>
{ Array.from(Array(blocksMaxCount)).map((item, index) => <LatestBlocksItemSkeleton key={ index }/>) }
......@@ -92,9 +101,9 @@ const LatestBlocks = () => {
</Text>
</Box>
) }
<VStack spacing={ `${ BLOCK_MARGIN }px` } mb={ 4 } height={ `${ BLOCK_HEIGHT * blocksCount + BLOCK_MARGIN * (blocksCount - 1) }px` } overflow="hidden">
<VStack spacing={ `${ BLOCK_MARGIN }px` } mb={ 4 } height={ `${ blockHeight * blocksCount + BLOCK_MARGIN * (blocksCount - 1) }px` } overflow="hidden">
<AnimatePresence initial={ false } >
{ dataToShow.map((block => <LatestBlocksItem key={ block.height } block={ block } h={ BLOCK_HEIGHT }/>)) }
{ dataToShow.map((block => <LatestBlocksItem key={ block.height } block={ block } h={ blockHeight }/>)) }
</AnimatePresence>
</VStack>
<Flex justifyContent="center">
......
......@@ -13,6 +13,7 @@ import React from 'react';
import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg';
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
......@@ -56,11 +57,14 @@ const LatestBlocksItem = ({ block, h }: Props) => {
<Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" fontSize="sm">
<GridItem>Txn</GridItem>
<GridItem><Text variant="secondary">{ block.tx_count }</Text></GridItem>
{ /* */ }
{ !appConfig.L2.isL2Network && (
<>
<GridItem>Reward</GridItem>
<GridItem><Text variant="secondary">{ totalReward.toFixed() }</Text></GridItem>
<GridItem>Miner</GridItem>
<GridItem><AddressLink type="address" alias={ block.miner.name } hash={ block.miner.hash } truncation="constant" maxW="100%"/></GridItem>
</>
) }
</Grid>
</Box>
);
......
......@@ -8,6 +8,8 @@ import {
} from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
const LatestBlocksItemSkeleton = () => {
return (
<Box
......@@ -27,10 +29,14 @@ const LatestBlocksItemSkeleton = () => {
<Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" fontSize="sm">
<GridItem><Skeleton w="30px" h="15px"/></GridItem>
<GridItem><Skeleton w="93px" h="15px"/></GridItem>
{ !appConfig.L2.isL2Network && (
<>
<GridItem><Skeleton w="30px" h="15px"/></GridItem>
<GridItem><Skeleton w="93px" h="15px"/></GridItem>
<GridItem><Skeleton w="30px" h="15px"/></GridItem>
<GridItem><Skeleton w="93px" h="15px"/></GridItem>
</>
) }
</Grid>
</Box>
);
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as depositMock from 'mocks/deposits/deposits';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import LatestDeposits from './LatestDeposits';
test('default view +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(buildApiUrl('homepage_deposits'), (route) => route.fulfill({
status: 200,
body: JSON.stringify(depositMock.data.items),
}));
const component = await mount(
<TestApp>
<LatestDeposits/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Box, Flex, Text, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import useApiQuery from 'lib/api/useApiQuery';
import useGradualIncrement from 'lib/hooks/useGradualIncrement';
import useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import LinkInternal from 'ui/shared/LinkInternal';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import LatestDepositsItem from './LatestDepositsItem';
import LatestDepositsItemSkeleton from './LatestDepositsItemSkeleton';
const LatestDeposits = () => {
const isMobile = useIsMobile();
const itemsCount = isMobile ? 2 : 6;
const { data, isLoading, isError } = useApiQuery('homepage_deposits');
const [ num, setNum ] = useGradualIncrement(0);
const [ socketAlert, setSocketAlert ] = React.useState('');
const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please reload page.');
}, []);
const handleSocketError = React.useCallback(() => {
setSocketAlert('An error has occurred while fetching new transactions. Please reload page.');
}, []);
const handleNewDepositMessage: SocketMessage.NewDeposits['handler'] = React.useCallback((payload) => {
setNum(payload.deposits);
}, [ setNum ]);
const channel = useSocketChannel({
topic: 'optimism_deposits:new_deposits',
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: false,
});
useSocketMessage({
channel,
event: 'deposits',
handler: handleNewDepositMessage,
});
if (isLoading) {
return (
<>
<Skeleton h="32px" w="100%" borderBottomLeftRadius={ 0 } borderBottomRightRadius={ 0 }/>
{ Array.from(Array(itemsCount)).map((item, index) => <LatestDepositsItemSkeleton key={ index }/>) }
</>
);
}
if (isError) {
return <Text mt={ 4 }>No data. Please reload page.</Text>;
}
if (data) {
const depositsUrl = route({ pathname: '/deposits' });
return (
<>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ depositsUrl } num={ num } alert={ socketAlert } type="deposit"/>
<Box mb={{ base: 3, lg: 4 }}>
{ data.slice(0, itemsCount).map((item => <LatestDepositsItem key={ item.l2_tx_hash } item={ item }/>)) }
</Box>
<Flex justifyContent="center">
<LinkInternal fontSize="sm" href={ depositsUrl }>View all deposits</LinkInternal>
</Flex>
</>
);
}
return null;
};
export default LatestDeposits;
import {
Box,
Flex,
Grid,
Icon,
Text,
} from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { DepositsItem } from 'types/api/deposits';
import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg';
import txIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs';
import useIsMobile from 'lib/hooks/useIsMobile';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
type Props = {
item: DepositsItem;
}
const LatestTxsItem = ({ item }: Props) => {
const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
const isMobile = useIsMobile();
const l1BlockLink = (
<LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height]', query: { height: item.l1_block_number.toString() } }) }
fontWeight={ 700 }
display="inline-flex"
mr={ 2 }
>
<Icon as={ blockIcon } boxSize="30px" mr={ 1 }/>
{ item.l1_block_number }
</LinkExternal>
);
const l1TxLink = (
<LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
maxW="100%"
display="inline-flex"
alignItems="center"
overflow="hidden"
>
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box>
</LinkExternal>
);
const l2TxLink = (
<LinkInternal
href={ route({ pathname: '/tx/[hash]', query: { hash: item.l2_tx_hash } }) }
display="flex"
alignItems="center"
overflow="hidden"
w="100%"
>
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l2_tx_hash }/></Box>
</LinkInternal>
);
const content = (() => {
if (isMobile) {
return (
<>
<Flex justifyContent="space-between" alignItems="center" mb={ 1 }>
{ l1BlockLink }
<Text variant="secondary">{ timeAgo }</Text>
</Flex>
<Grid gridTemplateColumns="56px auto">
<Text lineHeight="30px">L1 txn</Text>
{ l1TxLink }
<Text lineHeight="30px">L2 txn</Text>
{ l2TxLink }
</Grid>
</>
);
}
return (
<Grid width="100%" columnGap={ 4 } rowGap={ 2 } templateColumns="max-content max-content auto" w="100%">
{ l1BlockLink }
<Text lineHeight="30px">L1 txn</Text>
{ l1TxLink }
<Text variant="secondary">{ timeAgo }</Text>
<Text lineHeight="30px">L2 txn</Text>
{ l2TxLink }
</Grid>
);
})();
return (
<Box
width="100%"
borderTop="1px solid"
borderColor="divider"
py={ 4 }
px={{ base: 0, lg: 4 }}
_last={{ borderBottom: '1px solid', borderColor: 'divider' }}
fontSize="sm"
>
{ content }
</Box>
);
};
export default React.memo(LatestTxsItem);
import {
Box,
Flex,
Skeleton,
} from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
const LatestTxsItemSkeleton = () => {
const isMobile = useIsMobile();
return (
<Box
width="100%"
borderTop="1px solid"
borderColor="divider"
py={ 4 }
px={{ base: 0, lg: 4 }}
_last={{ borderBottom: '1px solid', borderColor: 'divider' }}
>
{ isMobile && (
<>
<Flex justifyContent="space-between" alignItems="center" mt={ 1 } mb={ 4 }>
<Skeleton w="120px" h="20px"></Skeleton>
<Skeleton w="80px" h="20px"></Skeleton>
</Flex>
<Skeleton w="100%" h="20px" mb={ 2 }></Skeleton>
<Skeleton w="100%" h="20px" mb={ 2 }></Skeleton>
</>
) }
{ !isMobile && (
<>
<Flex w="100%" mb={ 2 } h="30px" alignItems="center" justifyContent="space-between">
<Skeleton w="120px" h="20px"></Skeleton>
<Skeleton w="calc(100% - 120px - 48px)" h="20px"></Skeleton>
</Flex><Flex w="100%" h="30px" alignItems="center" justifyContent="space-between">
<Skeleton w="120px" h="20px"></Skeleton>
<Skeleton w="calc(100% - 120px - 48px)" h="20px"></Skeleton>
</Flex>
</>
) }
</Box>
);
};
export default LatestTxsItemSkeleton;
import { Box, Heading, Flex, Text, Skeleton } from '@chakra-ui/react';
import { Box, Flex, Text, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
......@@ -18,10 +18,8 @@ const LatestTransactions = () => {
const { num, socketAlert } = useNewTxsSocket();
let content;
if (isLoading) {
content = (
return (
<>
<Skeleton h="32px" w="100%" borderBottomLeftRadius={ 0 } borderBottomRightRadius={ 0 }/>
{ Array.from(Array(txsCount)).map((item, index) => <LatestTxsItemSkeleton key={ index }/>) }
......@@ -30,12 +28,12 @@ const LatestTransactions = () => {
}
if (isError) {
content = <Text mt={ 4 }>No data. Please reload page.</Text>;
return <Text mt={ 4 }>No data. Please reload page.</Text>;
}
if (data) {
const txsUrl = route({ pathname: '/txs' });
content = (
return (
<>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ txsUrl } num={ num } alert={ socketAlert }/>
<Box mb={{ base: 3, lg: 4 }}>
......@@ -48,12 +46,7 @@ const LatestTransactions = () => {
);
}
return (
<Box flexGrow={ 1 }>
<Heading as="h4" size="sm" mb={ 4 }>Latest transactions</Heading>
{ content }
</Box>
);
return null;
};
export default LatestTransactions;
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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