Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve refdocs, Add a getting-started guide, Restore searchable docs #4

Open
wants to merge 12 commits into
base: dshuiski/minimal-example
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/example/minimal/output/
/example/minimal/.psa-stash
/example/minimal/.spago/
/generated-docs/
/node_modules
/output/
/result
Expand Down
38 changes: 26 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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)" ] || \
Expand All @@ -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}

Expand All @@ -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
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,73 @@ applications.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Compatibility](#compatibility)
- [Applications](#applications)
- [Preliminaries](#preliminaries)
- [Getting Started](#getting-started)
- [Functionality](#functionality)
- [Development Workflows](#development-workflows)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

### Compatibility

| hydra-sdk | hydra-node | cardano-node |
| ----------- | ------------ | ------------ |
| **`0.1.0`** | **`0.19.0`** | **`10.1.2`** |

### Applications

Please refer to [hydra-auction-offchain](https://github.com/mlabs-haskell/hydra-auction-offchain)
for a full-fledged example that utilizes this SDK.

### 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.

### Getting Started

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. 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.

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.

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 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 it may take several hours on the first
run to synchronize the node. 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.

### Functionality

The `HydraSdk.Process` module provides an interface for spinning up a hydra-node
Expand Down
13 changes: 1 addition & 12 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
packageLockOnly = true;
packages = with pkgs; [
fd
jq
hydra.packages.${system}.hydra-node
nixpkgs-fmt
nodePackages.prettier
Expand All @@ -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;
Expand Down
36 changes: 36 additions & 0 deletions src/Internal/Extra/AppManager.purs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
-- | This module provides an experimental and opinionated interface
-- | for managing multiple Hydra applications.
module HydraSdk.Internal.Extra.AppManager
( ActiveApp
, AppManager
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/Internal/Lib/AVar.purs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
-- | This module provides convenience functionality for working with
-- | asynchronous variables (AVars).
module HydraSdk.Internal.Lib.AVar
( modify
) where
Expand All @@ -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
Expand Down
33 changes: 33 additions & 0 deletions src/Internal/Lib/Codec.purs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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": <constructor name>", "value": <value> }`
-- |
-- | Applying `fixTaggedSumCodec` to the codec built via `sumGenericCodec`
-- | flattens the JSON representation by moving all `<value>` fields
-- | alongside the `"tag"` field if `<value>` is itself an object.
sumGenericCodec
:: forall a rep row
. Generic a rep
Expand Down Expand Up @@ -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_
Expand All @@ -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

Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/Internal/Lib/Logger.purs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
-- | This module provides convenience logging functionality.
module HydraSdk.Internal.Lib.Logger
( log'
) where
Expand All @@ -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: _ })
Expand Down
4 changes: 4 additions & 0 deletions src/Internal/Lib/Transaction.purs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
-- | This module provides convenience functions for working with
-- | `Cardano.Types.Transaction`.
module HydraSdk.Internal.Lib.Transaction
( reSignTransaction
, setAuxDataHash
Expand All @@ -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.
Copy link
Member

@euonymos euonymos Dec 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having an explanation of why this is needed would be nice.

reSignTransaction :: Transaction -> Contract Transaction
reSignTransaction tx = signTransaction (tx # _witnessSet <<< _vkeys .~ mempty)
7 changes: 4 additions & 3 deletions src/Internal/Lib/WebSocket.purs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading