diff --git a/go.mod b/go.mod index 96ef8fd..553bc96 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/smartcontractkit/mcms v0.40.1 github.com/stretchr/testify v1.11.1 golang.org/x/mod v0.33.0 + golang.org/x/sync v0.19.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -37,6 +38,22 @@ require ( github.com/avast/retry-go/v4 v4.7.0 // indirect github.com/awalterschulze/gographviz v2.0.3+incompatible // indirect github.com/aws/aws-sdk-go v1.55.8 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.4 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.12 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect + github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.59.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect + github.com/aws/aws-sdk-go-v2/service/kms v1.50.1 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect + github.com/aws/smithy-go v1.24.2 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -218,6 +235,7 @@ require ( github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260226130359-963f935e0396 // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260205130626-db2a2aab956b // indirect + github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4 // indirect github.com/smartcontractkit/chainlink-sui v0.0.0-20260205175622-33e65031f9a9 // indirect github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.18 // indirect github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5 // indirect @@ -238,8 +256,10 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/supranational/blst v0.3.16 // indirect + github.com/suzuki-shunsuke/go-convmap v0.2.1 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/testcontainers/testcontainers-go v0.41.0 // indirect + github.com/testcontainers/testcontainers-go/modules/postgres v0.41.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect @@ -285,7 +305,6 @@ require ( golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect golang.org/x/net v0.50.0 // indirect golang.org/x/oauth2 v0.35.0 // indirect - golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/term v0.40.0 // indirect golang.org/x/text v0.34.0 // indirect diff --git a/go.sum b/go.sum index 79a869b..8be895f 100644 --- a/go.sum +++ b/go.sum @@ -476,12 +476,19 @@ github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgS github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= +github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A= +github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= @@ -568,6 +575,8 @@ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= +github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= @@ -823,6 +832,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/suzuki-shunsuke/go-convmap v0.2.1 h1:g94CxI6ENYluXZhdEH+1WVGhMAE8nLvAmWLUCwBw6W0= +github.com/suzuki-shunsuke/go-convmap v0.2.1/go.mod h1:3XfGRbtyNBMGfXAxhROSRki6/UIlUX31Qt6DvdI6lUs= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= diff --git a/link/changesets/deploy_link_token.go b/link/changesets/deploy_link_token.go new file mode 100644 index 0000000..b8c2af8 --- /dev/null +++ b/link/changesets/deploy_link_token.go @@ -0,0 +1,307 @@ +// Package changesets provides reusable LINK token changesets. +package changesets + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/common" + eth_types "github.com/ethereum/go-ethereum/core/types" + "github.com/gagliardetto/solana-go" + chainsel "github.com/smartcontractkit/chain-selectors" + "golang.org/x/sync/errgroup" + + solCommonUtil "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/common" + solTokenUtil "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/tokens" + cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + cldf_solana "github.com/smartcontractkit/chainlink-deployments-framework/chain/solana" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + linkcontracts "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/contracts/link" + "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/generated/link_token_interface" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/initial/link_token" + + cldchangesetscommon "github.com/smartcontractkit/cld-changesets/pkg/common" +) + +var _ cldf.ChangeSet[[]uint64] = DeployLinkToken +var _ cldf.ChangeSet[[]uint64] = DeployStaticLinkToken +var _ cldf.ChangeSet[DeploySolanaLinkTokenConfig] = DeploySolanaLinkToken + +// DeployLinkToken deploys a link token contract to the chain identified by the ChainSelector. +func DeployLinkToken(e cldf.Environment, chains []uint64) (cldf.ChangesetOutput, error) { + if err := validateSelectorsInEnvironment(e, chains); err != nil { + return cldf.ChangesetOutput{}, err + } + if err := validateNoDuplicateSelectors(chains); err != nil { + return cldf.ChangesetOutput{}, err + } + if err := validateSelectorsFamily(chains, chainsel.FamilyEVM); err != nil { + return cldf.ChangesetOutput{}, err + } + if err := validateNoExistingContract(e, chains, linkTokenTypeAndVersion()); err != nil { + return cldf.ChangesetOutput{}, err + } + + out := newLinkTokenOutput() + deployGrp := errgroup.Group{} + for _, chain := range chains { + deployGrp.Go(func() error { + deploy, err := deployLinkTokenContractEVM( + e.Logger, e.BlockChains.EVMChains()[chain], out.AddressBook, //nolint:staticcheck // SA1019: legacy changeset still supports AddressBook output. + ) + if err != nil { + e.Logger.Errorw("Failed to deploy link token", "chain", chain, "err", err) + + return fmt.Errorf("failed to deploy link token for chain %d: %w", chain, err) + } + + return saveAddressRef(out.DataStore, chain, deploy.Address.String(), linkTokenTypeAndVersion(), "") + }) + } + + return out, deployGrp.Wait() +} + +// DeployStaticLinkToken deploys a static link token contract to the chain identified by the ChainSelector. +func DeployStaticLinkToken(e cldf.Environment, chains []uint64) (cldf.ChangesetOutput, error) { + if err := validateSelectorsInEnvironment(e, chains); err != nil { + return cldf.ChangesetOutput{}, err + } + if err := validateNoDuplicateSelectors(chains); err != nil { + return cldf.ChangesetOutput{}, err + } + if err := validateSelectorsFamily(chains, chainsel.FamilyEVM); err != nil { + return cldf.ChangesetOutput{}, err + } + if err := validateNoExistingContract(e, chains, staticLinkTokenTypeAndVersion()); err != nil { + return cldf.ChangesetOutput{}, err + } + + out := newLinkTokenOutput() + for _, chainSel := range chains { + chain, ok := e.BlockChains.EVMChains()[chainSel] + if !ok { + return cldf.ChangesetOutput{}, fmt.Errorf("chain not found in environment: %d", chainSel) + } + deploy, err := cldf.DeployContract[*link_token_interface.LinkToken](e.Logger, chain, out.AddressBook, //nolint:staticcheck // SA1019: legacy changeset still supports AddressBook output. + func(chain cldf_evm.Chain) cldf.ContractDeploy[*link_token_interface.LinkToken] { + linkTokenAddr, tx, linkToken, err2 := link_token_interface.DeployLinkToken( + chain.DeployerKey, + chain.Client, + ) + + return cldf.ContractDeploy[*link_token_interface.LinkToken]{ + Address: linkTokenAddr, + Contract: linkToken, + Tx: tx, + Tv: staticLinkTokenTypeAndVersion(), + Err: err2, + } + }) + if err != nil { + e.Logger.Errorw("Failed to deploy static link token", "chain", chain.String(), "err", err) + return cldf.ChangesetOutput{}, err + } + if err := saveAddressRef(out.DataStore, chainSel, deploy.Address.String(), staticLinkTokenTypeAndVersion(), ""); err != nil { + return cldf.ChangesetOutput{}, err + } + } + + return out, nil +} + +func deployLinkTokenContractEVM( + lggr logger.Logger, + chain cldf_evm.Chain, + ab cldf.AddressBook, +) (*cldf.ContractDeploy[*link_token.LinkToken], error) { + linkToken, err := cldf.DeployContract[*link_token.LinkToken](lggr, chain, ab, + func(chain cldf_evm.Chain) cldf.ContractDeploy[*link_token.LinkToken] { + var ( + linkTokenAddr common.Address + tx *eth_types.Transaction + linkToken *link_token.LinkToken + err2 error + ) + if !chain.IsZkSyncVM { + linkTokenAddr, tx, linkToken, err2 = link_token.DeployLinkToken( + chain.DeployerKey, + chain.Client, + ) + } else { + linkTokenAddr, _, linkToken, err2 = link_token.DeployLinkTokenZk( + nil, + chain.ClientZkSyncVM, + chain.DeployerKeyZkSyncVM, + chain.Client, + ) + } + + return cldf.ContractDeploy[*link_token.LinkToken]{ + Address: linkTokenAddr, + Contract: linkToken, + Tx: tx, + Tv: linkTokenTypeAndVersion(), + Err: err2, + } + }) + if err != nil { + lggr.Errorw("Failed to deploy link token", "chain", chain.String(), "err", err) + + return linkToken, err + } + + return linkToken, nil +} + +type DeploySolanaLinkTokenConfig struct { + ChainSelector uint64 + TokenPrivKey solana.PrivateKey + TokenDecimals uint8 +} + +func DeploySolanaLinkToken(e cldf.Environment, cfg DeploySolanaLinkTokenConfig) (cldf.ChangesetOutput, error) { + chain, ok := e.BlockChains.SolanaChains()[cfg.ChainSelector] + if !ok { + return cldf.ChangesetOutput{}, fmt.Errorf("chain not found in environment: %d", cfg.ChainSelector) + } + if err := validateNoExistingContract(e, []uint64{cfg.ChainSelector}, linkTokenTypeAndVersion()); err != nil { + return cldf.ChangesetOutput{}, err + } + + mint := cfg.TokenPrivKey + instructions, err := solTokenUtil.CreateToken( + context.Background(), + solana.TokenProgramID, + mint.PublicKey(), + chain.DeployerKey.PublicKey(), + cfg.TokenDecimals, + chain.Client, + cldf_solana.SolDefaultCommitment, + ) + if err != nil { + e.Logger.Errorw("Failed to generate instructions for link token deployment", "chain", chain.String(), "err", err) + return cldf.ChangesetOutput{}, err + } + err = chain.Confirm(instructions, solCommonUtil.AddSigners(mint)) + if err != nil { + e.Logger.Errorw("Failed to confirm instructions for link token deployment", "chain", chain.String(), "err", err) + return cldf.ChangesetOutput{}, err + } + + tv := linkTokenTypeAndVersion() + e.Logger.Infow("Deployed contract", "Contract", tv.String(), "addr", mint.PublicKey().String(), "chain", chain.String()) + + out := newLinkTokenOutput() + if err := out.AddressBook.Save(chain.Selector, mint.PublicKey().String(), tv); err != nil { //nolint:staticcheck // SA1019: legacy changeset still supports AddressBook output. + e.Logger.Errorw("Failed to save link token", "chain", chain.String(), "err", err) + return cldf.ChangesetOutput{}, err + } + if err := saveAddressRef(out.DataStore, chain.Selector, mint.PublicKey().String(), tv, ""); err != nil { + e.Logger.Errorw("Failed to save link token in datastore", "chain", chain.String(), "err", err) + return cldf.ChangesetOutput{}, err + } + + return out, nil +} + +func newLinkTokenOutput() cldf.ChangesetOutput { + return cldf.ChangesetOutput{ + AddressBook: cldf.NewMemoryAddressBook(), + DataStore: datastore.NewMemoryDataStore(), + } +} + +func linkTokenTypeAndVersion() cldf.TypeAndVersion { + return cldf.NewTypeAndVersion(linkcontracts.LinkToken, cldchangesetscommon.Version1_0_0) +} + +func staticLinkTokenTypeAndVersion() cldf.TypeAndVersion { + return cldf.NewTypeAndVersion(linkcontracts.StaticLinkToken, cldchangesetscommon.Version1_0_0) +} + +func saveAddressRef(ds datastore.MutableDataStore, chainSelector uint64, address string, tv cldf.TypeAndVersion, qualifier string) error { + return ds.Addresses().Add(datastore.AddressRef{ + ChainSelector: chainSelector, + Address: address, + Type: datastore.ContractType(tv.Type.String()), + Version: &tv.Version, + Qualifier: qualifier, + Labels: datastore.NewLabelSet(), + }) +} + +func validateSelectorsInEnvironment(e cldf.Environment, chains []uint64) error { + for _, chain := range chains { + if !e.BlockChains.Exists(chain) { + return fmt.Errorf("chain %d not found in environment", chain) + } + } + + return nil +} + +func validateNoDuplicateSelectors(chains []uint64) error { + seen := make(map[uint64]struct{}, len(chains)) + for _, chain := range chains { + if _, ok := seen[chain]; ok { + return fmt.Errorf("duplicate chain selector found: %d", chain) + } + seen[chain] = struct{}{} + } + + return nil +} + +func validateSelectorsFamily(chains []uint64, family string) error { + for _, chain := range chains { + selectorFamily, err := chainsel.GetSelectorFamily(chain) + if err != nil { + return fmt.Errorf("failed to get family for chain selector %d: %w", chain, err) + } + if selectorFamily != family { + return fmt.Errorf("chain selector %d is not in the %s family", chain, family) + } + } + + return nil +} + +func validateNoExistingContract(e cldf.Environment, chains []uint64, tv cldf.TypeAndVersion) error { + if e.ExistingAddresses != nil { //nolint:staticcheck // SA1019: legacy changeset still supports AddressBook state. + for _, chain := range chains { + addresses, err := e.ExistingAddresses.AddressesForChain(chain) //nolint:staticcheck // SA1019: legacy changeset still supports AddressBook state. + if err != nil { + continue + } + for _, existingTV := range addresses { + if sameTypeAndVersion(existingTV, tv) { + return fmt.Errorf("%s contract already exists for chain selector %d in address book", tv.Type, chain) + } + } + } + } + + if e.DataStore == nil { + return nil + } + for _, chain := range chains { + refs := e.DataStore.Addresses().Filter( + datastore.AddressRefByChainSelector(chain), + datastore.AddressRefByType(datastore.ContractType(tv.Type.String())), + ) + for _, ref := range refs { + if ref.Version == nil || ref.Version.Equal(&tv.Version) { + return fmt.Errorf("%s contract already exists for chain selector %d in datastore", tv.Type, chain) + } + } + } + + return nil +} + +func sameTypeAndVersion(a, b cldf.TypeAndVersion) bool { + return a.Type == b.Type && a.Version.Equal(&b.Version) +} diff --git a/link/changesets/deploy_link_token_test.go b/link/changesets/deploy_link_token_test.go new file mode 100644 index 0000000..afbbf9b --- /dev/null +++ b/link/changesets/deploy_link_token_test.go @@ -0,0 +1,290 @@ +package changesets + +import ( + "testing" + + chain_selectors "github.com/smartcontractkit/chain-selectors" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + cldf_solana "github.com/smartcontractkit/chainlink-deployments-framework/chain/solana" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + linkcontracts "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/contracts/link" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment" + "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/runtime" + + cldchangesetscommon "github.com/smartcontractkit/cld-changesets/pkg/common" + evmstate "github.com/smartcontractkit/cld-changesets/pkg/family/evm" +) + +func TestDeployLinkToken(t *testing.T) { + t.Parallel() + + selectors := []uint64{ + chain_selectors.TEST_90000001.Selector, + chain_selectors.TEST_90000002.Selector, + } + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, selectors), + )) + require.NoError(t, err) + + err = rt.Exec( + runtime.ChangesetTask(cldf.CreateLegacyChangeSet(DeployLinkToken), selectors), + ) + require.NoError(t, err) + + for _, selector := range selectors { + chain := rt.Environment().BlockChains.EVMChains()[selector] + addrs, addrsErr := rt.State().AddressBook.AddressesForChain(selector) + require.NoError(t, addrsErr) + + state, stateErr := evmstate.MaybeLoadLinkTokenChainState(chain, addrs) + require.NoError(t, stateErr) + + _, viewErr := state.GenerateLinkView() + require.NoError(t, viewErr) + } + + refs, err := rt.State().DataStore.Addresses().Fetch() + require.NoError(t, err) + require.Len(t, refs, len(selectors)) + for _, ref := range refs { + require.Equal(t, datastore.ContractType(linkcontracts.LinkToken), ref.Type) + require.True(t, cldchangesetscommon.Version1_0_0.Equal(ref.Version)) + } +} + +func TestDeployLinkTokenRejectsInvalidSelectorsBeforeDeploy(t *testing.T) { + t.Parallel() + + evmSelector := chain_selectors.TEST_90000001.Selector + solSelector := chain_selectors.TEST_22222222222222222222222222222222222222222222.Selector + env := cldf.Environment{ + BlockChains: cldf_chain.NewBlockChainsFromSlice([]cldf_chain.BlockChain{ + cldf_evm.Chain{Selector: evmSelector}, + cldf_solana.Chain{Selector: solSelector}, + }), + } + + _, err := DeployLinkToken(env, []uint64{evmSelector, evmSelector}) + require.ErrorContains(t, err, "duplicate chain selector found") + + _, err = DeployLinkToken(env, []uint64{evmSelector, solSelector}) + require.ErrorContains(t, err, "is not in the evm family") + + _, err = DeployStaticLinkToken(env, []uint64{evmSelector, evmSelector}) + require.ErrorContains(t, err, "duplicate chain selector found") + + _, err = DeployStaticLinkToken(env, []uint64{evmSelector, solSelector}) + require.ErrorContains(t, err, "is not in the evm family") +} + +func TestDeployLinkTokenRejectsExistingStateBeforeDeploy(t *testing.T) { + t.Parallel() + + evmSelector := chain_selectors.TEST_90000001.Selector + solSelector := chain_selectors.TEST_22222222222222222222222222222222222222222222.Selector + const ( + evmAddress = "0xeC91988D7dD84d8adE801b739172ad15c860A700" + solAddress = "J6oVJ42pE6eXdTCcCidhjzHWS7Sxz6yMsXHxXphT1U7Y" + ) + + tests := []struct { + name string + env cldf.Environment + run func(cldf.Environment) (cldf.ChangesetOutput, error) + wantErr string + }{ + { + name: "link token exists in address book with labels", + env: cldf.Environment{ + BlockChains: cldf_chain.NewBlockChainsFromSlice([]cldf_chain.BlockChain{ + cldf_evm.Chain{Selector: evmSelector}, + }), + ExistingAddresses: addressBookWith(t, evmSelector, evmAddress, typeAndVersionWithLabels(linkTokenTypeAndVersion(), "migrated")), + }, + run: func(env cldf.Environment) (cldf.ChangesetOutput, error) { + return DeployLinkToken(env, []uint64{evmSelector}) + }, + wantErr: "LinkToken contract already exists", + }, + { + name: "link token exists in address book without labels", + env: cldf.Environment{ + BlockChains: cldf_chain.NewBlockChainsFromSlice([]cldf_chain.BlockChain{ + cldf_evm.Chain{Selector: evmSelector}, + }), + ExistingAddresses: addressBookWith(t, evmSelector, evmAddress, linkTokenTypeAndVersion()), + }, + run: func(env cldf.Environment) (cldf.ChangesetOutput, error) { + return DeployLinkToken(env, []uint64{evmSelector}) + }, + wantErr: "LinkToken contract already exists", + }, + { + name: "link token exists in datastore with non-empty qualifier", + env: cldf.Environment{ + BlockChains: cldf_chain.NewBlockChainsFromSlice([]cldf_chain.BlockChain{ + cldf_evm.Chain{Selector: evmSelector}, + }), + DataStore: datastoreWith(t, evmSelector, evmAddress, linkTokenTypeAndVersion(), "migrated"), + }, + run: func(env cldf.Environment) (cldf.ChangesetOutput, error) { + return DeployLinkToken(env, []uint64{evmSelector}) + }, + wantErr: "LinkToken contract already exists", + }, + { + name: "link token exists in datastore with nil version", + env: cldf.Environment{ + BlockChains: cldf_chain.NewBlockChainsFromSlice([]cldf_chain.BlockChain{ + cldf_evm.Chain{Selector: evmSelector}, + }), + DataStore: datastoreWithNilVersion(t, evmSelector, evmAddress, linkcontracts.LinkToken, "migrated"), + }, + run: func(env cldf.Environment) (cldf.ChangesetOutput, error) { + return DeployLinkToken(env, []uint64{evmSelector}) + }, + wantErr: "LinkToken contract already exists", + }, + { + name: "static link token exists in datastore", + env: cldf.Environment{ + BlockChains: cldf_chain.NewBlockChainsFromSlice([]cldf_chain.BlockChain{ + cldf_evm.Chain{Selector: evmSelector}, + }), + DataStore: datastoreWith(t, evmSelector, evmAddress, staticLinkTokenTypeAndVersion(), ""), + }, + run: func(env cldf.Environment) (cldf.ChangesetOutput, error) { + return DeployStaticLinkToken(env, []uint64{evmSelector}) + }, + wantErr: "StaticLinkToken contract already exists", + }, + { + name: "solana link token exists in datastore", + env: cldf.Environment{ + BlockChains: cldf_chain.NewBlockChainsFromSlice([]cldf_chain.BlockChain{ + cldf_solana.Chain{Selector: solSelector}, + }), + DataStore: datastoreWith(t, solSelector, solAddress, linkTokenTypeAndVersion(), ""), + }, + run: func(env cldf.Environment) (cldf.ChangesetOutput, error) { + return DeploySolanaLinkToken(env, DeploySolanaLinkTokenConfig{ChainSelector: solSelector}) + }, + wantErr: "LinkToken contract already exists", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + _, err := tt.run(tt.env) + require.ErrorContains(t, err, tt.wantErr) + }) + } +} + +func TestDeployStaticLinkToken(t *testing.T) { + t.Parallel() + + selector := chain_selectors.TEST_90000001.Selector + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithEVMSimulated(t, []uint64{selector}), + )) + require.NoError(t, err) + + chain := rt.Environment().BlockChains.EVMChains()[selector] + + err = rt.Exec( + runtime.ChangesetTask(cldf.CreateLegacyChangeSet(DeployStaticLinkToken), []uint64{selector}), + ) + require.NoError(t, err) + + addrs, err := rt.State().AddressBook.AddressesForChain(selector) + require.NoError(t, err) + + state, err := evmstate.MaybeLoadStaticLinkTokenState(chain, addrs) + require.NoError(t, err) + + _, err = state.GenerateStaticLinkView() + require.NoError(t, err) + + refs, err := rt.State().DataStore.Addresses().Fetch() + require.NoError(t, err) + require.Len(t, refs, 1) + require.Equal(t, datastore.ContractType(linkcontracts.StaticLinkToken), refs[0].Type) + require.True(t, cldchangesetscommon.Version1_0_0.Equal(refs[0].Version)) +} + +func addressBookWith(t *testing.T, selector uint64, address string, tv cldf.TypeAndVersion) cldf.AddressBook { + t.Helper() + + ab := cldf.NewMemoryAddressBook() + require.NoError(t, ab.Save(selector, address, tv)) + + return ab +} + +func datastoreWith(t *testing.T, selector uint64, address string, tv cldf.TypeAndVersion, qualifier string) datastore.DataStore { + t.Helper() + + ds := datastore.NewMemoryDataStore() + require.NoError(t, saveAddressRef(ds, selector, address, tv, qualifier)) + + return ds.Seal() +} + +func datastoreWithNilVersion(t *testing.T, selector uint64, address string, contractType cldf.ContractType, qualifier string) datastore.DataStore { + t.Helper() + + ds := datastore.NewMemoryDataStore() + require.NoError(t, ds.Addresses().Add(datastore.AddressRef{ + ChainSelector: selector, + Address: address, + Type: datastore.ContractType(contractType.String()), + Qualifier: qualifier, + })) + + return ds.Seal() +} + +func typeAndVersionWithLabels(tv cldf.TypeAndVersion, labels ...string) cldf.TypeAndVersion { + for _, label := range labels { + tv.Labels.Add(label) + } + + return tv +} + +func TestDeployLinkTokenZk(t *testing.T) { + tests.SkipFlakey(t, "https://smartcontract-it.atlassian.net/browse/CCIP-6427") + + t.Parallel() + + selector := chain_selectors.TEST_90000050.Selector + rt, err := runtime.New(t.Context(), runtime.WithEnvOpts( + environment.WithZKSyncContainer(t, []uint64{selector}), + )) + require.NoError(t, err) + + chain := rt.Environment().BlockChains.EVMChains()[selector] + + err = rt.Exec( + runtime.ChangesetTask(cldf.CreateLegacyChangeSet(DeployLinkToken), []uint64{selector}), + ) + require.NoError(t, err) + + addrs, err := rt.State().AddressBook.AddressesForChain(selector) + require.NoError(t, err) + + state, err := evmstate.MaybeLoadLinkTokenChainState(chain, addrs) + require.NoError(t, err) + + _, err = state.GenerateLinkView() + require.NoError(t, err) +}