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__ ...@@ -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_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_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_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_AD_ADBUTLER_ON=__PLACEHOLDER_FOR_NEXT_PUBLIC_AD_ADBUTLER_ON__
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=__PLACEHOLDER_FOR_NEXT_PUBLIC_GRAPHIQL_TRANSACTION__ NEXT_PUBLIC_GRAPHIQL_TRANSACTION=__PLACEHOLDER_FOR_NEXT_PUBLIC_GRAPHIQL_TRANSACTION__
......
...@@ -28,7 +28,7 @@ jobs: ...@@ -28,7 +28,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Inject slug/short variables - name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4 uses: rlespinasse/github-slug-action@v4.4.1
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
......
...@@ -24,7 +24,7 @@ jobs: ...@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Inject slug/short variables - name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4 uses: rlespinasse/github-slug-action@v4.4.1
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
......
...@@ -24,7 +24,7 @@ jobs: ...@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Inject slug/short variables - name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4 uses: rlespinasse/github-slug-action@v4.4.1
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
......
...@@ -365,6 +365,7 @@ ...@@ -365,6 +365,7 @@
"options": [ "options": [
"", "",
"--update-snapshots", "--update-snapshots",
"--ui",
], ],
"default": "" "default": ""
}, },
......
This diff is collapsed.
This diff is collapsed.
...@@ -16,7 +16,9 @@ NEXT_PUBLIC_NETWORK_SMALL_LOGO= ...@@ -16,7 +16,9 @@ NEXT_PUBLIC_NETWORK_SMALL_LOGO=
NEXT_PUBLIC_NETWORK_RPC_URL=https://core.poa.network NEXT_PUBLIC_NETWORK_RPC_URL=https://core.poa.network
NEXT_PUBLIC_IS_TESTNET=true 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_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace/eth-goerli.json
NEXT_PUBLIC_IS_L2_NETWORK=false
# api config # api config
NEXT_PUBLIC_API_HOST=blockscout.com NEXT_PUBLIC_API_HOST=blockscout.com
NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.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 = { ...@@ -7,6 +7,49 @@ const config: NextjsOptions = {
// We recommend adjusting this value in production, or using tracesSampler // We recommend adjusting this value in production, or using tracesSampler
// for finer control // for finer control
tracesSampleRate: 1.0, 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; export default config;
...@@ -9,6 +9,49 @@ export const config: Sentry.BrowserOptions = { ...@@ -9,6 +9,49 @@ export const config: Sentry.BrowserOptions = {
// We recommend adjusting this value in production, or using tracesSampler // We recommend adjusting this value in production, or using tracesSampler
// for finer control // for finer control
tracesSampleRate: 1.0, 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 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: ...@@ -5,7 +5,7 @@ blockscout:
app: blockscout app: blockscout
enabled: true enabled: true
image: 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: replicas:
app: 1 app: 1
# init container # init container
...@@ -83,9 +83,9 @@ blockscout: ...@@ -83,9 +83,9 @@ blockscout:
_default: prod _default: prod
ECTO_USE_SSL: ECTO_USE_SSL:
_default: 'false' _default: 'false'
ENABLE_RUST_VERIFICATION_SERVICE: MICROSERVICE_SC_VERIFIER_ENABLED:
_default: 'true' _default: 'true'
RUST_VERIFICATION_SERVICE_URL: MICROSERVICE_SC_VERIFIER_URL:
_default: http://sc-verifier-svc:8043 _default: http://sc-verifier-svc:8043
ACCOUNT_ENABLED: ACCOUNT_ENABLED:
_default: 'true' _default: 'true'
......
...@@ -33,7 +33,7 @@ blockscout: ...@@ -33,7 +33,7 @@ blockscout:
ACCOUNT_AUTH0_CLIENT_ID: ACCOUNT_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:xasZDDg1IuGbKSTm9Opjh43RcqDSzE3dMdmynP0gejo=,iv:TTRqPOkwMxAQIpmc7DklBoPdZzn3FsAprxwXtHY/KSk=,tag:ZI7zQ5zyjjYOqjzy2DyY7Q==,type:str] _default: ENC[AES256_GCM,data:xasZDDg1IuGbKSTm9Opjh43RcqDSzE3dMdmynP0gejo=,iv:TTRqPOkwMxAQIpmc7DklBoPdZzn3FsAprxwXtHY/KSk=,tag:ZI7zQ5zyjjYOqjzy2DyY7Q==,type:str]
ACCOUNT_AUTH0_CLIENT_SECRET: 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: ACCOUNT_AUTH0_CALLBACK_URL:
_default: ENC[AES256_GCM,data:GhyNTVlRvDpfW6swKExjVy65I3S+q6ITJWH8xp5TXuudp7hAtI4ujkYmXaa7D1BCaineTLhQ8UgHXFAgodOlrsGP1g41LIh6T2SGhrXTGGLbFw==,iv:ZflinC/h35jkrVAx0x7Z7U9mRridl85f7f6nfoc0Xts=,tag:40zL2KM9uPg/lRYv5JMxqw==,type:str] _default: ENC[AES256_GCM,data:GhyNTVlRvDpfW6swKExjVy65I3S+q6ITJWH8xp5TXuudp7hAtI4ujkYmXaa7D1BCaineTLhQ8UgHXFAgodOlrsGP1g41LIh6T2SGhrXTGGLbFw==,iv:ZflinC/h35jkrVAx0x7Z7U9mRridl85f7f6nfoc0Xts=,tag:40zL2KM9uPg/lRYv5JMxqw==,type:str]
ACCOUNT_AUTH0_LOGOUT_RETURN_URL: ACCOUNT_AUTH0_LOGOUT_RETURN_URL:
...@@ -144,8 +144,8 @@ sops: ...@@ -144,8 +144,8 @@ sops:
azure_kv: [] azure_kv: []
hc_vault: [] hc_vault: []
age: [] age: []
lastmodified: "2023-03-08T15:55:49Z" lastmodified: "2023-04-18T09:26:42Z"
mac: ENC[AES256_GCM,data:nl/CflZ5t09n3swP1vpR2rVjLVIW+H10/FCImDgeWuwt7F+0Whko3/UrPMypdfJeqiuCjWCmuynqjYqqZn2zjKabvc9bdo/RiQCyrdm7XIA+REA9XnKPnghYjkJYNm5kOLct5zif9Rq4wTfPOIpMRKnYvXrpnmiz5tE/lFXbYMw=,iv:mifwaplxQSJb5Q1CPWgR3hT5bTHTodNAoBEDKtdVLHI=,tag:rCAgVJxV52JV8O4CEZuLRg==,type:str] mac: ENC[AES256_GCM,data:4MdP3Fi3SDxyXjhyI7jogYdigziLkp7oh4M6BthtT2FuUu+XAGnONAdccxULJfXtKS6ue27BtV/pKWhdKeOqKIN23BJERqa4cFgYlyd85XAyL400/fEKEO4nCe2c9VIqNRUQls28/WkRjzRjvzQMMTIRIAJvfrJUYRpIabXWqAo=,iv:Z4shgbH8uJNuoJvmyyQjQ+fZsXuSC3nIGYb6GYO/nac=,tag:IDZvpGiZH41IwAgSidF3xQ==,type:str]
pgp: pgp:
- created_at: "2022-09-14T13:42:28Z" - created_at: "2022-09-14T13:42:28Z"
enc: | enc: |
......
...@@ -108,9 +108,9 @@ blockscout: ...@@ -108,9 +108,9 @@ blockscout:
_default: 'true' _default: 'true'
CHAIN_ID: CHAIN_ID:
_default: 5 _default: 5
ENABLE_RUST_VERIFICATION_SERVICE: MICROSERVICE_SC_VERIFIER_ENABLED:
_default: 'true' _default: 'true'
RUST_VERIFICATION_SERVICE_URL: MICROSERVICE_SC_VERIFIER_URL:
_default: http://eth-bytecode-db-svc.eth-bytecode-db-testing.svc.cluster.local:80 _default: http://eth-bytecode-db-svc.eth-bytecode-db-testing.svc.cluster.local:80
INDEXER_MEMORY_LIMIT: INDEXER_MEMORY_LIMIT:
_default: 5 _default: 5
......
This diff is collapsed.
...@@ -18,13 +18,13 @@ import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } f ...@@ -18,13 +18,13 @@ import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } f
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts'; import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig } from 'types/api/contract'; import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig } from 'types/api/contract';
import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts'; 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 { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction'; import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log'; import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
import type { OutputRootsResponse } from 'types/api/outputRoots'; import type { OutputRootsResponse } from 'types/api/outputRoots';
import type { RawTracesResponse } from 'types/api/rawTrace'; 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 { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats';
import type { import type {
TokenCounters, TokenCounters,
...@@ -39,9 +39,10 @@ import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/toke ...@@ -39,9 +39,10 @@ import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/toke
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction'; import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
import type { TxnBatchesResponse } from 'types/api/txnBatches'; import type { TxnBatchesResponse } from 'types/api/txnBatches';
import type { TTxsFilters } from 'types/api/txsFilters'; import type { TTxsFilters } from 'types/api/txsFilters';
import type { TxStateChanges } from 'types/api/txStateChanges';
import type { VisualizedContract } from 'types/api/visualization'; import type { VisualizedContract } from 'types/api/visualization';
import type { WithdrawalsResponse } from 'types/api/withdrawals'; 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'; import appConfig from 'configs/app/config';
...@@ -162,6 +163,10 @@ export const RESOURCES = { ...@@ -162,6 +163,10 @@ export const RESOURCES = {
path: '/api/v2/transactions/:hash/raw-trace', path: '/api/v2/transactions/:hash/raw-trace',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
}, },
tx_state_changes: {
path: '/api/v2/transactions/:hash/state-changes',
pathParams: [ 'hash' as const ],
},
// ADDRESSES // ADDRESSES
addresses: { addresses: {
...@@ -334,6 +339,9 @@ export const RESOURCES = { ...@@ -334,6 +339,9 @@ export const RESOURCES = {
homepage_blocks: { homepage_blocks: {
path: '/api/v2/main-page/blocks', path: '/api/v2/main-page/blocks',
}, },
homepage_deposits: {
path: '/api/v2/main-page/optimism-deposits',
},
homepage_txs: { homepage_txs: {
path: '/api/v2/main-page/transactions', path: '/api/v2/main-page/transactions',
}, },
...@@ -484,6 +492,7 @@ Q extends 'homepage_chart_txs' ? ChartTransactionResponse : ...@@ -484,6 +492,7 @@ Q extends 'homepage_chart_txs' ? ChartTransactionResponse :
Q extends 'homepage_chart_market' ? ChartMarketResponse : Q extends 'homepage_chart_market' ? ChartMarketResponse :
Q extends 'homepage_blocks' ? Array<Block> : Q extends 'homepage_blocks' ? Array<Block> :
Q extends 'homepage_txs' ? Array<Transaction> : Q extends 'homepage_txs' ? Array<Transaction> :
Q extends 'homepage_deposits' ? Array<DepositsItem> :
Q extends 'homepage_indexing_status' ? IndexingStatus : Q extends 'homepage_indexing_status' ? IndexingStatus :
Q extends 'stats_counters' ? Counters : Q extends 'stats_counters' ? Counters :
Q extends 'stats_lines' ? StatsCharts : Q extends 'stats_lines' ? StatsCharts :
...@@ -498,6 +507,7 @@ Q extends 'tx_internal_txs' ? InternalTransactionsResponse : ...@@ -498,6 +507,7 @@ Q extends 'tx_internal_txs' ? InternalTransactionsResponse :
Q extends 'tx_logs' ? LogsResponseTx : Q extends 'tx_logs' ? LogsResponseTx :
Q extends 'tx_token_transfers' ? TokenTransferResponse : Q extends 'tx_token_transfers' ? TokenTransferResponse :
Q extends 'tx_raw_trace' ? RawTracesResponse : Q extends 'tx_raw_trace' ? RawTracesResponse :
Q extends 'tx_state_changes' ? TxStateChanges :
Q extends 'addresses' ? AddressesResponse : Q extends 'addresses' ? AddressesResponse :
Q extends 'address' ? Address : Q extends 'address' ? Address :
Q extends 'address_counters' ? AddressCounters : Q extends 'address_counters' ? AddressCounters :
...@@ -519,6 +529,7 @@ Q extends 'token_instance_transfers' ? TokenInstanceTransferResponse : ...@@ -519,6 +529,7 @@ Q extends 'token_instance_transfers' ? TokenInstanceTransferResponse :
Q extends 'token_inventory' ? TokenInventoryResponse : Q extends 'token_inventory' ? TokenInventoryResponse :
Q extends 'tokens' ? TokensResponse : Q extends 'tokens' ? TokensResponse :
Q extends 'search' ? SearchResult : Q extends 'search' ? SearchResult :
Q extends 'search_check_redirect' ? SearchRedirectResult :
Q extends 'contract' ? SmartContract : Q extends 'contract' ? SmartContract :
Q extends 'contract_methods_read' ? Array<SmartContractReadMethod> : Q extends 'contract_methods_read' ? Array<SmartContractReadMethod> :
Q extends 'contract_methods_read_proxy' ? Array<SmartContractReadMethod> : Q extends 'contract_methods_read_proxy' ? Array<SmartContractReadMethod> :
......
...@@ -15,3 +15,5 @@ export const YEAR = 365 * DAY; ...@@ -15,3 +15,5 @@ export const YEAR = 365 * DAY;
export const Kb = 1_000; export const Kb = 1_000;
export const Mb = 1_000 * Kb; export const Mb = 1_000 * Kb;
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
...@@ -15,7 +15,10 @@ const REPORT_URI = process.env.SENTRY_CSP_REPORT_URI; ...@@ -15,7 +15,10 @@ const REPORT_URI = process.env.SENTRY_CSP_REPORT_URI;
export function app(): CspDev.DirectiveDescriptor { export function app(): CspDev.DirectiveDescriptor {
return { return {
'default-src': [ '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': [ 'connect-src': [
......
...@@ -9,18 +9,21 @@ export function googleAnalytics(): CspDev.DirectiveDescriptor { ...@@ -9,18 +9,21 @@ export function googleAnalytics(): CspDev.DirectiveDescriptor {
return { return {
'connect-src': [ 'connect-src': [
'*.google-analytics.com',
'*.analytics.google.com',
'https://www.googletagmanager.com', 'https://www.googletagmanager.com',
'https://www.google-analytics.com',
'https://stats.g.doubleclick.net', 'https://stats.g.doubleclick.net',
], ],
'script-src': [ 'script-src': [
// inline script hash, see ui/shared/GoogleAnalytics.tsx // inline script hash, see ui/shared/GoogleAnalytics.tsx
'\'sha256-NTmEg2dBnojQfTYrYJEmp3nG7V66756qPbQMCIBrctk=\'', '\'sha256-NTmEg2dBnojQfTYrYJEmp3nG7V66756qPbQMCIBrctk=\'',
'https://www.googletagmanager.com', 'https://www.googletagmanager.com',
'https://www.google-analytics.com', '*.google-analytics.com',
'*.analytics.google.com',
], ],
'img-src': [ '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 { export default function getErrorStatusCode(error: Error | undefined): number | undefined {
return ( const cause = getErrorCause(error);
error && 'cause' in error && return cause && 'status' in cause && typeof cause.status === 'number' ? cause.status : undefined;
typeof error.cause === 'object' && error.cause !== null &&
'status' in error.cause && typeof error.cause.status === 'number' &&
error.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'; ...@@ -3,6 +3,7 @@ import type { Channel } from 'phoenix';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address'; import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import type { NewBlockSocketResponse } from 'types/api/block'; import type { NewBlockSocketResponse } from 'types/api/block';
import type { SmartContractVerificationResponse } from 'types/api/contract'; import type { SmartContractVerificationResponse } from 'types/api/contract';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
...@@ -10,8 +11,10 @@ export type SocketMessageParams = SocketMessage.NewBlock | ...@@ -10,8 +11,10 @@ export type SocketMessageParams = SocketMessage.NewBlock |
SocketMessage.BlocksIndexStatus | SocketMessage.BlocksIndexStatus |
SocketMessage.InternalTxsIndexStatus | SocketMessage.InternalTxsIndexStatus |
SocketMessage.TxStatusUpdate | SocketMessage.TxStatusUpdate |
SocketMessage.TxRawTrace |
SocketMessage.NewTx | SocketMessage.NewTx |
SocketMessage.NewPendingTx | SocketMessage.NewPendingTx |
SocketMessage.NewDeposits |
SocketMessage.AddressBalance | SocketMessage.AddressBalance |
SocketMessage.AddressCurrentCoinBalance | SocketMessage.AddressCurrentCoinBalance |
SocketMessage.AddressTokenBalance | SocketMessage.AddressTokenBalance |
...@@ -19,7 +22,9 @@ SocketMessage.AddressCoinBalance | ...@@ -19,7 +22,9 @@ SocketMessage.AddressCoinBalance |
SocketMessage.AddressTxs | SocketMessage.AddressTxs |
SocketMessage.AddressTxsPending | SocketMessage.AddressTxsPending |
SocketMessage.AddressTokenTransfer | SocketMessage.AddressTokenTransfer |
SocketMessage.AddressChangedBytecode |
SocketMessage.TokenTransfers | SocketMessage.TokenTransfers |
SocketMessage.TokenTotalSupply |
SocketMessage.ContractVerification | SocketMessage.ContractVerification |
SocketMessage.Unknown; SocketMessage.Unknown;
...@@ -35,8 +40,10 @@ export namespace SocketMessage { ...@@ -35,8 +40,10 @@ export namespace SocketMessage {
export type BlocksIndexStatus = SocketMessageParamsGeneric<'block_index_status', {finished: boolean; ratio: string}>; 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 InternalTxsIndexStatus = SocketMessageParamsGeneric<'internal_txs_index_status', {finished: boolean; ratio: string}>;
export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>; export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>;
export type TxRawTrace = SocketMessageParamsGeneric<'raw_trace', RawTracesResponse>;
export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>; export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>;
export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_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 AddressBalance = SocketMessageParamsGeneric<'balance', { balance: string; block_number: number; exchange_rate: string }>;
export type AddressCurrentCoinBalance = export type AddressCurrentCoinBalance =
SocketMessageParamsGeneric<'current_coin_balance', { coin_balance: string; block_number: number; exchange_rate: string }>; SocketMessageParamsGeneric<'current_coin_balance', { coin_balance: string; block_number: number; exchange_rate: string }>;
...@@ -45,7 +52,9 @@ export namespace SocketMessage { ...@@ -45,7 +52,9 @@ export namespace SocketMessage {
export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>; export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>;
export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>; export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>;
export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>; 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 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 ContractVerification = SocketMessageParamsGeneric<'verification_result', SmartContractVerificationResponse>;
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>; export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
} }
...@@ -6,12 +6,12 @@ export default function getConfirmationString(durations: Array<number>) { ...@@ -6,12 +6,12 @@ export default function getConfirmationString(durations: Array<number>) {
const [ lower, upper ] = durations.map((time) => time / 1_000); const [ lower, upper ] = durations.map((time) => time / 1_000);
if (!upper) { if (!upper) {
return `Confirmed within ${ lower } secs`; return `Confirmed within ${ lower.toLocaleString() } secs`;
} }
if (lower === 0) { 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 = { ...@@ -36,6 +36,7 @@ export const token: Address = {
name: null, name: null,
private_tags: [], private_tags: [],
watchlist_names: [], watchlist_names: [],
watchlist_address_id: null,
public_tags: [], public_tags: [],
token: tokenInfo, token: tokenInfo,
block_number_balance_updated_at: 8201413, block_number_balance_updated_at: 8201413,
...@@ -84,6 +85,7 @@ export const contract: Address = { ...@@ -84,6 +85,7 @@ export const contract: Address = {
public_tags: [ privateTag ], public_tags: [ privateTag ],
token: null, token: null,
watchlist_names: [ watchlistName ], watchlist_names: [ watchlistName ],
watchlist_address_id: 42,
}; };
export const validator: Address = { export const validator: Address = {
...@@ -113,4 +115,5 @@ export const validator: Address = { ...@@ -113,4 +115,5 @@ export const validator: Address = {
public_tags: [], public_tags: [],
token: null, token: null,
watchlist_names: [], watchlist_names: [],
watchlist_address_id: null,
}; };
...@@ -8,7 +8,12 @@ export const verified: Partial<SmartContract> = { ...@@ -8,7 +8,12 @@ export const verified: Partial<SmartContract> = {
constructor_args: 'constructor_args', constructor_args: 'constructor_args',
creation_bytecode: 'creation_bytecode', creation_bytecode: 'creation_bytecode',
deployed_bytecode: 'deployed_bytecode', deployed_bytecode: 'deployed_bytecode',
compiler_settings: 'compiler_settings', compiler_settings: {
evmVersion: 'london',
remappings: [
'@openzeppelin/=node_modules/@openzeppelin/',
],
},
evm_version: 'default', evm_version: 'default',
is_verified: true, is_verified: true,
name: 'WPOA', name: 'WPOA',
......
...@@ -85,7 +85,7 @@ export const erc721: TokenTransfer = { ...@@ -85,7 +85,7 @@ export const erc721: TokenTransfer = {
method: 'updateSmartAsset', method: 'updateSmartAsset',
}; };
export const erc1155: TokenTransfer = { export const erc1155A: TokenTransfer = {
from: { from: {
hash: '0x0000000000000000000000000000000000000000', hash: '0x0000000000000000000000000000000000000000',
implementation_name: null, implementation_name: null,
...@@ -128,26 +128,44 @@ export const erc1155: TokenTransfer = { ...@@ -128,26 +128,44 @@ export const erc1155: TokenTransfer = {
log_index: '1', log_index: '1',
}; };
export const erc1155multiple: TokenTransfer = { export const erc1155B: TokenTransfer = {
...erc1155, ...erc1155A,
token: { token: {
...erc1155.token, ...erc1155A.token,
name: 'SastanaNFT', name: 'SastanaNFT',
symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY', symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY',
}, },
total: [ total: { token_id: '12345678', value: '100000000000000000000', decimals: null },
{ token_id: '12345678', value: '100000000000000000000', decimals: null }, };
{ token_id: '483200961027732618117991942553110860267520', value: '200000000000000000000', decimals: null },
{ token_id: '456', value: '42', 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 = { export const mixTokens: TokenTransferResponse = {
items: [ items: [
erc20, erc20,
erc721, erc721,
erc1155, erc1155A,
erc1155multiple, erc1155B,
erc1155C,
erc1155D,
], ],
next_page_params: null, next_page_params: null,
}; };
...@@ -39,5 +39,11 @@ export const withIndexedFields: DecodedInput = { ...@@ -39,5 +39,11 @@ export const withIndexedFields: DecodedInput = {
type: 'uint256', type: 'uint256',
value: '31567373703130350', 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 = { ...@@ -100,8 +100,10 @@ export const withTokenTransfer: Transaction = {
token_transfers: [ token_transfers: [
tokenTransferMock.erc20, tokenTransferMock.erc20,
tokenTransferMock.erc721, tokenTransferMock.erc721,
tokenTransferMock.erc1155, tokenTransferMock.erc1155A,
tokenTransferMock.erc1155multiple, tokenTransferMock.erc1155B,
tokenTransferMock.erc1155C,
tokenTransferMock.erc1155D,
], ],
tx_types: [ tx_types: [
'token_transfer', 'token_transfer',
...@@ -240,3 +242,11 @@ export const withActionsUniswap: Transaction = { ...@@ -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 @@ ...@@ -16,6 +16,7 @@
"build": "next build", "build": "next build",
"build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse HEAD) -t blockscout ./", "build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse HEAD) -t blockscout ./",
"start": "next start", "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", "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": "./node_modules/.bin/eslint . --ext .js,.jsx,.ts,.tsx",
"lint:eslint:fix": "./node_modules/.bin/eslint . --ext .js,.jsx,.ts,.tsx --fix", "lint:eslint:fix": "./node_modules/.bin/eslint . --ext .js,.jsx,.ts,.tsx --fix",
...@@ -24,7 +25,7 @@ ...@@ -24,7 +25,7 @@
"format-svg": "./node_modules/.bin/svgo -r ./icons", "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": "./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: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": "jest",
"test:jest:watch": "jest --watch" "test:jest:watch": "jest --watch"
}, },
...@@ -57,7 +58,7 @@ ...@@ -57,7 +58,7 @@
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"lodash": "^4.0.0", "lodash": "^4.0.0",
"monaco-editor": "^0.34.1", "monaco-editor": "^0.34.1",
"next": "12.2.5", "next": "13.3.0",
"nextjs-routes": "^1.0.8", "nextjs-routes": "^1.0.8",
"node-fetch": "^3.2.9", "node-fetch": "^3.2.9",
"papaparse": "^5.3.2", "papaparse": "^5.3.2",
...@@ -78,7 +79,7 @@ ...@@ -78,7 +79,7 @@
"wagmi": "^0.10.6" "wagmi": "^0.10.6"
}, },
"devDependencies": { "devDependencies": {
"@playwright/experimental-ct-react": "1.31.0", "@playwright/experimental-ct-react": "1.32.3",
"@svgr/webpack": "^6.5.1", "@svgr/webpack": "^6.5.1",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@total-typescript/ts-reset": "^0.3.7", "@total-typescript/ts-reset": "^0.3.7",
...@@ -99,7 +100,7 @@ ...@@ -99,7 +100,7 @@
"css-loader": "^6.7.3", "css-loader": "^6.7.3",
"dotenv-cli": "^6.0.0", "dotenv-cli": "^6.0.0",
"eslint": "^8.32.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-es5": "^1.5.0",
"eslint-plugin-import-helpers": "^1.2.1", "eslint-plugin-import-helpers": "^1.2.1",
"eslint-plugin-jest": "^27.1.6", "eslint-plugin-jest": "^27.1.6",
...@@ -112,7 +113,7 @@ ...@@ -112,7 +113,7 @@
"lint-staged": ">=10", "lint-staged": ">=10",
"mockdate": "^3.0.5", "mockdate": "^3.0.5",
"next-transpile-modules": "^10.0.0", "next-transpile-modules": "^10.0.0",
"playwright": "1.31.0", "playwright": "1.32.3",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
"svgo": "^2.8.0", "svgo": "^2.8.0",
"ts-jest": "^29.0.3", "ts-jest": "^29.0.3",
......
...@@ -17,6 +17,8 @@ import AppError from 'ui/shared/AppError/AppError'; ...@@ -17,6 +17,8 @@ import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary'; import ErrorBoundary from 'ui/shared/ErrorBoundary';
import GoogleAnalytics from 'ui/shared/GoogleAnalytics'; import GoogleAnalytics from 'ui/shared/GoogleAnalytics';
import 'lib/setLocale';
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
useConfigSentry(); useConfigSentry();
const [ queryClient ] = useState(() => new QueryClient({ const [ queryClient ] = useState(() => new QueryClient({
......
...@@ -5,6 +5,7 @@ import React from 'react'; ...@@ -5,6 +5,7 @@ import React from 'react';
import getSeo from 'lib/next/block/getSeo'; import getSeo from 'lib/next/block/getSeo';
import Block from 'ui/pages/Block'; import Block from 'ui/pages/Block';
import Page from 'ui/shared/Page/Page';
const BlockPage: NextPage<RoutedQuery<'/block/[height]'>> = ({ height }: RoutedQuery<'/block/[height]'>) => { const BlockPage: NextPage<RoutedQuery<'/block/[height]'>> = ({ height }: RoutedQuery<'/block/[height]'>) => {
const { title, description } = getSeo({ height }); const { title, description } = getSeo({ height });
...@@ -14,7 +15,9 @@ const BlockPage: NextPage<RoutedQuery<'/block/[height]'>> = ({ height }: RoutedQ ...@@ -14,7 +15,9 @@ const BlockPage: NextPage<RoutedQuery<'/block/[height]'>> = ({ height }: RoutedQ
<title>{ title }</title> <title>{ title }</title>
<meta name="description" content={ description }/> <meta name="description" content={ description }/>
</Head> </Head>
<Block/> <Page>
<Block/>
</Page>
</> </>
); );
}; };
......
import type { NextPage } from 'next'; import type { GetServerSideProps, NextPage } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; 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'; import CsvExport from 'ui/pages/CsvExport';
const CsvExportPage: NextPage = () => { const CsvExportPage: NextPage = () => {
...@@ -19,4 +22,12 @@ const CsvExportPage: NextPage = () => { ...@@ -19,4 +22,12 @@ const CsvExportPage: NextPage = () => {
export default CsvExportPage; 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 Head from 'next/head';
import { route } from 'nextjs-routes';
import React from 'react'; 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 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'; import SearchResults from 'ui/pages/SearchResults';
const SearchResultsPage: NextPage = () => { const SearchResultsPage: NextPage = () => {
...@@ -27,53 +19,4 @@ const SearchResultsPage: NextPage = () => { ...@@ -27,53 +19,4 @@ const SearchResultsPage: NextPage = () => {
export default SearchResultsPage; export default SearchResultsPage;
export const getServerSideProps: GetServerSideProps<Props> = async({ req, res, resolvedUrl, query }) => { export { getServerSideProps } from 'lib/next/getServerSideProps';
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 });
};
...@@ -61,6 +61,8 @@ export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transacti ...@@ -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: '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: '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: '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 { export function sendMessage(socket: WebSocket, channel: Channel, msg: string, payload: unknown): void {
socket.send(JSON.stringify([ socket.send(JSON.stringify([
...channel, ...channel,
......
...@@ -4,6 +4,10 @@ const semanticTokens = { ...@@ -4,6 +4,10 @@ const semanticTokens = {
'default': 'blackAlpha.200', 'default': 'blackAlpha.200',
_dark: 'whiteAlpha.200', _dark: 'whiteAlpha.200',
}, },
text: {
'default': 'blackAlpha.800',
_dark: 'whiteAlpha.800',
},
text_secondary: { text_secondary: {
'default': 'gray.500', 'default': 'gray.500',
_dark: 'gray.400', _dark: 'gray.400',
......
...@@ -32,6 +32,7 @@ export interface Address { ...@@ -32,6 +32,7 @@ export interface Address {
private_tags: Array<AddressTag> | null; private_tags: Array<AddressTag> | null;
public_tags: Array<AddressTag> | null; public_tags: Array<AddressTag> | null;
token: TokenInfo | null; token: TokenInfo | null;
watchlist_address_id: number | null;
watchlist_names: Array<WatchlistName> | null; watchlist_names: Array<WatchlistName> | null;
} }
......
...@@ -30,7 +30,10 @@ export interface SmartContract { ...@@ -30,7 +30,10 @@ export interface SmartContract {
file_path: string; file_path: string;
additional_sources: Array<{ file_path: string; source_code: string }>; additional_sources: Array<{ file_path: string; source_code: string }>;
external_libraries: Array<SmartContractExternalLibrary> | null; external_libraries: Array<SmartContractExternalLibrary> | null;
compiler_settings: unknown; compiler_settings?: {
evmVersion?: string;
remappings?: Array<string>;
};
verified_twin_address_hash: string | null; verified_twin_address_hash: string | null;
minimal_proxy_address_hash: string | null; minimal_proxy_address_hash: string | null;
} }
...@@ -124,6 +127,7 @@ export interface SmartContractVerificationConfigRaw { ...@@ -124,6 +127,7 @@ export interface SmartContractVerificationConfigRaw {
verification_options: Array<string>; verification_options: Array<string>;
vyper_compiler_versions: Array<string>; vyper_compiler_versions: Array<string>;
vyper_evm_versions: Array<string>; vyper_evm_versions: Array<string>;
is_rust_verifier_microservice_enabled: boolean;
} }
export interface SmartContractVerificationConfig extends SmartContractVerificationConfigRaw { export interface SmartContractVerificationConfig extends SmartContractVerificationConfigRaw {
......
...@@ -7,6 +7,6 @@ export interface DecodedInput { ...@@ -7,6 +7,6 @@ export interface DecodedInput {
export interface DecodedInputParams { export interface DecodedInputParams {
name: string; name: string;
type: string; type: string;
value: string; value: string | Array<unknown> | Record<string, unknown>;
indexed?: boolean; indexed?: boolean;
} }
...@@ -2,9 +2,9 @@ import type { AddressParam } from './addressParams'; ...@@ -2,9 +2,9 @@ import type { AddressParam } from './addressParams';
export type TokenType = 'ERC-20' | 'ERC-721' | 'ERC-1155'; export type TokenType = 'ERC-20' | 'ERC-721' | 'ERC-1155';
export interface TokenInfo { export interface TokenInfo<T extends TokenType = TokenType> {
address: string; address: string;
type: TokenType; type: T;
symbol: string | null; symbol: string | null;
name: string | null; name: string | null;
decimals: string | null; decimals: string | null;
...@@ -18,8 +18,6 @@ export interface TokenCounters { ...@@ -18,8 +18,6 @@ export interface TokenCounters {
transfers_count: string; transfers_count: string;
} }
export type TokenInfoGeneric<Type extends TokenType> = Omit<TokenInfo, 'type'> & { type: Type };
export interface TokenHolders { export interface TokenHolders {
items: Array<TokenHolder>; items: Array<TokenHolder>;
next_page_params: TokenHoldersPagination; next_page_params: TokenHoldersPagination;
...@@ -43,7 +41,7 @@ export interface TokenInstance { ...@@ -43,7 +41,7 @@ export interface TokenInstance {
animation_url: string | null; animation_url: string | null;
external_app_url: string | null; external_app_url: string | null;
metadata: Record<string, unknown> | null; metadata: Record<string, unknown> | null;
owner: AddressParam; owner: AddressParam | null;
token: TokenInfo; token: TokenInfo;
} }
......
import type { AddressParam } from './addressParams'; import type { AddressParam } from './addressParams';
import type { TokenInfoGeneric, TokenType } from './token'; import type { TokenInfo, TokenType } from './token';
export type Erc20TotalPayload = { export type Erc20TotalPayload = {
decimals: string | null; decimals: string | null;
...@@ -18,19 +18,21 @@ export type Erc1155TotalPayload = { ...@@ -18,19 +18,21 @@ export type Erc1155TotalPayload = {
export type TokenTransfer = ( export type TokenTransfer = (
{ {
token: TokenInfoGeneric<'ERC-20'>; token: TokenInfo<'ERC-20'>;
total: Erc20TotalPayload; total: Erc20TotalPayload;
} | } |
{ {
token: TokenInfoGeneric<'ERC-721'>; token: TokenInfo<'ERC-721'>;
total: Erc721TotalPayload; total: Erc721TotalPayload;
} | } |
{ {
token: TokenInfoGeneric<'ERC-1155'>; token: TokenInfo<'ERC-1155'>;
total: Erc1155TotalPayload | Array<Erc1155TotalPayload>; total: Erc1155TotalPayload;
} }
) & TokenTransferBase ) & TokenTransferBase
export type TokenTotal = Erc20TotalPayload | Erc721TotalPayload | Erc1155TotalPayload;
interface TokenTransferBase { interface TokenTransferBase {
type: 'token_transfer' | 'token_burning' | 'token_spawning' | 'token_minting'; type: 'token_transfer' | 'token_burning' | 'token_spawning' | 'token_minting';
tx_hash: string; tx_hash: string;
......
...@@ -9,16 +9,9 @@ export type TransactionRevertReason = { ...@@ -9,16 +9,9 @@ export type TransactionRevertReason = {
raw: string; raw: string;
} | DecodedInput; } | DecodedInput;
export type Transaction = ( export type Transaction = {
{ to: AddressParam | null;
to: AddressParam; created_contract: AddressParam | null;
created_contract: null;
} |
{
to: null;
created_contract: AddressParam;
}
) & {
hash: string; hash: string;
result: string; result: string;
confirmations: number; confirmations: number;
...@@ -50,6 +43,10 @@ export type Transaction = ( ...@@ -50,6 +43,10 @@ export type Transaction = (
tx_types: Array<TransactionType>; tx_types: Array<TransactionType>;
tx_tag: string | null; tx_tag: string | null;
actions: Array<TxAction>; actions: Array<TxAction>;
l1_fee?: string;
l1_fee_scalar?: string;
l1_gas_price?: string;
l1_gas_used?: string;
} }
export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending; 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) => { ...@@ -79,7 +79,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
<Th width="17%">Block</Th> <Th width="17%">Block</Th>
<Th width="17%">Age</Th> <Th width="17%">Age</Th>
<Th width="16%">Txn</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> <Th width="25%" isNumeric>Reward { appConfig.network.currency.symbol }</Th>
</Tr> </Tr>
</Thead> </Thead>
......
...@@ -4,6 +4,7 @@ import React from 'react'; ...@@ -4,6 +4,7 @@ import React from 'react';
import type { CsvExportType } from 'types/client/address'; import type { CsvExportType } from 'types/client/address';
import appConfig from 'configs/app/config';
import svgFileIcon from 'icons/files/csv.svg'; import svgFileIcon from 'icons/files/csv.svg';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
...@@ -17,6 +18,10 @@ interface Props { ...@@ -17,6 +18,10 @@ interface Props {
const AddressCsvExportLink = ({ className, address, type }: Props) => { const AddressCsvExportLink = ({ className, address, type }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
if (!appConfig.reCaptcha.siteKey) {
return null;
}
return ( return (
<Tooltip isDisabled={ !isMobile } label="Download CSV"> <Tooltip isDisabled={ !isMobile } label="Download CSV">
<LinkInternal <LinkInternal
......
...@@ -56,6 +56,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -56,6 +56,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
implementation_name: null, implementation_name: null,
implementation_address: null, implementation_address: null,
token: null, token: null,
watchlist_address_id: null,
watchlist_names: null, watchlist_names: null,
creation_tx_hash: null, creation_tx_hash: null,
block_number_balance_updated_at: null, block_number_balance_updated_at: null,
......
...@@ -2,7 +2,7 @@ import { Box } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { erc1155 } from 'mocks/tokens/tokenTransfer'; import { erc1155A } from 'mocks/tokens/tokenTransfer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
...@@ -20,7 +20,7 @@ const hooksConfig = { ...@@ -20,7 +20,7 @@ const hooksConfig = {
test('with token filter and pagination +@mobile', async({ mount, page }) => { test('with token filter and pagination +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, 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( const component = await mount(
...@@ -37,7 +37,7 @@ test('with token filter and pagination +@mobile', async({ mount, page }) => { ...@@ -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 }) => { test('with token filter and no pagination +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ items: [ erc1155 ] }), body: JSON.stringify({ items: [ erc1155A ] }),
})); }));
const component = await mount( const component = await mount(
......
...@@ -26,7 +26,6 @@ import HashStringShorten from 'ui/shared/HashStringShorten'; ...@@ -26,7 +26,6 @@ import HashStringShorten from 'ui/shared/HashStringShorten';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter'; import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList'; import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable'; import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
...@@ -161,12 +160,11 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -161,12 +160,11 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0); const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0);
const isActionBarHidden = !tokenFilter && !numActiveFilters && !data?.items.length && !currentAddress; const isActionBarHidden = !tokenFilter && !numActiveFilters && !data?.items.length && !currentAddress;
const items = data?.items?.reduce(flattenTotal, []); const content = data?.items ? (
const content = items ? (
<> <>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<TokenTransferTable <TokenTransferTable
data={ items } data={ data?.items }
baseAddress={ currentAddress } baseAddress={ currentAddress }
showTxInfo showTxInfo
top={ isActionBarHidden ? 0 : 80 } top={ isActionBarHidden ? 0 : 80 }
...@@ -187,7 +185,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -187,7 +185,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
/> />
) } ) }
<TokenTransferList <TokenTransferList
data={ items } data={ data?.items }
baseAddress={ currentAddress } baseAddress={ currentAddress }
showTxInfo showTxInfo
enableTimeIncrement enableTimeIncrement
......
import { test, expect } from '@playwright/experimental-ct-react'; import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import * as contractMock from 'mocks/contract/info'; import * as contractMock from 'mocks/contract/info';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
...@@ -15,6 +16,14 @@ const hooksConfig = { ...@@ -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 }) => { test('verified with changed byte code +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({ await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200, status: 200,
...@@ -24,11 +33,32 @@ test('verified with changed byte code +@mobile +@dark-mode', async({ mount, page ...@@ -24,11 +33,32 @@ test('verified with changed byte code +@mobile +@dark-mode', async({ mount, page
const component = await mount( const component = await mount(
<TestApp> <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 }/> <ContractCode addressHash={ addressHash }/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:' + addressHash.toLowerCase());
socketServer.sendMessage(socket, channel, 'changed_bytecode', {});
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -41,7 +71,7 @@ test('verified with multiple sources +@mobile', async({ mount, page }) => { ...@@ -41,7 +71,7 @@ test('verified with multiple sources +@mobile', async({ mount, page }) => {
await mount( await mount(
<TestApp> <TestApp>
<ContractCode addressHash={ addressHash }/> <ContractCode addressHash={ addressHash } noSocket/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -60,7 +90,7 @@ test('verified via sourcify', async({ mount, page }) => { ...@@ -60,7 +90,7 @@ test('verified via sourcify', async({ mount, page }) => {
await mount( await mount(
<TestApp> <TestApp>
<ContractCode addressHash={ addressHash }/> <ContractCode addressHash={ addressHash } noSocket/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -77,7 +107,7 @@ test('self destructed', async({ mount, page }) => { ...@@ -77,7 +107,7 @@ test('self destructed', async({ mount, page }) => {
await mount( await mount(
<TestApp> <TestApp>
<ContractCode addressHash={ addressHash }/> <ContractCode addressHash={ addressHash } noSocket/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -95,7 +125,7 @@ test('with twin address alert +@mobile', async({ mount, page }) => { ...@@ -95,7 +125,7 @@ test('with twin address alert +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractCode addressHash={ addressHash }/> <ContractCode addressHash={ addressHash } noSocket/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -112,7 +142,7 @@ test('with proxy address alert +@mobile', async({ mount, page }) => { ...@@ -112,7 +142,7 @@ test('with proxy address alert +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractCode addressHash={ addressHash }/> <ContractCode addressHash={ addressHash } noSocket/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -129,7 +159,7 @@ test('non verified', async({ mount, page }) => { ...@@ -129,7 +159,7 @@ test('non verified', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ContractCode addressHash={ addressHash }/> <ContractCode addressHash={ addressHash } noSocket/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
......
...@@ -2,8 +2,12 @@ import { Flex, Skeleton, Button, Grid, GridItem, Text, Alert, Link, chakra, Box ...@@ -2,8 +2,12 @@ import { Flex, Skeleton, Button, Grid, GridItem, Text, Alert, Link, chakra, Box
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs'; 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 Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
...@@ -16,6 +20,8 @@ import ContractSourceCode from './ContractSourceCode'; ...@@ -16,6 +20,8 @@ import ContractSourceCode from './ContractSourceCode';
type Props = { type Props = {
addressHash?: string; addressHash?: string;
// prop for pw tests only
noSocket?: boolean;
} }
const InfoItem = chakra(({ label, value, className }: { label: string; value: string; className?: string }) => ( 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 ...@@ -25,15 +31,33 @@ const InfoItem = chakra(({ label, value, className }: { label: string; value: st
</GridItem> </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', { const { data, isLoading, isError } = useApiQuery('contract', {
pathParams: { hash: addressHash }, pathParams: { hash: addressHash },
queryOptions: { queryOptions: {
enabled: Boolean(addressHash), enabled: Boolean(addressHash) && (noSocket || isSocketOpen),
refetchOnMount: false, 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) { if (isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
...@@ -117,7 +141,7 @@ const ContractCode = ({ addressHash }: Props) => { ...@@ -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> } { data.sourcify_repo_url && <LinkExternal href={ data.sourcify_repo_url } fontSize="md">View contract in Sourcify repository</LinkExternal> }
</Alert> </Alert>
) } ) }
{ data.is_changed_bytecode && ( { (data.is_changed_bytecode || isChangedBytecodeSocket) && (
<Alert status="warning"> <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. Warning! Contract bytecode has been changed and does not match the verified one. Therefore, interaction with this smart contract may be risky.
</Alert> </Alert>
...@@ -177,6 +201,7 @@ const ContractCode = ({ addressHash }: Props) => { ...@@ -177,6 +201,7 @@ const ContractCode = ({ addressHash }: Props) => {
isViper={ Boolean(data.is_vyper_contract) } isViper={ Boolean(data.is_vyper_contract) }
filePath={ data.file_path } filePath={ data.file_path }
additionalSource={ data.additional_sources } additionalSource={ data.additional_sources }
remappings={ data.compiler_settings?.remappings }
/> />
) } ) }
{ Boolean(data.compiler_settings) && ( { Boolean(data.compiler_settings) && (
......
...@@ -20,7 +20,7 @@ interface ResultComponentProps<T extends SmartContractMethod> { ...@@ -20,7 +20,7 @@ interface ResultComponentProps<T extends SmartContractMethod> {
interface Props<T extends SmartContractMethod> { interface Props<T extends SmartContractMethod> {
data: T; 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; ResultComponent: (props: ResultComponentProps<T>) => JSX.Element | null;
isWrite?: boolean; isWrite?: boolean;
} }
...@@ -44,13 +44,23 @@ const sortFields = (data: Array<SmartContractMethodInput>) => ([ a ]: [string, s ...@@ -44,13 +44,23 @@ const sortFields = (data: Array<SmartContractMethodInput>) => ([ a ]: [string, s
}; };
const castFieldValue = (data: Array<SmartContractMethodInput>) => ([ key, value ]: [ string, string ], index: number) => { 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, parseArrayValue(value) ];
} }
return [ key, 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>) => { const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit, ResultComponent, isWrite }: Props<T>) => {
......
...@@ -64,7 +64,7 @@ const ContractMethodField = ({ control, name, valueType, placeholder, setValue, ...@@ -64,7 +64,7 @@ const ContractMethodField = ({ control, name, valueType, placeholder, setValue,
/> />
<InputRightElement w="auto" right={ 1 }> <InputRightElement w="auto" right={ 1 }>
{ field.value && <InputClearButton onClick={ handleClear } isDisabled={ isDisabled }/> } { field.value && <InputClearButton onClick={ handleClear } isDisabled={ isDisabled }/> }
{ hasZerosControl && <ContractMethodFieldZeroes onClick={ handleAddZeroesClick }/> } { hasZerosControl && <ContractMethodFieldZeroes onClick={ handleAddZeroesClick } isDisabled={ isDisabled }/> }
</InputRightElement> </InputRightElement>
</InputGroup> </InputGroup>
</FormControl> </FormControl>
......
...@@ -20,9 +20,10 @@ import { times } from 'lib/html-entities'; ...@@ -20,9 +20,10 @@ import { times } from 'lib/html-entities';
interface Props { interface Props {
onClick: (power: number) => void; onClick: (power: number) => void;
isDisabled?: boolean;
} }
const ContractMethodFieldZeroes = ({ onClick }: Props) => { const ContractMethodFieldZeroes = ({ onClick, isDisabled }: Props) => {
const [ selectedOption, setSelectedOption ] = React.useState<number | undefined>(18); const [ selectedOption, setSelectedOption ] = React.useState<number | undefined>(18);
const [ customValue, setCustomValue ] = React.useState<number>(); const [ customValue, setCustomValue ] = React.useState<number>();
const { isOpen, onToggle, onClose } = useDisclosure(); const { isOpen, onToggle, onClose } = useDisclosure();
...@@ -60,6 +61,7 @@ const ContractMethodFieldZeroes = ({ onClick }: Props) => { ...@@ -60,6 +61,7 @@ const ContractMethodFieldZeroes = ({ onClick }: Props) => {
colorScheme="gray" colorScheme="gray"
display="inline" display="inline"
onClick={ handleButtonClick } onClick={ handleButtonClick }
isDisabled={ isDisabled }
> >
{ times } { times }
<chakra.span>10</chakra.span> <chakra.span>10</chakra.span>
...@@ -76,6 +78,7 @@ const ContractMethodFieldZeroes = ({ onClick }: Props) => { ...@@ -76,6 +78,7 @@ const ContractMethodFieldZeroes = ({ onClick }: Props) => {
ml={ 1 } ml={ 1 }
p={ 0 } p={ 0 }
onClick={ onToggle } onClick={ onToggle }
isDisabled={ isDisabled }
> >
<Icon as={ iconEastMini } transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } boxSize={ 6 }/> <Icon as={ iconEastMini } transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } boxSize={ 6 }/>
</Button> </Button>
......
...@@ -38,7 +38,7 @@ const ContractRead = ({ addressHash, isProxy, isCustomAbi }: Props) => { ...@@ -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', { return apiFetch<'contract_method_query', SmartContractQueryMethodRead>('contract_method_query', {
pathParams: { hash: addressHash }, pathParams: { hash: addressHash },
queryParams: { queryParams: {
......
...@@ -16,9 +16,10 @@ interface Props { ...@@ -16,9 +16,10 @@ interface Props {
isViper: boolean; isViper: boolean;
filePath?: string; filePath?: string;
additionalSource?: SmartContract['additional_sources']; 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 = ( const heading = (
<Text fontWeight={ 500 }> <Text fontWeight={ 500 }>
<span>Contract source code</span> <span>Contract source code</span>
...@@ -56,7 +57,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi ...@@ -56,7 +57,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
{ diagramLink } { diagramLink }
{ copyToClipboard } { copyToClipboard }
</Flex> </Flex>
<CodeEditor data={ editorData }/> <CodeEditor data={ editorData } remappings={ remappings }/>
</section> </section>
); );
}; };
......
import _capitalize from 'lodash/capitalize';
import React from 'react'; import React from 'react';
import { useAccount, useSigner } from 'wagmi'; import { useAccount, useSigner, useNetwork, useSwitchNetwork } from 'wagmi';
import type { SmartContractWriteMethod } from 'types/api/contract'; import type { SmartContractWriteMethod } from 'types/api/contract';
...@@ -16,7 +15,7 @@ import ContractCustomAbiAlert from './ContractCustomAbiAlert'; ...@@ -16,7 +15,7 @@ import ContractCustomAbiAlert from './ContractCustomAbiAlert';
import ContractImplementationAddress from './ContractImplementationAddress'; import ContractImplementationAddress from './ContractImplementationAddress';
import ContractMethodCallable from './ContractMethodCallable'; import ContractMethodCallable from './ContractMethodCallable';
import ContractWriteResult from './ContractWriteResult'; import ContractWriteResult from './ContractWriteResult';
import { getNativeCoinValue, isExtendedError } from './utils'; import { getNativeCoinValue } from './utils';
interface Props { interface Props {
addressHash?: string; addressHash?: string;
...@@ -27,6 +26,8 @@ interface Props { ...@@ -27,6 +26,8 @@ interface Props {
const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => { const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
const { data: signer } = useSigner(); const { data: signer } = useSigner();
const { isConnected } = useAccount(); const { isConnected } = useAccount();
const { chain } = useNetwork();
const { switchNetworkAsync } = useSwitchNetwork();
const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_write_proxy' : 'contract_methods_write', { const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_write_proxy' : 'contract_methods_write', {
pathParams: { hash: addressHash }, pathParams: { hash: addressHash },
...@@ -51,55 +52,39 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => { ...@@ -51,55 +52,39 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
return contract; 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) { if (!isConnected) {
throw new Error('Wallet is not connected'); throw new Error('Wallet is not connected');
} }
try { if (chain?.id && String(chain.id) !== config.network.id) {
if (!_contract) { await switchNetworkAsync?.(Number(config.network.id));
throw new Error('Something went wrong. Try again later.'); }
}
if (!_contract) {
if (item.type === 'receive') { throw new Error('Something went wrong. Try again later.');
const value = args[0] ? getNativeCoinValue(args[0]) : '0'; }
const result = await signer?.sendTransaction({
to: addressHash, if (item.type === 'receive') {
value, const value = args[0] ? getNativeCoinValue(args[0]) : '0';
}); const result = await signer?.sendTransaction({
return { hash: result?.hash as string }; to: addressHash,
}
const _args = item.stateMutability === 'payable' ? args.slice(0, -1) : args;
const value = item.stateMutability === 'payable' ? getNativeCoinValue(args[args.length - 1]) : undefined;
const methodName = item.type === 'fallback' ? 'fallback' : item.name;
const result = await _contract[methodName](..._args, {
gasLimit: 100_000,
value, value,
}); });
return { hash: result?.hash as string };
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 ]);
const _args = item.stateMutability === 'payable' ? args.slice(0, -1) : args;
const value = item.stateMutability === 'payable' ? getNativeCoinValue(args[args.length - 1]) : undefined;
const methodName = item.type === 'fallback' ? 'fallback' : item.name;
const result = await _contract[methodName](..._args, {
gasLimit: 100_000,
value,
});
return { hash: result.hash as string };
}, [ _contract, addressHash, chain, isConnected, signer, switchNetworkAsync ]);
const renderContent = React.useCallback((item: SmartContractWriteMethod, index: number, id: number) => { const renderContent = React.useCallback((item: SmartContractWriteMethod, index: number, id: number) => {
return ( return (
......
...@@ -2,13 +2,18 @@ import BigNumber from 'bignumber.js'; ...@@ -2,13 +2,18 @@ import BigNumber from 'bignumber.js';
import config from 'configs/app/config'; 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; const _value = Array.isArray(value) ? value[0] : value;
if (typeof _value !== 'string') {
return '0';
}
return BigNumber(_value).times(10 ** config.network.currency.decimals).toString(); return BigNumber(_value).times(10 ** config.network.currency.decimals).toString();
}; };
export const addZeroesAllowed = (valueType: string) => { export const addZeroesAllowed = (valueType: string) => {
if (valueType.includes('[]')) { if (valueType.includes('[')) {
return false; return false;
} }
......
...@@ -8,7 +8,7 @@ import type { UserInfo } from 'types/api/account'; ...@@ -8,7 +8,7 @@ import type { UserInfo } from 'types/api/account';
import starFilledIcon from 'icons/star_filled.svg'; import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg'; import starOutlineIcon from 'icons/star_outline.svg';
import { resourceKey } from 'lib/api/resources'; 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 useLoginUrl from 'lib/hooks/useLoginUrl';
import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing'; import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
import WatchlistAddModal from 'ui/watchlist/AddressModal/AddressModal'; import WatchlistAddModal from 'ui/watchlist/AddressModal/AddressModal';
...@@ -17,10 +17,10 @@ import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal'; ...@@ -17,10 +17,10 @@ import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal';
interface Props { interface Props {
className?: string; className?: string;
hash: string; hash: string;
isAdded: boolean; watchListId: number | null;
} }
const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => { const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
const addModalProps = useDisclosure(); const addModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -30,15 +30,13 @@ const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => { ...@@ -30,15 +30,13 @@ const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => {
const isAuth = Boolean(profileData); const isAuth = Boolean(profileData);
const loginUrl = useLoginUrl(); const loginUrl = useLoginUrl();
const watchListQuery = useApiQuery('watchlist', { queryOptions: { enabled: isAdded } });
const handleClick = React.useCallback(() => { const handleClick = React.useCallback(() => {
if (!isAuth) { if (!isAuth) {
window.location.assign(loginUrl); window.location.assign(loginUrl);
return; return;
} }
isAdded ? deleteModalProps.onOpen() : addModalProps.onOpen(); watchListId ? deleteModalProps.onOpen() : addModalProps.onOpen();
}, [ addModalProps, deleteModalProps, isAdded, isAuth, loginUrl ]); }, [ addModalProps, deleteModalProps, watchListId, isAuth, loginUrl ]);
const handleAddOrDeleteSuccess = React.useCallback(async() => { const handleAddOrDeleteSuccess = React.useCallback(async() => {
const queryKey = getResourceKey('address', { pathParams: { hash: router.query.hash?.toString() } }); const queryKey = getResourceKey('address', { pathParams: { hash: router.query.hash?.toString() } });
...@@ -57,18 +55,15 @@ const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => { ...@@ -57,18 +55,15 @@ const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => {
const formData = React.useMemo(() => { const formData = React.useMemo(() => {
return { return {
address_hash: hash, address_hash: hash,
// FIXME temporary solution id: String(watchListId),
// 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 || '',
}; };
}, [ hash, watchListQuery.data ]); }, [ hash, watchListId ]);
return ( 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 <IconButton
isActive={ isAdded } isActive={ Boolean(watchListId) }
className={ className } className={ className }
aria-label="edit" aria-label="edit"
variant="outline" variant="outline"
...@@ -76,7 +71,7 @@ const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => { ...@@ -76,7 +71,7 @@ const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => {
pl="6px" pl="6px"
pr="6px" pr="6px"
onClick={ handleClick } onClick={ handleClick }
icon={ <Icon as={ isAdded ? starFilledIcon : starOutlineIcon } boxSize={ 5 }/> } icon={ <Icon as={ watchListId ? starFilledIcon : starOutlineIcon } boxSize={ 5 }/> }
onFocusCapture={ usePreventFocusAfterModalClosing() } onFocusCapture={ usePreventFocusAfterModalClosing() }
/> />
</Tooltip> </Tooltip>
......
...@@ -83,7 +83,7 @@ const TokenSelect = ({ onClick }: Props) => { ...@@ -83,7 +83,7 @@ const TokenSelect = ({ onClick }: Props) => {
} }
<Tooltip label="Show all tokens"> <Tooltip label="Show all tokens">
<Box> <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 <IconButton
aria-label="Show all tokens" aria-label="Show all tokens"
variant="outline" variant="outline"
......
...@@ -55,7 +55,7 @@ const AddressesListItem = ({ ...@@ -55,7 +55,7 @@ const AddressesListItem = ({
) } ) }
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Txn count</Text> <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> </HStack>
</ListItemMobile> </ListItemMobile>
); );
......
...@@ -60,7 +60,7 @@ const AddressesTableItem = ({ ...@@ -60,7 +60,7 @@ const AddressesTableItem = ({
</Td> </Td>
) } ) }
<Td isNumeric> <Td isNumeric>
<Text lineHeight="24px">{ Number(item.tx_count).toLocaleString('en') }</Text> <Text lineHeight="24px">{ Number(item.tx_count).toLocaleString() }</Text>
</Td> </Td>
</Tr> </Tr>
); );
......
...@@ -15,7 +15,7 @@ const AppLink = ({ url, external, id, title }: Props) => { ...@@ -15,7 +15,7 @@ const AppLink = ({ url, external, id, title }: Props) => {
{ title } { title }
</LinkOverlay> </LinkOverlay>
) : ( ) : (
<NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref> <NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref legacyBehavior>
<LinkOverlay> <LinkOverlay>
{ title } { title }
</LinkOverlay> </LinkOverlay>
......
...@@ -27,7 +27,7 @@ const AppModalLink = ({ url, external, id }: Props) => { ...@@ -27,7 +27,7 @@ const AppModalLink = ({ url, external, id }: Props) => {
{ ...buttonProps } { ...buttonProps }
>Launch app</Button> >Launch app</Button>
) : ( ) : (
<NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref> <NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref legacyBehavior>
<Button <Button
as="a" as="a"
{ ...buttonProps } { ...buttonProps }
......
...@@ -112,7 +112,7 @@ const BlockDetails = ({ query }: Props) => { ...@@ -112,7 +112,7 @@ const BlockDetails = ({ query }: Props) => {
title="Size" title="Size"
hint="Size of the block in bytes" hint="Size of the block in bytes"
> >
{ data.size.toLocaleString('en') } { data.size.toLocaleString() }
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Timestamp" title="Timestamp"
...@@ -141,7 +141,7 @@ const BlockDetails = ({ query }: Props) => { ...@@ -141,7 +141,7 @@ const BlockDetails = ({ query }: Props) => {
{ /* api doesn't return the block processing time yet */ } { /* api doesn't return the block processing time yet */ }
{ /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ } { /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ }
</DetailsInfoItem> </DetailsInfoItem>
{ !totalReward.isEqualTo(ZERO) && ( { !appConfig.L2.isL2Network && !totalReward.isEqualTo(ZERO) && (
<DetailsInfoItem <DetailsInfoItem
title="Block reward" title="Block reward"
hint={ hint={
......
...@@ -48,7 +48,7 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -48,7 +48,7 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
</Flex> </Flex>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Size</Text> <Text fontWeight={ 500 }>Size</Text>
<Text variant="secondary">{ data.size.toLocaleString('en') } bytes</Text> <Text variant="secondary">{ data.size.toLocaleString() } bytes</Text>
</Flex> </Flex>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>{ capitalize(getNetworkValidatorTitle()) }</Text> <Text fontWeight={ 500 }>{ capitalize(getNetworkValidatorTitle()) }</Text>
...@@ -77,20 +77,24 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -77,20 +77,24 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
) } ) }
</Flex> </Flex>
</Box> </Box>
<Flex columnGap={ 2 }> { !appConfig.L2.isL2Network && (
<Text fontWeight={ 500 }>Reward { appConfig.network.currency.symbol }</Text> <Flex columnGap={ 2 }>
<Text variant="secondary">{ totalReward.toFixed() }</Text> <Text fontWeight={ 500 }>Reward { appConfig.network.currency.symbol }</Text>
</Flex> <Text variant="secondary">{ totalReward.toFixed() }</Text>
<Box>
<Text fontWeight={ 500 }>Burnt fees</Text>
<Flex columnGap={ 4 } mt={ 2 }>
<Flex>
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/>
<Text variant="secondary" ml={ 1 }>{ burntFees.div(WEI).toFixed() }</Text>
</Flex>
<Utilization ml={ 4 } value={ burntFees.div(txFees).toNumber() }/>
</Flex> </Flex>
</Box> ) }
{ !appConfig.L2.isL2Network && (
<Box>
<Text fontWeight={ 500 }>Burnt fees</Text>
<Flex columnGap={ 4 } mt={ 2 }>
<Flex>
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/>
<Text variant="secondary" ml={ 1 }>{ burntFees.div(WEI).toFixed() }</Text>
</Flex>
<Utilization ml={ 4 } value={ burntFees.div(txFees).toNumber() }/>
</Flex>
</Box>
) }
</ListItemMobile> </ListItemMobile>
); );
}; };
......
...@@ -24,11 +24,11 @@ const BlocksTable = ({ data, top, page }: Props) => { ...@@ -24,11 +24,11 @@ const BlocksTable = ({ data, top, page }: Props) => {
<Tr> <Tr>
<Th width="125px">Block</Th> <Th width="125px">Block</Th>
<Th width="120px">Size, bytes</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="64px" isNumeric>Txn</Th>
<Th width="35%">Gas used</Th> <Th width={ appConfig.L2.isL2Network ? '63%' : '35%' }>Gas used</Th>
<Th width="22%">Reward { appConfig.network.currency.symbol }</Th> { !appConfig.L2.isL2Network && <Th width="22%">Reward { appConfig.network.currency.symbol }</Th> }
<Th width="22%">Burnt fees { appConfig.network.currency.symbol }</Th> { !appConfig.L2.isL2Network && <Th width="22%">Burnt fees { appConfig.network.currency.symbol }</Th> }
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
......
...@@ -6,6 +6,7 @@ import React from 'react'; ...@@ -6,6 +6,7 @@ import React from 'react';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config';
import flameIcon from 'icons/flame.svg'; import flameIcon from 'icons/flame.svg';
import getBlockTotalReward from 'lib/block/getBlockTotalReward'; import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import { WEI } from 'lib/consts'; import { WEI } from 'lib/consts';
...@@ -28,7 +29,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -28,7 +29,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
const txFees = BigNumber(data.tx_fees || 0); const txFees = BigNumber(data.tx_fees || 0);
const separatorColor = useColorModeValue('gray.200', 'gray.700'); const separatorColor = useColorModeValue('gray.200', 'gray.700');
const burntFeesIconColor = useColorModeValue('gray.500', 'inherit');
return ( return (
<Tr <Tr
as={ motion.tr } as={ motion.tr }
...@@ -52,7 +53,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -52,7 +53,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
</Flex> </Flex>
<BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement }/> <BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement }/>
</Td> </Td>
<Td fontSize="sm">{ data.size.toLocaleString('en') }</Td> <Td fontSize="sm">{ data.size.toLocaleString() }</Td>
<Td fontSize="sm"> <Td fontSize="sm">
<AddressLink type="address" alias={ data.miner.name } hash={ data.miner.hash } truncation="constant" display="inline-flex" maxW="100%"/> <AddressLink type="address" alias={ data.miner.name } hash={ data.miner.hash } truncation="constant" display="inline-flex" maxW="100%"/>
</Td> </Td>
...@@ -63,34 +64,38 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -63,34 +64,38 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
</LinkInternal> </LinkInternal>
) : data.tx_count } ) : data.tx_count }
</Td> </Td>
<Td fontSize="sm"> { !appConfig.L2.isL2Network && (
<Box>{ BigNumber(data.gas_used || 0).toFormat() }</Box> <Td fontSize="sm">
<Flex mt={ 2 }> <Box>{ BigNumber(data.gas_used || 0).toFormat() }</Box>
<Tooltip label="Gas Used %"> <Flex mt={ 2 }>
<Box> <Tooltip label="Gas Used %">
<Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }/> <Box>
<Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }/>
</Box>
</Tooltip>
{ data.gas_target_percentage && (
<>
<TextSeparator color={ separatorColor } mx={ 1 }/>
<GasUsedToTargetRatio value={ data.gas_target_percentage }/>
</>
) }
</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={ burntFeesIconColor }/>
{ burntFees.dividedBy(WEI).toFixed(8) }
</Flex>
<Tooltip label="Burnt fees / Txn fees * 100%">
<Box w="min-content">
<Utilization mt={ 2 } value={ burntFees.div(txFees).toNumber() }/>
</Box> </Box>
</Tooltip> </Tooltip>
{ data.gas_target_percentage && ( </Td>
<> ) }
<TextSeparator color={ separatorColor } mx={ 1 }/>
<GasUsedToTargetRatio value={ data.gas_target_percentage }/>
</>
) }
</Flex>
</Td>
<Td fontSize="sm">{ totalReward.toFixed(8) }</Td>
<Td fontSize="sm">
<Flex alignItems="center" columnGap={ 1 }>
<Icon as={ flameIcon } boxSize={ 5 } color={ useColorModeValue('gray.500', 'inherit') }/>
{ burntFees.dividedBy(WEI).toFixed(8) }
</Flex>
<Tooltip label="Burnt fees / Txn fees * 100%">
<Box w="min-content">
<Utilization mt={ 2 } value={ burntFees.div(txFees).toNumber() }/>
</Box>
</Tooltip>
</Td>
</Tr> </Tr>
); );
}; };
......
...@@ -16,6 +16,7 @@ const hooksConfig = { ...@@ -16,6 +16,7 @@ const hooksConfig = {
const hash = '0x2F99338637F027CFB7494E46B49987457beCC6E3'; const hash = '0x2F99338637F027CFB7494E46B49987457beCC6E3';
const formConfig: SmartContractVerificationConfig = { const formConfig: SmartContractVerificationConfig = {
is_rust_verifier_microservice_enabled: true,
solidity_compiler_versions: [ solidity_compiler_versions: [
'v0.8.17+commit.8df45f5f', 'v0.8.17+commit.8df45f5f',
'v0.8.16+commit.07a7930e', 'v0.8.16+commit.07a7930e',
......
...@@ -22,15 +22,6 @@ import ContractVerificationVyperContract from './methods/ContractVerificationVyp ...@@ -22,15 +22,6 @@ import ContractVerificationVyperContract from './methods/ContractVerificationVyp
import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile'; import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile';
import { prepareRequestBody, formatSocketErrors, getDefaultValues } from './utils'; 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 { interface Props {
method?: SmartContractVerificationMethod; method?: SmartContractVerificationMethod;
config: SmartContractVerificationConfig; config: SmartContractVerificationConfig;
...@@ -122,8 +113,18 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro ...@@ -122,8 +113,18 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
handler: handleNewSocketMessage, 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 method = watch('method');
const content = METHOD_COMPONENTS[method?.value] || null; const content = methods[method?.value] || null;
const methodValue = method?.value; const methodValue = method?.value;
useUpdateEffect(() => { useUpdateEffect(() => {
......
...@@ -11,9 +11,10 @@ import ContractVerificationFormRow from '../ContractVerificationFormRow'; ...@@ -11,9 +11,10 @@ import ContractVerificationFormRow from '../ContractVerificationFormRow';
interface Props { interface Props {
hint?: string; hint?: string;
isReadOnly?: boolean;
} }
const ContractVerificationFieldName = ({ hint }: Props) => { const ContractVerificationFieldName = ({ hint, isReadOnly }: Props) => {
const { formState, control } = useFormContext<FormFields>(); const { formState, control } = useFormContext<FormFields>();
const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'name'>}) => { const renderControl = React.useCallback(({ field }: {field: ControllerRenderProps<FormFields, 'name'>}) => {
...@@ -26,13 +27,13 @@ const ContractVerificationFieldName = ({ hint }: Props) => { ...@@ -26,13 +27,13 @@ const ContractVerificationFieldName = ({ hint }: Props) => {
required required
isInvalid={ Boolean(error) } isInvalid={ Boolean(error) }
maxLength={ 255 } maxLength={ 255 }
isDisabled={ formState.isSubmitting } isDisabled={ formState.isSubmitting || isReadOnly }
autoComplete="off" autoComplete="off"
/> />
<InputPlaceholder text="Contract name" error={ error }/> <InputPlaceholder text="Contract name" error={ error }/>
</FormControl> </FormControl>
); );
}, [ formState.errors, formState.isSubmitting ]); }, [ formState.errors, formState.isSubmitting, isReadOnly ]);
return ( return (
<ContractVerificationFormRow> <ContractVerificationFormRow>
......
import React from 'react'; import React from 'react';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import ContractVerificationMethod from '../ContractVerificationMethod'; import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldAutodetectArgs from '../fields/ContractVerificationFieldAutodetectArgs'; import ContractVerificationFieldAutodetectArgs from '../fields/ContractVerificationFieldAutodetectArgs';
import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode'; import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode';
...@@ -10,16 +12,16 @@ import ContractVerificationFieldLibraries from '../fields/ContractVerificationFi ...@@ -10,16 +12,16 @@ import ContractVerificationFieldLibraries from '../fields/ContractVerificationFi
import ContractVerificationFieldName from '../fields/ContractVerificationFieldName'; import ContractVerificationFieldName from '../fields/ContractVerificationFieldName';
import ContractVerificationFieldOptimization from '../fields/ContractVerificationFieldOptimization'; import ContractVerificationFieldOptimization from '../fields/ContractVerificationFieldOptimization';
const ContractVerificationFlattenSourceCode = () => { const ContractVerificationFlattenSourceCode = ({ config }: { config: SmartContractVerificationConfig }) => {
return ( return (
<ContractVerificationMethod title="Contract verification via Solidity (flattened source code)"> <ContractVerificationMethod title="Contract verification via Solidity (flattened source code)">
<ContractVerificationFieldName/> { !config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldName/> }
<ContractVerificationFieldIsYul/> { config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldIsYul/> }
<ContractVerificationFieldCompiler/> <ContractVerificationFieldCompiler/>
<ContractVerificationFieldEvmVersion/> <ContractVerificationFieldEvmVersion/>
<ContractVerificationFieldOptimization/> <ContractVerificationFieldOptimization/>
<ContractVerificationFieldCode/> <ContractVerificationFieldCode/>
<ContractVerificationFieldAutodetectArgs/> { !config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldAutodetectArgs/> }
<ContractVerificationFieldLibraries/> <ContractVerificationFieldLibraries/>
</ContractVerificationMethod> </ContractVerificationMethod>
); );
......
import React from 'react'; import React from 'react';
import type { SmartContractVerificationConfig } from 'types/api/contract';
import ContractVerificationMethod from '../ContractVerificationMethod'; import ContractVerificationMethod from '../ContractVerificationMethod';
import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode'; import ContractVerificationFieldCode from '../fields/ContractVerificationFieldCode';
import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler'; import ContractVerificationFieldCompiler from '../fields/ContractVerificationFieldCompiler';
import ContractVerificationFieldConstructorArgs from '../fields/ContractVerificationFieldConstructorArgs'; import ContractVerificationFieldConstructorArgs from '../fields/ContractVerificationFieldConstructorArgs';
import ContractVerificationFieldEvmVersion from '../fields/ContractVerificationFieldEvmVersion';
import ContractVerificationFieldName from '../fields/ContractVerificationFieldName'; import ContractVerificationFieldName from '../fields/ContractVerificationFieldName';
const ContractVerificationVyperContract = () => { const ContractVerificationVyperContract = ({ config }: { config: SmartContractVerificationConfig }) => {
return ( return (
<ContractVerificationMethod title="Contract verification via Vyper (contract)"> <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/> <ContractVerificationFieldCompiler isVyper/>
{ config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldEvmVersion isVyper/> }
<ContractVerificationFieldCode isVyper/> <ContractVerificationFieldCode isVyper/>
<ContractVerificationFieldConstructorArgs/> { !config?.is_rust_verifier_microservice_enabled && <ContractVerificationFieldConstructorArgs/> }
</ContractVerificationMethod> </ContractVerificationMethod>
); );
}; };
......
...@@ -14,7 +14,7 @@ interface MethodOption { ...@@ -14,7 +14,7 @@ interface MethodOption {
export interface FormFieldsFlattenSourceCode { export interface FormFieldsFlattenSourceCode {
method: MethodOption; method: MethodOption;
is_yul: boolean; is_yul: boolean;
name: string; name: string | undefined;
compiler: Option | null; compiler: Option | null;
evm_version: Option | null; evm_version: Option | null;
is_optimization_enabled: boolean; is_optimization_enabled: boolean;
...@@ -53,9 +53,10 @@ export interface FormFieldsMultiPartFile { ...@@ -53,9 +53,10 @@ export interface FormFieldsMultiPartFile {
export interface FormFieldsVyperContract { export interface FormFieldsVyperContract {
method: MethodOption; method: MethodOption;
name: string; name: string;
evm_version: Option | null;
compiler: Option | null; compiler: Option | null;
code: string; code: string;
constructor_args: string; constructor_args: string | undefined;
} }
export interface FormFieldsVyperMultiPartFile { export interface FormFieldsVyperMultiPartFile {
......
...@@ -84,8 +84,9 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> ...@@ -84,8 +84,9 @@ export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields>
value: 'vyper-code' as const, value: 'vyper-code' as const,
label: METHOD_LABELS['vyper-code'], label: METHOD_LABELS['vyper-code'],
}, },
name: '', name: 'Vyper_contract',
compiler: null, compiler: null,
evm_version: null,
code: '', code: '',
constructor_args: '', constructor_args: '',
}, },
...@@ -113,6 +114,13 @@ export function getDefaultValues(method: SmartContractVerificationMethod, config ...@@ -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; return defaultValues;
} }
...@@ -145,7 +153,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { ...@@ -145,7 +153,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
is_optimization_enabled: _data.is_optimization_enabled, is_optimization_enabled: _data.is_optimization_enabled,
is_yul_contract: _data.is_yul, is_yul_contract: _data.is_yul,
optimization_runs: _data.optimization_runs, optimization_runs: _data.optimization_runs,
contract_name: _data.name, contract_name: _data.name || undefined,
libraries: reduceLibrariesArray(_data.libraries), libraries: reduceLibrariesArray(_data.libraries),
evm_version: _data.evm_version?.value, evm_version: _data.evm_version?.value,
autodetect_constructor_args: _data.autodetect_constructor_args, autodetect_constructor_args: _data.autodetect_constructor_args,
...@@ -196,6 +204,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] { ...@@ -196,6 +204,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
return { return {
compiler_version: _data.compiler?.value, compiler_version: _data.compiler?.value,
evm_version: _data.evm_version?.value,
source_code: _data.code, source_code: _data.code,
contract_name: _data.name, contract_name: _data.name,
constructor_args: _data.constructor_args, constructor_args: _data.constructor_args,
......
...@@ -20,10 +20,11 @@ type Props = { item: DepositsItem }; ...@@ -20,10 +20,11 @@ type Props = { item: DepositsItem };
const DepositsListItem = ({ item }: Props) => { const DepositsListItem = ({ item }: Props) => {
const timeAgo = dayjs(item.l1_block_timestamp).fromNow(); const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
const items = [ return (
{ <ListItemMobileGrid.Container>
name: 'L1 block No',
value: ( <ListItemMobileGrid.Label>L1 block No</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<LinkExternal <LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height]', query: { height: item.l1_block_number.toString() } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height]', query: { height: item.l1_block_number.toString() } }) }
fontWeight={ 600 } fontWeight={ 600 }
...@@ -32,11 +33,10 @@ const DepositsListItem = ({ item }: Props) => { ...@@ -32,11 +33,10 @@ const DepositsListItem = ({ item }: Props) => {
<Icon as={ blockIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ blockIcon } boxSize={ 6 } mr={ 1 }/>
{ item.l1_block_number } { item.l1_block_number }
</LinkExternal> </LinkExternal>
), </ListItemMobileGrid.Value>
},
{ <ListItemMobileGrid.Label>L2 txn hash</ListItemMobileGrid.Label>
name: 'L2 txn hash', <ListItemMobileGrid.Value>
value: (
<LinkInternal <LinkInternal
href={ route({ pathname: '/tx/[hash]', query: { hash: item.l2_tx_hash } }) } href={ route({ pathname: '/tx/[hash]', query: { hash: item.l2_tx_hash } }) }
display="flex" display="flex"
...@@ -48,15 +48,13 @@ const DepositsListItem = ({ item }: Props) => { ...@@ -48,15 +48,13 @@ const DepositsListItem = ({ item }: Props) => {
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l2_tx_hash }/></Box> <Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l2_tx_hash }/></Box>
</LinkInternal> </LinkInternal>
), </ListItemMobileGrid.Value>
},
{ <ListItemMobileGrid.Label>Age</ListItemMobileGrid.Label>
name: 'Age', <ListItemMobileGrid.Value>{ timeAgo }</ListItemMobileGrid.Value>
value: timeAgo,
}, <ListItemMobileGrid.Label>L1 txn hash</ListItemMobileGrid.Label>
{ <ListItemMobileGrid.Value>
name: 'L1 txn hash',
value: (
<LinkExternal <LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
maxW="100%" maxW="100%"
...@@ -66,11 +64,10 @@ const DepositsListItem = ({ item }: Props) => { ...@@ -66,11 +64,10 @@ const DepositsListItem = ({ item }: Props) => {
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box> <Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box>
</LinkExternal> </LinkExternal>
), </ListItemMobileGrid.Value>
},
{ <ListItemMobileGrid.Label>L1 txn origin</ListItemMobileGrid.Label>
name: 'L1 txn origin', <ListItemMobileGrid.Value>
value: (
<LinkExternal <LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/address/[hash]', query: { hash: item.l1_tx_origin } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/address/[hash]', query: { hash: item.l1_tx_origin } }) }
maxW="100%" maxW="100%"
...@@ -80,15 +77,13 @@ const DepositsListItem = ({ item }: Props) => { ...@@ -80,15 +77,13 @@ const DepositsListItem = ({ item }: Props) => {
<AddressIcon address={{ hash: item.l1_tx_origin, is_contract: false, implementation_name: '' }} mr={ 2 }/> <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> <Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_origin }/></Box>
</LinkExternal> </LinkExternal>
), </ListItemMobileGrid.Value>
},
{ <ListItemMobileGrid.Label>Gas limit</ListItemMobileGrid.Label>
name: 'Gas limit', <ListItemMobileGrid.Value>{ BigNumber(item.l2_tx_gas_limit).toFormat() }</ListItemMobileGrid.Value>
value: BigNumber(item.l2_tx_gas_limit).toFormat(),
},
];
return <ListItemMobileGrid items={ items } gridTemplateColumns="92px auto"/>; </ListItemMobileGrid.Container>
);
}; };
export default DepositsListItem; export default DepositsListItem;
...@@ -7,6 +7,7 @@ import React from 'react'; ...@@ -7,6 +7,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
...@@ -17,12 +18,20 @@ import LinkInternal from 'ui/shared/LinkInternal'; ...@@ -17,12 +18,20 @@ import LinkInternal from 'ui/shared/LinkInternal';
import LatestBlocksItem from './LatestBlocksItem'; import LatestBlocksItem from './LatestBlocksItem';
import LatestBlocksItemSkeleton from './LatestBlocksItemSkeleton'; import LatestBlocksItemSkeleton from './LatestBlocksItemSkeleton';
const BLOCK_HEIGHT = 166; const BLOCK_HEIGHT_L1 = 166;
const BLOCK_HEIGHT_L2 = 112;
const BLOCK_MARGIN = 12; const BLOCK_MARGIN = 12;
const LatestBlocks = () => { const LatestBlocks = () => {
const blockHeight = appConfig.L2.isL2Network ? BLOCK_HEIGHT_L2 : BLOCK_HEIGHT_L1;
const isMobile = useIsMobile(); 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 { data, isLoading, isError } = useApiQuery('homepage_blocks');
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -60,7 +69,7 @@ const LatestBlocks = () => { ...@@ -60,7 +69,7 @@ const LatestBlocks = () => {
<VStack <VStack
spacing={ `${ BLOCK_MARGIN }px` } spacing={ `${ BLOCK_MARGIN }px` }
mb={ 6 } mb={ 6 }
height={ `${ BLOCK_HEIGHT * blocksMaxCount + BLOCK_MARGIN * (blocksMaxCount - 1) }px` } height={ `${ blockHeight * blocksMaxCount + BLOCK_MARGIN * (blocksMaxCount - 1) }px` }
overflow="hidden" overflow="hidden"
> >
{ Array.from(Array(blocksMaxCount)).map((item, index) => <LatestBlocksItemSkeleton key={ index }/>) } { Array.from(Array(blocksMaxCount)).map((item, index) => <LatestBlocksItemSkeleton key={ index }/>) }
...@@ -92,9 +101,9 @@ const LatestBlocks = () => { ...@@ -92,9 +101,9 @@ const LatestBlocks = () => {
</Text> </Text>
</Box> </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 } > <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> </AnimatePresence>
</VStack> </VStack>
<Flex justifyContent="center"> <Flex justifyContent="center">
......
...@@ -13,6 +13,7 @@ import React from 'react'; ...@@ -13,6 +13,7 @@ import React from 'react';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg'; import blockIcon from 'icons/block.svg';
import getBlockTotalReward from 'lib/block/getBlockTotalReward'; import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import BlockTimestamp from 'ui/blocks/BlockTimestamp'; import BlockTimestamp from 'ui/blocks/BlockTimestamp';
...@@ -56,11 +57,14 @@ const LatestBlocksItem = ({ block, h }: Props) => { ...@@ -56,11 +57,14 @@ const LatestBlocksItem = ({ block, h }: Props) => {
<Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" fontSize="sm"> <Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" fontSize="sm">
<GridItem>Txn</GridItem> <GridItem>Txn</GridItem>
<GridItem><Text variant="secondary">{ block.tx_count }</Text></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>Reward</GridItem>
<GridItem>Miner</GridItem> <GridItem><Text variant="secondary">{ totalReward.toFixed() }</Text></GridItem>
<GridItem><AddressLink type="address" alias={ block.miner.name } hash={ block.miner.hash } truncation="constant" maxW="100%"/></GridItem> <GridItem>Miner</GridItem>
<GridItem><AddressLink type="address" alias={ block.miner.name } hash={ block.miner.hash } truncation="constant" maxW="100%"/></GridItem>
</>
) }
</Grid> </Grid>
</Box> </Box>
); );
......
...@@ -8,6 +8,8 @@ import { ...@@ -8,6 +8,8 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config';
const LatestBlocksItemSkeleton = () => { const LatestBlocksItemSkeleton = () => {
return ( return (
<Box <Box
...@@ -27,10 +29,14 @@ const LatestBlocksItemSkeleton = () => { ...@@ -27,10 +29,14 @@ const LatestBlocksItemSkeleton = () => {
<Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" fontSize="sm"> <Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" fontSize="sm">
<GridItem><Skeleton w="30px" h="15px"/></GridItem> <GridItem><Skeleton w="30px" h="15px"/></GridItem>
<GridItem><Skeleton w="93px" h="15px"/></GridItem> <GridItem><Skeleton w="93px" h="15px"/></GridItem>
<GridItem><Skeleton w="30px" h="15px"/></GridItem> { !appConfig.L2.isL2Network && (
<GridItem><Skeleton w="93px" h="15px"/></GridItem> <>
<GridItem><Skeleton w="30px" h="15px"/></GridItem> <GridItem><Skeleton w="30px" h="15px"/></GridItem>
<GridItem><Skeleton w="93px" 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> </Grid>
</Box> </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 { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -18,10 +18,8 @@ const LatestTransactions = () => { ...@@ -18,10 +18,8 @@ const LatestTransactions = () => {
const { num, socketAlert } = useNewTxsSocket(); const { num, socketAlert } = useNewTxsSocket();
let content;
if (isLoading) { if (isLoading) {
content = ( return (
<> <>
<Skeleton h="32px" w="100%" borderBottomLeftRadius={ 0 } borderBottomRightRadius={ 0 }/> <Skeleton h="32px" w="100%" borderBottomLeftRadius={ 0 } borderBottomRightRadius={ 0 }/>
{ Array.from(Array(txsCount)).map((item, index) => <LatestTxsItemSkeleton key={ index }/>) } { Array.from(Array(txsCount)).map((item, index) => <LatestTxsItemSkeleton key={ index }/>) }
...@@ -30,12 +28,12 @@ const LatestTransactions = () => { ...@@ -30,12 +28,12 @@ const LatestTransactions = () => {
} }
if (isError) { if (isError) {
content = <Text mt={ 4 }>No data. Please reload page.</Text>; return <Text mt={ 4 }>No data. Please reload page.</Text>;
} }
if (data) { if (data) {
const txsUrl = route({ pathname: '/txs' }); const txsUrl = route({ pathname: '/txs' });
content = ( return (
<> <>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ txsUrl } num={ num } alert={ socketAlert }/> <SocketNewItemsNotice borderBottomRadius={ 0 } url={ txsUrl } num={ num } alert={ socketAlert }/>
<Box mb={{ base: 3, lg: 4 }}> <Box mb={{ base: 3, lg: 4 }}>
...@@ -48,12 +46,7 @@ const LatestTransactions = () => { ...@@ -48,12 +46,7 @@ const LatestTransactions = () => {
); );
} }
return ( return null;
<Box flexGrow={ 1 }>
<Heading as="h4" size="sm" mb={ 4 }>Latest transactions</Heading>
{ content }
</Box>
);
}; };
export default LatestTransactions; 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