diff --git a/govtool/backend/app/Main.hs b/govtool/backend/app/Main.hs index c2d3ca711..d82821424 100644 --- a/govtool/backend/app/Main.hs +++ b/govtool/backend/app/Main.hs @@ -125,6 +125,7 @@ startApp vvaConfig sentryService = do networkInfoCache <- newCache networkTotalStakeCache <- newCache dRepVotingPowerListCache <- newCache + accountInfoCache <- newCache return $ CacheEnv { proposalListCache , getProposalCache @@ -139,6 +140,7 @@ startApp vvaConfig sentryService = do , networkInfoCache , networkTotalStakeCache , dRepVotingPowerListCache + , accountInfoCache } let connectionString = encodeUtf8 (dbSyncConnectionString $ getter vvaConfig) diff --git a/govtool/backend/sql/get-account-info.sql b/govtool/backend/sql/get-account-info.sql new file mode 100644 index 000000000..d11fe68a4 --- /dev/null +++ b/govtool/backend/sql/get-account-info.sql @@ -0,0 +1,23 @@ +SELECT + sa.id, + sa.view, + CASE + WHEN sa.script_hash IS NOT NULL THEN true + ELSE false + END AS is_script_based, + CASE + WHEN ( + SELECT COALESCE(MAX(epoch_no), 0) + FROM stake_registration sr + WHERE sr.addr_id = sa.id + ) > ( + SELECT COALESCE(MAX(epoch_no), 0) + FROM stake_deregistration sd + WHERE sd.addr_id = sa.id + ) THEN true + ELSE false + END AS is_registered +FROM + stake_address sa +WHERE + sa.hash_raw = decode(?, 'hex'); diff --git a/govtool/backend/src/VVA/API.hs b/govtool/backend/src/VVA/API.hs index 9488710a6..f8eaaa224 100644 --- a/govtool/backend/src/VVA/API.hs +++ b/govtool/backend/src/VVA/API.hs @@ -40,6 +40,7 @@ import VVA.Config import qualified VVA.DRep as DRep import qualified VVA.Epoch as Epoch import VVA.Network as Network +import VVA.Account as Account import qualified VVA.Proposal as Proposal import qualified VVA.Transaction as Transaction import qualified VVA.Types as Types @@ -85,6 +86,7 @@ type VVAApi = :<|> "network" :> "metrics" :> Get '[JSON] GetNetworkMetricsResponse :<|> "network" :> "info" :> Get '[JSON] GetNetworkInfoResponse :<|> "network" :> "total-stake" :> Get '[JSON] GetNetworkTotalStakeResponse + :<|> "account" :> Capture "stakeKey" HexText :> Get '[JSON] GetAccountInfoResponse server :: App m => ServerT VVAApi m server = drepList @@ -103,6 +105,7 @@ server = drepList :<|> getNetworkMetrics :<|> getNetworkInfo :<|> getNetworkTotalStake + :<|> getAccountInfo mapDRepType :: Types.DRepType -> DRepType mapDRepType Types.DRep = NormalDRep @@ -527,3 +530,14 @@ getNetworkMetrics = do , getNetworkMetricsResponseQuorumNumerator = networkMetricsQuorumNumerator , getNetworkMetricsResponseQuorumDenominator = networkMetricsQuorumDenominator } + +getAccountInfo :: App m => HexText -> m GetAccountInfoResponse +getAccountInfo (unHexText -> stakeKey) = do + CacheEnv {accountInfoCache} <- asks vvaCache + Types.AccountInfo {..} <- Account.accountInfo stakeKey + return $ GetAccountInfoResponse + { getAccountInfoResponseId = accountInfoId + , getAccountInfoResponseView = accountInfoView + , getAccountInfoResponseIsRegistered = accountInfoIsRegistered + , getAccountInfoResponseIsScriptBased = accountInfoIsScriptBased + } diff --git a/govtool/backend/src/VVA/API/Types.hs b/govtool/backend/src/VVA/API/Types.hs index 6b517a9ce..dac0a9e0d 100644 --- a/govtool/backend/src/VVA/API/Types.hs +++ b/govtool/backend/src/VVA/API/Types.hs @@ -1032,3 +1032,26 @@ instance ToSchema GetNetworkMetricsResponse where & description ?~ "GetNetworkMetricsResponse" & example ?~ toJSON exampleGetNetworkMetricsResponse + +data GetAccountInfoResponse + = GetAccountInfoResponse + { getAccountInfoResponseId :: Integer + , getAccountInfoResponseView :: Text + , getAccountInfoResponseIsRegistered :: Bool + , getAccountInfoResponseIsScriptBased :: Bool + } + deriving (Generic, Show) +deriveJSON (jsonOptions "getAccountInfoResponse") ''GetAccountInfoResponse +exampleGetAccountInfoResponse :: Text +exampleGetAccountInfoResponse = + "{\"stakeKey\": \"stake1u9\"," + <> " \"id\": \"1\"," + <> "\"view\": \"stake_test1uzapf83wydusjln97rqr7fen6vgrz5087yqdxm0akqdqkgstj2345\"," + <> "\"isRegistered\": false," + <> "\"isScriptBased\": false}" +instance ToSchema GetAccountInfoResponse where + declareNamedSchema _ = pure $ NamedSchema (Just "GetAccountInfoResponse") $ mempty + & type_ ?~ OpenApiObject + & description ?~ "GetAccountInfoResponse" + & example + ?~ toJSON exampleGetAccountInfoResponse diff --git a/govtool/backend/src/VVA/Account.hs b/govtool/backend/src/VVA/Account.hs new file mode 100644 index 000000000..10aa92e52 --- /dev/null +++ b/govtool/backend/src/VVA/Account.hs @@ -0,0 +1,35 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} + +module VVA.Account where + +import Control.Monad.Except (MonadError, throwError) +import Control.Monad.Reader (MonadIO, MonadReader, liftIO) +import Data.ByteString (ByteString) +import Data.FileEmbed (embedFile) +import Data.String (fromString) +import qualified Database.PostgreSQL.Simple as SQL +import VVA.Types (AppError(..), AccountInfo(..)) +import Data.Text (Text, unpack) +import qualified Data.Text.Encoding as Text +import qualified Data.Text.IO as Text +import Data.Has (Has) +import VVA.Pool (ConnectionPool, withPool) + +sqlFrom :: ByteString -> SQL.Query +sqlFrom = fromString . unpack . Text.decodeUtf8 + +accountInfoSql :: SQL.Query +accountInfoSql = sqlFrom $(embedFile "sql/get-account-info.sql") + +accountInfo :: + (Has ConnectionPool r, MonadReader r m, MonadIO m, MonadError AppError m) => + Text -> + m AccountInfo +accountInfo stakeKey = withPool $ \conn -> do + result <- liftIO $ SQL.query conn accountInfoSql (SQL.Only stakeKey) + case result of + [(id, view, is_registered, is_script_based)] -> + return $ AccountInfo id view is_registered is_script_based + _ -> throwError $ CriticalError "Could not query the account info." diff --git a/govtool/backend/src/VVA/Types.hs b/govtool/backend/src/VVA/Types.hs index e6e674925..2d461d6dc 100644 --- a/govtool/backend/src/VVA/Types.hs +++ b/govtool/backend/src/VVA/Types.hs @@ -260,6 +260,7 @@ data CacheEnv , networkInfoCache :: Cache.Cache () NetworkInfo , networkTotalStakeCache :: Cache.Cache () NetworkTotalStake , dRepVotingPowerListCache :: Cache.Cache Text [DRepVotingPowerList] + , accountInfoCache :: Cache.Cache Text AccountInfo } data NetworkInfo @@ -302,3 +303,11 @@ data Delegation , delegationIsDRepScriptBased :: Bool , delegationTxHash :: Text } + +data AccountInfo + = AccountInfo + { accountInfoId :: Integer + , accountInfoView :: Text + , accountInfoIsRegistered :: Bool + , accountInfoIsScriptBased :: Bool + } diff --git a/govtool/backend/vva-be.cabal b/govtool/backend/vva-be.cabal index 9b1345f50..9ae62c3dd 100644 --- a/govtool/backend/vva-be.cabal +++ b/govtool/backend/vva-be.cabal @@ -37,6 +37,7 @@ extra-source-files: sql/get-dreps-voting-power-list.sql sql/get-filtered-dreps-voting-power.sql sql/get-previous-enacted-governance-action-proposal-details.sql + sql/get-account-info.sql executable vva-be main-is: Main.hs @@ -124,4 +125,5 @@ library , VVA.Pool , VVA.Types , VVA.Network + , VVA.Account ghc-options: -threaded diff --git a/govtool/frontend/src/models/api.ts b/govtool/frontend/src/models/api.ts index bc374ccad..4e1361498 100644 --- a/govtool/frontend/src/models/api.ts +++ b/govtool/frontend/src/models/api.ts @@ -261,3 +261,10 @@ type DRepVotingPower = { }; export type DRepVotingPowerListResponse = DRepVotingPower[]; + +export type Account = { + id: number, + view: string, + isRegistered: boolean, + isScriptBased: boolean +} diff --git a/govtool/frontend/src/pages/ProposalDiscussion.tsx b/govtool/frontend/src/pages/ProposalDiscussion.tsx index d2f28667c..196f73c15 100644 --- a/govtool/frontend/src/pages/ProposalDiscussion.tsx +++ b/govtool/frontend/src/pages/ProposalDiscussion.tsx @@ -12,7 +12,7 @@ import { useValidateMutation } from "@/hooks/mutations"; import { useScreenDimension } from "@/hooks/useScreenDimension"; import { Footer, TopNav } from "@/components/organisms"; import { useGetDRepVotingPowerList, useGetVoterInfo } from "@/hooks"; -import { getAdaHolderVotingPower } from "@/services"; +import { getAdaHolderVotingPower, getAccount } from "@/services"; const ProposalDiscussion = React.lazy( () => import("@intersect.mbo/pdf-ui/cjs"), @@ -81,6 +81,7 @@ export const ProposalDiscussionPillar = () => { setUsername={setUsername} epochParams={epochParams} getAdaHolderVotingPower={getAdaHolderVotingPower} + getAccount={getAccount} {...snackbarContext} /> diff --git a/govtool/frontend/src/services/requests/getAccount.ts b/govtool/frontend/src/services/requests/getAccount.ts new file mode 100644 index 000000000..f6badbebf --- /dev/null +++ b/govtool/frontend/src/services/requests/getAccount.ts @@ -0,0 +1,8 @@ +import { Account } from "@/models"; +import { API } from "../API"; + +export const getAccount = async ({ stakeKey }: { stakeKey?: string }) => { + const response = await API.get(`/account/${stakeKey || ""}`); + + return response.data; +}; diff --git a/govtool/frontend/src/services/requests/getAdaHolderVotingPower.ts b/govtool/frontend/src/services/requests/getAdaHolderVotingPower.ts index ec7a36755..c08b17b30 100644 --- a/govtool/frontend/src/services/requests/getAdaHolderVotingPower.ts +++ b/govtool/frontend/src/services/requests/getAdaHolderVotingPower.ts @@ -4,8 +4,10 @@ export const getAdaHolderVotingPower = async ({ stakeKey, }: { stakeKey?: string; -}): Promise => { - const response = await API.get(`/ada-holder/get-voting-power/${stakeKey}`); +}) => { + const response = await API.get( + `/ada-holder/get-voting-power/${stakeKey}`, + ); return response.data; }; diff --git a/govtool/frontend/src/services/requests/index.ts b/govtool/frontend/src/services/requests/index.ts index 0448747f2..598fe76d3 100644 --- a/govtool/frontend/src/services/requests/index.ts +++ b/govtool/frontend/src/services/requests/index.ts @@ -22,3 +22,4 @@ export * from "./postDRepRemoveVote"; export * from "./postDRepRetire"; export * from "./postDRepVote"; export * from "./getDRepVotingPowerList"; +export * from "./getAccount"; diff --git a/govtool/frontend/src/types/@intersect.mbo.d.ts b/govtool/frontend/src/types/@intersect.mbo.d.ts index f980a30bb..813a34eca 100644 --- a/govtool/frontend/src/types/@intersect.mbo.d.ts +++ b/govtool/frontend/src/types/@intersect.mbo.d.ts @@ -5,7 +5,7 @@ enum MetadataValidationStatus { INCORRECT_FORMAT = "INCORRECT_FORMAT", } declare module "@intersect.mbo/pdf-ui/cjs" { - import { EpochParams } from "@/models"; + import { EpochParams, Account } from "@/models"; type ProposalDiscussionProps = { pdfApiUrl: string; @@ -37,6 +37,11 @@ declare module "@intersect.mbo/pdf-ui/cjs" { }: { stakeKey?: string; }) => Promise; + getAccount: ({ + stakeKey, + }: { + stakeKey?: string; + }) => Promise; }; type GovernanceActionsOutcomesProps = {