├── .husky └── pre-commit ├── .prettierignore ├── public ├── favicon.ico ├── vercel.svg ├── thirteen.svg └── next.svg ├── src ├── theme │ ├── components.ts │ ├── colors.ts │ └── index.ts ├── encoding │ ├── index.ts │ ├── proposal.ts │ └── msg.ts ├── styles │ ├── globals.css │ └── Home.module.css ├── pages │ ├── _document.tsx │ ├── _app.tsx │ ├── parameters │ │ └── index.tsx │ ├── validators │ │ └── index.tsx │ ├── proposals │ │ └── index.tsx │ ├── index.tsx │ ├── blocks │ │ ├── [height].tsx │ │ └── index.tsx │ ├── txs │ │ └── [hash].tsx │ └── accounts │ │ └── [address].tsx ├── components │ ├── LoadingPage │ │ └── index.tsx │ ├── Parameters │ │ ├── DistributionParameters.tsx │ │ ├── StakingParameters.tsx │ │ ├── MintParameters.tsx │ │ ├── SlashingParameters.tsx │ │ └── GovParameters.tsx │ ├── Layout │ │ └── index.tsx │ ├── Connect │ │ └── index.tsx │ ├── Datatable │ │ └── index.tsx │ ├── Sidebar │ │ └── index.tsx │ └── Navbar │ │ └── index.tsx ├── utils │ ├── constant.ts │ └── helper.ts ├── rpc │ ├── subscribe │ │ └── index.ts │ ├── client │ │ └── index.ts │ ├── query │ │ └── index.ts │ └── abci │ │ └── index.ts └── store │ ├── index.ts │ ├── connectSlice.ts │ ├── streamSlice.ts │ └── paramsSlice.ts ├── .prettierrc.json ├── next.config.js ├── .eslintrc.json ├── .gitignore ├── tsconfig.json ├── package.json ├── README.md └── LICENSE /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | public 4 | next-env.d.ts 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olimdzhon/dexplorer/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/theme/components.ts: -------------------------------------------------------------------------------- 1 | export const components = { 2 | Heading: { 3 | baseStyle: {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /src/encoding/index.ts: -------------------------------------------------------------------------------- 1 | export type { DecodeMsg } from './msg' 2 | export { decodeMsg } from './msg' 3 | export type { DecodeContentProposal } from './proposal' 4 | export { decodeContentProposal } from './proposal' 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "plugin:prettier/recommended"], 3 | "rules": { 4 | "prettier/prettier": 1, 5 | "react-hooks/exhaustive-deps": 0, 6 | "react-hooks/rules-of-hooks": 0 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | max-width: 100vw; 4 | overflow-x: hidden; 5 | } 6 | 7 | a { 8 | color: inherit; 9 | text-decoration: none; 10 | } 11 | 12 | @media (prefers-color-scheme: dark) { 13 | html { 14 | color-scheme: dark; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/theme/colors.ts: -------------------------------------------------------------------------------- 1 | import { Colors } from '@chakra-ui/react' 2 | 3 | export const colors: Colors = { 4 | 'light-container': '#FFFFFF', 5 | 'light-bg': '#F3F4F6', 6 | 'light-theme': '#00B5D8', //cyan.500 7 | 'dark-container': '#2A334C', 8 | 'dark-bg': '#171D30', 9 | 'dark-theme': '#0BC5EA', //cyan.400 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { ColorModeScript } from '@chakra-ui/react' 2 | import { Html, Head, Main, NextScript } from 'next/document' 3 | import theme from '../theme' 4 | 5 | export default function Document() { 6 | return ( 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | import { ChakraProvider } from '@chakra-ui/react' 4 | import Layout from '@/components/Layout' 5 | import theme from '@/theme' 6 | import { wrapper } from '@/store' 7 | 8 | function App({ Component, pageProps }: AppProps) { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | ) 16 | } 17 | 18 | export default wrapper.withRedux(App) 19 | -------------------------------------------------------------------------------- /.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*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /src/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { Theme, extendTheme } from '@chakra-ui/react' 2 | import { colors } from './colors' 3 | import { components } from './components' 4 | 5 | const theme: Theme = extendTheme({ 6 | config: { 7 | initialColorMode: 'light', 8 | useSystemColorMode: false, 9 | } as Theme['config'], 10 | fonts: { 11 | heading: 'Inter, sans-serif', 12 | body: 'Inter, sans-serif', 13 | mono: 'IBM Plex Mono, monospace', 14 | }, 15 | styles: {}, 16 | colors, 17 | components, 18 | }) as Theme 19 | 20 | export default theme 21 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | "paths": { 18 | "@/*": ["./src/*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /src/encoding/proposal.ts: -------------------------------------------------------------------------------- 1 | import { TextProposal } from 'cosmjs-types/cosmos/gov/v1beta1/gov' 2 | 3 | const TYPE = { 4 | TextProposal: '/cosmos.gov.v1beta1.TextProposal', 5 | } 6 | 7 | export interface DecodeContentProposal { 8 | typeUrl: string 9 | data: TextProposal | null 10 | } 11 | 12 | export const decodeContentProposal = ( 13 | typeUrl: string, 14 | value: Uint8Array 15 | ): DecodeContentProposal => { 16 | let data = null 17 | switch (typeUrl) { 18 | case TYPE.TextProposal: 19 | data = TextProposal.decode(value) 20 | break 21 | default: 22 | data = TextProposal.decode(value) 23 | break 24 | } 25 | 26 | return { 27 | typeUrl, 28 | data, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/LoadingPage/index.tsx: -------------------------------------------------------------------------------- 1 | import { useColorModeValue, Flex, Spinner } from '@chakra-ui/react' 2 | import Head from 'next/head' 3 | 4 | export default function LoadingPage() { 5 | return ( 6 | <> 7 | 8 | Dexplorer 9 | 10 | 11 | 12 | 13 | 20 | 21 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/constant.ts: -------------------------------------------------------------------------------- 1 | export const LS_RPC_ADDRESS = 'RPC_ADDRESS' 2 | export const LS_RPC_ADDRESS_LIST = 'RPC_ADDRESS_LIST' 3 | export const GOV_PARAMS_TYPE = { 4 | VOTING: 'voting', 5 | DEPOSIT: 'deposit', 6 | TALLY: 'tallying', 7 | } 8 | 9 | export type proposalStatus = { 10 | id: number 11 | status: string 12 | color: string 13 | } 14 | export const proposalStatusList: proposalStatus[] = [ 15 | { 16 | id: 0, 17 | status: 'UNSPECIFIED', 18 | color: 'gray', 19 | }, 20 | { 21 | id: 1, 22 | status: 'DEPOSIT PERIOD', 23 | color: 'blue', 24 | }, 25 | { 26 | id: 2, 27 | status: 'VOTING PERIOD', 28 | color: 'blue', 29 | }, 30 | { 31 | id: 3, 32 | status: 'PASSED', 33 | color: 'green', 34 | }, 35 | { 36 | id: 4, 37 | status: 'REJECTED', 38 | color: 'red', 39 | }, 40 | { 41 | id: 5, 42 | status: 'FAILED', 43 | color: 'red', 44 | }, 45 | ] 46 | -------------------------------------------------------------------------------- /src/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 6rem; 7 | min-height: 100vh; 8 | } 9 | 10 | .description { 11 | display: inherit; 12 | justify-content: inherit; 13 | align-items: inherit; 14 | font-size: 0.85rem; 15 | max-width: var(--max-width); 16 | width: 100%; 17 | z-index: 2; 18 | font-family: var(--font-mono); 19 | } 20 | 21 | .description a { 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | gap: 0.5rem; 26 | } 27 | 28 | .description p { 29 | position: relative; 30 | margin: 0; 31 | padding: 1rem; 32 | background-color: rgba(var(--callout-rgb), 0.5); 33 | border: 1px solid rgba(var(--callout-border-rgb), 0.3); 34 | border-radius: var(--border-radius); 35 | } 36 | 37 | .code { 38 | font-weight: 700; 39 | font-family: var(--font-mono); 40 | } 41 | 42 | @media (prefers-color-scheme: dark) { 43 | .vercelLogo { 44 | filter: invert(1); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/rpc/subscribe/index.ts: -------------------------------------------------------------------------------- 1 | import { NewBlockEvent, Tendermint37Client } from '@cosmjs/tendermint-rpc' 2 | import { TxEvent } from '@cosmjs/tendermint-rpc/build/tendermint37' 3 | import { Subscription } from 'xstream' 4 | 5 | export function subscribeNewBlock( 6 | tmClient: Tendermint37Client, 7 | callback: (event: NewBlockEvent) => any 8 | ): Subscription { 9 | const stream = tmClient.subscribeNewBlock() 10 | const subscription = stream.subscribe({ 11 | next: (event) => { 12 | callback(event) 13 | }, 14 | error: (err) => { 15 | console.error(err) 16 | subscription.unsubscribe() 17 | }, 18 | }) 19 | 20 | return subscription 21 | } 22 | 23 | export function subscribeTx( 24 | tmClient: Tendermint37Client, 25 | callback: (event: TxEvent) => any 26 | ): Subscription { 27 | const stream = tmClient.subscribeTx() 28 | const subscription = stream.subscribe({ 29 | next: (event) => { 30 | callback(event) 31 | }, 32 | error: (err) => { 33 | console.error(err) 34 | subscription.unsubscribe() 35 | }, 36 | }) 37 | 38 | return subscription 39 | } 40 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/rpc/client/index.ts: -------------------------------------------------------------------------------- 1 | import { replaceHTTPtoWebsocket } from '@/utils/helper' 2 | import { Tendermint37Client, WebsocketClient } from '@cosmjs/tendermint-rpc' 3 | import { StreamingSocket } from '@cosmjs/socket' 4 | 5 | export async function validateConnection(rpcAddress: string): Promise { 6 | return new Promise((resolve) => { 7 | const wsUrl = replaceHTTPtoWebsocket(rpcAddress) 8 | const path = wsUrl.endsWith('/') ? 'websocket' : '/websocket' 9 | const socket = new StreamingSocket(wsUrl + path, 3000) 10 | socket.events.subscribe({ 11 | error: () => { 12 | resolve(false) 13 | }, 14 | }) 15 | 16 | socket.connect() 17 | socket.connected.then(() => resolve(true)).catch(() => resolve(false)) 18 | }) 19 | } 20 | 21 | export async function connectWebsocketClient( 22 | rpcAddress: string 23 | ): Promise { 24 | return new Promise(async (resolve, reject) => { 25 | try { 26 | const wsUrl = replaceHTTPtoWebsocket(rpcAddress) 27 | const wsClient = new WebsocketClient(wsUrl, (err) => { 28 | reject(err) 29 | }) 30 | const tmClient = await Tendermint37Client.create(wsClient) 31 | if (!tmClient) { 32 | reject(new Error('cannot create tendermint client')) 33 | } 34 | 35 | const status = await tmClient.status() 36 | if (!status) { 37 | reject(new Error('cannot get client status')) 38 | } 39 | 40 | resolve(tmClient) 41 | } catch (err) { 42 | reject(err) 43 | } 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit' 2 | import { connectSlice } from './connectSlice' 3 | import { streamSlice } from './streamSlice' 4 | import { paramsSlice } from './paramsSlice' 5 | import { createWrapper } from 'next-redux-wrapper' 6 | 7 | const makeStore = () => 8 | configureStore({ 9 | reducer: { 10 | [connectSlice.name]: connectSlice.reducer, 11 | [streamSlice.name]: streamSlice.reducer, 12 | [paramsSlice.name]: paramsSlice.reducer, 13 | }, 14 | middleware: (getDefaultMiddleware) => 15 | getDefaultMiddleware({ 16 | serializableCheck: { 17 | // Ignore these action types 18 | ignoredActions: [ 19 | 'stream/setNewBlock', 20 | 'stream/setSubsNewBlock', 21 | 'stream/setTxEvent', 22 | 'stream/setSubsTxEvent', 23 | 'connect/setTmClient', 24 | ], 25 | // Ignore these paths in the state 26 | ignoredPaths: [ 27 | 'connect.tmClient', 28 | 'stream.subsNewBlock', 29 | 'stream.subsTxEvent', 30 | 'stream.newBlock', 31 | 'stream.txEvent', 32 | ], 33 | }, 34 | }), 35 | devTools: true, 36 | }) 37 | 38 | export type AppStore = ReturnType 39 | export type AppState = ReturnType 40 | export type AppThunk = ThunkAction< 41 | ReturnType, 42 | AppState, 43 | unknown, 44 | Action 45 | > 46 | 47 | export const wrapper = createWrapper(makeStore) 48 | -------------------------------------------------------------------------------- /src/store/connectSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit' 2 | import { AppState } from './index' 3 | import { HYDRATE } from 'next-redux-wrapper' 4 | import { Tendermint37Client } from '@cosmjs/tendermint-rpc' 5 | 6 | // Type for our state 7 | export interface ConnectState { 8 | rpcAddress: string 9 | connectState: boolean 10 | tmClient: Tendermint37Client | null 11 | } 12 | 13 | // Initial state 14 | const initialState: ConnectState = { 15 | rpcAddress: '', 16 | connectState: false, 17 | tmClient: null, 18 | } 19 | 20 | // Actual Slice 21 | export const connectSlice = createSlice({ 22 | name: 'connect', 23 | initialState, 24 | reducers: { 25 | // Action to set the address 26 | setRPCAddress(state, action) { 27 | state.rpcAddress = action.payload 28 | }, 29 | // Action to set the connection status 30 | setConnectState(state, action) { 31 | state.connectState = action.payload 32 | }, 33 | // Action to set the client 34 | setTmClient(state, action) { 35 | state.tmClient = action.payload 36 | }, 37 | }, 38 | 39 | // Special reducer for hydrating the state. Special case for next-redux-wrapper 40 | extraReducers: { 41 | [HYDRATE]: (state, action) => { 42 | return { 43 | ...state, 44 | ...action.payload.connect, 45 | } 46 | }, 47 | }, 48 | }) 49 | 50 | export const { setRPCAddress, setConnectState, setTmClient } = 51 | connectSlice.actions 52 | 53 | export const selectRPCAddress = (state: AppState) => state.connect.rpcAddress 54 | export const selectConnectState = (state: AppState) => 55 | state.connect.connectState 56 | export const selectTmClient = (state: AppState) => state.connect.tmClient 57 | 58 | export default connectSlice.reducer 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dexplorer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "prepare": "husky install", 10 | "lint": "next lint", 11 | "format": "prettier --check .", 12 | "format:fix": "prettier --write ." 13 | }, 14 | "dependencies": { 15 | "@chakra-ui/icons": "^2.0.18", 16 | "@chakra-ui/react": "^2.5.5", 17 | "@cosmjs/crypto": "^0.30.1", 18 | "@cosmjs/encoding": "^0.30.1", 19 | "@cosmjs/socket": "^0.30.1", 20 | "@cosmjs/stargate": "^0.30.1", 21 | "@cosmjs/tendermint-rpc": "^0.30.1", 22 | "@emotion/react": "^11.10.6", 23 | "@emotion/styled": "^11.10.6", 24 | "@reduxjs/toolkit": "^1.9.3", 25 | "@tanstack/react-table": "^8.8.5", 26 | "@types/node": "18.15.11", 27 | "@types/react": "18.0.31", 28 | "@types/react-dom": "18.0.11", 29 | "bech32": "^2.0.0", 30 | "cosmjs-types": "^0.9.0", 31 | "dayjs": "^1.11.7", 32 | "eslint": "8.37.0", 33 | "eslint-config-next": "13.2.4", 34 | "framer-motion": "^10.10.0", 35 | "next": "13.2.4", 36 | "next-redux-wrapper": "^8.1.0", 37 | "react": "18.2.0", 38 | "react-dom": "18.2.0", 39 | "react-icons": "^4.8.0", 40 | "react-redux": "^8.0.5", 41 | "typescript": "5.0.3", 42 | "xstream": "^11.14.0" 43 | }, 44 | "devDependencies": { 45 | "eslint-config-prettier": "^8.8.0", 46 | "eslint-plugin-prettier": "^4.2.1", 47 | "husky": "^8.0.3", 48 | "lint-staged": "^13.2.2", 49 | "prettier": "^2.8.7" 50 | }, 51 | "repository": { 52 | "type": "git", 53 | "url": "https://github.com/arifintahu/dexplorer.git" 54 | }, 55 | "lint-staged": { 56 | "*.+(ts|tsx)": "eslint --fix", 57 | "*.+(ts|tsx|json|css|md)": "prettier --write ." 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/pages/parameters/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Divider, 3 | HStack, 4 | Heading, 5 | Icon, 6 | Link, 7 | Text, 8 | useColorModeValue, 9 | } from '@chakra-ui/react' 10 | import Head from 'next/head' 11 | import NextLink from 'next/link' 12 | import { FiChevronRight, FiHome } from 'react-icons/fi' 13 | import MintParameters from '@/components/Parameters/MintParameters' 14 | import StakingParameters from '@/components/Parameters/StakingParameters' 15 | import DistributionParameters from '@/components/Parameters/DistributionParameters' 16 | import SlashingParameters from '@/components/Parameters/SlashingParameters' 17 | import GovParameters from '@/components/Parameters/GovParameters' 18 | 19 | export default function Parameters() { 20 | return ( 21 | <> 22 | 23 | Parameters | Dexplorer 24 | 25 | 26 | 27 | 28 |
29 | 30 | Parameters 31 | 32 | 40 | 45 | 46 | 47 | Parameters 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 | 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /src/store/streamSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit' 2 | import { AppState } from './index' 3 | import { HYDRATE } from 'next-redux-wrapper' 4 | import { NewBlockEvent, TxEvent } from '@cosmjs/tendermint-rpc' 5 | import { Subscription } from 'xstream' 6 | 7 | // Type for our state 8 | export interface StreamState { 9 | newBlock: NewBlockEvent | null 10 | txEvent: TxEvent | null 11 | subsNewBlock: Subscription | null 12 | subsTxEvent: Subscription | null 13 | } 14 | 15 | // Initial state 16 | const initialState: StreamState = { 17 | newBlock: null, 18 | txEvent: null, 19 | subsNewBlock: null, 20 | subsTxEvent: null, 21 | } 22 | 23 | // Actual Slice 24 | export const streamSlice = createSlice({ 25 | name: 'stream', 26 | initialState, 27 | reducers: { 28 | // Action to set the new block 29 | setNewBlock(state, action) { 30 | state.newBlock = action.payload 31 | }, 32 | 33 | // Action to set the tx event 34 | setTxEvent(state, action) { 35 | state.txEvent = action.payload 36 | }, 37 | 38 | // Action to set the subs state new block 39 | setSubsNewBlock(state, action) { 40 | state.subsNewBlock = action.payload 41 | }, 42 | 43 | // Action to set the subs state tx event 44 | setSubsTxEvent(state, action) { 45 | state.subsTxEvent = action.payload 46 | }, 47 | }, 48 | 49 | // Special reducer for hydrating the state. Special case for next-redux-wrapper 50 | extraReducers: { 51 | [HYDRATE]: (state, action) => { 52 | return { 53 | ...state, 54 | ...action.payload.stream, 55 | } 56 | }, 57 | }, 58 | }) 59 | 60 | export const { setNewBlock, setTxEvent, setSubsNewBlock, setSubsTxEvent } = 61 | streamSlice.actions 62 | 63 | export const selectNewBlock = (state: AppState) => state.stream.newBlock 64 | export const selectTxEvent = (state: AppState) => state.stream.txEvent 65 | 66 | export const selectSubsNewBlock = (state: AppState) => state.stream.subsNewBlock 67 | export const selectSubsTxEvent = (state: AppState) => state.stream.subsTxEvent 68 | 69 | export default streamSlice.reducer 70 | -------------------------------------------------------------------------------- /src/rpc/query/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Account, 3 | Block, 4 | Coin, 5 | IndexedTx, 6 | StargateClient, 7 | } from '@cosmjs/stargate' 8 | import { 9 | Tendermint37Client, 10 | TxSearchResponse, 11 | ValidatorsResponse, 12 | } from '@cosmjs/tendermint-rpc' 13 | 14 | export async function getChainId( 15 | tmClient: Tendermint37Client 16 | ): Promise { 17 | const client = await StargateClient.create(tmClient) 18 | return client.getChainId() 19 | } 20 | 21 | export async function getValidators( 22 | tmClient: Tendermint37Client 23 | ): Promise { 24 | return tmClient.validatorsAll() 25 | } 26 | 27 | export async function getBlock( 28 | tmClient: Tendermint37Client, 29 | height: number 30 | ): Promise { 31 | const client = await StargateClient.create(tmClient) 32 | return client.getBlock(height) 33 | } 34 | 35 | export async function getTx( 36 | tmClient: Tendermint37Client, 37 | hash: string 38 | ): Promise { 39 | const client = await StargateClient.create(tmClient) 40 | return client.getTx(hash) 41 | } 42 | 43 | export async function getAccount( 44 | tmClient: Tendermint37Client, 45 | address: string 46 | ): Promise { 47 | const client = await StargateClient.create(tmClient) 48 | return client.getAccount(address) 49 | } 50 | 51 | export async function getAllBalances( 52 | tmClient: Tendermint37Client, 53 | address: string 54 | ): Promise { 55 | const client = await StargateClient.create(tmClient) 56 | return client.getAllBalances(address) 57 | } 58 | 59 | export async function getBalanceStaked( 60 | tmClient: Tendermint37Client, 61 | address: string 62 | ): Promise { 63 | const client = await StargateClient.create(tmClient) 64 | return client.getBalanceStaked(address) 65 | } 66 | 67 | export async function getTxsBySender( 68 | tmClient: Tendermint37Client, 69 | address: string, 70 | page: number, 71 | perPage: number 72 | ): Promise { 73 | return tmClient.txSearch({ 74 | query: `message.sender='${address}'`, 75 | prove: true, 76 | order_by: 'desc', 77 | page: page, 78 | per_page: perPage, 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /src/encoding/msg.ts: -------------------------------------------------------------------------------- 1 | import { MsgSend } from 'cosmjs-types/cosmos/bank/v1beta1/tx' 2 | import { MsgWithdrawDelegatorReward } from 'cosmjs-types/cosmos/distribution/v1beta1/tx' 3 | import { MsgDelegate } from 'cosmjs-types/cosmos/staking/v1beta1/tx' 4 | import { MsgUpdateClient } from 'cosmjs-types/ibc/core/client/v1/tx' 5 | import { MsgAcknowledgement } from 'cosmjs-types/ibc/core/channel/v1/tx' 6 | import { 7 | MsgExec, 8 | MsgGrant, 9 | MsgRevoke, 10 | } from 'cosmjs-types/cosmos/authz/v1beta1/tx' 11 | import { MsgTransfer } from 'cosmjs-types/ibc/applications/transfer/v1/tx' 12 | 13 | const TYPE = { 14 | MsgSend: '/cosmos.bank.v1beta1.MsgSend', 15 | MsgWithdrawDelegatorReward: 16 | '/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward', 17 | MsgDelegate: '/cosmos.staking.v1beta1.MsgDelegate', 18 | MsgUpdateClient: '/ibc.core.client.v1.MsgUpdateClient', 19 | MsgAcknowledgement: '/ibc.core.channel.v1.MsgAcknowledgement', 20 | MsgExec: '/cosmos.authz.v1beta1.MsgExec', 21 | MsgGrant: '/cosmos.authz.v1beta1.MsgGrant', 22 | MsgRevoke: '/cosmos.authz.v1beta1.MsgRevoke', 23 | MsgTransfer: '/ibc.applications.transfer.v1.MsgTransfer', 24 | } 25 | 26 | export interface DecodeMsg { 27 | typeUrl: string 28 | data: Object | null 29 | } 30 | 31 | export const decodeMsg = (typeUrl: string, value: Uint8Array): DecodeMsg => { 32 | let data = null 33 | switch (typeUrl) { 34 | case TYPE.MsgSend: 35 | data = MsgSend.decode(value) 36 | break 37 | case TYPE.MsgWithdrawDelegatorReward: 38 | data = MsgWithdrawDelegatorReward.decode(value) 39 | break 40 | case TYPE.MsgDelegate: 41 | data = MsgDelegate.decode(value) 42 | break 43 | case TYPE.MsgUpdateClient: 44 | data = MsgUpdateClient.decode(value) 45 | break 46 | case TYPE.MsgAcknowledgement: 47 | data = MsgAcknowledgement.decode(value) 48 | break 49 | case TYPE.MsgExec: 50 | data = MsgExec.decode(value) 51 | break 52 | case TYPE.MsgGrant: 53 | data = MsgGrant.decode(value) 54 | break 55 | case TYPE.MsgRevoke: 56 | data = MsgRevoke.decode(value) 57 | break 58 | case TYPE.MsgTransfer: 59 | data = MsgTransfer.decode(value) 60 | break 61 | default: 62 | break 63 | } 64 | 65 | return { 66 | typeUrl, 67 | data, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Dexplorer

3 | 4 |

5 | Disposable Cosmos-based Blockchain Explorer 6 |
7 |
8 | Report Issues 9 | · 10 | Request Feature 11 |

12 | 13 | [![GitHub](https://img.shields.io/github/license/arifintahu/dexplorer.svg)](https://github.com/arifintahu/dexplorer/blob/main/LICENSE) 14 | [![Dexplorer Deploy](https://vercelbadge.vercel.app/api/arifintahu/dexplorer)](https://github.com/arifintahu/dexplorer/deployments/activity_log) 15 | [![Contributors](https://img.shields.io/github/contributors/arifintahu/dexplorer)](https://github.com/arifintahu/dexplorer/graphs/contributors) 16 | 17 |
18 | 19 | `Dexplorer` is a disposable light explorer for Cosmos-based blockchains. It is designed to connect to any Cosmos SDK chain using only WebSocket RPC. This can be useful when developing Cosmos-based chains and exploring blockchain data through a UI. 20 | 21 | ## Features 22 | 23 | - The ability to connect to any Cosmos-based RPC 24 | - A dashboard to easily monitor chain activity 25 | - The ability to subscribe to the latest blocks and transactions 26 | - A search function that allows you to quickly find blocks, transactions, and accounts 27 | - A list of active validators 28 | - A list of proposals 29 | - Blockchain parameters 30 | 31 | ## How is Dexplorer different from other explorers? 32 | 33 | `Dexplorer` is only a frontend app, meaning there is no cache or pre-processing. It pulls data from RPC as needed. 34 | 35 | ## Contributing 36 | 37 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated. 38 | 39 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star! Thanks again! 40 | 41 | 1. Fork the project 42 | 2. Create your feature branch ~ `git checkout -b feature/feature-name` 43 | 3. Commit your changes ~ `git commit -m 'Add some feature-name'` 44 | 4. Push to the branch ~ `git push origin feature/feature-name` 45 | 5. Open a Pull Request to original repo branch `main` 46 | 47 | ## Contributors 48 | 49 | [@arifintahu](https://github.com/arifintahu) 50 | -------------------------------------------------------------------------------- /src/store/paramsSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit' 2 | import { AppState } from './index' 3 | import { HYDRATE } from 'next-redux-wrapper' 4 | import { Params as StakingParams } from 'cosmjs-types/cosmos/staking/v1beta1/staking' 5 | import { Params as MintParams } from 'cosmjs-types/cosmos/mint/v1beta1/mint' 6 | import { Params as DistributionParams } from 'cosmjs-types/cosmos/distribution/v1beta1/distribution' 7 | import { Params as SlashingParams } from 'cosmjs-types/cosmos/slashing/v1beta1/slashing' 8 | import { 9 | VotingParams, 10 | DepositParams, 11 | TallyParams, 12 | } from 'cosmjs-types/cosmos/gov/v1/gov' 13 | 14 | // Type for our state 15 | export interface ParamsState { 16 | stakingParams: StakingParams | null 17 | mintParams: MintParams | null 18 | distributionParams: DistributionParams | null 19 | slashingParams: SlashingParams | null 20 | govVotingParams: VotingParams | null 21 | govDepositParams: DepositParams | null 22 | govTallyParams: TallyParams | null 23 | } 24 | 25 | // Initial state 26 | const initialState: ParamsState = { 27 | stakingParams: null, 28 | mintParams: null, 29 | distributionParams: null, 30 | slashingParams: null, 31 | govVotingParams: null, 32 | govDepositParams: null, 33 | govTallyParams: null, 34 | } 35 | 36 | // Actual Slice 37 | export const paramsSlice = createSlice({ 38 | name: 'params', 39 | initialState, 40 | reducers: { 41 | setStakingParams(state, action) { 42 | state.stakingParams = action.payload 43 | }, 44 | setMintParams(state, action) { 45 | state.mintParams = action.payload 46 | }, 47 | setDistributionParams(state, action) { 48 | state.distributionParams = action.payload 49 | }, 50 | setSlashingParams(state, action) { 51 | state.slashingParams = action.payload 52 | }, 53 | setGovVotingParams(state, action) { 54 | state.govVotingParams = action.payload 55 | }, 56 | setGovDepositParams(state, action) { 57 | state.govDepositParams = action.payload 58 | }, 59 | setGovTallyParams(state, action) { 60 | state.govTallyParams = action.payload 61 | }, 62 | }, 63 | 64 | // Special reducer for hydrating the state. Special case for next-redux-wrapper 65 | extraReducers: { 66 | [HYDRATE]: (state, action) => { 67 | return { 68 | ...state, 69 | ...action.payload.params, 70 | } 71 | }, 72 | }, 73 | }) 74 | 75 | export const { 76 | setStakingParams, 77 | setMintParams, 78 | setDistributionParams, 79 | setSlashingParams, 80 | setGovVotingParams, 81 | setGovDepositParams, 82 | setGovTallyParams, 83 | } = paramsSlice.actions 84 | 85 | export const selectStakingParams = (state: AppState) => 86 | state.params.stakingParams 87 | export const selectMintParams = (state: AppState) => state.params.mintParams 88 | export const selectDistributionParams = (state: AppState) => 89 | state.params.distributionParams 90 | export const selectSlashingParams = (state: AppState) => 91 | state.params.slashingParams 92 | export const selectGovVotingParams = (state: AppState) => 93 | state.params.govVotingParams 94 | export const selectGovDepositParams = (state: AppState) => 95 | state.params.govDepositParams 96 | export const selectGovTallyParams = (state: AppState) => 97 | state.params.govTallyParams 98 | 99 | export default paramsSlice.reducer 100 | -------------------------------------------------------------------------------- /src/components/Parameters/DistributionParameters.tsx: -------------------------------------------------------------------------------- 1 | import { InfoOutlineIcon } from '@chakra-ui/icons' 2 | import { 3 | Box, 4 | Flex, 5 | Heading, 6 | SimpleGrid, 7 | Skeleton, 8 | Text, 9 | Tooltip, 10 | useColorModeValue, 11 | } from '@chakra-ui/react' 12 | import { useState, useEffect } from 'react' 13 | import { useSelector, useDispatch } from 'react-redux' 14 | import { selectTmClient } from '@/store/connectSlice' 15 | import { 16 | selectDistributionParams, 17 | setDistributionParams, 18 | } from '@/store/paramsSlice' 19 | import { queryDistributionParams } from '@/rpc/abci' 20 | import { convertRateToPercent } from '@/utils/helper' 21 | 22 | export default function DistributionParameters() { 23 | const [isHidden, setIsHidden] = useState(false) 24 | const [isLoaded, setIsLoaded] = useState(false) 25 | const dispatch = useDispatch() 26 | const tmClient = useSelector(selectTmClient) 27 | const params = useSelector(selectDistributionParams) 28 | 29 | useEffect(() => { 30 | if (tmClient && !params && !isLoaded) { 31 | queryDistributionParams(tmClient) 32 | .then((response) => { 33 | if (response.params) { 34 | dispatch(setDistributionParams(response.params)) 35 | } 36 | setIsLoaded(true) 37 | }) 38 | .catch((err) => { 39 | console.error(err) 40 | setIsHidden(true) 41 | }) 42 | } 43 | 44 | if (params) { 45 | setIsLoaded(true) 46 | } 47 | }, [tmClient, params, isLoaded]) 48 | 49 | return ( 50 | 116 | ) 117 | } 118 | -------------------------------------------------------------------------------- /src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import relativeTime from 'dayjs/plugin/relativeTime' 3 | import duration from 'dayjs/plugin/duration' 4 | import { toHex } from '@cosmjs/encoding' 5 | import { bech32 } from 'bech32' 6 | import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin' 7 | 8 | export const timeFromNow = (date: string): string => { 9 | dayjs.extend(relativeTime) 10 | return dayjs(date).fromNow() 11 | } 12 | 13 | export const trimHash = (txHash: Uint8Array): string => { 14 | const hash = toHex(txHash).toUpperCase() 15 | const first = hash.slice(0, 5) 16 | const last = hash.slice(hash.length - 5, hash.length) 17 | return first + '...' + last 18 | } 19 | 20 | export const displayDate = (date: string): string => { 21 | return dayjs(date).format('YYYY-MM-DD HH:mm:ss') 22 | } 23 | 24 | export const displayDurationSeconds = (seconds: number | undefined): string => { 25 | if (!seconds) { 26 | return `` 27 | } 28 | dayjs.extend(duration) 29 | dayjs.extend(relativeTime) 30 | return dayjs.duration({ seconds: seconds }).humanize() 31 | } 32 | 33 | export const replaceHTTPtoWebsocket = (url: string): string => { 34 | return url.replace('http', 'ws') 35 | } 36 | 37 | export const isBech32Address = (address: string): Boolean => { 38 | try { 39 | const decoded = bech32.decode(address) 40 | if (decoded.prefix.includes('valoper')) { 41 | return false 42 | } 43 | 44 | if (decoded.words.length < 1) { 45 | return false 46 | } 47 | 48 | const encoded = bech32.encode(decoded.prefix, decoded.words) 49 | return encoded === address 50 | } catch (e) { 51 | return false 52 | } 53 | } 54 | 55 | export const convertVotingPower = (tokens: string): string => { 56 | return Math.round(Number(tokens) / 10 ** 6).toLocaleString(undefined) 57 | } 58 | 59 | export const convertRateToPercent = (rate: string | undefined): string => { 60 | if (!rate) { 61 | return `` 62 | } 63 | const commission = (Number(rate) / 10 ** 16).toLocaleString(undefined, { 64 | minimumFractionDigits: 2, 65 | maximumFractionDigits: 2, 66 | }) 67 | return `${commission}%` 68 | } 69 | 70 | export const displayCoin = (deposit: Coin) => { 71 | if (deposit.denom.startsWith('u')) { 72 | const amount = Math.round(Number(deposit.amount) / 10 ** 6) 73 | const symbol = deposit.denom.slice(1).toUpperCase() 74 | return `${amount.toLocaleString()} ${symbol}` 75 | } 76 | return `${Number(deposit.amount).toLocaleString()} ${deposit.denom}` 77 | } 78 | 79 | export const getTypeMsg = (typeUrl: string): string => { 80 | const arr = typeUrl.split('.') 81 | if (arr.length) { 82 | return arr[arr.length - 1].replace('Msg', '') 83 | } 84 | return '' 85 | } 86 | 87 | export const isValidUrl = (urlString: string): Boolean => { 88 | var urlPattern = new RegExp( 89 | '^(https?:\\/\\/)?' + // validate protocol 90 | '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // validate domain name 91 | '((\\d{1,3}\\.){3}\\d{1,3}))' + // validate OR ip (v4) address 92 | '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // validate port and path 93 | '(\\?[;&a-z\\d%_.~+=-]*)?' + // validate query string 94 | '(\\#[-a-z\\d_]*)?$', 95 | 'i' 96 | ) // validate fragment locator 97 | return !!urlPattern.test(urlString) 98 | } 99 | 100 | export const normalizeUrl = (urlString: string): string => { 101 | if (!urlString.startsWith('https://') && !urlString.startsWith('http://')) { 102 | return `https://${urlString}` 103 | } 104 | 105 | return urlString 106 | } 107 | 108 | export const getUrlFromPath = (pathString: string): string => { 109 | const regex = /(?:\?|&)rpc=([^&]+)/ 110 | const match = regex.exec(pathString) 111 | return match ? decodeURIComponent(match[1]) : '' 112 | } 113 | 114 | export function removeTrailingSlash(url: string): string { 115 | // Check if the URL ends with a trailing slash 116 | if (url.endsWith('/')) { 117 | // Remove the trailing slash 118 | return url.slice(0, -1) 119 | } 120 | // Return the URL as is if it doesn't end with a trailing slash 121 | return url 122 | } 123 | -------------------------------------------------------------------------------- /src/components/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect, useState } from 'react' 2 | import { useDispatch, useSelector } from 'react-redux' 3 | import Sidebar from '../Sidebar' 4 | import Connect from '../Connect' 5 | import LoadingPage from '../LoadingPage' 6 | import Navbar from '../Navbar' 7 | import { 8 | selectConnectState, 9 | selectTmClient, 10 | setConnectState, 11 | setTmClient, 12 | setRPCAddress, 13 | } from '@/store/connectSlice' 14 | import { subscribeNewBlock, subscribeTx } from '@/rpc/subscribe' 15 | import { 16 | setNewBlock, 17 | selectNewBlock, 18 | setTxEvent, 19 | selectTxEvent, 20 | setSubsNewBlock, 21 | setSubsTxEvent, 22 | } from '@/store/streamSlice' 23 | import { NewBlockEvent } from '@cosmjs/tendermint-rpc' 24 | import { TxEvent } from '@cosmjs/tendermint-rpc/build/tendermint37' 25 | import { LS_RPC_ADDRESS } from '@/utils/constant' 26 | import { validateConnection, connectWebsocketClient } from '@/rpc/client' 27 | import { NextRouter, useRouter } from 'next/router' 28 | import { getUrlFromPath, isValidUrl, normalizeUrl } from '@/utils/helper' 29 | 30 | export default function Layout({ children }: { children: ReactNode }) { 31 | const connectState = useSelector(selectConnectState) 32 | const tmClient = useSelector(selectTmClient) 33 | const newBlock = useSelector(selectNewBlock) 34 | const txEvent = useSelector(selectTxEvent) 35 | const dispatch = useDispatch() 36 | const router = useRouter() 37 | const [isLoading, setIsLoading] = useState(true) 38 | 39 | useEffect(() => { 40 | if (tmClient && !newBlock) { 41 | const subscription = subscribeNewBlock(tmClient, updateNewBlock) 42 | dispatch(setSubsNewBlock(subscription)) 43 | } 44 | 45 | if (tmClient && !txEvent) { 46 | const subscription = subscribeTx(tmClient, updateTxEvent) 47 | dispatch(setSubsTxEvent(subscription)) 48 | } 49 | }, [tmClient, newBlock, txEvent, dispatch]) 50 | 51 | useEffect(() => { 52 | if (isLoading) { 53 | const url = getQueryUrl(router) 54 | if (url.length) { 55 | const address = normalizeUrl(url) 56 | connect(address) 57 | return 58 | } 59 | 60 | const address = window.localStorage.getItem(LS_RPC_ADDRESS) 61 | if (!address) { 62 | setIsLoading(false) 63 | return 64 | } 65 | 66 | connect(address) 67 | } 68 | }, [isLoading]) 69 | 70 | const updateNewBlock = (event: NewBlockEvent): void => { 71 | dispatch(setNewBlock(event)) 72 | } 73 | 74 | const updateTxEvent = (event: TxEvent): void => { 75 | dispatch(setTxEvent(event)) 76 | } 77 | 78 | const getQueryUrl = (router: NextRouter): string => { 79 | if (router.route !== '/') { 80 | return '' 81 | } 82 | 83 | const url = getUrlFromPath(router.asPath) 84 | if (!isValidUrl(url)) { 85 | return '' 86 | } 87 | return url 88 | } 89 | 90 | const connect = async (address: string) => { 91 | try { 92 | const isValid = await validateConnection(address) 93 | if (!isValid) { 94 | window.localStorage.removeItem(LS_RPC_ADDRESS) 95 | setIsLoading(false) 96 | return 97 | } 98 | 99 | const tmClient = await connectWebsocketClient(address) 100 | if (!tmClient) { 101 | window.localStorage.removeItem(LS_RPC_ADDRESS) 102 | setIsLoading(false) 103 | return 104 | } 105 | 106 | dispatch(setConnectState(true)) 107 | dispatch(setTmClient(tmClient)) 108 | dispatch(setRPCAddress(address)) 109 | 110 | setIsLoading(false) 111 | window.localStorage.setItem(LS_RPC_ADDRESS, address) 112 | } catch (err) { 113 | console.error(err) 114 | window.localStorage.removeItem(LS_RPC_ADDRESS) 115 | setIsLoading(false) 116 | return 117 | } 118 | } 119 | 120 | return ( 121 | <> 122 | {isLoading ? : <>} 123 | {connectState && !isLoading ? ( 124 | 125 | 126 | {children} 127 | 128 | ) : ( 129 | <> 130 | )} 131 | {!connectState && !isLoading ? : <>} 132 | 133 | ) 134 | } 135 | -------------------------------------------------------------------------------- /src/components/Parameters/StakingParameters.tsx: -------------------------------------------------------------------------------- 1 | import { InfoOutlineIcon } from '@chakra-ui/icons' 2 | import { 3 | Box, 4 | Flex, 5 | Heading, 6 | SimpleGrid, 7 | Skeleton, 8 | Text, 9 | Tooltip, 10 | useColorModeValue, 11 | } from '@chakra-ui/react' 12 | import { useState, useEffect } from 'react' 13 | import { useSelector, useDispatch } from 'react-redux' 14 | import { selectTmClient } from '@/store/connectSlice' 15 | import { selectStakingParams, setStakingParams } from '@/store/paramsSlice' 16 | import { queryStakingParams } from '@/rpc/abci' 17 | import { displayDurationSeconds } from '@/utils/helper' 18 | 19 | export default function StakingParameters() { 20 | const [isHidden, setIsHidden] = useState(false) 21 | const [isLoaded, setIsLoaded] = useState(false) 22 | const dispatch = useDispatch() 23 | const tmClient = useSelector(selectTmClient) 24 | const params = useSelector(selectStakingParams) 25 | 26 | useEffect(() => { 27 | if (tmClient && !params && !isLoaded) { 28 | queryStakingParams(tmClient) 29 | .then((response) => { 30 | if (response.params) { 31 | dispatch(setStakingParams(response.params)) 32 | } 33 | setIsLoaded(true) 34 | }) 35 | .catch((err) => { 36 | console.error(err) 37 | setIsHidden(true) 38 | }) 39 | } 40 | 41 | if (params) { 42 | setIsLoaded(true) 43 | } 44 | }, [tmClient, params, isLoaded]) 45 | 46 | return ( 47 | 123 | ) 124 | } 125 | -------------------------------------------------------------------------------- /src/components/Parameters/MintParameters.tsx: -------------------------------------------------------------------------------- 1 | import { InfoOutlineIcon } from '@chakra-ui/icons' 2 | import { 3 | Box, 4 | Flex, 5 | Heading, 6 | SimpleGrid, 7 | Skeleton, 8 | Text, 9 | Tooltip, 10 | useColorModeValue, 11 | } from '@chakra-ui/react' 12 | import { useState, useEffect } from 'react' 13 | import { useSelector, useDispatch } from 'react-redux' 14 | import { selectTmClient } from '@/store/connectSlice' 15 | import { selectMintParams, setMintParams } from '@/store/paramsSlice' 16 | import { queryMintParams } from '@/rpc/abci' 17 | import { convertRateToPercent } from '@/utils/helper' 18 | 19 | export default function MintParameters() { 20 | const [isHidden, setIsHidden] = useState(false) 21 | const [isLoaded, setIsLoaded] = useState(false) 22 | const dispatch = useDispatch() 23 | const tmClient = useSelector(selectTmClient) 24 | const params = useSelector(selectMintParams) 25 | 26 | useEffect(() => { 27 | if (tmClient && !params && !isLoaded) { 28 | queryMintParams(tmClient) 29 | .then((response) => { 30 | if (response.params) { 31 | dispatch(setMintParams(response.params)) 32 | } 33 | setIsLoaded(true) 34 | }) 35 | .catch((err) => { 36 | console.error(err) 37 | setIsHidden(true) 38 | }) 39 | } 40 | 41 | if (params) { 42 | setIsLoaded(true) 43 | } 44 | }, [tmClient, params, isLoaded]) 45 | 46 | return ( 47 | 133 | ) 134 | } 135 | -------------------------------------------------------------------------------- /src/components/Parameters/SlashingParameters.tsx: -------------------------------------------------------------------------------- 1 | import { InfoOutlineIcon } from '@chakra-ui/icons' 2 | import { 3 | Box, 4 | Flex, 5 | Heading, 6 | SimpleGrid, 7 | Skeleton, 8 | Text, 9 | Tooltip, 10 | useColorModeValue, 11 | } from '@chakra-ui/react' 12 | import { useState, useEffect } from 'react' 13 | import { useSelector, useDispatch } from 'react-redux' 14 | import { selectTmClient } from '@/store/connectSlice' 15 | import { selectSlashingParams, setSlashingParams } from '@/store/paramsSlice' 16 | import { querySlashingParams } from '@/rpc/abci' 17 | import { displayDurationSeconds, convertRateToPercent } from '@/utils/helper' 18 | import { fromUtf8 } from '@cosmjs/encoding' 19 | 20 | export default function SlashingParameters() { 21 | const [isHidden, setIsHidden] = useState(false) 22 | const [isLoaded, setIsLoaded] = useState(false) 23 | const dispatch = useDispatch() 24 | const tmClient = useSelector(selectTmClient) 25 | const params = useSelector(selectSlashingParams) 26 | 27 | useEffect(() => { 28 | if (tmClient && !params && !isLoaded) { 29 | querySlashingParams(tmClient) 30 | .then((response) => { 31 | if (response.params) { 32 | dispatch(setSlashingParams(response.params)) 33 | } 34 | setIsLoaded(true) 35 | }) 36 | .catch((err) => { 37 | console.error(err) 38 | setIsHidden(true) 39 | }) 40 | } 41 | 42 | if (params) { 43 | setIsLoaded(true) 44 | } 45 | }, [tmClient, params, isLoaded]) 46 | 47 | return ( 48 | 134 | ) 135 | } 136 | -------------------------------------------------------------------------------- /src/rpc/abci/index.ts: -------------------------------------------------------------------------------- 1 | import { Tendermint37Client } from '@cosmjs/tendermint-rpc' 2 | import { QueryClient } from '@cosmjs/stargate' 3 | import { PageRequest } from 'cosmjs-types/cosmos/base/query/v1beta1/pagination' 4 | import { 5 | QueryValidatorsRequest, 6 | QueryValidatorsResponse, 7 | QueryParamsRequest as QueryStakingParamsRequest, 8 | QueryParamsResponse as QueryStakingParamsResponse, 9 | } from 'cosmjs-types/cosmos/staking/v1beta1/query' 10 | import { 11 | QueryParamsRequest as QueryMintParamsRequest, 12 | QueryParamsResponse as QueryMintParamsResponse, 13 | } from 'cosmjs-types/cosmos/mint/v1beta1/query' 14 | import { 15 | QueryProposalsRequest, 16 | QueryProposalsResponse, 17 | QueryParamsRequest as QueryGovParamsRequest, 18 | QueryParamsResponse as QueryGovParamsResponse, 19 | } from 'cosmjs-types/cosmos/gov/v1/query' 20 | import { 21 | QueryParamsRequest as QueryDistributionParamsRequest, 22 | QueryParamsResponse as QueryDistributionParamsResponse, 23 | } from 'cosmjs-types/cosmos/distribution/v1beta1/query' 24 | import { 25 | QueryParamsRequest as QuerySlashingParamsRequest, 26 | QueryParamsResponse as QuerySlashingParamsResponse, 27 | } from 'cosmjs-types/cosmos/slashing/v1beta1/query' 28 | 29 | export async function queryActiveValidators( 30 | tmClient: Tendermint37Client, 31 | page: number, 32 | perPage: number 33 | ): Promise { 34 | const queryClient = new QueryClient(tmClient) 35 | const req = QueryValidatorsRequest.encode({ 36 | status: 'BOND_STATUS_BONDED', 37 | pagination: PageRequest.fromJSON({ 38 | offset: page * perPage, 39 | limit: perPage, 40 | countTotal: true, 41 | }), 42 | }).finish() 43 | const { value } = await queryClient.queryAbci( 44 | '/cosmos.staking.v1beta1.Query/Validators', 45 | req 46 | ) 47 | return QueryValidatorsResponse.decode(value) 48 | } 49 | 50 | export async function queryProposals( 51 | tmClient: Tendermint37Client, 52 | page: number, 53 | perPage: number 54 | ): Promise { 55 | const queryClient = new QueryClient(tmClient) 56 | const proposalsRequest = QueryProposalsRequest.fromPartial({ 57 | pagination: PageRequest.fromJSON({ 58 | offset: page * perPage, 59 | limit: perPage, 60 | countTotal: true, 61 | reverse: true, 62 | }), 63 | }) 64 | const req = QueryProposalsRequest.encode(proposalsRequest).finish() 65 | const { value } = await queryClient.queryAbci( 66 | '/cosmos.gov.v1.Query/Proposals', 67 | req 68 | ) 69 | return QueryProposalsResponse.decode(value) 70 | } 71 | 72 | export async function queryStakingParams( 73 | tmClient: Tendermint37Client 74 | ): Promise { 75 | const queryClient = new QueryClient(tmClient) 76 | const req = QueryStakingParamsRequest.encode({}).finish() 77 | const { value } = await queryClient.queryAbci( 78 | '/cosmos.staking.v1beta1.Query/Params', 79 | req 80 | ) 81 | return QueryStakingParamsResponse.decode(value) 82 | } 83 | 84 | export async function queryMintParams( 85 | tmClient: Tendermint37Client 86 | ): Promise { 87 | const queryClient = new QueryClient(tmClient) 88 | const req = QueryMintParamsRequest.encode({}).finish() 89 | const { value } = await queryClient.queryAbci( 90 | '/cosmos.mint.v1beta1.Query/Params', 91 | req 92 | ) 93 | return QueryMintParamsResponse.decode(value) 94 | } 95 | 96 | export async function queryGovParams( 97 | tmClient: Tendermint37Client, 98 | paramsType: string 99 | ): Promise { 100 | const queryClient = new QueryClient(tmClient) 101 | const req = QueryGovParamsRequest.encode({ 102 | paramsType: paramsType, 103 | }).finish() 104 | const { value } = await queryClient.queryAbci( 105 | '/cosmos.gov.v1.Query/Params', 106 | req 107 | ) 108 | return QueryGovParamsResponse.decode(value) 109 | } 110 | 111 | export async function queryDistributionParams( 112 | tmClient: Tendermint37Client 113 | ): Promise { 114 | const queryClient = new QueryClient(tmClient) 115 | const req = QueryDistributionParamsRequest.encode({}).finish() 116 | const { value } = await queryClient.queryAbci( 117 | '/cosmos.distribution.v1beta1.Query/Params', 118 | req 119 | ) 120 | return QueryDistributionParamsResponse.decode(value) 121 | } 122 | 123 | export async function querySlashingParams( 124 | tmClient: Tendermint37Client 125 | ): Promise { 126 | const queryClient = new QueryClient(tmClient) 127 | const req = QuerySlashingParamsRequest.encode({}).finish() 128 | const { value } = await queryClient.queryAbci( 129 | '/cosmos.slashing.v1beta1.Query/Params', 130 | req 131 | ) 132 | return QuerySlashingParamsResponse.decode(value) 133 | } 134 | -------------------------------------------------------------------------------- /src/pages/validators/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import { 3 | Box, 4 | Divider, 5 | HStack, 6 | Heading, 7 | Icon, 8 | Link, 9 | useColorModeValue, 10 | Text, 11 | useToast, 12 | } from '@chakra-ui/react' 13 | import { useEffect, useState } from 'react' 14 | import { useSelector } from 'react-redux' 15 | import NextLink from 'next/link' 16 | import { FiChevronRight, FiHome } from 'react-icons/fi' 17 | import { selectTmClient } from '@/store/connectSlice' 18 | import { queryActiveValidators } from '@/rpc/abci' 19 | import DataTable from '@/components/Datatable' 20 | import { createColumnHelper } from '@tanstack/react-table' 21 | import { convertRateToPercent, convertVotingPower } from '@/utils/helper' 22 | 23 | type ValidatorData = { 24 | validator: string 25 | status: string 26 | votingPower: string 27 | commission: string 28 | } 29 | 30 | const columnHelper = createColumnHelper() 31 | 32 | const columns = [ 33 | columnHelper.accessor('validator', { 34 | cell: (info) => info.getValue(), 35 | header: 'Validator', 36 | }), 37 | columnHelper.accessor('status', { 38 | cell: (info) => info.getValue(), 39 | header: 'Status', 40 | }), 41 | columnHelper.accessor('votingPower', { 42 | cell: (info) => info.getValue(), 43 | header: 'Voting Power', 44 | meta: { 45 | isNumeric: true, 46 | }, 47 | }), 48 | columnHelper.accessor('commission', { 49 | cell: (info) => info.getValue(), 50 | header: 'Commission', 51 | meta: { 52 | isNumeric: true, 53 | }, 54 | }), 55 | ] 56 | 57 | export default function Validators() { 58 | const tmClient = useSelector(selectTmClient) 59 | const [page, setPage] = useState(0) 60 | const [perPage, setPerPage] = useState(10) 61 | const [total, setTotal] = useState(0) 62 | const [data, setData] = useState([]) 63 | const [isLoading, setIsLoading] = useState(true) 64 | const toast = useToast() 65 | 66 | useEffect(() => { 67 | if (tmClient) { 68 | setIsLoading(true) 69 | queryActiveValidators(tmClient, page, perPage) 70 | .then((response) => { 71 | setTotal(Number(response.pagination?.total)) 72 | const validatorData: ValidatorData[] = response.validators.map( 73 | (val) => { 74 | return { 75 | validator: val.description?.moniker ?? '', 76 | status: val.status === 3 ? 'Active' : '', 77 | votingPower: convertVotingPower(val.tokens), 78 | commission: convertRateToPercent( 79 | val.commission?.commissionRates?.rate 80 | ), 81 | } 82 | } 83 | ) 84 | setData(validatorData) 85 | setIsLoading(false) 86 | }) 87 | .catch(() => { 88 | toast({ 89 | title: 'Failed to fetch datatable', 90 | description: '', 91 | status: 'error', 92 | duration: 5000, 93 | isClosable: true, 94 | }) 95 | }) 96 | } 97 | }, [tmClient, page, perPage]) 98 | 99 | const onChangePagination = (value: { 100 | pageIndex: number 101 | pageSize: number 102 | }) => { 103 | setPage(value.pageIndex) 104 | setPerPage(value.pageSize) 105 | } 106 | 107 | return ( 108 | <> 109 | 110 | Blocks | Dexplorer 111 | 112 | 113 | 114 | 115 |
116 | 117 | Validators 118 | 119 | 127 | 132 | 133 | 134 | Validators 135 | 136 | 143 | 150 | 151 |
152 | 153 | ) 154 | } 155 | -------------------------------------------------------------------------------- /src/pages/proposals/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import { 3 | Box, 4 | Divider, 5 | HStack, 6 | Heading, 7 | Icon, 8 | Link, 9 | Text, 10 | useToast, 11 | useColorModeValue, 12 | Tag, 13 | Badge, 14 | } from '@chakra-ui/react' 15 | import { useEffect, useState } from 'react' 16 | import { useSelector } from 'react-redux' 17 | import NextLink from 'next/link' 18 | import { FiChevronRight, FiHome } from 'react-icons/fi' 19 | import { selectTmClient } from '@/store/connectSlice' 20 | import { queryProposals } from '@/rpc/abci' 21 | import DataTable from '@/components/Datatable' 22 | import { createColumnHelper } from '@tanstack/react-table' 23 | import { getTypeMsg, displayDate } from '@/utils/helper' 24 | import { proposalStatus, proposalStatusList } from '@/utils/constant' 25 | 26 | type Proposal = { 27 | id: bigint 28 | title: string 29 | types: string 30 | status: proposalStatus | undefined 31 | votingEnd: string 32 | } 33 | 34 | const columnHelper = createColumnHelper() 35 | 36 | const columns = [ 37 | columnHelper.accessor('id', { 38 | cell: (info) => `#${info.getValue()}`, 39 | header: '#ID', 40 | }), 41 | columnHelper.accessor('title', { 42 | cell: (info) => info.getValue(), 43 | header: 'Title', 44 | }), 45 | columnHelper.accessor('types', { 46 | cell: (info) => {info.getValue()}, 47 | header: 'Types', 48 | }), 49 | columnHelper.accessor('status', { 50 | cell: (info) => { 51 | const value = info.getValue() 52 | if (!value) { 53 | return '' 54 | } 55 | return {value.status} 56 | }, 57 | header: 'Status', 58 | }), 59 | columnHelper.accessor('votingEnd', { 60 | cell: (info) => info.getValue(), 61 | header: 'Voting End', 62 | }), 63 | ] 64 | 65 | export default function Proposals() { 66 | const tmClient = useSelector(selectTmClient) 67 | const [page, setPage] = useState(0) 68 | const [perPage, setPerPage] = useState(10) 69 | const [total, setTotal] = useState(0) 70 | const [proposals, setProposals] = useState([]) 71 | const [isLoading, setIsLoading] = useState(true) 72 | const toast = useToast() 73 | 74 | useEffect(() => { 75 | if (tmClient) { 76 | setIsLoading(true) 77 | queryProposals(tmClient, page, perPage) 78 | .then((response) => { 79 | setTotal(Number(response.pagination?.total)) 80 | const proposalsList: Proposal[] = response.proposals.map((val) => { 81 | const votingEnd = val.votingEndTime?.nanos 82 | ? new Date( 83 | Number(val.votingEndTime?.seconds) * 1000 84 | ).toISOString() 85 | : null 86 | return { 87 | id: val.id, 88 | title: val.title, 89 | types: getTypeMsg( 90 | val.messages.length ? val.messages[0].typeUrl : '' 91 | ), 92 | status: proposalStatusList.find( 93 | (item) => item.id === Number(val.status.toString()) 94 | ), 95 | votingEnd: votingEnd ? displayDate(votingEnd) : '', 96 | } 97 | }) 98 | setProposals(proposalsList) 99 | setIsLoading(false) 100 | }) 101 | .catch((err) => { 102 | console.error(err) 103 | toast({ 104 | title: 'Failed to fetch proposals', 105 | description: '', 106 | status: 'error', 107 | duration: 5000, 108 | isClosable: true, 109 | }) 110 | }) 111 | } 112 | }, [tmClient, page, perPage]) 113 | 114 | const onChangePagination = (value: { 115 | pageIndex: number 116 | pageSize: number 117 | }) => { 118 | setPage(value.pageIndex) 119 | setPerPage(value.pageSize) 120 | } 121 | 122 | return ( 123 | <> 124 | 125 | Proposals | Dexplorer 126 | 127 | 128 | 129 | 130 |
131 | 132 | Proposals 133 | 134 | 142 | 147 | 148 | 149 | Proposals 150 | 151 | 158 | 165 | 166 |
167 | 168 | ) 169 | } 170 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import { 3 | useColorModeValue, 4 | FlexProps, 5 | Heading, 6 | Divider, 7 | HStack, 8 | Icon, 9 | Link, 10 | Text, 11 | SimpleGrid, 12 | Box, 13 | VStack, 14 | Skeleton, 15 | } from '@chakra-ui/react' 16 | import { 17 | FiHome, 18 | FiChevronRight, 19 | FiBox, 20 | FiClock, 21 | FiCpu, 22 | FiUsers, 23 | } from 'react-icons/fi' 24 | import { IconType } from 'react-icons' 25 | import NextLink from 'next/link' 26 | import { useEffect, useState } from 'react' 27 | import { useSelector } from 'react-redux' 28 | import { getValidators } from '@/rpc/query' 29 | import { selectTmClient } from '@/store/connectSlice' 30 | import { selectNewBlock } from '@/store/streamSlice' 31 | import { displayDate } from '@/utils/helper' 32 | import { StatusResponse } from '@cosmjs/tendermint-rpc' 33 | 34 | export default function Home() { 35 | const tmClient = useSelector(selectTmClient) 36 | const newBlock = useSelector(selectNewBlock) 37 | const [validators, setValidators] = useState() 38 | const [isLoaded, setIsLoaded] = useState(false) 39 | const [status, setStatus] = useState() 40 | 41 | useEffect(() => { 42 | if (tmClient) { 43 | tmClient.status().then((response) => setStatus(response)) 44 | getValidators(tmClient).then((response) => setValidators(response.total)) 45 | } 46 | }, [tmClient]) 47 | 48 | useEffect(() => { 49 | if ((!isLoaded && newBlock) || (!isLoaded && status)) { 50 | setIsLoaded(true) 51 | } 52 | }, [isLoaded, newBlock, status]) 53 | 54 | return ( 55 | <> 56 | 57 | Home | Dexplorer 58 | 59 | 60 | 61 | 62 |
63 | 64 | Home 65 | 66 | 74 | 79 | 80 | 81 | Home 82 | 83 | 84 | 85 | 86 | 97 | 98 | 99 | 114 | 115 | 116 | 117 | 128 | 129 | 130 | 131 | 138 | 139 | 140 | 141 |
142 | 143 | ) 144 | } 145 | 146 | interface BoxInfoProps extends FlexProps { 147 | bgColor: string 148 | color: string 149 | icon: IconType 150 | name: string 151 | value: string | number | undefined 152 | } 153 | const BoxInfo = ({ 154 | bgColor, 155 | color, 156 | icon, 157 | name, 158 | value, 159 | ...rest 160 | }: BoxInfoProps) => { 161 | return ( 162 | 169 | 180 | 181 | 182 | {value} 183 | {name} 184 | 185 | ) 186 | } 187 | -------------------------------------------------------------------------------- /src/components/Parameters/GovParameters.tsx: -------------------------------------------------------------------------------- 1 | import { InfoOutlineIcon } from '@chakra-ui/icons' 2 | import { 3 | Box, 4 | Flex, 5 | Heading, 6 | SimpleGrid, 7 | Skeleton, 8 | Text, 9 | Tooltip, 10 | useColorModeValue, 11 | } from '@chakra-ui/react' 12 | import { useState, useEffect } from 'react' 13 | import { useSelector, useDispatch } from 'react-redux' 14 | import { selectTmClient } from '@/store/connectSlice' 15 | import { 16 | selectGovVotingParams, 17 | selectGovDepositParams, 18 | selectGovTallyParams, 19 | setGovVotingParams, 20 | setGovDepositParams, 21 | setGovTallyParams, 22 | } from '@/store/paramsSlice' 23 | import { queryGovParams } from '@/rpc/abci' 24 | import { 25 | displayDurationSeconds, 26 | convertRateToPercent, 27 | displayCoin, 28 | } from '@/utils/helper' 29 | import { fromUtf8 } from '@cosmjs/encoding' 30 | import { GOV_PARAMS_TYPE } from '@/utils/constant' 31 | 32 | export default function GovParameters() { 33 | const [isHidden, setIsHidden] = useState(false) 34 | const [isLoaded, setIsLoaded] = useState(false) 35 | const dispatch = useDispatch() 36 | const tmClient = useSelector(selectTmClient) 37 | const votingParams = useSelector(selectGovVotingParams) 38 | const depositParams = useSelector(selectGovDepositParams) 39 | const tallyParams = useSelector(selectGovTallyParams) 40 | 41 | useEffect(() => { 42 | if ( 43 | tmClient && 44 | !votingParams && 45 | !depositParams && 46 | !tallyParams && 47 | !isLoaded 48 | ) { 49 | Promise.all([ 50 | queryGovParams(tmClient, GOV_PARAMS_TYPE.VOTING), 51 | queryGovParams(tmClient, GOV_PARAMS_TYPE.DEPOSIT), 52 | queryGovParams(tmClient, GOV_PARAMS_TYPE.TALLY), 53 | ]) 54 | .then((responses) => { 55 | if (responses[0].params) { 56 | dispatch(setGovVotingParams(responses[0].params)) 57 | } 58 | if (responses[1].params) { 59 | dispatch(setGovDepositParams(responses[1].params)) 60 | } 61 | if (responses[2].params) { 62 | dispatch(setGovTallyParams(responses[2].params)) 63 | } 64 | setIsLoaded(true) 65 | }) 66 | .catch((err) => { 67 | console.error(err) 68 | setIsHidden(true) 69 | }) 70 | } 71 | 72 | if (votingParams && depositParams && tallyParams) { 73 | setIsLoaded(true) 74 | } 75 | }, [tmClient, votingParams, depositParams, tallyParams, isLoaded]) 76 | 77 | return ( 78 | 170 | ) 171 | } 172 | -------------------------------------------------------------------------------- /src/components/Connect/index.tsx: -------------------------------------------------------------------------------- 1 | import { FormEvent, ChangeEvent, useState } from 'react' 2 | import { 3 | Stack, 4 | FormControl, 5 | Input, 6 | Button, 7 | useColorModeValue, 8 | Heading, 9 | Text, 10 | Container, 11 | Flex, 12 | Box, 13 | IconButton, 14 | } from '@chakra-ui/react' 15 | import { CheckIcon } from '@chakra-ui/icons' 16 | import { useDispatch } from 'react-redux' 17 | import { 18 | setConnectState, 19 | setTmClient, 20 | setRPCAddress, 21 | } from '@/store/connectSlice' 22 | import Head from 'next/head' 23 | import { LS_RPC_ADDRESS, LS_RPC_ADDRESS_LIST } from '@/utils/constant' 24 | import { validateConnection, connectWebsocketClient } from '@/rpc/client' 25 | import { removeTrailingSlash } from '@/utils/helper' 26 | import { FiZap } from 'react-icons/fi' 27 | 28 | const chainList = [ 29 | { 30 | name: 'Cosmos Hub', 31 | rpc: 'https://cosmoshub-rpc.lavenderfive.com', 32 | }, 33 | { 34 | name: 'Osmosis', 35 | rpc: 'https://rpc-osmosis.ecostake.com', 36 | }, 37 | ] 38 | 39 | export default function Connect() { 40 | const [address, setAddress] = useState('') 41 | const [state, setState] = useState<'initial' | 'submitting' | 'success'>( 42 | 'initial' 43 | ) 44 | const [error, setError] = useState(false) 45 | const dispatch = useDispatch() 46 | 47 | const submitForm = async (e: FormEvent) => { 48 | e.preventDefault() 49 | const addr = removeTrailingSlash(address) 50 | await connectClient(addr) 51 | } 52 | 53 | const connectClient = async (rpcAddress: string) => { 54 | try { 55 | setError(false) 56 | setState('submitting') 57 | 58 | if (!rpcAddress) { 59 | setError(true) 60 | setState('initial') 61 | return 62 | } 63 | 64 | const isValid = await validateConnection(rpcAddress) 65 | if (!isValid) { 66 | setError(true) 67 | setState('initial') 68 | return 69 | } 70 | 71 | const tmClient = await connectWebsocketClient(rpcAddress) 72 | 73 | if (!tmClient) { 74 | setError(true) 75 | setState('initial') 76 | return 77 | } 78 | 79 | dispatch(setConnectState(true)) 80 | dispatch(setTmClient(tmClient)) 81 | dispatch(setRPCAddress(rpcAddress)) 82 | setState('success') 83 | 84 | window.localStorage.setItem(LS_RPC_ADDRESS, rpcAddress) 85 | window.localStorage.setItem( 86 | LS_RPC_ADDRESS_LIST, 87 | JSON.stringify([rpcAddress]) 88 | ) 89 | } catch (err) { 90 | console.error(err) 91 | setError(true) 92 | setState('initial') 93 | return 94 | } 95 | } 96 | 97 | const selectChain = (rpcAddress: string) => { 98 | setAddress(rpcAddress) 99 | connectClient(rpcAddress) 100 | } 101 | 102 | return ( 103 | <> 104 | 105 | Dexplorer | Connect 106 | 107 | 108 | 109 | 110 | 118 | 125 | 132 | Dexplorer 133 | 134 | 135 | Disposable Cosmos SDK Chain Explorer 136 | 137 | 143 | 144 | ) => 160 | setAddress(e.target.value) 161 | } 162 | /> 163 | 164 | 165 | 180 | 181 | 182 | 187 | {error ? 'Oh no, cannot connect to websocket client! 😢' : ''} 188 | 189 | 190 | 191 | 198 | Try out these RPCs 199 | 200 | {chainList.map((chain) => { 201 | return ( 202 | 215 | 216 | 217 | {chain.name} 218 | 219 | 220 | {chain.rpc} 221 | 222 | 223 | selectChain(chain.rpc)} 225 | backgroundColor={useColorModeValue( 226 | 'light-theme', 227 | 'dark-theme' 228 | )} 229 | color={'white'} 230 | _hover={{ 231 | backgroundColor: useColorModeValue( 232 | 'dark-theme', 233 | 'light-theme' 234 | ), 235 | }} 236 | aria-label="Connect RPC" 237 | size="sm" 238 | fontSize="20" 239 | icon={} 240 | /> 241 | 242 | ) 243 | })} 244 | 245 | 246 | 247 | ) 248 | } 249 | -------------------------------------------------------------------------------- /src/components/Datatable/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useMemo, useEffect } from 'react' 2 | import { 3 | Table, 4 | Thead, 5 | Tbody, 6 | Tr, 7 | Th, 8 | Td, 9 | chakra, 10 | Flex, 11 | Tooltip, 12 | IconButton, 13 | NumberInput, 14 | NumberInputField, 15 | NumberInputStepper, 16 | NumberIncrementStepper, 17 | NumberDecrementStepper, 18 | Select, 19 | Text, 20 | SkeletonText, 21 | } from '@chakra-ui/react' 22 | import { 23 | ArrowLeftIcon, 24 | ArrowRightIcon, 25 | ChevronLeftIcon, 26 | ChevronRightIcon, 27 | TriangleDownIcon, 28 | TriangleUpIcon, 29 | } from '@chakra-ui/icons' 30 | import { 31 | useReactTable, 32 | flexRender, 33 | getCoreRowModel, 34 | ColumnDef, 35 | SortingState, 36 | getSortedRowModel, 37 | PaginationState, 38 | } from '@tanstack/react-table' 39 | 40 | export type DataTableProps = { 41 | data: Data[] 42 | columns: ColumnDef[] 43 | total: number 44 | isLoading?: boolean 45 | onChangePagination: Function 46 | } 47 | 48 | export default function DataTable({ 49 | data, 50 | columns, 51 | total, 52 | isLoading, 53 | onChangePagination, 54 | }: DataTableProps) { 55 | const [sorting, setSorting] = useState([]) 56 | const [pageCount, setPageCount] = useState(0) 57 | 58 | const [{ pageIndex, pageSize }, setPagination] = useState({ 59 | pageIndex: 0, 60 | pageSize: 10, 61 | }) 62 | const pagination = useMemo( 63 | () => ({ 64 | pageIndex, 65 | pageSize, 66 | }), 67 | [pageIndex, pageSize] 68 | ) 69 | 70 | useEffect(() => { 71 | if (total > 0) { 72 | const totalPage = Math.ceil(total / pagination.pageSize) 73 | setPageCount(totalPage) 74 | } 75 | onChangePagination(pagination) 76 | }, [total, pagination]) 77 | 78 | const table = useReactTable({ 79 | columns, 80 | data, 81 | pageCount: pageCount, 82 | getCoreRowModel: getCoreRowModel(), 83 | onSortingChange: setSorting, 84 | getSortedRowModel: getSortedRowModel(), 85 | state: { 86 | sorting, 87 | pagination, 88 | }, 89 | onPaginationChange: setPagination, 90 | manualPagination: true, 91 | }) 92 | 93 | return ( 94 | <> 95 | 96 | 97 | {table.getHeaderGroups().map((headerGroup) => ( 98 | 99 | {headerGroup.headers.map((header) => { 100 | // see https://tanstack.com/table/v8/docs/api/core/column-def#meta to type this correctly 101 | const meta: any = header.column.columnDef.meta 102 | return ( 103 | 123 | ) 124 | })} 125 | 126 | ))} 127 | 128 | {isLoading ? ( 129 | 130 | {table.getRowModel().rows.map((row) => ( 131 | 132 | {row.getVisibleCells().map((cell) => { 133 | return ( 134 | 137 | ) 138 | })} 139 | 140 | ))} 141 | 142 | ) : ( 143 | 144 | {table.getRowModel().rows.map((row) => ( 145 | 146 | {row.getVisibleCells().map((cell) => { 147 | const meta: any = cell.column.columnDef.meta 148 | return ( 149 | 155 | ) 156 | })} 157 | 158 | ))} 159 | 160 | )} 161 |
108 | {flexRender( 109 | header.column.columnDef.header, 110 | header.getContext() 111 | )} 112 | 113 | 114 | {header.column.getIsSorted() ? ( 115 | header.column.getIsSorted() === 'desc' ? ( 116 | 117 | ) : ( 118 | 119 | ) 120 | ) : null} 121 | 122 |
135 | 136 |
150 | {flexRender( 151 | cell.column.columnDef.cell, 152 | cell.getContext() 153 | )} 154 |
162 | 163 | 164 | 165 | table.setPageIndex(0)} 167 | isDisabled={!table.getCanPreviousPage()} 168 | icon={} 169 | mr={4} 170 | aria-label="First Page" 171 | /> 172 | 173 | 174 | table.previousPage()} 176 | disabled={!table.getCanPreviousPage()} 177 | icon={} 178 | aria-label="Previous Page" 179 | /> 180 | 181 | 182 | 183 | 184 | 185 | Page{' '} 186 | 187 | {table.getState().pagination.pageIndex + 1} 188 | {' '} 189 | of{' '} 190 | 191 | {table.getPageCount()} 192 | 193 | 194 | Go to page:{' '} 195 | { 202 | const page = value ? Number(value) - 1 : 0 203 | table.setPageIndex(page) 204 | }} 205 | defaultValue={pageIndex + 1} 206 | > 207 | 208 | 209 | 210 | 211 | 212 | 213 | 226 | 227 | 228 | 229 | 230 | table.nextPage()} 232 | disabled={!table.getCanNextPage()} 233 | icon={} 234 | aria-label="Next Page" 235 | /> 236 | 237 | 238 | table.setPageIndex(table.getPageCount() - 1)} 240 | disabled={!table.getCanNextPage()} 241 | icon={} 242 | ml={4} 243 | aria-label="Last Page" 244 | /> 245 | 246 | 247 | 248 | 249 | ) 250 | } 251 | -------------------------------------------------------------------------------- /src/components/Sidebar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useEffect, useState } from 'react' 2 | import { 3 | IconButton, 4 | Box, 5 | CloseButton, 6 | Flex, 7 | Icon, 8 | useColorModeValue, 9 | Link, 10 | Drawer, 11 | DrawerContent, 12 | Text, 13 | useDisclosure, 14 | BoxProps, 15 | FlexProps, 16 | Button, 17 | Heading, 18 | } from '@chakra-ui/react' 19 | import { 20 | FiHome, 21 | FiBox, 22 | FiCompass, 23 | FiStar, 24 | FiSliders, 25 | FiMenu, 26 | FiLogOut, 27 | FiGithub, 28 | FiAlertCircle, 29 | } from 'react-icons/fi' 30 | import { IconType } from 'react-icons' 31 | import NextLink from 'next/link' 32 | import { useRouter } from 'next/router' 33 | import { selectSubsNewBlock, selectSubsTxEvent } from '@/store/streamSlice' 34 | import { useSelector } from 'react-redux' 35 | import { LS_RPC_ADDRESS, LS_RPC_ADDRESS_LIST } from '@/utils/constant' 36 | 37 | interface LinkItemProps { 38 | name: string 39 | icon: IconType 40 | route: string 41 | isBlank?: boolean 42 | } 43 | const LinkItems: Array = [ 44 | { name: 'Home', icon: FiHome, route: '/' }, 45 | { name: 'Blocks', icon: FiBox, route: '/blocks' }, 46 | { name: 'Validators', icon: FiCompass, route: '/validators' }, 47 | { name: 'Proposals', icon: FiStar, route: '/proposals' }, 48 | { name: 'Parameters', icon: FiSliders, route: '/parameters' }, 49 | ] 50 | const RefLinkItems: Array = [ 51 | { 52 | name: 'Github', 53 | icon: FiGithub, 54 | route: 'https://github.com/arifintahu/dexplorer', 55 | isBlank: true, 56 | }, 57 | { 58 | name: 'Report Issues', 59 | icon: FiAlertCircle, 60 | route: 'https://github.com/arifintahu/dexplorer/issues', 61 | isBlank: true, 62 | }, 63 | ] 64 | 65 | export default function Sidebar({ children }: { children: ReactNode }) { 66 | const { isOpen, onOpen, onClose } = useDisclosure() 67 | 68 | return ( 69 | 70 | onClose} 72 | display={{ base: 'none', md: 'block' }} 73 | /> 74 | 83 | 84 | 85 | 86 | 87 | {/* mobilenav */} 88 | 89 | 90 | {children} 91 | 92 | 93 | ) 94 | } 95 | 96 | interface SidebarProps extends BoxProps { 97 | onClose: () => void 98 | } 99 | 100 | const SidebarContent = ({ onClose, ...rest }: SidebarProps) => { 101 | const subsNewBlock = useSelector(selectSubsNewBlock) 102 | const subsTxEvent = useSelector(selectSubsTxEvent) 103 | 104 | const handleDisconnect = () => { 105 | subsNewBlock?.unsubscribe() 106 | subsTxEvent?.unsubscribe() 107 | window.localStorage.removeItem(LS_RPC_ADDRESS) 108 | window.localStorage.removeItem(LS_RPC_ADDRESS_LIST) 109 | window.location.replace('/') 110 | } 111 | 112 | return ( 113 | 122 | 123 | 124 | 130 | 131 | Dexplorer 132 | 133 | 137 | 138 | {LinkItems.map((link) => ( 139 | 140 | {link.name} 141 | 142 | ))} 143 | 152 | Links 153 | 154 | {RefLinkItems.map((link) => ( 155 | 161 | {link.name} 162 | 163 | ))} 164 | 165 | 166 | 174 | 175 | 176 | 177 | ) 178 | } 179 | 180 | interface NavItemProps extends FlexProps { 181 | icon: IconType 182 | children: string | number 183 | route: string 184 | isBlank?: boolean 185 | } 186 | const NavItem = ({ icon, children, route, isBlank, ...rest }: NavItemProps) => { 187 | const router = useRouter() 188 | const [isSelected, setIsSelected] = useState(false) 189 | 190 | useEffect(() => { 191 | if (route === '/') { 192 | setIsSelected(router.route === route) 193 | } else { 194 | setIsSelected(router.route.includes(route)) 195 | } 196 | }, [router]) 197 | 198 | return ( 199 | 206 | 226 | {icon && ( 227 | 237 | )} 238 | {children} 239 | 240 | 241 | ) 242 | } 243 | 244 | interface MobileProps extends FlexProps { 245 | onOpen: () => void 246 | } 247 | const MobileNav = ({ onOpen, ...rest }: MobileProps) => { 248 | return ( 249 | 260 | } 265 | /> 266 | 267 | 268 | Dexplorer 269 | 270 | 271 | ) 272 | } 273 | -------------------------------------------------------------------------------- /src/pages/blocks/[height].tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Divider, 4 | HStack, 5 | Heading, 6 | Icon, 7 | Link, 8 | Table, 9 | TableContainer, 10 | Tag, 11 | Tbody, 12 | Td, 13 | Text, 14 | Th, 15 | Thead, 16 | Tr, 17 | useColorModeValue, 18 | useToast, 19 | } from '@chakra-ui/react' 20 | import { FiChevronRight, FiHome } from 'react-icons/fi' 21 | import NextLink from 'next/link' 22 | import Head from 'next/head' 23 | import { useRouter } from 'next/router' 24 | import { useEffect, useState } from 'react' 25 | import { useSelector } from 'react-redux' 26 | import { getBlock } from '@/rpc/query' 27 | import { selectTmClient } from '@/store/connectSlice' 28 | import { Block, Coin } from '@cosmjs/stargate' 29 | import { Tx as TxData } from 'cosmjs-types/cosmos/tx/v1beta1/tx' 30 | import { sha256 } from '@cosmjs/crypto' 31 | import { toHex } from '@cosmjs/encoding' 32 | import { timeFromNow, trimHash, displayDate, getTypeMsg } from '@/utils/helper' 33 | 34 | export default function DetailBlock() { 35 | const router = useRouter() 36 | const toast = useToast() 37 | const { height } = router.query 38 | const tmClient = useSelector(selectTmClient) 39 | const [block, setBlock] = useState(null) 40 | 41 | interface Tx { 42 | data: TxData 43 | hash: Uint8Array 44 | } 45 | const [txs, setTxs] = useState([]) 46 | 47 | useEffect(() => { 48 | if (tmClient && height) { 49 | getBlock(tmClient, parseInt(height as string, 10)) 50 | .then(setBlock) 51 | .catch(showError) 52 | } 53 | }, [tmClient, height]) 54 | 55 | useEffect(() => { 56 | if (block?.txs.length && !txs.length) { 57 | for (const rawTx of block.txs) { 58 | const data = TxData.decode(rawTx) 59 | const hash = sha256(rawTx) 60 | setTxs((prevTxs) => [ 61 | ...prevTxs, 62 | { 63 | data, 64 | hash, 65 | }, 66 | ]) 67 | } 68 | } 69 | }, [block]) 70 | 71 | const renderMessages = (messages: any) => { 72 | if (messages.length == 1) { 73 | return ( 74 | 75 | {getTypeMsg(messages[0].typeUrl)} 76 | 77 | ) 78 | } else if (messages.length > 1) { 79 | return ( 80 | 81 | {getTypeMsg(messages[0].typeUrl)} 82 | +{messages.length - 1} 83 | 84 | ) 85 | } 86 | 87 | return '' 88 | } 89 | 90 | const getFee = (fees: Coin[] | undefined) => { 91 | if (fees && fees.length) { 92 | return ( 93 | 94 | {fees[0].amount} 95 | {fees[0].denom} 96 | 97 | ) 98 | } 99 | return '' 100 | } 101 | 102 | const showError = (err: Error) => { 103 | const errMsg = err.message 104 | let error = null 105 | try { 106 | error = JSON.parse(errMsg) 107 | } catch (e) { 108 | error = { 109 | message: 'Invalid', 110 | data: errMsg, 111 | } 112 | } 113 | 114 | toast({ 115 | title: error.message, 116 | description: error.data, 117 | status: 'error', 118 | duration: 5000, 119 | isClosable: true, 120 | }) 121 | } 122 | 123 | return ( 124 | <> 125 | 126 | Detail Block | Dexplorer 127 | 128 | 129 | 130 | 131 |
132 | 133 | Block 134 | 135 | 143 | 148 | 149 | 150 | 156 | 157 | Blocks 158 | 159 | 160 | 161 | Block #{height} 162 | 163 | 170 | 171 | Header 172 | 173 | 174 | 175 | 176 | 177 | 178 | 181 | 182 | 183 | 184 | 187 | 188 | 189 | 190 | 193 | 200 | 201 | 202 | 205 | 206 | 207 | 208 | 211 | 212 | 213 | 214 |
179 | Chain Id 180 | {block?.header.chainId}
185 | Height 186 | {block?.header.height}
191 | Block Time 192 | 194 | {block?.header.time 195 | ? `${timeFromNow(block?.header.time)} ( ${displayDate( 196 | block?.header.time 197 | )} )` 198 | : ''} 199 |
203 | Block Hash 204 | {block?.id}
209 | Number of Tx 210 | {block?.txs.length}
215 |
216 |
217 | 218 | 225 | 226 | Transactions 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | {txs.map((tx) => ( 242 | 243 | 253 | 254 | 255 | 256 | 261 | 262 | ))} 263 | 264 |
Tx HashMessagesFeeHeightTime
244 | 250 | {trimHash(tx.hash)} 251 | 252 | {renderMessages(tx.data.body?.messages)}{getFee(tx.data.authInfo?.fee?.amount)}{height} 257 | {block?.header.time 258 | ? timeFromNow(block?.header.time) 259 | : ''} 260 |
265 |
266 |
267 |
268 | 269 | ) 270 | } 271 | -------------------------------------------------------------------------------- /src/pages/blocks/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import { useEffect, useState } from 'react' 3 | import { useSelector } from 'react-redux' 4 | import { NewBlockEvent, TxEvent } from '@cosmjs/tendermint-rpc' 5 | import { 6 | Box, 7 | Divider, 8 | HStack, 9 | Heading, 10 | Icon, 11 | Link, 12 | Table, 13 | useColorModeValue, 14 | TableContainer, 15 | Tbody, 16 | Td, 17 | Text, 18 | Th, 19 | Thead, 20 | Tr, 21 | Tabs, 22 | TabList, 23 | Tab, 24 | TabPanels, 25 | TabPanel, 26 | Tag, 27 | TagLeftIcon, 28 | TagLabel, 29 | } from '@chakra-ui/react' 30 | import NextLink from 'next/link' 31 | import { FiChevronRight, FiHome, FiCheck, FiX } from 'react-icons/fi' 32 | import { selectNewBlock, selectTxEvent } from '@/store/streamSlice' 33 | import { toHex } from '@cosmjs/encoding' 34 | import { TxBody } from 'cosmjs-types/cosmos/tx/v1beta1/tx' 35 | import { timeFromNow, trimHash, getTypeMsg } from '@/utils/helper' 36 | 37 | const MAX_ROWS = 20 38 | 39 | interface Tx { 40 | TxEvent: TxEvent 41 | Timestamp: Date 42 | } 43 | 44 | export default function Blocks() { 45 | const newBlock = useSelector(selectNewBlock) 46 | const txEvent = useSelector(selectTxEvent) 47 | const [blocks, setBlocks] = useState([]) 48 | 49 | const [txs, setTxs] = useState([]) 50 | 51 | useEffect(() => { 52 | if (newBlock) { 53 | updateBlocks(newBlock) 54 | } 55 | }, [newBlock]) 56 | 57 | useEffect(() => { 58 | if (txEvent) { 59 | updateTxs(txEvent) 60 | } 61 | }, [txEvent]) 62 | 63 | const updateBlocks = (block: NewBlockEvent) => { 64 | if (blocks.length) { 65 | if (block.header.height > blocks[0].header.height) { 66 | setBlocks((prevBlocks) => [block, ...prevBlocks.slice(0, MAX_ROWS - 1)]) 67 | } 68 | } else { 69 | setBlocks([block]) 70 | } 71 | } 72 | 73 | const updateTxs = (txEvent: TxEvent) => { 74 | const tx = { 75 | TxEvent: txEvent, 76 | Timestamp: new Date(), 77 | } 78 | if (txs.length) { 79 | if ( 80 | txEvent.height >= txs[0].TxEvent.height && 81 | txEvent.hash != txs[0].TxEvent.hash 82 | ) { 83 | setTxs((prevTx) => [tx, ...prevTx.slice(0, MAX_ROWS - 1)]) 84 | } 85 | } else { 86 | setTxs([tx]) 87 | } 88 | } 89 | 90 | const renderMessages = (data: Uint8Array | undefined) => { 91 | if (data) { 92 | const txBody = TxBody.decode(data) 93 | const messages = txBody.messages 94 | 95 | if (messages.length == 1) { 96 | return ( 97 | 98 | {getTypeMsg(messages[0].typeUrl)} 99 | 100 | ) 101 | } else if (messages.length > 1) { 102 | return ( 103 | 104 | {getTypeMsg(messages[0].typeUrl)} 105 | +{messages.length - 1} 106 | 107 | ) 108 | } 109 | } 110 | 111 | return '' 112 | } 113 | 114 | return ( 115 | <> 116 | 117 | Blocks | Dexplorer 118 | 119 | 120 | 121 | 122 |
123 | 124 | Blocks 125 | 126 | 134 | 139 | 140 | 141 | Blocks 142 | 143 | 150 | 151 | 152 | 156 | Blocks 157 | 158 | 162 | Transactions 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | {blocks.map((block) => ( 179 | 180 | 192 | 193 | 194 | 197 | 198 | ))} 199 | 200 |
HeightApp HashTxsTime
181 | 187 | 188 | {block.header.height} 189 | 190 | 191 | {toHex(block.header.appHash)}{block.txs.length} 195 | {timeFromNow(block.header.time.toISOString())} 196 |
201 |
202 |
203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | {txs.map((tx) => ( 217 | 218 | 232 | 245 | 246 | 247 | 248 | 249 | ))} 250 | 251 |
Tx HashResultMessagesHeightTime
219 | 227 | 228 | {trimHash(tx.TxEvent.hash)} 229 | 230 | 231 | 233 | {tx.TxEvent.result.code == 0 ? ( 234 | 235 | 236 | Success 237 | 238 | ) : ( 239 | 240 | 241 | Error 242 | 243 | )} 244 | {renderMessages(tx.TxEvent.result.data)}{tx.TxEvent.height}{timeFromNow(tx.Timestamp.toISOString())}
252 |
253 |
254 |
255 |
256 |
257 |
258 | 259 | ) 260 | } 261 | -------------------------------------------------------------------------------- /src/pages/txs/[hash].tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Card, 4 | CardBody, 5 | CardHeader, 6 | Divider, 7 | HStack, 8 | Heading, 9 | Icon, 10 | Link, 11 | Table, 12 | TableContainer, 13 | Tag, 14 | TagLabel, 15 | TagLeftIcon, 16 | Tbody, 17 | Td, 18 | Text, 19 | Tr, 20 | useColorModeValue, 21 | useToast, 22 | } from '@chakra-ui/react' 23 | import { FiChevronRight, FiHome, FiCheck, FiX } from 'react-icons/fi' 24 | import NextLink from 'next/link' 25 | import Head from 'next/head' 26 | import { useRouter } from 'next/router' 27 | import { useEffect, useState } from 'react' 28 | import { useSelector } from 'react-redux' 29 | import { selectTmClient } from '@/store/connectSlice' 30 | import { getTx, getBlock } from '@/rpc/query' 31 | import { IndexedTx, Block, Coin } from '@cosmjs/stargate' 32 | import { Tx } from 'cosmjs-types/cosmos/tx/v1beta1/tx' 33 | import { 34 | timeFromNow, 35 | displayDate, 36 | isBech32Address, 37 | getTypeMsg, 38 | } from '@/utils/helper' 39 | import { decodeMsg, DecodeMsg } from '@/encoding' 40 | 41 | export default function DetailBlock() { 42 | const router = useRouter() 43 | const toast = useToast() 44 | const { hash } = router.query 45 | const tmClient = useSelector(selectTmClient) 46 | const [tx, setTx] = useState(null) 47 | const [txData, setTxData] = useState(null) 48 | const [block, setBlock] = useState(null) 49 | const [msgs, setMsgs] = useState([]) 50 | 51 | useEffect(() => { 52 | if (tmClient && hash) { 53 | getTx(tmClient, hash as string) 54 | .then(setTx) 55 | .catch(showError) 56 | } 57 | }, [tmClient, hash]) 58 | 59 | useEffect(() => { 60 | if (tmClient && tx?.height) { 61 | getBlock(tmClient, tx?.height).then(setBlock).catch(showError) 62 | } 63 | }, [tmClient, tx]) 64 | 65 | useEffect(() => { 66 | if (tx?.tx) { 67 | const data = Tx.decode(tx?.tx) 68 | setTxData(data) 69 | } 70 | }, [tx]) 71 | 72 | useEffect(() => { 73 | if (txData?.body?.messages.length && !msgs.length) { 74 | for (const message of txData?.body?.messages) { 75 | const msg = decodeMsg(message.typeUrl, message.value) 76 | setMsgs((prevMsgs) => [...prevMsgs, msg]) 77 | } 78 | } 79 | }, [txData]) 80 | 81 | const getFee = (fees: Coin[] | undefined) => { 82 | if (fees && fees.length) { 83 | return ( 84 | 85 | {fees[0].amount} 86 | {fees[0].denom} 87 | 88 | ) 89 | } 90 | return '' 91 | } 92 | 93 | const showMsgData = (msgData: any) => { 94 | if (msgData) { 95 | if (Array.isArray(msgData)) { 96 | return JSON.stringify(msgData) 97 | } 98 | 99 | if (!Array.isArray(msgData) && msgData.length) { 100 | if (isBech32Address(msgData)) { 101 | return ( 102 | 108 | {msgData} 109 | 110 | ) 111 | } else { 112 | return String(msgData) 113 | } 114 | } 115 | } 116 | 117 | return '' 118 | } 119 | 120 | const showError = (err: Error) => { 121 | const errMsg = err.message 122 | let error = null 123 | try { 124 | error = JSON.parse(errMsg) 125 | } catch (e) { 126 | error = { 127 | message: 'Invalid', 128 | data: errMsg, 129 | } 130 | } 131 | 132 | toast({ 133 | title: error.message, 134 | description: error.data, 135 | status: 'error', 136 | duration: 5000, 137 | isClosable: true, 138 | }) 139 | } 140 | 141 | return ( 142 | <> 143 | 144 | Detail Transaction | Dexplorer 145 | 146 | 147 | 148 | 149 |
150 | 151 | Transaction 152 | 153 | 161 | 166 | 167 | 168 | 174 | Blocks 175 | 176 | 177 | Tx 178 | 179 | 186 | 187 | Information 188 | 189 | 190 | 191 | 192 | 193 | 194 | 197 | 198 | 199 | 200 | 203 | 204 | 205 | 206 | 209 | 222 | 223 | 224 | 227 | 237 | 238 | 239 | 242 | 249 | 250 | 251 | 254 | 255 | 256 | 257 | 260 | 263 | 264 | 265 | 268 | 269 | 270 | 271 |
195 | Chain Id 196 | {block?.header.chainId}
201 | Tx Hash 202 | {tx?.hash}
207 | Status 208 | 210 | {tx?.code == 0 ? ( 211 | 212 | 213 | Success 214 | 215 | ) : ( 216 | 217 | 218 | Error 219 | 220 | )} 221 |
225 | Height 226 | 228 | 234 | {tx?.height} 235 | 236 |
240 | Time 241 | 243 | {block?.header.time 244 | ? `${timeFromNow(block?.header.time)} ( ${displayDate( 245 | block?.header.time 246 | )} )` 247 | : ''} 248 |
252 | Fee 253 | {getFee(txData?.authInfo?.fee?.amount)}
258 | Gas (used / wanted) 259 | 261 | {tx?.gasUsed ? `${tx.gasUsed} / ${tx.gasWanted}` : ''} 262 |
266 | Memo 267 | {txData?.body?.memo}
272 |
273 |
274 | 275 | 282 | 283 | Messages 284 | 285 | 286 | {msgs.map((msg, index) => ( 287 | 288 | 289 | {getTypeMsg(msg.typeUrl)} 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 300 | 301 | 302 | {Object.keys(msg.data ?? {}).map((key) => ( 303 | 304 | 307 | 312 | 313 | ))} 314 | 315 |
298 | typeUrl 299 | {msg.typeUrl}
305 | {key} 306 | 308 | {showMsgData( 309 | msg.data ? msg.data[key as keyof {}] : '' 310 | )} 311 |
316 |
317 |
318 |
319 | ))} 320 |
321 |
322 | 323 | ) 324 | } 325 | -------------------------------------------------------------------------------- /src/pages/accounts/[address].tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Divider, 4 | HStack, 5 | Heading, 6 | Icon, 7 | Link, 8 | Tab, 9 | TabList, 10 | TabPanel, 11 | TabPanels, 12 | Table, 13 | TableContainer, 14 | Tabs, 15 | Tag, 16 | Tbody, 17 | Td, 18 | Text, 19 | Th, 20 | Thead, 21 | Tr, 22 | useColorModeValue, 23 | useToast, 24 | } from '@chakra-ui/react' 25 | import { FiChevronRight, FiHome } from 'react-icons/fi' 26 | import NextLink from 'next/link' 27 | import Head from 'next/head' 28 | import { useRouter } from 'next/router' 29 | import { useEffect, useState } from 'react' 30 | import { useSelector } from 'react-redux' 31 | import { 32 | getAccount, 33 | getAllBalances, 34 | getBalanceStaked, 35 | getTxsBySender, 36 | } from '@/rpc/query' 37 | import { selectTmClient } from '@/store/connectSlice' 38 | import { Account, Coin } from '@cosmjs/stargate' 39 | import { TxSearchResponse } from '@cosmjs/tendermint-rpc' 40 | import { toHex } from '@cosmjs/encoding' 41 | import { TxBody } from 'cosmjs-types/cosmos/tx/v1beta1/tx' 42 | import { trimHash, getTypeMsg } from '@/utils/helper' 43 | 44 | export default function DetailAccount() { 45 | const router = useRouter() 46 | const toast = useToast() 47 | const { address } = router.query 48 | const tmClient = useSelector(selectTmClient) 49 | const [account, setAccount] = useState(null) 50 | const [allBalances, setAllBalances] = useState([]) 51 | const [balanceStaked, setBalanceStaked] = useState(null) 52 | const [txSearch, setTxSearch] = useState(null) 53 | 54 | interface Tx { 55 | data: TxBody 56 | height: number 57 | hash: Uint8Array 58 | } 59 | const [txs, setTxs] = useState([]) 60 | 61 | useEffect(() => { 62 | if (tmClient && address) { 63 | if (!account) { 64 | getAccount(tmClient, address as string) 65 | .then(setAccount) 66 | .catch(showError) 67 | } 68 | 69 | if (!allBalances.length) { 70 | getAllBalances(tmClient, address as string) 71 | .then(setAllBalances) 72 | .catch(showError) 73 | } 74 | 75 | if (!balanceStaked) { 76 | getBalanceStaked(tmClient, address as string) 77 | .then(setBalanceStaked) 78 | .catch(showError) 79 | } 80 | 81 | getTxsBySender(tmClient, address as string, 1, 30) 82 | .then(setTxSearch) 83 | .catch(showError) 84 | } 85 | }, [tmClient, account, allBalances, balanceStaked]) 86 | 87 | useEffect(() => { 88 | if (txSearch?.txs.length && !txs.length) { 89 | for (const rawTx of txSearch.txs) { 90 | if (rawTx.result.data) { 91 | const data = TxBody.decode(rawTx.result.data) 92 | setTxs((prevTxs) => [ 93 | ...prevTxs, 94 | { 95 | data, 96 | hash: rawTx.hash, 97 | height: rawTx.height, 98 | }, 99 | ]) 100 | } 101 | } 102 | } 103 | }, [txSearch]) 104 | 105 | const showError = (err: Error) => { 106 | const errMsg = err.message 107 | let error = null 108 | try { 109 | error = JSON.parse(errMsg) 110 | } catch (e) { 111 | error = { 112 | message: 'Invalid', 113 | data: errMsg, 114 | } 115 | } 116 | 117 | toast({ 118 | title: error.message, 119 | description: error.data, 120 | status: 'error', 121 | duration: 5000, 122 | isClosable: true, 123 | }) 124 | } 125 | 126 | const renderMessages = (messages: any) => { 127 | if (messages.length == 1) { 128 | return ( 129 | 130 | {getTypeMsg(messages[0].typeUrl)} 131 | 132 | ) 133 | } else if (messages.length > 1) { 134 | return ( 135 | 136 | {getTypeMsg(messages[0].typeUrl)} 137 | +{messages.length - 1} 138 | 139 | ) 140 | } 141 | 142 | return '' 143 | } 144 | 145 | return ( 146 | <> 147 | 148 | Detail Account | Dexplorer 149 | 150 | 151 | 152 | 153 |
154 | 155 | Account 156 | 157 | 165 | 170 | 171 | 172 | Accounts 173 | 174 | Detail 175 | 176 | 183 | 184 | Profile 185 | 186 | 187 | 188 | 189 | 190 | 191 | 194 | 195 | 196 | 197 | 200 | 216 | 217 | 218 | 221 | 222 | 223 | 224 | 227 | 228 | 229 | 230 |
192 | Address 193 | {address}
198 | Pub Key 199 | 201 | 202 | 203 | @Type 204 | Key 205 | 206 | 207 | 208 |

{account?.pubkey?.type}

209 |
210 | 211 |

{account?.pubkey?.value}

212 |
213 |
214 |
215 |
219 | Account Number 220 | {account?.accountNumber}
225 | Sequence 226 | {account?.sequence}
231 |
232 |
233 | 234 | 241 | 242 | Balances 243 | 244 | 245 | 246 | 247 | Available 248 | Delegated 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | {allBalances.map((item, index) => ( 262 | 263 | 264 | 265 | 266 | ))} 267 | 268 |
DenomAmount
{item.denom}{item.amount}
269 |
270 |
271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 |
DenomAmount
{balanceStaked?.denom}{balanceStaked?.amount}
287 |
288 |
289 |
290 |
291 |
292 | 293 | 300 | 301 | Transactions 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | {txs.map((tx) => ( 316 | 317 | 331 | 332 | 333 | 347 | 348 | ))} 349 | 350 |
Tx HashMessagesMemoHeight
318 | 324 | 327 | {trimHash(tx.hash)} 328 | 329 | 330 | {renderMessages(tx.data.messages)}{tx.data.memo} 334 | 340 | 343 | {tx.height} 344 | 345 | 346 |
351 |
352 |
353 |
354 | 355 | ) 356 | } 357 | -------------------------------------------------------------------------------- /src/components/Navbar/index.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, FormEvent, useEffect, useState } from 'react' 2 | import { useRouter } from 'next/router' 3 | import { useSelector } from 'react-redux' 4 | import { selectTmClient, selectRPCAddress } from '@/store/connectSlice' 5 | import { 6 | Box, 7 | Heading, 8 | Text, 9 | HStack, 10 | Icon, 11 | IconButton, 12 | Input, 13 | Skeleton, 14 | useColorMode, 15 | Button, 16 | useColorModeValue, 17 | useDisclosure, 18 | useToast, 19 | Modal, 20 | ModalOverlay, 21 | ModalContent, 22 | ModalHeader, 23 | ModalCloseButton, 24 | ModalBody, 25 | ModalFooter, 26 | Flex, 27 | Stack, 28 | FormControl, 29 | } from '@chakra-ui/react' 30 | import { 31 | FiRadio, 32 | FiSearch, 33 | FiRefreshCcw, 34 | FiZap, 35 | FiTrash2, 36 | } from 'react-icons/fi' 37 | import { selectNewBlock } from '@/store/streamSlice' 38 | import { CheckIcon, MoonIcon, SunIcon } from '@chakra-ui/icons' 39 | import { StatusResponse } from '@cosmjs/tendermint-rpc' 40 | import { connectWebsocketClient, validateConnection } from '@/rpc/client' 41 | import { LS_RPC_ADDRESS, LS_RPC_ADDRESS_LIST } from '@/utils/constant' 42 | import { removeTrailingSlash } from '@/utils/helper' 43 | 44 | const heightRegex = /^\d+$/ 45 | const txhashRegex = /^[A-Z\d]{64}$/ 46 | const addrRegex = /^[a-z\d]+1[a-z\d]{38,58}$/ 47 | 48 | export default function Navbar() { 49 | const router = useRouter() 50 | const tmClient = useSelector(selectTmClient) 51 | const address = useSelector(selectRPCAddress) 52 | const newBlock = useSelector(selectNewBlock) 53 | const toast = useToast() 54 | const [status, setStatus] = useState() 55 | 56 | const [state, setState] = useState<'initial' | 'submitting' | 'success'>( 57 | 'initial' 58 | ) 59 | const [newAddress, setNewAddress] = useState('') 60 | const [error, setError] = useState(false) 61 | 62 | const { colorMode, toggleColorMode } = useColorMode() 63 | const { isOpen, onOpen, onClose } = useDisclosure() 64 | const { 65 | isOpen: isOpenRPCs, 66 | onOpen: onOpenRPCs, 67 | onClose: onCloseRPCs, 68 | } = useDisclosure() 69 | 70 | const [inputSearch, setInputSearch] = useState('') 71 | const [isLoadedSkeleton, setIsLoadedSkeleton] = useState(false) 72 | const [rpcList, setRPCList] = useState([]) 73 | 74 | useEffect(() => { 75 | if (tmClient) { 76 | tmClient.status().then((response) => setStatus(response)) 77 | } 78 | }, [tmClient]) 79 | 80 | useEffect(() => { 81 | if (newBlock || status) { 82 | setIsLoadedSkeleton(true) 83 | } 84 | }, [tmClient, newBlock, status]) 85 | 86 | const handleInputSearch = (event: any) => { 87 | setInputSearch(event.target.value as string) 88 | } 89 | 90 | const handleSearch = () => { 91 | if (!inputSearch) { 92 | toast({ 93 | title: 'Please enter a value!', 94 | status: 'warning', 95 | isClosable: true, 96 | }) 97 | return 98 | } 99 | 100 | if (heightRegex.test(inputSearch)) { 101 | router.push('/blocks/' + inputSearch) 102 | } else if (txhashRegex.test(inputSearch)) { 103 | router.push('/txs/' + inputSearch) 104 | } else if (addrRegex.test(inputSearch)) { 105 | router.push('/accounts/' + inputSearch) 106 | } else { 107 | toast({ 108 | title: 'Invalid Height, Transaction or Account Address!', 109 | status: 'error', 110 | isClosable: true, 111 | }) 112 | return 113 | } 114 | setTimeout(() => { 115 | onClose() 116 | }, 500) 117 | } 118 | 119 | const submitForm = async (e: FormEvent) => { 120 | e.preventDefault() 121 | const rpcAddresses = getRPCList() 122 | const addr = removeTrailingSlash(newAddress) 123 | if (rpcAddresses.includes(addr)) { 124 | toast({ 125 | title: 'This RPC Address is already in the list!', 126 | status: 'warning', 127 | isClosable: true, 128 | }) 129 | return 130 | } 131 | await connectClient(addr) 132 | window.localStorage.setItem( 133 | LS_RPC_ADDRESS_LIST, 134 | JSON.stringify([addr, ...rpcAddresses]) 135 | ) 136 | setRPCList(getRPCList()) 137 | } 138 | 139 | const connectClient = async (rpcAddress: string) => { 140 | try { 141 | setError(false) 142 | setState('submitting') 143 | 144 | if (!rpcAddress) { 145 | setError(true) 146 | setState('initial') 147 | return 148 | } 149 | 150 | const isValid = await validateConnection(rpcAddress) 151 | if (!isValid) { 152 | setError(true) 153 | setState('initial') 154 | return 155 | } 156 | 157 | const tc = await connectWebsocketClient(rpcAddress) 158 | 159 | if (!tc) { 160 | setError(true) 161 | setState('initial') 162 | return 163 | } 164 | 165 | window.localStorage.setItem(LS_RPC_ADDRESS, rpcAddress) 166 | window.location.reload() 167 | setState('success') 168 | } catch (err) { 169 | console.error(err) 170 | setError(true) 171 | setState('initial') 172 | return 173 | } 174 | } 175 | 176 | const getRPCList = () => { 177 | const rpcAddresses = JSON.parse( 178 | window.localStorage.getItem(LS_RPC_ADDRESS_LIST) || '[]' 179 | ) 180 | return rpcAddresses 181 | } 182 | 183 | const onChangeRPC = () => { 184 | setRPCList(getRPCList()) 185 | setState('initial') 186 | setNewAddress('') 187 | setError(false) 188 | onOpenRPCs() 189 | } 190 | 191 | const selectChain = (rpcAddress: string) => { 192 | connectClient(rpcAddress) 193 | } 194 | 195 | const removeChain = (rpcAddress: string) => { 196 | const rpcList = getRPCList() 197 | const updatedList = rpcList.filter((rpc: string) => rpc !== rpcAddress) 198 | window.localStorage.setItem( 199 | LS_RPC_ADDRESS_LIST, 200 | JSON.stringify(updatedList) 201 | ) 202 | setRPCList(getRPCList()) 203 | } 204 | 205 | return ( 206 | <> 207 | 217 | 218 | 219 | 227 | 228 | 229 | 230 | {newBlock?.header.chainId 231 | ? newBlock?.header.chainId 232 | : status?.nodeInfo.network} 233 | 234 | 235 | 236 | {address} 237 | 238 | 239 | } 245 | onClick={onChangeRPC} 246 | /> 247 | 248 | 249 | 250 | } 256 | onClick={onOpen} 257 | /> 258 | : } 264 | onClick={toggleColorMode} 265 | /> 266 | 267 | 268 | 269 | 270 | 271 | Search 272 | 273 | 274 | 281 | 282 | 283 | 284 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | Change Connection 304 | 305 | 306 | 312 | 313 | ) => 329 | setNewAddress(e.target.value) 330 | } 331 | /> 332 | 333 | 334 | 352 | 353 | 354 | 355 | {error ? 'Oh no, cannot connect to websocket client! 😢' : '‎'} 356 | 357 | 358 | Available RPCs 359 | 360 | 361 | {rpcList.map((rpc) => ( 362 | 372 | 373 | 374 | {rpc} 375 | 376 | 377 | {rpc !== address ? ( 378 | 379 | selectChain(rpc)} 381 | backgroundColor={useColorModeValue( 382 | 'light-theme', 383 | 'dark-theme' 384 | )} 385 | color={'white'} 386 | _hover={{ 387 | backgroundColor: useColorModeValue( 388 | 'dark-theme', 389 | 'light-theme' 390 | ), 391 | }} 392 | aria-label="Connect RPC" 393 | size="sm" 394 | fontSize="20" 395 | icon={} 396 | /> 397 | removeChain(rpc)} 399 | backgroundColor={useColorModeValue( 400 | 'red.500', 401 | 'red.400' 402 | )} 403 | color={'white'} 404 | _hover={{ 405 | backgroundColor: useColorModeValue( 406 | 'red.400', 407 | 'red.500' 408 | ), 409 | }} 410 | aria-label="Remove RPC" 411 | size="sm" 412 | fontSize="20" 413 | icon={} 414 | /> 415 | 416 | ) : ( 417 | 418 | Connected 419 | 420 | )} 421 | 422 | ))} 423 | 424 | 425 | 426 | 427 | 428 | ) 429 | } 430 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | --------------------------------------------------------------------------------