Module 7 · Phase 2 · ~120 min · Real Go & live Canton
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.
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.
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)
Canton commands are constructed from the protobuf CreateCommand / ExerciseCommand shapes wrapped in a Command. The fields you need:
"cantonctl".PackageService.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}}
}
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.
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.
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.
Token field in your Config and a unary client interceptor that injects it as authorization: Bearer ....grpcurl + the package service to inspect the template's expected fields; types matter (Text vs Party vs Numeric).ListPackages.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.