Module 7 · Phase 1 · ~90 min · Real Canton + Go
Stand up a local Canton sandbox in Docker, generate Go stubs from the official Ledger API protos, dial the participant's gRPC endpoint, and confirm you can talk to it. Done when cantonctl ping succeeds.
Use cantonctl-starter/ in this same directory as your working module. It already has:
go.mod with grpc + protobuf deps.buf.yaml + buf.gen.yaml for protobuf codegen.internal/ledger/client.go with a Dial function and a placeholder Ping.cmd/cantonctl/main.go with a ping subcommand wired up.cd cantonctl-starter
go mod tidy
go build ./... # should compile but Ping returns "not implemented"
Check the Canton 3.5 docs for the current published Docker image. The shape:
docker run --rm -it \
-p 5011:5011 \
digitalasset/canton-open-source:<version> \
daemon --config /examples/01-simple-topology/simple-topology.conf
Wait for "Ledger API server listening on 5011" in the logs.
Verify with grpcurl (separate terminal):
grpcurl -plaintext localhost:5011 list
You should see a list of services including com.daml.ledger.api.v2.CommandService, UpdateService, etc.
# In some scratch dir, NOT inside cantonctl-starter:
git clone https://github.com/digital-asset/daml.git
cd daml
git checkout v<version-matching-your-sandbox>
# Find the proto tree for Ledger API v2:
find . -path '*ledger-api*v2*' -name '*.proto' | head -20
The path varies by daml version. Once located, copy the entire v2 directory (and any imported proto trees it needs — typically google/api/, scalapb/, etc.) into your cantonctl-starter/proto/.
Practical tip: when you run buf generate, it'll complain if it can't find an imported proto. Iteratively copy missing files until it stops complaining.
cd cantonctl-starter
buf generate
You should now see .pb.go and _grpc.pb.go files generated under proto/. Update internal/ledger/client.go's import paths to match the actual package paths the codegen produced (look for the go_package option on the protos).
go mod tidy
This pulls in any transitive dependencies (well-known types, etc.).
Two options for what "ping" calls:
grpc.health.v1.Health/Check) — if Canton's participant exposes it. Standard, machine-readable.Pick one. Update Ping in internal/ledger/client.go:
import versionpb "example.com/cantonctl/proto/com/daml/ledger/api/v2"
func (c *Client) Ping(ctx context.Context) error {
vc := versionpb.NewVersionServiceClient(c.conn)
resp, err := vc.GetLedgerApiVersion(ctx, &versionpb.GetLedgerApiVersionRequest{})
if err != nil {
return fmt.Errorf("version: %w", err)
}
slog.Info("sandbox version", "version", resp.GetVersion())
return nil
}
(Real package paths and method signatures vary by Canton version — adjust to whatever buf generate produced.)
go run ./cmd/cantonctl ping --endpoint localhost:5011
# Expected output (JSON to stderr):
{"time":"...","level":"INFO","msg":"sandbox version","version":"3.5.0"}
{"time":"...","level":"INFO","msg":"ping ok","endpoint":"localhost:5011"}
docker ps + the sandbox logs.NewClient expects host:port without scheme. Pass localhost:5011, not grpc://localhost:5011.cantonctl ping --endpoint localhost:5011 exits 0 with a structured log line containing the sandbox's version. The starter project compiles cleanly, go vet ./... passes.
You've now got a real Go binary that talks to Canton over gRPC. Phases 2–4 build on this exact directory.