From 7b384f7f9a0907183c56f024e377ff5c0d107df3 Mon Sep 17 00:00:00 2001 From: Gustavo Gama Date: Wed, 14 Jan 2026 16:48:47 -0300 Subject: [PATCH] feat: add mcms/utils package with buildInspector and buildConverters --- sdk/aptos/inspector.go | 2 + sdk/solana/inspector.go | 2 + sdk/sui/chain_metadata.go | 13 ++++ sdk/sui/inspector.go | 2 + utils/aptos.go | 35 +++++++++ utils/converters.go | 46 +++++++++++ utils/inspectors.go | 160 ++++++++++++++++++++++++++++++++++++++ utils/sui.go | 30 +++++++ 8 files changed, 290 insertions(+) create mode 100644 utils/aptos.go create mode 100644 utils/converters.go create mode 100644 utils/inspectors.go create mode 100644 utils/sui.go diff --git a/sdk/aptos/inspector.go b/sdk/aptos/inspector.go index 81fdcf3f8..2922253a0 100644 --- a/sdk/aptos/inspector.go +++ b/sdk/aptos/inspector.go @@ -16,6 +16,8 @@ import ( var _ sdk.Inspector = &Inspector{} +type RPCClient aptos.AptosRpcClient + type Inspector struct { ConfigTransformer client aptos.AptosRpcClient diff --git a/sdk/solana/inspector.go b/sdk/solana/inspector.go index fbe5109a4..bafc6249e 100644 --- a/sdk/solana/inspector.go +++ b/sdk/solana/inspector.go @@ -18,6 +18,8 @@ import ( var _ sdk.Inspector = (*Inspector)(nil) +type RPCClient = *rpc.Client + // Inspector is an Inspector implementation for Solana chains, giving access to the state of the MCMS contract type Inspector struct { ConfigTransformer diff --git a/sdk/sui/chain_metadata.go b/sdk/sui/chain_metadata.go index c3d9e3879..0bdb842a3 100644 --- a/sdk/sui/chain_metadata.go +++ b/sdk/sui/chain_metadata.go @@ -10,6 +10,19 @@ import ( type TimelockRole uint8 +func TimelockRoleFromAction(action types.TimelockAction) TimelockRole { + switch action { + case types.TimelockActionSchedule: + return TimelockRoleProposer + case types.TimelockActionBypass: + return TimelockRoleBypasser + case types.TimelockActionCancel: + return TimelockRoleCanceller + } + + return TimelockRoleProposer // FIXME: the enum should have an Unset/Unknown value +} + func (t TimelockRole) String() string { switch t { case TimelockRoleBypasser: diff --git a/sdk/sui/inspector.go b/sdk/sui/inspector.go index e6a4193f4..4765b44f0 100644 --- a/sdk/sui/inspector.go +++ b/sdk/sui/inspector.go @@ -27,6 +27,8 @@ const ( var _ sdk.Inspector = &Inspector{} +type RPCClient sui.ISuiAPI + type Inspector struct { ConfigTransformer client sui.ISuiAPI diff --git a/utils/aptos.go b/utils/aptos.go new file mode 100644 index 000000000..400b9a345 --- /dev/null +++ b/utils/aptos.go @@ -0,0 +1,35 @@ +package utils + +import ( + "errors" + + "github.com/smartcontractkit/mcms" + "github.com/smartcontractkit/mcms/sdk/aptos" + "github.com/smartcontractkit/mcms/types" +) + +func AptosRoleFromProposal(proposal *mcms.TimelockProposal) (aptos.TimelockRole, error) { + if proposal == nil { + return 0, errors.New("aptos timelock proposal is needed") + } + + role, err := AptosRoleFromAction(proposal.Action) + if err != nil { + return 0, err + } + + return role, nil +} + +func AptosRoleFromAction(action types.TimelockAction) (aptos.TimelockRole, error) { + switch action { + case types.TimelockActionBypass: + return aptos.TimelockRoleBypasser, nil + case types.TimelockActionSchedule: + return aptos.TimelockRoleProposer, nil + case types.TimelockActionCancel: + return aptos.TimelockRoleCanceller, nil + default: + return 0, errors.New("unknown timelock action") + } +} diff --git a/utils/converters.go b/utils/converters.go new file mode 100644 index 000000000..c9ff58f89 --- /dev/null +++ b/utils/converters.go @@ -0,0 +1,46 @@ +package utils + +import ( + "fmt" + + chainsel "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/mcms" + "github.com/smartcontractkit/mcms/sdk" + "github.com/smartcontractkit/mcms/sdk/aptos" + "github.com/smartcontractkit/mcms/sdk/evm" + "github.com/smartcontractkit/mcms/sdk/solana" + "github.com/smartcontractkit/mcms/sdk/sui" + "github.com/smartcontractkit/mcms/types" +) + +// BuildConvertersForTimelockProposal constructs a map of chain selectors to their respective timelock converters based on the provided timelock proposal. +func BuildConvertersForTimelockProposal( + proposal mcms.TimelockProposal, +) (map[types.ChainSelector]sdk.TimelockConverter, error) { + converters := make(map[types.ChainSelector]sdk.TimelockConverter) + for chainSelector := range proposal.ChainMetadata { + fam, err := types.GetChainSelectorFamily(chainSelector) + if err != nil { + return nil, fmt.Errorf("error getting chain family: %w", err) + } + + var converter sdk.TimelockConverter + switch fam { + case chainsel.FamilyEVM: + converter = &evm.TimelockConverter{} + case chainsel.FamilySolana: + converter = solana.TimelockConverter{} + case chainsel.FamilyAptos: + converter = aptos.NewTimelockConverter() + case chainsel.FamilySui: + converter, _ = sui.NewTimelockConverter() + default: + return nil, fmt.Errorf("unsupported chain family %s", fam) + } + + converters[chainSelector] = converter + } + + return converters, nil +} diff --git a/utils/inspectors.go b/utils/inspectors.go new file mode 100644 index 000000000..539826bec --- /dev/null +++ b/utils/inspectors.go @@ -0,0 +1,160 @@ +package utils + +import ( + "context" + "fmt" + + chainsel "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/mcms" + "github.com/smartcontractkit/mcms/sdk" + "github.com/smartcontractkit/mcms/sdk/aptos" + "github.com/smartcontractkit/mcms/sdk/evm" + "github.com/smartcontractkit/mcms/sdk/solana" + "github.com/smartcontractkit/mcms/sdk/sui" + "github.com/smartcontractkit/mcms/types" +) + +// BuildInspectorsForTimelockProposal() gets a map of inspectors for the given proposal +func BuildInspectorsForTimelockProposal( + proposal mcms.TimelockProposal, + chainClientProvider BlockChainClientProvider, +) (map[types.ChainSelector]sdk.Inspector, error) { + inspectors := map[types.ChainSelector]sdk.Inspector{} + for chainSelector := range proposal.ChainMetadata { + inspector, err := buildInspectorForChainSelector(proposal, chainSelector, chainClientProvider) + if err != nil { + return nil, fmt.Errorf("failed to build inspector for chain selector %d: %w", chainSelector, err) + } + inspectors[chainSelector] = inspector + } + + return inspectors, nil +} + +func buildInspectorForChainSelector( + proposal mcms.TimelockProposal, + chainSelector types.ChainSelector, + chainClientProvider BlockChainClientProvider, +) (sdk.Inspector, error) { + client, err := chainClientProvider.GetClient(uint64(chainSelector)) + if err != nil { + return nil, fmt.Errorf("failed to get blockchain client for chain selector %d: %w", chainSelector, err) + } + inspector, err := getInspectorFromChainSelector(client, chainSelector, proposal.Action) + if err != nil { + return nil, fmt.Errorf("failed to get inspector for chain selector %d: %w", chainSelector, err) + } + + return inspector, nil +} + +// BuildInspectorsForChainSelectors allows us to implement convenience wrappers for +// the features we currently have to hide behind the "inspector" interface. +// For instance, GetOpCount. +// Ideally, we'd separate the mcms.TimelockProposal interface from the implementation, +// which could allow use to make GetOpCount a method of the TimelockProposal type itself (right now +// we can't because of import cycles) +func GetOpCount( + ctx context.Context, + proposal mcms.TimelockProposal, + chainSelector types.ChainSelector, + chainClientProvider BlockChainClientProvider, +) (uint64, error) { + inspector, err := buildInspectorForChainSelector(proposal, chainSelector, chainClientProvider) + if err != nil { + return 0, fmt.Errorf("failed to build inspector for chain selector %d: %w", chainSelector, err) + } + + chainMetadata, ok := proposal.ChainMetadata[chainSelector] + if !ok { + return 0, fmt.Errorf("failed to find chain metadata for chain selector %d: %w", chainSelector, err) + } + + return inspector.GetOpCount(ctx, chainMetadata.MCMAddress) +} + +// getInspectorFromChainSelector returns an inspector for the given chain selector and chain clients +func getInspectorFromChainSelector( + chainClient any, selector types.ChainSelector, action types.TimelockAction, +) (sdk.Inspector, error) { + fam, err := types.GetChainSelectorFamily(selector) + if err != nil { + return nil, fmt.Errorf("error getting chainClient family: %w", err) + } + + var inspector sdk.Inspector + switch fam { + case chainsel.FamilyEVM: + evmClient, ok := chainClient.(evm.ContractDeployBackend) + if !ok { + return nil, fmt.Errorf("invalid EVM client type for selector %d", selector) + } + inspector = evm.NewInspector(evmClient) + + case chainsel.FamilySolana: + solanaClient, ok := chainClient.(solana.RPCClient) + if !ok { + return nil, fmt.Errorf("invalid Solana client type for selector %d", selector) + } + inspector = solana.NewInspector(solanaClient) + + case chainsel.FamilyAptos: + role, rerr := AptosRoleFromAction(action) + if rerr != nil { + return nil, fmt.Errorf("error getting aptos role from proposal: %w", rerr) + } + aptosClient, ok := chainClient.(aptos.RPCClient) + if !ok { + return nil, fmt.Errorf("invalid Aptos client type for selector %d", selector) + } + inspector = aptos.NewInspector(aptosClient, role) + + case chainsel.FamilySui: + suiClient, ok := chainClient.(sui.RPCClient) + if !ok { + return nil, fmt.Errorf("invalid Sui client type for selector %d", selector) + } + // FIXME: where do we get the bindSigner and mcmsPackageID set as nil and "" below? + inspector, err = sui.NewInspector(suiClient, nil, "", sui.TimelockRoleFromAction(action)) + if err != nil { + return nil, fmt.Errorf("failed to create Sui inspector for selector %d", selector) + } + + default: + return nil, fmt.Errorf("unsupported chain family %s", fam) + } + + return inspector, nil +} + +// This requires an addition to CLDf which we'd need to negotiate with the CLD team: +// diff --git i/chain/blockchain.go w/chain/blockchain.go +// index e80aac9..9a3b8b6 100644 +// --- i/chain/blockchain.go +// +++ w/chain/blockchain.go +// +// @@ -82,6 +82,18 @@ func (b BlockChains) GetBySelector(selector uint64) (BlockChain, error) { +// return nil, ErrBlockChainNotFound +// } +// +// +// GetClient returns the rpc client for the given selector as an opaque type +// +func (b BlockChains) GetClient(selector uint64) (any, error) { +// + chain, ok := b.chains[selector] +// + if !ok { +// + return nil, ErrBlockChainNotFound +// + } +// + +// + // FIXME: needs to be tested; +// + // we could also avoid reflection by adding an "RPCClient()" method to the BlockChain interface +// + // or we could do a "switch (chain.Type()" +// + return reflect.ValueOf(chain).Elem().FieldByName("Client").Interface(), nil +// +} +// + +// +// // Exists checks if a chain with the given selector exists. +// func (b BlockChains) Exists(selector uint64) bool { +// _, ok := b.chains[selector] +type BlockChainClientProvider interface { + GetClient(selector uint64) (any, error) +} diff --git a/utils/sui.go b/utils/sui.go new file mode 100644 index 000000000..1f62b27db --- /dev/null +++ b/utils/sui.go @@ -0,0 +1,30 @@ +package utils + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/smartcontractkit/mcms" + "github.com/smartcontractkit/mcms/sdk/sui" + "github.com/smartcontractkit/mcms/types" +) + +func SuiMetadataFromProposal(selector types.ChainSelector, proposal *mcms.TimelockProposal) (sui.AdditionalFieldsMetadata, error) { + if proposal == nil { + return sui.AdditionalFieldsMetadata{}, errors.New("sui timelock proposal is needed") + } + + var metadata sui.AdditionalFieldsMetadata + err := json.Unmarshal([]byte(proposal.ChainMetadata[selector].AdditionalFields), &metadata) + if err != nil { + return sui.AdditionalFieldsMetadata{}, fmt.Errorf("error unmarshaling sui chain metadata: %w", err) + } + + err = metadata.Validate() + if err != nil { + return sui.AdditionalFieldsMetadata{}, fmt.Errorf("error validating sui chain metadata: %w", err) + } + + return metadata, nil +}