├── .eslintrc.json ├── public ├── favicon.ico ├── regionx-logo.png ├── kusama-logo.svg └── polkadot-logo.svg ├── app ├── twitter-image.png ├── opengraph-image.png ├── page.tsx ├── history │ └── page.tsx ├── subscribe │ ├── post-tx.ts │ ├── subscribe-tx.ts │ └── page.tsx └── layout.tsx ├── postcss.config.js ├── types └── nav.ts ├── next.config.mjs ├── components ├── ui │ ├── skeleton.tsx │ ├── input.tsx │ ├── download-json.button.tsx │ ├── sonner.tsx │ ├── checkbox.tsx │ ├── tooltip.tsx │ ├── button.tsx │ ├── dialog.tsx │ ├── alert-dialog.tsx │ ├── select.tsx │ └── dropdown-menu.tsx ├── theme-provider.tsx ├── footer.tsx ├── tailwind-indicator.tsx ├── theme-toggle.tsx ├── tooltip-title.tsx ├── network-select.tsx ├── site-header.tsx ├── main-nav.tsx ├── providers.tsx ├── registered-chains.tsx ├── chain-select.tsx ├── Chart.tsx ├── connect-button.tsx ├── consumption-chart.tsx ├── consumption-grid.tsx ├── icons.tsx └── historic-consumption.tsx ├── components.json ├── lib ├── fonts.ts └── utils.ts ├── .gitignore ├── README.md ├── hooks ├── use-registered-chains.ts └── use-consumption.ts ├── tsconfig.json ├── common ├── types.ts └── chaindata.ts ├── prettier.config.js ├── config └── site.ts ├── providers └── chain-provider.tsx ├── package.json ├── styles └── globals.css └── tailwind.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoredApe8461/CorespaceWeigher-ui/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /app/twitter-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoredApe8461/CorespaceWeigher-ui/HEAD/app/twitter-image.png -------------------------------------------------------------------------------- /app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoredApe8461/CorespaceWeigher-ui/HEAD/app/opengraph-image.png -------------------------------------------------------------------------------- /public/regionx-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoredApe8461/CorespaceWeigher-ui/HEAD/public/regionx-logo.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /types/nav.ts: -------------------------------------------------------------------------------- 1 | export interface NavItem { 2 | title: string 3 | href?: string 4 | disabled?: boolean 5 | external?: boolean 6 | } 7 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | export default nextConfig 7 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "tailwind": { 5 | "config": "tailwind.config.js", 6 | "css": "app/globals.css", 7 | "baseColor": "slate", 8 | "cssVariables": true 9 | }, 10 | "rsc": false, 11 | "aliases": { 12 | "utils": "@/lib/utils", 13 | "components": "@/components" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ThemeProvider as NextThemesProvider } from "next-themes" 5 | import { type ThemeProviderProps } from "next-themes/dist/types" 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children} 9 | } 10 | -------------------------------------------------------------------------------- /lib/fonts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JetBrains_Mono as FontMono, 3 | Inter as FontSans, 4 | Inter, 5 | } from "next/font/google" 6 | 7 | export const fontSans = FontSans({ 8 | subsets: ["latin"], 9 | variable: "--font-sans", 10 | }) 11 | 12 | export const fontMono = FontMono({ 13 | subsets: ["latin"], 14 | variable: "--font-mono", 15 | }) 16 | 17 | export const inter = Inter({ subsets: ["latin"] }) 18 | -------------------------------------------------------------------------------- /components/footer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import Link from "next/link" 3 | 4 | import Image from "next/image" 5 | 6 | export function Footer() { 7 | return ( 8 |
9 | 10 |

RegionX

11 | RegionX 12 | 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /.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 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | 8 | export function uppercaseFirstLetter(str: string) { 9 | return str.charAt(0).toUpperCase() + str.slice(1) 10 | } 11 | 12 | export const truncateHash = ( 13 | hash: string | undefined, 14 | paddingLength = 6 15 | ): string | undefined => { 16 | if (!hash?.length) return undefined 17 | if (hash.length <= paddingLength * 2 + 1) return hash 18 | return hash.replace( 19 | hash.substring(paddingLength, hash.length - paddingLength), 20 | "…" 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /components/tailwind-indicator.tsx: -------------------------------------------------------------------------------- 1 | export function TailwindIndicator() { 2 | if (process.env.NODE_ENV === "production") return null 3 | 4 | return ( 5 |
6 |
xs
7 |
sm
8 |
md
9 |
lg
10 |
xl
11 |
2xl
12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Polkadot utilization tracker 2 | 3 | Keeps track of blockspace utilization of each Polkadot parachain. 4 | 5 | ## Usage 6 | 7 | ```bash 8 | # Install dependencies 9 | npm install 10 | 11 | # Run dev server 12 | npm run dev 13 | 14 | # Build for prod and export static website 15 | npm run build 16 | ``` 17 | 18 | ## Features 19 | 20 | - useInkathon 21 | - Next.js 13 App Directory 22 | - Radix UI Primitives 23 | - Tailwind CSS 24 | - Icons from [Lucide](https://lucide.dev) 25 | - Dark mode with `next-themes` 26 | - Tailwind CSS class sorting, merging and linting. 27 | 28 | ## License 29 | 30 | Licensed under the [MIT license](https://github.com/shadcn/ui/blob/main/LICENSE.md). 31 | -------------------------------------------------------------------------------- /components/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { Moon, Sun } from "lucide-react" 5 | import { useTheme } from "next-themes" 6 | 7 | import { Button } from "@/components/ui/button" 8 | 9 | export function ThemeToggle() { 10 | const { setTheme, theme } = useTheme() 11 | 12 | return ( 13 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /hooks/use-registered-chains.ts: -------------------------------------------------------------------------------- 1 | import { Network } from "@/common/types" 2 | import { useQuery } from "react-query" 3 | 4 | import { siteConfig } from "@/config/site" 5 | 6 | export type TypeRegisteredChainQuery = { 7 | name: string 8 | rpcs: string[] 9 | para_id: number 10 | relay_chain: Network 11 | expiry_timestamp: Date 12 | }[] 13 | 14 | export function useRegisteredChains() { 15 | const endpoint = `${siteConfig.backendUrl}/registry` 16 | 17 | return useQuery({ 18 | queryKey: "registeredChains", 19 | queryFn: async () => { 20 | const res = await fetch(endpoint) 21 | if (!res.ok) { 22 | throw new Error("Network response was not ok") 23 | } 24 | return res.json() 25 | }, 26 | retry: 1, 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "noEmit": true, 9 | "incremental": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "baseUrl": ".", 17 | "paths": { 18 | "@/*": ["./*"] 19 | }, 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ], 25 | "strictNullChecks": true 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /common/types.ts: -------------------------------------------------------------------------------- 1 | export type ConsumptionDatum = { 2 | group: string 3 | ref_time: { 4 | normal: number 5 | operational: number 6 | mandatory: number 7 | } 8 | proof_size: { 9 | normal: number 10 | operational: number 11 | mandatory: number 12 | } 13 | count: number 14 | } 15 | 16 | export type ConsumptionDataSeries = { 17 | label: string 18 | data: ConsumptionDatum[] 19 | } 20 | 21 | export type Consumption = { 22 | normal: number 23 | operational: number 24 | mandatory: number 25 | total: number 26 | } 27 | 28 | export type DataDisplay = "ref_time" | "proof_size" | "both" 29 | 30 | export type Network = "polkadot" | "kusama" 31 | export type DateRange = "hour" | "day" | "week" | "month" | "year" | "all" 32 | export type Grouping = "day" | "minute" | "week" | "month" | "year" 33 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /components/ui/download-json.button.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "./button" 2 | 3 | const DownloadJSONButton = ({ 4 | jsonData, 5 | fileName, 6 | children, 7 | className, 8 | ...props 9 | }: { 10 | jsonData: any 11 | fileName: string 12 | className: string 13 | children: React.ReactNode 14 | props?: any 15 | }) => { 16 | const downloadJson = () => { 17 | const jsonBlob = new Blob([JSON.stringify(jsonData)], { 18 | type: "application/json", 19 | }) 20 | const url = window.URL.createObjectURL(jsonBlob) 21 | const link = document.createElement("a") 22 | link.href = url 23 | link.setAttribute("download", fileName) 24 | document.body.appendChild(link) 25 | link.click() 26 | document.body.removeChild(link) 27 | } 28 | 29 | return ( 30 | 33 | ) 34 | } 35 | 36 | export default DownloadJSONButton 37 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config} */ 2 | module.exports = { 3 | endOfLine: "lf", 4 | semi: false, 5 | singleQuote: false, 6 | tabWidth: 2, 7 | trailingComma: "es5", 8 | importOrder: [ 9 | "^(react/(.*)$)|^(react$)", 10 | "^(next/(.*)$)|^(next$)", 11 | "", 12 | "", 13 | "^types$", 14 | "^@/types/(.*)$", 15 | "^@/config/(.*)$", 16 | "^@/lib/(.*)$", 17 | "^@/hooks/(.*)$", 18 | "^@/components/ui/(.*)$", 19 | "^@/components/(.*)$", 20 | "^@/styles/(.*)$", 21 | "^@/app/(.*)$", 22 | "", 23 | "^[./]", 24 | ], 25 | importOrderSeparation: false, 26 | importOrderSortSpecifiers: true, 27 | importOrderBuiltinModulesToTop: true, 28 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"], 29 | importOrderMergeDuplicateImports: true, 30 | importOrderCombineTypeAndValueImports: true, 31 | plugins: ["@ianvs/prettier-plugin-sort-imports"], 32 | } 33 | -------------------------------------------------------------------------------- /components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from "next-themes" 2 | import { Toaster as Sonner } from "sonner" 3 | 4 | type ToasterProps = React.ComponentProps 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme() 8 | 9 | return ( 10 | 26 | ) 27 | } 28 | 29 | export { Toaster } 30 | -------------------------------------------------------------------------------- /components/tooltip-title.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link" 4 | 5 | import { 6 | Tooltip, 7 | TooltipContent, 8 | TooltipProvider, 9 | TooltipTrigger, 10 | } from "./ui/tooltip" 11 | 12 | export function TooltipTitle() { 13 | return ( 14 | <> 15 | See all the historic consumption data for{" "} 16 | 17 | 18 | 19 | registered 20 | 21 | 22 |
23 | Our service provides a way to see the historic consumption data 24 | for all the registered chains.{" "} 25 | 26 | ↪ Register a chain here. 27 | 28 |
29 |
30 |
31 |
{" "} 32 | parachains 33 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /config/site.ts: -------------------------------------------------------------------------------- 1 | import { DateRange } from "@/common/types" 2 | 3 | export type SiteConfig = typeof siteConfig 4 | 5 | export const siteConfig = { 6 | name: "Polkadot Weigher", 7 | description: "Displays utilization of Polkadot parachains.", 8 | backendUrl: "https://api.polkadot-weigher.com", 9 | blockExplorer: "https://polkadot.subscan.io//extrinsic/", 10 | subscriptionCost: "100000000000", // cost to register a parachain 11 | 12 | mainNav: [ 13 | { 14 | title: "Historic Consumption", 15 | href: "/history", 16 | }, 17 | { 18 | title: "Parachain Registration", 19 | href: "/subscribe", 20 | }, 21 | ], 22 | links: { 23 | github: "https://github.com/RegionX-Labs/CorespaceWeigher-ui", 24 | }, 25 | defaultDateRange: "week" as DateRange, 26 | rangeGroupingMap: { 27 | hour: "minute", 28 | day: "hour", 29 | week: "hour", 30 | month: "day", 31 | year: "month", 32 | all: "month", 33 | } as Record, 34 | } 35 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import ChainSelect from "@/components/chain-select" 2 | import ConsumptionGrid from "@/components/consumption-grid" 3 | import NetworkSelect from "@/components/network-select" 4 | 5 | export default function IndexPage() { 6 | return ( 7 |
8 |
9 |
10 |

11 | Current Block Consumption 12 |

13 |

14 | Monitor the real-time block space utilization of all Polkadot and Kusama parachains. 15 |

16 |
17 |
18 | 19 | 20 |
21 | 22 |
23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /app/history/page.tsx: -------------------------------------------------------------------------------- 1 | import ChainSelect from "@/components/chain-select" 2 | import { HistoricConsumption } from "@/components/historic-consumption" 3 | import NetworkSelect from "@/components/network-select" 4 | import { TooltipTitle } from "@/components/tooltip-title" 5 | 6 | export default async function HistoryPage() { 7 | return ( 8 |
9 |
10 |
11 |

12 | Historic Consumption 13 |

14 |

15 | 16 |

17 |
18 |
19 | 20 | 21 |
22 | 23 |
24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /public/kusama-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 3 | import { Check } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Checkbox = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | 22 | 23 | 24 | 25 | )) 26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 27 | 28 | export { Checkbox } 29 | -------------------------------------------------------------------------------- /components/network-select.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Network } from "@/common/types" 4 | import { useChain } from "@/providers/chain-provider" 5 | 6 | import { 7 | Select, 8 | SelectContent, 9 | SelectItem, 10 | SelectTrigger, 11 | SelectValue, 12 | } from "@/components/ui/select" 13 | 14 | import { Icons } from "./icons" 15 | 16 | const NetworkSelect = () => { 17 | const { network, setNetwork } = useChain() 18 | 19 | return ( 20 | 39 | ) 40 | } 41 | 42 | export default NetworkSelect 43 | -------------------------------------------------------------------------------- /components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const TooltipProvider = TooltipPrimitive.Provider 7 | 8 | const Tooltip = TooltipPrimitive.Root 9 | 10 | const TooltipTrigger = TooltipPrimitive.Trigger 11 | 12 | const TooltipContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, sideOffset = 4, ...props }, ref) => ( 16 | 25 | )) 26 | TooltipContent.displayName = TooltipPrimitive.Content.displayName 27 | 28 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 29 | -------------------------------------------------------------------------------- /public/polkadot-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /components/site-header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | import { siteConfig } from "@/config/site" 4 | import { buttonVariants } from "@/components/ui/button" 5 | import { Icons } from "@/components/icons" 6 | import { MainNav } from "@/components/main-nav" 7 | import { ThemeToggle } from "@/components/theme-toggle" 8 | 9 | export function SiteHeader() { 10 | return ( 11 |
12 |
13 | 14 |
15 | 33 |
34 |
35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /components/main-nav.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import Link from "next/link" 3 | import { LucidePercent, LucideScale, LucideWheat } from "lucide-react" 4 | 5 | import { NavItem } from "@/types/nav" 6 | import { siteConfig } from "@/config/site" 7 | import { cn } from "@/lib/utils" 8 | import { Icons } from "@/components/icons" 9 | 10 | interface MainNavProps { 11 | items?: NavItem[] 12 | } 13 | 14 | export function MainNav({ items }: MainNavProps) { 15 | return ( 16 |
17 | 18 | 19 | {siteConfig.name} 20 | 21 | {items?.length ? ( 22 | 39 | ) : null} 40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /components/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ChainProvider } from "@/providers/chain-provider" 4 | import { TooltipProvider } from "@radix-ui/react-tooltip" 5 | import { 6 | SubstrateChain, 7 | UseInkathonProvider, 8 | development, 9 | } from "@scio-labs/use-inkathon"; 10 | import { QueryClient, QueryClientProvider } from "react-query" 11 | 12 | import { ThemeProvider } from "./theme-provider" 13 | 14 | const polkadotRelay: SubstrateChain = { 15 | network: "Polkadot", 16 | name: "Polkadot Relay Chain", 17 | rpcUrls: ["wss://rpc.polkadot.io"], 18 | ss58Prefix: 0, 19 | testnet: false, 20 | } 21 | 22 | const rococoTestnet: SubstrateChain = { 23 | network: "Rococo", 24 | name: "Rococo Testnet", 25 | rpcUrls: ["wss://rococo-rpc.polkadot.io"], 26 | ss58Prefix: 42, 27 | testnet: true, 28 | } 29 | 30 | export function Providers({ children }: { children: React.ReactNode }) { 31 | const queryClient = new QueryClient() 32 | 33 | return ( 34 | 35 | 36 | 37 | 42 | {children} 43 | 44 | 45 | 46 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /common/chaindata.ts: -------------------------------------------------------------------------------- 1 | import { gql, request } from "graphql-request" 2 | 3 | const graphqlUrl = "https://squid.subsquid.io/chaindata/v/v4/graphql" 4 | 5 | export type Chain = { 6 | id: string 7 | name: string 8 | paraId: number | null 9 | relay: { 10 | id: string 11 | } | null 12 | rpcs: Array<{ url: string }> 13 | logo: string 14 | } 15 | 16 | const chainsQuery = gql` 17 | query Chains($relayId: String!) { 18 | chains(where: { relay: { id_eq: $relayId } }) { 19 | id 20 | name 21 | paraId 22 | relay { 23 | id 24 | } 25 | rpcs { 26 | url 27 | } 28 | logo 29 | } 30 | } 31 | `; 32 | 33 | const chainQuery = gql` 34 | query Chains($id: String!) { 35 | chains(where: { id_eq: $id }) { 36 | id 37 | name 38 | rpcs { 39 | url 40 | } 41 | logo 42 | } 43 | } 44 | `; 45 | 46 | export const getChains = async (network: "polkadot" | "kusama"): Promise> => { 47 | const response: any = await request(graphqlUrl, chainsQuery, { 48 | relayId: network, 49 | }) 50 | 51 | const chains = response.chains 52 | 53 | let relayChain = ((await request(graphqlUrl, chainQuery, {id: network})) as any).chains[0]; 54 | // On the backend the relay chains are registered with paraId zero. 55 | relayChain.paraId = 0; 56 | relayChain.relay = { id: network }; 57 | chains.push(relayChain); 58 | 59 | chains.sort((a: Chain, b: Chain) => a.name.localeCompare(b.name)) 60 | 61 | return chains 62 | } 63 | -------------------------------------------------------------------------------- /components/registered-chains.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | 5 | import Image from "next/image" 6 | import { Chain, getChains } from "@/common/chaindata" 7 | import { useRegisteredChains } from "@/hooks/use-registered-chains"; 8 | import { useEffect, useState } from "react"; 9 | 10 | 11 | export function RegisteredChains() { 12 | const { data: registeredChains, isLoading, isError } = useRegisteredChains(); 13 | const [chains, setChains]: [Array, any] = useState([]); 14 | 15 | useEffect(() => { 16 | getRegisteredChains(); 17 | }, [registeredChains]) 18 | 19 | const getRegisteredChains = async () => { 20 | if(!registeredChains) return; 21 | 22 | // Fetch chains based on the current network 23 | const polkadotChains = await getChains("polkadot"); 24 | const kusamaChains = await getChains("kusama"); 25 | 26 | console.log(kusamaChains); 27 | 28 | let registered = polkadotChains.concat(kusamaChains).filter((chain) => 29 | registeredChains.some( 30 | (regChain) => 31 | regChain.relay_chain.toLowerCase() === 32 | chain.relay?.id.toLowerCase() && 33 | regChain.para_id === chain.paraId 34 | )); 35 | 36 | setChains(registered); 37 | } 38 | 39 | return ( 40 |
41 |

Registered chains:

42 |
43 | {chains.map((chain) => ( 44 | {chain.name} 45 | ))} 46 |
47 |
48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /app/subscribe/post-tx.ts: -------------------------------------------------------------------------------- 1 | import { error } from "console" 2 | 3 | import { siteConfig } from "@/config/site" 4 | 5 | import { uppercaseFirstLetter } from "../../lib/utils" 6 | 7 | export async function registerWithServer( 8 | paymentBlockNumber: number, 9 | paraId: number, 10 | relayChain: string 11 | ) { 12 | const postData = { 13 | payment_block_number: paymentBlockNumber, 14 | para: [uppercaseFirstLetter(relayChain), paraId], 15 | } 16 | 17 | const endpoint = siteConfig.backendUrl + "/register_para" 18 | 19 | const response = await fetch(endpoint, { 20 | method: "POST", 21 | headers: { 22 | "Content-Type": "application/json", 23 | }, 24 | body: JSON.stringify(postData), 25 | }) 26 | 27 | const responseBody = await response.text() 28 | 29 | if (!response.ok) { 30 | throw new Error(`Failed to register chain: ${responseBody}`) 31 | } 32 | 33 | return responseBody 34 | } 35 | 36 | export async function extendWithServer( 37 | paymentBlockNumber: number, 38 | paraId: number, 39 | relayChain: string 40 | ) { 41 | const postData = { 42 | payment_block_number: paymentBlockNumber, 43 | para: [uppercaseFirstLetter(relayChain), paraId], 44 | } 45 | 46 | const endpoint = siteConfig.backendUrl + "/extend-subscription" 47 | 48 | const response = await fetch(endpoint, { 49 | method: "POST", 50 | headers: { 51 | "Content-Type": "application/json", 52 | }, 53 | body: JSON.stringify(postData), 54 | }) 55 | 56 | const responseBody = await response.text() 57 | 58 | if (!response.ok) { 59 | throw new Error(`Failed to register chain: ${responseBody}`) 60 | } 61 | 62 | return responseBody 63 | } 64 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css" 2 | import { Metadata } from "next" 3 | import { Toaster } from "sonner" 4 | 5 | import { siteConfig } from "@/config/site" 6 | import { fontSans, inter } from "@/lib/fonts" 7 | import { cn } from "@/lib/utils" 8 | import { Providers } from "@/components/providers" 9 | import { SiteHeader } from "@/components/site-header" 10 | import { TailwindIndicator } from "@/components/tailwind-indicator" 11 | import { Footer } from "@/components/footer" 12 | import { RegisteredChains } from "@/components/registered-chains" 13 | 14 | export const metadata: Metadata = { 15 | title: { 16 | default: siteConfig.name, 17 | template: `%s - ${siteConfig.name}`, 18 | }, 19 | description: siteConfig.description, 20 | icons: { 21 | icon: "/favicon.ico", 22 | shortcut: "/regionx-logo.png", 23 | apple: "/apple-touch-icon.png", 24 | }, 25 | } 26 | 27 | interface RootLayoutProps { 28 | children: React.ReactNode 29 | } 30 | 31 | export default function RootLayout({ children }: RootLayoutProps) { 32 | return ( 33 | <> 34 | 35 | 36 | 42 | 43 |
44 | 45 |
{children}
46 | 47 | 48 |
49 |
50 | 51 |
52 | 53 | 54 | 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /providers/chain-provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Dispatch, 3 | ReactNode, 4 | createContext, 5 | useContext, 6 | useEffect, 7 | useState, 8 | } from "react" 9 | import { Chain } from "@/common/chaindata" 10 | import { Network } from "@/common/types" 11 | import { ApiPromise, WsProvider } from "@polkadot/api" 12 | 13 | interface ChainContextType { 14 | chain: Chain | undefined 15 | setChain: (chain: Chain | undefined) => void 16 | network: Network 17 | setNetwork: (network: Network) => void 18 | api: ApiPromise | undefined 19 | isApiLoading?: boolean 20 | } 21 | 22 | // Create a context with a default value 23 | const ChainContext = createContext(undefined) 24 | 25 | interface ChainProviderProps { 26 | children: ReactNode 27 | } 28 | 29 | // Create a provider component 30 | export const ChainProvider: React.FC = ({ 31 | children, 32 | }: { 33 | children: ReactNode 34 | }) => { 35 | const [chain, setChain]: [Chain | undefined, Dispatch] = 36 | useState() 37 | const [network, setNetwork] = useState("polkadot") 38 | const [api, setApi]: [ 39 | ApiPromise | undefined, 40 | Dispatch 41 | ] = useState() 42 | const [isApiLoading, setIsApiLoading] = useState(true) 43 | 44 | useEffect(() => { 45 | const initApi = async () => { 46 | if (!chain) { 47 | return 48 | } 49 | setIsApiLoading(true) 50 | const wsProvider = new WsProvider(chain.rpcs[0].url) 51 | const newApi = await ApiPromise.create({ provider: wsProvider }) 52 | setApi(newApi) 53 | setIsApiLoading(false) 54 | } 55 | 56 | initApi() 57 | 58 | return () => { 59 | api?.disconnect() 60 | } 61 | }, [chain]) 62 | 63 | return ( 64 | 67 | {children} 68 | 69 | ) 70 | } 71 | 72 | export const useChain = (): ChainContextType => { 73 | const context = useContext(ChainContext) 74 | if (!context) { 75 | throw new Error("useChain must be used within a ChainProvider") 76 | } 77 | return context 78 | } 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "polkadot-weigher", 3 | "version": "1.0.0", 4 | "private": true, 5 | "author": { 6 | "name": "Niklas Plessing", 7 | "twitter": "@niftesty", 8 | "github": "niklasp" 9 | }, 10 | "scripts": { 11 | "dev": "next dev", 12 | "build": "next build", 13 | "start": "next start", 14 | "lint": "next lint", 15 | "lint:fix": "next lint --fix", 16 | "preview": "next build && next start", 17 | "typecheck": "tsc --noEmit", 18 | "format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache", 19 | "format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache" 20 | }, 21 | "dependencies": { 22 | "@polkadot/api": "^10.11.2", 23 | "@polkadot/types": "^10.11.2", 24 | "@polkadot/util": "^12.6.2", 25 | "@radix-ui/react-alert-dialog": "^1.0.5", 26 | "@radix-ui/react-checkbox": "^1.0.4", 27 | "@radix-ui/react-dialog": "^1.0.5", 28 | "@radix-ui/react-dropdown-menu": "^2.0.6", 29 | "@radix-ui/react-select": "^2.0.0", 30 | "@radix-ui/react-slot": "^1.0.2", 31 | "@radix-ui/react-tooltip": "^1.0.7", 32 | "@scio-labs/use-inkathon": "^0.8.1", 33 | "class-variance-authority": "^0.4.0", 34 | "clsx": "^1.2.1", 35 | "date-fns": "^3.3.1", 36 | "graphql-request": "^6.1.0", 37 | "lucide-react": "0.105.0-alpha.4", 38 | "moment": "^2.30.1", 39 | "next": "^14.1.0", 40 | "next-themes": "^0.2.1", 41 | "react": "^18.2.0", 42 | "react-dom": "^18.2.0", 43 | "react-query": "^3.39.3", 44 | "recharts": "^2.11.0", 45 | "sharp": "^0.33.2", 46 | "sonner": "^1.4.0", 47 | "tailwind-merge": "^1.13.2", 48 | "tailwindcss-animate": "^1.0.6" 49 | }, 50 | "devDependencies": { 51 | "@ianvs/prettier-plugin-sort-imports": "^3.7.2", 52 | "@types/node": "^17.0.45", 53 | "@types/react": "^18.2.14", 54 | "@types/react-dom": "^18.2.6", 55 | "@typescript-eslint/parser": "^5.61.0", 56 | "autoprefixer": "^10.4.14", 57 | "eslint": "^8.44.0", 58 | "eslint-config-next": "13.0.0", 59 | "eslint-config-prettier": "^8.8.0", 60 | "eslint-plugin-react": "^7.32.2", 61 | "eslint-plugin-tailwindcss": "^3.13.0", 62 | "postcss": "^8.4.24", 63 | "prettier": "^2.8.8", 64 | "tailwindcss": "^3.3.2", 65 | "typescript": "^4.9.5" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --gradient: #134E5E; 8 | --background: 205 98.3% 98.44%; 9 | --foreground: 205 6.6000000000000005% 0.88%; 10 | 11 | --muted: 205 6.6000000000000005% 92.2%; 12 | --muted-foreground: 205 3.3000000000000003% 42.2%; 13 | 14 | --popover: 205 37.199999999999996% 92.2%; 15 | --popover-foreground: 205 6.6000000000000005% 1.1%; 16 | 17 | --card: 205 37.199999999999996% 92.2%; 18 | --card-foreground: 205 6.6000000000000005% 1.1%; 19 | 20 | --border: 205 11.600000000000001% 89.88%; 21 | --input: 205 11.600000000000001% 89.88%; 22 | 23 | --primary: 205 66% 22%; 24 | --primary-foreground: 205 1.32% 92.2%; 25 | 26 | --secondary: 205 3.3000000000000003% 96.1%; 27 | --secondary-foreground: 205 4.96% 12.2%; 28 | 29 | --accent: 205 3.3000000000000003% 96.1%; 30 | --accent-foreground: 205 4.96% 12.2%; 31 | 32 | --destructive: 0 84.2% 60.2%; 33 | --destructive-foreground: 0 0% 98%; 34 | 35 | --ring: 205 66% 22%; 36 | 37 | --radius: 0.5rem; 38 | } 39 | 40 | .dark { 41 | 42 | --gradient: #4CB8C4; 43 | 44 | --background: 198 33.15% 4.24%; 45 | --foreground: 198 5.1% 97.65%; 46 | 47 | --muted: 198 25.5% 15.9%; 48 | --muted-foreground: 198 5.1% 55.3%; 49 | 50 | --popover: 198 54.8% 6.890000000000001%; 51 | --popover-foreground: 198 5.1% 97.65%; 52 | 53 | --card: 198 54.8% 6.890000000000001%; 54 | --card-foreground: 198 5.1% 97.65%; 55 | 56 | --border: 198 25.5% 15.9%; 57 | --input: 198 25.5% 15.9%; 58 | 59 | --primary: 198 51% 53%; 60 | --primary-foreground: 198 5.1% 5.300000000000001%; 61 | 62 | --secondary: 198 25.5% 15.9%; 63 | --secondary-foreground: 198 5.1% 97.65%; 64 | 65 | --accent: 198 25.5% 15.9%; 66 | --accent-foreground: 198 5.1% 97.65%; 67 | 68 | --destructive: 0 62.8% 30.6%; 69 | --destructive-foreground: 198 5.1% 97.65%; 70 | 71 | --ring: 198 51% 53%; 72 | 73 | } 74 | } 75 | 76 | @layer base { 77 | * { 78 | @apply border-border; 79 | } 80 | body { 81 | @apply bg-background text-foreground; 82 | font-feature-settings: "rlig" 1, "calt" 1; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { fontFamily } = require("tailwindcss/defaultTheme") 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | darkMode: ["class"], 6 | content: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"], 7 | theme: { 8 | container: { 9 | center: true, 10 | padding: "2rem", 11 | screens: { 12 | "2xl": "1400px", 13 | }, 14 | }, 15 | extend: { 16 | colors: { 17 | border: "hsl(var(--border))", 18 | input: "hsl(var(--input))", 19 | ring: "hsl(var(--ring))", 20 | background: "hsl(var(--background))", 21 | foreground: "hsl(var(--foreground))", 22 | primary: { 23 | DEFAULT: "hsl(var(--primary))", 24 | foreground: "hsl(var(--primary-foreground))", 25 | }, 26 | secondary: { 27 | DEFAULT: "hsl(var(--secondary))", 28 | foreground: "hsl(var(--secondary-foreground))", 29 | }, 30 | destructive: { 31 | DEFAULT: "hsl(var(--destructive))", 32 | foreground: "hsl(var(--destructive-foreground))", 33 | }, 34 | muted: { 35 | DEFAULT: "hsl(var(--muted))", 36 | foreground: "hsl(var(--muted-foreground))", 37 | }, 38 | accent: { 39 | DEFAULT: "hsl(var(--accent))", 40 | foreground: "hsl(var(--accent-foreground))", 41 | }, 42 | popover: { 43 | DEFAULT: "hsl(var(--popover))", 44 | foreground: "hsl(var(--popover-foreground))", 45 | }, 46 | card: { 47 | DEFAULT: "hsl(var(--card))", 48 | foreground: "hsl(var(--card-foreground))", 49 | }, 50 | }, 51 | borderRadius: { 52 | lg: `var(--radius)`, 53 | md: `calc(var(--radius) - 2px)`, 54 | sm: "calc(var(--radius) - 4px)", 55 | }, 56 | fontFamily: { 57 | sans: ['Inter', 'sans-serif'], 58 | }, 59 | keyframes: { 60 | "accordion-down": { 61 | from: { height: 0 }, 62 | to: { height: "var(--radix-accordion-content-height)" }, 63 | }, 64 | "accordion-up": { 65 | from: { height: "var(--radix-accordion-content-height)" }, 66 | to: { height: 0 }, 67 | }, 68 | }, 69 | animation: { 70 | "accordion-down": "accordion-down 0.2s ease-out", 71 | "accordion-up": "accordion-up 0.2s ease-out", 72 | }, 73 | }, 74 | }, 75 | plugins: [require("tailwindcss-animate")], 76 | } 77 | -------------------------------------------------------------------------------- /hooks/use-consumption.ts: -------------------------------------------------------------------------------- 1 | import { ConsumptionDatum, DateRange } from "@/common/types" 2 | import { useChain } from "@/providers/chain-provider" 3 | import { useQuery } from "react-query" 4 | 5 | import { siteConfig } from "@/config/site" 6 | 7 | export const useConsumption = ({ 8 | range, 9 | start, 10 | end, 11 | page, 12 | pageSize, 13 | grouping, 14 | }: { 15 | range: DateRange 16 | start?: Date 17 | end?: Date 18 | page?: number 19 | pageSize?: number 20 | grouping?: string //used instead of range when provided 21 | }) => { 22 | const { chain, network, api } = useChain() 23 | 24 | // Convert Date objects to Unix timestamp strings 25 | const startTimestamp = start?.getTime().toString() 26 | const endTimestamp = end?.getTime().toString() 27 | 28 | const desiredGrouping = grouping 29 | ? grouping 30 | : siteConfig.rangeGroupingMap[range] 31 | const chainId = chain?.paraId 32 | 33 | // Use URLSearchParams to construct query parameters with conditional values 34 | const params = new URLSearchParams({ 35 | grouping: desiredGrouping, 36 | ...(start !== undefined && { start: startTimestamp }), 37 | ...(end !== undefined && { end: endTimestamp }), 38 | ...(page !== undefined && { page: page.toString() }), 39 | ...(pageSize !== undefined && { page_size: pageSize.toString() }), 40 | }) 41 | 42 | // Construct the endpoint URL with query parameters 43 | const endpoint = `${ 44 | siteConfig.backendUrl 45 | }/consumption/${network}/${chainId}?${params.toString()}` 46 | 47 | return useQuery({ 48 | queryKey: [ 49 | "consumption", 50 | network, 51 | chainId, 52 | range, 53 | startTimestamp, 54 | endTimestamp, 55 | page, 56 | pageSize, 57 | ], 58 | enabled: !!(chainId !== null) && !!network && !!api && !!range, 59 | queryFn: async () => { 60 | const res = await fetch(endpoint) 61 | 62 | if (!res.ok) { 63 | // When not ok, throw an error to be caught by React Query error handling 64 | throw new Error("Network response was not ok") 65 | } 66 | 67 | const consumption = await res.json() 68 | return consumption 69 | }, 70 | retry: false, 71 | }) 72 | } 73 | 74 | export const useEarliestConsumption = () => { 75 | // make use of the useConsumption hook to get the earliest consumption 76 | // for the current chain 77 | return useConsumption({ 78 | range: "day", 79 | pageSize: 1, 80 | page: 0, 81 | grouping: "minute", 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | import { Loader2 } from "lucide-react" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const buttonVariants = cva( 9 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background", 10 | { 11 | variants: { 12 | variant: { 13 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 16 | outline: 17 | "border border-input hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "underline-offset-4 hover:underline text-primary", 22 | }, 23 | size: { 24 | default: "h-10 py-2 px-4", 25 | sm: "h-9 px-3 rounded-md", 26 | lg: "h-16 px-8 rounded-md", 27 | icon: "h-10 w-10", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | isLoading?: boolean 42 | } 43 | 44 | const Button = React.forwardRef( 45 | ( 46 | { 47 | className, 48 | variant, 49 | size, 50 | isLoading, 51 | asChild = false, 52 | children, 53 | ...props 54 | }, 55 | ref 56 | ) => { 57 | const Comp = asChild ? Slot : "button" 58 | 59 | if (isLoading) 60 | return ( 61 | 69 | 70 | {children} 71 | 72 | ) 73 | 74 | return ( 75 | 80 | {children} 81 | 82 | ) 83 | } 84 | ) 85 | Button.displayName = "Button" 86 | 87 | export { Button, buttonVariants } 88 | -------------------------------------------------------------------------------- /components/chain-select.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useEffect, useState } from "react" 4 | import Image from "next/image" 5 | import { Chain, getChains } from "@/common/chaindata" 6 | import { useChain } from "@/providers/chain-provider" 7 | 8 | import { useRegisteredChains } from "@/hooks/use-registered-chains" 9 | import { 10 | Select, 11 | SelectContent, 12 | SelectItem, 13 | SelectTrigger, 14 | SelectValue, 15 | } from "@/components/ui/select" 16 | 17 | const ChainSelect = ({ 18 | onlyRegistered = false, 19 | }: { 20 | onlyRegistered?: boolean 21 | }) => { 22 | const { chain, setChain, network } = useChain(); 23 | const { data: registeredChains, isLoading, isError } = useRegisteredChains(); 24 | 25 | const [chains, setChains]: [Array, any] = useState([]); 26 | const [chainId, setChainId] = useState( 27 | chain?.paraId ?? undefined 28 | ); 29 | 30 | useEffect(() => { 31 | // Fetch chains based on the current network 32 | getChains(network).then((allChains) => { 33 | let filteredChains = allChains; 34 | 35 | // If onlyRegistered is true, further filter the chains to include only those that are registered 36 | if (onlyRegistered && registeredChains) { 37 | // filter chains to only include those that are registered on the current network i.e. relay and paraid are in the registeredChains array 38 | filteredChains = allChains.filter((chain) => 39 | registeredChains.some( 40 | (regChain) => 41 | regChain.relay_chain.toLowerCase() === 42 | chain.relay?.id.toLowerCase() && 43 | regChain.para_id === chain.paraId 44 | ) 45 | ) 46 | } 47 | 48 | setChains(filteredChains) 49 | }) 50 | }, [network, registeredChains, onlyRegistered]) 51 | 52 | return ( 53 | <> 54 | 83 | 84 | ) 85 | } 86 | 87 | export default ChainSelect 88 | -------------------------------------------------------------------------------- /components/Chart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { AxisOptions, Chart } from "react-charts"; 3 | import axios from "axios"; 4 | 5 | type Consumption = { 6 | blockNumber: number; 7 | consumed: number; 8 | }; 9 | 10 | function ReftimeChart({ 11 | normal, 12 | mandatory, 13 | operational, 14 | total 15 | }: { 16 | normal: Consumption[]; 17 | operational: Consumption[]; 18 | mandatory: Consumption[]; 19 | total: Consumption[]; 20 | }) { 21 | const data = React.useMemo( 22 | () => [ 23 | { 24 | label: "Total consumption", 25 | data: total, 26 | }, 27 | { 28 | label: "Normal consumption", 29 | data: normal, 30 | }, 31 | { 32 | label: "Operational consumption", 33 | data: operational, 34 | }, 35 | { 36 | label: "Mandatory consumption", 37 | data: mandatory, 38 | }, 39 | ], 40 | [], 41 | ); 42 | 43 | const primaryAxis = React.useMemo( 44 | (): AxisOptions => ({ 45 | getValue: (d) => d.blockNumber, 46 | position: "bottom", 47 | }), 48 | [], 49 | ); 50 | 51 | const secondaryAxes = React.useMemo( 52 | (): AxisOptions[] => [ 53 | { 54 | getValue: (d) => d.consumed, 55 | stacked: false, 56 | }, 57 | ], 58 | [], 59 | ); 60 | 61 | const seriesColors = ['#4FD0BC', '#A4D04F', '#7B4FD0', "#d04f64"]; 62 | 63 | return ( 64 |
65 | { 68 | return { 69 | color: seriesColors[series.index], 70 | strokeWidth: 4, 71 | } 72 | }, 73 | data: data, 74 | primaryAxis, 75 | secondaryAxes, 76 | }} 77 | /> 78 |
79 | ); 80 | } 81 | 82 | const DataProvider = () => { 83 | const [normalConsumption, setNormalConsumption]: [Consumption[], any] = 84 | useState([]); 85 | const [operationalConsumption, setOperationalConsumption]: [ 86 | Consumption[], 87 | any, 88 | ] = useState([]); 89 | const [mandatoryConsumption, setMandatoryConsumption]: [Consumption[], any] = 90 | useState([]); 91 | const [totalConsumption, setTotalConsumption]: [Consumption[], any] = 92 | useState([]); 93 | 94 | useEffect(() => { 95 | fetchData(); 96 | }, []); 97 | 98 | const fetchData = async () => { 99 | const result: any[] = ( 100 | await axios.get("http://localhost:8000/consumption/polkadot/2004") 101 | ).data; 102 | 103 | const normal = result.map((record) => { 104 | return { 105 | blockNumber: record.block_number, 106 | consumed: record.normal * 100, 107 | }; 108 | }); 109 | const operational = result.map((record) => { 110 | return { 111 | blockNumber: record.block_number, 112 | consumed: record.operational * 100, 113 | }; 114 | }); 115 | const mandatory = result.map((record) => { 116 | return { 117 | blockNumber: record.block_number, 118 | consumed: record.mandatory * 100, 119 | }; 120 | }); 121 | const total = result.map((record) => { 122 | return { 123 | blockNumber: record.block_number, 124 | consumed: (record.normal + record.operational + record.mandatory) * 100, 125 | }; 126 | }); 127 | 128 | setNormalConsumption(normal); 129 | setOperationalConsumption(operational); 130 | setMandatoryConsumption(mandatory); 131 | setTotalConsumption(total); 132 | }; 133 | 134 | return ( 135 | <> 136 | {normalConsumption.length > 0 && ( 137 | 143 | )} 144 | 145 | ); 146 | }; 147 | 148 | export default DataProvider; 149 | -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as DialogPrimitive from "@radix-ui/react-dialog" 3 | import { X } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Dialog = DialogPrimitive.Root 8 | 9 | const DialogTrigger = DialogPrimitive.Trigger 10 | 11 | const DialogPortal = DialogPrimitive.Portal 12 | 13 | const DialogClose = DialogPrimitive.Close 14 | 15 | const DialogOverlay = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => ( 19 | 27 | )) 28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 29 | 30 | const DialogContent = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, children, ...props }, ref) => ( 34 | 35 | 36 | 44 | {children} 45 | 46 | 47 | Close 48 | 49 | 50 | 51 | )) 52 | DialogContent.displayName = DialogPrimitive.Content.displayName 53 | 54 | const DialogHeader = ({ 55 | className, 56 | ...props 57 | }: React.HTMLAttributes) => ( 58 |
65 | ) 66 | DialogHeader.displayName = "DialogHeader" 67 | 68 | const DialogFooter = ({ 69 | className, 70 | ...props 71 | }: React.HTMLAttributes) => ( 72 |
79 | ) 80 | DialogFooter.displayName = "DialogFooter" 81 | 82 | const DialogTitle = React.forwardRef< 83 | React.ElementRef, 84 | React.ComponentPropsWithoutRef 85 | >(({ className, ...props }, ref) => ( 86 | 94 | )) 95 | DialogTitle.displayName = DialogPrimitive.Title.displayName 96 | 97 | const DialogDescription = React.forwardRef< 98 | React.ElementRef, 99 | React.ComponentPropsWithoutRef 100 | >(({ className, ...props }, ref) => ( 101 | 106 | )) 107 | DialogDescription.displayName = DialogPrimitive.Description.displayName 108 | 109 | export { 110 | Dialog, 111 | DialogPortal, 112 | DialogOverlay, 113 | DialogClose, 114 | DialogTrigger, 115 | DialogContent, 116 | DialogHeader, 117 | DialogFooter, 118 | DialogTitle, 119 | DialogDescription, 120 | } 121 | -------------------------------------------------------------------------------- /app/subscribe/subscribe-tx.ts: -------------------------------------------------------------------------------- 1 | import { Chain } from "@/common/chaindata" 2 | import { Network } from "@/common/types" 3 | import { ApiPromise } from "@polkadot/api" 4 | import { SubmittableExtrinsicFunction } from "@polkadot/api/types" 5 | import { AccountId } from "@polkadot/types/interfaces" 6 | import { 7 | AnyTuple, 8 | Callback, 9 | IKeyringPair, 10 | ISubmittableResult, 11 | } from "@polkadot/types/types" 12 | import { BN, bnToBn } from "@polkadot/util" 13 | import { 14 | TransferBalanceResult, 15 | checkIfBalanceSufficient, 16 | getExtrinsicErrorMessage, 17 | } from "@scio-labs/use-inkathon" 18 | import { toast } from "sonner" 19 | 20 | import { siteConfig } from "@/config/site" 21 | import { uppercaseFirstLetter } from "@/lib/utils" 22 | 23 | export type SubmitSubscriptionResult = TransferBalanceResult & { 24 | toastId?: string | number 25 | chain?: Chain 26 | network?: Network 27 | } 28 | 29 | /** 30 | * Transfers a given amount of tokens from one account to another. 31 | */ 32 | export const subscribeTx = async ( 33 | api: ApiPromise, 34 | fromAccount: IKeyringPair | string, 35 | network: Network, 36 | chainDecimals: number, 37 | chain: Chain, 38 | allowDeath?: boolean, 39 | statusCb?: Callback 40 | ): Promise => { 41 | const amount: bigint | BN | string | number = siteConfig.subscriptionCost 42 | 43 | const hasSufficientBalance = await checkIfBalanceSufficient( 44 | api, 45 | fromAccount, 46 | amount 47 | ) 48 | 49 | if (!hasSufficientBalance) { 50 | return Promise.reject({ 51 | errorMessage: "TokenBelowMinimum", 52 | } satisfies SubmitSubscriptionResult) 53 | } 54 | 55 | const toAddress = 56 | "1C8BPSDDuaK2Rk8LgjFKpm9BsuFva7EfJWZ6s8XGg7FVwPn" 57 | 58 | const toastId = toast.loading( 59 | `Awaiting signature for subscribing to ${ 60 | chain.name 61 | } on ${uppercaseFirstLetter(network)}...` 62 | ) 63 | 64 | return new Promise(async (resolve, reject) => { 65 | try { 66 | const remarkFn = api.tx.system.remark( 67 | `regionx-weigher::${uppercaseFirstLetter(network)}:${chain.paraId}` 68 | ) 69 | 70 | const transferFn = (api.tx.balances[ 71 | allowDeath ? "transferAllowDeath" : "transferKeepAlive" 72 | ] || api.tx.balances["transfer"]) as SubmittableExtrinsicFunction< 73 | "promise", 74 | AnyTuple 75 | > 76 | 77 | const batchAll = api.tx.utility.batchAll([ 78 | transferFn(toAddress, bnToBn(amount)), 79 | remarkFn, 80 | ]) 81 | 82 | const unsub = await batchAll.signAndSend( 83 | fromAccount, 84 | async (result: ISubmittableResult) => { 85 | statusCb?.(result) 86 | const { status, dispatchError, events = [], txHash } = result 87 | 88 | if (status.isReady) { 89 | toast.loading(`Sending transaction`, { 90 | id: toastId, 91 | }) 92 | console.log(`Sending transaction with hash ${txHash}`) 93 | } else if (status.isInBlock) { 94 | toast.loading(`Transaction in block. Waiting for finalization...`, { 95 | id: toastId, 96 | }) 97 | console.log(`Transaction in block with hash ${txHash}`) 98 | } else if (status.isFinalized) { 99 | console.log(`Transaction included at blockHash ${txHash}`) 100 | 101 | if (dispatchError) { 102 | console.error("Error while transferring balance:", dispatchError) 103 | if (dispatchError.isModule) { 104 | // for module errors, we have the section indexed, lookup 105 | const decoded = api?.registry.findMetaError( 106 | dispatchError.asModule 107 | ) 108 | const { docs, name, section } = decoded || {} 109 | 110 | console.error( 111 | `Error while transferring balance: ${docs?.join(" ")}` 112 | ) 113 | reject(docs?.join(" ")) 114 | } else { 115 | // Other, CannotLookup, BadOrigin, no extra info 116 | console.error( 117 | "Error while transferring balance:", 118 | dispatchError 119 | ) 120 | reject({ status: "error", message: dispatchError.toString() }) 121 | } 122 | } 123 | 124 | resolve({ result, toastId, chain, network }) 125 | 126 | unsub?.() 127 | } 128 | } 129 | ) 130 | } catch (e: any) { 131 | console.error("Error while transferring balance:", e) 132 | reject({ 133 | errorMessage: getExtrinsicErrorMessage(e), 134 | errorEvent: e, 135 | } satisfies TransferBalanceResult) 136 | } 137 | }) 138 | } 139 | -------------------------------------------------------------------------------- /components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" 3 | 4 | import { cn } from "@/lib/utils" 5 | import { buttonVariants } from "@/components/ui/button" 6 | 7 | const AlertDialog = AlertDialogPrimitive.Root 8 | 9 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger 10 | 11 | const AlertDialogPortal = AlertDialogPrimitive.Portal 12 | 13 | const AlertDialogOverlay = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef 16 | >(({ className, ...props }, ref) => ( 17 | 25 | )) 26 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName 27 | 28 | const AlertDialogContent = React.forwardRef< 29 | React.ElementRef, 30 | React.ComponentPropsWithoutRef 31 | >(({ className, ...props }, ref) => ( 32 | 33 | 34 | 42 | 43 | )) 44 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName 45 | 46 | const AlertDialogHeader = ({ 47 | className, 48 | ...props 49 | }: React.HTMLAttributes) => ( 50 |
57 | ) 58 | AlertDialogHeader.displayName = "AlertDialogHeader" 59 | 60 | const AlertDialogFooter = ({ 61 | className, 62 | ...props 63 | }: React.HTMLAttributes) => ( 64 |
71 | ) 72 | AlertDialogFooter.displayName = "AlertDialogFooter" 73 | 74 | const AlertDialogTitle = React.forwardRef< 75 | React.ElementRef, 76 | React.ComponentPropsWithoutRef 77 | >(({ className, ...props }, ref) => ( 78 | 83 | )) 84 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName 85 | 86 | const AlertDialogDescription = React.forwardRef< 87 | React.ElementRef, 88 | React.ComponentPropsWithoutRef 89 | >(({ className, ...props }, ref) => ( 90 | 95 | )) 96 | AlertDialogDescription.displayName = 97 | AlertDialogPrimitive.Description.displayName 98 | 99 | const AlertDialogAction = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName 110 | 111 | const AlertDialogCancel = React.forwardRef< 112 | React.ElementRef, 113 | React.ComponentPropsWithoutRef 114 | >(({ className, ...props }, ref) => ( 115 | 124 | )) 125 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName 126 | 127 | export { 128 | AlertDialog, 129 | AlertDialogPortal, 130 | AlertDialogOverlay, 131 | AlertDialogTrigger, 132 | AlertDialogContent, 133 | AlertDialogHeader, 134 | AlertDialogFooter, 135 | AlertDialogTitle, 136 | AlertDialogDescription, 137 | AlertDialogAction, 138 | AlertDialogCancel, 139 | } 140 | -------------------------------------------------------------------------------- /components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SelectPrimitive from "@radix-ui/react-select" 3 | import { Check, ChevronDown, ChevronUp } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Select = SelectPrimitive.Root 8 | 9 | const SelectGroup = SelectPrimitive.Group 10 | 11 | const SelectValue = SelectPrimitive.Value 12 | 13 | const SelectTrigger = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef 16 | >(({ className, children, ...props }, ref) => ( 17 | span]:line-clamp-1", 21 | className 22 | )} 23 | {...props} 24 | > 25 | {children} 26 | 27 | 28 | 29 | 30 | )) 31 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 32 | 33 | const SelectScrollUpButton = React.forwardRef< 34 | React.ElementRef, 35 | React.ComponentPropsWithoutRef 36 | >(({ className, ...props }, ref) => ( 37 | 45 | 46 | 47 | )) 48 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName 49 | 50 | const SelectScrollDownButton = React.forwardRef< 51 | React.ElementRef, 52 | React.ComponentPropsWithoutRef 53 | >(({ className, ...props }, ref) => ( 54 | 62 | 63 | 64 | )) 65 | SelectScrollDownButton.displayName = 66 | SelectPrimitive.ScrollDownButton.displayName 67 | 68 | const SelectContent = React.forwardRef< 69 | React.ElementRef, 70 | React.ComponentPropsWithoutRef 71 | >(({ className, children, position = "popper", ...props }, ref) => ( 72 | 73 | 84 | 85 | 92 | {children} 93 | 94 | 95 | 96 | 97 | )) 98 | SelectContent.displayName = SelectPrimitive.Content.displayName 99 | 100 | const SelectLabel = React.forwardRef< 101 | React.ElementRef, 102 | React.ComponentPropsWithoutRef 103 | >(({ className, ...props }, ref) => ( 104 | 109 | )) 110 | SelectLabel.displayName = SelectPrimitive.Label.displayName 111 | 112 | const SelectItem = React.forwardRef< 113 | React.ElementRef, 114 | React.ComponentPropsWithoutRef 115 | >(({ className, children, ...props }, ref) => ( 116 | 124 | {children} 125 | 126 | )) 127 | SelectItem.displayName = SelectPrimitive.Item.displayName 128 | 129 | const SelectSeparator = React.forwardRef< 130 | React.ElementRef, 131 | React.ComponentPropsWithoutRef 132 | >(({ className, ...props }, ref) => ( 133 | 138 | )) 139 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName 140 | 141 | export { 142 | Select, 143 | SelectGroup, 144 | SelectValue, 145 | SelectTrigger, 146 | SelectContent, 147 | SelectLabel, 148 | SelectItem, 149 | SelectSeparator, 150 | SelectScrollUpButton, 151 | SelectScrollDownButton, 152 | } 153 | -------------------------------------------------------------------------------- /components/connect-button.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { FC, useState } from "react" 4 | import Image from "next/image" 5 | import Link from "next/link" 6 | import { InjectedAccount } from "@polkadot/extension-inject/types" 7 | import { encodeAddress } from "@polkadot/util-crypto" 8 | import { 9 | SubstrateWalletPlatform, 10 | allSubstrateWallets, 11 | isWalletInstalled, 12 | useInkathon, 13 | } from "@scio-labs/use-inkathon" 14 | import { ArrowUpRight, CheckCircle, ChevronDown } from "lucide-react" 15 | 16 | import { truncateHash } from "@/lib/utils" 17 | import { Button } from "@/components/ui/button" 18 | import { 19 | DropdownMenu, 20 | DropdownMenuContent, 21 | DropdownMenuItem, 22 | DropdownMenuSeparator, 23 | DropdownMenuTrigger, 24 | } from "@/components/ui/dropdown-menu" 25 | 26 | export interface ConnectButtonProps { 27 | size?: "default" | "sm" | "lg" | "icon" | null | undefined 28 | } 29 | 30 | export const ConnectButton: FC = ({ size }) => { 31 | const { 32 | activeChain, 33 | connect, 34 | disconnect, 35 | isConnecting, 36 | activeAccount, 37 | accounts, 38 | setActiveAccount, 39 | } = useInkathon() 40 | 41 | // Sort installed wallets first 42 | const [browserWallets] = useState([ 43 | ...allSubstrateWallets.filter( 44 | (w) => 45 | w.platforms.includes(SubstrateWalletPlatform.Browser) && 46 | isWalletInstalled(w) 47 | ), 48 | ...allSubstrateWallets.filter( 49 | (w) => 50 | w.platforms.includes(SubstrateWalletPlatform.Browser) && 51 | !isWalletInstalled(w) 52 | ), 53 | ]) 54 | 55 | // Connect Button 56 | if (!activeAccount) 57 | return ( 58 | 59 | 60 | 70 | 71 | 72 | {!activeAccount && 73 | browserWallets.map((w) => 74 | isWalletInstalled(w) ? ( 75 | { 79 | connect?.(undefined, w) 80 | }} 81 | > 82 | {w.name} 83 | 84 | ) : ( 85 | 86 | 87 |
88 |

{w.name}

89 | 90 |
91 |

Not installed

92 | 93 |
94 | ) 95 | )} 96 |
97 |
98 | ) 99 | 100 | // Account Menu & Disconnect Button 101 | return ( 102 |
103 | 104 | 105 | 122 | 123 | 127 | {/* Available Accounts/Wallets */} 128 | 129 | {(accounts || []).map((acc) => { 130 | if(acc.type == "ethereum") return; 131 | const encodedAddress = encodeAddress( 132 | acc.address, 133 | activeChain?.ss58Prefix || 42 134 | ) 135 | const truncatedEncodedAddress = truncateHash(encodedAddress, 10) 136 | 137 | return ( 138 | { 145 | setActiveAccount?.(acc) 146 | }} 147 | > 148 |
149 |
150 | 151 |

{truncatedEncodedAddress}

152 |
153 | {acc.address === activeAccount?.address && ( 154 | 155 | )} 156 |
157 |
158 | ) 159 | })} 160 | 161 | {/* Disconnect Button */} 162 | 163 | disconnect?.()} 166 | > 167 |
Disconnect
168 |
169 |
170 |
171 |
172 | ) 173 | } 174 | 175 | export interface AccountNameProps { 176 | account: InjectedAccount 177 | } 178 | 179 | export const AccountName: FC = ({ account, ...rest }) => { 180 | return
{account.name}
181 | } 182 | -------------------------------------------------------------------------------- /components/consumption-chart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react" 2 | import { useTheme } from "next-themes" 3 | import { 4 | CartesianGrid, 5 | Legend, 6 | Line, 7 | LineChart, 8 | ResponsiveContainer, 9 | Tooltip, 10 | XAxis, 11 | YAxis, 12 | } from "recharts" 13 | 14 | import { ConsumptionDatum } from "../common/types" 15 | 16 | export type RawData = { 17 | ref_time: { 18 | normal: number 19 | operational: number 20 | mandatory: number 21 | } 22 | proof_size: { 23 | normal: number 24 | operational: number 25 | mandatory: number 26 | } 27 | count: number 28 | } 29 | 30 | type Props = { 31 | data: ConsumptionDatum[] // The updated prop type to match the new data structure. 32 | grouping: string 33 | refTimeDisplayed: boolean 34 | proofSizeDisplayed: boolean 35 | } 36 | 37 | type ChartDatum = { 38 | date: string // Used for labeling the x-axis. 39 | avgRefTimeNormal: number 40 | avgRefTimeOperational: number 41 | avgRefTimeMandatory: number 42 | avgRefTimeTotal: number 43 | avgProofSizeNormal: number 44 | avgProofSizeOperational: number 45 | avgProofSizeMandatory: number 46 | avgProofSizeTotal: number 47 | } 48 | 49 | const colors = { 50 | ref_time: { 51 | normal: "#D32F2F", // Red 700 52 | operational: "#1976D2", // Blue 700 53 | mandatory: "#388E3C", // Green 700 54 | total: "#FBC02D", // Yellow 700 55 | }, 56 | proof_size: { 57 | normal: "#7B1FA2", // Purple 700 58 | operational: "#F57C00", // Orange 700 59 | mandatory: "#C2185B", // Pink 700 60 | total: "#00796B", // Teal 700 61 | }, 62 | } 63 | 64 | const ConsumptionChart: React.FC = ({ 65 | data, 66 | refTimeDisplayed, 67 | proofSizeDisplayed, 68 | }) => { 69 | const { theme } = useTheme() 70 | 71 | const formatData = (data: ConsumptionDatum[]): ChartDatum[] => { 72 | return data.map((datum: ConsumptionDatum) => ({ 73 | date: datum.group, 74 | avgRefTimeNormal: datum.ref_time.normal / datum.count, 75 | avgRefTimeOperational: datum.ref_time.operational / datum.count, 76 | avgRefTimeMandatory: datum.ref_time.mandatory / datum.count, 77 | avgRefTimeTotal: 78 | (datum.ref_time.normal + 79 | datum.ref_time.operational + 80 | datum.ref_time.mandatory) / 81 | datum.count, 82 | avgProofSizeNormal: datum.proof_size.normal / datum.count, 83 | avgProofSizeOperational: datum.proof_size.operational / datum.count, 84 | avgProofSizeMandatory: datum.proof_size.mandatory / datum.count, 85 | avgProofSizeTotal: 86 | (datum.proof_size.normal + 87 | datum.proof_size.operational + 88 | datum.proof_size.mandatory) / 89 | datum.count, 90 | })) 91 | } 92 | 93 | const formattedData = useMemo(() => formatData(data), [data]) 94 | 95 | const formatYAxisTick = (value: any) => `${(value * 100).toFixed(2)}%` 96 | const formatTooltip = (value: any, name: any) => { 97 | return `${(value * 100).toFixed(2)}%` 98 | } 99 | 100 | return ( 101 |
102 | 103 | 107 | 108 | 109 | 110 | 116 | 117 | {refTimeDisplayed && ( 118 | <> 119 | 126 | 133 | 140 | 147 | 148 | )} 149 | {proofSizeDisplayed && ( 150 | <> 151 | 159 | 167 | 175 | 183 | 184 | )} 185 | 186 | 187 | {/*
{JSON.stringify(data, null, 2)}
*/} 188 |
189 | ) 190 | } 191 | 192 | export default ConsumptionChart 193 | -------------------------------------------------------------------------------- /components/consumption-grid.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Dispatch, useEffect, useState } from "react" 4 | import Image from "next/image" 5 | import { Chain } from "@/common/chaindata" 6 | import { Consumption } from "@/common/types" 7 | import { useChain } from "@/providers/chain-provider" 8 | import { ApiPromise } from "@polkadot/api" 9 | 10 | import { Icons } from "./icons" 11 | 12 | const ConsumptionGrid = () => { 13 | const { chain, api } = useChain() 14 | 15 | const [consumption, setConsumption]: [Consumption, Dispatch] = 16 | useState({ normal: 0, operational: 0, mandatory: 0, total: 0 }) 17 | const [loading, setLoading] = useState(true) 18 | const [blockNumber, setBlockNumber] = useState("") 19 | 20 | useEffect(() => { 21 | if (!api) { 22 | return 23 | } 24 | 25 | const interval = setInterval(() => { 26 | getConsumption(api) 27 | }, 2000) 28 | 29 | return () => clearInterval(interval) 30 | }, [api]) 31 | 32 | useEffect(() => { 33 | setLoading(true) 34 | }, [chain]) 35 | 36 | const getConsumption = async (api: any) => { 37 | if (!chain) { 38 | return 39 | } 40 | 41 | const weightLimit: any = api.consts.system.blockWeights.toJSON() 42 | 43 | const blockConsumption: any = ( 44 | await api.query.system.blockWeight() 45 | ).toJSON() 46 | const blockNumber: any = (await api.query.system.number()).toHuman() 47 | 48 | const totalConsumption = getTotalConsumption(blockConsumption) 49 | const maxBlockRefTime = weightLimit.maxBlock.refTime 50 | ? weightLimit.maxBlock.refTime 51 | : weightLimit.maxBlock 52 | 53 | const normal = 54 | blockConsumption.normal.refTime !== undefined 55 | ? blockConsumption.normal.refTime 56 | : blockConsumption.normal 57 | const operational = 58 | blockConsumption.operational.refTime !== undefined 59 | ? blockConsumption.operational.refTime 60 | : blockConsumption.operational 61 | const mandatory = 62 | blockConsumption.mandatory.refTime !== undefined 63 | ? blockConsumption.mandatory.refTime 64 | : blockConsumption.mandatory 65 | 66 | let updatedConsumption: Consumption = { 67 | total: 68 | Number( 69 | parseFloat( 70 | (totalConsumption / maxBlockRefTime).toString() 71 | ).toPrecision(3) 72 | ) * 100, 73 | normal: 74 | Number( 75 | parseFloat((normal / maxBlockRefTime).toString()).toPrecision(3) 76 | ) * 100, 77 | operational: 78 | Number( 79 | parseFloat((operational / maxBlockRefTime).toString()).toPrecision(3) 80 | ) * 100, 81 | mandatory: 82 | Number( 83 | parseFloat((mandatory / maxBlockRefTime).toString()).toPrecision(3) 84 | ) * 100, 85 | } 86 | setLoading(false) 87 | setConsumption(updatedConsumption) 88 | setBlockNumber(blockNumber) 89 | } 90 | 91 | const getTotalConsumption = (blockConsumption: any) => { 92 | if (blockConsumption.mandatory.refTime) { 93 | return ( 94 | blockConsumption.mandatory.refTime + 95 | blockConsumption.normal.refTime + 96 | blockConsumption.operational.refTime 97 | ) 98 | } else { 99 | return ( 100 | blockConsumption.mandatory + 101 | blockConsumption.normal + 102 | blockConsumption.operational 103 | ) 104 | } 105 | } 106 | 107 | const getDispatchClassConsumption = (dispatchClass: string): number => { 108 | // @ts-ignore 109 | return consumption[dispatchClass.toLowerCase()].toString().substring(0, 4) 110 | } 111 | 112 | const data = [ 113 | { 114 | title: "Normal", 115 | content: `Dispatches in this class represent normal user-triggered transactions. These types of dispatches only consume a portion of a block's total weight limit. Normal dispatches are sent to the transaction pool.`, 116 | }, 117 | { 118 | title: "Operational", 119 | content: `Unlike normal dispatches, which represent usage of network capabilities, operational dispatches are those that provide network capabilities. Operational dispatches can consume the entire weight limit of a block.`, 120 | }, 121 | { 122 | title: "Mandatory", 123 | content: `The mandatory dispatches are included in a block even if they cause the block to surpass its weight limit. This dispatch class is intended to represent functions that are part of the block validation process. `, 124 | }, 125 | ] 126 | 127 | if (!chain) { 128 | return ( 129 |
130 |
131 |
132 | Select a chain to view block consumption 133 |
134 |
135 |
136 | ) 137 | } 138 | 139 | return ( 140 | <> 141 |
142 |
143 |
144 |
145 | {chain && ( 146 | 153 | )} 154 | {chain?.name} 155 |
156 | Total weight consumed at block #{blockNumber} 157 | {loading ? ( 158 | 159 | ) : ( 160 |

161 | {consumption.total.toString().substring(0, 4)}% 162 |

163 | )} 164 |
165 |
166 | {data.map((card, index) => ( 167 |
171 |
172 |

173 | {card.title} 174 |

175 |

176 | {card.content} 177 |

178 | {loading ? ( 179 | 180 | ) : ( 181 |

182 | {getDispatchClassConsumption(card.title)}% 183 |

184 | )} 185 |
186 | ))} 187 |
188 |
189 |
190 | 191 | ) 192 | } 193 | 194 | export default ConsumptionGrid 195 | -------------------------------------------------------------------------------- /components/icons.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | LucideProps, 3 | Moon, 4 | SunMedium, 5 | Twitter, 6 | type Icon as LucideIcon, 7 | } from "lucide-react" 8 | 9 | import { cn } from "@/lib/utils" 10 | 11 | export type Icon = LucideIcon 12 | 13 | export const Icons = { 14 | sun: SunMedium, 15 | moon: Moon, 16 | twitter: Twitter, 17 | logo: (props: LucideProps) => ( 18 | 19 | 23 | 24 | ), 25 | gitHub: (props: LucideProps) => ( 26 | 27 | 31 | 32 | ), 33 | polkadot: (props: LucideProps) => ( 34 | 45 | 46 | 47 | 48 | 49 | 56 | 63 | 64 | 72 | 73 | 81 | 82 | 90 | 91 | 99 | 100 | 101 | 102 | 103 | ), 104 | kusama: (props: LucideProps) => ( 105 | 115 | 116 | 117 | 122 | 126 | 127 | 128 | 129 | ), 130 | loading: ({ className }: { className: string }) => ( 131 | 143 | 144 | 145 | ), 146 | } 147 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 3 | import { Check, ChevronRight, Circle } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const DropdownMenu = DropdownMenuPrimitive.Root 8 | 9 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 10 | 11 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 12 | 13 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 14 | 15 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 16 | 17 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 18 | 19 | const DropdownMenuSubTrigger = React.forwardRef< 20 | React.ElementRef, 21 | React.ComponentPropsWithoutRef & { 22 | inset?: boolean 23 | } 24 | >(({ className, inset, children, ...props }, ref) => ( 25 | 34 | {children} 35 | 36 | 37 | )) 38 | DropdownMenuSubTrigger.displayName = 39 | DropdownMenuPrimitive.SubTrigger.displayName 40 | 41 | const DropdownMenuSubContent = React.forwardRef< 42 | React.ElementRef, 43 | React.ComponentPropsWithoutRef 44 | >(({ className, ...props }, ref) => ( 45 | 53 | )) 54 | DropdownMenuSubContent.displayName = 55 | DropdownMenuPrimitive.SubContent.displayName 56 | 57 | const DropdownMenuContent = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, sideOffset = 4, ...props }, ref) => ( 61 | 62 | 71 | 72 | )) 73 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 74 | 75 | const DropdownMenuItem = React.forwardRef< 76 | React.ElementRef, 77 | React.ComponentPropsWithoutRef & { 78 | inset?: boolean 79 | } 80 | >(({ className, inset, ...props }, ref) => ( 81 | 90 | )) 91 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 92 | 93 | const DropdownMenuCheckboxItem = React.forwardRef< 94 | React.ElementRef, 95 | React.ComponentPropsWithoutRef 96 | >(({ className, children, checked, ...props }, ref) => ( 97 | 106 | 107 | 108 | 109 | 110 | 111 | {children} 112 | 113 | )) 114 | DropdownMenuCheckboxItem.displayName = 115 | DropdownMenuPrimitive.CheckboxItem.displayName 116 | 117 | const DropdownMenuRadioItem = React.forwardRef< 118 | React.ElementRef, 119 | React.ComponentPropsWithoutRef 120 | >(({ className, children, ...props }, ref) => ( 121 | 129 | 130 | 131 | 132 | 133 | 134 | {children} 135 | 136 | )) 137 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 138 | 139 | const DropdownMenuLabel = React.forwardRef< 140 | React.ElementRef, 141 | React.ComponentPropsWithoutRef & { 142 | inset?: boolean 143 | } 144 | >(({ className, inset, ...props }, ref) => ( 145 | 154 | )) 155 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 156 | 157 | const DropdownMenuSeparator = React.forwardRef< 158 | React.ElementRef, 159 | React.ComponentPropsWithoutRef 160 | >(({ className, ...props }, ref) => ( 161 | 166 | )) 167 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 168 | 169 | const DropdownMenuShortcut = ({ 170 | className, 171 | ...props 172 | }: React.HTMLAttributes) => { 173 | return ( 174 | 178 | ) 179 | } 180 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 181 | 182 | export { 183 | DropdownMenu, 184 | DropdownMenuTrigger, 185 | DropdownMenuContent, 186 | DropdownMenuItem, 187 | DropdownMenuCheckboxItem, 188 | DropdownMenuRadioItem, 189 | DropdownMenuLabel, 190 | DropdownMenuSeparator, 191 | DropdownMenuShortcut, 192 | DropdownMenuGroup, 193 | DropdownMenuPortal, 194 | DropdownMenuSub, 195 | DropdownMenuSubContent, 196 | DropdownMenuSubTrigger, 197 | DropdownMenuRadioGroup, 198 | } 199 | -------------------------------------------------------------------------------- /components/historic-consumption.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useEffect, useState } from "react" 4 | import Link from "next/link" 5 | import { DataDisplay, DateRange } from "@/common/types" 6 | import { useChain } from "@/providers/chain-provider" 7 | import { 8 | addDays, 9 | addHours, 10 | addMonths, 11 | addWeeks, 12 | endOfDay, 13 | endOfHour, 14 | endOfMonth, 15 | endOfWeek, 16 | format, 17 | isBefore, 18 | startOfDay, 19 | startOfHour, 20 | startOfMonth, 21 | startOfWeek, 22 | subDays, 23 | subHours, 24 | subMonths, 25 | subWeeks, 26 | subYears, 27 | } from "date-fns" 28 | import { 29 | CalendarIcon, 30 | ChevronLeftIcon, 31 | ChevronRightIcon, 32 | } from "lucide-react" 33 | 34 | import { useConsumption, useEarliestConsumption } from "@/hooks/use-consumption" 35 | 36 | import ConsumptionChart from "./consumption-chart" 37 | import { Button } from "./ui/button" 38 | import { Checkbox } from "./ui/checkbox" 39 | import DownloadJSONButton from "./ui/download-json.button" 40 | import { 41 | Select, 42 | SelectContent, 43 | SelectItem, 44 | SelectTrigger, 45 | SelectValue, 46 | } from "./ui/select" 47 | import { Skeleton } from "./ui/skeleton" 48 | 49 | const dateRangeOptions: DateRange[] = ["hour", "day", "week", "month", "year", "all"] 50 | 51 | export function HistoricConsumption() { 52 | const [range, setRange] = useState("hour") 53 | const [currentRangeStart, setCurrentRangeStart] = useState(() => 54 | startOfHour(new Date()) 55 | ) 56 | const [currentRangeEnd, setCurrentRangeEnd] = useState(() => 57 | endOfHour(new Date()) 58 | ) 59 | 60 | const [dataDisplay, setDataDisplay] = useState("ref_time") 61 | 62 | const [refTimeDisplayed, setRefTimeDisplayed] = useState(true) 63 | const [proofSizeDisplayed, setProofSizeDisplayed] = useState(false) 64 | 65 | const { chain } = useChain() 66 | const { 67 | data: historicConsumption, 68 | isLoading, 69 | isError, 70 | error, 71 | isFetching, 72 | } = useConsumption({ range, start: currentRangeStart, end: currentRangeEnd }) 73 | 74 | const { data: earliestDataEntry } = useEarliestConsumption(); 75 | const earliestDataDate = earliestDataEntry?.[0].group 76 | ? new Date(earliestDataEntry[0].group) 77 | : new Date("2020-01-01"); 78 | 79 | const latestDataDate = new Date(); // Example latest data date, assuming it's today for simplicity 80 | 81 | const today = new Date(); 82 | const endOfToday = endOfHour(today); 83 | 84 | //Determine if the "previous" or "next" buttons should be disabled 85 | const disablePrevious = isBefore( 86 | currentRangeStart, 87 | earliestDataDate 88 | ) 89 | const disableNext = !isBefore(currentRangeEnd, endOfToday) 90 | 91 | const formatDateRangeDisplay = (range: DateRange, start: Date, end: Date) => { 92 | switch (range) { 93 | case "hour": 94 | return format(start, "MMM dd, yyyy hh:mm") // Single day 95 | case "day": 96 | return format(start, "MMM dd, yyyy") // Single day 97 | case "week": 98 | return `${format(start, "MMM dd")} - ${format(end, "MMM dd, yyyy")}` // Week range within the same year 99 | case "month": 100 | return format(start, "MMMM yyyy") // Full month 101 | case "year": 102 | return format(start, "yyyy") // Full year 103 | case "all": 104 | return "All time" // All data available 105 | default: 106 | return "" // Fallback for unexpected range values 107 | } 108 | } 109 | 110 | const displayedDateRange = formatDateRangeDisplay( 111 | range, 112 | currentRangeStart, 113 | currentRangeEnd 114 | ); 115 | 116 | // Handlers for changing the week/month 117 | const goToPrevious = () => { 118 | if (range === "hour") { 119 | setCurrentRangeStart((prev) => subHours(prev, 1)) 120 | setCurrentRangeEnd((prev) => subHours(prev, 1)) 121 | }else if (range === "day") { 122 | setCurrentRangeStart((prev) => subDays(prev, 1)) 123 | setCurrentRangeEnd((prev) => subDays(prev, 1)) 124 | } else if (range === "week") { 125 | setCurrentRangeStart((prev) => subWeeks(prev, 1)) 126 | setCurrentRangeEnd((prev) => subWeeks(prev, 1)) 127 | } else if (range === "month") { 128 | setCurrentRangeStart((prev) => subMonths(prev, 1)) 129 | setCurrentRangeEnd((prev) => subMonths(prev, 1)) 130 | } else if (range === "year") { 131 | setCurrentRangeStart((prev) => subYears(prev, 12)) 132 | setCurrentRangeEnd((prev) => subYears(prev, 12)) 133 | } else if (range === "all") { 134 | setCurrentRangeStart(earliestDataDate) 135 | setCurrentRangeEnd(latestDataDate) 136 | } 137 | } 138 | 139 | const goToNext = () => { 140 | if (range === "hour") { 141 | setCurrentRangeStart((prev) => addHours(prev, 1)) 142 | setCurrentRangeEnd((prev) => addHours(prev, 1)) 143 | }else if (range === "day") { 144 | setCurrentRangeStart((prev) => addDays(prev, 1)) 145 | setCurrentRangeEnd((prev) => addDays(prev, 1)) 146 | } else if (range === "week") { 147 | setCurrentRangeStart((prev) => addWeeks(prev, 1)) 148 | setCurrentRangeEnd((prev) => addWeeks(prev, 1)) 149 | } else if (range === "month") { 150 | setCurrentRangeStart((prev) => addMonths(prev, 1)) 151 | setCurrentRangeEnd((prev) => addMonths(prev, 1)) 152 | } else if (range === "year") { 153 | setCurrentRangeStart((prev) => addMonths(prev, 12)) 154 | setCurrentRangeEnd((prev) => addMonths(prev, 12)) 155 | } else if (range === "all") { 156 | setCurrentRangeStart(earliestDataDate) 157 | setCurrentRangeEnd(latestDataDate) 158 | } 159 | } 160 | 161 | // Effect to adjust range when `range` state changes 162 | useEffect(() => { 163 | if (range === "hour") { 164 | setCurrentRangeStart(startOfHour(new Date())) 165 | setCurrentRangeEnd(endOfHour(new Date())) 166 | } else if (range === "day") { 167 | setCurrentRangeStart(startOfDay(new Date())) 168 | setCurrentRangeEnd(endOfDay(new Date())) 169 | } else if (range === "week") { 170 | setCurrentRangeStart(startOfWeek(new Date(), { weekStartsOn: 1 })) 171 | setCurrentRangeEnd(endOfWeek(new Date(), { weekStartsOn: 1 })) 172 | } else if (range === "month") { 173 | setCurrentRangeStart(startOfMonth(new Date())) 174 | setCurrentRangeEnd(endOfMonth(new Date())) 175 | } 176 | }, [range]) 177 | 178 | if (!chain) { 179 | return ( 180 |
181 |
182 |
183 | Select a chain to view historic consumption charts 184 |
185 |
186 |
187 | ) 188 | } 189 | 190 | if (isError) { 191 | return ( 192 |
193 |
194 |
195 | Error fetching historic consumption data. It seems the selected 196 | chain is not registered yet or there is no data yet. 197 | 198 | 199 | 200 |
201 |
202 |
203 | ) 204 | } 205 | 206 | return ( 207 |
208 |
209 |
210 |
211 | Viewing data for: 212 | {displayedDateRange} 213 |
214 |
215 | 225 | 241 | 242 | 250 |
251 |
252 |
253 |
254 |
255 | 260 | Export JSON 261 | 262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 | setRefTimeDisplayed(!refTimeDisplayed)} 273 | /> 274 | 280 |
281 |
282 | setProofSizeDisplayed(!proofSizeDisplayed)} 286 | /> 287 | 293 |
294 |
295 |
296 |
297 | {error || isError ? ( 298 | "error" 299 | ) : isLoading || !historicConsumption ? ( 300 | 301 | Loading Chart 302 | 303 | ) : ( 304 | 310 | )} 311 |
312 |
313 | ) 314 | } 315 | -------------------------------------------------------------------------------- /app/subscribe/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useEffect, useState } from "react" 4 | import Image from "next/image" 5 | import { useChain } from "@/providers/chain-provider" 6 | import { bnToBn } from "@polkadot/util" 7 | import { formatBalance, useBalance, useInkathon } from "@scio-labs/use-inkathon" 8 | import { toast } from "sonner" 9 | 10 | import { siteConfig } from "@/config/site" 11 | import { uppercaseFirstLetter } from "@/lib/utils" 12 | import { useRegisteredChains } from "@/hooks/use-registered-chains" 13 | import { 14 | AlertDialog, 15 | AlertDialogAction, 16 | AlertDialogCancel, 17 | AlertDialogContent, 18 | AlertDialogFooter, 19 | AlertDialogHeader, 20 | AlertDialogTitle, 21 | AlertDialogTrigger, 22 | } from "@/components/ui/alert-dialog" 23 | import { Button } from "@/components/ui/button" 24 | import ChainSelect from "@/components/chain-select" 25 | import { ConnectButton } from "@/components/connect-button" 26 | import NetworkSelect from "@/components/network-select" 27 | 28 | import { extendWithServer, registerWithServer } from "./post-tx" 29 | import { subscribeTx } from "./subscribe-tx" 30 | 31 | type ChainStatus = { 32 | registered: boolean 33 | expiryInDays?: number | undefined 34 | } 35 | 36 | enum Operation { 37 | Register, 38 | Extend 39 | }; 40 | 41 | export default function SubscribePage() { 42 | const { activeAccount, api, isConnected } = useInkathon() 43 | const { tokenDecimals, freeBalance } = useBalance( 44 | activeAccount?.address, 45 | true 46 | ) 47 | 48 | const { chain: selectedChain, network: selectedNetwork } = useChain() 49 | const { data: registeredChains, isLoading, isError } = useRegisteredChains() 50 | 51 | const [chainStatus, setChainStatus] = useState({ 52 | registered: false, 53 | expiryInDays: undefined, 54 | }) 55 | 56 | function getTimeLeft(expiryTimestamp: string): number | undefined { 57 | const expiry = new Date(parseInt(expiryTimestamp) * 1000) // Convert Unix timestamp to milliseconds 58 | const now = new Date() 59 | const diffInMilliseconds = expiry.getTime() - now.getTime() 60 | 61 | if (diffInMilliseconds <= 0) { 62 | return -1 63 | } 64 | 65 | const diffInSeconds = Math.floor(diffInMilliseconds / 1000) 66 | const diffInMinutes = Math.floor(diffInSeconds / 60) 67 | const diffInHours = Math.floor(diffInMinutes / 60) 68 | const diffInDays = Math.floor(diffInHours / 24) 69 | 70 | let timeLeftString = "" 71 | 72 | if (diffInDays > 0) { 73 | timeLeftString += `${diffInDays} day${diffInDays > 1 ? "s" : ""} ` 74 | } 75 | 76 | return diffInDays 77 | } 78 | 79 | useEffect(() => { 80 | const registeredChain = registeredChains?.find( 81 | (chain) => 82 | chain.relay_chain.toLowerCase() === 83 | selectedChain?.relay?.id.toLowerCase() && 84 | chain.para_id === selectedChain?.paraId 85 | ) 86 | 87 | const chainStatus: ChainStatus = { 88 | registered: !!registeredChain, 89 | expiryInDays: registeredChain?.expiry_timestamp 90 | ? getTimeLeft(registeredChain.expiry_timestamp.toString()) 91 | : undefined, 92 | } 93 | 94 | setChainStatus(chainStatus) 95 | }, [selectedChain, registeredChains]) 96 | 97 | async function handleSubscribe(op: Operation) { 98 | if (!api || !activeAccount || !selectedNetwork || 99 | selectedChain?.paraId == null || selectedChain?.paraId == undefined) { 100 | return 101 | } 102 | 103 | const { result, toastId, chain, network } = await subscribeTx( 104 | api, 105 | activeAccount.address, 106 | selectedNetwork, 107 | tokenDecimals, 108 | selectedChain 109 | ) 110 | 111 | //@ts-ignore 112 | const blockNumber = result?.blockNumber.toString() 113 | 114 | if (!blockNumber) { 115 | console.error("error getting the block number") 116 | return 117 | } 118 | 119 | try { 120 | if(op == Operation.Extend) { 121 | const _ = await extendWithServer( 122 | parseInt(blockNumber), 123 | selectedChain.paraId, 124 | selectedNetwork 125 | ); 126 | }else if(op == Operation.Register) { 127 | const _ = await registerWithServer( 128 | parseInt(blockNumber), 129 | selectedChain.paraId, 130 | selectedNetwork 131 | ); 132 | } 133 | } catch (e) { 134 | //@ts-ignore 135 | toast.error(e.message, { id: toastId }) 136 | return 137 | } 138 | 139 | toast.success( 140 | `Subscription of ${chain?.name} on ${uppercaseFirstLetter( 141 | network || "" 142 | )} successful!`, 143 | { 144 | id: toastId, 145 | duration: 5000, 146 | action: { 147 | label: "↗ Subscan", 148 | onClick: () => 149 | window.open( 150 | `${siteConfig.blockExplorer}${result?.txHash}`, 151 | "_blank" 152 | ), 153 | }, 154 | } 155 | ) 156 | } 157 | 158 | const isAccountBalanceInsufficient = freeBalance?.lt( 159 | bnToBn(siteConfig.subscriptionCost) 160 | ) 161 | 162 | return ( 163 |
164 |
165 |
166 |

167 | Register a Parachain 168 |

169 |

170 | Register your chain to start tracking its consumption. This will 171 | allow you to see the consumption of your chain on the historic 172 | consumption page. 173 |

174 |
175 | 176 |
177 | 178 | 179 |
180 | 181 | {selectedChain && ( 182 |
183 | {chainStatus && chainStatus.registered ? ( 184 |
185 |

186 | {selectedChain.name} is already registered and will expire in:{" "} 187 | {chainStatus.expiryInDays} days 188 |

189 | {chainStatus.expiryInDays && chainStatus.expiryInDays < 30 && ( 190 |
191 | 192 | 193 | 194 | 210 | 211 | 212 | 213 | 214 | {selectedChain.name} Subscription 215 | 216 | 217 |
218 |

219 | Extending {selectedChain.name} subscription 220 | PolkadotWeigher will allow you to track its 221 | consumption for the upcoming period of 90 days. 222 |

223 |

224 | The subscription renewal costs 10 DOT and will expire 225 | in 90days. 226 |

227 |
228 | 229 | Cancel 230 | handleSubscribe(Operation.Extend)} 233 | disabled={isAccountBalanceInsufficient} 234 | > 235 | Renew 236 | 237 | 238 | {isAccountBalanceInsufficient && ( 239 |
240 | ⚠️ Your account balance is too low to subscribe 241 |
242 | )} 243 |
244 |
245 |
246 | )} 247 |
248 | ) : ( 249 |
250 |
251 |
252 |

253 | {selectedChain.name} is not registered yet. Registering a chain will allow: 254 |

255 |
256 | 260 | 264 | 268 | 272 | 276 | 280 |
281 |
282 | 283 | 284 | 285 | 301 | 302 | 303 | 304 | 305 | {selectedChain.name} Subscription 306 | 307 | 308 |
309 |

310 | Registering {selectedChain.name} on 311 | PolkadotWeigher will allow you to track its 312 | consumption for the upcoming period of 90 days. 313 |

314 |

315 | The subscription costs 10 DOT and will expire 316 | in 90days. 317 |

318 |
319 | 320 | Cancel 321 | handleSubscribe(Operation.Register)} 324 | disabled={isAccountBalanceInsufficient} 325 | > 326 | Register 327 | 328 | 329 | {isAccountBalanceInsufficient && ( 330 |
331 | ⚠️ Your account balance is too low to register 332 |
333 | )} 334 |
335 |
336 |
337 |
338 |
339 |
340 | )} 341 | 342 |
343 | )} 344 |
345 |
346 | ) 347 | } 348 | 349 | interface FeatureProps { 350 | title: string, 351 | content: string, 352 | } 353 | 354 | const Fetaure = ({title, content}: FeatureProps) => { 355 | return ( 356 |
357 |

{title}

358 |

359 | {content} 360 |

361 |
362 | ) 363 | } 364 | 365 | const ReportIssue = () => { 366 | return ( 367 | 381 | ) 382 | } 383 | --------------------------------------------------------------------------------