Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6db6028
feat: add timelock converter and inspector helpers
ecPablo Jan 14, 2026
f42a485
feat: add mocks
ecPablo Jan 14, 2026
5848e84
feat: refactor to use chain access interface instead
ecPablo Jan 15, 2026
84d099e
fix: use constructor functions for timelock converter
ecPablo Jan 15, 2026
16ce269
feat: add inspector builder and chain accessor interface
ecPablo Jan 15, 2026
0a9164a
Delete mcms
ecPablo Jan 15, 2026
12ce8b3
fix: update e2e tests workflow for pushes to main trigger (#558)
ecPablo Dec 3, 2025
ae0d708
Update docs link in README.md (#559)
ecPablo Dec 4, 2025
4265edb
Bump golangci-lint (#557)
krebernisak Dec 4, 2025
0051860
build(deps-dev): bump @changesets/changelog-github from 0.5.0 to 0.5.…
dependabot[bot] Dec 4, 2025
7e17658
feat(sui): integrate timelock converter (#570)
rodrigombsoares Jan 12, 2026
8aad17c
chore: bump version (#571)
gustavogama-cll Jan 12, 2026
a5ab214
Version Packages (#572)
app-token-issuer-engops[bot] Jan 12, 2026
3a6f011
fix: change pointer receiver evm converter (#576)
ecPablo Jan 14, 2026
241f0a0
build(deps): bump github.com/smartcontractkit/chain-selectors from 1.…
dependabot[bot] Jan 14, 2026
4aa9bd8
Add TON support (#486)
krebernisak Jan 15, 2026
9b4bf16
fix: linting errors
ecPablo Jan 16, 2026
359c64a
Merge branch 'main' into ecpablo/add-inspectors-helpers
ecPablo Jan 16, 2026
fed18c7
fix: refactor inspector API and converter for consistency
ecPablo Jan 16, 2026
44991a9
fix: refactor inspector SUI client name and fix unit tests
ecPablo Jan 16, 2026
a5ef822
fix: refactor to have a chainaccess pkg with inspectors, converters a…
ecPablo Jan 16, 2026
9616e3a
fix: remove timelock proposal helper
ecPablo Jan 16, 2026
677cf02
fix: linting
ecPablo Jan 16, 2026
56dd5bb
fix: rename chain accessor and pkg name
ecPablo Jan 16, 2026
0ed8e3e
fix: lint errors
ecPablo Jan 16, 2026
e76ab00
fix: mocks generation
ecPablo Jan 16, 2026
991ee66
fix: unit tests
ecPablo Jan 16, 2026
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
5 changes: 5 additions & 0 deletions .changeset/stupid-worlds-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smartcontractkit/mcms": minor
---

add timelock converter and inspector helpers
1 change: 1 addition & 0 deletions .mockery.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ packages:
config:
dir: "./sdk/ton/mocks"
filename: "api.go"
github.com/smartcontractkit/mcms/chainwrappers:
github.com/smartcontractkit/mcms/sdk:
github.com/smartcontractkit/mcms/sdk/evm:
github.com/smartcontractkit/mcms/sdk/evm/bindings:
Expand Down
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
golang 1.24.4
golang 1.25.3
golangci-lint 2.1.6
mockery 2.53.5
nodejs 20.16.0
Expand Down
18 changes: 18 additions & 0 deletions chainwrappers/chainaccessor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package chainwrappers

import (
aptoslib "github.com/aptos-labs/aptos-go-sdk"
"github.com/block-vision/sui-go-sdk/sui"
solrpc "github.com/gagliardetto/solana-go/rpc"

evmsdk "github.com/smartcontractkit/mcms/sdk/evm"
suisuisdk "github.com/smartcontractkit/mcms/sdk/sui"
)

type ChainAccessor interface {
Selectors() []uint64
EVMClient(selector uint64) (evmsdk.ContractDeployBackend, bool)
SolanaClient(selector uint64) (*solrpc.Client, bool)
AptosClient(selector uint64) (aptoslib.AptosRpcClient, bool)
SuiClient(selector uint64) (sui.ISuiAPI, suisuisdk.SuiSigner, bool)
}
40 changes: 40 additions & 0 deletions chainwrappers/converters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package chainwrappers

import (
"fmt"

chainsel "github.com/smartcontractkit/chain-selectors"

"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/types"
)

// BuildConverters constructs a map of chain selectors to their respective timelock converters based on the provided timelock proposal.
func BuildConverters(chainMetadata map[types.ChainSelector]types.ChainMetadata) (map[types.ChainSelector]sdk.TimelockConverter, error) {
converters := make(map[types.ChainSelector]sdk.TimelockConverter)
for chainMeta := range chainMetadata {
fam, err := types.GetChainSelectorFamily(chainMeta)
if err != nil {
return nil, fmt.Errorf("error getting chain family: %w", err)
}

var converter sdk.TimelockConverter
switch fam {
case chainsel.FamilyEVM:
converter = evm.NewTimelockConverter()
case chainsel.FamilySolana:
converter = solana.NewTimelockConverter()
case chainsel.FamilyAptos:
converter = aptos.NewTimelockConverter()
default:
return nil, fmt.Errorf("unsupported chain family %s", fam)
}

converters[chainMeta] = converter
}

return converters, nil
}
75 changes: 75 additions & 0 deletions chainwrappers/converters_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package chainwrappers

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/smartcontractkit/mcms/internal/testutils/chaintest"
"github.com/smartcontractkit/mcms/sdk/aptos"
"github.com/smartcontractkit/mcms/sdk/evm"
"github.com/smartcontractkit/mcms/sdk/solana"
"github.com/smartcontractkit/mcms/types"
)

func TestBuildConverters(t *testing.T) {
t.Parallel()

tests := []struct {
name string
metadata map[types.ChainSelector]types.ChainMetadata
expectTypes map[types.ChainSelector]any
expectErr string
}{
{
name: "supported families",
metadata: map[types.ChainSelector]types.ChainMetadata{
chaintest.Chain2Selector: {},
chaintest.Chain4Selector: {},
chaintest.Chain5Selector: {},
},
expectTypes: map[types.ChainSelector]any{
chaintest.Chain2Selector: (*evm.TimelockConverter)(nil),
chaintest.Chain4Selector: (*solana.TimelockConverter)(nil),
chaintest.Chain5Selector: (*aptos.TimelockConverter)(nil),
},
},
{
name: "unsupported family",
metadata: map[types.ChainSelector]types.ChainMetadata{
chaintest.Chain6Selector: {},
},
expectErr: "unsupported chain family",
},
{
name: "invalid selector",
metadata: map[types.ChainSelector]types.ChainMetadata{
chaintest.ChainInvalidSelector: {},
},
expectErr: "error getting chain family",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

converters, err := BuildConverters(tc.metadata)

if tc.expectErr != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.expectErr)

return
}

require.NoError(t, err)
require.Len(t, converters, len(tc.expectTypes))
for selector, expectedType := range tc.expectTypes {
converter, ok := converters[selector]
require.True(t, ok)
require.IsType(t, expectedType, converter)
}
})
}
}
90 changes: 90 additions & 0 deletions chainwrappers/inspectors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package chainwrappers

import (
"fmt"

chainsel "github.com/smartcontractkit/chain-selectors"

"github.com/smartcontractkit/mcms/sdk"
"github.com/smartcontractkit/mcms/sdk/aptos"
"github.com/smartcontractkit/mcms/sdk/evm"
"github.com/smartcontractkit/mcms/sdk/solana"
sdkSui "github.com/smartcontractkit/mcms/sdk/sui"
"github.com/smartcontractkit/mcms/types"
)

// BuildInspectors gets a map of inspectors for the given chain metadata and chain clients
func BuildInspectors(
chains ChainAccessor,
chainMetadata map[types.ChainSelector]types.ChainMetadata,
action types.TimelockAction) (map[types.ChainSelector]sdk.Inspector, error) {
inspectors := map[types.ChainSelector]sdk.Inspector{}
for chainSelector, metadata := range chainMetadata {
inspector, err := BuildInspector(chains, chainSelector, action, metadata)
if err != nil {
return nil, err
}
inspectors[chainSelector] = inspector
}

return inspectors, nil
}

// BuildInspector constructs a chain-family-specific Inspector from ChainAccessor plus metadata.
func BuildInspector(
chains ChainAccessor,
selector types.ChainSelector,
action types.TimelockAction,
metadata types.ChainMetadata,
) (sdk.Inspector, error) {
if chains == nil {
return nil, fmt.Errorf("chain access is required")
}

family, err := types.GetChainSelectorFamily(selector)
if err != nil {
return nil, fmt.Errorf("error getting chain family: %w", err)
}

rawSelector := uint64(selector)
switch family {
case chainsel.FamilyEVM:
client, ok := chains.EVMClient(rawSelector)
if !ok {
return nil, fmt.Errorf("missing EVM chain client for selector %d", rawSelector)
}

return evm.NewInspector(client), nil
case chainsel.FamilySolana:
client, ok := chains.SolanaClient(rawSelector)
if !ok {
return nil, fmt.Errorf("missing Solana chain client for selector %d", rawSelector)
}

return solana.NewInspector(client), nil
case chainsel.FamilyAptos:
client, ok := chains.AptosClient(rawSelector)
if !ok {
return nil, fmt.Errorf("missing Aptos chain client for selector %d", rawSelector)
}
role, err := aptos.AptosRoleFromAction(action)
if err != nil {
return nil, fmt.Errorf("error determining aptos role: %w", err)
}

return aptos.NewInspector(client, role), nil
case chainsel.FamilySui:
client, signer, ok := chains.SuiClient(rawSelector)
if !ok {
return nil, fmt.Errorf("missing Sui chain client for selector %d", rawSelector)
}
suiMetadata, err := sdkSui.SuiMetadata(metadata)
if err != nil {
return nil, fmt.Errorf("error parsing sui metadata: %w", err)
}

return sdkSui.NewInspector(client, signer, suiMetadata.McmsPackageID, suiMetadata.Role)
default:
return nil, fmt.Errorf("unsupported chain family %s", family)
}
}
78 changes: 78 additions & 0 deletions chainwrappers/inspectors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package chainwrappers

import (
"testing"

chainsel "github.com/smartcontractkit/chain-selectors"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/mcms/chainwrappers/mocks"

mcmsTypes "github.com/smartcontractkit/mcms/types"
)

func TestMCMInspectorBuilder_BuildInspectors(t *testing.T) {
t.Parallel()

tests := []struct {
name string
chainMetadata map[mcmsTypes.ChainSelector]mcmsTypes.ChainMetadata
chainAccess *mocks.ChainAccessor
setup func(access *mocks.ChainAccessor)
expectErr bool
errContains string
expectedInspectorsCount int
}{
{
name: "empty input",
chainMetadata: map[mcmsTypes.ChainSelector]mcmsTypes.ChainMetadata{},
chainAccess: mocks.NewChainAccessor(t),
expectErr: false,
},
{
name: "missing chain client",
chainMetadata: map[mcmsTypes.ChainSelector]mcmsTypes.ChainMetadata{
1: {MCMAddress: "0xabc", StartingOpCount: 0},
},
chainAccess: mocks.NewChainAccessor(t),
expectErr: true,
errContains: "error getting chain family: chain family not found for selector 1",
},
{
name: "valid input",
chainMetadata: map[mcmsTypes.ChainSelector]mcmsTypes.ChainMetadata{
mcmsTypes.ChainSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector): {MCMAddress: "0xabc", StartingOpCount: 0},
mcmsTypes.ChainSelector(chainsel.SOLANA_DEVNET.Selector): {MCMAddress: "0xabc", StartingOpCount: 0},
},
chainAccess: mocks.NewChainAccessor(t),
expectErr: false,
setup: func(access *mocks.ChainAccessor) {
access.EXPECT().EVMClient(mock.Anything).Return(nil, true)
access.EXPECT().SolanaClient(mock.Anything).Return(nil, true)
},
expectedInspectorsCount: 2,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if tc.chainAccess == nil {
tc.chainAccess = mocks.NewChainAccessor(t)
}
if tc.expectedInspectorsCount > 0 {
tc.setup(tc.chainAccess)
}

inspectors, err := BuildInspectors(tc.chainAccess, tc.chainMetadata, mcmsTypes.TimelockActionSchedule)
if tc.expectErr {
require.Error(t, err)
require.Contains(t, err.Error(), tc.errContains)
} else {
require.NoError(t, err)
require.Len(t, inspectors, tc.expectedInspectorsCount)
}
})
}
}
Loading
Loading