Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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/tame-lamps-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink-deployments-framework": minor
---

port proposalutils helpers from chainlink/deployment so they are part of the framework
71 changes: 71 additions & 0 deletions experimental/proposalutils/inspectors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package proposalutils

import (
"fmt"

mcmschainwrappers "github.com/smartcontractkit/mcms/chainwrappers"
mcmssdk "github.com/smartcontractkit/mcms/sdk"

mcmstypes "github.com/smartcontractkit/mcms/types"

cldfmcmsadapters "github.com/smartcontractkit/chainlink-deployments-framework/chain/mcms/adapters"
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
)

type mcmsInspectorOptions struct {
TimelockAction mcmstypes.TimelockAction
}

// MCMSInspectorOption configures how MCMS inspectors are built.
type MCMSInspectorOption func(*mcmsInspectorOptions)

// WithTimelockAction sets the timelock action used by the inspector.
// When omitted, the default action is TimelockActionSchedule.
func WithTimelockAction(action mcmstypes.TimelockAction) MCMSInspectorOption {
return func(opts *mcmsInspectorOptions) {
opts.TimelockAction = action
}
}

// McmsInspectorForChain builds an mcmssdk.Inspector for a single chain in the given environment.
// The chain must be present in env.BlockChains, otherwise an error is returned.
func McmsInspectorForChain(env cldf.Environment, chain uint64, opts ...MCMSInspectorOption) (mcmssdk.Inspector, error) {
var options mcmsInspectorOptions
for _, opt := range opts {
opt(&options)
}

action := mcmstypes.TimelockActionSchedule
if options.TimelockAction != "" {
action = options.TimelockAction
}

chainAccessor := cldfmcmsadapters.Wrap(env.BlockChains)

return mcmschainwrappers.BuildInspector(&chainAccessor, mcmstypes.ChainSelector(chain), action,
mcmstypes.ChainMetadata{})
}

// McmsInspectors builds an mcmssdk.Inspector for every chain in the environment,
// returning them keyed by uint64 chain selector. All inspectors use the default
// TimelockActionSchedule action.
func McmsInspectors(env cldf.Environment) (map[uint64]mcmssdk.Inspector, error) {
chainsMetadata := map[mcmstypes.ChainSelector]mcmstypes.ChainMetadata{}
for chainSelector := range env.BlockChains.All() {
chainsMetadata[mcmstypes.ChainSelector(chainSelector)] = mcmstypes.ChainMetadata{}
}

chainAccessor := cldfmcmsadapters.Wrap(env.BlockChains)

mcmsInspectors, err := mcmschainwrappers.BuildInspectors(&chainAccessor, chainsMetadata, mcmstypes.TimelockActionSchedule)
if err != nil {
return nil, fmt.Errorf("failed to build inspectors: %w", err)
}

inspectors := make(map[uint64]mcmssdk.Inspector, len(mcmsInspectors))
for chainSelector, inspector := range mcmsInspectors {
inspectors[uint64(chainSelector)] = inspector
}

return inspectors, nil
}
160 changes: 160 additions & 0 deletions experimental/proposalutils/inspectors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package proposalutils

import (
"testing"

chainsel "github.com/smartcontractkit/chain-selectors"
mcmstypes "github.com/smartcontractkit/mcms/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-deployments-framework/chain"
cldfevm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
)

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

tests := []struct {
name string
action mcmstypes.TimelockAction
}{
{
name: "sets schedule action",
action: mcmstypes.TimelockActionSchedule,
},
{
name: "sets cancel action",
action: mcmstypes.TimelockActionCancel,
},
{
name: "sets bypass action",
action: mcmstypes.TimelockActionBypass,
},
}

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

var opts mcmsInspectorOptions
WithTimelockAction(tt.action)(&opts)
assert.Equal(t, tt.action, opts.TimelockAction)
})
}
}

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

evmSelector := chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector

tests := []struct {
name string
chains map[uint64]chain.BlockChain
chain uint64
opts []MCMSInspectorOption
wantErr string
}{
{
name: "success with default action",
chains: map[uint64]chain.BlockChain{
evmSelector: cldfevm.Chain{Selector: evmSelector},
},
chain: evmSelector,
},
{
name: "success with custom action",
chains: map[uint64]chain.BlockChain{
evmSelector: cldfevm.Chain{Selector: evmSelector},
},
chain: evmSelector,
opts: []MCMSInspectorOption{WithTimelockAction(mcmstypes.TimelockActionBypass)},
},
{
name: "error when chain not in environment",
chains: nil,
chain: evmSelector,
wantErr: "missing",
},
}

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

env := cldf.Environment{
BlockChains: chain.NewBlockChains(tt.chains),
}

inspector, err := McmsInspectorForChain(env, tt.chain, tt.opts...)

if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
assert.Nil(t, inspector)

return
}

require.NoError(t, err)
assert.NotNil(t, inspector)
})
}
}

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

evmSelector1 := chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector
evmSelector2 := chainsel.ETHEREUM_MAINNET.Selector

tests := []struct {
name string
chains map[uint64]chain.BlockChain
wantLen int
wantSelectors []uint64
}{
{
name: "empty blockchains returns empty map",
chains: nil,
wantLen: 0,
wantSelectors: nil,
},
{
name: "single chain returns single inspector with uint64 key",
chains: map[uint64]chain.BlockChain{
evmSelector1: cldfevm.Chain{Selector: evmSelector1},
},
wantLen: 1,
wantSelectors: []uint64{evmSelector1},
},
{
name: "multiple chains returns inspector per chain",
chains: map[uint64]chain.BlockChain{
evmSelector1: cldfevm.Chain{Selector: evmSelector1},
evmSelector2: cldfevm.Chain{Selector: evmSelector2},
},
wantLen: 2,
wantSelectors: []uint64{evmSelector1, evmSelector2},
},
}

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

env := cldf.Environment{
BlockChains: chain.NewBlockChains(tt.chains),
}

inspectors, err := McmsInspectors(env)
require.NoError(t, err)
require.Len(t, inspectors, tt.wantLen)

for _, sel := range tt.wantSelectors {
assert.NotNil(t, inspectors[sel], "expected inspector for selector %d", sel)
}
})
}
}
14 changes: 14 additions & 0 deletions experimental/proposalutils/mcms_role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package proposalutils

// MCMSRole represents a named role within the MCMS system (e.g. proposer, bypasser, canceller).
type MCMSRole string

const (
ProposerRole MCMSRole = "PROPOSER"
BypasserRole MCMSRole = "BYPASSER"
CancellerRole MCMSRole = "CANCELLER"
)

func (role MCMSRole) String() string {
return string(role)
}
30 changes: 30 additions & 0 deletions experimental/proposalutils/mcms_role_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package proposalutils

import (
"testing"

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

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

tests := []struct {
name string
role MCMSRole
want string
}{
{name: "proposer", role: ProposerRole, want: "PROPOSER"},
{name: "bypasser", role: BypasserRole, want: "BYPASSER"},
{name: "canceller", role: CancellerRole, want: "CANCELLER"},
{name: "custom role", role: MCMSRole("CUSTOM"), want: "CUSTOM"},
{name: "empty", role: MCMSRole(""), want: ""},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.want, tt.role.String())
})
}
}
64 changes: 64 additions & 0 deletions experimental/proposalutils/operations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package proposalutils

import (
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/common"
"github.com/gagliardetto/solana-go"
chain_selectors "github.com/smartcontractkit/chain-selectors"
mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm"
mcmssolanasdk "github.com/smartcontractkit/mcms/sdk/solana"
mcmstypes "github.com/smartcontractkit/mcms/types"
)

// TransactionForChain builds an mcmstypes.Transaction for the given chain selector.
// It currently supports EVM and Solana chains; other chain families return an error.
func TransactionForChain(
chain uint64, toAddress string, data []byte, value *big.Int, contractType string, tags []string,
) (mcmstypes.Transaction, error) {
chainFamily, err := mcmstypes.GetChainSelectorFamily(mcmstypes.ChainSelector(chain))
if err != nil {
return mcmstypes.Transaction{}, fmt.Errorf("failed to get chain family for chain %d: %w", chain, err)
}

var tx mcmstypes.Transaction

switch chainFamily {
case chain_selectors.FamilyEVM:
if !common.IsHexAddress(toAddress) {
return mcmstypes.Transaction{}, fmt.Errorf("invalid EVM address: %s", toAddress)
}
tx = mcmsevmsdk.NewTransaction(common.HexToAddress(toAddress), data, value, contractType, tags)

case chain_selectors.FamilySolana:
accounts := []*solana.AccountMeta{} // FIXME: how to pass accounts to support solana?
var err error
tx, err = mcmssolanasdk.NewTransaction(toAddress, data, value, accounts, contractType, tags)
if err != nil {
return mcmstypes.Transaction{}, fmt.Errorf("failed to create solana transaction: %w", err)
}

default:
return mcmstypes.Transaction{}, fmt.Errorf("unsupported chain family %s", chainFamily)
}

return tx, nil
}

// BatchOperationForChain creates an mcmstypes.BatchOperation containing a single transaction
// for the given chain selector. It delegates to TransactionForChain, so it supports EVM and
// Solana chains.
func BatchOperationForChain(
chain uint64, toAddress string, data []byte, value *big.Int, contractType string, tags []string,
) (mcmstypes.BatchOperation, error) {
tx, err := TransactionForChain(chain, toAddress, data, value, contractType, tags)
if err != nil {
return mcmstypes.BatchOperation{}, fmt.Errorf("failed to create transaction for chain: %w", err)
}

return mcmstypes.BatchOperation{
ChainSelector: mcmstypes.ChainSelector(chain),
Transactions: []mcmstypes.Transaction{tx},
}, nil
}
Loading
Loading