Skip to content

Commit 81cff66

Browse files
committed
Add minimal example
1 parent fde4449 commit 81cff66

File tree

16 files changed

+7627
-1
lines changed

16 files changed

+7627
-1
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
/example/minimal/docker/cluster/keys/*.sk
2+
/example/minimal/docker/cluster/keys/*.vk
3+
/example/minimal/output/
4+
/example/minimal/.psa-stash
5+
/example/minimal/.spago/
16
/node_modules
27
/output/
38
/result

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: build, format, repl, docs
1+
.PHONY: build, format, repl, docs, build-example, run-example
22

33
ps-sources := $(shell fd --no-ignore-parent -epurs)
44
nix-sources := $(shell fd --no-ignore-parent -enix --exclude='spago*')
@@ -31,3 +31,10 @@ repl: requires-nix-shell
3131
docs:
3232
nix build .#docs
3333
${open-in-browser} result/generated-docs/html/index.html
34+
35+
build-example:
36+
cd example/minimal && \
37+
spago build --purs-args ${purs-args}
38+
39+
run-example:
40+
docker compose -f example/minimal/docker/cluster/docker-compose.yaml up --build --no-attach cardano-node

example/minimal/app/Main.purs

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
module HydraSdk.Example.Minimal.Main
2+
( main
3+
) where
4+
5+
import Prelude
6+
7+
import Cardano.AsCbor (encodeCbor)
8+
import Contract.CborBytes (cborBytesToHex)
9+
import Contract.Log (logError', logInfo', logTrace', logWarn')
10+
import Contract.Monad (ContractEnv, stopContractEnv)
11+
import Contract.Transaction (submit)
12+
import Control.Monad.Error.Class (throwError)
13+
import Control.Monad.Reader (ask)
14+
import Data.Argonaut (stringifyWithIndent)
15+
import Data.Codec.Argonaut (encode) as CA
16+
import Data.Either (Either(Left, Right))
17+
import Data.Log.Level (LogLevel(Info, Error))
18+
import Data.Map (empty, fromFoldable) as Map
19+
import Data.Maybe (Maybe(Just, Nothing))
20+
import Data.Posix.Signal (Signal(SIGINT, SIGTERM))
21+
import Data.Traversable (traverse_)
22+
import Data.UInt (fromInt) as UInt
23+
import Effect (Effect)
24+
import Effect.Aff (Aff, launchAff_, runAff_)
25+
import Effect.Aff.Class (liftAff)
26+
import Effect.Class (liftEffect)
27+
import Effect.Exception (error, message)
28+
import Effect.Ref (Ref)
29+
import Effect.Ref (new, read, write) as Ref
30+
import HydraSdk.Example.Minimal.App
31+
( AppLogger
32+
, AppM
33+
, AppState
34+
, appLogger
35+
, initApp
36+
, readHeadStatus
37+
, runAppEff
38+
, runContractInApp
39+
)
40+
import HydraSdk.Example.Minimal.App (setHeadStatus, setUtxoSnapshot) as App
41+
import HydraSdk.Example.Minimal.Config (configFromArgv)
42+
import HydraSdk.Lib (log')
43+
import HydraSdk.NodeApi
44+
( HydraNodeApiWebSocket
45+
, HydraTxRetryStrategy(RetryTxWithParams, DontRetryTx)
46+
, commitRequest
47+
, mkHydraNodeApiWebSocket
48+
)
49+
import HydraSdk.Process (spawnHydraNode)
50+
import HydraSdk.Types
51+
( HydraHeadStatus
52+
( HeadStatus_Idle
53+
, HeadStatus_Initializing
54+
, HeadStatus_Open
55+
, HeadStatus_Closed
56+
)
57+
, HydraNodeApi_InMessage(Greetings, HeadIsInitializing, HeadIsOpen)
58+
, HydraSnapshot(HydraSnapshot)
59+
, hydraSnapshotCodec
60+
, mkSimpleCommitRequest
61+
, printHeadStatus
62+
, printHost
63+
, printHostPort
64+
, toUtxoMap
65+
)
66+
import Node.ChildProcess (ChildProcess, kill)
67+
import Node.Process (onSignal, onUncaughtException)
68+
import URI.Port (toInt) as Port
69+
70+
type AppHandle =
71+
{ cleanupHandler :: Effect Unit
72+
, hydraNodeProcess :: ChildProcess
73+
}
74+
75+
main :: Effect Unit
76+
main =
77+
launchAff_ do
78+
config <- liftEffect configFromArgv
79+
appState <- initApp config
80+
let logger = appLogger
81+
appHandle <- startDelegateServer appState logger
82+
liftEffect do
83+
onUncaughtException \err -> do
84+
runAppEff appState logger $ logError' $ "UNCAUGHT EXCEPTION: " <> message err
85+
appHandle.cleanupHandler
86+
onSignal SIGINT appHandle.cleanupHandler
87+
onSignal SIGTERM appHandle.cleanupHandler
88+
89+
startDelegateServer :: AppState -> AppLogger -> Aff AppHandle
90+
startDelegateServer state logger = do
91+
hydraNodeApiWsRef <- liftEffect $ Ref.new Nothing
92+
hydraNodeProcess <- spawnHydraNode state.config.hydraNodeStartupParams
93+
{ apiServerStartedHandler:
94+
Just $ appEff do
95+
let
96+
wsUrl = "ws://" <> printHostPort
97+
state.config.hydraNodeStartupParams.hydraNodeApiAddress
98+
hydraNodeApiWs <- mkHydraNodeApiWebSocket
99+
{ url: wsUrl
100+
, runM: appEff
101+
, handlers:
102+
{ connectHandler: const (pure unit)
103+
, messageHandler: \ws -> messageHandler ws
104+
, errorHandler: \_ws err ->
105+
logError' $ "hydra-node API WebSocket error: " <> show err
106+
}
107+
, txRetryStrategies:
108+
{ close:
109+
RetryTxWithParams
110+
{ delaySec: 90
111+
, maxRetries: top
112+
, successPredicate: (_ >= HeadStatus_Closed) <$> readHeadStatus
113+
, failHandler: pure unit
114+
}
115+
, contest: DontRetryTx
116+
}
117+
}
118+
liftEffect $ Ref.write (Just hydraNodeApiWs) hydraNodeApiWsRef
119+
, stdoutHandler:
120+
Just (appEff <<< logTrace' <<< append "[hydra-node:stdout] ")
121+
, stderrHandler:
122+
Just (appEff <<< logWarn' <<< append "[hydra-node:stderr] ")
123+
}
124+
pure
125+
{ cleanupHandler: cleanupHandler (\logLevel -> appEff <<< log' logLevel Map.empty)
126+
{ hydraNodeProcess
127+
, hydraNodeApiWsRef
128+
, contractEnv: state.contractEnv
129+
}
130+
, hydraNodeProcess
131+
}
132+
where
133+
appEff :: forall a. AppM a -> Effect Unit
134+
appEff = runAppEff state logger
135+
136+
messageHandler
137+
:: HydraNodeApiWebSocket AppM
138+
-> Either String HydraNodeApi_InMessage
139+
-> AppM Unit
140+
messageHandler ws =
141+
case _ of
142+
Left _rawMessage -> pure unit
143+
Right message ->
144+
case message of
145+
Greetings { headStatus } -> do
146+
setHeadStatus headStatus
147+
when (headStatus == HeadStatus_Idle) $ liftEffect ws.initHead
148+
HeadIsInitializing _ -> do
149+
setHeadStatus HeadStatus_Initializing
150+
{ commitUtxo, config: { hydraNodeStartupParams: { hydraNodeApiAddress } } } <- ask
151+
let
152+
payload = mkSimpleCommitRequest $ Map.fromFoldable [ commitUtxo ]
153+
serverConfig =
154+
{ port: UInt.fromInt $ Port.toInt hydraNodeApiAddress.port
155+
, host: printHost hydraNodeApiAddress
156+
, secure: false
157+
, path: Nothing
158+
}
159+
liftAff (commitRequest serverConfig payload) >>= case _ of
160+
Left httpErr ->
161+
throwError $ error $ "Commit request failed with error: "
162+
<> show httpErr
163+
Right { cborHex: commitTx } -> do
164+
txHash <- runContractInApp $ submit commitTx
165+
logInfo' $ "Submitted Commit transaction: " <> cborBytesToHex
166+
(encodeCbor txHash)
167+
HeadIsOpen { headId, utxo } -> do
168+
setHeadStatus HeadStatus_Open
169+
logInfo' $ "Head ID: " <> cborBytesToHex (encodeCbor headId)
170+
setUtxoSnapshot $ HydraSnapshot
171+
{ snapshotNumber: zero
172+
, utxo
173+
}
174+
_ -> pure unit
175+
176+
setHeadStatus :: HydraHeadStatus -> AppM Unit
177+
setHeadStatus status = do
178+
App.setHeadStatus status
179+
logInfo' $ "New Head status: " <> printHeadStatus status
180+
181+
setUtxoSnapshot :: HydraSnapshot -> AppM Unit
182+
setUtxoSnapshot snapshot = do
183+
App.setUtxoSnapshot snapshot
184+
let snapshotFormatted = stringifyWithIndent 2 $ CA.encode hydraSnapshotCodec snapshot
185+
logInfo' $ "New confirmed snapshot: " <> snapshotFormatted
186+
187+
cleanupHandler
188+
:: forall (m :: Type -> Type)
189+
. (LogLevel -> String -> Effect Unit)
190+
-> { hydraNodeProcess :: ChildProcess
191+
, hydraNodeApiWsRef :: Ref (Maybe (HydraNodeApiWebSocket m))
192+
, contractEnv :: ContractEnv
193+
}
194+
-> Effect Unit
195+
cleanupHandler logger { hydraNodeProcess, hydraNodeApiWsRef, contractEnv } = do
196+
logger Info "Killing hydra-node."
197+
kill SIGINT hydraNodeProcess
198+
logger Info "Closing hydra-node API WebSocket connection."
199+
Ref.read hydraNodeApiWsRef >>= traverse_ _.baseWs.close
200+
logger Info "Finalizing CTL Contract environment."
201+
runAff_
202+
( case _ of
203+
Left err ->
204+
logger Error $ "stopContractEnv failed with error: "
205+
<> message err
206+
Right _ ->
207+
logger Info "Successfully completed all cleanup actions -> exiting."
208+
)
209+
(stopContractEnv contractEnv)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"hydraNodeStartupParams": {
3+
"nodeId": "1063b00f-0d5b-46b6-9aff-88114a25f688",
4+
"hydraNodeAddress": "0.0.0.0:7000",
5+
"hydraNodeApiAddress": "127.0.0.1:7001",
6+
"persistDir": "hydra-persist",
7+
"hydraSigningKey": "keys/hydra-a.sk",
8+
"cardanoSigningKey": "keys/cardano-a.sk",
9+
"network": {
10+
"tag": "testnet",
11+
"magic": 1
12+
},
13+
"nodeSocket": "node-ipc/node.socket",
14+
"pparams": "protocol-parameters.json",
15+
"hydraScriptsTxHash": "03f8deb122fbbd98af8eb58ef56feda37728ec957d39586b78198a0cf624412a",
16+
"contestPeriodSec": 60,
17+
"peers": [
18+
{
19+
"hydraNodeAddress": "delegate-node-b:7002",
20+
"hydraVerificationKey": "keys/hydra-b.vk",
21+
"cardanoVerificationKey": "keys/cardano-b.vk"
22+
}
23+
]
24+
},
25+
"blockfrostApiKey": null,
26+
"logLevel": "trace",
27+
"commitOutRef": null
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"hydraNodeStartupParams": {
3+
"nodeId": "91fd9af9-1eb8-42e7-8cb3-20bca7a4f13b",
4+
"hydraNodeAddress": "0.0.0.0:7002",
5+
"hydraNodeApiAddress": "127.0.0.1:7003",
6+
"persistDir": "hydra-persist",
7+
"hydraSigningKey": "keys/hydra-b.sk",
8+
"cardanoSigningKey": "keys/cardano-b.sk",
9+
"network": {
10+
"tag": "testnet",
11+
"magic": 1
12+
},
13+
"nodeSocket": "node-ipc/node.socket",
14+
"pparams": "protocol-parameters.json",
15+
"hydraScriptsTxHash": "03f8deb122fbbd98af8eb58ef56feda37728ec957d39586b78198a0cf624412a",
16+
"contestPeriodSec": 60,
17+
"peers": [
18+
{
19+
"hydraNodeAddress": "delegate-node-a:7000",
20+
"hydraVerificationKey": "keys/hydra-a.vk",
21+
"cardanoVerificationKey": "keys/cardano-a.vk"
22+
}
23+
]
24+
},
25+
"blockfrostApiKey": null,
26+
"logLevel": "trace",
27+
"commitOutRef": null
28+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
services:
2+
cardano-node:
3+
image: ghcr.io/intersectmbo/cardano-node:9.2.0
4+
environment:
5+
- NETWORK=preprod
6+
volumes:
7+
- node-db:/data/db
8+
- node-ipc:/ipc
9+
restart: on-failure
10+
healthcheck:
11+
test: ["CMD-SHELL", "[ \"$(cardano-cli query tip --testnet-magic=1 --socket-path=/ipc/node.socket | tail -c 9 | head -c 5)\" == \"100.0\" ]"]
12+
interval: 10s
13+
retries: 8640
14+
logging:
15+
driver: "json-file"
16+
options:
17+
max-size: "200k"
18+
max-file: "10"
19+
20+
delegate-node-a:
21+
build:
22+
context: "../.."
23+
dockerfile: "docker/node/Dockerfile"
24+
depends_on:
25+
cardano-node:
26+
condition: service_healthy
27+
command: "config.json"
28+
volumes:
29+
- type: "bind"
30+
source: "./config-a.json"
31+
target: "/app/config.json"
32+
- type: "bind"
33+
source: "keys"
34+
target: "/app/keys"
35+
- type: "volume"
36+
source: "node-ipc"
37+
target: "/app/node-ipc"
38+
- type: "volume"
39+
source: "hydra-persist-a"
40+
target: "/app/hydra-persist"
41+
42+
delegate-node-b:
43+
build:
44+
context: "../.."
45+
dockerfile: "docker/node/Dockerfile"
46+
depends_on:
47+
cardano-node:
48+
condition: service_healthy
49+
command: "config.json"
50+
volumes:
51+
- type: "bind"
52+
source: "./config-b.json"
53+
target: "/app/config.json"
54+
- type: "bind"
55+
source: "keys"
56+
target: "/app/keys"
57+
- type: "volume"
58+
source: "node-ipc"
59+
target: "/app/node-ipc"
60+
- type: "volume"
61+
source: "hydra-persist-b"
62+
target: "/app/hydra-persist"
63+
64+
volumes:
65+
hydra-persist-a:
66+
hydra-persist-b:
67+
node-db:
68+
node-ipc:

example/minimal/docker/cluster/keys/.gitkeep

Whitespace-only changes.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# syntax=docker/dockerfile:1
2+
3+
FROM node:18
4+
WORKDIR /app
5+
6+
COPY package.json package-lock.json .
7+
RUN npm clean-install --production=false --loglevel=verbose
8+
RUN npm install purescript@0.15.8
9+
RUN npm install spago@0.21.0
10+
11+
COPY packages.dhall spago.dhall .
12+
RUN npx --no-install spago install
13+
14+
COPY src src
15+
COPY app app
16+
COPY protocol-parameters.json .
17+
RUN npx --no-install spago build
18+
19+
RUN curl -LO https://github.com/input-output-hk/hydra/releases/download/0.19.0/hydra-x86_64-linux-0.19.0.zip
20+
RUN unzip -d /usr/local/bin/ hydra-x86_64-linux-0.19.0.zip
21+
RUN chmod +x /usr/local/bin/hydra-node
22+
23+
ENTRYPOINT ["npx", "--no-install", "spago", "-q", "run", "--main", "HydraSdk.Example.Minimal.Main", "--exec-args"]
24+
CMD ["--help"]

0 commit comments

Comments
 (0)