diff --git a/.gitignore b/.gitignore index fc75b04..f5d9490 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /example/minimal/output/ /example/minimal/.psa-stash /example/minimal/.spago/ +/generated-docs/ /node_modules /output/ /result @@ -10,3 +11,4 @@ /.psci_modules/ /.spago/ /.spago2nix/ +.psc-ide-port diff --git a/Makefile b/Makefile index ec716ec..60fc196 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,10 @@ -.PHONY: build, format, repl, docs, build-example, run-example, docker-cleanup +.PHONY: build, format, repl, docs, build-example, run-example, docker-cleanup, gen-keys ps-sources := $(shell fd --no-ignore-parent -epurs) nix-sources := $(shell fd --no-ignore-parent -enix --exclude='spago*') purs-args := "--stash --censor-lib --censor-codes=ImplicitImport,ImplicitQualifiedImport,ImplicitQualifiedImportReExport,UserDefinedWarning" example-docker := example/minimal/docker/cluster/docker-compose.yaml - -system := $(shell uname -s) -ifeq (${system},Linux) - open-in-browser := xdg-open -else - open-in-browser := open -endif +example-keys := example/minimal/docker/cluster/keys/ requires-nix-shell: @[ "$(IN_NIX_SHELL)" ] || \ @@ -29,11 +23,13 @@ format: requires-nix-shell repl: requires-nix-shell spago repl -docs: - nix build .#docs - ${open-in-browser} result/generated-docs/html/index.html +docs: requires-nix-shell + mv package.json package.json.old + jq 'del(.type)' package.json.old > package.json + spago docs --open + mv -f package.json.old package.json -build-example: +build-example: requires-nix-shell cd example/minimal && \ spago build --purs-args ${purs-args} @@ -43,3 +39,21 @@ run-example: docker-cleanup docker-cleanup: docker compose -f ${example-docker} rm --force --stop docker volume rm -f cluster_hydra-persist-a cluster_hydra-persist-b + +gen-keys: requires-nix-shell + @hydra-node gen-hydra-key --output-file ${example-keys}/hydra-a + @hydra-node gen-hydra-key --output-file ${example-keys}/hydra-b + @cardano-cli address key-gen \ + --signing-key-file ${example-keys}/cardano-a.sk \ + --verification-key-file ${example-keys}/cardano-a.vk + @cardano-cli address build \ + --payment-verification-key-file ${example-keys}/cardano-a.vk \ + --testnet-magic 1 + @echo + @cardano-cli address key-gen \ + --signing-key-file ${example-keys}/cardano-b.sk \ + --verification-key-file ${example-keys}/cardano-b.vk + @cardano-cli address build \ + --payment-verification-key-file ${example-keys}/cardano-b.vk \ + --testnet-magic 1 + @echo diff --git a/README.md b/README.md index 1118e24..83620c2 100644 --- a/README.md +++ b/README.md @@ -2,31 +2,102 @@ [Cardano Hydra](https://hydra.family/head-protocol/) SDK (Software Development Kit) for PureScript. This library offers -various interfaces to facilitate rapid development of Hydra-based -applications. +various interfaces to facilitate the rapid development +of Hydra-based applications. **Table of Contents** -- [Applications](#applications) -- [Functionality](#functionality) +- [Compatibility](#compatibility) +- [Preliminaries](#preliminaries) - [Development Workflows](#development-workflows) +- [Getting Started with Example](#getting-started-with-example) +- [SDK Core Functionality](#sdk-core-functionality) +- [SDK Additionals: AppManager](#sdk-additionals-appmanager) +- [Applications](#applications) -### Applications +## Compatibility -Please refer to [hydra-auction-offchain](https://github.com/mlabs-haskell/hydra-auction-offchain) -for a full-fledged example that utilizes this SDK. +| hydra-sdk | hydra-node | cardano-node | +| ----------- | ------------ | ------------ | +| **`0.1.0`** | **`0.19.0`** | **`10.1.2`** | + + +## Preliminaries + +Since using this SDK implies a certain degree of understanding of the Hydra +protocol, it is advisable to get familiar with the ["Hydra: Fast Isomorphic State Channels" paper](https://iohk.io/en/research/library/papers/hydra-fast-isomorphic-state-channels/) +and the [Hydra Head protocol documentation](https://hydra.family/head-protocol/docs/) +before proceeding with the **Getting Started** guide below. + +## Development Workflows + +Before executing most of the commands listed below, first enter the Nix +development shell by running `nix develop`. + +* **Build docs and open them in the browser**: `make docs` +* **Build the project**: `make build` +* **Format code**: `make format` + +## Getting Started with Example + +The simplest way to get a sense of how this SDK can help build Hydra-based +applications is to run our minimal example and then adapt or extend it to suit +your specific requirements. + +Go to `example/minimal` and follow the step-by-step guide below to spin up +a cluster of two nodes, with each node running the minimal example logic. + +1. Enter the Nix development environment by running `nix develop` from the root +directory of this repository. This will put you in the shell with all the +necessary executables required to continue with the setup procedure. -### Functionality +2. In [example/minimal/docker/cluster/](example/minimal/docker/cluster/) you +can find configuration files for both nodes. The only field that needs to be +updated here is the `blockfrostApiKey`, which should be set to a valid +Blockfrost API key **for preprod**. +Visit the [Blockfrost website](https://blockfrost.io/) to generate a fresh API key. -The `HydraSdk.Process` module provides an interface for spinning up a hydra-node +3. Execute `make gen-keys` to generate the necessary Cardano and Hydra keys +required by the underlying Hydra nodes. Cardano keys are used to authenticate +on-chain transactions, while the Hydra keys are used for multi-signing snapshots +within a Hydra Head. This command will output two Cardano preprod addresses +that must be pre-funded with sufficient tADA to run properly functioning Hydra +nodes. +Use the [Testnets faucet](https://docs.cardano.org/cardano-testnets/tools/faucet/) +to request tADA. + +4. Finally, execute `make run-example` to launch a Hydra Head +with two participants both running the minimal example logic. + +NOTE: Hydra nodes require a fully synchronized Cardano node to operate +correctly. No additional setup actions need to be performed to +configure the Cardano node as it is already included in the Docker +Compose configuration. +However, node synchronization may take several hours on the first run. +Keep in mind that Hydra nodes are configured to run only once +the Cardano node is fully synchronized, +and the `cardano-node` output is suppressed to not +interfere with useful Hydra Head logs. +As a result, there will be no output during synchronization. +Instead, use `docker logs` or run `cardano-cli query tip` +from within the `cardano-node` Docker container +to track the synchronization progress. + +## SDK Core Functionality + +The SDK exposes the following top-level modules which constitus it API which +is considered to be relatvely stable. You also can use `Internal` modules +at yor own risk. + +* `HydraSdk.Process` module provides an interface for spinning up a hydra-node as a Node.js subprocess. -The `HydraSdk.NodeApi` module exports functions for connecting to the Hydra Node +* `HydraSdk.NodeApi` module exports functions for connecting to the Hydra Node WebSocket API and sending HTTP requests. The primary function provided by this module is `mkHydraNodeApiWebSocket`, which establishes a WebSocket connection to the hydra-node, attaches specified handlers for incoming messages, and returns a @@ -35,21 +106,65 @@ Hydra Node API. It also allows to specify retry strategies for Hydra transactions that may be silently dropped by cardano-node, particularly for Close and Contest transactions. -The `HydraSdk.Types` module re-exports various Hydra domain-specific types +* `HydraSdk.Types` module re-exports various Hydra domain-specific types (such as `HydraHeadStatus` and `HydraNodeApi_InMessage`), along with other utility types (e.g., `HostPort` and `Network`) used by the components of this library. -`HydraSdk.Extra.AppManager` provides an opinionated interface for managing -multiple Hydra application instances, with each instance running a separate -hydra-node process, as implemented in [hydra-auction-offchain](https://github.com/mlabs-haskell/hydra-auction-offchain). -For more information, refer to the [AppManager README](src/Extra/README.md). +* Lastly, `HydraSdk.Lib` module contains useful helpers like codecs for many types, +logging action and some others. -### Development Workflows +## SDK Additionals: AppManager -Before executing most of the commands listed below, first enter the Nix -development shell by running `nix develop`. +Under `Extra` folder, modules with additional functionality are located, +currently being the only one for managing multiple app instances. -**Build the project** (requires Nix shell): `make build` -**Format code** (requires Nix shell): `make format` -**Build docs and open them in the browser**: `make docs` +Originally the SDK was designed for Hydra applications, where Hydra Heads were +operated by designated delegates. In that model a delegate can be anyone who wants +to participate as a provider of computational resources to host Hydra +nodes. Delegates must form a group upfront to maintain Hydra +consensus. Upon forming a group, all members need to specify +information about their peers - such as Hydra node addresses, public +keys, etc. That way there exists a strict correspondence between +delegate configurations to start a functioning Hydra Head. + +Each application instance should monitor its underlying Hydra node and +have access to information about other Hydra Head participants. +AppManager is an opinionated interface for managing multiple app +instances within a delegate group. This interface is utilized by +applications like `hydra-auction-offchain`, enabling delegates to host +multiple Layer-2 auctions simultaneously. + +At the core of AppManager is the concept of slots. When delegates +decide to form a group, they agree on the configurations for their +future Hydra nodes. Since each delegate have to specify information +about their peers (hydra-node address, public keys, etc.), there must +be strict correspondence between delegate configurations to start +a working Hydra Head. This is where slots come into play. Each slot +represents a set of delegate configurations sufficient to spin up a +properly configured Hydra Head. In hydra-auction-offchain, slot +numbers are implicitly derived from the configurations provided to +the delegate-server, with the first configuration corresponding to +slot 0, the second to slot 1, and so forth. Users are expected to +reserve slots before making an initial Layer-1 commitment, such as +announcing an auction. Upon reservation, they receive secrets from +each delegate, which can later be provided to host a Layer-2 +application in the reserved slot. + +One clear drawback of this approach is the potential for malicious +actors to reserve all available slots within a delegate group, +effectively paralyzing its operations. In real-world applications, a +flooding detection mechanism should be implemented to prevent this +scenario, although there seems to be no obvious incentive for anyone +to carry out such an attack. + +Coming with the limitations mentioned, this approach simplifies things +since it neither requires communication and synchronization between +delegates during runtime nor does it rely on a central server to +orchestrate the initialization of Hydra Heads, making it a great fit +for hydra-auction-offchain and hopefully other Hydra applications. + +## Applications + +Please refer to [hydra-auction-offchain](https://github.com/mlabs-haskell/hydra-auction-offchain) +for a full-fledged example that utilizes this SDK. \ No newline at end of file diff --git a/flake.nix b/flake.nix index f0d061b..1208505 100644 --- a/flake.nix +++ b/flake.nix @@ -56,6 +56,7 @@ packageLockOnly = true; packages = with pkgs; [ fd + jq hydra.packages.${system}.hydra-node nixpkgs-fmt nodePackages.prettier @@ -76,18 +77,6 @@ } ); - packages = perSystem (system: - let - pkgs = nixpkgsFor system; - project = psProjectFor system pkgs; - in - { - docs = project.buildPursDocs { - packageName = projectName; - }; - } - ); - apps = perSystem (system: let pkgs = nixpkgsFor system; diff --git a/src/Extra/AppManager.purs b/src/Extra/AppManager.purs index 14a955a..a85d403 100644 --- a/src/Extra/AppManager.purs +++ b/src/Extra/AppManager.purs @@ -1,3 +1,7 @@ +--| An opinionated interface for managing multiple Hydra application instances, +--| with each instance running a separate hydra-node process. +--| Please refer to README.md for more details. + module HydraSdk.Extra.AppManager ( module ExportAppManager ) where diff --git a/src/Extra/README.md b/src/Extra/README.md deleted file mode 100644 index 86186d0..0000000 --- a/src/Extra/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# AppManager - -The SDK is designed for Hydra applications, where Hydra Heads are -operated by designated delegates. A delegate can be anyone who wants -to participate as a provider of computational resources to host Hydra -nodes. Delegates must form a group upfront to maintain Hydra -consensus. Upon forming a group, all members need to specify -information about their peers - such as Hydra node addresses, public -keys, etc. That way there exists a strict correspondence between -delegate configurations to start a functioning Hydra Head. - -Each application instance should monitor its underlying Hydra node and -have access to information about other Hydra Head participants. -AppManager is an opinionated interface for managing multiple app -instances within a delegate group. This interface is utilized by -applications like `hydra-auction-offchain`, enabling delegates to host -multiple Layer-2 auctions simultaneously. - -At the core of AppManager is the concept of slots. When delegates -decide to form a group, they agree on the configurations for their -future Hydra nodes. Since each delegate have to specify information -about their peers (hydra-node address, public keys, etc.), there must -be strict correspondence between delegate configurations to start -a working Hydra Head. This is where slots come into play. Each slot -represents a set of delegate configurations sufficient to spin up a -properly configured Hydra Head. In hydra-auction-offchain, slot -numbers are implicitly derived from the configurations provided to -the delegate-server, with the first configuration corresponding to -slot 0, the second to slot 1, and so forth. Users are expected to -reserve slots before making an initial Layer-1 commitment, such as -announcing an auction. Upon reservation, they receive secrets from -each delegate, which can later be provided to host a Layer-2 -application in the reserved slot. - -One clear drawback of this approach is the potential for malicious -actors to reserve all available slots within a delegate group, -effectively paralyzing its operations. In real-world applications, a -flooding detection mechanism should be implemented to prevent this -scenario, although there seems to be no obvious incentive for anyone -to carry out such an attack. - -Coming with the limitations mentioned, this approach simplifies things -since it neither requires communication and synchronization between -delegates during runtime nor does it rely on a central server to -orchestrate the initialization of Hydra Heads, making it a great fit -for hydra-auction-offchain and hopefully other Hydra applications. diff --git a/src/Internal/Extra/AppManager.purs b/src/Internal/Extra/AppManager.purs index 6e31516..a7f9318 100644 --- a/src/Internal/Extra/AppManager.purs +++ b/src/Internal/Extra/AppManager.purs @@ -1,3 +1,5 @@ +-- | This module provides an experimental and opinionated interface +-- | for managing multiple Hydra applications. module HydraSdk.Internal.Extra.AppManager ( ActiveApp , AppManager @@ -36,27 +38,61 @@ import Effect.Aff.Class (class MonadAff, liftAff) import Effect.Class (liftEffect) import Effect.Exception (Error) +-- | The index of the slot within the `AppManager` that can be +-- | reserved / occupied to host a Hydra application with a properly +-- | configured Hydra Head. type AppManagerSlot = Int + +-- | A secret used to authenticate application hosting requests +-- | for a reserved slot. type ReservationCode = UUID +-- | Represents an active application within the `AppManager`, i.e., +-- | a hosted application with an established Hydra Head. +-- | +-- | `state`: The state of the running application. Typically a record +-- | with mutable, thread-safe variables used to track the current +-- | Hydra Head status, UTxO snapshot, and other relevant +-- | information. +-- | +-- | `config`: The configuration of the running application. +-- | +-- | `occupiedSlot`: The number of the slot this application occupies. type ActiveApp appState appConfig = { state :: appState , config :: appConfig , occupiedSlot :: AppManagerSlot } +-- | Represents a reserved slot within the `AppManager`. +-- | +-- | `config`: App configuration corresponding to the reserved slot. +-- | This configuration is used to spin up an application instance +-- | given the correct reservation code is provided. +-- | +-- | `reservationCode`: A secret used to authenticate hosting requests +-- | for this reservation. +-- | +-- | `reservationMonitor`: Fiber of the assigned reservation monitor, +-- | which will remove the reservation and free the slot once +-- | the configured slot reservation period has expired. type ReservedSlot appConfig = { config :: appConfig , reservationCode :: ReservationCode , reservationMonitor :: Fiber Unit } +-- | Hydra application manager. Tracks active application instances +-- | and slots for future hosting requests. type AppManager appId appState appConfigAvailable appConfigActive = { activeApps :: Map appId (ActiveApp appState appConfigActive) , reservedSlots :: Map AppManagerSlot (ReservedSlot appConfigAvailable) , availableSlots :: Map AppManagerSlot appConfigAvailable } +-- | Applies an effectful function to the `AppManager` stored in +-- | an asynchronous variable (AVar) in a safe manner. +-- TODO: Consider using `HydraSdk.Internal.Lib.AVar.modify` instead? withAppManager :: forall m appId appState appConfigAvailable appConfigActive a . MonadAff m diff --git a/src/Internal/Lib/AVar.purs b/src/Internal/Lib/AVar.purs index 952edfc..e4e84ef 100644 --- a/src/Internal/Lib/AVar.purs +++ b/src/Internal/Lib/AVar.purs @@ -1,3 +1,5 @@ +-- | This module provides convenience functionality for working with +-- | asynchronous variables (AVars). module HydraSdk.Internal.Lib.AVar ( modify ) where @@ -9,6 +11,11 @@ import Effect.AVar (AVar) import Effect.Aff.AVar (put, take) as AVar import Effect.Aff.Class (class MonadAff, liftAff) +-- | Applies a monadic function to the value stored in an asynchronous +-- | variable (AVar). If the AVar is empty, it blocks until the +-- | variable is filled. This function is guaranteed to not leave +-- | the AVar empty - it either updates the value or throws an error, +-- | preserving the current value. modify :: forall (m :: Type -> Type) (e :: Type) (a :: Type) . MonadAff m diff --git a/src/Internal/Lib/Codec.purs b/src/Internal/Lib/Codec.purs index 8243c9b..aac0371 100644 --- a/src/Internal/Lib/Codec.purs +++ b/src/Internal/Lib/Codec.purs @@ -1,3 +1,5 @@ +-- | This module provides bidirectional JSON codecs for commonly used +-- | types in the SDK along with useful helper functions. module HydraSdk.Internal.Lib.Codec ( class FromVariantGeneric , class ToVariantGeneric @@ -96,6 +98,11 @@ import Partial.Unsafe (unsafePartial) import Prim.Row (class Cons) as Row import Type.Proxy (Proxy(Proxy)) +-- | Builds a flat sum type codec from the provided sum type codec by +-- | embedding the `"value"` contents during encoding and combining +-- | them under `"value"` during decoding to make it compatible +-- | with the Haskell Aeson format. +-- FIXME: handle all edge cases fixTaggedSumCodec :: forall a. CA.JsonCodec a -> CA.JsonCodec a fixTaggedSumCodec (CA.Codec dec enc) = CA.Codec decFixed encFixed where @@ -123,6 +130,17 @@ fixTaggedSumCodec (CA.Codec dec enc) = CA.Codec decFixed encFixed A.fromObject $ Obj.union valueObj $ Obj.delete "value" obj _, _ -> json +-- | Builds a codec for a generic sum type from the provided `Variant` codec, +-- | where all `Variant` labels must correspond to constructor names. Reduces +-- | boilerplate compared to the standard method for handling sum types +-- | but imposes the aforementioned constraint on field names. +-- | +-- | The sum type will be encoded as a JSON object of the form: +-- | `{ "tag": ", "value": }` +-- | +-- | Applying `fixTaggedSumCodec` to the codec built via `sumGenericCodec` +-- | flattens the JSON representation by moving all `` fields +-- | alongside the `"tag"` field if `` is itself an object. sumGenericCodec :: forall a rep row . Generic a rep @@ -194,6 +212,13 @@ instance Generic.Inl x -> toVariantGeneric x Generic.Inr x -> toVariantGeneric x +-- | Converts `JsonDecodeError` defined in `Data.Codec.Argonaut` to +-- | `JsonDecodeError` defined in `Data.Argonaut.Decode.Error`. +-- | Both types are identical and can be converted without +-- | any loss of information. +-- | +-- | Relevant issue: +-- | https://github.com/purescript-contrib/purescript-argonaut-codecs/issues/83 fromCaJsonDecodeError :: CA.JsonDecodeError -> A.JsonDecodeError fromCaJsonDecodeError = case _ of CA.TypeMismatch type_ -> A.TypeMismatch type_ @@ -203,16 +228,21 @@ fromCaJsonDecodeError = case _ of CA.Named name err -> A.Named name $ fromCaJsonDecodeError err CA.MissingValue -> A.MissingValue +-- | Attempts to decode the given JSON file using the specified codec. caDecodeFile :: forall a. CA.JsonCodec a -> FilePath -> Effect (Either CA.JsonDecodeError a) caDecodeFile codec = map (caDecodeString codec) <<< FSSync.readTextFile Encoding.UTF8 +-- | Attempts to parse a string as JSON and then decode it using +-- | the specified codec. caDecodeString :: forall a. CA.JsonCodec a -> String -> Either CA.JsonDecodeError a caDecodeString codec jsonStr = do json <- lmap (const (CA.TypeMismatch "JSON")) $ A.parseJson jsonStr CA.decode codec json +-- | Converts the provided value into a JSON string using +-- | the specified codec. caEncodeString :: forall a. CA.JsonCodec a -> a -> String caEncodeString codec = A.stringify <<< CA.encode codec @@ -237,6 +267,9 @@ cborBytesCodec = wrapIso CborBytes byteArrayCodec dataHashCodec :: CA.JsonCodec DataHash dataHashCodec = asCborCodec "DataHash" +-- | Bidirectional JSON codec for `DateTime`. Attempts to handle the ambiguity +-- | in timestamps returned by hydra-node, where some may include nanoseconds +-- | while others omit fractional seconds entirely, etc. dateTimeCodec :: CA.JsonCodec DateTime dateTimeCodec = CA.prismaticCodec diff --git a/src/Internal/Lib/Logger.purs b/src/Internal/Lib/Logger.purs index ea924ac..7995983 100644 --- a/src/Internal/Lib/Logger.purs +++ b/src/Internal/Lib/Logger.purs @@ -1,3 +1,4 @@ +-- | This module provides convenience logging functionality. module HydraSdk.Internal.Lib.Logger ( log' ) where @@ -10,6 +11,10 @@ import Data.Log.Level (LogLevel) import Data.Log.Tag (TagSet) import Effect.Class (liftEffect) +-- | Logs a message with the specified log level and tag set. +-- | +-- | Taken without modifications from Control.Monad.Logger.Class, where +-- | this function is not exported. log' :: forall m. MonadLogger m => LogLevel -> TagSet -> String -> m Unit log' level tags message = (log <<< { level, message, tags, timestamp: _ }) diff --git a/src/Internal/Lib/Transaction.purs b/src/Internal/Lib/Transaction.purs index 921fc12..3160600 100644 --- a/src/Internal/Lib/Transaction.purs +++ b/src/Internal/Lib/Transaction.purs @@ -1,3 +1,5 @@ +-- | This module provides convenience functions for working with +-- | `Cardano.Types.Transaction`. module HydraSdk.Internal.Lib.Transaction ( reSignTransaction , setAuxDataHash @@ -15,10 +17,12 @@ import Contract.Transaction (signTransaction) import Data.Lens ((.~)) import Data.Newtype (unwrap) +-- | Computes and sets the transaction auxiliary data hash. setAuxDataHash :: Transaction -> Transaction setAuxDataHash tx = tx # _body <<< _auxiliaryDataHash .~ (hashAuxiliaryData <$> (unwrap tx).auxiliaryData) +-- | Removes existing vkey witnesses and signs the transaction. reSignTransaction :: Transaction -> Contract Transaction reSignTransaction tx = signTransaction (tx # _witnessSet <<< _vkeys .~ mempty) diff --git a/src/Internal/Lib/WebSocket.purs b/src/Internal/Lib/WebSocket.purs index ac5dbdc..841a202 100644 --- a/src/Internal/Lib/WebSocket.purs +++ b/src/Internal/Lib/WebSocket.purs @@ -7,7 +7,7 @@ module HydraSdk.Internal.Lib.WebSocket import Prelude -import Contract.Log (logTrace') +import Contract.Log (logError', logTrace') import Control.Monad.Logger.Class (class MonadLogger) import Ctl.Internal.JsWebSocket ( JsWebSocket @@ -61,10 +61,11 @@ mkWebSocket builder = \callback -> _onWsMessage ws wsLogger \msgRaw -> builder.runM do - logTrace' $ "onMessage raw: " <> msgRaw + logTrace' $ "mkWebSocket:onMessage: Raw message: " <> msgRaw case caDecodeString builder.inMsgCodec msgRaw of Left decodeErr -> do - logTrace' $ "onMessage decode error: " <> CA.printJsonDecodeError decodeErr + logError' $ "mkWebSocket:onMessage: Decode error: " <> + CA.printJsonDecodeError decodeErr callback $ Left msgRaw Right msg -> callback $ Right msg diff --git a/src/Internal/NodeApi/Http.purs b/src/Internal/NodeApi/Http.purs index 7608834..bdb8ee3 100644 --- a/src/Internal/NodeApi/Http.purs +++ b/src/Internal/NodeApi/Http.purs @@ -1,3 +1,5 @@ +-- | This module provides functions for making requests +-- | to hydra-node HTTP endpoints. module HydraSdk.Internal.NodeApi.Http ( commitRequest ) where @@ -20,6 +22,19 @@ import HydraSdk.Internal.Types.CommitRequest import HydraSdk.Internal.Types.Tx (HydraTx, hydraTxCodec) import HydraSdk.Internal.Types.UtxoMap (hydraUtxoMapCodec) +-- | Builds a Hydra Commit transaction ready for submission +-- | to the L1 network. +-- | +-- | Sends a request to the hydra-node API HTTP server as specified +-- | by the first argument. The second argument is used to pass +-- | the request body, which can either correspond to a simple request +-- | consisting of plain utxos or a full commit request with +-- | a blueprint transaction to be complemented by the hydra-node. +-- | The later type allows building Commit transactions of arbitrary +-- | complexity. +-- | +-- | For details on the motivation behind blueprint transactions, see this discussion: +-- | https://github.com/cardano-scaling/hydra/discussions/1337 commitRequest :: ServerConfig -> HydraCommitRequest -> Aff (Either HttpError HydraTx) commitRequest serverConfig req = handleResponse hydraTxCodec <$> diff --git a/src/Internal/NodeApi/WebSocket.purs b/src/Internal/NodeApi/WebSocket.purs index 93e45e1..4025f7f 100644 --- a/src/Internal/NodeApi/WebSocket.purs +++ b/src/Internal/NodeApi/WebSocket.purs @@ -1,3 +1,5 @@ +-- | This module provides an interface for establishing a connection +-- | and interacting with the hydra-node WebSocket API. module HydraSdk.Internal.NodeApi.WebSocket ( HydraNodeApiHandlers , HydraNodeApiWebSocket @@ -28,6 +30,8 @@ import HydraSdk.Internal.Types.NodeApiMessage ) import HydraSdk.Internal.Types.Tx (mkHydraTx) +-- | A record with operations that can be executed by the client to interact +-- | with the hydra-node WebSocket API. type HydraNodeApiWebSocket (m :: Type -> Type) = { baseWs :: WebSocket m HydraNodeApi_InMessage HydraNodeApi_OutMessage , initHead :: Effect Unit @@ -38,29 +42,37 @@ type HydraNodeApiWebSocket (m :: Type -> Type) = , fanout :: Effect Unit } +-- | Handlers to attach to the hydra-node API WebSocket connection. +-- | +-- | `messageHandler`: Attempts to decode incoming messages to +-- | `HydraNodeApi_InMessage`. On decoding failure, logs the error and passes +-- | the raw message instead. type HydraNodeApiHandlers (m :: Type -> Type) = { connectHandler :: HydraNodeApiWebSocket m -> m Unit , errorHandler :: HydraNodeApiWebSocket m -> String -> m Unit , messageHandler :: HydraNodeApiWebSocket m -> Either String HydraNodeApi_InMessage -> m Unit - -- ^ Attempts to decode incoming messages to `HydraNodeApi_InMessage`. - -- On decoding failure, logs the error and passes the raw message instead. } +-- | Configuration parameters for the hydra-node API WebSocket. +-- | +-- | `url`: Address of the hydra-node API WebSocket. +-- | +-- | `runM`: Since the handlers of the underlying raw WebSocket are executed in +-- | the `Effect` monad, this function allows running client monad +-- | computations within that context. +-- | +-- | `handlers`: Handlers to attach to the established WebSocket connection. +-- | +-- | `txRetryStrategies`: Retry strategies for transactions that may be silently +-- | dropped by the cardano-node due to limitations of the hydra-node. type HydraNodeApiWebSocketBuilder (m :: Type -> Type) = { url :: Url - -- ^ Address of the hydra-node API WebSocket. , runM :: m Unit -> Effect Unit - -- ^ Since the handlers of the underlying raw WebSocket are executed in the - -- `Effect` monad, this function allows running client monad computations - -- within that context. , handlers :: HydraNodeApiHandlers m - -- ^ Handlers to attach to the established WebSocket connection. , txRetryStrategies :: { close :: HydraTxRetryStrategy m , contest :: HydraTxRetryStrategy m } - -- ^ Retry strategies for transactions that may be silently dropped by the - -- cardano-node due to limitations of the hydra-node. } -- | Retry strategy to apply when submitting a Hydra transaction. @@ -79,6 +91,9 @@ data HydraTxRetryStrategy (m :: Type -> Type) } | DontRetryTx +-- | A default success predicate for the retry strategy for Close transaction. +-- | Checks whether the Hydra Head has been successfully closed by inspecting +-- | the current Head status. defaultCloseHeadSuccessPredicate :: forall (m :: Type -> Type) . Functor m diff --git a/src/Internal/Process/HydraNode.purs b/src/Internal/Process/HydraNode.purs index 6f9b2dc..f9edbae 100644 --- a/src/Internal/Process/HydraNode.purs +++ b/src/Internal/Process/HydraNode.purs @@ -1,3 +1,5 @@ +-- | This module provides an interface for spinning up a hydra-node +-- | as a Node.js child process. module HydraSdk.Internal.Process.HydraNode ( HydraHeadPeer , HydraNodeHandlers @@ -40,6 +42,7 @@ import Node.Encoding (Encoding(UTF8)) as Encoding import Node.Path (FilePath) import Node.Stream (onDataString) +-- | Parameters to be passed to the hydra-node child process on startup. type HydraNodeStartupParams = { nodeId :: String , hydraNodeAddress :: HostPort @@ -55,6 +58,7 @@ type HydraNodeStartupParams = , peers :: Array HydraHeadPeer } +-- | Bidirectional JSON codec for `HydraNodeStartupParams`. hydraNodeStartupParamsCodec :: CA.JsonCodec HydraNodeStartupParams hydraNodeStartupParamsCodec = CA.object "HydraNodeStartupParams" $ CAR.record @@ -72,12 +76,16 @@ hydraNodeStartupParamsCodec = , peers: CA.array hydraHeadPeerCodec } +-- | Configuration parameters for a single Hydra Head peer. When setting up a +-- | Hydra Head, each node must specify the network addresses and public key +-- | information of its respective peers. type HydraHeadPeer = { hydraNodeAddress :: HostPort , hydraVerificationKey :: FilePath , cardanoVerificationKey :: FilePath } +-- | Bi-directional JSON codec for `HydraHeadPeer`. hydraHeadPeerCodec :: CA.JsonCodec HydraHeadPeer hydraHeadPeerCodec = CA.object "HydraHeadPeer" $ CAR.record @@ -86,12 +94,15 @@ hydraHeadPeerCodec = , cardanoVerificationKey: CA.string } +-- | Optional handlers to attach to the newly spawned hydra-node child process. type HydraNodeHandlers = { apiServerStartedHandler :: Maybe (Effect Unit) , stdoutHandler :: Maybe (String -> Effect Unit) , stderrHandler :: Maybe (String -> Effect Unit) } +-- | Record with no-op handlers, useful for specifying individual handlers +-- | with minimal code. noopHydraNodeHandlers :: HydraNodeHandlers noopHydraNodeHandlers = { apiServerStartedHandler: Nothing diff --git a/src/Internal/Types/HeadStatus.purs b/src/Internal/Types/HeadStatus.purs index 0894610..b113a77 100644 --- a/src/Internal/Types/HeadStatus.purs +++ b/src/Internal/Types/HeadStatus.purs @@ -31,6 +31,8 @@ data HydraHeadStatus | HeadStatus_FanoutPossible | HeadStatus_Final +-- | Checks if the Hydra Head has been closed by inspecting +-- | its current status. isHeadClosed :: HydraHeadStatus -> Boolean isHeadClosed status = (status == HeadStatus_Closed) diff --git a/src/Internal/Types/NodeApiMessage.purs b/src/Internal/Types/NodeApiMessage.purs index 994032c..3a80cf8 100644 --- a/src/Internal/Types/NodeApiMessage.purs +++ b/src/Internal/Types/NodeApiMessage.purs @@ -72,6 +72,7 @@ import HydraSdk.Internal.Types.UtxoMap (HydraUtxoMap, hydraUtxoMapCodec) ---------------------------------------------------------------------- -- Incoming messages +-- | Represents incoming messages from the hydra-node API WebSocket server. -- TODO: add missing variants: PostTxOnChainFailed, CommandFailed, IgnoredHeadInitializing data HydraNodeApi_InMessage = Greetings GreetingsMessage @@ -471,6 +472,8 @@ invalidInputMessageCodec = ---------------------------------------------------------------------- -- Outcoming messages +-- | Represents messages that can be sent to the hydra-node API +-- | WebSocket server. data HydraNodeApi_OutMessage = Init | Abort diff --git a/src/Lib.purs b/src/Lib.purs index 51321c7..3c174ac 100644 --- a/src/Lib.purs +++ b/src/Lib.purs @@ -1,3 +1,4 @@ +-- | Useful stuff for building an application with HydraSdk. module HydraSdk.Lib ( module ExportAVar , module ExportCodec diff --git a/src/NodeApi.purs b/src/NodeApi.purs index 2a7b5a2..6b9b427 100644 --- a/src/NodeApi.purs +++ b/src/NodeApi.purs @@ -1,3 +1,4 @@ +-- | API to comminicate with the Hydra Node via HTTP/WebSockets. module HydraSdk.NodeApi ( module ExportHttp , module ExportWebSocket diff --git a/src/Process.purs b/src/Process.purs index f81321b..5e26f3f 100644 --- a/src/Process.purs +++ b/src/Process.purs @@ -1,3 +1,4 @@ +-- | API for spinning up a hydra-node in a Node.js' subprocess. module HydraSdk.Process ( module ExportHydraNode ) where diff --git a/src/Types.purs b/src/Types.purs index 6bd78fc..fe67859 100644 --- a/src/Types.purs +++ b/src/Types.purs @@ -1,3 +1,6 @@ +--| Re-exports various Hydra domain-specific types +--| (such as `HydraHeadStatus` and `HydraNodeApi_InMessage`), +--| along with other utility types (e.g., `HostPort` and `Network`). module HydraSdk.Types ( module ExportArgonautJson , module ExportCommitRequest