diff --git a/go.mod b/go.mod index a75eac1..38f6ae2 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,10 @@ require ( github.com/gagliardetto/solana-go v1.13.0 github.com/smartcontractkit/ccip-owner-contracts v0.1.0 github.com/smartcontractkit/chain-selectors v1.0.97 + github.com/smartcontractkit/chainlink-common v0.10.1-0.20260217160002-b56cb5356cc7 github.com/smartcontractkit/chainlink-deployments-framework v0.98.0 + github.com/smartcontractkit/chainlink-evm v0.3.3 + github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260421142741-9c7fbaf7c828 github.com/smartcontractkit/mcms v0.40.1 github.com/stretchr/testify v1.11.1 gopkg.in/yaml.v3 v3.0.1 @@ -209,7 +212,6 @@ require ( github.com/smartcontractkit/chainlink-aptos v0.0.0-20260306142855-8d629e752265 // indirect github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d // indirect github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5 // indirect - github.com/smartcontractkit/chainlink-common v0.10.1-0.20260217160002-b56cb5356cc7 // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 // indirect github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396 // indirect github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 // indirect diff --git a/go.sum b/go.sum index 8be5eb1..4cc5e17 100644 --- a/go.sum +++ b/go.sum @@ -633,8 +633,8 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= -github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -746,6 +746,10 @@ github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9 github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10/go.mod h1:oiDa54M0FwxevWwyAX773lwdWvFYYlYHHQV1LQ5HpWY= github.com/smartcontractkit/chainlink-deployments-framework v0.98.0 h1:Ov/KOEtubOHXX8oa9UtARhHmkQNCOIjWNt+Zi0AuzHM= github.com/smartcontractkit/chainlink-deployments-framework v0.98.0/go.mod h1:24dwRW1PYolrlxSth///ddG3auGqR+50xaJiXfUHhkg= +github.com/smartcontractkit/chainlink-evm v0.3.3 h1:JqwyJEtnNEUaoQQPoOBTT4sn2lpdIZHtf0Hr0M60YDw= +github.com/smartcontractkit/chainlink-evm v0.3.3/go.mod h1:q0ZBvaoisNaqC8NcMYWNPTjee88nQktDEeJMQHq3hVI= +github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260421142741-9c7fbaf7c828 h1:BmsFk/TSHL6dPPR86GTqgSrUXLSINNFC6cfpFRrQX+4= +github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260421142741-9c7fbaf7c828/go.mod h1:a260YnLyWq2NHLUN5cSVyMGk9nhO6RguCaTI2rsVqyA= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396 h1:03tbcwjyIEjvHba1IWOj1sfThwebm2XNzyFHSuZtlWc= github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY= diff --git a/pkg/common/types.go b/pkg/common/types.go new file mode 100644 index 0000000..622ccab --- /dev/null +++ b/pkg/common/types.go @@ -0,0 +1,36 @@ +package common + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" +) + +type ContractMetaData struct { + TypeAndVersion string `json:"typeAndVersion,omitempty"` + Address common.Address `json:"address,omitempty"` + Owner common.Address `json:"owner,omitempty"` +} + +func NewContractMetaData(tv Meta, addr common.Address) (ContractMetaData, error) { + tvStr, err := tv.TypeAndVersion(nil) + if err != nil { + return ContractMetaData{}, fmt.Errorf("failed to get type and version addr %s: %w", addr.String(), err) + } + owner, err := tv.Owner(nil) + if err != nil { + return ContractMetaData{}, fmt.Errorf("failed to get owner addr %s: %w", addr.String(), err) + } + + return ContractMetaData{ + TypeAndVersion: tvStr, + Address: addr, + Owner: owner, + }, nil +} + +type Meta interface { + TypeAndVersion(opts *bind.CallOpts) (string, error) + Owner(opts *bind.CallOpts) (common.Address, error) +} diff --git a/pkg/common/version.go b/pkg/common/version.go new file mode 100644 index 0000000..b53c72c --- /dev/null +++ b/pkg/common/version.go @@ -0,0 +1,21 @@ +package common + +import ( + "github.com/Masterminds/semver/v3" +) + +var ( + Version0_5_0 = *semver.MustParse("0.5.0") + Version1_0_0 = *semver.MustParse("1.0.0") + Version1_1_0 = *semver.MustParse("1.1.0") + Version1_2_0 = *semver.MustParse("1.2.0") + Version1_5_0 = *semver.MustParse("1.5.0") + Version1_5_1 = *semver.MustParse("1.5.1") + Version1_6_0 = *semver.MustParse("1.6.0") + Version1_6_1 = *semver.MustParse("1.6.1") + Version1_6_1Dev = *semver.MustParse("1.6.1-dev") + Version1_6_2 = *semver.MustParse("1.6.2") + Version1_6_3Dev = *semver.MustParse("1.6.3-dev") + Version1_6_3 = *semver.MustParse("1.6.3") + Version1_7_0 = *semver.MustParse("1.7.0") +) diff --git a/pkg/contract/link/view/v1_0/link_token.go b/pkg/contract/link/view/v1_0/link_token.go new file mode 100644 index 0000000..79412f6 --- /dev/null +++ b/pkg/contract/link/view/v1_0/link_token.go @@ -0,0 +1,61 @@ +package v1_0 + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + linkcontracts "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/contracts/link" + + "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/initial/link_token" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + + cldchangesetscommon "github.com/smartcontractkit/cld-changesets/pkg/common" +) + +type LinkTokenView struct { + cldchangesetscommon.ContractMetaData + Decimals uint8 `json:"decimals"` + Supply *big.Int `json:"supply"` + Minters []common.Address `json:"minters"` + Burners []common.Address `json:"burners"` +} + +func GenerateLinkTokenView(lt *link_token.LinkToken) (LinkTokenView, error) { + owner, err := lt.Owner(nil) + if err != nil { + owner = common.Address{} + } + decimals, err := lt.Decimals(nil) + if err != nil { + return LinkTokenView{}, fmt.Errorf("failed to get decimals %s: %w", lt.Address(), err) + } + totalSupply, err := lt.TotalSupply(nil) + if err != nil { + return LinkTokenView{}, fmt.Errorf("failed to get total supply %s: %w", lt.Address(), err) + } + minters, err := lt.GetMinters(nil) + if err != nil { + minters = []common.Address{} + } + burners, err := lt.GetBurners(nil) + if err != nil { + burners = []common.Address{} + } + + return LinkTokenView{ + ContractMetaData: cldchangesetscommon.ContractMetaData{ + TypeAndVersion: cldf.TypeAndVersion{ + Type: linkcontracts.LinkToken, + Version: cldchangesetscommon.Version1_0_0, + }.String(), + Address: lt.Address(), + Owner: owner, + }, + Decimals: decimals, + Supply: totalSupply, + Minters: minters, + Burners: burners, + }, nil +} diff --git a/pkg/contract/link/view/v1_0/link_token_test.go b/pkg/contract/link/view/v1_0/link_token_test.go new file mode 100644 index 0000000..cac8484 --- /dev/null +++ b/pkg/contract/link/view/v1_0/link_token_test.go @@ -0,0 +1,87 @@ +package v1_0 + +import ( + "math/big" + "testing" + + chainselectors "github.com/smartcontractkit/chain-selectors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + + "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/initial/link_token" + + cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" +) + +func TestLinkTokenView(t *testing.T) { + t.Parallel() + + selector := chainselectors.TEST_90000001.Selector + env, err := environment.New(t.Context(), + environment.WithEVMSimulated(t, []uint64{selector}), + ) + require.NoError(t, err) + + chain := env.BlockChains.EVMChains()[selector] + _, tx, lt, err := link_token.DeployLinkToken(chain.DeployerKey, chain.Client) + require.NoError(t, err) + _, err = chain.Confirm(tx) + require.NoError(t, err) + + testLinkTokenViewWithChain(t, chain, lt) +} + +func TestLinkTokenViewZk(t *testing.T) { + // Timeouts in CI + tests.SkipFlakey(t, "https://smartcontract-it.atlassian.net/browse/CCIP-6427") + t.Parallel() + + selector := chainselectors.TEST_90000050.Selector + env, err := environment.New(t.Context(), + environment.WithZKSyncContainer(t, []uint64{selector}), + ) + require.NoError(t, err) + + chain := env.BlockChains.EVMChains()[selector] + _, _, lt, err := link_token.DeployLinkTokenZk(nil, chain.ClientZkSyncVM, chain.DeployerKeyZkSyncVM, chain.Client) + require.NoError(t, err) + + testLinkTokenViewWithChain(t, chain, lt) +} + +func testLinkTokenViewWithChain(t *testing.T, chain cldf_evm.Chain, lt *link_token.LinkToken) { + t.Helper() + + v, err := GenerateLinkTokenView(lt) + require.NoError(t, err) + + assert.Equal(t, v.Owner, chain.DeployerKey.From) + assert.Equal(t, "LinkToken 1.0.0", v.TypeAndVersion) + assert.Equal(t, uint8(18), v.Decimals) + // Initially nothing minted and no minters/burners. + assert.Equal(t, "0", v.Supply.String()) + require.Empty(t, v.Minters) + require.Empty(t, v.Burners) + + // Add some minters + tx, err := lt.GrantMintAndBurnRoles(chain.DeployerKey, chain.DeployerKey.From) + require.NoError(t, err) + _, err = chain.Confirm(tx) + require.NoError(t, err) + tx, err = lt.Mint(chain.DeployerKey, chain.DeployerKey.From, big.NewInt(100)) + require.NoError(t, err) + _, err = chain.Confirm(tx) + require.NoError(t, err) + + v, err = GenerateLinkTokenView(lt) + require.NoError(t, err) + + assert.Equal(t, "100", v.Supply.String()) + require.Len(t, v.Minters, 1) + require.Equal(t, v.Minters[0].String(), chain.DeployerKey.From.String()) + require.Len(t, v.Burners, 1) + require.Equal(t, v.Burners[0].String(), chain.DeployerKey.From.String()) +} diff --git a/pkg/contract/link/view/v1_0/static_link_token.go b/pkg/contract/link/view/v1_0/static_link_token.go new file mode 100644 index 0000000..14ae1db --- /dev/null +++ b/pkg/contract/link/view/v1_0/static_link_token.go @@ -0,0 +1,43 @@ +package v1_0 + +import ( + "fmt" + "math/big" + + linkcontracts "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/contracts/link" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/link_token_interface" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + + "github.com/smartcontractkit/cld-changesets/pkg/common" +) + +type StaticLinkTokenView struct { + common.ContractMetaData + Decimals uint8 `json:"decimals"` + Supply *big.Int `json:"supply"` +} + +func GenerateStaticLinkTokenView(lt *link_token_interface.LinkToken) (StaticLinkTokenView, error) { + decimals, err := lt.Decimals(nil) + if err != nil { + return StaticLinkTokenView{}, fmt.Errorf("failed to get decimals %s: %w", lt.Address(), err) + } + totalSupply, err := lt.TotalSupply(nil) + if err != nil { + return StaticLinkTokenView{}, fmt.Errorf("failed to get total supply %s: %w", lt.Address(), err) + } + + return StaticLinkTokenView{ + ContractMetaData: common.ContractMetaData{ + TypeAndVersion: cldf.TypeAndVersion{ + Type: linkcontracts.StaticLinkToken, + Version: common.Version1_0_0, + }.String(), + Address: lt.Address(), + // No owner. + }, + Decimals: decimals, + Supply: totalSupply, + }, nil +} diff --git a/pkg/contract/link/view/v1_0/static_link_token_test.go b/pkg/contract/link/view/v1_0/static_link_token_test.go new file mode 100644 index 0000000..92a99db --- /dev/null +++ b/pkg/contract/link/view/v1_0/static_link_token_test.go @@ -0,0 +1,39 @@ +package v1_0 + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + chain_selectors "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" + + "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/link_token_interface" +) + +func TestStaticLinkTokenView(t *testing.T) { + t.Parallel() + + selector := chain_selectors.TEST_90000001.Selector + env, err := environment.New(t.Context(), + environment.WithEVMSimulated(t, []uint64{selector}), + ) + require.NoError(t, err) + + chain := env.BlockChains.EVMChains()[selector] + _, tx, lt, err := link_token_interface.DeployLinkToken(chain.DeployerKey, chain.Client) + require.NoError(t, err) + _, err = chain.Confirm(tx) + require.NoError(t, err) + v, err := GenerateStaticLinkTokenView(lt) + require.NoError(t, err) + + assert.Equal(t, v.Owner, common.HexToAddress("0x0")) // Ownerless + assert.Equal(t, "StaticLinkToken 1.0.0", v.TypeAndVersion) + assert.Equal(t, uint8(18), v.Decimals) + assert.Equal(t, "1000000000000000000000000000", v.Supply.String()) +} diff --git a/pkg/contract/mcms/view/v1_0/mcms.go b/pkg/contract/mcms/view/v1_0/mcms.go new file mode 100644 index 0000000..12a30c1 --- /dev/null +++ b/pkg/contract/mcms/view/v1_0/mcms.go @@ -0,0 +1,305 @@ +package v1_0 + +import ( + "context" + "fmt" + "math/big" + "slices" + + "github.com/ethereum/go-ethereum/common" + "github.com/gagliardetto/solana-go" + owner_helpers "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" + mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" + "github.com/smartcontractkit/mcms/sdk/evm/bindings" + mcmssolanasdk "github.com/smartcontractkit/mcms/sdk/solana" + mcmstypes "github.com/smartcontractkit/mcms/types" + + "github.com/smartcontractkit/chainlink-evm/pkg/utils" + + cldcommon "github.com/smartcontractkit/cld-changesets/pkg/common" +) + +type Role struct { + ID common.Hash + Name string +} + +const ( + EXECUTOR_ROLE_STR = "EXECUTOR_ROLE" + //nolint:gosec // Public role identifier constant, not a credential. + BYPASSER_ROLE_STR = "BYPASSER_ROLE" + CANCELLER_ROLE_STR = "CANCELLER_ROLE" + PROPOSER_ROLE_STR = "PROPOSER_ROLE" + ADMIN_ROLE_STR = "ADMIN_ROLE" +) + +// https://github.com/smartcontractkit/ccip-owner-contracts/blob/9d81692b324ce7ea2ef8a75e683889edbc7e2dd0/src/RBACTimelock.sol#L71 +// Just to avoid invoking the Go binding to get these. +var ( + ADMIN_ROLE = Role{ + ID: utils.MustHash(ADMIN_ROLE_STR), + Name: ADMIN_ROLE_STR, + } + PROPOSER_ROLE = Role{ + ID: utils.MustHash(PROPOSER_ROLE_STR), + Name: PROPOSER_ROLE_STR, + } + BYPASSER_ROLE = Role{ + ID: utils.MustHash(BYPASSER_ROLE_STR), + Name: BYPASSER_ROLE_STR, + } + CANCELLER_ROLE = Role{ + ID: utils.MustHash(CANCELLER_ROLE_STR), + Name: CANCELLER_ROLE_STR, + } + EXECUTOR_ROLE = Role{ + ID: utils.MustHash(EXECUTOR_ROLE_STR), + Name: EXECUTOR_ROLE_STR, + } +) + +// --- evm --- + +type MCMSView struct { + cldcommon.ContractMetaData + // Note config is json marshallable. + Config mcmstypes.Config `json:"config"` +} + +func GenerateMCMSView(mcms owner_helpers.ManyChainMultiSig) (MCMSView, error) { + owner, err := mcms.Owner(nil) + if err != nil { + return MCMSView{}, err + } + mcmsConfig, err := mcms.GetConfig(nil) + if err != nil { + return MCMSView{}, err + } + + mapSigners := func(in []owner_helpers.ManyChainMultiSigSigner) []bindings.ManyChainMultiSigSigner { + out := make([]bindings.ManyChainMultiSigSigner, len(in)) + for i, s := range in { + out[i] = bindings.ManyChainMultiSigSigner{Addr: s.Addr, Index: s.Index, Group: s.Group} + } + + return out + } + + parsedConfig, err := mcmsevmsdk.NewConfigTransformer().ToConfig(bindings.ManyChainMultiSigConfig{ + Signers: mapSigners(mcmsConfig.Signers), + GroupQuorums: mcmsConfig.GroupQuorums, + GroupParents: mcmsConfig.GroupParents, + }) + if err != nil { + return MCMSView{}, err + } + + return MCMSView{ + // Has no type and version on the contract + ContractMetaData: cldcommon.ContractMetaData{ + Owner: owner, + Address: mcms.Address(), + }, + Config: *parsedConfig, + }, nil +} + +type TimelockView struct { + cldcommon.ContractMetaData + MembersByRole map[string][]common.Address `json:"membersByRole"` +} + +func GenerateTimelockView(tl owner_helpers.RBACTimelock) (TimelockView, error) { + membersByRole := make(map[string][]common.Address) + for _, role := range []Role{ADMIN_ROLE, PROPOSER_ROLE, BYPASSER_ROLE, CANCELLER_ROLE, EXECUTOR_ROLE} { + numMembers, err := tl.GetRoleMemberCount(nil, role.ID) + if err != nil { + return TimelockView{}, fmt.Errorf("get role member count for role %s (%s): %w", role.Name, role.ID.Hex(), err) + } + memberCount := numMembers.Int64() + for i := range memberCount { + member, err2 := tl.GetRoleMember(nil, role.ID, big.NewInt(i)) + if err2 != nil { + return TimelockView{}, fmt.Errorf("get role member for role %s (%s) at index %d: %w", role.Name, role.ID.Hex(), i, err2) + } + membersByRole[role.Name] = append(membersByRole[role.Name], member) + } + } + + return TimelockView{ + // Has no type and version or owner. + ContractMetaData: cldcommon.ContractMetaData{ + Address: tl.Address(), + }, + MembersByRole: membersByRole, + }, nil +} + +type CallProxyView struct { + cldcommon.ContractMetaData +} + +func GenerateCallProxyView(cp owner_helpers.CallProxy) (CallProxyView, error) { + return CallProxyView{ + ContractMetaData: cldcommon.ContractMetaData{ + Address: cp.Address(), + }, + }, nil +} + +type MCMSWithTimelockView struct { + Bypasser MCMSView `json:"bypasser"` + Canceller MCMSView `json:"canceller"` + Proposer MCMSView `json:"proposer"` + Timelock TimelockView `json:"timelock"` + CallProxy CallProxyView `json:"callProxy"` +} + +func GenerateMCMSWithTimelockView( + bypasser owner_helpers.ManyChainMultiSig, + canceller owner_helpers.ManyChainMultiSig, + proposer owner_helpers.ManyChainMultiSig, + timelock owner_helpers.RBACTimelock, + callProxy owner_helpers.CallProxy, +) (MCMSWithTimelockView, error) { + timelockView, err := GenerateTimelockView(timelock) + if err != nil { + return MCMSWithTimelockView{}, err + } + callProxyView, err := GenerateCallProxyView(callProxy) + if err != nil { + return MCMSWithTimelockView{}, err + } + bypasserView, err := GenerateMCMSView(bypasser) + if err != nil { + return MCMSWithTimelockView{}, err + } + proposerView, err := GenerateMCMSView(proposer) + if err != nil { + return MCMSWithTimelockView{}, err + } + cancellerView, err := GenerateMCMSView(canceller) + if err != nil { + return MCMSWithTimelockView{}, err + } + + return MCMSWithTimelockView{ + Timelock: timelockView, + Bypasser: bypasserView, + Proposer: proposerView, + Canceller: cancellerView, + CallProxy: callProxyView, + }, nil +} + +// --- solana --- + +type MCMSWithTimelockViewSolana struct { + Bypasser MCMViewSolana `json:"bypasser"` + Canceller MCMViewSolana `json:"canceller"` + Proposer MCMViewSolana `json:"proposer"` + Timelock TimelockViewSolana `json:"timelock"` +} + +func GenerateMCMSWithTimelockViewSolana( + ctx context.Context, + inspector *mcmssolanasdk.Inspector, + timelockInspector *mcmssolanasdk.TimelockInspector, + mcmProgram solana.PublicKey, + proposerMcmSeed [32]byte, + cancellerMcmSeed [32]byte, + bypasserMcmSeed [32]byte, + timelockProgram solana.PublicKey, + timelockSeed [32]byte, +) (MCMSWithTimelockViewSolana, error) { + timelockView, err := GenerateTimelockViewSolana(ctx, timelockInspector, timelockProgram, timelockSeed) + if err != nil { + return MCMSWithTimelockViewSolana{}, fmt.Errorf("unable to generate timelock view: %w", err) + } + bypasserView, err := GenerateMCMViewSolana(ctx, inspector, mcmProgram, bypasserMcmSeed) + if err != nil { + return MCMSWithTimelockViewSolana{}, fmt.Errorf("unable to generate bypasser mcm view: %w", err) + } + proposerView, err := GenerateMCMViewSolana(ctx, inspector, mcmProgram, proposerMcmSeed) + if err != nil { + return MCMSWithTimelockViewSolana{}, fmt.Errorf("unable to generate proposer mcm view: %w", err) + } + cancellerView, err := GenerateMCMViewSolana(ctx, inspector, mcmProgram, cancellerMcmSeed) + if err != nil { + return MCMSWithTimelockViewSolana{}, fmt.Errorf("unable to generate canceller mcm view: %w", err) + } + + return MCMSWithTimelockViewSolana{ + Timelock: timelockView, + Bypasser: bypasserView, + Proposer: proposerView, + Canceller: cancellerView, + }, nil +} + +type MCMViewSolana struct { + ProgramID solana.PublicKey `json:"programID"` + Seed string `json:"seed"` + Owner solana.PublicKey `json:"owner"` + Config mcmstypes.Config `json:"config"` +} + +func GenerateMCMViewSolana( + ctx context.Context, inspector *mcmssolanasdk.Inspector, programID solana.PublicKey, seed [32]byte, +) (MCMViewSolana, error) { + address := mcmssolanasdk.ContractAddress(programID, mcmssolanasdk.PDASeed(seed)) + config, err := inspector.GetConfig(ctx, address) + if err != nil { + return MCMViewSolana{}, fmt.Errorf("unable to get config from mcm (%v): %w", address, err) + } + + return MCMViewSolana{ + ProgramID: programID, + Seed: string(seed[:]), + Owner: solana.PublicKey{}, // FIXME: needs inspector.GetOwner() in mcms solana sdk + Config: *config, + }, nil +} + +type TimelockViewSolana struct { + ProgramID solana.PublicKey `json:"programID"` + Seed string `json:"seed"` + Owner solana.PublicKey `json:"owner"` + Proposers []string `json:"proposers"` + Executors []string `json:"executors"` + Cancellers []string `json:"cancellers"` + Bypassers []string `json:"bypassers"` +} + +func GenerateTimelockViewSolana( + ctx context.Context, inspector *mcmssolanasdk.TimelockInspector, programID solana.PublicKey, seed [32]byte, +) (TimelockViewSolana, error) { + address := mcmssolanasdk.ContractAddress(programID, mcmssolanasdk.PDASeed(seed)) + + proposers, err := inspector.GetProposers(ctx, address) + if err != nil { + return TimelockViewSolana{}, fmt.Errorf("unable to get proposers from timelock (%v): %w", address, err) + } + executors, err := inspector.GetExecutors(ctx, address) + if err != nil { + return TimelockViewSolana{}, fmt.Errorf("unable to get executors from timelock (%v): %w", address, err) + } + cancellers, err := inspector.GetCancellers(ctx, address) + if err != nil { + return TimelockViewSolana{}, fmt.Errorf("unable to get cancellers from timelock (%v): %w", address, err) + } + bypassers, err := inspector.GetBypassers(ctx, address) + if err != nil { + return TimelockViewSolana{}, fmt.Errorf("unable to get bypassers from timelock (%v): %w", address, err) + } + + return TimelockViewSolana{ + ProgramID: programID, + Seed: string(seed[:]), + Owner: solana.PublicKey{}, // FIXME: needs inspector.GetOwner() in mcms solana sdk + Proposers: slices.Sorted(slices.Values(proposers)), + Executors: slices.Sorted(slices.Values(executors)), + Cancellers: slices.Sorted(slices.Values(cancellers)), + Bypassers: slices.Sorted(slices.Values(bypassers)), + }, nil +} diff --git a/pkg/family/solana/state.go b/pkg/family/solana/state.go index 8dcc2de..f5146f7 100644 --- a/pkg/family/solana/state.go +++ b/pkg/family/solana/state.go @@ -6,7 +6,6 @@ import ( "github.com/gagliardetto/solana-go" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - mcmscontracts "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/contracts/mcms" mcmssolanasdk "github.com/smartcontractkit/mcms/sdk/solana" )