Skip to content

Commit e130c59

Browse files
authored
refactor(modular-cmds): introduce mcms cmds (#727)
## MCMS Commands Migration ### Migrated to Modular Commands (`engine/cld/commands/mcms/`) The following commands have been fully migrated to the new modular architecture: | Command | Description | |---------|-------------| | `analyze-proposal` | Analyzes MCMS proposals | | `convert-upf` | Converts proposals to Universal Proposal Format | | `execute-fork` | Executes proposals on forked environments (Anvil) | | `error-decode-evm` | Decodes EVM transaction errors | These commands are delegated from the legacy `mcmsv2` for backward compatibility. All deprecated code is removed. Code is now split into several files for better readibility. https://smartcontract-it.atlassian.net/browse/CLD-1159
1 parent 901ba0b commit e130c59

59 files changed

Lines changed: 3007 additions & 4804 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

engine/cld/commands/commands.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ import (
3232

3333
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/commands/addressbook"
3434
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/commands/datastore"
35+
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/commands/mcms"
3536
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/commands/state"
3637
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/domain"
38+
"github.com/smartcontractkit/chainlink-deployments-framework/experimental/analyzer"
3739
"github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger"
3840
)
3941

@@ -80,3 +82,19 @@ func (c *Commands) AddressBook(dom domain.Domain) (*cobra.Command, error) {
8082
Domain: dom,
8183
})
8284
}
85+
86+
// MCMSConfig holds configuration for MCMS commands.
87+
type MCMSConfig struct {
88+
// ProposalContextProvider creates proposal context for analysis.
89+
// This is domain-specific and must be provided by the user.
90+
ProposalContextProvider analyzer.ProposalContextProvider
91+
}
92+
93+
// MCMS creates the mcms command group for proposal analysis and conversion.
94+
func (c *Commands) MCMS(dom domain.Domain, cfg MCMSConfig) (*cobra.Command, error) {
95+
return mcms.NewCommand(mcms.Config{
96+
Logger: c.lggr,
97+
Domain: dom,
98+
ProposalContextProvider: cfg.ProposalContextProvider,
99+
})
100+
}

engine/cld/commands/flags/flags.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ func MustString(s string, _ error) string { return s }
1818
// Safe to use with registered flags where GetBool cannot fail.
1919
func MustBool(b bool, _ error) bool { return b }
2020

21+
// MustUint64 returns the uint64 value, ignoring the error.
22+
// Safe to use with registered flags where GetUint64 cannot fail.
23+
func MustUint64(u uint64, _ error) uint64 { return u }
24+
2125
// Environment adds the required --environment/-e flag to a command.
2226
// Retrieve the value with cmd.Flags().GetString("environment").
2327
//
@@ -68,3 +72,48 @@ func Output(cmd *cobra.Command, defaultValue string) {
6872
return pflag.NormalizedName(name)
6973
})
7074
}
75+
76+
// --- MCMS shared flags ---
77+
78+
// Proposal adds the required --proposal/-p flag for specifying proposal file path.
79+
// Retrieve the value with cmd.Flags().GetString("proposal").
80+
//
81+
// Usage:
82+
//
83+
// flags.Proposal(cmd)
84+
// // later in RunE:
85+
// proposalPath, _ := cmd.Flags().GetString("proposal")
86+
func Proposal(cmd *cobra.Command) {
87+
cmd.Flags().StringP("proposal", "p", "", "Absolute file path containing the proposal (required)")
88+
_ = cmd.MarkFlagRequired("proposal")
89+
}
90+
91+
// ProposalKind adds the --proposalKind/-k flag for specifying proposal type.
92+
// The defaultKind parameter should be a valid proposal kind string (e.g., "timelock").
93+
// Retrieve the value with cmd.Flags().GetString("proposalKind").
94+
//
95+
// Usage:
96+
//
97+
// flags.ProposalKind(cmd, "timelock")
98+
// // later in RunE:
99+
// kind, _ := cmd.Flags().GetString("proposalKind")
100+
func ProposalKind(cmd *cobra.Command, defaultKind string) {
101+
cmd.Flags().StringP("proposalKind", "k", defaultKind, "The type of proposal being ingested")
102+
}
103+
104+
// ChainSelector adds the --selector/-s flag for specifying chain selector.
105+
// If required is true, the flag is marked as required.
106+
// Retrieve the value with cmd.Flags().GetUint64("selector").
107+
//
108+
// Usage:
109+
//
110+
// flags.ChainSelector(cmd, false) // optional
111+
// flags.ChainSelector(cmd, true) // required
112+
// // later in RunE:
113+
// selector, _ := cmd.Flags().GetUint64("selector")
114+
func ChainSelector(cmd *cobra.Command, required bool) {
115+
cmd.Flags().Uint64P("selector", "s", 0, "Chain selector")
116+
if required {
117+
_ = cmd.MarkFlagRequired("selector")
118+
}
119+
}
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
package mcms
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
chainsel "github.com/smartcontractkit/chain-selectors"
8+
"github.com/smartcontractkit/mcms"
9+
"github.com/smartcontractkit/mcms/sdk"
10+
"github.com/smartcontractkit/mcms/sdk/aptos"
11+
"github.com/smartcontractkit/mcms/sdk/evm"
12+
"github.com/smartcontractkit/mcms/sdk/solana"
13+
"github.com/smartcontractkit/mcms/sdk/sui"
14+
"github.com/smartcontractkit/mcms/sdk/ton"
15+
"github.com/smartcontractkit/mcms/types"
16+
"github.com/xssnick/tonutils-go/tlb"
17+
18+
suibindings "github.com/smartcontractkit/chainlink-sui/bindings"
19+
)
20+
21+
// getInspectorFromChainSelector returns an inspector for the given chain selector.
22+
func getInspectorFromChainSelector(cfg *forkConfig) (sdk.Inspector, error) {
23+
fam, err := types.GetChainSelectorFamily(types.ChainSelector(cfg.chainSelector))
24+
if err != nil {
25+
return nil, fmt.Errorf("error getting chain family: %w", err)
26+
}
27+
28+
var inspector sdk.Inspector
29+
switch fam {
30+
case chainsel.FamilyEVM:
31+
evmChain := cfg.blockchains.EVMChains()[cfg.chainSelector]
32+
inspector = evm.NewInspector(evmChain.Client)
33+
case chainsel.FamilySolana:
34+
solanaChain := cfg.blockchains.SolanaChains()[cfg.chainSelector]
35+
inspector = solana.NewInspector(solanaChain.Client)
36+
case chainsel.FamilyAptos:
37+
role, err := aptosRoleFromProposal(cfg.timelockProposal)
38+
if err != nil {
39+
return nil, fmt.Errorf("error getting aptos role from proposal: %w", err)
40+
}
41+
aptosChain := cfg.blockchains.AptosChains()[cfg.chainSelector]
42+
inspector = aptos.NewInspector(aptosChain.Client, *role)
43+
case chainsel.FamilySui:
44+
metadata, err := suiMetadataFromProposal(types.ChainSelector(cfg.chainSelector), cfg.timelockProposal)
45+
if err != nil {
46+
return nil, fmt.Errorf("error getting sui metadata from proposal: %w", err)
47+
}
48+
suiChain := cfg.blockchains.SuiChains()[cfg.chainSelector]
49+
inspector, err = sui.NewInspector(suiChain.Client, suiChain.Signer, metadata.McmsPackageID, metadata.Role)
50+
if err != nil {
51+
return nil, fmt.Errorf("error creating sui inspector: %w", err)
52+
}
53+
case chainsel.FamilyTon:
54+
tonChain := cfg.blockchains.TonChains()[cfg.chainSelector]
55+
inspector = ton.NewInspector(tonChain.Client)
56+
default:
57+
return nil, fmt.Errorf("unsupported chain family %s", fam)
58+
}
59+
60+
return inspector, nil
61+
}
62+
63+
// createExecutable creates an MCMS executable for the proposal.
64+
func createExecutable(cfg *forkConfig) (*mcms.Executable, error) {
65+
executors := make(map[types.ChainSelector]sdk.Executor, len(cfg.proposal.ChainMetadata))
66+
for chainSelector := range cfg.proposal.ChainMetadata {
67+
if cfg.chainSelector == 0 || cfg.chainSelector == uint64(chainSelector) {
68+
executor, err := getExecutorWithChainOverride(cfg, chainSelector)
69+
if err != nil {
70+
return &mcms.Executable{}, fmt.Errorf("unable to get executor with chain override: %w", err)
71+
}
72+
executors[chainSelector] = executor
73+
}
74+
}
75+
76+
return mcms.NewExecutable(&cfg.proposal, executors)
77+
}
78+
79+
// createTimelockExecutable creates a timelock executable for the proposal.
80+
func createTimelockExecutable(ctx context.Context, cfg *forkConfig) (*mcms.TimelockExecutable, error) {
81+
executors := make(map[types.ChainSelector]sdk.TimelockExecutor, len(cfg.timelockProposal.ChainMetadata))
82+
for chainSelector := range cfg.timelockProposal.ChainMetadata {
83+
if cfg.chainSelector != 0 && cfg.chainSelector != uint64(chainSelector) {
84+
continue
85+
}
86+
executor, err := getTimelockExecutorWithChainOverride(cfg, chainSelector)
87+
if err != nil {
88+
return &mcms.TimelockExecutable{}, err
89+
}
90+
executors[chainSelector] = executor
91+
}
92+
93+
return mcms.NewTimelockExecutable(ctx, cfg.timelockProposal, executors)
94+
}
95+
96+
// getExecutorWithChainOverride returns an executor for the given chain selector.
97+
func getExecutorWithChainOverride(cfg *forkConfig, chainSelector types.ChainSelector) (sdk.Executor, error) {
98+
family, err := types.GetChainSelectorFamily(chainSelector)
99+
if err != nil {
100+
return nil, fmt.Errorf("error getting chain family: %w", err)
101+
}
102+
103+
encoders, err := cfg.proposal.GetEncoders()
104+
if err != nil {
105+
return nil, fmt.Errorf("error getting encoders: %w", err)
106+
}
107+
encoder, ok := encoders[chainSelector]
108+
if !ok {
109+
return nil, fmt.Errorf("unable to get encoder from proposal for chain selector %v", chainSelector)
110+
}
111+
112+
switch family {
113+
case chainsel.FamilyEVM:
114+
evmEncoder, ok := encoder.(*evm.Encoder)
115+
if !ok {
116+
return nil, fmt.Errorf("invalid encoder type: %T", encoder)
117+
}
118+
c := cfg.blockchains.EVMChains()[uint64(chainSelector)]
119+
120+
return evm.NewExecutor(evmEncoder, c.Client, c.DeployerKey), nil
121+
122+
case chainsel.FamilySolana:
123+
solanaEncoder, ok := encoder.(*solana.Encoder)
124+
if !ok {
125+
return nil, fmt.Errorf("invalid encoder type: %T", encoder)
126+
}
127+
c := cfg.blockchains.SolanaChains()[uint64(chainSelector)]
128+
129+
return solana.NewExecutor(solanaEncoder, c.Client, *c.DeployerKey), nil
130+
131+
case chainsel.FamilyAptos:
132+
aptosEncoder, ok := encoder.(*aptos.Encoder)
133+
if !ok {
134+
return nil, fmt.Errorf("error getting encoder for chain %d", cfg.chainSelector)
135+
}
136+
role, err := aptosRoleFromProposal(cfg.timelockProposal)
137+
if err != nil {
138+
return nil, fmt.Errorf("error getting aptos role from proposal: %w", err)
139+
}
140+
c := cfg.blockchains.AptosChains()[uint64(chainSelector)]
141+
142+
return aptos.NewExecutor(c.Client, c.DeployerSigner, aptosEncoder, *role), nil
143+
144+
case chainsel.FamilySui:
145+
suiEncoder, ok := encoder.(*sui.Encoder)
146+
if !ok {
147+
return nil, fmt.Errorf("error getting encoder for chain %d", cfg.chainSelector)
148+
}
149+
metadata, err := suiMetadataFromProposal(chainSelector, cfg.timelockProposal)
150+
if err != nil {
151+
return nil, fmt.Errorf("error getting sui metadata from proposal: %w", err)
152+
}
153+
c := cfg.blockchains.SuiChains()[uint64(chainSelector)]
154+
entrypointEncoder := suibindings.NewCCIPEntrypointArgEncoder(metadata.RegistryObj, metadata.DeployerStateObj)
155+
156+
return sui.NewExecutor(c.Client, c.Signer, suiEncoder, entrypointEncoder, metadata.McmsPackageID, metadata.Role, cfg.timelockProposal.ChainMetadata[chainSelector].MCMAddress, metadata.AccountObj, metadata.RegistryObj, metadata.TimelockObj)
157+
158+
case chainsel.FamilyTon:
159+
tonEncoder, ok := encoder.(*ton.Encoder)
160+
if !ok {
161+
return nil, fmt.Errorf("invalid encoder type for TON chain %d: expected *ton.Encoder, got %T", chainSelector, encoder)
162+
}
163+
c := cfg.blockchains.TonChains()[uint64(chainSelector)]
164+
opts := ton.ExecutorOpts{
165+
Encoder: tonEncoder,
166+
Client: c.Client,
167+
Wallet: c.Wallet,
168+
Amount: tlb.MustFromTON(defaultTONExecutorAmount),
169+
}
170+
171+
return ton.NewExecutor(opts)
172+
173+
default:
174+
return nil, fmt.Errorf("unsupported chain family %s", family)
175+
}
176+
}
177+
178+
// getTimelockExecutorWithChainOverride returns a timelock executor for the given chain selector.
179+
func getTimelockExecutorWithChainOverride(cfg *forkConfig, chainSelector types.ChainSelector) (sdk.TimelockExecutor, error) {
180+
family, err := types.GetChainSelectorFamily(chainSelector)
181+
if err != nil {
182+
return nil, fmt.Errorf("error getting chain family: %w", err)
183+
}
184+
185+
var executor sdk.TimelockExecutor
186+
switch family {
187+
case chainsel.FamilyEVM:
188+
c := cfg.blockchains.EVMChains()[uint64(chainSelector)]
189+
executor = evm.NewTimelockExecutor(c.Client, c.DeployerKey)
190+
case chainsel.FamilySolana:
191+
c := cfg.blockchains.SolanaChains()[uint64(chainSelector)]
192+
executor = solana.NewTimelockExecutor(c.Client, *c.DeployerKey)
193+
case chainsel.FamilyAptos:
194+
c := cfg.blockchains.AptosChains()[uint64(chainSelector)]
195+
executor = aptos.NewTimelockExecutor(c.Client, c.DeployerSigner)
196+
case chainsel.FamilySui:
197+
c := cfg.blockchains.SuiChains()[uint64(chainSelector)]
198+
metadata, err := suiMetadataFromProposal(chainSelector, cfg.timelockProposal)
199+
if err != nil {
200+
return nil, fmt.Errorf("error getting sui metadata from proposal: %w", err)
201+
}
202+
entrypointEncoder := suibindings.NewCCIPEntrypointArgEncoder(metadata.RegistryObj, metadata.DeployerStateObj)
203+
executor, err = sui.NewTimelockExecutor(c.Client, c.Signer, entrypointEncoder, metadata.McmsPackageID, metadata.RegistryObj, metadata.AccountObj)
204+
if err != nil {
205+
return nil, fmt.Errorf("error creating sui timelock executor: %w", err)
206+
}
207+
case chainsel.FamilyTon:
208+
c := cfg.blockchains.TonChains()[uint64(chainSelector)]
209+
opts := ton.TimelockExecutorOpts{
210+
Client: c.Client,
211+
Wallet: c.Wallet,
212+
Amount: tlb.MustFromTON(defaultTONExecutorAmount),
213+
}
214+
215+
return ton.NewTimelockExecutor(opts)
216+
default:
217+
return nil, fmt.Errorf("unsupported chain family %s", family)
218+
}
219+
220+
return executor, nil
221+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package mcms
2+
3+
import (
4+
"errors"
5+
6+
"github.com/smartcontractkit/mcms"
7+
"github.com/smartcontractkit/mcms/sdk/aptos"
8+
"github.com/smartcontractkit/mcms/types"
9+
)
10+
11+
// aptosRoleFromProposal extracts the Aptos role from a timelock proposal.
12+
func aptosRoleFromProposal(proposal *mcms.TimelockProposal) (*aptos.TimelockRole, error) {
13+
if proposal == nil {
14+
return nil, errors.New("aptos timelock proposal is needed")
15+
}
16+
17+
switch proposal.Action {
18+
case types.TimelockActionBypass:
19+
role := aptos.TimelockRoleBypasser
20+
21+
return &role, nil
22+
case types.TimelockActionSchedule:
23+
role := aptos.TimelockRoleProposer
24+
25+
return &role, nil
26+
case types.TimelockActionCancel:
27+
role := aptos.TimelockRoleCanceller
28+
29+
return &role, nil
30+
default:
31+
return nil, errors.New("unknown timelock action")
32+
}
33+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package mcms
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
7+
"github.com/smartcontractkit/mcms"
8+
"github.com/smartcontractkit/mcms/sdk/sui"
9+
"github.com/smartcontractkit/mcms/types"
10+
)
11+
12+
// suiMetadataFromProposal extracts Sui metadata from a timelock proposal.
13+
func suiMetadataFromProposal(selector types.ChainSelector, proposal *mcms.TimelockProposal) (sui.AdditionalFieldsMetadata, error) {
14+
if proposal == nil {
15+
return sui.AdditionalFieldsMetadata{}, errors.New("sui timelock proposal is needed")
16+
}
17+
18+
var metadata sui.AdditionalFieldsMetadata
19+
err := json.Unmarshal([]byte(proposal.ChainMetadata[selector].AdditionalFields), &metadata)
20+
if err != nil {
21+
return sui.AdditionalFieldsMetadata{}, err
22+
}
23+
24+
err = metadata.Validate()
25+
if err != nil {
26+
return sui.AdditionalFieldsMetadata{}, err
27+
}
28+
29+
return metadata, nil
30+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package mcms
2+
3+
// defaultTONExecutorAmount is the default amount of TON for MCMS/Timelock executor transactions.
4+
const defaultTONExecutorAmount = "0.1"

0 commit comments

Comments
 (0)