From 934bff1454ea423d414143b988b425ad2f4d90b4 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Fri, 16 Aug 2024 13:31:25 -0500 Subject: [PATCH] Implement `getbalance` --- CHANGELOG.md | 2 ++ src/Zenith/DB.hs | 29 ++++++++++++++++++++- src/Zenith/RPC.hs | 62 +++++++++++++++++++++++++++++++++++++++++---- src/Zenith/Types.hs | 8 ++++++ test/ServerSpec.hs | 37 +++++++++++++++++++++++++++ zenith-openrpc.json | 2 +- 6 files changed, 133 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83933ab..2af69d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `listwallets` RPC method - `listaccounts` RPC method - `listaddresses` RPC method +- `listreceived` RPC method +- `getbalance` RPC method ### Changed diff --git a/src/Zenith/DB.hs b/src/Zenith/DB.hs index e2ac0b2..129a958 100644 --- a/src/Zenith/DB.hs +++ b/src/Zenith/DB.hs @@ -72,7 +72,8 @@ import ZcashHaskell.Types , ZcashNet(..) ) import Zenith.Types - ( HexStringDB(..) + ( AccountBalance(..) + , HexStringDB(..) , OrchardSpendingKeyDB(..) , PhraseDB(..) , RseedDB(..) @@ -1693,6 +1694,32 @@ getUnconfirmedBalance pool za = do let oBal = sum oAmts return . fromIntegral $ tBal + sBal + oBal +getPoolBalance :: ConnectionPool -> ZcashAccountId -> IO AccountBalance +getPoolBalance pool za = do + trNotes <- getWalletUnspentTrNotes pool za + let tAmts = map (walletTrNoteValue . entityVal) trNotes + let tBal = sum tAmts + sapNotes <- getWalletUnspentSapNotes pool za + let sAmts = map (walletSapNoteValue . entityVal) sapNotes + let sBal = sum sAmts + orchNotes <- getWalletUnspentOrchNotes pool za + let oAmts = map (walletOrchNoteValue . entityVal) orchNotes + let oBal = sum oAmts + return $ AccountBalance tBal sBal oBal + +getUnconfPoolBalance :: ConnectionPool -> ZcashAccountId -> IO AccountBalance +getUnconfPoolBalance pool za = do + trNotes <- getWalletUnspentUnconfirmedTrNotes pool za + let tAmts = map (walletTrNoteValue . entityVal) trNotes + let tBal = sum tAmts + sapNotes <- getWalletUnspentUnconfirmedSapNotes pool za + let sAmts = map (walletSapNoteValue . entityVal) sapNotes + let sBal = sum sAmts + orchNotes <- getWalletUnspentUnconfirmedOrchNotes pool za + let oAmts = map (walletOrchNoteValue . entityVal) orchNotes + let oBal = sum oAmts + return $ AccountBalance tBal sBal oBal + clearWalletTransactions :: ConnectionPool -> IO () clearWalletTransactions pool = do runNoLoggingT $ diff --git a/src/Zenith/RPC.hs b/src/Zenith/RPC.hs index 6b38be3..ab6f507 100644 --- a/src/Zenith/RPC.hs +++ b/src/Zenith/RPC.hs @@ -19,13 +19,12 @@ import Data.Int import qualified Data.Text as T import qualified Data.Text.Encoding as E import qualified Data.Vector as V -import Database.Esqueleto.Experimental (toSqlKey) +import Database.Esqueleto.Experimental (entityKey, toSqlKey) import Servant import Text.Read (readMaybe) import ZcashHaskell.Orchard (parseAddress) import ZcashHaskell.Types ( RpcError(..) - , ValidAddress(..) , ZcashNet(..) , ZebraGetBlockChainInfo(..) , ZebraGetInfo(..) @@ -33,10 +32,13 @@ import ZcashHaskell.Types import Zenith.Core (checkBlockChain, checkZebra) import Zenith.DB ( findNotesByAddress + , getAccountById , getAccounts , getAddressById , getAddresses , getExternalAddresses + , getPoolBalance + , getUnconfPoolBalance , getWalletNotes , getWallets , initPool @@ -45,7 +47,8 @@ import Zenith.DB , toZcashWalletAPI ) import Zenith.Types - ( Config(..) + ( AccountBalance(..) + , Config(..) , ZcashAccountAPI(..) , ZcashAddressAPI(..) , ZcashNoteAPI(..) @@ -59,6 +62,7 @@ data ZenithMethod | ListAccounts | ListAddresses | ListReceived + | GetBalance | UnknownMethod deriving (Eq, Prelude.Show) @@ -68,6 +72,7 @@ instance ToJSON ZenithMethod where toJSON ListAccounts = Data.Aeson.String "listaccounts" toJSON ListAddresses = Data.Aeson.String "listaddresses" toJSON ListReceived = Data.Aeson.String "listreceived" + toJSON GetBalance = Data.Aeson.String "getbalance" toJSON UnknownMethod = Data.Aeson.Null instance FromJSON ZenithMethod where @@ -78,6 +83,7 @@ instance FromJSON ZenithMethod where "listaccounts" -> pure ListAccounts "listaddresses" -> pure ListAddresses "listreceived" -> pure ListReceived + "getbalance" -> pure GetBalance _ -> pure UnknownMethod data ZenithParams @@ -86,6 +92,7 @@ data ZenithParams | AccountsParams !Int | AddressesParams !Int | NotesParams !T.Text + | BalanceParams !Int64 | TestParams !T.Text deriving (Eq, Prelude.Show) @@ -96,6 +103,8 @@ instance ToJSON ZenithParams where toJSON (AddressesParams n) = Data.Aeson.Array $ V.fromList [jsonNumber n] toJSON (TestParams t) = Data.Aeson.Array $ V.fromList [Data.Aeson.String t] toJSON (NotesParams t) = Data.Aeson.Array $ V.fromList [Data.Aeson.String t] + toJSON (BalanceParams n) = + Data.Aeson.Array $ V.fromList [jsonNumber $ fromIntegral n] data ZenithResponse = InfoResponse !T.Text !ZenithInfo @@ -103,6 +112,7 @@ data ZenithResponse | AccountListResponse !T.Text ![ZcashAccountAPI] | AddressListResponse !T.Text ![ZcashAddressAPI] | NoteListResponse !T.Text ![ZcashNoteAPI] + | BalanceResponse !T.Text !AccountBalance !AccountBalance | ErrorResponse !T.Text !Double !T.Text deriving (Eq, Prelude.Show) @@ -118,6 +128,8 @@ instance ToJSON ZenithResponse where , "id" .= i , "error" .= object ["code" .= c, "message" .= m] ] + toJSON (BalanceResponse i c u) = + packRpcResponse i $ object ["confirmed" .= c, "unconfirmed" .= u] instance FromJSON ZenithResponse where parseJSON = @@ -137,11 +149,19 @@ instance FromJSON ZenithResponse where case r1 of Object k -> do v <- k .:? "version" + v5 <- k .:? "unconfirmed" case (v :: Maybe String) of - Nothing -> fail "Unknown result" Just _v' -> do k1 <- parseJSON r1 pure $ InfoResponse i k1 + Nothing -> + case (v5 :: Maybe AccountBalance) of + Just _v5' -> do + k6 <- parseJSON r1 + j1 <- k6 .: "confirmed" + j2 <- k6 .: "unconfirmed" + pure $ BalanceResponse i j1 j2 + Nothing -> fail "Unknown object" Array n -> do if V.null n then fail "Malformed JSON" @@ -151,6 +171,7 @@ instance FromJSON ZenithResponse where v1 <- n' .:? "lastSync" v2 <- n' .:? "wallet" v3 <- n' .:? "ua" + v4 <- n' .:? "amountZats" case (v1 :: Maybe Int) of Just _v1' -> do k2 <- parseJSON r1 @@ -165,7 +186,12 @@ instance FromJSON ZenithResponse where Just _v3' -> do k4 <- parseJSON r1 pure $ AddressListResponse i k4 - Nothing -> fail "Unknown object" + Nothing -> + case (v4 :: Maybe Int) of + Just _v4' -> do + k5 <- parseJSON r1 + pure $ NoteListResponse i k5 + Nothing -> fail "Unknown object" _anyOther -> fail "Malformed JSON" _anyOther -> fail "Malformed JSON" Just e1 -> pure $ ErrorResponse i (ecode e1) (emessage e1) @@ -248,6 +274,16 @@ instance FromJSON RpcCall where pure $ RpcCall v i ListReceived (NotesParams x) else pure $ RpcCall v i ListReceived BadParams _anyOther -> pure $ RpcCall v i ListReceived BadParams + GetBalance -> do + p <- obj .: "params" + case p of + Array a -> + if V.length a == 1 + then do + x <- parseJSON $ V.head a + pure $ RpcCall v i GetBalance (BalanceParams x) + else pure $ RpcCall v i GetBalance BadParams + _anyOther -> pure $ RpcCall v i GetBalance BadParams type ZenithRPC = "status" :> Get '[ JSON] Value :<|> BasicAuth "zenith-realm" Bool :> ReqBody @@ -400,6 +436,22 @@ zenithServer config = getinfo :<|> handleRPC return $ NoteListResponse (callId req) nList _anyOtherParams -> return $ ErrorResponse (callId req) (-32602) "Invalid params" + GetBalance -> + case parameters req of + BalanceParams i -> do + let dbPath = c_dbPath config + pool <- liftIO $ runNoLoggingT $ initPool dbPath + acc <- liftIO $ getAccountById pool $ toSqlKey i + case acc of + Just acc' -> do + c <- liftIO $ getPoolBalance pool $ entityKey acc' + u <- liftIO $ getUnconfPoolBalance pool $ entityKey acc' + return $ BalanceResponse (callId req) c u + Nothing -> + return $ + ErrorResponse (callId req) (-32006) "Account does not exist." + _anyOtherParams -> + return $ ErrorResponse (callId req) (-32602) "Invalid params" authenticate :: Config -> BasicAuthCheck Bool authenticate config = BasicAuthCheck check diff --git a/src/Zenith/Types.hs b/src/Zenith/Types.hs index 987c994..279c18a 100644 --- a/src/Zenith/Types.hs +++ b/src/Zenith/Types.hs @@ -175,6 +175,14 @@ data ZcashNoteAPI = ZcashNoteAPI $(deriveJSON defaultOptions {fieldLabelModifier = drop 3} ''ZcashNoteAPI) +data AccountBalance = AccountBalance + { acb_transparent :: !Int64 + , acb_sapling :: !Int64 + , acb_orchard :: !Int64 + } deriving (Eq, Prelude.Show) + +$(deriveJSON defaultOptions {fieldLabelModifier = drop 4} ''AccountBalance) + -- ** `zebrad` -- | Type for modeling the tree state response data ZebraTreeInfo = ZebraTreeInfo diff --git a/test/ServerSpec.hs b/test/ServerSpec.hs index 30593ca..080dec6 100644 --- a/test/ServerSpec.hs +++ b/test/ServerSpec.hs @@ -190,6 +190,43 @@ main = do case res of Left e -> assertFailure e Right (ErrorResponse i c m) -> c `shouldBe` (-32004) + describe "Balance" $ do + describe "getbalance" $ do + it "bad credentials" $ do + res <- + makeZenithCall + "127.0.0.1" + nodePort + "baduser" + "idontknow" + GetBalance + BlankParams + res `shouldBe` Left "Invalid credentials" + describe "correct credentials" $ do + it "no parameters" $ do + res <- + makeZenithCall + "127.0.0.1" + nodePort + nodeUser + nodePwd + GetBalance + BlankParams + case res of + Left e -> assertFailure e + Right (ErrorResponse i c m) -> c `shouldBe` (-32602) + it "unknown index" $ do + res <- + makeZenithCall + "127.0.0.1" + nodePort + nodeUser + nodePwd + GetBalance + (BalanceParams 17) + case res of + Left e -> assertFailure e + Right (ErrorResponse i c m) -> c `shouldBe` (-32006) startAPI :: Config -> IO () startAPI config = do diff --git a/zenith-openrpc.json b/zenith-openrpc.json index 1b6890a..da3e860 100644 --- a/zenith-openrpc.json +++ b/zenith-openrpc.json @@ -247,7 +247,7 @@ "name": "getbalance", "summary": "Get the balance of the given account", "description": "Get the balance of the given account, including any unconfirmed balance.", - "tags": [{"$ref": "#/components/tags/draft"},{"$ref": "#/components/tags/wip"}], + "tags": [], "params": [{ "$ref": "#/components/contentDescriptors/AccountId"}], "result": { "name": "Balance",