diff --git a/.commitlintrc.json b/.commitlintrc.json index cc3116586..08b772fed 100644 --- a/.commitlintrc.json +++ b/.commitlintrc.json @@ -15,6 +15,7 @@ "deployment", "certificate", "dx", + "config", "stats" ] ] diff --git a/.dockerignore b/.dockerignore index 9c820219e..68d7baa79 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,7 +12,6 @@ apps/indexer/.env* apps/landing/.env* apps/provider-console/.env* apps/provider-proxy/.env* -apps/stats-web/.env* **/.next *.md diff --git a/.github/workflows/docker-build-stats-web.yml b/.github/workflows/docker-build-stats-web.yml index 9d822b542..13c13133e 100644 --- a/.github/workflows/docker-build-stats-web.yml +++ b/.github/workflows/docker-build-stats-web.yml @@ -24,4 +24,4 @@ jobs: - name: Build the Docker image if: steps.filter.outputs.stats-web == 'true' - run: npm run dc:build -- stats-web + run: npm run dc:build -- --build-arg DEPLOYMENT_ENV=production stats-web diff --git a/apps/stats-web/.gitignore b/apps/stats-web/.gitignore index fd3dbb571..47fa2a149 100644 --- a/apps/stats-web/.gitignore +++ b/apps/stats-web/.gitignore @@ -15,6 +15,7 @@ # production /build +env-config.schema.js # misc .DS_Store diff --git a/apps/stats-web/env/.env.production b/apps/stats-web/env/.env.production new file mode 100644 index 000000000..dd649e84e --- /dev/null +++ b/apps/stats-web/env/.env.production @@ -0,0 +1,9 @@ +NEXT_PUBLIC_NODE_ENV=$NODE_ENV +NEXT_PUBLIC_BASE_API_MAINNET_URL=https://console-api.akash.network +NEXT_PUBLIC_BASE_API_SANDBOX_URL=https://console-api-sandbox.akash.network +NEXT_PUBLIC_BASE_API_TESTNET_URL=https://console-api-testnet.akash.network +NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_BASE_API_MAINNET_URL + +BASE_API_MAINNET_URL=$NEXT_PUBLIC_BASE_API_MAINNET_URL +BASE_API_TESTNET_URL=$NEXT_PUBLIC_BASE_API_TESTNET_URL +BASE_API_SANDBOX_URL=$NEXT_PUBLIC_BASE_API_SANDBOX_URL \ No newline at end of file diff --git a/apps/stats-web/env/.env.sample b/apps/stats-web/env/.env.sample new file mode 100644 index 000000000..0e09e65e8 --- /dev/null +++ b/apps/stats-web/env/.env.sample @@ -0,0 +1 @@ +NEXT_PUBLIC_API_BASE_URL= \ No newline at end of file diff --git a/apps/stats-web/env/.env.staging b/apps/stats-web/env/.env.staging new file mode 100644 index 000000000..3778aa132 --- /dev/null +++ b/apps/stats-web/env/.env.staging @@ -0,0 +1,9 @@ +NEXT_PUBLIC_NODE_ENV=$NODE_ENV +NEXT_PUBLIC_BASE_API_MAINNET_URL=https://console-api-mainnet-staging.akash.network +NEXT_PUBLIC_BASE_API_SANDBOX_URL=https://console-api-sandbox-staging.akash.network +NEXT_PUBLIC_BASE_API_TESTNET_URL=$NEXT_PUBLIC_BASE_API_MAINNET_URL +NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_BASE_API_MAINNET_URL + +BASE_API_MAINNET_URL=$NEXT_PUBLIC_BASE_API_MAINNET_URL +BASE_API_TESTNET_URL=$NEXT_PUBLIC_BASE_API_TESTNET_URL +BASE_API_SANDBOX_URL=$NEXT_PUBLIC_BASE_API_SANDBOX_URL \ No newline at end of file diff --git a/apps/stats-web/next.config.js b/apps/stats-web/next.config.js index 35ad76b7c..273c77080 100644 --- a/apps/stats-web/next.config.js +++ b/apps/stats-web/next.config.js @@ -1,5 +1,16 @@ +require("@akashnetwork/env-loader"); const { version } = require("./package.json"); +try { + const { browserEnvSchema } = require("./env-config.schema"); + + browserEnvSchema.parse(process.env); +} catch (error) { + if (error.message.includes("Cannot find module")) { + console.warn("No env-config.schema.js found, skipping env validation"); + } +} + /** @type {import('next').NextConfig} */ const nextConfig = { output: "standalone", diff --git a/apps/stats-web/package.json b/apps/stats-web/package.json index 6584bc329..c9a54267f 100644 --- a/apps/stats-web/package.json +++ b/apps/stats-web/package.json @@ -3,13 +3,15 @@ "version": "0.20.0", "private": true, "scripts": { - "build": "next build", + "build": "npm run build-env-schemas && next build", + "build-env-schemas": "tsc src/config/env-config.schema.ts --outDir . --skipLibCheck", "dev": "next dev", "format": "prettier --write ./*.{ts,js,json} **/*.{ts,tsx,js,json}", "lint": "eslint .", "start": "next start" }, "dependencies": { + "@akashnetwork/network-store": "*", "@akashnetwork/ui": "*", "@cosmjs/encoding": "^0.32.4", "@json2csv/plainjs": "^7.0.4", diff --git a/apps/stats-web/src/app/(home)/DashboardContainer.tsx b/apps/stats-web/src/app/(home)/DashboardContainer.tsx index 9d374e93d..0e5d985ea 100644 --- a/apps/stats-web/src/app/(home)/DashboardContainer.tsx +++ b/apps/stats-web/src/app/(home)/DashboardContainer.tsx @@ -5,14 +5,14 @@ import { Spinner } from "@akashnetwork/ui/components"; import { Dashboard } from "./Dashboard"; import { Title } from "@/components/Title"; -import { useSelectedNetwork } from "@/hooks/useSelectedNetwork"; import { useMarketData } from "@/queries"; import { useDashboardData } from "@/queries/useDashboardData"; +import { networkStore } from "@/store/network.store"; export const DashboardContainer: React.FunctionComponent = () => { const { data: dashboardData, isLoading: isLoadingDashboardData } = useDashboardData(); const { data: marketData, isLoading: isLoadingMarketData } = useMarketData(); - const selectedNetwork = useSelectedNetwork(); + const selectedNetwork = networkStore.useSelectedNetwork(); const isLoading = isLoadingMarketData || isLoadingDashboardData; return ( diff --git a/apps/stats-web/src/app/addresses/[address]/deployments/[dseq]/page.tsx b/apps/stats-web/src/app/addresses/[address]/deployments/[dseq]/page.tsx index 71f6cf4bf..0dac31a87 100644 --- a/apps/stats-web/src/app/addresses/[address]/deployments/[dseq]/page.tsx +++ b/apps/stats-web/src/app/addresses/[address]/deployments/[dseq]/page.tsx @@ -1,19 +1,28 @@ +import type { Network } from "@akashnetwork/network-store"; import { Metadata } from "next"; +import { z } from "zod"; import { DeploymentInfo } from "./DeploymentInfo"; import PageContainer from "@/components/PageContainer"; import { Title } from "@/components/Title"; -import { getNetworkBaseApiUrl } from "@/lib/constants"; +import { networkId } from "@/config/env-config.schema"; import { UrlService } from "@/lib/urlUtils"; +import { serverApiUrlService } from "@/services/api-url/server-api-url.service"; import { DeploymentDetail } from "@/types"; -interface IProps { - params: { address: string; dseq: string }; - searchParams: { [key: string]: string | string[] | undefined }; -} +const DeploymentDetailPageSchema = z.object({ + params: z.object({ + address: z.string(), + dseq: z.string() + }), + searchParams: z.object({ + network: networkId + }) +}); +type DeploymentDetailPageProps = z.infer; -export async function generateMetadata({ params: { address, dseq } }: IProps): Promise { +export async function generateMetadata({ params: { address, dseq } }: DeploymentDetailPageProps): Promise { const url = `https://stats.akash.network${UrlService.deployment(address, dseq)}`; return { @@ -27,8 +36,8 @@ export async function generateMetadata({ params: { address, dseq } }: IProps): P }; } -async function fetchDeploymentData(address: string, dseq: string, network: string): Promise { - const apiUrl = getNetworkBaseApiUrl(network); +async function fetchDeploymentData(address: string, dseq: string, network: Network["id"]): Promise { + const apiUrl = serverApiUrlService.getBaseApiUrlFor(network); const response = await fetch(`${apiUrl}/v1/deployment/${address}/${dseq}`); if (!response.ok) { @@ -39,8 +48,12 @@ async function fetchDeploymentData(address: string, dseq: string, network: strin return response.json(); } -export default async function DeploymentDetailPage({ params: { address, dseq }, searchParams: { network } }: IProps) { - const deployment = await fetchDeploymentData(address, dseq, network as string); +export default async function DeploymentDetailPage(props: DeploymentDetailPageProps) { + const { + params: { address, dseq }, + searchParams: { network } + } = DeploymentDetailPageSchema.parse(props); + const deployment = await fetchDeploymentData(address, dseq, network); return ( diff --git a/apps/stats-web/src/app/addresses/[address]/page.tsx b/apps/stats-web/src/app/addresses/[address]/page.tsx index ba0464822..2f26909cb 100644 --- a/apps/stats-web/src/app/addresses/[address]/page.tsx +++ b/apps/stats-web/src/app/addresses/[address]/page.tsx @@ -1,4 +1,6 @@ -import { Metadata } from "next"; +import type { Network } from "@akashnetwork/network-store"; +import type { Metadata } from "next"; +import { z } from "zod"; import { AddressInfo } from "./AddressInfo"; import AddressLayout from "./AddressLayout"; @@ -7,16 +9,23 @@ import { AssetList } from "./AssetList"; import { LatestTransactions } from "./LatestTransactions"; import { Title } from "@/components/Title"; -import { getNetworkBaseApiUrl } from "@/lib/constants"; +import { networkId } from "@/config/env-config.schema"; import { UrlService } from "@/lib/urlUtils"; +import { serverApiUrlService } from "@/services/api-url/server-api-url.service"; import { AddressDetail } from "@/types"; -interface IProps { - params: { address: string }; - searchParams: { [key: string]: string | string[] | undefined }; -} +const AddressDetailPageSchema = z.object({ + params: z.object({ + address: z.string(), + dseq: z.string() + }), + searchParams: z.object({ + network: networkId + }) +}); +type AddressDetailPageProps = z.infer; -export async function generateMetadata({ params: { address } }: IProps): Promise { +export async function generateMetadata({ params: { address } }: AddressDetailPageProps): Promise { const url = `https://stats.akash.network${UrlService.address(address)}`; return { @@ -30,8 +39,8 @@ export async function generateMetadata({ params: { address } }: IProps): Promise }; } -async function fetchAddressData(address: string, network: string): Promise { - const apiUrl = getNetworkBaseApiUrl(network); +async function fetchAddressData(address: string, network: Network["id"]): Promise { + const apiUrl = serverApiUrlService.getBaseApiUrlFor(network); const response = await fetch(`${apiUrl}/v1/addresses/${address}`); if (!response.ok) { @@ -42,8 +51,12 @@ async function fetchAddressData(address: string, network: string): Promise diff --git a/apps/stats-web/src/app/blocks/[height]/page.tsx b/apps/stats-web/src/app/blocks/[height]/page.tsx index 7f36b23d9..16955584d 100644 --- a/apps/stats-web/src/app/blocks/[height]/page.tsx +++ b/apps/stats-web/src/app/blocks/[height]/page.tsx @@ -1,28 +1,36 @@ +import type { Network } from "@akashnetwork/network-store"; import { Card, CardContent, Table, TableBody, TableHead, TableHeader, TableRow } from "@akashnetwork/ui/components"; import { SearchX } from "lucide-react"; -import { Metadata } from "next"; +import type { Metadata } from "next"; +import { z } from "zod"; import { BlockInfo } from "./BlockInfo"; import { TransactionRow } from "@/components/blockchain/TransactionRow"; import PageContainer from "@/components/PageContainer"; import { Title } from "@/components/Title"; -import { getNetworkBaseApiUrl } from "@/lib/constants"; +import { networkId } from "@/config/env-config.schema"; +import { serverApiUrlService } from "@/services/api-url/server-api-url.service"; import { BlockDetail } from "@/types"; -interface IProps { - params: { height: string }; - searchParams: { [key: string]: string | string[] | undefined }; -} +const BlockDetailPageSchema = z.object({ + params: z.object({ + height: z.string() + }), + searchParams: z.object({ + network: networkId + }) +}); +type BlockDetailPageProps = z.infer; -export async function generateMetadata({ params: { height } }: IProps): Promise { +export async function generateMetadata({ params: { height } }: BlockDetailPageProps): Promise { return { title: `Block #${height}` }; } -async function fetchBlockData(height: string, network: string): Promise { - const apiUrl = getNetworkBaseApiUrl(network); +async function fetchBlockData(height: string, network: Network["id"]): Promise { + const apiUrl = serverApiUrlService.getBaseApiUrlFor(network); const response = await fetch(`${apiUrl}/v1/blocks/${height}`); if (!response.ok) { @@ -33,8 +41,12 @@ async function fetchBlockData(height: string, network: string): Promise diff --git a/apps/stats-web/src/app/graph/[snapshot]/GraphContainer.tsx b/apps/stats-web/src/app/graph/[snapshot]/GraphContainer.tsx index bf73b139b..72e1c7ad0 100644 --- a/apps/stats-web/src/app/graph/[snapshot]/GraphContainer.tsx +++ b/apps/stats-web/src/app/graph/[snapshot]/GraphContainer.tsx @@ -9,7 +9,7 @@ import dynamic from "next/dynamic"; import { DiffNumber } from "@/components/DiffNumber"; import { DiffPercentageChip } from "@/components/DiffPercentageChip"; import { TimeRange } from "@/components/graph/TimeRange"; -import { selectedRangeValues } from "@/lib/constants"; +import { SELECTED_RANGE_VALUES } from "@/config/date.config"; import { percIncrease, udenomToDenom } from "@/lib/mathHelpers"; import { SNAPSHOT_NOT_FOUND } from "@/lib/snapshotsUrlHelpers"; import { bytesToShrink } from "@/lib/unitUtils"; @@ -25,7 +25,7 @@ export interface IGraphProps { } export default function GraphContainer({ snapshot }: IGraphProps) { - const [selectedRange, setSelectedRange] = useState(selectedRangeValues["7D"]); + const [selectedRange, setSelectedRange] = useState(SELECTED_RANGE_VALUES["7D"]); const { data: snapshotData, status } = useGraphSnapshot(snapshot); const snapshotMetadata = snapshotData && getSnapshotMetadata(snapshot as Snapshots); const rangedData = snapshotData && snapshotData.snapshots.slice(Math.max(snapshotData.snapshots.length - selectedRange, 0), snapshotData.snapshots.length); @@ -50,7 +50,7 @@ export default function GraphContainer({ snapshot }: IGraphProps) { const csvContent = parser.parse(rangedData.map(d => ({ date: d.date, value: snapshotMetadata.unitFn(d.value).value }))); const datePart = new Date().toISOString().substring(0, 10).replaceAll("-", ""); - const rangePart = Object.keys(selectedRangeValues).find(key => selectedRangeValues[key] === selectedRange); + const rangePart = Object.keys(SELECTED_RANGE_VALUES).find(key => SELECTED_RANGE_VALUES[key] === selectedRange); const fileName = `${snapshot}-${datePart}-${rangePart}.csv`; const encodedUri = encodeURI("data:text/csv;charset=utf-8," + csvContent); diff --git a/apps/stats-web/src/app/provider-graph/[snapshot]/GraphContainer.tsx b/apps/stats-web/src/app/provider-graph/[snapshot]/GraphContainer.tsx index ecec6a618..4d4234525 100644 --- a/apps/stats-web/src/app/provider-graph/[snapshot]/GraphContainer.tsx +++ b/apps/stats-web/src/app/provider-graph/[snapshot]/GraphContainer.tsx @@ -9,7 +9,7 @@ import dynamic from "next/dynamic"; import { DiffNumber } from "@/components/DiffNumber"; import { DiffPercentageChip } from "@/components/DiffPercentageChip"; import { TimeRange } from "@/components/graph/TimeRange"; -import { selectedRangeValues } from "@/lib/constants"; +import { SELECTED_RANGE_VALUES } from "@/config/date.config"; import { percIncrease } from "@/lib/mathHelpers"; import { getProviderSnapshotMetadata } from "@/lib/providerUtils"; import { SNAPSHOT_NOT_FOUND } from "@/lib/snapshotsUrlHelpers"; @@ -25,7 +25,7 @@ export interface IGraphProps { } export default function GraphContainer({ snapshot }: IGraphProps) { - const [selectedRange, setSelectedRange] = useState(selectedRangeValues["7D"]); + const [selectedRange, setSelectedRange] = useState(SELECTED_RANGE_VALUES["7D"]); const { data: snapshotData, status } = useProviderGraphSnapshot(snapshot); const snapshotMetadata = snapshotData && getProviderSnapshotMetadata(snapshot as ProviderSnapshots); const rangedData = snapshotData && snapshotData.snapshots.slice(Math.max(snapshotData.snapshots.length - selectedRange, 0), snapshotData.snapshots.length); @@ -50,7 +50,7 @@ export default function GraphContainer({ snapshot }: IGraphProps) { const csvContent = parser.parse(rangedData.map(d => ({ date: d.date, value: snapshotMetadata.unitFn(d.value).value }))); const datePart = new Date().toISOString().substring(0, 10).replaceAll("-", ""); - const rangePart = Object.keys(selectedRangeValues).find(key => selectedRangeValues[key] === selectedRange); + const rangePart = Object.keys(SELECTED_RANGE_VALUES).find(key => SELECTED_RANGE_VALUES[key] === selectedRange); const fileName = `${snapshot}-${datePart}-${rangePart}.csv`; const encodedUri = encodeURI("data:text/csv;charset=utf-8," + csvContent); diff --git a/apps/stats-web/src/app/transactions/[hash]/page.tsx b/apps/stats-web/src/app/transactions/[hash]/page.tsx index f63fd61ec..76bc605b6 100644 --- a/apps/stats-web/src/app/transactions/[hash]/page.tsx +++ b/apps/stats-web/src/app/transactions/[hash]/page.tsx @@ -1,35 +1,44 @@ import React from "react"; +import type { Network } from "@akashnetwork/network-store"; import { Alert, Card, CardContent } from "@akashnetwork/ui/components"; -import { Metadata } from "next"; +import type { Metadata } from "next"; +import { z } from "zod"; import { TransactionInfo } from "./TransactionInfo"; import PageContainer from "@/components/PageContainer"; import { Title } from "@/components/Title"; import { TxMessageRow } from "@/components/transactions/TxMessageRow"; +import { networkId } from "@/config/env-config.schema"; import { getSplitText } from "@/hooks/useShortText"; -import { getNetworkBaseApiUrl } from "@/lib/constants"; +import { serverApiUrlService } from "@/services/api-url/server-api-url.service"; import { TransactionDetail } from "@/types"; -interface IProps { - params: { hash: string }; - searchParams: { [key: string]: string | string[] | undefined }; -} +const TransactionDetailPageSchema = z.object({ + params: z.object({ + hash: z.string() + }), + searchParams: z.object({ + network: networkId + }) +}); +type TransactionDetailPageProps = z.infer; -export async function generateMetadata({ params: { hash } }: IProps): Promise { - const splittedTxHash = getSplitText(hash, 6, 6); +export async function generateMetadata({ params: { hash } }: TransactionDetailPageProps): Promise { + const splitTxHash = getSplitText(hash, 6, 6); return { - title: `Tx ${splittedTxHash}` + title: `Tx ${splitTxHash}` }; } -async function fetchTransactionData(hash: string, network: string): Promise { - const apiUrl = getNetworkBaseApiUrl(network); +async function fetchTransactionData(hash: string, network: Network["id"]): Promise { + const apiUrl = serverApiUrlService.getBaseApiUrlFor(network); + console.log("DEBUG apiUrl", apiUrl); const response = await fetch(`${apiUrl}/v1/transactions/${hash}`); if (!response.ok && response.status !== 404) { // This will activate the closest `error.js` Error Boundary - throw new Error("Error fetching transction data"); + throw new Error("Error fetching transaction data"); } else if (response.status === 404) { return null; } @@ -37,8 +46,12 @@ async function fetchTransactionData(hash: string, network: string): Promise diff --git a/apps/stats-web/src/app/validators/[address]/page.tsx b/apps/stats-web/src/app/validators/[address]/page.tsx index 079793141..4a0f68b38 100644 --- a/apps/stats-web/src/app/validators/[address]/page.tsx +++ b/apps/stats-web/src/app/validators/[address]/page.tsx @@ -1,21 +1,29 @@ -import { Metadata } from "next"; +import type { Network } from "@akashnetwork/network-store"; +import type { Metadata } from "next"; +import { z } from "zod"; import { ValidatorsInfo } from "./ValidatorInfo"; import PageContainer from "@/components/PageContainer"; import { Title } from "@/components/Title"; -import { getNetworkBaseApiUrl } from "@/lib/constants"; +import { networkId } from "@/config/env-config.schema"; import { UrlService } from "@/lib/urlUtils"; +import { serverApiUrlService } from "@/services/api-url/server-api-url.service"; import { ValidatorDetail } from "@/types"; -interface IProps { - params: { address: string }; - searchParams: { [key: string]: string | string[] | undefined }; -} +const ValidatorDetailPageSchema = z.object({ + params: z.object({ + address: z.string() + }), + searchParams: z.object({ + network: networkId + }) +}); +type ValidatorDetailPageProps = z.infer; -export async function generateMetadata({ params: { address }, searchParams: { network } }: IProps): Promise { +export async function generateMetadata({ params: { address }, searchParams: { network } }: ValidatorDetailPageProps): Promise { const url = `https://stats.akash.network${UrlService.validator(address)}`; - const apiUrl = getNetworkBaseApiUrl(network as string); + const apiUrl = serverApiUrlService.getBaseApiUrlFor(network); const response = await fetch(`${apiUrl}/v1/validators/${address}`); const data = (await response.json()) as ValidatorDetail; @@ -30,8 +38,8 @@ export async function generateMetadata({ params: { address }, searchParams: { ne }; } -async function fetchValidatorData(address: string, network: string): Promise { - const apiUrl = getNetworkBaseApiUrl(network); +async function fetchValidatorData(address: string, network: Network["id"]): Promise { + const apiUrl = serverApiUrlService.getBaseApiUrlFor(network); const response = await fetch(`${apiUrl}/v1/validators/${address}`); if (!response.ok) { @@ -42,8 +50,12 @@ async function fetchValidatorData(address: string, network: string): Promise diff --git a/apps/stats-web/src/components/graph/TimeRange.tsx b/apps/stats-web/src/components/graph/TimeRange.tsx index f1bd6ec76..aae2d5a4b 100644 --- a/apps/stats-web/src/components/graph/TimeRange.tsx +++ b/apps/stats-web/src/components/graph/TimeRange.tsx @@ -2,7 +2,7 @@ import { ReactNode } from "react"; import { ToggleGroup, ToggleGroupItem } from "@akashnetwork/ui/components"; -import { selectedRangeValues } from "@/lib/constants"; +import { SELECTED_RANGE_VALUES } from "@/config/date.config"; import { cn } from "@/lib/utils"; type Props = { @@ -20,24 +20,24 @@ export const TimeRange: React.FunctionComponent = ({ selectedRange, onRan _onRangeChange(selectedRangeValues["7D"])} + className={cn({ ["!bg-primary font-bold !text-white"]: selectedRange === SELECTED_RANGE_VALUES["7D"] })} + onClick={() => _onRangeChange(SELECTED_RANGE_VALUES["7D"])} size="sm" > 7D _onRangeChange(selectedRangeValues["1M"])} + className={cn({ ["!bg-primary font-bold !text-white"]: selectedRange === SELECTED_RANGE_VALUES["1M"] })} + onClick={() => _onRangeChange(SELECTED_RANGE_VALUES["1M"])} size="sm" > 1M _onRangeChange(selectedRangeValues["ALL"])} + className={cn({ ["!bg-primary font-bold !text-white"]: selectedRange === SELECTED_RANGE_VALUES["ALL"] })} + onClick={() => _onRangeChange(SELECTED_RANGE_VALUES["ALL"])} size="sm" > ALL diff --git a/apps/stats-web/src/components/layout/CustomGoogleAnalytics.tsx b/apps/stats-web/src/components/layout/CustomGoogleAnalytics.tsx index 18299f77f..e9c65409c 100644 --- a/apps/stats-web/src/components/layout/CustomGoogleAnalytics.tsx +++ b/apps/stats-web/src/components/layout/CustomGoogleAnalytics.tsx @@ -3,7 +3,7 @@ import { useReportWebVitals } from "next/web-vitals"; import { event, GoogleAnalytics as GAnalytics } from "nextjs-google-analytics"; -import { isProd } from "@/lib/constants"; +import { browserEnvConfig } from "@/config/browser-env.config"; export default function GoogleAnalytics() { useReportWebVitals(({ id, name, label, value }) => { @@ -15,5 +15,5 @@ export default function GoogleAnalytics() { }); }); - return <>{isProd && }; + return <>{browserEnvConfig.NEXT_PUBLIC_NODE_ENV === "production" && }; } diff --git a/apps/stats-web/src/components/layout/CustomProviders.tsx b/apps/stats-web/src/components/layout/CustomProviders.tsx index 6bd86fe45..a330a52c8 100644 --- a/apps/stats-web/src/components/layout/CustomProviders.tsx +++ b/apps/stats-web/src/components/layout/CustomProviders.tsx @@ -12,12 +12,13 @@ import { CustomIntlProvider } from "./CustomIntlProvider"; import { PricingProvider } from "@/context/PricingProvider"; import { customColors } from "@/lib/colors"; import { queryClient } from "@/queries"; +import { store } from "@/store/global.store"; function Providers({ children }: React.PropsWithChildren) { return ( - + diff --git a/apps/stats-web/src/components/layout/NetworkSelect.tsx b/apps/stats-web/src/components/layout/NetworkSelect.tsx index 9e81c351a..c2f57fe03 100644 --- a/apps/stats-web/src/components/layout/NetworkSelect.tsx +++ b/apps/stats-web/src/components/layout/NetworkSelect.tsx @@ -1,49 +1,22 @@ "use client"; -import React, { useEffect, useState } from "react"; +import React from "react"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, Spinner } from "@akashnetwork/ui/components"; -import { mainnetId, setNetworkVersion } from "@/lib/constants"; import { cn } from "@/lib/utils"; -import { initiateNetworkData, networks } from "@/store/networkStore"; +import { networkStore } from "@/store/network.store"; interface NetworkSelectProps { className?: string; } const NetworkSelect: React.FC = ({ className }) => { - const [isLoadingSettings, setIsLoadingSettings] = useState(true); - const [selectedNetworkId, setSelectedNetworkId] = useState(mainnetId); - - useEffect(() => { - async function init() { - await initiateNetworkData(); - setNetworkVersion(); - - const selectedNetworkId = localStorage.getItem("selectedNetworkId"); - if (selectedNetworkId) { - setSelectedNetworkId(selectedNetworkId); - } - - setIsLoadingSettings(false); - } - - init(); - }, []); - - const onSelectNetworkChange = (networkId: string) => { - setSelectedNetworkId(networkId); - - // Set in the settings and local storage - localStorage.setItem("selectedNetworkId", networkId); - // Reset the ui to reload the settings for the currently selected network - - location.reload(); - }; + const [{ isLoading: isLoadingNetworks, data: networks }] = networkStore.useNetworksStore(); + const [selectedNetworkId, setSelectedNetworkId] = networkStore.useSelectedNetworkIdStore({ reloadOnChange: true }); return ( - - {isLoadingSettings && } + {isLoadingNetworks && } diff --git a/apps/stats-web/src/config/browser-env.config.ts b/apps/stats-web/src/config/browser-env.config.ts new file mode 100644 index 000000000..903fec82c --- /dev/null +++ b/apps/stats-web/src/config/browser-env.config.ts @@ -0,0 +1,10 @@ +import { validateStaticEnvVars } from "./env-config.schema"; + +export const browserEnvConfig = validateStaticEnvVars({ + NEXT_PUBLIC_DEFAULT_NETWORK_ID: process.env.NEXT_PUBLIC_DEFAULT_NETWORK_ID, + NEXT_PUBLIC_API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL, + NEXT_PUBLIC_NODE_ENV: process.env.NEXT_PUBLIC_NODE_ENV, + NEXT_PUBLIC_BASE_API_TESTNET_URL: process.env.NEXT_PUBLIC_BASE_API_TESTNET_URL, + NEXT_PUBLIC_BASE_API_SANDBOX_URL: process.env.NEXT_PUBLIC_BASE_API_SANDBOX_URL, + NEXT_PUBLIC_BASE_API_MAINNET_URL: process.env.NEXT_PUBLIC_BASE_API_MAINNET_URL +}); diff --git a/apps/stats-web/src/config/date.config.ts b/apps/stats-web/src/config/date.config.ts new file mode 100644 index 000000000..de6acce2e --- /dev/null +++ b/apps/stats-web/src/config/date.config.ts @@ -0,0 +1,5 @@ +export const SELECTED_RANGE_VALUES: Record = { + "7D": 7, + "1M": 30, + ALL: Number.MAX_SAFE_INTEGER +}; diff --git a/apps/stats-web/src/config/denom.config.ts b/apps/stats-web/src/config/denom.config.ts new file mode 100644 index 000000000..eb079e6ba --- /dev/null +++ b/apps/stats-web/src/config/denom.config.ts @@ -0,0 +1,8 @@ +import type { MainnetNetworkId, SandboxNetworkId } from "@akashnetwork/akashjs/build/types/network"; +import { MAINNET_ID, SANDBOX_ID } from "@akashnetwork/network-store"; + +export const UAKT_DENOM = "uakt"; +export const USDC_IBC_DENOMS: Record = { + [MAINNET_ID]: "ibc/170C677610AC31DF0904FFE09CD3B5C657492170E7E52372E48756B71E56F2F1", + [SANDBOX_ID]: "ibc/12C6A0C374171B595A0A9E18B83FA09D295FB1F2D8C6DAA3AC28683471752D84" +}; diff --git a/apps/stats-web/src/config/env-config.schema.ts b/apps/stats-web/src/config/env-config.schema.ts new file mode 100644 index 000000000..1cb77e54f --- /dev/null +++ b/apps/stats-web/src/config/env-config.schema.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; + +export const networkId = z.enum(["mainnet", "sandbox", "testnet"]); +const coercedBoolean = () => z.enum(["true", "false"]).transform(val => val === "true"); + +export const browserEnvSchema = z.object({ + NEXT_PUBLIC_DEFAULT_NETWORK_ID: networkId.optional().default("mainnet"), + NEXT_PUBLIC_API_BASE_URL: z.string().url(), + NEXT_PUBLIC_NODE_ENV: z.enum(["development", "production", "test"]).optional().default("development"), + NEXT_PUBLIC_BASE_API_TESTNET_URL: z.string().url(), + NEXT_PUBLIC_BASE_API_SANDBOX_URL: z.string().url(), + NEXT_PUBLIC_BASE_API_MAINNET_URL: z.string().url() +}); + +export const serverEnvSchema = browserEnvSchema.extend({ + MAINTENANCE_MODE: coercedBoolean().optional().default("false"), + BASE_API_MAINNET_URL: z.string().url(), + BASE_API_TESTNET_URL: z.string().url(), + BASE_API_SANDBOX_URL: z.string().url() +}); + +export type BrowserEnvConfig = z.infer; +export type ServerEnvConfig = z.infer; + +export const validateStaticEnvVars = (config: Record) => browserEnvSchema.parse(config); +export const validateRuntimeEnvVars = (config: Record) => { + if (process.env.NEXT_PHASE === "phase-production-build") { + console.log("Skipping validation of serverEnvConfig during build"); + return config as ServerEnvConfig; + } else { + return serverEnvSchema.parse(config); + } +}; diff --git a/apps/stats-web/src/config/server-env.config.ts b/apps/stats-web/src/config/server-env.config.ts new file mode 100644 index 000000000..1b85264cc --- /dev/null +++ b/apps/stats-web/src/config/server-env.config.ts @@ -0,0 +1,5 @@ +import "@akashnetwork/env-loader"; + +import { validateRuntimeEnvVars } from "./env-config.schema"; + +export const serverEnvConfig = validateRuntimeEnvVars(process.env); diff --git a/apps/stats-web/src/context/PricingProvider/PricingProvider.tsx b/apps/stats-web/src/context/PricingProvider/PricingProvider.tsx index bfc47ef9a..78347b0a3 100644 --- a/apps/stats-web/src/context/PricingProvider/PricingProvider.tsx +++ b/apps/stats-web/src/context/PricingProvider/PricingProvider.tsx @@ -2,8 +2,8 @@ import React from "react"; +import { UAKT_DENOM } from "@/config/denom.config"; import { useUsdcDenom } from "@/hooks/useDenom"; -import { uAktDenom } from "@/lib/constants"; import { roundDecimal } from "@/lib/mathHelpers"; import { useMarketData } from "@/queries"; @@ -42,7 +42,7 @@ export const PricingProvider: React.FC = ({ children }) => { const getPriceForDenom = (denom: string): number => { switch (denom) { - case uAktDenom: + case UAKT_DENOM: return marketData?.price || 0; case usdcIbcDenom: return 1; // TODO Get price from API diff --git a/apps/stats-web/src/hooks/useDenom.ts b/apps/stats-web/src/hooks/useDenom.ts index dde453160..d53706f8f 100644 --- a/apps/stats-web/src/hooks/useDenom.ts +++ b/apps/stats-web/src/hooks/useDenom.ts @@ -1,22 +1,7 @@ -import { getSelectedNetwork, useSelectedNetwork } from "./useSelectedNetwork"; - -import { usdcIbcDenoms } from "@/lib/constants"; +import { USDC_IBC_DENOMS } from "@/config/denom.config"; +import { networkStore } from "@/store/network.store"; export const useUsdcDenom = () => { - const selectedNetwork = useSelectedNetwork(); - return usdcIbcDenoms[selectedNetwork.id]; -}; - -export const getUsdcDenom = () => { - const selectedNetwork = getSelectedNetwork(); - return usdcIbcDenoms[selectedNetwork.id as any]; -}; - -export const useSdlDenoms = () => { - const usdcDenom = useUsdcDenom(); - - return [ - { id: "uakt", label: "uAKT", tokenLabel: "AKT", value: "uakt" }, - { id: "uusdc", label: "uUSDC", tokenLabel: "USDC", value: usdcDenom } - ]; + const selectedNetworkId = networkStore.useSelectedNetworkId(); + return USDC_IBC_DENOMS[selectedNetworkId]; }; diff --git a/apps/stats-web/src/hooks/useSelectedNetwork.ts b/apps/stats-web/src/hooks/useSelectedNetwork.ts deleted file mode 100644 index 44c1f4e42..000000000 --- a/apps/stats-web/src/hooks/useSelectedNetwork.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useAtom } from "jotai"; -import { useEffectOnce } from "usehooks-ts"; - -import { mainnetId } from "@/lib/constants"; -import networkStore, { networks } from "@/store/networkStore"; - -export const getSelectedNetwork = () => { - const selectedNetworkId = localStorage.getItem("selectedNetworkId") ?? mainnetId; - const selectedNetwork = networks.find(n => n.id === selectedNetworkId); - - // return mainnet if selected network is not found - return selectedNetwork ?? networks[0]; -}; - -export const useSelectedNetwork = () => { - const [selectedNetwork, setSelectedNetwork] = useAtom(networkStore.selectedNetwork); - - useEffectOnce(() => { - const selectedNetworkId = localStorage.getItem("selectedNetworkId") ?? mainnetId; - setSelectedNetwork(networks.find(n => n.id === selectedNetworkId) || networks[0]); - }); - - return selectedNetwork ?? networks[0]; -}; diff --git a/apps/stats-web/src/lib/apiUtils.ts b/apps/stats-web/src/lib/apiUtils.ts index 87c6a5739..484ee4585 100644 --- a/apps/stats-web/src/lib/apiUtils.ts +++ b/apps/stats-web/src/lib/apiUtils.ts @@ -1,87 +1,57 @@ -import axios from "axios"; - -import { BASE_API_URL } from "./constants"; import { appendSearchParams } from "./urlUtils"; +import { browserApiUrlService } from "@/services/api-url/browser-api-url.service"; +import { networkStore } from "@/store/network.store"; + export class ApiUrlService { static dashboardData() { - return `${BASE_API_URL}/v1/dashboard-data`; + return `${this.baseApiUrl}/v1/dashboard-data`; } static marketData() { - return `${BASE_API_URL}/v1/market-data`; + return `${this.baseApiUrl}/v1/market-data`; } static proposals() { - return `${BASE_API_URL}/v1/proposals`; + return `${this.baseApiUrl}/v1/proposals`; } static validators() { - return `${BASE_API_URL}/v1/validators`; + return `${this.baseApiUrl}/v1/validators`; } static transactions(limit: number) { - return `${BASE_API_URL}/v1/transactions${appendSearchParams({ limit })}`; + return `${this.baseApiUrl}/v1/transactions${appendSearchParams({ limit })}`; } static addressTransactions(address: string, skip: number, limit: number) { - return `${BASE_API_URL}/v1/addresses/${address}/transactions/${skip}/${limit}`; + return `${this.baseApiUrl}/v1/addresses/${address}/transactions/${skip}/${limit}`; } static addressDeployments(address: string, skip: number, limit: number, reverseSorting: boolean, filters: { [key: string]: string }) { - return `${BASE_API_URL}/v1/addresses/${address}/deployments/${skip}/${limit}${appendSearchParams({ reverseSorting, ...filters })}`; + return `${this.baseApiUrl}/v1/addresses/${address}/deployments/${skip}/${limit}${appendSearchParams({ reverseSorting, ...filters })}`; } static graphData(snapshot: string) { - return `${BASE_API_URL}/v1/graph-data/${snapshot}`; + return `${this.baseApiUrl}/v1/graph-data/${snapshot}`; } static providerGraphData(snapshot: string) { - return `${BASE_API_URL}/v1/provider-graph-data/${snapshot}`; + return `${this.baseApiUrl}/v1/provider-graph-data/${snapshot}`; } static blocks(limit: number) { - return `${BASE_API_URL}/v1/blocks${appendSearchParams({ limit })}`; + return `${this.baseApiUrl}/v1/blocks${appendSearchParams({ limit })}`; } static providerAttributesSchema() { - return `${BASE_API_URL}/v1/provider-attributes-schema`; + return `${this.baseApiUrl}/v1/provider-attributes-schema`; } static networkCapacity() { - return `${BASE_API_URL}/v1/network-capacity`; + return `${this.baseApiUrl}/v1/network-capacity`; } static mainnetVersion() { - return `${BASE_API_URL}/v1/version/mainnet`; + return `${this.baseApiUrl}/v1/version/mainnet`; } static testnetVersion() { - return `${BASE_API_URL}/v1/version/testnet`; + return `${this.baseApiUrl}/v1/version/testnet`; } static sandboxVersion() { - return `${BASE_API_URL}/v1/version/sandbox`; + return `${this.baseApiUrl}/v1/version/sandbox`; } -} - -export async function loadWithPagination(baseUrl: string, dataKey: string, limit: number) { - let items: any[] = []; - let nextKey = null; - // let callCount = 1; - // let totalCount = null; - - do { - const _hasQueryParam = hasQueryParam(baseUrl); - let queryUrl = `${baseUrl}${_hasQueryParam ? "&" : "?"}pagination.limit=${limit}&pagination.count_total=true`; - if (nextKey) { - queryUrl += "&pagination.key=" + encodeURIComponent(nextKey); - } - // console.log(`Querying ${dataKey} [${callCount}] from : ${queryUrl}`); - const response = await axios.get(queryUrl); - const data = response.data; - - // if (!nextKey) { - // totalCount = data.pagination.total; - // } - items = items.concat(data[dataKey]); - nextKey = data.pagination.next_key; - // callCount++; - - // console.log(`Got ${items.length} of ${totalCount}`); - } while (nextKey); - - return items.filter(item => item); -} - -function hasQueryParam(url: string) { - return /[?&]/gm.test(url); + static get baseApiUrl() { + return browserApiUrlService.getBaseApiUrlFor(networkStore.selectedNetworkId); + } } diff --git a/apps/stats-web/src/lib/constants.ts b/apps/stats-web/src/lib/constants.ts deleted file mode 100644 index 3e2a46ed8..000000000 --- a/apps/stats-web/src/lib/constants.ts +++ /dev/null @@ -1,102 +0,0 @@ -export const mainnetId = "mainnet"; -export const testnetId = "testnet"; -export const sandboxId = "sandbox"; - -export const selectedRangeValues: { [key: string]: number } = { - "7D": 7, - "1M": 30, - ALL: Number.MAX_SAFE_INTEGER -}; - -const productionMainnetApiUrl = "https://console-api.akash.network"; -const productionTestnetApiUrl = "https://console-api-testnet.akash.network"; -const productionSandboxApiUrl = "https://console-api-sandbox.akash.network"; -const productionHostnames = ["stats.akash.network"]; - -export const isProd = process.env.NODE_ENV === "production"; -export const isMaintenanceMode = process.env.MAINTENANCE_MODE === "true"; -export const BASE_API_MAINNET_URL = getApiMainnetUrl(); -export const BASE_API_TESTNET_URL = getApiTestnetUrl(); -export const BASE_API_SANDBOX_URL = getApiSandboxUrl(); - -export const BASE_API_URL = getApiUrl(); - -export function getNetworkBaseApiUrl(network: string | null) { - switch (network) { - case testnetId: - return BASE_API_TESTNET_URL; - case sandboxId: - return BASE_API_SANDBOX_URL; - default: - return BASE_API_MAINNET_URL; - } -} - -export const uAktDenom = "uakt"; -export const usdcIbcDenoms: { [key: string]: string } = { - [mainnetId]: "ibc/170C677610AC31DF0904FFE09CD3B5C657492170E7E52372E48756B71E56F2F1", - [sandboxId]: "ibc/12C6A0C374171B595A0A9E18B83FA09D295FB1F2D8C6DAA3AC28683471752D84" -}; - -function getApiMainnetUrl() { - if (process.env.API_MAINNET_BASE_URL) return process.env.API_MAINNET_BASE_URL; - if (typeof window === "undefined") return "http://localhost:3080"; - if (productionHostnames.includes(window.location?.hostname)) return productionMainnetApiUrl; - return "http://localhost:3080"; -} - -function getApiTestnetUrl() { - if (process.env.API_TESTNET_BASE_URL) return process.env.API_TESTNET_BASE_URL; - if (typeof window === "undefined") return "http://localhost:3080"; - if (productionHostnames.includes(window.location?.hostname)) return productionTestnetApiUrl; - return "http://localhost:3080"; -} - -function getApiSandboxUrl() { - if (process.env.API_SANDBOX_BASE_URL) return process.env.API_SANDBOX_BASE_URL; - if (typeof window === "undefined") return "http://localhost:3080"; - if (productionHostnames.includes(window.location?.hostname)) return productionSandboxApiUrl; - return "http://localhost:3080"; -} - -function getApiUrl() { - if (process.env.API_BASE_URL) return process.env.API_BASE_URL; - if (typeof window === "undefined") return "http://localhost:3080"; - if (productionHostnames.includes(window.location?.hostname)) { - try { - const _selectedNetworkId = localStorage.getItem("selectedNetworkId"); - return getNetworkBaseApiUrl(_selectedNetworkId); - } catch (e) { - console.error(e); - return productionMainnetApiUrl; - } - } - return "http://localhost:3080"; -} - -export let selectedNetworkId = ""; -export let networkVersion: "v1beta2" | "v1beta3"; - -export function setNetworkVersion() { - const _selectedNetworkId = localStorage.getItem("selectedNetworkId"); - - switch (_selectedNetworkId) { - case mainnetId: - networkVersion = "v1beta3"; - selectedNetworkId = mainnetId; - break; - case testnetId: - networkVersion = "v1beta3"; - selectedNetworkId = testnetId; - break; - case sandboxId: - networkVersion = "v1beta3"; - selectedNetworkId = sandboxId; - break; - - default: - networkVersion = "v1beta3"; - selectedNetworkId = mainnetId; - break; - } -} diff --git a/apps/stats-web/src/lib/urlUtils.ts b/apps/stats-web/src/lib/urlUtils.ts index d57940e4f..d6e070404 100644 --- a/apps/stats-web/src/lib/urlUtils.ts +++ b/apps/stats-web/src/lib/urlUtils.ts @@ -1,30 +1,20 @@ -import { selectedNetworkId } from "./constants"; - -function getSelectedNetworkQueryParam() { - if (selectedNetworkId) { - return selectedNetworkId; - } else if (typeof window !== "undefined") { - return new URLSearchParams(window.location.search).get("network"); - } - - return undefined; -} +import { networkStore } from "@/store/network.store"; export class UrlService { static home = () => "/"; static graph = (snapshot: string) => `/graph/${snapshot}`; static providerGraph = (snapshot: string) => `/provider-graph/${snapshot}`; static blocks = () => `/blocks`; - static block = (height: number) => `/blocks/${height}${appendSearchParams({ network: getSelectedNetworkQueryParam() as string })}`; + static block = (height: number) => `/blocks/${height}${appendSearchParams({ network: networkStore.selectedNetworkId })}`; static transactions = () => `/transactions`; - static transaction = (hash: string) => `/transactions/${hash}${appendSearchParams({ network: getSelectedNetworkQueryParam() as string })}`; - static address = (address: string) => `/addresses/${address}${appendSearchParams({ network: getSelectedNetworkQueryParam() as string })}`; + static transaction = (hash: string) => `/transactions/${hash}${appendSearchParams({ network: networkStore.selectedNetworkId })}`; + static address = (address: string) => `/addresses/${address}${appendSearchParams({ network: networkStore.selectedNetworkId })}`; static addressTransactions = (address: string) => `/addresses/${address}/transactions`; static addressDeployments = (address: string) => `/addresses/${address}/deployments`; static deployment = (owner: string, dseq: string) => - `/addresses/${owner}/deployments/${dseq}${appendSearchParams({ network: getSelectedNetworkQueryParam() as string })}`; + `/addresses/${owner}/deployments/${dseq}${appendSearchParams({ network: networkStore.selectedNetworkId })}`; static validators = () => "/validators"; - static validator = (address: string) => `/validators/${address}${appendSearchParams({ network: getSelectedNetworkQueryParam() as string })}`; + static validator = (address: string) => `/validators/${address}${appendSearchParams({ network: networkStore.selectedNetworkId })}`; static proposals = () => "/proposals"; static proposal = (id: number) => `/proposals/${id}`; } @@ -64,9 +54,3 @@ export function isValidHttpUrl(str: string): boolean { return url.protocol === "http:" || url.protocol === "https:"; } - -export function handleDocClick(ev: Event, url: string) { - ev.preventDefault(); - - window.open(url, "_blank"); -} diff --git a/apps/stats-web/src/services/api-url/api-url.service.ts b/apps/stats-web/src/services/api-url/api-url.service.ts new file mode 100644 index 000000000..5c13fb9cd --- /dev/null +++ b/apps/stats-web/src/services/api-url/api-url.service.ts @@ -0,0 +1,36 @@ +import type { NetworkId } from "@akashnetwork/akashjs/build/types/network"; +import { SANDBOX_ID, TESTNET_ID } from "@akashnetwork/network-store"; + +import type { BrowserEnvConfig, ServerEnvConfig } from "@/config/env-config.schema"; + +export class ApiUrlService { + constructor( + private readonly config: + | Pick + | Pick + ) {} + + getBaseApiUrlFor(network: NetworkId) { + if ("BASE_API_MAINNET_URL" in this.config) { + switch (network) { + case TESTNET_ID: + return this.config.BASE_API_TESTNET_URL; + case SANDBOX_ID: + return this.config.BASE_API_SANDBOX_URL; + default: + return this.config.BASE_API_MAINNET_URL; + } + } + + if ("NEXT_PUBLIC_BASE_API_MAINNET_URL" in this.config) { + switch (network) { + case TESTNET_ID: + return this.config.NEXT_PUBLIC_BASE_API_TESTNET_URL; + case SANDBOX_ID: + return this.config.NEXT_PUBLIC_BASE_API_SANDBOX_URL; + default: + return this.config.NEXT_PUBLIC_BASE_API_MAINNET_URL; + } + } + } +} diff --git a/apps/stats-web/src/services/api-url/browser-api-url.service.ts b/apps/stats-web/src/services/api-url/browser-api-url.service.ts new file mode 100644 index 000000000..ca1a02faf --- /dev/null +++ b/apps/stats-web/src/services/api-url/browser-api-url.service.ts @@ -0,0 +1,4 @@ +import { browserEnvConfig } from "@/config/browser-env.config"; +import { ApiUrlService } from "@/services/api-url/api-url.service"; + +export const browserApiUrlService = new ApiUrlService(browserEnvConfig); diff --git a/apps/stats-web/src/services/api-url/server-api-url.service.ts b/apps/stats-web/src/services/api-url/server-api-url.service.ts new file mode 100644 index 000000000..5b17432a7 --- /dev/null +++ b/apps/stats-web/src/services/api-url/server-api-url.service.ts @@ -0,0 +1,4 @@ +import { serverEnvConfig } from "@/config/server-env.config"; +import { ApiUrlService } from "@/services/api-url/api-url.service"; + +export const serverApiUrlService = new ApiUrlService(serverEnvConfig); diff --git a/apps/stats-web/src/store/global.store.ts b/apps/stats-web/src/store/global.store.ts new file mode 100644 index 000000000..2ff6e1b41 --- /dev/null +++ b/apps/stats-web/src/store/global.store.ts @@ -0,0 +1,3 @@ +import { createStore } from "jotai"; + +export const store = createStore(); diff --git a/apps/stats-web/src/store/network.store.ts b/apps/stats-web/src/store/network.store.ts new file mode 100644 index 000000000..da9ef9579 --- /dev/null +++ b/apps/stats-web/src/store/network.store.ts @@ -0,0 +1,10 @@ +import { NetworkStore } from "@akashnetwork/network-store"; + +import { browserEnvConfig } from "@/config/browser-env.config"; +import { store } from "@/store/global.store"; + +export const networkStore = NetworkStore.create({ + defaultNetworkId: browserEnvConfig.NEXT_PUBLIC_DEFAULT_NETWORK_ID, + apiBaseUrl: browserEnvConfig.NEXT_PUBLIC_API_BASE_URL, + store +}); diff --git a/apps/stats-web/src/store/networkStore.ts b/apps/stats-web/src/store/networkStore.ts deleted file mode 100644 index 7eba30159..000000000 --- a/apps/stats-web/src/store/networkStore.ts +++ /dev/null @@ -1,67 +0,0 @@ -import axios from "axios"; -import { atom } from "jotai"; - -import { ApiUrlService } from "@/lib/apiUtils"; -import { mainnetId, sandboxId, testnetId } from "@/lib/constants"; -import { Network } from "@/types/network"; - -export let networks: Network[] = [ - { - id: mainnetId, - title: "Mainnet", - description: "Akash Network mainnet network.", - chainId: "akashnet-2", - versionUrl: ApiUrlService.mainnetVersion(), - rpcEndpoint: "https://rpc.cosmos.directory/akash", - enabled: true, - version: null // Set asynchronously - }, - { - id: testnetId, - title: "GPU Testnet", - description: "Testnet of the new GPU features.", - chainId: "testnet-02", - versionUrl: ApiUrlService.testnetVersion(), - enabled: false, - version: null // Set asynchronously - }, - { - id: sandboxId, - title: "Sandbox", - description: "Sandbox of the mainnet version.", - chainId: "sandbox-01", - versionUrl: ApiUrlService.sandboxVersion(), - version: null, // Set asynchronously - enabled: true - } -]; - -/** - * Get the actual versions and metadata of the available networks - */ -export const initiateNetworkData = async () => { - networks = await Promise.all( - networks.map(async network => { - let version = null; - try { - const response = await axios.get(network.versionUrl, { timeout: 10000 }); - version = response.data; - } catch (error) { - console.log(error); - } - - return { - ...network, - version - }; - }) - ); -}; - -const selectedNetwork = atom(networks[0]); - -const networkStore = { - selectedNetwork -}; - -export default networkStore; diff --git a/docker-compose.yml b/docker-compose.yml index e9f993d5e..73023682c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,3 +42,4 @@ services: - /app/node_modules - /app/apps/stats-web/node_modules - /app/apps/stats-web/.next + - ./packages:/app/packages diff --git a/package-lock.json b/package-lock.json index e5ab20bb3..c99691fe0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -726,6 +726,7 @@ "apps/stats-web": { "version": "0.20.0", "dependencies": { + "@akashnetwork/network-store": "*", "@akashnetwork/ui": "*", "@cosmjs/encoding": "^0.32.4", "@json2csv/plainjs": "^7.0.4", @@ -7054,9 +7055,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.4.tgz", - "integrity": "sha512-AH3mO4JlFUqsYcwFUHb1wAKlebHU/Hv2u2kb1pAuRanDZ7pD/A/KPD98RHZmwsJpdHQwfEc/06mgpSzwrJYnNg==", + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.6.tgz", + "integrity": "sha512-BtJZb+hYXGaVJJivpnDoi3JFVn80SHKCiiRUW3kk1SY6UCUy5dWFFSbh+tGi5lHAughzeduMyxbLt3pspvXNSg==", "cpu": [ "arm64" ], @@ -7069,9 +7070,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.4.tgz", - "integrity": "sha512-QVadW73sWIO6E2VroyUjuAxhWLZWEpiFqHdZdoQ/AMpN9YWGuHV8t2rChr0ahy+irKX5mlDU7OY68k3n4tAZTg==", + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.6.tgz", + "integrity": "sha512-ZHRbGpH6KHarzm6qEeXKSElSXh8dS2DtDPjQt3IMwY8QVk7GbdDYjvV4NgSnDA9huGpGgnyy3tH8i5yHCqVkiQ==", "cpu": [ "x64" ], @@ -7084,9 +7085,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.4.tgz", - "integrity": "sha512-KT6GUrb3oyCfcfJ+WliXuJnD6pCpZiosx2X3k66HLR+DMoilRb76LpWPGb4tZprawTtcnyrv75ElD6VncVamUQ==", + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.6.tgz", + "integrity": "sha512-O4HqUEe3ZvKshXHcDUXn1OybN4cSZg7ZdwHJMGCXSUEVUqGTJVsOh17smqilIjooP/sIJksgl+1kcf2IWMZWHg==", "cpu": [ "arm64" ], @@ -7099,9 +7100,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.4.tgz", - "integrity": "sha512-Alv8/XGSs/ytwQcbCHwze1HmiIkIVhDHYLjczSVrf0Wi2MvKn/blt7+S6FJitj3yTlMwMxII1gIJ9WepI4aZ/A==", + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.6.tgz", + "integrity": "sha512-xUcdhr2hfalG8RDDGSFxQ75yOG894UlmFS4K2M0jLrUhauRBGOtUOxoDVwiIIuZQwZ3Y5hDsazNjdYGB0cQ9yQ==", "cpu": [ "arm64" ], @@ -7114,9 +7115,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.4.tgz", - "integrity": "sha512-ze0ShQDBPCqxLImzw4sCdfnB3lRmN3qGMB2GWDRlq5Wqy4G36pxtNOo2usu/Nm9+V2Rh/QQnrRc2l94kYFXO6Q==", + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.6.tgz", + "integrity": "sha512-InosKxw8UMcA/wEib5n2QttwHSKHZHNSbGcMepBM0CTcNwpxWzX32KETmwbhKod3zrS8n1vJ+DuJKbL9ZAB0Ag==", "cpu": [ "x64" ], @@ -7129,9 +7130,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.4.tgz", - "integrity": "sha512-8dwC0UJoc6fC7PX70csdaznVMNr16hQrTDAMPvLPloazlcaWfdPogq+UpZX6Drqb1OBlwowz8iG7WR0Tzk/diQ==", + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.6.tgz", + "integrity": "sha512-d4QXfJmt5pGJ7cG8qwxKSBnO5AXuKAFYxV7qyDRHnUNvY/dgDh+oX292gATpB2AAHgjdHd5ks1wXxIEj6muLUQ==", "cpu": [ "x64" ], @@ -7144,9 +7145,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.4.tgz", - "integrity": "sha512-jxyg67NbEWkDyvM+O8UDbPAyYRZqGLQDTPwvrBBeOSyVWW/jFQkQKQ70JDqDSYg1ZDdl+E3nkbFbq8xM8E9x8A==", + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.6.tgz", + "integrity": "sha512-AlgIhk4/G+PzOG1qdF1b05uKTMsuRatFlFzAi5G8RZ9h67CVSSuZSbqGHbJDlcV1tZPxq/d4G0q6qcHDKWf4aQ==", "cpu": [ "arm64" ], @@ -7159,9 +7160,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.4.tgz", - "integrity": "sha512-twrmN753hjXRdcrZmZttb/m5xaCBFa48Dt3FbeEItpJArxriYDunWxJn+QFXdJ3hPkm4u7CKxncVvnmgQMY1ag==", + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.6.tgz", + "integrity": "sha512-hNukAxq7hu4o5/UjPp5jqoBEtrpCbOmnUqZSKNJG8GrUVzfq0ucdhQFVrHcLRMvQcwqqDh1a5AJN9ORnNDpgBQ==", "cpu": [ "ia32" ], @@ -7174,9 +7175,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.4.tgz", - "integrity": "sha512-tkLrjBzqFTP8DVrAAQmZelEahfR9OxWpFR++vAI9FBhCiIxtwHwBHC23SBHCTURBtwB4kc/x44imVOnkKGNVGg==", + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.6.tgz", + "integrity": "sha512-NANtw+ead1rSDK1jxmzq3TYkl03UNK2KHqUYf1nIhNci6NkeqBD4s1njSzYGIlSHxCK+wSaL8RXZm4v+NF/pMw==", "cpu": [ "x64" ], @@ -36432,7 +36433,8 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { - "axios": "^1.7.2" + "axios": "^1.7.2", + "lodash": "^4.17.21" }, "devDependencies": { "@akashnetwork/akashjs": "^0.10.0" diff --git a/packages/network-store/package.json b/packages/network-store/package.json index 4959ea4b8..b7ea9e61b 100644 --- a/packages/network-store/package.json +++ b/packages/network-store/package.json @@ -11,7 +11,8 @@ "lint": "eslint ." }, "dependencies": { - "axios": "^1.7.2" + "axios": "^1.7.2", + "lodash": "^4.17.21" }, "devDependencies": { "@akashnetwork/akashjs": "^0.10.0" diff --git a/packages/network-store/src/network.store.ts b/packages/network-store/src/network.store.ts index f756f8069..f919c4a90 100644 --- a/packages/network-store/src/network.store.ts +++ b/packages/network-store/src/network.store.ts @@ -2,6 +2,7 @@ import axios from "axios"; import { atom } from "jotai"; import { getDefaultStore, useAtom } from "jotai"; import { atomWithStorage } from "jotai/utils"; +import cloneDeep from "lodash/cloneDeep"; import { INITIAL_NETWORKS_CONFIG } from "./network.config"; import type { Network } from "./network.type"; @@ -31,9 +32,11 @@ export class NetworkStore { return new NetworkStore(options); } - readonly networksStore = atom({ isLoading: true, error: undefined, data: INITIAL_NETWORKS_CONFIG }); + private readonly STORAGE_KEY = "selectedNetworkId"; - private readonly selectedNetworkIdStore = atomWithStorage("selectedNetworkId", this.options.defaultNetworkId, undefined, { getOnInit: true }); + readonly networksStore = atom({ isLoading: true, error: undefined, data: cloneDeep(INITIAL_NETWORKS_CONFIG) }); + + private readonly selectedNetworkIdStore = atomWithStorage(this.STORAGE_KEY, this.options.defaultNetworkId); private readonly selectedNetworkStore = atom( get => { @@ -42,8 +45,8 @@ export class NetworkStore { return networks.find(n => n.id === networkId) ?? networks[0]; }, - async (get, set, next) => { - await set(this.selectedNetworkIdStore, next.id); + (get, set, next) => { + set(this.selectedNetworkIdStore, next.id); } ); @@ -71,13 +74,14 @@ export class NetworkStore { constructor(private readonly options: NetworkStoreOptions) { this.store = options.store || getDefaultStore(); + this.initiateNetworkFromUrlQuery(); this.initiateNetworks(); } private async initiateNetworks() { const errors: { network: Network; error: Error }[] = []; const networks = await Promise.all( - INITIAL_NETWORKS_CONFIG.map(async network => { + cloneDeep(INITIAL_NETWORKS_CONFIG).map(async network => { try { network.versionUrl = this.options.apiBaseUrl + network.versionUrl; network.nodesUrl = this.options.apiBaseUrl + network.nodesUrl; @@ -101,6 +105,24 @@ export class NetworkStore { } } + private initiateNetworkFromUrlQuery(): Network["id"] { + if (typeof window === "undefined") { + return; + } + + const url = new URL(window.location.href); + + if (!url.searchParams.has("network")) { + return; + } + + const raw = url.searchParams.get("network"); + + if (INITIAL_NETWORKS_CONFIG.some(({ id }) => id === raw)) { + window.localStorage.setItem(this.STORAGE_KEY, JSON.stringify(raw)); + } + } + useNetworksStore() { return useAtom(this.networksStore); }