From 47e7edd93d66cd95349b787733be9588dc78339d Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Tue, 22 Sep 2026 10:32:42 +0200 Subject: [PATCH 1/3] poc: evaluate go-daml for Ledger API connectivity probes Adds an isolated experimental package that dials a participant with noders-team/go-daml (GetLedgerApiVersion + GetLedgerEnd) so we can compare it against the existing dazl-client path without changing production WaitForHealthy or token dial wiring. --- go.mod | 14 ++- go.sum | 96 +++++++++++++++++-- internal/poc/godamlprobe/doc.go | 11 +++ internal/poc/godamlprobe/probe.go | 78 +++++++++++++++ .../poc/godamlprobe/probe_integration_test.go | 64 +++++++++++++ internal/poc/godamlprobe/probe_test.go | 41 ++++++++ 6 files changed, 289 insertions(+), 15 deletions(-) create mode 100644 internal/poc/godamlprobe/doc.go create mode 100644 internal/poc/godamlprobe/probe.go create mode 100644 internal/poc/godamlprobe/probe_integration_test.go create mode 100644 internal/poc/godamlprobe/probe_test.go diff --git a/go.mod b/go.mod index 46648fc3..4308f67f 100644 --- a/go.mod +++ b/go.mod @@ -8,11 +8,12 @@ require ( github.com/digital-asset/dazl-client/v8 v8.9.0 github.com/mattn/go-isatty v0.0.22 github.com/muesli/termenv v0.16.0 + github.com/noders-team/go-daml v0.7.2 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.9 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/sys v0.45.0 - google.golang.org/grpc v1.81.1 + golang.org/x/sys v0.46.0 + google.golang.org/grpc v1.82.1 google.golang.org/protobuf v1.36.11 ) @@ -28,13 +29,16 @@ require ( github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/rs/zerolog v1.34.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/net v0.55.0 // indirect - golang.org/x/text v0.37.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/text v0.38.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect ) diff --git a/go.sum b/go.sum index bc450862..eb51d5c5 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,15 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= @@ -20,44 +30,103 @@ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfa github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= +github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/digital-asset/dazl-client/v8 v8.9.0 h1:F2qTUWtHAjhGyRGV+xTim+VAFwM99FpcOx4+wowvPnY= github.com/digital-asset/dazl-client/v8 v8.9.0/go.mod h1:q1KevCJ8FpH8je2MnnjN8/QUfhstB4fKpyKyqDtqFh0= +github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI= +github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= +github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= +github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/noders-team/go-daml v0.7.2 h1:6pLWLumh8LcX9rKhEsMBtHZVtqNIaXq9q2fEB3JUJAI= +github.com/noders-team/go-daml v0.7.2/go.mod h1:aFnPCwm5MVTDZFC9gN8dPsU8tpsQM/VbwXvG2VbJyqk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/runc v1.3.6 h1:SLGIymCtsk80iNPWgbc8dtjI30r+5mTVV+4dN8/17Sk= +github.com/opencontainers/runc v1.3.6/go.mod h1:o1wyv76EDlTkcf0KTFgN8bMWLPvgF/HfX709lDv+rr4= +github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw= +github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= @@ -76,20 +145,27 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/poc/godamlprobe/doc.go b/internal/poc/godamlprobe/doc.go new file mode 100644 index 00000000..7232425e --- /dev/null +++ b/internal/poc/godamlprobe/doc.go @@ -0,0 +1,11 @@ +// Package godamlprobe is an experimental PoC that dials a Canton participant +// Ledger API endpoint using github.com/noders-team/go-daml and reports whether +// the participant accepts the call (connectivity + auth). +// +// This package is intentionally NOT wired into production readiness paths +// (ComposeRunner.WaitForHealthy, localnet doctor, or the Web UI reconciler). +// Production ledger dials continue to use internal/canton/ledger (dazl-client). +// +// The PoC exists to evaluate whether go-daml is a viable alternative client +// for a cheap unary probe (GetLedgerApiVersion + GetLedgerEnd) against LocalNet. +package godamlprobe diff --git a/internal/poc/godamlprobe/probe.go b/internal/poc/godamlprobe/probe.go new file mode 100644 index 00000000..d1a796e7 --- /dev/null +++ b/internal/poc/godamlprobe/probe.go @@ -0,0 +1,78 @@ +package godamlprobe + +import ( + "context" + "fmt" + "strings" + + "github.com/noders-team/go-daml/pkg/auth" + godamlclient "github.com/noders-team/go-daml/pkg/client" + "github.com/noders-team/go-daml/pkg/model" +) + +// Options configures a go-daml connectivity probe against a participant +// Ledger API gRPC endpoint. +type Options struct { + // Endpoint is host:port, e.g. "localhost:5001". Required. + Endpoint string + + // Token is a bearer JWT the participant accepts. Empty is allowed only + // for participants configured without auth (rare outside tests). + Token string +} + +// Result is what a successful probe observed. Offset is the ledger end; +// LedgerAPIVersion is the participant-reported API version string. +type Result struct { + Endpoint string + LedgerAPIVersion string + Offset int64 +} + +// Probe dials the participant with go-daml and runs two cheap unaries: +// GetLedgerApiVersion (go-daml's Ping) and GetLedgerEnd. Success means the +// endpoint is reachable and the token (if any) was accepted. +// +// LocalNet participants speak plaintext gRPC; go-daml defaults to insecure +// transport when no TLS config is set, which matches that layout. +func Probe(ctx context.Context, opts Options) (Result, error) { + endpoint := strings.TrimSpace(opts.Endpoint) + if endpoint == "" { + return Result{}, fmt.Errorf("godamlprobe.Probe: endpoint is required") + } + + cl, err := godamlclient.NewDamlClient(endpoint, auth.NewBearerTokenProvider(opts.Token)). + Build(ctx) + if err != nil { + return Result{}, fmt.Errorf("godamlprobe.Probe: dial %s: %w", endpoint, err) + } + defer cl.Close() + + // GetLedgerAPIVersion is go-daml's Ping implementation — one unary + // that proves dial + auth + stub wiring. GetLedgerEnd is the same + // readiness probe internal/canton/ledger documents for dazl-client. + ver, err := cl.VersionService.GetLedgerAPIVersion(ctx, &model.GetLedgerAPIVersionRequest{}) + if err != nil { + return Result{}, fmt.Errorf("godamlprobe.Probe: GetLedgerAPIVersion %s: %w", endpoint, err) + } + + end, err := cl.StateService.GetLedgerEnd(ctx, &model.GetLedgerEndRequest{}) + if err != nil { + return Result{}, fmt.Errorf("godamlprobe.Probe: GetLedgerEnd %s: %w", endpoint, err) + } + + version := "" + if ver != nil { + version = ver.Version + } + var offset int64 + if end != nil { + offset = end.Offset + } + + return Result{ + Endpoint: endpoint, + LedgerAPIVersion: version, + Offset: offset, + }, nil +} diff --git a/internal/poc/godamlprobe/probe_integration_test.go b/internal/poc/godamlprobe/probe_integration_test.go new file mode 100644 index 00000000..2aa1bdcf --- /dev/null +++ b/internal/poc/godamlprobe/probe_integration_test.go @@ -0,0 +1,64 @@ +//go:build integration + +package godamlprobe_test + +import ( + "context" + "os" + "strings" + "testing" + "time" + + "github.com/bitdynamics-ab/canton-devkit/internal/poc/godamlprobe" + "github.com/bitdynamics-ab/canton-devkit/internal/splice" +) + +// TestProbe_AgainstLocalNet exercises go-daml against a running LocalNet +// participant. Skips cleanly when the endpoint is not reachable so +// developers without a booted instance are not blocked. +// +// go test -tags=integration ./internal/poc/godamlprobe/... +// +// Override the endpoint with CANTON_DEVKIT_TEST_LEDGER_ENDPOINT when the +// registry-mapped host port is not localhost:5001. +func TestProbe_AgainstLocalNet(t *testing.T) { + endpoint := os.Getenv("CANTON_DEVKIT_TEST_LEDGER_ENDPOINT") + if endpoint == "" { + endpoint = "localhost:5001" + } + + token, err := splice.SignToken(splice.CredentialInputs{ + Role: splice.RoleAppProvider, + User: "ledger-api-user", + Audience: "https://canton.network.global", + }) + if err != nil { + t.Fatalf("SignToken: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + res, err := godamlprobe.Probe(ctx, godamlprobe.Options{ + Endpoint: endpoint, + Token: token, + }) + if err != nil { + if strings.Contains(err.Error(), "connection refused") || + strings.Contains(err.Error(), "Unavailable") || + strings.Contains(err.Error(), "dial") { + t.Skipf("LocalNet not reachable at %s — run `localnet up` first: %v", endpoint, err) + } + t.Fatalf("Probe: %v", err) + } + + if res.Endpoint != endpoint { + t.Errorf("Endpoint = %q, want %q", res.Endpoint, endpoint) + } + if res.LedgerAPIVersion == "" { + t.Error("LedgerAPIVersion is empty; expected a non-empty participant version string") + } + if res.Offset < 0 { + t.Errorf("Offset = %d, want >= 0", res.Offset) + } +} diff --git a/internal/poc/godamlprobe/probe_test.go b/internal/poc/godamlprobe/probe_test.go new file mode 100644 index 00000000..46cd6456 --- /dev/null +++ b/internal/poc/godamlprobe/probe_test.go @@ -0,0 +1,41 @@ +package godamlprobe + +import ( + "context" + "strings" + "testing" + "time" +) + +func TestProbe_RequiresEndpoint(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + _, err := Probe(ctx, Options{Token: "unused"}) + if err == nil { + t.Fatal("Probe with empty endpoint: want error, got nil") + } + if !strings.Contains(err.Error(), "endpoint is required") { + t.Fatalf("error = %q, want substring %q", err.Error(), "endpoint is required") + } +} + +func TestProbe_UnreachableEndpoint(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + // Reserved TEST-NET-1 address that is not listening locally. + _, err := Probe(ctx, Options{ + Endpoint: "127.0.0.1:1", + Token: "not-a-real-token", + }) + if err == nil { + t.Fatal("Probe against closed port: want error, got nil") + } + msg := err.Error() + if !strings.Contains(msg, "godamlprobe.Probe") { + t.Fatalf("error = %q, want godamlprobe.Probe prefix", msg) + } +} From e81d8a168df986f120cea00a98b7bdcfedc01da2 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Tue, 22 Sep 2026 12:35:28 +0200 Subject: [PATCH 2/3] feat(localnet): require Ledger API readiness via go-daml after Docker health Wire the go-daml connectivity probe into up/start/restart so bring-up fails with LEDGER_UNREACHABLE when app-provider gRPC is not ready, instead of reporting success on container health alone. --- frontend/src/api.ts | 5 +- frontend/src/screens/remediation.test.ts | 8 +- frontend/src/screens/remediation.ts | 9 ++ internal/localnet/coded_error.go | 8 ++ internal/localnet/ledger_ready.go | 120 ++++++++++++++++++ internal/localnet/ledger_ready_test.go | 149 +++++++++++++++++++++++ internal/localnet/progress.go | 8 +- internal/localnet/progress_test.go | 2 +- internal/localnet/restart.go | 34 +++++- internal/localnet/restart_test.go | 4 + internal/localnet/stopstart.go | 34 +++++- internal/localnet/up.go | 50 ++++++-- internal/localnet/up_profiles_test.go | 2 + internal/localnet/up_test.go | 65 +++++++++- internal/poc/godamlprobe/doc.go | 16 ++- 15 files changed, 471 insertions(+), 43 deletions(-) create mode 100644 internal/localnet/ledger_ready.go create mode 100644 internal/localnet/ledger_ready_test.go diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 1c216372..cd149297 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -414,7 +414,8 @@ export type ErrorCode = | "DISK_LOW" | "PREFLIGHT_FAILED" | "CANTON_OOM" - | "CONTAINER_UNHEALTHY"; + | "CONTAINER_UNHEALTHY" + | "LEDGER_UNREACHABLE"; export interface PreflightSection { title: string; @@ -1485,7 +1486,7 @@ export const STEP_LABELS: Record = { fetch_splice: "Fetch Splice LocalNet", persist_state: "Persist state + write overlay", start_services: "Starting services", - wait_healthy: "Wait for services to become healthy", + wait_healthy: "Wait for services and Ledger API to become healthy", capture_jwts: "Capture JWTs · register endpoints", }; diff --git a/frontend/src/screens/remediation.test.ts b/frontend/src/screens/remediation.test.ts index da82e028..f476a160 100644 --- a/frontend/src/screens/remediation.test.ts +++ b/frontend/src/screens/remediation.test.ts @@ -29,11 +29,12 @@ describe("remediationForCode", () => { "PREFLIGHT_FAILED", "CANTON_OOM", "CONTAINER_UNHEALTHY", + "LEDGER_UNREACHABLE", ]; - // 7 codes that MUST render a panel — the remaining 2 - // (PREFLIGHT_FAILED, undefined) intentionally return null - // because they're too generic to be actionable on their own. + // Codes that MUST render a panel — PREFLIGHT_FAILED and undefined + // intentionally return null because they're too generic to be + // actionable on their own. const codesWithPanel: ErrorCode[] = [ "PORTS_IN_USE", "DOCKER_DOWN", @@ -43,6 +44,7 @@ describe("remediationForCode", () => { "DISK_LOW", "CANTON_OOM", "CONTAINER_UNHEALTHY", + "LEDGER_UNREACHABLE", ]; it("undefined code returns null (no panel rendered)", () => { diff --git a/frontend/src/screens/remediation.ts b/frontend/src/screens/remediation.ts index 1ef5471b..9546fdd4 100644 --- a/frontend/src/screens/remediation.ts +++ b/frontend/src/screens/remediation.ts @@ -89,6 +89,15 @@ export function remediationForCode(code?: ErrorCode): Remediation | null { "Common causes: postgres connection limit, splice waiting on canton, ledger API not ready.", ], }; + case "LEDGER_UNREACHABLE": + return { + title: "Ledger API did not become ready", + steps: [ + "Docker reported containers healthy, but the app-provider participant Ledger API did not accept a gRPC probe.", + "Check canton logs: `dpm localnet container logs canton`.", + "Confirm the instance captured `participant_ledger_app-provider` (`dpm localnet status `), then retry start or up.", + ], + }; case "PREFLIGHT_FAILED": case undefined: return null; diff --git a/internal/localnet/coded_error.go b/internal/localnet/coded_error.go index dc3ec785..1676d6f9 100644 --- a/internal/localnet/coded_error.go +++ b/internal/localnet/coded_error.go @@ -104,4 +104,12 @@ const ( // from a bare wait_healthy timeout — the frontend can offer a // generic "see logs" affordance instead of suggesting nothing. ErrCodeContainerUnhealthy = "CONTAINER_UNHEALTHY" + + // ErrCodeLedgerUnreachable: Docker containers reported healthy + // but the app-provider Ledger API gRPC probe (go-daml + // GetLedgerApiVersion + GetLedgerEnd) never succeeded within + // the ledger readiness budget. Remediation: check participant + // logs / `localnet logs`, confirm participant_ledger_app-provider + // was captured, retry start/up. + ErrCodeLedgerUnreachable = "LEDGER_UNREACHABLE" ) diff --git a/internal/localnet/ledger_ready.go b/internal/localnet/ledger_ready.go new file mode 100644 index 00000000..a2ae30f6 --- /dev/null +++ b/internal/localnet/ledger_ready.go @@ -0,0 +1,120 @@ +package localnet + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + "github.com/bitdynamics-ab/canton-devkit/internal/poc/godamlprobe" + "github.com/bitdynamics-ab/canton-devkit/internal/registry" + "github.com/bitdynamics-ab/canton-devkit/internal/splice" +) + +// ledgerReadyTimeout caps how long we poll the participant after Docker +// reports healthy. Canton can still be finishing ledger API bind for a +// short window after container healthchecks flip green. +// Package vars (not consts) so unit tests can shrink the budget. +var ledgerReadyTimeout = 5 * time.Minute + +var ledgerReadyPollWait = 2 * time.Second + +// probeLedgerFn is the go-daml dial seam. Production uses +// godamlprobe.Probe; unit tests replace it to avoid a live participant. +var probeLedgerFn = godamlprobe.Probe + +// ensureLedgerReadyFn is the seam up/start/restart call after Docker +// WaitForHealthy succeeds. Tests replace it with a no-op so hermetic +// bring-up stubs do not need a reachable Ledger API. +var ensureLedgerReadyFn = ensureLedgerReady + +// ensureLedgerReady resolves the app-provider Ledger API endpoint and a +// bearer JWT, then polls go-daml GetLedgerApiVersion + GetLedgerEnd until +// success or ctx expires. Role defaults to app-provider (operator node). +// +// ports should already include participant_ledger_app-provider from +// CaptureCantonPorts. creds may be empty — when so, a JWT is signed from +// projectDir's auth env files. +func ensureLedgerReady( + ctx context.Context, + projectDir string, + ports map[string]int, + creds map[string]registry.Credential, +) (godamlprobe.Result, error) { + endpoint, err := ledgerEndpointFromPorts(ports, string(splice.RoleAppProvider)) + if err != nil { + return godamlprobe.Result{}, err + } + token, err := ledgerTokenForRole(projectDir, creds, splice.RoleAppProvider) + if err != nil { + return godamlprobe.Result{}, err + } + + deadlineCtx, cancel := context.WithTimeout(ctx, ledgerReadyTimeout) + defer cancel() + + var lastErr error + for { + res, err := probeLedgerFn(deadlineCtx, godamlprobe.Options{ + Endpoint: endpoint, + Token: token, + }) + if err == nil { + return res, nil + } + lastErr = err + select { + case <-deadlineCtx.Done(): + if lastErr == nil { + lastErr = deadlineCtx.Err() + } + return godamlprobe.Result{}, fmt.Errorf( + "ledger API at %s (role %s) did not become ready: %w", + endpoint, splice.RoleAppProvider, lastErr) + case <-time.After(ledgerReadyPollWait): + } + } +} + +func ledgerEndpointFromPorts(ports map[string]int, role string) (string, error) { + if role == "" { + role = string(splice.RoleAppProvider) + } + key := "participant_ledger_" + role + port, ok := ports[key] + if !ok || port <= 0 { + return "", fmt.Errorf( + "ledger endpoint for role %s not captured (missing %s in instance ports)", + role, key) + } + return "localhost:" + strconv.Itoa(port), nil +} + +func ledgerTokenForRole( + projectDir string, + creds map[string]registry.Credential, + role splice.Role, +) (string, error) { + if c, ok := creds[string(role)]; ok && strings.TrimSpace(c.JWT) != "" { + return c.JWT, nil + } + if projectDir == "" { + return "", fmt.Errorf("no JWT for role %s and projectDir is empty", role) + } + inputs, err := splice.LoadCredentialInputs(projectDir) + if err != nil { + return "", fmt.Errorf("load credential inputs for ledger probe: %w", err) + } + for _, in := range inputs { + if in.Role != role { + continue + } + tok, err := splice.SignToken(in) + if err != nil { + return "", fmt.Errorf("sign JWT for role %s: %w", role, err) + } + return tok, nil + } + return "", fmt.Errorf("no auth env inputs for role %s in %s", role, projectDir) +} diff --git a/internal/localnet/ledger_ready_test.go b/internal/localnet/ledger_ready_test.go new file mode 100644 index 00000000..df3d685a --- /dev/null +++ b/internal/localnet/ledger_ready_test.go @@ -0,0 +1,149 @@ +package localnet + +import ( + "context" + "errors" + "strings" + "testing" + "time" + + "github.com/bitdynamics-ab/canton-devkit/internal/poc/godamlprobe" + "github.com/bitdynamics-ab/canton-devkit/internal/registry" + "github.com/bitdynamics-ab/canton-devkit/internal/splice" +) + +func stubEnsureLedgerReadyOK(t *testing.T) { + t.Helper() + prev := ensureLedgerReadyFn + ensureLedgerReadyFn = func(context.Context, string, map[string]int, map[string]registry.Credential) (godamlprobe.Result, error) { + return godamlprobe.Result{ + Endpoint: "localhost:9", + LedgerAPIVersion: "test", + Offset: 0, + }, nil + } + t.Cleanup(func() { ensureLedgerReadyFn = prev }) +} + +func TestLedgerEndpointFromPorts(t *testing.T) { + t.Parallel() + ep, err := ledgerEndpointFromPorts(map[string]int{ + "participant_ledger_app-provider": 61169, + }, string(splice.RoleAppProvider)) + if err != nil { + t.Fatalf("ledgerEndpointFromPorts: %v", err) + } + if ep != "localhost:61169" { + t.Fatalf("endpoint = %q, want localhost:61169", ep) + } + + _, err = ledgerEndpointFromPorts(map[string]int{}, string(splice.RoleAppProvider)) + if err == nil { + t.Fatal("missing port: want error") + } + if !strings.Contains(err.Error(), "participant_ledger_app-provider") { + t.Fatalf("error = %q, want missing-key hint", err.Error()) + } +} + +func TestEnsureLedgerReady_Success(t *testing.T) { + oldProbe := probeLedgerFn + t.Cleanup(func() { probeLedgerFn = oldProbe }) + + var calls int + probeLedgerFn = func(_ context.Context, opts godamlprobe.Options) (godamlprobe.Result, error) { + calls++ + if opts.Endpoint != "localhost:5001" { + t.Fatalf("endpoint = %q, want localhost:5001", opts.Endpoint) + } + if opts.Token != "tok" { + t.Fatalf("token = %q, want tok", opts.Token) + } + return godamlprobe.Result{ + Endpoint: opts.Endpoint, + LedgerAPIVersion: "3.3.0", + Offset: 42, + }, nil + } + + res, err := ensureLedgerReady(context.Background(), "", map[string]int{ + "participant_ledger_app-provider": 5001, + }, map[string]registry.Credential{ + string(splice.RoleAppProvider): {JWT: "tok"}, + }) + if err != nil { + t.Fatalf("ensureLedgerReady: %v", err) + } + if calls != 1 { + t.Fatalf("probe calls = %d, want 1", calls) + } + if res.Offset != 42 || res.LedgerAPIVersion != "3.3.0" { + t.Fatalf("result = %+v", res) + } +} + +func TestEnsureLedgerReady_RetriesThenSucceeds(t *testing.T) { + oldProbe := probeLedgerFn + oldWait := ledgerReadyPollWait + t.Cleanup(func() { + probeLedgerFn = oldProbe + ledgerReadyPollWait = oldWait + }) + ledgerReadyPollWait = 5 * time.Millisecond + + var calls int + probeLedgerFn = func(context.Context, godamlprobe.Options) (godamlprobe.Result, error) { + calls++ + if calls < 3 { + return godamlprobe.Result{}, errors.New("connection refused") + } + return godamlprobe.Result{Endpoint: "localhost:5001", Offset: 1}, nil + } + + res, err := ensureLedgerReady(context.Background(), "", map[string]int{ + "participant_ledger_app-provider": 5001, + }, map[string]registry.Credential{ + string(splice.RoleAppProvider): {JWT: "tok"}, + }) + if err != nil { + t.Fatalf("ensureLedgerReady: %v", err) + } + if calls != 3 { + t.Fatalf("probe calls = %d, want 3", calls) + } + if res.Endpoint != "localhost:5001" { + t.Fatalf("endpoint = %q", res.Endpoint) + } +} + +func TestEnsureLedgerReady_Timeout(t *testing.T) { + oldProbe := probeLedgerFn + oldWait := ledgerReadyPollWait + oldTimeout := ledgerReadyTimeout + t.Cleanup(func() { + probeLedgerFn = oldProbe + ledgerReadyPollWait = oldWait + ledgerReadyTimeout = oldTimeout + }) + ledgerReadyPollWait = 5 * time.Millisecond + ledgerReadyTimeout = 40 * time.Millisecond + + probeLedgerFn = func(context.Context, godamlprobe.Options) (godamlprobe.Result, error) { + return godamlprobe.Result{}, errors.New("Unavailable") + } + + _, err := ensureLedgerReady(context.Background(), "", map[string]int{ + "participant_ledger_app-provider": 5001, + }, map[string]registry.Credential{ + string(splice.RoleAppProvider): {JWT: "tok"}, + }) + if err == nil { + t.Fatal("want timeout error") + } + if !strings.Contains(err.Error(), "did not become ready") { + t.Fatalf("error = %q", err.Error()) + } + if !strings.Contains(err.Error(), "app-provider") { + t.Fatalf("error should name role, got %q", err.Error()) + } +} diff --git a/internal/localnet/progress.go b/internal/localnet/progress.go index 1b1ec093..68994857 100644 --- a/internal/localnet/progress.go +++ b/internal/localnet/progress.go @@ -42,8 +42,10 @@ const ( // StepStartServices: 6 · "Starting services" — `docker compose // up -d --wait`. The longest single phase. StepStartServices Step = "start_services" - // StepWaitHealthy: 7 · "Wait for services to become healthy" — - // container --wait + adapter-specific readiness probes. + // StepWaitHealthy: 7 · "Wait for services and Ledger API to become + // healthy" — Docker compose health polling, then a Ledger API + // connectivity probe (go-daml GetLedgerApiVersion + GetLedgerEnd on + // app-provider). StepWaitHealthy Step = "wait_healthy" // StepCaptureJWTs: 8 · "Capture JWTs · register endpoints" — // sign dev-secret tokens for sv/app-provider/app-user, write @@ -132,7 +134,7 @@ var stepLabel = map[Step]string{ StepFetchSplice: "Fetching Splice LocalNet", StepPersistState: "Persisting state + writing overlay", StepStartServices: "Starting services", - StepWaitHealthy: "Waiting for services to become healthy", + StepWaitHealthy: "Waiting for services and Ledger API to become healthy", StepCaptureJWTs: "Capturing JWTs · registering endpoints", } diff --git a/internal/localnet/progress_test.go b/internal/localnet/progress_test.go index 1271b182..77f9742f 100644 --- a/internal/localnet/progress_test.go +++ b/internal/localnet/progress_test.go @@ -41,7 +41,7 @@ func TestTextProgress_StartStepOutput(t *testing.T) { name: "wait_healthy, no detail", step: StepWaitHealthy, detail: "", - want: "Waiting for services to become healthy...\n", + want: "Waiting for services and Ledger API to become healthy...\n", }, { name: "preflight with detail (hypothetical future use)", diff --git a/internal/localnet/restart.go b/internal/localnet/restart.go index 97a4a008..03b4b926 100644 --- a/internal/localnet/restart.go +++ b/internal/localnet/restart.go @@ -174,14 +174,36 @@ func RunRestart(ctx context.Context, out io.Writer, errw io.Writer, opts *Restar _ = registry.Write(state) return ExitTimeout } - } - // Re-capture Canton's ephemeral gRPC ports (see godoc). Best- - // effort: an empty result (docker unavailable in tests) is a - // no-op merge that leaves the cached ports untouched. - for key, port := range CaptureCantonPorts(ctx, state.ComposeProject) { - state.Ports[key] = port + // Re-capture Canton's ephemeral gRPC ports (see godoc). Best- + // effort: an empty result (docker unavailable in tests) is a + // no-op merge that leaves the cached ports untouched. + for key, port := range CaptureCantonPorts(ctx, state.ComposeProject) { + state.Ports[key] = port + } + if len(state.Credentials) == 0 && state.ProjectDir != "" { + if creds := captureCredentials(state.ProjectDir, errw); creds != nil { + state.Credentials = creds + } + } + ledgerRes, err := ensureLedgerReadyFn(ctx, state.ProjectDir, state.Ports, state.Credentials) + if err != nil { + if ctx.Err() != nil { + _, _ = fmt.Fprintln(errw, "Interrupted while waiting for Ledger API") + return ExitTimeout + } + _, _ = fmt.Fprintf(errw, "Ledger API did not become ready after restart: %s\n", err) + state.Status = registry.StatusPartial + _ = registry.Write(state) + return ExitTimeout + } + _, _ = fmt.Fprintf(out, "Ledger API ready at %s (role app-provider)\n", ledgerRes.Endpoint) + } else { + for key, port := range CaptureCantonPorts(ctx, state.ComposeProject) { + state.Ports[key] = port + } } + state.Status = registry.StatusRunning if err := registry.Write(state); err != nil { _, _ = fmt.Fprintf(errw, "Warning: could not persist post-restart state: %s\n", err) diff --git a/internal/localnet/restart_test.go b/internal/localnet/restart_test.go index cc44a54e..3ebef534 100644 --- a/internal/localnet/restart_test.go +++ b/internal/localnet/restart_test.go @@ -60,6 +60,7 @@ func okRestarter(restartCalled *bool, gotServices *[]string) func(string, []stri func TestRunRestart_HappyPath(t *testing.T) { seedRunningInstance(t, "rs-happy") + stubEnsureLedgerReadyOK(t) called := false var out, errBuf bytes.Buffer @@ -85,6 +86,7 @@ func TestRunRestart_HappyPath(t *testing.T) { func TestRunRestart_ServiceScoped(t *testing.T) { seedRunningInstance(t, "rs-svc") + stubEnsureLedgerReadyOK(t) var got []string var out, errBuf bytes.Buffer @@ -191,6 +193,7 @@ func TestRunRestart_UnknownServiceRejected(t *testing.T) { func TestRunRestart_KnownServiceAccepted(t *testing.T) { seedRunningInstance(t, "rs-known") + stubEnsureLedgerReadyOK(t) stubProjectContainers(t, "canton", "splice") var got []string @@ -210,6 +213,7 @@ func TestRunRestart_KnownServiceAccepted(t *testing.T) { func TestRunRestart_PortRecapturePersisted(t *testing.T) { seedRunningInstance(t, "rs-ports") + stubEnsureLedgerReadyOK(t) // Seed stale ports into the registry. st, _ := registry.Read("rs-ports") diff --git a/internal/localnet/stopstart.go b/internal/localnet/stopstart.go index 16992419..c2e8d186 100644 --- a/internal/localnet/stopstart.go +++ b/internal/localnet/stopstart.go @@ -280,13 +280,37 @@ func fastStart(ctx context.Context, out, errw io.Writer, opts *StartOptions, sta _ = registry.Write(state) return ExitTimeout } - } - // Re-capture Canton's ephemeral gRPC ports — Docker may re-assign - // them on start. Best-effort merge (empty result is a no-op). - for key, port := range CaptureCantonPorts(ctx, state.ComposeProject) { - state.Ports[key] = port + // Re-capture Canton's ephemeral gRPC ports — Docker may re-assign + // them on start. Best-effort merge (empty result is a no-op). + for key, port := range CaptureCantonPorts(ctx, state.ComposeProject) { + state.Ports[key] = port + } + if len(state.Credentials) == 0 && state.ProjectDir != "" { + if creds := captureCredentials(state.ProjectDir, errw); creds != nil { + state.Credentials = creds + } + } + ledgerRes, err := ensureLedgerReadyFn(ctx, state.ProjectDir, state.Ports, state.Credentials) + if err != nil { + if ctx.Err() != nil { + _, _ = fmt.Fprintln(errw, "Interrupted while waiting for Ledger API") + return ExitTimeout + } + _, _ = fmt.Fprintf(errw, "Ledger API did not become ready after start: %s\n", err) + state.Status = registry.StatusPartial + _ = registry.Write(state) + return ExitTimeout + } + _, _ = fmt.Fprintf(out, "Ledger API ready at %s (role app-provider)\n", ledgerRes.Endpoint) + } else { + // --no-wait: still refresh ports when possible so consumers + // are not left with stale bindings, but skip the ledger probe. + for key, port := range CaptureCantonPorts(ctx, state.ComposeProject) { + state.Ports[key] = port + } } + state.Status = registry.StatusRunning if err := registry.Write(state); err != nil { _, _ = fmt.Fprintf(errw, "Warning: could not persist post-start state: %s\n", err) diff --git a/internal/localnet/up.go b/internal/localnet/up.go index 2a4ed493..d9c3d07a 100644 --- a/internal/localnet/up.go +++ b/internal/localnet/up.go @@ -612,7 +612,9 @@ func RunUp(ctx context.Context, prog Progress, opts *UpOptions) int { } prog.FinishStep(StepStartServices, "") - // 7. Wait for services to become healthy. + // 7. Wait for services to become healthy, then probe the + // app-provider Ledger API via go-daml. Docker health alone is not + // enough — token / explorer paths need a live participant gRPC. prog.StartStep(StepWaitHealthy, "") if err := runner.WaitForHealthy(ctx); err != nil { markFailed(state, prog.Err()) @@ -638,13 +640,38 @@ func RunUp(ctx context.Context, prog Progress, opts *UpOptions) int { WithCode(err, code)) return ExitRuntimeFailure } + + prog.UpdateStep(StepWaitHealthy, "Capturing participant ports", -1) + for key, port := range CaptureCantonPorts(ctx, state.ComposeProject) { + state.Ports[key] = port + } + for key, port := range CaptureMetricsPorts(ctx, state.ComposeProject) { + state.Ports[key] = port + } + + prog.UpdateStep(StepWaitHealthy, "Probing Ledger API (app-provider)", -1) + if creds := captureCredentials(projectDir, prog.Err()); creds != nil { + state.Credentials = creds + } + ledgerRes, err := ensureLedgerReadyFn(ctx, projectDir, state.Ports, state.Credentials) + if err != nil { + markFailed(state, prog.Err()) + if ctx.Err() != nil { + prog.FailStep(StepWaitHealthy, "Timed out waiting for Ledger API", nil) + return ExitTimeout + } + prog.FailStep(StepWaitHealthy, "Ledger API not reachable", + WithCode(err, ErrCodeLedgerUnreachable)) + return ExitRuntimeFailure + } + prog.UpdateStep(StepWaitHealthy, + fmt.Sprintf("Ledger API ready at %s (role app-provider, offset %d)", + ledgerRes.Endpoint, ledgerRes.Offset), -1) prog.FinishStep(StepWaitHealthy, "") - // 8. Capture Canton participant ports. The Canton container - // exposes Ledger/Admin/JSON APIs per party role on Docker-ephemeral - // host ports — ask docker what they ended up as and persist them so - // consumers (Web UI Explorer, DAR Manager, token tooling) can dial - // without a manual `--admin-host=localhost:` flag. + // 8. Capture Canton participant ports. Ports were already + // queried for the ledger probe above; re-query is idempotent and + // picks up any late-published bindings. // Best-effort: any port that fails to query is silently omitted, // not stamped as 0. for key, port := range CaptureCantonPorts(ctx, state.ComposeProject) { @@ -673,12 +700,13 @@ func RunUp(ctx context.Context, prog Progress, opts *UpOptions) int { state.ImageDigests = digests } - // 9. Capture JWTs and persist running state. (UI ports were - // pre-allocated in step 5; Canton participant gRPC/JSON API - // ports were just captured in step 8.) + // 9. Persist running state. JWTs were captured during the ledger + // probe step above; refresh only if that path left Credentials empty. prog.StartStep(StepCaptureJWTs, "") - if creds := captureCredentials(projectDir, prog.Err()); creds != nil { - state.Credentials = creds + if len(state.Credentials) == 0 { + if creds := captureCredentials(projectDir, prog.Err()); creds != nil { + state.Credentials = creds + } } state.Status = registry.StatusRunning if err := registry.Write(state); err != nil { diff --git a/internal/localnet/up_profiles_test.go b/internal/localnet/up_profiles_test.go index 96faf1ca..62cb1376 100644 --- a/internal/localnet/up_profiles_test.go +++ b/internal/localnet/up_profiles_test.go @@ -135,6 +135,7 @@ func TestRunUp_RejectsUnknownProfile(t *testing.T) { // full set, with no double-counting. func TestRunUp_InheritsOptInProfilesOnReup(t *testing.T) { t.Setenv("CANTON_DEVKIT_REGISTRY", t.TempDir()) + stubEnsureLedgerReadyOK(t) projectDir := fakeSpliceProjectDir(t) base := adapterBaseProfiles(t) @@ -195,6 +196,7 @@ func TestRunUp_InheritsOptInProfilesOnReup(t *testing.T) { // deliberately change or drop the opt-ins. func TestRunUp_ExplicitProfilesOverrideStoredOnReup(t *testing.T) { t.Setenv("CANTON_DEVKIT_REGISTRY", t.TempDir()) + stubEnsureLedgerReadyOK(t) projectDir := fakeSpliceProjectDir(t) base := adapterBaseProfiles(t) diff --git a/internal/localnet/up_test.go b/internal/localnet/up_test.go index 1a7fbb4b..abe20f68 100644 --- a/internal/localnet/up_test.go +++ b/internal/localnet/up_test.go @@ -3,6 +3,7 @@ package localnet import ( "bytes" "context" + "errors" "io" "os" "path/filepath" @@ -10,6 +11,7 @@ import ( "testing" "time" + "github.com/bitdynamics-ab/canton-devkit/internal/poc/godamlprobe" "github.com/bitdynamics-ab/canton-devkit/internal/registry" "github.com/bitdynamics-ab/canton-devkit/internal/splice" ) @@ -125,6 +127,7 @@ func TestRunUp_RejectsConcurrentSameNameOp(t *testing.T) { // would deadlock under real docker) func TestRunUp_HappyPath_FakeDriven(t *testing.T) { t.Setenv("CANTON_DEVKIT_REGISTRY", t.TempDir()) + stubEnsureLedgerReadyOK(t) // Build a fake project dir containing the env/ files // LoadCredentialInputs expects. Three roles, each with VALIDATOR_USER @@ -266,6 +269,7 @@ func (d *doneRecorder) Done(string) { d.doneCalls++ } // on "running" forever, never refreshing the dashboard. func TestRunUp_EmitsDoneOnSuccess(t *testing.T) { t.Setenv("CANTON_DEVKIT_REGISTRY", t.TempDir()) + stubEnsureLedgerReadyOK(t) projectDir := t.TempDir() envDir := filepath.Join(projectDir, "env") @@ -304,6 +308,7 @@ func TestRunUp_EmitsDoneOnSuccess(t *testing.T) { func TestRunUp_AlphaVersionWarns(t *testing.T) { t.Setenv("CANTON_DEVKIT_REGISTRY", t.TempDir()) + stubEnsureLedgerReadyOK(t) projectDir := t.TempDir() envDir := filepath.Join(projectDir, "env") @@ -389,6 +394,59 @@ func (s *composeRunnerStub) WaitForHealthy(context.Context) error { return nil } +// TestRunUp_LedgerUnreachableFails pins that a failed go-daml Ledger +// API probe after Docker health marks the instance failed and returns +// ExitRuntimeFailure — containers-up-but-ledger-down must not look like +// a successful bring-up. +func TestRunUp_LedgerUnreachableFails(t *testing.T) { + t.Setenv("CANTON_DEVKIT_REGISTRY", t.TempDir()) + prev := ensureLedgerReadyFn + ensureLedgerReadyFn = func(context.Context, string, map[string]int, map[string]registry.Credential) (godamlprobe.Result, error) { + return godamlprobe.Result{}, errors.New("connection refused") + } + t.Cleanup(func() { ensureLedgerReadyFn = prev }) + + projectDir := t.TempDir() + envDir := filepath.Join(projectDir, "env") + if err := os.MkdirAll(envDir, 0o755); err != nil { + t.Fatalf("mkdir env: %v", err) + } + for name, body := range map[string]string{ + "sv-auth-on.env": "AUTH_SV_VALIDATOR_USER_NAME=sv-user\nAUTH_SV_AUDIENCE=sv-aud\n", + "app-provider-auth-on.env": "AUTH_APP_PROVIDER_VALIDATOR_USER_NAME=ap-user\nAUTH_APP_PROVIDER_AUDIENCE=ap-aud\n", + "app-user-auth-on.env": "AUTH_APP_USER_VALIDATOR_USER_NAME=au-user\nAUTH_APP_USER_AUDIENCE=au-aud\n", + } { + if err := os.WriteFile(filepath.Join(envDir, name), []byte(body), 0o644); err != nil { + t.Fatalf("write %s: %v", name, err) + } + } + + var out, errBuf bytes.Buffer + code := RunUp(context.Background(), + &TextProgress{OutW: &out, ErrW: &errBuf}, + &UpOptions{ + Name: "ledger-down", + Version: splice.LatestAlias, + SkipPreflight: true, + FetchFn: func(_ context.Context, _ splice.Version, _ string, _ io.Writer) (string, error) { + return projectDir, nil + }, + NewRunner: func(string, []string, []string, []string, string, io.Writer) composeOps { + return &composeRunnerStub{} + }, + }) + if code != ExitRuntimeFailure { + t.Fatalf("RunUp = %d, want ExitRuntimeFailure; stderr=%q", code, errBuf.String()) + } + st, err := registry.Read("ledger-down") + if err != nil { + t.Fatalf("registry.Read: %v", err) + } + if st.Status != registry.StatusFailed { + t.Errorf("status = %q, want failed", st.Status) + } +} + // TestRunUp_CLIByteEquivalence pins the CLI output contract: the // TextProgress-backed RunUp must emit the same set of header lines // today's users see, AND must NOT emit new lines for the five @@ -402,6 +460,7 @@ func (s *composeRunnerStub) WaitForHealthy(context.Context) error { // is "these phases are visible / those phases are silent." func TestRunUp_CLIByteEquivalence(t *testing.T) { t.Setenv("CANTON_DEVKIT_REGISTRY", t.TempDir()) + stubEnsureLedgerReadyOK(t) projectDir := t.TempDir() envDir := filepath.Join(projectDir, "env") @@ -441,9 +500,9 @@ func TestRunUp_CLIByteEquivalence(t *testing.T) { // VISIBLE step lines — must be present. mustContain := []string{ - "Starting Canton LocalNet", // the verbatim header preserved via prog.Out() - "Starting services...", // StepStartServices via TextProgress.StartStep - "Waiting for services to become healthy...", // StepWaitHealthy + "Starting Canton LocalNet", // the verbatim header preserved via prog.Out() + "Starting services...", // StepStartServices via TextProgress.StartStep + "Waiting for services and Ledger API to become healthy...", // StepWaitHealthy "is ready", // Done() success marker (welcome line: `"x" is ready · Splice …`) } // SkipPreflight is true in this test, so "Running preflight checks..." diff --git a/internal/poc/godamlprobe/doc.go b/internal/poc/godamlprobe/doc.go index 7232425e..438edd40 100644 --- a/internal/poc/godamlprobe/doc.go +++ b/internal/poc/godamlprobe/doc.go @@ -1,11 +1,9 @@ -// Package godamlprobe is an experimental PoC that dials a Canton participant -// Ledger API endpoint using github.com/noders-team/go-daml and reports whether -// the participant accepts the call (connectivity + auth). +// Package godamlprobe dials a Canton participant Ledger API endpoint using +// github.com/noders-team/go-daml and reports whether the participant accepts +// the call (connectivity + auth). // -// This package is intentionally NOT wired into production readiness paths -// (ComposeRunner.WaitForHealthy, localnet doctor, or the Web UI reconciler). -// Production ledger dials continue to use internal/canton/ledger (dazl-client). -// -// The PoC exists to evaluate whether go-daml is a viable alternative client -// for a cheap unary probe (GetLedgerApiVersion + GetLedgerEnd) against LocalNet. +// Production readiness (localnet up / start / restart after Docker health) +// calls Probe via internal/localnet.ensureLedgerReady. Command paths that +// submit ledger transactions continue to use internal/canton/ledger +// (dazl-client); this package is the connectivity probe only. package godamlprobe From 5165ffc9f91df7ac031b7668a736b4d8e0af8930 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Fri, 9 Oct 2026 19:02:18 +0200 Subject: [PATCH 3/3] fix(localnet): probe Ledger readiness with dazl-client, not go-daml go-daml and dazl-client both register the same Ledger API protobuf files, so linking both panics at process init and fails CI. Keep the production readiness gate but dial via internal/canton/ledger. --- go.mod | 4 - go.sum | 76 ------------------ internal/localnet/coded_error.go | 2 +- internal/localnet/ledger_ready.go | 77 +++++++++++++++--- internal/localnet/ledger_ready_test.go | 33 +++++--- internal/localnet/progress.go | 4 +- internal/localnet/up.go | 2 +- internal/localnet/up_test.go | 9 +-- internal/poc/godamlprobe/doc.go | 9 --- internal/poc/godamlprobe/probe.go | 78 ------------------- .../poc/godamlprobe/probe_integration_test.go | 64 --------------- internal/poc/godamlprobe/probe_test.go | 41 ---------- 12 files changed, 97 insertions(+), 302 deletions(-) delete mode 100644 internal/poc/godamlprobe/doc.go delete mode 100644 internal/poc/godamlprobe/probe.go delete mode 100644 internal/poc/godamlprobe/probe_integration_test.go delete mode 100644 internal/poc/godamlprobe/probe_test.go diff --git a/go.mod b/go.mod index 4308f67f..5c85fd17 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/digital-asset/dazl-client/v8 v8.9.0 github.com/mattn/go-isatty v0.0.22 github.com/muesli/termenv v0.16.0 - github.com/noders-team/go-daml v0.7.2 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.9 go.yaml.in/yaml/v3 v3.0.4 @@ -29,14 +28,11 @@ require ( github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rs/zerolog v1.34.0 // indirect - github.com/shopspring/decimal v1.4.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/net v0.56.0 // indirect golang.org/x/text v0.38.0 // indirect diff --git a/go.sum b/go.sum index eb51d5c5..57979ef4 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,5 @@ -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= -github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= @@ -30,103 +20,44 @@ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfa github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= -github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= -github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/digital-asset/dazl-client/v8 v8.9.0 h1:F2qTUWtHAjhGyRGV+xTim+VAFwM99FpcOx4+wowvPnY= github.com/digital-asset/dazl-client/v8 v8.9.0/go.mod h1:q1KevCJ8FpH8je2MnnjN8/QUfhstB4fKpyKyqDtqFh0= -github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI= -github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= -github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= -github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= -github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/noders-team/go-daml v0.7.2 h1:6pLWLumh8LcX9rKhEsMBtHZVtqNIaXq9q2fEB3JUJAI= -github.com/noders-team/go-daml v0.7.2/go.mod h1:aFnPCwm5MVTDZFC9gN8dPsU8tpsQM/VbwXvG2VbJyqk= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opencontainers/runc v1.3.6 h1:SLGIymCtsk80iNPWgbc8dtjI30r+5mTVV+4dN8/17Sk= -github.com/opencontainers/runc v1.3.6/go.mod h1:o1wyv76EDlTkcf0KTFgN8bMWLPvgF/HfX709lDv+rr4= -github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw= -github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= -github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= @@ -148,9 +79,6 @@ golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQz golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= @@ -165,7 +93,3 @@ google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBN google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/localnet/coded_error.go b/internal/localnet/coded_error.go index 1676d6f9..3e829779 100644 --- a/internal/localnet/coded_error.go +++ b/internal/localnet/coded_error.go @@ -106,7 +106,7 @@ const ( ErrCodeContainerUnhealthy = "CONTAINER_UNHEALTHY" // ErrCodeLedgerUnreachable: Docker containers reported healthy - // but the app-provider Ledger API gRPC probe (go-daml + // but the app-provider Ledger API gRPC probe (dazl-client // GetLedgerApiVersion + GetLedgerEnd) never succeeded within // the ledger readiness budget. Remediation: check participant // logs / `localnet logs`, confirm participant_ledger_app-provider diff --git a/internal/localnet/ledger_ready.go b/internal/localnet/ledger_ready.go index a2ae30f6..9dbf8440 100644 --- a/internal/localnet/ledger_ready.go +++ b/internal/localnet/ledger_ready.go @@ -7,7 +7,7 @@ import ( "strings" "time" - "github.com/bitdynamics-ab/canton-devkit/internal/poc/godamlprobe" + "github.com/bitdynamics-ab/canton-devkit/internal/canton/ledger" "github.com/bitdynamics-ab/canton-devkit/internal/registry" "github.com/bitdynamics-ab/canton-devkit/internal/splice" ) @@ -20,9 +20,24 @@ var ledgerReadyTimeout = 5 * time.Minute var ledgerReadyPollWait = 2 * time.Second -// probeLedgerFn is the go-daml dial seam. Production uses -// godamlprobe.Probe; unit tests replace it to avoid a live participant. -var probeLedgerFn = godamlprobe.Probe +// LedgerReadyResult is what a successful readiness probe observed. +type LedgerReadyResult struct { + Endpoint string + LedgerAPIVersion string + Offset int64 +} + +// ledgerProbeOptions configures a single dial attempt against a participant. +type ledgerProbeOptions struct { + Endpoint string + Token string +} + +// probeLedgerFn is the dial seam. Production uses probeLedger (dazl-client +// via internal/canton/ledger). Unit tests replace it to avoid a live +// participant. go-daml cannot be used here: its generated protos collide +// with dazl-client in the same process (protobuf registry panic). +var probeLedgerFn = probeLedger // ensureLedgerReadyFn is the seam up/start/restart call after Docker // WaitForHealthy succeeds. Tests replace it with a no-op so hermetic @@ -30,8 +45,8 @@ var probeLedgerFn = godamlprobe.Probe var ensureLedgerReadyFn = ensureLedgerReady // ensureLedgerReady resolves the app-provider Ledger API endpoint and a -// bearer JWT, then polls go-daml GetLedgerApiVersion + GetLedgerEnd until -// success or ctx expires. Role defaults to app-provider (operator node). +// bearer JWT, then polls GetLedgerApiVersion + GetLedgerEnd until success +// or ctx expires. Role defaults to app-provider (operator node). // // ports should already include participant_ledger_app-provider from // CaptureCantonPorts. creds may be empty — when so, a JWT is signed from @@ -41,14 +56,14 @@ func ensureLedgerReady( projectDir string, ports map[string]int, creds map[string]registry.Credential, -) (godamlprobe.Result, error) { +) (LedgerReadyResult, error) { endpoint, err := ledgerEndpointFromPorts(ports, string(splice.RoleAppProvider)) if err != nil { - return godamlprobe.Result{}, err + return LedgerReadyResult{}, err } token, err := ledgerTokenForRole(projectDir, creds, splice.RoleAppProvider) if err != nil { - return godamlprobe.Result{}, err + return LedgerReadyResult{}, err } deadlineCtx, cancel := context.WithTimeout(ctx, ledgerReadyTimeout) @@ -56,7 +71,7 @@ func ensureLedgerReady( var lastErr error for { - res, err := probeLedgerFn(deadlineCtx, godamlprobe.Options{ + res, err := probeLedgerFn(deadlineCtx, ledgerProbeOptions{ Endpoint: endpoint, Token: token, }) @@ -69,7 +84,7 @@ func ensureLedgerReady( if lastErr == nil { lastErr = deadlineCtx.Err() } - return godamlprobe.Result{}, fmt.Errorf( + return LedgerReadyResult{}, fmt.Errorf( "ledger API at %s (role %s) did not become ready: %w", endpoint, splice.RoleAppProvider, lastErr) case <-time.After(ledgerReadyPollWait): @@ -77,6 +92,46 @@ func ensureLedgerReady( } } +// probeLedger dials the participant with dazl-client and runs the two +// cheap unaries that prove connectivity + auth. +func probeLedger(ctx context.Context, opts ledgerProbeOptions) (LedgerReadyResult, error) { + endpoint := strings.TrimSpace(opts.Endpoint) + if endpoint == "" { + return LedgerReadyResult{}, fmt.Errorf("ledger probe: endpoint is required") + } + + client, err := ledger.Dial(ctx, ledger.DialOptions{ + Endpoint: endpoint, + Token: ledger.StaticToken(opts.Token), + PlainText: true, + }) + if err != nil { + return LedgerReadyResult{}, fmt.Errorf("ledger probe: dial %s: %w", endpoint, err) + } + defer func() { _ = client.Close() }() + + ver, err := client.LedgerApiVersion(ctx) + if err != nil { + return LedgerReadyResult{}, fmt.Errorf("ledger probe: GetLedgerApiVersion %s: %w", endpoint, err) + } + + end, err := client.LedgerEnd(ctx) + if err != nil { + return LedgerReadyResult{}, fmt.Errorf("ledger probe: GetLedgerEnd %s: %w", endpoint, err) + } + + version := "" + if ver != nil { + version = ver.GetVersion() + } + + return LedgerReadyResult{ + Endpoint: endpoint, + LedgerAPIVersion: version, + Offset: end.Offset, + }, nil +} + func ledgerEndpointFromPorts(ports map[string]int, role string) (string, error) { if role == "" { role = string(splice.RoleAppProvider) diff --git a/internal/localnet/ledger_ready_test.go b/internal/localnet/ledger_ready_test.go index df3d685a..45f9c3a7 100644 --- a/internal/localnet/ledger_ready_test.go +++ b/internal/localnet/ledger_ready_test.go @@ -7,7 +7,6 @@ import ( "testing" "time" - "github.com/bitdynamics-ab/canton-devkit/internal/poc/godamlprobe" "github.com/bitdynamics-ab/canton-devkit/internal/registry" "github.com/bitdynamics-ab/canton-devkit/internal/splice" ) @@ -15,8 +14,8 @@ import ( func stubEnsureLedgerReadyOK(t *testing.T) { t.Helper() prev := ensureLedgerReadyFn - ensureLedgerReadyFn = func(context.Context, string, map[string]int, map[string]registry.Credential) (godamlprobe.Result, error) { - return godamlprobe.Result{ + ensureLedgerReadyFn = func(context.Context, string, map[string]int, map[string]registry.Credential) (LedgerReadyResult, error) { + return LedgerReadyResult{ Endpoint: "localhost:9", LedgerAPIVersion: "test", Offset: 0, @@ -51,7 +50,7 @@ func TestEnsureLedgerReady_Success(t *testing.T) { t.Cleanup(func() { probeLedgerFn = oldProbe }) var calls int - probeLedgerFn = func(_ context.Context, opts godamlprobe.Options) (godamlprobe.Result, error) { + probeLedgerFn = func(_ context.Context, opts ledgerProbeOptions) (LedgerReadyResult, error) { calls++ if opts.Endpoint != "localhost:5001" { t.Fatalf("endpoint = %q, want localhost:5001", opts.Endpoint) @@ -59,7 +58,7 @@ func TestEnsureLedgerReady_Success(t *testing.T) { if opts.Token != "tok" { t.Fatalf("token = %q, want tok", opts.Token) } - return godamlprobe.Result{ + return LedgerReadyResult{ Endpoint: opts.Endpoint, LedgerAPIVersion: "3.3.0", Offset: 42, @@ -92,12 +91,12 @@ func TestEnsureLedgerReady_RetriesThenSucceeds(t *testing.T) { ledgerReadyPollWait = 5 * time.Millisecond var calls int - probeLedgerFn = func(context.Context, godamlprobe.Options) (godamlprobe.Result, error) { + probeLedgerFn = func(context.Context, ledgerProbeOptions) (LedgerReadyResult, error) { calls++ if calls < 3 { - return godamlprobe.Result{}, errors.New("connection refused") + return LedgerReadyResult{}, errors.New("connection refused") } - return godamlprobe.Result{Endpoint: "localhost:5001", Offset: 1}, nil + return LedgerReadyResult{Endpoint: "localhost:5001", Offset: 1}, nil } res, err := ensureLedgerReady(context.Background(), "", map[string]int{ @@ -128,8 +127,8 @@ func TestEnsureLedgerReady_Timeout(t *testing.T) { ledgerReadyPollWait = 5 * time.Millisecond ledgerReadyTimeout = 40 * time.Millisecond - probeLedgerFn = func(context.Context, godamlprobe.Options) (godamlprobe.Result, error) { - return godamlprobe.Result{}, errors.New("Unavailable") + probeLedgerFn = func(context.Context, ledgerProbeOptions) (LedgerReadyResult, error) { + return LedgerReadyResult{}, errors.New("Unavailable") } _, err := ensureLedgerReady(context.Background(), "", map[string]int{ @@ -147,3 +146,17 @@ func TestEnsureLedgerReady_Timeout(t *testing.T) { t.Fatalf("error should name role, got %q", err.Error()) } } + +func TestProbeLedger_RequiresEndpoint(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + _, err := probeLedger(ctx, ledgerProbeOptions{Token: "unused"}) + if err == nil { + t.Fatal("empty endpoint: want error") + } + if !strings.Contains(err.Error(), "endpoint is required") { + t.Fatalf("error = %q", err.Error()) + } +} diff --git a/internal/localnet/progress.go b/internal/localnet/progress.go index 68994857..84b5b5e8 100644 --- a/internal/localnet/progress.go +++ b/internal/localnet/progress.go @@ -44,8 +44,8 @@ const ( StepStartServices Step = "start_services" // StepWaitHealthy: 7 · "Wait for services and Ledger API to become // healthy" — Docker compose health polling, then a Ledger API - // connectivity probe (go-daml GetLedgerApiVersion + GetLedgerEnd on - // app-provider). + // connectivity probe (GetLedgerApiVersion + GetLedgerEnd on + // app-provider via dazl-client). StepWaitHealthy Step = "wait_healthy" // StepCaptureJWTs: 8 · "Capture JWTs · register endpoints" — // sign dev-secret tokens for sv/app-provider/app-user, write diff --git a/internal/localnet/up.go b/internal/localnet/up.go index d9c3d07a..55cf4884 100644 --- a/internal/localnet/up.go +++ b/internal/localnet/up.go @@ -613,7 +613,7 @@ func RunUp(ctx context.Context, prog Progress, opts *UpOptions) int { prog.FinishStep(StepStartServices, "") // 7. Wait for services to become healthy, then probe the - // app-provider Ledger API via go-daml. Docker health alone is not + // app-provider Ledger API (dazl-client). Docker health alone is not // enough — token / explorer paths need a live participant gRPC. prog.StartStep(StepWaitHealthy, "") if err := runner.WaitForHealthy(ctx); err != nil { diff --git a/internal/localnet/up_test.go b/internal/localnet/up_test.go index abe20f68..81414b35 100644 --- a/internal/localnet/up_test.go +++ b/internal/localnet/up_test.go @@ -11,7 +11,6 @@ import ( "testing" "time" - "github.com/bitdynamics-ab/canton-devkit/internal/poc/godamlprobe" "github.com/bitdynamics-ab/canton-devkit/internal/registry" "github.com/bitdynamics-ab/canton-devkit/internal/splice" ) @@ -394,15 +393,15 @@ func (s *composeRunnerStub) WaitForHealthy(context.Context) error { return nil } -// TestRunUp_LedgerUnreachableFails pins that a failed go-daml Ledger -// API probe after Docker health marks the instance failed and returns +// TestRunUp_LedgerUnreachableFails pins that a failed Ledger API probe +// after Docker health marks the instance failed and returns // ExitRuntimeFailure — containers-up-but-ledger-down must not look like // a successful bring-up. func TestRunUp_LedgerUnreachableFails(t *testing.T) { t.Setenv("CANTON_DEVKIT_REGISTRY", t.TempDir()) prev := ensureLedgerReadyFn - ensureLedgerReadyFn = func(context.Context, string, map[string]int, map[string]registry.Credential) (godamlprobe.Result, error) { - return godamlprobe.Result{}, errors.New("connection refused") + ensureLedgerReadyFn = func(context.Context, string, map[string]int, map[string]registry.Credential) (LedgerReadyResult, error) { + return LedgerReadyResult{}, errors.New("connection refused") } t.Cleanup(func() { ensureLedgerReadyFn = prev }) diff --git a/internal/poc/godamlprobe/doc.go b/internal/poc/godamlprobe/doc.go deleted file mode 100644 index 438edd40..00000000 --- a/internal/poc/godamlprobe/doc.go +++ /dev/null @@ -1,9 +0,0 @@ -// Package godamlprobe dials a Canton participant Ledger API endpoint using -// github.com/noders-team/go-daml and reports whether the participant accepts -// the call (connectivity + auth). -// -// Production readiness (localnet up / start / restart after Docker health) -// calls Probe via internal/localnet.ensureLedgerReady. Command paths that -// submit ledger transactions continue to use internal/canton/ledger -// (dazl-client); this package is the connectivity probe only. -package godamlprobe diff --git a/internal/poc/godamlprobe/probe.go b/internal/poc/godamlprobe/probe.go deleted file mode 100644 index d1a796e7..00000000 --- a/internal/poc/godamlprobe/probe.go +++ /dev/null @@ -1,78 +0,0 @@ -package godamlprobe - -import ( - "context" - "fmt" - "strings" - - "github.com/noders-team/go-daml/pkg/auth" - godamlclient "github.com/noders-team/go-daml/pkg/client" - "github.com/noders-team/go-daml/pkg/model" -) - -// Options configures a go-daml connectivity probe against a participant -// Ledger API gRPC endpoint. -type Options struct { - // Endpoint is host:port, e.g. "localhost:5001". Required. - Endpoint string - - // Token is a bearer JWT the participant accepts. Empty is allowed only - // for participants configured without auth (rare outside tests). - Token string -} - -// Result is what a successful probe observed. Offset is the ledger end; -// LedgerAPIVersion is the participant-reported API version string. -type Result struct { - Endpoint string - LedgerAPIVersion string - Offset int64 -} - -// Probe dials the participant with go-daml and runs two cheap unaries: -// GetLedgerApiVersion (go-daml's Ping) and GetLedgerEnd. Success means the -// endpoint is reachable and the token (if any) was accepted. -// -// LocalNet participants speak plaintext gRPC; go-daml defaults to insecure -// transport when no TLS config is set, which matches that layout. -func Probe(ctx context.Context, opts Options) (Result, error) { - endpoint := strings.TrimSpace(opts.Endpoint) - if endpoint == "" { - return Result{}, fmt.Errorf("godamlprobe.Probe: endpoint is required") - } - - cl, err := godamlclient.NewDamlClient(endpoint, auth.NewBearerTokenProvider(opts.Token)). - Build(ctx) - if err != nil { - return Result{}, fmt.Errorf("godamlprobe.Probe: dial %s: %w", endpoint, err) - } - defer cl.Close() - - // GetLedgerAPIVersion is go-daml's Ping implementation — one unary - // that proves dial + auth + stub wiring. GetLedgerEnd is the same - // readiness probe internal/canton/ledger documents for dazl-client. - ver, err := cl.VersionService.GetLedgerAPIVersion(ctx, &model.GetLedgerAPIVersionRequest{}) - if err != nil { - return Result{}, fmt.Errorf("godamlprobe.Probe: GetLedgerAPIVersion %s: %w", endpoint, err) - } - - end, err := cl.StateService.GetLedgerEnd(ctx, &model.GetLedgerEndRequest{}) - if err != nil { - return Result{}, fmt.Errorf("godamlprobe.Probe: GetLedgerEnd %s: %w", endpoint, err) - } - - version := "" - if ver != nil { - version = ver.Version - } - var offset int64 - if end != nil { - offset = end.Offset - } - - return Result{ - Endpoint: endpoint, - LedgerAPIVersion: version, - Offset: offset, - }, nil -} diff --git a/internal/poc/godamlprobe/probe_integration_test.go b/internal/poc/godamlprobe/probe_integration_test.go deleted file mode 100644 index 2aa1bdcf..00000000 --- a/internal/poc/godamlprobe/probe_integration_test.go +++ /dev/null @@ -1,64 +0,0 @@ -//go:build integration - -package godamlprobe_test - -import ( - "context" - "os" - "strings" - "testing" - "time" - - "github.com/bitdynamics-ab/canton-devkit/internal/poc/godamlprobe" - "github.com/bitdynamics-ab/canton-devkit/internal/splice" -) - -// TestProbe_AgainstLocalNet exercises go-daml against a running LocalNet -// participant. Skips cleanly when the endpoint is not reachable so -// developers without a booted instance are not blocked. -// -// go test -tags=integration ./internal/poc/godamlprobe/... -// -// Override the endpoint with CANTON_DEVKIT_TEST_LEDGER_ENDPOINT when the -// registry-mapped host port is not localhost:5001. -func TestProbe_AgainstLocalNet(t *testing.T) { - endpoint := os.Getenv("CANTON_DEVKIT_TEST_LEDGER_ENDPOINT") - if endpoint == "" { - endpoint = "localhost:5001" - } - - token, err := splice.SignToken(splice.CredentialInputs{ - Role: splice.RoleAppProvider, - User: "ledger-api-user", - Audience: "https://canton.network.global", - }) - if err != nil { - t.Fatalf("SignToken: %v", err) - } - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - res, err := godamlprobe.Probe(ctx, godamlprobe.Options{ - Endpoint: endpoint, - Token: token, - }) - if err != nil { - if strings.Contains(err.Error(), "connection refused") || - strings.Contains(err.Error(), "Unavailable") || - strings.Contains(err.Error(), "dial") { - t.Skipf("LocalNet not reachable at %s — run `localnet up` first: %v", endpoint, err) - } - t.Fatalf("Probe: %v", err) - } - - if res.Endpoint != endpoint { - t.Errorf("Endpoint = %q, want %q", res.Endpoint, endpoint) - } - if res.LedgerAPIVersion == "" { - t.Error("LedgerAPIVersion is empty; expected a non-empty participant version string") - } - if res.Offset < 0 { - t.Errorf("Offset = %d, want >= 0", res.Offset) - } -} diff --git a/internal/poc/godamlprobe/probe_test.go b/internal/poc/godamlprobe/probe_test.go deleted file mode 100644 index 46cd6456..00000000 --- a/internal/poc/godamlprobe/probe_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package godamlprobe - -import ( - "context" - "strings" - "testing" - "time" -) - -func TestProbe_RequiresEndpoint(t *testing.T) { - t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - - _, err := Probe(ctx, Options{Token: "unused"}) - if err == nil { - t.Fatal("Probe with empty endpoint: want error, got nil") - } - if !strings.Contains(err.Error(), "endpoint is required") { - t.Fatalf("error = %q, want substring %q", err.Error(), "endpoint is required") - } -} - -func TestProbe_UnreachableEndpoint(t *testing.T) { - t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - defer cancel() - - // Reserved TEST-NET-1 address that is not listening locally. - _, err := Probe(ctx, Options{ - Endpoint: "127.0.0.1:1", - Token: "not-a-real-token", - }) - if err == nil { - t.Fatal("Probe against closed port: want error, got nil") - } - msg := err.Error() - if !strings.Contains(msg, "godamlprobe.Probe") { - t.Fatalf("error = %q, want godamlprobe.Probe prefix", msg) - } -}