Skip to content

Commit e38816f

Browse files
authored
feat: add config transformer functionality [DPA-1409] (#255)
Adds configuration transformer functionality to the solana sdk ## AI Summary: This pull request introduces a new configuration transformer functionality for Solana and updates the existing configuration transformer for EVM. The changes include modifications to the `ConfigTransformer` interface, implementation of the Solana transformer, and updates to tests. ### New Features: * Added a new configuration transformer for Solana, including the `AdditionalConfig` struct and the `ToChainConfig` method implementation in `sdk/solana/config_transformer.go`. [[1]](diffhunk://#diff-e668d47f11b486cfa0ba79551c77dbb35570e15152d2ae5f7d1708ae2aa48680L4-R26) [[2]](diffhunk://#diff-e668d47f11b486cfa0ba79551c77dbb35570e15152d2ae5f7d1708ae2aa48680L55-R100) ### Interface Updates: * Modified the `ConfigTransformer` interface to accept an additional chain-specific configuration parameter. ### Implementation Updates: * Updated the EVM `ConfigTransformer` to support the new interface by adding a chain-specific configuration parameter to the `ToChainConfig` method. [[1]](diffhunk://#diff-eb9910cff7faabe1b397889073e7b58998a9c95918839b87779808412fab84f4L140-R140) [[2]](diffhunk://#diff-fe1f07d4fe69ce3f43ced9b53edfbbabe82ed2fb364d017f8c914414e9b5c402R66) [[3]](diffhunk://#diff-6e8114ad8cf44c0e74542ad75d01b9f810b06aa111c956b5fade449578976a7fL253-R253) ### Testing Enhancements: * Added tests for the new Solana `ToChainConfig` method in `sdk/solana/config_transformer_test.go`. ### Documentation: * Added a changeset entry for the new Solana configuration transformer functionality.
1 parent d6d880c commit e38816f

8 files changed

+226
-10
lines changed

.changeset/lazy-lemons-fold.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smartcontractkit/mcms": minor
3+
---
4+
5+
Add config transformer functionality for solana.

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ require (
1818
github.com/spf13/cast v1.7.1
1919
github.com/stretchr/testify v1.10.0
2020
golang.org/x/tools v0.29.0
21+
gotest.tools/v3 v3.5.1
2122
)
2223

2324
require (

internal/testutils/evmsim/evmsim.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ func (s *SimulatedChain) SetMCMSConfig(
137137
}
138138

139139
transformer := evm.ConfigTransformer{}
140-
bindConfig, err := transformer.ToChainConfig(*cfg)
140+
bindConfig, err := transformer.ToChainConfig(*cfg, nil)
141141
require.NoError(t, err)
142142

143143
signerAddrs := make([]common.Address, len(bindConfig.Signers))

sdk/config_transformer.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import (
66

77
// ConfigTransformer is the interface used to create the configuration of an MCMS contract.
88
// R in this case is the chain-specific struct that is used to configure the contract.
9+
// C is the chain-specific additional configuration that is required to configure the contract.
910
// the interface allows conversion between the chain-specific struct and the chain-agnostic.
10-
type ConfigTransformer[R any] interface {
11+
type ConfigTransformer[R any, C any] interface {
1112
// ToChainConfig converts the chain agnostic config to the chain-specific config
12-
ToChainConfig(contract string, cfg types.Config) (R, error)
13+
ToChainConfig(cfg types.Config, chainSpecificConfig C) (R, error)
1314

1415
// ToConfig Maps the chain-specific config to the chain-agnostic config
1516
ToConfig(onchainConfig R) (*types.Config, error)

sdk/evm/config_transformer.go

+11
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ import (
88
"github.com/ethereum/go-ethereum/common"
99

1010
"github.com/smartcontractkit/mcms/internal/utils/safecast"
11+
"github.com/smartcontractkit/mcms/sdk"
1112
sdkerrors "github.com/smartcontractkit/mcms/sdk/errors"
1213
"github.com/smartcontractkit/mcms/sdk/evm/bindings"
1314
"github.com/smartcontractkit/mcms/types"
1415
)
1516

17+
var _ sdk.ConfigTransformer[bindings.ManyChainMultiSigConfig, any] = (*ConfigTransformer)(nil)
18+
1619
const maxUint8Value = 255
1720

1821
type ConfigTransformer struct{}
@@ -62,6 +65,7 @@ func (e *ConfigTransformer) ToConfig(
6265
// ToChainConfig converts a chain-agnostic types.Config to an EVM ManyChainMultiSigConfig
6366
func (e *ConfigTransformer) ToChainConfig(
6467
cfg types.Config,
68+
_ any,
6569
) (bindings.ManyChainMultiSigConfig, error) {
6670
var bindConfig bindings.ManyChainMultiSigConfig
6771

@@ -94,6 +98,13 @@ func (e *ConfigTransformer) ToChainConfig(
9498
}, nil
9599
}
96100

101+
// ExtractSetConfigInputs flattens a nested `*types.Config` into:
102+
// 1. groupQuorums: [32]uint8 where each index i holds the quorum for group i (zero-padded).
103+
// 2. groupParents: [32]uint8 where each index i holds the parent group’s index (or a sentinel).
104+
// 3. orderedSignerAddresses: a sorted slice of all signer addresses.
105+
// 4. orderedSignerGroups: a parallel slice of group indices for each signer.
106+
//
107+
// Returns an error if the structure cannot be flattened (e.g., too many groups).
97108
func ExtractSetConfigInputs(
98109
group *types.Config,
99110
) ([32]uint8, [32]uint8, []common.Address, []uint8, error) {

sdk/evm/config_transformer_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ func Test_SetConfigInputs(t *testing.T) {
331331
t.Parallel()
332332

333333
transformer := ConfigTransformer{}
334-
got, err := transformer.ToChainConfig(tt.giveConfig)
334+
got, err := transformer.ToChainConfig(tt.giveConfig, nil)
335335

336336
if tt.wantErr != "" {
337337
require.EqualError(t, err, tt.wantErr)

sdk/solana/config_transformer.go

+48-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
package solana
22

33
import (
4-
"fmt"
5-
64
"github.com/ethereum/go-ethereum/common"
5+
"github.com/gagliardetto/solana-go"
76
bindings "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/mcm"
87

8+
sdkerrors "github.com/smartcontractkit/mcms/sdk/errors"
9+
"github.com/smartcontractkit/mcms/sdk/evm"
910
"github.com/smartcontractkit/mcms/types"
1011
)
1112

13+
type AdditionalConfig struct {
14+
ChainID uint64
15+
MultisigID [32]uint8
16+
// Current Owner of the multisig ID
17+
Owner solana.PublicKey
18+
// Proposed Owner of the program when calling transfer_ownership
19+
ProposedOwner solana.PublicKey
20+
}
21+
22+
const maxUint8Value = 255
23+
1224
type ConfigTransformer struct{}
1325

1426
func NewConfigTransformer() *ConfigTransformer {
@@ -54,8 +66,38 @@ func (e *ConfigTransformer) ToConfig(
5466
}
5567

5668
// ToChainConfig converts a chain-agnostic types.Config to an Solana ManyChainMultiSigConfig
57-
func (e *ConfigTransformer) ToChainConfig(
58-
cfg types.Config,
59-
) (bindings.MultisigConfig, error) {
60-
return bindings.MultisigConfig{}, fmt.Errorf("sdk.solana.ConfigTransformer.ToChainConfig not implemented")
69+
func (e *ConfigTransformer) ToChainConfig(cfg types.Config, solanaConfig AdditionalConfig) (bindings.MultisigConfig, error) {
70+
// Populate additional Configs
71+
result := bindings.MultisigConfig{
72+
ChainId: solanaConfig.ChainID,
73+
MultisigId: solanaConfig.MultisigID,
74+
Owner: solanaConfig.Owner,
75+
ProposedOwner: solanaConfig.ProposedOwner,
76+
}
77+
// Populate the signers: we can reuse the evm implementation here as the signers structure is the same
78+
groupQuorums, groupParents, signerAddrs, signerGroups, err := evm.ExtractSetConfigInputs(&cfg)
79+
if err != nil {
80+
return bindings.MultisigConfig{}, err
81+
}
82+
// Check the length of signerAddresses up-front
83+
if len(signerAddrs) > maxUint8Value {
84+
return bindings.MultisigConfig{}, sdkerrors.NewTooManySignersError(uint64(len(signerAddrs)))
85+
}
86+
// Set the signers
87+
bindSigners := make([]bindings.McmSigner, len(signerAddrs))
88+
idx := uint8(0)
89+
for i, signerAddr := range signerAddrs {
90+
bindSigners[i] = bindings.McmSigner{
91+
EvmAddress: signerAddr,
92+
Group: signerGroups[i],
93+
Index: idx,
94+
}
95+
idx += 1
96+
}
97+
result.Signers = bindSigners
98+
// Set group quorums and group parents.
99+
result.GroupQuorums = groupQuorums
100+
result.GroupParents = groupParents
101+
102+
return result, nil
61103
}

sdk/solana/config_transformer_test.go

+156
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import (
44
"testing"
55

66
"github.com/ethereum/go-ethereum/common"
7+
"github.com/gagliardetto/solana-go"
78
"github.com/google/go-cmp/cmp"
89
bindings "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/mcm"
910
"github.com/stretchr/testify/require"
11+
"gotest.tools/v3/assert"
1012

1113
"github.com/smartcontractkit/mcms/types"
1214
)
@@ -158,3 +160,157 @@ func Test_ConfigTransformer_ToConfig(t *testing.T) {
158160
})
159161
}
160162
}
163+
164+
func Test_ConfigTransformer_ToChainConfig(t *testing.T) {
165+
t.Parallel()
166+
167+
var (
168+
signer1 = common.HexToAddress("0x1")
169+
signer2 = common.HexToAddress("0x2")
170+
signer3 = common.HexToAddress("0x3")
171+
signer4 = common.HexToAddress("0x4")
172+
)
173+
testOwner, err := solana.NewRandomPrivateKey()
174+
require.NoError(t, err)
175+
proposedOwner, err := solana.NewRandomPrivateKey()
176+
require.NoError(t, err)
177+
178+
tests := []struct {
179+
name string
180+
give types.Config
181+
want bindings.MultisigConfig
182+
additionalConfig AdditionalConfig
183+
wantErr string
184+
assert func(t *testing.T, got bindings.MultisigConfig)
185+
}{
186+
{
187+
name: "success: converts binding config to config",
188+
want: bindings.MultisigConfig{
189+
GroupQuorums: [32]uint8{1, 1},
190+
GroupParents: [32]uint8{0, 0},
191+
Owner: testOwner.PublicKey(),
192+
ProposedOwner: proposedOwner.PublicKey(),
193+
Signers: []bindings.McmSigner{
194+
{EvmAddress: signer1, Group: 0, Index: 0},
195+
{EvmAddress: signer2, Group: 1, Index: 1},
196+
},
197+
},
198+
additionalConfig: AdditionalConfig{
199+
ChainID: 0,
200+
MultisigID: [32]uint8{},
201+
Owner: testOwner.PublicKey(),
202+
ProposedOwner: proposedOwner.PublicKey(),
203+
},
204+
give: types.Config{
205+
Quorum: 1,
206+
Signers: []common.Address{signer1},
207+
GroupSigners: []types.Config{
208+
{
209+
Quorum: 1,
210+
Signers: []common.Address{signer2},
211+
GroupSigners: []types.Config{},
212+
},
213+
},
214+
},
215+
assert: func(t *testing.T, got bindings.MultisigConfig) {
216+
t.Helper()
217+
218+
assert.Equal(t, [32]uint8{1, 1}, got.GroupQuorums)
219+
assert.Equal(t, [32]uint8{0, 0}, got.GroupParents)
220+
assert.Equal(t, uint64(0), got.ChainId)
221+
assert.Equal(t, [32]uint8{}, got.MultisigId)
222+
assert.Equal(t, got.Owner, testOwner.PublicKey())
223+
assert.Equal(t, got.ProposedOwner, proposedOwner.PublicKey())
224+
},
225+
},
226+
{
227+
name: "success: nested config",
228+
give: types.Config{
229+
Quorum: 2,
230+
Signers: []common.Address{signer1}, // group 0 signers
231+
GroupSigners: []types.Config{
232+
{
233+
Quorum: 1,
234+
Signers: []common.Address{signer2}, // group 1 signers
235+
GroupSigners: []types.Config{
236+
{
237+
Quorum: 2,
238+
Signers: []common.Address{signer3, signer4}, // group 2 signers
239+
},
240+
},
241+
},
242+
},
243+
},
244+
want: bindings.MultisigConfig{
245+
Owner: testOwner.PublicKey(),
246+
ProposedOwner: proposedOwner.PublicKey(),
247+
ChainId: 1,
248+
MultisigId: [32]uint8{},
249+
GroupQuorums: [32]uint8{
250+
2, // group 0
251+
1, // group 1
252+
2, // group 2
253+
},
254+
GroupParents: [32]uint8{
255+
0, // group 0's parent => 0 by convention (no parent)
256+
0, // group 1's parent => group 0
257+
1, // group 2's parent => group 1
258+
},
259+
Signers: []bindings.McmSigner{
260+
{EvmAddress: signer1, Group: 0, Index: 0},
261+
{EvmAddress: signer2, Group: 1, Index: 1},
262+
{EvmAddress: signer3, Group: 2, Index: 2},
263+
{EvmAddress: signer4, Group: 2, Index: 3},
264+
},
265+
},
266+
additionalConfig: AdditionalConfig{
267+
ChainID: 1,
268+
MultisigID: [32]uint8{},
269+
Owner: testOwner.PublicKey(),
270+
ProposedOwner: proposedOwner.PublicKey(),
271+
},
272+
assert: func(t *testing.T, got bindings.MultisigConfig) {
273+
t.Helper()
274+
275+
require.Empty(t, cmp.Diff(got, bindings.MultisigConfig{
276+
Owner: testOwner.PublicKey(),
277+
ProposedOwner: proposedOwner.PublicKey(),
278+
ChainId: 1,
279+
MultisigId: [32]uint8{},
280+
GroupQuorums: [32]uint8{
281+
2, // group 0
282+
1, // group 1
283+
2, // group 2
284+
},
285+
GroupParents: [32]uint8{
286+
0, // group 0
287+
0, // group 1 has parent group 0
288+
1, // group 2 has parent group 1
289+
},
290+
Signers: []bindings.McmSigner{
291+
{EvmAddress: signer1, Group: 0, Index: 0},
292+
{EvmAddress: signer2, Group: 1, Index: 1},
293+
{EvmAddress: signer3, Group: 2, Index: 2},
294+
{EvmAddress: signer4, Group: 2, Index: 3},
295+
},
296+
}))
297+
},
298+
},
299+
}
300+
301+
for _, tt := range tests {
302+
t.Run(tt.name, func(t *testing.T) {
303+
t.Parallel()
304+
305+
transformer := ConfigTransformer{}
306+
got, err := transformer.ToChainConfig(tt.give, tt.additionalConfig)
307+
308+
if tt.wantErr != "" {
309+
require.EqualError(t, err, tt.wantErr)
310+
} else {
311+
require.NoError(t, err)
312+
require.Equal(t, tt.want, got)
313+
}
314+
})
315+
}
316+
}

0 commit comments

Comments
 (0)