From 5b2ce0c97bc5159d6079452ef72cce9ca1cb001e Mon Sep 17 00:00:00 2001 From: Yuriy Lazaryev Date: Fri, 18 Mar 2022 18:46:14 +0100 Subject: [PATCH 1/6] Rework CLI error reporting for the --blockfrost-token-file --- lib/shelley/cardano-wallet.cabal | 5 +-- lib/shelley/exe/cardano-wallet.hs | 13 +++++++- .../Wallet/Shelley/Launch/Blockfrost.hs | 32 ++++++++++++++++--- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/lib/shelley/cardano-wallet.cabal b/lib/shelley/cardano-wallet.cabal index 65a7611de3f..b9253e7cb36 100644 --- a/lib/shelley/cardano-wallet.cabal +++ b/lib/shelley/cardano-wallet.cabal @@ -36,8 +36,9 @@ library , bech32 , bech32-th , binary - , blockfrost-client >=0.3.1.0 && <0.4 - , blockfrost-client-core >=0.2.0.0 && <0.3 + , blockfrost-api + , blockfrost-client + , blockfrost-client-core , bytestring , cardano-addresses , cardano-api diff --git a/lib/shelley/exe/cardano-wallet.hs b/lib/shelley/exe/cardano-wallet.hs index d39e7ef9d0f..dc52f6d6ea6 100644 --- a/lib/shelley/exe/cardano-wallet.hs +++ b/lib/shelley/exe/cardano-wallet.hs @@ -143,7 +143,7 @@ import System.Environment import System.Exit ( ExitCode (..), exitWith ) import UnliftIO.Exception - ( withException ) + ( catch, withException ) import qualified Cardano.BM.Backend.EKGView as EKG import qualified Cardano.Wallet.Shelley.Launch.Blockfrost as Blockfrost @@ -240,6 +240,9 @@ cmdServe = command "serve" $ info (helper <*> helper' <*> cmd) $ pure $ NodeSource conn vData Light token -> BlockfrostSource <$> Blockfrost.readToken token + `catch` \(Blockfrost.TokenFileException fp) -> do + logError tr (MsgBlockfrostTokenError fp) + exitWith $ ExitFailure 1 exitWith =<< serveWallet blockchainSource @@ -265,6 +268,7 @@ cmdServe = command "serve" $ info (helper <*> helper' <*> cmd) $ withShutdownHandlerMaybe tr True = void . withShutdownHandler trShutdown where trShutdown = trMessage $ contramap (second (fmap MsgShutdownHandler)) tr + {------------------------------------------------------------------------------- Logging -------------------------------------------------------------------------------} @@ -280,6 +284,7 @@ data MainLog | MsgSigInt | MsgShutdownHandler ShutdownHandlerLog | MsgFailedToParseGenesis Text + | MsgBlockfrostTokenError FilePath deriving (Show) instance ToText MainLog where @@ -311,6 +316,12 @@ instance ToText MainLog where , "parameters." , "Here's (perhaps) some helpful hint:", hint ] + MsgBlockfrostTokenError tokenFile -> T.unwords + [ "File" + , T.pack tokenFile + , "specified in the --blockfrost-token-file\ + \ argument doesn't contain a valid Blockfrost API token." + ] withTracers :: LoggingOptions TracerSeverities diff --git a/lib/shelley/src/Cardano/Wallet/Shelley/Launch/Blockfrost.hs b/lib/shelley/src/Cardano/Wallet/Shelley/Launch/Blockfrost.hs index 94caaabea19..eede2e86b50 100644 --- a/lib/shelley/src/Cardano/Wallet/Shelley/Launch/Blockfrost.hs +++ b/lib/shelley/src/Cardano/Wallet/Shelley/Launch/Blockfrost.hs @@ -1,24 +1,38 @@ +{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE ScopedTypeVariables #-} module Cardano.Wallet.Shelley.Launch.Blockfrost - ( TokenFile + ( TokenFile (..) , readToken , tokenFileOption + , TokenFileException(..) ) where import Prelude -import Blockfrost.Client.Core - ( projectFromFile ) import Blockfrost.Client.Types ( Project (..) ) +import Blockfrost.Env + ( parseEnv ) +import Control.Exception + ( Exception, throw ) +import Data.Text + ( Text ) import Options.Applicative ( Parser, help, long, metavar, option, str ) +import qualified Data.Text as Text +import qualified Data.Text.IO as Text + newtype TokenFile = TokenFile FilePath deriving newtype (Eq, Show) +newtype TokenFileException = TokenFileException FilePath + deriving stock (Eq, Show) + deriving anyclass (Exception) + -- | --blockfrost-token-file FILE tokenFileOption :: Parser TokenFile tokenFileOption = option (TokenFile <$> str) $ mconcat @@ -31,4 +45,14 @@ tokenFileOption = option (TokenFile <$> str) $ mconcat ] readToken :: TokenFile -> IO Project -readToken (TokenFile fp) = projectFromFile fp +readToken (TokenFile fp) = Text.readFile fp >>= + either (throw (TokenFileException fp) . const) pure . mkProject + where + -- Can't use `Blockfrost.Client.Core.projectFromFile` as it uses `error` + -- and it leads to an unnecessary output that pollutes stdout. + mkProject :: Text -> Either Text Project + mkProject t = + let st = Text.strip t + tEnv = Text.dropEnd 32 st + token = Text.drop (Text.length tEnv) st + in Project <$> parseEnv tEnv <*> pure token From 9c1a91c69a86c2b2ce9bce705f5785978badee20 Mon Sep 17 00:00:00 2001 From: IOHK Date: Fri, 18 Mar 2022 17:52:59 +0000 Subject: [PATCH 2/6] Regenerate nix --- nix/materialized/stack-nix/cardano-wallet.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nix/materialized/stack-nix/cardano-wallet.nix b/nix/materialized/stack-nix/cardano-wallet.nix index 0c80c83090f..8b6e9a14226 100644 --- a/nix/materialized/stack-nix/cardano-wallet.nix +++ b/nix/materialized/stack-nix/cardano-wallet.nix @@ -40,6 +40,7 @@ (hsPkgs."bech32" or (errorHandler.buildDepError "bech32")) (hsPkgs."bech32-th" or (errorHandler.buildDepError "bech32-th")) (hsPkgs."binary" or (errorHandler.buildDepError "binary")) + (hsPkgs."blockfrost-api" or (errorHandler.buildDepError "blockfrost-api")) (hsPkgs."blockfrost-client" or (errorHandler.buildDepError "blockfrost-client")) (hsPkgs."blockfrost-client-core" or (errorHandler.buildDepError "blockfrost-client-core")) (hsPkgs."bytestring" or (errorHandler.buildDepError "bytestring")) From 4e92301f42af41cb2c9e09c5612391cf4d3fd513 Mon Sep 17 00:00:00 2001 From: Yuriy Lazaryev Date: Mon, 21 Mar 2022 09:46:36 +0100 Subject: [PATCH 3/6] refactor: rename qualified import to make hlint happy --- .../src/Cardano/Wallet/Shelley/Launch/Blockfrost.hs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/shelley/src/Cardano/Wallet/Shelley/Launch/Blockfrost.hs b/lib/shelley/src/Cardano/Wallet/Shelley/Launch/Blockfrost.hs index eede2e86b50..1d4396df1dc 100644 --- a/lib/shelley/src/Cardano/Wallet/Shelley/Launch/Blockfrost.hs +++ b/lib/shelley/src/Cardano/Wallet/Shelley/Launch/Blockfrost.hs @@ -23,8 +23,8 @@ import Data.Text import Options.Applicative ( Parser, help, long, metavar, option, str ) -import qualified Data.Text as Text -import qualified Data.Text.IO as Text +import qualified Data.Text.IO as T +import qualified Data.Text as T newtype TokenFile = TokenFile FilePath deriving newtype (Eq, Show) @@ -45,14 +45,14 @@ tokenFileOption = option (TokenFile <$> str) $ mconcat ] readToken :: TokenFile -> IO Project -readToken (TokenFile fp) = Text.readFile fp >>= +readToken (TokenFile fp) = T.readFile fp >>= either (throw (TokenFileException fp) . const) pure . mkProject where -- Can't use `Blockfrost.Client.Core.projectFromFile` as it uses `error` -- and it leads to an unnecessary output that pollutes stdout. mkProject :: Text -> Either Text Project mkProject t = - let st = Text.strip t - tEnv = Text.dropEnd 32 st - token = Text.drop (Text.length tEnv) st + let st = T.strip t + tEnv = T.dropEnd 32 st + token = T.drop (T.length tEnv) st in Project <$> parseEnv tEnv <*> pure token From d0ae20979d726540be70404890a118cb1913f9a2 Mon Sep 17 00:00:00 2001 From: Yuriy Lazaryev Date: Mon, 21 Mar 2022 09:58:33 +0100 Subject: [PATCH 4/6] refactor: less fragile assertion --- .../Cardano/Wallet/Shelley/Launch/BlockfrostSpec.hs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/shelley/test/unit/Cardano/Wallet/Shelley/Launch/BlockfrostSpec.hs b/lib/shelley/test/unit/Cardano/Wallet/Shelley/Launch/BlockfrostSpec.hs index 8b523205aac..85772fc7657 100644 --- a/lib/shelley/test/unit/Cardano/Wallet/Shelley/Launch/BlockfrostSpec.hs +++ b/lib/shelley/test/unit/Cardano/Wallet/Shelley/Launch/BlockfrostSpec.hs @@ -22,7 +22,7 @@ import Options.Applicative , info ) import Test.Hspec - ( Spec, describe, expectationFailure, it, shouldBe, shouldReturn ) + ( Spec, describe, expectationFailure, it, shouldReturn, shouldStartWith ) import Test.Utils.Platform ( isWindows ) import UnliftIO @@ -60,14 +60,8 @@ spec = describe "Blockfrost CLI options" $ do args = ["--blockfrost-token-file", mockSocketOrPipe] case execParserPure defaultPrefs parserInfo args of Failure pf | (help, _code, _int) <- execFailure pf "" -> - show help `shouldBe` - "Missing: --light\n\n\ - \Usage: (--node-socket " <> nodeSocketMetavar <> " | \ - \--light --blockfrost-token-file FILE)" + show help `shouldStartWith` "Missing: --light" result -> expectationFailure $ show result -nodeSocketMetavar :: String -nodeSocketMetavar = if isWindows then "PIPENAME" else "FILE" - mockSocketOrPipe :: String mockSocketOrPipe = if isWindows then "\\\\.\\pipe\\test" else "/tmp/pipe" From 91667ea175e52edacb2a30e854822f6b07906d62 Mon Sep 17 00:00:00 2001 From: Yuriy Lazaryev Date: Mon, 21 Mar 2022 10:47:42 +0100 Subject: [PATCH 5/6] test: readToken --- .../Wallet/Shelley/Launch/Blockfrost.hs | 32 +++++++++++-------- .../Wallet/Shelley/Launch/BlockfrostSpec.hs | 29 +++++++++++++++-- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/lib/shelley/src/Cardano/Wallet/Shelley/Launch/Blockfrost.hs b/lib/shelley/src/Cardano/Wallet/Shelley/Launch/Blockfrost.hs index 1d4396df1dc..85147901685 100644 --- a/lib/shelley/src/Cardano/Wallet/Shelley/Launch/Blockfrost.hs +++ b/lib/shelley/src/Cardano/Wallet/Shelley/Launch/Blockfrost.hs @@ -7,6 +7,7 @@ module Cardano.Wallet.Shelley.Launch.Blockfrost ( TokenFile (..) , readToken , tokenFileOption + , TokenException(..) , TokenFileException(..) ) where @@ -17,18 +18,22 @@ import Blockfrost.Client.Types import Blockfrost.Env ( parseEnv ) import Control.Exception - ( Exception, throw ) -import Data.Text - ( Text ) + ( Exception, IOException, catch, throw ) +import Control.Monad + ( when ) import Options.Applicative ( Parser, help, long, metavar, option, str ) -import qualified Data.Text.IO as T import qualified Data.Text as T +import qualified Data.Text.IO as T newtype TokenFile = TokenFile FilePath deriving newtype (Eq, Show) +data TokenException = EmptyToken FilePath | InvalidToken FilePath + deriving stock (Eq, Show) + deriving anyclass (Exception) + newtype TokenFileException = TokenFileException FilePath deriving stock (Eq, Show) deriving anyclass (Exception) @@ -45,14 +50,15 @@ tokenFileOption = option (TokenFile <$> str) $ mconcat ] readToken :: TokenFile -> IO Project -readToken (TokenFile fp) = T.readFile fp >>= - either (throw (TokenFileException fp) . const) pure . mkProject - where +readToken (TokenFile fp) = do -- Can't use `Blockfrost.Client.Core.projectFromFile` as it uses `error` -- and it leads to an unnecessary output that pollutes stdout. - mkProject :: Text -> Either Text Project - mkProject t = - let st = T.strip t - tEnv = T.dropEnd 32 st - token = T.drop (T.length tEnv) st - in Project <$> parseEnv tEnv <*> pure token + line <- T.readFile fp `catch` \(_ :: IOException) -> + throw $ TokenFileException fp + let tokenSrc = T.strip line + when (T.null tokenSrc) $ throw $ EmptyToken fp + let tEnv = T.dropEnd 32 tokenSrc + token = T.drop (T.length tEnv) tokenSrc + case Project <$> parseEnv tEnv <*> pure token of + Left _ -> throw $ InvalidToken fp + Right project -> pure project diff --git a/lib/shelley/test/unit/Cardano/Wallet/Shelley/Launch/BlockfrostSpec.hs b/lib/shelley/test/unit/Cardano/Wallet/Shelley/Launch/BlockfrostSpec.hs index 85772fc7657..3cda1b80f20 100644 --- a/lib/shelley/test/unit/Cardano/Wallet/Shelley/Launch/BlockfrostSpec.hs +++ b/lib/shelley/test/unit/Cardano/Wallet/Shelley/Launch/BlockfrostSpec.hs @@ -12,7 +12,11 @@ import Blockfrost.Env import Cardano.Wallet.Shelley.Launch ( Mode (Light, Normal), modeOption ) import Cardano.Wallet.Shelley.Launch.Blockfrost - ( readToken ) + ( TokenException (..) + , TokenFile (TokenFile) + , TokenFileException (TokenFileException) + , readToken + ) import Options.Applicative ( ParserFailure (execFailure) , ParserResult (CompletionInvoked, Failure, Success) @@ -22,7 +26,14 @@ import Options.Applicative , info ) import Test.Hspec - ( Spec, describe, expectationFailure, it, shouldReturn, shouldStartWith ) + ( Spec + , describe + , expectationFailure + , it + , shouldReturn + , shouldStartWith + , shouldThrow + ) import Test.Utils.Platform ( isWindows ) import UnliftIO @@ -63,5 +74,19 @@ spec = describe "Blockfrost CLI options" $ do show help `shouldStartWith` "Missing: --light" result -> expectationFailure $ show result + it "readToken throws in case of a non-existing token file" $ do + readToken (TokenFile "non-existing-file") + `shouldThrow` \(TokenFileException _) -> True + + it "readToken throws in case of an empty token file" $ + withSystemTempFile "blockfrost.token" $ \f h -> do + hClose h + readToken (TokenFile f) `shouldThrow` \(EmptyToken _) -> True + + it "readToken throws in case of an invalid token file content" $ + withSystemTempFile "blockfrost.token" $ \f h -> do + hClose h *> writeFile f "invalid" + readToken (TokenFile f) `shouldThrow` \(InvalidToken _) -> True + mockSocketOrPipe :: String mockSocketOrPipe = if isWindows then "\\\\.\\pipe\\test" else "/tmp/pipe" From e4954462c3cb79398cb256113ff571302614d37c Mon Sep 17 00:00:00 2001 From: Yuriy Lazaryev Date: Mon, 21 Mar 2022 11:38:56 +0100 Subject: [PATCH 6/6] improve blockfrost token error reporting --- lib/shelley/exe/cardano-wallet.hs | 25 +++++++++++++------ .../Wallet/Shelley/Launch/Blockfrost.hs | 19 ++++++-------- .../Wallet/Shelley/Launch/BlockfrostSpec.hs | 8 ++---- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/lib/shelley/exe/cardano-wallet.hs b/lib/shelley/exe/cardano-wallet.hs index dc52f6d6ea6..2048d021af6 100644 --- a/lib/shelley/exe/cardano-wallet.hs +++ b/lib/shelley/exe/cardano-wallet.hs @@ -236,12 +236,17 @@ cmdServe = command "serve" $ info (helper <*> helper' <*> cmd) $ setupDirectory (logInfo tr . MsgSetupDatabases) blockchainSource <- case mode of - Normal conn -> - pure $ NodeSource conn vData - Light token -> - BlockfrostSource <$> Blockfrost.readToken token - `catch` \(Blockfrost.TokenFileException fp) -> do - logError tr (MsgBlockfrostTokenError fp) + Normal conn -> pure $ NodeSource conn vData + Light token -> BlockfrostSource <$> Blockfrost.readToken token + `catch` \case + Blockfrost.BadTokenFile f -> do + logError tr $ MsgBlockfrostTokenFileError f + exitWith $ ExitFailure 1 + Blockfrost.EmptyToken f -> do + logError tr $ MsgBlockfrostTokenError f + exitWith $ ExitFailure 1 + Blockfrost.InvalidToken f -> do + logError tr $ MsgBlockfrostTokenError f exitWith $ ExitFailure 1 exitWith =<< serveWallet @@ -284,6 +289,7 @@ data MainLog | MsgSigInt | MsgShutdownHandler ShutdownHandlerLog | MsgFailedToParseGenesis Text + | MsgBlockfrostTokenFileError FilePath | MsgBlockfrostTokenError FilePath deriving (Show) @@ -316,9 +322,14 @@ instance ToText MainLog where , "parameters." , "Here's (perhaps) some helpful hint:", hint ] + MsgBlockfrostTokenFileError tokenFile -> T.unwords + [ "File" + , "'" <> T.pack tokenFile <> "'" + , "specified in the --blockfrost-token-file can't be read." + ] MsgBlockfrostTokenError tokenFile -> T.unwords [ "File" - , T.pack tokenFile + , "'" <> T.pack tokenFile <> "'" , "specified in the --blockfrost-token-file\ \ argument doesn't contain a valid Blockfrost API token." ] diff --git a/lib/shelley/src/Cardano/Wallet/Shelley/Launch/Blockfrost.hs b/lib/shelley/src/Cardano/Wallet/Shelley/Launch/Blockfrost.hs index 85147901685..566b1a9c7be 100644 --- a/lib/shelley/src/Cardano/Wallet/Shelley/Launch/Blockfrost.hs +++ b/lib/shelley/src/Cardano/Wallet/Shelley/Launch/Blockfrost.hs @@ -8,7 +8,6 @@ module Cardano.Wallet.Shelley.Launch.Blockfrost , readToken , tokenFileOption , TokenException(..) - , TokenFileException(..) ) where import Prelude @@ -30,11 +29,10 @@ import qualified Data.Text.IO as T newtype TokenFile = TokenFile FilePath deriving newtype (Eq, Show) -data TokenException = EmptyToken FilePath | InvalidToken FilePath - deriving stock (Eq, Show) - deriving anyclass (Exception) - -newtype TokenFileException = TokenFileException FilePath +data TokenException + = EmptyToken FilePath + | InvalidToken FilePath + | BadTokenFile FilePath deriving stock (Eq, Show) deriving anyclass (Exception) @@ -50,15 +48,14 @@ tokenFileOption = option (TokenFile <$> str) $ mconcat ] readToken :: TokenFile -> IO Project -readToken (TokenFile fp) = do +readToken (TokenFile f) = do -- Can't use `Blockfrost.Client.Core.projectFromFile` as it uses `error` -- and it leads to an unnecessary output that pollutes stdout. - line <- T.readFile fp `catch` \(_ :: IOException) -> - throw $ TokenFileException fp + line <- T.readFile f `catch` \(_ :: IOException) -> throw $ BadTokenFile f let tokenSrc = T.strip line - when (T.null tokenSrc) $ throw $ EmptyToken fp + when (T.null tokenSrc) $ throw $ EmptyToken f let tEnv = T.dropEnd 32 tokenSrc token = T.drop (T.length tEnv) tokenSrc case Project <$> parseEnv tEnv <*> pure token of - Left _ -> throw $ InvalidToken fp + Left _ -> throw $ InvalidToken f Right project -> pure project diff --git a/lib/shelley/test/unit/Cardano/Wallet/Shelley/Launch/BlockfrostSpec.hs b/lib/shelley/test/unit/Cardano/Wallet/Shelley/Launch/BlockfrostSpec.hs index 3cda1b80f20..45790eaf4a3 100644 --- a/lib/shelley/test/unit/Cardano/Wallet/Shelley/Launch/BlockfrostSpec.hs +++ b/lib/shelley/test/unit/Cardano/Wallet/Shelley/Launch/BlockfrostSpec.hs @@ -12,11 +12,7 @@ import Blockfrost.Env import Cardano.Wallet.Shelley.Launch ( Mode (Light, Normal), modeOption ) import Cardano.Wallet.Shelley.Launch.Blockfrost - ( TokenException (..) - , TokenFile (TokenFile) - , TokenFileException (TokenFileException) - , readToken - ) + ( TokenException (..), TokenFile (TokenFile), readToken ) import Options.Applicative ( ParserFailure (execFailure) , ParserResult (CompletionInvoked, Failure, Success) @@ -76,7 +72,7 @@ spec = describe "Blockfrost CLI options" $ do it "readToken throws in case of a non-existing token file" $ do readToken (TokenFile "non-existing-file") - `shouldThrow` \(TokenFileException _) -> True + `shouldThrow` \(BadTokenFile _) -> True it "readToken throws in case of an empty token file" $ withSystemTempFile "blockfrost.token" $ \f h -> do