Commit a313fd7e authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into tx-revert-reason

parents 553aad0e 00c7792b
......@@ -4,7 +4,6 @@ NEXT_PUBLIC_FOOTER_GITHUB_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_GITHUB_LINK
NEXT_PUBLIC_FOOTER_TWITTER_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_TWITTER_LINK
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_TELEGRAM_LINK
NEXT_PUBLIC_FOOTER_STAKING_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_STAKING_LINK
NEXT_PUBLIC_FOOTER_STAKING_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_STAKING_LINK
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=APP_NEXT_NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM
NEXT_PUBLIC_SENTRY_DSN=APP_NEXT_NEXT_PUBLIC_SENTRY_DSN
NEXT_PUBLIC_APP_INSTANCE=APP_NEXT_NEXT_PUBLIC_APP_INSTANCE
......
......@@ -4,11 +4,12 @@ on:
pull_request:
types:
- closed
- merged
workflow_dispatch:
jobs:
if_merged:
if: github.event.pull_request.merged == true
uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup.yaml@fix-e2e-tests
cleanup:
uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup.yaml@master
with:
appNamespace: review-front-$GITHUB_HEAD_REF_SLUG
secrets: inherit
name: Run E2E tests k8s
on:
push:
# push:
# pull_request:
workflow_dispatch
env:
K8S_LOCAL_PORT: ${{ secrets.K8S_LOCAL_PORT }}
......@@ -65,7 +66,7 @@ jobs:
deploy_and_tests:
needs: push_to_registry
uses: blockscout/blockscout-ci-cd/.github/workflows/e2e_new.yaml@fix-e2e-tests
uses: blockscout/blockscout-ci-cd/.github/workflows/e2e_new.yaml@master
with:
valuesDir: deploy/values/e2e
appName: e2e-front
......
name: Run code checks
on: [pull_request]
name: Linters
on:
pull_request:
push:
branches:
- main
jobs:
check_code:
name: Check code
name: Run code checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
cache: 'yarn'
- name: Install dependencies
run: yarn
run: yarn install --frozen-lockfile
- name: Run ESLint
run: yarn lint:eslint
- name: Compile TypeScript
......
name: playwright
name: Playwright
on: [pull_request]
jobs:
test:
name: Run components visual tests
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.27.0-focal
......@@ -11,11 +12,12 @@ jobs:
- uses: actions/checkout@v3
with:
lfs: 'true'
- uses: actions/setup-node@v2
- uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'yarn'
- name: Install dependencies
run: yarn
run: yarn install --frozen-lockfile
- name: Run your tests
run: HOME=/root yarn test-ct
- name: Upload test results
......
......@@ -3,6 +3,7 @@ name: Deploy review environment
on:
# push:
pull_request:
workflow_dispatch:
env:
K8S_LOCAL_PORT: ${{ secrets.K8S_LOCAL_PORT }}
......@@ -65,7 +66,7 @@ jobs:
deploy_frontend:
needs: push_to_registry
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@fix-e2e-tests
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master
with:
valuesDir: deploy/values/review
appNamespace: review-front-$GITHUB_HEAD_REF_SLUG
......
......@@ -54,11 +54,15 @@ jobs:
SENTRY_CSP_REPORT_URI=${{ secrets.SENTRY_CSP_REPORT_URI }}
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
deploy_frontend:
deploy_main:
needs: push_to_registry
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@deploy-smart-contract-verifier
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master
with:
valuesDir: deploy/values
appNamespace: frontend-main
appName: frontend
valuesDir: deploy/values/review
appNamespace: front-main
blockscoutIngressHost: blockscout
frontendIngressHost: frontend
frontendImage: ghcr.io/blockscout/frontend:main
gethIngressHost: geth
scVerifierIngressHost: sc-verifier
secrets: inherit
......@@ -33,11 +33,13 @@ We use [playwright experimental components testing](https://playwright.dev/docs/
To perform testing locally you need to install docker and run `yarn test-docker`
## Environment variables
### Variables list
The app instance could be customized by passing following variables to NodeJS environment at runtime.
**IMPORTANT NOTE!** For _production_ build purposes all json-like values should be single-quoted
### Network configuration
| Variable | Type | Description | Default value
| --- | --- | --- | --- |
| NEXT_PUBLIC_NETWORK_NAME | `string` | Displayed name of the network | `Gnosis Chain` |
......@@ -50,21 +52,37 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME | `string` *(optional)* | Network name for constructing url of token logos according to template `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/${assetsNamePath}/assets/${tokenAddress}/logo.png`. It should match network name in TrustWallet assets repo, see the full list [here](https://github.com/trustwallet/assets/tree/master/blockchains). If not provided, the network type will be used as its assets path part | `ethereum` |
| NEXT_PUBLIC_NETWORK_LOGO | `string` *(optional)* | Network logo; if not provided, will fallback to logo predefined in the project; if the project doesn't have logo for such network then the common placeholder will be shown; *Note* that logo height should be 20px and width less than 120px | `https://www.fillmurray.com/240/40` |
| NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED | `boolean` *(optional)* | Set to true if network has account feature | `true` |
| NEXT_PUBLIC_FEATURED_NETWORKS | `Array<FeaturedNetwork>` where `FeaturedNetwork` can have following [properties](#network-configuration-properties) | Configuration of featured networks that will be shown in the app menu | `[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'}]` |
*Note* the base path for the network is built up from its `type` and `subType` like so `https://blockscout.com/<type>/<subType>`
### UI configuration
| Variable | Type | Description | Default value
| --- | --- | --- | --- |
| NEXT_PUBLIC_FEATURED_NETWORKS | `Array<FeaturedNetwork>` where `FeaturedNetwork` can have following [properties](#featured-network-configuration-properties) | Configuration of featured networks that will be shown in the network menu | `[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'}]` |
| NEXT_PUBLIC_BLOCKSCOUT_VERSION | `string` *(optional)* | Current running version of Blockscout (used to display link to release in the footer) |
| NEXT_PUBLIC_FOOTER_GITHUB_LINK | `string` *(optional)* | Link to Github in the footer | `https://github.com/blockscout/blockscout` |
| NEXT_PUBLIC_FOOTER_TWITTER_LINK | `string` *(optional)* | Link to Twitter in the footer | `https://www.twitter.com/blockscoutcom` |
| NEXT_PUBLIC_FOOTER_TELEGRAM_LINK | `string` *(optional)* | Link to Telegram in the footer | `https://t.me/poa_network` |
| NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` *(optional)* | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` |
| NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | `https://airtable.com/shrqUAcjgGJ4jU88C` |
### App configuration
| Variable | Type | Description | Default value
| --- | --- | --- | --- |
| NEXT_PUBLIC_APP_INSTANCE | `string` *(optional)* | Name of app instance | `wonderful_kepler` |
| NEXT_PUBLIC_APP_PROTOCOL | `http \| https` *(optional)* | App protocol (`https` used as default value) | `https` |
| NEXT_PUBLIC_APP_HOST | `string` | App host | `blockscout.com` |
| NEXT_PUBLIC_APP_PORT | `number` *(optional)* | Port where app is running. Have to be provided if it is different to default port | `3000` |
### API configuration
| Variable | Type | Description | Default value
| --- | --- | --- | --- |
| NEXT_PUBLIC_API_ENDPOINT | `string` *(optional)* | By default the API endpoint base URL will be set as `https://blockscout.com`. If it is not the case, pass the API endpoint base URL in this variable | `https://blockscout.com` |
| NEXT_PUBLIC_API_BASE_PATH | `string` *(optional)* | Base path for API endpoint url | `/poa/core` |
| NEXT_PUBLIC_SENTRY_DSN | `string` *(optional)* | Client key for your Senty.io app | `<secret>` |
| SENTRY_CSP_REPORT_URI | `string` *(optional)* | URL for sending CSP-reports to your Senty.io app | `<secret>` |
### Featured network configuration properties
......@@ -75,8 +93,9 @@ The app instance could be customized by passing following variables to NodeJS en
| group | `mainnets \| testnets \| other` | Indicates in which tab network appears in the menu | `'mainnets'` |
| icon | `string` *(optional)* | Network icon; if not provided, will fallback to icon predefined in the project; if the project doesn't have icon for such network then the common placeholder will be shown; *Note* that icon size should be 30px by 30px | `'https://www.fillmurray.com/60/60'` |
*Note* the base path for the network is built up from its `type` and `subType` like so `https://blockscout.com/<type>/<subType>`
### Sentry.io setup
### External services configuration
TBD
\ No newline at end of file
| Variable | Type | Description | Default value
| --- | --- | --- | --- |
| NEXT_PUBLIC_SENTRY_DSN | `string` *(optional)* | Client key for your Senty.io app | `<secret>` |
| SENTRY_CSP_REPORT_URI | `string` *(optional)* | URL for sending CSP-reports to your Senty.io app | `<secret>` |
\ No newline at end of file
# app config
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_INSTANCE=local
# nav and footer 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
NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'}]
# marketplace config
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
# api config
NEXT_PUBLIC_API_ENDPOINT=https://blockscout.com
\ No newline at end of file
# nav and footer config
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'},{'title':'Optimism on Gnosis Chain','basePath':'/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60'},{'title':'Arbitrum on xDai','basePath':'/xdai/aox','group':'mainnets'},{'title':'Ethereum','basePath':'/eth/mainnet','group':'mainnets'},{'title':'Ethereum Classic','basePath':'/etx/mainnet','group':'mainnets'},{'title':'POA','basePath':'/poa/core','group':'mainnets'},{'title':'RSK','basePath':'/rsk/mainnet','group':'mainnets'},{'title':'Gnosis Chain Testnet','basePath':'/xdai/testnet','group':'testnets'},{'title':'POA Sokol','basePath':'/poa/sokol','group':'testnets'},{'title':'ARTIS Σ1','basePath':'/artis/sigma1','group':'other'},{'title':'LUKSO L14','basePath':'/lukso/l14','group':'other'},{'title':'Astar','basePath':'/astar','group':'other'}]
# current network config
NEXT_PUBLIC_NETWORK_NAME=POA
NEXT_PUBLIC_NETWORK_SHORT_NAME=POA
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=poa
......@@ -9,6 +13,6 @@ NEXT_PUBLIC_NETWORK_ID=99
NEXT_PUBLIC_NETWORK_CURRENCY=POA
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=0x029a799563238d0e75e20be2f4bda0ea68d00172
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'},{'title':'Optimism on Gnosis Chain','basePath':'/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60'},{'title':'Arbitrum on xDai','basePath':'/xdai/aox','group':'mainnets'},{'title':'Ethereum','basePath':'/eth/mainnet','group':'mainnets'},{'title':'Ethereum Classic','basePath':'/etx/mainnet','group':'mainnets'},{'title':'POA','basePath':'/poa/core','group':'mainnets'},{'title':'RSK','basePath':'/rsk/mainnet','group':'mainnets'},{'title':'Gnosis Chain Testnet','basePath':'/xdai/testnet','group':'testnets'},{'title':'POA Sokol','basePath':'/poa/sokol','group':'testnets'},{'title':'ARTIS Σ1','basePath':'/artis/sigma1','group':'other'},{'title':'LUKSO L14','basePath':'/lukso/l14','group':'other'},{'title':'Astar','basePath':'/astar','group':'other'}]
NEXT_PUBLIC_API_ENDPOINT=https://blockscout.com
# api config
NEXT_PUBLIC_API_BASE_PATH=/poa/core
const BASE_PATH = require('../../lib/link/basePath.js');
const PATHS = require('../../lib/link/paths.js');
const oldUrls = [
{
oldPath: '/account/tag_address',
newPath: `${ PATHS.private_tags }?tab=address`,
},
{
oldPath: '/account/tag_transaction',
newPath: `${ PATHS.private_tags }?tab=tx`,
},
{
oldPath: '/pending-transactions',
newPath: `${ PATHS.txs }?tab=pending`,
},
{
oldPath: '/tx/:id/internal-transactions',
newPath: `${ PATHS.tx }?tab=internal`,
},
{
oldPath: '/tx/:id/logs',
newPath: `${ PATHS.tx }?tab=logs`,
},
{
oldPath: '/tx/:id/raw-trace',
newPath: `${ PATHS.tx }?tab=raw_trace`,
},
{
oldPath: '/tx/:id/state',
newPath: `${ PATHS.tx }?tab=state`,
},
{
oldPath: '/uncles',
newPath: `${ PATHS.blocks }?tab=uncles`,
},
{
oldPath: '/reorgs',
newPath: `${ PATHS.blocks }?tab=reorgs`,
},
{
oldPath: '/block/:id/transactions',
newPath: `${ PATHS.block }?tab=txs`,
},
];
async function redirects() {
const homePagePath = '/' + [ process.env.NEXT_PUBLIC_NETWORK_TYPE, process.env.NEXT_PUBLIC_NETWORK_SUBTYPE ].filter(Boolean).join('/');
return [
{
source: '/',
destination: homePagePath,
permanent: false,
},
...oldUrls.map(item => {
const source = BASE_PATH.replaceAll('[', ':').replaceAll(']', '') + item.oldPath;
const destination = item.newPath.replaceAll('[', ':').replaceAll(']', '');
return { source, destination, permanent: false };
}),
];
}
......
[
{
"chainIds": [
1
"1"
],
"author": "xDaichain",
"id": "easy-staking",
......@@ -21,7 +21,7 @@
},
{
"chainIds": [
99
"99"
],
"author": "xDaichain",
"id": "curve",
......@@ -41,7 +41,7 @@
},
{
"chainIds": [
1
"1"
],
"author": "xDaichain",
"id": "honwyswap",
......@@ -61,7 +61,7 @@
},
{
"chainIds": [
99
"99"
],
"author": "xDaichain",
"id": "sushi",
......@@ -81,7 +81,7 @@
},
{
"chainIds": [
100
"100"
],
"author": "xDaichain",
"id": "bao-finance",
......@@ -101,7 +101,7 @@
},
{
"chainIds": [
99
"99"
],
"author": "xDaichain",
"id": "component",
......@@ -121,7 +121,7 @@
},
{
"chainIds": [
1
"1"
],
"author": "xDaichain",
"id": "pooltogether",
......@@ -141,7 +141,7 @@
},
{
"chainIds": [
99
"99"
],
"author": "xDaichain",
"id": "swapr",
......@@ -161,7 +161,7 @@
},
{
"chainIds": [
1
"1"
],
"author": "xDaichain",
"id": "levinswap",
......@@ -181,7 +181,7 @@
},
{
"chainIds": [
99
"99"
],
"author": "xDaichain",
"id": "omen",
......@@ -201,7 +201,7 @@
},
{
"chainIds": [
1
"1"
],
"author": "xDaichain",
"id": "nifty-ink",
......@@ -221,7 +221,7 @@
},
{
"chainIds": [
99
"99"
],
"author": "xDaichain",
"id": "treasure-chess",
......@@ -241,7 +241,7 @@
},
{
"chainIds": [
1
"1"
],
"author": "xDaichain",
"id": "unique-one",
......@@ -261,7 +261,7 @@
},
{
"chainIds": [
99
"99"
],
"author": "xDaichain",
"id": "cold-truth-culture",
......@@ -281,7 +281,7 @@
},
{
"chainIds": [
1
"1"
],
"author": "xDaichain",
"id": "xdai-bridge",
......@@ -301,7 +301,7 @@
},
{
"chainIds": [
99
"99"
],
"author": "xDaichain",
"id": "omni-bridge",
......@@ -321,7 +321,7 @@
},
{
"chainIds": [
1
"1"
],
"author": "xDaichain",
"id": "gnosis-safe",
......@@ -341,7 +341,7 @@
},
{
"chainIds": [
99
"99"
],
"author": "xDaichain",
"id": "multisender",
......@@ -361,7 +361,7 @@
},
{
"chainIds": [
1
"1"
],
"author": "xDaichain",
"id": "disperse",
......@@ -381,7 +381,7 @@
},
{
"chainIds": [
99
"99"
],
"author": "xDaichain",
"id": "symmetric",
......
/* eslint-disable max-len */
export const tx = {
hash: '0x1ea365d2144796f793883534aa51bf20d23292b19478994eede23dfc599e7c34',
status: 'ok' as Transaction['status'],
block_num: 15006918,
confirmation_num: 283,
confirmation_duration: 30,
timestamp: 1662623567695,
address_from: {
hash: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830',
type: 'Address',
alias: '',
},
address_to: {
hash: '0x35317007D203b8a86CA727ad44E473E40450E378',
type: 'Contract',
alias: '',
},
amount: {
value: 0.03,
value_usd: 35.5,
},
fee: {
value: 0.002395904453623692,
value_usd: 2.84,
},
gas_price: 0.000000017716513811,
gas_limit: 208420,
gas_used: 159319,
gas_fees: {
base: 13.538410068,
max: 20.27657523,
max_priority: 1.5,
},
burnt_fees: {
value: 0.002156925953623692,
value_usd: 2.55,
},
type: {
value: '2',
eip: 'EIP-1559',
},
nonce: 4,
position: 342,
input_hex: '0x42842e0e0000000000000000000000007767dac225a233ea1055d79fb227b1696d538b75000000000000000000000000fc3017c31fe752fc48e904050ea5d6edfc38a1b00000000000000000000000000000000000000000000000000000000000000e3b',
transferred_tokens: [
{ from: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', to: '0xF7A558692dFB5F456e291791da7FAE8Dd046574e', token: { symbol: 'VIK', hash: '0xADFE00d92e5A16e773891F59780e6e54f40B532e', name: 'Viktor Coin' }, amount: 192.7, usd: 194.05 },
{ from: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', to: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', token: { symbol: 'PAO', hash: '0xC98a06220239818B086CD96756d4E3bC41EC848f', name: 'POA Candy' }, amount: 76.1851851851846, usd: 194.05 },
],
txType: 'transaction' as TxType,
};
export type TxType = 'contract-call' | 'transaction' | 'token-transfer' | 'internal-tx' | 'multicall';
import type { Transaction } from 'types/api/transaction';
import type { Transaction } from 'types/api/transaction';
import { tx } from './tx';
import type { TxType } from './tx';
export const txs = [
{
...tx,
method: 'Withdraw',
txType: 'transaction' as TxType,
errorText: '',
},
{
...tx,
status: 'error' as Transaction['status'],
errorText: 'Error: (Awaiting internal transactions for reason)',
txType: 'contract-call' as TxType,
method: 'CommitHash CommitHash CommitHash CommitHash',
amount: {
value: 0.04,
value_usd: 35.5,
},
fee: {
value: 0.002295904453623692,
value_usd: 2.84,
},
},
{
...tx,
status: null,
txType: 'token-transfer' as TxType,
method: 'Multicall',
address_from: {
hash: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830',
alias: 'tkdkdkdkdkdkdkdkdkdkdkdkdkdkd.eth',
type: 'ENS name',
},
amount: {
value: 0.02,
value_usd: 35.5,
},
fee: {
value: 0.002495904453623692,
value_usd: 2.84,
},
errorText: '',
},
];
......@@ -23,10 +23,10 @@ function replace_envs {
envValue=$(env | grep "^$configName=" | grep -oe '[^=]*$');
# if config found
if [ -n "$configValue" ] && [ -n "$envValue" ]; then
if [ -n "$configValue" ]; then
# replace all
echo "Replace: ${configValue} with: ${envValue}"
find $nextFolder \( -type d -name .git -prune \) -o -type f -print0 | xargs -0 sed -i "s#$configValue#$envValue#g"
find $nextFolder \( -type d -name .git -prune \) -o -type f -print0 | xargs -0 sed -i "s#$configValue#${envValue-''}#g"
fi
done < $envFilename
}
......
......@@ -65,19 +65,21 @@ geth:
frontend:
environment:
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS:
_default: ENC[AES256_GCM,data:fhH2QYodAkC/fIi5yU7Ur567buyIqQeTNUgserFNPs31yUeySk+BItpt,iv:7T7o5yX0Mvt1bYD9Cb6LxW9qHX1nzA99ykejJ4/KdpY=,tag:lfNUdrHGwU3BtsSvrnccYA==,type:str]
_default: ENC[AES256_GCM,data:IVCGBLqP7IdmbDe9UbIFvJSBD7+g52chKzakELt2XuHDp9JvC4E+7xxp,iv:bDHp2llHAqhgI5N8swQALSDc6X3S0JCsXbJnEEDDJOc=,tag:Z1WsJXkqtq79WydIgUiDiA==,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:
_default: ENC[AES256_GCM,data:Hf4azYsGh2lysotK7afaHI85IaLBJeQmPlGE/lwokmX2eaQHVZJ/i5RsaoKoOCSiNyfAowr7P+6IBEC16BUTQMpbhYveBd7c/xjIsoomoHbTdoAdZA/QxNVpS4a2qVthruFudyT1BoZUHIGQ,iv:Mid+PfbslOyivrFSXopdeW96YvOcLP+g2RGcw1o7B98=,tag:IuPUsGdbZQodCMyi1DR04Q==,type:str]
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: null
_default: ENC[AES256_GCM,data:6MLOmBMJoB+dYoW8L4JYslO3F5tSFzhrkI+6rGo1a51s9sXZa9c=,iv:126unfUWSieigaq4Zne8321tSYoNy3EHk/qwEodqgH8=,tag:6BlrNkck5yANO2rqECjkuQ==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age: []
lastmodified: "2022-10-10T10:14:07Z"
mac: ENC[AES256_GCM,data:eflai5xePeklnUiY+nkOHl0nEQ6UMrDS6COo6W24402u5w2S3A0XP7ZhgrWn4K+AogId2nxwwDt+CcsM9E2m8tIvWQeY7S1ywI9kY10NiTamQrho+b9J3ZPBQC1zfbskyrtLeb93h+utSXOs1lipKxBMiD609TboQNj6mzZWFMk=,iv:LR7XW6sDulFT102H5lr+e4+f6n1erBz8lgeyMOgrevk=,tag:2jVNlPm6dMckewseADrMiA==,type:str]
lastmodified: "2022-10-14T12:09:11Z"
mac: ENC[AES256_GCM,data:CnD+9hsC9ZyVhZPo+DXZfPH8svMuk50llaAm3JxgOlzhbJ4yp969WxLhZSORvj520b9geBPLZRU7ujLGiHKhrNzAK438LI2QttKQDt3WSbPwkIGDh/zuA201+gpT73awUNfMKCoHVjq4iQ6ty4KP/NCw1ZMcS/c1WVuRYE9RTl8=,iv:rl8eKiXwrBDjns2hiwJ6f28XyuhjH2soHeR1MBBu2Ig=,tag:vu5jGEPMkvmcl7m8huWl7g==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
......
......@@ -249,7 +249,7 @@ scVerifier:
frontend:
enabled: true
image:
_default: ghcr.io/blockscout/frontend:prerelease-5ca79e55
_default: ghcr.io/blockscout/frontend:main
replicas:
app: 1
docker:
......@@ -284,9 +284,9 @@ frontend:
NEXT_PUBLIC_APP_HOST:
_default: localhost
NEXT_PUBLIC_APP_PORT:
_default: 3000
_default: 80
NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v4.1.7-beta
_default: v4.1.8-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
......@@ -298,7 +298,7 @@ frontend:
NEXT_PUBLIC_FOOTER_STAKING_LINK:
_default: https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_NETWORK_NAME:
_default: POA
_default: Sokol
NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: POA
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
......@@ -306,15 +306,16 @@ frontend:
NEXT_PUBLIC_NETWORK_TYPE:
_default: poa
NEXT_PUBLIC_NETWORK_SUBTYPE:
_default: core
_default: sokol
NEXT_PUBLIC_NETWORK_ID:
_default: 99
_default: 77
NEXT_PUBLIC_NETWORK_CURRENCY:
_default: POA
_default: SPOA
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED:
_default: 'true'
NEXT_PUBLIC_FEATURED_NETWORKS:
_default: "[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'},{'title':'Optimism on Gnosis Chain','basePath':'/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60'},{'title':'Arbitrum on xDai','basePath':'/xdai/aox','group':'mainnets'},{'title':'Ethereum','basePath':'/eth/mainnet','group':'mainnets'},{'title':'Ethereum Classic','basePath':'/etx/mainnet','group':'mainnets'},{'title':'POA','basePath':'/poa/core','group':'mainnets'},{'title':'RSK','basePath':'/rsk/mainnet','group':'mainnets'},{'title':'Gnosis Chain Testnet','basePath':'/xdai/testnet','group':'testnets'},{'title':'POA Sokol','basePath':'/poa/sokol','group':'testnets'},{'title':'ARTIS Σ1','basePath':'/artis/sigma1','group':'other'},{'title':'LUKSO L14','basePath':'/lukso/l14','group':'other'},{'title':'Astar','basePath':'/astar','group':'other'}]"
NEXT_PUBLIC_API_ENDPOINT:
_default: https://blockscout.com
NEXT_PUBLIC_API_BASE_PATH: /
NEXT_PUBLIC_API_BASE_PATH:
_default: /
......@@ -65,19 +65,21 @@ geth:
frontend:
environment:
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS:
_default: ENC[AES256_GCM,data:fhH2QYodAkC/fIi5yU7Ur567buyIqQeTNUgserFNPs31yUeySk+BItpt,iv:7T7o5yX0Mvt1bYD9Cb6LxW9qHX1nzA99ykejJ4/KdpY=,tag:lfNUdrHGwU3BtsSvrnccYA==,type:str]
_default: ENC[AES256_GCM,data:1SAbzZhCs/vzdftIX0WVLtImH27NJ6SwENee4uTu2p+ZyUso3nQCLUUm,iv:apyLxt2dQ5RN33ra1Q1sAy2cyplG9FSryksQru2ghlA=,tag:PVcCNt0bz1TfQewUebV5LA==,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:
_default: ENC[AES256_GCM,data:Hf4azYsGh2lysotK7afaHI85IaLBJeQmPlGE/lwokmX2eaQHVZJ/i5RsaoKoOCSiNyfAowr7P+6IBEC16BUTQMpbhYveBd7c/xjIsoomoHbTdoAdZA/QxNVpS4a2qVthruFudyT1BoZUHIGQ,iv:Mid+PfbslOyivrFSXopdeW96YvOcLP+g2RGcw1o7B98=,tag:IuPUsGdbZQodCMyi1DR04Q==,type:str]
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: null
_default: ENC[AES256_GCM,data:KVzLMvMQc6f37c+OWu7vpAdDYwKI048XVY7EVijS2zz6jRRFacg=,iv:LeWSYZeaPdBsOxcGcca8L1Rp3ilsR+R13icX8Q/VUBA=,tag:Bj6NTTqb3R8oxAprZ+7CVQ==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age: []
lastmodified: "2022-10-10T10:14:07Z"
mac: ENC[AES256_GCM,data:eflai5xePeklnUiY+nkOHl0nEQ6UMrDS6COo6W24402u5w2S3A0XP7ZhgrWn4K+AogId2nxwwDt+CcsM9E2m8tIvWQeY7S1ywI9kY10NiTamQrho+b9J3ZPBQC1zfbskyrtLeb93h+utSXOs1lipKxBMiD609TboQNj6mzZWFMk=,iv:LR7XW6sDulFT102H5lr+e4+f6n1erBz8lgeyMOgrevk=,tag:2jVNlPm6dMckewseADrMiA==,type:str]
lastmodified: "2022-10-14T12:08:39Z"
mac: ENC[AES256_GCM,data:esHv2aUvW4lMmoD5yRWu4OJEpOMkCa7TOyPS0HDkQL25g4TOdE+AfVZBE5wLeL1rePaId4bHnX0sF2Tov0d8xhCH3mv6+Vvmgi+75Oqu8+logQ4LyZSI0yIcvmdGVHhaO6u3u1qwYXHrityIVmiXQdBck5oq67uyT+jtSh1pXpc=,iv:YNWhRxSY0WLRm+wbVURpfxU2K67MAB5dpSILSMy9oCE=,tag:TsCcsGmMG4id5ITR5LrDjg==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
......
......@@ -249,7 +249,7 @@ scVerifier:
frontend:
enabled: true
image:
_default: ghcr.io/blockscout/frontend:prerelease-5ca79e55
_default: ghcr.io/blockscout/frontend:main
replicas:
app: 1
docker:
......@@ -284,7 +284,7 @@ frontend:
NEXT_PUBLIC_APP_HOST:
_default: localhost
NEXT_PUBLIC_APP_PORT:
_default: 3000
_default: 80
NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v4.1.8-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
......@@ -298,7 +298,7 @@ frontend:
NEXT_PUBLIC_FOOTER_STAKING_LINK:
_default: https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_NETWORK_NAME:
_default: POA
_default: Sokol
NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: POA
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
......@@ -306,15 +306,16 @@ frontend:
NEXT_PUBLIC_NETWORK_TYPE:
_default: poa
NEXT_PUBLIC_NETWORK_SUBTYPE:
_default: core
_default: sokol
NEXT_PUBLIC_NETWORK_ID:
_default: 99
_default: 77
NEXT_PUBLIC_NETWORK_CURRENCY:
_default: POA
_default: SPOA
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED:
_default: 'true'
NEXT_PUBLIC_FEATURED_NETWORKS:
_default: "[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'},{'title':'Optimism on Gnosis Chain','basePath':'/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60'},{'title':'Arbitrum on xDai','basePath':'/xdai/aox','group':'mainnets'},{'title':'Ethereum','basePath':'/eth/mainnet','group':'mainnets'},{'title':'Ethereum Classic','basePath':'/etx/mainnet','group':'mainnets'},{'title':'POA','basePath':'/poa/core','group':'mainnets'},{'title':'RSK','basePath':'/rsk/mainnet','group':'mainnets'},{'title':'Gnosis Chain Testnet','basePath':'/xdai/testnet','group':'testnets'},{'title':'POA Sokol','basePath':'/poa/sokol','group':'testnets'},{'title':'ARTIS Σ1','basePath':'/artis/sigma1','group':'other'},{'title':'LUKSO L14','basePath':'/lukso/l14','group':'other'},{'title':'Astar','basePath':'/astar','group':'other'}]"
NEXT_PUBLIC_API_ENDPOINT:
_default: https://blockscout.com
NEXT_PUBLIC_API_BASE_PATH: /
NEXT_PUBLIC_API_BASE_PATH:
_default: /
import BigNumber from 'bignumber.js';
export default function compareBns(value1: string | number, value2: string | number) {
const value1Bn = new BigNumber(value1);
const value2Bn = new BigNumber(value2);
if (value1Bn.isGreaterThan(value2Bn)) {
return 1;
}
if (value1Bn.isLessThan(value2Bn)) {
return -1;
}
return 0;
}
import BigNumber from 'bignumber.js';
import { WEI, GWEI } from 'lib/consts';
export default function getValueWithUnit(value: string | number, unit: 'wei' | 'gwei' | 'ether' = 'wei') {
let unitBn: BigNumber.Value;
switch (unit) {
case 'wei':
unitBn = WEI;
break;
case 'gwei':
unitBn = GWEI;
break;
default:
unitBn = new BigNumber(1);
}
const valueBn = new BigNumber(value);
const valueCurr = valueBn.dividedBy(unitBn);
return valueCurr;
}
const BASE_PATH = '/[network_type]/[network_sub_type]';
module.exports = BASE_PATH;
const BASE_PATH = require('./basePath');
const paths = {
network_index: `${ BASE_PATH }`,
watchlist: `${ BASE_PATH }/account/watchlist`,
private_tags: `${ BASE_PATH }/account/tag_address`,
public_tags: `${ BASE_PATH }/account/public_tags_request`,
api_keys: `${ BASE_PATH }/account/api_key`,
custom_abi: `${ BASE_PATH }/account/custom_abi`,
profile: `${ BASE_PATH }/auth/profile`,
txs: `${ BASE_PATH }/txs`,
tx: `${ BASE_PATH }/tx/[id]`,
blocks: `${ BASE_PATH }/blocks`,
block: `${ BASE_PATH }/block/[id]`,
tokens: `${ BASE_PATH }/tokens`,
token_index: `${ BASE_PATH }/token/[hash]`,
token_instance_item: `${ BASE_PATH }/token/[hash]/instance/[id]`,
address_index: `${ BASE_PATH }/address/[id]`,
address_contract_verification: `${ BASE_PATH }/address/[id]/contract_verifications/new`,
apps: `${ BASE_PATH }/apps`,
app_index: `${ BASE_PATH }/apps/[id]`,
search_results: `${ BASE_PATH }/search-results`,
other: `${ BASE_PATH }/search-results`,
// no slash required, it is correct
auth: `${ BASE_PATH }auth/auth0`,
};
module.exports = paths;
import appConfig from 'configs/app/config';
import PATHS from './paths.js';
export interface Route {
pattern: string;
crossNetworkNavigation?: boolean; // route will not change when switching networks
}
import appConfig from 'configs/app/config';
export type RouteName = keyof typeof ROUTES;
const BASE_PATH = '/[network_type]/[network_sub_type]';
export const ROUTES = {
// NETWORK MAIN PAGE
network_index: {
pattern: `${ BASE_PATH }`,
pattern: PATHS.network_index,
crossNetworkNavigation: true,
},
// ACCOUNT
watchlist: {
pattern: `${ BASE_PATH }/account/watchlist`,
pattern: PATHS.watchlist,
},
private_tags: {
pattern: `${ BASE_PATH }/account/tag_address`,
pattern: PATHS.private_tags,
},
public_tags: {
pattern: `${ BASE_PATH }/account/public_tags_request`,
pattern: PATHS.public_tags,
},
api_keys: {
pattern: `${ BASE_PATH }/account/api_key`,
pattern: PATHS.api_keys,
},
custom_abi: {
pattern: `${ BASE_PATH }/account/custom_abi`,
pattern: PATHS.custom_abi,
},
profile: {
pattern: `${ BASE_PATH }/auth/profile`,
pattern: PATHS.profile,
},
// TRANSACTIONS
txs: {
pattern: `${ BASE_PATH }/txs`,
pattern: PATHS.txs,
crossNetworkNavigation: true,
},
tx: {
pattern: `${ BASE_PATH }/tx/[id]`,
pattern: PATHS.tx,
},
// BLOCKS
blocks: {
pattern: `${ BASE_PATH }/blocks`,
pattern: PATHS.blocks,
crossNetworkNavigation: true,
},
block: {
pattern: `${ BASE_PATH }/block/[id]`,
pattern: PATHS.block,
},
// TOKENS
tokens: {
pattern: `${ BASE_PATH }/tokens`,
pattern: PATHS.tokens,
crossNetworkNavigation: true,
},
token_index: {
pattern: `${ BASE_PATH }/token/[hash]`,
pattern: PATHS.token_index,
crossNetworkNavigation: true,
},
token_instance_item: {
pattern: `${ BASE_PATH }/token/[hash]/instance/[id]`,
pattern: PATHS.token_instance_item,
},
// ADDRESSES
address_index: {
pattern: `${ BASE_PATH }/address/[id]`,
pattern: PATHS.address_index,
crossNetworkNavigation: true,
},
address_contract_verification: {
pattern: `${ BASE_PATH }/address/[id]/contract_verifications/new`,
pattern: PATHS.address_contract_verification,
crossNetworkNavigation: true,
},
// APPS
apps: {
pattern: `${ BASE_PATH }/apps`,
pattern: PATHS.apps,
},
app_index: {
pattern: `${ BASE_PATH }/apps/[id]`,
pattern: PATHS.app_index,
},
// SEARCH
search_results: {
pattern: `${ BASE_PATH }/apps`,
pattern: PATHS.search_results,
},
// ??? what URL will be here
other: {
pattern: `${ BASE_PATH }/search-results`,
pattern: PATHS.other,
},
// AUTH
auth: {
// no slash required, it is correct
pattern: `${ BASE_PATH }auth/auth0`,
pattern: PATHS.auth,
},
};
......
......@@ -14,7 +14,7 @@
"build:vercel": "./node_modules/.bin/dotenv -e ./configs/envs/.env.poa_core -e ./configs/envs/.env.common next build",
"build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse HEAD) -t blockscout ./",
"start": "next start",
"start:docker:poa_core": "docker run -p 3000:3000 --env-file ./configs/envs/.env.poa_core --env-file ./configs/envs/.env.common --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:tsc": "./node_modules/.bin/tsc -p ./tsconfig.json",
"prepare": "husky install",
......
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParams: Record<string, string> = {};
Object.entries(req.query).forEach(([ key, value ]) => {
searchParams[key] = Array.isArray(value) ? value.join(',') : (value || '');
});
const searchParamsStr = new URLSearchParams(searchParams).toString();
return `/v2/transactions/${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
......@@ -19,7 +19,7 @@ export interface Transaction {
from: AddressParam;
to: AddressParam;
created_contract: AddressParam;
value: number;
value: string;
fee: Fee;
gas_price: number;
type: number;
......
......@@ -26,7 +26,7 @@ export type AppItemPreview = {
}
export type AppItemOverview = AppItemPreview & {
chainIds: Array<number>;
chainIds: Array<string>;
author: string;
url: string;
description: string;
......
export type Unit = 'wei' | 'gwei' | 'ether';
......@@ -3,7 +3,7 @@ import React from 'react';
import TxsContent from 'ui/txs/TxsContent';
const BlockTxs = () => {
return <TxsContent showDescription={ false } showSortButton={ false }/>;
return <TxsContent showDescription={ false } showSortButton={ false } txs={ [] }/>;
};
export default BlockTxs;
......@@ -30,7 +30,7 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
useEffect(() => {
if (app && !isFrameLoading) {
ref?.current?.contentWindow?.postMessage({ blockscoutColorMode: colorMode, blockscoutChainId: appConfig.network.id }, app.url);
ref?.current?.contentWindow?.postMessage({ blockscoutColorMode: colorMode, blockscoutChainId: Number(appConfig.network.id) }, app.url);
}
}, [ isFrameLoading, app, colorMode, ref ]);
......@@ -38,7 +38,7 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
<Page wrapChildren={ false }>
<Center
as="main"
h="100%"
h="100vh"
paddingTop={{ base: '138px', lg: 0 }}
>
{ (isFrameLoading) && (
......
......@@ -2,45 +2,47 @@ import { Box, Text, chakra } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import { WEI, GWEI } from 'lib/consts';
import type { Unit } from 'types/unit';
import getValueWithUnit from 'lib/getValueWithUnit';
interface Props {
value: string;
unit?: 'wei' | 'gwei' | 'ether';
unit?: Unit;
currency?: string;
exchangeRate?: string;
exchangeRate?: string | null;
className?: string;
accuracy?: number;
accuracyUsd?: number;
}
const CurrencyValue = ({ value, currency = '', unit = 'wei', exchangeRate, className, accuracy, accuracyUsd }: Props) => {
let unitBn: BigNumber.Value;
switch (unit) {
case 'wei':
unitBn = WEI;
break;
case 'gwei':
unitBn = GWEI;
break;
default:
unitBn = new BigNumber(1);
}
const CurrencyValue = ({ value, currency = '', unit, exchangeRate, className, accuracy, accuracyUsd }: Props) => {
const valueCurr = getValueWithUnit(value, unit);
const valueResult = accuracy ? valueCurr.dp(accuracy).toFormat() : valueCurr.toFormat();
const valueBn = new BigNumber(value);
const valueCurr = valueBn.dividedBy(unitBn);
const exchangeRateBn = new BigNumber(exchangeRate || 0);
const usdBn = valueCurr.times(exchangeRateBn);
let usdContent;
if (exchangeRate !== undefined && exchangeRate !== null) {
const exchangeRateBn = new BigNumber(exchangeRate);
const usdBn = valueCurr.times(exchangeRateBn);
let usdResult: string;
if (accuracyUsd && !usdBn.isEqualTo(0)) {
const usdBnDp = usdBn.dp(accuracyUsd);
usdResult = usdBnDp.isEqualTo(0) ? usdBn.precision(accuracyUsd).toFormat() : usdBnDp.toFormat();
} else {
usdResult = usdBn.toFormat();
}
usdContent = (
<Text as="span" variant="secondary" fontWeight={ 400 }>(${ usdResult })</Text>
);
}
return (
<Box as="span" className={ className } display="inline-flex" rowGap={ 3 } columnGap={ 1 }>
<Text as="span">
{ accuracy ? valueCurr.toFixed(accuracy) : valueCurr.toFixed() }{ currency ? ` ${ currency }` : '' }
<Text display="inline-block">
{ valueResult }{ currency ? ` ${ currency }` : '' }
</Text>
{ exchangeRate !== undefined && exchangeRate !== null &&
// TODO: mb need to implement rounding to the first significant digit
<Text as="span" variant="secondary" fontWeight={ 400 }>(${ accuracyUsd ? usdBn.toFixed(accuracyUsd) : usdBn.toFixed() })</Text>
}
{ usdContent }
</Box>
);
};
......
......@@ -13,7 +13,7 @@ const SOCIAL_LINKS = [
{ link: appConfig.footerLinks.twitter, icon: twIcon, label: 'Twitter link' },
{ link: appConfig.footerLinks.telegram, icon: tgIcon, label: 'Telegram link' },
{ link: appConfig.footerLinks.staking, icon: statsIcon, label: 'Staking analytic link' },
].filter(({ link }) => link !== undefined);
].filter(({ link }) => link);
const VERSION_URL = `https://github.com/blockscout/blockscout/tree/${ appConfig.blockScoutVersion }`;
......@@ -47,20 +47,23 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
fontSize="xs"
{ ...getDefaultTransitionProps({ transitionProperty: 'width' }) }
>
<Stack direction={{ base: 'row', lg: isExpanded ? 'row' : 'column', xl: isCollapsed ? 'column' : 'row' }}>
{ SOCIAL_LINKS.map(sl => {
return (
<Link href={ sl.link } key={ sl.link } variant="secondary" w={ 5 } h={ 5 } aria-label={ sl.label }>
<Icon as={ sl.icon } boxSize={ 5 }/>
</Link>
);
}) }
</Stack>
{ SOCIAL_LINKS.length > 0 && (
<Stack direction={{ base: 'row', lg: isExpanded ? 'row' : 'column', xl: isCollapsed ? 'column' : 'row' }}>
{ SOCIAL_LINKS.map(sl => {
return (
<Link href={ sl.link } key={ sl.link } variant="secondary" w={ 5 } h={ 5 } aria-label={ sl.label }>
<Icon as={ sl.icon } boxSize={ 5 }/>
</Link>
);
}) }
</Stack>
) }
<Box display={{ base: 'block', lg: isExpanded ? 'block' : 'none', xl: isCollapsed ? 'none' : 'block' }}>
<Text variant="secondary" mb={ 8 }>
Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks.
</Text>
<Text variant="secondary">Version: <Link href={ VERSION_URL } target="_blank">{ appConfig.blockScoutVersion }</Link></Text>
{ appConfig.blockScoutVersion &&
<Text variant="secondary">Version: <Link href={ VERSION_URL } target="_blank">{ appConfig.blockScoutVersion }</Link></Text> }
</Box>
</VStack>
);
......
......@@ -183,14 +183,14 @@ const TxDetails = () => {
title="Value"
hint="Value sent in the native token (and USD) if applicable."
>
<CurrencyValue value={ String(data.value) } currency={ appConfig.network.currency } exchangeRate={ data.exchange_rate }/>
<CurrencyValue value={ data.value } currency={ appConfig.network.currency } exchangeRate={ data.exchange_rate }/>
</DetailsInfoItem>
<DetailsInfoItem
title="Transaction fee"
hint="Total transaction fee."
>
<CurrencyValue
value={ String(data.fee.value) }
value={ data.fee.value }
currency={ appConfig.network.currency }
exchangeRate={ data.exchange_rate }
flexWrap="wrap"
......
import { Box, Heading, Text, Flex, Link, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import appConfig from 'configs/app/config';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
import type { Transaction } from 'types/api/transaction';
import type { txs } from 'data/txs';
import { nbsp } from 'lib/html-entities';
import getValueWithUnit from 'lib/getValueWithUnit';
import link from 'lib/link/link';
import CurrencyValue from 'ui/shared/CurrencyValue';
import TextSeparator from 'ui/shared/TextSeparator';
import Utilization from 'ui/shared/Utilization';
const TxAdditionalInfo = ({ tx }: { tx: ArrayElement<typeof txs> }) => {
const TxAdditionalInfo = ({ tx }: { tx: Transaction }) => {
const sectionBorderColor = useColorModeValue('gray.200', 'whiteAlpha.200');
const sectionProps = {
borderBottom: '1px solid',
......@@ -31,40 +32,54 @@ const TxAdditionalInfo = ({ tx }: { tx: ArrayElement<typeof txs> }) => {
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Transaction fee</Text>
<Flex>
<Text>{ tx.fee.value }{ nbsp }{ appConfig.network.currency }</Text>
<Text variant="secondary" ml={ 1 }>(${ tx.fee.value_usd.toFixed(2) })</Text>
<CurrencyValue
value={ tx.fee.value }
currency={ appConfig.network.currency }
exchangeRate={ tx.exchange_rate }
accuracyUsd={ 2 }
/>
</Flex>
</Box>
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Gas limit & usage by transaction</Text>
<Flex>
<Text>{ tx.gas_used.toLocaleString('en') }</Text>
<TextSeparator/>
<Text>{ tx.gas_limit.toLocaleString('en') }</Text>
<Utilization ml={ 4 } value={ tx.gas_used / tx.gas_limit }/>
</Flex>
</Box>
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Gas fees (Gwei)</Text>
<Box>
<Text as="span" fontWeight="500">Base: </Text>
<Text fontWeight="600" as="span">{ tx.gas_fees.base }</Text>
{ tx.gas_used !== null && (
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Gas limit & usage by transaction</Text>
<Flex>
<Text>{ BigNumber(tx.gas_used).toFormat() }</Text>
<TextSeparator/>
<Text>{ BigNumber(tx.gas_limit).toFormat() }</Text>
<Utilization ml={ 4 } value={ Number(BigNumber(tx.gas_used).dividedBy(BigNumber(tx.gas_limit)).toFixed(2)) }/>
</Flex>
</Box>
<Box>
<Text as="span" fontWeight="500">Max: </Text>
<Text fontWeight="600" as="span">{ tx.gas_fees.max }</Text>
) }
{ (tx.base_fee_per_gas !== null || tx.max_fee_per_gas !== null || tx.max_priority_fee_per_gas !== null) && (
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Gas fees (Gwei)</Text>
{ tx.base_fee_per_gas !== null && (
<Box>
<Text as="span" fontWeight="500">Base: </Text>
<Text fontWeight="600" as="span">{ getValueWithUnit(tx.base_fee_per_gas, 'gwei').toFormat() }</Text>
</Box>
) }
{ tx.max_fee_per_gas !== null && (
<Box>
<Text as="span" fontWeight="500">Max: </Text>
<Text fontWeight="600" as="span">{ getValueWithUnit(tx.max_fee_per_gas, 'gwei').toFormat() }</Text>
</Box>
) }
{ tx.max_priority_fee_per_gas !== null && (
<Box>
<Text as="span" fontWeight="500">Max priority: </Text>
<Text fontWeight="600" as="span">{ getValueWithUnit(tx.max_priority_fee_per_gas, 'gwei').toFormat() }</Text>
</Box>
) }
</Box>
<Box>
<Text as="span" fontWeight="500">Max priority: </Text>
<Text fontWeight="600" as="span">{ tx.gas_fees.max_priority }</Text>
</Box>
</Box>
) }
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Others</Text>
<Box>
<Text as="span" fontWeight="500">Txn type: </Text>
<Text fontWeight="600" as="span">{ tx.type.value }</Text>
<Text fontWeight="400" as="span" ml={ 1 } color="gray.500">({ tx.type.eip })</Text>
<Text fontWeight="600" as="span">{ tx.type }</Text>
{ tx.type === 2 && <Text fontWeight="400" as="span" ml={ 1 } color="gray.500">(EIP-1559)</Text> }
</Box>
<Box>
<Text as="span" fontWeight="500">Nonce: </Text>
......
import { Box, HStack, Show } from '@chakra-ui/react';
import React, { useCallback, useEffect, useState } from 'react';
import type { TransactionsResponse } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort';
import { txs } from 'data/txs';
import compareBns from 'lib/bigint/compareBns';
import FilterButton from 'ui/shared/FilterButton';
import FilterInput from 'ui/shared/FilterInput';
import Pagination from 'ui/shared/Pagination';
......@@ -13,11 +14,12 @@ import TxsListItem from './TxsListItem';
import TxsTable from './TxsTable';
type Props = {
txs: TransactionsResponse['items'];
showDescription?: boolean;
showSortButton?: boolean;
}
const TxsContent = ({ showSortButton = true, showDescription = true }: Props) => {
const TxsContent = ({ showSortButton = true, showDescription = true, txs }: Props) => {
const [ sorting, setSorting ] = useState<Sort>();
const [ sortedTxs, setSortedTxs ] = useState(txs);
......@@ -50,21 +52,21 @@ const TxsContent = ({ showSortButton = true, showDescription = true }: Props) =>
useEffect(() => {
switch (sorting) {
case 'val-desc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => tx1.amount.value - tx2.amount.value));
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx1.value, tx2.value)));
break;
case 'val-asc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => tx2.amount.value - tx1.amount.value));
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx2.value, tx1.value)));
break;
case 'fee-desc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => tx1.fee.value - tx2.fee.value));
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx1.fee.value, tx2.fee.value)));
break;
case 'fee-asc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => tx2.fee.value - tx1.fee.value));
setSortedTxs([ ...txs ].sort((tx1, tx2) => compareBns(tx2.fee.value, tx1.fee.value)));
break;
default:
setSortedTxs(txs);
}
}, [ sorting ]);
}, [ sorting, txs ]);
return (
<>
......@@ -93,7 +95,7 @@ const TxsContent = ({ showSortButton = true, showDescription = true }: Props) =>
placeholder="Search by addresses, hash, method..."
/>
</HStack>
<Show below="lg">{ sortedTxs.map(tx => <TxsListItem tx={ tx } key={ tx.hash }/>) }</Show>
<Show below="lg"><Box>{ sortedTxs.map(tx => <TxsListItem tx={ tx } key={ tx.hash }/>) }</Box></Show>
<Show above="lg"><TxsTable txs={ sortedTxs } sort={ sort } sorting={ sorting }/></Show>
<Box mx={{ base: 0, lg: 6 }} my={{ base: 6, lg: 3 }}>
<Pagination currentPage={ 1 }/>
......
......@@ -13,12 +13,12 @@ import {
import appConfig from 'configs/app/config';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
import type { Transaction } from 'types/api/transaction';
import type { txs } from 'data/txs';
import rightArrowIcon from 'icons/arrows/east.svg';
import transactionIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs';
import getValueWithUnit from 'lib/getValueWithUnit';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
......@@ -28,7 +28,7 @@ import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxAdditionalInfoButton from 'ui/txs/TxAdditionalInfoButton';
import TxType from 'ui/txs/TxType';
const TxsListItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
const TxsListItem = ({ tx }: {tx: Transaction}) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const iconColor = useColorModeValue('blue.600', 'blue.300');
......@@ -36,11 +36,13 @@ const TxsListItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
return (
<>
<Box width="100%" borderBottom="1px solid" borderColor={ borderColor } _first={{ borderTop: '1px solid', borderColor: { borderColor } }}>
<Box width="100%" borderBottom="1px solid" borderColor={ borderColor } _first={{ borderTop: '1px solid', borderColor }}>
<Flex justifyContent="space-between" mt={ 4 }>
<HStack>
<TxType type={ tx.txType }/>
<TxStatus status={ tx.status } errorText={ tx.errorText }/>
{ /* TODO: we don't recieve type from api */ }
{ /* <TxType type={ tx.type }/> */ }
<TxType type="transaction"/>
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/>
</HStack>
<TxAdditionalInfoButton onClick={ onOpen }/>
</Flex>
......@@ -65,6 +67,7 @@ const TxsListItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
</Flex>
<Flex mt={ 3 }>
<Text as="span" whiteSpace="pre">Method </Text>
{ /* TODO: we don't recieve method from api */ }
<Text
as="span"
variant="secondary"
......@@ -72,19 +75,22 @@ const TxsListItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
whiteSpace="nowrap"
textOverflow="ellipsis"
>
{ tx.method }
{ /* { tx.method } */ }
CommitHash
</Text>
</Flex>
<Box mt={ 2 }>
<Text as="span">Block </Text>
<Link href={ link('block', { id: tx.block_num.toString() }) }>{ tx.block_num }</Link>
</Box>
{ tx.block !== null && (
<Box mt={ 2 }>
<Text as="span">Block </Text>
<Link href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</Link>
</Box>
) }
<Flex alignItems="center" height={ 6 } mt={ 6 }>
<Address width="calc((100%-40px)/2)">
<AddressIcon hash={ tx.address_from.hash }/>
<AddressIcon hash={ tx.from.hash }/>
<AddressLink
hash={ tx.address_from.hash }
alias={ tx.address_from.alias }
hash={ tx.from.hash }
alias={ tx.from.name }
fontWeight="500"
ml={ 2 }
/>
......@@ -96,10 +102,10 @@ const TxsListItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
color="gray.500"
/>
<Address width="calc((100%-40px)/2)">
<AddressIcon hash={ tx.address_to.hash }/>
<AddressIcon hash={ tx.to.hash }/>
<AddressLink
hash={ tx.address_to.hash }
alias={ tx.address_to.alias }
hash={ tx.to.hash }
alias={ tx.to.name }
fontWeight="500"
ml={ 2 }
/>
......@@ -107,11 +113,11 @@ const TxsListItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
</Flex>
<Box mt={ 2 }>
<Text as="span">Value { appConfig.network.currency } </Text>
<Text as="span" variant="secondary">{ tx.amount.value.toFixed(8) }</Text>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.value).toFormat() }</Text>
</Box>
<Box mt={ 2 } mb={ 3 }>
<Text as="span">Fee { appConfig.network.currency } </Text>
<Text as="span" variant="secondary">{ tx.fee.value.toFixed(8) }</Text>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.fee.value).toFormat() }</Text>
</Box>
</Box>
<Modal isOpen={ isOpen } onClose={ onClose } size="full">
......
import { Show, Alert } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { TransactionsResponse } from 'types/api/transaction';
import useFetch from 'lib/hooks/useFetch';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TxsContent from './TxsContent';
import TxsSkeletonDesktop from './TxsSkeletonDesktop';
import TxsSkeletonMobile from './TxsSkeletonMobile';
const TxsValidated = () => {
const fetch = useFetch();
const { data, isLoading, isError } =
useQuery<unknown, unknown, TransactionsResponse>([ 'transactions_pending' ], async() => fetch('/api/transactions/?filter=pending'));
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<>
<Show below="lg"><TxsSkeletonMobile isPending/></Show>
<Show above="lg"><TxsSkeletonDesktop isPending/></Show>
</>
);
}
if (!data || !data.items) {
return <Alert>There are no transactions.</Alert>;
}
const TxsPending = () => {
return <TxsContent showDescription={ false }/>;
return <TxsContent txs={ data.items } showDescription={ false }/>;
};
export default TxsPending;
export default TxsValidated;
import { Skeleton, Flex } from '@chakra-ui/react';
import React from 'react';
import SkeletonTable from 'ui/shared/SkeletonTable';
const TxsInternalsSkeletonDesktop = ({ isPending }: {isPending?: boolean}) => {
return (
<>
{ !isPending && <Skeleton h={ 6 } w="100%" mb={ 12 }/> }
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }>
<Skeleton w="78px"/>
<Skeleton w="360px"/>
</Flex>
<SkeletonTable columns={ [ '32px', '20%', '18%', '15%', '11%', '292px', '18%', '18%' ] }/>
</>
);
};
export default TxsInternalsSkeletonDesktop;
import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const TxInternalsSkeletonMobile = ({ isPending }: {isPending?: boolean}) => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<>
{ !isPending && <Skeleton h={ 6 } w="100%" mb={ 12 }/> }
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }>
<Skeleton w="36px" flexShrink={ 0 }/>
<Skeleton w="36px" flexShrink={ 0 }/>
<Skeleton w="100%"/>
</Flex>
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
flexDirection="column"
paddingBottom={ 3 }
paddingTop={ 4 }
borderTopWidth="1px"
borderColor={ borderColor }
_last={{
borderBottomWidth: '1px',
}}
>
<Flex h={ 6 }>
<Skeleton w="100px" mr={ 2 } h={ 6 }/>
<Skeleton w="100px" h={ 6 }/>
</Flex>
<Skeleton w="100%" h="30px" mt={ 3 }/>
<Skeleton w="50%" h={ 6 } mt={ 3 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
<Skeleton w="100%" h={ 6 } mt={ 6 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
</Flex>
)) }
</Box>
</>
);
};
export default TxInternalsSkeletonMobile;
......@@ -2,15 +2,15 @@ import { Link, Table, Thead, Tbody, Tr, Th, TableContainer, Icon } from '@chakra
import appConfig from 'configs/app/config';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort';
import type { txs as data } from 'data/txs';
import rightArrowIcon from 'icons/arrows/east.svg';
import TxsTableItem from './TxsTableItem';
type Props = {
txs: typeof data;
txs: Array<Transaction>;
sort: (field: 'val' | 'fee') => () => void;
sorting: Sort;
}
......
......@@ -18,15 +18,15 @@ import {
} from '@chakra-ui/react';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
import type { Transaction } from 'types/api/transaction';
import type { txs } from 'data/txs';
import rightArrowIcon from 'icons/arrows/east.svg';
import dayjs from 'lib/date/dayjs';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CurrencyValue from 'ui/shared/CurrencyValue';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
......@@ -34,23 +34,22 @@ import TxAdditionalInfoButton from 'ui/txs/TxAdditionalInfoButton';
import TxType from './TxType';
const TxsTableItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
const TxsTableItem = ({ tx }: {tx: Transaction}) => {
const addressFrom = (
<Address>
<Tooltip label={ tx.address_from.type }>
<Box display="flex"><AddressIcon hash={ tx.address_from.hash }/></Box>
<Tooltip label={ tx.from.implementation_name }>
<Box display="flex"><AddressIcon hash={ tx.from.hash }/></Box>
</Tooltip>
<AddressLink hash={ tx.address_from.hash } alias={ tx.address_from.alias } fontWeight="500" ml={ 2 }/>
<AddressLink hash={ tx.from.hash } alias={ tx.from.name } fontWeight="500" ml={ 2 }/>
</Address>
);
const addressTo = (
<Address>
<Tooltip label={ tx.address_to.type }>
<Box display="flex"> <AddressIcon hash={ tx.address_to.hash }/></Box>
<Tooltip label={ tx.to.implementation_name }>
<Box display="flex"><AddressIcon hash={ tx.to.hash }/></Box>
</Tooltip>
<AddressLink hash={ tx.address_to.hash } alias={ tx.address_to.alias } fontWeight="500" ml={ 2 }/>
<AddressLink hash={ tx.to.hash } alias={ tx.to.name } fontWeight="500" ml={ 2 }/>
</Address>
);
......@@ -77,8 +76,10 @@ const TxsTableItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
</Td>
<Td>
<VStack alignItems="start">
<TxType type={ tx.txType }/>
<TxStatus status={ tx.status } errorText={ tx.errorText }/>
{ /* TODO: we don't recieve type from api */ }
{ /* <TxType type={ tx.type }/> */ }
<TxType type="transaction"/>
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/>
</VStack>
</Td>
<Td>
......@@ -94,17 +95,26 @@ const TxsTableItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
</VStack>
</Td>
<Td>
<TruncatedTextTooltip label={ tx.method }>
{ /* TODO: we don't recieve method from api */ }
{ /* <TruncatedTextTooltip label={ tx.method }>
<Tag
colorScheme={ tx.method === 'Multicall' ? 'teal' : 'gray' }
>
{ tx.method }
</Tag>
</TruncatedTextTooltip> */ }
<TruncatedTextTooltip label="CommitHash">
<Tag
colorScheme="gray"
>
CommitHash
</Tag>
</TruncatedTextTooltip>
</Td>
<Td>
<Link href={ link('block', { id: tx.block_num.toString() }) }>{ tx.block_num }</Link>
{ tx.block && <Link href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</Link> }
</Td>
{ /* TODO: fix "show" problem */ }
<Show above="xl">
<Td>
{ addressFrom }
......@@ -133,10 +143,10 @@ const TxsTableItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
</Td>
</Show>
<Td isNumeric>
{ tx.amount.value.toFixed(8) }
<CurrencyValue value={ tx.value }/>
</Td>
<Td isNumeric>
{ tx.fee.value.toFixed(8) }
<CurrencyValue value={ tx.fee.value } accuracy={ 8 }/>
</Td>
</Tr>
);
......
import { Show, Alert } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { TransactionsResponse } from 'types/api/transaction';
import useFetch from 'lib/hooks/useFetch';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TxsContent from './TxsContent';
import TxsSkeletonDesktop from './TxsSkeletonDesktop';
import TxsSkeletonMobile from './TxsSkeletonMobile';
const TxsValidated = () => {
return <TxsContent/>;
const fetch = useFetch();
const { data, isLoading, isError } =
useQuery<unknown, unknown, TransactionsResponse>([ 'transactions_validated' ], async() => fetch('/api/transactions/?filter=validated'));
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<>
<Show below="lg"><TxsSkeletonMobile/></Show>
<Show above="lg"><TxsSkeletonDesktop/></Show>
</>
);
}
if (!data || !data.items) {
return <Alert>There are no transactions.</Alert>;
}
return <TxsContent txs={ data.items }/>;
};
export default TxsValidated;
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