From 4ee09238d8f1db1a23fb65ba784a390e46f1a732 Mon Sep 17 00:00:00 2001 From: Rene Vergara Date: Sat, 24 Aug 2024 07:45:42 -0500 Subject: [PATCH] Implement `getnewwallet` RPC method --- CHANGELOG.md | 1 + src/Zenith/RPC.hs | 68 +++++++++++++++++++++++++++++++++++++++++-- test/ServerSpec.hs | 71 ++++++++++++++++++++++++++++++++++++++++++++- zcash-haskell | 2 +- zenith-openrpc.json | 38 ++++++++++++++++++++++-- 5 files changed, 173 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2af69d6..091e91c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `listaddresses` RPC method - `listreceived` RPC method - `getbalance` RPC method +- `getnewwallet` RPC method ### Changed diff --git a/src/Zenith/RPC.hs b/src/Zenith/RPC.hs index ab6f507..b1e4ff1 100644 --- a/src/Zenith/RPC.hs +++ b/src/Zenith/RPC.hs @@ -16,12 +16,14 @@ import Control.Monad.IO.Class (liftIO) import Control.Monad.Logger (runNoLoggingT) import Data.Aeson import Data.Int +import Data.Scientific (floatingOrInteger) import qualified Data.Text as T import qualified Data.Text.Encoding as E import qualified Data.Vector as V -import Database.Esqueleto.Experimental (entityKey, toSqlKey) +import Database.Esqueleto.Experimental (entityKey, fromSqlKey, toSqlKey) import Servant import Text.Read (readMaybe) +import ZcashHaskell.Keys (generateWalletSeedPhrase) import ZcashHaskell.Orchard (parseAddress) import ZcashHaskell.Types ( RpcError(..) @@ -31,7 +33,8 @@ import ZcashHaskell.Types ) import Zenith.Core (checkBlockChain, checkZebra) import Zenith.DB - ( findNotesByAddress + ( ZcashWallet(..) + , findNotesByAddress , getAccountById , getAccounts , getAddressById @@ -42,6 +45,7 @@ import Zenith.DB , getWalletNotes , getWallets , initPool + , saveWallet , toZcashAccountAPI , toZcashAddressAPI , toZcashWalletAPI @@ -49,8 +53,10 @@ import Zenith.DB import Zenith.Types ( AccountBalance(..) , Config(..) + , PhraseDB(..) , ZcashAccountAPI(..) , ZcashAddressAPI(..) + , ZcashNetDB(..) , ZcashNoteAPI(..) , ZcashWalletAPI(..) ) @@ -63,6 +69,7 @@ data ZenithMethod | ListAddresses | ListReceived | GetBalance + | GetNewWallet | UnknownMethod deriving (Eq, Prelude.Show) @@ -73,6 +80,7 @@ instance ToJSON ZenithMethod where toJSON ListAddresses = Data.Aeson.String "listaddresses" toJSON ListReceived = Data.Aeson.String "listreceived" toJSON GetBalance = Data.Aeson.String "getbalance" + toJSON GetNewWallet = Data.Aeson.String "getnewwallet" toJSON UnknownMethod = Data.Aeson.Null instance FromJSON ZenithMethod where @@ -84,6 +92,7 @@ instance FromJSON ZenithMethod where "listaddresses" -> pure ListAddresses "listreceived" -> pure ListReceived "getbalance" -> pure GetBalance + "getnewwallet" -> pure GetNewWallet _ -> pure UnknownMethod data ZenithParams @@ -93,6 +102,7 @@ data ZenithParams | AddressesParams !Int | NotesParams !T.Text | BalanceParams !Int64 + | NameParams !T.Text | TestParams !T.Text deriving (Eq, Prelude.Show) @@ -103,6 +113,7 @@ 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 (NameParams t) = Data.Aeson.Array $ V.fromList [Data.Aeson.String t] toJSON (BalanceParams n) = Data.Aeson.Array $ V.fromList [jsonNumber $ fromIntegral n] @@ -113,6 +124,7 @@ data ZenithResponse | AddressListResponse !T.Text ![ZcashAddressAPI] | NoteListResponse !T.Text ![ZcashNoteAPI] | BalanceResponse !T.Text !AccountBalance !AccountBalance + | NewItemResponse !T.Text !Int64 | ErrorResponse !T.Text !Double !T.Text deriving (Eq, Prelude.Show) @@ -130,6 +142,7 @@ instance ToJSON ZenithResponse where ] toJSON (BalanceResponse i c u) = packRpcResponse i $ object ["confirmed" .= c, "unconfirmed" .= u] + toJSON (NewItemResponse i ix) = packRpcResponse i ix instance FromJSON ZenithResponse where parseJSON = @@ -193,6 +206,10 @@ instance FromJSON ZenithResponse where pure $ NoteListResponse i k5 Nothing -> fail "Unknown object" _anyOther -> fail "Malformed JSON" + Number k -> do + case floatingOrInteger k of + Left _e -> fail "Unknown value" + Right k' -> pure $ NewItemResponse i k' _anyOther -> fail "Malformed JSON" Just e1 -> pure $ ErrorResponse i (ecode e1) (emessage e1) @@ -284,6 +301,16 @@ instance FromJSON RpcCall where pure $ RpcCall v i GetBalance (BalanceParams x) else pure $ RpcCall v i GetBalance BadParams _anyOther -> pure $ RpcCall v i GetBalance BadParams + GetNewWallet -> 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 GetNewWallet (NameParams x) + else pure $ RpcCall v i GetNewWallet BadParams + _anyOther -> pure $ RpcCall v i GetNewWallet BadParams type ZenithRPC = "status" :> Get '[ JSON] Value :<|> BasicAuth "zenith-realm" Bool :> ReqBody @@ -452,6 +479,43 @@ zenithServer config = getinfo :<|> handleRPC ErrorResponse (callId req) (-32006) "Account does not exist." _anyOtherParams -> return $ ErrorResponse (callId req) (-32602) "Invalid params" + GetNewWallet -> + case parameters req of + NameParams t -> do + let host = c_zebraHost config + let port = c_zebraPort config + let dbPath = c_dbPath config + sP <- liftIO generateWalletSeedPhrase + pool <- liftIO $ runNoLoggingT $ initPool dbPath + bInfo <- + liftIO $ try $ checkBlockChain host port :: Handler + (Either IOError ZebraGetBlockChainInfo) + case bInfo of + Left _e1 -> + return $ + ErrorResponse (callId req) (-32000) "Zebra not available" + Right bI -> do + r <- + liftIO $ + saveWallet pool $ + ZcashWallet + t + (ZcashNetDB $ zgb_net bI) + (PhraseDB sP) + (zgb_blocks bI) + 0 + case r of + Nothing -> + return $ + ErrorResponse + (callId req) + (-32007) + "Entity with that name already exists." + Just r' -> + return $ + NewItemResponse (callId req) $ fromSqlKey $ entityKey r' + _anyOtherParams -> + return $ ErrorResponse (callId req) (-32602) "Invalid params" authenticate :: Config -> BasicAuthCheck Bool authenticate config = BasicAuthCheck check diff --git a/test/ServerSpec.hs b/test/ServerSpec.hs index 080dec6..5bafd43 100644 --- a/test/ServerSpec.hs +++ b/test/ServerSpec.hs @@ -26,7 +26,7 @@ import Zenith.RPC , authenticate , zenithServer ) -import Zenith.Types (Config(..)) +import Zenith.Types (Config(..), ZcashWalletAPI(..)) main :: IO () main = do @@ -95,6 +95,75 @@ main = do "zh" (-32001) "No wallets available. Please create one first" + describe "getnewwallet" $ do + it "bad credentials" $ do + res <- + makeZenithCall + "127.0.0.1" + nodePort + "baduser" + "idontknow" + GetNewWallet + BlankParams + res `shouldBe` Left "Invalid credentials" + describe "correct credentials" $ do + it "no params" $ do + res <- + makeZenithCall + "127.0.0.1" + nodePort + nodeUser + nodePwd + GetNewWallet + BlankParams + case res of + Left e -> assertFailure e + Right r -> + r `shouldBe` ErrorResponse "zh" (-32602) "Invalid params" + it "Valid params" $ do + res <- + makeZenithCall + "127.0.0.1" + nodePort + nodeUser + nodePwd + GetNewWallet + (NameParams "Main") + case res of + Left e -> assertFailure e + Right r -> r `shouldBe` NewItemResponse "zh" 1 + it "duplicate name" $ do + res <- + makeZenithCall + "127.0.0.1" + nodePort + nodeUser + nodePwd + GetNewWallet + (NameParams "Main") + case res of + Left e -> assertFailure e + Right r -> + r `shouldBe` + ErrorResponse + "zh" + (-32007) + "Entity with that name already exists." + describe "listwallet" $ do + it "wallet exists" $ do + res <- + makeZenithCall + "127.0.0.1" + nodePort + nodeUser + nodePwd + ListWallets + BlankParams + case res of + Left e -> assertFailure e + Right (WalletListResponse i k) -> + zw_name (head k) `shouldBe` "Main" + Right _ -> assertFailure "Unexpected response" describe "Accounts" $ do describe "listaccounts" $ do it "bad credentials" $ do diff --git a/zcash-haskell b/zcash-haskell index 939ae68..0b2fae2 160000 --- a/zcash-haskell +++ b/zcash-haskell @@ -1 +1 @@ -Subproject commit 939ae687e8485f5ffce2f09d49c23aac7e14bf72 +Subproject commit 0b2fae2b5db6878b7669d639a5cb8c73b986906e diff --git a/zenith-openrpc.json b/zenith-openrpc.json index da3e860..d1ec86e 100644 --- a/zenith-openrpc.json +++ b/zenith-openrpc.json @@ -101,14 +101,33 @@ "name": "getnewwallet", "summary": "Create a new wallet", "description": "Create a new wallet for Zenith.", - "tags": [{"$ref": "#/components/tags/draft"}], - "params": [], + "tags": [], + "params": [ + { "$ref": "#/components/contentDescriptors/Name"} + ], + "paramStructure": "by-position", "result": { "name": "Wallet", "schema": { "$ref": "#/components/contentDescriptors/WalletId" } - } + }, + "examples": [ + { + "name": "GetNewWallet example", + "summary": "Create a wallet", + "description": "Creates a new wallet with the given name", + "params": [ "Main" ], + "result": { + "name": "GetNewWallet result", + "value": 1 + } + } + ], + "errors": [ + { "$ref": "#/components/errors/ZebraNotAvailable" }, + { "$ref": "#/components/errors/DuplicateName" } + ] }, { "name": "listaccounts", @@ -439,6 +458,15 @@ "schema": { "type": "string" } + }, + "Name": { + "name": "Name", + "summary": "A user-friendly name", + "description": "A string that represents an entity in Zenith, like a wallet, an account or an address.", + "required": true, + "schema": { + "type": "string" + } } }, "schemas": { @@ -536,6 +564,10 @@ "InvalidAccount": { "code": -32006, "message": "Account does not exist." + }, + "DuplicateName": { + "code": -32007, + "message": "Entity with that name already exists." } } }