Skip to content

Commit ca31efb

Browse files
authored
feat: Canton MCMS proposal analyzer (#1031)
## Desc Canton MCMS proposal analyzer ## Todos When MCMS get's new release, change go mod to point to that release: smartcontractkit/mcms#779
1 parent ab657bd commit ca31efb

14 files changed

Lines changed: 1549 additions & 14 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": minor
3+
---
4+
5+
feat(analyzer): implement Canton MCMS proposal analyzer
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package analyzer
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"reflect"
7+
"sort"
8+
9+
mcmssdk "github.com/smartcontractkit/mcms/sdk"
10+
mcmscantonsdk "github.com/smartcontractkit/mcms/sdk/canton"
11+
"github.com/smartcontractkit/mcms/types"
12+
)
13+
14+
func AnalyzeCantonTransactions(ctx ProposalContext, chainSelector uint64, txs []types.Transaction) ([]*DecodedCall, error) {
15+
decoder := mcmscantonsdk.NewDecoder()
16+
decodedTxs := make([]*DecodedCall, len(txs))
17+
for i, op := range txs {
18+
analyzedTransaction, err := AnalyzeCantonTransaction(ctx, decoder, chainSelector, op)
19+
if err != nil {
20+
return nil, fmt.Errorf("failed to analyze Canton transaction %d: %w", i, err)
21+
}
22+
decodedTxs[i] = analyzedTransaction
23+
}
24+
25+
return decodedTxs, nil
26+
}
27+
28+
// AnalyzeCantonTransaction decodes a single Canton MCMS transaction. Each transaction already
29+
// describes the target call (Daml choice or factory deploy) via its AdditionalFields, so it is decoded directly
30+
func AnalyzeCantonTransaction(ctx ProposalContext, decoder *mcmscantonsdk.Decoder, chainSelector uint64, mcmsTx types.Transaction) (*DecodedCall, error) {
31+
contractType, contractVersion := resolveContractInfo(ctx, chainSelector, mcmsTx)
32+
33+
var additionalFields mcmscantonsdk.AdditionalFields
34+
if err := json.Unmarshal(mcmsTx.AdditionalFields, &additionalFields); err != nil {
35+
return nil, fmt.Errorf("failed to unmarshal Canton additional fields: %w", err)
36+
}
37+
38+
// Pass the resolved contract type as the fallback contract-type key
39+
// The decoder prefers the entity name parsed from AdditionalFields.TargetTemplateID when present.
40+
decodedOp, err := decoder.Decode(mcmsTx, contractType)
41+
if err != nil {
42+
errStr := fmt.Errorf("failed to decode Canton transaction %q: %w", additionalFields.FunctionName, err)
43+
44+
return &DecodedCall{
45+
Address: mcmsTx.To,
46+
Method: errStr.Error(),
47+
ContractType: contractType,
48+
ContractVersion: contractVersion,
49+
}, nil
50+
}
51+
52+
namedArgs, err := cantonToNamedFields(decodedOp)
53+
if err != nil {
54+
return nil, fmt.Errorf("failed to convert decoded operation to named arguments: %w", err)
55+
}
56+
57+
return &DecodedCall{
58+
Address: mcmsTx.To,
59+
Method: decodedOp.MethodName(),
60+
Inputs: namedArgs,
61+
Outputs: []NamedField{},
62+
ContractType: contractType,
63+
ContractVersion: contractVersion,
64+
}, nil
65+
}
66+
67+
// cantonToNamedFields is like toNamedFields but uses cantonFieldValue so that nested Daml records
68+
// (returned as map[string]any by the Canton decoder's toDisplayArg) become StructField.
69+
// Kept here rather than in utils.go to avoid leaking Canton-specific logic into the shared utility.
70+
func cantonToNamedFields(decodedOp mcmssdk.DecodedOperation) ([]NamedField, error) {
71+
args := decodedOp.Args()
72+
keys := decodedOp.Keys()
73+
if len(keys) != len(args) {
74+
return nil, fmt.Errorf("mismatched keys and arguments length: %d keys, %d arguments", len(keys), len(args))
75+
}
76+
namedArgs := make([]NamedField, len(args))
77+
for i := range args {
78+
namedArgs[i] = NamedField{
79+
Name: keys[i],
80+
Value: cantonFieldValue(args[i]),
81+
RawValue: args[i],
82+
}
83+
}
84+
85+
return namedArgs, nil
86+
}
87+
88+
// cantonFieldValue is like getFieldValue but also converts map[string]any to StructField.
89+
// Canton decoded args use map[string]any for nested Daml records (via toDisplayArg in the Canton
90+
// decoder). This is Canton-scoped: other chains (e.g. TON) also return map[string]any in some
91+
// decoded fields but rely on the default fmt.Sprintf("%v", ...) rendering via getFieldValue.
92+
func cantonFieldValue(argument any) FieldValue {
93+
if m, ok := argument.(map[string]any); ok {
94+
return mapToStructField(m)
95+
}
96+
// For slices, recurse so nested maps within arrays are also converted.
97+
// Recurse into slices/arrays so nested maps within them are also converted — but not []byte,
98+
// which must fall through to getFieldValue so it renders as BytesField (hex-preview) rather
99+
// than ArrayField with individual byte elements.
100+
if _, isByteSlice := argument.([]byte); !isByteSlice {
101+
if kind := reflect.TypeOf(argument); kind != nil {
102+
if kind.Kind() == reflect.Array || kind.Kind() == reflect.Slice {
103+
array := ArrayField{}
104+
v := reflect.ValueOf(argument)
105+
for i := range v.Len() {
106+
array.Elements = append(array.Elements, cantonFieldValue(v.Index(i).Interface()))
107+
}
108+
109+
return array
110+
}
111+
}
112+
}
113+
114+
return getFieldValue(argument)
115+
}
116+
117+
// mapToStructField converts a map[string]any to a StructField with sorted keys.
118+
func mapToStructField(m map[string]any) StructField {
119+
keys := make([]string, 0, len(m))
120+
for k := range m {
121+
keys = append(keys, k)
122+
}
123+
sort.Strings(keys)
124+
125+
fields := make([]NamedField, 0, len(m))
126+
for _, k := range keys {
127+
fields = append(fields, NamedField{
128+
Name: k,
129+
Value: cantonFieldValue(m[k]),
130+
})
131+
}
132+
133+
return StructField{Fields: fields}
134+
}

0 commit comments

Comments
 (0)