Module 7 · Phase 1 · ~90 min · Real Canton + Go

Phase 1 — Connect to a Canton Sandbox

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.

The starter project

Use cantonctl-starter/ in this same directory as your working module. It already has:

cd cantonctl-starter
go mod tidy
go build ./...   # should compile but Ping returns "not implemented"

Step 1 — Run the Canton sandbox

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.

Step 2 — Get the Ledger API protos

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

Step 3 — Generate Go stubs

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

Step 4 — Implement Ping

Two options for what "ping" calls:

  1. VersionService.GetLedgerApiVersion — returns the participant's API version. Conceptually the most direct "is this real?" call.
  2. The gRPC health check protocol (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.)

Step 5 — Run it

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"}

Common failures

Done when

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.