├── .dockerignore ├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── Dockerfile ├── README.md ├── components ├── address │ ├── internal-txs-table │ │ └── index.tsx │ ├── token-txs-table │ │ └── index.tsx │ └── txs-table │ │ └── index.tsx ├── blockchain │ ├── batches-table-card │ │ └── index.tsx │ ├── blocks-table-card │ │ └── index.tsx │ ├── internal-txs-table │ │ └── index.tsx │ ├── pending-txs-table │ │ └── index.tsx │ ├── tx-internal-detail-table │ │ └── index.tsx │ └── txs-table │ │ └── index.tsx ├── common │ ├── address-avatar │ │ └── index.tsx │ ├── address-qrcode │ │ └── index.tsx │ ├── chart │ │ └── index.tsx │ ├── dot-text │ │ ├── index.module.scss │ │ └── index.tsx │ ├── link │ │ └── index.tsx │ ├── loading │ │ ├── index.module.scss │ │ └── index.tsx │ ├── overview-content │ │ └── index.tsx │ ├── page-title │ │ └── index.tsx │ ├── pagination.tsx │ ├── search-input │ │ └── index.tsx │ ├── skeleton-table.tsx │ ├── tab-card │ │ └── index.tsx │ └── table-col-components │ │ ├── index.module.scss │ │ └── index.tsx ├── contract │ ├── contract-tab │ │ ├── contract-function.tsx │ │ ├── contract-functions-panel.tsx │ │ └── index.tsx │ ├── verify-contract-page │ │ └── index.tsx │ └── wallet-connector │ │ └── index.tsx └── tokens │ ├── token-detail-holders-table │ └── index.tsx │ ├── token-detail-txs-table │ └── index.tsx │ ├── token-page │ └── index.tsx │ ├── token-table │ └── index.tsx │ └── token-txs-table │ └── index.tsx ├── constants ├── antd-theme-tokens.ts ├── api.ts ├── index.ts └── routes.ts ├── hooks └── common │ └── useMenuCollapsed.ts ├── layout ├── container │ └── index.tsx ├── footer │ ├── index.module.scss │ └── index.tsx ├── header │ └── index.tsx └── menu │ ├── config.tsx │ ├── index.module.scss │ └── index.tsx ├── next.config.js ├── package.json ├── pages ├── 404.tsx ├── _app.tsx ├── address │ └── [address] │ │ ├── index.module.scss │ │ └── index.tsx ├── api │ ├── [...trpc].ts │ ├── openapi.json.ts │ └── trpc │ │ └── [trpc].ts ├── batches │ ├── [batch] │ │ ├── blocks │ │ │ └── index.tsx │ │ ├── index.module.scss │ │ └── index.tsx │ └── index.tsx ├── blocks │ ├── [block] │ │ ├── index.module.scss │ │ └── index.tsx │ └── index.tsx ├── charts │ ├── [chart] │ │ └── index.tsx │ └── index.tsx ├── home │ ├── index.module.scss │ └── index.tsx ├── index.tsx ├── publishContract │ └── [contractAddress] │ │ └── index.tsx ├── token │ └── [token] │ │ └── index.tsx ├── tokens-nft │ └── index.tsx ├── tokens-nft1155 │ └── index.tsx ├── tokens │ └── index.tsx ├── tx │ └── [tx] │ │ └── index.tsx ├── txs │ └── index.tsx └── verifyContract │ └── index.tsx ├── prisma ├── fix-id.sh ├── schema.prisma └── schema.sql ├── public ├── imgs │ ├── a.sol │ ├── back.png │ ├── charts │ │ ├── area.png │ │ └── line.png │ ├── contract │ │ ├── arrow.png │ │ ├── copy.png │ │ ├── full.png │ │ ├── info.png │ │ ├── link.png │ │ ├── right.png │ │ ├── verify.png │ │ ├── verify_failed.png │ │ ├── verify_loading.png │ │ └── verify_success.png │ ├── home │ │ ├── block_icon.png │ │ ├── gwei_icon.png │ │ ├── tps_icon.png │ │ └── trans_icon.png │ ├── logo.png │ ├── logo_text.png │ ├── logo_text_unifra.png │ ├── logo_unifra.png │ ├── overview │ │ ├── decimals.png │ │ ├── fee.png │ │ ├── gas.png │ │ ├── gas_limit.png │ │ ├── gas_price.png │ │ ├── gas_used.png │ │ ├── holders.png │ │ ├── logs_icon.png │ │ ├── size.png │ │ ├── supply.png │ │ ├── trans.png │ │ └── value.png │ ├── qa.png │ └── search_bg.png └── svgs │ ├── arrow.svg │ ├── blockchain.svg │ ├── checked.svg │ ├── checking.svg │ ├── home.svg │ ├── hourglass.svg │ ├── logo_text.svg │ ├── resource.svg │ ├── search.svg │ ├── time.svg │ ├── toggle_menu.svg │ ├── token.svg │ └── verify.svg ├── server ├── context.ts ├── prisma.ts ├── redis.ts ├── routers │ ├── _app.ts │ ├── address.ts │ ├── batch.ts │ ├── block.ts │ ├── contract.ts │ ├── openapi.ts │ ├── stat.ts │ ├── summary.ts │ ├── token.ts │ ├── transaction.ts │ └── util.ts ├── trpc.ts └── verify.ts ├── smart-contract-verifier ├── config.toml └── docker-compose.yml ├── styles ├── global.css └── global.less ├── tsconfig.json ├── types └── index.ts ├── utils ├── blockies.ts ├── index.ts ├── message.ts ├── transformer.ts └── trpc.ts ├── windi.config.ts ├── worker └── index.ts └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | node_modules 4 | npm-debug.log 5 | README.md 6 | .next 7 | .git -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL= 2 | REDIS_URL= 3 | VERIFICATION_URL= 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | build 3 | scripts 4 | analyze 5 | node_modules -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env 30 | *.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | node_modules 39 | node_modules 40 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm format 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Install dependencies only when needed 2 | FROM node:alpine AS deps 3 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 4 | RUN apk add --no-cache libc6-compat 5 | WORKDIR /app 6 | COPY package.json yarn.lock ./ 7 | RUN yarn install --frozen-lockfile 8 | 9 | # Rebuild the source code only when needed 10 | FROM node:alpine AS builder 11 | WORKDIR /app 12 | COPY . . 13 | COPY --from=deps /app/node_modules ./node_modules 14 | RUN yarn build && yarn install --production --ignore-scripts --prefer-offline 15 | 16 | # Production image, copy all the files and run next 17 | FROM node:alpine AS runner 18 | WORKDIR /app 19 | 20 | ENV NODE_ENV production 21 | 22 | RUN addgroup -g 1001 -S nodejs 23 | RUN adduser -S nextjs -u 1001 24 | 25 | # You only need to copy next.config.js if you are NOT using the default configuration 26 | # COPY --from=builder /app/next.config.js ./ 27 | COPY --from=builder /app/public ./public 28 | COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next 29 | COPY --from=builder /app/node_modules ./node_modules 30 | COPY --from=builder /app/package.json ./package.json 31 | 32 | USER nextjs 33 | 34 | EXPOSE 3000 35 | 36 | ENV PORT 3000 37 | 38 | # Next.js collects completely anonymous telemetry data about general usage. 39 | # Learn more here: https://nextjs.org/telemetry 40 | # Uncomment the following line in case you want to disable telemetry. 41 | # ENV NEXT_TELEMETRY_DISABLED 1 42 | 43 | CMD ["node_modules/.bin/next", "start"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scroll Explorer 2 | 3 | Scroll Explorer is the explorer for the [Scroll](https://scroll.io/) Network. It allows you to search for transactions, blocks, and addresses on the Scroll Network. 4 | 5 | ## Installation 6 | 7 | To install this project, follow these steps: 8 | 9 | 1. Clone the repository to your local machine. 10 | 2. Navigate to the project directory. 11 | 3. Run `yarn` to install all dependencies. 12 | 13 | ## Configuration and Setup 14 | 15 | Before running the project, you need to configure and set up the environment. Follow these steps: 16 | 17 | 1. Copy the `.env.example` file to `.env`. 18 | 2. Add the necessary environment variables in the `.env` file. For example: 19 | 20 | ``` 21 | DATABASE_URL="postgres://user:password@localhost:5432/mydatabase" 22 | REDIS_URL="redis://localhost:6379" 23 | VERIFICATION_URL="http://localhost:8050" 24 | ``` 25 | 26 | 3. Run `yarn prisma:push` to create the necessary database tables. 27 | 4. Run `yarn dev` to start the development server. 28 | 5. If you need contract verification, you need to run the verification server. 29 | 30 | ``` 31 | cd smart-contract-verifier 32 | docker-compose up 33 | ``` 34 | 35 | ## Technologies Used 36 | 37 | This project utilizes the following technologies: 38 | 39 | - Next.js 40 | - Prisma 41 | - TRPC 42 | - BullMQ 43 | 44 | ## Contributing 45 | 46 | If you would like to contribute to this project, please follow these steps: 47 | 48 | 1. Fork the repository. 49 | 2. Create a new branch for your feature or bug fix. 50 | 3. Make your changes and commit them. 51 | 4. Push your changes to your forked repository. 52 | 5. Submit a pull request to the main repository. 53 | -------------------------------------------------------------------------------- /components/address/internal-txs-table/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { Table } from 'antd' 4 | import { generate } from 'shortid' 5 | 6 | import { internalTxColumns } from '@/components/blockchain/internal-txs-table' 7 | import SkeletonTable from '@/components/common/skeleton-table' 8 | import { PAGE_SIZE } from '@/constants' 9 | import { getPaginationConfig } from '@/utils' 10 | import { trpc } from '@/utils/trpc' 11 | 12 | const getColumns = () => { 13 | const columns = [...internalTxColumns] 14 | const parentTransactionHashCol = columns[2] 15 | 16 | columns.splice(2, 2) 17 | columns.unshift(parentTransactionHashCol) 18 | 19 | return columns 20 | } 21 | 22 | interface AddressInternalTxsTableProps { 23 | address: string 24 | isContract: boolean | undefined 25 | } 26 | 27 | const AddressInternalTxsTable: React.FC = ({ address, isContract }) => { 28 | const [current, setCurrent] = useState(1) 29 | const [pageSize, setPageSize] = useState(PAGE_SIZE) 30 | 31 | const { isLoading: addressLoading, data: addressInternalTxList } = trpc.address.getAddressInternalTxList.useQuery( 32 | { offset: (current - 1) * pageSize, limit: pageSize, address }, 33 | { enabled: !!address && isContract !== undefined && !isContract } 34 | ) 35 | 36 | const { isLoading: contractLoading, data: contractTxList } = trpc.contract.getContractTransactionList.useQuery( 37 | { offset: (current - 1) * pageSize, limit: pageSize, address }, 38 | { enabled: !!address && isContract !== undefined && isContract } 39 | ) 40 | 41 | return ( 42 | 43 | 56 | 57 | ) 58 | } 59 | 60 | export { AddressInternalTxsTable } 61 | -------------------------------------------------------------------------------- /components/address/token-txs-table/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { Table } from 'antd' 4 | 5 | import SkeletonTable, { SkeletonTableColumnsType } from '@/components/common/skeleton-table' 6 | import { getTokenTxsColumns } from '@/components/tokens/token-txs-table' 7 | import { PAGE_SIZE } from '@/constants' 8 | import { TokenTypeEnum } from '@/types' 9 | import { getPaginationConfig } from '@/utils' 10 | import { trpc } from '@/utils/trpc' 11 | 12 | const AddressTokenTxsTable: React.FC<{ address: string | undefined; type: TokenTypeEnum }> = ({ address = '', type }) => { 13 | const [current, setCurrent] = useState(1) 14 | const [pageSize, setPageSize] = useState(PAGE_SIZE) 15 | 16 | const { isLoading, data } = trpc.address.getAddressTokenTxList.useQuery( 17 | { offset: (current - 1) * pageSize, limit: pageSize, address, tokenType: type }, 18 | { enabled: !!address } 19 | ) 20 | 21 | return ( 22 | 23 |
`${transactionHash}${logIndex}`} 25 | dataSource={data?.list} 26 | columns={getTokenTxsColumns(type)} 27 | pagination={getPaginationConfig({ current, pageSize, total: data?.count, setCurrent, setPageSize })} 28 | /> 29 | 30 | ) 31 | } 32 | 33 | export default AddressTokenTxsTable 34 | -------------------------------------------------------------------------------- /components/address/txs-table/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { Table } from 'antd' 4 | 5 | import { txColumns } from '@/components/blockchain/txs-table' 6 | import SkeletonTable from '@/components/common/skeleton-table' 7 | import { PAGE_SIZE, TABLE_CONFIG } from '@/constants' 8 | import { getPaginationConfig } from '@/utils' 9 | import { trpc } from '@/utils/trpc' 10 | 11 | interface AddressTxsTableProps { 12 | address: string 13 | isContract: boolean | undefined 14 | } 15 | const AddressTxsTable: React.FC = ({ address, isContract }) => { 16 | const [current, setCurrent] = useState(1) 17 | const [pageSize, setPageSize] = useState(PAGE_SIZE) 18 | 19 | const { isLoading: addressLoading, data: addressTxs } = trpc.address.getAddressTxList.useQuery( 20 | { 21 | offset: (current - 1) * pageSize, 22 | limit: pageSize, 23 | address 24 | }, 25 | { enabled: !!address && isContract !== undefined && !isContract } 26 | ) 27 | 28 | const { isLoading: contractLoading, data: contractTxs } = trpc.contract.getContractTransactionList.useQuery( 29 | { offset: (current - 1) * pageSize, limit: pageSize, address }, 30 | { enabled: !!address && isContract !== undefined && isContract } 31 | ) 32 | 33 | return ( 34 | 35 |
42 | 43 | ) 44 | } 45 | 46 | export { AddressTxsTable } 47 | -------------------------------------------------------------------------------- /components/blockchain/batches-table-card/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { Card, Table, Tooltip } from 'antd' 4 | 5 | import Link from '@/components/common/link' 6 | import SkeletonTable from '@/components/common/skeleton-table' 7 | import { L1StatusLabel } from '@/components/common/table-col-components' 8 | import { PAGE_SIZE, TABLE_CONFIG } from '@/constants' 9 | import { BatchType, BlockType, LinkTypeEnum } from '@/types' 10 | import { formatNum, getPaginationConfig, transDisplayTime, transDisplayTimeAgo } from '@/utils' 11 | import { trpc } from '@/utils/trpc' 12 | 13 | const columns = [ 14 | { title: 'L1 Status', width: TABLE_CONFIG.COL_WIDHT.L1_STATUS, render: ({ status }: BlockType) => }, 15 | { title: 'Batch Index', dataIndex: 'idx', render: (idx: number) => }, 16 | { 17 | title: 'Age', 18 | dataIndex: 'commitTime', 19 | render: (commitTime: number) => {transDisplayTimeAgo(commitTime)} 20 | }, 21 | { 22 | title: 'Txn', 23 | dataIndex: 'transactionCount', 24 | render: (num: string) => formatNum(num) 25 | }, 26 | { 27 | title: 'Commit Tx Hash', 28 | dataIndex: 'commitHash', 29 | render: (commitHash: string) => ( 30 | 31 | ) 32 | }, 33 | { 34 | title: 'Finalized Tx Hash', 35 | dataIndex: 'proofHash', 36 | render: (proofHash: string) => ( 37 | 38 | ) 39 | } 40 | ] 41 | 42 | const BatchesTableCard: React.FC = () => { 43 | const [current, setCurrent] = useState(1) 44 | const [pageSize, setPageSize] = useState(PAGE_SIZE) 45 | 46 | const { isLoading, data } = trpc.batch.getBatchList.useQuery({ offset: (current - 1) * pageSize, limit: pageSize }) 47 | 48 | return ( 49 | 50 | 51 |
58 | 59 | 60 | ) 61 | } 62 | 63 | export default BatchesTableCard 64 | -------------------------------------------------------------------------------- /components/blockchain/blocks-table-card/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { Card, Table, Tooltip } from 'antd' 4 | 5 | import Link from '@/components/common/link' 6 | import SkeletonTable, { SkeletonTableColumnsType } from '@/components/common/skeleton-table' 7 | import { L1StatusLabel } from '@/components/common/table-col-components' 8 | import { PAGE_SIZE, TABLE_CONFIG } from '@/constants' 9 | import { BlockType, LinkTypeEnum } from '@/types' 10 | import { convertNum, formatNum, getPaginationConfig, transDisplayTime, transDisplayTimeAgo } from '@/utils' 11 | import { trpc } from '@/utils/trpc' 12 | 13 | const columns = [ 14 | { title: 'L1 Status', width: TABLE_CONFIG.COL_WIDHT.L1_STATUS, render: ({ l1Status }: BlockType) => }, 15 | { title: 'Block', dataIndex: 'blockNumber', render: (num: number | bigint) => }, 16 | { title: 'Age', dataIndex: 'blockTime', render: (time: number) => {transDisplayTimeAgo(time)} }, 17 | { 18 | title: 'Txn', 19 | dataIndex: 'transactionCount', 20 | render: (num: number | bigint, { blockNumber }: BlockType) => ( 21 | 22 | {convertNum(num)} 23 | 24 | ) 25 | }, 26 | { 27 | title: 'Validator', 28 | dataIndex: 'validator', 29 | render: (validator: string) => 30 | }, 31 | { title: 'Gas Used', dataIndex: 'gasUsed', render: (num: string) => formatNum(num) }, 32 | { title: 'Gas Limit', dataIndex: 'gasLimit', render: (num: string) => formatNum(num) } 33 | ] 34 | 35 | const BlocksTableCard: React.FC<{ block_numbers?: number[] }> = props => { 36 | const [current, setCurrent] = useState(1) 37 | const [pageSize, setPageSize] = useState(PAGE_SIZE) 38 | 39 | const { isLoading, data } = trpc.block.getBlockList.useQuery( 40 | { page: current, limit: pageSize, blockNumbers: props.block_numbers }, 41 | { enabled: !props.block_numbers } 42 | ) 43 | 44 | return ( 45 | 46 | 47 |
54 | 55 | 56 | ) 57 | } 58 | 59 | export default BlocksTableCard 60 | -------------------------------------------------------------------------------- /components/blockchain/internal-txs-table/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { Table, Tooltip } from 'antd' 4 | import { useRouter } from 'next/router' 5 | import { generate } from 'shortid' 6 | 7 | import Link from '@/components/common/link' 8 | import SkeletonTable from '@/components/common/skeleton-table' 9 | import { TransArrowIcon } from '@/components/common/table-col-components' 10 | import { PAGE_SIZE, TABLE_CONFIG } from '@/constants' 11 | import { LinkTypeEnum } from '@/types' 12 | import { convertNum, getTxsPaginationConfig, transDisplayNum, transDisplayTime, transDisplayTimeAgo } from '@/utils' 13 | import { trpc } from '@/utils/trpc' 14 | 15 | export const internalTxColumns = [ 16 | { title: 'Block', dataIndex: 'blockNumber', render: (num: number | bigint) => }, 17 | { title: 'Age', dataIndex: 'blockTime', render: (time: number) => {transDisplayTimeAgo(time)} }, 18 | { 19 | title: 'Parent Txn Hash', 20 | dataIndex: 'parentTransactionHash', 21 | render: (parentTransactionHash: string) => 22 | }, 23 | { title: 'Type', dataIndex: 'op' }, 24 | { 25 | title: 'From', 26 | dataIndex: 'from', 27 | render: (from: string) => 28 | }, 29 | { title: '', width: TABLE_CONFIG.COL_WIDHT.TRANS_ARROW_ICON, render: () => }, 30 | { title: 'To', dataIndex: 'to', render: (to: string) => }, 31 | { title: 'Value', dataIndex: 'value', render: (num: string) => transDisplayNum({ num }) } 32 | ] 33 | 34 | const InternalTxsTable: React.FC = () => { 35 | const router = useRouter() 36 | const search: any = router?.query 37 | 38 | const [current, setCurrent] = useState(1) 39 | const [pageSize, setPageSize] = useState(PAGE_SIZE) 40 | 41 | const { isLoading, data } = trpc.transaction.getInternalTransactionList.useQuery({ 42 | offset: (current - 1) * pageSize, 43 | limit: pageSize, 44 | identity: search?.internalBlock 45 | }) 46 | 47 | return ( 48 | 49 |
55 | 56 | ) 57 | } 58 | 59 | export default InternalTxsTable 60 | -------------------------------------------------------------------------------- /components/blockchain/pending-txs-table/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { Table } from 'antd' 4 | 5 | import Link from '@/components/common/link' 6 | import { MethodLabel, TransArrowIcon } from '@/components/common/table-col-components' 7 | import { PAGE_SIZE, TABLE_CONFIG } from '@/constants' 8 | import { LinkTypeEnum } from '@/types' 9 | import { convertGwei, formatNum, getPaginationConfig, transDisplayNum, transDisplayTimeAgo } from '@/utils' 10 | import { trpc } from '@/utils/trpc' 11 | 12 | const columns = [ 13 | { title: 'Txn Hash', dataIndex: 'hash', render: (num: number) => }, 14 | { title: 'Nonce', dataIndex: 'nonce' }, 15 | { title: 'Method', dataIndex: 'methodName', render: (method: string) => }, 16 | { title: 'Last Seen', dataIndex: 'lastSeen', render: (time: number) => transDisplayTimeAgo(time) }, 17 | { title: 'Gas Limit', dataIndex: 'gasLimit', render: (num: string) => formatNum(num) }, 18 | { title: 'Gas Price', dataIndex: 'gasPrice', render: (num: string) => convertGwei(num) }, 19 | { 20 | title: 'From', 21 | dataIndex: 'from', 22 | render: (from: string) => 23 | }, 24 | { title: '', width: TABLE_CONFIG.COL_WIDHT.TRANS_ARROW_ICON, render: () => }, 25 | { title: 'To', dataIndex: 'to', render: (to: string) => }, 26 | { title: 'Value', dataIndex: 'value', render: (num: string) => transDisplayNum({ num }) } 27 | ] 28 | 29 | const PendingTxsTable: React.FC = () => { 30 | const [current, setCurrent] = useState(1) 31 | const [pageSize, setPageSize] = useState(PAGE_SIZE) 32 | 33 | const { data } = trpc.transaction.getPendingTransactionList.useQuery({ offset: (current - 1) * pageSize, limit: pageSize }) 34 | 35 | return ( 36 |
42 | ) 43 | } 44 | 45 | export default PendingTxsTable 46 | -------------------------------------------------------------------------------- /components/blockchain/tx-internal-detail-table/index.tsx: -------------------------------------------------------------------------------- 1 | import { DatabaseFilled } from '@ant-design/icons' 2 | import { Table } from 'antd' 3 | import { generate } from 'shortid' 4 | 5 | import Link from '@/components/common/link' 6 | import Loading from '@/components/common/loading' 7 | import { TransArrowIcon } from '@/components/common/table-col-components' 8 | import { TABLE_CONFIG } from '@/constants' 9 | import { LinkTypeEnum } from '@/types' 10 | import { transDisplayNum } from '@/utils' 11 | import { trpc } from '@/utils/trpc' 12 | 13 | const internalTransferColumns = [ 14 | { title: 'Type Trace Address', dataIndex: 'typeTraceAddress' }, 15 | { title: 'From', dataIndex: 'from', render: (from: string) => }, 16 | { title: '', width: TABLE_CONFIG.COL_WIDHT.TRANS_ARROW_ICON, render: () => }, 17 | { title: 'To', dataIndex: 'to', render: (to: string) => }, 18 | { title: 'Value', dataIndex: 'value', render: (num: string) => transDisplayNum({ num }) } 19 | ] 20 | 21 | const TxInternalDetailTable: React.FC<{ from: string; to: string; tx: string }> = ({ from, to, tx }) => { 22 | const { isLoading, data: txDetail } = trpc.transaction.getInternalTransactionDetail.useQuery(tx, { enabled: !!tx }) 23 | 24 | if (isLoading) return 25 | 26 | return ( 27 |
28 |
29 | 30 | The contract call 31 | From 32 | 33 | To 34 | 35 | 36 | produced {txDetail?.length} Internal Transaction{(txDetail?.length ?? 0) > 1 ? 's' : ''} 37 | 38 |
39 |
40 | 41 | ) 42 | } 43 | 44 | export default TxInternalDetailTable 45 | -------------------------------------------------------------------------------- /components/blockchain/txs-table/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { Table, Tooltip } from 'antd' 4 | 5 | import Link from '@/components/common/link' 6 | import Pagination from '@/components/common/pagination' 7 | import SkeletonTable, { SkeletonTableColumnsType } from '@/components/common/skeleton-table' 8 | import { L1StatusLabel, MethodLabel, TransArrowIcon } from '@/components/common/table-col-components' 9 | import { TABLE_CONFIG } from '@/constants' 10 | import { LinkTypeEnum, TxType } from '@/types' 11 | import { convertGwei, transDisplayNum, transDisplayTime, transDisplayTimeAgo } from '@/utils' 12 | import message from '@/utils/message' 13 | import { trpc } from '@/utils/trpc' 14 | 15 | export const txColumns = [ 16 | { title: 'L1 Status', fixed: true, width: TABLE_CONFIG.COL_WIDHT.L1_STATUS, render: ({ l1Status }: TxType) => }, 17 | { 18 | title: 'Txn Hash', 19 | dataIndex: 'hash', 20 | render: (hash: string) => 21 | }, 22 | { title: 'Method', dataIndex: 'inputData', render: (inputData: string) => }, 23 | { title: 'Block', dataIndex: 'blockNumber', render: (num: number) => }, 24 | { 25 | title: 'Age', 26 | dataIndex: 'blockTime', 27 | width: TABLE_CONFIG.COL_WIDHT.AGE, 28 | render: (time: number) => {transDisplayTimeAgo(time)} 29 | }, 30 | { 31 | title: 'From', 32 | dataIndex: 'from', 33 | render: (from: string) => 34 | }, 35 | { title: '', width: TABLE_CONFIG.COL_WIDHT.TRANS_ARROW_ICON, render: () => }, 36 | { title: 'To', dataIndex: 'to', render: (to: string) => }, 37 | { title: 'Value', dataIndex: 'value', render: (num: string) => transDisplayNum({ num }) }, 38 | { title: 'Txn Fee', dataIndex: 'fee', width: TABLE_CONFIG.COL_WIDHT.TXFEE, render: (num: string) => transDisplayNum({ num: num, fixedNum: 9 }) }, 39 | { title: 'Gas Price', dataIndex: 'gasPrice', render: (num: string) => convertGwei(num) } 40 | ] 41 | 42 | const TxsTable: React.FC = () => { 43 | const [current, setCurrent] = useState(1) 44 | const [pageSize, setPageSize] = useState(20) 45 | const [input, setInput] = useState<{ limit: number; cursor?: number }>({ limit: pageSize }) 46 | const { isFetching, data, error } = trpc.transaction.getTransactionList.useQuery(input) 47 | console.log('current', current) 48 | const onCursorChange = (value: string) => { 49 | let cursor 50 | switch (value) { 51 | case 'next': 52 | cursor = data?.cursor 53 | setCurrent(current + 1) 54 | break 55 | case 'prev': 56 | cursor = data?.cursor ? data?.cursor + pageSize * 2 : undefined 57 | setCurrent(current - 1) 58 | break 59 | case 'first': 60 | cursor = undefined 61 | setCurrent(1) 62 | break 63 | case 'last': 64 | cursor = 0 65 | setCurrent(-1) 66 | break 67 | default: 68 | cursor = undefined 69 | break 70 | } 71 | setInput({ ...input, cursor }) 72 | } 73 | if (error) { 74 | console.log(error) 75 | message.error('Internal Server Error') 76 | } 77 | return ( 78 | 79 | { 84 | setPageSize(value) 85 | setInput({ ...input, limit: value }) 86 | }} 87 | onCursorChange={onCursorChange} 88 | /> 89 |
90 | { 95 | setPageSize(value) 96 | setInput({ ...input, limit: value }) 97 | }} 98 | onCursorChange={onCursorChange} 99 | /> 100 | 101 | ) 102 | } 103 | 104 | export default TxsTable 105 | -------------------------------------------------------------------------------- /components/common/address-avatar/index.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, useEffect, useRef, useState } from 'react' 2 | 3 | import Image from 'next/image' 4 | 5 | import { renderIcon } from '@/utils/blockies' 6 | 7 | const AddressAvatar: React.FC<{ style?: CSSProperties; className?: string; address: string | undefined; size?: number }> = ({ 8 | style = {}, 9 | className = '', 10 | address, 11 | size = 20 12 | }) => { 13 | const [imgSrc, setImgSrc] = useState() 14 | const canvasRef = useRef(null) 15 | 16 | useEffect(() => { 17 | if (address) { 18 | const canvas: any = canvasRef.current 19 | renderIcon({ seed: address.toLowerCase() }, canvas) 20 | const updatedDataUrl = canvas?.toDataURL() 21 | 22 | if (updatedDataUrl !== imgSrc) { 23 | setImgSrc(updatedDataUrl) 24 | } 25 | } 26 | }, [imgSrc, address]) 27 | 28 | return ( 29 | <> 30 | 31 | {!!imgSrc && } 32 | 33 | ) 34 | } 35 | 36 | export default AddressAvatar 37 | -------------------------------------------------------------------------------- /components/common/address-qrcode/index.tsx: -------------------------------------------------------------------------------- 1 | import { QRCodeSVG } from 'qrcode.react' 2 | 3 | const AddressQrcode: React.FC<{ address: string; size?: number }> = ({ address, size = 220 }) => { 4 | return 5 | } 6 | 7 | export default AddressQrcode 8 | -------------------------------------------------------------------------------- /components/common/chart/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from 'react' 2 | 3 | import dayjs from 'dayjs' 4 | import { Area, AreaChart, CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts' 5 | 6 | import { formatNumWithSymbol } from '@/utils' 7 | 8 | type CardPropsType = { 9 | data: any[] 10 | xDataKey: string 11 | yDataKey: string 12 | xTickStyle?: any 13 | yTickStyle?: any 14 | xTickFormatter?: (value: any, index: number) => string 15 | yTickFormatter?: (value: any, index: number) => string 16 | tooltipTitle?: string 17 | tooltipValueFormatter?: (value: string) => any 18 | strokeColor?: string 19 | showGrid?: boolean 20 | gridXColor?: string 21 | gridDashed?: boolean 22 | xAngle?: number 23 | xPanding?: { left?: number; right?: number } 24 | yWidth?: number 25 | xUnit?: string 26 | yUnit?: string 27 | yTickCount?: number 28 | yDomain?: any[] 29 | xInterval?: 0 | 1 | 'preserveStart' | 'preserveEnd' | 'preserveStartEnd' 30 | type?: 'area' | 'line' 31 | dot?: any 32 | } 33 | 34 | const Chart: React.FC = ({ 35 | data, 36 | xDataKey, 37 | yDataKey, 38 | xTickStyle = { fontWeight: 300, color: '#54617A', opacity: 0.5 }, 39 | yTickStyle = { fontWeight: 300, color: '#54617A', opacity: 0.5 }, 40 | xTickFormatter = value => (value && 'auto' !== value ? dayjs(value).format(`MMM 'DD`) : ''), 41 | yTickFormatter = value => formatNumWithSymbol(value, 0), 42 | tooltipTitle, 43 | tooltipValueFormatter, 44 | strokeColor = '#FDC2A0', 45 | showGrid = true, 46 | gridXColor = '#edeef0', 47 | gridDashed = false, 48 | xAngle = 0, 49 | xPanding = { left: 20, right: 20 }, 50 | yWidth = 60, 51 | xUnit = '', 52 | yUnit = '', 53 | yTickCount = 5, 54 | yDomain, 55 | xInterval = 'preserveEnd', 56 | type = 'line', 57 | dot = { stroke: '#FF4D2C', strokeWidth: 1 } 58 | }) => { 59 | const renderCustomTooltip = useCallback( 60 | (external: any) => { 61 | const { active, payload, label } = external 62 | 63 | if (active && payload && payload.length) { 64 | return ( 65 |
66 |
{dayjs(label).format('dddd,MMMM DD,YYYY')}
67 |
68 |
{tooltipTitle}
69 |
{payload?.[0]?.payload?.[yDataKey]}
70 |
71 |
72 | ) 73 | } 74 | 75 | return null 76 | }, 77 | [tooltipTitle, yDataKey] 78 | ) 79 | 80 | const lineAreaProps = useMemo( 81 | () => ({ 82 | dataKey: yDataKey, 83 | stroke: strokeColor, 84 | strokeWidth: 1, 85 | dot, 86 | r: 1, 87 | activeDot: { 88 | stroke: '#F26412', 89 | strokeWidth: 1, 90 | r: 2 91 | }, 92 | isAnimationActive: false 93 | }), 94 | [dot, strokeColor, yDataKey] 95 | ) 96 | 97 | const chartContent = useMemo( 98 | () => ( 99 | <> 100 | 101 | 102 | 103 | 104 | 105 | 106 | 119 | 131 | {showGrid && } 132 | 133 | {'line' === type && } 134 | {'area' === type && } 135 | 136 | ), 137 | [ 138 | gridDashed, 139 | gridXColor, 140 | lineAreaProps, 141 | renderCustomTooltip, 142 | showGrid, 143 | type, 144 | xAngle, 145 | xDataKey, 146 | xInterval, 147 | xPanding, 148 | xTickFormatter, 149 | xTickStyle, 150 | xUnit, 151 | yDomain, 152 | yTickCount, 153 | yTickFormatter, 154 | yTickStyle, 155 | yUnit, 156 | yWidth 157 | ] 158 | ) 159 | 160 | const chartWrap = useMemo(() => { 161 | switch (type) { 162 | case 'area': 163 | return {chartContent} 164 | case 'line': 165 | return {chartContent} 166 | default: 167 | return <> 168 | } 169 | }, [type, data, chartContent]) 170 | 171 | return ( 172 | 173 | {chartWrap} 174 | 175 | ) 176 | } 177 | 178 | export default Chart 179 | -------------------------------------------------------------------------------- /components/common/dot-text/index.module.scss: -------------------------------------------------------------------------------- 1 | .dotWrap { 2 | @apply w-fit flex items-center; 3 | 4 | > div:last-child { 5 | @apply w-13px text-left; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /components/common/dot-text/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | import classNames from 'classnames' 4 | 5 | import style from './index.module.scss' 6 | 7 | const DotText: React.FC<{ text: string; time?: number; className?: string }> = ({ text, time = 500, className = '' }) => { 8 | const [dot, setDot] = useState('') 9 | 10 | useEffect(() => { 11 | const timer = setInterval(() => { 12 | text && 13 | setDot(pre => { 14 | if (pre.length < 3) { 15 | return (pre += '.') 16 | } 17 | return '' 18 | }) 19 | }, time) 20 | return () => { 21 | timer && clearInterval(timer) 22 | } 23 | }, [text, time]) 24 | 25 | return ( 26 |
27 |
{text}
28 |
{dot}
29 |
30 | ) 31 | } 32 | 33 | export default DotText 34 | -------------------------------------------------------------------------------- /components/common/link/index.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, ReactNode } from 'react' 2 | import { generatePath } from 'react-router-dom' 3 | 4 | import { getAddress, isAddress } from '@ethersproject/address' 5 | import { Tooltip } from 'antd' 6 | import classNames from 'classnames' 7 | import Image from 'next/image' 8 | 9 | import ROUTES from '@/constants/routes' 10 | import { LinkTypeEnum } from '@/types' 11 | import { getCrossBrowserTxUrl, stringifyQueryUrl } from '@/utils' 12 | 13 | type LinkPropsType = { 14 | style?: CSSProperties 15 | className?: string 16 | type: LinkTypeEnum 17 | value: string | number | undefined | null 18 | children?: ReactNode 19 | ellipsis?: boolean 20 | width?: number | string 21 | target?: string 22 | } 23 | 24 | export const getLinkRoute = (type: LinkTypeEnum | undefined, value: any = '') => { 25 | const _value = isAddress(value) ? getAddress(value) : value 26 | 27 | switch (type) { 28 | case LinkTypeEnum.BLOCK: 29 | return generatePath(ROUTES.BLOCK_CHAIN.DETAIL.BLOCK, { block: _value }) 30 | 31 | case LinkTypeEnum.BLOCKS: 32 | // TODO: is this wrong? 33 | return stringifyQueryUrl(ROUTES.BLOCK_CHAIN.TXNS, { block: _value }) 34 | 35 | case LinkTypeEnum.BATCH: 36 | return generatePath(ROUTES.BLOCK_CHAIN.DETAIL.BATCH, { batch: _value }) 37 | 38 | case LinkTypeEnum.BATCHES: 39 | return stringifyQueryUrl(ROUTES.BLOCK_CHAIN.BATCHES, { batch: _value }) 40 | 41 | case LinkTypeEnum.CONTRACT_INTERNAL_TXS: 42 | return stringifyQueryUrl(ROUTES.BLOCK_CHAIN.TXNS, { internalBlock: _value }) 43 | 44 | case LinkTypeEnum.TX: 45 | return generatePath(ROUTES.BLOCK_CHAIN.DETAIL.TX, { tx: _value }) 46 | 47 | case LinkTypeEnum.CONTRACT: 48 | return generatePath(ROUTES.BLOCK_CHAIN.DETAIL.ADDRESS, { address: _value }) 49 | 50 | case LinkTypeEnum.ADDRESS: 51 | return generatePath(ROUTES.BLOCK_CHAIN.DETAIL.ADDRESS, { address: _value }) 52 | 53 | case LinkTypeEnum.TOKEN: 54 | return generatePath(ROUTES.BLOCK_CHAIN.DETAIL.TOKEN, { token: _value }) 55 | 56 | case LinkTypeEnum.CROSS_BROWSER_TX: 57 | return getCrossBrowserTxUrl(_value) 58 | 59 | default: 60 | return ROUTES.HOME 61 | } 62 | } 63 | 64 | const Link: React.FC = ({ style = {}, className = '', type, value, children, ellipsis = false, width = 150, target }) => { 65 | const _value: any = children ?? value 66 | const data = isAddress(_value || '') ? getAddress(_value || '') : _value 67 | if (!data) { 68 | return
-
69 | } 70 | 71 | return ( 72 | 77 | 78 | {ellipsis ? ( 79 |
80 | {data} 81 |
82 | ) : ( 83 | data 84 | )} 85 |
86 |
87 | ) 88 | } 89 | 90 | export default Link 91 | 92 | export const TokenLink: React.FC<{ 93 | className?: string 94 | name?: string 95 | symbol?: string 96 | tokenAddress: string 97 | ellipsis?: boolean 98 | img?: string 99 | imgSize?: number 100 | imgLineHeight?: number 101 | desc?: string 102 | }> = ({ className = '', name = '', symbol = '', tokenAddress, ellipsis = false, img, imgSize = 18, desc = '' }) => { 103 | return ( 104 | <> 105 |
106 | {!!img && } 107 | 108 | {!!name || !!symbol ? `${name}${!!name && !!symbol ? ' (' : ''}${symbol}${!!name && !!symbol ? ')' : ''}` : tokenAddress} 109 | 110 |
111 | {!!desc && ( 112 |
113 | {desc} 114 |
115 | )} 116 | 117 | ) 118 | } 119 | -------------------------------------------------------------------------------- /components/common/loading/index.module.scss: -------------------------------------------------------------------------------- 1 | .loadingWrap { 2 | @apply w-full h-full flexCenter fixed left-0 top-0 z-9999 bg-[rgba(255,255,255,0.5)]; 3 | 4 | .loading { 5 | @apply w-60px h-20px flex justify-between items-center; 6 | 7 | span { 8 | @apply w-6px h-full inline-block rounded-4; 9 | background: linear-gradient(to right, #e6c4ac, #d9a280); 10 | animation: loading 0.5s ease infinite; 11 | 12 | &:nth-child(2) { 13 | animation-delay: 0.1s; 14 | } 15 | 16 | &:nth-child(3) { 17 | animation-delay: 0.2s; 18 | } 19 | 20 | &:nth-child(4) { 21 | animation-delay: 0.3s; 22 | } 23 | 24 | &:nth-child(5) { 25 | animation-delay: 0.4s; 26 | } 27 | } 28 | } 29 | } 30 | 31 | @keyframes loading { 32 | 0%, 33 | 100% { 34 | @apply h-20px; 35 | background: linear-gradient(to right, #e6c4ac, #d9a280); 36 | } 37 | 38 | 50% { 39 | @apply h-40px my-[-10px]; 40 | background: linear-gradient(to right, #d9a280, #cb8158); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /components/common/loading/index.tsx: -------------------------------------------------------------------------------- 1 | import style from './index.module.scss' 2 | 3 | const Loading: React.FC = () => { 4 | return ( 5 |
6 |
7 | {new Array(5).fill('').map((_, index) => ( 8 | 9 | ))} 10 |
11 |
12 | ) 13 | } 14 | 15 | export default Loading 16 | -------------------------------------------------------------------------------- /components/common/overview-content/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | 3 | import { Col, Row, Tooltip } from 'antd' 4 | import Image from 'next/image' 5 | 6 | import { getImgSrc } from '@/utils' 7 | 8 | type OverviewCardsProps = { 9 | className?: string 10 | data: { 11 | img: string 12 | content: { 13 | label: string 14 | value: string | ReactNode 15 | tooltip?: string 16 | }[] 17 | colSpan?: number 18 | }[] 19 | } 20 | 21 | export const OverviewCards: React.FC = ({ className = '', data }) => ( 22 | 23 | {data?.map(({ img, content, colSpan = 6 }) => ( 24 |
25 |
26 |
27 | {content?.map(({ label, value, tooltip }, index) => ( 28 |
29 |
30 | {label} 31 | {!!tooltip && ( 32 | 33 | 34 | 35 | )} 36 |
37 |
{value}
38 |
39 | ))} 40 |
41 | 42 |
43 | 44 | ))} 45 | 46 | ) 47 | 48 | export type OverviewCellContentType = { 49 | label: string 50 | tooltip?: string | undefined 51 | value: string | ReactNode | undefined 52 | colSpan?: number | undefined 53 | }[] 54 | 55 | type OverviewCellContentProps = { 56 | className?: string 57 | data: OverviewCellContentType 58 | } 59 | 60 | export const OverviewCellContent: React.FC = ({ className = '', data }) => ( 61 | 62 | {data?.map(({ label, tooltip, value, colSpan = 12 }) => ( 63 | 64 |
65 |
{label}:
66 | {!!tooltip && ( 67 | 68 | 69 | 70 | )} 71 |
72 |
{value}
73 | 74 | ))} 75 | 76 | ) 77 | -------------------------------------------------------------------------------- /components/common/page-title/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | 3 | import Image from 'next/image' 4 | import { useRouter } from 'next/router' 5 | 6 | import { getImgSrc } from '@/utils' 7 | 8 | type PageTitleProps = { 9 | title: ReactNode | string 10 | showBack?: boolean 11 | } 12 | 13 | const PageTitle: React.FC = ({ title, showBack = false }) => { 14 | const router = useRouter() 15 | 16 | return ( 17 |
18 | {showBack && ( 19 | router.back()} 25 | /> 26 | )} 27 |
{title}
28 |
29 | ) 30 | } 31 | 32 | export default PageTitle 33 | -------------------------------------------------------------------------------- /components/common/pagination.tsx: -------------------------------------------------------------------------------- 1 | import { LeftOutlined, RightOutlined } from '@ant-design/icons' 2 | import { Button, Select } from 'antd' 3 | 4 | import { formatNum } from '@/utils' 5 | 6 | interface PaginationProps { 7 | currentPage: number 8 | totalResults: number 9 | pageSize: number 10 | onCursorChange: (direction: 'first' | 'prev' | 'next' | 'last') => void 11 | onPageSizeChange: (pageSize: number) => void 12 | } 13 | 14 | export default function Pagination({ currentPage, totalResults, pageSize, onCursorChange, onPageSizeChange }: PaginationProps) { 15 | const totalPages = Math.ceil(totalResults / pageSize) 16 | const cur = currentPage < 0 ? totalPages + currentPage + 1 : currentPage 17 | 18 | return ( 19 |
20 |
A total of {formatNum(totalResults)} transactions found
21 |
22 | 25 | 36 | 37 |
38 | } 37 | suffix={isFetching && } 38 | className={classNames('px-16px py-10px', noBorder && 'border-none shadow-none')} 39 | placeholder="Search by Address / Txn Hash / Block / Token" 40 | onPressEnter={(e: any) => setContent(e.target.value)} 41 | allowClear 42 | /> 43 | 44 | setShowErrorModal(false)} centered> 45 |
46 | 47 |
Search not found
48 |
Oops! This is an invalid search string.
49 |
50 |
51 | 52 | ) 53 | } 54 | 55 | export default SearchInput 56 | -------------------------------------------------------------------------------- /components/common/skeleton-table.tsx: -------------------------------------------------------------------------------- 1 | // skeleton-table.tsx 2 | import { Skeleton, SkeletonProps, Table, TablePaginationConfig } from 'antd' 3 | import { ColumnsType } from 'antd/lib/table' 4 | 5 | export type SkeletonTableColumnsType = {} 6 | 7 | type SkeletonTableProps = SkeletonProps & { 8 | columns: ColumnsType 9 | rowCount?: number 10 | pagination?: false | TablePaginationConfig 11 | } 12 | 13 | export default function SkeletonTable({ 14 | loading = false, 15 | active = false, 16 | pagination = false, 17 | rowCount = 10, 18 | columns, 19 | children, 20 | className 21 | }: SkeletonTableProps): JSX.Element { 22 | return loading ? ( 23 |
({ 27 | key: `key${index}` 28 | }))} 29 | columns={columns.map(column => { 30 | return { 31 | ...column, 32 | render: function renderPlaceholder() { 33 | return 34 | } 35 | } 36 | })} 37 | /> 38 | ) : ( 39 | <>{children} 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /components/common/tab-card/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect, useState } from 'react' 2 | 3 | import { Card, Tabs } from 'antd' 4 | import { useRouter } from 'next/router' 5 | 6 | export type TabCardListProps = { label: string | ReactNode; children: ReactNode }[] 7 | 8 | type TabCardPropsType = { 9 | className?: string 10 | defaultActiveKey?: string 11 | tabList: TabCardListProps 12 | } 13 | 14 | const TabCard: React.FC = ({ className = '', defaultActiveKey = '0', tabList }) => { 15 | const router = useRouter() 16 | const search: any = router?.query 17 | 18 | const [activeKey, setActiveKey] = useState(defaultActiveKey) 19 | 20 | useEffect(() => { 21 | setActiveKey(defaultActiveKey) 22 | }, [defaultActiveKey, search]) 23 | 24 | return ( 25 | 26 | ({ label, key: index + '', children }))} 29 | onChange={key => setActiveKey(key)} 30 | /> 31 | 32 | ) 33 | } 34 | 35 | export default TabCard 36 | -------------------------------------------------------------------------------- /components/common/table-col-components/index.module.scss: -------------------------------------------------------------------------------- 1 | .activeL1StatusLineTimeSvg { 2 | @apply fill-[#1E78E5] mb-[-1px]; 3 | 4 | circle { 5 | @apply stroke-[#1E78E5]; 6 | } 7 | 8 | path { 9 | @apply stroke-white; 10 | } 11 | } 12 | 13 | .inActiveL1StatusLineSvgWrap svg rect { 14 | @apply fill-[#C0C6CC]; 15 | } 16 | -------------------------------------------------------------------------------- /components/contract/contract-tab/contract-functions-panel.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react' 2 | 3 | import { ArrowRightOutlined } from '@ant-design/icons' 4 | import { Collapse } from 'antd' 5 | 6 | import { InteractiveAbiFunction } from './contract-function' 7 | 8 | interface ContractFunctionsPanelProps { 9 | contract: any 10 | abi: any 11 | type: 'read' | 'write' 12 | } 13 | 14 | const ContractFunctionsPanel: React.FC = ({ contract, abi, type }) => { 15 | const fns = Object.values(contract?.interface?.functions ?? {}) 16 | const writeFunctions = useMemo(() => { 17 | return fns?.filter((fn: any) => fn.stateMutability !== 'pure' && fn.stateMutability !== 'view' && 'stateMutability' in fn) 18 | }, [fns]) 19 | const viewFunctions = useMemo(() => { 20 | return fns?.filter((fn: any) => fn.stateMutability === 'pure' || fn.stateMutability === 'view') 21 | }, [fns]) 22 | if (!fns) { 23 | return null 24 | } 25 | 26 | return ( 27 | }> 28 | {type === 'read' 29 | ? viewFunctions.map((fn: any, index: number) => ( 30 | 31 | 32 | 33 | )) 34 | : writeFunctions.map((fn: any, index: number) => ( 35 | 36 | 37 | 38 | ))} 39 | 40 | ) 41 | } 42 | 43 | export default ContractFunctionsPanel 44 | -------------------------------------------------------------------------------- /components/contract/verify-contract-page/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useMemo } from 'react' 2 | 3 | import { Card } from 'antd' 4 | import classNames from 'classnames' 5 | import { useRouter } from 'next/router' 6 | 7 | import PageTitle from '@/components/common/page-title' 8 | import { ContractCompilerTypeEnum } from '@/pages/verifyContract' 9 | 10 | const VerifyContractPage: React.FC<{ step?: number; children: ReactNode; footer: ReactNode }> = ({ step = 1, children, footer }) => { 11 | const { query } = useRouter() 12 | const { contractCompilerType } = query 13 | 14 | const verifyContractCardHeaderStepsData = useMemo( 15 | () => [ 16 | { title: 'Verify & Publish Contract Source Code', subTitle: 'COMPILER TYPE AND VERSION SELECTION' }, 17 | { 18 | title: 'Verify & Publish Contract Source Code', 19 | subTitle: undefined === contractCompilerType ? 'FILE UPLOAD' : ContractCompilerTypeEnum[contractCompilerType] 20 | } 21 | ], 22 | [contractCompilerType] 23 | ) 24 | 25 | const verifyContractCardHeader = useMemo( 26 | () => ( 27 |
28 | {verifyContractCardHeaderStepsData.map(({ title, subTitle }, index) => ( 29 |
30 |
31 |
32 |
37 | {index + 1} 38 |
39 |
{title}
40 |
41 |
{subTitle}
42 |
43 | {index < verifyContractCardHeaderStepsData.length - 1 &&
} 44 |
45 | ))} 46 |
47 | ), 48 | [step, verifyContractCardHeaderStepsData] 49 | ) 50 | 51 | return ( 52 | <> 53 | 54 | 55 |
{children}
56 |
{footer}
57 |
58 | 59 | ) 60 | } 61 | 62 | export default VerifyContractPage 63 | -------------------------------------------------------------------------------- /components/contract/wallet-connector/index.tsx: -------------------------------------------------------------------------------- 1 | import { WalletOutlined } from '@ant-design/icons' 2 | import { Badge, Button } from 'antd' 3 | import { useAccount, useConnect, useEnsName } from 'wagmi' 4 | import { InjectedConnector } from 'wagmi/connectors/injected' 5 | 6 | import { shortAddress } from '@/utils' 7 | 8 | export default function WalletConnector() { 9 | const { address, isConnected } = useAccount() 10 | const { data: ensName } = useEnsName({ address }) 11 | const { connect, isLoading } = useConnect({ 12 | connector: new InjectedConnector() 13 | }) 14 | 15 | return ( 16 |
17 | {isConnected ? ( 18 | 19 | ) : ( 20 | 23 | )} 24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /components/tokens/token-detail-holders-table/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from 'react' 2 | 3 | import { Progress, Table, Tooltip } from 'antd' 4 | import BigNumber from 'bignumber.js' 5 | import { generate } from 'shortid' 6 | 7 | import Link from '@/components/common/link' 8 | import SkeletonTable, { SkeletonTableColumnsType } from '@/components/common/skeleton-table' 9 | import { PAGE_SIZE } from '@/constants' 10 | import { LinkTypeEnum, TokenHolderType, TokenTypeEnum } from '@/types' 11 | import { getPaginationConfig, transDisplayNum } from '@/utils' 12 | import { trpc } from '@/utils/trpc' 13 | 14 | const TokenDetailHoldersTable: React.FC<{ tokenAddress: string; type: TokenTypeEnum }> = ({ tokenAddress, type }) => { 15 | const [firstDataPercent, setFirstDataPercent] = useState(1) 16 | const [current, setCurrent] = useState(1) 17 | const [pageSize, setPageSize] = useState(PAGE_SIZE) 18 | 19 | const { isLoading, data } = trpc.token.getTokenHolders.useQuery( 20 | { offset: (current - 1) * pageSize, limit: pageSize, address: tokenAddress }, 21 | { enabled: !!tokenAddress } 22 | ) 23 | 24 | useEffect(() => { 25 | setFirstDataPercent(Number(data?.list?.[0]?.percentage) ?? 1) 26 | }, [data?.list]) 27 | 28 | const columns = useMemo( 29 | () => [ 30 | { title: 'Rank', width: 60, render: (_data: unknown, _: TokenHolderType, index: number) => index + (current - 1) * pageSize + 1 }, 31 | { title: 'Address', dataIndex: 'address', render: (address: string) => }, 32 | ...(type === TokenTypeEnum.ERC1155 33 | ? [ 34 | { 35 | title: 'TokenID', 36 | dataIndex: 'tokenId', 37 | render: (tokenId = 0) => ( 38 | 39 |
{new BigNumber(tokenId, 16)?.toString(10)}
40 |
41 | ) 42 | } 43 | ] 44 | : []), 45 | { 46 | title: 'Quantity', 47 | render: ({ value }: TokenHolderType) => transDisplayNum({ num: value, fixedNum: 6, suffix: '', decimals: 0 }) 48 | }, 49 | { 50 | title: 'Percentage', 51 | render: ({ percentage }: TokenHolderType) => ( 52 |
53 |
{transDisplayNum({ num: Number(percentage), fixedNum: 4, suffix: '%', decimals: 0 })}
54 | 60 |
61 | ) 62 | } 63 | ], 64 | [current, firstDataPercent, pageSize, type] 65 | ) 66 | 67 | return ( 68 | 69 |
75 | 76 | ) 77 | } 78 | 79 | export default TokenDetailHoldersTable 80 | -------------------------------------------------------------------------------- /components/tokens/token-detail-txs-table/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from 'react' 2 | 3 | import { Table } from 'antd' 4 | 5 | import SkeletonTable, { SkeletonTableColumnsType } from '@/components/common/skeleton-table' 6 | import { MethodLabel } from '@/components/common/table-col-components' 7 | import { getTokenTxsColumns } from '@/components/tokens/token-txs-table' 8 | import { PAGE_SIZE } from '@/constants' 9 | import { TokenTypeEnum } from '@/types' 10 | import { getPaginationConfig } from '@/utils' 11 | import { trpc } from '@/utils/trpc' 12 | 13 | const TokenDetailTxsTable: React.FC<{ tokenAddress: string; type: TokenTypeEnum; setCount?: any }> = ({ tokenAddress, type, setCount }) => { 14 | const [current, setCurrent] = useState(1) 15 | const [pageSize, setPageSize] = useState(PAGE_SIZE) 16 | const { isLoading, data } = trpc.token.getTokenTransactionList.useQuery( 17 | { 18 | offset: (current - 1) * pageSize, 19 | limit: pageSize, 20 | address: tokenAddress, 21 | tokenType: type 22 | }, 23 | { enabled: !!tokenAddress } 24 | ) 25 | 26 | useEffect(() => { 27 | setCount && setCount(Number(data?.count)) 28 | }, [data?.count, setCount]) 29 | 30 | const columns = useMemo(() => { 31 | const cols = getTokenTxsColumns(Number(type)) 32 | 33 | cols.splice(1, 0, { 34 | title: 'Method', 35 | dataIndex: 'methodId', 36 | render: (methodId: string) => 37 | }) 38 | cols.splice(TokenTypeEnum.ERC20 === Number(type) ? 7 : 8, 1) 39 | TokenTypeEnum.ERC20 !== Number(type) && cols.splice(-1, 1) 40 | 41 | return cols 42 | }, [type]) 43 | 44 | return ( 45 | 46 |
`${transactionHash}${logIndex}`} 48 | dataSource={data?.list} 49 | columns={columns} 50 | pagination={getPaginationConfig({ current, pageSize, total: Number(data?.count), setCurrent, setPageSize })} 51 | /> 52 | 53 | ) 54 | } 55 | 56 | export default TokenDetailTxsTable 57 | -------------------------------------------------------------------------------- /components/tokens/token-page/index.tsx: -------------------------------------------------------------------------------- 1 | import PageTitle from '@/components/common/page-title' 2 | import TabCard from '@/components/common/tab-card' 3 | import TokenTable from '@/components/tokens/token-table' 4 | import TokenTxsTable from '@/components/tokens/token-txs-table' 5 | import { TokenTypeEnum } from '@/types' 6 | 7 | const TitleData = { 8 | [TokenTypeEnum.ERC20]: { title: 'Token Tracker', label: 'ERC-20' }, 9 | [TokenTypeEnum.ERC721]: { title: 'Non-Fungible Token Tracker', label: 'ERC-721' }, 10 | [TokenTypeEnum.ERC1155]: { title: 'Multi-Token Token Tracker', label: 'ERC-1155' } 11 | } 12 | 13 | const TokenPage: React.FC<{ type: TokenTypeEnum }> = ({ type }) => ( 14 | <> 15 | 18 |
{TitleData?.[type]?.title}
19 |
{TitleData?.[type]?.label}
20 | 21 | } 22 | /> 23 | }, 26 | { label: `${TitleData?.[type]?.label} Transfers`, children: } 27 | ]} 28 | /> 29 | 30 | ) 31 | 32 | export default TokenPage 33 | -------------------------------------------------------------------------------- /components/tokens/token-table/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from 'react' 2 | 3 | import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons' 4 | import { Table } from 'antd' 5 | import classNames from 'classnames' 6 | 7 | import { TokenLink } from '@/components/common/link' 8 | import SkeletonTable, { SkeletonTableColumnsType } from '@/components/common/skeleton-table' 9 | import { PAGE_SIZE } from '@/constants' 10 | import { TokenType, TokenTypeEnum } from '@/types' 11 | import { convertNum, formatNum, getPaginationConfig } from '@/utils' 12 | import { trpc } from '@/utils/trpc' 13 | 14 | const TokenTable: React.FC<{ type: TokenTypeEnum }> = ({ type }) => { 15 | const [current, setCurrent] = useState(1) 16 | const [pageSize, setPageSize] = useState(PAGE_SIZE) 17 | 18 | useEffect(() => { 19 | setCurrent(1) 20 | setPageSize(PAGE_SIZE) 21 | }, [type]) 22 | 23 | const { isLoading, data } = trpc.token.getTokenList.useQuery({ offset: (current - 1) * pageSize, limit: pageSize, tokenType: type }, { enabled: !!type }) 24 | 25 | const columns = useMemo(() => { 26 | const cols = [ 27 | { title: '#', width: 60, render: (_data: unknown, _: TokenType, index: number) => index + (current - 1) * pageSize + 1 }, 28 | { 29 | title: 'Token', 30 | render: ({ name, symbol, contractAddress, logo_path, description }: TokenType) => ( 31 | 32 | ) 33 | } 34 | ] 35 | const otherCols = 36 | TokenTypeEnum.ERC20 === type 37 | ? [ 38 | { title: 'Price', dataIndex: 'Price', width: 150, render: (num: number) => formatNum(num || '0.0', '$') }, 39 | { 40 | title: 'Change (%)', 41 | dataIndex: 'Change', 42 | width: 150, 43 | render: (num: number) => ( 44 |
0 ? 'text-green' : 'text-red', 'flex items-center')}> 45 | {0 !== Number(num ?? 0) && 46 | ((num ?? 0) > 0 ? : )} 47 | {0 === Number(num ?? 0) ? '--' : formatNum(num)} 48 |
49 | ) 50 | }, 51 | { title: 'Market Cap', dataIndex: 'OnChainMarketCap', width: 150, render: (num: number) => formatNum(num || '0.0', '$') }, 52 | { title: 'Holders', dataIndex: 'holders', width: 150, render: (num: number | bigint) => formatNum(convertNum(num)) } 53 | ] 54 | : [ 55 | { title: 'Transfers (24H)', dataIndex: 'trans24h', width: 150, render: (num: number) => formatNum(num) }, 56 | { title: 'Transfers (3D)', dataIndex: 'trans3d', width: 150, render: (num: number) => formatNum(num) } 57 | ] 58 | 59 | return [...cols, ...otherCols] 60 | }, [type, current, pageSize]) 61 | 62 | return ( 63 | 64 |
70 | 71 | ) 72 | } 73 | 74 | export default TokenTable 75 | -------------------------------------------------------------------------------- /components/tokens/token-txs-table/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import { Table, Tooltip } from 'antd' 4 | import BigNumber from 'bignumber.js' 5 | 6 | import Link, { TokenLink } from '@/components/common/link' 7 | import SkeletonTable, { SkeletonTableColumnsType } from '@/components/common/skeleton-table' 8 | import { TransArrowIcon } from '@/components/common/table-col-components' 9 | import { PAGE_SIZE, TABLE_CONFIG } from '@/constants' 10 | import { LinkTypeEnum, TokenTxType, TokenTypeEnum } from '@/types' 11 | import { getPaginationConfig, transDisplayNum, transDisplayTime, transDisplayTimeAgo } from '@/utils' 12 | import { trpc } from '@/utils/trpc' 13 | 14 | export const getTokenTxsColumns = (type: TokenTypeEnum) => { 15 | const cols = [ 16 | { 17 | title: 'Txn Hash', 18 | dataIndex: 'transactionHash', 19 | render: (hash: string) => 20 | }, 21 | { 22 | title: 'Age', 23 | dataIndex: 'blockTime', 24 | render: (time: number | bigint) => {transDisplayTimeAgo(Number(time))} 25 | }, 26 | { 27 | title: 'From', 28 | dataIndex: 'from', 29 | render: (from: string) => 30 | }, 31 | { title: '', width: TABLE_CONFIG.COL_WIDHT.TRANS_ARROW_ICON, render: () => }, 32 | { title: 'To', dataIndex: 'to', render: (to: string) => }, 33 | { title: 'Value', dataIndex: 'value', render: (num: string, { decimals }: TokenTxType) => transDisplayNum({ num, decimals, suffix: '' }) }, 34 | { 35 | title: 'Token', 36 | render: ({ name, symbol, contract, logo_path }: TokenTxType) => 37 | } 38 | ] 39 | 40 | if (TokenTypeEnum.ERC20 !== type) { 41 | cols.splice(5, 0, { 42 | title: 'TokenID', 43 | dataIndex: 'tokenId', 44 | render: (tokenId: string) => ( 45 | 46 |
{new BigNumber(tokenId, 16)?.toString(10)}
47 |
48 | ) 49 | }) 50 | } 51 | 52 | TokenTypeEnum.ERC721 === type && cols.splice(6, 1) 53 | 54 | return cols 55 | } 56 | 57 | const TokenTxsTable: React.FC<{ type: TokenTypeEnum }> = ({ type }) => { 58 | const [current, setCurrent] = useState(1) 59 | const [pageSize, setPageSize] = useState(PAGE_SIZE) 60 | 61 | const { isLoading, data } = trpc.token.getTokenTransactionList.useQuery( 62 | { offset: (current - 1) * pageSize, limit: pageSize, tokenType: type }, 63 | { enabled: true } 64 | ) 65 | 66 | return ( 67 | 68 |
`${transactionHash}${logIndex}`} 70 | dataSource={data?.list} 71 | columns={getTokenTxsColumns(Number(type))} 72 | pagination={getPaginationConfig({ current, pageSize, total: Number(data?.count), setCurrent, setPageSize })} 73 | /> 74 | 75 | ) 76 | } 77 | 78 | export default TokenTxsTable 79 | -------------------------------------------------------------------------------- /constants/antd-theme-tokens.ts: -------------------------------------------------------------------------------- 1 | const ANTD_THEME_CONFIG = { 2 | token: { 3 | colorPrimary: '#cb8158', 4 | colorText: '#333', 5 | colorTextHeading: '#333', 6 | colorSplit: '#e7e7e7', 7 | colorBorder: '#e7e7e7', 8 | colorBorderSecondary: '#e7e7e7', 9 | borderRadius: 4, 10 | colorBgContainerDisabled: '#e5e7eb' 11 | } 12 | } 13 | 14 | export default ANTD_THEME_CONFIG 15 | -------------------------------------------------------------------------------- /constants/api.ts: -------------------------------------------------------------------------------- 1 | // status enum 2 | export const VerifyStatus = { 3 | Pending: 1, 4 | Pass: 2, 5 | Fail: 3 6 | } as const 7 | -------------------------------------------------------------------------------- /constants/index.ts: -------------------------------------------------------------------------------- 1 | export const BROWSER_TITLE = 'Scroll' 2 | export const CROSS_BROWSER_URL = 'https://goerli.etherscan.io' 3 | export const CHAIN_TOKEN = 'Eth' 4 | export const CHAIN_TOKEN_DECIMALS = 18 5 | export const BLOCK_INTERVAL = 5 6 | 7 | export const CONTENT_MIN_WIDTH = 1200 8 | export const PAGE_SIZE = 20 9 | export const TIME_FORMATTER = 'MMM-DD-YYYY HH:mm:ss A ZUTC' 10 | export const LOCAL_STORAGE_COLLAPSED_KEY = 'APP_MENU_COLLAPSED' 11 | export const TABLE_CONFIG = { 12 | SCROLL_CONFIG: { x: 1450 }, 13 | COL_WIDHT: { 14 | L1_STATUS: 160, 15 | TRANS_ARROW_ICON: 50, 16 | ADDRESS: 65, 17 | TXHASH: 120, 18 | TXFEE: 160, 19 | AGE: 140 20 | } 21 | } 22 | 23 | export const TIPS = { 24 | netErr: 'net error!', 25 | copied: 'copy successfully!' 26 | } 27 | -------------------------------------------------------------------------------- /constants/routes.ts: -------------------------------------------------------------------------------- 1 | const ROUTES = { 2 | HOME: '/', 3 | BLOCK_CHAIN: { 4 | TXNS: '/txs', 5 | PENDING_TXNS: '/txsPending', 6 | CONTRACT_TXNS: '/txsInternal', 7 | BLOCKS: '/blocks', 8 | BATCHES: '/batches', 9 | DETAIL: { 10 | TX: '/tx/:tx', 11 | BLOCK: '/blocks/:block', 12 | BATCH: '/batches/:batch', 13 | ADDRESS: '/address/:address', 14 | TOKEN: '/token/:token' 15 | } 16 | }, 17 | CONTRACT: { 18 | VERIFY: '/verifyContract', 19 | PUBLISH: '/publishContract/:contractAddress' 20 | }, 21 | TOKENS: { 22 | ERC20: '/tokens', 23 | ERC20_TRANS: '/tokentxns', 24 | ERC721: '/tokens-nft', 25 | ERC721_TRANS: '/tokentxns-nft', 26 | ERC1155: '/tokens-nft1155', 27 | ERC1155_TRANS: '/tokentxns-nft1155' 28 | }, 29 | CHARTS: { 30 | INDEX: '/charts', 31 | DETAIL: '/charts/:chart' 32 | } 33 | } 34 | 35 | export default ROUTES 36 | -------------------------------------------------------------------------------- /hooks/common/useMenuCollapsed.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | export const useMenuCollapsed = (): [boolean, (value: boolean) => void] => { 4 | const [menuCollapsed, setMenuCollapsed] = useState(false) 5 | 6 | useEffect(() => { 7 | const handleResize = () => { 8 | if (window.innerWidth < 768) { 9 | setMenuCollapsed(true) 10 | } else { 11 | setMenuCollapsed(false) 12 | } 13 | } 14 | 15 | handleResize() 16 | window.addEventListener('resize', handleResize) 17 | 18 | return () => { 19 | window.removeEventListener('resize', handleResize) 20 | } 21 | }, []) 22 | 23 | return [menuCollapsed, setMenuCollapsed] 24 | } 25 | -------------------------------------------------------------------------------- /layout/container/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import Scrollbars from 'react-custom-scrollbars' 3 | 4 | import { configureChains } from '@wagmi/core' 5 | import { scrollTestnet } from '@wagmi/core/chains' 6 | import { InjectedConnector } from '@wagmi/core/connectors/injected' 7 | import { publicProvider } from '@wagmi/core/providers/public' 8 | import { ConfigProvider } from 'antd' 9 | import Head from 'next/head' 10 | import { WagmiConfig, createClient } from 'wagmi' 11 | 12 | import { BROWSER_TITLE, CONTENT_MIN_WIDTH } from '@/constants' 13 | import ANTD_THEME_CONFIG from '@/constants/antd-theme-tokens' 14 | import Header from '@/layout/header' 15 | import Menu from '@/layout/menu' 16 | 17 | const { chains, provider } = configureChains([scrollTestnet], [publicProvider()]) 18 | 19 | const client = createClient({ 20 | autoConnect: true, 21 | connectors: [new InjectedConnector({ chains })], 22 | provider 23 | }) 24 | 25 | const Container: React.FC<{ children: ReactNode }> = ({ children }) => { 26 | return ( 27 | 28 | 29 | 30 | 31 | {BROWSER_TITLE} 32 | 33 |
34 | 35 |
36 |
37 |
38 | 39 |
40 | {children} 41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | ) 49 | } 50 | 51 | export default Container 52 | -------------------------------------------------------------------------------- /layout/footer/index.module.scss: -------------------------------------------------------------------------------- 1 | .footerWrap { 2 | background-image: url(/imgs/footer_bg.png); 3 | @apply w-full bg-[#21325b] py-30px mt-80px; 4 | 5 | .iconWrap { 6 | @apply flexCenter; 7 | 8 | > a { 9 | @apply mr-10px last:mr-0 text-20px text-white transition-opacity hover:opacity-70; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /layout/footer/index.tsx: -------------------------------------------------------------------------------- 1 | import { FacebookFilled, MediumSquareFilled, RedditSquareFilled, TwitterSquareFilled } from '@ant-design/icons' 2 | import Image from 'next/image' 3 | 4 | import { BROWSER_TITLE } from '@/constants' 5 | import { getImgSrc } from '@/utils' 6 | 7 | import style from './index.module.scss' 8 | 9 | const iconData = [ 10 | { href: 'https://twitter.com/UnifraPlatform', icon: }, 11 | // { href: '', icon: }, 12 | { href: 'https://discord.com/EPwDVewaSd', icon: }, 13 | { href: 'https://medium.com/@unifra', icon: } 14 | ] 15 | 16 | const Footer: React.FC = () => { 17 | return ( 18 |
19 |
20 |
21 |
22 | 23 |
Powered by {BROWSER_TITLE}
24 |
25 |
{BROWSER_TITLE} is a Block Explorer and Analytics Platform for
26 |
A decentralized smart contracts platform.
27 |
28 |
29 |
30 | {BROWSER_TITLE} © {new Date().getFullYear()} 31 |
32 |
33 | {iconData.map(({ href, icon }, index) => ( 34 | 35 | {icon} 36 | 37 | ))} 38 |
39 |
40 |
41 |
42 | ) 43 | } 44 | 45 | export default Footer 46 | -------------------------------------------------------------------------------- /layout/header/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react' 2 | 3 | import ArrowSvg from '@svgs/arrow.svg' 4 | import { Select } from 'antd' 5 | 6 | import SearchInput from '@/components/common/search-input' 7 | 8 | const Header: React.FC = () => { 9 | return ( 10 |
11 | 12 | 108 | 109 | 110 | 115 | 116 | 120 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | ) 133 | } 134 | 135 | export default ContractVerify 136 | -------------------------------------------------------------------------------- /prisma/fix-id.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Replace the values inside these quotes with your actual database credentials 4 | PG_DSN="postgres://postgres:12345678@10.2.0.223:5432/bsc_explorer" 5 | 6 | # Define an array of table names that need to be fixed 7 | TABLES=("accountBalance" "balanceChange" "config" "internalTransaction" "tokenTransfer" "transaction" "transactionLogs" "functionInfo") 8 | 9 | # Loop over each table and fix its schema 10 | for TABLE_NAME in "${TABLES[@]}"; do 11 | echo "Fixing schema for table $TABLE_NAME..." 12 | 13 | # Add the "id" column as optional 14 | psql $PG_DSN -c "ALTER TABLE \"$TABLE_NAME\" ADD COLUMN id SERIAL PRIMARY KEY;" 15 | 16 | # Populate the "id" column with unique values 17 | psql $PG_DSN -c "UPDATE \"$TABLE_NAME\" SET id = DEFAULT;" 18 | 19 | # Make the "id" column required 20 | psql $PG_DSN -c "ALTER TABLE \"$TABLE_NAME\" ALTER COLUMN id SET NOT NULL;" 21 | done 22 | 23 | echo "Schema fixes complete." 24 | -------------------------------------------------------------------------------- /prisma/schema.sql: -------------------------------------------------------------------------------- 1 | -- This file is used to define some sql that must be executed manually 2 | 3 | -- postgresql.conf 4 | -- shared_preload_libraries = 'pg_cron' # (change requires restart) 5 | -- CREATE EXTENSION pg_cron; 6 | 7 | -- This is a view that can be used to get the number of transactions per day 8 | CREATE MATERIALIZED VIEW daily_transaction_count AS 9 | SELECT TO_CHAR(to_timestamp("blockTime"), 'YYYY-MM-DD') AS date, 10 | COUNT(*) AS count 11 | FROM transaction 12 | GROUP BY date 13 | ORDER BY date ASC; 14 | 15 | CREATE OR REPLACE FUNCTION refresh_daily_transaction_count() RETURNS void AS $$ 16 | BEGIN 17 | REFRESH MATERIALIZED VIEW CONCURRENTLY daily_transaction_count; 18 | END; 19 | $$ LANGUAGE plpgsql; 20 | 21 | SELECT cron.schedule('0 * * * *', 'SELECT refresh_daily_transaction_count()'); -- every hour 22 | 23 | 24 | -- This is a view that can be used to get the number of token transfers per days 25 | CREATE MATERIALIZED VIEW daily_token_transfer_counts 26 | AS 27 | SELECT 28 | TO_CHAR(to_timestamp("blockTime"), 'YYYY-MM-DD') AS date, 29 | "tokenType", 30 | COUNT(*) AS count 31 | FROM "tokenTransfer" 32 | GROUP BY date, "tokenType" 33 | ORDER BY date ASC; 34 | 35 | CREATE OR REPLACE FUNCTION refresh_daily_token_transfer_counts() RETURNS void AS $$ 36 | BEGIN 37 | REFRESH MATERIALIZED VIEW CONCURRENTLY daily_token_transfer_counts; 38 | END; 39 | $$ LANGUAGE plpgsql; 40 | 41 | SELECT cron.schedule('0 * * * *', 'SELECT refresh_daily_token_transfer_counts()'); -- every hour 42 | 43 | 44 | -- This is a view that can be used to get the number of unique addresses per day 45 | CREATE MATERIALIZED VIEW daily_unique_address_count AS 46 | SELECT TO_CHAR(to_timestamp("blockTime"), 'YYYY-MM-DD') AS date, 47 | COUNT(DISTINCT "to") AS count 48 | FROM "transaction" 49 | GROUP BY date 50 | ORDER BY date ASC; 51 | 52 | CREATE OR REPLACE FUNCTION refresh_daily_unique_address_count() RETURNS void AS $$ 53 | BEGIN 54 | REFRESH MATERIALIZED VIEW CONCURRENTLY daily_unique_address_count; 55 | END; 56 | $$ LANGUAGE plpgsql; 57 | 58 | SELECT cron.schedule('0 * * * *', 'SELECT refresh_daily_unique_address_count()'); -- every hour 59 | 60 | -- This is a view that can be used to get the number of token holders per day, and the number of token transfers per day 61 | CREATE MATERIALIZED VIEW token_list_materialized AS 62 | SELECT 63 | contract."contractAddress", contract.symbol, contract."contractType", contract.name, 64 | "tokenListMaintain".description, "tokenListMaintain".tag, "tokenListMaintain".logo_path, "tokenListMaintain".list_priority, 65 | COALESCE(holders_count.count, 0) AS holders, 66 | COALESCE(trans24h_count.count, 0) AS trans24h, 67 | COALESCE(trans3d_count.count, 0) AS trans3d 68 | FROM contract 69 | LEFT OUTER JOIN "tokenListMaintain" 70 | ON contract."contractAddress" = "tokenListMaintain".contract_address 71 | LEFT OUTER JOIN ( 72 | SELECT "accountBalance".contract, COUNT("accountBalance".address) AS count 73 | FROM "accountBalance" 74 | WHERE "accountBalance".value > 0 75 | GROUP BY "accountBalance".contract 76 | ) AS holders_count 77 | ON contract."contractAddress" = holders_count.contract 78 | LEFT OUTER JOIN ( 79 | SELECT "tokenTransfer".contract, COUNT("tokenTransfer".id) AS count 80 | FROM "tokenTransfer" 81 | WHERE "tokenTransfer"."blockTime" > (EXTRACT(EPOCH FROM NOW()) - 86400) 82 | GROUP BY "tokenTransfer".contract 83 | ) AS trans24h_count 84 | ON contract."contractAddress" = trans24h_count.contract 85 | LEFT OUTER JOIN ( 86 | SELECT "tokenTransfer".contract, COUNT("tokenTransfer".id) AS count 87 | FROM "tokenTransfer" 88 | WHERE "tokenTransfer"."blockTime" > (EXTRACT(EPOCH FROM NOW()) - 3 * 86400) 89 | GROUP BY "tokenTransfer".contract 90 | ) AS trans3d_count 91 | ON contract."contractAddress" = trans3d_count.contract; 92 | 93 | CREATE OR REPLACE FUNCTION refresh_token_list_materialized() RETURNS void AS $$ 94 | BEGIN 95 | REFRESH MATERIALIZED VIEW CONCURRENTLY token_list_materialized; 96 | END; 97 | $$ LANGUAGE plpgsql; 98 | 99 | SELECT cron.schedule('0,30 * * * *', 'SELECT refresh_token_list_materialized()'); -- twice per hour -------------------------------------------------------------------------------- /public/imgs/a.sol: -------------------------------------------------------------------------------- 1 | contract A { function g() public { L.f(); } } library L { function f() public returns (uint) { return 3; } } -------------------------------------------------------------------------------- /public/imgs/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/back.png -------------------------------------------------------------------------------- /public/imgs/charts/area.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/charts/area.png -------------------------------------------------------------------------------- /public/imgs/charts/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/charts/line.png -------------------------------------------------------------------------------- /public/imgs/contract/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/contract/arrow.png -------------------------------------------------------------------------------- /public/imgs/contract/copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/contract/copy.png -------------------------------------------------------------------------------- /public/imgs/contract/full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/contract/full.png -------------------------------------------------------------------------------- /public/imgs/contract/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/contract/info.png -------------------------------------------------------------------------------- /public/imgs/contract/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/contract/link.png -------------------------------------------------------------------------------- /public/imgs/contract/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/contract/right.png -------------------------------------------------------------------------------- /public/imgs/contract/verify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/contract/verify.png -------------------------------------------------------------------------------- /public/imgs/contract/verify_failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/contract/verify_failed.png -------------------------------------------------------------------------------- /public/imgs/contract/verify_loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/contract/verify_loading.png -------------------------------------------------------------------------------- /public/imgs/contract/verify_success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/contract/verify_success.png -------------------------------------------------------------------------------- /public/imgs/home/block_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/home/block_icon.png -------------------------------------------------------------------------------- /public/imgs/home/gwei_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/home/gwei_icon.png -------------------------------------------------------------------------------- /public/imgs/home/tps_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/home/tps_icon.png -------------------------------------------------------------------------------- /public/imgs/home/trans_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/home/trans_icon.png -------------------------------------------------------------------------------- /public/imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/logo.png -------------------------------------------------------------------------------- /public/imgs/logo_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/logo_text.png -------------------------------------------------------------------------------- /public/imgs/logo_text_unifra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/logo_text_unifra.png -------------------------------------------------------------------------------- /public/imgs/logo_unifra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/logo_unifra.png -------------------------------------------------------------------------------- /public/imgs/overview/decimals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/overview/decimals.png -------------------------------------------------------------------------------- /public/imgs/overview/fee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/overview/fee.png -------------------------------------------------------------------------------- /public/imgs/overview/gas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/overview/gas.png -------------------------------------------------------------------------------- /public/imgs/overview/gas_limit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/overview/gas_limit.png -------------------------------------------------------------------------------- /public/imgs/overview/gas_price.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/overview/gas_price.png -------------------------------------------------------------------------------- /public/imgs/overview/gas_used.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/overview/gas_used.png -------------------------------------------------------------------------------- /public/imgs/overview/holders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/overview/holders.png -------------------------------------------------------------------------------- /public/imgs/overview/logs_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/overview/logs_icon.png -------------------------------------------------------------------------------- /public/imgs/overview/size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/overview/size.png -------------------------------------------------------------------------------- /public/imgs/overview/supply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/overview/supply.png -------------------------------------------------------------------------------- /public/imgs/overview/trans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/overview/trans.png -------------------------------------------------------------------------------- /public/imgs/overview/value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/overview/value.png -------------------------------------------------------------------------------- /public/imgs/qa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/qa.png -------------------------------------------------------------------------------- /public/imgs/search_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unifralabs/scroll-explorer/42e9a4e855975181d21283d5eeae6571591d498f/public/imgs/search_bg.png -------------------------------------------------------------------------------- /public/svgs/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/svgs/blockchain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/svgs/checked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/svgs/checking.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/svgs/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/svgs/hourglass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/svgs/resource.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/svgs/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/svgs/time.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/svgs/toggle_menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/svgs/token.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/svgs/verify.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /server/context.ts: -------------------------------------------------------------------------------- 1 | import * as trpc from '@trpc/server' 2 | import * as trpcNext from '@trpc/server/adapters/next' 3 | 4 | interface CreateContextOptions { 5 | // session: Session | null 6 | } 7 | 8 | /** 9 | * Inner function for `createContext` where we create the context. 10 | * This is useful for testing when we don't want to mock Next.js' request/response 11 | */ 12 | export async function createContextInner(_opts: CreateContextOptions) { 13 | return {} 14 | } 15 | 16 | export type Context = trpc.inferAsyncReturnType 17 | 18 | /** 19 | * Creates context for an incoming request 20 | * @link https://trpc.io/docs/context 21 | */ 22 | export async function createContext(opts: trpcNext.CreateNextContextOptions): Promise { 23 | // for API-response caching see https://trpc.io/docs/caching 24 | 25 | return await createContextInner({}) 26 | } 27 | -------------------------------------------------------------------------------- /server/prisma.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from '@prisma/client' 2 | 3 | declare global { 4 | var prisma: PrismaClient | undefined 5 | } 6 | 7 | const prisma = 8 | global.prisma || 9 | new PrismaClient({ 10 | log: [ 11 | { 12 | emit: 'event', 13 | level: 'query' 14 | }, 15 | { 16 | emit: 'stdout', 17 | level: 'error' 18 | }, 19 | { 20 | emit: 'stdout', 21 | level: 'info' 22 | }, 23 | { 24 | emit: 'stdout', 25 | level: 'warn' 26 | } 27 | ] 28 | }) 29 | 30 | if (process.env.NODE_ENV === 'development') global.prisma = prisma 31 | 32 | export default prisma 33 | 34 | prisma.$on('query', e => { 35 | const color = e.duration > 5000 ? '\x1b[31m%s\x1b[0m' : '%s' 36 | console.log(color, 'Query: ' + e.query) 37 | console.log(color, 'Params: ' + e.params) 38 | console.log(color, 'Duration: ' + e.duration + 'ms') 39 | }) 40 | 41 | export const getBlockHeight = async (): Promise => { 42 | const height = await prisma.block.aggregate({ 43 | _max: { 44 | blockNumber: true 45 | } 46 | }) 47 | 48 | return Number(height._max.blockNumber) 49 | } 50 | 51 | export const getEstimatedTransactionCount = async (): Promise => { 52 | const res = (await prisma.$queryRaw` 53 | SELECT reltuples::bigint AS estimate 54 | FROM pg_class 55 | WHERE relname = 'transaction'; 56 | `) as { estimate: string }[] 57 | 58 | return Number(res[0].estimate) 59 | } 60 | 61 | export const getEstimatedInternalTransactionCount = async (): Promise => { 62 | const res = (await prisma.$queryRaw` 63 | SELECT reltuples::bigint AS estimate 64 | FROM pg_class 65 | WHERE relname = 'internalTransaction'; 66 | `) as { estimate: string }[] 67 | return Number(res[0].estimate) 68 | } 69 | 70 | export const insertVerifyStatus = async (uid: string, status: number, contractAddress: string): Promise => { 71 | await prisma.contractVerifyJob.create({ 72 | data: { 73 | uid, 74 | contractAddress: contractAddress.toLowerCase(), 75 | status 76 | } 77 | }) 78 | } 79 | 80 | export const updateVerifyStatus = async (uid: string, status: number): Promise => { 81 | await prisma.contractVerifyJob.update({ 82 | where: { 83 | uid 84 | }, 85 | data: { 86 | status 87 | } 88 | }) 89 | } 90 | 91 | export const updateContract = async (address: string, source: any, input: string): Promise => { 92 | const compilerSettings = JSON.parse(source.compilerSettings) 93 | const optimizationEnabled = compilerSettings.optimizer.enabled 94 | const optimizationRuns = compilerSettings.optimizer.runs 95 | 96 | return await prisma.contract.update({ 97 | where: { 98 | contractAddress: address.toLowerCase() 99 | }, 100 | data: { 101 | status: 1, 102 | contractABI: source.abi, 103 | standardJson: input, 104 | compiler: source.compilerVersion, 105 | license: 'No license (None)', 106 | optimizationEnabled: optimizationEnabled ? 'Yes with ' + optimizationRuns + ' runs' : 'No', 107 | evmVersion: 'default(compiler defaults)' 108 | } 109 | }) 110 | } 111 | 112 | export const getByteCode = async (contractAddress: string): Promise => { 113 | const code = await prisma.contract.findFirst({ 114 | where: { 115 | contractAddress: contractAddress.toLowerCase() 116 | }, 117 | select: { 118 | byteCode: true 119 | } 120 | }) 121 | 122 | return code?.byteCode ?? '' 123 | } 124 | -------------------------------------------------------------------------------- /server/redis.ts: -------------------------------------------------------------------------------- 1 | import Redis from 'ioredis' 2 | 3 | const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379') 4 | 5 | // log redis errors 6 | redis.on('error', err => { 7 | console.error('Redis error:', err) 8 | }) 9 | 10 | export default redis 11 | -------------------------------------------------------------------------------- /server/routers/_app.ts: -------------------------------------------------------------------------------- 1 | import { publicProcedure, router } from '../trpc' 2 | import { addressRouter } from './address' 3 | import { batchRouter } from './batch' 4 | import { blockRouter } from './block' 5 | import { contractRouter } from './contract' 6 | import { statRouter } from './stat' 7 | import { summaryRouter } from './summary' 8 | import { tokenRouter } from './token' 9 | import { transactionRouter } from './transaction' 10 | import { utilRouter } from './util' 11 | 12 | export const appRouter = router({ 13 | healthcheck: publicProcedure.query(() => 'yay!'), 14 | block: blockRouter, 15 | transaction: transactionRouter, 16 | batch: batchRouter, 17 | token: tokenRouter, 18 | address: addressRouter, 19 | contract: contractRouter, 20 | util: utilRouter, 21 | summary: summaryRouter, 22 | stat: statRouter 23 | }) 24 | 25 | // export type definition of API 26 | export type AppRouter = typeof appRouter 27 | -------------------------------------------------------------------------------- /server/routers/batch.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import prisma from '../prisma' 4 | import { publicProcedure, router } from '../trpc' 5 | 6 | export const batchRouter = router({ 7 | getBatchCount: publicProcedure.query(async () => { 8 | return prisma.batch.count() 9 | }), 10 | getBatchList: publicProcedure 11 | .input( 12 | z.object({ 13 | offset: z.number().optional().default(0), 14 | limit: z.number().optional().default(20) 15 | }) 16 | ) 17 | .query(async ({ input }) => { 18 | const batchList = await prisma.$queryRaw` 19 | SELECT 20 | idx, 21 | "batchHash", 22 | "commitHash", 23 | extract(epoch from "commitTime") AS "commitTime", 24 | "proofHash", 25 | extract(epoch from "proofTime") AS "proofTime", 26 | "blockNumbers", 27 | "blockCount", 28 | "transactionCount", 29 | status 30 | FROM batch 31 | ORDER BY idx DESC 32 | LIMIT ${input.limit} OFFSET ${input.offset} 33 | ` 34 | const batchCount = await prisma.batch.count() 35 | return { 36 | count: batchCount, 37 | list: batchList 38 | } 39 | }), 40 | getBatchDetail: publicProcedure.input(z.number()).query(async ({ input }) => { 41 | const batchs = (await prisma.$queryRaw` 42 | SELECT 43 | idx, 44 | "batchHash", 45 | "commitHash", 46 | extract(epoch from "commitTime") AS "commitTime", 47 | "proofHash", 48 | extract(epoch from "proofTime") AS "proofTime", 49 | "blockNumbers", 50 | "blockCount", 51 | "transactionCount", 52 | status 53 | FROM batch 54 | WHERE idx = ${input} 55 | `) as any[] 56 | if (batchs.length === 0) { 57 | return null 58 | } 59 | const batch = batchs[0] 60 | return batch 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /server/routers/block.ts: -------------------------------------------------------------------------------- 1 | import { isHexString } from '@ethersproject/bytes' 2 | import { z } from 'zod' 3 | 4 | import prisma, { getBlockHeight } from '../prisma' 5 | import { publicProcedure, router } from '../trpc' 6 | 7 | export const blockRouter = router({ 8 | getBlockHeight: publicProcedure.query(async () => { 9 | return await getBlockHeight() 10 | }), 11 | getFinalizedBlockHeight: publicProcedure.query(async () => { 12 | const res = (await prisma.$queryRaw` 13 | SELECT (SELECT MAX(a) FROM unnest("blockNumbers") a) AS highest 14 | FROM batch 15 | WHERE status = 2 16 | ORDER BY idx DESC 17 | LIMIT 1; 18 | `) as { highest: number }[] 19 | return res[0].highest || 0 20 | }), 21 | getBlockCount: publicProcedure.query(async () => { 22 | return await prisma.block.count() 23 | }), 24 | getBlockList: publicProcedure 25 | .input( 26 | z.object({ 27 | // # order by blockNumber 28 | // # 0: asc by blockNumber 29 | // # 1: desc by blockNumber (default) 30 | order: z.number().min(0).max(1).default(1), 31 | blockNumbers: z.array(z.number()).optional(), 32 | page: z.number().min(1).default(1), 33 | offset: z.number().optional().default(0), 34 | limit: z.number().optional().default(20) 35 | }) 36 | ) 37 | .query(async ({ input }) => { 38 | const orderBy = input.order === 1 ? 'block."blockNumber" DESC' : 'block."blockNumber" ASC' 39 | const limitClause = `LIMIT ${input.limit}` 40 | let blockNumberFilter = '' 41 | if (input.blockNumbers) { 42 | blockNumberFilter = `AND block."blockNumber" IN (${input.blockNumbers.join(',')})` 43 | } 44 | if (input.page) { 45 | const height = await getBlockHeight() 46 | blockNumberFilter += `AND block."blockNumber" ${input.order === 1 ? '<' : '>'} ${(height - (input.page - 1)) * input.limit}` 47 | } 48 | const offsetClause = input.offset === 0 ? '' : `OFFSET ${input.offset}` 49 | const sql = ` 50 | SELECT block.*, 51 | batch."commitHash" as "l1CommitTransactionHash", 52 | batch."proofHash" as "l1FinalizeTransactionHash", 53 | COALESCE(batch."status", 0) as "l1Status" 54 | FROM block 55 | LEFT JOIN batch ON array [block."blockNumber"] <@ (batch."blockNumbers") 56 | WHERE 1=1 ${blockNumberFilter} 57 | ORDER BY ${orderBy} 58 | ${limitClause} 59 | ${offsetClause} 60 | ` 61 | const count = input.blockNumbers ? input.blockNumbers.length : await prisma.block.count() 62 | const blockList = await prisma.$queryRawUnsafe(sql) 63 | return { 64 | count, 65 | list: blockList 66 | } 67 | }), 68 | getBlockDetail: publicProcedure 69 | .input( 70 | z.object({ 71 | identity: z.union([z.string(), z.number()]) 72 | }) 73 | ) 74 | .query(async ({ input }) => { 75 | const whereFilter = isHexString(input.identity) ? `WHERE "blockHash" = '${input.identity}'` : `WHERE "blockNumber" = ${input.identity}` 76 | const querySql = ` 77 | WITH block_data AS ( 78 | SELECT block.*, 79 | COALESCE(batch."status", 0) as "l1Status" 80 | FROM block 81 | LEFT JOIN batch ON ARRAY [block."blockNumber"] <@ (batch."blockNumbers") 82 | ${whereFilter} ), 83 | transaction_count AS ( 84 | SELECT COUNT(*) as count 85 | FROM public."internalTransaction" 86 | ${whereFilter} 87 | ) 88 | SELECT (SELECT count FROM transaction_count) as "internalTransactionCountJoined", 89 | block_data.* 90 | FROM block_data 91 | ` 92 | 93 | const blocks = (await prisma.$queryRawUnsafe(querySql)) as any[] 94 | if (blocks.length === 0) { 95 | return null 96 | } 97 | return blocks[0] 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /server/routers/contract.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import { VerifyStatus } from '@/constants/api' 4 | import { ContractDetailType } from '@/types' 5 | import { generateUid } from '@/utils' 6 | import { VerifyJobType, queue } from '@/worker' 7 | 8 | import prisma from '../prisma' 9 | import { publicProcedure, router } from '../trpc' 10 | import { getCompilerVersions } from '../verify' 11 | 12 | export const contractRouter = router({ 13 | getContractDetail: publicProcedure.input(z.string()).query(async ({ input }) => { 14 | const address = input.trim().toLowerCase() 15 | const contract = (await prisma.$queryRaw` 16 | SELECT contract.*, "tokenListMaintain".logo_path 17 | FROM contract 18 | LEFT OUTER JOIN "tokenListMaintain" 19 | ON contract."contractAddress" = "tokenListMaintain".contract_address 20 | WHERE contract."contractAddress" = ${address} 21 | `) as ContractDetailType[] 22 | if (contract.length === 0) { 23 | return null 24 | } 25 | return contract[0] 26 | }), 27 | getContractTransactionList: publicProcedure 28 | .input( 29 | z.object({ 30 | address: z.string(), 31 | offset: z.number().default(0), 32 | limit: z.number().default(10) 33 | }) 34 | ) 35 | .query(async ({ input }) => { 36 | const address = input.address.trim().toLowerCase() 37 | 38 | const txCount = (await prisma.$queryRaw` 39 | SELECT COUNT(*) 40 | FROM transaction 41 | WHERE transaction."to" = ${address} 42 | `) as any[] 43 | 44 | const txList = (await prisma.$queryRaw` 45 | SELECT transaction.* 46 | FROM transaction 47 | WHERE transaction."to" = ${address} AND transaction."handled" = true 48 | ORDER BY transaction."blockNumber" DESC, transaction."transactionIndex" ASC 49 | OFFSET ${input.offset} LIMIT ${input.limit} 50 | `) as any[] 51 | 52 | return { 53 | count: txCount[0].count, 54 | list: txList 55 | } 56 | }), 57 | 58 | getCompilerVersions: publicProcedure.query(async () => { 59 | const res = await getCompilerVersions() 60 | const jsonRes = await res.json() 61 | return jsonRes.compilerVersions 62 | }), 63 | verifyMultiPart: publicProcedure 64 | .input( 65 | z.object({ 66 | contractAddress: z.string().length(42), 67 | compilerVersion: z.string(), 68 | sourceFiles: z.any(), 69 | evmVersion: z.string().optional(), 70 | optimizationRuns: z.number().optional(), 71 | libraries: z.record(z.string(), z.string()).optional() 72 | }) 73 | ) 74 | .output( 75 | z.object({ 76 | message: z.string(), 77 | result: z.string(), 78 | status: z.string() 79 | }) 80 | ) 81 | .mutation(async ({ input }) => { 82 | // TODO: Check if contract is already verified 83 | 84 | const uid = generateUid(input.contractAddress) 85 | await queue.add(VerifyJobType.SolidityVerifyMultiPart, { 86 | type: 'json_api', 87 | params: input, 88 | uid 89 | }) 90 | 91 | return { 92 | message: 'OK', 93 | result: uid, 94 | status: '1' 95 | } 96 | }), 97 | // Etherscan style api for contract verification 98 | // ?module=contract&action=verifysourcecode&codeformat={solidity-standard-json-input}&contractaddress={contractaddress}&contractname={contractname}&compilerversion={compilerversion}&sourceCode={sourceCode} 99 | verifyStandardJson: publicProcedure 100 | .meta({ 101 | openapi: { 102 | method: 'POST', 103 | path: '/contract', 104 | tags: ['contract'], 105 | summary: 'Etherscan style api for contract verification - verifysourcecode' 106 | } 107 | }) 108 | .input( 109 | z.object({ 110 | module: z.literal('contract'), 111 | action: z.literal('verifysourcecode'), 112 | contractaddress: z.string().length(42), 113 | sourceCode: z.string(), 114 | codeformat: z.literal('solidity-standard-json-input'), 115 | contractname: z.string(), 116 | compilerversion: z.string(), 117 | constructorArguements: z.string().optional() 118 | }) 119 | ) 120 | .output( 121 | z.object({ 122 | message: z.string(), 123 | result: z.string(), 124 | status: z.string() 125 | }) 126 | ) 127 | .mutation(async ({ input }) => { 128 | // TODO: Check if contract is already verified 129 | 130 | const uid = generateUid(input.contractaddress) 131 | await queue.add(VerifyJobType.SolidityVerifyStandardJson, { 132 | type: 'json_api', 133 | params: { 134 | contractaddress: input.contractaddress, 135 | contractname: input.contractname, 136 | compilerversion: input.compilerversion, 137 | constructorArguements: input.constructorArguements 138 | }, 139 | sourceCode: input.sourceCode, 140 | uid 141 | }) 142 | 143 | return { 144 | message: 'OK', 145 | result: uid, 146 | status: '1' 147 | } 148 | }), 149 | // ?module=contract&action=checkverifystatus&guid=0x95ad51f4406bf2AF31e3A2e2d75262EE19432261643b13f1 150 | checkverifystatus: publicProcedure 151 | .meta({ 152 | openapi: { 153 | method: 'GET', 154 | path: '/contract', 155 | tags: ['contract'], 156 | summary: 'Etherscan style api for contract verification - checkverifystatus' 157 | } 158 | }) 159 | .input( 160 | z.object({ 161 | module: z.literal('contract'), 162 | action: z.literal('checkverifystatus'), 163 | guid: z.string() 164 | }) 165 | ) 166 | .output( 167 | z.object({ 168 | message: z.string(), 169 | result: z.string(), 170 | status: z.string() 171 | }) 172 | ) 173 | .query(async ({ input }) => { 174 | const status = await prisma.contractVerifyJob.findFirst({ 175 | where: { 176 | uid: input.guid 177 | }, 178 | select: { 179 | status: true 180 | } 181 | }) 182 | let result = 'Pending in queue' 183 | switch (status?.status) { 184 | case VerifyStatus.Pass: { 185 | result = 'Pass - Verified' 186 | break 187 | } 188 | case VerifyStatus.Pending: { 189 | result = 'Pending in queue' 190 | break 191 | } 192 | case VerifyStatus.Fail: { 193 | result = 'Fail - Unable to verify' 194 | break 195 | } 196 | } 197 | 198 | return { 199 | message: 'OK', 200 | result: result, 201 | status: '1' 202 | } 203 | }) 204 | }) 205 | -------------------------------------------------------------------------------- /server/routers/openapi.ts: -------------------------------------------------------------------------------- 1 | import { generateOpenApiDocument } from 'trpc-openapi' 2 | 3 | import { appRouter } from './_app' 4 | 5 | // Generate OpenAPI schema document 6 | export const openApiDocument = generateOpenApiDocument(appRouter, { 7 | title: 'Contract Verification API', 8 | description: 'API for contract verification', 9 | version: '1.0.0', 10 | baseUrl: 'http://localhost:3000/api', 11 | tags: ['contract'] 12 | }) 13 | -------------------------------------------------------------------------------- /server/routers/stat.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import prisma from '../prisma' 4 | import { publicProcedure, router } from '../trpc' 5 | 6 | export const statRouter = router({ 7 | getDailyTxCount: publicProcedure 8 | .input( 9 | z.object({ 10 | timeStart: z.number(), 11 | timeEnd: z.number() 12 | }) 13 | ) 14 | .query(async ({ input }) => { 15 | const res = (await prisma.$queryRaw` 16 | SELECT * FROM daily_transaction_count 17 | WHERE date >= TO_CHAR(to_timestamp(${input.timeStart}), 'YYYY-MM-DD') 18 | AND date <= TO_CHAR(to_timestamp(${input.timeEnd}), 'YYYY-MM-DD') 19 | ORDER BY date ASC 20 | `) as { date: string; count: number }[] 21 | return res 22 | }), 23 | 24 | getDailyTokenTransferCount: publicProcedure 25 | .input( 26 | z.object({ 27 | tokenType: z.number(), 28 | timeStart: z.number(), 29 | timeEnd: z.number() 30 | }) 31 | ) 32 | .query(async ({ input }) => { 33 | const res = (await prisma.$queryRaw` 34 | SELECT * FROM daily_token_transfer_counts 35 | WHERE date >= TO_CHAR(to_timestamp(${input.timeStart}), 'YYYY-MM-DD') 36 | AND date <= TO_CHAR(to_timestamp(${input.timeEnd}), 'YYYY-MM-DD') 37 | AND "tokenType" = ${input.tokenType} 38 | ORDER BY date ASC 39 | `) as { date: string; count: number }[] 40 | return res 41 | }), 42 | 43 | getUniqueAddressesCount: publicProcedure 44 | .input( 45 | z.object({ 46 | timeStart: z.number(), 47 | timeEnd: z.number() 48 | }) 49 | ) 50 | .query(async ({ input }) => { 51 | const res = (await prisma.$queryRaw` 52 | SELECT * FROM daily_unique_address_count 53 | WHERE date >= TO_CHAR(to_timestamp(${input.timeStart}), 'YYYY-MM-DD') 54 | AND date <= TO_CHAR(to_timestamp(${input.timeEnd}), 'YYYY-MM-DD') 55 | ORDER BY date ASC 56 | `) as { date: string; count: number }[] 57 | return res 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /server/routers/summary.ts: -------------------------------------------------------------------------------- 1 | import prisma from '../prisma' 2 | import redis from '../redis' 3 | import { publicProcedure, router } from '../trpc' 4 | 5 | const RedisSummaryAvgTPS24hKey = 'scroll-summary-avg-tps-24h' 6 | const RedisSummaryAvgPrice24Key = 'scroll-summary-avg-price-24h' 7 | 8 | export const summaryRouter = router({ 9 | getAvgTps24h: publicProcedure.query(async () => { 10 | const v = await redis.get(RedisSummaryAvgTPS24hKey) 11 | if (v !== null) { 12 | return v 13 | } else { 14 | const txCount = await prisma.transaction.count({ 15 | where: { 16 | blockTime: { 17 | gt: Math.floor(Date.now() / 1000) - 86400 18 | } 19 | } 20 | }) 21 | const tps = txCount / 86400 22 | return tps.toString() 23 | } 24 | }), 25 | 26 | getAvgPrice24h: publicProcedure.query(async () => { 27 | const v = await redis.get(RedisSummaryAvgPrice24Key) 28 | if (v !== null) { 29 | return v 30 | } else { 31 | const aggregations = await prisma.transaction.aggregate({ 32 | _sum: { 33 | gasPrice: true 34 | }, 35 | _count: true, 36 | where: { 37 | blockTime: { 38 | gt: Math.floor(Date.now() / 1000) - 86400 39 | } 40 | } 41 | }) 42 | let price = 0.0 43 | const gasPrice = aggregations._sum.gasPrice 44 | const txCount = aggregations._count 45 | if (gasPrice !== null && txCount !== 0) { 46 | price = Number(gasPrice) / txCount 47 | price = parseFloat((price / 1000000000).toFixed(2)) 48 | } 49 | return price.toString() 50 | } 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /server/routers/token.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | import prisma from '../prisma' 4 | import { publicProcedure, router } from '../trpc' 5 | 6 | export const tokenRouter = router({ 7 | getTokenDetail: publicProcedure.input(z.string()).query(async ({ input }) => { 8 | const address = input.trim().toLowerCase() 9 | const [contract, accountBalanceCount] = await Promise.all([ 10 | prisma.$queryRaw` 11 | SELECT contract.*, "tokenListMaintain".tag, "tokenListMaintain".logo_path 12 | FROM contract 13 | LEFT OUTER JOIN "tokenListMaintain" ON contract."contractAddress" = "tokenListMaintain".contract_address 14 | WHERE contract."contractAddress" = ${address} 15 | ` as any, 16 | prisma.$queryRaw` 17 | SELECT COUNT(*) 18 | FROM "accountBalance" 19 | WHERE "accountBalance".contract = ${address} 20 | AND "accountBalance".value > 0 21 | ` as any 22 | ]) 23 | if (!contract[0]) { 24 | return null 25 | } 26 | 27 | const token = { 28 | ...contract[0], 29 | holders: accountBalanceCount[0].count 30 | } 31 | return token 32 | }), 33 | getTokenHolders: publicProcedure 34 | .input( 35 | z.object({ 36 | address: z.string(), 37 | offset: z.number().min(0), 38 | limit: z.number().min(1).max(100).default(10) 39 | }) 40 | ) 41 | .query(async ({ input }) => { 42 | const address = input.address.toLowerCase() 43 | const contract = await prisma.contract.findUnique({ 44 | where: { 45 | contractAddress: address 46 | } 47 | }) 48 | if (!contract) { 49 | return null 50 | } 51 | 52 | const accountBalanceCount = await prisma.accountBalance.count({ 53 | where: { 54 | contract: address, 55 | value: { 56 | gt: 0 57 | } 58 | } 59 | }) 60 | const accountBalances = await prisma.accountBalance.findMany({ 61 | select: { 62 | address: true, 63 | value: true, 64 | tokenId: true 65 | }, 66 | where: { 67 | contract: address, 68 | value: { 69 | gt: 0 70 | } 71 | }, 72 | orderBy: { 73 | value: 'desc' 74 | }, 75 | skip: input.offset, 76 | take: input.limit 77 | }) 78 | 79 | let balances = [] 80 | for (const balance of accountBalances) { 81 | const balanceObj = { 82 | address: balance.address, 83 | value: '0', 84 | tokenId: balance.tokenId, 85 | percentage: '0' 86 | } 87 | if (Number(contract.totalSupply) == 0) { 88 | balanceObj.percentage = '0' 89 | balanceObj.value = balance.value ? balance.value.toString() : '0' 90 | } else { 91 | balanceObj.percentage = ((Number(balance.value) / contract.totalSupply.toNumber()) * 100).toString() 92 | balanceObj.value = balance.value ? (balance.value.toNumber() / 10 ** Number(contract.decimals)).toString() : '0' 93 | } 94 | balances.push(balanceObj) 95 | } 96 | 97 | return { 98 | count: accountBalanceCount, 99 | list: balances 100 | } 101 | }), 102 | getTokenList: publicProcedure 103 | .input( 104 | z.object({ 105 | offset: z.number().min(0), 106 | limit: z.number().min(1).max(100).default(10), 107 | tokenType: z.number().min(1).max(3).default(1) 108 | }) 109 | ) 110 | .query(async ({ input }) => { 111 | const tokensCountPromise = prisma.$queryRaw` 112 | SELECT COUNT(*) FROM "token_list_materialized" WHERE "contractType" = ${input.tokenType} 113 | ` 114 | // if erc20, return erc20 list order by holders 115 | if (input.tokenType == 1) { 116 | const tokensPromise = prisma.$queryRaw` 117 | SELECT * FROM "token_list_materialized" 118 | WHERE "contractType" = ${input.tokenType} 119 | ORDER BY "holders" DESC 120 | OFFSET ${input.offset} 121 | LIMIT ${input.limit} 122 | ` 123 | const [tokensCount, tokens] = await Promise.all([tokensCountPromise, tokensPromise]) 124 | const count = tokensCount as any[] 125 | return { 126 | count: count[0].count, 127 | list: tokens 128 | } 129 | } 130 | // if erc721/erc1155, return erc721/erc1155 list order by trans24h 131 | if (input.tokenType == 2 || input.tokenType == 3) { 132 | const tokensPromise = prisma.$queryRaw` 133 | SELECT * FROM "token_list_materialized" 134 | WHERE "contractType" = ${input.tokenType} 135 | ORDER BY "trans24h" DESC 136 | OFFSET ${input.offset} 137 | LIMIT ${input.limit} 138 | ` 139 | const [tokensCount, tokens] = await Promise.all([tokensCountPromise, tokensPromise]) 140 | const count = tokensCount as any[] 141 | return { 142 | count: count[0].count, 143 | list: tokens 144 | } 145 | } 146 | 147 | return null 148 | }), 149 | getTokenTransactionList: publicProcedure 150 | .input( 151 | z.object({ 152 | address: z.string().optional(), 153 | tokenType: z.number().min(1).max(3).default(1), 154 | order: z.number().min(0).max(1).default(1), 155 | offset: z.number().min(0).default(0), 156 | limit: z.number().min(1).max(100).default(10) 157 | }) 158 | ) 159 | .query(async ({ input }) => { 160 | const address = input.address?.toLowerCase() 161 | const queryAddressFilter = address ? `AND "tokenTransfer".contract = '${address}'` : '' 162 | 163 | const transCount = (await prisma.$queryRawUnsafe(` 164 | SELECT COUNT(*) 165 | FROM "tokenTransfer" 166 | INNER JOIN "contract" ON "tokenTransfer".contract = "contract"."contractAddress" 167 | WHERE "tokenType" = ${input.tokenType} 168 | ${queryAddressFilter} 169 | `)) as any[] 170 | 171 | const trans = (await prisma.$queryRawUnsafe(` 172 | WITH filtered_data AS ( 173 | SELECT * 174 | FROM "tokenTransfer" 175 | WHERE "tokenType" = ${input.tokenType} 176 | ${queryAddressFilter} 177 | ORDER BY "blockTime" DESC 178 | OFFSET ${input.offset} 179 | LIMIT ${input.limit} 180 | ) 181 | SELECT filtered_data.*, "contract".name, "contract".symbol, "contract".decimals, "tokenListMaintain".logo_path 182 | FROM filtered_data 183 | LEFT OUTER JOIN "tokenListMaintain" ON filtered_data.contract = "tokenListMaintain".contract_address 184 | INNER JOIN "contract" ON filtered_data.contract = "contract"."contractAddress" 185 | `)) as any[] 186 | 187 | let tokenTransfers = [] 188 | for (const tran of trans) { 189 | tokenTransfers.push(tran) 190 | } 191 | return { 192 | count: transCount ? transCount[0].count : 0, 193 | list: tokenTransfers 194 | } 195 | }) 196 | }) 197 | -------------------------------------------------------------------------------- /server/routers/util.ts: -------------------------------------------------------------------------------- 1 | import { isHexString } from '@ethersproject/bytes' 2 | import { z } from 'zod' 3 | 4 | import { isPositiveInteger } from '@/utils' 5 | 6 | import prisma from '../prisma' 7 | import { publicProcedure, router } from '../trpc' 8 | 9 | export const utilRouter = router({ 10 | search: publicProcedure.input(z.string()).query(async ({ input }) => { 11 | const inputTrimmed = input.trim().toLowerCase() 12 | const inputLength = inputTrimmed.length 13 | 14 | // maybe block height 15 | if (isPositiveInteger(inputTrimmed)) { 16 | const blockCount = await prisma.block.count({ 17 | where: { 18 | blockNumber: parseInt(inputTrimmed) 19 | } 20 | }) 21 | if (blockCount > 0) { 22 | return { result: 'block' } 23 | } 24 | return { result: null } 25 | } 26 | 27 | // maybe block hash or tx hash or user address or contract address 28 | if (isHexString(inputTrimmed)) { 29 | if (inputLength === 42) { 30 | // maybe address-contract 31 | const contractCount = await prisma.contract.count({ 32 | where: { contractAddress: inputTrimmed } 33 | }) 34 | if (contractCount > 0) { 35 | return { result: 'address-contract' } 36 | } 37 | 38 | // maybe address-user (from transaction) 39 | const transactionCount = await prisma.transaction.count({ 40 | where: { OR: [{ from: inputTrimmed }, { to: inputTrimmed }] } 41 | }) 42 | if (transactionCount > 0) { 43 | return { result: 'address-user' } 44 | } 45 | 46 | // maybe address-user (from internal transaction) 47 | const internalTransactionCount = await prisma.internalTransaction.count({ 48 | where: { to: inputTrimmed } 49 | }) 50 | if (internalTransactionCount > 0) { 51 | return { result: 'address-user' } 52 | } 53 | 54 | // not match 55 | return { result: null } 56 | } else if (inputLength === 66) { 57 | // maybe block hash 58 | const blockCount = await prisma.block.count({ 59 | where: { blockHash: inputTrimmed } 60 | }) 61 | if (blockCount > 0) { 62 | return { result: 'block' } 63 | } 64 | 65 | // maybe tx hash 66 | const transactionCount = await prisma.transaction.count({ 67 | where: { hash: inputTrimmed } 68 | }) 69 | if (transactionCount > 0) { 70 | return { result: 'transaction' } 71 | } 72 | 73 | // not match 74 | return { result: null } 75 | } 76 | } 77 | return { result: null } 78 | }), 79 | getSolcVersions: publicProcedure.query(async () => { 80 | const conf = await prisma.config.findUnique({ 81 | where: { 82 | key: 'solcVersion' 83 | } 84 | }) 85 | if (!conf) { 86 | return null 87 | } 88 | return JSON.parse(conf.value as string) 89 | }), 90 | getEvmVersions: publicProcedure.query(async () => { 91 | const conf = await prisma.config.findUnique({ 92 | where: { 93 | key: 'evmVersion' 94 | } 95 | }) 96 | if (!conf) { 97 | return null 98 | } 99 | return JSON.parse(conf.value as string) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /server/trpc.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is your entry point to setup the root configuration for tRPC on the server. 3 | * - `initTRPC` should only be used once per app. 4 | * - We export only the functionality that we use so we can enforce which base procedures should be used 5 | * 6 | * Learn how to create protected base procedures and other things below: 7 | * @see https://trpc.io/docs/v10/router 8 | * @see https://trpc.io/docs/v10/procedures 9 | */ 10 | import { initTRPC } from '@trpc/server' 11 | import { OpenApiMeta } from 'trpc-openapi' 12 | 13 | import { transformer } from '../utils/transformer' 14 | import { Context } from './context' 15 | 16 | const t = initTRPC 17 | .context() 18 | .meta() 19 | .create({ 20 | /** 21 | * @see https://trpc.io/docs/v10/data-transformers 22 | */ 23 | transformer, 24 | /** 25 | * @see https://trpc.io/docs/v10/error-formatting 26 | */ 27 | errorFormatter({ shape }) { 28 | return shape 29 | } 30 | }) 31 | 32 | /** 33 | * Create a router 34 | * @see https://trpc.io/docs/v10/router 35 | */ 36 | export const router = t.router 37 | 38 | /** 39 | * Create an unprotected procedure 40 | * @see https://trpc.io/docs/v10/procedures 41 | **/ 42 | export const publicProcedure = t.procedure 43 | 44 | /** 45 | * @see https://trpc.io/docs/v10/middlewares 46 | */ 47 | export const middleware = t.middleware 48 | 49 | /** 50 | * @see https://trpc.io/docs/v10/merging-routers 51 | */ 52 | export const mergeRouters = t.mergeRouters 53 | -------------------------------------------------------------------------------- /server/verify.ts: -------------------------------------------------------------------------------- 1 | export type VerifyStandardJsonInputParams = { 2 | bytecode: string 3 | bytecodeType: string 4 | compilerVersion: string 5 | input: string 6 | } 7 | 8 | export type VerifyMultiPartParams = { 9 | bytecode: string 10 | bytecodeType: string 11 | compilerVersion: string 12 | evmVersion?: string 13 | optimizationRuns?: number 14 | sourceFiles: Record 15 | libraries?: Record 16 | } 17 | 18 | export function verifyStandardJsonInput(params: VerifyStandardJsonInputParams) { 19 | const api = process.env.VERIFICATION_URL + '/api/v2/verifier/solidity/sources:verify-standard-json' 20 | return fetch(api, { 21 | method: 'POST', 22 | headers: { 23 | 'Content-Type': 'application/json' 24 | }, 25 | body: JSON.stringify(params) 26 | }) 27 | } 28 | 29 | export function verifyMultiPart(params: VerifyMultiPartParams) { 30 | const api = process.env.VERIFICATION_URL + '/api/v2/verifier/solidity/sources:verify-multi-part' 31 | return fetch(api, { 32 | method: 'POST', 33 | headers: { 34 | 'Content-Type': 'application/json' 35 | }, 36 | body: JSON.stringify(params) 37 | }) 38 | } 39 | 40 | export function getCompilerVersions() { 41 | const api = process.env.VERIFICATION_URL + '/api/v2/verifier/solidity/versions' 42 | return fetch(api, { 43 | method: 'GET', 44 | headers: { 45 | 'Content-Type': 'application/json' 46 | } 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /smart-contract-verifier/config.toml: -------------------------------------------------------------------------------- 1 | [server.http] 2 | enabled = true 3 | addr = "0.0.0.0:8050" 4 | max_body_size = 2097152 5 | 6 | [server.grpc] 7 | enabled = false 8 | addr = "0.0.0.0:8051" 9 | 10 | 11 | [solidity] 12 | enabled = true 13 | compilers_dir = "/tmp/solidity-compilers" 14 | refresh_versions_schedule = "0 0 * * * * *" 15 | 16 | [solidity.fetcher.list] 17 | # It depends on the OS you are running the service on 18 | list_url = "https://solc-bin.ethereum.org/linux-amd64/list.json" 19 | # list_url = "https://solc-bin.ethereum.org/macosx-amd64/list.json" 20 | # list_url = "https://solc-bin.ethereum.org/windows-amd64/list.json" 21 | 22 | #[solidity.fetcher.s3] 23 | #access_key = "access_key" 24 | #secret_key = "secret_key" 25 | #region = "region" 26 | #endpoint = "endpoint" 27 | ## The only required field for the s3 fetcher 28 | #bucket = "bucket" 29 | 30 | [vyper] 31 | enabled = true 32 | compilers_dir = "/tmp/vyper-compilers" 33 | refresh_versions_schedule = "0 0 * * * * *" 34 | 35 | [vyper.fetcher.list] 36 | list_url = "https://raw.githubusercontent.com/blockscout/solc-bin/main/vyper.list.json" 37 | # list_url = "https://raw.githubusercontent.com/blockscout/solc-bin/main/vyper.macos.list.json" 38 | 39 | [sourcify] 40 | enabled = true 41 | api_url = "https://sourcify.dev/server/" 42 | verification_attempts = 3 43 | request_timeout = 15 44 | 45 | [metrics] 46 | enabled = false 47 | addr = "0.0.0.0:6060" 48 | route = "/metrics" 49 | 50 | [jaeger] 51 | enabled = false 52 | agent_endpoint = "localhost:6831" 53 | 54 | [compilers] 55 | # if omitted, number of CPU cores would be used 56 | max_threads = 8 57 | 58 | # [extensions.solidity.sig_provider] 59 | # url = "http://127.0.0.1:8051/" 60 | 61 | # [extensions.vyper.sig_provider] 62 | # url = "http://127.0.0.1:8051/" 63 | 64 | # [extensions.sourcify.sig_provider] 65 | # url = "http://127.0.0.1:8051/" 66 | -------------------------------------------------------------------------------- /smart-contract-verifier/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | smart-contract-verifier: 4 | image: ghcr.io/blockscout/smart-contract-verifier:v1.1.0 5 | ports: 6 | - '8050:8050' 7 | - '8051:8051' 8 | environment: 9 | - SMART_CONTRACT_VERIFIER__CONFIG=/app/config.toml 10 | volumes: 11 | ## optional: you can use default config or provide custom via file 12 | - ./config.toml:/app/config.toml 13 | ## optional: provide volume or folder to store compilers between launches 14 | - /tmp/compilers:/tmp/compilers 15 | -------------------------------------------------------------------------------- /styles/global.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v5.0.1 | 20191019 3 | License: none (public domain) 4 | */ 5 | html, 6 | body, 7 | div, 8 | span, 9 | applet, 10 | object, 11 | iframe, 12 | h1, 13 | h2, 14 | h3, 15 | h4, 16 | h5, 17 | h6, 18 | p, 19 | blockquote, 20 | pre, 21 | a, 22 | abbr, 23 | acronym, 24 | address, 25 | big, 26 | cite, 27 | code, 28 | del, 29 | dfn, 30 | em, 31 | img, 32 | ins, 33 | kbd, 34 | q, 35 | s, 36 | samp, 37 | small, 38 | strike, 39 | strong, 40 | sub, 41 | sup, 42 | tt, 43 | var, 44 | b, 45 | u, 46 | i, 47 | center, 48 | dl, 49 | dt, 50 | dd, 51 | menu, 52 | ol, 53 | ul, 54 | li, 55 | fieldset, 56 | form, 57 | label, 58 | legend, 59 | table, 60 | caption, 61 | tbody, 62 | tfoot, 63 | thead, 64 | tr, 65 | th, 66 | td, 67 | article, 68 | aside, 69 | canvas, 70 | details, 71 | embed, 72 | figure, 73 | figcaption, 74 | footer, 75 | header, 76 | hgroup, 77 | main, 78 | menu, 79 | nav, 80 | output, 81 | ruby, 82 | section, 83 | summary, 84 | time, 85 | mark, 86 | audio, 87 | video { 88 | margin: 0; 89 | padding: 0; 90 | border: 0; 91 | font-size: 100%; 92 | font: inherit; 93 | vertical-align: baseline; 94 | } 95 | 96 | /* HTML5 display-role reset for older browsers */ 97 | article, 98 | aside, 99 | details, 100 | figcaption, 101 | figure, 102 | footer, 103 | header, 104 | hgroup, 105 | main, 106 | menu, 107 | nav, 108 | section { 109 | display: block; 110 | } 111 | 112 | /* HTML5 hidden-attribute fix for newer browsers */ 113 | *[hidden] { 114 | display: none; 115 | } 116 | 117 | body { 118 | line-height: 1; 119 | } 120 | 121 | menu, 122 | ol, 123 | ul { 124 | list-style: none; 125 | } 126 | 127 | blockquote, 128 | q { 129 | quotes: none; 130 | } 131 | 132 | blockquote:before, 133 | blockquote:after, 134 | q:before, 135 | q:after { 136 | content: ''; 137 | content: none; 138 | } 139 | 140 | table { 141 | border-collapse: collapse; 142 | border-spacing: 0; 143 | } 144 | 145 | a { 146 | color: #cb8158; 147 | text-decoration: none; 148 | } 149 | 150 | a:hover { 151 | color: #d9a280; 152 | } 153 | 154 | b, 155 | strong { 156 | font-weight: bold !important; 157 | } 158 | 159 | html, 160 | body, 161 | #root { 162 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, 163 | Segoe UI Symbol, Noto Color Emoji; 164 | font-feature-settings: 'tnum', 'tnum'; 165 | @apply w-full bg-page; 166 | } 167 | 168 | textarea { 169 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace !important; 170 | } 171 | 172 | /* unifrascan-tooltip */ 173 | .unifrascan-tooltip { 174 | @apply max-w-600px !important; 175 | } 176 | 177 | .unifrascan-tooltip .unifrascan-tooltip-inner { 178 | @apply text-12; 179 | } 180 | 181 | /* unifrascan-pagination */ 182 | .unifrascan-spin-container > .unifrascan-pagination:first-child { 183 | @apply mt-0; 184 | } 185 | 186 | .unifrascan-spin-container > .unifrascan-pagination:last-child { 187 | @apply mb-0; 188 | } 189 | 190 | .unifrascan-pagination { 191 | @apply flex justify-end items-center; 192 | } 193 | 194 | .unifrascan-pagination .unifrascan-pagination-total-text { 195 | @apply flex-1 text-[#999]; 196 | } 197 | 198 | /* unifrascan-table */ 199 | .unifrascan-table-thead > tr > th { 200 | @apply font-bold text-secondText !important; 201 | } 202 | 203 | /* unifrascan-date */ 204 | .small-date-range-picker .unifrascan-picker-input > input { 205 | @apply text-12; 206 | } 207 | 208 | /* unifrascan-tabs */ 209 | .unifrascan-tabs-tab { 210 | @apply text-[#0009] text-14 px-20px py-14px !important; 211 | } 212 | 213 | .unifrascan-tabs-tab.unifrascan-tabs-tab-active .unifrascan-tabs-tab-btn { 214 | @apply font-500; 215 | } 216 | 217 | .unifrascan-tabs-tab + .unifrascan-tabs-tab { 218 | @apply ml-40px !important; 219 | } 220 | 221 | /* unifrascan-card */ 222 | .unifrascan-card { 223 | box-shadow: 0 0 20px rgba(215, 222, 227, 0.21); 224 | } 225 | 226 | .unifrascan-card-head { 227 | @apply font-500 !important; 228 | } 229 | 230 | .unifrascan-card-bordered { 231 | @apply border-none !important; 232 | } 233 | 234 | /* unifrascan-collapse */ 235 | .unifrascan-collapse { 236 | @apply border-none !important; 237 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); 238 | } 239 | 240 | .unifrascan-collapse-header { 241 | @apply bg-[#fafafa]; 242 | } 243 | 244 | .modal-with-no-footer-border .unifrascan-modal-footer { 245 | @apply border-none py-16px; 246 | } 247 | 248 | /* header network select */ 249 | .header-select-wrap { 250 | @apply w-180px h-26px font-400 leading-26px rounded-22px; 251 | background: linear-gradient(93.7deg, #ac621f 6.27%, #9e4817 94.49%); 252 | 253 | .unifrascan-select-selection-item { 254 | @apply text-white h-26px leading-24px px-6px !important; 255 | } 256 | 257 | svg { 258 | @apply transform rotate-180; 259 | 260 | path { 261 | @apply fill-white; 262 | } 263 | } 264 | } 265 | 266 | .header-select-popup-wrap { 267 | @apply border-1px border-solid border-border p-6px !important; 268 | box-shadow: 0 1px 10px rgba(0, 0, 0, 0.05), 0 4px 5px rgba(0, 0, 0, 0.08), 0 2px 4px -1px rgba(0, 0, 0, 0.12); 269 | 270 | .unifrascan-select-item-option-content { 271 | @apply text-main; 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /styles/global.less: -------------------------------------------------------------------------------- 1 | @import '~antd/lib/style/themes/default.less'; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": "./", 18 | "paths": { 19 | "@/*": ["./*"], 20 | "@imgs/*": ["./public/imgs/*"], 21 | "@svgs/*": ["./public/svgs/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /utils/blockies.ts: -------------------------------------------------------------------------------- 1 | // The random number is a js implementation of the Xorshift PRNG 2 | const randseed = new Array(4) // Xorshift: [x, y, z, w] 32 bit values 3 | 4 | function seedrand(seed) { 5 | randseed.fill(0) 6 | 7 | for (let i = 0; i < seed.length; i++) { 8 | randseed[i % 4] = (randseed[i % 4] << 5) - randseed[i % 4] + seed.charCodeAt(i) 9 | } 10 | } 11 | 12 | function rand() { 13 | // based on Java's String.hashCode(), expanded to 4 32bit values 14 | const t = randseed[0] ^ (randseed[0] << 11) 15 | 16 | randseed[0] = randseed[1] 17 | randseed[1] = randseed[2] 18 | randseed[2] = randseed[3] 19 | randseed[3] = randseed[3] ^ (randseed[3] >> 19) ^ t ^ (t >> 8) 20 | 21 | return (randseed[3] >>> 0) / ((1 << 31) >>> 0) 22 | } 23 | 24 | function createColor() { 25 | //saturation is the whole color spectrum 26 | const h = Math.floor(rand() * 360) 27 | //saturation goes from 40 to 100, it avoids greyish colors 28 | const s = rand() * 60 + 40 + '%' 29 | //lightness can be anything from 0 to 100, but probabilities are a bell curve around 50% 30 | const l = (rand() + rand() + rand() + rand()) * 25 + '%' 31 | 32 | return 'hsl(' + h + ',' + s + ',' + l + ')' 33 | } 34 | 35 | function createImageData(size) { 36 | const width = size // Only support square icons for now 37 | const height = size 38 | 39 | const dataWidth = Math.ceil(width / 2) 40 | const mirrorWidth = width - dataWidth 41 | 42 | const data: any[] = [] 43 | for (let y = 0; y < height; y++) { 44 | let row: any[] = [] 45 | for (let x = 0; x < dataWidth; x++) { 46 | // this makes foreground and background color to have a 43% (1/2.3) probability 47 | // spot color has 13% chance 48 | row[x] = Math.floor(rand() * 2.3) 49 | } 50 | const r = row.slice(0, mirrorWidth) 51 | r.reverse() 52 | row = row.concat(r) 53 | 54 | for (let i = 0; i < row.length; i++) { 55 | data.push(row[i]) 56 | } 57 | } 58 | 59 | return data 60 | } 61 | 62 | function buildOpts(opts) { 63 | const newOpts: any = {} 64 | 65 | newOpts.seed = opts.seed || Math.floor(Math.random() * Math.pow(10, 16)).toString(16) 66 | 67 | seedrand(newOpts.seed) 68 | 69 | newOpts.size = opts.size || 8 70 | newOpts.scale = opts.scale || 4 71 | newOpts.color = opts.color || createColor() 72 | newOpts.bgcolor = opts.bgcolor || createColor() 73 | newOpts.spotcolor = opts.spotcolor || createColor() 74 | 75 | return newOpts 76 | } 77 | 78 | export function renderIcon(opts, canvas) { 79 | if (!canvas) return 80 | 81 | opts = buildOpts(opts || {}) 82 | const imageData = createImageData(opts.size) 83 | const width = Math.sqrt(imageData.length) 84 | 85 | canvas.width = opts.size * opts.scale 86 | canvas.height = opts.size * opts.scale 87 | 88 | const cc = canvas.getContext('2d') 89 | cc.fillStyle = opts.bgcolor 90 | cc.fillRect(0, 0, canvas.width, canvas.height) 91 | cc.fillStyle = opts.color 92 | 93 | for (let i = 0; i < imageData.length; i++) { 94 | // if data is 0, leave the background 95 | if (imageData[i]) { 96 | const row = Math.floor(i / width) 97 | const col = i % width 98 | 99 | // if data is 2, choose spot color, if 1 choose foreground 100 | cc.fillStyle = imageData[i] == 1 ? opts.color : opts.spotcolor 101 | 102 | cc.fillRect(col * opts.scale, row * opts.scale, opts.scale, opts.scale) 103 | } 104 | } 105 | 106 | return canvas 107 | } 108 | 109 | function createIcon(opts) { 110 | var canvas = document.createElement('canvas') 111 | 112 | renderIcon(opts, canvas) 113 | 114 | return canvas 115 | } 116 | -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js' 2 | import dayjs from 'dayjs' 3 | import utc from 'dayjs/plugin/utc' 4 | import { stringifyUrl } from 'query-string' 5 | import { format } from 'timeago.js' 6 | 7 | import { CHAIN_TOKEN, CROSS_BROWSER_URL, TIME_FORMATTER } from '@/constants' 8 | import { ApiPaginationParams } from '@/types' 9 | 10 | export const getImgSrc = (path: string) => require(`../public/imgs/${path}.png`) 11 | 12 | export const convertNum = (num: number | bigint | string) => { 13 | if (typeof num === 'string') { 14 | return num 15 | } 16 | if (typeof num === 'bigint') { 17 | return num.toString() 18 | } 19 | return num 20 | } 21 | 22 | export const formatNum = (num: number | string, preffix = '', suffix = '') => { 23 | if (undefined === num || null === num || '' === num) return '-' 24 | 25 | const suf = suffix ? ` ${suffix}` : '' 26 | const str = num.toString() 27 | if (str.length <= 3) return preffix + str + suf 28 | let integer: any = [] 29 | let floater: any = [] 30 | if (!str.includes('.')) { 31 | integer = str.split('') 32 | } else { 33 | const ary = str.split('.') 34 | integer = ary[0].split('') 35 | floater = ary[1] 36 | } 37 | let count = 0 38 | integer.length % 3 === 0 ? (count = integer.length / 3 - 1) : (count = Math.floor(integer.length / 3)) 39 | for (let i = 0; i < count; i++) { 40 | integer.splice(integer.length - (i + 1) * 3 - i, 0, ',') 41 | } 42 | let finalStr = '' 43 | floater.length == 0 ? (finalStr = integer.join('')) : (finalStr = integer.join('') + '.' + floater) 44 | return preffix + finalStr + suf 45 | } 46 | 47 | export const formatNumWithSymbol = (num = 0, digits = 2) => { 48 | const si = [ 49 | { value: 1, symbol: '' }, 50 | { value: 1e3, symbol: 'K' }, 51 | { value: 1e6, symbol: 'M' }, 52 | { value: 1e9, symbol: 'G' }, 53 | { value: 1e12, symbol: 'T' }, 54 | { value: 1e15, symbol: 'P' }, 55 | { value: 1e18, symbol: 'E' } 56 | ] 57 | 58 | const rx = /\.0+$|(\.[0-9]*[1-9])0+$/ 59 | let i 60 | for (i = si.length - 1; i > 0; i--) { 61 | if (num >= si[i].value) { 62 | break 63 | } 64 | } 65 | return `${formatNum((num / si[i].value).toFixed(digits).replace(rx, '$1'))} ${si[i].symbol}` 66 | } 67 | 68 | export const convertBalance = ({ balance, decimals = 18 }: { balance: any; decimals?: number }) => 69 | new BigNumber(balance.toString()).div(new BigNumber(10).pow(decimals)).toString() 70 | 71 | export const convertGwei = (num: string | number | undefined) => 72 | new BigNumber(new BigNumber(num ?? 0).div(new BigNumber(10).pow(9)).toFixed(8, BigNumber.ROUND_FLOOR)).toFixed() + ' Gwei' 73 | 74 | export const transDisplayNum = ({ 75 | num, 76 | fixedNum = 6, 77 | preffix = '', 78 | suffix = CHAIN_TOKEN, 79 | decimals = 18 80 | }: { 81 | num: string | number | BigNumber | null | undefined 82 | fixedNum?: number 83 | preffix?: string 84 | suffix?: string 85 | decimals?: number 86 | }): string => { 87 | if (!!!Number(num)) return `${preffix}0${suffix ? ` ${suffix}` : ''}` 88 | 89 | return formatNum( 90 | new BigNumber( 91 | new BigNumber(convertBalance({ balance: num, decimals: null === decimals ? 0 : decimals })).toFixed(fixedNum, BigNumber.ROUND_FLOOR) 92 | ).toFixed(), 93 | preffix, 94 | suffix 95 | ) 96 | } 97 | 98 | export const transDisplayTime = (time?: number | bigint | null) => { 99 | if (!!!time) return '-' 100 | 101 | dayjs.extend(utc) 102 | return dayjs(Number(time) * 1000) 103 | .utc() 104 | .format(TIME_FORMATTER) 105 | } 106 | 107 | export const transDisplayTimeAgo = (time?: number | bigint | null) => { 108 | if (!!!time) return '-' 109 | 110 | return format(Number(time) * 1000) 111 | } 112 | 113 | export const transApiPaginationParams = ({ page = 1, limit, ...data }: ApiPaginationParams) => ({ offset: limit * (page - 1), limit, ...data }) 114 | 115 | export const transBlockListApiPaginationParams = ({ page = 1, limit, ...data }: ApiPaginationParams) => { 116 | return { page, limit, ...data } 117 | } 118 | 119 | export const getPaginationConfig: any = ({ current, pageSize, total, totalLabel = 'transactions', setCurrent, setPageSize }: any) => ({ 120 | size: 'small', 121 | position: ['topRight', 'bottomRight'], 122 | current, 123 | pageSize, 124 | total, 125 | showTotal: (total: number) => `A total of ${formatNum(total)} ${totalLabel} found`, 126 | onChange: (page: number, pageSize: number) => { 127 | setCurrent(page) 128 | setPageSize(pageSize) 129 | } 130 | }) 131 | 132 | export const getTxsPaginationConfig: any = ({ current, pageSize, total, totalLimit = 500000, totalLabel = 'transactions', setCurrent, setPageSize }: any) => ({ 133 | size: 'small', 134 | position: ['topRight', 'bottomRight'], 135 | current, 136 | pageSize, 137 | total: total < totalLimit ? total : totalLimit, 138 | showTotal: () => `A total of ${formatNum(total)} ${totalLabel} found`, 139 | onChange: (page: number, pageSize: number) => { 140 | setCurrent(page) 141 | setPageSize(pageSize) 142 | } 143 | }) 144 | 145 | export const stringifyQueryUrl = (url: string, query: any) => stringifyUrl({ url, query }) 146 | 147 | export const getCrossBrowserTxUrl = (tx: string) => `${CROSS_BROWSER_URL}/tx/${tx}` 148 | 149 | export const expandLeft0x = (param: string) => (param.slice(0, 2).toLowerCase() !== '0x' ? `0x${param}` : param) 150 | 151 | export const generateUid = (address: string): string => { 152 | const timestamp = Math.floor(Date.now() / 1000) 153 | .toString(16) 154 | .toLowerCase() 155 | 156 | return address + timestamp 157 | } 158 | 159 | export const isPositiveInteger = (str: string) => { 160 | const n = Math.floor(Number(str)) 161 | return n !== Infinity && String(n) === str && n >= 0 162 | } 163 | 164 | export const shortAddress = (address: string | undefined, length = 6) => { 165 | if (!address) return '' 166 | 167 | return `${address.slice(0, length)}...${address.slice(-length)}` 168 | } 169 | -------------------------------------------------------------------------------- /utils/message.ts: -------------------------------------------------------------------------------- 1 | import { message } from 'antd' 2 | 3 | interface MessageInterface { 4 | success: (params: any) => void 5 | error: (params: any) => void 6 | warn: (params: any) => void 7 | } 8 | 9 | class Message implements MessageInterface { 10 | private invokeAntdMessage(method, params): void { 11 | message.destroy() 12 | message[method](params) 13 | } 14 | 15 | success(...params: any): void { 16 | this.invokeAntdMessage('success', params) 17 | } 18 | 19 | error(...params: any): void { 20 | this.invokeAntdMessage('error', params) 21 | } 22 | 23 | warn(...params: any): void { 24 | this.invokeAntdMessage('warn', params) 25 | } 26 | } 27 | 28 | export default new Message() 29 | -------------------------------------------------------------------------------- /utils/transformer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * If you need to add transformers for special data types like `Temporal.Instant` or `Temporal.Date`, `Decimal.js`, etc you can do so here. 3 | * Make sure to import this file rather than `superjson` directly. 4 | * @see https://github.com/blitz-js/superjson#recipes 5 | */ 6 | import superjson from 'superjson' 7 | import { SuperJSONValue } from 'superjson/dist/types' 8 | 9 | export const transformer = { 10 | input: superjson, 11 | output: { 12 | serialize: (object: SuperJSONValue) => { 13 | // convert bigint to number 14 | const json = JSON.stringify(object, (key, value) => { 15 | if (typeof value === 'bigint') { 16 | return Number(value) 17 | } 18 | return value 19 | }) 20 | return superjson.serialize(JSON.parse(json)) 21 | }, 22 | 23 | deserialize: superjson.deserialize 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /utils/trpc.ts: -------------------------------------------------------------------------------- 1 | import { httpLink, loggerLink } from '@trpc/client' 2 | import { createTRPCNext } from '@trpc/next' 3 | import { inferRouterInputs, inferRouterOutputs } from '@trpc/server' 4 | import { NextPageContext } from 'next' 5 | 6 | // ℹ️ Type-only import: 7 | // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export 8 | import type { AppRouter } from '../server/routers/_app' 9 | import { transformer } from './transformer' 10 | 11 | function getBaseUrl() { 12 | if (typeof window !== 'undefined') { 13 | return '' 14 | } 15 | // reference for vercel.com 16 | if (process.env.VERCEL_URL) { 17 | return `https://${process.env.VERCEL_URL}` 18 | } 19 | 20 | // reference for render.com 21 | if (process.env.RENDER_INTERNAL_HOSTNAME) { 22 | return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}` 23 | } 24 | 25 | // assume localhost 26 | return `http:/localhost:${process.env.PORT ?? 3000}` 27 | } 28 | 29 | /** 30 | * Extend `NextPageContext` with meta data that can be picked up by `responseMeta()` when server-side rendering 31 | */ 32 | export interface SSRContext extends NextPageContext { 33 | /** 34 | * Set HTTP Status code 35 | * @example 36 | * const utils = trpc.useContext(); 37 | * if (utils.ssrContext) { 38 | * utils.ssrContext.status = 404; 39 | * } 40 | */ 41 | status?: number 42 | } 43 | 44 | /** 45 | * A set of strongly-typed React hooks from your `AppRouter` type signature with `createReactQueryHooks`. 46 | * @link https://trpc.io/docs/react#3-create-trpc-hooks 47 | */ 48 | export const trpc = createTRPCNext({ 49 | config({ ctx }) { 50 | /** 51 | * If you want to use SSR, you need to use the server's full URL 52 | * @link https://trpc.io/docs/ssr 53 | */ 54 | return { 55 | /** 56 | * @link https://trpc.io/docs/data-transformers 57 | */ 58 | transformer, 59 | /** 60 | * @link https://trpc.io/docs/links 61 | */ 62 | links: [ 63 | // adds pretty logs to your console in development and logs errors in production 64 | loggerLink({ 65 | enabled: opts => process.env.NODE_ENV === 'development' || (opts.direction === 'down' && opts.result instanceof Error) 66 | }), 67 | 68 | // httpBatchLink({ 69 | httpLink({ 70 | url: `${getBaseUrl()}/api/trpc`, 71 | /** 72 | * Set custom request headers on every request from tRPC 73 | * @link https://trpc.io/docs/ssr 74 | */ 75 | headers() { 76 | if (!ctx?.req?.headers) { 77 | return {} 78 | } 79 | // To use SSR properly, you need to forward the client's headers to the server 80 | // This is so you can pass through things like cookies when we're server-side rendering 81 | 82 | const { 83 | // If you're using Node 18 before 18.15.0, omit the "connection" header 84 | connection: _connection, 85 | ...headers 86 | } = ctx.req.headers 87 | return headers 88 | } 89 | }) 90 | ], 91 | /** 92 | * @link https://tanstack.com/query/v4/docs/react/reference/QueryClient 93 | */ 94 | // queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } }, 95 | // Change options globally 96 | queryClientConfig: { 97 | defaultOptions: { 98 | queries: { 99 | refetchOnMount: false, 100 | refetchOnWindowFocus: false 101 | } 102 | } 103 | } 104 | } 105 | }, 106 | /** 107 | * @link https://trpc.io/docs/ssr 108 | */ 109 | ssr: false 110 | }) 111 | 112 | export type RouterInput = inferRouterInputs 113 | export type RouterOutput = inferRouterOutputs 114 | -------------------------------------------------------------------------------- /windi.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'windicss/helpers' 2 | 3 | export default defineConfig({ 4 | preflight: false, 5 | attributify: true, 6 | extract: { 7 | include: ['**/*.{jsx,tsx,css}'], 8 | exclude: ['node_modules', '.git', '.next'] 9 | }, 10 | theme: { 11 | extend: { 12 | colors: { 13 | page: '#f1f2f2', 14 | main: '#cb8158', 15 | lightMain: '#f3ccb6', 16 | darkMain: '#A7570F', 17 | red: '#ff4d4f', 18 | lightRed: '#fff1f0', 19 | green: '#00c29e', 20 | lightGreen: '#e6f9f5', 21 | orange: '#f9761a', 22 | lightOrange: '#f9761a1a', 23 | secondText: '#4C506B', 24 | border: '#e7e7e7' 25 | }, 26 | spacing: { 27 | fit: 'fit-content', 28 | 4: '4px', 29 | 8: '8px', 30 | 12: '12px', 31 | 16: '16px', 32 | 24: '24px', 33 | 32: '32px' 34 | }, 35 | borderRadius: { 36 | 4: '4px', 37 | 8: '8px', 38 | 12: '12px' 39 | }, 40 | lineHeight: { 41 | 24: '24px', 42 | 32: '32px' 43 | } 44 | }, 45 | fontSize: { 46 | 12: '12px', 47 | 14: '14px', 48 | 16: '16px', 49 | 24: '24px' 50 | } 51 | }, 52 | shortcuts: { 53 | flexCenter: 'flex justify-center items-center', 54 | ellipsis: 'w-full whitespace-nowrap overflow-hidden overflow-ellipsis' 55 | } 56 | }) 57 | -------------------------------------------------------------------------------- /worker/index.ts: -------------------------------------------------------------------------------- 1 | import { Queue, Worker } from 'bullmq' 2 | 3 | import { VerifyStatus } from '@/constants/api' 4 | import { getByteCode, insertVerifyStatus, updateContract, updateVerifyStatus } from '@/server/prisma' 5 | import { VerifyMultiPartParams, VerifyStandardJsonInputParams, verifyMultiPart, verifyStandardJsonInput } from '@/server/verify' 6 | 7 | export interface VerifyJob { 8 | type: string 9 | params: SolidityStandardJsonVerifyParams | SolidityMultiPartVerifyParams 10 | sourceCode: string 11 | uid: string 12 | } 13 | 14 | export type SolidityStandardJsonVerifyParams = { 15 | contractaddress: string 16 | contractname: string 17 | compilerversion: string 18 | constructorArguements: unknown[] // You may replace this with a more specific type 19 | } 20 | 21 | export type SolidityMultiPartVerifyParams = { 22 | contractAddress: string 23 | compilerVersion: string 24 | evmVersion?: string 25 | optimizationRuns?: number 26 | sourceFiles: Record 27 | libraries?: Record 28 | } 29 | 30 | export const connection = { 31 | host: process.env.REDIS_HOST ?? 'localhost', 32 | port: parseInt(process.env.REDIS_PORT ?? '6379') 33 | } as const 34 | 35 | // queue 36 | export const queueName = 'VerifyContract' 37 | export const queue = new Queue(queueName, { connection }) 38 | export enum VerifyJobType { 39 | SolidityVerifyStandardJson = 'SolidityVerifyStandardJson', 40 | SolidityVerifyMultiPart = 'SolidityVerifyMultiPart' 41 | } 42 | 43 | const updateContractInfo = async (res: Response, job: any, contractAddress: string): Promise => { 44 | try { 45 | const resData = await (res.headers.get('content-type')?.includes('application/json') ? res.json() : res.text()) 46 | if (res.ok && resData.status === 'SUCCESS') { 47 | await Promise.all([updateContract(contractAddress, resData.source, job.data.sourceCode), updateVerifyStatus(job.data.uid, VerifyStatus.Pass)]) 48 | } else { 49 | console.error(`❌ Worker ${worker.name} job w${job?.id} - ${job?.name} failed(res): ${JSON.stringify(resData)}`) 50 | await updateVerifyStatus(job.data.uid, VerifyStatus.Fail) 51 | } 52 | } catch (err) { 53 | console.error(`❌ Worker ${worker.name} job ${job?.id} - ${job?.name} failed: ${err}`) 54 | await updateVerifyStatus(job.data.uid, VerifyStatus.Fail) 55 | } 56 | } 57 | 58 | // worker 59 | export const worker = new Worker( 60 | queueName, 61 | async job => { 62 | switch (job.name) { 63 | case VerifyJobType.SolidityVerifyStandardJson: { 64 | // insert status 65 | const verifyParams = job.data.params as SolidityStandardJsonVerifyParams 66 | await insertVerifyStatus(job.data.uid, VerifyStatus.Pending, verifyParams.contractaddress) 67 | 68 | const bytecode = await getByteCode(verifyParams.contractaddress) 69 | const params: VerifyStandardJsonInputParams = { 70 | bytecode, 71 | bytecodeType: 'CREATION_INPUT', // TODO: support DEPLOYED_BYTECODE 72 | compilerVersion: verifyParams.compilerversion, 73 | input: job.data.sourceCode 74 | } 75 | const res = await verifyStandardJsonInput(params) 76 | await updateContractInfo(res, job, verifyParams.contractaddress) 77 | break 78 | } 79 | case VerifyJobType.SolidityVerifyMultiPart: { 80 | // insert status 81 | const { contractAddress, ...verifyParams } = job.data.params as SolidityMultiPartVerifyParams 82 | await insertVerifyStatus(job.data.uid, VerifyStatus.Pending, contractAddress) 83 | 84 | const bytecode = await getByteCode(contractAddress) 85 | const params: VerifyMultiPartParams = { 86 | bytecode, 87 | bytecodeType: 'CREATION_INPUT', // TODO: support DEPLOYED_BYTECODE 88 | ...verifyParams 89 | } 90 | const res = await verifyMultiPart(params) 91 | await updateContractInfo(res, job, contractAddress) 92 | break 93 | } 94 | default: 95 | console.log(worker.name, 'Got job with unknown name', job.name) 96 | break 97 | } 98 | }, 99 | { connection } 100 | ) 101 | 102 | worker.on('completed', job => { 103 | console.info('🎉 Worker', worker.name, 'job', job.id, '-', job.name, 'completed') 104 | }) 105 | worker.on('failed', (job, err) => { 106 | console.error('❌ Worker', worker.name, 'job', job?.id, '-', job?.name, 'failed', err) 107 | }) 108 | worker.on('error', err => { 109 | console.error('🔥 Worker', worker.name, 'error', err) 110 | }) 111 | worker.on('stalled', jobId => { 112 | console.warn('🚨 Worker', worker.name, 'job', jobId, 'stalled') 113 | }) 114 | // worker.on('active', job => { 115 | // console.info('Worker', worker.name, 'job', job.id, 'active') 116 | // }) 117 | worker.on('paused', () => { 118 | console.warn('⏸️ Worker', worker.name, 'paused') 119 | }) 120 | // worker.on('drained', () => { 121 | // console.warn('Worker', worker.name, 'drained') 122 | // }) 123 | --------------------------------------------------------------------------------