diff --git a/lib/jormungandr/cardano-wallet-jormungandr.cabal b/lib/jormungandr/cardano-wallet-jormungandr.cabal index a5ef387df25..0879ed9aeab 100644 --- a/lib/jormungandr/cardano-wallet-jormungandr.cabal +++ b/lib/jormungandr/cardano-wallet-jormungandr.cabal @@ -52,6 +52,7 @@ library , extra , filepath , fmt + , generic-lens , http-client , http-types , iohk-monitoring @@ -66,6 +67,7 @@ library , text-class , time , transformers + , unordered-containers , yaml , warp hs-source-dirs: diff --git a/lib/jormungandr/exe/cardano-wallet-jormungandr.hs b/lib/jormungandr/exe/cardano-wallet-jormungandr.hs index 0f62a59fc44..ec26295efac 100644 --- a/lib/jormungandr/exe/cardano-wallet-jormungandr.hs +++ b/lib/jormungandr/exe/cardano-wallet-jormungandr.hs @@ -73,6 +73,8 @@ import Cardano.Wallet.Version ( showVersion, version ) import Control.Applicative ( optional, (<|>) ) +import Data.List + ( isPrefixOf ) import Data.Maybe ( fromMaybe ) import Data.Text @@ -90,11 +92,14 @@ import Options.Applicative , helper , info , long + , many , metavar + , option , progDesc - , some , str ) +import Options.Applicative.Types + ( readerAsk, readerError ) import System.Exit ( exitWith ) import System.FilePath @@ -158,8 +163,9 @@ data LaunchArgs = LaunchArgs } data JormungandrArgs = JormungandrArgs - { genesisBlock :: Either (Hash "Genesis") FilePath - , extraJormungandrArgs :: [Text] + { _genesisBlock :: Either (Hash "Genesis") FilePath + , _configFile :: Maybe FilePath + , _extraJormungandrArgs :: [String] } cmdLaunch @@ -178,24 +184,26 @@ cmdLaunch dataDir = command "launch" $ info (helper <*> cmd) $ mempty <*> verbosityOption <*> (JormungandrArgs <$> genesisBlockOption + <*> configFileOption <*> extraArguments) exec (LaunchArgs listen nodePort mStateDir verbosity jArgs) = do - let minSeverity = verbosityToMinSeverity verbosity - (cfg, sb, tr) <- initTracer minSeverity "launch" - case genesisBlock jArgs of + let minSeverity_ = verbosityToMinSeverity verbosity + (cfg, sb, tr) <- initTracer minSeverity_ "launch" + case _genesisBlock jArgs of Right block0File -> requireFilePath block0File Left _ -> pure () - let stateDir = fromMaybe (dataDir "testnet") mStateDir - let databaseDir = stateDir "wallets" + let stateDir_ = fromMaybe (dataDir "testnet") mStateDir + let databaseDir = stateDir_ "wallets" let cp = JormungandrConfig - { _stateDir = stateDir - , _genesisBlock = genesisBlock jArgs - , _restApiPort = fromIntegral . getPort <$> nodePort - , _minSeverity = minSeverity - , _outputStream = Inherit - , _extraArgs = extraJormungandrArgs jArgs + { stateDir = stateDir_ + , genesisBlock = _genesisBlock jArgs + , restApiPort = fromIntegral . getPort <$> nodePort + , minSeverity = minSeverity_ + , outputStream = Inherit + , configFile = _configFile jArgs + , extraArgs = _extraJormungandrArgs jArgs } - setupDirectory (logInfo tr) stateDir + setupDirectory (logInfo tr) stateDir_ setupDirectory (logInfo tr) databaseDir logInfo tr $ "Running as v" <> T.pack (showVersion version) exitWith =<< serveWallet @@ -272,8 +280,21 @@ genesisHashOption = optionT $ mempty <> metavar "STRING" <> help "Blake2b_256 hash of the genesis block, in base 16." +-- | [--config=FILE.YAML] +configFileOption :: Parser (Maybe FilePath) +configFileOption = optional $ option str $ mempty + <> long "config" + <> metavar "FILE.YAML" + <> help "Config file for jormungandr (note that this will be copied to a temporary location)" + -- | -- [ARGUMENTS...] -extraArguments :: Parser [Text] -extraArguments = some $ argument str $ mempty +extraArguments :: Parser [String] +extraArguments = many $ argument jmArg $ mempty <> metavar "[-- ARGUMENTS...]" <> help "Extra arguments to be passed to jormungandr." + where + jmArg = do + arg <- readerAsk + if "--config" `isPrefixOf` arg + then readerError "The --config option must be placed before the --" + else pure arg diff --git a/lib/jormungandr/src/Cardano/Wallet/Jormungandr.hs b/lib/jormungandr/src/Cardano/Wallet/Jormungandr.hs index 801100ad572..6ee32a1a1e3 100644 --- a/lib/jormungandr/src/Cardano/Wallet/Jormungandr.hs +++ b/lib/jormungandr/src/Cardano/Wallet/Jormungandr.hs @@ -1,6 +1,7 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} @@ -54,7 +55,6 @@ import Cardano.Wallet.Jormungandr.Network , ErrGetBlockchainParams (..) , ErrStartup (..) , JormungandrBackend (..) - , JormungandrConnParams (..) , withNetworkLayer ) import Cardano.Wallet.Jormungandr.Primitive.Types @@ -85,6 +85,8 @@ import Control.DeepSeq ( NFData ) import Data.Function ( (&) ) +import Data.Generics.Internal.VL.Lens + ( (^.) ) import Data.Text ( Text ) import Data.Text.Class @@ -126,7 +128,7 @@ serveWallet (cfg, sb, tr) databaseDir listen lj beforeMainLoop = do logInfo tr $ "Node is Jörmungandr on " <> toText (networkVal @n) withNetworkLayer tr lj $ \case Right (cp, nl) -> do - let nPort = Port $ baseUrlPort $ _restApi cp + let nPort = Port $ baseUrlPort $ cp ^. #restApi waitForService "Jörmungandr" (sb, tr) nPort $ waitForNetwork nl defaultRetryPolicy let (_, bp) = staticBlockchainParameters nl diff --git a/lib/jormungandr/src/Cardano/Wallet/Jormungandr/Compatibility.hs b/lib/jormungandr/src/Cardano/Wallet/Jormungandr/Compatibility.hs index fdf0c73ba2c..34283be801f 100644 --- a/lib/jormungandr/src/Cardano/Wallet/Jormungandr/Compatibility.hs +++ b/lib/jormungandr/src/Cardano/Wallet/Jormungandr/Compatibility.hs @@ -25,6 +25,7 @@ module Cardano.Wallet.Jormungandr.Compatibility , BaseUrl (..) , Scheme (..) , genConfigFile + , genConfigFileYaml , localhostBaseUrl , baseUrlToText ) where @@ -66,6 +67,10 @@ import Data.ByteString ( ByteString ) import Data.ByteString.Base58 ( bitcoinAlphabet, decodeBase58, encodeBase58 ) +import Data.Function + ( (&) ) +import Data.List + ( foldl' ) import Data.Maybe ( fromJust, isJust ) import Data.Proxy @@ -85,11 +90,13 @@ import qualified Cardano.Byron.Codec.Cbor as CBOR import qualified Cardano.Wallet.Primitive.Types as W import qualified Codec.Binary.Bech32 as Bech32 import qualified Codec.CBOR.Write as CBOR -import qualified Data.Aeson as Aeson +import qualified Data.Aeson.Types as Aeson import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as B8 +import qualified Data.HashMap.Strict as HM import qualified Data.Text as T import qualified Data.Text.Encoding as T +import qualified Data.Yaml as Yaml -- | A type representing the Jormungandr as a network target. This has an -- influence on binary serializer & network primitives. See also 'TxId' @@ -223,29 +230,67 @@ instance KnownNetwork n => DecodeAddress (Jormungandr n) where <> B8.unpack (BS.pack [discriminant]) <> "." --- | Generate a configuration file for Jörmungandr@0.3.999 +-- | Generate a configuration file for Jörmungandr@0.6 +-- Will throw "YamlException" or "IOException" when files cannot be +-- read/written. +genConfigFileYaml + :: FilePath + -- ^ State directory + -> PortNumber + -- ^ P2P port + -> BaseUrl + -- ^ Rest API base URL + -> Maybe FilePath + -- ^ User's config file + -> IO FilePath +genConfigFileYaml stateDir addressPort restApiUrl baseConfig = do + let nodeConfigFile = stateDir "jormungandr-config.yaml" + base <- maybe (pure Aeson.Null) Yaml.decodeFileThrow baseConfig + genConfigFile stateDir addressPort restApiUrl base + & Yaml.encodeFile nodeConfigFile + pure nodeConfigFile + +-- | Create a Jormungandr config by first making some defaults, then adding the +-- user's config file (if provided), then setting the API port to the value that +-- we have chosen. genConfigFile :: FilePath -> PortNumber -> BaseUrl -> Aeson.Value -genConfigFile stateDir addressPort (BaseUrl _ host port _) = object - [ "storage" .= (stateDir "chain") - , "rest" .= object - [ "listen" .= String listen ] - , "p2p" .= object - [ "trusted_peers" .= ([] :: [()]) - , "topics_of_interest" .= object - [ "messages" .= String "low" - , "blocks" .= String "normal" + -> Aeson.Value +genConfigFile stateDir addressPort (BaseUrl _ host port _) user = + mergeObjects [defaults, user, override] + where + defaults = object + [ "storage" .= (stateDir "chain") + , "p2p" .= object + [ "trusted_peers" .= ([] :: [()]) + , "topics_of_interest" .= object + [ "messages" .= String "low" + , "blocks" .= String "normal" + ] + , "public_address" .= String publicAddress ] - , "public_address" .= String publicAddress ] - ] - where + override = object + [ "rest" .= object + [ "listen" .= String listen ] + ] listen = T.pack $ mconcat [host, ":", show port] publicAddress = T.pack $ mconcat ["/ip4/127.0.0.1/tcp/", show addressPort] +-- | Recursively merge JSON objects, with the rightmost values taking +-- precedence. +mergeObjects :: [Aeson.Value] -> Aeson.Value +mergeObjects = foldl' merge Aeson.Null + where + merge :: Aeson.Value -> Aeson.Value -> Aeson.Value + merge (Aeson.Object a) (Aeson.Object b) = Aeson.Object $ + HM.unionWith merge a b + merge a Aeson.Null = a + merge _ b = b + {------------------------------------------------------------------------------- Base URL -------------------------------------------------------------------------------} diff --git a/lib/jormungandr/src/Cardano/Wallet/Jormungandr/Network.hs b/lib/jormungandr/src/Cardano/Wallet/Jormungandr/Network.hs index 8ed2d43102d..03132f97696 100644 --- a/lib/jormungandr/src/Cardano/Wallet/Jormungandr/Network.hs +++ b/lib/jormungandr/src/Cardano/Wallet/Jormungandr/Network.hs @@ -1,4 +1,5 @@ {-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} @@ -100,7 +101,7 @@ import Cardano.Wallet.Jormungandr.BlockHeaders , updateUnstableBlocks ) import Cardano.Wallet.Jormungandr.Compatibility - ( Jormungandr, genConfigFile, localhostBaseUrl ) + ( Jormungandr, genConfigFileYaml, localhostBaseUrl ) import Cardano.Wallet.Network ( Cursor, NetworkLayer (..), NextBlocksResult (..), defaultRetryPolicy ) import Cardano.Wallet.Network.Ports @@ -125,49 +126,47 @@ import Data.ByteArray.Encoding ( Base (Base16), convertToBase ) import Data.Coerce ( coerce ) -import Data.Function - ( (&) ) +import Data.Generics.Internal.VL.Lens + ( (^.) ) import Data.Quantity ( Quantity (..) ) import Data.Text ( Text ) import Data.Word ( Word32 ) -import System.Directory - ( removeFile ) -import System.FilePath - ( () ) +import GHC.Generics + ( Generic ) import qualified Cardano.Wallet.Jormungandr.Binary as J import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Lazy as BL import qualified Data.Char as C import qualified Data.Text as T -import qualified Data.Yaml as Yaml -- | Whether to start Jormungandr with the given config, or to connect to an -- already running Jormungandr REST API using the given parameters. data JormungandrBackend = UseRunning JormungandrConnParams | Launch JormungandrConfig - deriving (Show, Eq) + deriving (Show, Eq, Generic) -- | Parameters for connecting to a Jormungandr REST API. data JormungandrConnParams = JormungandrConnParams - { _genesisHash :: Hash "Genesis" - , _restApi :: BaseUrl - } deriving (Show, Eq) + { genesisHash :: Hash "Genesis" + , restApi :: BaseUrl + } deriving (Show, Eq, Generic) -- | A subset of the Jormungandr configuration parameters, used for starting the -- Jormungandr node backend. data JormungandrConfig = JormungandrConfig - { _stateDir :: FilePath - , _genesisBlock :: Either (Hash "Genesis") FilePath - , _restApiPort :: Maybe PortNumber - , _minSeverity :: Severity - , _outputStream :: StdStream - , _extraArgs :: [Text] - } deriving (Show, Eq) + { stateDir :: FilePath + , genesisBlock :: Either (Hash "Genesis") FilePath + , restApiPort :: Maybe PortNumber + , minSeverity :: Severity + , outputStream :: StdStream + , configFile :: Maybe FilePath + , extraArgs :: [String] + } deriving (Show, Eq, Generic) -- | Starts the network layer and runs the given action with a -- 'NetworkLayer'. The caller is responsible for handling errors which may have @@ -444,37 +443,42 @@ withJormungandr -> (JormungandrConnParams -> IO a) -- ^ Action to run while node is running. -> IO (Either ErrStartup a) -withJormungandr tr (JormungandrConfig stateDir block0 mPort logSeverity output extraArgs) cb = +withJormungandr tr cfg cb = bracket setupConfig cleanupConfig startBackend where - nodeConfigFile = stateDir "jormungandr-config.yaml" setupConfig = do - apiPort <- maybe getRandomPort pure mPort + apiPort <- maybe getRandomPort pure (cfg ^. #restApiPort) p2pPort <- getRandomPort let baseUrl = localhostBaseUrl $ fromIntegral apiPort - genConfigFile stateDir p2pPort baseUrl - & Yaml.encodeFile nodeConfigFile + nodeConfigFile <- genConfigFileYaml + (cfg ^. #stateDir) + p2pPort + baseUrl + (cfg ^. #configFile) logInfo tr $ mempty <> "Generated Jörmungandr's configuration to: " <> T.pack nodeConfigFile - pure (apiPort, baseUrl) - cleanupConfig _ = removeFile nodeConfigFile - - startBackend (apiPort, baseUrl) = getGenesisBlockArg block0 >>= \case - Right (block0H, genesisBlockArg) -> do - let args = genesisBlockArg ++ - [ "--config", nodeConfigFile - , "--log-level", C.toLower <$> show logSeverity - ] ++ map T.unpack extraArgs - let cmd = Command "jormungandr" args (return ()) output - let tr' = transformLauncherTrace tr - res <- withBackendProcess tr' cmd $ - waitForPort defaultRetryPolicy apiPort >>= \case - True -> Right <$> cb (JormungandrConnParams block0H baseUrl) - False -> pure $ Left ErrStartupNodeNotListening - pure $ either (Left . ErrStartupCommandExited) id res - - Left e -> pure $ Left e + pure (apiPort, baseUrl, nodeConfigFile) + cleanupConfig (_, _, _cfgFile) = pure () -- removeFile cfgFile + + startBackend (apiPort, baseUrl, nodeConfigFile) = + getGenesisBlockArg (cfg ^. #genesisBlock) >>= \case + Right (block0H, genesisBlockArg) -> do + let logLevel = C.toLower <$> show (cfg ^. #minSeverity) + let args = genesisBlockArg ++ + [ "--config", nodeConfigFile + , "--log-level", logLevel + ] ++ (cfg ^. #extraArgs) + let cmd = Command "jormungandr" args + (return ()) (cfg ^. #outputStream) + let tr' = transformLauncherTrace tr + res <- withBackendProcess tr' cmd $ + waitForPort defaultRetryPolicy apiPort >>= \case + True -> Right <$> cb (JormungandrConnParams block0H baseUrl) + False -> pure $ Left ErrStartupNodeNotListening + pure $ either (Left . ErrStartupCommandExited) id res + + Left e -> pure $ Left e getGenesisBlockArg :: Either (Hash "Genesis") FilePath diff --git a/lib/jormungandr/test/integration/Cardano/Wallet/Jormungandr/Launch.hs b/lib/jormungandr/test/integration/Cardano/Wallet/Jormungandr/Launch.hs index 156ed83a25c..aed907078df 100644 --- a/lib/jormungandr/test/integration/Cardano/Wallet/Jormungandr/Launch.hs +++ b/lib/jormungandr/test/integration/Cardano/Wallet/Jormungandr/Launch.hs @@ -34,8 +34,6 @@ import System.IO import System.IO.Temp ( createTempDirectory, getCanonicalTemporaryDirectory ) -import qualified Data.Text as T - -- | Starts jormungandr on a random port using the integration tests config. -- The data directory will be stored in a unique location under the system -- temporary directory. @@ -51,10 +49,11 @@ setupConfig = do Nothing minBound (UseHandle logFile) - ["--secret", T.pack (dir "secret.yaml")] + Nothing + ["--secret", dir "secret.yaml"] teardownConfig :: JormungandrConfig -> IO () -teardownConfig (JormungandrConfig d _ _ _ output _) = do +teardownConfig (JormungandrConfig d _ _ _ output _ _) = do case output of UseHandle h -> hClose h _ -> pure () diff --git a/nix/.stack.nix/cardano-wallet-jormungandr.nix b/nix/.stack.nix/cardano-wallet-jormungandr.nix index 7c4fab5fa94..e90038adad0 100644 --- a/nix/.stack.nix/cardano-wallet-jormungandr.nix +++ b/nix/.stack.nix/cardano-wallet-jormungandr.nix @@ -81,6 +81,7 @@ in { system, compiler, flags, pkgs, hsPkgs, pkgconfPkgs, ... }: (hsPkgs."extra" or (buildDepError "extra")) (hsPkgs."filepath" or (buildDepError "filepath")) (hsPkgs."fmt" or (buildDepError "fmt")) + (hsPkgs."generic-lens" or (buildDepError "generic-lens")) (hsPkgs."http-client" or (buildDepError "http-client")) (hsPkgs."http-types" or (buildDepError "http-types")) (hsPkgs."iohk-monitoring" or (buildDepError "iohk-monitoring")) @@ -95,6 +96,7 @@ in { system, compiler, flags, pkgs, hsPkgs, pkgconfPkgs, ... }: (hsPkgs."text-class" or (buildDepError "text-class")) (hsPkgs."time" or (buildDepError "time")) (hsPkgs."transformers" or (buildDepError "transformers")) + (hsPkgs."unordered-containers" or (buildDepError "unordered-containers")) (hsPkgs."yaml" or (buildDepError "yaml")) (hsPkgs."warp" or (buildDepError "warp")) ];