Module 7 · Phase 2 · ~120 min · Real Go & live Canton

Phase 2 — Submit Commands

Make Canton do something. Submit a command via CommandService.SubmitAndWait; create a contract on the sandbox; receive a completion offset. By the end, cantonctl submit creates real ledger state.

What's in the sandbox

The Canton sandbox loads a default Daml model — typically something with simple template(s) you can create. The actual templates depend on the version. Use grpcurl to discover what packages and templates are loaded:

grpcurl -plaintext localhost:5011 list
grpcurl -plaintext localhost:5011 com.daml.ledger.api.v2.PackageService/ListPackages

If the sandbox loaded an example DAR, you'll see one or more package IDs. You can also upload your own DAR via the Admin API — but for this phase, working with what's pre-loaded is fine. If nothing is pre-loaded, the docs walk through uploading a sample DAR.

Step 1 — Add the CommandService stub to the client

In internal/ledger/client.go:

import commandpb "example.com/cantonctl/proto/com/daml/ledger/api/v2"

type Client struct {
    conn     *grpc.ClientConn
    commands commandpb.CommandServiceClient
    cfg      Config
}

// In Dial(), after creating conn:
c.commands = commandpb.NewCommandServiceClient(conn)

Step 2 — Build a Command

Canton commands are constructed from the protobuf CreateCommand / ExerciseCommand shapes wrapped in a Command. The fields you need:

The hardest part is constructing create_arguments — it's a recursive Value protobuf with case-typed fields. You'll write helper constructors:

func recordValue(fields ...struct{ name string; value *valuepb.Value }) *valuepb.Value {
    rec := &valuepb.Record{Fields: make([]*valuepb.RecordField, len(fields))}
    for i, f := range fields {
        rec.Fields[i] = &valuepb.RecordField{Label: f.name, Value: f.value}
    }
    return &valuepb.Value{Sum: &valuepb.Value_Record{Record: rec}}
}

func stringValue(s string) *valuepb.Value {
    return &valuepb.Value{Sum: &valuepb.Value_Text{Text: s}}
}

func partyValue(p string) *valuepb.Value {
    return &valuepb.Value{Sum: &valuepb.Value_Party{Party: p}}
}

Step 3 — Add Submit to the client

type SubmitOpts struct {
    CommandID     string
    Party         string
    PackageID     string
    ModuleName    string
    EntityName    string
    Args          *valuepb.Value
}

func (c *Client) Submit(ctx context.Context, opts SubmitOpts) (string, error) {
    create := &commandpb.CreateCommand{
        TemplateId: &valuepb.Identifier{
            PackageId:  opts.PackageID,
            ModuleName: opts.ModuleName,
            EntityName: opts.EntityName,
        },
        CreateArguments: opts.Args.GetRecord(),
    }
    cmd := &commandpb.Command{Command: &commandpb.Command_Create{Create: create}}

    req := &commandpb.SubmitAndWaitRequest{
        Commands: &commandpb.Commands{
            ApplicationId: "cantonctl",
            CommandId:     opts.CommandID,
            Party:         opts.Party,         // older field; some versions use ActAs []string
            Commands:      []*commandpb.Command{cmd},
        },
    }
    resp, err := c.commands.SubmitAndWaitForUpdateId(ctx, req)
    if err != nil {
        return "", fmt.Errorf("submit: %w", err)
    }
    return resp.GetUpdateId(), nil
}

The exact RPC method (SubmitAndWait vs SubmitAndWaitForUpdateId vs SubmitAndWaitForTransaction) depends on the Canton version. Pick the one that returns enough information to know the submission landed.

Step 4 — CLI subcommand

In cmd/cantonctl/main.go, add a submit case that builds SubmitOpts from flags and calls c.Submit. Example flags: --party, --package, --module, --entity, --field (repeatable: name=value).

Resist the urge to over-engineer the CLI here — phase 4 is when you adopt cobra and clean it up.

Step 5 — Run it

The exact invocation depends on what's loaded in your sandbox. Example shape:

go run ./cmd/cantonctl submit \
  --party Alice \
  --package <packageID-from-grpcurl> \
  --module Iou \
  --entity Iou \
  --field issuer=Alice \
  --field owner=Bob \
  --field amount=100.0 \
  --field currency=USD

If it returns an update_id (a long hex string), you just created a contract on Canton from Go. That's the milestone.

Common failures

Done when

One successful submit returns a non-empty update_id, and re-running with the same command_id produces the same update_id (idempotency). You've created real Canton state from Go.