diff --git a/apps/api/src/services/db/transactionsService.ts b/apps/api/src/services/db/transactionsService.ts index a4fa940b4..be2d0ebbd 100644 --- a/apps/api/src/services/db/transactionsService.ts +++ b/apps/api/src/services/db/transactionsService.ts @@ -3,6 +3,7 @@ import { AkashBlock as Block, AkashMessage as Message } from "@akashnetwork/clou import { msgToJSON } from "@src/utils/protobuf"; import { QueryTypes } from "sequelize"; import { chainDb } from "@src/db/dbConnection"; +import { ApiTransactionResponse } from "@src/types/transactions"; export async function getTransactions(limit: number) { const _limit = Math.min(limit, 100); @@ -41,7 +42,7 @@ export async function getTransactions(limit: number) { })); } -export async function getTransaction(hash: string) { +export async function getTransaction(hash: string): Promise { const tx = await Transaction.findOne({ where: { hash: hash @@ -62,7 +63,9 @@ export async function getTransaction(hash: string) { ] }); - if (!tx) return null; + if (!tx) { + return null; + } const messages = await Message.findAll({ where: { diff --git a/apps/api/src/services/external/apiNodeService.ts b/apps/api/src/services/external/apiNodeService.ts index 3c2a9bfd8..399b7a06a 100644 --- a/apps/api/src/services/external/apiNodeService.ts +++ b/apps/api/src/services/external/apiNodeService.ts @@ -1,6 +1,6 @@ import fetch from "node-fetch"; import { getDeploymentRelatedMessages } from "../db/deploymentService"; -import { averageBlockCountInAMonth } from "@src/utils/constants"; +import { apiNodeUrl, averageBlockCountInAMonth, betaTypeVersion, betaTypeVersionMarket } from "@src/utils/constants"; import { coinToAsset } from "@src/utils/coin"; import { getTransactionByAddress } from "@src/services/db/transactionsService"; import axios from "axios"; @@ -8,7 +8,6 @@ import { Validator } from "@akashnetwork/cloudmos-shared/dbSchemas/base"; import { Op } from "sequelize"; import { Deployment, Lease, Provider, ProviderAttribute } from "@akashnetwork/cloudmos-shared/dbSchemas/akash"; import { cacheKeys, cacheResponse } from "@src/caching/helpers"; -import { env } from "@src/utils/env"; import { CosmosGovProposalResponse, CosmosGovProposalsResponse, @@ -31,15 +30,7 @@ import { CosmosDistributionParamsResponse } from "@src/types/rest/cosmosDistribu import { CosmosDistributionValidatorsCommissionResponse } from "@src/types/rest/cosmosDistributionValidatorsCommissionResponse"; import { getProviderList } from "../db/providerStatusService"; -const defaultNodeUrlMapping: { [key: string]: string } = { - mainnet: "https://api.akashnet.net:443", - sandbox: "https://api.sandbox-01.aksh.pw", - testnet: "https://api.testnet-02.aksh.pw" -}; -const apiNodeUrl = env.RestApiNodeUrl ?? defaultNodeUrlMapping[env.Network] ?? defaultNodeUrlMapping.mainnet; -const betaTypeVersion = "v1beta3"; -const betaTypeVersionMarket = "v1beta4"; export async function getChainStats() { const result = await cacheResponse( diff --git a/apps/api/src/types/rest/cosmosTransactionResponse.ts b/apps/api/src/types/rest/cosmosTransactionResponse.ts new file mode 100644 index 000000000..ec0cca3d6 --- /dev/null +++ b/apps/api/src/types/rest/cosmosTransactionResponse.ts @@ -0,0 +1,147 @@ +export type RestCosmostTransactionResponse = { + tx: { + body: { + messages: { + "@type": string; + client_id: string; + header: { + "@type": string; + signed_header: { + header: { + version: { + block: string; + app: string; + }; + chain_id: string; + height: string; + time: string; + last_block_id: { + hash: string; + part_set_header: { + total: number; + hash: string; + }; + }; + last_commit_hash: string; + data_hash: string; + validators_hash: string; + next_validators_hash: string; + consensus_hash: string; + app_hash: string; + last_results_hash: string; + evidence_hash: string; + proposer_address: string; + }; + commit: { + height: string; + round: number; + block_id: { + hash: string; + part_set_header: { + total: number; + hash: string; + }; + }; + signatures: { + block_id_flag: string; + validator_address: string; + timestamp: string; + signature: string; + }[]; + }; + }; + }; + }[]; + }; + auth_info: { + signer_infos: [ + { + public_key: { + "@type": string; + key: string; + }; + mode_info: { + single: { + mode: string; + }; + }; + sequence: string; + } + ]; + fee: { + amount: [ + { + denom: string; + amount: string; + } + ]; + gas_limit: string; + payer: string; + granter: string; + }; + }; + signatures: string[]; + }; + tx_response: { + height: string; + txhash: string; + codespace: string; + code: number; + data: string; + raw_log: string; + logs: { + msg_index: number; + log: string; + events: { + type: string; + attributes: { + key: string; + value: string; + }[]; + }[]; + }[]; + info: string; + gas_wanted: string; + gas_used: string; + tx: { + type: string; + value: { + msg: { + type: string; + value: { + from_address: string; + to_address: string; + amount: { + denom: string; + amount: string; + }[]; + }; + }[]; + fee: { + amount: { + denom: string; + amount: string; + }[]; + gas: string; + }; + signatures: { + pub_key: { + type: string; + value: string; + }; + signature: string; + }[]; + memo: string; + }; + }; + timestamp: string; + events: { + type: string; + attributes: { + key: string; + value: string; + index: boolean; + }[]; + }[]; + }; +}; diff --git a/apps/api/src/types/transactions.ts b/apps/api/src/types/transactions.ts new file mode 100644 index 000000000..9c7ebe40b --- /dev/null +++ b/apps/api/src/types/transactions.ts @@ -0,0 +1,19 @@ +export type ApiTransactionResponse = { + height: number | string; + datetime: Date; + hash: string; + multisigThreshold?: number; + signers?: string[]; + isSuccess: boolean; + error: string | null; + gasUsed: number; + gasWanted: number; + fee: number; + memo: string; + messages: { + id: string; + type: string; + data: unknown; + relatedDeploymentId: string | null; + }[]; +}; diff --git a/apps/api/src/utils/constants.ts b/apps/api/src/utils/constants.ts index 2aeb898dc..7c2eada31 100644 --- a/apps/api/src/utils/constants.ts +++ b/apps/api/src/utils/constants.ts @@ -14,3 +14,13 @@ export const openApiExampleAddress = "akash13265twfqejnma6cc93rw5dxk4cldyz2zyy8c export const openApiExampleProviderAddress = "akash18ga02jzaq8cw52anyhzkwta5wygufgu6zsz6xc"; export const openApiExampleTransactionHash = "A19F1950D97E576F0D7B591D71A8D0366AA8BA0A7F3DA76F44769188644BE9EB"; export const openApiExampleValidatorAddress = "akashvaloper14mt78hz73d9tdwpdvkd59ne9509kxw8yj7qy8f"; + +export const defaultNodeUrlMapping: { [key: string]: string } = { + mainnet: "https://api.akashnet.net:443", + sandbox: "https://api.sandbox-01.aksh.pw", + testnet: "https://api.testnet-02.aksh.pw" +}; + +export const apiNodeUrl = env.RestApiNodeUrl ?? defaultNodeUrlMapping[env.Network] ?? defaultNodeUrlMapping.mainnet; +export const betaTypeVersion = "v1beta3"; +export const betaTypeVersionMarket = "v1beta4"; diff --git a/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx b/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx index 202d07fd6..5158d91da 100644 --- a/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx +++ b/apps/deploy-web/src/context/WalletProvider/WalletProvider.tsx @@ -2,7 +2,7 @@ import React, { useRef } from "react"; import { useState, useEffect } from "react"; import { SigningStargateClient } from "@cosmjs/stargate"; -import { uAktDenom } from "@src/utils/constants"; +import { STATS_APP_URL, uAktDenom } from "@src/utils/constants"; import { EncodeObject } from "@cosmjs/proto-signing"; import { TransactionModal } from "@src/components/layout/TransactionModal"; import { event } from "nextjs-google-analytics"; @@ -325,7 +325,7 @@ export function useWallet() { const TransactionSnackbarContent = ({ snackMessage, transactionHash }) => { const selectedNetwork = useSelectedNetwork(); - const txUrl = transactionHash && `https://stats.akash.network/transactions/${transactionHash}?network=${selectedNetwork.id}`; + const txUrl = transactionHash && `${STATS_APP_URL}/transactions/${transactionHash}?network=${selectedNetwork.id}`; return ( <> diff --git a/apps/deploy-web/src/utils/constants.ts b/apps/deploy-web/src/utils/constants.ts index 1ab7a9a9c..be8d08d37 100644 --- a/apps/deploy-web/src/utils/constants.ts +++ b/apps/deploy-web/src/utils/constants.ts @@ -24,6 +24,7 @@ export enum RouteStepKeys { const productionMainnetApiUrl = "https://api.cloudmos.io"; const productionTestnetApiUrl = "https://api-testnet.cloudmos.io"; const productionSandboxApiUrl = "https://api-sandbox.cloudmos.io"; +const productionStatsAppUrl = "https://stats.akash.network"; const productionHostnames = ["deploy.cloudmos.io", "console.akash.network", "staging-console.akash.network", "beta.cloudmos.io"]; export const isProd = process.env.NODE_ENV === "production"; @@ -31,6 +32,7 @@ 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 STATS_APP_URL = getStatsAppUrl(); export const BASE_API_URL = getApiUrl(); @@ -100,6 +102,13 @@ function getApiUrl() { return "http://localhost:3080"; } +function getStatsAppUrl() { + if (process.env.STATS_APP_URL) return process.env.STATS_APP_URL; + if (typeof window === "undefined") return "http://localhost:3001"; + if (productionHostnames.includes(window.location?.hostname)) return productionStatsAppUrl; + return "http://localhost:3001"; +} + function getProviderProxyHttpUrl() { if (typeof window === "undefined") return "http://localhost:3040"; if (window.location?.hostname === "deploybeta.cloudmos.io") return "https://deployproxybeta.cloudmos.io"; diff --git a/apps/stats-web/.eslintrc.json b/apps/stats-web/.eslintrc.json index ed5490144..bffb357a7 100644 --- a/apps/stats-web/.eslintrc.json +++ b/apps/stats-web/.eslintrc.json @@ -1,3 +1,3 @@ { - "extends": "next/core-web-vitals", + "extends": "next/core-web-vitals" } diff --git a/apps/stats-web/package-lock.json b/apps/stats-web/package-lock.json index 42e3d280b..d3f9c9c62 100644 --- a/apps/stats-web/package-lock.json +++ b/apps/stats-web/package-lock.json @@ -1,12 +1,12 @@ { "name": "stats-web", - "version": "0.19.3", + "version": "0.19.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stats-web", - "version": "0.19.3", + "version": "0.19.5", "dependencies": { "@cosmjs/encoding": "^0.32.0", "@json2csv/plainjs": "^7.0.4", diff --git a/apps/stats-web/src/app/addresses/[address]/deployments/[dseq]/DeploymentInfo.tsx b/apps/stats-web/src/app/addresses/[address]/deployments/[dseq]/DeploymentInfo.tsx index fac6b9e65..813025f93 100644 --- a/apps/stats-web/src/app/addresses/[address]/deployments/[dseq]/DeploymentInfo.tsx +++ b/apps/stats-web/src/app/addresses/[address]/deployments/[dseq]/DeploymentInfo.tsx @@ -8,15 +8,14 @@ import { Title } from "@/components/Title"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent } from "@/components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { useFriendlyMessageType } from "@/hooks/useFriendlyMessageType"; import { getSplitText } from "@/hooks/useShortText"; import { roundDecimal, udenomToDenom } from "@/lib/mathHelpers"; import { bytesToShrink } from "@/lib/unitUtils"; import { UrlService } from "@/lib/urlUtils"; -import { cn } from "@/lib/utils"; import { DeploymentDetail } from "@/types"; import Link from "next/link"; import { FormattedNumber, FormattedTime } from "react-intl"; +import { EventRow } from "./EventRow"; interface IProps { deployment: DeploymentDetail; @@ -90,17 +89,7 @@ export function DeploymentInfo({ deployment }: IProps) { {deployment.events.map((event, i) => ( - - - - {getSplitText(event.txHash, 6, 6)} - - - {useFriendlyMessageType(event.type)} - - - - + ))} diff --git a/apps/stats-web/src/app/addresses/[address]/deployments/[dseq]/EventRow.tsx b/apps/stats-web/src/app/addresses/[address]/deployments/[dseq]/EventRow.tsx new file mode 100644 index 000000000..7615c4f39 --- /dev/null +++ b/apps/stats-web/src/app/addresses/[address]/deployments/[dseq]/EventRow.tsx @@ -0,0 +1,30 @@ +import { TableCell, TableRow } from "@/components/ui/table"; +import { useFriendlyMessageType } from "@/hooks/useFriendlyMessageType"; +import { getSplitText } from "@/hooks/useShortText"; +import { UrlService } from "@/lib/urlUtils"; +import Link from "next/link"; +import { FormattedTime } from "react-intl"; + +export const EventRow = ({ + event +}: React.PropsWithChildren<{ + event: { + txHash: string; + date: string; + type: string; + }; +}>) => { + return ( + + + + {getSplitText(event.txHash, 6, 6)} + + + {useFriendlyMessageType(event.type)} + + + + + ); +}; diff --git a/apps/stats-web/src/app/addresses/[address]/transactions/TransactionTypeCell.tsx b/apps/stats-web/src/app/addresses/[address]/transactions/TransactionTypeCell.tsx new file mode 100644 index 000000000..380dcee4e --- /dev/null +++ b/apps/stats-web/src/app/addresses/[address]/transactions/TransactionTypeCell.tsx @@ -0,0 +1,22 @@ +import { Badge } from "@/components/ui/badge"; +import { useFriendlyMessageType } from "@/hooks/useFriendlyMessageType"; +import { TransactionRowType } from "@/lib/zod/transactionRow"; +import { Row } from "@tanstack/react-table"; + +export const TransactionTypeCell = ({ + row +}: React.PropsWithChildren<{ + row: Row; +}>) => { + const friendlyMessage = useFriendlyMessageType(row.original.messages[0].type); + const firstMessageType = row.original.messages[0].isReceiver ? "Receive" : friendlyMessage; + + return ( + <> + + {firstMessageType} + + {row.original.messages.length > 1 ? " +" + (row.original.messages.length - 1) : ""} + + ); +}; diff --git a/apps/stats-web/src/app/addresses/[address]/transactions/columns.tsx b/apps/stats-web/src/app/addresses/[address]/transactions/columns.tsx index 1658389ea..f9ba383ae 100644 --- a/apps/stats-web/src/app/addresses/[address]/transactions/columns.tsx +++ b/apps/stats-web/src/app/addresses/[address]/transactions/columns.tsx @@ -4,12 +4,11 @@ import { DataTableColumnHeader } from "@/components/table/data-table-column-head import { UrlService } from "@/lib/urlUtils"; import { AccessorColumnDef } from "@tanstack/react-table"; import Link from "next/link"; -import { Badge } from "@/components/ui/badge"; import { getSplitText } from "@/hooks/useShortText"; -import { useFriendlyMessageType } from "@/hooks/useFriendlyMessageType"; import { AKTAmount } from "@/components/AKTAmount"; import { FormattedRelativeTime } from "react-intl"; import { TransactionRowType } from "@/lib/zod/transactionRow"; +import { TransactionTypeCell } from "./TransactionTypeCell"; export const columns: AccessorColumnDef[] = [ { @@ -28,16 +27,7 @@ export const columns: AccessorColumnDef[] = [ enableSorting: false, header: ({ column }) => , cell: ({ row }) => { - const firstMessageType = row.original.messages[0].isReceiver ? "Receive" : useFriendlyMessageType(row.original.messages[0].type); - - return ( - <> - - {firstMessageType} - - {row.original.messages.length > 1 ? " +" + (row.original.messages.length - 1) : ""} - - ); + return ; } }, { diff --git a/apps/stats-web/src/app/errors.tsx b/apps/stats-web/src/app/error.tsx similarity index 62% rename from apps/stats-web/src/app/errors.tsx rename to apps/stats-web/src/app/error.tsx index 65ae27eb5..975376567 100644 --- a/apps/stats-web/src/app/errors.tsx +++ b/apps/stats-web/src/app/error.tsx @@ -1,5 +1,8 @@ "use client"; // Error components must be Client Components +import PageContainer from "@/components/PageContainer"; +import { Title } from "@/components/Title"; +import { Button } from "@/components/ui/button"; import { useEffect } from "react"; export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { @@ -9,16 +12,17 @@ export default function Error({ error, reset }: { error: Error & { digest?: stri }, [error]); return ( -
-

Something went wrong!

- -
+ + ); } diff --git a/apps/stats-web/src/app/transactions/[hash]/errors.tsx b/apps/stats-web/src/app/transactions/[hash]/error.tsx similarity index 62% rename from apps/stats-web/src/app/transactions/[hash]/errors.tsx rename to apps/stats-web/src/app/transactions/[hash]/error.tsx index 65ae27eb5..975376567 100644 --- a/apps/stats-web/src/app/transactions/[hash]/errors.tsx +++ b/apps/stats-web/src/app/transactions/[hash]/error.tsx @@ -1,5 +1,8 @@ "use client"; // Error components must be Client Components +import PageContainer from "@/components/PageContainer"; +import { Title } from "@/components/Title"; +import { Button } from "@/components/ui/button"; import { useEffect } from "react"; export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { @@ -9,16 +12,17 @@ export default function Error({ error, reset }: { error: Error & { digest?: stri }, [error]); return ( -
-

Something went wrong!

- -
+ + ); } diff --git a/apps/stats-web/src/app/transactions/[hash]/page.tsx b/apps/stats-web/src/app/transactions/[hash]/page.tsx index f2fc56b3f..b35fa2a97 100644 --- a/apps/stats-web/src/app/transactions/[hash]/page.tsx +++ b/apps/stats-web/src/app/transactions/[hash]/page.tsx @@ -8,6 +8,7 @@ import { Title } from "@/components/Title"; import { Card, CardContent } from "@/components/ui/card"; import { TransactionInfo } from "./TransactionInfo"; import { TxMessageRow } from "@/components/transactions/TxMessageRow"; +import { Alert } from "@/components/ui/alert"; interface IProps { params: { hash: string }; @@ -21,13 +22,15 @@ export async function generateMetadata({ params: { hash } }: IProps, parent: Res }; } -async function fetchTransactionData(hash: string, network: string): Promise { +async function fetchTransactionData(hash: string, network: string): Promise { const apiUrl = getNetworkBaseApiUrl(network); const response = await fetch(`${apiUrl}/v1/transactions/${hash}`); - if (!response.ok) { + if (!response.ok && response.status !== 404) { // This will activate the closest `error.js` Error Boundary - throw new Error("Error fetching block data"); + throw new Error("Error fetching transction data"); + } else if (response.status === 404) { + return null; } return response.json(); @@ -40,21 +43,27 @@ export default async function TransactionDetailPage({ params: { hash }, searchPa Transaction Details - - -
- - Messages - - - {transaction.messages.map(msg => ( - - - - - - ))} -
+ {transaction ? ( + <> + + +
+ + Messages + + + {transaction.messages.map(msg => ( + + + + + + ))} +
+ + ) : ( + Transaction not found or indexed yet. Please wait a few seconds before refreshing. + )}
); } diff --git a/apps/stats-web/src/components/AKTAmount.tsx b/apps/stats-web/src/components/AKTAmount.tsx index bf71523e4..391e58037 100644 --- a/apps/stats-web/src/components/AKTAmount.tsx +++ b/apps/stats-web/src/components/AKTAmount.tsx @@ -13,7 +13,7 @@ type Props = { notation?: "standard" | "scientific" | "engineering" | "compact" | undefined; }; -export const AKTAmount: React.FunctionComponent = ({ uakt, showUSD, showAKTLabel, digits = 6, notation }) => { +export const AKTAmount = ({ uakt, showUSD, showAKTLabel, digits = 6, notation }: React.PropsWithChildren) => { const { isLoaded: isPriceLoaded, aktToUSD } = usePricing(); const aktAmount = udenomToDenom(uakt, 6); diff --git a/apps/stats-web/src/components/SearchBar.tsx b/apps/stats-web/src/components/SearchBar.tsx index f088348bd..c34e57cde 100644 --- a/apps/stats-web/src/components/SearchBar.tsx +++ b/apps/stats-web/src/components/SearchBar.tsx @@ -29,6 +29,7 @@ const SearchBar: React.FunctionComponent = ({}) => { useEffect(() => { setSearchType(getSearchType(searchTerms)); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchTerms]); function onSearchTermsChange(ev: React.ChangeEvent) { diff --git a/apps/stats-web/src/components/blockchain/TransactionRow.tsx b/apps/stats-web/src/components/blockchain/TransactionRow.tsx index a1ea04416..c242aba29 100644 --- a/apps/stats-web/src/components/blockchain/TransactionRow.tsx +++ b/apps/stats-web/src/components/blockchain/TransactionRow.tsx @@ -18,7 +18,8 @@ type Props = { export const TransactionRow: React.FunctionComponent = ({ transaction, blockHeight, isSimple }) => { const txHash = getSplitText(transaction.hash, 6, 6); - const firstMessageType = transaction.messages[0].isReceiver ? "Receive" : useFriendlyMessageType(transaction.messages[0].type); + const friendlyMessage = useFriendlyMessageType(transaction.messages[0].type); + const firstMessageType = transaction.messages[0].isReceiver ? "Receive" : friendlyMessage; return ( @@ -46,7 +47,7 @@ export const TransactionRow: React.FunctionComponent = ({ transaction, bl {blockHeight} - + , variant: "default", - href: "https://deploy.cloudmos.io", + href: "https://console.akash.network", isExternal: true, rel: "noreferrer" }, diff --git a/apps/stats-web/src/components/layout/Nav.tsx b/apps/stats-web/src/components/layout/Nav.tsx index 7336103fa..9cdcb5d93 100644 --- a/apps/stats-web/src/components/layout/Nav.tsx +++ b/apps/stats-web/src/components/layout/Nav.tsx @@ -61,7 +61,7 @@ export const Nav = () => { - +