Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 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
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/inspectors:
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
21 changes: 21 additions & 0 deletions chainsmetadata/aptos_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package chainsmetadata

import (
"errors"

"github.com/smartcontractkit/mcms/sdk/aptos"
"github.com/smartcontractkit/mcms/types"
)

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")
}
}
64 changes: 64 additions & 0 deletions chainsmetadata/aptos_helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package chainsmetadata

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

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

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

tests := []struct {
name string
action mcmsTypes.TimelockAction
expectedRole aptos.TimelockRole
expectError bool
}{
{
name: "bypass action returns bypasser role",
action: mcmsTypes.TimelockActionBypass,
expectedRole: aptos.TimelockRoleBypasser,
expectError: false,
},
{
name: "schedule action returns proposer role",
action: mcmsTypes.TimelockActionSchedule,
expectedRole: aptos.TimelockRoleProposer,
expectError: false,
},
{
name: "cancel action returns canceller role",
action: mcmsTypes.TimelockActionCancel,
expectedRole: aptos.TimelockRoleCanceller,
expectError: false,
},
{
name: "unknown action returns error",
action: mcmsTypes.TimelockAction("unknown"),
expectedRole: 0,
expectError: true,
},
}

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

role, err := AptosRoleFromAction(tt.action)

if tt.expectError {
require.Error(t, err)
assert.Equal(t, "unknown timelock action", err.Error())
assert.Equal(t, tt.expectedRole, role)
} else {
require.NoError(t, err)
assert.Equal(t, tt.expectedRole, role)
}
})
}
}
24 changes: 24 additions & 0 deletions chainsmetadata/sui_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package chainsmetadata

import (
"encoding/json"
"fmt"

"github.com/smartcontractkit/mcms/sdk/sui"
"github.com/smartcontractkit/mcms/types"
)

func SuiMetadata(chainMetadata types.ChainMetadata) (sui.AdditionalFieldsMetadata, error) {
var metadata sui.AdditionalFieldsMetadata
err := json.Unmarshal([]byte(chainMetadata.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
}
77 changes: 77 additions & 0 deletions chainsmetadata/sui_helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package chainsmetadata

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/mcms/sdk/sui"
"github.com/smartcontractkit/mcms/types"
)

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

validMetadata := types.ChainMetadata{
AdditionalFields: []byte(`{"mcms_package_id":"0x1","role":1,"account_obj":"0x2","registry_obj":"0x3","timelock_obj":"0x4","deployer_state_obj":"0x5"}`),
}

tests := []struct {
name string
metadata types.ChainMetadata
expectError bool
errorMsg string
}{
{
name: "valid metadata returns success",
metadata: validMetadata,
},
{
name: "invalid JSON returns error",
metadata: types.ChainMetadata{
AdditionalFields: []byte(`{"mcms_package_id":"0x1","role":1`),
},
expectError: true,
errorMsg: "error unmarshaling sui chain metadata",
},
{
name: "missing required fields returns validation error",
metadata: types.ChainMetadata{
AdditionalFields: []byte(`{"role":1}`),
},
expectError: true,
errorMsg: "error validating sui chain metadata",
},
{
name: "empty additional fields returns unmarshaling error",
metadata: types.ChainMetadata{
AdditionalFields: nil,
},
expectError: true,
errorMsg: "error unmarshaling sui chain metadata",
},
}

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

metadata, err := SuiMetadata(tt.metadata)
if tt.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.errorMsg)
assert.Equal(t, sui.AdditionalFieldsMetadata{}, metadata)

return
}

require.NoError(t, err)
assert.Equal(t, "0x1", metadata.McmsPackageID)
assert.Equal(t, sui.TimelockRole(1), metadata.Role)
assert.Equal(t, "0x2", metadata.AccountObj)
assert.Equal(t, "0x3", metadata.RegistryObj)
assert.Equal(t, "0x4", metadata.TimelockObj)
})
}
}
41 changes: 41 additions & 0 deletions converters/converters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package proposalutils

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

// BuildConverters constructs a map of chain selectors to their respective timelock converters based on the provided timelock proposal.
func BuildConverters(proposal mcms.TimelockProposal) (map[types.ChainSelector]sdk.TimelockConverter, error) {
converters := make(map[types.ChainSelector]sdk.TimelockConverter)
for chainMeta := range proposal.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
}
36 changes: 25 additions & 11 deletions docs/docs/contributing/integrating-new-chain-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ All chain family integrations must implement interfaces defined in the `/sdk` fo
| `TimelockInspector` | **Required** | Query timelock contract state | [timelock_inspector.go](https://github.com/smartcontractkit/mcms/blob/main/sdk/timelock_inspector.go) |
| `TimelockConverter` | **Required** | Convert batch operations to timelock operations | [timelock_converter.go](https://github.com/smartcontractkit/mcms/blob/main/sdk/timelock_converter.go) |

### ChainAccess Registry Adapter

The MCMS SDK intentionally avoids importing chain-registry implementations (for example, the Chainlink Deployments Framework). Instead, shared tooling must expose the `ChainAccess` interface defined in [`sdk/chainclient.go`](https://github.com/smartcontractkit/mcms/blob/main/sdk/chainclient.go) so inspectors and proposal tooling can fetch RPC clients without pulling in external dependencies.

Your adapter should:

- Implement `Selectors() []uint64` and the per-family lookup helpers (`EVMClient`, `SolanaClient`, `AptosClient`, `Sui`) by delegating to your registry.
- Return chain clients that satisfy `bind.ContractBackend`/`bind.DeployBackend` for EVM, `*solrpc.Client` for Solana, `aptoslib.AptosRpcClient` for Aptos, and `(sui.ISuiAPI, SuiSigner)` for Sui, etc.
- Live in the repository that already depends on your registry (e.g., CLDF or deployment tooling) so `mcms` itself stays agnostic.

This boundary keeps MCMS reusable across environments while still allowing downstream systems to map their chain catalogs into MCMS inspectors.

## Required Interfaces

### Executor Interface
Expand Down Expand Up @@ -360,6 +372,7 @@ Use the `/sdk/errors/` package for standardized error handling:
Each interface implementation needs a corresponding `_test.go` file with comprehensive coverage (>80%). Test all public methods with both success and failure cases using table-driven tests. Mock external dependencies (RPC clients, contracts) in `sdk/<chain>/mocks/`.

**Test Examples:**

- [EVM Tests](https://github.com/smartcontractkit/mcms/blob/main/sdk/evm/executor_test.go) | [Mock Examples](https://github.com/smartcontractkit/mcms/tree/main/sdk/evm/mocks)
- [Solana Tests](https://github.com/smartcontractkit/mcms/blob/main/sdk/solana/encoder_test.go) | [Mocks](https://github.com/smartcontractkit/mcms/tree/main/sdk/solana/mocks)
- [Aptos Tests](https://github.com/smartcontractkit/mcms/blob/main/sdk/aptos/inspector_test.go) | [Mocks](https://github.com/smartcontractkit/mcms/tree/main/sdk/aptos/mocks)
Expand All @@ -368,19 +381,20 @@ Each interface implementation needs a corresponding `_test.go` file with compreh

Create test suite under `/e2e/tests/<chain-family>/` covering:

| Test Category | Example | Key Coverage |
|---------------|---------|--------------|
| **Config Management** | [solana/set_config.go](https://github.com/smartcontractkit/mcms/blob/main/e2e/tests/solana/set_config.go) | Set/update config, retrieve and verify, clearRoot flag |
| **Root Operations** | [solana/set_root.go](https://github.com/smartcontractkit/mcms/blob/main/e2e/tests/solana/set_root.go) | Set root with signatures, quorum requirements, expiration |
| **Operation Execution** | [solana/execute.go](https://github.com/smartcontractkit/mcms/blob/main/e2e/tests/solana/execute.go) | Execute with valid proof, verify effects, test invalid proofs |
| **Contract Inspection** | [solana/inspection.go](https://github.com/smartcontractkit/mcms/blob/main/e2e/tests/solana/inspection.go) | Query config, op count, root, metadata |
| **Simulation** (optional) | [solana/simulator.go](https://github.com/smartcontractkit/mcms/blob/main/e2e/tests/solana/simulator.go) | Simulate valid/invalid ops, verify no state changes |
| **Timelock Conversion** (optional) | [solana/timelock_converter.go](https://github.com/smartcontractkit/mcms/blob/main/e2e/tests/solana/timelock_converter.go) | Convert batch to timelock ops, verify IDs and actions |
| **Timelock Execution** (optional) | [solana/timelock_execution.go](https://github.com/smartcontractkit/mcms/blob/main/e2e/tests/solana/timelock_execution.go) | Schedule with delay, execute after delay, predecessors |
| **Timelock Inspection** (optional) | [solana/timelock_inspection.go](https://github.com/smartcontractkit/mcms/blob/main/e2e/tests/solana/timelock_inspection.go) | Query roles, operation status, minimum delay |
| **Timelock Cancellation** (optional) | [aptos/timelock_cancel.go](https://github.com/smartcontractkit/mcms/blob/main/e2e/tests/aptos/timelock_cancel.go) | Cancel pending ops, verify cancellation |
| Test Category | Example | Key Coverage |
|--------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------|
| **Config Management** | [solana/set_config.go](https://github.com/smartcontractkit/mcms/blob/main/e2e/tests/solana/set_config.go) | Set/update config, retrieve and verify, clearRoot flag |
| **Root Operations** | [solana/set_root.go](https://github.com/smartcontractkit/mcms/blob/main/e2e/tests/solana/set_root.go) | Set root with signatures, quorum requirements, expiration |
| **Operation Execution** | [solana/execute.go](https://github.com/smartcontractkit/mcms/blob/main/e2e/tests/solana/execute.go) | Execute with valid proof, verify effects, test invalid proofs |
| **Contract Inspection** | [solana/inspection.go](https://github.com/smartcontractkit/mcms/blob/main/e2e/tests/solana/inspection.go) | Query config, op count, root, metadata |
| **Simulation** (optional) | [solana/simulator.go](https://github.com/smartcontractkit/mcms/blob/main/e2e/tests/solana/simulator.go) | Simulate valid/invalid ops, verify no state changes |
| **Timelock Conversion** (optional) | [solana/timelock_converter.go](https://github.com/smartcontractkit/mcms/blob/main/e2e/tests/solana/timelock_converter.go) | Convert batch to timelock ops, verify IDs and actions |
| **Timelock Execution** (optional) | [solana/timelock_execution.go](https://github.com/smartcontractkit/mcms/blob/main/e2e/tests/solana/timelock_execution.go) | Schedule with delay, execute after delay, predecessors |
| **Timelock Inspection** (optional) | [solana/timelock_inspection.go](https://github.com/smartcontractkit/mcms/blob/main/e2e/tests/solana/timelock_inspection.go) | Query roles, operation status, minimum delay |
| **Timelock Cancellation** (optional) | [aptos/timelock_cancel.go](https://github.com/smartcontractkit/mcms/blob/main/e2e/tests/aptos/timelock_cancel.go) | Cancel pending ops, verify cancellation |

**Test Suite Setup:**

1. Create `e2e/config.<chain>.toml` ([example](https://github.com/smartcontractkit/mcms/blob/main/e2e/config.evm.toml))
2. Update [e2e/tests/setup.go](https://github.com/smartcontractkit/mcms/blob/main/e2e/tests/setup.go) with blockchain node and RPC clients
3. Add suite to [e2e/tests/runner_test.go](https://github.com/smartcontractkit/mcms/blob/main/e2e/tests/runner_test.go)
Expand Down
Loading
Loading