From d0404b38874f4760836ed3ead3d8a3923cb94560 Mon Sep 17 00:00:00 2001 From: Ed Castro Date: Wed, 16 Oct 2024 17:10:26 -0300 Subject: [PATCH] feat: polish module creation form and community validator graphs Co-authored-by: PsicoThePato Co-authored-by: Kelvin Steiner --- .../components/proposal/register-module.tsx | 59 +++++-- .../components/charts/combined-area-chart.tsx | 107 ------------ .../components/charts/module-bar-chart.tsx | 23 ++- .../components/charts/subnet-pie-chart.tsx | 47 +---- .../src/app/components/delegated-scroll.tsx | 2 +- apps/commune-validator/src/app/page.tsx | 161 +++++++++--------- apps/commune-worker/src/common/index.ts | 4 +- packages/providers/src/context/commune.tsx | 2 +- packages/providers/src/hooks/index.ts | 22 +++ packages/subspace/queries/index.ts | 12 ++ packages/utils/index.ts | 122 +++++++++---- 11 files changed, 265 insertions(+), 296 deletions(-) delete mode 100644 apps/commune-validator/src/app/components/charts/combined-area-chart.tsx diff --git a/apps/commune-governance/src/app/components/proposal/register-module.tsx b/apps/commune-governance/src/app/components/proposal/register-module.tsx index 0aa033d2..27ff5131 100644 --- a/apps/commune-governance/src/app/components/proposal/register-module.tsx +++ b/apps/commune-governance/src/app/components/proposal/register-module.tsx @@ -4,12 +4,18 @@ import MarkdownPreview from "@uiw/react-markdown-preview"; import { z } from "zod"; import type { TransactionResult } from "@commune-ts/types"; +import { useModuleBurn, useSubnetList } from "@commune-ts/providers/hooks"; import { useCommune } from "@commune-ts/providers/use-commune"; import { toast } from "@commune-ts/providers/use-toast"; import { Button, Input, Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, Separator, Tabs, TabsContent, @@ -18,6 +24,7 @@ import { Textarea, TransactionStatus, } from "@commune-ts/ui"; +import { formatToken } from "@commune-ts/utils"; import { cairo } from "~/utils/fonts"; @@ -28,7 +35,11 @@ const moduleSchema = z.object({ export function RegisterModule(): JSX.Element { const router = useRouter(); - const { isConnected, registerModule, balance } = useCommune(); + const { isConnected, registerModule, balance, api } = useCommune(); + const { data: subnetList, isLoading: isSubnetListLoading } = + useSubnetList(api); + + const { data: moduleBurn } = useModuleBurn(api); const [subnetName, setSubnetName] = useState(""); const [address, setAddress] = useState(""); @@ -53,6 +64,17 @@ export function RegisterModule(): JSX.Element { setTransactionStatus(callbackReturn); } + function getModuleBurn(subnetId: string) { + if (!moduleBurn) { + return 0; + } + if (Number(subnetId) === 0) { + return 0; + } + + return formatToken(Number(moduleBurn[subnetId])); + } + async function uploadFile(fileToUpload: File): Promise { try { setUploading(true); @@ -139,10 +161,7 @@ export function RegisterModule(): JSX.Element { } return ( -
+ Edit Content @@ -161,12 +180,28 @@ export function RegisterModule(): JSX.Element { type="text" value={moduleId} /> - setSubnetName(e.target.value)} - placeholder="Subnet Name (eg. General)" - type="text" - value={subnetName} - /> + setAddress(e.target.value)} placeholder="Address (eg. 0.0.0.0:8000)" @@ -209,9 +244,11 @@ export function RegisterModule(): JSX.Element { type="submit" variant="default-green" disabled={!isConnected} + className="text-green-500" > {uploading ? "Uploading..." : "Submit Module"} + {transactionStatus.status && ( - - Area Chart - Stacked - - Showing total visitors for the last 6 months - - - - - - - value.slice(0, 3)} - /> - } - /> - - - - - - -
-
-
- Trending up by 5.2% this month{" "} - -
-
- January - June 2024 -
-
-
-
- - ); -} diff --git a/apps/commune-validator/src/app/components/charts/module-bar-chart.tsx b/apps/commune-validator/src/app/components/charts/module-bar-chart.tsx index 38bd5848..09f56e80 100644 --- a/apps/commune-validator/src/app/components/charts/module-bar-chart.tsx +++ b/apps/commune-validator/src/app/components/charts/module-bar-chart.tsx @@ -1,7 +1,6 @@ "use client"; import type { ChartConfig } from "node_modules/@commune-ts/ui/src/components/chart"; -import { ArrowTrendingUpIcon } from "@heroicons/react/24/outline"; import { Bar, BarChart, @@ -15,7 +14,6 @@ import { Card, CardContent, CardDescription, - CardFooter, CardHeader, CardTitle, ChartContainer, @@ -47,6 +45,11 @@ const chartConfig = { } satisfies ChartConfig; export function ModuleBarChart({ chartData }: ModuleBarChartProps) { + const maxPercWeight = Math.max( + ...chartData.map((module) => module.percWeight), + ); + const xAxisDomain = [0, maxPercWeight * 1.2]; + return ( @@ -76,7 +79,12 @@ export function ModuleBarChart({ chartData }: ModuleBarChartProps) { tickFormatter={(value) => value.slice(0, 3)} hide /> - + } @@ -105,15 +113,6 @@ export function ModuleBarChart({ chartData }: ModuleBarChartProps) { - -
- Trending up by 5.2% this month{" "} - -
-
- Showing total visitors for the last 6 months -
-
); } diff --git a/apps/commune-validator/src/app/components/charts/subnet-pie-chart.tsx b/apps/commune-validator/src/app/components/charts/subnet-pie-chart.tsx index 4c779fbc..330c4a51 100644 --- a/apps/commune-validator/src/app/components/charts/subnet-pie-chart.tsx +++ b/apps/commune-validator/src/app/components/charts/subnet-pie-chart.tsx @@ -1,15 +1,13 @@ "use client"; import * as React from "react"; -import { ArrowTrendingUpIcon } from "@heroicons/react/16/solid"; -import { Cell, Label, Pie, PieChart } from "recharts"; +import { Cell, Pie, PieChart } from "recharts"; import type { ChartConfig } from "@commune-ts/ui"; import { Card, CardContent, CardDescription, - CardFooter, CardHeader, CardTitle, ChartContainer, @@ -31,6 +29,7 @@ interface SubnetData { subnetName: string; stakeWeight: number; percWeight: number; + percFormat: string; } interface SubnetPieChartProps { @@ -48,8 +47,6 @@ const chartConfig = { } satisfies ChartConfig; export function SubnetPieChart({ chartData }: SubnetPieChartProps) { - const totalStakeWeight = 100; - const getFillColor = (index: number) => { return chartColors[index % chartColors.length]; }; @@ -65,7 +62,7 @@ export function SubnetPieChart({ chartData }: SubnetPieChartProps) { ( ))} - - -
- Other Subnets: 5.2% - -
-
- Showing total stake allocation for the last block -
-
); } diff --git a/apps/commune-validator/src/app/components/delegated-scroll.tsx b/apps/commune-validator/src/app/components/delegated-scroll.tsx index 4e27b8e4..3c4f4954 100644 --- a/apps/commune-validator/src/app/components/delegated-scroll.tsx +++ b/apps/commune-validator/src/app/components/delegated-scroll.tsx @@ -11,7 +11,7 @@ interface DelegatedScrollProps { export function DelegatedScroll({ data }: DelegatedScrollProps) { return ( - +
{data.map((prop, index) => ( diff --git a/apps/commune-validator/src/app/page.tsx b/apps/commune-validator/src/app/page.tsx index 261b4860..5a3f45ce 100644 --- a/apps/commune-validator/src/app/page.tsx +++ b/apps/commune-validator/src/app/page.tsx @@ -20,19 +20,20 @@ import { Label, Separator, } from "@commune-ts/ui"; - -// import { fromNano } from "@commune-ts/utils"; +import { fromNano } from "@commune-ts/utils"; import { useDelegateModuleStore } from "~/stores/delegateModuleStore"; import { useDelegateSubnetStore } from "~/stores/delegateSubnetStore"; import { api } from "~/trpc/react"; -// import { separateTopNModules } from "./components/charts/_common"; +import { separateTopNModules } from "./components/charts/_common"; // import { CombinedAreaChart } from "./components/charts/combined-area-chart"; -// import { ModuleBarChart } from "./components/charts/module-bar-chart"; -// import { SubnetPieChart } from "./components/charts/subnet-pie-chart"; +import { ModuleBarChart } from "./components/charts/module-bar-chart"; +import { SubnetPieChart } from "./components/charts/subnet-pie-chart"; import { DelegatedScroll } from "./components/delegated-scroll"; import { StatsCard } from "./components/stats-card"; +const TOP_MODULES_NUM = 7; + function _repeatUntil(total: number, xs: T[]) { const result: T[] = []; for (let i = 0; i < total; i++) { @@ -50,20 +51,21 @@ export default function Page() { const { delegatedModules } = useDelegateModuleStore(); const { data: modules } = api.module.all.useQuery(); - // const { data: computedWeightedModules } = - // api.module.allComputedModuleWeightsLastBlock.useQuery(); - - // const moduleStakeData = computedWeightedModules - // ? separateTopNModules(8)(computedWeightedModules) - // // .sort((a, b) => Number(b.stakeWeight - a.stakeWeight)) - // .map((module) => { - // return { - // moduleName: module.moduleName ?? "", - // stakeWeight: String(module.stakeWeight), - // percWeight: module.percWeight * 100, - // }; - // }) - // : null; + const { data: computedWeightedModules } = + api.module.allComputedModuleWeightsLastBlock.useQuery(); + + const moduleStakeData = computedWeightedModules + ? separateTopNModules(TOP_MODULES_NUM)(computedWeightedModules) + // .sort((a, b) => Number(b.stakeWeight - a.stakeWeight)) + .map((module) => { + return { + moduleName: module.moduleName ?? "", + stakeWeight: String(module.stakeWeight), + percWeight: module.percWeight, + percFormat: `${(module.percWeight * 100).toFixed(1)} %`, + }; + }) + : null; const delegatedModulesData = delegatedModules.map((module) => ({ name: module.name, @@ -77,12 +79,13 @@ export default function Page() { const { data: computedWeightedSubnets } = api.subnet.allComputedSubnetWeightsLastBlock.useQuery(); - console.log(computedWeightedSubnets); - // const subnetData = computedWeightedSubnets?.map((s) => ({ - // stakeWeight: parseInt(fromNano(s.stakeWeight)), - // subnetName: s.subnetName, - // percWeight: s.percWeight, - // })); + + const subnetData = computedWeightedSubnets?.map((s) => ({ + stakeWeight: parseInt(fromNano(s.stakeWeight)), + subnetName: s.subnetName, + percWeight: s.percWeight, + percFormat: `${(s.percWeight * 100).toFixed(1)} %`, + })); const delegatedSubnetsData = delegatedSubnets.map((subnet) => ({ name: subnet.name, @@ -164,69 +167,65 @@ export default function Page() { />
- {/*
- {moduleStakeData ? ( - - ) : ( - <> - )} - {subnetData ? : <>} - -
*/} - +
+ {/* TODO: spinners */} + {moduleStakeData ? ( + + ) : ( + <> + )} + {subnetData ? : <>} +
+ + + Your Selected Modules + + Edit Your Module List + + + + {selectedAccount && delegatedModulesData.length ? ( + + ) : ( +

+ {delegatedModulesData.length + ? "Connect your wallet to view your modules." + : "You have not selected any modules."} +

+ )} +
+
+ + + Your Selected Subnets + + Edit Your Subnet List + + + + {selectedAccount && delegatedSubnetsData.length ? ( + + ) : ( +

+ {delegatedSubnetsData.length + ? "Connect your wallet to view your subnets." + : "You have not selected any subnets."} +

+ )} +
+
+
+
+ + - - -
- - - Your Selected Modules - - - {selectedAccount && delegatedModulesData.length ? ( - - ) : ( -

- {delegatedModulesData.length - ? "Connect your wallet to view your modules." - : "You have not selected any modules."} -

- )} - -
-
- - - Your Selected Subnets - - - {selectedAccount && delegatedSubnetsData.length ? ( - - ) : ( -

- {delegatedSubnetsData.length - ? "Connect your wallet to view your subnets." - : "You have not selected any subnets."} -

- )} - -
-
-
); diff --git a/apps/commune-worker/src/common/index.ts b/apps/commune-worker/src/common/index.ts index 732da792..7d3c9578 100644 --- a/apps/commune-worker/src/common/index.ts +++ b/apps/commune-worker/src/common/index.ts @@ -71,8 +71,8 @@ export async function getApplications( applicationTypes: DaoApplicationStatus[], ) { const dao_entries = await queryDaosEntries(api); - const pending_daos = dao_entries.filter( - (app) => applicationTypes.includes(app.status), + const pending_daos = dao_entries.filter((app) => + applicationTypes.includes(app.status), ); const dao_hash_map: Record = pending_daos.reduce( (hashmap, dao) => { diff --git a/packages/providers/src/context/commune.tsx b/packages/providers/src/context/commune.tsx index 31d592e2..b99f88ea 100644 --- a/packages/providers/src/context/commune.tsx +++ b/packages/providers/src/context/commune.tsx @@ -42,7 +42,7 @@ import { useUnrewardedProposals, useUserTotalStaked, } from "../hooks"; -import { calculateAmount, formatToken, fromNano, toNano } from "../utils"; +import { calculateAmount } from "../utils"; interface CommuneApiState { web3Accounts: (() => Promise) | null; diff --git a/packages/providers/src/hooks/index.ts b/packages/providers/src/hooks/index.ts index a14efc38..0cfc27c9 100644 --- a/packages/providers/src/hooks/index.ts +++ b/packages/providers/src/hooks/index.ts @@ -6,6 +6,8 @@ import type { ApiPromise } from "@polkadot/api"; import { useQueries, useQuery } from "@tanstack/react-query"; import { + getModuleBurn, + getSubnetList, processVotesAndStakes, queryBalance, queryDaosEntries, @@ -168,6 +170,26 @@ export function useUserTotalStaked( }); } +export function useSubnetList(api: Api | Nullish) { + return useQuery({ + queryKey: ["subnet_list"], + enabled: api != null, + queryFn: () => getSubnetList(api!), + staleTime: STAKE_STALE_TIME, + refetchOnWindowFocus: false, + }); +} + +export function useModuleBurn(api: Api | Nullish) { + return useQuery({ + queryKey: ["module_burn"], + enabled: api != null, + queryFn: () => getModuleBurn(api!), + staleTime: STAKE_STALE_TIME, + refetchOnWindowFocus: false, + }); +} + // == Custom metadata == interface BaseProposal { diff --git a/packages/subspace/queries/index.ts b/packages/subspace/queries/index.ts index b740342d..c717ad4b 100644 --- a/packages/subspace/queries/index.ts +++ b/packages/subspace/queries/index.ts @@ -692,3 +692,15 @@ export async function queryChain( }); return modulePropMap; } + +export async function getSubnetList(api: Api): Promise> { + const result = await queryChain(api, { subspaceModule: ["subnetNames"] }); + const subnetNames = result.subnetNames; + return subnetNames as Record; +} + +export async function getModuleBurn(api: Api): Promise> { + const result = await queryChain(api, { subspaceModule: ["burn"] }); + const burn = result.burn; + return burn as Record; +} diff --git a/packages/utils/index.ts b/packages/utils/index.ts index 709bb542..f5d81f57 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -2,6 +2,7 @@ import { BN, stringToHex } from "@polkadot/util"; import { CID } from "multiformats/cid"; import { match } from "rustie"; import { AssertionError } from "tsafe"; + import type { AnyTuple, Api, @@ -107,10 +108,14 @@ export function assertOrThrow( // == Calc == // TODO: fix wrong `bigintDivision` -export function bigintDivision_WRONG(a: bigint, b: bigint, precision = 8n): number { +export function bigintDivision_WRONG( + a: bigint, + b: bigint, + precision = 8n, +): number { if (b === 0n) return NaN; const base = 10n ** precision; - return Number(a) * Number(base) / Number(b) / Number(base); + return (Number(a) * Number(base)) / Number(b) / Number(base); } export function bigintDivision(a: bigint, b: bigint, precision = 8n): number { @@ -285,15 +290,37 @@ export type SubspacePalletName = | "subnetEmissionModule"; export type SubspaceStorageName = - | "emission" | "incentive" | "dividends" | "lastUpdate" - | "metadata" | "registrationBlock" | "name" | "address" - | "keys" | "subnetNames" | "immunityPeriod" | "minAllowedWeights" - | "maxAllowedWeights" | "tempo" | "maxAllowedUids" | "founder" - | "founderShare" | "incentiveRatio" | "trustRatio" | "maxWeightAge" - | "bondsMovingAverage" | "maximumSetWeightCallsPerEpoch" | "minValidatorStake" - | "maxAllowedValidators" | "moduleBurnConfig" | "subnetMetadata" - | "subnetGovernanceConfig" | "subnetEmission" | "delegationFee" - | "stakeFrom"; + | "emission" + | "incentive" + | "dividends" + | "lastUpdate" + | "metadata" + | "registrationBlock" + | "name" + | "address" + | "keys" + | "subnetNames" + | "immunityPeriod" + | "minAllowedWeights" + | "maxAllowedWeights" + | "tempo" + | "maxAllowedUids" + | "founder" + | "founderShare" + | "incentiveRatio" + | "trustRatio" + | "maxWeightAge" + | "bondsMovingAverage" + | "maximumSetWeightCallsPerEpoch" + | "minValidatorStake" + | "maxAllowedValidators" + | "moduleBurnConfig" + | "subnetMetadata" + | "subnetGovernanceConfig" + | "subnetEmission" + | "delegationFee" + | "stakeFrom" + | "burn"; // TODO: add MinimumAllowedStake, stakeFrom @@ -301,7 +328,10 @@ export function standardizeUidToSS58address( outerRecord: Record>, uidToKey: Record, ): Record> { - const processedRecord: Record> = {} as Record>; + const processedRecord: Record> = {} as Record< + T, + Record + >; const entries = Object.entries(outerRecord) as [T, Record][]; for (const [outerKey, innerRecord] of entries) { @@ -324,29 +354,48 @@ export function standardizeUidToSS58address( return processedRecord; } - type StorageTypes = "VecMapping" | "NetuidMap" | "SimpleMap" | "DoubleMap"; export function getSubspaceStorageMappingKind( prop: SubspaceStorageName, ): StorageTypes | null { const vecProps: SubspaceStorageName[] = [ - "emission", "incentive", "dividends", "lastUpdate", + "emission", + "incentive", + "dividends", + "lastUpdate", ]; const netuidMapProps: SubspaceStorageName[] = [ - "metadata", "registrationBlock", "name", "address", "keys" + "metadata", + "registrationBlock", + "name", + "address", + "keys", ]; const simpleMapProps: SubspaceStorageName[] = [ - "minAllowedWeights", "maxWeightAge", "maxAllowedWeights", "trustRatio", - "tempo", "founderShare", "subnetNames", "immunityPeriod", "maxAllowedUids", - "founder", "incentiveRatio", "bondsMovingAverage", - "maximumSetWeightCallsPerEpoch", "minValidatorStake", "maxAllowedValidators", - "moduleBurnConfig", "subnetMetadata", "subnetGovernanceConfig", "subnetEmission", - "delegationFee" - ] - const doubleMapProps: SubspaceStorageName[] = [ - "stakeFrom" + "minAllowedWeights", + "maxWeightAge", + "maxAllowedWeights", + "trustRatio", + "tempo", + "founderShare", + "subnetNames", + "immunityPeriod", + "maxAllowedUids", + "founder", + "incentiveRatio", + "bondsMovingAverage", + "maximumSetWeightCallsPerEpoch", + "minValidatorStake", + "maxAllowedValidators", + "moduleBurnConfig", + "subnetMetadata", + "subnetGovernanceConfig", + "subnetEmission", + "delegationFee", + "burn", ]; + const doubleMapProps: SubspaceStorageName[] = ["stakeFrom"]; const mapping = { VecMapping: vecProps, NetuidMap: netuidMapProps, @@ -373,7 +422,8 @@ export async function getPropsToMap( const value = getSubspaceStorageMappingKind(storageName); if (value === "NetuidMap") { - const entries = await api.query[palletName]?.[storageName]?.entries(netuid); + const entries = + await api.query[palletName]?.[storageName]?.entries(netuid); if (entries === undefined) { throw new Error(`No entries for ${palletName}.${storageName}`); } @@ -382,7 +432,10 @@ export async function getPropsToMap( if (storageQuery === undefined) { throw new Error(`${palletName}.${storageName} doesn't exist`); } - const entries = value === "NetuidMap" ? await storageQuery(netuid) : await storageQuery(); + const entries = + value === "NetuidMap" + ? await storageQuery(netuid) + : await storageQuery(); switch (value) { case "VecMapping": @@ -428,21 +481,20 @@ export class SimpleMap implements ChainEntry { queryStorage() { const storageData: Record = {}; - this.entry.forEach(entry => { + this.entry.forEach((entry) => { const key = entry[0].args[0]?.toPrimitive() as string; const value = entry[1].toPrimitive() as string; storageData[key] = value; - } - ) + }); return storageData; } } export class NetuidMapEntries implements ChainEntry { - constructor(private readonly entries: [StorageKey, Codec][]) { } + constructor(private readonly entries: [StorageKey, Codec][]) {} queryStorage() { const moduleIdToPropValue: Record = {}; - this.entries.forEach(entry => { + this.entries.forEach((entry) => { const moduleCodec = entry[1]; const moduleId = entry[0].args[1]?.toPrimitive() as number; moduleIdToPropValue[moduleId] = moduleCodec.toPrimitive() as string; @@ -451,27 +503,24 @@ export class NetuidMapEntries implements ChainEntry { } } - export class DoubleMapEntries implements ChainEntry { - constructor(private readonly entries: [StorageKey, Codec][]) { } + constructor(private readonly entries: [StorageKey, Codec][]) {} queryStorage() { // eslint-disable-next-line @typescript-eslint/no-explicit-any const moduleIdToPropValue: Record> = {}; - this.entries.forEach(entry => { + this.entries.forEach((entry) => { const keyFrom = entry[0].args[0]?.toPrimitive() as string; const keyTo = entry[0].args[1]?.toPrimitive() as string; if (moduleIdToPropValue[keyFrom] === undefined) { moduleIdToPropValue[keyFrom] = {}; } moduleIdToPropValue[keyFrom][keyTo] = entry[1].toPrimitive() as string; - }); return moduleIdToPropValue; } } - export function parseAddress(valueRaw: Codec): DaoApplications | null { const value = valueRaw.toPrimitive(); const validated = DAO_APPLICATIONS_SCHEMA.safeParse(value); @@ -628,4 +677,3 @@ export const signData = async ( address, }; }; -