├── .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 |
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 | [](https://github.com/arifintahu/dexplorer/blob/main/LICENSE)
14 | [](https://github.com/arifintahu/dexplorer/deployments/activity_log)
15 | [](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 |
58 |
59 |
63 |
68 |
69 |
70 | Distribution Parameters
71 |
72 |
73 |
74 |
75 |
76 |
77 | Base Proposer Reward
78 |
79 |
80 | {convertRateToPercent(params?.baseProposerReward)}
81 |
82 |
83 |
84 |
85 |
86 |
87 | Bonus Proposer Reward
88 |
89 |
90 | {convertRateToPercent(params?.bonusProposerReward)}
91 |
92 |
93 |
94 |
95 |
96 |
97 | Community Tax
98 |
99 |
100 | {convertRateToPercent(params?.communityTax)}
101 |
102 |
103 |
104 |
105 |
106 |
107 | Withdraw Addr Enabled
108 |
109 |
110 | {params?.withdrawAddrEnabled ? 'True' : 'False'}
111 |
112 |
113 |
114 |
115 |
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 |
55 |
56 |
60 |
65 |
66 |
67 | Staking Parameters
68 |
69 |
70 |
71 |
72 |
73 |
74 | Unbonding Time
75 |
76 |
77 | {displayDurationSeconds(Number(params?.unbondingTime?.seconds))}
78 |
79 |
80 |
81 |
82 |
83 |
84 | Max Validators
85 |
86 |
87 | {params?.maxValidators ?? ''}
88 |
89 |
90 |
91 |
92 |
93 |
94 | Max Entries
95 |
96 |
97 | {params?.maxEntries ?? ''}
98 |
99 |
100 |
101 |
102 |
103 |
104 | Historical Entries
105 |
106 |
107 | {params?.historicalEntries.toLocaleString() ?? ''}
108 |
109 |
110 |
111 |
112 |
113 |
114 | Bond Denom
115 |
116 |
117 | {params?.bondDenom ?? ''}
118 |
119 |
120 |
121 |
122 |
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 |
55 |
56 |
60 |
65 |
66 |
67 | Minting Parameters
68 |
69 |
70 |
71 |
72 |
73 |
74 | Blocks per Year
75 |
76 |
77 | {params?.blocksPerYear ? Number(params?.blocksPerYear) : ''}
78 |
79 |
80 |
81 |
82 |
83 |
84 | Goal Bonded
85 |
86 |
87 | {convertRateToPercent(params?.goalBonded)}
88 |
89 |
90 |
91 |
92 |
93 |
94 | Inflation Max
95 |
96 |
97 | {convertRateToPercent(params?.inflationMax)}
98 |
99 |
100 |
101 |
102 |
103 |
104 | Inflation Min
105 |
106 |
107 | {convertRateToPercent(params?.inflationMin)}
108 |
109 |
110 |
111 |
112 |
113 |
114 | Inflation Rate Change
115 |
116 |
117 | {convertRateToPercent(params?.inflationRateChange)}
118 |
119 |
120 |
121 |
122 |
123 |
124 | Mint Denom
125 |
126 |
127 | {params?.mintDenom ?? ''}
128 |
129 |
130 |
131 |
132 |
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 |
56 |
57 |
61 |
66 |
67 |
68 | Slashing Parameters
69 |
70 |
71 |
72 |
73 |
74 |
75 | Signed Blocks Window
76 |
77 |
78 | {params?.signedBlocksWindow
79 | ? Number(params?.signedBlocksWindow)
80 | : ''}
81 |
82 |
83 |
84 |
85 |
86 |
87 | Min Signed Per Window
88 |
89 |
90 | {convertRateToPercent(
91 | fromUtf8(params?.minSignedPerWindow ?? new Uint8Array())
92 | )}
93 |
94 |
95 |
96 |
97 |
98 |
99 | Downtime Jail Duration
100 |
101 |
102 | {displayDurationSeconds(
103 | Number(params?.downtimeJailDuration?.seconds)
104 | )}
105 |
106 |
107 |
108 |
109 |
110 |
111 | Slash Fraction Doublesign
112 |
113 |
114 | {convertRateToPercent(
115 | fromUtf8(params?.slashFractionDoubleSign ?? new Uint8Array())
116 | )}
117 |
118 |
119 |
120 |
121 |
122 |
123 | Slash Fraction Downtime
124 |
125 |
126 | {convertRateToPercent(
127 | fromUtf8(params?.slashFractionDowntime ?? new Uint8Array())
128 | )}
129 |
130 |
131 |
132 |
133 |
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 |
86 |
87 |
91 |
96 |
97 |
98 | Governance Parameters
99 |
100 |
101 |
102 |
103 |
104 |
105 | Min Deposit
106 |
107 |
108 | {depositParams?.minDeposit.length
109 | ? displayCoin(depositParams?.minDeposit[0])
110 | : ''}
111 |
112 |
113 |
114 |
115 |
116 |
117 | Max Deposit Period
118 |
119 |
120 | {displayDurationSeconds(
121 | Number(depositParams?.maxDepositPeriod?.seconds)
122 | )}
123 |
124 |
125 |
126 |
127 |
128 |
129 | Voting Period
130 |
131 |
132 | {displayDurationSeconds(
133 | Number(votingParams?.votingPeriod?.seconds)
134 | )}
135 |
136 |
137 |
138 |
139 |
140 |
141 | Quorum
142 |
143 |
144 | {convertRateToPercent(tallyParams?.quorum)}
145 |
146 |
147 |
148 |
149 |
150 |
151 | Threshold
152 |
153 |
154 | {convertRateToPercent(tallyParams?.threshold)}
155 |
156 |
157 |
158 |
159 |
160 |
161 | Veto Threshold
162 |
163 |
164 | {convertRateToPercent(tallyParams?.vetoThreshold)}
165 |
166 |
167 |
168 |
169 |
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 |
178 | {state === 'success' ? : 'Connect'}
179 |
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 |
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 |
123 | )
124 | })}
125 |
126 | ))}
127 |
128 | {isLoading ? (
129 |
130 | {table.getRowModel().rows.map((row) => (
131 |
132 | {row.getVisibleCells().map((cell) => {
133 | return (
134 |
135 |
136 |
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 |
150 | {flexRender(
151 | cell.column.columnDef.cell,
152 | cell.getContext()
153 | )}
154 |
155 | )
156 | })}
157 |
158 | ))}
159 |
160 | )}
161 |
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 | {
217 | table.setPageSize(Number(e.target.value))
218 | }}
219 | >
220 | {[10, 20, 30, 40, 50].map((pageSize) => (
221 |
222 | Show {pageSize}
223 |
224 | ))}
225 |
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 | }
168 | colorScheme="red"
169 | variant="outline"
170 | onClick={handleDisconnect}
171 | >
172 | Disconnect All
173 |
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 |
179 | Chain Id
180 |
181 | {block?.header.chainId}
182 |
183 |
184 |
185 | Height
186 |
187 | {block?.header.height}
188 |
189 |
190 |
191 | Block Time
192 |
193 |
194 | {block?.header.time
195 | ? `${timeFromNow(block?.header.time)} ( ${displayDate(
196 | block?.header.time
197 | )} )`
198 | : ''}
199 |
200 |
201 |
202 |
203 | Block Hash
204 |
205 | {block?.id}
206 |
207 |
208 |
209 | Number of Tx
210 |
211 | {block?.txs.length}
212 |
213 |
214 |
215 |
216 |
217 |
218 |
225 |
226 | Transactions
227 |
228 |
229 |
230 |
231 |
232 |
233 | Tx Hash
234 | Messages
235 | Fee
236 | Height
237 | Time
238 |
239 |
240 |
241 | {txs.map((tx) => (
242 |
243 |
244 |
250 | {trimHash(tx.hash)}
251 |
252 |
253 | {renderMessages(tx.data.body?.messages)}
254 | {getFee(tx.data.authInfo?.fee?.amount)}
255 | {height}
256 |
257 | {block?.header.time
258 | ? timeFromNow(block?.header.time)
259 | : ''}
260 |
261 |
262 | ))}
263 |
264 |
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 | Height
172 | App Hash
173 | Txs
174 | Time
175 |
176 |
177 |
178 | {blocks.map((block) => (
179 |
180 |
181 |
187 |
188 | {block.header.height}
189 |
190 |
191 |
192 | {toHex(block.header.appHash)}
193 | {block.txs.length}
194 |
195 | {timeFromNow(block.header.time.toISOString())}
196 |
197 |
198 | ))}
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 | Tx Hash
209 | Result
210 | Messages
211 | Height
212 | Time
213 |
214 |
215 |
216 | {txs.map((tx) => (
217 |
218 |
219 |
227 |
228 | {trimHash(tx.TxEvent.hash)}
229 |
230 |
231 |
232 |
233 | {tx.TxEvent.result.code == 0 ? (
234 |
235 |
236 | Success
237 |
238 | ) : (
239 |
240 |
241 | Error
242 |
243 | )}
244 |
245 | {renderMessages(tx.TxEvent.result.data)}
246 | {tx.TxEvent.height}
247 | {timeFromNow(tx.Timestamp.toISOString())}
248 |
249 | ))}
250 |
251 |
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 |
195 | Chain Id
196 |
197 | {block?.header.chainId}
198 |
199 |
200 |
201 | Tx Hash
202 |
203 | {tx?.hash}
204 |
205 |
206 |
207 | Status
208 |
209 |
210 | {tx?.code == 0 ? (
211 |
212 |
213 | Success
214 |
215 | ) : (
216 |
217 |
218 | Error
219 |
220 | )}
221 |
222 |
223 |
224 |
225 | Height
226 |
227 |
228 |
234 | {tx?.height}
235 |
236 |
237 |
238 |
239 |
240 | Time
241 |
242 |
243 | {block?.header.time
244 | ? `${timeFromNow(block?.header.time)} ( ${displayDate(
245 | block?.header.time
246 | )} )`
247 | : ''}
248 |
249 |
250 |
251 |
252 | Fee
253 |
254 | {getFee(txData?.authInfo?.fee?.amount)}
255 |
256 |
257 |
258 | Gas (used / wanted)
259 |
260 |
261 | {tx?.gasUsed ? `${tx.gasUsed} / ${tx.gasWanted}` : ''}
262 |
263 |
264 |
265 |
266 | Memo
267 |
268 | {txData?.body?.memo}
269 |
270 |
271 |
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 |
298 | typeUrl
299 |
300 | {msg.typeUrl}
301 |
302 | {Object.keys(msg.data ?? {}).map((key) => (
303 |
304 |
305 | {key}
306 |
307 |
308 | {showMsgData(
309 | msg.data ? msg.data[key as keyof {}] : ''
310 | )}
311 |
312 |
313 | ))}
314 |
315 |
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 |
192 | Address
193 |
194 | {address}
195 |
196 |
197 |
198 | Pub Key
199 |
200 |
201 |
202 |
203 | @Type
204 | Key
205 |
206 |
207 |
208 | {account?.pubkey?.type}
209 |
210 |
211 | {account?.pubkey?.value}
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 | Account Number
220 |
221 | {account?.accountNumber}
222 |
223 |
224 |
225 | Sequence
226 |
227 | {account?.sequence}
228 |
229 |
230 |
231 |
232 |
233 |
234 |
241 |
242 | Balances
243 |
244 |
245 |
246 |
247 | Available
248 | Delegated
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 | Denom
257 | Amount
258 |
259 |
260 |
261 | {allBalances.map((item, index) => (
262 |
263 | {item.denom}
264 | {item.amount}
265 |
266 | ))}
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 | Denom
277 | Amount
278 |
279 |
280 |
281 |
282 | {balanceStaked?.denom}
283 | {balanceStaked?.amount}
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
300 |
301 | Transactions
302 |
303 |
304 |
305 |
306 |
307 |
308 | Tx Hash
309 | Messages
310 | Memo
311 | Height
312 |
313 |
314 |
315 | {txs.map((tx) => (
316 |
317 |
318 |
324 |
327 | {trimHash(tx.hash)}
328 |
329 |
330 |
331 | {renderMessages(tx.data.messages)}
332 | {tx.data.memo}
333 |
334 |
340 |
343 | {tx.height}
344 |
345 |
346 |
347 |
348 | ))}
349 |
350 |
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 |
294 | Confirm
295 |
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 |
350 | {state === 'success' ? : 'Connect'}
351 |
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 |
--------------------------------------------------------------------------------