diff --git a/justfile b/justfile index e19e0826c06..701d255eb35 100644 --- a/justfile +++ b/justfile @@ -31,10 +31,10 @@ bench target: local-cluster: nix shell '.#local-cluster' '.#cardano-node' \ -c "local-cluster" \ - control \ --cluster-configs lib/local-cluster/test/data/cluster-configs \ --cluster-logs ignore-me/cluster.logs \ - --socket-path ignore-me/cluster.socket + --socket-path ignore-me/cluster.socket \ + --monitoring-port 12788 # run unit tests on a match unit-tests-cabal-match match: diff --git a/lib/local-cluster/data/swagger.json b/lib/local-cluster/data/swagger.json index 872a83a4674..5a33a98b1b2 100644 --- a/lib/local-cluster/data/swagger.json +++ b/lib/local-cluster/data/swagger.json @@ -133,6 +133,69 @@ }, "Ready": { "type": "boolean" + }, + "SendAssets": { + "properties": { + "assets": { + "items": { + "properties": { + "address": { + "type": "string" + }, + "bundle": { + "properties": { + "assets": { + "items": { + "properties": { + "asset": { + "properties": { + "name": { + "type": "string" + }, + "policy": { + "type": "string" + } + }, + "type": "object" + }, + "quantity": { + "type": "integer" + } + }, + "type": "object" + }, + "type": "array" + }, + "coin": { + "type": "integer" + } + }, + "type": "object" + }, + "metadata": { + "items": { + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "array" + }, + "batch-size": { + "type": "integer" + } + }, + "type": "object" } } }, @@ -210,6 +273,28 @@ }, "summary": "Check if the local-cluster is ready" } + }, + "/send/assets": { + "parameters": [ + { + "in": "path", + "name": "assets", + "schema": { + "$ref": "#/components/schemas/SendAssets" + } + } + ], + "post": { + "responses": { + "204": { + "content": { + "application/json": {} + }, + "description": "No Content" + } + }, + "summary": "Send assets to the faucet" + } } } } \ No newline at end of file diff --git a/lib/local-cluster/lib/Cardano/Wallet/Faucet/Gen/Address.hs b/lib/local-cluster/lib/Cardano/Wallet/Faucet/Gen/Address.hs new file mode 100644 index 00000000000..8443ea639f6 --- /dev/null +++ b/lib/local-cluster/lib/Cardano/Wallet/Faucet/Gen/Address.hs @@ -0,0 +1,94 @@ +{-# LANGUAGE ScopedTypeVariables #-} + +module Cardano.Wallet.Faucet.Gen.Address + ( genAddress + , NetworkTag (..) + , allTags + ) +where + +import Prelude + +import Cardano.Address + ( Address + , unsafeMkAddress + ) +import Data.Bits + ( Bits (shiftL, (.|.)) + ) +import Data.ByteString + ( ByteString + ) +import Data.Word + ( Word8 + ) +import Test.QuickCheck + ( Arbitrary (arbitrary) + , Gen + , elements + , vectorOf + ) + +import qualified Data.ByteString as BS + +{- +ADDRESS = %b0000 | NETWORK-TAG | KEY-HASH | KEY-HASH ; type 00, Base Shelley address + \ %b0001 | NETWORK-TAG | SCRIPT-HASH | KEY-HASH ; type 01, Base Shelley address + \ %b0010 | NETWORK-TAG | KEY-HASH | SCRIPT-HASH ; type 02, Base Shelley address + \ %b0011 | NETWORK-TAG | SCRIPT-HASH | SCRIPT-HASH ; type 03, Base Shelley address + \ %b0100 | NETWORK-TAG | KEY-HASH | POINTER ; type 04, Pointer Shelley address + \ %b0101 | NETWORK-TAG | SCRIPT-HASH | POINTER ; type 05, Pointer Shelley address + \ %b0110 | NETWORK-TAG | KEY-HASH ; type 06, Payment Shelley address + \ %b0111 | NETWORK-TAG | SCRIPT-HASH ; type 07, Payment Shelley address + \ %b1000 | BYRON-PAYLOAD ; type 08, Byron / Bootstrap address + \ %b1110 | NETWORK-TAG | KEY-HASH ; type 14, Stake Shelley address + \ %b1111 | NETWORK-TAG | SCRIPT-HASH ; type 15, Stake Shelley address + +NETWORK-TAG = %b0000 ; Testnet + \ %b0001 ; Mainnet + +POINTER = VARIABLE-LENGTH-UINT ; slot number + | VARIABLE-LENGTH-UINT ; transaction index + | VARIABLE-LENGTH-UINT ; certificate index + +VARIABLE-LENGTH-UINT = (%b1 | UINT7 | VARIABLE-LENGTH-UINT) + / (%b0 | UINT7) +UINT7 = 7BIT + +KEY-HASH = 28OCTET + +SCRIPT-HASH= 28OCTET + +BYRON-PAYLOAD = *OCTET ; see 'Byron Addresses' section or cddl specification. + +-} + +data NetworkTag = TestnetTag | MainnetTag + +tag :: NetworkTag -> Word8 +tag TestnetTag = 0 +tag MainnetTag = 1 + +allTags :: [NetworkTag] +allTags = [TestnetTag, MainnetTag] + +genPrefix :: [NetworkTag] -> [Word8] -> Gen Word8 +genPrefix ts xs = do + network <- elements ts + prefix <- elements xs + pure $ prefix `shiftL` 4 .|. tag network + +genHash :: Gen ByteString +genHash = BS.pack <$> vectorOf 28 arbitrary + +-- | Generate a random address for the given networks excluding the Byron addresses. +genAddress :: [NetworkTag] -> Gen Address +genAddress tags = fmap unsafeMkAddress $ do + hash1 <- genHash + hash2 <- genHash + prefixONE <- genPrefix tags [6, 7] + prefixTWO <- genPrefix tags [0, 1, 2, 3] + elements + [ prefixONE `BS.cons` hash1 + , prefixTWO `BS.cons` hash1 <> hash2 + ] diff --git a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Monitoring/Monitor.hs b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http.hs similarity index 92% rename from lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Monitoring/Monitor.hs rename to lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http.hs index ad488478a39..5073554bde5 100644 --- a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Monitoring/Monitor.hs +++ b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http.hs @@ -4,7 +4,7 @@ {-# LANGUAGE TupleSections #-} {-# OPTIONS_GHC -Wno-missing-local-signatures #-} -module Cardano.Wallet.Launch.Cluster.Monitoring.Monitor +module Cardano.Wallet.Launch.Cluster.Http ( MsgHttpMonitoring (..) , MonitorConfiguration (..) , withMonitoring @@ -13,14 +13,14 @@ where import Prelude -import Cardano.Wallet.Launch.Cluster.Monitoring.Http.Client +import Cardano.Wallet.Launch.Cluster.Http.Monitor.Client ( RunQuery , withHttpClient ) -import Cardano.Wallet.Launch.Cluster.Monitoring.Http.Logging +import Cardano.Wallet.Launch.Cluster.Http.Monitor.Logging ( MsgHttpMonitoring (..) ) -import Cardano.Wallet.Launch.Cluster.Monitoring.Http.Server +import Cardano.Wallet.Launch.Cluster.Http.Monitor.Server ( mkHandlers , withHttpServer ) diff --git a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/API.hs b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/API.hs new file mode 100644 index 00000000000..263972e58b0 --- /dev/null +++ b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/API.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE TypeOperators #-} + +module Cardano.Wallet.Launch.Cluster.Http.API + ( API + ) +where + +import Cardano.Wallet.Launch.Cluster.Http.Faucet.API + ( FaucetAPI + ) +import Cardano.Wallet.Launch.Cluster.Http.Monitor.API + ( ControlAPI + ) +import Servant.API + ( (:<|>) + ) + +-- | The API for the monitoring server and the query cluster application +type API n = ControlAPI :<|> FaucetAPI n diff --git a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Client.hs b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Client.hs new file mode 100644 index 00000000000..5658e08dab0 --- /dev/null +++ b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Client.hs @@ -0,0 +1,116 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Cardano.Wallet.Launch.Cluster.Http.Client + ( withHttpClient + , MsgClient (..) + ) +where + +import Prelude + +import Cardano.Wallet.Launch.Cluster.Http.Faucet.Client + ( MsgFaucetClient + , RunFaucetQ + , mkFaucet + , newFaucetQ + ) +import Cardano.Wallet.Launch.Cluster.Http.Monitor.Client + ( MsgMonitorClient + , RunMonitorQ + , mkMonitorClient + , newRunQuery + ) +import Cardano.Wallet.Primitive.NetworkId + ( HasSNetworkId + , SNetworkId + ) +import Control.Monad.Cont + ( ContT (..) + ) +import Control.Monad.IO.Class + ( liftIO + ) +import Control.Tracer + ( Tracer + , traceWith + ) +import Data.Functor.Contravariant + ( (>$<) + ) +import Data.Text.Class + ( ToText (..) + ) +import Network.HTTP.Client + ( ManagerSettings (..) + , defaultManagerSettings + , newManager + , responseTimeoutNone + ) +import Network.Socket + ( PortNumber + ) +import Servant.Client + ( BaseUrl (..) + , ClientM + , Scheme (..) + , mkClientEnv + , runClientM + ) +import UnliftIO + ( MonadUnliftIO + , throwIO + ) + +data MsgClient + = MsgClientStart + | MsgClientDone + | MsgMonitorClient MsgMonitorClient + | MsgFaucetClient MsgFaucetClient + deriving stock (Show) + +instance ToText MsgClient where + toText = \case + MsgClientStart -> "HTTP client started" + MsgClientDone -> "HTTP client done" + MsgMonitorClient msg -> toText msg + MsgFaucetClient msg -> toText msg + +-- | Produce a closure over the http client of an http monitoring server that +-- can be used to query the server. +withHttpClient + :: (MonadUnliftIO m, HasSNetworkId n) + => SNetworkId n + -> Tracer m MsgClient + -- ^ how to trace the http client operations + -> PortNumber + -- ^ Monitoring port to attach to (http://localhost is hardcoded) + -> ContT () m (RunMonitorQ m, RunFaucetQ m) +withHttpClient networkId tracer httpPort = ContT $ \continue -> do + let tr = traceWith tracer + tr MsgClientStart + let url = BaseUrl Http "localhost" (fromIntegral httpPort) "" + manager <- + liftIO + $ newManager + $ defaultManagerSettings + { managerResponseTimeout = responseTimeoutNone + } + let + query :: ClientM a -> IO a + query f = do + r <- runClientM f $ mkClientEnv manager url + either throwIO pure r + runQuery <- newRunQuery query (MsgMonitorClient >$< tracer) mkMonitorClient + runFaucet <- + newFaucetQ + query + (MsgFaucetClient >$< tracer) + $ mkFaucet networkId + continue (runQuery, runFaucet) + + tr MsgClientDone diff --git a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Faucet/API.hs b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Faucet/API.hs new file mode 100644 index 00000000000..0f048acce79 --- /dev/null +++ b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Faucet/API.hs @@ -0,0 +1,27 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeOperators #-} + +module Cardano.Wallet.Launch.Cluster.Http.Faucet.API + ( FaucetAPI + , SendFaucetAssetsAPI + ) +where + +import Cardano.Wallet.Launch.Cluster.Http.Faucet.SendFaucetAssets + ( SendFaucetAssets + , WithNetwork (..) + ) +import Servant + ( JSON + , PostNoContent + , ReqBody + , (:>) + ) + +type SendFaucetAssetsAPI n = + "send" + :> "faucet-assets" + :> ReqBody '[JSON] (WithNetwork SendFaucetAssets n) + :> PostNoContent + +type FaucetAPI n = SendFaucetAssetsAPI n diff --git a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Faucet/Client.hs b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Faucet/Client.hs new file mode 100644 index 00000000000..54d503965a5 --- /dev/null +++ b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Faucet/Client.hs @@ -0,0 +1,106 @@ +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +module Cardano.Wallet.Launch.Cluster.Http.Faucet.Client + ( newFaucetQ + , mkFaucet + , FaucetQ (..) + , AnyFaucetQ (..) + , RunFaucetQ (..) + , Faucet + , MsgFaucetClient (..) + ) +where + +import Prelude + +import Cardano.Wallet.Launch.Cluster.Http.Faucet.API + ( SendFaucetAssetsAPI + ) +import Cardano.Wallet.Launch.Cluster.Http.Faucet.SendFaucetAssets + ( SendFaucetAssets + , WithNetwork (..) + ) +import Cardano.Wallet.Primitive.NetworkId + ( HasSNetworkId + , SNetworkId + ) +import Control.Monad.IO.Class + ( MonadIO (..) + ) +import Control.Tracer + ( Tracer + , traceWith + ) +import Data.Data + ( Proxy (Proxy) + ) +import Data.Functor + ( ($>) + ) +import Data.Text.Class + ( ToText (..) + ) +import Servant + ( NoContent + ) +import Servant.Client + ( ClientM + , client + ) +import UnliftIO + ( MonadUnliftIO + ) + +-- | Queries that can be run against the local cluster +data FaucetQ a where + SendFaucetAssetsQ :: SendFaucetAssets -> FaucetQ () + +-- | Existential wrapper for any application query that has a show instance +data AnyFaucetQ = forall a. Show a => AnyFaucetQ (FaucetQ a) + +instance Show AnyFaucetQ where + show (AnyFaucetQ (SendFaucetAssetsQ _)) = "SendFaucetAssets" + +-- | Opaque record of the client application +newtype Faucet n = Faucet + { sendFaucetAssets :: WithNetwork SendFaucetAssets n -> ClientM NoContent + } + +-- | Construct the client application given the network id witness +mkFaucet :: forall n. HasSNetworkId n => SNetworkId n -> Faucet n +mkFaucet _ = + Faucet + { sendFaucetAssets = client (Proxy @(SendFaucetAssetsAPI n)) + } + +newtype MsgFaucetClient = MsgFaucetRequest AnyFaucetQ + deriving stock Show + +instance ToText MsgFaucetClient where + toText (MsgFaucetRequest q) = "Faucet request: " <> toText (show q) + +-- | Run any query against the monitoring server. +newtype RunFaucetQ m + = RunFaucetQ (forall a. Show a => FaucetQ a -> m a) + +-- | Construct the run function for the client application +newFaucetQ + :: MonadUnliftIO m + => (forall a. ClientM a -> IO a) + -> Tracer m MsgFaucetClient + -> Faucet n + -> m (RunFaucetQ m) +newFaucetQ query tr Faucet{..} = pure + $ RunFaucetQ + $ \request -> do + traceWith tr (MsgFaucetRequest $ AnyFaucetQ request) + case request of + SendFaucetAssetsQ assets -> + liftIO + $ query + $ sendFaucetAssets (WithNetwork assets) $> () diff --git a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Faucet/OpenApi.hs b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Faucet/OpenApi.hs new file mode 100644 index 00000000000..5f19839b6da --- /dev/null +++ b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Faucet/OpenApi.hs @@ -0,0 +1,159 @@ +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Cardano.Wallet.Launch.Cluster.Http.Faucet.OpenApi + ( faucetDefinitions + , faucetPaths + , sendAssetsSchema + ) where + +import Prelude + +import Control.Lens + ( At (..) + , (&) + , (.~) + , (?~) + ) +import Data.HashMap.Strict.InsOrd + ( InsOrdHashMap + ) +import Data.OpenApi + ( Definitions + , HasContent (..) + , HasIn (..) + , HasItems (..) + , HasName (..) + , HasParameters (..) + , HasPost (..) + , HasProperties (..) + , HasSchema (..) + , HasSummary (..) + , HasType (..) + , OpenApiItems (..) + , OpenApiType (..) + , Operation + , ParamLocation (..) + , PathItem + , Reference (..) + , Referenced (..) + , Response + , Schema + , _Inline + ) +import Network.HTTP.Media + ( MediaType + ) + +faucetDefinitions :: Definitions Schema +faucetDefinitions = + [ ("SendAssets", sendAssetsSchema) + ] + +sendAssetsSchema :: Schema +sendAssetsSchema = + mempty + & type_ ?~ OpenApiObject + & properties + .~ [ ("batch-size", Inline $ mempty & type_ ?~ OpenApiInteger) + , ("assets", Inline assetsSchema) + ] + +assetsSchema :: Schema +assetsSchema = + mempty + & type_ ?~ OpenApiArray + & items + ?~ OpenApiItemsObject + (Inline assetSchema) + +assetSchema :: Schema +assetSchema = + mempty + & type_ ?~ OpenApiObject + & properties + .~ [ ("address", Inline $ mempty & type_ ?~ OpenApiString) + , ("bundle", Inline bundleSchema) + , ("metadata", Inline metadataSchema) + ] + +metadataSchema :: Schema +metadataSchema = + mempty + & type_ ?~ OpenApiArray + & items + ?~ OpenApiItemsObject + (Inline metadataValueSchema) + +metadataValueSchema :: Schema +metadataValueSchema = + mempty + & type_ ?~ OpenApiObject + & properties + .~ [ ("key", Inline $ mempty & type_ ?~ OpenApiString) + , ("value", Inline $ mempty & type_ ?~ OpenApiString) + ] + +bundleSchema :: Schema +bundleSchema = + mempty + & type_ ?~ OpenApiObject + & properties + .~ [ ("assets", Inline assetsQuantitySchema) + , ("coin", Inline $ mempty & type_ ?~ OpenApiInteger) + ] + +assetsQuantitySchema :: Schema +assetsQuantitySchema = + mempty + & type_ ?~ OpenApiArray + & items + ?~ OpenApiItemsObject + (Inline assetQuantitySchema) + +assetQuantitySchema :: Schema +assetQuantitySchema = + mempty + & type_ ?~ OpenApiObject + & properties + .~ [ ("asset", Inline assetNameSchema) + , ("quantity", Inline $ mempty & type_ ?~ OpenApiInteger) + ] + +assetNameSchema :: Schema +assetNameSchema = + mempty + & type_ ?~ OpenApiObject + & properties + .~ [ ("name", Inline $ mempty & type_ ?~ OpenApiString) + , ("policy", Inline $ mempty & type_ ?~ OpenApiString) + ] + +faucetPaths :: InsOrdHashMap FilePath PathItem +faucetPaths = [sendFaucetAssetsPath] + +sendFaucetAssetsPath :: (FilePath, PathItem) +sendFaucetAssetsPath = ("/send/assets", pathItem) + where + pathItem :: PathItem + pathItem = + mempty + & post ?~ operation + & parameters + .~ [ Inline + $ mempty + & in_ .~ ParamPath + & name .~ "assets" + & schema ?~ Ref (Reference "SendAssets") + ] + operation :: Operation + operation = + mempty + & summary ?~ summary' + & at 204 ?~ at204 + summary' = "Send assets to the faucet" + at204 :: Referenced Response + at204 = + "No Content" + & _Inline . content . at ("application/json" :: MediaType) + ?~ mempty diff --git a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Faucet/SendFaucetAssets.hs b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Faucet/SendFaucetAssets.hs new file mode 100644 index 00000000000..848ab7dba3d --- /dev/null +++ b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Faucet/SendFaucetAssets.hs @@ -0,0 +1,257 @@ +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE KindSignatures #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +module Cardano.Wallet.Launch.Cluster.Http.Faucet.SendFaucetAssets + ( SendFaucetAssets (..) + , WithNetwork (..) + , AssetMetadata + , genSendFaucetAssets + ) +where + +import Prelude + +import Cardano.Wallet.Address.Encoding + ( decodeAddress + , encodeAddress + ) +import Cardano.Wallet.Faucet.Gen.Address + ( NetworkTag (..) + , genAddress + ) +import Cardano.Wallet.Launch.Cluster.Http.Faucet.OpenApi + ( sendAssetsSchema + ) +import Cardano.Wallet.Primitive.NetworkId + ( HasSNetworkId (sNetworkId) + , NetworkDiscriminant (..) + , SNetworkId (..) + ) +import Cardano.Wallet.Primitive.Types.Address + ( Address (..) + ) +import Cardano.Wallet.Primitive.Types.AssetId + ( AssetId (..) + ) +import Cardano.Wallet.Primitive.Types.Coin + ( Coin (Coin) + ) +import Cardano.Wallet.Primitive.Types.TokenBundle + ( TokenBundle + , fromFlatList + , toFlatList + ) +import Cardano.Wallet.Primitive.Types.TokenBundle.Gen + ( genTokenBundleSmallRange + ) +import Cardano.Wallet.Primitive.Types.TokenQuantity + ( TokenQuantity + ) +import Cardano.Wallet.Util + ( ShowFmt (..) + ) +import Control.Monad + ( (>=>) + ) +import Data.Aeson + ( FromJSON (parseJSON) + , KeyValue ((.=)) + , ToJSON (toJSON) + , Value + , object + , withObject + , (.:) + ) +import Data.Aeson.Types + ( Parser + ) +import Data.Bifunctor + ( first + ) +import Data.OpenApi + ( NamedSchema (..) + , ToSchema (..) + ) +import Data.Typeable + ( Typeable + ) +import Test.QuickCheck + ( Arbitrary (arbitrary) + , Gen + , listOf + ) + +import qualified Cardano.Address as Addr + +type AssetMetadata = [(String, String)] + +-- | Payload to send assets to a list of addresses +data SendFaucetAssets = SendFaucetAssets + { batchSize :: Int + -- ^ batch size + , assets :: [(Address, (TokenBundle, AssetMetadata))] + -- ^ List of addresses and the assets to send to each address + } + deriving stock (Eq, Show) + +-- | WithNetwork carries network discriminant around a value +newtype WithNetwork a (n :: NetworkDiscriminant) = WithNetwork a + deriving stock (Eq, Show) + +instance HasSNetworkId n => ToJSON (WithNetwork SendFaucetAssets n) where + toJSON (WithNetwork SendFaucetAssets{batchSize, assets}) = + object + [ "batch-size" .= batchSize + , "assets" .= renderAssets (sNetworkId @n) assets + ] + +instance Typeable n => ToSchema (WithNetwork SendFaucetAssets n) where + declareNamedSchema _ = + pure + $ NamedSchema + (Just "WithNetwork SendFaucetAssets") + sendAssetsSchema + +instance HasSNetworkId n => FromJSON (WithNetwork SendFaucetAssets n) where + parseJSON = withObject "SendFaucetAssets" $ \o -> do + batchSize <- o .: "batch-size" + assets <- o .: "assets" >>= parseAssets (sNetworkId @n) + pure $ WithNetwork $ SendFaucetAssets{batchSize, assets} + +--- assets parsing/rendering --------------------------------------------------- + +parseAssets + :: SNetworkId n + -> Value + -> Parser [(Address, (TokenBundle, AssetMetadata))] +parseAssets n = parseJSON >=> mapM (parseAsset n) + +parseAsset + :: SNetworkId n + -> Value + -> Parser (Address, (TokenBundle, AssetMetadata)) +parseAsset n = withObject "Asset" $ \o -> do + addr <- o .: "address" >>= parseAddress n + bundle <- o .: "bundle" >>= parseBundle + metadata <- o .: "metadata" >>= parseAssetMetadata + pure (addr, (bundle, metadata)) + +renderAssets + :: SNetworkId n + -> [(Address, (TokenBundle, AssetMetadata))] + -> Value +renderAssets n = toJSON . map (renderAsset n) + +renderAsset + :: SNetworkId n + -> (Address, (TokenBundle, AssetMetadata)) + -> Value +renderAsset n (addr, (bundle, metadata)) = + object + [ "address" .= renderAddress n addr + , "bundle" .= renderBundle bundle + , "metadata" .= renderAssetMetadata metadata + ] + +parseAssetMetadata :: Value -> Parser AssetMetadata +parseAssetMetadata = parseJSON >=> mapM parseMetadataValue + +parseMetadataValue :: Value -> Parser (String, String) +parseMetadataValue = withObject "MetadataValue" $ \o -> do + k <- o .: "key" + v <- o .: "value" + pure (k, v) + +renderAssetMetadata :: AssetMetadata -> Value +renderAssetMetadata = + toJSON + . map (\(k, v) -> object ["key" .= k, "value" .= v]) + +-- address parsing/rendering --------------------------------------------------- + +renderAddress :: SNetworkId n -> Address -> Value +renderAddress n = toJSON . encodeAddress n + +parseAddress :: SNetworkId n -> Value -> Parser Address +parseAddress n x = do + parseJSON x + >>= eitherToParser + . first (\e -> ShowFmt $ show (x, e)) + . decodeAddress n + +eitherToParser :: Show s => Either s a -> Parser a +eitherToParser = either (fail . show) pure + +--- bundle parsing/rendering --------------------------------------------------- + +parseBundle :: Value -> Parser TokenBundle +parseBundle = parseJSON >=> fmap (uncurry fromFlatList) . parseBundle' + +parseBundle' :: Value -> Parser (Coin, [(AssetId, TokenQuantity)]) +parseBundle' = withObject "Bundle" $ \o -> do + c <- o .: "coin" >>= parseCoin + xs <- o .: "assets" >>= mapM parseAssetQuantity + pure (c, xs) + +parseCoin :: Value -> Parser Coin +parseCoin = fmap Coin . parseJSON + +parseAssetQuantity :: Value -> Parser (AssetId, TokenQuantity) +parseAssetQuantity = withObject "AssetQuantity" $ \o -> do + asset <- o .: "asset" >>= parseAssetId + quantity <- o .: "quantity" + pure (asset, quantity) + +parseAssetId :: Value -> Parser AssetId +parseAssetId = withObject "AssetId" $ \o -> do + tp <- o .: "policy" + n <- o .: "name" + pure $ AssetId tp n + +renderBundle :: TokenBundle -> Value +renderBundle = toJSON . renderBundle' . toFlatList + +renderBundle' :: (Coin, [(AssetId, TokenQuantity)]) -> Value +renderBundle' (c, xs) = + object + [ "coin" .= renderCoin c + , "assets" .= map renderAssetQuantity xs + ] + +renderAssetQuantity :: (AssetId, TokenQuantity) -> Value +renderAssetQuantity (AssetId tp n, tq) = + object + [ "asset" + .= object + [ "policy" .= tp + , "name" .= n + ] + , "quantity" .= tq + ] + +renderCoin :: Coin -> Value +renderCoin (Coin c) = toJSON c + +-- | Generate a 'SendFaucetAssets' payload +genSendFaucetAssets + :: forall n + . HasSNetworkId n + => Gen (WithNetwork SendFaucetAssets n) +genSendFaucetAssets = do + batchSize <- arbitrary + assets <- listOf $ genAsset $ case sNetworkId @n of + SMainnet -> MainnetTag + STestnet _ -> TestnetTag + pure $ WithNetwork SendFaucetAssets{batchSize, assets} + +genAsset :: NetworkTag -> Gen (Address, (TokenBundle, [(String, String)])) +genAsset tag = do + addr <- Address . Addr.unAddress <$> genAddress [tag] + bundle <- genTokenBundleSmallRange + metadata <- listOf ((,) <$> arbitrary <*> arbitrary) + pure (addr, (bundle, metadata)) diff --git a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Faucet/Server.hs b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Faucet/Server.hs new file mode 100644 index 00000000000..25519d60a60 --- /dev/null +++ b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Faucet/Server.hs @@ -0,0 +1,121 @@ +{-# LANGUAGE RecordWildCards #-} + +module Cardano.Wallet.Launch.Cluster.Http.Faucet.Server + ( FaucetHandlers (..) + , mkFaucetHandlers + , newNodeConnVar + , NodeConnVar (..) + , mkFaucetServer + ) +where + +import Prelude + +import Cardano.Launcher.Node + ( CardanoNodeConn + ) +import Cardano.Wallet.Launch.Cluster + ( Config + ) +import Cardano.Wallet.Launch.Cluster.ClusterM + ( ClusterM + , runClusterM + ) +import Cardano.Wallet.Launch.Cluster.Faucet + ( sendFaucetAssetsTo + ) +import Cardano.Wallet.Launch.Cluster.Http.Faucet.API + ( FaucetAPI + ) +import Cardano.Wallet.Launch.Cluster.Http.Faucet.SendFaucetAssets + ( SendFaucetAssets (..) + , WithNetwork (..) + ) +import Cardano.Wallet.Primitive.Types.Address + ( Address (..) + ) +import Control.Exception + ( throwIO + ) +import Data.Bifunctor + ( first + ) +import Data.Functor + ( ($>) + ) +import Servant + ( Handler + , HasServer (..) + , NoContent (..) + , ServerError (..) + , err500 + ) +import UnliftIO + ( MonadIO (..) + , atomically + , newTVarIO + , readTVarIO + , writeTVar + ) + +import qualified Cardano.Address as Address + +-- | Handlers for local-cluster application +newtype FaucetHandlers = FaucetHandlers + { handleSendAssets :: SendFaucetAssets -> IO () + } + +-- Handler for sending assets to some addresses +sendFaucetAssetsHandler + :: CardanoNodeConn + -> SendFaucetAssets + -> ClusterM () +sendFaucetAssetsHandler + relayConnection + SendFaucetAssets{..} = do + sendFaucetAssetsTo relayConnection batchSize + $ first mkLibAddress <$> assets + +mkLibAddress :: Address -> Address.Address +mkLibAddress (Address a) = Address.unsafeMkAddress a + +-- | A thread-safe variable to store the connection to the Cardano node +-- The connection could not be available at the time of creation +data NodeConnVar = NodeConnVar + { getNodeConn :: IO (Maybe CardanoNodeConn) + , setNodeConn :: CardanoNodeConn -> IO () + } + +-- | Create a new, empty 'NodeConnVar' +newNodeConnVar :: IO NodeConnVar +newNodeConnVar = do + var <- newTVarIO Nothing + pure + NodeConnVar + { getNodeConn = readTVarIO var + , setNodeConn = atomically . writeTVar var . Just + } + +-- | Create an application handlers record +mkFaucetHandlers + :: NodeConnVar + -> Config + -> FaucetHandlers +mkFaucetHandlers mRelayConnection config = + FaucetHandlers + { handleSendAssets = \ass -> do + mConn <- getNodeConn mRelayConnection + case mConn of + Just relayConnection -> + runClusterM config + $ sendFaucetAssetsHandler relayConnection ass + Nothing -> + throwIO + err500{errBody = "The relay node is not available yet"} + } + +mkFaucetServer + :: FaucetHandlers + -> ServerT (FaucetAPI n) Handler +mkFaucetServer handlers (WithNetwork ass) = + liftIO $ handleSendAssets handlers ass $> NoContent diff --git a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Logging.hs b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Logging.hs new file mode 100644 index 00000000000..3ea0874b957 --- /dev/null +++ b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Logging.hs @@ -0,0 +1,47 @@ +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE LambdaCase #-} + +module Cardano.Wallet.Launch.Cluster.Http.Logging + ( MsgHttpService (..) + ) +where + +import Prelude + +import Cardano.Wallet.Launch.Cluster.Http.Client + ( MsgClient + ) +import Data.Text.Class + ( ToText (..) + ) +import Network.Socket + ( PortNumber + ) + +-- | Messages for the HTTP monitoring service +data MsgHttpService + = MsgHttpServicePort PortNumber + | MsgHttpServiceQuery MsgClient + | MsgHttpServiceServerStarted + | MsgHttpServiceServerStopped + | MsgHttpServiceClientStarted + | MsgHttpServiceClientStopped + | MsgHttpServiceDone + deriving stock (Show) + +instance ToText MsgHttpService where + toText = \case + MsgHttpServicePort port -> + "HTTP monitoring service started on port " <> toText (show port) + MsgHttpServiceQuery msgClient -> + "HTTP monitoring query: " <> toText msgClient + MsgHttpServiceServerStarted -> + "HTTP monitoring server started" + MsgHttpServiceServerStopped -> + "HTTP monitoring server stopped" + MsgHttpServiceClientStarted -> + "HTTP monitoring client started" + MsgHttpServiceClientStopped -> + "HTTP monitoring client stopped" + MsgHttpServiceDone -> + "HTTP monitoring done" diff --git a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Monitoring/Http/API.hs b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Monitor/API.hs similarity index 83% rename from lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Monitoring/Http/API.hs rename to lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Monitor/API.hs index a6d208d2741..56da92797fc 100644 --- a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Monitoring/Http/API.hs +++ b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Monitor/API.hs @@ -9,15 +9,19 @@ {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -Wno-orphans #-} -module Cardano.Wallet.Launch.Cluster.Monitoring.Http.API - ( API - , ApiT (..) +module Cardano.Wallet.Launch.Cluster.Http.Monitor.API + ( ApiT (..) + , ReadyAPI + , StepAPI + , SwitchAPI + , ObserveAPI + , ControlAPI ) where import Prelude -import Cardano.Wallet.Launch.Cluster.Monitoring.Http.OpenApi +import Cardano.Wallet.Launch.Cluster.Http.Monitor.OpenApi ( monitorStateSchema , observationSchema ) @@ -59,12 +63,13 @@ import Servant.API , (:>) ) --- | The API for the monitoring server -type API = - "ready" :> Get '[JSON] Bool - :<|> "control" :> "step" :> PostNoContent - :<|> "control" :> "switch" :> Post '[JSON] (ApiT MonitorState) - :<|> "control" :> "observe" :> Get '[JSON] (ApiT (History, MonitorState)) +type ReadyAPI = "ready" :> Get '[JSON] Bool +type StepAPI = "control" :> "step" :> PostNoContent +type SwitchAPI = "control" :> "switch" :> Post '[JSON] (ApiT MonitorState) +type ObserveAPI = "control" :> "observe" :> Get '[JSON] (ApiT (History, MonitorState)) + +-- | The API to control the monitoring server +type ControlAPI = ReadyAPI :<|> StepAPI :<|> SwitchAPI :<|> ObserveAPI -- | A newtype wrapper to avoid orphan instances newtype ApiT a = ApiT {unApiT :: a} diff --git a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Monitor/Client.hs b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Monitor/Client.hs new file mode 100644 index 00000000000..0c4cf1705f0 --- /dev/null +++ b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Monitor/Client.hs @@ -0,0 +1,151 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +module Cardano.Wallet.Launch.Cluster.Http.Monitor.Client + ( RunMonitorQ (..) + , MonitorQ (..) + , MsgMonitorClient (..) + , AnyMonitorQ (..) + , newRunQuery + , mkMonitorClient + ) +where + +import Prelude + +import Cardano.Wallet.Launch.Cluster.Http.Monitor.API + ( ApiT (..) + , ObserveAPI + , ReadyAPI + , StepAPI + , SwitchAPI + ) +import Cardano.Wallet.Launch.Cluster.Monitoring.Phase + ( History + ) +import Control.Monad + ( unless + ) +import Control.Monad.IO.Class + ( liftIO + ) +import Control.Monitoring.Tracing + ( MonitorState + ) +import Control.Retry + ( RetryPolicyM + , RetryStatus (..) + , capDelay + , exponentialBackoff + , recoverAll + ) +import Control.Tracer + ( Tracer + , traceWith + ) +import Data.Functor + ( ($>) + ) +import Data.Text.Class + ( ToText (..) + ) +import Servant + ( NoContent + , Proxy (..) + ) +import Servant.Client + ( ClientM + , client + ) +import UnliftIO + ( MonadUnliftIO + , UnliftIO (..) + , askUnliftIO + ) + +-- | Queries that can be sent to the monitoring server via HTTP. +data MonitorQ a where + ReadyQ :: MonitorQ Bool + ObserveQ :: MonitorQ (History, MonitorState) + StepQ :: MonitorQ () + SwitchQ :: MonitorQ MonitorState + +data MonitorClient = MonitorClient + { ready :: ClientM Bool + , observe :: ClientM (ApiT (History, MonitorState)) + , step :: ClientM NoContent + , switch :: ClientM (ApiT MonitorState) + } + +mkMonitorClient + :: MonitorClient +mkMonitorClient = + let ready = client (Proxy @ReadyAPI) + observe = client (Proxy @ObserveAPI) + step = client (Proxy @StepAPI) + switch = client (Proxy @SwitchAPI) + in MonitorClient{..} + +-- | A showable existential wrapper around a 'Query' value, for logging purposes. +data AnyMonitorQ = forall a. Show a => AnyQuery (MonitorQ a) + +instance Show AnyMonitorQ where + show (AnyQuery ReadyQ) = "Ready" + show (AnyQuery ObserveQ) = "Observe" + show (AnyQuery StepQ) = "Step" + show (AnyQuery SwitchQ) = "Switch" + +-- | Run any query against the monitoring server. +newtype RunMonitorQ m = RunQuery (forall a. Show a => MonitorQ a -> m a) + +-- | Messages that can be logged by the http client. +data MsgMonitorClient + = MsgMonitorClientReq AnyMonitorQ + | MsgMonitorClientRetry AnyMonitorQ + deriving stock (Show) + +instance ToText MsgMonitorClient where + toText = \case + MsgMonitorClientReq q -> "Client request: " <> toText (show q) + MsgMonitorClientRetry q -> "Client retry: " <> toText (show q) + +newRunQuery + :: MonadUnliftIO m + => (forall a. ClientM a -> IO a) + -> Tracer m MsgMonitorClient + -> MonitorClient + -> m (RunMonitorQ m) +newRunQuery query tr MonitorClient{ready, observe, step, switch} = + do + UnliftIO unlift <- askUnliftIO + pure $ RunQuery $ \request -> do + traceWith tr $ MsgMonitorClientReq $ AnyQuery request + liftIO $ case request of + ReadyQ -> recoverAll retryPolicy + $ \rt -> do + unless (firstTry rt) + $ unlift + $ traceWith tr + $ MsgMonitorClientRetry + $ AnyQuery request + query ready + ObserveQ -> unApiT <$> query observe + StepQ -> query step $> () + SwitchQ -> unApiT <$> query switch + +retryPolicy :: RetryPolicyM IO +retryPolicy = capDelay (60 * oneSecond) $ exponentialBackoff oneSecond + where + oneSecond = 1_000_000 :: Int + +firstTry :: RetryStatus -> Bool +firstTry (RetryStatus 0 _ _) = True +firstTry _ = False diff --git a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Monitoring/Http/OpenApi.hs b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Monitor/OpenApi.hs similarity index 77% rename from lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Monitoring/Http/OpenApi.hs rename to lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Monitor/OpenApi.hs index 402a54d8bbb..1de0e7158e3 100644 --- a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Monitoring/Http/OpenApi.hs +++ b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Monitor/OpenApi.hs @@ -1,10 +1,9 @@ {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE ScopedTypeVariables #-} -module Cardano.Wallet.Launch.Cluster.Monitoring.Http.OpenApi - ( generateOpenapi3 - , apiSchema - , definitions +module Cardano.Wallet.Launch.Cluster.Http.Monitor.OpenApi + ( monitoringPaths + , monitoringDefinitions , monitorStateSchema , observationSchema ) where @@ -18,35 +17,22 @@ import Control.Lens , (?~) ) import Data.Aeson -import Data.Aeson.Encode.Pretty - ( encodePretty - ) import Data.HashMap.Strict.InsOrd ( InsOrdHashMap ) import Data.OpenApi ( Definitions - , HasComponents (..) , HasContent (..) , HasDescription (..) , HasEnum (..) , HasGet (..) - , HasInfo (..) , HasItems (..) - , HasLicense (license) , HasOneOf (..) - , HasPaths (..) , HasPost (..) , HasProperties (..) , HasSchema (..) - , HasSchemas (..) , HasSummary (..) - , HasTitle (..) , HasType (..) - , HasUrl (..) - , HasVersion (..) - , License - , OpenApi , OpenApiItems (..) , OpenApiType (..) , Operation @@ -54,7 +40,6 @@ import Data.OpenApi , Reference (..) , Referenced (..) , Schema - , URL (..) , _Inline ) import Data.Text @@ -64,28 +49,12 @@ import Network.HTTP.Media ( MediaType ) -import qualified Data.ByteString.Lazy.Char8 as BL - -generateOpenapi3 :: BL.ByteString -generateOpenapi3 = encodePretty apiSchema - -- jsonMediaType :: MediaType - -- jsonMediaType = "application/json" - -apiSchema :: OpenApi -apiSchema :: OpenApi = - mempty - & info . title .~ "Cardano Wallet Monitoring API" - & info . version .~ "0.1.0.0" - & info . description ?~ "This is the API for the monitoring server" - & info . license ?~ license' - & paths .~ paths' - & components . schemas .~ definitions - -definitions :: Definitions Schema -definitions = [ ("Ready", mempty & type_ ?~ OpenApiBoolean) - , ("MonitorState", monitorStateSchema) - , ("Observation", observationSchema) - ] +monitoringDefinitions :: Definitions Schema +monitoringDefinitions = + [ ("Ready", mempty & type_ ?~ OpenApiBoolean) + , ("MonitorState", monitorStateSchema) + , ("Observation", observationSchema) + ] monitorStateSchema :: Schema monitorStateSchema = @@ -108,7 +77,7 @@ historySchema = & type_ ?~ OpenApiArray & items ?~ OpenApiItemsObject - (Inline timedPhaseSchema) + (Inline timedPhaseSchema) timedPhaseSchema :: Schema timedPhaseSchema = @@ -160,13 +129,8 @@ relayNodeSchema = & type_ ?~ OpenApiString & description ?~ "The socket file or pipe of a relay node" -license' :: License -license' = - "Apache 2" - & url ?~ URL "https://www.apache.org/licenses/LICENSE-2.0.html" - -paths' :: InsOrdHashMap FilePath PathItem -paths' = +monitoringPaths :: InsOrdHashMap FilePath PathItem +monitoringPaths = [ readyPath , controlStepPath , controlSwitchPath diff --git a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Monitor/Server.hs b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Monitor/Server.hs new file mode 100644 index 00000000000..e458794fc52 --- /dev/null +++ b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Monitor/Server.hs @@ -0,0 +1,84 @@ +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Cardano.Wallet.Launch.Cluster.Http.Monitor.Server + ( mkControlHandlers + , ControlHandlers + , mkControlServer + ) +where + +import Prelude + +import Cardano.Wallet.Launch.Cluster.Http.Monitor.API + ( ApiT (..) + , ControlAPI + ) +import Cardano.Wallet.Launch.Cluster.Monitoring.Phase + ( History (..) + , Phase (..) + ) +import Control.Monad.IO.Class + ( MonadIO (..) + ) +import Control.Monitoring.Monitor + ( Monitor (..) + ) +import Control.Monitoring.Tracing + ( MonitorState + ) +import Data.Foldable + ( find + ) +import Data.Functor + ( ($>) + ) +import Data.Maybe + ( isJust + ) +import Servant + ( Handler + , NoContent (..) + , (:<|>) (..) + ) +import Servant.Server + ( HasServer (..) + ) + +isReady :: Phase -> Bool +isReady (Cluster _) = True +isReady _ = False + +-- | Create handlers for the monitoring API +mkControlHandlers + :: Monitor IO a History + -> ControlHandlers +mkControlHandlers monitor = + ControlHandlers + { handleReady = do + s <- history . fst <$> observe monitor + pure $ isJust $ find (isReady . snd) s + , handleStep = step monitor + , handleSwitch = do + switch monitor + snd <$> observe monitor + , handleObserve = observe monitor + } + +-- | Handlers for the monitoring API, opaque. +data ControlHandlers = ControlHandlers + { handleReady :: IO Bool + , handleStep :: IO () + , handleSwitch :: IO MonitorState + , handleObserve :: IO (History, MonitorState) + } + +mkControlServer + :: ControlHandlers + -> ServerT ControlAPI Handler +mkControlServer ControlHandlers{..} = + liftIO (handleReady) + :<|> liftIO (handleStep $> NoContent) + :<|> liftIO (ApiT <$> handleSwitch) + :<|> liftIO (ApiT <$> handleObserve) diff --git a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/OpenApi.hs b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/OpenApi.hs new file mode 100644 index 00000000000..586d80c4497 --- /dev/null +++ b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/OpenApi.hs @@ -0,0 +1,65 @@ +{-# LANGUAGE ScopedTypeVariables #-} + +module Cardano.Wallet.Launch.Cluster.Http.OpenApi + ( generateOpenapi3 + , apiSchema + , definitions + ) where + +import Prelude + +import Control.Lens + ( (&) + , (.~) + , (?~) + ) +import Data.Aeson.Encode.Pretty + ( encodePretty + ) +import Data.OpenApi + ( Definitions + , HasComponents (..) + , HasDescription (..) + , HasInfo (..) + , HasLicense (license) + , HasPaths (..) + , HasSchemas (..) + , HasTitle (..) + , HasUrl (..) + , HasVersion (..) + , License + , OpenApi + , Schema + , URL (..) + ) + +import Cardano.Wallet.Launch.Cluster.Http.Faucet.OpenApi + ( faucetDefinitions + , faucetPaths + ) +import Cardano.Wallet.Launch.Cluster.Http.Monitor.OpenApi + ( monitoringDefinitions + , monitoringPaths + ) +import qualified Data.ByteString.Lazy.Char8 as BL + +generateOpenapi3 :: BL.ByteString +generateOpenapi3 = encodePretty apiSchema + +apiSchema :: OpenApi +apiSchema :: OpenApi = + mempty + & info . title .~ "Cardano Wallet Monitoring API" + & info . version .~ "0.1.0.0" + & info . description ?~ "This is the API for the monitoring server" + & info . license ?~ license' + & paths .~ (faucetPaths <> monitoringPaths) + & components . schemas .~ definitions + +definitions :: Definitions Schema +definitions = faucetDefinitions <> monitoringDefinitions + +license' :: License +license' = + "Apache 2" + & url ?~ URL "https://www.apache.org/licenses/LICENSE-2.0.html" diff --git a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Server.hs b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Server.hs new file mode 100644 index 00000000000..b7f3b38056f --- /dev/null +++ b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Server.hs @@ -0,0 +1,89 @@ +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +module Cardano.Wallet.Launch.Cluster.Http.Server + ( withHttpServer + , httpServer + , mkControlHandlers + , ControlHandlers + ) +where + +import Prelude + +import Cardano.Wallet.Launch.Cluster.Http.API + ( API + ) +import Cardano.Wallet.Launch.Cluster.Http.Faucet.Server + ( FaucetHandlers (..) + , mkFaucetServer + ) +import Cardano.Wallet.Launch.Cluster.Http.Monitor.Server + ( ControlHandlers + , mkControlHandlers + , mkControlServer + ) +import Cardano.Wallet.Primitive.NetworkId + ( HasSNetworkId + , SNetworkId + ) +import Control.Monad + ( (<=<) + ) +import Control.Monad.Cont + ( ContT (..) + ) +import Network.Socket + ( PortNumber + ) +import Network.Wai.Handler.Warp + ( run + ) +import Servant + ( Application + , Proxy (..) + , (:<|>) (..) + ) +import Servant.Server + ( serve + ) +import UnliftIO + ( async + , link + ) + +server + :: forall n + . HasSNetworkId n + => SNetworkId n + -> ControlHandlers + -> FaucetHandlers + -> Application +server _ handlers appHandlers = + serve (Proxy @(API n)) + $ mkControlServer handlers + :<|> mkFaucetServer appHandlers + +-- | Run a HTTP server that serves the monitoring API +httpServer + :: HasSNetworkId n + => SNetworkId n + -> PortNumber + -> ControlHandlers + -> FaucetHandlers + -> IO () +httpServer api port handlers appHandlers = + run (fromIntegral port) $ server api handlers appHandlers + +-- | Start a HTTP server in a linked thread that serves the monitoring API +withHttpServer + :: HasSNetworkId n + => SNetworkId n + -> PortNumber + -> ControlHandlers + -> FaucetHandlers + -> ContT r IO () +withHttpServer api port handlers app = ContT $ \k -> do + link <=< async $ httpServer api port handlers app + k () diff --git a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Service.hs b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Service.hs new file mode 100644 index 00000000000..95ce865e0b4 --- /dev/null +++ b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Http/Service.hs @@ -0,0 +1,141 @@ +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# OPTIONS_GHC -Wno-missing-local-signatures #-} + +module Cardano.Wallet.Launch.Cluster.Http.Service + ( MsgHttpService (..) + , ServiceConfiguration (..) + , withService + , withServiceServer + , withServiceClient + ) +where + +import Prelude + +import Cardano.Wallet.Launch.Cluster.Config + ( Config + ) +import Cardano.Wallet.Launch.Cluster.Http.Client + ( withHttpClient + ) +import Cardano.Wallet.Launch.Cluster.Http.Faucet.Client + ( RunFaucetQ + ) +import Cardano.Wallet.Launch.Cluster.Http.Faucet.Server + ( NodeConnVar + , mkFaucetHandlers + ) +import Cardano.Wallet.Launch.Cluster.Http.Logging + ( MsgHttpService (..) + ) +import Cardano.Wallet.Launch.Cluster.Http.Monitor.Client + ( RunMonitorQ + ) +import Cardano.Wallet.Launch.Cluster.Http.Server + ( mkControlHandlers + , withHttpServer + ) +import Cardano.Wallet.Launch.Cluster.Monitoring.Phase + ( History (..) + , Phase + ) +import Cardano.Wallet.Launch.Cluster.Monitoring.TimedMonitor + ( timedMonitor + ) +import Cardano.Wallet.Network.Ports + ( getRandomPort + ) +import Cardano.Wallet.Primitive.NetworkId + ( HasSNetworkId + , SNetworkId + ) +import Control.Monad.Cont + ( ContT (..) + ) +import Control.Monad.IO.Class + ( MonadIO (..) + ) +import Control.Monitoring.Monitor + ( monitorTracer + ) +import Control.Monitoring.Tracing + ( MonitorState + , withTracingState + ) +import Control.Tracer + ( Tracer (..) + , traceWith + ) +import Data.Functor.Contravariant + ( (>$<) + ) +import Data.Profunctor + ( Profunctor (..) + ) +import Network.Socket + ( PortNumber + ) + +-- | Configuration for the monitoring service +data ServiceConfiguration = ServiceConfiguration + { servicePort :: Maybe PortNumber + -- ^ The port to run the monitoring service on + -- If `Nothing`, a random port will be chosen + , monitorInitialState :: MonitorState + -- ^ The initial state of the monitor + } + deriving stock (Show) + +-- | Start a monitoring service, returning a tracer to write `Phase` values to +-- and a function to interact with the monitoring service +withService + :: HasSNetworkId n + => SNetworkId n + -> NodeConnVar + -> Config + -> Tracer IO MsgHttpService + -- ^ Tracer for logging the monitoring operations + -> ServiceConfiguration + -- ^ Configuration for the monitoring service + -> ContT () IO (Tracer IO Phase, (RunMonitorQ IO, RunFaucetQ IO)) +withService network conn clusterConfig tr config = do + (port, tracer) <- withServiceServer network conn clusterConfig tr config + queries <- withServiceClient network port tr + pure (tracer, queries) + +withServiceClient + :: HasSNetworkId n + => SNetworkId n + -> PortNumber + -> Tracer IO MsgHttpService + -> ContT () IO (RunMonitorQ IO, RunFaucetQ IO) +withServiceClient network port tr = do + liftIO $ traceWith tr MsgHttpServiceClientStarted + queries <- withHttpClient network (MsgHttpServiceQuery >$< tr) port + ContT $ \k -> do + k queries + traceWith tr MsgHttpServiceClientStopped + +withServiceServer + :: HasSNetworkId n + => SNetworkId n + -> NodeConnVar + -> Config + -> Tracer IO MsgHttpService + -> ServiceConfiguration + -> ContT () IO (PortNumber, Tracer IO Phase) +withServiceServer network conn clusterConfig tr ServiceConfiguration{..} = do + monitor <- liftIO $ withTracingState timedMonitor monitorInitialState + port <- liftIO $ maybe getRandomPort pure servicePort + liftIO $ traceWith tr $ MsgHttpServicePort port + withHttpServer + network + port + (mkControlHandlers $ rmap History monitor) + (mkFaucetHandlers conn clusterConfig) + liftIO $ traceWith tr MsgHttpServiceServerStarted + ContT $ \k -> do + k (port, monitorTracer monitor) + traceWith tr MsgHttpServiceServerStopped diff --git a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Monitoring/Http/Client.hs b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Monitoring/Http/Client.hs deleted file mode 100644 index 2468e3a192b..00000000000 --- a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Monitoring/Http/Client.hs +++ /dev/null @@ -1,173 +0,0 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE NumericUnderscores #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeApplications #-} - -module Cardano.Wallet.Launch.Cluster.Monitoring.Http.Client - ( withHttpClient - , RunQuery (..) - , Query (..) - , MsgClient (..) - , AnyQuery (..) - ) -where - -import Prelude - -import Cardano.Wallet.Launch.Cluster.Monitoring.Http.API - ( API - , ApiT (..) - ) -import Cardano.Wallet.Launch.Cluster.Monitoring.Phase - ( History - ) -import Control.Monad - ( unless - ) -import Control.Monad.Cont - ( ContT (..) - ) -import Control.Monad.IO.Class - ( liftIO - ) -import Control.Monitoring.Tracing - ( MonitorState - ) -import Control.Retry - ( RetryPolicyM - , RetryStatus (..) - , capDelay - , exponentialBackoff - , recoverAll - ) -import Control.Tracer - ( Tracer - , traceWith - ) -import Data.Functor - ( ($>) - ) -import Network.HTTP.Client - ( ManagerSettings (..) - , defaultManagerSettings - , newManager - , responseTimeoutNone - ) -import Network.Socket - ( PortNumber - ) -import Servant - ( NoContent - , Proxy (..) - , (:<|>) (..) - ) -import Servant.Client - ( BaseUrl (..) - , ClientM - , Scheme (..) - , client - , mkClientEnv - , runClientM - ) -import UnliftIO - ( MonadUnliftIO - , UnliftIO (..) - , askUnliftIO - , throwIO - ) - --- | Queries that can be sent to the monitoring server via HTTP. -data Query a where - ReadyQ :: Query Bool - ObserveQ :: Query (History, MonitorState) - StepQ :: Query () - SwitchQ :: Query MonitorState - -data Client = Client - { ready :: ClientM Bool - , observe :: ClientM (ApiT (History, MonitorState)) - , step :: ClientM NoContent - , switch :: ClientM (ApiT MonitorState) - } - -mkClient :: Client -mkClient = - let ready :<|> step :<|> switch :<|> observe = client (Proxy @API) - in Client{..} - --- | A showable existential wrapper around a 'Query' value, for logging purposes. -data AnyQuery = forall a. Show a => AnyQuery (Query a) - -instance Show AnyQuery where - show (AnyQuery ReadyQ) = "Ready" - show (AnyQuery ObserveQ) = "Observe" - show (AnyQuery StepQ) = "Step" - show (AnyQuery SwitchQ) = "Switch" - --- | Run any query against the monitoring server. -newtype RunQuery m = RunQuery (forall a. Show a => Query a -> m a) - --- | Messages that can be logged by the http client. -data MsgClient - = MsgClientStart - | MsgClientReq AnyQuery - | MsgClientRetry AnyQuery - | MsgClientDone - deriving stock (Show) - --- | Produce a closure over the http client of an http monitoring server that --- can be used to query the server. -withHttpClient - :: MonadUnliftIO m - => Tracer m MsgClient - -- ^ how to trace the http client operations - -> PortNumber - -- ^ Monitoring port to attach to (http://localhost is hardcoded) - -> ContT () m (RunQuery m) -withHttpClient tracer httpPort = ContT $ \continue -> do - let tr = traceWith tracer - tr MsgClientStart - UnliftIO unlift <- askUnliftIO - let url = BaseUrl Http "localhost" (fromIntegral httpPort) "" - manager <- - liftIO - $ newManager - $ defaultManagerSettings - { managerResponseTimeout = responseTimeoutNone - } - let - query :: ClientM a -> IO a - query f = do - r <- runClientM f $ mkClientEnv manager url - either throwIO pure r - Client{ready, observe, step, switch} = mkClient - continue $ RunQuery $ \request -> do - tr $ MsgClientReq $ AnyQuery request - liftIO $ case request of - ReadyQ -> recoverAll retryPolicy - $ \rt -> do - unless (firstTry rt) - $ unlift - $ tr - $ MsgClientRetry - $ AnyQuery request - query ready - ObserveQ -> unApiT <$> query observe - StepQ -> query step $> () - SwitchQ -> unApiT <$> query switch - - tr MsgClientDone - -retryPolicy :: RetryPolicyM IO -retryPolicy = capDelay (60 * oneSecond) $ exponentialBackoff oneSecond - where - oneSecond = 1_000_000 :: Int - -firstTry :: RetryStatus -> Bool -firstTry (RetryStatus 0 _ _) = True -firstTry _ = False diff --git a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Monitoring/Http/Logging.hs b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Monitoring/Http/Logging.hs deleted file mode 100644 index 4d1303e2dd9..00000000000 --- a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Monitoring/Http/Logging.hs +++ /dev/null @@ -1,23 +0,0 @@ -{-# LANGUAGE DerivingStrategies #-} - -module Cardano.Wallet.Launch.Cluster.Monitoring.Http.Logging - ( MsgHttpMonitoring (..) - ) -where - -import Prelude - -import Cardano.Wallet.Launch.Cluster.Monitoring.Http.Client - ( MsgClient - ) -import Network.Socket - ( PortNumber - ) - --- | Messages for the HTTP monitoring service -data MsgHttpMonitoring - = MsgHttpMonitoringPort PortNumber - | MsgHttpMonitoringQuery MsgClient - | MsgHttpMonitoringServerStarted - | MsgHttpMonitoringDone - deriving stock (Show) diff --git a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Monitoring/Http/Server.hs b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Monitoring/Http/Server.hs deleted file mode 100644 index 629b353d22a..00000000000 --- a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Monitoring/Http/Server.hs +++ /dev/null @@ -1,119 +0,0 @@ -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE TypeApplications #-} - -module Cardano.Wallet.Launch.Cluster.Monitoring.Http.Server - ( withHttpServer - , httpServer - , mkHandlers - , Handlers - ) -where - -import Prelude - -import Cardano.Wallet.Launch.Cluster.Monitoring.Http.API - ( API - , ApiT (..) - ) -import Cardano.Wallet.Launch.Cluster.Monitoring.Phase - ( History (..) - , Phase (..) - ) -import Control.Monad - ( (<=<) - ) -import Control.Monad.Cont - ( ContT (..) - ) -import Control.Monad.IO.Class - ( MonadIO (..) - ) -import Control.Monitoring.Monitor - ( Monitor (..) - ) -import Control.Monitoring.Tracing - ( MonitorState - ) -import Data.Foldable - ( find - ) -import Data.Functor - ( ($>) - ) -import Data.Maybe - ( isJust - ) -import Network.Socket - ( PortNumber - ) -import Network.Wai.Handler.Warp - ( run - ) -import Servant - ( Application - , Handler - , NoContent (..) - , Proxy (..) - , (:<|>) (..) - ) -import Servant.Server - ( serve - ) -import UnliftIO - ( async - , link - ) - -isReady :: Phase -> Bool -isReady (Cluster _) = True -isReady _ = False - --- | Create handlers for the monitoring API -mkHandlers - :: Monitor IO a History - -> Handlers -mkHandlers monitor = - Handlers - { handleReady = do - s <- history . fst <$> observe monitor - pure $ isJust $ find (isReady . snd) s - , handleStep = step monitor - , handleSwitch = do - switch monitor - snd <$> observe monitor - , handleObserve = observe monitor - } - --- | Handlers for the monitoring API, opaque. -data Handlers = Handlers - { handleReady :: IO Bool - , handleStep :: IO () - , handleSwitch :: IO MonitorState - , handleObserve :: IO (History, MonitorState) - } - -server - :: Handlers - -> IO Application -server handlers = do - let lIO :: forall a. IO a -> Handler a - lIO = liftIO - pure - $ serve (Proxy @API) - $ lIO (handleReady handlers) - :<|> lIO (handleStep handlers $> NoContent) - :<|> lIO (ApiT <$> handleSwitch handlers) - :<|> lIO (ApiT <$> handleObserve handlers) - --- | Run a HTTP server that serves the monitoring API -httpServer :: PortNumber -> Handlers -> IO () -httpServer port handlers = server handlers >>= run (fromIntegral port) - --- | Start a HTTP server in a linked thread that serves the monitoring API -withHttpServer - :: PortNumber - -> Handlers - -> ContT r IO () -withHttpServer port handlers = ContT $ \k -> do - link <=< async $ httpServer port handlers - k () diff --git a/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Monitoring/TimedMonitor.hs b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Monitoring/TimedMonitor.hs new file mode 100644 index 00000000000..373b47d77d9 --- /dev/null +++ b/lib/local-cluster/lib/Cardano/Wallet/Launch/Cluster/Monitoring/TimedMonitor.hs @@ -0,0 +1,47 @@ +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TupleSections #-} + +module Cardano.Wallet.Launch.Cluster.Monitoring.TimedMonitor + ( timedMonitor + ) +where + +import Prelude + +import Control.Concurrent.Class.MonadSTM + ( MonadSTM + ) +import Control.Monad.IO.Class + ( MonadIO (..) + ) +import Control.Monitoring.Folder + ( mkTracingFromFold + ) +import Control.Monitoring.Monitor + ( Monitor + , mkMonitor + ) +import Control.Monitoring.Tracing + ( AnyTracing (AnyTracing) + , StateS (..) + , Tracing + ) +import Data.Time + ( UTCTime + , getCurrentTime + ) + +import qualified Control.Foldl as F + +-- | A monitor that patch the current time to the value being monitored. +timedMonitor + :: forall w m a + . (MonadSTM m, MonadIO m) + => StateS w + -> m (Monitor m a [(UTCTime, a)]) +timedMonitor initialState = do + let tracer :: Tracing w (UTCTime, a) [(UTCTime, a)] + tracer = mkTracingFromFold F.list initialState + mkMonitor + (AnyTracing initialState tracer) + (\x -> (,x) <$> liftIO getCurrentTime) diff --git a/lib/local-cluster/local-cluster.cabal b/lib/local-cluster/local-cluster.cabal index 8eba687dd03..ad07c202e26 100644 --- a/lib/local-cluster/local-cluster.cabal +++ b/lib/local-cluster/local-cluster.cabal @@ -10,32 +10,21 @@ maintainer: hal@cardanofoundation.org copyright: 2023 Cardano Foundation category: Web build-type: Simple -data-files: - data/swagger.json +data-files: data/swagger.json common language default-language: Haskell2010 default-extensions: NoImplicitPrelude OverloadedStrings + ghc-options: - -fhelpful-errors - -fprint-expanded-synonyms - -freverse-errors - -fwarn-incomplete-uni-patterns - -fwarn-unused-do-bind - -Wall - -Wcompat - -Werror=incomplete-patterns - -Widentities - -Wincomplete-record-updates - -Wincomplete-uni-patterns - -Wmissing-deriving-strategies - -Wmissing-local-signatures - -Wpartial-fields - -Wredundant-constraints - -Wtabs - -Wunused-foralls + -fhelpful-errors -fprint-expanded-synonyms -freverse-errors + -fwarn-incomplete-uni-patterns -fwarn-unused-do-bind -Wall -Wcompat + -Werror=incomplete-patterns -Widentities + -Wincomplete-record-updates -Wincomplete-uni-patterns + -Wmissing-deriving-strategies -Wmissing-local-signatures + -Wpartial-fields -Wredundant-constraints -Wtabs -Wunused-foralls -Wunused-packages flag release @@ -43,7 +32,6 @@ flag release default: False manual: True - library import: language hs-source-dirs: lib @@ -51,6 +39,7 @@ library Cardano.Node.Cli.Launcher Cardano.Wallet.Cli.Launcher Cardano.Wallet.Faucet + Cardano.Wallet.Faucet.Gen.Address Cardano.Wallet.Launch.Cluster Cardano.Wallet.Launch.Cluster.Aeson Cardano.Wallet.Launch.Cluster.CardanoCLI @@ -64,16 +53,26 @@ library Cardano.Wallet.Launch.Cluster.Faucet Cardano.Wallet.Launch.Cluster.FileOf Cardano.Wallet.Launch.Cluster.GenesisFiles + Cardano.Wallet.Launch.Cluster.Http.API + Cardano.Wallet.Launch.Cluster.Http.Client + Cardano.Wallet.Launch.Cluster.Http.Faucet.API + Cardano.Wallet.Launch.Cluster.Http.Faucet.Client + Cardano.Wallet.Launch.Cluster.Http.Faucet.OpenApi + Cardano.Wallet.Launch.Cluster.Http.Faucet.SendFaucetAssets + Cardano.Wallet.Launch.Cluster.Http.Faucet.Server + Cardano.Wallet.Launch.Cluster.Http.Logging + Cardano.Wallet.Launch.Cluster.Http.Monitor.API + Cardano.Wallet.Launch.Cluster.Http.Monitor.Client + Cardano.Wallet.Launch.Cluster.Http.Monitor.OpenApi + Cardano.Wallet.Launch.Cluster.Http.Monitor.Server + Cardano.Wallet.Launch.Cluster.Http.OpenApi + Cardano.Wallet.Launch.Cluster.Http.Server + Cardano.Wallet.Launch.Cluster.Http.Service Cardano.Wallet.Launch.Cluster.KeyRegistration Cardano.Wallet.Launch.Cluster.Logging Cardano.Wallet.Launch.Cluster.MonetaryPolicyScript - Cardano.Wallet.Launch.Cluster.Monitoring.Http.API - Cardano.Wallet.Launch.Cluster.Monitoring.Http.Client - Cardano.Wallet.Launch.Cluster.Monitoring.Http.Logging - Cardano.Wallet.Launch.Cluster.Monitoring.Http.OpenApi - Cardano.Wallet.Launch.Cluster.Monitoring.Http.Server - Cardano.Wallet.Launch.Cluster.Monitoring.Monitor Cardano.Wallet.Launch.Cluster.Monitoring.Phase + Cardano.Wallet.Launch.Cluster.Monitoring.TimedMonitor Cardano.Wallet.Launch.Cluster.Node.GenNodeConfig Cardano.Wallet.Launch.Cluster.Node.GenTopology Cardano.Wallet.Launch.Cluster.Node.NodeParams @@ -88,14 +87,15 @@ library Cardano.Wallet.Launch.Cluster.Tx Cardano.Wallet.Launch.Cluster.UnsafeInterval Control.Monitoring.Concurrent + Control.Monitoring.Folder Control.Monitoring.Monitor Control.Monitoring.Tracing - Control.Monitoring.Folder if flag(release) ghc-options: -O2 -Werror build-depends: + , address-derivation-discovery , aeson , aeson-pretty , aeson-qq @@ -135,7 +135,6 @@ library , lens , machines , memory - , network , mtl , network , OddWord @@ -145,6 +144,7 @@ library , ouroboros-network-api , pathtype , profunctors + , QuickCheck , retry , servant , servant-client @@ -164,7 +164,7 @@ executable local-cluster import: language main-is: local-cluster.hs hs-source-dirs: exe - ghc-options: -threaded -rtsopts + ghc-options: -threaded -rtsopts if flag(release) ghc-options: -O2 -Werror @@ -186,16 +186,16 @@ executable local-cluster , with-utf8 test-suite test - import: language - type: exitcode-stdio-1.0 - main-is: test.hs - ghc-options: -threaded -rtsopts - hs-source-dirs: test/unit - + import: language + type: exitcode-stdio-1.0 + main-is: test.hs + ghc-options: -threaded -rtsopts + hs-source-dirs: test/unit build-depends: , aeson , base , bytestring + , cardano-wallet-primitive , cardano-wallet-test-utils , contra-tracer , foldl @@ -211,9 +211,11 @@ test-suite test build-tool-depends: hspec-discover:hspec-discover other-modules: - Cardano.Wallet.Launch.Cluster.Monitoring.Http.APISpec - Cardano.Wallet.Launch.Cluster.Monitoring.Http.OpenAPISpec - Cardano.Wallet.Launch.Cluster.Monitoring.MonitorSpec + Cardano.Wallet.Launch.Cluster.Http.Faucet.APISpec + Cardano.Wallet.Launch.Cluster.Http.Faucet.SendFaucetAssetsSpec + Cardano.Wallet.Launch.Cluster.Http.Monitor.APISpec + Cardano.Wallet.Launch.Cluster.Http.OpenAPISpec + Cardano.Wallet.Launch.Cluster.Http.ServiceSpec Control.Monitoring.MonitorSpec Control.Monitoring.TracingSpec Paths_local_cluster diff --git a/lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Http/Faucet/APISpec.hs b/lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Http/Faucet/APISpec.hs new file mode 100644 index 00000000000..3787b137d00 --- /dev/null +++ b/lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Http/Faucet/APISpec.hs @@ -0,0 +1,54 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeApplications #-} + +module Cardano.Wallet.Launch.Cluster.Http.Faucet.APISpec + ( spec + ) +where + +import Prelude + +import Cardano.Wallet.Launch.Cluster.Http.Faucet.SendFaucetAssets + ( genSendFaucetAssets + ) +import Cardano.Wallet.Primitive.NetworkId + ( NetworkDiscriminant (Mainnet) + ) +import Data.Aeson + ( FromJSON (..) + , Result (..) + , ToJSON (..) + , fromJSON + ) +import Data.OpenApi + ( ToSchema + , validateToJSON + ) +import Test.Hspec + ( Expectation + , Spec + , describe + , it + , shouldBe + ) +import Test.QuickCheck + ( forAll + ) + +jsonRoundtrip :: (ToJSON a, FromJSON a, Eq a, Show a) => a -> IO () +jsonRoundtrip a = fromJSON (toJSON a) `shouldBe` Success a + +validate :: (ToJSON t, ToSchema t) => t -> Expectation +validate x = validateToJSON x `shouldBe` [] + +spec :: Spec +spec = do + describe "/send/assets endpoint" $ do + it "json response roundtrips" + $ forAll + (genSendFaucetAssets @Mainnet) + jsonRoundtrip + it "json response validates random data" + $ forAll + (genSendFaucetAssets @Mainnet) + validate diff --git a/lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Http/Faucet/SendFaucetAssetsSpec.hs b/lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Http/Faucet/SendFaucetAssetsSpec.hs new file mode 100644 index 00000000000..df3f8667fac --- /dev/null +++ b/lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Http/Faucet/SendFaucetAssetsSpec.hs @@ -0,0 +1,42 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeApplications #-} + +module Cardano.Wallet.Launch.Cluster.Http.Faucet.SendFaucetAssetsSpec + ( spec + ) where + +import Prelude + +import Cardano.Wallet.Launch.Cluster.Http.Faucet.SendFaucetAssets + ( genSendFaucetAssets + ) +import Cardano.Wallet.Primitive.NetworkId + ( NetworkDiscriminant (..) + ) +import Data.Aeson + ( FromJSON + , Result (..) + , ToJSON + , fromJSON + , toJSON + ) +import Test.Hspec + ( Spec + , describe + , it + , shouldBe + ) +import Test.QuickCheck + ( forAll + ) + +jsonRoundtrip :: (ToJSON a, FromJSON a, Eq a, Show a) => a -> IO () +jsonRoundtrip a = fromJSON (toJSON a) `shouldBe` Success a + +spec :: Spec +spec = do + describe "SendFaucetAssets" $ do + it "json instances roundtrips for Mainnet" $ do + forAll (genSendFaucetAssets @Mainnet) jsonRoundtrip + it "json instances roundtrips for Testnet" $ do + forAll (genSendFaucetAssets @(Testnet 42)) jsonRoundtrip diff --git a/lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Monitoring/Http/APISpec.hs b/lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Http/Monitor/APISpec.hs similarity index 92% rename from lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Monitoring/Http/APISpec.hs rename to lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Http/Monitor/APISpec.hs index 47e49ecdb5e..2d32b3cea1e 100644 --- a/lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Monitoring/Http/APISpec.hs +++ b/lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Http/Monitor/APISpec.hs @@ -1,4 +1,6 @@ -module Cardano.Wallet.Launch.Cluster.Monitoring.Http.APISpec +{-# LANGUAGE DataKinds #-} + +module Cardano.Wallet.Launch.Cluster.Http.Monitor.APISpec ( spec , genObservation , genMonitorState @@ -7,7 +9,7 @@ where import Prelude -import Cardano.Wallet.Launch.Cluster.Monitoring.Http.API +import Cardano.Wallet.Launch.Cluster.Http.Monitor.API ( ApiT (..) ) import Cardano.Wallet.Launch.Cluster.Monitoring.Phase @@ -74,7 +76,7 @@ spec = do genObservation :: Gen (History, MonitorState) genObservation = do history' <- - History <$> listOf ((,) <$> genUTCTime <*> genPhase) + History <$> listOf ((,) <$> genUTCTime <*> genPhase) state <- genMonitorState pure (history', state) diff --git a/lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Monitoring/Http/OpenAPISpec.hs b/lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Http/OpenAPISpec.hs similarity index 90% rename from lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Monitoring/Http/OpenAPISpec.hs rename to lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Http/OpenAPISpec.hs index d293fadf031..7f589be0dde 100644 --- a/lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Monitoring/Http/OpenAPISpec.hs +++ b/lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Http/OpenAPISpec.hs @@ -1,10 +1,10 @@ -module Cardano.Wallet.Launch.Cluster.Monitoring.Http.OpenAPISpec +module Cardano.Wallet.Launch.Cluster.Http.OpenAPISpec ( spec ) where import Prelude -import Cardano.Wallet.Launch.Cluster.Monitoring.Http.OpenApi +import Cardano.Wallet.Launch.Cluster.Http.OpenApi ( generateOpenapi3 ) import Paths_local_cluster diff --git a/lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Monitoring/MonitorSpec.hs b/lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Http/ServiceSpec.hs similarity index 72% rename from lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Monitoring/MonitorSpec.hs rename to lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Http/ServiceSpec.hs index 61ac72cb744..3d91e5c411b 100644 --- a/lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Monitoring/MonitorSpec.hs +++ b/lib/local-cluster/test/unit/Cardano/Wallet/Launch/Cluster/Http/ServiceSpec.hs @@ -1,22 +1,25 @@ -module Cardano.Wallet.Launch.Cluster.Monitoring.MonitorSpec +module Cardano.Wallet.Launch.Cluster.Http.ServiceSpec ( spec ) where import Prelude -import Cardano.Wallet.Launch.Cluster.Monitoring.Http.Client - ( Query (..) - , RunQuery (..) +import Cardano.Wallet.Launch.Cluster.Http.Monitor.Client + ( MonitorQ (..) + , RunMonitorQ (..) ) -import Cardano.Wallet.Launch.Cluster.Monitoring.Monitor - ( MonitorConfiguration (..) - , withMonitoring +import Cardano.Wallet.Launch.Cluster.Http.Service + ( ServiceConfiguration (..) + , withService ) import Cardano.Wallet.Launch.Cluster.Monitoring.Phase ( History (..) , Phase (..) ) +import Cardano.Wallet.Primitive.NetworkId + ( SNetworkId (SMainnet) + ) import Control.Monad ( unless ) @@ -51,36 +54,37 @@ import UnliftIO.Concurrent ( threadDelay ) -testMonitoring +testService :: MonitorState - -> (Tracer IO Phase -> RunQuery IO -> IO ()) + -> (Tracer IO Phase -> RunMonitorQ IO -> IO ()) -> IO () -testMonitoring w f = +testService w f = evalContT $ do - (tracer, query) <- - withMonitoring nullTracer - $ MonitorConfiguration Nothing w + (tracer, (query, _)) <- + withService SMainnet (error "No connection") + (error "No cluster") nullTracer + $ ServiceConfiguration Nothing w liftIO $ f tracer query spec :: Spec spec = do - describe "withMonitoring" $ do + describe "withService control" $ do it "can start" $ do - testMonitoring Step $ \_ _ -> pure () + testService Step $ \_ _ -> pure () it "can query" $ do - testMonitoring Step $ \_ (RunQuery query) -> do + testService Step $ \_ (RunQuery query) -> do result <- query ReadyQ result `shouldBe` False it "can trace" $ do - testMonitoring Run $ \tracer _ -> do + testService Run $ \tracer _ -> do traceWith tracer RetrievingFunds it "can report readiness" $ do - testMonitoring Run $ \tracer (RunQuery query) -> do + testService Run $ \tracer (RunQuery query) -> do traceWith tracer (Cluster Nothing) result <- query ReadyQ result `shouldBe` True it "can step the tracer thread" $ do - testMonitoring Step $ \tracer (RunQuery query) -> do + testService Step $ \tracer (RunQuery query) -> do tracer' <- async $ do traceWith tracer (Cluster Nothing) fix $ \loop -> do @@ -88,7 +92,7 @@ spec = do unless result $ query StepQ >> loop wait tracer' it "can report the phase history" $ do - testMonitoring Run $ \tracer (RunQuery query) -> do + testService Run $ \tracer (RunQuery query) -> do traceWith tracer RetrievingFunds traceWith tracer Metadata traceWith tracer Genesis @@ -111,7 +115,7 @@ spec = do ] state `shouldBe` Run it "can switch from step to run" $ do - testMonitoring Step $ \tracer (RunQuery query) -> do + testService Step $ \tracer (RunQuery query) -> do tracer' <- async $ do traceWith tracer RetrievingFunds state <- query SwitchQ diff --git a/lib/wallet-e2e/cardano-wallet-e2e.cabal b/lib/wallet-e2e/cardano-wallet-e2e.cabal index f7e2d3a6df9..b684312c015 100644 --- a/lib/wallet-e2e/cardano-wallet-e2e.cabal +++ b/lib/wallet-e2e/cardano-wallet-e2e.cabal @@ -68,6 +68,7 @@ library , cardano-wallet-primitive , effectful-core ^>=2.2.2.0 , effectful-th ^>=1.0.0.1 + , extra , faucet , http-client ^>=0.7.13.1 , http-types ^>=0.12.3 diff --git a/lib/wallet-e2e/src/Cardano/Wallet/Spec/Network/Local.hs b/lib/wallet-e2e/src/Cardano/Wallet/Spec/Network/Local.hs index 18700a77b65..6171e1b0124 100644 --- a/lib/wallet-e2e/src/Cardano/Wallet/Spec/Network/Local.hs +++ b/lib/wallet-e2e/src/Cardano/Wallet/Spec/Network/Local.hs @@ -21,6 +21,9 @@ import Control.Monad.Trans.Resource import System.IO ( openFile ) +import System.IO.Extra + ( newTempFile + ) import System.Path ( relFile , () @@ -48,23 +51,28 @@ configuredNetwork (DirOf stateDir) (DirOf clusterConfigsDir) = do pure ConfiguredNetwork{configuredNetworkWallet = walletApi, ..} where startCluster :: ResourceT IO WalletApi = do - (_clusterReleaseKey, _clusterProcess) <- - allocate startLocalClusterProcess stopProcess - pure WalletApi - { walletInstanceApiUrl = "http://localhost:8090/v2" - , walletInstanceApiHost = "localhost" - , walletInstanceApiPort = 8090 - } + (_, (socketFile, _)) <- allocate newTempFile snd + (_clusterReleaseKey, _clusterProcess) <- + allocate (startLocalClusterProcess socketFile) stopProcess + pure + WalletApi + { walletInstanceApiUrl = "http://localhost:8090/v2" + , walletInstanceApiHost = "localhost" + , walletInstanceApiPort = 8090 + } - startLocalClusterProcess :: IO (Process () () ()) - startLocalClusterProcess = do + startLocalClusterProcess :: FilePath -> IO (Process () () ()) + startLocalClusterProcess socketPath = do let clusterLog = stateDir relFile "cluster.log" handle <- openFile (toFilePath clusterLog) AppendMode putStrLn $ "Writing cluster logs to " <> toFilePath clusterLog startProcess - $ setStderr (useHandleClose handle) - $ setStdout (useHandleClose handle) - $ proc "local-cluster" - [ "--cluster-configs" - , toFilePath clusterConfigsDir - ] + $ setStderr (useHandleClose handle) + $ setStdout (useHandleClose handle) + $ proc + "local-cluster" + [ "--cluster-configs" + , toFilePath clusterConfigsDir + , "--socket-path" + , socketPath + ]