diff --git a/pkg/family/evm/link_state.go b/pkg/family/evm/link_state.go new file mode 100644 index 0000000..1591717 --- /dev/null +++ b/pkg/family/evm/link_state.go @@ -0,0 +1,108 @@ +package evm + +import ( + "errors" + "fmt" + + "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/link_token_interface" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/initial/link_token" + + cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + linkcontracts "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/contracts/link" + + cldchangesetscommon "github.com/smartcontractkit/cld-changesets/pkg/common" + v1_0 "github.com/smartcontractkit/cld-changesets/pkg/contract/link/view/v1_0" +) + +type LinkTokenState struct { + LinkToken *link_token.LinkToken +} + +// GenerateLinkView generates the LinkTokenView for the LinkTokenState. +func (s LinkTokenState) GenerateLinkView() (v1_0.LinkTokenView, error) { + if s.LinkToken == nil { + return v1_0.LinkTokenView{}, errors.New("link token not found") + } + + return v1_0.GenerateLinkTokenView(s.LinkToken) +} + +// MaybeLoadLinkTokenChainState loads the LinkTokenState for the given chain and addresses. +func MaybeLoadLinkTokenChainState(chain cldf_evm.Chain, addresses map[string]cldf.TypeAndVersion) (*LinkTokenState, error) { + state := LinkTokenState{} + // todo(ggoh): version should be configurable? + linkToken := cldf.NewTypeAndVersion(linkcontracts.LinkToken, cldchangesetscommon.Version1_0_0) + + wantTypes := []cldf.TypeAndVersion{linkToken} + + // Ensure we either have the bundle or not. + _, err := cldf.EnsureDeduped(addresses, wantTypes) + if err != nil { + return nil, fmt.Errorf("unable to check link token on chain %s error: %w", chain.Name(), err) + } + + for address, tv := range addresses { + if tv.Type == linkToken.Type && tv.Version.String() == linkToken.Version.String() { + addr, err := evmContractAddr(chain, address, tv) + if err != nil { + return nil, err + } + lt, err := link_token.NewLinkToken(addr, chain.Client) + if err != nil { + return nil, fmt.Errorf("failed to bind %s on chain %s (selector=%d) at address %q: %w", + tv.Type, chain.Name(), chain.Selector, address, err) + } + state.LinkToken = lt + } + } + + // todo(ggoh): should return error when link token is not found? + return &state, nil +} + +type StaticLinkTokenState struct { + StaticLinkToken *link_token_interface.LinkToken +} + +// GenerateStaticLinkView generates the StaticLinkTokenView for the StaticLinkTokenState. +func (s StaticLinkTokenState) GenerateStaticLinkView() (v1_0.StaticLinkTokenView, error) { + if s.StaticLinkToken == nil { + return v1_0.StaticLinkTokenView{}, errors.New("static link token not found") + } + + return v1_0.GenerateStaticLinkTokenView(s.StaticLinkToken) +} + +// MaybeLoadStaticLinkTokenState loads the StaticLinkTokenState for the given chain and addresses. +func MaybeLoadStaticLinkTokenState(chain cldf_evm.Chain, addresses map[string]cldf.TypeAndVersion) (*StaticLinkTokenState, error) { + state := StaticLinkTokenState{} + // todo(ggoh): version should be configurable? + staticLinkToken := cldf.NewTypeAndVersion(linkcontracts.StaticLinkToken, cldchangesetscommon.Version1_0_0) + + wantTypes := []cldf.TypeAndVersion{staticLinkToken} + + // Ensure we either have the bundle or not. + _, err := cldf.EnsureDeduped(addresses, wantTypes) + if err != nil { + return nil, fmt.Errorf("unable to check static link token on chain %s error: %w", chain.Name(), err) + } + + for address, tv := range addresses { + if tv.Type == staticLinkToken.Type && tv.Version.String() == staticLinkToken.Version.String() { + addr, err := evmContractAddr(chain, address, tv) + if err != nil { + return nil, err + } + lt, err := link_token_interface.NewLinkToken(addr, chain.Client) + if err != nil { + return nil, fmt.Errorf("failed to bind %s on chain %s (selector=%d) at address %q: %w", + tv.Type, chain.Name(), chain.Selector, address, err) + } + state.StaticLinkToken = lt + } + } + + // todo(ggoh): should return error when link token is not found? + return &state, nil +} diff --git a/pkg/family/evm/link_state_test.go b/pkg/family/evm/link_state_test.go new file mode 100644 index 0000000..d98c24b --- /dev/null +++ b/pkg/family/evm/link_state_test.go @@ -0,0 +1,171 @@ +package evm + +import ( + "fmt" + "testing" + + chainsel "github.com/smartcontractkit/chain-selectors" + cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + linkcontracts "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/contracts/link" + "github.com/stretchr/testify/require" + + cldchangesetscommon "github.com/smartcontractkit/cld-changesets/pkg/common" +) + +func TestLinkTokenState_GenerateLinkView(t *testing.T) { + t.Parallel() + t.Run("nil binding", func(t *testing.T) { + t.Parallel() + _, err := LinkTokenState{}.GenerateLinkView() + require.ErrorContains(t, err, "link token not found") + }) +} + +func TestStaticLinkTokenState_GenerateStaticLinkView(t *testing.T) { + t.Parallel() + t.Run("nil binding", func(t *testing.T) { + t.Parallel() + _, err := StaticLinkTokenState{}.GenerateStaticLinkView() + require.ErrorContains(t, err, "static link token not found") + }) +} + +func TestMaybeLoadLinkTokenChainState(t *testing.T) { + t.Parallel() + chain := testSepoliaChain(t) + linkTV := cldf.NewTypeAndVersion(linkcontracts.LinkToken, cldchangesetscommon.Version1_0_0) + + t.Run("empty addresses map returns non-nil state with nil LinkToken", func(t *testing.T) { + t.Parallel() + got, err := MaybeLoadLinkTokenChainState(chain, map[string]cldf.TypeAndVersion{}) + require.NoError(t, err) + require.NotNil(t, got) + require.Nil(t, got.LinkToken) + }) + + t.Run("nil addresses map returns non-nil state with nil LinkToken", func(t *testing.T) { + t.Parallel() + got, err := MaybeLoadLinkTokenChainState(chain, nil) + require.NoError(t, err) + require.NotNil(t, got) + require.Nil(t, got.LinkToken) + }) + + t.Run("duplicate link token addresses returns wrapped error", func(t *testing.T) { + t.Parallel() + addrs := map[string]cldf.TypeAndVersion{ + "0x0000000000000000000000000000000000000001": linkTV, + "0x0000000000000000000000000000000000000002": linkTV, + } + _, err := MaybeLoadLinkTokenChainState(chain, addrs) + require.ErrorContains(t, err, fmt.Sprintf( + "unable to check link token on chain %s error: found more than one instance of contract", + chain.Name())) + }) + + t.Run("no matching link token version leaves binding nil", func(t *testing.T) { + t.Parallel() + addrs := map[string]cldf.TypeAndVersion{ + "0x0000000000000000000000000000000000000001": cldf.NewTypeAndVersion(linkcontracts.LinkToken, cldchangesetscommon.Version1_1_0), + } + got, err := MaybeLoadLinkTokenChainState(chain, addrs) + require.NoError(t, err) + require.NotNil(t, got) + require.Nil(t, got.LinkToken) + }) + + t.Run("invalid link token address returns error", func(t *testing.T) { + t.Parallel() + addrs := map[string]cldf.TypeAndVersion{ + "not-a-valid-hex-addr": linkTV, + } + _, err := MaybeLoadLinkTokenChainState(chain, addrs) + require.ErrorContains(t, err, fmt.Sprintf( + "chain %s (selector=%d) contract %s %s address \"not-a-valid-hex-addr\": not a valid hex-encoded EVM address", + chain.Name(), chain.Selector, linkTV.Type, linkTV.Version.String())) + }) + + t.Run("zero link token address returns error", func(t *testing.T) { + t.Parallel() + addrs := map[string]cldf.TypeAndVersion{ + "0x0000000000000000000000000000000000000000": linkTV, + } + _, err := MaybeLoadLinkTokenChainState(chain, addrs) + require.ErrorContains(t, err, fmt.Sprintf( + "chain %s (selector=%d) contract %s %s address \"0x0000000000000000000000000000000000000000\": EVM address must not be the zero address", + chain.Name(), chain.Selector, linkTV.Type, linkTV.Version.String())) + }) +} + +func TestMaybeLoadStaticLinkTokenState(t *testing.T) { + t.Parallel() + chain := testSepoliaChain(t) + staticTV := cldf.NewTypeAndVersion(linkcontracts.StaticLinkToken, cldchangesetscommon.Version1_0_0) + + t.Run("empty addresses map returns non-nil state with nil StaticLinkToken", func(t *testing.T) { + t.Parallel() + got, err := MaybeLoadStaticLinkTokenState(chain, map[string]cldf.TypeAndVersion{}) + require.NoError(t, err) + require.NotNil(t, got) + require.Nil(t, got.StaticLinkToken) + }) + + t.Run("nil addresses map returns non-nil state with nil StaticLinkToken", func(t *testing.T) { + t.Parallel() + got, err := MaybeLoadStaticLinkTokenState(chain, nil) + require.NoError(t, err) + require.NotNil(t, got) + require.Nil(t, got.StaticLinkToken) + }) + + t.Run("duplicate static link token addresses returns wrapped error", func(t *testing.T) { + t.Parallel() + addrs := map[string]cldf.TypeAndVersion{ + "0x0000000000000000000000000000000000000001": staticTV, + "0x0000000000000000000000000000000000000002": staticTV, + } + _, err := MaybeLoadStaticLinkTokenState(chain, addrs) + require.ErrorContains(t, err, fmt.Sprintf( + "unable to check static link token on chain %s error: found more than one instance of contract", + chain.Name())) + }) + + t.Run("no matching static link token version leaves binding nil", func(t *testing.T) { + t.Parallel() + addrs := map[string]cldf.TypeAndVersion{ + "0x0000000000000000000000000000000000000001": cldf.NewTypeAndVersion(linkcontracts.StaticLinkToken, cldchangesetscommon.Version1_1_0), + } + got, err := MaybeLoadStaticLinkTokenState(chain, addrs) + require.NoError(t, err) + require.NotNil(t, got) + require.Nil(t, got.StaticLinkToken) + }) + + t.Run("invalid static link token address returns error", func(t *testing.T) { + t.Parallel() + addrs := map[string]cldf.TypeAndVersion{ + "not-a-valid-hex-addr": staticTV, + } + _, err := MaybeLoadStaticLinkTokenState(chain, addrs) + require.ErrorContains(t, err, fmt.Sprintf( + "chain %s (selector=%d) contract %s %s address \"not-a-valid-hex-addr\": not a valid hex-encoded EVM address", + chain.Name(), chain.Selector, staticTV.Type, staticTV.Version.String())) + }) + + t.Run("zero static link token address returns error", func(t *testing.T) { + t.Parallel() + addrs := map[string]cldf.TypeAndVersion{ + "0x0000000000000000000000000000000000000000": staticTV, + } + _, err := MaybeLoadStaticLinkTokenState(chain, addrs) + require.ErrorContains(t, err, fmt.Sprintf( + "chain %s (selector=%d) contract %s %s address \"0x0000000000000000000000000000000000000000\": EVM address must not be the zero address", + chain.Name(), chain.Selector, staticTV.Type, staticTV.Version.String())) + }) +} + +func testSepoliaChain(t *testing.T) cldf_evm.Chain { + t.Helper() + return cldf_evm.Chain{Selector: chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector} +}