Commit 389cbeb4 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into tx-state-api

parents 54cae6f5 fcddd709
......@@ -62,3 +62,4 @@ NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_GOOGLE_AN
# l2 config
NEXT_PUBLIC_IS_L2_NETWORK=__PLACEHOLDER_FOR_NEXT_PUBLIC_IS_L2_NETWORKL__
NEXT_PUBLIC_L1_BASE_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_L1_BASE_URL__
NEXT_PUBLIC_L2_WITHDRAWAL_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_L2_WITHDRAWAL_URL__
......@@ -52,7 +52,6 @@ jobs:
name: Run unit tests with Jest
needs: [ lint, type_check ]
runs-on: ubuntu-latest
if: ${{ false }} # disable since there are no jest test yet
steps:
- name: Checkout repo
uses: actions/checkout@v3
......
......@@ -139,6 +139,7 @@ The app instance could be customized by passing following variables to NodeJS en
| --- | --- | --- | --- |
| NEXT_PUBLIC_IS_L2_NETWORK | `boolean` *(optional)* | Set to true for L2 solutions (Optimism Bedrock based) | false |
| NEXT_PUBLIC_L1_BASE_URL | `string` *(optional)* | Base Blockscout URL for L1 network | `'http://eth-goerli.blockscout.com'` |
| NEXT_PUBLIC_L2_WITHDRAWAL_URL | `string` *(optional)* | URL for L2 -> L1 withdrawals | `https://app.optimism.io/bridge/withdraw` |
### Marketplace app configuration properties
......
......@@ -108,6 +108,7 @@ const config = Object.freeze({
L2: {
isL2Network: getEnvValue(process.env.NEXT_PUBLIC_IS_L2_NETWORK) === 'true',
L1BaseUrl: getEnvValue(process.env.NEXT_PUBLIC_L1_BASE_URL),
withdrawalUrl: getEnvValue(process.env.NEXT_PUBLIC_L2_WITHDRAWAL_URL) || '',
},
statsApi: {
endpoint: getEnvValue(process.env.NEXT_PUBLIC_STATS_API_HOST),
......
# ui config
NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Ethereum','url':'https://blockscout.com/eth/mainnet','group':'mainnets','type':'eth_mainnet'},{'title':'Ethereum Classic','url':'https://blockscout.com/etx/mainnet','group':'mainnets','type':'etc_mainnet'},{'title':'Gnosis Chain','url':'https://blockscout.com/xdai/mainnet','group':'mainnets','type':'xdai_mainnet'},{'title':'Astar (EVM)','url':'https://blockscout.com/astar','group':'mainnets','type':'astar'},{'title':'Shiden (EVM)','url':'https://blockscout.com/shiden','group':'mainnets','type':'astar'},{'title':'Klaytn Mainnet (Cypress)','url':'https://klaytn-mainnet.aws-k8s.blockscout.com/','group':'mainnets','type':'klaytn'},{'title':'Goerli','url':'https://blockscout.com/eth/goerli/','group':'testnets','type':'goerli'},{'title':'Optimism Goerli','url':'https://blockscout.com/optimism/goerli/','group':'testnets','type':'optimism_goerli'},{'title':'Optimism Bedrock Alpha','url':'https://blockscout.com/optimism/bedrock-alpha','group':'testnets','type':'optimism_bedrock_alpha'},{'title':'Gnosis Chiado','url':'https://blockscout.com/gnosis/chiado/','group':'testnets','type':'gnosis_chiado'},{'title':'Shibuya (EVM)','url':'https://blockscout.com/shibuya','group':'testnets','type':'shibuya'},{'title':'Optimism Opcraft','url':'https://blockscout.com/optimism/opcraft','group':'other','type':'optimism_opcraft'},{'title':'Optimism on Gnosis Chain','url':'https://blockscout.com/xdai/optimism','group':'other','type':'optimism_gnosis'},{'title':'ARTIS-Σ1','url':'https://blockscout.com/artis/sigma1','group':'other','type':'artis_sigma1'},{'title':'LUKSO L14','url':'https://blockscout.com/lukso/l14','group':'other','type':'lukso_l14'},{'title':'POA','url':'https://blockscout.com/poa/core','group':'other','type':'poa_core'},{'title':'POA Sokol','url':'https://blockscout.com/poa/sokol','group':'other','type':'poa_sokol'}]
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/ethereum/goerli/transaction','address':'/ethereum/ethereum/goerli/address'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address'}}]
# network config
NEXT_PUBLIC_NETWORK_NAME=Goerli
NEXT_PUBLIC_NETWORK_SHORT_NAME=Goerli
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=ethereum
NEXT_PUBLIC_NETWORK_TYPE=goerli
NEXT_PUBLIC_NETWORK_ID=5
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.ankr.com/eth_goerli
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_MARKETPLACE_APP_LIST=[{'author': 'Blockscout','id':'token-approval-tracker','title':'Token Approval Tracker','logo':'https://approval-tracker.vercel.app/icon-192.png','categories':['security','tools'],'shortDescription':'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.','site':'https://docs.blockscout.com/for-users/blockscout-apps/token-approval-tracker','description':'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.','url':'https://approval-tracker.vercel.app/'},{'author': 'Revoke','id':'revoke.cash','title':'Revoke.cash','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FVBMGyUFnd6CScjfK7CYQ%252Frevoke_sing.png%3Falt%3Dmedia%26token%3D9ab94986-7ab1-41c8-bf7e-d9ce11d23182','categories':['security','tools'],'shortDescription': 'Revoke.cash comes in as a preventative tool to manage your token allowances and practice proper wallet hygiene. By regularly revoking active allowances you reduce the chances of becoming the victim of allowance exploits.','site': 'https://revoke.cash/about','description': 'Revoke.cash comes in as a preventative tool to manage your token allowances and practice proper wallet hygiene. By regularly revoking active allowances you reduce the chances of becoming the victim of allowance exploits.','url':'https://revoke.cash/'},{'author':'Aave','id': 'aave','title': 'Aave','logo': 'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FrZkUTIUCG7Zx8BW6Em34%252FAave.png%3Falt%3Dmedia%26token%3D249797a4-4c1e-4372-9cd2-3e48e05e5f30','categories':['tools'],'shortDescription':'Aave is a decentralised non-custodial liquidity market protocol where users can participate as suppliers or borrowers. Suppliers provide liquidity to the market to earn a passive income, while borrowers are able to borrow in an overcollateralised (perpetually) or undercollateralised (one-block liquidity) fashion.','site': 'https://docs.aave.com/faq/','description': 'Aave is a decentralised non-custodial liquidity market protocol where users can participate as suppliers or borrowers. Suppliers provide liquidity to the market to earn a passive income, while borrowers are able to borrow in an overcollateralised (perpetually) or undercollateralised (one-block liquidity) fashion.','url': 'https://staging.aave.com/'},{'author':'LooksRare','id':'looksrare','external':true,'title':'LooksRare','logo': 'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FeAI4gy3qPMt68mZOZHAx%252FLooksRare.png%3Falt%3Dmedia%26token%3D44c01439-ae09-40aa-b904-3a9ce5b2e002','categories':['tools'],'shortDescription': 'LooksRare is the web3 NFT Marketplace where traders and collectors have earned over $1.3 Billion in rewards.','site':'https://docs.looksrare.org/about/welcome-to-looksrare','description':'LooksRare is the web3 NFT Marketplace where traders and collectors have earned over $1.3 Billion in rewards.','url': 'https://goerli.looksrare.org/'},{'author':'zkSync Bridge','id':'zksync-bridge','external':true,'title':'zkSync Bridge','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FrtQsaAz9BjGBc35tVAnq%252FzkSync.png%3Falt%3Dmedia%26token%3D5c18171c-8ccf-4a88-8f44-680cbf238115','categories':['security','tools'],'shortDescription':'zkSync 2.0 Goerli Bridge','site':'https://v2-docs.zksync.io/dev/','description':'zkSync 2.0 Goerli Bridge','url':'https://portal.zksync.io/bridge'},{'author':'dYdX','id':'dydx','external':true,'title':'dYdX','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FCrOglR72wpi0UhEscwe4%252Fdxdy.png%3Falt%3Dmedia%26token%3D8811909e-93e3-487c-9614-dffce37223e9','categories': ['security','tools'],'shortDescription':'dYdX is a leading decentralized exchange that currently supports perpetual trading. dYdX runs on smart contracts on the Ethereum blockchain, and allows users to trade with no intermediaries.','site':'https://help.dydx.exchange/en/articles/3047379-introduction-and-overview','description':'dYdX is a leading decentralized exchange that currently supports perpetual trading. dYdX runs on smart contracts on the Ethereum blockchain, and allows users to trade with no intermediaries.','url':'https://trade.stage.dydx.exchange/portfolio/overview'},{'author':'MetalSwap','id':'metalswap','title':'MetalSwap','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252F8xqldTvxb6avrwVVc3rS%252FMetalSwap.png%3Falt%3Dmedia%26token%3D92d2db99-853a-487d-8d8c-8cdeaeaaf014','categories':['security','tools'],'shortDescription':'MetalSwap is a decentralised platform that enables hedging swaps in financial markets with the aim of providing a hedge for commodity traders and an investment opportunity for those who contribute to the shared liquidity of the project.','site':'https://docs.metalswap.finance/','description':'MetalSwap is a decentralised platform that enables hedging swaps in financial markets with the aim of providing a hedge for commodity traders and an investment opportunity for those who contribute to the shared liquidity of the project.','url':'https://demo.metalswap.finance/'},{'author':'FaucetDao','id':'faucetdao','title':'FaucetDao','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252Ffnnt3ZNZhzRwqMM5YYjD%252FPlaceholder.png%3Falt%3Dmedia%26token%3D507571bb-d76f-4d96-a35e-2b278608f7ca','categories':['tools'],'shortDescription':'FaucetDao is a decentralised community fund providing liquidity and support to early-stage well vetted blockchain projects.','site':'https://linktr.ee/faucet_dao','description':'FaucetDao is a decentralised community fund providing liquidity and support to early-stage well vetted blockchain projects.','url':'https://www.faucetdao.shop/swap?chain=goerli'},{'author':'Uniswap','id':'uniswap','title':'Uniswap','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FJc0QAyeaBmFIL97tSmGv%252FUniswap.png%3Falt%3Dmedia%26token%3D5d25d796-c273-4e22-92fa-ff85206bec76','categories':['tools'],'shortDescription':'Uniswap is a cryptocurrency exchange which uses a decentralized network protocol.','site':'https://docs.uniswap.org/','description':'Uniswap is a cryptocurrency exchange which uses a decentralized network protocol.','url':'https://app.uniswap.org/swap'}]
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
# api config
NEXT_PUBLIC_API_HOST=blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.blockscout.com
# l2 config
NEXT_PUBLIC_IS_L2_NETWORK=true
NEXT_PUBLIC_L1_BASE_URL='https://eth-goerli.blockscout.com/'
......@@ -29,5 +29,5 @@ NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.blockscout.com
# l2 config
NEXT_PUBLIC_IS_L2_NETWORK=true
NEXT_PUBLIC_L1_BASE_URL=https://blockscout-main.test.aws-k8s.blockscout.com/
NEXT_PUBLIC_L1_BASE_URL=https://blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_L2_WITHDRAWAL_URL=https://app.optimism.io/bridge/withdraw
\ No newline at end of file
......@@ -5,7 +5,7 @@ blockscout:
app: blockscout
enabled: true
image:
_default: &image blockscout/blockscout-optimism-l2-advanced:5.1.0-prerelease-7a8c745e
_default: &image blockscout/blockscout-optimism-l2-advanced:5.1.0-prerelease-303a0afa
replicas:
app: 1
# init container
......@@ -110,33 +110,37 @@ blockscout:
API_V2_ENABLED:
_default: 'true'
FIRST_BLOCK:
_default: '4066066'
_default: '4667000'
TRACE_FIRST_BLOCK:
_default: '4066066'
_default: '4667000'
LAST_BLOCK:
_default: '4076066'
_default: '4677000'
TRACE_LAST_BLOCK:
_default: '4076066'
_default: '4677000'
DISABLE_REALTIME_INDEXER:
_default: 'true'
INDEXER_OPTIMISM_PORTAL_L1:
_default: 'false'
INDEXER_OPTIMISM_L1_RPC:
_default: http://65.108.226.29:8545
INDEXER_OPTIMISM_L1_PORTAL_CONTRACT:
_default: 0x5b47E1A08Ea6d985D6649300584e6722Ec4B1383
INDEXER_OPTIMISM_WITHDRAWALS_START_BLOCK_L1:
INDEXER_OPTIMISM_L1_WITHDRAWALS_START_BLOCK:
_default: '8299683'
INDEXER_OPTIMISM_WITHDRAWALS_START_BLOCK_L2:
INDEXER_OPTIMISM_L2_WITHDRAWALS_START_BLOCK:
_default: '4066066'
INDEXER_OPTIMISM_MESSAGE_PASSER_L2:
INDEXER_OPTIMISM_L2_MESSAGE_PASSER_CONTRACT:
_default: 0x4200000000000000000000000000000000000016
INDEXER_OPTIMISM_OUTPUT_ROOTS_START_BLOCK_L1:
INDEXER_OPTIMISM_L1_OUTPUT_ROOTS_START_BLOCK:
_default: '8299683'
INDEXER_OPTIMISM_OUTPUT_ORACLE_L1:
INDEXER_OPTIMISM_L1_OUTPUT_ORACLE_CONTRACT:
_default: 0xE6Dfba0953616Bacab0c9A8ecb3a9BBa77FC15c0
INDEXER_OPTIMISM_BATCH_START_BLOCK_L1:
INDEXER_OPTIMISM_L1_BATCH_START_BLOCK:
_default: '8381594'
INDEXER_OPTIMISM_BATCH_INBOX:
INDEXER_OPTIMISM_L1_BATCH_INBOX:
_default: 0xff00000000000000000000000000000000000420
INDEXER_OPTIMISM_BATCH_SUBMITTER:
INDEXER_OPTIMISM_L1_BATCH_SUBMITTER:
_default: 0x7431310e026b69bfc676c0013e12a1a11411eec9
INDEXER_OPTIMISM_L1_DEPOSITS_START_BLOCK:
_default: '8381594'
postgres:
enabled: true
......@@ -310,13 +314,16 @@ frontend:
- "/search-results"
- "/token"
- "/tokens"
- "/accounts"
- "/visualize"
- "/api-docs"
- "/csv-export"
- "/verified-contracts"
- "/graphiql"
- "/accounts"
- "/visualize"
- "/api-docs"
- "/output-roots"
- "/txn-batches"
- "/withdrawals"
- "/deposits"
resources:
limits:
memory:
......@@ -396,7 +403,7 @@ frontend:
NEXT_PUBLIC_IS_L2_NETWORK:
_default: "true"
NEXT_PUBLIC_L1_BASE_URL:
_default: https://blockscout-main.test.aws-k8s.blockscout.com
_default: https://eth-goerli.blockscout.com/
NEXT_PUBLIC_L2_WITHDRAWAL_URL:
_default: https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_GRAPHIQL_TRANSACTION:
......
......@@ -175,12 +175,12 @@ postgres:
resources:
limits:
memory:
_default: "6Gi"
_default: "8Gi"
cpu:
_default: "2"
requests:
memory:
_default: "6Gi"
_default: "8Gi"
cpu:
_default: "2"
# node label
......@@ -303,12 +303,12 @@ frontend:
memory:
_default: "0.1Gi"
cpu:
_default: "0.2"
_default: "0.5"
requests:
memory:
_default: "0.1Gi"
cpu:
_default: "0.2"
_default: "0.5"
environment:
# ui config
NEXT_PUBLIC_FEATURED_NETWORKS:
......
......@@ -40,6 +40,10 @@ frontend:
- "/csv-export"
- "/verified-contracts"
- "/graphiql"
- "/output-roots"
- "/txn-batches"
- "/withdrawals"
- "/deposits"
resources:
limits:
......
......@@ -44,14 +44,14 @@ frontend:
resources:
limits:
memory:
_default: "0.1Gi"
_default: "2Gi"
cpu:
_default: "0.1"
_default: "2"
requests:
memory:
_default: "0.1Gi"
_default: "2Gi"
cpu:
_default: "0.1"
_default: "2"
environment:
NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v4.1.8-beta
......
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" fill-rule="evenodd" d="M14.806 21.313H4.582A.583.583 0 0 1 4 20.731V6.179c0-.322.261-.583.582-.583h2.654V3.27c0-.321.26-.582.582-.582h7.313c.007 0 .013.002.02.004.005.001.01.003.017.003a.558.558 0 0 1 .153.031l.02.006a.575.575 0 0 1 .194.118l4.285 4.11-.002.002a.58.58 0 0 1 .181.419v9.064a1.96 1.96 0 0 1-1.958 1.959h-1.277v.95a1.96 1.96 0 0 1-1.96 1.959ZM18.31 6.798l-2.598-2.506v1.713c0 .437.356.793.793.793h1.805Zm-3.42-3.289H8.058v14.072h10.327a.794.794 0 0 0 .793-.793V7.62h-2.671c-1.08 0-1.616-.535-1.616-1.615V3.509ZM7.236 6.42H4.822v14.07H15.15c.437 0 .793-.7.793-1.136v-.951H7.818a.582.582 0 0 1-.582-.583V6.42Zm9.482 4.532a.39.39 0 1 0 0-.78h-5.12l.502-.504a.39.39 0 0 0-.55-.549l-1.168 1.169a.39.39 0 0 0 .059.599.39.39 0 0 0 .216.065h6.06Zm-.012 1.363h-6.061a.39.39 0 0 0 0 .779h5.12l-.502.504a.39.39 0 1 0 .549.55l1.168-1.17a.39.39 0 0 0-.058-.598.39.39 0 0 0-.216-.065Z" clip-rule="evenodd"/>
<path fill="currentColor" d="M7.236 5.596v.25h.25v-.25h-.25Zm7.915-2.905-.07.24.07-.24Zm.017.003.014-.25-.014.25Zm.153.031.085-.235-.003-.001-.082.236Zm.02.006.072-.239-.072.24Zm.031.012.104-.228h-.001l-.103.228Zm.162.106.173-.18-.173.18Zm4.287 4.111.177.177.18-.18-.184-.177-.173.18Zm-.002.002-.176-.177-.18.18.183.177.173-.18Zm-3.054 11.442v-.25h-.25v.25h.25ZM15.713 4.292l.174-.18-.424-.408v.588h.25Zm2.598 2.506v.25h.62l-.446-.43-.174.18ZM8.058 3.51v-.25h-.25v.25h.25Zm6.833 0h.25v-.25h-.25v.25ZM8.058 17.581h-.25v.25h.25v-.25Zm11.12-9.96h.25v-.25h-.25v.25ZM4.822 6.418v-.25h-.25v.25h.25Zm2.414 0h.25v-.25h-.25v.25ZM4.822 20.49h-.25v.25h.25v-.25Zm11.12-2.087h.25v-.25h-.25v.25Zm1.051-7.567-.177-.177.177.177Zm0-.551-.177.177.177-.177Zm-5.396-.114-.177-.177-.425.427h.602v-.25Zm.503-.505.177.177.007-.007.006-.007-.19-.163Zm.093-.268.25-.01-.25.01Zm-.114-.26.177-.177-.177.177Zm-.26-.114.01-.25-.01.25Zm-.268.093-.163-.19-.007.007-.007.007.177.176Zm-1.169 1.169.177.177-.177-.177Zm-.107.199.245.05-.245-.05Zm.022.225-.232.095.001.001.23-.096Zm.143.175.139-.209-.139.209Zm.217.065v-.25.25Zm6.049 1.363v.25-.25Zm-6.337.114.177.177-.177-.177Zm0 .55.177-.176-.177.177Zm5.396.115.177.176.425-.426h-.602v.25Zm-.502.504.162.19.008-.007.007-.007-.177-.176Zm-.098.126.225.11-.225-.11Zm-.038.155-.25-.01.25.01Zm.026.157.233-.091-.233.09Zm.088.133-.177.177.177-.177Zm.29.114-.01-.25.01.25Zm.154-.039.11.225-.11-.225Zm.127-.097-.177-.177-.007.007-.006.007.19.163Zm1.168-1.168-.176-.178v.001l.176.177Zm.086-.425.23-.095v-.001l-.23.096Zm-.144-.174-.139.208.139-.208Zm-12.34 9.184h10.224v-.5H4.582v.5Zm-.832-.832c0 .46.373.832.832.832v-.5a.333.333 0 0 1-.332-.332h-.5Zm0-14.552V20.73h.5V6.179h-.5Zm.832-.833a.833.833 0 0 0-.832.833h.5c0-.184.149-.333.332-.333v-.5Zm2.654 0H4.582v.5h2.654v-.5Zm-.25-2.077v2.327h.5V3.27h-.5Zm.832-.832a.832.832 0 0 0-.832.832h.5c0-.183.148-.332.332-.332v-.5Zm7.313 0H7.818v.5h7.313v-.5Zm.09.014c-.002 0-.041-.014-.09-.014v.5a.202.202 0 0 1-.042-.004c-.007-.002-.013-.004-.009-.002l.14-.48Zm-.04-.006a.2.2 0 0 1 .034.004l.006.002-.14.48s.034.01.073.013l.027-.5Zm.222.044a.808.808 0 0 0-.22-.044l-.03.499a.307.307 0 0 1 .085.017l.165-.472Zm.01.003a2.077 2.077 0 0 1-.007-.002s-.001 0 0 0l-.17.47.032.01.145-.478Zm.062.022a.443.443 0 0 0-.062-.022l-.145.479a.198.198 0 0 1 .008.002l-.006-.002.205-.457Zm.232.154a.823.823 0 0 0-.231-.153l-.208.455c.041.018.07.038.093.06l.346-.362Zm4.287 4.112-4.287-4.111-.346.36 4.287 4.112.346-.361Zm.002.359.002-.002-.354-.354-.001.002.353.354Zm.254.242a.83.83 0 0 0-.257-.6l-.347.361a.33.33 0 0 1 .104.239h.5Zm0 9.064V7.381h-.5v9.064h.5Zm-2.208 2.209a2.21 2.21 0 0 0 2.208-2.209h-.5a1.71 1.71 0 0 1-1.708 1.709v.5Zm-1.277 0h1.277v-.5h-1.277v.5Zm.25.7v-.95h-.5v.95h.5Zm-2.21 2.209a2.21 2.21 0 0 0 2.21-2.208h-.5a1.71 1.71 0 0 1-1.71 1.708v.5Zm.735-17.09 2.598 2.505.347-.36-2.598-2.506-.348.36Zm.423 1.532V4.292h-.5v1.713h.5Zm.543.543a.544.544 0 0 1-.543-.543h-.5c0 .575.468 1.043 1.043 1.043v-.5Zm1.805 0h-1.805v.5h1.805v-.5ZM8.058 3.76h6.833v-.5H8.058v.5Zm.25 13.822V3.51h-.5v14.072h.5Zm10.077-.25H8.058v.5h10.327v-.5Zm.543-.543c0 .3-.244.543-.543.543v.5c.575 0 1.043-.468 1.043-1.043h-.5Zm0-9.168v9.168h.5V7.62h-.5Zm-2.421.25h2.67v-.5h-2.67v.5ZM14.64 6.005c0 .578.143 1.057.476 1.39.333.332.812.475 1.39.475v-.5c-.502 0-.831-.124-1.037-.33-.205-.205-.33-.533-.33-1.035h-.5Zm0-2.496v2.496h.5V3.509h-.5ZM4.822 6.67h2.414v-.5H4.822v.5Zm.25 13.822V6.419h-.5V20.49h.5Zm10.077-.25H4.822v.5h10.326v-.5Zm.543-.886c0 .162-.07.401-.195.599-.13.21-.264.287-.348.287v.5c.352 0 .616-.272.772-.522.164-.26.27-.59.27-.864h-.5Zm0-.951v.95h.5v-.95h-.5Zm-7.874.25h8.124v-.5H7.818v.5Zm-.832-.833c0 .46.372.833.832.833v-.5a.332.332 0 0 1-.332-.333h-.5Zm0-11.402v11.4h.5V6.42h-.5Zm9.83 4.24a.14.14 0 0 1-.098.042v.5a.64.64 0 0 0 .452-.188l-.354-.353Zm.041-.098a.14.14 0 0 1-.04.099l.353.353a.64.64 0 0 0 .187-.452h-.5Zm-.04-.098a.14.14 0 0 1 .04.098h.5a.64.64 0 0 0-.187-.452l-.354.354Zm-.1-.041a.14.14 0 0 1 .1.04l.353-.353a.64.64 0 0 0-.452-.187v.5Zm-5.12 0h5.12v-.5h-5.12v.5Zm.326-.931-.503.504.355.353.502-.504-.354-.353Zm.02-.082a.14.14 0 0 1-.033.096l.38.325a.64.64 0 0 0 .153-.44l-.5.019Zm-.04-.093a.14.14 0 0 1 .04.093l.5-.02a.64.64 0 0 0-.187-.427l-.353.354Zm-.094-.041a.14.14 0 0 1 .094.04l.353-.353a.64.64 0 0 0-.427-.187l-.02.5Zm-.096.033a.14.14 0 0 1 .096-.033l.02-.5a.64.64 0 0 0-.44.153l.324.38Zm-1.154 1.155 1.168-1.168-.353-.353-1.168 1.168.353.354Zm-.039.072a.139.139 0 0 1 .039-.071l-.353-.355a.64.64 0 0 0-.176.328l.49.098Zm.008.081a.139.139 0 0 1-.008-.08l-.49-.1a.64.64 0 0 0 .036.37l.462-.19Zm.05.061a.14.14 0 0 1-.05-.062l-.462.192c.049.117.13.217.236.287l.277-.417Zm.078.024a.14.14 0 0 1-.077-.024l-.277.417a.64.64 0 0 0 .356.107l-.002-.5Zm6.062 0h-6.061v.5h6.06v-.5Zm-6.073 1.863h6.06v-.5h-6.06v.5Zm-.099.04a.14.14 0 0 1 .099-.04v-.5a.64.64 0 0 0-.452.187l.353.354Zm-.04.1a.14.14 0 0 1 .04-.1l-.353-.353a.639.639 0 0 0-.188.452h.5Zm.04.098a.14.14 0 0 1-.04-.099h-.5c0 .17.067.333.187.452l.353-.353Zm.099.04a.14.14 0 0 1-.099-.04l-.353.353a.64.64 0 0 0 .452.188v-.5Zm5.12 0h-5.12v.5h5.12v-.5Zm-.325.931.502-.504-.354-.353-.502.505.354.352Zm-.05.06a.138.138 0 0 1 .035-.046l-.325-.38a.639.639 0 0 0-.16.207l.45.218Zm-.014.054a.14.14 0 0 1 .014-.055l-.45-.218a.64.64 0 0 0-.063.254l.5.02Zm.01.057a.139.139 0 0 1-.01-.057l-.5-.019a.64.64 0 0 0 .045.258l.465-.182Zm.031.047a.139.139 0 0 1-.03-.047l-.466.182a.64.64 0 0 0 .143.219l.353-.354Zm.048.032a.139.139 0 0 1-.048-.032l-.353.354a.64.64 0 0 0 .219.143l.182-.465Zm.056.01a.139.139 0 0 1-.056-.01l-.182.465a.64.64 0 0 0 .258.044l-.02-.5Zm.056-.015a.138.138 0 0 1-.056.014l.02.5a.64.64 0 0 0 .253-.064l-.217-.45Zm.045-.035a.138.138 0 0 1-.045.035l.217.45a.64.64 0 0 0 .208-.16l-.38-.325Zm1.181-1.182-1.168 1.168.354.354 1.168-1.168-.354-.354Zm.04-.072a.139.139 0 0 1-.04.071l.353.355a.64.64 0 0 0 .176-.327l-.49-.099Zm-.009-.08a.14.14 0 0 1 .008.08l.49.099a.64.64 0 0 0-.035-.37l-.463.19Zm-.05-.062a.14.14 0 0 1 .051.063l.461-.193a.64.64 0 0 0-.236-.286l-.276.416Zm-.078-.023a.14.14 0 0 1 .078.023l.276-.416a.639.639 0 0 0-.355-.107l.001.5Z"/>
</svg>
......@@ -17,10 +17,12 @@ import type { AddressesResponse } from 'types/api/addresses';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block';
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod, SmartContractVerificationConfig } from 'types/api/contract';
import type { VerifiedContractsResponse, VerifiedContractsFilters } from 'types/api/contracts';
import type { VerifiedContractsResponse, VerifiedContractsFilters, VerifiedContractsCounters } from 'types/api/contracts';
import type { DepositsResponse } from 'types/api/deposits';
import type { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
import type { OutputRootsResponse } from 'types/api/outputRoots';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchResult, SearchResultFilters } from 'types/api/search';
import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats';
......@@ -35,9 +37,11 @@ import type {
import type { TokensResponse, TokensFilters, TokenInstanceTransferResponse } from 'types/api/tokens';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
import type { TxnBatchesResponse } from 'types/api/txnBatches';
import type { TTxsFilters } from 'types/api/txsFilters';
import type { TxStateChanges } from 'types/api/txStateChanges';
import type { VisualizedContract } from 'types/api/visualization';
import type { WithdrawalsResponse } from 'types/api/withdrawals';
import type ArrayElement from 'types/utils/ArrayElement';
import appConfig from 'configs/app/config';
......@@ -269,6 +273,9 @@ export const RESOURCES = {
paginationFields: [ 'items_count' as const, 'smart_contract_id' as const ],
filterFields: [ 'q' as const, 'filter' as const ],
},
verified_contracts_counters: {
path: '/api/v2/smart-contracts/counters',
},
// TOKEN
token: {
......@@ -364,6 +371,47 @@ export const RESOURCES = {
path: '/graphql',
},
// L2
deposits: {
path: '/api/v2/optimism/deposits',
paginationFields: [ 'nonce' as const, 'items_count' as const ],
filterFields: [],
},
deposits_count: {
path: '/api/v2/optimism/deposits/count',
},
withdrawals: {
path: '/api/v2/optimism/withdrawals',
paginationFields: [ 'nonce' as const, 'items_count' as const ],
filterFields: [],
},
withdrawals_count: {
path: '/api/v2/optimism/withdrawals/count',
},
output_roots: {
path: '/api/v2/optimism/output-roots',
paginationFields: [ 'index' as const, 'items_count' as const ],
filterFields: [],
},
output_roots_count: {
path: '/api/v2/optimism/output-roots/count',
},
txn_batches: {
path: '/api/v2/optimism/txn-batches',
paginationFields: [ 'block_number' as const, 'items_count' as const ],
filterFields: [],
},
txn_batches_count: {
path: '/api/v2/optimism/txn-batches/count',
},
// DEPRECATED
old_api: {
path: '/api',
......@@ -422,7 +470,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'address_logs' | 'address_tokens' |
'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' |
'token_instance_transfers' |
'verified_contracts';
'verified_contracts' |
'output_roots' | 'withdrawals' | 'txn_batches' | 'deposits';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
......@@ -482,8 +531,17 @@ Q extends 'contract_methods_read_proxy' ? Array<SmartContractReadMethod> :
Q extends 'contract_methods_write' ? Array<SmartContractWriteMethod> :
Q extends 'contract_methods_write_proxy' ? Array<SmartContractWriteMethod> :
Q extends 'verified_contracts' ? VerifiedContractsResponse :
Q extends 'verified_contracts_counters' ? VerifiedContractsCounters :
Q extends 'visualize_sol2uml' ? VisualizedContract :
Q extends 'contract_verification_config' ? SmartContractVerificationConfig :
Q extends 'output_roots' ? OutputRootsResponse :
Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'deposits' ? DepositsResponse :
Q extends 'txn_batches' ? TxnBatchesResponse :
Q extends 'output_roots_count' ? number :
Q extends 'withdrawals_count' ? number :
Q extends 'deposits_count' ? number :
Q extends 'txn_batches_count' ? number :
never;
/* eslint-enable @typescript-eslint/indent */
......
......@@ -8,6 +8,7 @@ function generateCspPolicy() {
descriptors.googleAnalytics(),
descriptors.googleFonts(),
descriptors.googleReCaptcha(),
descriptors.monaco(),
descriptors.sentry(),
descriptors.walletConnect(),
);
......
......@@ -3,5 +3,6 @@ export { app } from './app';
export { googleAnalytics } from './googleAnalytics';
export { googleFonts } from './googleFonts';
export { googleReCaptcha } from './googleReCaptcha';
export { monaco } from './monaco';
export { sentry } from './sentry';
export { walletConnect } from './walletConnect';
import type CspDev from 'csp-dev';
import { KEY_WORDS } from '../utils';
export function monaco(): CspDev.DirectiveDescriptor {
return {
'script-src': [
KEY_WORDS.BLOB,
'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/loader.js',
'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/editor/editor.main.js',
'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/editor/editor.main.nls.js',
'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/basic-languages/solidity/solidity.js',
'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/base/worker/workerMain.js',
],
'style-src': [
'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/editor/editor.main.css',
],
'font-src': [
'https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/min/vs/base/browser/ui/codicons/codicon/codicon.ttf',
],
};
}
......@@ -6,12 +6,13 @@ import appConfig from 'configs/app/config';
import abiIcon from 'icons/ABI.svg';
import apiKeysIcon from 'icons/API.svg';
import appsIcon from 'icons/apps.svg';
// import withdrawalsIcon from 'icons/arrows/north-east.svg';
import withdrawalsIcon from 'icons/arrows/north-east.svg';
import depositsIcon from 'icons/arrows/south-east.svg';
import blocksIcon from 'icons/block.svg';
import gearIcon from 'icons/gear.svg';
import globeIcon from 'icons/globe-b.svg';
import graphQLIcon from 'icons/graphQL.svg';
// import outputRootsIcon from 'icons/output_roots.svg';
import outputRootsIcon from 'icons/output_roots.svg';
import privateTagIcon from 'icons/privattags.svg';
import profileIcon from 'icons/profile.svg';
import publicTagIcon from 'icons/publictags.svg';
......@@ -21,11 +22,10 @@ import statsIcon from 'icons/stats.svg';
import tokensIcon from 'icons/token.svg';
import topAccountsIcon from 'icons/top-accounts.svg';
import transactionsIcon from 'icons/transactions.svg';
// import depositsIcon from 'icons/arrows/south-east.svg';
// import txnBatchIcon from 'icons/txn_batches.svg';
import txnBatchIcon from 'icons/txn_batches.svg';
import verifiedIcon from 'icons/verified.svg';
import watchlistIcon from 'icons/watchlist.svg';
// import { rightLineArrow } from 'lib/html-entities';
import { rightLineArrow } from 'lib/html-entities';
type NavItemCommon = {
text: string;
......@@ -83,14 +83,15 @@ export default function useNavItems(): ReturnType {
const blocks = {
text: 'Blocks',
nextRoute: { pathname: '/blocks' as const },
icon: blocksIcon, isActive: pathname.startsWith('/block'),
icon: blocksIcon,
isActive: pathname === '/blocks' || pathname === '/block/[height]',
isNewUi: true,
};
const txs = {
text: 'Transactions',
nextRoute: { pathname: '/txs' as const },
icon: transactionsIcon,
isActive: pathname.startsWith('/tx'),
isActive: pathname === '/txs' || pathname === '/tx/[hash]',
isNewUi: true,
};
const verifiedContracts =
......@@ -102,16 +103,16 @@ export default function useNavItems(): ReturnType {
[
txs,
// eslint-disable-next-line max-len
// { text: `Deposits (L1${ rightLineArrow }L2)`, nextRoute: { pathname: '/deposits' as const }, icon: depositsIcon, isActive: pathname === '/deposits', isNewUi: true },
{ text: `Deposits (L1${ rightLineArrow }L2)`, nextRoute: { pathname: '/deposits' as const }, icon: depositsIcon, isActive: pathname === '/deposits', isNewUi: true },
// eslint-disable-next-line max-len
// { text: `Withdrawals (L2${ rightLineArrow }L1)`, nextRoute: { pathname: '/withdrawals' as const }, icon: withdrawalsIcon, isActive: pathname === '/withdrawals', isNewUi: true },
{ text: `Withdrawals (L2${ rightLineArrow }L1)`, nextRoute: { pathname: '/withdrawals' as const }, icon: withdrawalsIcon, isActive: pathname === '/withdrawals', isNewUi: true },
],
[
blocks,
// eslint-disable-next-line max-len
// { text: 'Txn batches', nextRoute: { pathname: '/txn-batches' as const }, icon: txnBatchIcon, isActive: pathname === '/txn-batches', isNewUi: true },
{ text: 'Txn batches', nextRoute: { pathname: '/txn-batches' as const }, icon: txnBatchIcon, isActive: pathname === '/txn-batches', isNewUi: true },
// eslint-disable-next-line max-len
// { text: 'Output roots', nextRoute: { pathname: '/output-roots' as const }, icon: outputRootsIcon, isActive: pathname === '/output-roots', isNewUi: true },
{ text: 'Output roots', nextRoute: { pathname: '/output-roots' as const }, icon: outputRootsIcon, isActive: pathname === '/output-roots', isNewUi: true },
],
[
topAccounts,
......
......@@ -19,3 +19,6 @@ export const minus = String.fromCharCode(8722); // −
export const leftLineArrow = String.fromCharCode(8592); // ←
export const rightLineArrow = String.fromCharCode(8594); // →
export const apos = String.fromCharCode(39); // apostrophe '
export const shift = String.fromCharCode(8679); // upwards white arrow ⇧
export const cmd = String.fromCharCode(8984); // place of interest sign ⌘
export const alt = String.fromCharCode(9095); // alternate key symbol ⎇
export default function isMetaKey(event: React.KeyboardEvent) {
return event.metaKey || event.getModifierState('Meta') || event.getModifierState('OS');
}
const stripLeadingSlash = (str: string) => str[0] === '/' ? str.slice(1) : str;
export default stripLeadingSlash;
import type { VerifiedContractsCounters } from 'types/api/contracts';
export const verifiedContractsCountersMock: VerifiedContractsCounters = {
smart_contracts: '123456789',
new_smart_contracts_24h: '12345',
verified_smart_contracts: '654321',
new_verified_smart_contracts_24h: '0',
};
export const data = {
items: [
{
l1_block_number: 8382841,
l1_block_timestamp: '2022-05-27T01:13:48.000000Z',
l1_tx_hash: '0xaf3e5f4ef03eac22a622b3434c5dc9f4465aa291900a86bcf0ad9fb14429f05e',
l1_tx_origin: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5',
l2_tx_gas_limit: '2156928',
l2_tx_hash: '0xb9212c76069b926917816767e4c5a0ef80e519b1ac1c3d3fb5818078f4984667',
},
{
l1_block_number: 8382841,
l1_block_timestamp: '2022-05-27T01:13:48.000000Z',
l1_tx_hash: '0xa280f18cc72f9ad904087eb262c236048e935ad184a85bbd042d544c172c10bf',
l1_tx_origin: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5',
l2_tx_gas_limit: '1216064',
l2_tx_hash: '0xaaaeb47a78b5c42d870f8d831a683a7cefe1b031a992170b28b43b82bd08318c',
},
{
l1_block_number: 8382834,
l1_block_timestamp: '2022-06-27T01:11:48.000000Z',
l1_tx_hash: '0xfca8cc5440bffa8b975873c02bba3ff3380dd75fbc3260d10179e282cf72d6d4',
l1_tx_origin: '0x6197d1eef304eb5284a0f6720f79403b4e9bf3a5',
l2_tx_gas_limit: '405824',
l2_tx_hash: '0xa0604ebf2614ad708aeefa83f766fb25928dadb5ffb2f45028f5b4f1fa4d9358',
},
],
next_page_params: {
items_count: 50,
l1_block_number: 8382363,
tx_hash: '0x2012f0ce966ce6573e7826e9235f227edf5a2f8382b8d646c979f85a77e15c05',
},
};
export const outputRootsData = {
items: [
{
l1_block_number: 8456113,
l1_timestamp: '2022-02-08T12:08:48.000000Z',
l1_tx_hash: '0x19455a53758d5de89070164ff09c40d93f1b4447e721090f03aa150f6159265a',
l2_block_number: 5214988,
l2_output_index: 9926,
output_root: '0xa7de9bd3986ce5ca8de9f0ab6c7473f4cebe225fb13b57cc5c8472de84a8bab3',
},
{
l1_block_number: 8456099,
l1_timestamp: '2022-02-08T12:05:24.000000Z',
l1_tx_hash: '0x6aa081e8e33a085e4ec7124fcd8a5f7d36aac0828f176e80d4b70e313a11695b',
l2_block_number: 5214868,
l2_output_index: 9925,
output_root: '0x4ec2822d2f7b4f834d693d88f8a4cf15899882915980a21756d29cfd9f9f3898',
},
{
l1_block_number: 8456078,
l1_timestamp: '2022-02-08T12:00:48.000000Z',
l1_tx_hash: '0x4238988b0959e41a7b09cef67f58698e05e3bcc29b8d2f60e6c77dc68c91f16e',
l2_block_number: 5214748,
l2_output_index: 9924,
output_root: '0x78b2e13c20f4bbfb4a008127edaaf25aa476f933669edd4856305bf4ab64a92b',
},
],
next_page_params: {
index: 9877,
items_count: 50,
},
};
export const txnBatchesData = {
items: [
{
epoch_number: 8547349,
l1_tx_hashes: [
'0x5bc94d02b65743dfaa9e10a2d6e175aff2a05cce2128c8eaf848bd84ab9325c5',
'0x92a51bc623111dbb91f243e3452e60fab6f090710357f9d9b75ac8a0f67dfd9d',
],
l1_timestamp: '2023-02-24T10:16:12.000000Z',
l2_block_number: 5902836,
tx_count: 0,
},
{
epoch_number: 8547348,
l1_tx_hashes: [
'0xc45f846ee28ce9ba116ce2d378d3dd00b55d324b833b3ecd4241c919c572c4aa',
],
l1_timestamp: '2023-02-24T10:16:00.000000Z',
l2_block_number: 5902835,
tx_count: 0,
},
{
epoch_number: 8547348,
l1_tx_hashes: [
'0x48139721f792d3a68c3781b4cf50e66e8fc7dbb38adff778e09066ea5be9adb8',
],
l1_timestamp: '2023-02-24T10:16:00.000000Z',
l2_block_number: 5902834,
tx_count: 0,
},
],
next_page_params: {
block_number: 5902834,
items_count: 50,
},
};
export const data = {
items: [
{
challenge_period_end: null,
from: {
hash: '0x67aab90c548b284be30b05c376001b4db90b87ba',
implementation_name: null,
is_contract: false,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
l1_tx_hash: '0x1a235bee32ac10cb7efdad98415737484ca66386e491cde9e17d42b136dca684',
l2_timestamp: '2022-02-15T12:50:02.000000Z',
l2_tx_hash: '0x918cd8c5c24c17e06cd02b0379510c4ad56324bf153578fb9caaaa2fe4e7dc35',
msg_nonce: 396,
msg_nonce_raw: '1766847064778384329583297500742918515827483896875618958121606201292620172',
msg_nonce_version: 1,
status: 'Ready to prove',
},
{
challenge_period_end: null,
from: null,
l1_tx_hash: null,
l2_timestamp: null,
l2_tx_hash: '0x2f117bee32ac10cb7efdad98415737484ca66386e491cde9e17d42b136def593',
msg_nonce: 391,
msg_nonce_raw: '1766847064778384329583297500742918515827483896875618958121606201292620167',
msg_nonce_version: 1,
status: 'Ready to prove',
},
{
challenge_period_end: '2022-11-11T12:50:02.000000Z',
from: null,
l1_tx_hash: null,
l2_timestamp: null,
l2_tx_hash: '0xe14b1f46838176702244a5343629bcecf728ca2d9881d47b4ce46e00c387d7e3',
msg_nonce: 390,
msg_nonce_raw: '1766847064778384329583297500742918515827483896875618958121606201292620166',
msg_nonce_version: 1,
status: 'Ready for relay',
},
],
next_page_params: {
items_count: 50,
nonce: '1766847064778384329583297500742918515827483896875618958121606201292620123',
},
};
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Deposits from 'ui/pages/Deposits';
const DepositsPage: NextPage = () => {
const title = getNetworkTitle();
return (
<>
<Head>
<title>{ title }</title>
</Head>
<Deposits/>
</>
);
};
export default DepositsPage;
export { getServerSideProps } from 'lib/next/getServerSidePropsL2';
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import OutputRoots from 'ui/pages/OutputRoots';
const OutputRootsPage: NextPage = () => {
const title = getNetworkTitle();
return (
<>
<Head>
<title>{ title }</title>
</Head>
<OutputRoots/>
</>
);
};
export default OutputRootsPage;
export { getServerSideProps } from 'lib/next/getServerSidePropsL2';
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import TxnBatches from 'ui/pages/TxnBatches';
const TxnBatchesPage: NextPage = () => {
const title = getNetworkTitle();
return (
<>
<Head>
<title>{ title }</title>
</Head>
<TxnBatches/>
</>
);
};
export default TxnBatchesPage;
export { getServerSideProps } from 'lib/next/getServerSidePropsL2';
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Withdrawals from 'ui/pages/Withdrawals';
const WithdrawalsPage: NextPage = () => {
const title = getNetworkTitle();
return (
<>
<Head>
<title>{ title }</title>
</Head>
<Withdrawals/>
</>
);
};
export default WithdrawalsPage;
export { getServerSideProps } from 'lib/next/getServerSidePropsL2';
......@@ -96,6 +96,7 @@ const baseStyle = definePartsStyle({
borderTopRightRadius: 'base',
overflow: 'unset',
fontVariant: 'normal',
fontVariantLigatures: 'no-contextual',
},
});
......
......@@ -8,6 +8,7 @@ const global = (props: StyleFunctionProps) => ({
bg: mode('white', 'black')(props),
...getDefaultTransitionProps(),
'-webkit-tap-highlight-color': 'transparent',
'font-variant-ligatures': 'no-contextual',
},
mark: {
bgColor: 'yellow.200',
......
......@@ -24,3 +24,10 @@ export interface VerifiedContractsFilters {
q: string | undefined;
filter: 'vyper' | 'solidity' | undefined;
}
export type VerifiedContractsCounters = {
new_smart_contracts_24h: string;
new_verified_smart_contracts_24h: string;
smart_contracts: string;
verified_smart_contracts: string;
}
export type DepositsItem = {
l1_block_number: number;
l1_tx_hash: string;
l1_block_timestamp: string;
l1_tx_origin: string;
l2_tx_gas_limit: string;
l2_tx_hash: string;
}
export type DepositsResponse = {
items: Array<DepositsItem>;
next_page_params: {
items_count: number;
l1_block_number: number;
tx_hash: string;
};
total: number;
}
export type OutputRootsItem = {
l1_block_number: number;
l1_timestamp: string;
l1_tx_hash: string;
l2_block_number: number;
l2_output_index: number;
output_root: string;
}
export type OutputRootsResponse = {
items: Array<OutputRootsItem>;
total: number;
next_page_params: {
index: number;
items_count: number;
};
}
export type TxnBatchesItem = {
epoch_number: number;
l1_tx_hashes: Array<string>;
l1_timestamp: string;
l2_block_number: number;
tx_count: number;
}
export type TxnBatchesResponse = {
items: Array<TxnBatchesItem>;
total: number;
next_page_params: {
index: number;
items_count: number;
};
}
import type { AddressParam } from './addressParams';
export type WithdrawalsItem = {
'challenge_period_end': string | null;
'from': AddressParam | null;
'l1_tx_hash': string | null;
'l2_timestamp': string | null;
'l2_tx_hash': string;
'msg_nonce': number;
'msg_nonce_version': number;
'status': string;
}
export type WithdrawalStatus =
'In challenge period' |
'Ready for relay' |
'Relayed' |
'Waiting for state root' |
'Ready to prove';
export type WithdrawalsResponse = {
items: Array<WithdrawalsItem>;
'next_page_params': {
'items_count': number;
'nonce': string;
};
total: number;
}
......@@ -25,19 +25,23 @@ declare module "nextjs-routes" {
| DynamicRoute<"/block/[height]", { "height": string }>
| StaticRoute<"/blocks">
| StaticRoute<"/csv-export">
| StaticRoute<"/deposits">
| StaticRoute<"/graph">
| StaticRoute<"/graphiql">
| StaticRoute<"/">
| StaticRoute<"/login">
| StaticRoute<"/output-roots">
| StaticRoute<"/search-results">
| StaticRoute<"/stats">
| DynamicRoute<"/token/[hash]", { "hash": string }>
| DynamicRoute<"/token/[hash]/instance/[id]", { "hash": string; "id": string }>
| StaticRoute<"/tokens">
| DynamicRoute<"/tx/[hash]", { "hash": string }>
| StaticRoute<"/txn-batches">
| StaticRoute<"/txs">
| StaticRoute<"/verified-contracts">
| StaticRoute<"/visualize/sol2uml">;
| StaticRoute<"/visualize/sol2uml">
| StaticRoute<"/withdrawals">;
interface StaticRoute<Pathname> {
pathname: Pathname;
......
import { Box, Hide, Show, Table, Tbody, Th, Tr } from '@chakra-ui/react';
import { Hide, Show, Table, Tbody, Th, Tr } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -12,10 +12,8 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import SocketAlert from 'ui/shared/SocketAlert';
import { default as Thead } from 'ui/shared/TheadSticky';
......@@ -71,67 +69,51 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
handler: handleNewSocketMessage,
});
const content = (() => {
if (query.isLoading) {
return (
<>
<Hide below="lg" ssr={ false }>
<SkeletonTable columns={ [ '17%', '17%', '16%', '25%', '25%' ] } isLong/>
</Hide>
<Show below="lg" ssr={ false }>
<SkeletonList/>
</Show>
</>
);
}
if (query.isError) {
return <DataFetchAlert/>;
}
if (query.data.items.length === 0) {
return 'There is no validated blocks for this address';
}
return (
<>
<Hide below="lg" ssr={ false }>
<Table variant="simple" size="sm">
<Thead top={ query.isPaginationVisible ? 80 : 0 }>
<Tr>
<Th width="17%">Block</Th>
<Th width="17%">Age</Th>
<Th width="16%">Txn</Th>
<Th width="25%">GasUsed</Th>
<Th width="25%" isNumeric>Reward { appConfig.network.currency.symbol }</Th>
</Tr>
</Thead>
<Tbody>
{ query.data.items.map((item) => (
<AddressBlocksValidatedTableItem key={ item.height } { ...item } page={ query.pagination.page }/>
)) }
</Tbody>
</Table>
</Hide>
<Show below="lg" ssr={ false }>
{ query.data.items.map((item) => (
<AddressBlocksValidatedListItem key={ item.height } { ...item } page={ query.pagination.page }/>
)) }
</Show>
</>
);
})();
const content = query.data?.items ? (
<>
{ socketAlert && <SocketAlert mb={ 6 }/> }
<Hide below="lg" ssr={ false }>
<Table variant="simple" size="sm">
<Thead top={ query.isPaginationVisible ? 80 : 0 }>
<Tr>
<Th width="17%">Block</Th>
<Th width="17%">Age</Th>
<Th width="16%">Txn</Th>
<Th width="25%">GasUsed</Th>
<Th width="25%" isNumeric>Reward { appConfig.network.currency.symbol }</Th>
</Tr>
</Thead>
<Tbody>
{ query.data.items.map((item) => (
<AddressBlocksValidatedTableItem key={ item.height } { ...item } page={ query.pagination.page }/>
)) }
</Tbody>
</Table>
</Hide>
<Show below="lg" ssr={ false }>
{ query.data.items.map((item) => (
<AddressBlocksValidatedListItem key={ item.height } { ...item } page={ query.pagination.page }/>
)) }
</Show>
</>
) : null;
const actionBar = query.isPaginationVisible ? (
<ActionBar mt={ -6 } showShadow={ query.isLoading }>
<Pagination ml="auto" { ...query.pagination }/>
</ActionBar>
) : null;
return (
<Box>
{ query.isPaginationVisible && (
<ActionBar mt={ -6 } showShadow={ query.isLoading }>
<Pagination ml="auto" { ...query.pagination }/>
</ActionBar>
) }
{ socketAlert && <SocketAlert mb={ 6 }/> }
{ content }
</Box>
<DataListDisplay
isError={ query.isError }
isLoading={ query.isLoading }
items={ query.data?.items }
skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: [ '17%', '17%', '16%', '25%', '25%' ] }}
emptyText="There are no validated blocks for this address."
content={ content }
actionBar={ actionBar }
/>
);
};
......
import { Text, Show, Hide } from '@chakra-ui/react';
import { Show, Hide } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -10,12 +10,9 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString';
import AddressIntTxsTable from 'ui/address/internals/AddressIntTxsTable';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import AddressCsvExportLink from './AddressCsvExportLink';
import AddressTxsFilter from './AddressTxsFilter';
......@@ -43,57 +40,40 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
onFilterChange({ filter: newVal });
}, [ onFilterChange ]);
const content = (() => {
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<>
<Show below="lg" ssr={ false }>
<SkeletonList/>
</Show>
<Hide below="lg" ssr={ false }>
<SkeletonTable columns={ [ '15%', '15%', '10%', '20%', '20%', '20%' ] } isLong/>
</Hide>
</>
);
}
if (data.items.length === 0 && !filterValue) {
return <Text as="span">There are no internal transactions for this address.</Text>;
}
if (data.items.length === 0) {
return <EmptySearchResult text={ `Couldn${ apos }t find any transaction that matches your query.` }/>;
}
return (
<>
<Show below="lg" ssr={ false }>
<AddressIntTxsList data={ data.items } currentAddress={ hash }/>
</Show>
<Hide below="lg" ssr={ false }>
<AddressIntTxsTable data={ data.items } currentAddress={ hash }/>
</Hide>
</>
);
})();
return (
const content = data?.items ? (
<>
<ActionBar mt={ -6 } justifyContent="left" showShadow={ isLoading }>
<AddressTxsFilter
defaultFilter={ filterValue }
onFilterChange={ handleFilterChange }
isActive={ Boolean(filterValue) }
/>
<AddressCsvExportLink address={ hash } type="internal-transactions" ml={{ base: 2, lg: 'auto' }}/>
{ isPaginationVisible && <Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/> }
</ActionBar>
{ content }
<Show below="lg" ssr={ false }>
<AddressIntTxsList data={ data.items } currentAddress={ hash }/>
</Show>
<Hide below="lg" ssr={ false }>
<AddressIntTxsTable data={ data.items } currentAddress={ hash }/>
</Hide>
</>
) : null ;
const actionBar = (
<ActionBar mt={ -6 } justifyContent="left" showShadow={ isLoading }>
<AddressTxsFilter
defaultFilter={ filterValue }
onFilterChange={ handleFilterChange }
isActive={ Boolean(filterValue) }
/>
<AddressCsvExportLink address={ hash } type="internal-transactions" ml={{ base: 2, lg: 'auto' }}/>
{ isPaginationVisible && <Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/> }
</ActionBar>
);
return (
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
items={ data?.items }
skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: [ '15%', '15%', '10%', '20%', '20%', '20%' ] }}
filterProps={{ emptyFilteredText: `Couldn${ apos }t find any transaction that matches your query.`, hasActiveFilters: Boolean(filterValue) }}
emptyText="There are no internal transactions for this address."
content={ content }
actionBar={ actionBar }
/>
);
};
......
import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay';
import LogItem from 'ui/shared/logs/LogItem';
import LogSkeleton from 'ui/shared/logs/LogSkeleton';
import Pagination from 'ui/shared/Pagination';
......@@ -20,35 +19,26 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>
scrollRef,
});
if (isError) {
return <DataFetchAlert/>;
}
const bar = isPaginationVisible ? (
const actionBar = isPaginationVisible ? (
<ActionBar mt={ -6 } showShadow>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) : null;
if (isLoading) {
return (
<Box>
{ bar }
<LogSkeleton/>
<LogSkeleton/>
</Box>
);
}
const content = data?.items ? data.items.map((item, index) => <LogItem key={ index } { ...item } type="address"/>) : null;
if (data.items.length === 0) {
return <span>There are no logs for this address.</span>;
}
const skeleton = <><LogSkeleton/><LogSkeleton/></>;
return (
<>
{ bar }
{ data.items.map((item, index) => <LogItem key={ index } { ...item } type="address"/>) }
</>
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
items={ data?.items }
emptyText="There are no logs for this address."
content={ content }
actionBar={ actionBar }
skeletonProps={{ customSkeleton: skeleton }}
/>
);
};
......
......@@ -13,7 +13,7 @@ const API_URL = buildApiUrl('address_token_transfers', { hash: '0xd789a607CEac2f
const hooksConfig = {
router: {
query: { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', token_hash: '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859' },
query: { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', token: '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859' },
},
};
......
......@@ -20,13 +20,10 @@ import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import TOKEN_TYPE from 'lib/token/tokenTypes';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay';
import HashStringShorten from 'ui/shared/HashStringShorten';
import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TokenLogo from 'ui/shared/TokenLogo';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
......@@ -76,7 +73,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
const [ socketAlert, setSocketAlert ] = React.useState('');
const [ newItemsCount, setNewItemsCount ] = React.useState(0);
const tokenFilter = getQueryParamString(router.query.token_hash) || undefined;
const tokenFilter = getQueryParamString(router.query.token) || undefined;
const [ filters, setFilters ] = React.useState<Filters>(
{
......@@ -164,67 +161,40 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0);
const isActionBarHidden = !tokenFilter && !numActiveFilters && !data?.items.length && !currentAddress;
const content = (() => {
if (isLoading) {
return (
<>
<Hide below="lg" ssr={ false }>
<SkeletonTable columns={ [ '44px', '185px', '160px', '25%', '25%', '25%', '25%' ] } isLong/>
</Hide>
<Show below="lg" ssr={ false }>
<SkeletonList/>
</Show>
</>
);
}
if (isError) {
return <DataFetchAlert/>;
}
if (!data.items?.length && !numActiveFilters) {
return <Text as="span">There are no token transfers</Text>;
}
if (!data.items?.length) {
return <EmptySearchResult text={ `Couldn${ apos }t find any token transfer that matches your query.` }/>;
}
const items = data.items.reduce(flattenTotal, []);
return (
<>
<Hide below="lg" ssr={ false }>
<TokenTransferTable
data={ items }
baseAddress={ currentAddress }
showTxInfo
top={ isActionBarHidden ? 0 : 80 }
enableTimeIncrement
showSocketInfo={ pagination.page === 1 && !tokenFilter }
socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount }
/>
</Hide>
<Show below="lg" ssr={ false }>
{ pagination.page === 1 && !tokenFilter && (
<SocketNewItemsNotice
url={ window.location.href }
num={ newItemsCount }
alert={ socketAlert }
type="token_transfer"
borderBottomRadius={ 0 }
/>
) }
<TokenTransferList
data={ items }
baseAddress={ currentAddress }
showTxInfo
enableTimeIncrement
const items = data?.items?.reduce(flattenTotal, []);
const content = items ? (
<>
<Hide below="lg" ssr={ false }>
<TokenTransferTable
data={ items }
baseAddress={ currentAddress }
showTxInfo
top={ isActionBarHidden ? 0 : 80 }
enableTimeIncrement
showSocketInfo={ pagination.page === 1 && !tokenFilter }
socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount }
/>
</Hide>
<Show below="lg" ssr={ false }>
{ pagination.page === 1 && !tokenFilter && (
<SocketNewItemsNotice
url={ window.location.href }
num={ newItemsCount }
alert={ socketAlert }
type="token_transfer"
borderBottomRadius={ 0 }
/>
</Show>
</>
);
})();
) }
<TokenTransferList
data={ items }
baseAddress={ currentAddress }
showTxInfo
enableTimeIncrement
/>
</Show>
</>
) : null;
const tokenFilterComponent = tokenFilter && (
<Flex alignItems="center" flexWrap="wrap" mb={{ base: isActionBarHidden ? 3 : 6, lg: 0 }} mr={ 4 }>
......@@ -249,7 +219,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
</Flex>
);
return (
const actionBar = (
<>
{ isMobile && tokenFilterComponent }
{ !isActionBarHidden && (
......@@ -269,9 +239,27 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
{ isPaginationVisible && <Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/> }
</ActionBar>
) }
{ content }
</>
);
return (
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
items={ data?.items }
skeletonProps={{
isLongSkeleton: true,
skeletonDesktopColumns: [ '44px', '185px', '160px', '25%', '25%', '25%', '25%' ],
}}
emptyText="There are no token transfers."
filterProps={{
emptyFilteredText: `Couldn${ apos }t find any token transfer that matches your query.`,
hasActiveFilters: Boolean(numActiveFilters),
}}
content={ content }
actionBar={ actionBar }
/>
);
};
export default AddressTokenTransfers;
......@@ -9,7 +9,7 @@ import appConfig from 'configs/app/config';
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import Utilization from 'ui/shared/Utilization/Utilization';
type Props = Block & {
......
import { Box, Hide, Show, Table, Tbody, Th, Tr } from '@chakra-ui/react';
import { Hide, Show, Table, Tbody, Th, Tr } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
......@@ -6,11 +6,9 @@ import type { AddressCoinBalanceHistoryResponse } from 'types/api/address';
import appConfig from 'configs/app/config';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import { default as Thead } from 'ui/shared/TheadSticky';
import AddressCoinBalanceListItem from './AddressCoinBalanceListItem';
......@@ -25,66 +23,51 @@ interface Props {
const AddressCoinBalanceHistory = ({ query }: Props) => {
const content = (() => {
if (query.isLoading) {
return (
<>
<Hide below="lg" ssr={ false }>
<SkeletonTable columns={ [ '25%', '25%', '25%', '25%', '120px' ] }/>
</Hide>
<Show below="lg" ssr={ false }>
<SkeletonList/>
</Show>
</>
);
}
const content = query.data?.items ? (
<>
<Hide below="lg" ssr={ false }>
<Table variant="simple" size="sm">
<Thead top={ query.isPaginationVisible ? 80 : 0 }>
<Tr>
<Th width="20%">Block</Th>
<Th width="20%">Txn</Th>
<Th width="20%">Age</Th>
<Th width="20%" isNumeric pr={ 1 }>Balance { appConfig.network.currency.symbol }</Th>
<Th width="20%" isNumeric>Delta</Th>
</Tr>
</Thead>
<Tbody>
{ query.data.items.map((item) => (
<AddressCoinBalanceTableItem key={ item.block_number } { ...item } page={ query.pagination.page }/>
)) }
</Tbody>
</Table>
</Hide>
<Show below="lg" ssr={ false }>
{ query.data.items.map((item) => (
<AddressCoinBalanceListItem key={ item.block_number } { ...item } page={ query.pagination.page }/>
)) }
</Show>
</>
) : null;
if (query.isError) {
return <DataFetchAlert/>;
}
if (query.data.items.length === 0 && !query.isPaginationVisible) {
return <span>There is no coin balance history for this address</span>;
}
return (
<>
<Hide below="lg" ssr={ false }>
<Table variant="simple" size="sm">
<Thead top={ query.isPaginationVisible ? 80 : 0 }>
<Tr>
<Th width="20%">Block</Th>
<Th width="20%">Txn</Th>
<Th width="20%">Age</Th>
<Th width="20%" isNumeric pr={ 1 }>Balance { appConfig.network.currency.symbol }</Th>
<Th width="20%" isNumeric>Delta</Th>
</Tr>
</Thead>
<Tbody>
{ query.data.items.map((item) => (
<AddressCoinBalanceTableItem key={ item.block_number } { ...item } page={ query.pagination.page }/>
)) }
</Tbody>
</Table>
</Hide>
<Show below="lg" ssr={ false }>
{ query.data.items.map((item) => (
<AddressCoinBalanceListItem key={ item.block_number } { ...item } page={ query.pagination.page }/>
)) }
</Show>
</>
);
})();
const actionBar = query.isPaginationVisible ? (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...query.pagination }/>
</ActionBar>
) : null;
return (
<Box mt={ 8 }>
{ query.isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...query.pagination }/>
</ActionBar>
) }
{ content }
</Box>
<DataListDisplay
mt={ 8 }
isError={ query.isError }
isLoading={ query.isLoading }
items={ query.data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '25%', '25%', '25%', '25%', '120px' ] }}
emptyText="There is no coin balance history for this address."
content={ content }
actionBar={ actionBar }
/>
);
};
......
......@@ -11,7 +11,7 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
type Props = AddressCoinBalanceHistoryItem & {
page: number;
......
......@@ -20,6 +20,7 @@ test('verified with changed byte code +@mobile +@dark-mode', async({ mount, page
status: 200,
body: JSON.stringify(contractMock.withChangedByteCode),
}));
await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort());
const component = await mount(
<TestApp>
......@@ -36,6 +37,7 @@ test('verified with multiple sources +@mobile', async({ mount, page }) => {
status: 200,
body: JSON.stringify(contractMock.withMultiplePaths),
}));
await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort());
await mount(
<TestApp>
......@@ -54,6 +56,7 @@ test('verified via sourcify', async({ mount, page }) => {
status: 200,
body: JSON.stringify(contractMock.verifiedViaSourcify),
}));
await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort());
await mount(
<TestApp>
......@@ -70,6 +73,7 @@ test('self destructed', async({ mount, page }) => {
status: 200,
body: JSON.stringify(contractMock.selfDestructed),
}));
await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort());
await mount(
<TestApp>
......@@ -87,6 +91,7 @@ test('with twin address alert +@mobile', async({ mount, page }) => {
status: 200,
body: JSON.stringify(contractMock.withTwinAddress),
}));
await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort());
const component = await mount(
<TestApp>
......@@ -103,6 +108,7 @@ test('with proxy address alert +@mobile', async({ mount, page }) => {
status: 200,
body: JSON.stringify(contractMock.withProxyAddress),
}));
await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort());
const component = await mount(
<TestApp>
......@@ -119,6 +125,7 @@ test('non verified', async({ mount, page }) => {
status: 200,
body: JSON.stringify(contractMock.nonVerified),
}));
await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort());
const component = await mount(
<TestApp>
......
import { Box, chakra, Flex, Text, Tooltip } from '@chakra-ui/react';
import { Flex, Text, Tooltip } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { SmartContract } from 'types/api/contract';
import CodeEditor from 'ui/shared/CodeEditor';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import LinkInternal from 'ui/shared/LinkInternal';
import CodeEditor from 'ui/shared/monaco/CodeEditor';
import formatFilePath from 'ui/shared/monaco/utils/formatFilePath';
interface Props {
data: string;
......@@ -37,38 +38,25 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
</Tooltip>
) : null;
if (!additionalSource?.length) {
return (
<section>
<Flex justifyContent="space-between" alignItems="center" mb={ 3 }>
{ heading }
{ diagramLink }
<CopyToClipboard text={ data }/>
</Flex>
<CodeEditor value={ data } id="source_code"/>
</section>
);
}
const editorData = React.useMemo(() => {
const defaultName = isViper ? '/index.vy' : '/index.sol';
return [
{ file_path: formatFilePath(filePath || defaultName), source_code: data },
...(additionalSource || []).map((source) => ({ ...source, file_path: formatFilePath(source.file_path) })) ];
}, [ additionalSource, data, filePath, isViper ]);
const copyToClipboard = editorData.length === 1 ?
<CopyToClipboard text={ editorData[0].source_code }/> :
null;
return (
<section>
<Flex justifyContent="space-between" alignItems="center" mb={ 3 }>
{ heading }
{ diagramLink }
{ copyToClipboard }
</Flex>
<Flex flexDir="column" rowGap={ 3 }>
{ [ { file_path: filePath, source_code: data }, ...additionalSource ].map((item, index, array) => (
<Box key={ index }>
<Flex justifyContent="space-between" alignItems="flex-end" mb={ 3 }>
<chakra.span fontSize="sm" wordBreak="break-all" lineHeight="20px">
File { index + 1 } of { array.length }: { item.file_path }
</chakra.span>
<CopyToClipboard text={ item.source_code } ml={ 4 }/>
</Flex>
<CodeEditor value={ item.source_code } id={ `source_code_${ index }` }/>
</Box>
)) }
</Flex>
<CodeEditor data={ editorData }/>
</section>
);
};
......
......@@ -13,7 +13,7 @@ import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
......
......@@ -6,7 +6,7 @@ import type { AddressTokenBalance } from 'types/api/address';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TokenLogo from 'ui/shared/TokenLogo';
import AddressAddToMetaMask from '../details/AddressAddToMetaMask';
......
import { Grid, Skeleton, Text } from '@chakra-ui/react';
import { Grid, Skeleton } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
......@@ -6,7 +6,7 @@ import type { AddressTokensResponse } from 'types/api/address';
import useIsMobile from 'lib/hooks/useIsMobile';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
......@@ -24,52 +24,48 @@ const TokensWithIds = ({ tokensQuery }: Props) => {
const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery;
if (isError) {
return <DataFetchAlert/>;
}
const bar = isMobile && isPaginationVisible && (
const actionBar = isMobile && isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
);
if (isLoading) {
return (
<>
{ bar }
<Grid
w="100%"
columnGap={{ base: 3, lg: 6 }}
rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
</Grid>
</>
);
}
const skeleton = (
<Grid
w="100%"
columnGap={{ base: 3, lg: 6 }}
rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
</Grid>
);
if (!data.items.length) {
return <Text as="span">There are no tokens of selected type.</Text>;
}
const content = data?.items ? (
<Grid
w="100%"
columnGap={{ base: 3, lg: 6 }}
rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
{ data.items.map(item => <NFTItem key={ item.token.address } { ...item }/>) }
</Grid>
) : null;
return (
<>
{ bar }
<Grid
w="100%"
columnGap={{ base: 3, lg: 6 }}
rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
{ data.items.map(item => <NFTItem key={ item.token.address } { ...item }/>) }
</Grid>
</>
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
items={ data?.items }
emptyText="There are no tokens of selected type."
content={ content }
actionBar={ actionBar }
skeletonProps={{ customSkeleton: skeleton }}
/>
);
};
......
import { Text, Show, Hide } from '@chakra-ui/react';
import { Show, Hide } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
......@@ -6,11 +6,9 @@ import type { AddressTokensResponse } from 'types/api/address';
import useIsMobile from 'lib/hooks/useIsMobile';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import TokensListItem from './TokensListItem';
import TokensTable from './TokensTable';
......@@ -27,36 +25,31 @@ const TokensWithoutIds = ({ tokensQuery }: Props) => {
const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery;
if (isError) {
return <DataFetchAlert/>;
}
const bar = isMobile && isPaginationVisible && (
const actionBar = isMobile && isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
);
if (isLoading) {
return (
<>
{ bar }
<Hide below="lg" ssr={ false }><SkeletonTable columns={ [ '30%', '30%', '10%', '20%', '10%' ] }/></Hide>
<Show below="lg" ssr={ false }><SkeletonList/></Show>
</>
);
}
if (data.items.length === 0) {
return <Text as="span">There are no tokens of selected type.</Text>;
}
return (
const content = data?.items ? (
<>
{ bar }
<Hide below="lg" ssr={ false }><TokensTable data={ data.items } top={ isPaginationVisible ? 72 : 0 }/></Hide>
<Show below="lg" ssr={ false }>{ data.items.map(item => <TokensListItem key={ item.token.address } { ...item }/>) }</Show>
</>
<Show below="lg" ssr={ false }>{ data.items.map(item => <TokensListItem key={ item.token.address } { ...item }/>) }</Show></>
) : null;
return (
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
items={ data?.items }
skeletonProps={{
isLongSkeleton: true,
skeletonDesktopColumns: [ '30%', '30%', '10%', '20%', '10%' ],
}}
emptyText="There are no tokens of selected type."
content={ content }
actionBar={ actionBar }
/>
);
};
......
......@@ -8,7 +8,7 @@ import appConfig from 'configs/app/config';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import ListItemMobile from 'ui/shared/ListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
type Props = {
item: AddressesItem;
......
......@@ -3,7 +3,7 @@ import React, { useCallback } from 'react';
import type { ApiKey } from 'types/api/account';
import ApiKeySnippet from 'ui/shared/ApiKeySnippet';
import ListItemMobile from 'ui/shared/ListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
interface Props {
......
import { Text, Show, Hide, Alert } from '@chakra-ui/react';
import { Show, Hide, Alert } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
......@@ -13,11 +13,9 @@ import useSocketMessage from 'lib/socket/useSocketMessage';
import BlocksList from 'ui/blocks/BlocksList';
import BlocksTable from 'ui/blocks/BlocksTable';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
type QueryResult = UseQueryResult<BlocksResponse> & {
pagination: PaginationProps;
......@@ -76,51 +74,34 @@ const BlocksContent = ({ type, query }: Props) => {
handler: handleNewBlockMessage,
});
const content = (() => {
if (query.isLoading) {
return (
<>
<Show below="lg" key="skeleton-mobile" ssr={ false }>
<SkeletonList/>
</Show>
<Hide below="lg" key="skeleton-desktop" ssr={ false }>
<SkeletonTable columns={ [ '125px', '120px', '21%', '64px', '35%', '22%', '22%' ] }/>
</Hide>
</>
);
}
if (query.isError) {
return <DataFetchAlert/>;
}
if (query.data.items.length === 0) {
return <Text as="span">There are no blocks.</Text>;
}
return (
<>
{ socketAlert && <Alert status="warning" mb={ 6 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> }
<Show below="lg" key="content-mobile" ssr={ false }>
<BlocksList data={ query.data.items }/>
</Show>
<Hide below="lg" key="content-desktop" ssr={ false }>
<BlocksTable data={ query.data.items } top={ query.isPaginationVisible ? 80 : 0 } page={ query.pagination.page }/>
</Hide>
</>
);
})();
return (
const content = query.data?.items ? (
<>
{ isMobile && query.isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...query.pagination }/>
</ActionBar>
) }
{ content }
{ socketAlert && <Alert status="warning" mb={ 6 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> }
<Show below="lg" key="content-mobile" ssr={ false }>
<BlocksList data={ query.data.items }/>
</Show>
<Hide below="lg" key="content-desktop" ssr={ false }>
<BlocksTable data={ query.data.items } top={ query.isPaginationVisible ? 80 : 0 } page={ query.pagination.page }/>
</Hide>
</>
) : null;
const actionBar = isMobile && query.isPaginationVisible ? (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...query.pagination }/>
</ActionBar>
) : null;
return (
<DataListDisplay
isError={ query.isError }
isLoading={ query.isLoading }
items={ query.data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '125px', '120px', '21%', '64px', '35%', '22%', '22%' ] }}
emptyText="There are no blocks."
content={ content }
actionBar={ actionBar }
/>
);
};
......
......@@ -15,7 +15,7 @@ import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressLink from 'ui/shared/address/AddressLink';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import Utilization from 'ui/shared/Utilization/Utilization';
interface Props {
......
......@@ -92,7 +92,7 @@ test.describe('sourcify', () => {
});
testWithSocket.describe.configure({ mode: 'serial', timeout: 20_000 });
testWithSocket('with multiple contracts +@mobile', async({ mount, page, createSocket }) => {
testWithSocket('with multiple contracts', async({ mount, page, createSocket }) => {
const component = await mount(
<TestApp withSocket>
<ContractVerificationForm config={ formConfig } hash={ hash }/>
......
import { Button, chakra, useUpdateEffect } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { route } from 'nextjs-routes';
import React from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form';
......@@ -20,7 +20,7 @@ import ContractVerificationSourcify from './methods/ContractVerificationSourcify
import ContractVerificationStandardInput from './methods/ContractVerificationStandardInput';
import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract';
import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile';
import { prepareRequestBody, formatSocketErrors, DEFAULT_VALUES } from './utils';
import { prepareRequestBody, formatSocketErrors, getDefaultValues } from './utils';
const METHOD_COMPONENTS = {
'flattened-code': <ContractVerificationFlattenSourceCode/>,
......@@ -40,14 +40,13 @@ interface Props {
const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Props) => {
const formApi = useForm<FormFields>({
mode: 'onBlur',
defaultValues: methodFromQuery ? DEFAULT_VALUES[methodFromQuery] : undefined,
defaultValues: methodFromQuery ? getDefaultValues(methodFromQuery, config) : undefined,
});
const { control, handleSubmit, watch, formState, setError, reset } = formApi;
const submitPromiseResolver = React.useRef<(value: unknown) => void>();
const apiFetch = useApiFetch();
const toast = useToast();
const router = useRouter();
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => {
const body = prepareRequestBody(data);
......@@ -86,8 +85,8 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
isClosable: true,
});
router.push({ pathname: '/address/[hash]', query: { hash, tab: 'contract' } }, undefined, { shallow: false });
}, [ hash, router, setError, toast ]);
window.location.assign(route({ pathname: '/address/[hash]', query: { hash, tab: 'contract' } }));
}, [ hash, setError, toast ]);
const handleSocketError = React.useCallback(() => {
if (!formState.isSubmitting) {
......@@ -129,7 +128,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
useUpdateEffect(() => {
if (methodValue) {
reset(DEFAULT_VALUES[methodValue]);
reset(getDefaultValues(methodValue, config));
}
// !!! should run only when method is changed
}, [ methodValue ]);
......
......@@ -60,14 +60,14 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props
const renderPopoverListItem = React.useCallback((method: SmartContractVerificationMethod) => {
switch (method) {
case 'flattened-code':
return <ListItem>Verification through flattened source code.</ListItem>;
return <ListItem key={ method }>Verification through flattened source code.</ListItem>;
case 'multi-part':
return <ListItem>Verification of multi-part Solidity files.</ListItem>;
return <ListItem key={ method }>Verification of multi-part Solidity files.</ListItem>;
case 'sourcify':
return <ListItem>Verification through <Link href="https://sourcify.dev/" target="_blank">Sourcify</Link>.</ListItem>;
return <ListItem key={ method }>Verification through <Link href="https://sourcify.dev/" target="_blank">Sourcify</Link>.</ListItem>;
case 'standard-input':
return (
<ListItem>
<ListItem key={ method }>
<span>Verification using </span>
<Link
href="https://docs.soliditylang.org/en/latest/using-the-compiler.html#input-description"
......@@ -79,9 +79,9 @@ const ContractVerificationFieldMethod = ({ control, isDisabled, methods }: Props
</ListItem>
);
case 'vyper-code':
return <ListItem>Verification of Vyper contract.</ListItem>;
return <ListItem key={ method }>Verification of Vyper contract.</ListItem>;
case 'vyper-multi-part':
return <ListItem>Verification of multi-part Vyper files.</ListItem>;
return <ListItem key={ method }>Verification of multi-part Vyper files.</ListItem>;
}
}, []);
......
......@@ -15,8 +15,8 @@ export interface FormFieldsFlattenSourceCode {
method: MethodOption;
is_yul: boolean;
name: string;
compiler: Option;
evm_version: Option;
compiler: Option | null;
evm_version: Option | null;
is_optimization_enabled: boolean;
optimization_runs: string;
code: string;
......@@ -28,7 +28,7 @@ export interface FormFieldsFlattenSourceCode {
export interface FormFieldsStandardInput {
method: MethodOption;
name: string;
compiler: Option;
compiler: Option | null;
sources: Array<File>;
autodetect_constructor_args: boolean;
constructor_args: string;
......@@ -42,8 +42,8 @@ export interface FormFieldsSourcify {
export interface FormFieldsMultiPartFile {
method: MethodOption;
compiler: Option;
evm_version: Option;
compiler: Option | null;
evm_version: Option | null;
is_optimization_enabled: boolean;
optimization_runs: string;
sources: Array<File>;
......@@ -53,15 +53,15 @@ export interface FormFieldsMultiPartFile {
export interface FormFieldsVyperContract {
method: MethodOption;
name: string;
compiler: Option;
compiler: Option | null;
code: string;
constructor_args: string;
}
export interface FormFieldsVyperMultiPartFile {
method: MethodOption;
compiler: Option;
evm_version: Option;
compiler: Option | null;
evm_version: Option | null;
sources: Array<File>;
}
......
......@@ -10,7 +10,7 @@ import type {
FormFieldsVyperContract,
FormFieldsVyperMultiPartFile,
} from './types';
import type { SmartContractVerificationMethod, SmartContractVerificationError } from 'types/api/contract';
import type { SmartContractVerificationMethod, SmartContractVerificationError, SmartContractVerificationConfig } from 'types/api/contract';
import type { Params as FetchParams } from 'lib/hooks/useFetch';
......@@ -32,7 +32,7 @@ export const METHOD_LABELS: Record<SmartContractVerificationMethod, string> = {
'vyper-multi-part': 'Vyper (Multi-part files)',
};
export const DEFAULT_VALUES = {
export const DEFAULT_VALUES: Record<SmartContractVerificationMethod, FormFields> = {
'flattened-code': {
method: {
value: 'flattened-code' as const,
......@@ -75,7 +75,7 @@ export const DEFAULT_VALUES = {
compiler: null,
evm_version: null,
is_optimization_enabled: true,
optimization_runs: 200,
optimization_runs: '200',
sources: [],
libraries: [],
},
......@@ -100,6 +100,22 @@ export const DEFAULT_VALUES = {
},
};
export function getDefaultValues(method: SmartContractVerificationMethod, config: SmartContractVerificationConfig) {
const defaultValues = DEFAULT_VALUES[method];
if ('evm_version' in defaultValues) {
if (method === 'flattened-code' || method === 'multi-part') {
defaultValues.evm_version = config.solidity_evm_versions.find((value) => value === 'default') ? { label: 'default', value: 'default' } : null;
}
if (method === 'vyper-multi-part') {
defaultValues.evm_version = config.vyper_evm_versions.find((value) => value === 'default') ? { label: 'default', value: 'default' } : null;
}
}
return defaultValues;
}
export function isValidVerificationMethod(method?: string): method is SmartContractVerificationMethod {
return method && SUPPORTED_VERIFICATION_METHODS.includes(method) ? true : false;
}
......@@ -141,7 +157,7 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
const _data = data as FormFieldsStandardInput;
const body = new FormData();
body.set('compiler_version', _data.compiler?.value);
_data.compiler && body.set('compiler_version', _data.compiler.value);
body.set('contract_name', _data.name);
body.set('autodetect_constructor_args', String(Boolean(_data.autodetect_constructor_args)));
body.set('constructor_args', _data.constructor_args);
......@@ -163,8 +179,8 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
const _data = data as FormFieldsMultiPartFile;
const body = new FormData();
body.set('compiler_version', _data.compiler?.value);
body.set('evm_version', _data.evm_version?.value);
_data.compiler && body.set('compiler_version', _data.compiler.value);
_data.evm_version && body.set('evm_version', _data.evm_version.value);
body.set('is_optimization_enabled', String(Boolean(_data.is_optimization_enabled)));
_data.is_optimization_enabled && body.set('optimization_runs', _data.optimization_runs);
......@@ -190,8 +206,8 @@ export function prepareRequestBody(data: FormFields): FetchParams['body'] {
const _data = data as FormFieldsVyperMultiPartFile;
const body = new FormData();
body.set('compiler_version', _data.compiler?.value);
body.set('evm_version', _data.evm_version?.value);
_data.compiler && body.set('compiler_version', _data.compiler.value);
_data.evm_version && body.set('evm_version', _data.evm_version.value);
addFilesToFormData(body, _data.sources);
return body;
......
......@@ -3,7 +3,7 @@ import React, { useCallback } from 'react';
import type { CustomAbi } from 'types/api/account';
import AddressSnippet from 'ui/shared/AddressSnippet';
import ListItemMobile from 'ui/shared/ListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
interface Props {
......
import { Box, Icon } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react';
import type { DepositsItem } from 'types/api/deposits';
import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg';
import txIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs';
import AddressIcon from 'ui/shared/address/AddressIcon';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
type Props = { item: DepositsItem };
const DepositsListItem = ({ item }: Props) => {
const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
const items = [
{
name: 'L1 block No',
value: (
<LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height]', query: { height: item.l1_block_number.toString() } }) }
fontWeight={ 600 }
display="inline-flex"
>
<Icon as={ blockIcon } boxSize={ 6 } mr={ 1 }/>
{ item.l1_block_number }
</LinkExternal>
),
},
{
name: 'L2 txn hash',
value: (
<LinkInternal
href={ route({ pathname: '/tx/[hash]', query: { hash: item.l2_tx_hash } }) }
display="flex"
width="fit-content"
alignItems="center"
overflow="hidden"
w="100%"
>
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l2_tx_hash }/></Box>
</LinkInternal>
),
},
{
name: 'Age',
value: timeAgo,
},
{
name: 'L1 txn hash',
value: (
<LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
maxW="100%"
display="inline-flex"
overflow="hidden"
>
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box>
</LinkExternal>
),
},
{
name: 'L1 txn origin',
value: (
<LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/address/[hash]', query: { hash: item.l1_tx_origin } }) }
maxW="100%"
display="inline-flex"
overflow="hidden"
>
<AddressIcon address={{ hash: item.l1_tx_origin, is_contract: false, implementation_name: '' }} mr={ 2 }/>
<Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_origin }/></Box>
</LinkExternal>
),
},
{
name: 'Gas limit',
value: BigNumber(item.l2_tx_gas_limit).toFormat(),
},
];
return <ListItemMobileGrid items={ items } gridTemplateColumns="92px auto"/>;
};
export default DepositsListItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { DepositsItem } from 'types/api/deposits';
import { default as Thead } from 'ui/shared/TheadSticky';
import DepositsTableItem from './DepositsTableItem';
type Props = {
items: Array<DepositsItem>;
top: number;
}
const DepositsTable = ({ items, top }: Props) => {
return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }>
<Tr>
<Th>L1 block No</Th>
<Th>L2 txn hash</Th>
<Th>Age</Th>
<Th>L1 txn hash</Th>
<Th>L1 txn origin</Th>
<Th isNumeric>Gas limit</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item) => (
<DepositsTableItem key={ item.l2_tx_hash } item={ item }/>
)) }
</Tbody>
</Table>
);
};
export default DepositsTable;
import { Box, Td, Tr, Text, Icon } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react';
import type { DepositsItem } from 'types/api/deposits';
import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg';
import txIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs';
import AddressIcon from 'ui/shared/address/AddressIcon';
import HashStringShorten from 'ui/shared/HashStringShorten';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
type Props = { item: DepositsItem };
const WithdrawalsTableItem = ({ item }: Props) => {
const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
return (
<Tr>
<Td verticalAlign="middle" fontWeight={ 600 }>
<LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height]', query: { height: item.l1_block_number.toString() } }) }
fontWeight={ 600 }
display="inline-flex"
>
<Icon as={ blockIcon } boxSize={ 6 } mr={ 1 }/>
{ item.l1_block_number }
</LinkExternal>
</Td>
<Td verticalAlign="middle">
<LinkInternal
href={ route({ pathname: '/tx/[hash]', query: { hash: item.l2_tx_hash } }) }
display="flex"
width="fit-content"
alignItems="center"
overflow="hidden"
w="100%"
>
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShorten hash={ item.l2_tx_hash }/></Box>
</LinkInternal>
</Td>
<Td verticalAlign="middle" pr={ 12 }>
<Text variant="secondary">{ timeAgo }</Text>
</Td>
<Td verticalAlign="middle">
<LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
maxW="100%"
display="inline-flex"
overflow="hidden"
>
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShorten hash={ item.l1_tx_hash }/></Box>
</LinkExternal>
</Td>
<Td verticalAlign="middle">
<LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/address/[hash]', query: { hash: item.l1_tx_origin } }) }
maxW="100%"
display="inline-flex"
overflow="hidden"
>
<AddressIcon address={{ hash: item.l1_tx_origin, is_contract: false, implementation_name: '' }} mr={ 2 }/>
<Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShorten hash={ item.l1_tx_origin }/></Box>
</LinkExternal>
</Td>
<Td verticalAlign="middle" isNumeric>
<Text variant="secondary">{ BigNumber(item.l2_tx_gas_limit).toFormat() }</Text>
</Td>
</Tr>
);
};
export default WithdrawalsTableItem;
import { Box, Flex, Text, Icon } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { OutputRootsItem } from 'types/api/outputRoots';
import appConfig from 'configs/app/config';
import txIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
type Props = { item: OutputRootsItem };
const OutputRootsListItem = ({ item }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow();
const items = [
{
name: 'L2 output index',
value: item.l2_output_index,
},
{
name: 'Age',
value: timeAgo,
},
{
name: 'L2 block #',
value: (
<LinkInternal
display="flex"
width="fit-content"
alignItems="center"
href={ route({ pathname: '/block/[height]', query: { height: item.l2_block_number.toString() } }) }
>
{ item.l2_block_number }
</LinkInternal>
),
},
{
name: 'L1 txn hash',
value: (
<LinkExternal
maxW="100%"
display="inline-flex"
overflow="hidden"
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
>
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box>
</LinkExternal>
),
},
{
name: 'Output root',
value: (
<Flex overflow="hidden" whiteSpace="nowrap" alignItems="center" w="100%" justifyContent="space-between">
<Text variant="secondary" w="calc(100% - 24px)"><HashStringShortenDynamic hash={ item.output_root }/></Text>
<CopyToClipboard text={ item.output_root }/>
</Flex>
),
},
];
return <ListItemMobileGrid items={ items } gridTemplateColumns="100px auto"/>;
};
export default OutputRootsListItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { OutputRootsItem } from 'types/api/outputRoots';
import { default as Thead } from 'ui/shared/TheadSticky';
import OutputRootsTableItem from './OutputRootsTableItem';
type Props = {
items: Array<OutputRootsItem>;
top: number;
}
const OutputRootsTable = ({ items, top }: Props) => {
return (
<Table variant="simple" size="sm" minW="900px">
<Thead top={ top }>
<Tr>
<Th width="140px">L2 output index</Th>
<Th width="20%">Age</Th>
<Th width="20%">L2 block #</Th>
<Th width="30%">L1 txn hash</Th>
<Th width="30%">Output root</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item) => (
<OutputRootsTableItem key={ item.l2_output_index } item={ item }/>
)) }
</Tbody>
</Table>
);
};
export default OutputRootsTable;
import { Box, Flex, Td, Tr, Text, Icon } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { OutputRootsItem } from 'types/api/outputRoots';
import appConfig from 'configs/app/config';
import txIcon from 'icons/transactions.svg';
import txBatchIcon from 'icons/txBatch.svg';
import dayjs from 'lib/date/dayjs';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
type Props = { item: OutputRootsItem };
const OutputRootsTableItem = ({ item }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow();
return (
<Tr>
<Td verticalAlign="middle">
<Text>{ item.l2_output_index }</Text>
</Td>
<Td verticalAlign="middle">
<Text variant="secondary">{ timeAgo }</Text>
</Td>
<Td verticalAlign="middle">
<LinkInternal
fontWeight={ 600 }
display="flex"
width="fit-content"
alignItems="center"
href={ route({ pathname: '/block/[height]', query: { height: item.l2_block_number.toString() } }) }
>
<Icon as={ txBatchIcon } boxSize={ 6 } mr={ 1 }/>
{ item.l2_block_number }
</LinkInternal>
</Td>
<Td verticalAlign="middle" pr={ 12 }>
<Flex>
<LinkExternal
maxW="100%"
display="inline-flex"
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
>
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box>
</LinkExternal>
</Flex>
</Td>
<Td verticalAlign="middle">
<Flex overflow="hidden" whiteSpace="nowrap" w="100%" alignItems="center">
<Box w="calc(100% - 36px)"><HashStringShortenDynamic hash={ item.output_root }/></Box>
<CopyToClipboard text={ item.output_root } ml={ 2 }/>
</Flex>
</Td>
</Tr>
);
};
export default OutputRootsTableItem;
......@@ -5,12 +5,10 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import AddressesListItem from 'ui/addresses/AddressesListItem';
import AddressesTable from 'ui/addresses/AddressesTable';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
const PAGE_SIZE = 50;
......@@ -19,60 +17,50 @@ const Accounts = () => {
resourceName: 'addresses',
});
const content = (() => {
if (isError) {
return <DataFetchAlert/>;
}
const bar = isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
);
if (isLoading) {
return (
<>
{ bar }
<SkeletonList display={{ base: 'block', lg: 'none' }}/>
<SkeletonTable display={{ base: 'none', lg: 'block' }} columns={ [ '64px', '30%', '20%', '20%', '15%', '15%' ] }/>
</>
);
}
const pageStartIndex = (pagination.page - 1) * PAGE_SIZE + 1;
const actionBar = isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
);
return (
<>
{ bar }
<Hide below="lg" ssr={ false }>
<AddressesTable
top={ isPaginationVisible ? 80 : 0 }
items={ data.items }
totalSupply={ data.total_supply }
pageStartIndex={ pageStartIndex }
/>
</Hide>
<Show below="lg" ssr={ false }>
{ data.items.map((item, index) => {
return (
<AddressesListItem
key={ item.hash }
item={ item }
index={ pageStartIndex + index }
totalSupply={ data.total_supply }
/>
);
}) }
</Show>
</>
);
})();
const pageStartIndex = (pagination.page - 1) * PAGE_SIZE + 1;
const content = data?.items ? (
<>
<Hide below="lg" ssr={ false }>
<AddressesTable
top={ isPaginationVisible ? 80 : 0 }
items={ data.items }
totalSupply={ data.total_supply }
pageStartIndex={ pageStartIndex }
/>
</Hide>
<Show below="lg" ssr={ false }>
{ data.items.map((item, index) => {
return (
<AddressesListItem
key={ item.hash }
item={ item }
index={ pageStartIndex + index }
totalSupply={ data.total_supply }
/>
);
}) }
</Show>
</>
) : null;
return (
<Page>
<PageTitle text="Top accounts" withTextAd/>
{ content }
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '64px', '30%', '20%', '20%', '15%', '15%' ] }}
emptyText="There are no accounts."
content={ content }
actionBar={ actionBar }
/>
</Page>
);
};
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { data as depositsData } from 'mocks/deposits/deposits';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import Deposits from './Deposits';
const DEPOSITS_API_URL = buildApiUrl('deposits');
const DEPOSITS_COUNT_API_URL = buildApiUrl('deposits_count');
test('base view +@mobile', async({ mount, page }) => {
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200,
body: '',
}));
await page.route(DEPOSITS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(depositsData),
}));
await page.route(DEPOSITS_COUNT_API_URL, (route) => route.fulfill({
status: 200,
body: '3971111',
}));
const component = await mount(
<TestApp>
<Deposits/>
</TestApp>,
);
await expect(component.locator('main')).toHaveScreenshot();
});
import { Flex, Hide, Show, Skeleton, Text } from '@chakra-ui/react';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { rightLineArrow, nbsp } from 'lib/html-entities';
import DepositsListItem from 'ui/deposits/DepositsListItem';
import DepositsTable from 'ui/deposits/DepositsTable';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination';
const Deposits = () => {
const isMobile = useIsMobile();
const { data, isError, isLoading, isPaginationVisible, pagination } = useQueryWithPages({
resourceName: 'deposits',
});
const countersQuery = useApiQuery('deposits_count');
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>{ data.items.map((item => <DepositsListItem key={ item.l2_tx_hash } item={ item }/>)) }</Show>
<Hide below="lg" ssr={ false }><DepositsTable items={ data.items } top={ isPaginationVisible ? 80 : 0 }/></Hide>
</>
) : null;
const text = (() => {
if (countersQuery.isLoading) {
return (
<Skeleton
w={{ base: '100%', lg: '320px' }}
h="24px"
mb={{ base: 7, lg: isPaginationVisible ? 0 : 7 }}
mt={{ base: 1, lg: isPaginationVisible ? 0 : 7 }}
/>
);
}
if (countersQuery.isError) {
return null;
}
return (
<Text mb={{ base: 6, lg: isPaginationVisible ? 0 : 6 }} lineHeight={{ base: '24px', lg: '32px' }}>
A total of { countersQuery.data.toLocaleString('en') } deposits found
</Text>
);
})();
const actionBar = (
<>
{ (isMobile || !isPaginationVisible) && text }
{ isPaginationVisible && (
<ActionBar mt={ -6 }>
<Flex alignItems="center" justifyContent="space-between" w="100%">
{ !isMobile && text }
<Pagination ml="auto" { ...pagination }/>
</Flex>
</ActionBar>
) }
</>
);
return (
<Page>
<PageTitle text={ `Deposits (L1${ nbsp }${ rightLineArrow }${ nbsp }L2)` } withTextAd/>
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: Array(7).fill(`${ 100 / 7 }%`), skeletonDesktopMinW: '950px' }}
emptyText="There are no withdrawals."
content={ content }
actionBar={ actionBar }
/>
</Page>
);
};
export default Deposits;
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { outputRootsData } from 'mocks/outputRoots/outputRoots';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import OutputRoots from './OutputRoots';
const OUTPUT_ROOTS_API_URL = buildApiUrl('output_roots');
const OUTPUT_ROOTS_COUNT_API_URL = buildApiUrl('output_roots_count');
test('base view +@mobile', async({ mount, page }) => {
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200,
body: '',
}));
await page.route(OUTPUT_ROOTS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(outputRootsData),
}));
await page.route(OUTPUT_ROOTS_COUNT_API_URL, (route) => route.fulfill({
status: 200,
body: '9927',
}));
const component = await mount(
<TestApp>
<OutputRoots/>
</TestApp>,
);
await expect(component.locator('main')).toHaveScreenshot();
});
import { Flex, Hide, Show, Skeleton, Text } from '@chakra-ui/react';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import OutputRootsListItem from 'ui/outputRoots/OutputRootsListItem';
import OutputRootsTable from 'ui/outputRoots/OutputRootsTable';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination';
const OutputRoots = () => {
const isMobile = useIsMobile();
const { data, isError, isLoading, isPaginationVisible, pagination } = useQueryWithPages({
resourceName: 'output_roots',
});
const countersQuery = useApiQuery('output_roots_count');
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>{ data.items.map((item => <OutputRootsListItem key={ item.l2_output_index } item={ item }/>)) }</Show>
<Hide below="lg" ssr={ false }><OutputRootsTable items={ data.items } top={ isPaginationVisible ? 80 : 0 }/></Hide>
</>
) : null;
const text = (() => {
if (countersQuery.isLoading || isLoading) {
return (
<Skeleton
w={{ base: '100%', lg: '400px' }}
h={{ base: '48px', lg: '24px' }}
mb={{ base: 6, lg: isPaginationVisible ? 0 : 7 }}
mt={{ base: 0, lg: isPaginationVisible ? 0 : 7 }}
/>
);
}
if (countersQuery.isError || isError || data?.items.length === 0) {
return null;
}
return (
<Flex mb={{ base: 6, lg: isPaginationVisible ? 0 : 6 }} flexWrap="wrap">
L2 output index
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].l2_output_index } </Text>to
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].l2_output_index } </Text>
(total of { countersQuery.data.toLocaleString('en') } roots)
</Flex>
);
})();
const actionBar = (
<>
{ (isMobile || !isPaginationVisible) && text }
{ isPaginationVisible && (
<ActionBar mt={ -6 }>
<Flex alignItems="center" justifyContent="space-between" w="100%">
{ !isMobile && text }
<Pagination ml="auto" { ...pagination }/>
</Flex>
</ActionBar>
) }
</>
);
return (
<Page>
<PageTitle text="Output roots" withTextAd/>
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '140px', '20%', '20%', '30%', '30%' ] }}
emptyText="There are no output roots."
content={ content }
actionBar={ actionBar }
/>
</Page>
);
};
export default OutputRoots;
......@@ -21,8 +21,7 @@ const hooksConfig = {
};
// FIXME: idk why mobile test doesn't work (it's ok locally)
// test('base view +@mobile +@dark-mode', async({ mount, page }) => {
test('base view +@dark-mode', async({ mount, page }) => {
test('base view +@mobile +@dark-mode', async({ mount, page }) => {
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200,
body: '',
......
......@@ -45,7 +45,7 @@ const TransactionPageContent = () => {
.filter((explorer) => explorer.paths.tx)
.map((explorer) => {
const url = new URL(explorer.paths.tx + '/' + hash, explorer.baseUrl);
return <LinkExternal key={ explorer.baseUrl } href={ url.toString() }>{ `Open in ${ explorer.title }` }</LinkExternal>;
return <LinkExternal key={ explorer.baseUrl } href={ url.toString() }>Open in { explorer.title }</LinkExternal>;
});
const additionals = (
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { txnBatchesData } from 'mocks/txnBatches/txnBatches';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import TxnBatches from './TxnBatches';
const TXN_BATCHES_API_URL = buildApiUrl('txn_batches');
const TXN_BATCHES_COUNT_API_URL = buildApiUrl('txn_batches_count');
test('base view +@mobile', async({ mount, page }) => {
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200,
body: '',
}));
await page.route(TXN_BATCHES_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txnBatchesData),
}));
await page.route(TXN_BATCHES_COUNT_API_URL, (route) => route.fulfill({
status: 200,
body: '1235016',
}));
const component = await mount(
<TestApp>
<TxnBatches/>
</TestApp>,
);
await expect(component.locator('main')).toHaveScreenshot();
});
import { Flex, Hide, Show, Skeleton, Text } from '@chakra-ui/react';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { nbsp } from 'lib/html-entities';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination';
import TxnBatchesListItem from 'ui/txnBatches/TxnBatchesListItem';
import TxnBatchesTable from 'ui/txnBatches/TxnBatchesTable';
const TxnBatches = () => {
const isMobile = useIsMobile();
const { data, isError, isLoading, isPaginationVisible, pagination } = useQueryWithPages({
resourceName: 'txn_batches',
});
const countersQuery = useApiQuery('txn_batches_count');
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>{ data.items.map((item => <TxnBatchesListItem key={ item.l2_block_number } item={ item }/>)) }</Show>
<Hide below="lg" ssr={ false }><TxnBatchesTable items={ data.items } top={ isPaginationVisible ? 80 : 0 }/></Hide>
</>
) : null;
const text = (() => {
if (countersQuery.isLoading || isLoading) {
return (
<Skeleton
w={{ base: '100%', lg: '400px' }}
h={{ base: '48px', lg: '24px' }}
mb={{ base: 6, lg: isPaginationVisible ? 0 : 7 }}
mt={{ base: 0, lg: isPaginationVisible ? 0 : 7 }}
/>
);
}
if (countersQuery.isError || isError || data.items.length === 0) {
return null;
}
return (
<Flex mb={{ base: 6, lg: isPaginationVisible ? 0 : 6 }} flexWrap="wrap">
Tx batch (L2 block)
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].l2_block_number } </Text>to
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].l2_block_number } </Text>
(total of { countersQuery.data.toLocaleString('en') } batches)
</Flex>
);
})();
const actionBar = (
<>
{ (isMobile || !isPaginationVisible) && text }
{ isPaginationVisible && (
<ActionBar mt={ -6 }>
<Flex alignItems="center" justifyContent="space-between" w="100%">
{ !isMobile && text }
<Pagination ml="auto" { ...pagination }/>
</Flex>
</ActionBar>
) }
</>
);
return (
<Page>
<PageTitle text={ `Tx batches (L2${ nbsp }blocks)` } withTextAd/>
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '170px', '170px', '160px', '100%', '150px' ] }}
emptyText="There are no tx batches."
content={ content }
actionBar={ actionBar }
/>
</Page>
);
};
export default TxnBatches;
......@@ -2,6 +2,7 @@ import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as textAdMock from 'mocks/ad/textAd';
import { verifiedContractsCountersMock } from 'mocks/contracts/counters';
import * as verifiedContractsMock from 'mocks/contracts/index';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
......@@ -9,6 +10,7 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import VerifiedContracts from './VerifiedContracts';
const VERIFIED_CONTRACTS_API_URL = buildApiUrl('verified_contracts');
const VERIFIED_CONTRACTS_COUNTERS_API_URL = buildApiUrl('verified_contracts_counters');
test.beforeEach(async({ page }) => {
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
......@@ -28,6 +30,10 @@ test('base view +@mobile', async({ mount, page }) => {
status: 200,
body: JSON.stringify(verifiedContractsMock.baseResponse),
}));
await page.route(VERIFIED_CONTRACTS_COUNTERS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(verifiedContractsCountersMock),
}));
const component = await mount(
<TestApp>
......
import { Box, Flex, Hide, Show, Text } from '@chakra-ui/react';
import { Box, Hide, HStack, Show } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { VerifiedContractsFilters } from 'types/api/contracts';
import useDebounce from 'lib/hooks/useDebounce';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay';
import FilterInput from 'ui/shared/filters/FilterInput';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import Sort from 'ui/shared/sort/Sort';
import type { SortField, Sort as TSort } from 'ui/verifiedContracts/utils';
import { SORT_OPTIONS, sortFn, getNextSortValue } from 'ui/verifiedContracts/utils';
import VerifiedContractsCounters from 'ui/verifiedContracts/VerifiedContractsCounters';
import VerifiedContractsFilter from 'ui/verifiedContracts/VerifiedContractsFilter';
import VerifiedContractsList from 'ui/verifiedContracts/VerifiedContractsList';
import VerifiedContractsTable from 'ui/verifiedContracts/VerifiedContractsTable';
......@@ -31,6 +30,8 @@ const VerifiedContracts = () => {
const debouncedSearchTerm = useDebounce(searchTerm || '', 300);
const isMobile = useIsMobile();
const { isError, isLoading, data, isPaginationVisible, pagination, onFilterChange } = useQueryWithPages({
resourceName: 'verified_contracts',
filters: { q: debouncedSearchTerm, filter: type },
......@@ -82,72 +83,55 @@ const VerifiedContracts = () => {
/>
);
const bar = (
const actionBar = (
<>
<Show below="lg" ssr={ false }>
<Flex columnGap={ 3 } mb={ 6 }>
{ typeFilter }
{ sortButton }
{ filterInput }
</Flex>
</Show>
<ActionBar mt={ -6 }>
<Hide below="lg" ssr={ false }>
<Flex columnGap={ 3 }>
<HStack spacing={ 3 } mb={ 6 } display={{ base: 'flex', lg: 'none' }}>
{ typeFilter }
{ sortButton }
{ filterInput }
</HStack>
{ (!isMobile || isPaginationVisible) && (
<ActionBar mt={ -6 }>
<HStack spacing={ 3 } display={{ base: 'none', lg: 'flex' }}>
{ typeFilter }
{ filterInput }
</Flex>
</Hide>
{ isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
</ActionBar>
</HStack>
{ isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
</ActionBar>
) }
</>
);
const content = (() => {
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<>
<Show below="lg" ssr={ false }>
<SkeletonList/>
</Show>
<Hide below="lg" ssr={ false }>
<SkeletonTable columns={ [ '50%', '130px', '130px', '50%', '80px', '110px' ] }/>
</Hide>
</>
);
}
if (data.items.length === 0 && !searchTerm && !type) {
return <Text as="span">There are no verified contracts</Text>;
}
const sortedData = data?.items.slice().sort(sortFn(sort));
if (data.items.length === 0) {
return <EmptySearchResult text={ `Couldn${ apos }t find any contract that matches your query.` }/>;
}
const sortedData = data.items.slice().sort(sortFn(sort));
return (
<>
<Show below="lg" ssr={ false }>
<VerifiedContractsList data={ sortedData }/>
</Show>
<Hide below="lg" ssr={ false }>
<VerifiedContractsTable data={ sortedData } sort={ sort } onSortToggle={ handleSortToggle }/>
</Hide>
</>
);
})();
const content = sortedData ? (
<>
<Show below="lg" ssr={ false }>
<VerifiedContractsList data={ sortedData }/>
</Show>
<Hide below="lg" ssr={ false }>
<VerifiedContractsTable data={ sortedData } sort={ sort } onSortToggle={ handleSortToggle }/>
</Hide>
</>
) : null;
return (
<Box>
<PageTitle text="Verified contracts" withTextAd/>
{ bar }
{ content }
<VerifiedContractsCounters/>
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '50%', '130px', '130px', '50%', '80px', '110px' ] }}
emptyText="There are no verified contracts."
filterProps={{
emptyFilteredText: `Couldn${ apos }t find any contract that matches your query.`,
hasActiveFilters: Boolean(searchTerm || type),
}}
content={ content }
actionBar={ actionBar }
/>
</Box>
);
};
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { data as withdrawalsData } from 'mocks/withdrawals/withdrawals';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import Withdrawals from './Withdrawals';
const WITHDRAWALS_API_URL = buildApiUrl('withdrawals');
const WITHDRAWALS_COUNT_API_URL = buildApiUrl('withdrawals_count');
test('base view +@mobile', async({ mount, page }) => {
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200,
body: '',
}));
await page.route(WITHDRAWALS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(withdrawalsData),
}));
await page.route(WITHDRAWALS_COUNT_API_URL, (route) => route.fulfill({
status: 200,
body: '397',
}));
const component = await mount(
<TestApp>
<Withdrawals/>
</TestApp>,
);
await expect(component.locator('main')).toHaveScreenshot();
});
import { Flex, Hide, Show, Skeleton, Text } from '@chakra-ui/react';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { rightLineArrow, nbsp } from 'lib/html-entities';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination';
import WithdrawalsListItem from 'ui/withdrawals/WithdrawalsListItem';
import WithdrawalsTable from 'ui/withdrawals/WithdrawalsTable';
const Withdrawals = () => {
const isMobile = useIsMobile();
const { data, isError, isLoading, isPaginationVisible, pagination } = useQueryWithPages({
resourceName: 'withdrawals',
});
const countersQuery = useApiQuery('withdrawals_count');
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>{ data.items.map((item => <WithdrawalsListItem key={ item.l2_tx_hash } item={ item }/>)) }</Show>
<Hide below="lg" ssr={ false }><WithdrawalsTable items={ data.items } top={ isPaginationVisible ? 80 : 0 }/></Hide>
</>
) : null;
const text = (() => {
if (countersQuery.isLoading) {
return (
<Skeleton
w={{ base: '100%', lg: '320px' }}
h="24px"
mb={{ base: 6, lg: isPaginationVisible ? 0 : 7 }}
mt={{ base: 0, lg: isPaginationVisible ? 0 : 7 }}
/>
);
}
if (countersQuery.isError) {
return null;
}
return (
<Text mb={{ base: 6, lg: isPaginationVisible ? 0 : 6 }} lineHeight={{ base: '24px', lg: '32px' }}>
A total of { countersQuery.data.toLocaleString('en') } withdrawals found
</Text>
);
})();
const actionBar = (
<>
{ (isMobile || !isPaginationVisible) && text }
{ isPaginationVisible && (
<ActionBar mt={ -6 }>
<Flex alignItems="center" justifyContent="space-between" w="100%">
{ !isMobile && text }
<Pagination ml="auto" { ...pagination }/>
</Flex>
</ActionBar>
) }
</>
);
return (
<Page>
<PageTitle text={ `Withdrawals (L2${ nbsp }${ rightLineArrow }${ nbsp }L1)` } withTextAd/>
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: Array(7).fill(`${ 100 / 7 }%`), skeletonDesktopMinW: '950px' }}
emptyText="There are no withdrawals."
content={ content }
actionBar={ actionBar }
/>
</Page>
);
};
export default Withdrawals;
......@@ -4,7 +4,7 @@ import React, { useCallback } from 'react';
import type { AddressTag } from 'types/api/account';
import AddressSnippet from 'ui/shared/AddressSnippet';
import ListItemMobile from 'ui/shared/ListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
interface Props {
......
......@@ -3,7 +3,7 @@ import React, { useCallback } from 'react';
import type { TransactionTag } from 'types/api/account';
import ListItemMobile from 'ui/shared/ListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import TransactionSnippet from 'ui/shared/TransactionSnippet';
......
......@@ -4,7 +4,7 @@ import React, { useCallback } from 'react';
import type { PublicTag } from 'types/api/account';
import AddressSnippet from 'ui/shared/AddressSnippet';
import ListItemMobile from 'ui/shared/ListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
......
......@@ -13,7 +13,7 @@ import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TokenLogo from 'ui/shared/TokenLogo';
interface Props {
......
import { chakra, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type ReactAce from 'react-ace/lib/ace';
interface Props {
id: string;
value: string;
className?: string;
}
const CodeEditorBase = chakra(({ id, value, className }: Props) => {
const [ AceEditor, setAceEditor ] = React.useState<{default: typeof ReactAce} | null>(null);
React.useEffect(() => {
const load = async() => {
const component = await import('react-ace');
await import('ace-builds/src-noconflict/mode-csharp');
await import('ace-builds/src-noconflict/theme-tomorrow');
await import('ace-builds/src-noconflict/theme-tomorrow_night');
await import('ace-builds/src-noconflict/ext-language_tools');
setAceEditor(component);
};
load();
return () => {
setAceEditor(null);
};
}, []);
const theme = useColorModeValue('tomorrow', 'tomorrow_night');
if (!AceEditor) {
return null;
}
return (
<AceEditor.default
className={ className }
mode="csharp" // TODO need to find mode for solidity
theme={ theme }
value={ value }
name={ id }
editorProps={{ $blockScrolling: true }}
readOnly
width="100%"
showPrintMargin={ false }
maxLines={ 25 }
/>
);
});
const CodeEditor = ({ id, value }: Props) => {
// see theme/components/Textarea.ts variantFilledInactive
const bgColor = useColorModeValue('#f5f5f6', '#1a1b1b');
const gutterBgColor = useColorModeValue('gray.100', '#25282c');
return (
<CodeEditorBase
id={ id }
value={ value }
bgColor={ bgColor }
borderRadius="md"
overflow="hidden"
sx={{
'.ace_gutter': {
backgroundColor: gutterBgColor,
},
}}
/>
);
};
export default React.memo(CodeEditor);
......@@ -4,10 +4,10 @@ import React, { useEffect, useState } from 'react';
import CopyIcon from 'icons/copy.svg';
const CopyToClipboard = ({ text, className }: {text: string; className?: string}) => {
const { hasCopied, onCopy } = useClipboard(text, 3000);
const { hasCopied, onCopy } = useClipboard(text, 1000);
const [ copied, setCopied ] = useState(false);
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
const { isOpen, onOpen, onToggle, onClose } = useDisclosure();
const { isOpen, onOpen, onClose } = useDisclosure();
useEffect(() => {
if (hasCopied) {
......@@ -17,13 +17,8 @@ const CopyToClipboard = ({ text, className }: {text: string; className?: string}
}
}, [ hasCopied ]);
const handleClick = React.useCallback(() => {
onToggle();
onCopy();
}, [ onCopy, onToggle ]);
return (
<Tooltip label={ copied ? 'Copied' : 'Copy to clipboard' } isOpen={ isOpen }>
<Tooltip label={ copied ? 'Copied' : 'Copy to clipboard' } isOpen={ isOpen || copied }>
<IconButton
aria-label="copy"
icon={ <CopyIcon/> }
......@@ -32,7 +27,7 @@ const CopyToClipboard = ({ text, className }: {text: string; className?: string}
variant="simple"
display="inline-block"
flexShrink={ 0 }
onClick={ handleClick }
onClick={ onCopy }
className={ className }
onMouseEnter={ onOpen }
onMouseLeave={ onClose }
......
import { Box, Text, chakra } from '@chakra-ui/react';
import React from 'react';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import DataFetchAlert from './DataFetchAlert';
import SkeletonList from './skeletons/SkeletonList';
import SkeletonTable from './skeletons/SkeletonTable';
type SkeletonProps =
{ customSkeleton: React.ReactNode } |
{
skeletonDesktopColumns: Array<string>;
isLongSkeleton?: boolean;
skeletonDesktopMinW?: string;
}
type FilterProps = {
hasActiveFilters: boolean;
emptyFilteredText: string;
};
type Props = {
isError: boolean;
isLoading: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
items?: Array<any>;
emptyText: string;
actionBar?: React.ReactNode;
content: React.ReactNode;
className?: string;
skeletonProps: SkeletonProps;
filterProps?: FilterProps;
}
const DataListDisplay = (props: Props) => {
if (props.isError) {
return <DataFetchAlert/>;
}
if (props.isLoading) {
return (
<>
{ props.actionBar }
{ 'customSkeleton' in props.skeletonProps && props.skeletonProps.customSkeleton }
{ 'skeletonDesktopColumns' in props.skeletonProps && (
<>
<SkeletonList display={{ base: 'block', lg: 'none' }}/>
<SkeletonTable
display={{ base: 'none', lg: 'block' }}
columns={ props.skeletonProps.skeletonDesktopColumns || [] }
isLong={ props.skeletonProps.isLongSkeleton }
minW={ props.skeletonProps.skeletonDesktopMinW }
/>
</>
) }
</>
);
}
if (props.filterProps?.hasActiveFilters && !props.items?.length) {
return (
<>
{ props.actionBar }
<EmptySearchResult text={ props.filterProps.emptyFilteredText }/>
</>
);
}
if (!props.items?.length) {
return <Text as="span">{ props.emptyText }</Text>;
}
return (
<Box className={ props.className }>
{ props.actionBar }
{ props.content }
</Box>
);
};
export default chakra(DataListDisplay);
......@@ -25,7 +25,7 @@ interface AsyncSelectProps extends AsyncProps<Option, boolean, GroupBase<Option>
type Props = RegularSelectProps | AsyncSelectProps;
const FancySelect = (props: Props) => {
const FancySelect = (props: Props, ref: React.LegacyRef<HTMLDivElement>) => {
const menuZIndex = useToken('zIndices', 'dropdown');
const { colorMode } = useColorMode();
......@@ -42,6 +42,7 @@ const FancySelect = (props: Props) => {
variant="floating"
size={ props.size || 'md' }
isRequired={ props.isRequired }
ref={ ref }
{ ...(props.error ? { 'aria-invalid': true } : {}) }
{ ...(props.isDisabled ? { 'aria-disabled': true } : {}) }
{ ...(props.value ? { 'data-active': true } : {}) }
......
import { Grid, Text, chakra } from '@chakra-ui/react';
import { motion } from 'framer-motion';
import React from 'react';
type Item = {
name: string;
value: string | React.ReactNode;
}
interface Props {
items: Array<Item>;
className?: string;
isAnimated?: boolean;
}
const ListItemMobileGrid = ({ isAnimated, items, className }: Props) => {
return (
<Grid
as={ motion.div }
w="100%"
initial={ isAnimated ? { opacity: 0, scale: 0.97 } : { opacity: 1, scale: 1 } }
animate={{ opacity: 1, scale: 1 }}
transitionDuration="normal"
transitionTimingFunction="linear"
rowGap={ 4 }
columnGap={ 2 }
gridTemplateColumns="max-content auto"
paddingY={ 6 }
borderColor="divider"
borderTopWidth="1px"
_last={{
borderBottomWidth: '1px',
}}
className={ className }
fontSize="sm"
>
{ items.map(item => Boolean(item.value) && (
<>
<Text >{ item.name }</Text>
{ typeof item.value === 'string' ? <Text variant="secondary">{ item.value }</Text> : item.value }
</>
)) }
</Grid>
);
};
export default chakra(ListItemMobileGrid);
......@@ -11,7 +11,7 @@ import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag';
import ListItemMobile from 'ui/shared/ListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
......
......@@ -50,7 +50,7 @@ const AddressLink = (props: Props) => {
} else if (type === 'block') {
url = route({ pathname: '/block/[height]', query: { height: props.blockHeight } });
} else if (type === 'address_token') {
url = route({ pathname: '/address/[hash]', query: { hash, tab: 'token_transfers', token_hash: props.tokenHash, scroll_to_tabs: 'true' } });
url = route({ pathname: '/address/[hash]', query: { hash, tab: 'token_transfers', token: props.tokenHash, scroll_to_tabs: 'true' } });
} else {
url = route({ pathname: '/address/[hash]', query: { hash } });
}
......
import type { SystemStyleObject } from '@chakra-ui/react';
import { Box, useColorMode, Flex } from '@chakra-ui/react';
import type { EditorProps } from '@monaco-editor/react';
import MonacoEditor from '@monaco-editor/react';
import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import React from 'react';
import type { File, Monaco } from './types';
import useClientRect from 'lib/hooks/useClientRect';
import useIsMobile from 'lib/hooks/useIsMobile';
import isMetaKey from 'lib/isMetaKey';
import CodeEditorBreadcrumbs from './CodeEditorBreadcrumbs';
import CodeEditorLoading from './CodeEditorLoading';
import CodeEditorSideBar, { CONTAINER_WIDTH as SIDE_BAR_WIDTH } from './CodeEditorSideBar';
import CodeEditorTabs from './CodeEditorTabs';
import addFileImportDecorations from './utils/addFileImportDecorations';
import getFullPathOfImportedFile from './utils/getFullPathOfImportedFile';
import * as themes from './utils/themes';
import useThemeColors from './utils/useThemeColors';
const EDITOR_OPTIONS: EditorProps['options'] = {
readOnly: true,
minimap: { enabled: false },
scrollbar: {
alwaysConsumeMouseWheel: true,
},
dragAndDrop: false,
};
const TABS_HEIGHT = 35;
const BREADCRUMBS_HEIGHT = 22;
const EDITOR_HEIGHT = 500;
interface Props {
data: Array<File>;
}
const CodeEditor = ({ data }: Props) => {
const [ instance, setInstance ] = React.useState<Monaco | undefined>();
const [ editor, setEditor ] = React.useState<monaco.editor.IStandaloneCodeEditor | undefined>();
const [ index, setIndex ] = React.useState(0);
const [ tabs, setTabs ] = React.useState([ data[index].file_path ]);
const [ isMetaPressed, setIsMetaPressed ] = React.useState(false);
const [ containerRect, containerNodeRef ] = useClientRect<HTMLDivElement>();
const { colorMode } = useColorMode();
const isMobile = useIsMobile();
const themeColors = useThemeColors();
const editorWidth = containerRect ? containerRect.width - (isMobile ? 0 : SIDE_BAR_WIDTH) : 0;
React.useEffect(() => {
instance?.editor.setTheme(colorMode === 'light' ? 'blockscout-light' : 'blockscout-dark');
}, [ colorMode, instance?.editor ]);
const handleEditorDidMount = React.useCallback((editor: monaco.editor.IStandaloneCodeEditor, monaco: Monaco) => {
setInstance(monaco);
setEditor(editor);
monaco.editor.defineTheme('blockscout-light', themes.light);
monaco.editor.defineTheme('blockscout-dark', themes.dark);
monaco.editor.setTheme(colorMode === 'light' ? 'blockscout-light' : 'blockscout-dark');
const loadedModels = monaco.editor.getModels();
const loadedModelsPaths = loadedModels.map((model) => model.uri.path);
const newModels = data.slice(1)
.filter((file) => !loadedModelsPaths.includes(file.file_path))
.map((file) => monaco.editor.createModel(file.source_code, 'sol', monaco.Uri.parse(file.file_path)));
loadedModels.concat(newModels).forEach(addFileImportDecorations);
editor.addAction({
id: 'close-tab',
label: 'Close current tab',
keybindings: [
monaco.KeyMod.Alt | monaco.KeyCode.KeyW,
],
contextMenuGroupId: 'navigation',
contextMenuOrder: 1.7,
run: function(editor) {
const model = editor.getModel();
const path = model?.uri.path;
if (path) {
handleTabClose(path, true);
}
},
});
// componentDidMount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ ]);
const handleSelectFile = React.useCallback((index: number, lineNumber?: number) => {
setIndex(index);
setTabs((prev) => prev.some((item) => item === data[index].file_path) ? prev : ([ ...prev, data[index].file_path ]));
if (lineNumber !== undefined && !Object.is(lineNumber, NaN)) {
window.setTimeout(() => {
editor?.revealLineInCenter(lineNumber);
}, 0);
}
editor?.focus();
}, [ data, editor ]);
const handleTabSelect = React.useCallback((path: string) => {
const index = data.findIndex((item) => item.file_path === path);
if (index > -1) {
setIndex(index);
}
}, [ data ]);
const handleTabClose = React.useCallback((path: string, _isActive?: boolean) => {
setTabs((prev) => {
if (prev.length > 1) {
const tabIndex = prev.findIndex((item) => item === path);
const isActive = _isActive !== undefined ? _isActive : data[index].file_path === path;
if (isActive) {
const nextActiveIndex = data.findIndex((item) => item.file_path === prev[(tabIndex === 0 ? 1 : tabIndex - 1)]);
setIndex(nextActiveIndex);
}
return prev.filter((item) => item !== path);
}
return prev;
});
}, [ data, index ]);
const handleClick = React.useCallback((event: React.MouseEvent) => {
if (!isMetaPressed && !isMobile) {
return;
}
const target = event.target as HTMLSpanElement;
const isImportLink = target.classList.contains('import-link');
if (isImportLink) {
const path = target.innerText;
const fullPath = getFullPathOfImportedFile(data[index].file_path, path);
const fileIndex = data.findIndex((file) => file.file_path === fullPath);
if (fileIndex > -1) {
event.stopPropagation();
handleSelectFile(fileIndex);
}
}
}, [ data, handleSelectFile, index, isMetaPressed, isMobile ]);
const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => {
isMetaKey(event) && setIsMetaPressed(true);
}, []);
const handleKeyUp = React.useCallback(() => {
setIsMetaPressed(false);
}, []);
const containerSx: SystemStyleObject = React.useMemo(() => ({
'.editor-container': {
position: 'absolute',
top: 0,
left: 0,
width: `${ editorWidth }px`,
height: '100%',
},
'.highlight': {
backgroundColor: themeColors['custom.findMatchHighlightBackground'],
},
'&&.meta-pressed .import-link': {
_hover: {
color: themeColors['custom.fileLink.hoverForeground'],
textDecoration: 'underline',
cursor: 'pointer',
},
},
}), [ editorWidth, themeColors ]);
if (data.length === 1) {
return (
<Box overflow="hidden" borderRadius="md" height={ `${ EDITOR_HEIGHT }px` }>
<MonacoEditor
language="sol"
path={ data[index].file_path }
defaultValue={ data[index].source_code }
options={ EDITOR_OPTIONS }
onMount={ handleEditorDidMount }
loading={ <CodeEditorLoading/> }
/>
</Box>
);
}
return (
<Flex
className={ isMetaPressed ? 'meta-pressed' : undefined }
overflow="hidden"
borderRadius="md"
width="100%"
height={ `${ EDITOR_HEIGHT + TABS_HEIGHT + BREADCRUMBS_HEIGHT }px` }
position="relative"
ref={ containerNodeRef }
sx={ containerSx }
onClick={ handleClick }
onKeyDown={ handleKeyDown }
onKeyUp={ handleKeyUp }
>
<Box flexGrow={ 1 }>
<CodeEditorTabs tabs={ tabs } activeTab={ data[index].file_path } onTabSelect={ handleTabSelect } onTabClose={ handleTabClose }/>
<CodeEditorBreadcrumbs path={ data[index].file_path }/>
<MonacoEditor
className="editor-container"
height={ `${ EDITOR_HEIGHT }px` }
language="sol"
path={ data[index].file_path }
defaultValue={ data[index].source_code }
options={ EDITOR_OPTIONS }
onMount={ handleEditorDidMount }
loading={ <CodeEditorLoading/> }
/>
</Box>
<CodeEditorSideBar
data={ data }
onFileSelect={ handleSelectFile }
monaco={ instance }
editor={ editor }
selectedFile={ data[index].file_path }
/>
</Flex>
);
};
export default React.memo(CodeEditor);
import { Flex, Box } from '@chakra-ui/react';
import React from 'react';
import stripLeadingSlash from 'lib/stripLeadingSlash';
import useThemeColors from 'ui/shared/monaco/utils/useThemeColors';
interface Props {
path: string;
}
const CodeEditorBreadcrumbs = ({ path }: Props) => {
const chunks = stripLeadingSlash(path).split('/');
const themeColors = useThemeColors();
return (
<Flex
color={ themeColors['breadcrumbs.foreground'] }
bgColor={ themeColors['editor.background'] }
pl="16px"
pr="8px"
flexWrap="wrap"
fontSize="13px"
lineHeight="22px"
alignItems="center"
>
{ chunks.map((chunk, index) => {
return (
<React.Fragment key={ index }>
{ index !== 0 && (
<Box
className="codicon codicon-breadcrumb-separator"
boxSize="16px"
_before={{
content: '"\\eab6"',
}}/>
) }
<Box>{ chunk }</Box>
</React.Fragment>
);
}) }
</Flex>
);
};
export default React.memo(CodeEditorBreadcrumbs);
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { File } from './types';
import CodeEditorFileTree from './CodeEditorFileTree';
import CoderEditorCollapseButton from './CoderEditorCollapseButton';
import composeFileTree from './utils/composeFileTree';
interface Props {
data: Array<File>;
onFileSelect: (index: number) => void;
selectedFile: string;
isActive: boolean;
setActionBarRenderer: React.Dispatch<React.SetStateAction<(() => JSX.Element) | undefined>>;
}
const CodeEditorFileExplorer = ({ data, onFileSelect, selectedFile, isActive, setActionBarRenderer }: Props) => {
const [ key, setKey ] = React.useState(0);
const tree = React.useMemo(() => {
return composeFileTree(data);
}, [ data ]);
const handleCollapseButtonClick = React.useCallback(() => {
setKey((prev) => prev + 1);
}, []);
const renderActionBar = React.useCallback(() => {
return (
<CoderEditorCollapseButton onClick={ handleCollapseButtonClick } label="Collapse folders"/>
);
}, [ handleCollapseButtonClick ]);
const handleFileClick = React.useCallback((event: React.MouseEvent) => {
const filePath = (event.currentTarget as HTMLDivElement).getAttribute('data-file-path');
const fileIndex = data.findIndex((item) => item.file_path === filePath);
if (fileIndex > -1) {
onFileSelect(fileIndex);
}
}, [ data, onFileSelect ]);
React.useEffect(() => {
isActive && setActionBarRenderer(() => renderActionBar);
}, [ isActive, renderActionBar, setActionBarRenderer ]);
return (
<Box>
<CodeEditorFileTree key={ key } tree={ tree } onItemClick={ handleFileClick } isCollapsed={ key > 0 } selectedFile={ selectedFile }/>
</Box>
);
};
export default React.memo(CodeEditorFileExplorer);
import type { ChakraProps } from '@chakra-ui/react';
import { Box, Accordion, AccordionButton, AccordionItem, AccordionPanel, Icon, chakra } from '@chakra-ui/react';
import React from 'react';
import type { FileTree } from './types';
import iconFile from './icons/file.svg';
import iconFolderOpen from './icons/folder-open.svg';
import iconFolder from './icons/folder.svg';
import iconSolidity from './icons/solidity.svg';
import useThemeColors from './utils/useThemeColors';
interface Props {
tree: FileTree;
level?: number;
isCollapsed?: boolean;
onItemClick: (event: React.MouseEvent) => void;
selectedFile: string;
}
const CodeEditorFileTree = ({ tree, level = 0, onItemClick, isCollapsed, selectedFile }: Props) => {
const itemProps: ChakraProps = {
borderWidth: '0px',
cursor: 'pointer',
lineHeight: '22px',
_last: {
borderBottomWidth: '0px',
},
};
const themeColors = useThemeColors();
return (
<Accordion allowMultiple defaultIndex={ isCollapsed ? undefined : tree.map((item, index) => index) } reduceMotion>
{
tree.map((leaf, index) => {
const leafName = <chakra.span overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">{ leaf.name }</chakra.span>;
if ('children' in leaf) {
return (
<AccordionItem key={ index } { ...itemProps }>
{ ({ isExpanded }) => (
<>
<AccordionButton
pr="8px"
py="0"
pl={ `${ 8 + 8 * level }px` }
_hover={{ bgColor: themeColors['custom.list.hoverBackground'] }}
fontSize="13px"
lineHeight="22px"
h="22px"
transitionDuration="0"
>
<Box
className="codicon codicon-tree-item-expanded"
transform={ isExpanded ? 'rotate(0deg)' : 'rotate(-90deg)' }
boxSize="16px"
mr="2px"
/>
<Icon as={ isExpanded ? iconFolderOpen : iconFolder } boxSize="16px" mr="4px"/>
{ leafName }
</AccordionButton>
<AccordionPanel p="0">
<CodeEditorFileTree
tree={ leaf.children }
level={ level + 1 }
onItemClick={ onItemClick }
isCollapsed={ isCollapsed }
selectedFile={ selectedFile }
/>
</AccordionPanel>
</>
) }
</AccordionItem>
);
}
const icon = /.sol|.yul|.vy$/.test(leaf.name) ? iconSolidity : iconFile;
return (
<AccordionItem
key={ index }
{ ...itemProps }
pl={ `${ 26 + (level * 8) }px` }
pr="8px"
onClick={ onItemClick }
data-file-path={ leaf.file_path }
display="flex"
alignItems="center"
overflow="hidden"
_hover={{
bgColor: selectedFile === leaf.file_path ? themeColors['list.inactiveSelectionBackground'] : themeColors['custom.list.hoverBackground'],
}}
bgColor={ selectedFile === leaf.file_path ? themeColors['list.inactiveSelectionBackground'] : 'none' }
>
<Icon as={ icon } boxSize="16px" mr="4px"/>
{ leafName }
</AccordionItem>
);
})
}
</Accordion>
);
};
export default React.memo(CodeEditorFileTree);
import { Center } from '@chakra-ui/react';
import React from 'react';
import ContentLoader from 'ui/shared/ContentLoader';
import useThemeColors from './utils/useThemeColors';
const CodeEditorLoading = () => {
const themeColors = useThemeColors();
return (
<Center bgColor={ themeColors['editor.background'] } w="100%" h="100%">
<ContentLoader/>
</Center>
);
};
export default React.memo(CodeEditorLoading);
import type { ChakraProps } from '@chakra-ui/react';
import { Accordion, Box, Input, InputGroup, InputRightElement, useBoolean } from '@chakra-ui/react';
import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import React from 'react';
import type { File, Monaco, SearchResult } from './types';
import useDebounce from 'lib/hooks/useDebounce';
import CodeEditorSearchSection from './CodeEditorSearchSection';
import CoderEditorCollapseButton from './CoderEditorCollapseButton';
import useThemeColors from './utils/useThemeColors';
interface Props {
data: Array<File>;
monaco: Monaco | undefined;
onFileSelect: (index: number, lineNumber?: number) => void;
isInputStuck: boolean;
isActive: boolean;
setActionBarRenderer: React.Dispatch<React.SetStateAction<(() => JSX.Element) | undefined>>;
defaultValue: string;
}
const CodeEditorSearch = ({ monaco, data, onFileSelect, isInputStuck, isActive, setActionBarRenderer, defaultValue }: Props) => {
const [ searchTerm, changeSearchTerm ] = React.useState('');
const [ searchResults, setSearchResults ] = React.useState<Array<SearchResult>>([]);
const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>([]);
const [ isMatchCase, setMatchCase ] = useBoolean();
const [ isMatchWholeWord, setMatchWholeWord ] = useBoolean();
const [ isMatchRegex, setMatchRegex ] = useBoolean();
const decorations = React.useRef<Record<string, Array<string>>>({});
const themeColors = useThemeColors();
const debouncedSearchTerm = useDebounce(searchTerm, 300);
React.useEffect(() => {
changeSearchTerm(defaultValue);
}, [ defaultValue ]);
React.useEffect(() => {
if (!monaco) {
return;
}
if (!debouncedSearchTerm) {
setSearchResults([]);
}
const models = monaco.editor.getModels();
const matches = models.map((model) => model.findMatches(debouncedSearchTerm, false, isMatchRegex, isMatchCase, isMatchWholeWord ? 'true' : null, false));
models.forEach((model, index) => {
const filePath = model.uri.path;
const prevDecorations = decorations.current[filePath] || [];
const newDecorations: Array<monaco.editor.IModelDeltaDecoration> = matches[index].map(({ range }) => ({ range, options: { className: 'highlight' } }));
const newDecorationsIds = model.deltaDecorations(prevDecorations, newDecorations);
decorations.current[filePath] = newDecorationsIds;
});
const result: Array<SearchResult> = matches
.map((match, index) => {
const model = models[index];
return {
file_path: model.uri.path,
matches: match.map(({ range }) => ({ ...range, lineContent: model.getLineContent(range.startLineNumber) })),
};
})
.filter(({ matches }) => matches.length > 0);
setSearchResults(result.length > 0 ? result : []);
}, [ debouncedSearchTerm, isMatchCase, isMatchRegex, isMatchWholeWord, monaco ]);
React.useEffect(() => {
setExpandedSections(searchResults.map((item, index) => index));
}, [ searchResults ]);
const handleSearchTermChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
changeSearchTerm(event.target.value);
}, []);
const handleResultItemClick = React.useCallback((filePath: string, lineNumber: number) => {
const fileIndex = data.findIndex((item) => item.file_path === filePath);
if (fileIndex > -1) {
onFileSelect(fileIndex, Number(lineNumber));
}
}, [ data, onFileSelect ]);
const handleAccordionStateChange = React.useCallback((newValue: Array<number>) => {
setExpandedSections(newValue);
}, []);
const handleToggleCollapseClick = React.useCallback(() => {
if (expandedSections.length === 0) {
setExpandedSections(searchResults.map((item, index) => index));
} else {
setExpandedSections([]);
}
}, [ expandedSections.length, searchResults ]);
const renderActionBar = React.useCallback(() => {
return (
<CoderEditorCollapseButton
onClick={ handleToggleCollapseClick }
label={ expandedSections.length === 0 ? 'Expand all' : 'Collapse all' }
isDisabled={ searchResults.length === 0 }
isCollapsed={ expandedSections.length === 0 }
/>
);
}, [ expandedSections.length, handleToggleCollapseClick, searchResults.length ]);
React.useEffect(() => {
isActive && setActionBarRenderer(() => renderActionBar);
}, [ isActive, renderActionBar, setActionBarRenderer ]);
const buttonProps: ChakraProps = {
boxSize: '20px',
p: '1px',
cursor: 'pointer',
borderRadius: '3px',
borderWidth: '1px',
borderColor: 'transparent',
};
const searchResultNum = (() => {
if (!debouncedSearchTerm) {
return null;
}
const totalResults = searchResults.map(({ matches }) => matches.length).reduce((result, item) => result + item, 0);
if (!totalResults) {
return (
<Box px="8px" fontSize="13px" lineHeight="18px" mb="8px">
No results found. Review your settings for configured exclusions.
</Box>
);
}
return (
<Box px="8px" fontSize="13px" lineHeight="18px" mb="8px">
{ totalResults } result{ totalResults > 1 ? 's' : '' } in { searchResults.length } file{ searchResults.length > 1 ? 's' : '' }
</Box>
);
})();
return (
<Box>
<InputGroup
px="8px"
position="sticky"
top="35px"
left="0"
zIndex="2"
bgColor={ themeColors['sideBar.background'] }
pb="8px"
boxShadow={ isInputStuck ? 'md' : 'none' }
>
<Input
size="xs"
onChange={ handleSearchTermChange }
value={ searchTerm }
placeholder="Search"
variant="unstyled"
color={ themeColors['input.foreground'] }
bgColor={ themeColors['input.background'] }
borderRadius="none"
fontSize="13px"
lineHeight="20px"
borderWidth="1px"
borderColor={ themeColors['input.background'] }
py="2px"
pl="4px"
pr="75px"
transitionDuration="0"
_focus={{
borderColor: themeColors.focusBorder,
}}
/>
<InputRightElement w="auto" h="auto" right="12px" top="3px" columnGap="2px">
<Box
{ ...buttonProps }
className="codicon codicon-case-sensitive"
onClick={ setMatchCase.toggle }
bgColor={ isMatchCase ? themeColors['custom.inputOption.activeBackground'] : 'transparent' }
_hover={{ bgColor: isMatchCase ? themeColors['custom.inputOption.activeBackground'] : themeColors['custom.inputOption.hoverBackground'] }}
title="Match Case"
aria-label="Match Case"
/>
<Box
{ ...buttonProps }
className="codicon codicon-whole-word"
bgColor={ isMatchWholeWord ? themeColors['custom.inputOption.activeBackground'] : 'transparent' }
onClick={ setMatchWholeWord.toggle }
_hover={{ bgColor: isMatchWholeWord ? themeColors['custom.inputOption.activeBackground'] : themeColors['custom.inputOption.hoverBackground'] }}
title="Match Whole Word"
aria-label="Match Whole Word"
/>
<Box
{ ...buttonProps }
className="codicon codicon-regex"
bgColor={ isMatchRegex ? themeColors['custom.inputOption.activeBackground'] : 'transparent' }
onClick={ setMatchRegex.toggle }
_hover={{ bgColor: isMatchRegex ? themeColors['custom.inputOption.activeBackground'] : themeColors['custom.inputOption.hoverBackground'] }}
title="Use Regular Expression"
aria-label="Use Regular Expression"
/>
</InputRightElement>
</InputGroup>
{ searchResultNum }
<Accordion
key={ debouncedSearchTerm }
allowMultiple
index={ expandedSections }
onChange={ handleAccordionStateChange }
reduceMotion
>
{ searchResults.map((item) => <CodeEditorSearchSection key={ item.file_path } data={ item } onItemClick={ handleResultItemClick }/>) }
</Accordion>
</Box>
);
};
export default React.memo(CodeEditorSearch);
import { Box, chakra } from '@chakra-ui/react';
import React from 'react';
import type { SearchResult } from './types';
import type ArrayElement from 'types/utils/ArrayElement';
import useThemeColors from './utils/useThemeColors';
interface Props extends ArrayElement<SearchResult['matches']> {
filePath: string;
onClick: (event: React.MouseEvent) => void;
}
const calculateStartPosition = (lineContent: string, startColumn: number) => {
let start = 0;
for (let index = 0; index < startColumn; index++) {
const element = lineContent[index];
if (element === ' ') {
start = index + 1;
continue;
}
}
return start ? start - 1 : 0;
};
const CodeEditorSearchResultItem = ({ lineContent, filePath, onClick, startLineNumber, startColumn, endColumn }: Props) => {
const start = calculateStartPosition(lineContent, startColumn);
const themeColors = useThemeColors();
return (
<Box
pr="8px"
pl="36px"
fontSize="13px"
lineHeight="22px"
whiteSpace="nowrap"
overflow="hidden"
textOverflow="ellipsis"
cursor="pointer"
data-file-path={ filePath }
data-line-number={ startLineNumber }
onClick={ onClick }
transitionDuration="0"
_hover={{ bgColor: themeColors['custom.list.hoverBackground'] }}
>
<span>{ lineContent.slice(start, startColumn - 1) }</span>
<chakra.span bgColor={ themeColors['custom.findMatchHighlightBackground'] }>
{ lineContent.slice(startColumn - 1, endColumn - 1) }
</chakra.span>
<span>{ lineContent.slice(endColumn - 1) }</span>
</Box>
);
};
export default React.memo(CodeEditorSearchResultItem);
import { AccordionButton, AccordionItem, AccordionPanel, Icon, Box } from '@chakra-ui/react';
import React from 'react';
import type { SearchResult } from './types';
import CodeEditorSearchResultItem from './CodeEditorSearchResultItem';
import iconFile from './icons/file.svg';
import iconSolidity from './icons/solidity.svg';
import getFileName from './utils/getFileName';
import useThemeColors from './utils/useThemeColors';
interface Props {
data: SearchResult;
onItemClick: (filePath: string, lineNumber: number) => void;
}
const CodeEditorSearchSection = ({ data, onItemClick }: Props) => {
const fileName = getFileName(data.file_path);
const handleFileLineClick = React.useCallback((event: React.MouseEvent) => {
const lineNumber = Number((event.currentTarget as HTMLDivElement).getAttribute('data-line-number'));
if (!Object.is(lineNumber, NaN)) {
onItemClick(data.file_path, Number(lineNumber));
}
}, [ data.file_path, onItemClick ]);
const icon = /.sol|.yul|.vy$/.test(fileName) ? iconSolidity : iconFile;
const themeColors = useThemeColors();
return (
<AccordionItem borderWidth="0px" _last={{ borderBottomWidth: '0px' }} >
{ ({ isExpanded }) => (
<>
<AccordionButton
py={ 0 }
px={ 2 }
_hover={{ bgColor: themeColors['custom.list.hoverBackground'] }}
fontSize="13px"
transitionDuration="0"
lineHeight="22px"
alignItems="center"
>
<Box
className="codicon codicon-tree-item-expanded"
transform={ isExpanded ? 'rotate(0deg)' : 'rotate(-90deg)' }
width="20px"
height="22px"
py="3px"
/>
<Icon as={ icon } boxSize="16px" mr="4px"/>
<span>{ fileName }</span>
<Box className="monaco-count-badge" ml="auto" bgColor={ themeColors['badge.background'] }>{ data.matches.length }</Box>
</AccordionButton>
<AccordionPanel p={ 0 }>
{ data.matches.map((match) => (
<CodeEditorSearchResultItem
key={ data.file_path + '_' + match.startLineNumber + '_' + match.startColumn }
filePath={ data.file_path }
onClick={ handleFileLineClick }
{ ...match }
/>
),
) }
</AccordionPanel>
</>
) }
</AccordionItem>
);
};
export default React.memo(CodeEditorSearchSection);
import type { HTMLChakraProps } from '@chakra-ui/react';
import { Box, Tab, TabList, TabPanel, TabPanels, Tabs, useBoolean } from '@chakra-ui/react';
import _throttle from 'lodash/throttle';
import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import React from 'react';
import type { File, Monaco } from './types';
import { shift, cmd } from 'lib/html-entities';
import CodeEditorFileExplorer from './CodeEditorFileExplorer';
import CodeEditorSearch from './CodeEditorSearch';
import useThemeColors from './utils/useThemeColors';
interface Props {
monaco: Monaco | undefined;
editor: monaco.editor.IStandaloneCodeEditor | undefined;
data: Array<File>;
onFileSelect: (index: number, lineNumber?: number) => void;
selectedFile: string;
}
export const CONTAINER_WIDTH = 250;
const CodeEditorSideBar = ({ onFileSelect, data, monaco, editor, selectedFile }: Props) => {
const [ isStuck, setIsStuck ] = React.useState(false);
const [ isDrawerOpen, setIsDrawerOpen ] = useBoolean(false);
const [ tabIndex, setTabIndex ] = React.useState(0);
const [ searchValue, setSearchValue ] = React.useState('');
const [ actionBarRenderer, setActionBarRenderer ] = React.useState<() => JSX.Element>();
const themeColors = useThemeColors();
const tabProps: HTMLChakraProps<'button'> = {
fontFamily: 'heading',
textTransform: 'uppercase',
fontSize: '11px',
lineHeight: '35px',
fontWeight: 500,
color: themeColors['tab.inactiveForeground'],
_selected: {
color: themeColors['tab.activeForeground'],
},
px: 0,
letterSpacing: 0.3,
};
const handleScrollThrottled = React.useRef(_throttle((event: React.SyntheticEvent) => {
setIsStuck((event.target as HTMLDivElement).scrollTop > 0);
}, 100));
const handleFileSelect = React.useCallback((index: number, lineNumber?: number) => {
isDrawerOpen && setIsDrawerOpen.off();
onFileSelect(index, lineNumber);
}, [ isDrawerOpen, onFileSelect, setIsDrawerOpen ]);
React.useEffect(() => {
if (editor && monaco) {
editor.addAction({
id: 'file-explorer',
label: 'Show File Explorer',
keybindings: [
monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyE,
],
contextMenuGroupId: 'navigation',
contextMenuOrder: 1.5,
run: function() {
setTabIndex(0);
},
});
editor.addAction({
id: 'search-in-files',
label: 'Show Search in Files',
keybindings: [
monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF,
],
contextMenuGroupId: 'navigation',
contextMenuOrder: 1.6,
run: function(editor) {
setTabIndex(1);
const selection = editor.getSelection();
const selectedValue = selection ? editor.getModel()?.getValueInRange(selection) : '';
setSearchValue(selectedValue || '');
},
});
}
}, [ editor, monaco ]);
return (
<>
<Box
w={ `${ CONTAINER_WIDTH }px` }
flexShrink={ 0 }
bgColor={ themeColors['sideBar.background'] }
fontSize="13px"
overflowY="scroll"
onScroll={ handleScrollThrottled.current }
position={{ base: 'absolute', lg: 'relative' }}
right={{ base: isDrawerOpen ? '0' : `-${ CONTAINER_WIDTH }px`, lg: '0' }}
top={{ base: 0, lg: undefined }}
h="100%"
pb="22px"
boxShadow={{ base: isDrawerOpen ? 'md' : 'none', lg: 'none' }}
zIndex={{ base: '2', lg: undefined }}
transitionProperty="right"
transitionDuration="normal"
transitionTimingFunction="ease-in-out"
borderTopRightRadius="md"
borderBottomRightRadius="md"
>
<Tabs isLazy lazyBehavior="keepMounted" variant="unstyled" size="13px" index={ tabIndex } onChange={ setTabIndex }>
<TabList
columnGap={ 3 }
position="sticky"
top={ 0 }
left={ 0 }
bgColor={ themeColors['sideBar.background'] }
zIndex="1"
px={ 2 }
boxShadow={ isStuck ? 'md' : 'none' }
borderTopRightRadius="md"
>
<Tab { ...tabProps } title={ `File explorer (${ shift + cmd }E)` }>Explorer</Tab>
<Tab { ...tabProps } title={ `Search in files (${ shift + cmd }F)` }>Search</Tab>
{ actionBarRenderer?.() }
</TabList>
<TabPanels>
<TabPanel p={ 0 }>
<CodeEditorFileExplorer
data={ data }
onFileSelect={ handleFileSelect }
selectedFile={ selectedFile }
isActive={ tabIndex === 0 }
setActionBarRenderer={ setActionBarRenderer }
/>
</TabPanel>
<TabPanel p={ 0 }>
<CodeEditorSearch
data={ data }
onFileSelect={ handleFileSelect }
monaco={ monaco }
isInputStuck={ isStuck }
isActive={ tabIndex === 1 }
setActionBarRenderer={ setActionBarRenderer }
defaultValue={ searchValue }
/>
</TabPanel>
</TabPanels>
</Tabs>
</Box>
<Box
boxSize="24px"
p="4px"
position="absolute"
display={{ base: 'block', lg: 'none' }}
right={ isDrawerOpen ? `${ CONTAINER_WIDTH - 1 }px` : '0' }
top="calc(50% - 12px)"
backgroundColor={ themeColors['sideBar.background'] }
borderTopLeftRadius="4px"
borderBottomLeftRadius="4px"
boxShadow="md"
onClick={ setIsDrawerOpen.toggle }
zIndex="1"
transitionProperty="right"
transitionDuration="normal"
transitionTimingFunction="ease-in-out"
title={ isDrawerOpen ? 'Open sidebar' : 'Close sidebar' }
aria-label={ isDrawerOpen ? 'Open sidebar' : 'Close sidebar' }
>
<Box
className="codicon codicon-tree-item-expanded"
transform={ isDrawerOpen ? 'rotate(-90deg)' : 'rotate(+90deg)' }
boxSize="16px"
/>
</Box>
</>
);
};
export default React.memo(CodeEditorSideBar);
import { Flex, Icon, chakra, Box } from '@chakra-ui/react';
import React from 'react';
import { alt } from 'lib/html-entities';
import useThemeColors from 'ui/shared/monaco/utils/useThemeColors';
import iconFile from './icons/file.svg';
import iconSolidity from './icons/solidity.svg';
import getFilePathParts from './utils/getFilePathParts';
interface Props {
isActive?: boolean;
path: string;
onClick: (path: string) => void;
onClose: (path: string) => void;
isCloseDisabled: boolean;
tabsPathChunks: Array<Array<string>>;
}
const CodeEditorTab = ({ isActive, path, onClick, onClose, isCloseDisabled, tabsPathChunks }: Props) => {
const [ fileName, folderName ] = getFilePathParts(path, tabsPathChunks);
const themeColors = useThemeColors();
const handleClick = React.useCallback(() => {
onClick(path);
}, [ onClick, path ]);
const handleClose = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation();
!isCloseDisabled && onClose(path);
}, [ isCloseDisabled, onClose, path ]);
const icon = /.sol|.yul|.vy$/.test(fileName) ? iconSolidity : iconFile;
return (
<Flex
pl="10px"
pr="4px"
fontSize="13px"
lineHeight="34px"
bgColor={ isActive ? themeColors['tab.activeBackground'] : themeColors['tab.inactiveBackground'] }
borderRightWidth="1px"
borderRightColor={ themeColors['tab.border'] }
borderBottomWidth="1px"
borderBottomColor={ isActive ? 'transparent' : themeColors['tab.border'] }
color={ isActive ? themeColors['tab.activeForeground'] : themeColors['tab.inactiveForeground'] }
alignItems="center"
fontWeight={ 400 }
cursor="pointer"
onClick={ handleClick }
_hover={{
'.codicon-close': {
visibility: 'visible',
},
}}
userSelect="none"
>
<Icon as={ icon } boxSize="16px" mr="4px"/>
<span>{ fileName }</span>
{ folderName && <chakra.span fontSize="11px" opacity={ 0.8 } ml={ 1 }>{ folderName[0] === '.' ? '' : '...' }{ folderName }</chakra.span> }
<Box
className="codicon codicon-close"
boxSize="20px"
ml="4px"
p="2px"
title={ `Close ${ isActive ? `(${ alt }W)` : '' }` }
aria-label="Close"
onClick={ handleClose }
borderRadius="sm"
opacity={ isCloseDisabled ? 0.3 : 1 }
visibility={{ base: 'visible', lg: isActive ? 'visible' : 'hidden' }}
color={ themeColors['icon.foreground'] }
_hover={{ bgColor: isCloseDisabled ? 'transparent' : themeColors['custom.inputOption.hoverBackground'] }}
/>
</Flex>
);
};
export default React.memo(CodeEditorTab);
import { Flex } from '@chakra-ui/react';
import React from 'react';
import CodeEditorTab from './CodeEditorTab';
import useThemeColors from './utils/useThemeColors';
interface Props {
tabs: Array<string>;
activeTab: string;
onTabSelect: (tab: string) => void;
onTabClose: (tab: string) => void;
}
const CodeEditorTabs = ({ tabs, activeTab, onTabSelect, onTabClose }: Props) => {
const themeColors = useThemeColors();
const tabsPathChunks = React.useMemo(() => {
return tabs.map((tab) => tab.split('/'));
}, [ tabs ]);
return (
<Flex
bgColor={ themeColors['sideBar.background'] }
flexWrap="wrap"
>
{ tabs.map((tab) => (
<CodeEditorTab
key={ tab }
path={ tab }
isActive={ activeTab === tab }
onClick={ onTabSelect }
onClose={ onTabClose }
isCloseDisabled={ tabs.length === 1 }
tabsPathChunks={ tabsPathChunks }
/>
)) }
</Flex>
);
};
export default React.memo(CodeEditorTabs);
import { Box } from '@chakra-ui/react';
import React from 'react';
import useThemeColors from './utils/useThemeColors';
interface Props {
onClick: () => void;
label: string;
isDisabled?: boolean;
isCollapsed?: boolean;
}
const CoderEditorCollapseButton = ({ onClick, label, isDisabled, isCollapsed }: Props) => {
const themeColors = useThemeColors();
return (
<Box
ml="auto"
alignSelf="center"
className={ isCollapsed ? 'codicon codicon-search-expand-results' : 'codicon codicon-collapse-all' }
opacity={ isDisabled ? 0.6 : 1 }
boxSize="20px"
p="2px"
borderRadius="sm"
_before={{
content: isCollapsed ? '"\\eb95"' : '"\\eac5"',
}}
_hover={{
bgColor: themeColors['custom.inputOption.hoverBackground'],
}}
onClick={ onClick }
cursor="pointer"
title={ label }
aria-label={ label }
/>
);
};
export default React.memo(CoderEditorCollapseButton);
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m5 2H6v16h12v-9h-7V4z" fill="#42a5f5"/>
</svg>
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7a2 2 0 0 1 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#90a4ae"/>
</svg>
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M10 4H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#90a4ae"/>
</svg>
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="#0288d1">
<path d="m5.747 14.046 6.254 8.61 6.252-8.61-6.254 3.807z"/>
<path d="M11.999 1.343 5.747 11.83l6.252 3.807 6.253-3.807z"/>
</g>
</svg>
import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
export interface File {
file_path: string;
source_code: string;
}
export interface FileTreeFile extends File {
name: string;
}
export interface FileTreeFolder {
name: string;
children: Array<FileTreeFile | FileTreeFolder>;
}
export type FileTree = Array<FileTreeFile | FileTreeFolder>;
export type Monaco = typeof monaco;
export interface SearchResult {
file_path: string;
matches: Array<
Pick<monaco.editor.FindMatch['range'], 'startColumn' | 'endColumn' | 'startLineNumber' | 'endLineNumber'> &
{ lineContent: string }
>;
}
import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
export default function addFileImportDecorations(model: monaco.editor.ITextModel) {
const matches = model.findMatches('^import "((\\/|\\.)(\\w|\\.|\\/|-)+)"', false, true, false, null, true);
const decorations: Array<monaco.editor.IModelDeltaDecoration> = matches.map(({ range }) => ({
range: {
...range,
startColumn: range.startColumn + 8,
endColumn: range.endColumn - 1,
},
options: {
inlineClassName: 'import-link',
hoverMessage: {
value: 'Cmd/Win + click to open file',
},
},
}));
model.deltaDecorations([], decorations);
}
import composeFileTree from './composeFileTree';
const files = [
{
file_path: 'index.sol',
source_code: 'zero',
},
{
file_path: 'contracts/Zeta.eth.sol',
source_code: 'one',
},
{
file_path: '/_openzeppelin/contracts/utils/Context.sol',
source_code: 'two',
},
{
file_path: '/_openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol',
source_code: 'three',
},
{
file_path: '/_openzeppelin/contracts/token/ERC20/IERC20.sol',
source_code: 'four',
},
{
file_path: '/_openzeppelin/contracts/token/ERC20/ERC20.sol',
source_code: 'five',
},
];
test('builds correct file tree', () => {
const result = composeFileTree(files);
expect(result).toMatchInlineSnapshot(`
[
{
"children": [
{
"children": [
{
"children": [
{
"children": [
{
"children": [
{
"file_path": "/_openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol",
"name": "IERC20Metadata.sol",
"source_code": "three",
},
],
"name": "extensions",
},
{
"file_path": "/_openzeppelin/contracts/token/ERC20/ERC20.sol",
"name": "ERC20.sol",
"source_code": "five",
},
{
"file_path": "/_openzeppelin/contracts/token/ERC20/IERC20.sol",
"name": "IERC20.sol",
"source_code": "four",
},
],
"name": "ERC20",
},
],
"name": "token",
},
{
"children": [
{
"file_path": "/_openzeppelin/contracts/utils/Context.sol",
"name": "Context.sol",
"source_code": "two",
},
],
"name": "utils",
},
],
"name": "contracts",
},
],
"name": "_openzeppelin",
},
{
"children": [
{
"file_path": "contracts/Zeta.eth.sol",
"name": "Zeta.eth.sol",
"source_code": "one",
},
],
"name": "contracts",
},
{
"file_path": "index.sol",
"name": "index.sol",
"source_code": "zero",
},
]
`);
});
import type { File, FileTree } from '../types';
import stripLeadingSlash from 'lib/stripLeadingSlash';
import sortFileTree from './sortFileTree';
export default function composeFileTree(files: Array<File>) {
const result: FileTree = [];
type Level = {
result: FileTree;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} & Record<string, any>;
const level: Level = { result };
files.forEach((file) => {
const path = stripLeadingSlash(file.file_path);
const segments = path.split('/');
segments.reduce((acc, segment, currentIndex, array) => {
if (!acc[segment]) {
acc[segment] = { result: [] };
acc.result.push({
name: segment,
...(currentIndex === array.length - 1 ? file : { children: acc[segment].result }),
});
}
acc.result.sort(sortFileTree);
return acc[segment];
}, level);
});
return result.sort(sortFileTree);
}
// ensure that path always starts with /
export default function formatFilePath(path: string) {
if (path[0] === '.' && path[1] === '/') {
return path.slice(1);
}
if (path[0] === '/') {
return path;
}
return '/' + path;
}
export default function getFileName(path: string) {
const chunks = path.split('/');
return chunks[chunks.length - 1];
}
import getFilePathParts from './getFilePathParts';
it('computes correct chunks if all file name are unique', () => {
const result = getFilePathParts(
'/src/utils/Ownable.sol',
[
'/src/utils/EIP712.sol'.split('/'),
'/src/token/BaseERC20.sol'.split('/'),
],
);
expect(result).toEqual([ 'Ownable.sol', undefined ]);
});
it('computes correct chunks if files with the same name is not in folders with the same name', () => {
const result = getFilePathParts(
'/src/utils/Ownable.sol',
[
'/src/utils/EIP712.sol'.split('/'),
'/lib/_openzeppelin/contracts/contracts/access/Ownable.sol'.split('/'),
],
);
expect(result).toEqual([ 'Ownable.sol', '/utils' ]);
});
it('computes correct chunks if files with the same name is in folders with the same name', () => {
const result = getFilePathParts(
'/src/utils/Ownable.sol',
[
'/src/utils/EIP712.sol'.split('/'),
'/lib/_openzeppelin/contracts/src/utils/Ownable.sol'.split('/'),
],
);
expect(result).toEqual([ 'Ownable.sol', './src/utils' ]);
});
it('computes correct chunks if file is in root directory', () => {
const result = getFilePathParts(
'/Ownable.sol',
[
'/src/utils/EIP712.sol'.split('/'),
'/lib/_openzeppelin/contracts/src/utils/Ownable.sol'.split('/'),
],
);
expect(result).toEqual([ 'Ownable.sol', './' ]);
});
export default function getFilePathParts(path: string, tabsPathChunks: Array<Array<string>>): [string, string | undefined] {
const chunks = path.split('/');
const fileName = chunks[chunks.length - 1];
const folderName = getFolderName(chunks, tabsPathChunks);
return [ fileName, folderName ];
}
function getFolderName(chunks: Array<string>, tabsPathChunks: Array<Array<string>>): string | undefined {
const fileName = chunks[chunks.length - 1];
const otherTabsPathChunks = tabsPathChunks.filter((item) => item.join('/') !== chunks.join('/'));
const tabsWithSameFileName = otherTabsPathChunks.filter((tabChunks) => tabChunks[tabChunks.length - 1] === fileName);
if (tabsWithSameFileName.length === 0 || chunks.length <= 1) {
return;
}
if (chunks.length === 2) {
return './' + chunks[chunks.length - 2];
}
let result = '/' + chunks[chunks.length - 2];
for (let index = 3; index <= chunks.length; index++) {
const element = chunks[chunks.length - index];
if (element === '') {
result = '.' + result;
}
const subFolderNames = tabsWithSameFileName.map((tab) => tab[tab.length - index]);
if (subFolderNames.includes(element)) {
result = '/' + element + result;
} else {
break;
}
}
return result;
}
import getFullPathOfImportedFile from './getFullPathOfImportedFile';
it('construct correct absolute path', () => {
const result = getFullPathOfImportedFile(
'/foo/bar/baz/index.sol',
'./.././../abc/contract.sol',
);
expect(result).toBe('/foo/abc/contract.sol');
});
it('returns undefined if imported file is outside the base file folder', () => {
const result = getFullPathOfImportedFile(
'/index.sol',
'../../abc/contract.sol',
);
expect(result).toBeUndefined();
});
it('returns unmodified path if it is already absolute', () => {
const result = getFullPathOfImportedFile(
'/index.sol',
'/abc/contract.sol',
);
expect(result).toBe('/abc/contract.sol');
});
it('returns undefined for external path', () => {
const result = getFullPathOfImportedFile(
'/index.sol',
'https://github.com/ethereum/dapp/contract.sol',
);
expect(result).toBeUndefined();
});
import stripLeadingSlash from 'lib/stripLeadingSlash';
export default function getFullPathOfImportedFile(baseFilePath: string, importedFilePath: string) {
if (importedFilePath[0] === '/') {
return importedFilePath;
}
if (importedFilePath[0] !== '.') {
return;
}
const baseFileChunks = stripLeadingSlash(baseFilePath).split('/');
const importedFileChunks = importedFilePath.split('/');
const result: Array<string> = baseFileChunks.slice(0, -1);
for (let index = 0; index < importedFileChunks.length - 1; index++) {
const element = importedFileChunks[index];
if (element === '.') {
continue;
}
if (element === '..') {
if (result.length === 0) {
break;
}
result.pop();
continue;
}
result.push(element);
}
if (result.length === 0) {
return;
}
result.push(importedFileChunks[importedFileChunks.length - 1]);
return '/' + result.join('/');
}
import type { FileTree } from '../types';
import type ArrayElement from 'types/utils/ArrayElement';
export default function sortFileTree(a: ArrayElement<FileTree>, b: ArrayElement<FileTree>) {
if ('children' in a && !('children' in b)) {
return -1;
}
if ('children' in b && !('children' in a)) {
return 1;
}
return a.name.localeCompare(b.name);
}
export const light = {
base: 'vs' as const,
inherit: true,
rules: [],
colors: {
'editor.background': '#f5f5f6',
'editorWidget.background': '#f5f5f6',
'tab.activeBackground': '#f5f5f6',
'tab.inactiveBackground': 'rgb(236, 236, 236)',
'tab.activeForeground': '#101112', // black
'tab.inactiveForeground': '#4a5568', // gray.600
'tab.border': 'rgb(243, 243, 243)',
'icon.foreground': '#616161',
'input.foreground': '#616161',
'input.background': '#fff',
'list.inactiveSelectionBackground': '#e4e6f1',
'breadcrumbs.foreground': 'rgb(97, 97, 97)',
'badge.background': '#c4c4c4',
'sideBar.background': '#eee',
focusBorder: '#0090f1',
// not able to use rgba for standard variables, so we use custom prefix here
'custom.list.hoverBackground': 'rgba(16, 17, 18, 0.08)', // blackAlpha.200
'custom.findMatchHighlightBackground': 'rgba(234,92,0,0.33)',
'custom.inputOption.activeBackground': 'rgba(0, 144, 241, 0.2)',
'custom.inputOption.hoverBackground': 'rgba(184, 184, 184, 0.31)',
// don't know the name of this variables in vscode
'custom.fileLink.hoverForeground': '#4299E1', // blue.400
} as const,
};
export const dark = {
base: 'vs-dark' as const,
inherit: true,
rules: [],
colors: {
'editor.background': '#1a1b1b',
'editorWidget.background': '#1a1b1b',
'tab.activeBackground': '#1a1b1b', // black
'tab.inactiveBackground': 'rgb(45, 45, 45)',
'tab.activeForeground': '#fff', // white
'tab.inactiveForeground': '#a0aec0', // gray.400
'tab.border': 'rgb(37, 37, 38)',
'icon.foreground': '#616161',
'input.foreground': '#cccccc',
'input.background': '#3c3c3c',
'list.inactiveSelectionBackground': '#37373d',
'badge.background': '#4d4d4d',
'breadcrumbs.foreground': 'rgb(97, 97, 97)',
'sideBar.background': '#222',
focusBorder: '#007fd4',
// not able to use rgba for standard variables, so we use custom prefix here
'custom.list.hoverBackground': 'rgba(255, 255, 255, 0.08)', // whiteAlpha.200
'custom.findMatchHighlightBackground': 'rgba(234,92,0,0.33)',
'custom.inputOption.activeBackground': 'rgba(0, 127, 212, 0.4)',
'custom.inputOption.hoverBackground': 'rgba(90, 93, 94, 0.31)',
// don't know the name of this variables in vscode
'custom.fileLink.hoverForeground': '#4299E1', // blue.400
} as const,
};
import { useColorModeValue } from '@chakra-ui/react';
import * as themes from './themes';
export default function useThemeColors() {
const theme = useColorModeValue(themes.light, themes.dark);
return theme.colors;
}
import type { ResponsiveValue } from '@chakra-ui/react';
import { AspectRatio, chakra, Icon, Image, shouldForwardProp, Skeleton, useColorModeValue } from '@chakra-ui/react';
import { Box, AspectRatio, chakra, Icon, Image, shouldForwardProp, Skeleton, useColorModeValue } from '@chakra-ui/react';
import type { Property } from 'csstype';
import React from 'react';
......@@ -30,6 +30,7 @@ const Fallback = ({ className, padding }: FallbackProps) => {
const NftImage = ({ url, className, fallbackPadding, objectFit }: Props) => {
const [ isLoading, setIsLoading ] = React.useState(true);
const [ isError, setIsError ] = React.useState(false);
const handleLoad = React.useCallback(() => {
setIsLoading(false);
......@@ -37,10 +38,37 @@ const NftImage = ({ url, className, fallbackPadding, objectFit }: Props) => {
const handleLoadError = React.useCallback(() => {
setIsLoading(false);
setIsError(true);
}, []);
const _objectFit = objectFit || 'contain';
const content = (() => {
// as of ChakraUI v2.5.3
// fallback prop of Image component doesn't work well with loading prop lazy strategy
// so we have to render fallback and loader manually
if (isError || !url) {
return <Fallback className={ className } padding={ fallbackPadding }/>;
}
return (
<Box>
{ isLoading && <Skeleton position="absolute" left={ 0 } top={ 0 } w="100%" h="100%" zIndex="1"/> }
<Image
w="100%"
h="100%"
objectFit={ _objectFit }
src={ url }
opacity={ isLoading ? 0 : 1 }
alt="Token instance image"
onError={ handleLoadError }
onLoad={ handleLoad }
loading={ url ? 'lazy' : undefined }
/>
</Box>
);
})();
return (
<AspectRatio
className={ className }
......@@ -54,17 +82,7 @@ const NftImage = ({ url, className, fallbackPadding, objectFit }: Props) => {
},
}}
>
<Image
w="100%"
h="100%"
objectFit={ _objectFit }
src={ url || undefined }
alt="Token instance image"
fallback={ url && isLoading ? <Skeleton/> : <Fallback className={ className } padding={ fallbackPadding }/> }
onError={ handleLoadError }
onLoad={ handleLoad }
loading={ url ? 'lazy' : undefined }
/>
{ content }
</AspectRatio>
);
};
......
......@@ -32,7 +32,7 @@ const Sort = <Sort extends string>({ sort, setSort, options }: Props<Sort>) => {
return (
<Menu>
<MenuButton>
<MenuButton as="div">
<SortButton
isActive={ isOpen || Boolean(sort) }
onClick={ onToggle }
......
import { Text } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
......@@ -7,10 +6,9 @@ import type { TokenHolders, TokenInfo } from 'types/api/token';
import useIsMobile from 'lib/hooks/useIsMobile';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import TokenHoldersList from './TokenHoldersList';
import TokenHoldersTable from './TokenHoldersTable';
......@@ -30,36 +28,31 @@ const TokenHoldersContent = ({ holdersQuery, tokenQuery }: Props) => {
return <DataFetchAlert/>;
}
const bar = isMobile && holdersQuery.isPaginationVisible && (
const actionBar = isMobile && holdersQuery.isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...holdersQuery.pagination }/>
</ActionBar>
);
if (holdersQuery.isLoading || tokenQuery.isLoading) {
return (
<>
{ bar }
{ isMobile && <SkeletonList/> }
{ !isMobile && (
<SkeletonTable columns={ [ '100%', '300px', '175px' ] } isLong/>
) }
</>
);
}
const items = holdersQuery.data.items;
if (!items?.length) {
return <Text as="span">There are no holders for this token.</Text>;
}
const items = holdersQuery.data?.items;
return (
const content = items && tokenQuery.data ? (
<>
{ bar }
{ !isMobile && <TokenHoldersTable data={ items } token={ tokenQuery.data } top={ holdersQuery.isPaginationVisible ? 80 : 0 }/> }
{ isMobile && <TokenHoldersList data={ items } token={ tokenQuery.data }/> }
</>
) : null;
return (
<DataListDisplay
isError={ holdersQuery.isError || tokenQuery.isError }
isLoading={ holdersQuery.isLoading || tokenQuery.isLoading }
items={ holdersQuery.data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '100%', '300px', '175px' ] }}
emptyText="There are no holders for this token."
content={ content }
actionBar={ actionBar }
/>
);
};
......
......@@ -7,7 +7,7 @@ import type { TokenHolder, TokenInfo } from 'types/api/token';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import ListItemMobile from 'ui/shared/ListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import Utilization from 'ui/shared/Utilization/Utilization';
interface Props {
......
import { Grid, Text, Skeleton } from '@chakra-ui/react';
import { Grid, Skeleton } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
......@@ -6,7 +6,7 @@ import type { TokenInventoryResponse } from 'types/api/token';
import useIsMobile from 'lib/hooks/useIsMobile';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
......@@ -21,53 +21,51 @@ type Props = {
const TokenInventory = ({ inventoryQuery }: Props) => {
const isMobile = useIsMobile();
if (inventoryQuery.isError) {
return <DataFetchAlert/>;
}
const bar = isMobile && inventoryQuery.isPaginationVisible && (
const actionBar = isMobile && inventoryQuery.isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...inventoryQuery.pagination }/>
</ActionBar>
);
if (inventoryQuery.isLoading) {
return (
<>
{ bar }
<Grid
w="100%"
columnGap={{ base: 3, lg: 6 }}
rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
</Grid>
</>
);
}
const skeleton = (
<Grid
w="100%"
columnGap={{ base: 3, lg: 6 }}
rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
</Grid>
);
const items = inventoryQuery.data.items;
const items = inventoryQuery.data?.items;
if (!items?.length) {
return <Text as="span">There are no tokens.</Text>;
}
const content = items ? (
<Grid
w="100%"
columnGap={{ base: 3, lg: 6 }}
rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
{ items.map((item) => <TokenInventoryItem key={ item.token.address + '_' + item.id } item={ item }/>) }
</Grid>
) : null;
return (
<>
{ bar }
<Grid
w="100%"
columnGap={{ base: 3, lg: 6 }}
rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
{ items.map((item) => <TokenInventoryItem key={ item.token.address + '_' + item.id } item={ item }/>) }
</Grid></>
<DataListDisplay
isError={ inventoryQuery.isError }
isLoading={ inventoryQuery.isLoading }
items={ items }
emptyText="There are no tokens."
content={ content }
actionBar={ actionBar }
skeletonProps={{ customSkeleton: skeleton }}
/>
);
};
......
import { Hide, Show, Text } from '@chakra-ui/react';
import { Hide, Show } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -11,11 +11,9 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferList from 'ui/token/TokenTransfer/TokenTransferList';
......@@ -61,66 +59,55 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => {
handler: handleNewTransfersMessage,
});
const content = (() => {
if (isLoading) {
return (
<>
<Hide below="lg" ssr={ false }>
<SkeletonTable columns={ [ '45%', '15%', '36px', '15%', '25%' ] } isLong/>
</Hide>
<Show below="lg" ssr={ false }>
<SkeletonList/>
</Show>
</>
);
}
const items = data?.items?.reduce(flattenTotal, []);
if (isError) {
return <DataFetchAlert/>;
}
const content = items ? (
if (!data.items?.length) {
return <Text as="span">There are no token transfers</Text>;
}
const items = data.items.reduce(flattenTotal, []);
return (
<>
<Hide below="lg" ssr={ false }>
<TokenTransferTable
data={ items }
top={ isPaginationVisible ? 80 : 0 }
showSocketInfo={ pagination.page === 1 }
socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount }
tokenId={ tokenId }
<>
<Hide below="lg" ssr={ false }>
<TokenTransferTable
data={ items }
top={ isPaginationVisible ? 80 : 0 }
showSocketInfo={ pagination.page === 1 }
socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount }
tokenId={ tokenId }
/>
</Hide>
<Show below="lg" ssr={ false }>
{ pagination.page === 1 && (
<SocketNewItemsNotice
url={ window.location.href }
num={ newItemsCount }
alert={ socketAlert }
type="token_transfer"
borderBottomRadius={ 0 }
/>
</Hide>
<Show below="lg" ssr={ false }>
{ pagination.page === 1 && (
<SocketNewItemsNotice
url={ window.location.href }
num={ newItemsCount }
alert={ socketAlert }
type="token_transfer"
borderBottomRadius={ 0 }
/>
) }
<TokenTransferList data={ items } tokenId={ tokenId }/>
</Show>
</>
);
})();
) }
<TokenTransferList data={ items } tokenId={ tokenId }/>
</Show>
</>
) : null;
const actionBar = isMobile && isPaginationVisible ? (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) : null;
return (
<>
{ isMobile && isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) }
{ content }
</>
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
items={ data?.items }
skeletonProps={{
isLongSkeleton: true,
skeletonDesktopColumns: [ '45%', '15%', '36px', '15%', '25%' ],
}}
emptyText="There are no token transfers."
content={ content }
actionBar={ actionBar }
/>
);
};
......
......@@ -11,7 +11,7 @@ import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import ListItemMobile from 'ui/shared/ListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer & {tokenId?: string};
......
......@@ -93,14 +93,12 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
/>
</Flex>
<Grid
mt={ 8 }
mt={ 5 }
columnGap={ 8 }
rowGap={{ base: 1, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: '200px minmax(0, 1fr)' }}
overflow="hidden"
>
{ divider }
<DetailsSponsoredItem/>
{ hasMetadata && (
<>
{ divider }
......@@ -148,6 +146,8 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
) }
</>
) }
{ divider }
<DetailsSponsoredItem/>
</Grid>
</>
);
......
......@@ -4,6 +4,7 @@ import React from 'react';
import MetadataItemArray from './MetadataItemArray';
import MetadataItemObject from './MetadataItemObject';
import MetadataItemPrimitive from './MetadataItemPrimitive';
import { sortFields } from './utils';
interface Props {
data: Record<string, unknown>;
......@@ -30,32 +31,32 @@ const MetadataAccordion = ({ data, level = 0 }: Props) => {
case 'string':
case 'number':
case 'boolean': {
return <MetadataItemPrimitive name={ name } value={ value } isFlat={ isFlat } level={ level }/>;
return <MetadataItemPrimitive key={ name } name={ name } value={ value } isFlat={ isFlat } level={ level }/>;
}
case 'object': {
if (value === null) {
return <MetadataItemPrimitive name={ name } value={ value } isFlat={ isFlat } level={ level }/>;
return <MetadataItemPrimitive key={ name } name={ name } value={ value } isFlat={ isFlat } level={ level }/>;
}
if (Array.isArray(value) && value.length > 0) {
return <MetadataItemArray name={ name } value={ value } level={ level }/>;
return <MetadataItemArray key={ name } name={ name } value={ value } level={ level }/>;
}
if (Object.keys(value).length > 0) {
return <MetadataItemObject name={ name } value={ value as Record<string, unknown> } level={ level }/>;
return <MetadataItemObject key={ name } name={ name } value={ value as Record<string, unknown> } level={ level }/>;
}
}
// eslint-disable-next-line no-fallthrough
default: {
return <MetadataItemPrimitive name={ name } value={ String(value) } isFlat={ isFlat } level={ level }/>;
return <MetadataItemPrimitive key={ name } name={ name } value={ String(value) } isFlat={ isFlat } level={ level }/>;
}
}
}, [ level, isFlat ]);
return (
<Accordion allowMultiple fontSize="sm" ml={{ base: level === 0 ? 0 : 6, lg: `${ ml }px` }} defaultIndex={ level === 0 ? [ 0 ] : undefined }>
{ Object.entries(data).map(([ key, value ]) => renderItem(key, value)) }
{ Object.entries(data).sort(sortFields).map(([ key, value ]) => renderItem(key, value)) }
</Accordion>
);
};
......
......@@ -7,3 +7,24 @@ export function formatName(_name: string) {
return _upperFirst(name.trim());
}
const PINNED_FIELDS = [ 'name', 'description' ];
export function sortFields([ nameA ]: [string, unknown], [ nameB ]: [string, unknown]): number {
const pinnedIndexA = PINNED_FIELDS.indexOf(nameA.toLowerCase());
const pinnedIndexB = PINNED_FIELDS.indexOf(nameB.toLowerCase());
if (pinnedIndexA === -1 && pinnedIndexB === -1) {
return 0;
}
if (pinnedIndexB === -1) {
return -1;
}
if (pinnedIndexA === -1) {
return 1;
}
return pinnedIndexA > pinnedIndexB ? 1 : -1;
}
import { Hide, HStack, Show, Text } from '@chakra-ui/react';
import { Hide, HStack, Show } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
......@@ -9,15 +9,13 @@ import useDebounce from 'lib/hooks/useDebounce';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities';
import TOKEN_TYPE from 'lib/token/tokenTypes';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay';
import FilterInput from 'ui/shared/filters/FilterInput';
import PopoverFilter from 'ui/shared/filters/PopoverFilter';
import TokenTypeFilter from 'ui/shared/filters/TokenTypeFilter';
import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import TokensListItem from './TokensListItem';
import TokensTable from './TokensTable';
......@@ -67,7 +65,7 @@ const Tokens = () => {
/>
);
const bar = (
const actionBar = (
<>
<HStack spacing={ 3 } mb={ 6 } display={{ base: 'flex', lg: 'none' }}>
{ typeFilter }
......@@ -83,36 +81,28 @@ const Tokens = () => {
</>
);
if (isLoading) {
return (
<>
{ bar }
<SkeletonList display={{ base: 'block', lg: 'none' }}/>
<SkeletonTable display={{ base: 'none', lg: 'block' }} columns={ [ '25px', '33%', '33%', '33%', '110px' ] }/>
</>
);
}
if (!data.items.length) {
if (debouncedFilter || type) {
return (
<>
{ bar }
<EmptySearchResult text={ `Couldn${ apos }t find token that matches your filter query.` }/>;
</>
);
}
return <Text as="span">There are no tokens</Text>;
}
return (
const content = data?.items ? (
<>
{ bar }
<Show below="lg" ssr={ false }>
{ data.items.map((item, index) => <TokensListItem key={ item.address } token={ item } index={ index } page={ pagination.page }/>) }
</Show>
<Hide below="lg" ssr={ false }><TokensTable items={ data.items } page={ pagination.page }/></Hide>
</>
<Hide below="lg" ssr={ false }><TokensTable items={ data.items } page={ pagination.page }/></Hide></>
) : null;
return (
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '25px', '33%', '33%', '33%', '110px' ] }}
emptyText="There are no tokens."
filterProps={{
emptyFilteredText: `Couldn${ apos }t find token that matches your filter query.`,
hasActiveFilters: Boolean(debouncedFilter || type),
}}
content={ content }
actionBar={ actionBar }
/>
);
};
......
......@@ -7,7 +7,7 @@ import getCurrencyValue from 'lib/getCurrencyValue';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TokenLogo from 'ui/shared/TokenLogo';
type Props = {
......
import { Box, Text, Show, Hide } from '@chakra-ui/react';
import { Show, Hide } from '@chakra-ui/react';
import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import { SECOND } from 'lib/consts';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
// import { apos } from 'lib/html-entities';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay';
// import FilterInput from 'ui/shared/filters/FilterInput';
// import TxInternalsFilter from 'ui/tx/internals/TxInternalsFilter';
import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import TxInternalsList from 'ui/tx/internals/TxInternalsList';
import TxInternalsTable from 'ui/tx/internals/TxInternalsTable';
import type { Sort, SortField } from 'ui/tx/internals/utils';
......@@ -82,8 +78,6 @@ const TxInternals = () => {
},
});
const isMobile = useIsMobile();
// const handleFilterChange = React.useCallback((nextValue: Array<TxInternalsType>) => {
// setFilters(nextValue);
// }, []);
......@@ -98,52 +92,43 @@ const TxInternals = () => {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
}
if (isLoading || txInfo.isLoading) {
return (
<>
<Show below="lg" ssr={ false }><SkeletonList/></Show>
<Hide below="lg" ssr={ false }><SkeletonTable columns={ [ '28%', '20%', '24px', '20%', '16%', '16%' ] }/></Hide>
</>
);
}
if (isError || txInfo.isError) {
return <DataFetchAlert/>;
}
if (data.items.length === 0) {
return <Text as="span">There are no internal transactions for this transaction.</Text>;
}
const content = (() => {
const filteredData = data.items
.slice()
// .filter(({ type }) => filters.length > 0 ? filters.includes(type) : true)
// .filter(searchFn(searchTerm))
.sort(sortFn(sort));
if (filteredData.length === 0) {
return <EmptySearchResult text={ `Couldn${ apos }t find any transaction that matches your query.` }/>;
}
return isMobile ?
<TxInternalsList data={ filteredData }/> :
<TxInternalsTable data={ filteredData } sort={ sort } onSortToggle={ handleSortToggle } top={ isPaginationVisible ? 80 : 0 }/>;
})();
const filteredData = data?.items
.slice()
// .filter(({ type }) => filters.length > 0 ? filters.includes(type) : true)
// .filter(searchFn(searchTerm))
.sort(sortFn(sort));
const content = filteredData ? (
<>
<Show below="lg" ssr={ false }><TxInternalsList data={ filteredData }/></Show>
<Hide below="lg" ssr={ false }>
<TxInternalsTable data={ filteredData } sort={ sort } onSortToggle={ handleSortToggle } top={ isPaginationVisible ? 80 : 0 }/>
</Hide>
</>
) : null;
const actionBar = isPaginationVisible ? (
<ActionBar mt={ -6 }>
{ /* <TxInternalsFilter onFilterChange={ handleFilterChange } defaultFilters={ filters } appliedFiltersNum={ filters.length }/> */ }
{ /* <FilterInput onChange={ setSearchTerm } maxW="360px" ml={ 3 } size="xs" placeholder="Search by addresses, hash, method..."/> */ }
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) : null;
return (
<Box>
{ isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) }
{ /* <Flex mb={ 6 }>
<TxInternalsFilter onFilterChange={ handleFilterChange } defaultFilters={ filters } appliedFiltersNum={ filters.length }/>
<FilterInput onChange={ setSearchTerm } maxW="360px" ml={ 3 } size="xs" placeholder="Search by addresses, hash, method..."/>
</Flex> */ }
{ content }
</Box>
<DataListDisplay
isError={ isError || txInfo.isError }
isLoading={ isLoading || txInfo.isLoading }
items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '28%', '20%', '24px', '20%', '16%', '16%' ] }}
emptyText="There are no internal transactions for this transaction."
// filterProps={{
// emptyFilteredText: `Couldn${ apos }t find any transaction that matches your query.`.
// hasActiveFilters: Boolean(filters.length || searchTerm),
// }}
content={ content }
actionBar={ actionBar }
/>
);
};
......
import { Hide, Show, Text } from '@chakra-ui/react';
import { Hide, Show } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -9,12 +9,10 @@ import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities';
import TOKEN_TYPE from 'lib/token/tokenTypes';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
......@@ -57,56 +55,47 @@ const TxTokenTransfer = () => {
const numActiveFilters = typeFilter.length;
const isActionBarHidden = !numActiveFilters && !tokenTransferQuery.data?.items.length;
const content = (() => {
if (txsInfo.isLoading || tokenTransferQuery.isLoading) {
return (
<>
<Hide below="lg" ssr={ false }>
<SkeletonTable columns={ [ '185px', '25%', '25%', '25%', '25%' ] }
/>
</Hide>
<Show below="lg" ssr={ false }>
<SkeletonList/>
</Show>
</>
);
}
if (!tokenTransferQuery.data.items?.length && !numActiveFilters) {
return <Text as="span">There are no token transfers</Text>;
}
if (!tokenTransferQuery.data.items?.length) {
return <EmptySearchResult text={ `Couldn${ apos }t find any token transfer that matches your query.` }/>;
}
const items = tokenTransferQuery.data.items.reduce(flattenTotal, []);
return (
<>
<Hide below="lg" ssr={ false }>
<TokenTransferTable data={ items } top={ isActionBarHidden ? 0 : 80 }/>
</Hide>
<Show below="lg" ssr={ false }>
<TokenTransferList data={ items }/>
</Show>
</>
);
})();
const items = tokenTransferQuery.data?.items?.reduce(flattenTotal, []);
return (
const content = items ? (
<>
{ !isActionBarHidden && (
<ActionBar mt={ -6 }>
<TokenTransferFilter
defaultTypeFilters={ typeFilter }
onTypeFilterChange={ handleTypeFilterChange }
appliedFiltersNum={ numActiveFilters }
/>
{ tokenTransferQuery.isPaginationVisible && <Pagination ml="auto" { ...tokenTransferQuery.pagination }/> }
</ActionBar>
) }
{ content }
<Hide below="lg" ssr={ false }>
<TokenTransferTable data={ items } top={ isActionBarHidden ? 0 : 80 }/>
</Hide>
<Show below="lg" ssr={ false }>
<TokenTransferList data={ items }/>
</Show>
</>
) : null;
const actionBar = !isActionBarHidden ? (
<ActionBar mt={ -6 }>
<TokenTransferFilter
defaultTypeFilters={ typeFilter }
onTypeFilterChange={ handleTypeFilterChange }
appliedFiltersNum={ numActiveFilters }
/>
{ tokenTransferQuery.isPaginationVisible && <Pagination ml="auto" { ...tokenTransferQuery.pagination }/> }
</ActionBar>
) : null;
return (
<DataListDisplay
isError={ txsInfo.isError || tokenTransferQuery.isError }
isLoading={ txsInfo.isLoading || tokenTransferQuery.isLoading }
items={ tokenTransferQuery.data?.items }
skeletonProps={{
isLongSkeleton: true,
skeletonDesktopColumns: [ '185px', '25%', '25%', '25%', '25%' ],
}}
emptyText="There are no token transfers."
filterProps={{
emptyFilteredText: `Couldn${ apos }t find any token transfer that matches your query.`,
hasActiveFilters: Boolean(numActiveFilters),
}}
content={ content }
actionBar={ actionBar }
/>
);
};
......
......@@ -9,7 +9,7 @@ import eastArrowIcon from 'icons/arrows/east.svg';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import ListItemMobile from 'ui/shared/ListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
......
......@@ -6,7 +6,7 @@ import type { TxStateChange } from 'types/api/txStateChanges';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import ListItemMobile from 'ui/shared/ListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import { getStateElements } from './utils';
......
import { Box, Icon, VStack } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { TxnBatchesItem } from 'types/api/txnBatches';
import appConfig from 'configs/app/config';
import txIcon from 'icons/transactions.svg';
import txBatchIcon from 'icons/txBatch.svg';
import dayjs from 'lib/date/dayjs';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
type Props = { item: TxnBatchesItem };
const TxnBatchesListItem = ({ item }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow();
const items = [
{
name: 'L2 block #',
value: (
<LinkInternal
fontWeight={ 600 }
display="flex"
width="fit-content"
alignItems="center"
href={ route({ pathname: '/block/[height]', query: { height: item.l2_block_number.toString() } }) }
>
<Icon as={ txBatchIcon } boxSize={ 6 } mr={ 1 }/>
{ item.l2_block_number }
</LinkInternal>
),
},
{
name: 'L2 block txn count',
value: (
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: item.l2_block_number.toString(), tab: 'txs' } }) }>
{ item.tx_count }
</LinkInternal>
),
},
{
name: 'Epoch number',
value: (
<LinkExternal
fontWeight={ 600 }
display="inline-flex"
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height]', query: { height: item.epoch_number.toString() } }) }
>
{ item.epoch_number }
</LinkExternal>
),
},
{
name: 'L1 txn hash',
value: (
<VStack spacing={ 3 } w="100%" overflow="hidden">
{ item.l1_tx_hashes.map(hash => (
<LinkExternal
maxW="100%"
display="inline-flex"
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: hash } }) }
key={ hash }
>
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ hash }/></Box>
</LinkExternal>
)) }
</VStack>
),
},
{
name: 'Age',
value: timeAgo,
},
];
return <ListItemMobileGrid items={ items } gridTemplateColumns="100px auto"/>;
};
export default TxnBatchesListItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { TxnBatchesItem } from 'types/api/txnBatches';
import { default as Thead } from 'ui/shared/TheadSticky';
import TxnBatchesTableItem from './TxnBatchesTableItem';
type Props = {
items: Array<TxnBatchesItem>;
top: number;
}
const TxnBatchesTable = ({ items, top }: Props) => {
return (
<Table variant="simple" size="sm" minW="850px">
<Thead top={ top }>
<Tr>
<Th width="170px">L2 block #</Th>
<Th width="170px">L2 block txn count</Th>
<Th width="160px">Epoch number</Th>
<Th width="100%">L1 txn hash</Th>
<Th width="150px">Age</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item) => (
<TxnBatchesTableItem key={ item.l2_block_number } item={ item }/>
)) }
</Tbody>
</Table>
);
};
export default TxnBatchesTable;
import { Box, Td, Tr, Text, Icon, VStack } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { TxnBatchesItem } from 'types/api/txnBatches';
import appConfig from 'configs/app/config';
import txIcon from 'icons/transactions.svg';
import txBatchIcon from 'icons/txBatch.svg';
import dayjs from 'lib/date/dayjs';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
type Props = { item: TxnBatchesItem };
const TxnBatchesTableItem = ({ item }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow();
return (
<Tr>
<Td>
<LinkInternal
fontWeight={ 600 }
display="flex"
width="fit-content"
alignItems="center"
href={ route({ pathname: '/block/[height]', query: { height: item.l2_block_number.toString() } }) }
>
<Icon as={ txBatchIcon } boxSize={ 6 } mr={ 1 }/>
{ item.l2_block_number }
</LinkInternal>
</Td>
<Td>
<LinkInternal
href={ route({ pathname: '/block/[height]', query: { height: item.l2_block_number.toString(), tab: 'txs' } }) }
lineHeight="24px"
>
{ item.tx_count }
</LinkInternal>
</Td>
<Td>
<LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height]', query: { height: item.epoch_number.toString() } }) }
fontWeight={ 600 }
lineHeight="24px"
display="inline-flex"
>
{ item.epoch_number }
</LinkExternal>
</Td>
<Td pr={ 12 }>
<VStack spacing={ 3 }>
{ item.l1_tx_hashes.map(hash => (
<LinkExternal
maxW="100%"
display="inline-flex"
key={ hash }
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: hash } }) }
>
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ hash }/></Box>
</LinkExternal>
)) }
</VStack>
</Td>
<Td>
<Text variant="secondary" lineHeight="24px">{ timeAgo }</Text>
</Td>
</Tr>
);
};
export default TxnBatchesTableItem;
import { Text, Box, Show, Hide } from '@chakra-ui/react';
import { Box, Show, Hide } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
......@@ -6,10 +6,8 @@ import type { TxsResponse } from 'types/api/transaction';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressCsvExportLink from 'ui/address/AddressCsvExportLink';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TxsHeaderMobile from './TxsHeaderMobile';
......@@ -50,92 +48,75 @@ const TxsContent = ({
const { data, isLoading, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query);
const isMobile = useIsMobile();
const content = (() => {
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<>
<Show below="lg" ssr={ false }><SkeletonList/></Show>
<Hide below="lg" ssr={ false }>
<SkeletonTable
columns={ showBlockInfo ?
[ '32px', '22%', '160px', '20%', '18%', '292px', '20%', '20%' ] :
[ '32px', '22%', '160px', '20%', '292px', '20%', '20%' ]
}
isLong={ hasLongSkeleton }
/>
</Hide>
</>
);
}
const txs = data.items;
if (!txs.length) {
return <Text as="span">There are no transactions.</Text>;
}
return (
<>
<Show below="lg" ssr={ false }>
<Box>
{ showSocketInfo && (
<SocketNewItemsNotice
url={ window.location.href }
num={ socketInfoNum }
alert={ socketInfoAlert }
borderBottomRadius={ 0 }
>
{ ({ content }) => <Box>{ content }</Box> }
</SocketNewItemsNotice>
) }
{ txs.map(tx => (
<TxsListItem
tx={ tx }
key={ tx.hash }
showBlockInfo={ showBlockInfo }
currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
/>
)) }
</Box>
</Show>
<Hide below="lg" ssr={ false }>
<TxsTable
txs={ txs }
sort={ setSortByField }
sorting={ sorting }
showBlockInfo={ showBlockInfo }
showSocketInfo={ showSocketInfo }
socketInfoAlert={ socketInfoAlert }
socketInfoNum={ socketInfoNum }
top={ top || query.isPaginationVisible ? 80 : 0 }
currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
/>
</Hide>
</>
);
})();
return (
const content = data?.items ? (
<>
{ isMobile && (
<TxsHeaderMobile
mt={ -6 }
<Show below="lg" ssr={ false }>
<Box>
{ showSocketInfo && (
<SocketNewItemsNotice
url={ window.location.href }
num={ socketInfoNum }
alert={ socketInfoAlert }
borderBottomRadius={ 0 }
>
{ ({ content }) => <Box>{ content }</Box> }
</SocketNewItemsNotice>
) }
{ data.items.map(tx => (
<TxsListItem
tx={ tx }
key={ tx.hash }
showBlockInfo={ showBlockInfo }
currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
/>
)) }
</Box>
</Show>
<Hide below="lg" ssr={ false }>
<TxsTable
txs={ data.items }
sort={ setSortByField }
sorting={ sorting }
setSorting={ setSortByValue }
paginationProps={ query.pagination }
showPagination={ query.isPaginationVisible }
filterComponent={ filter }
linkSlot={ currentAddress ? <AddressCsvExportLink address={ currentAddress } type="transactions" ml={ 2 }/> : null }
showBlockInfo={ showBlockInfo }
showSocketInfo={ showSocketInfo }
socketInfoAlert={ socketInfoAlert }
socketInfoNum={ socketInfoNum }
top={ top || query.isPaginationVisible ? 80 : 0 }
currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
/>
) }
{ content }
</Hide>
</>
) : null;
const actionBar = isMobile ? (
<TxsHeaderMobile
mt={ -6 }
sorting={ sorting }
setSorting={ setSortByValue }
paginationProps={ query.pagination }
showPagination={ query.isPaginationVisible }
filterComponent={ filter }
linkSlot={ currentAddress ? <AddressCsvExportLink address={ currentAddress } type="transactions" ml={ 2 }/> : null }
/>
) : null;
return (
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
items={ data?.items }
skeletonProps={{
isLongSkeleton: hasLongSkeleton,
skeletonDesktopColumns: showBlockInfo ?
[ '32px', '22%', '160px', '20%', '18%', '292px', '20%', '20%' ] :
[ '32px', '22%', '160px', '20%', '292px', '20%', '20%' ],
}}
emptyText="There are no transactions."
content={ content }
actionBar={ actionBar }
/>
);
};
......
......@@ -20,7 +20,7 @@ import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxType from 'ui/txs/TxType';
......
import { Flex, Skeleton } from '@chakra-ui/react';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import VerifiedContractsCountersItem from './VerifiedContractsCountersItem';
const VerifiedContractsCounters = () => {
const countersQuery = useApiQuery('verified_contracts_counters');
if (countersQuery.isError) {
return null;
}
const content = (() => {
if (countersQuery.isLoading) {
const item = <Skeleton w={{ base: '100%', lg: 'calc((100% - 12px)/2)' }} h="69px" borderRadius="12px"/>;
return (
<>
{ item }
{ item }
</>
);
}
return (
<>
<VerifiedContractsCountersItem
name="Total contracts"
total={ countersQuery.data.smart_contracts }
new24={ countersQuery.data.new_smart_contracts_24h }
/>
<VerifiedContractsCountersItem
name="Verified contracts"
total={ countersQuery.data.verified_smart_contracts }
new24={ countersQuery.data.new_verified_smart_contracts_24h }
/>
</>
);
})();
return (
<Flex columnGap={ 3 } rowGap={ 3 } flexDirection={{ base: 'column', lg: 'row' }} mb={ 6 }>
{ content }
</Flex>
);
};
export default VerifiedContractsCounters;
import { Box, Flex, Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
type Props = {
name: string;
total: string;
new24: string;
}
const VerifiedContractsCountersItem = ({ name, total, new24 }: Props) => {
const itemBgColor = useColorModeValue('blue.50', 'blue.800');
return (
<Box
w={{ base: '100%', lg: 'calc((100% - 12px)/2)' }}
borderRadius="12px"
backgroundColor={ itemBgColor }
p={ 3 }
>
<Text variant="secondary" fontSize="xs">{ name }</Text>
<Flex alignItems="baseline">
<Text fontWeight={ 600 } mr={ 2 } fontSize="lg">{ Number(total).toLocaleString('en') }</Text>
{ Number(new24) > 0 && (
<>
<Text fontWeight={ 600 } mr={ 1 } fontSize="lg" color="green.500">+{ Number(new24).toLocaleString('en') }</Text>
<Text variant="secondary" fontSize="sm">(24h)</Text>
</>
) }
</Flex>
</Box>
);
};
export default VerifiedContractsCountersItem;
......@@ -13,7 +13,7 @@ import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import HashStringShorten from 'ui/shared/HashStringShorten';
import ListItemMobile from 'ui/shared/ListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
interface Props {
data: VerifiedContract;
......
......@@ -6,7 +6,7 @@ import type { TWatchlistItem } from 'types/client/account';
import useApiFetch from 'lib/api/useApiFetch';
import useToast from 'lib/hooks/useToast';
import ListItemMobile from 'ui/shared/ListItemMobile';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import WatchListAddressItem from './WatchListAddressItem';
......
import { Box, Icon } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { WithdrawalsItem } from 'types/api/withdrawals';
import appConfig from 'configs/app/config';
import txIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
type Props = { item: WithdrawalsItem };
const WithdrawalsListItem = ({ item }: Props) => {
const timeAgo = item.l2_timestamp ? dayjs(item.l2_timestamp).fromNow() : '';
const timeToEnd = item.challenge_period_end ? dayjs(item.challenge_period_end).fromNow(true) + ' left' : '';
const items = [
{
name: 'Msg nonce',
value: item.msg_nonce_version + '-' + item.msg_nonce,
},
{
name: 'From',
value: item.from ? (
<Address>
<AddressIcon address={ item.from }/>
<AddressLink hash={ item.from?.hash } type="address" truncation="dynamic" ml={ 2 }/>
</Address>
) : null,
},
{
name: 'L2 txn hash',
value: (
<LinkInternal
href={ route({ pathname: '/tx/[hash]', query: { hash: item.l2_tx_hash } }) }
display="flex"
width="fit-content"
alignItems="center"
overflow="hidden"
w="100%"
>
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l2_tx_hash }/></Box>
</LinkInternal>
),
},
{
name: 'Age',
value: timeAgo,
},
{
name: 'Status',
value: item.status === 'Ready for relay' ?
<LinkExternal href={ appConfig.L2.withdrawalUrl }>{ item.status }</LinkExternal> :
item.status,
},
{
name: 'L1 txn hash',
value: item.l1_tx_hash ? (
<LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
maxW="100%"
display="inline-flex"
overflow="hidden"
>
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box>
</LinkExternal>
) : null,
},
{
name: 'Time left',
value: timeToEnd,
},
];
return <ListItemMobileGrid items={ items } gridTemplateColumns="92px auto"/>;
};
export default WithdrawalsListItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { WithdrawalsItem } from 'types/api/withdrawals';
import { default as Thead } from 'ui/shared/TheadSticky';
import WithdrawalsTableItem from './WithdrawalsTableItem';
type Props = {
items: Array<WithdrawalsItem>;
top: number;
}
const WithdrawalsTable = ({ items, top }: Props) => {
return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }>
<Tr>
<Th>Msg nonce</Th>
<Th>From</Th>
<Th>L2 txn hash</Th>
<Th>Age</Th>
<Th>Status</Th>
<Th>L1 txn hash</Th>
<Th>Time left</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item) => (
<WithdrawalsTableItem key={ item.l2_tx_hash } item={ item }/>
)) }
</Tbody>
</Table>
);
};
export default WithdrawalsTable;
import { Box, Td, Tr, Text, Icon } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import type { WithdrawalsItem } from 'types/api/withdrawals';
import appConfig from 'configs/app/config';
import txIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import HashStringShorten from 'ui/shared/HashStringShorten';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
type Props = { item: WithdrawalsItem };
const WithdrawalsTableItem = ({ item }: Props) => {
const timeAgo = item.l2_timestamp ? dayjs(item.l2_timestamp).fromNow() : 'N/A';
const timeToEnd = item.challenge_period_end ? dayjs(item.challenge_period_end).fromNow(true) + ' left' : '-';
return (
<Tr>
<Td verticalAlign="middle" fontWeight={ 600 }>
<Text>{ item.msg_nonce_version + '-' + item.msg_nonce }</Text>
</Td>
<Td verticalAlign="middle">
{ item.from ? (
<Address>
<AddressIcon address={ item.from }/>
<AddressLink hash={ item.from.hash } type="address" truncation="constant" ml={ 2 }/>
</Address>
) : 'N/A' }
</Td>
<Td verticalAlign="middle">
<LinkInternal
href={ route({ pathname: '/tx/[hash]', query: { hash: item.l2_tx_hash } }) }
display="flex"
width="fit-content"
alignItems="center"
>
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShorten hash={ item.l2_tx_hash }/></Box>
</LinkInternal>
</Td>
<Td verticalAlign="middle" pr={ 12 }>
<Text variant="secondary">{ timeAgo }</Text>
</Td>
<Td verticalAlign="middle">
{ item.status === 'Ready for relay' ?
<LinkExternal href={ appConfig.L2.withdrawalUrl }>{ item.status }</LinkExternal> :
<Text>{ item.status }</Text>
}
</Td>
<Td verticalAlign="middle">
{ item.l1_tx_hash ? (
<LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
>
<HashStringShorten hash={ item.l1_tx_hash }/>
</LinkExternal>
) :
'N/A'
}
</Td>
<Td verticalAlign="middle">
<Text variant="secondary">{ timeToEnd }</Text>
</Td>
</Tr>
);
};
export default WithdrawalsTableItem;
......@@ -3045,6 +3045,21 @@
resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz#af577b477c683fad17c619a78208cede06f9605c"
integrity sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q==
"@monaco-editor/loader@^1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.3.2.tgz#04effbb87052d19cd7d3c9d81c0635490f9bb6d8"
integrity sha512-BTDbpHl3e47r3AAtpfVFTlAi7WXv4UQ/xZmz8atKl4q7epQV5e7+JbigFDViWF71VBi4IIBdcWP57Hj+OWuc9g==
dependencies:
state-local "^1.0.6"
"@monaco-editor/react@^4.4.6":
version "4.4.6"
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.4.6.tgz#8ae500b0edf85276d860ed702e7056c316548218"
integrity sha512-Gr3uz3LYf33wlFE3eRnta4RxP5FSNxiIV9ENn2D2/rN8KgGAD8ecvcITRtsbbyuOuNkwbuHYxfeaz2Vr+CtyFA==
dependencies:
"@monaco-editor/loader" "^1.3.2"
prop-types "^15.7.2"
"@motionone/animation@^10.12.0":
version "10.14.0"
resolved "https://registry.yarnpkg.com/@motionone/animation/-/animation-10.14.0.tgz#2f2a3517183bb58d82e389aac777fe0850079de6"
......@@ -5327,11 +5342,6 @@ abort-controller@^3.0.0:
dependencies:
event-target-shim "^5.0.0"
ace-builds@^1.14.0, ace-builds@^1.4.14:
version "1.14.0"
resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.14.0.tgz#85a6733b4fa17b0abc3dbfe38cd8d823cad79716"
integrity sha512-3q8LvawomApRCt4cC0OzxVjDsZ609lDbm8l0Xl9uqG06dKEq4RT0YXLUyk7J2SxmqIp5YXzZNw767Dr8GKUruw==
acorn-globals@^7.0.0:
version "7.0.1"
resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-7.0.1.tgz#0dbf05c44fa7c94332914c02066d5beff62c40c3"
......@@ -6403,6 +6413,20 @@ css-box-model@1.2.1:
dependencies:
tiny-invariant "^1.0.6"
css-loader@^6.7.3:
version "6.7.3"
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.3.tgz#1e8799f3ccc5874fdd55461af51137fcc5befbcd"
integrity sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==
dependencies:
icss-utils "^5.1.0"
postcss "^8.4.19"
postcss-modules-extract-imports "^3.0.0"
postcss-modules-local-by-default "^4.0.0"
postcss-modules-scope "^3.0.0"
postcss-modules-values "^4.0.0"
postcss-value-parser "^4.2.0"
semver "^7.3.8"
css-select@^4.1.3:
version "4.3.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b"
......@@ -6432,6 +6456,11 @@ css.escape@1.5.1:
resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb"
integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
csso@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529"
......@@ -6866,11 +6895,6 @@ detect-node-es@^1.1.0:
resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493"
integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==
diff-match-patch@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37"
integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==
diff-sequences@^29.3.1:
version "29.3.1"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.3.1.tgz#104b5b95fe725932421a9c6e5b4bef84c3f2249e"
......@@ -8504,6 +8528,11 @@ iconv-lite@0.6, iconv-lite@0.6.3:
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
icss-utils@^5.0.0, icss-utils@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
ieee754@^1.1.13, ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
......@@ -9710,12 +9739,7 @@ lodash.debounce@^4, lodash.debounce@^4.0.8:
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
lodash.get@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==
lodash.isequal@4.5.0, lodash.isequal@^4.5.0:
lodash.isequal@4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
......@@ -9946,6 +9970,11 @@ mockdate@^3.0.5:
resolved "https://registry.yarnpkg.com/mockdate/-/mockdate-3.0.5.tgz#789be686deb3149e7df2b663d2bc4392bc3284fb"
integrity sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==
monaco-editor@^0.34.1:
version "0.34.1"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.34.1.tgz#1b75c4ad6bc4c1f9da656d740d98e0b850a22f87"
integrity sha512-FKc80TyiMaruhJKKPz5SpJPIjL+dflGvz4CpuThaPMc94AyN7SeC9HQ8hrvaxX7EyHdJcUY5i4D0gNyJj1vSZQ==
motion@10.15.3:
version "10.15.3"
resolved "https://registry.yarnpkg.com/motion/-/motion-10.15.3.tgz#4a9f63a751dcf83c195f1192a069caebed59112f"
......@@ -10639,6 +10668,47 @@ popmotion@11.0.3:
style-value-types "5.0.0"
tslib "^2.1.0"
postcss-modules-extract-imports@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d"
integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==
postcss-modules-local-by-default@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c"
integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==
dependencies:
icss-utils "^5.0.0"
postcss-selector-parser "^6.0.2"
postcss-value-parser "^4.1.0"
postcss-modules-scope@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06"
integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==
dependencies:
postcss-selector-parser "^6.0.4"
postcss-modules-values@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c"
integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==
dependencies:
icss-utils "^5.0.0"
postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
version "6.0.11"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc"
integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==
dependencies:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@8.4.14:
version "8.4.14"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf"
......@@ -10648,7 +10718,7 @@ postcss@8.4.14:
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.4.21:
postcss@^8.4.19, postcss@^8.4.21:
version "8.4.21"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4"
integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==
......@@ -10876,17 +10946,6 @@ randombytes@^2.1.0:
dependencies:
safe-buffer "^5.1.0"
react-ace@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-10.1.0.tgz#d348eac2b16475231779070b6cd16768deed565f"
integrity sha512-VkvUjZNhdYTuKOKQpMIZi7uzZZVgzCjM7cLYu6F64V0mejY8a2XTyPUIMszC6A4trbeMIHbK5fYFcT/wkP/8VA==
dependencies:
ace-builds "^1.4.14"
diff-match-patch "^1.0.5"
lodash.get "^4.4.2"
lodash.isequal "^4.5.0"
prop-types "^15.7.2"
react-async-script@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/react-async-script/-/react-async-script-1.2.0.tgz#ab9412a26f0b83f5e2e00de1d2befc9400834b21"
......@@ -11556,7 +11615,7 @@ secure-json-parse@^2.4.0:
resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.5.0.tgz#f929829df2adc7ccfb53703569894d051493a6ac"
integrity sha512-ZQruFgZnIWH+WyO9t5rWt4ZEGqCKPwhiw+YbzTwpmT9elgLrLcfuyUiSnwwjUiVy9r4VM3urtbNF1xmEh9IL2w==
semver@7.x, semver@^7.3.5, semver@^7.3.7:
semver@7.x, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8:
version "7.3.8"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
......@@ -11754,6 +11813,11 @@ stack-utils@^2.0.3:
dependencies:
escape-string-regexp "^2.0.0"
state-local@^1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/state-local/-/state-local-1.0.7.tgz#da50211d07f05748d53009bee46307a37db386d5"
integrity sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==
stream-browserify@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f"
......@@ -11959,6 +12023,11 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
style-loader@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575"
integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==
style-value-types@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-5.0.0.tgz#76c35f0e579843d523187989da866729411fc8ad"
......@@ -12511,7 +12580,7 @@ utf-8-validate@^5.0.2:
dependencies:
node-gyp-build "^4.3.0"
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
......
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