Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func runCodeGen(darFile, outputDir, pkgFile string, debugMode bool, generateHexC
dalfToProcess = append(dalfToProcess, manifest.MainDalf)
dalfToProcess = append(dalfToProcess, dalfs...)

result, err := codegen.CodegenDalfs(dalfToProcess, reader, pkgFile, dalfManifest, generateHexCodec, model2.ExternalPackages{})
result, err := codegen.CodegenDalfs(dalfToProcess, reader, pkgFile, dalfManifest, generateHexCodec, model2.ExternalPackages{}, model2.FieldHints{})
if err != nil {
return err
}
Expand Down
6 changes: 3 additions & 3 deletions codegen/astgen/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package astgen
import (
"fmt"

"github.com/smartcontractkit/go-daml/codegen/astgen/v3"
v3 "github.com/smartcontractkit/go-daml/codegen/astgen/v3"
model2 "github.com/smartcontractkit/go-daml/codegen/model"
)

Expand All @@ -18,10 +18,10 @@ type AstGen interface {
GetTemplateStructs(ifcByModule map[string]model2.InterfaceMap) (map[string]*model2.TmplStruct, model2.ExternalPackages, error)
}

func GetAstGenFromVersion(payload []byte, ext model2.ExternalPackages, ver string) (AstGen, error) {
func GetAstGenFromVersion(payload []byte, ext model2.ExternalPackages, hints model2.FieldHints, ver string) (AstGen, error) {
switch ver {
case V3:
return v3.NewCodegenAst(payload, ext), nil
return v3.NewCodegenAst(payload, ext, hints), nil
default:
return nil, fmt.Errorf("none supported version")
}
Expand Down
29 changes: 16 additions & 13 deletions codegen/astgen/v3/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,15 @@ type codeGenAst struct {
// For keeping track of which packages have been imported, will start off empty and be populated as we process the
// DAML LF and encounter references to external packages. This allows us to include only the necessary imports in the generated code
importedPackages map[string]model.ExternalPackage
// Caller-supplied hints for fields that need non-default hex encoding tags
fieldHints model.FieldHints
}

func NewCodegenAst(payload []byte, externalPackages model.ExternalPackages) *codeGenAst {
func NewCodegenAst(payload []byte, externalPackages model.ExternalPackages, fieldHints model.FieldHints) *codeGenAst {
return &codeGenAst{
payload: payload,
externalPackages: externalPackages,
fieldHints: fieldHints,
}
}

Expand Down Expand Up @@ -216,10 +219,10 @@ func (c *codeGenAst) getTemplates(
RawType: field.String(),
IsOptional: isOptional,
IsEnum: c.isEnumType(typeExtracted, pkg),
IsBytes: model.BytesFieldNames[fieldExtracted],
IsBytesHex: model.BytesHexFieldNames[fieldExtracted],
IsUint32: model.Uint32FieldNames[fieldExtracted],
IsUint32List: model.Uint32ListFieldNames[fieldExtracted],
IsBytes: c.fieldHints.BytesFields[fieldExtracted],
IsBytesHex: c.fieldHints.BytesHexFields[fieldExtracted],
IsUint32: c.fieldHints.Uint32Fields[fieldExtracted],
IsUint32List: c.fieldHints.Uint32ListFields[fieldExtracted],
})
}
default:
Expand Down Expand Up @@ -439,10 +442,10 @@ func (c *codeGenAst) getDataTypes(pkg *daml.Package, module *daml.Module, module
Type: typeExtracted,
RawType: field.String(),
IsOptional: isOptional,
IsBytes: model.BytesFieldNames[fieldExtracted],
IsBytesHex: model.BytesHexFieldNames[fieldExtracted],
IsUint32: model.Uint32FieldNames[fieldExtracted],
IsUint32List: model.Uint32ListFieldNames[fieldExtracted],
IsBytes: c.fieldHints.BytesFields[fieldExtracted],
IsBytesHex: c.fieldHints.BytesHexFields[fieldExtracted],
IsUint32: c.fieldHints.Uint32Fields[fieldExtracted],
IsUint32List: c.fieldHints.Uint32ListFields[fieldExtracted],
})
}
case *daml.DefDataType_Variant:
Expand All @@ -457,10 +460,10 @@ func (c *codeGenAst) getDataTypes(pkg *daml.Package, module *daml.Module, module
Type: typeExtracted,
RawType: field.String(),
IsOptional: true,
IsBytes: model.BytesFieldNames[fieldExtracted],
IsBytesHex: model.BytesHexFieldNames[fieldExtracted],
IsUint32: model.Uint32FieldNames[fieldExtracted],
IsUint32List: model.Uint32ListFieldNames[fieldExtracted],
IsBytes: c.fieldHints.BytesFields[fieldExtracted],
IsBytesHex: c.fieldHints.BytesHexFields[fieldExtracted],
IsUint32: c.fieldHints.Uint32Fields[fieldExtracted],
IsUint32List: c.fieldHints.Uint32ListFields[fieldExtracted],
})
}
case *daml.DefDataType_Enum:
Expand Down
10 changes: 5 additions & 5 deletions codegen/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func GetManifest(dar fs.FS) (*model2.Manifest, error) {
return manifest, nil
}

func CodegenDalfs(dalfToProcess []string, dar fs.FS, pkgFile string, dalfManifest *model2.Manifest, generateHexCodec bool, externalPackages model2.ExternalPackages) (map[string]string, error) {
func CodegenDalfs(dalfToProcess []string, dar fs.FS, pkgFile string, dalfManifest *model2.Manifest, generateHexCodec bool, externalPackages model2.ExternalPackages, fieldHints model2.FieldHints) (map[string]string, error) {
// ensure stable processing order across runs
sort.Strings(dalfToProcess)

Expand Down Expand Up @@ -137,7 +137,7 @@ func CodegenDalfs(dalfToProcess []string, dar fs.FS, pkgFile string, dalfManifes
continue
}

pkg, err := GetAST(dalfContent, dalfManifest, ifcByModule, externalPackages)
pkg, err := GetAST(dalfContent, dalfManifest, ifcByModule, externalPackages, fieldHints)
if err != nil {
return nil, fmt.Errorf("failed to generate AST: %w", err)
}
Expand Down Expand Up @@ -268,15 +268,15 @@ func GetInterfaces(payload []byte, manifest *model2.Manifest) (map[string]*model
return nil, fmt.Errorf("unsupported sdk version %s", manifest.SdkVersion)
}

gen, err := astgen.GetAstGenFromVersion(payload, model2.ExternalPackages{}, version)
gen, err := astgen.GetAstGenFromVersion(payload, model2.ExternalPackages{}, model2.FieldHints{}, version)
if err != nil {
return nil, err
}

return gen.GetInterfaces()
}

func GetAST(payload []byte, manifest *model2.Manifest, ifcByModule map[string]model2.InterfaceMap, externalPackages model2.ExternalPackages) (*model2.Package, error) {
func GetAST(payload []byte, manifest *model2.Manifest, ifcByModule map[string]model2.InterfaceMap, externalPackages model2.ExternalPackages, fieldHints model2.FieldHints) (*model2.Package, error) {
var version string
if strings.HasPrefix(manifest.SdkVersion, astgen.V3) {
version = astgen.V3
Expand All @@ -286,7 +286,7 @@ func GetAST(payload []byte, manifest *model2.Manifest, ifcByModule map[string]mo
return nil, fmt.Errorf("unsupported sdk version %s", manifest.SdkVersion)
}

gen, err := astgen.GetAstGenFromVersion(payload, externalPackages, version)
gen, err := astgen.GetAstGenFromVersion(payload, externalPackages, fieldHints, version)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion codegen/codegen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func TestGetMainDalfV3(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, dalfContent)

ast, err := GetAST(dalfContent, manifest, nil, model.ExternalPackages{})
ast, err := GetAST(dalfContent, manifest, nil, model.ExternalPackages{}, model.FieldHints{})
require.Nil(t, err)
require.NotEmpty(t, ast.Structs)

Expand Down
73 changes: 15 additions & 58 deletions codegen/model/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,65 +80,22 @@ func (t BytesHex) IsBytesHex() bool {
// The hardcoded maps (BytesFieldNames, BytesHexFieldNames) work around this limitation by explicitly listing
// field names that need hex encoding tags.

// BytesHexFieldNames contains field names that should use uint16 length prefix
// encoding (hex:"bytes16" tag) instead of the default uint8 length prefix.
// FieldHints allows callers to declare which struct field names need non-default
// hex encoding tags. Because the Daml compiler erases type synonyms (e.g. BytesHex
// becomes Text in the compiled Daml-LF), go-daml cannot infer the correct encoding
// from the .dalf alone. Callers that know the encoding semantics of their contracts
// populate these maps and pass a FieldHints value to CodegenDalfs / GetAST.
//
// Background: The Daml BytesHex type synonym is expanded to Text by the compiler,
// so we cannot detect it from the compiled Daml LF. Instead, we use a field name
// allowlist for fields that are known to potentially exceed 255 bytes.
//
// Fields requiring bytes16 encoding (from MCMS/Codec.daml):
// - operationData: Serialized choice parameters in TimelockCall (encodeUint16)
// - predecessor: Hex hash in ScheduleBatchParams (encodeUint16)
// - salt: Hex value in ScheduleBatchParams (encodeUint16)
//
// To add a new field: add its name (case-sensitive) as a key with value true.
var BytesHexFieldNames = map[string]bool{
"operationData": true,
"predecessor": true,
"salt": true,
}

// BytesFieldNames contains field names that should use uint8 length prefix
// encoding (hex:"bytes" tag). These are fixed-size hex fields ≤255 bytes.
//
// From MCMS/CCIP contracts:
// - signerAddress: EVM signer address (20 bytes)
// - chainFamilySelector: Chain family selector (4 bytes)
// - root: Merkle root hash (32 bytes)
// - newRoot: New merkle root hash (32 bytes)
var BytesFieldNames = map[string]bool{
"signerAddress": true,
"chainFamilySelector": true,
"root": true,
"newRoot": true,
}

// Uint32FieldNames contains field names where INT64 should encode as 4-byte uint32.
// This matches the Daml MCMS/Codec.daml encoding which uses encodeUint32 for these fields.
//
// From MCMS/Codec.daml encodeSignerInfo:
// - signerIndex: encodeUint32 (4 bytes)
// - signerGroup: encodeUint32 (4 bytes)
var Uint32FieldNames = map[string]bool{
"signerIndex": true,
"signerGroup": true,
}

// Uint32ListFieldNames contains field names where []INT64 should encode as
// length + uint32 elements (4 bytes each) instead of uint64 elements (8 bytes).
// This matches the Daml MCMS/Codec.daml encoding which uses encodeUint32 for each element.
//
// From MCMS/Codec.daml encodeSetConfigParams and encodeMultisigConfig:
// - groupQuorums: list of encodeUint32 (4 bytes each)
// - groupParents: list of encodeUint32 (4 bytes each)
// - apGroupQuorums: used in AdminParams.AP_SetConfig
// - apGroupParents: used in AdminParams.AP_SetConfig
var Uint32ListFieldNames = map[string]bool{
"groupQuorums": true,
"groupParents": true,
"apGroupQuorums": true,
"apGroupParents": true,
// An empty (zero-value) FieldHints is valid and means no special encoding is applied.
type FieldHints struct {
// BytesFields: field names that should receive a hex:"bytes" tag (uint8 length prefix, ≤255 bytes).
BytesFields map[string]bool
// BytesHexFields: field names that should receive a hex:"bytes16" tag (uint16 length prefix).
BytesHexFields map[string]bool
// Uint32Fields: field names where INT64 should be encoded as a 4-byte uint32 (hex:"uint32" tag).
Uint32Fields map[string]bool
// Uint32ListFields: field names where []INT64 should be encoded as []uint32 (hex:"[]uint32" tag).
Uint32ListFields map[string]bool
}

type Int64 struct {
Expand Down
28 changes: 21 additions & 7 deletions codegen/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,29 @@ func TestDecapitalize(t *testing.T) {
}
}

func TestBytesHexFieldNames(t *testing.T) {
// Test that BytesHexFieldNames contains expected entries
if !model.BytesHexFieldNames["operationData"] {
t.Error("BytesHexFieldNames should contain 'operationData'")
func TestFieldHints(t *testing.T) {
// An empty FieldHints should not match any field name
empty := model.FieldHints{}
if empty.BytesHexFields["operationData"] {
t.Error("empty FieldHints.BytesHexFields should not match any field")
}
if empty.BytesFields["signerAddress"] {
t.Error("empty FieldHints.BytesFields should not match any field")
}

// Test that non-BytesHex fields are not in the map
if model.BytesHexFieldNames["someOtherField"] {
t.Error("BytesHexFieldNames should not contain 'someOtherField'")
// A populated FieldHints should match exactly the configured fields
hints := model.FieldHints{
BytesHexFields: map[string]bool{"operationData": true},
BytesFields: map[string]bool{"signerAddress": true},
}
if !hints.BytesHexFields["operationData"] {
t.Error("hints.BytesHexFields should contain 'operationData'")
}
if hints.BytesHexFields["someOtherField"] {
t.Error("hints.BytesHexFields should not contain 'someOtherField'")
}
if !hints.BytesFields["signerAddress"] {
t.Error("hints.BytesFields should contain 'signerAddress'")
}
}

Expand Down