Commit f6ac190a authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into gradual-increment

parents d178bbb0 c7a3c3f9
......@@ -195,7 +195,7 @@ module.exports = {
groups: [
'module',
'/types/',
[ '/^configs/', '/^data/', '/^deploy/', '/^icons/', '/^lib/', '/^pages/', '/^playwright/', '/^theme/', '/^ui/' ],
[ '/^configs/', '/^data/', '/^deploy/', '/^icons/', '/^lib/', '/^mocks/', '/^pages/', '/^playwright/', '/^theme/', '/^ui/' ],
[ 'parent', 'sibling', 'index' ],
],
alphabetize: { order: 'asc', ignoreCase: true },
......
......@@ -67,6 +67,7 @@ jobs:
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master
with:
env_vars: VALUES_DIR=deploy/values/review,APP_NAME=bs-stack
globalEnv: review
appNamespace: review-front-$GITHUB_HEAD_REF_SLUG
blockscoutIngressHost: blockscout
frontendIngressHost: blockscout
......
......@@ -47,3 +47,5 @@ yarn-error.log*
/test-results/
/playwright-report/
/playwright/.cache/
/playwright/.browser/
/playwright/envs.js
......@@ -102,15 +102,56 @@
"instanceLimit": 1
}
},
// PW TESTS
{
"type": "npm",
"script": "test:pw:docker",
"type": "shell",
"command": "${input:pwDebugFlag} yarn test:pw:local ${relativeFileDirname}/${fileBasename} ${input:pwArgs}",
"problemMatcher": [],
"label": "test: playwright",
"label": "test: playwright: local for current file",
"detail": "run visual components tests for current file",
"presentation": {
"reveal": "always",
"panel": "new",
"focus": true,
},
"icon": {
"color": "terminal.ansiBlue",
"id": "beaker"
},
"runOptions": {
"instanceLimit": 1
},
},
{
"type": "shell",
"command": "yarn test:pw:docker ${relativeFileDirname}/${fileBasename} ${input:pwArgs}",
"problemMatcher": [],
"label": "test: playwright: docker for current file",
"detail": "run visual components tests for current file",
"presentation": {
"reveal": "always",
"panel": "new",
"focus": true,
},
"icon": {
"color": "terminal.ansiBlue",
"id": "beaker"
},
"runOptions": {
"instanceLimit": 1
},
},
{
"type": "shell",
"command": "yarn test:pw:docker ${input:pwArgs}",
"problemMatcher": [],
"label": "test: playwright: docker for all files",
"detail": "run visual components tests",
"presentation": {
"reveal": "always",
"panel": "new",
"focus": true,
},
"icon": {
"color": "terminal.ansiBlue",
......@@ -120,6 +161,8 @@
"instanceLimit": 1
}
},
// JEST TESTS
{
"type": "npm",
"script": "test:jest",
......@@ -129,6 +172,7 @@
"presentation": {
"reveal": "always",
"panel": "new",
"focus": true,
},
"icon": {
"color": "terminal.ansiBlue",
......@@ -148,6 +192,7 @@
"reveal": "always",
"panel": "new",
"close": true,
"focus": true,
},
"icon": {
"color": "terminal.ansiBlue",
......@@ -157,6 +202,26 @@
"instanceLimit": 1
}
},
{
"type": "shell",
"command": "yarn test:jest ${relativeFileDirname}/${fileBasename} --watch",
"problemMatcher": [],
"label": "test: jest: watch curent file",
"detail": "run jest tests in watch mode for current file",
"presentation": {
"reveal": "always",
"panel": "new",
"focus": true,
},
"icon": {
"color": "terminal.ansiBlue",
"id": "beaker"
},
"runOptions": {
"instanceLimit": 1
},
},
{
"type": "npm",
"script": "build:docker",
......@@ -168,6 +233,7 @@
"panel": "new",
"close": true,
"revealProblems": "onProblem",
"focus": true,
},
"icon": {
"color": "terminal.ansiRed",
......@@ -217,5 +283,29 @@
"instanceLimit": 1
}
},
]
],
"inputs": [
{
"type": "pickString",
"id": "pwDebugFlag",
"description": "What debug flag you want to use?",
"options": [
"",
"PWDEBUG=1",
"DEBUG=pw:browser,pw:api",
"DEBUG=*",
],
"default": ""
},
{
"type": "pickString",
"id": "pwArgs",
"description": "What args you want to pass?",
"options": [
"",
"--update-snapshots",
],
"default": ""
},
],
}
\ No newline at end of file
......@@ -66,7 +66,7 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` *(optional)* | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` *(optional)* | Set to false if network doesn't have gas tracker | `true` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` *(optional)* | Set to false if average block time is useless for the network | `true` |
| NEXT_PUBLIC_MARKETPLACE_APP_LIST | `Array<MarketplaceApp>` where `MarketplaceApp` can have following [properties](#marketplace-app-configuration-properties) | List of apps that will be shown on the marketplace page | `[{'author': 'Bob', 'id': 'app', 'title': 'The App', 'logo': 'https://foo.app/icon.png', 'categories': ['security'], 'shortDescription': 'Awesome app', 'site': 'https://foo.app', 'description': 'The best app', 'url': 'https://foo.app/launch'}]` |
| NEXT_PUBLIC_MARKETPLACE_APP_LIST | `Array<MarketplaceApp>` where `MarketplaceApp` can have following [properties](#marketplace-app-configuration-properties) | List of apps that will be shown on the marketplace page | `[{'author': 'Bob', 'id': 'app', 'external': true, 'title': 'The App', 'logo': 'https://foo.app/icon.png', 'categories': ['security'], 'shortDescription': 'Awesome app', 'site': 'https://foo.app', 'description': 'The best app', 'url': 'https://foo.app/launch'}]` |
| NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | `https://airtable.com/shrqUAcjgGJ4jU88C` |
| NEXT_PUBLIC_NETWORK_EXPLORERS | `Array<NetworkExplorer>` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` |
| NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` or `mining` *(optional)* | Verification type in the network | `mining` |
......@@ -125,6 +125,7 @@ The app instance could be customized by passing following variables to NodeJS en
| Property | Type | Description | Example value
| --- | --- | --- | --- |
| id | `string` | Used as slug for the app. Must be unique in the app list. | `'app'` |
| external | `boolean` | If true means that the application opens in a new window, but not in an iframe. | `true` |
| title | `string` | Displayed title of the app. | `'The App'` |
| logo | `string` | URL to logo file. Should be at least 144x144. | `'https://foo.app/icon.png'` |
| shortDescription | `string` | Displayed only in the app list. | `'Awesome app'` |
......
This diff is collapsed.
This diff is collapsed.
# app config
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3100
NEXT_PUBLIC_APP_INSTANCE=pw
NEXT_PUBLIC_APP_ENV=testing
# ui config
NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
# api config
NEXT_PUBLIC_API_HOST=blockscout.com
const PATHS = require('../../lib/link/paths');
const PATHS = require('../../lib/link/paths.json');
const oldUrls = [
{
......
This diff is collapsed.
......@@ -65,7 +65,7 @@ geth:
frontend:
environment:
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS:
_default: ENC[AES256_GCM,data:1SAbzZhCs/vzdftIX0WVLtImH27NJ6SwENee4uTu2p+ZyUso3nQCLUUm,iv:apyLxt2dQ5RN33ra1Q1sAy2cyplG9FSryksQru2ghlA=,tag:PVcCNt0bz1TfQewUebV5LA==,type:str]
_default: ENC[AES256_GCM,data:yShwsa6ajoFXg/6QSgEARkZRVVrwrdsR69NSmyvBH2O5EUQ0OvsWpW64,iv:K/HT6C9pYCK63LNyF3HERFc79vDS4cB0H4pINIlNhh0=,tag:X0HqeAP01diTvDOwoEP6lw==,type:str]
NEXT_PUBLIC_SENTRY_DSN:
_default: ENC[AES256_GCM,data:n/H2AH2n9ovn265iFbbrqeOOWS3s7FXgDv5FXJ2Dz133GuhTaqIU5psWjTraZ/Vh+dVM8M9zBLFfUanCWOz7cerq/QUoSVZjKvIvcyp6F072DGU=,iv:Co/pSR+U0vfkmWR+LDpxcQKDJl0WNbaEZihpzrRJcbc=,tag:HUiCX8WGJXWCDB59Y6iIbw==,type:str]
SENTRY_CSP_REPORT_URI:
......@@ -78,8 +78,8 @@ sops:
azure_kv: []
hc_vault: []
age: []
lastmodified: "2022-11-16T11:55:15Z"
mac: ENC[AES256_GCM,data:wd7HZEGH1fJO1ufaUeBtcjUaHS21rcOoiGaQPNptEyEPj6q/60rJ1YQmGqkgi3DNVnGly5FQyYjMIIY3/YqXqTZI6MBhJp4RmpCELbqqzQbAFvbomYURmqG/umeT2+kMrSIF/PXrt4d51e1cod2+H4OY9V09VerH9L07D0nTd48=,iv:dDeTSqvmwps4oQKRVgDqmMf/uxf7Egb+jufwTKtm6F4=,tag:CqOCA4XW7d3C5D4dflIFug==,type:str]
lastmodified: "2022-11-28T16:58:46Z"
mac: ENC[AES256_GCM,data:QJvVfWWWVDk5mI66T9J8EnEyVwmJoGEsWO9Pr8vK7jyC3rhAYD2WdKYfpkbwwMKrJzcMBe7UeaOeEY6aApuMNdobeEjsJAvstXCOBzMe5H9XtAFiAY+oxf8r4ELNvQP/gIBZSja+ehSbXBcaP4DkLn4FboaBhkoE8A37W2R6/QA=,iv:FnIC6iGLEZNwRSrbF81vF6eQuyq0yQHNPRTPrx3FB+8=,tag:LRnZCwYkCh4o8lDUcG2m9A==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
......
This diff is collapsed.
......@@ -95,7 +95,9 @@ const config: JestConfigWithTsJest = {
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
modulePathIgnorePatterns: [
'node_modules_linux',
],
// Activates notifications for test results
// notify: false,
......
......@@ -13,7 +13,7 @@ interface Props extends ChakraProviderProps {
export function Chakra({ cookies, theme, children }: Props) {
const colorModeManager =
typeof cookies === 'string' ?
cookieStorageManagerSSR(cookies) :
cookieStorageManagerSSR(typeof document !== 'undefined' ? document.cookie : cookies) :
localStorageManager;
return (
......
export default function getPlaceholderWithError(text: string, errorText?: string) {
return `${ text }${ errorText ? ' - ' + errorText : '' }`;
}
......@@ -5,10 +5,8 @@ import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import { animateScroll } from 'react-scroll';
import type { BlockFilters } from 'types/api/block';
import { PAGINATION_FIELDS } from 'types/api/pagination';
import type { PaginationParams, PaginatedResponse, PaginatedQueryKeys } from 'types/api/pagination';
import type { TTxsFilters } from 'types/api/txsFilters';
import type { PaginationParams, PaginatedResponse, PaginatedQueryKeys, PaginationFilters } from 'types/api/pagination';
import useFetch from 'lib/hooks/useFetch';
......@@ -16,7 +14,7 @@ interface Params<QueryName extends PaginatedQueryKeys> {
apiPath: string;
queryName: QueryName;
queryIds?: Array<string>;
filters?: TTxsFilters | BlockFilters;
filters?: PaginationFilters<QueryName>;
options?: Omit<UseQueryOptions<unknown, unknown, PaginatedResponse<QueryName>>, 'queryKey' | 'queryFn'>;
}
......
const paths = {
network_index: `/`,
watchlist: `/account/watchlist`,
private_tags: `/account/tag_address`,
public_tags: `/account/public_tags_request`,
api_keys: `/account/api_key`,
custom_abi: `/account/custom_abi`,
profile: `/auth/profile`,
txs: `/txs`,
tx: `/tx/:id`,
blocks: `/blocks`,
block: `/block/:id`,
tokens: `/tokens`,
token_index: `/token/:hash`,
token_instance_item: `/token/:hash/instance/:id`,
address_index: `/address/:id`,
address_contract_verification: `/address/:id/contract_verifications/new`,
apps: `/apps`,
app_index: `/apps/:id`,
search_results: `/search-results`,
other: `/search-results`,
auth: `/auth/auth0`,
};
module.exports = paths;
{
"network_index": "/",
"watchlist": "/account/watchlist",
"private_tags": "/account/tag_address",
"public_tags": "/account/public_tags_request",
"api_keys": "/account/api_key",
"custom_abi": "/account/custom_abi",
"profile": "/auth/profile",
"txs": "/txs",
"tx": "/tx/:id",
"blocks": "/blocks",
"block": "/block/:id",
"tokens": "/tokens",
"token_index": "/token/:hash",
"token_instance_item": "/token/:hash/instance/:id",
"address_index": "/address/:id",
"address_contract_verification": "/address/:id/contract_verifications/new",
"apps": "/apps",
"app_index": "/apps/:id",
"search_results": "/search-results",
"other": "/search-results",
"auth": "/auth/auth0",
"stats": "/stats"
}
import appConfig from 'configs/app/config';
import PATHS from './paths.js';
import PATHS from './paths.json';
export interface Route {
pattern: string;
......@@ -85,6 +85,10 @@ export const ROUTES = {
pattern: PATHS.app_index,
},
stats: {
pattern: PATHS.stats,
},
// SEARCH
search_results: {
pattern: PATHS.search_results,
......
......@@ -6,7 +6,7 @@ export const SocketContext = React.createContext<Socket | null>(null);
interface SocketProviderProps {
children: React.ReactNode;
url: string;
url?: string;
options?: Partial<SocketConnectOption>;
}
......@@ -14,6 +14,10 @@ export function SocketProvider({ children, options, url }: SocketProviderProps)
const [ socket, setSocket ] = useState<Socket | null>(null);
useEffect(() => {
if (!url) {
return;
}
const socketInstance = new Socket(url, options);
socketInstance.connect();
setSocket(socketInstance);
......
import type { AddressParam } from 'types/api/addressParams';
export const withName: AddressParam = {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
implementation_name: null,
is_contract: true,
is_verified: null,
name: 'ArianeeStore',
private_tags: [],
watchlist_names: [],
public_tags: [],
};
export const withoutName: AddressParam = {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
implementation_name: null,
is_contract: true,
is_verified: null,
name: null,
private_tags: [],
watchlist_names: [],
public_tags: [],
};
import type { AddressTag, WatchlistName } from 'types/api/addressParams';
export const privateTag: AddressTag = {
label: 'my-private-tag',
display_name: 'my private tag',
address_hash: '0x',
};
export const publicTag: AddressTag = {
label: 'some-public-tag',
display_name: 'some public tag',
address_hash: '0x',
};
export const watchlistName: WatchlistName = {
label: 'watchlist-name',
display_name: 'watchlist name',
};
import type { Block, BlocksResponse } from 'types/api/block';
export const base: Block = {
base_fee_per_gas: '10000000000',
burnt_fees: '5449200000000000',
burnt_fees_percentage: 20.292245650793845,
difficulty: '340282366920938463463374607431768211454',
extra_data: 'TODO',
gas_limit: '12500000',
gas_target_percentage: -91.28128,
gas_used: '544920',
gas_used_percentage: 4.35936,
hash: '0xccc75136de485434d578b73df66537c06b34c3c9b12d085daf95890c914fc2bc',
height: 30146364,
miner: {
hash: '0xdAd49e6CbDE849353ab27DeC6319E687BFc91A41',
implementation_name: null,
is_contract: false,
is_verified: null,
name: 'Alex Emelyanov',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
nonce: '0x0000000000000000',
parent_hash: '0x44125f0eb36a9d942e0c23bb4e8117f7ba86a9537a69b59c0025986ed2b7500f',
priority_fee: '23211757500000000',
rewards: [
{
reward: '500000000000000000',
type: 'POA Mania Reward',
},
{
reward: '1026853607500000000',
type: 'Validator Reward',
},
{
reward: '500000000000000000',
type: 'Emission Reward',
},
],
size: 2448,
state_root: 'TODO',
timestamp: '2022-11-11T11:59:35Z',
total_difficulty: '10258276095980170141167591583995189665817672619',
tx_count: 5,
tx_fees: '26853607500000000',
type: 'block',
uncles_hashes: [],
};
export const genesis = {
base_fee_per_gas: null,
burnt_fees: null,
burnt_fees_percentage: null,
difficulty: '131072',
extra_data: 'TODO',
gas_limit: '6700000',
gas_target_percentage: -100,
gas_used: '0',
gas_used_percentage: 0,
hash: '0x39f02c003dde5b073b3f6e1700fc0b84b4877f6839bb23edadd3d2d82a488634',
height: 0,
miner: {
hash: '0x0000000000000000000000000000000000000000',
implementation_name: null,
is_contract: false,
is_verified: null,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
nonce: '0x0000000000000000',
parent_hash: '0x0000000000000000000000000000000000000000000000000000000000000000',
priority_fee: null,
rewards: [],
size: 533,
state_root: 'TODO',
timestamp: '2017-12-16T00:13:24.000000Z',
total_difficulty: '131072',
tx_count: 0,
tx_fees: '0',
type: 'block',
uncles_hashes: [],
};
export const base2: Block = {
...base,
height: base.height - 1,
size: 592,
miner: {
hash: '0xDfE10D55d9248B2ED66f1647df0b0A46dEb25165',
implementation_name: null,
is_contract: false,
is_verified: null,
name: 'Kiryl Ihnatsyeu',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
timestamp: '2022-11-11T11:46:05Z',
tx_count: 253,
gas_target_percentage: 23.6433,
gas_used: '6333342',
gas_used_percentage: 87.859504,
burnt_fees: '232438000000000000',
burnt_fees_percentage: 65.3333333333334,
rewards: [
{
reward: '500000000000000000',
type: 'Chore Reward',
},
{
reward: '1017432850000000000',
type: 'Miner Reward',
},
{
reward: '500000000000000000',
type: 'Emission Reward',
},
],
};
export const baseListResponse: BlocksResponse = {
items: [
base,
base2,
],
next_page_params: null,
};
import type { TokenTransfer, TokenTransferResponse } from 'types/api/tokenTransfer';
export const erc20: TokenTransfer = {
from: {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
implementation_name: null,
is_contract: true,
is_verified: true,
name: 'ArianeeStore',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
to: {
hash: '0x7d20a8D54F955b4483A66aB335635ab66e151c51',
implementation_name: null,
is_contract: true,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
token: {
address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420',
decimals: '18',
exchange_rate: null,
holders: '46554',
name: 'ARIANEE',
symbol: 'ARIA',
type: 'ERC-20',
},
total: {
decimals: '18',
value: '31567373703130350',
},
tx_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
type: 'token_transfer',
};
export const erc721: TokenTransfer = {
from: {
hash: '0x621C2a125ec4A6D8A7C7A655A18a2868d35eb43C',
implementation_name: null,
is_contract: false,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
to: {
hash: '0x47eE48AEBc4ab9Ed908b805b8c8dAAa71B31Db1A',
implementation_name: null,
is_contract: false,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
token: {
address: '0x363574E6C5C71c343d7348093D84320c76d5Dd29',
decimals: null,
exchange_rate: null,
holders: '63090',
name: 'Arianee Smart-Asset',
symbol: 'AriaSA',
type: 'ERC-721',
},
total: {
token_id: '875879856',
},
tx_hash: '0xf13bc7afe5e02b494dd2f22078381d36a4800ef94a0ccc147431db56c301e6cc',
type: 'token_transfer',
};
export const erc1155: TokenTransfer = {
from: {
hash: '0x0000000000000000000000000000000000000000',
implementation_name: null,
is_contract: false,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
to: {
hash: '0xBb36c792B9B45Aaf8b848A1392B0d6559202729E',
implementation_name: null,
is_contract: false,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
token: {
address: '0xF56b7693E4212C584de4a83117f805B8E89224CB',
decimals: null,
exchange_rate: null,
holders: '1',
name: null,
symbol: null,
type: 'ERC-1155',
},
total: {
token_id: '123',
value: '42',
decimals: null,
},
tx_hash: '0x05d6589367633c032d757a69c5fb16c0e33e3994b0d9d1483f82aeee1f05d746',
type: 'token_minting',
};
export const erc1155multiple: TokenTransfer = {
...erc1155,
token: {
...erc1155.token,
name: 'OLYMPIC',
},
total: [
{ token_id: '456', value: '42', decimals: null },
{ token_id: '12345678', value: '142', decimals: null },
{ token_id: '1000006457499', value: '11', decimals: null },
],
};
export const mixTokens: TokenTransferResponse = {
items: [
erc20,
erc721,
erc1155,
erc1155multiple,
],
next_page_params: null,
};
import type { DecodedInput } from 'types/api/decodedInput';
export const withoutIndexedFields: DecodedInput = {
method_call: 'CreditSpended(uint256 _type, uint256 _quantity)',
method_id: '58cdf94a',
parameters: [
{
name: '_type',
type: 'uint256',
value: '3',
},
{
name: '_quantity',
type: 'uint256',
value: '1',
},
],
};
export const withIndexedFields: DecodedInput = {
method_call: 'Transfer(address indexed from, address indexed to, uint256 value)',
method_id: 'ddf252ad',
parameters: [
{
indexed: true,
name: 'from',
type: 'address',
value: '0xd789a607ceac2f0e14867de4eb15b15c9ffb5859',
},
{
indexed: true,
name: 'to',
type: 'address',
value: '0x7d20a8d54f955b4483a66ab335635ab66e151c51',
},
{
indexed: false,
name: 'value',
type: 'uint256',
value: '31567373703130350',
},
],
};
import type { InternalTransaction, InternalTransactionsResponse } from 'types/api/internalTransaction';
export const base: InternalTransaction = {
block: 29611822,
created_contract: null,
error: null,
from: {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
implementation_name: null,
is_contract: true,
is_verified: true,
name: 'ArianeeStore',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
gas_limit: '757586',
index: 1,
success: true,
timestamp: '2022-10-10T14:43:05.000000Z',
to: {
hash: '0x502a9C8af2441a1E276909405119FaE21F3dC421',
implementation_name: null,
is_contract: true,
is_verified: true,
name: 'ArianeeCreditHistory',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
transaction_hash: '0xe9e27dfeb183066e26cfe556f74b7219b08df6951e25d14003d4fc7af8bbff61',
type: 'call',
value: '42000000000000000000',
};
export const typeStaticCall: InternalTransaction = {
...base,
type: 'staticcall',
to: {
...base.to,
name: null,
},
gas_limit: '63424243',
};
export const withContractCreated: InternalTransaction = {
...base,
type: 'delegatecall',
to: null,
from: {
...base.from,
name: null,
},
created_contract: {
hash: '0xdda21946FF3FAa027104b15BE6970CA756439F5a',
implementation_name: null,
is_contract: true,
is_verified: null,
name: 'Shavuha token',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
value: '1420000000000000000',
gas_limit: '5433',
};
export const baseResponse: InternalTransactionsResponse = {
items: [
base,
typeStaticCall,
withContractCreated,
],
next_page_params: null,
};
/* eslint-disable max-len */
import type { Transaction } from 'types/api/transaction';
import { publicTag, privateTag, watchlistName } from 'mocks/address/tag';
import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
import * as decodedInputDataMock from 'mocks/txs/decodedInputData';
export const base: Transaction = {
base_fee_per_gas: '10000000000',
block: 29611750,
confirmation_duration: [
0,
6364,
],
confirmations: 508299,
created_contract: null,
decoded_input: decodedInputDataMock.withoutIndexedFields,
exchange_rate: '0.00254428',
fee: {
type: 'actual',
value: '7143168000000000',
},
from: {
hash: '0x047A81aFB05D9B1f8844bf60fcA05DCCFbC584B9',
implementation_name: null,
is_contract: false,
name: null,
is_verified: null,
private_tags: [ ],
public_tags: [ publicTag ],
watchlist_names: [],
},
gas_limit: '800000',
gas_price: '48000000000',
gas_used: '148816',
hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
max_fee_per_gas: '40190625000',
max_priority_fee_per_gas: '28190625000',
method: 'updateSmartAsset',
nonce: 27831,
position: 7,
priority_fee: '1299672384375000',
raw_input: '0xfa4b78b90000000000000000000000000000000000000000000000000000000005001bcfe835d1028984e9e6e7d016b77164eacbcc6cc061e9333c0b37982b504f7ea791000000000000000000000000a79b29ad7e0196c95b87f4663ded82fbf2e3add8',
result: 'success',
revert_reason: null,
status: 'ok',
timestamp: '2022-10-10T14:34:30.000000Z',
to: {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
implementation_name: null,
is_contract: false,
is_verified: true,
name: null,
private_tags: [ privateTag ],
public_tags: [],
watchlist_names: [ watchlistName ],
},
token_transfers: [],
token_transfers_overflow: false,
tx_burnt_fee: '461030000000000',
tx_tag: null,
tx_types: [
'contract_call',
'token_transfer',
],
type: 2,
value: '42000000000000000000',
};
export const withContractCreation: Transaction = {
...base,
to: null,
created_contract: {
hash: '0xdda21946FF3FAa027104b15BE6970CA756439F5a',
implementation_name: null,
is_contract: true,
is_verified: null,
name: 'Shavuha token',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
};
export const withTokenTransfer: Transaction = {
...base,
to: {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
implementation_name: null,
is_contract: true,
is_verified: true,
name: 'ArianeeStore',
private_tags: [ privateTag ],
public_tags: [],
watchlist_names: [ watchlistName ],
},
token_transfers: [
tokenTransferMock.erc20,
tokenTransferMock.erc721,
tokenTransferMock.erc1155,
tokenTransferMock.erc1155multiple,
],
};
export const withDecodedRevertReason: Transaction = {
...base,
status: 'error',
result: 'Reverted',
revert_reason: {
method_call: 'SomeCustomError(address addr, uint256 balance)',
method_id: '50289a9f',
parameters: [
{
name: 'addr',
type: 'address',
value: '0xf26594f585de4eb0ae9de865d9053fee02ac6ef1',
},
{
name: 'balance',
type: 'uint256',
value: '123',
},
],
},
};
export const withRawRevertReason: Transaction = {
...base,
status: 'error',
result: 'Reverted',
revert_reason: {
raw: '4f6e6c79206368616972706572736f6e2063616e206769766520726967687420746f20766f74652e',
},
to: {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
implementation_name: null,
is_verified: true,
is_contract: true,
name: 'Bad guy',
private_tags: [ ],
public_tags: [],
watchlist_names: [ ],
},
};
export const pending: Transaction = {
...base,
base_fee_per_gas: null,
block: null,
confirmation_duration: [],
confirmations: 0,
decoded_input: null,
gas_used: null,
max_fee_per_gas: null,
max_priority_fee_per_gas: null,
method: null,
position: null,
priority_fee: null,
result: 'pending',
revert_reason: null,
status: null,
timestamp: null,
tx_burnt_fee: null,
tx_tag: null,
type: null,
value: '0',
};
......@@ -21,7 +21,8 @@
"lint:tsc": "./node_modules/.bin/tsc -p ./tsconfig.json",
"prepare": "husky install",
"format-svg": "./node_modules/.bin/svgo -r ./icons",
"test:pw": "playwright test -c playwright-ct.config.ts",
"test:pw": "./playwright/make-envs-script.sh && playwright test -c playwright-ct.config.ts",
"test:pw:local": "export NODE_PATH=$(pwd)/node_modules && yarn test:pw",
"test:pw:docker": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.28.0-focal ./playwright/run-tests.sh",
"test:jest": "jest",
"test:jest:watch": "jest --watch"
......@@ -67,6 +68,7 @@
"@types/phoenix": "^1.5.4",
"@types/react": "18.0.9",
"@types/react-dom": "18.0.5",
"@types/ws": "^8.5.3",
"@typescript-eslint/eslint-plugin": "^5.27.0",
"dotenv-cli": "^6.0.0",
"eslint": "8.16.0",
......@@ -84,7 +86,9 @@
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1",
"typescript": "4.7.2",
"vite-tsconfig-paths": "^3.5.2"
"vite-plugin-svgr": "^2.2.2",
"vite-tsconfig-paths": "^3.5.2",
"ws": "^8.11.0"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": "eslint --cache --fix"
......
......@@ -9,7 +9,7 @@ import { Chakra } from 'lib/Chakra';
import useConfigSentry from 'lib/hooks/useConfigSentry';
import type { ErrorType } from 'lib/hooks/useFetch';
import theme from 'theme';
import AppError from 'ui/shared/AppError';
import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary';
function MyApp({ Component, pageProps }: AppProps) {
......
......@@ -10,11 +10,11 @@ class MyDocument extends Document {
<Html lang="en">
<Head>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap"
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="icon" sizes="32x32" type="image/png" href="/static/favicon-32x32.png"/>
......
......@@ -27,7 +27,7 @@ import * as cookies from 'lib/cookies';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import type { Props as ServerSidePropsCommon } from 'lib/next/getServerSideProps';
import { getServerSideProps as getServerSidePropsCommon } from 'lib/next/getServerSideProps';
import AppError from 'ui/shared/AppError';
import AppError from 'ui/shared/AppError/AppError';
import Page from 'ui/shared/Page/Page';
type Props = ServerSidePropsCommon & {
......
import handler from 'lib/api/handler';
const getUrl = () => '/v2/stats';
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
// todo_tom leave only one api endpoint
import handler from 'lib/api/handler';
const getUrl = () => '/v2/stats';
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import Stats from '../ui/pages/Stats';
const StatsPage: NextPage = () => {
return (
<>
<Head><title>Ethereum Stats</title></Head>
<Stats/>
</>
);
};
export default StatsPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
import type { PlaywrightTestConfig } from '@playwright/experimental-ct-react';
import { devices } from '@playwright/experimental-ct-react';
import react from '@vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';
import tsconfigPaths from 'vite-tsconfig-paths';
/**
......@@ -10,7 +12,7 @@ const config: PlaywrightTestConfig = {
testMatch: /.*\.pw\.tsx/,
snapshotPathTemplate: '{testDir}/{testFileDir}/__screenshots__/{testFileName}_{arg}{ext}',
snapshotPathTemplate: '{testDir}/{testFileDir}/__screenshots__/{testFileName}_{projectName}_{arg}{ext}',
/* Maximum time one test can run for. */
timeout: 10 * 1000,
......@@ -42,15 +44,49 @@ const config: PlaywrightTestConfig = {
headless: true,
ctViteConfig: {
plugins: [ tsconfigPaths() ],
plugins: [
tsconfigPaths(),
react(),
svgr({
exportAsDefault: true,
}),
],
},
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: devices['Desktop Chrome'],
name: 'default',
grepInvert: /-@default/,
use: {
...devices['Desktop Chrome'],
viewport: { width: 1200, height: 750 },
},
},
{
name: 'mobile',
grep: /\+@mobile/,
use: {
...devices['iPhone 13 Pro'],
},
},
{
name: 'desktop xl',
grep: /\+@desktop-xl/,
use: {
...devices['Desktop Chrome'],
viewport: { width: 1600, height: 1000 },
},
},
{
name: 'dark color mode',
grep: /\+@dark-mode/,
use: {
...devices['Desktop Chrome'],
viewport: { width: 1200, height: 750 },
colorScheme: 'dark',
},
},
],
};
......
import { ChakraProvider } from '@chakra-ui/react';
import type { ColorMode } from '@chakra-ui/react';
import React from 'react';
import theme from 'theme';
type Props = {
children: React.ReactNode;
colorMode?: ColorMode;
}
const RenderWithChakra = ({ children, colorMode = 'light' }: Props) => {
return (
<ChakraProvider theme={{ ...theme, config: { ...theme.config, initialColorMode: colorMode } }}>
{ children }
</ChakraProvider>
);
};
export default RenderWithChakra;
import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
import { SocketProvider } from 'lib/socket/context';
import { PORT } from 'playwright/fixtures/socketServer';
import theme from 'theme';
type Props = {
children: React.ReactNode;
withSocket?: boolean;
}
const TestApp = ({ children, withSocket }: Props) => {
const [ queryClient ] = React.useState(() => new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: 0,
},
},
}));
return (
<ChakraProvider theme={ theme }>
<QueryClientProvider client={ queryClient }>
<SocketProvider url={ withSocket ? `ws://localhost:${ PORT }` : undefined }>
{ children }
</SocketProvider>
</QueryClientProvider>
</ChakraProvider>
);
};
export default TestApp;
import type { TestFixture } from '@playwright/test';
import type { WebSocket } from 'ws';
import { WebSocketServer } from 'ws';
import type { NewBlockSocketResponse } from 'types/api/block';
type ReturnType = () => Promise<WebSocket>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ArgsType = any;
type Channel = [string, string, string];
export interface SocketServerFixture {
createSocket: ReturnType;
}
export const PORT = 3200;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const createSocket: TestFixture<ReturnType, ArgsType> = async({ page }, use) => {
const socketServer = new WebSocketServer({ port: PORT });
const connectionPromise = new Promise<WebSocket>((resolve) => {
socketServer.on('connection', (socket: WebSocket) => {
resolve(socket);
});
});
await use(() => connectionPromise);
socketServer.close();
};
export const joinChannel = async(socket: WebSocket, channelName: string) => {
return new Promise<[string, string, string]>((resolve, reject) => {
socket.on('message', (msg) => {
try {
const payload: Array<string> = JSON.parse(msg.toString());
if (channelName === payload[2] && payload[3] === 'phx_join') {
socket.send(JSON.stringify([
payload[0],
payload[1],
payload[2],
'phx_reply',
{ response: {}, status: 'ok' },
]));
resolve([ payload[0], payload[1], payload[2] ]);
}
} catch (error) {
reject(error);
}
});
});
};
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'new_block', payload: NewBlockSocketResponse): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: string, payload: unknown): void {
socket.send(JSON.stringify([
...channel,
msg,
payload,
]));
}
This diff is collapsed.
......@@ -7,6 +7,7 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="/playwright/envs.js"></script>
<script type="module" src="/playwright/index.ts"></script>
</body>
</html>
// Import styles, initialize component theme here.
// import '../src/common.css';
import './fonts.css';
import { beforeMount } from '@playwright/experimental-ct-react/hooks';
import MockDate from 'mockdate';
import * as router from 'next/router';
const NEXT_ROUTER_MOCK = {
query: {},
};
beforeMount(async({ hooksConfig }) => {
// Before mount, redefine useRouter to return mock value from test.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: I really want to redefine this property :)
router.useRouter = () => hooksConfig?.router || NEXT_ROUTER_MOCK;
// set current date
MockDate.set('2022-11-11T12:00:00Z');
});
export {};
#!/bin/bash
targetFile='./playwright/envs.js'
declare -a envFiles=('./configs/envs/.env.pw' './configs/envs/.env.poa_core')
touch $targetFile;
truncate -s 0 $targetFile;
echo "Creating script file with envs"
echo "window.process = { env: { } };" >> $targetFile;
for envFile in "${envFiles[@]}"
do
# read each env file
while read line; do
# if it is a comment or an empty line, continue to next one
if [ "${line:0:1}" == "#" ] || [ "${line}" == "" ]; then
continue
fi
# split by "=" sign to get variable name and value
configName="$(cut -d'=' -f1 <<<"$line")";
configValue="$(cut -d'=' -f2- <<<"$line")";
# if there is a value, escape it and add line to target file
if [ -n "$configValue" ]; then
escapedConfigValue=$(echo $configValue | sed s/\'/\"/g);
echo "window.process.env.${configName} = '${escapedConfigValue}';" >> $targetFile;
fi
done < $envFile
done
echo "Done"
\ No newline at end of file
#!/bin/sh
yarn install
yarn test:pw
\ No newline at end of file
yarn install --modules-folder node_modules_linux
export NODE_PATH=$(pwd)/node_modules_linux
rm -rf ./playwright/.cache
yarn test:pw "$@"
\ No newline at end of file
......@@ -3,17 +3,39 @@ import {
createMultiStyleConfigHelpers,
defineStyle,
} from '@chakra-ui/styled-system';
import { mode } from '@chakra-ui/theme-tools';
import { runIfFn } from '@chakra-ui/utils';
const { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(parts.keys);
const baseStyleControl = defineStyle((props) => {
const { colorScheme: c } = props;
return {
_checked: {
bg: mode(`${ c }.500`, `${ c }.300`)(props),
borderColor: mode(`${ c }.500`, `${ c }.300`)(props),
_hover: {
bg: mode(`${ c }.600`, `${ c }.400`)(props),
borderColor: mode(`${ c }.600`, `${ c }.400`)(props),
},
},
_indeterminate: {
bg: mode(`${ c }.500`, `${ c }.300`)(props),
borderColor: mode(`${ c }.500`, `${ c }.300`)(props),
},
};
});
const baseStyleLabel = defineStyle({
_disabled: { opacity: 0.2 },
});
const baseStyle = definePartsStyle({
const baseStyle = definePartsStyle((props) => ({
label: baseStyleLabel,
});
control: runIfFn(baseStyleControl, props),
}));
const Checkbox = defineMultiStyleConfig({
baseStyle,
......
......@@ -4,6 +4,7 @@ import { getColor, mode } from '@chakra-ui/theme-tools';
import getDefaultFormColors from '../utils/getDefaultFormColors';
const baseStyle = defineStyle({
display: 'flex',
fontSize: 'md',
marginEnd: '3',
mb: '2',
......
......@@ -9,10 +9,16 @@ const { defineMultiStyleConfig, definePartsStyle } =
const baseStyleLabel = defineStyle({
_disabled: { opacity: 0.2 },
width: 'fit-content',
});
const baseStyleContainer = defineStyle({
width: 'fit-content',
});
const baseStyle = definePartsStyle({
label: baseStyleLabel,
container: baseStyleContainer,
});
const Radio = defineMultiStyleConfig({
......
import { switchAnatomy as parts } from '@chakra-ui/anatomy';
import { defineStyle, createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
import { mode } from '@chakra-ui/theme-tools';
const { defineMultiStyleConfig, definePartsStyle } =
createMultiStyleConfigHelpers(parts.keys);
const baseStyleTrack = defineStyle((props) => {
const { colorScheme: c } = props;
return {
_checked: {
bg: mode(`${ c }.500`, `${ c }.300`)(props),
_hover: {
bg: mode(`${ c }.600`, `${ c }.400`)(props),
},
},
};
});
const baseStyle = definePartsStyle((props) => ({
track: baseStyleTrack(props),
}));
const Switch = defineMultiStyleConfig({
baseStyle,
});
export default Switch;
......@@ -36,8 +36,7 @@ const sizes = {
fontSize: 'sm',
},
td: {
px: 4,
py: 6,
p: 4,
},
}),
sm: definePartsStyle({
......@@ -48,7 +47,7 @@ const sizes = {
},
td: {
px: '10px',
py: 6,
py: 4,
fontSize: 'sm',
fontWeight: 500,
},
......@@ -61,7 +60,7 @@ const sizes = {
},
td: {
px: '6px',
py: 6,
py: 4,
fontSize: 'sm',
fontWeight: 500,
},
......
......@@ -34,6 +34,7 @@ const baseStyleContainer = defineStyle({
display: 'inline-block',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
borderRadius: 'sm',
...transitionProps,
});
......
......@@ -13,6 +13,7 @@ import Popover from './Popover';
import Radio from './Radio';
import Skeleton from './Skeleton';
import Spinner from './Spinner';
import Switch from './Switch';
import Table from './Table';
import Tabs from './Tabs';
import Tag from './Tag';
......@@ -36,6 +37,7 @@ const components = {
Radio,
Skeleton,
Spinner,
Switch,
Tabs,
Table,
Tag,
......
......@@ -25,8 +25,11 @@ export default function getOutlinedFieldStyles(props: StyleFunctionProps) {
_disabled: {
opacity: 1,
backgroundColor: mode('gray.200', 'whiteAlpha.200')(props),
border: 'none',
borderColor: 'transparent',
cursor: 'not-allowed',
_hover: {
borderColor: 'transparent',
},
},
_invalid: {
borderColor: getColor(theme, ec),
......
......@@ -11,9 +11,10 @@ export interface WatchlistName {
export interface AddressParam {
hash: string;
implementation_name: string;
implementation_name: string | null;
name: string | null;
is_contract: boolean;
is_verified: boolean | null;
private_tags: Array<AddressTag> | null;
watchlist_names: Array<WatchlistName> | null;
public_tags: Array<AddressTag> | null;
......
......@@ -16,10 +16,10 @@ export interface Block {
total_difficulty: string;
gas_used: string | null;
gas_limit: string;
nonce: number;
base_fee_per_gas: number | null;
burnt_fees: number | null;
priority_fee: number | null;
nonce: string;
base_fee_per_gas: string | null;
burnt_fees: string | null;
priority_fee: string | null;
extra_data: string | null;
state_root: string | null;
rewards?: Array<Reward>;
......
......@@ -2,14 +2,21 @@ import type { AddressParam } from './addressParams';
export type TxInternalsType = 'call' | 'delegatecall' | 'staticcall' | 'create' | 'create2' | 'selfdestruct' | 'reward'
export interface InternalTransaction {
export type InternalTransaction = (
{
to: AddressParam;
created_contract: null;
} |
{
to: null;
created_contract: AddressParam;
}
) & {
error: string | null;
success: boolean;
type: TxInternalsType;
transaction_hash: string;
from: AddressParam;
to: AddressParam;
created_contract: AddressParam;
value: string;
index: number;
block: number;
......@@ -25,5 +32,5 @@ export interface InternalTransactionsResponse {
items_count: number;
transaction_hash: string;
transaction_index: number;
};
} | null;
}
......@@ -3,7 +3,7 @@ import type { DecodedInput } from './decodedInput';
export interface Log {
address: AddressParam;
topics: Array<string>;
topics: Array<string | null>;
data: string;
index: number;
decoded: DecodedInput | null;
......
import type { BlocksResponse, BlockTransactionsResponse } from 'types/api/block';
import type { BlocksResponse, BlockTransactionsResponse, BlockFilters } from 'types/api/block';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponse } from 'types/api/log';
import type { TokenTransferResponse } from 'types/api/tokenTransfer';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters';
import { QueryKeys } from 'types/client/queries';
import type { KeysOfObjectOrNull } from 'types/utils/KeysOfObjectOrNull';
......@@ -25,6 +26,13 @@ export type PaginatedResponse<Q extends PaginatedQueryKeys> =
Q extends QueryKeys.txTokenTransfers ? TokenTransferResponse :
never
export type PaginationFilters<Q extends PaginatedQueryKeys> =
Q extends QueryKeys.blocks ? BlockFilters :
Q extends QueryKeys.txsValidate ? TTxsFilters :
Q extends QueryKeys.txsPending ? TTxsFilters :
Q extends QueryKeys.txTokenTransfers ? TokenTransferFilters :
never
export type PaginationParams<Q extends PaginatedQueryKeys> = PaginatedResponse<Q>['next_page_params'];
type PaginationFields = {
......
export interface Reward {
reward: string;
type: 'Miner Reward' | 'Validator Reward' | 'Emission Reward' | 'Chore Reward' | 'Uncle Reward';
type: 'Miner Reward' | 'Validator Reward' | 'Emission Reward' | 'Chore Reward' | 'Uncle Reward' | 'POA Mania Reward';
}
......@@ -10,4 +10,5 @@ export type Stats = {
gas_prices: {average: number; fast: number; slow: number};
static_gas_price: string;
market_cap: string;
network_utilization_percentage: number;
}
import type { AddressParam } from './addressParams';
import type { TokenInfoGeneric } from './tokenInfo';
import type { TokenInfoGeneric, TokenType } from './tokenInfo';
export type Erc20TotalPayload = {
decimals: string | null;
......@@ -47,3 +47,7 @@ export interface TokenTransferResponse {
transaction_hash: string;
} | null;
}
export interface TokenTransferFilters {
type: Array<TokenType>;
}
......@@ -5,10 +5,18 @@ import type { TokenTransfer } from './tokenTransfer';
export type TransactionRevertReason = {
raw: string;
decoded: string;
} | DecodedInput;
export interface Transaction {
export type Transaction = (
{
to: AddressParam;
created_contract: null;
} |
{
to: null;
created_contract: AddressParam;
}
) & {
hash: string;
result: string;
confirmations: number;
......@@ -17,21 +25,19 @@ export interface Transaction {
timestamp: string | null;
confirmation_duration: Array<number>;
from: AddressParam;
to: AddressParam | null;
created_contract: AddressParam;
value: string;
fee: Fee;
gas_price: number;
type: number;
gas_price: string;
type: number | null;
gas_used: string | null;
gas_limit: string;
max_fee_per_gas: number | null;
max_priority_fee_per_gas: number | null;
priority_fee: number | null;
base_fee_per_gas: number | null;
tx_burnt_fee: number | null;
max_fee_per_gas: string | null;
max_priority_fee_per_gas: string | null;
priority_fee: string | null;
base_fee_per_gas: string | null;
tx_burnt_fee: string | null;
nonce: number;
position: number;
position: number | null;
revert_reason: TransactionRevertReason | null;
raw_input: string;
decoded_input: DecodedInput | null;
......
......@@ -19,15 +19,16 @@ export type MarketplaceCategory = { id: MarketplaceCategoriesIds; name: string }
export type AppItemPreview = {
id: string;
external: boolean;
title: string;
logo: string;
shortDescription: string;
categories: Array<MarketplaceCategoriesIds>;
url: string;
}
export type AppItemOverview = AppItemPreview & {
author: string;
url: string;
description: string;
site?: string;
twitter?: string;
......
......@@ -16,4 +16,5 @@ export enum QueryKeys {
chartsMarket = 'charts-market',
indexBlocks='indexBlocks',
indexTxs='indexTxs',
jsonRpcUrl='json-rpc-url'
}
export type StatsSection = { id: StatsSectionIds; title: string; charts: Array<StatsChart> }
export type StatsSectionIds = keyof typeof StatsSectionId;
export enum StatsSectionId {
'all',
'accounts',
'blocks',
'transactions',
'gas',
}
export type StatsInterval = { id: StatsIntervalIds; title: string }
export type StatsIntervalIds = keyof typeof StatsIntervalId;
export enum StatsIntervalId {
'all',
'oneMonth',
'threeMonths',
'sixMonths',
'oneYear',
}
export type StatsChart = {
visible?: boolean;
id: string;
title: string;
description: string;
apiMethodURL: string;
}
......@@ -15,9 +15,9 @@ import type { ApiKey, ApiKeys, ApiKeyErrors } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props = {
data?: ApiKey;
......@@ -33,7 +33,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255;
const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
mode: 'all',
defaultValues: {
token: data?.api_key || '',
......@@ -71,7 +71,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
});
}
return [ ...(prevData || []), response ];
return [ response, ...(prevData || []) ];
});
onClose();
......@@ -113,7 +113,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
maxLength={ NAME_MAX_LENGTH }
/>
<FormLabel>
{ getPlaceholderWithError('Application name for API key (e.g Web3 project)', errors.name?.message) }
<InputPlaceholder text="Application name for API key (e.g Web3 project)" error={ errors.name?.message }/>
</FormLabel>
</FormControl>
);
......@@ -145,7 +145,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Button
size="lg"
type="submit"
disabled={ !isValid }
disabled={ !isValid || !isDirty }
isLoading={ mutation.isLoading }
>
{ data ? 'Save' : 'Generate API key' }
......
......@@ -30,7 +30,7 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const renderText = useCallback(() => {
return (
<Text> API key for <Text fontWeight="600" as="span">{ ` "${ data.name || 'name' }" ` }</Text> will be deleted </Text>
<Text> API key for <Text fontWeight="700" as="span">{ ` "${ data.name || 'name' }" ` }</Text> will be deleted </Text>
);
}, [ data.name ]);
......
import { Box, Heading, Icon, IconButton, Image, Link, LinkBox, LinkOverlay, Text, useColorModeValue } from '@chakra-ui/react';
import NextLink from 'next/link';
import { Box, Heading, Icon, IconButton, Image, Link, LinkBox, Text, useColorModeValue } from '@chakra-ui/react';
import type { MouseEvent } from 'react';
import React, { useCallback } from 'react';
......@@ -8,9 +7,9 @@ import type { AppItemPreview } from 'types/client/apps';
import northEastIcon from 'icons/arrows/north-east.svg';
import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import link from 'lib/link/link';
import notEmpty from 'lib/notEmpty';
import AppCardLink from './AppCardLink';
import { APP_CATEGORIES } from './constants';
interface Props extends AppItemPreview {
......@@ -19,7 +18,10 @@ interface Props extends AppItemPreview {
onFavoriteClick: (id: string, isFavorite: boolean) => void;
}
const AppCard = ({ id,
const AppCard = ({
id,
url,
external,
title,
logo,
shortDescription,
......@@ -85,11 +87,12 @@ const AppCard = ({ id,
size={{ base: 'xs', sm: 'sm' }}
fontWeight="semibold"
>
<NextLink href={ link('app_index', { id: id }) } passHref>
<LinkOverlay>
{ title }
</LinkOverlay>
</NextLink>
<AppCardLink
id={ id }
url={ url }
external={ external }
title={ title }
/>
</Heading>
<Text
......
import { LinkOverlay } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
import link from 'lib/link/link';
type Props = {
id: string;
url: string;
external: boolean;
title: string;
}
const AppLink = ({ url, external, id, title }: Props) => {
return external ? (
<LinkOverlay href={ url } isExternal={ true }>
{ title }
</LinkOverlay>
) : (
<NextLink href={ link('app_index', { id: id }) } passHref>
<LinkOverlay>
{ title }
</LinkOverlay>
</NextLink>
);
};
export default AppLink;
......@@ -41,6 +41,8 @@ const AppList = ({ apps, onAppClick, displayedAppId, onModalClose, favoriteApps,
<AppCard
onInfoClick={ onAppClick }
id={ app.id }
external={ app.external }
url={ app.url }
title={ app.title }
logo={ app.logo }
shortDescription={ app.shortDescription }
......
import {
Box, Button, Flex, Heading, Icon, IconButton, Image, Link, List, Modal, ModalBody,
Box, Flex, Heading, Icon, IconButton, Image, Link, List, Modal, ModalBody,
ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Tag, Text,
} from '@chakra-ui/react';
import NextLink from 'next/link';
import React, { useCallback } from 'react';
import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps';
......@@ -14,10 +13,11 @@ import tgIcon from 'icons/social/telega.svg';
import twIcon from 'icons/social/tweet.svg';
import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities';
import link from 'lib/link/link';
import notEmpty from 'lib/notEmpty';
import AppModalLink from './AppModalLink';
import { APP_CATEGORIES } from './constants';
type Props = {
......@@ -35,6 +35,8 @@ const AppModal = ({
}: Props) => {
const {
title,
url,
external,
author,
description,
site,
......@@ -64,11 +66,13 @@ const AppModal = ({
onFavoriteClick(id, isFavorite);
}, [ onFavoriteClick, id, isFavorite ]);
const isMobile = useIsMobile();
return (
<Modal
isOpen={ Boolean(id) }
onClose={ onClose }
size={{ base: 'full', lg: 'md' }}
size={ isMobile ? 'full' : 'md' }
isCentered
>
<ModalOverlay/>
......@@ -119,16 +123,12 @@ const AppModal = ({
marginTop={{ base: 6, sm: 0 }}
>
<Box display="flex">
<NextLink href={ link('app_index', { id: id }) } passHref>
<Button
as="a"
size="sm"
marginRight={ 2 }
width={{ base: '100%', sm: 'auto' }}
>
Launch app
</Button>
</NextLink>
<AppModalLink
id={ id }
url={ url }
external={ external }
title={ title }
/>
<IconButton
aria-label="Mark as favorite"
......
import { Button } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
import link from 'lib/link/link';
type Props = {
id: string;
url: string;
external: boolean;
title: string;
}
const AppModalLink = ({ url, external, id }: Props) => {
const buttonProps = {
size: 'sm',
marginRight: 2,
width: { base: '100%', sm: 'auto' },
...(external ? {
target: '_blank',
rel: 'noopener noreferrer',
} : {}),
};
return external ? (
<Button
as="a"
href={ url }
{ ...buttonProps }
>Launch app</Button>
) : (
<NextLink href={ link('app_index', { id: id }) } passHref>
<Button
as="a"
{ ...buttonProps }
>Launch app</Button>
</NextLink>
);
};
export default AppModalLink;
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as blockMock from 'mocks/blocks/block';
import TestApp from 'playwright/TestApp';
import BlockDetails from './BlockDetails';
const API_URL = '/node-api/blocks/1';
const hooksConfig = {
router: {
query: { id: 1 },
},
};
test('regular block +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.base),
}));
const component = await mount(
<TestApp>
<BlockDetails/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL),
await page.getByText('View details').click();
await expect(component).toHaveScreenshot();
});
test('genesis block', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.genesis),
}));
const component = await mount(
<TestApp>
<BlockDetails/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL),
await page.getByText('View details').click();
await expect(component).toHaveScreenshot();
});
......@@ -228,7 +228,7 @@ const BlockDetails = () => {
</Tooltip>
) }
</DetailsInfoItem>
{ data.priority_fee !== null && data.priority_fee > 0 && (
{ data.priority_fee !== null && BigNumber(data.priority_fee).gt(ZERO) && (
<DetailsInfoItem
title="Priority fee / Tip"
hint="User-defined tips sent to validator for transaction priority/inclusion."
......
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as blockMock from 'mocks/blocks/block';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import BlocksContent from './BlocksContent';
const API_URL = '/node-api/blocks';
export 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('base view +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.baseListResponse),
}));
const component = await mount(
<TestApp>
<BlocksContent/>
</TestApp>,
);
await page.waitForResponse(API_URL);
await expect(component).toHaveScreenshot();
});
test('new item from socket', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.baseListResponse),
}));
const component = await mount(
<TestApp withSocket>
<BlocksContent/>
</TestApp>,
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'blocks:new_block');
socketServer.sendMessage(socket, channel, 'new_block', {
average_block_time: '6212.0',
block: {
...blockMock.base,
height: blockMock.base.height + 1,
timestamp: '2022-11-11T11:59:58Z',
},
});
await expect(component).toHaveScreenshot();
});
test('socket error', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.baseListResponse),
}));
const component = await mount(
<TestApp withSocket>
<BlocksContent/>
</TestApp>,
);
const socket = await createSocket();
await socketServer.joinChannel(socket, 'blocks:new_block');
socket.close();
await expect(component).toHaveScreenshot();
});
......@@ -2,7 +2,6 @@ import {
Box,
Button,
FormControl,
FormLabel,
Input,
Textarea,
useColorModeValue,
......@@ -16,11 +15,11 @@ import type { CustomAbi, CustomAbis, CustomAbiErrors } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props = {
data?: CustomAbi;
......@@ -37,7 +36,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255;
const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const { control, formState: { errors, isValid }, handleSubmit, setError } = useForm<Inputs>({
const { control, formState: { errors, isValid, isDirty }, handleSubmit, setError } = useForm<Inputs>({
defaultValues: {
contract_address_hash: data?.contract_address_hash || '',
name: data?.name || '',
......@@ -77,7 +76,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
});
}
return [ ...(prevData || []), response ];
return [ response, ...(prevData || []) ];
});
onClose();
......@@ -119,7 +118,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
isInvalid={ Boolean(errors.name) }
maxLength={ NAME_MAX_LENGTH }
/>
<FormLabel>{ getPlaceholderWithError('Project name', errors.name?.message) }</FormLabel>
<InputPlaceholder text="Project name" error={ errors.name?.message }/>
</FormControl>
);
}, [ errors, formBackgroundColor ]);
......@@ -133,7 +132,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
minH="300px"
isInvalid={ Boolean(errors.abi) }
/>
<FormLabel>{ getPlaceholderWithError(`Custom ABI [{...}] (JSON format)`, errors.abi?.message) }</FormLabel>
<InputPlaceholder text="Custom ABI [{...}] (JSON format)" error={ errors.abi?.message }/>
</FormControl>
);
}, [ errors, formBackgroundColor ]);
......@@ -171,7 +170,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Button
size="lg"
type="submit"
disabled={ !isValid }
disabled={ !isValid || !isDirty }
isLoading={ mutation.isLoading }
>
{ data ? 'Save' : 'Create custom ABI' }
......
......@@ -29,7 +29,7 @@ const DeleteCustomAbiModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const renderText = useCallback(() => {
return (
<Text>Custom ABI for<Text fontWeight="600" as="span">{ ` "${ data.name || 'name' }" ` }</Text>will be deleted</Text>
<Text>Custom ABI for<Text fontWeight="700" as="span">{ ` "${ data.name || 'name' }" ` }</Text>will be deleted</Text>
);
}, [ data.name ]);
......
......@@ -5,6 +5,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Block } from 'types/api/block';
import type { Stats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
......@@ -26,10 +27,14 @@ const LatestBlocks = () => {
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, Array<Block>>(
[ QueryKeys.indexBlocks ],
async() => await fetch(`/api/index/blocks`),
async() => await fetch(`/node-api/index/blocks`),
);
const queryClient = useQueryClient();
const statsQueryResult = useQuery<unknown, unknown, Stats>(
[ QueryKeys.stats ],
() => fetch('/node-api/stats'),
);
const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
queryClient.setQueryData([ QueryKeys.indexBlocks ], (prevData: Array<Block> | undefined) => {
......@@ -78,15 +83,19 @@ const LatestBlocks = () => {
content = (
<>
<Box mb={{ base: 6, lg: 9 }}>
<Text as="span" fontSize="sm">
Network utilization:{ nbsp }
</Text>
{ /* Not implemented in API yet */ }
<Text as="span" fontSize="sm" color="blue.400" fontWeight={ 700 }>
43.8%
</Text>
</Box>
{ statsQueryResult.isLoading && (
<Skeleton h="24px" w="170px" mb={{ base: 6, lg: 9 }}/>
) }
{ statsQueryResult.data?.network_utilization_percentage !== undefined && (
<Box mb={{ base: 6, lg: 9 }}>
<Text as="span" fontSize="sm">
Network utilization:{ nbsp }
</Text>
<Text as="span" fontSize="sm" color="blue.400" fontWeight={ 700 }>
{ statsQueryResult.data?.network_utilization_percentage.toFixed(2) }%
</Text>
</Box>
) }
<VStack spacing={ `${ BLOCK_MARGIN }px` } mb={ 6 } height={ `${ BLOCK_HEIGHT * blocksCount + BLOCK_MARGIN * (blocksCount - 1) }px` } overflow="hidden">
<AnimatePresence initial={ false } >
{ dataToShow.map((block => <LatestBlocksItem key={ block.height } block={ block } h={ BLOCK_HEIGHT }/>)) }
......@@ -101,7 +110,7 @@ const LatestBlocks = () => {
return (
<>
<Heading as="h4" fontSize="18px" mb={{ base: 3, lg: 8 }}>Latest Blocks</Heading>
<Heading as="h4" size="sm" mb={{ base: 4, lg: 7 }}>Latest Blocks</Heading>
{ content }
</>
);
......
......@@ -19,7 +19,7 @@ const LatestTransactions = () => {
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, Array<Transaction>>(
[ QueryKeys.indexTxs ],
async() => await fetch(`/api/index/txs`),
async() => await fetch(`/node-api/index/txs`),
);
let content;
......@@ -54,7 +54,7 @@ const LatestTransactions = () => {
return (
<>
<Heading as="h4" fontSize="18px" mb={{ base: 3, lg: 8 }}>Latest transactions</Heading>
<Heading as="h4" size="sm" mb={ 4 }>Latest transactions</Heading>
{ content }
</>
);
......
......@@ -40,7 +40,7 @@ const LatestBlocksItem = ({ tx }: Props) => {
const borderColor = useColorModeValue('gray.200', 'whiteAlpha.200');
const iconColor = useColorModeValue('blue.600', 'blue.300');
const dataTo = tx.to && tx.to.hash ? tx.to : tx.created_contract;
const dataTo = tx.to ? tx.to : tx.created_contract;
const timeAgo = useTimeAgoIncrement(tx.timestamp || '0', true);
const isMobile = useIsMobile();
......
......@@ -28,7 +28,7 @@ const Stats = () => {
const { data, isLoading, isError } = useQuery<unknown, unknown, Stats>(
[ QueryKeys.stats ],
async() => await fetch(`/api/index/stats`),
async() => await fetch(`/node-api/stats`),
);
if (isError) {
......
import { useToken } from '@chakra-ui/react';
import React from 'react';
import type { ChainIndicatorChartData } from './types';
import type { TimeChartData } from 'ui/shared/chart/types';
import ChartArea from 'ui/shared/chart/ChartArea';
import ChartLine from 'ui/shared/chart/ChartLine';
......@@ -11,7 +11,7 @@ import useChartSize from 'ui/shared/chart/useChartSize';
import useTimeChartController from 'ui/shared/chart/useTimeChartController';
interface Props {
data: ChainIndicatorChartData;
data: TimeChartData;
caption?: string;
}
......
import { Flex, Spinner } from '@chakra-ui/react';
import { Flex } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { ChainIndicatorChartData } from './types';
import type { TimeChartData } from 'ui/shared/chart/types';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ChainIndicatorChart from './ChainIndicatorChart';
type Props = UseQueryResult<ChainIndicatorChartData>;
type Props = UseQueryResult<TimeChartData>;
const ChainIndicatorChartContainer = ({ data, isError, isLoading }: Props) => {
const content = (() => {
if (isLoading) {
return <Spinner size="md" m="auto"/>;
return <ContentLoader mt="auto"/>;
}
if (isError) {
......
......@@ -18,7 +18,7 @@ interface Props {
}
const ChainIndicatorItem = ({ id, title, value, icon, isSelected, onClick, stats }: Props) => {
const bgColor = useColorModeValue('white', 'gray.900');
const bgColor = useColorModeValue('white', 'black');
const isMobile = useIsMobile();
const handleClick = React.useCallback(() => {
......
......@@ -40,8 +40,8 @@ const ChainIndicators = () => {
() => fetch('/node-api/stats'),
);
const bgColor = useColorModeValue('white', 'gray.900');
const listBgColor = useColorModeValue('gray.50', 'black');
const bgColor = useColorModeValue('white', 'black');
const listBgColor = useColorModeValue('gray.50', 'gray.900');
if (indicators.length === 0) {
return null;
......@@ -91,7 +91,16 @@ const ChainIndicators = () => {
<ChainIndicatorChartContainer { ...queryResult }/>
</Flex>
{ indicators.length > 1 && (
<Flex flexShrink={ 0 } flexDir="column" as="ul" p={ 3 } borderRadius="lg" bgColor={ listBgColor } rowGap={ 3 } order={{ base: 1, lg: 2 }}>
<Flex
flexShrink={ 0 }
flexDir="column"
as="ul"
p={ 3 }
borderRadius="lg"
bgColor={ listBgColor }
rowGap={ 3 }
order={{ base: 1, lg: 2 }}
>
{ indicators.map((indicator) => (
<ChainIndicatorItem
key={ indicator.id }
......
import type { ChartTransactionResponse, ChartMarketResponse } from 'types/api/charts';
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { Stats } from 'types/api/stats';
import type { QueryKeys } from 'types/client/queries';
import type { TimeChartDataItem } from 'ui/shared/chart/types';
import type { TimeChartData } from 'ui/shared/chart/types';
export type ChartsQueryKeys = QueryKeys.chartsTxs | QueryKeys.chartsMarket;
......@@ -16,7 +16,7 @@ export interface TChainIndicator<Q extends ChartsQueryKeys> {
api: {
queryName: Q;
path: string;
dataFn: (response: ChartsResponse<Q>) => ChainIndicatorChartData;
dataFn: (response: ChartsResponse<Q>) => TimeChartData;
};
}
......@@ -24,5 +24,3 @@ export type ChartsResponse<Q extends ChartsQueryKeys> =
Q extends QueryKeys.chartsTxs ? ChartTransactionResponse :
Q extends QueryKeys.chartsMarket ? ChartMarketResponse :
never;
export type ChainIndicatorChartData = Array<TimeChartDataItem>;
......@@ -2,13 +2,14 @@ import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { TChainIndicator, ChartsResponse, ChartsQueryKeys, ChainIndicatorChartData } from './types';
import type { TChainIndicator, ChartsResponse, ChartsQueryKeys } from './types';
import type { TimeChartData } from 'ui/shared/chart/types';
import useFetch from 'lib/hooks/useFetch';
type NotUndefined<T> = T extends undefined ? never : T;
export default function useFetchCharData<Q extends ChartsQueryKeys>(indicator: TChainIndicator<Q> | undefined): UseQueryResult<ChainIndicatorChartData> {
export default function useFetchChartData<Q extends ChartsQueryKeys>(indicator: TChainIndicator<Q> | undefined): UseQueryResult<TimeChartData> {
const fetch = useFetch();
type ResponseType = ChartsResponse<NotUndefined<typeof indicator>['api']['queryName']>;
......@@ -23,6 +24,6 @@ export default function useFetchCharData<Q extends ChartsQueryKeys>(indicator: T
return {
...queryResult,
data: queryResult.data && indicator ? indicator.api.dataFn(queryResult.data) : queryResult.data,
} as UseQueryResult<ChainIndicatorChartData>;
} as UseQueryResult<TimeChartData>;
}, [ indicator, queryResult ]);
}
......@@ -25,7 +25,7 @@ const dailyTxsIndicator: TChainIndicator<QueryKeys.chartsTxs> = {
.map((item) => ({ date: new Date(item.date), value: item.tx_count }))
.sort(sortByDateDesc),
name: 'Tx/day',
valueFormatter: (x) => shortenNumberWithLetter(x, undefined, { maximumFractionDigits: 2 }),
valueFormatter: (x: number) => shortenNumberWithLetter(x, undefined, { maximumFractionDigits: 2 }),
} ]),
},
};
......@@ -44,7 +44,7 @@ const coinPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = {
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) }))
.sort(sortByDateDesc),
name: `${ appConfig.network.currency.symbol } price`,
valueFormatter: (x) => '$' + x.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
valueFormatter: (x: number) => '$' + x.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
} ]),
},
};
......@@ -64,7 +64,7 @@ const marketPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = {
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) * Number(response.available_supply) }))
.sort(sortByDateDesc),
name: 'Market cap',
valueFormatter: (x) => '$' + shortenNumberWithLetter(x, undefined, { maximumFractionDigits: 0 }),
valueFormatter: (x: number) => '$' + shortenNumberWithLetter(x, undefined, { maximumFractionDigits: 0 }),
} ]),
},
};
......
......@@ -14,13 +14,12 @@ import ApiKeyListItem from 'ui/apiKey/ApiKeyTable/ApiKeyListItem';
import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable';
import DeleteApiKeyModal from 'ui/apiKey/DeleteApiKeyModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
import SkeletonTable from 'ui/shared/SkeletonTable';
import DataFetchAlert from '../shared/DataFetchAlert';
const DATA_LIMIT = 3;
const ApiKeysPage: React.FC = () => {
......@@ -58,7 +57,7 @@ const ApiKeysPage: React.FC = () => {
const description = (
<AccountPageDescription>
Create API keys to use for your RPC and EthRPC API requests. For more information, see { space }
<Link href="https://docs.blockscout.com/for-users/api#api-keys">“How to use a Blockscout API key”</Link>.
<Link href="https://docs.blockscout.com/for-users/api#api-keys" target="_blank">“How to use a Blockscout API key”</Link>.
</AccountPageDescription>
);
......@@ -108,7 +107,12 @@ const ApiKeysPage: React.FC = () => {
<>
{ description }
{ Boolean(data.length) && list }
<Stack marginTop={ 8 } spacing={ 5 } direction={{ base: 'column', lg: 'row' }}>
<Stack
marginTop={ 8 }
spacing={ 5 }
direction={{ base: 'column', lg: 'row' }}
align={{ base: 'start', lg: 'center' }}
>
<Button
size="lg"
onClick={ apiKeyModalProps.onOpen }
......
......@@ -24,7 +24,7 @@ const BlockPageContent = () => {
return (
<Page>
<PageTitle text={ `Block #${ router.query.id }` }/>
<RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 12 }}/>
<RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 8 }}/>
</Page>
);
};
......
......@@ -13,13 +13,12 @@ import CustomAbiListItem from 'ui/customAbi/CustomAbiTable/CustomAbiListItem';
import CustomAbiTable from 'ui/customAbi/CustomAbiTable/CustomAbiTable';
import DeleteCustomAbiModal from 'ui/customAbi/DeleteCustomAbiModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
import SkeletonTable from 'ui/shared/SkeletonTable';
import DataFetchAlert from '../shared/DataFetchAlert';
const CustomAbiPage: React.FC = () => {
const customAbiModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
......
......@@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { JsonRpcUrlResponse } from 'types/api/json-rpc-url';
import type { AppItemOverview } from 'types/client/apps';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import useFetch from 'lib/hooks/useFetch';
......@@ -27,7 +28,7 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
}, []);
const { data: jsonRpcUrlResponse } = useQuery<unknown, unknown, JsonRpcUrlResponse>(
[ 'json-rpc-url' ],
[ QueryKeys.jsonRpcUrl ],
async() => await fetch(`/node-api/config/json-rpc-url`),
{ refetchOnMount: false },
);
......
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