diff --git a/cmd/secrets/common/handler.go b/cmd/secrets/common/handler.go index 1f604e3a..acd00600 100644 --- a/cmd/secrets/common/handler.go +++ b/cmd/secrets/common/handler.go @@ -73,13 +73,13 @@ type Handler struct { func NewHandler(ctx *runtime.Context, secretsFilePath string) (*Handler, error) { var pk *ecdsa.PrivateKey var err error - if ctx.Settings.User.EthPrivateKey != "" { - pk, err = crypto.HexToECDSA(ctx.Settings.User.EthPrivateKey) + if ethKey := ctx.Settings.User.PrivateKey(settings.EVM); ethKey != "" { + pk, err = crypto.HexToECDSA(ethKey) if err != nil { return nil, fmt.Errorf("failed to decode the provided private key: %w", err) } } else { - ctx.Logger.Debug().Msg("No EthPrivateKey found in settings; assuming a multisig request.") + ctx.Logger.Debug().Msg("No EVM private key found in settings; assuming a multisig request.") } diff --git a/cmd/workflow/activate/activate_test.go b/cmd/workflow/activate/activate_test.go index f94522aa..6aec1bea 100644 --- a/cmd/workflow/activate/activate_test.go +++ b/cmd/workflow/activate/activate_test.go @@ -20,7 +20,7 @@ func TestNonInteractive_WithoutYes_ReturnsError(t *testing.T) { ctx := simulatedEnvironment.NewRuntimeContext() ctx.Settings = &settings.Settings{ User: settings.UserSettings{ - EthPrivateKey: chainsim.TestPrivateKey, + PrivateKeys: map[string]string{settings.EVM.Name: chainsim.TestPrivateKey}, }, } ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA @@ -47,7 +47,7 @@ func TestNonInteractive_WithYes_PassesGuard(t *testing.T) { ctx := simulatedEnvironment.NewRuntimeContext() ctx.Settings = &settings.Settings{ User: settings.UserSettings{ - EthPrivateKey: chainsim.TestPrivateKey, + PrivateKeys: map[string]string{settings.EVM.Name: chainsim.TestPrivateKey}, }, } ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA @@ -162,7 +162,7 @@ func TestWorkflowActivateCommand(t *testing.T) { ctx := simulatedEnvironment.NewRuntimeContext() ctx.Settings = &settings.Settings{ User: settings.UserSettings{ - EthPrivateKey: chainsim.TestPrivateKey, + PrivateKeys: map[string]string{settings.EVM.Name: chainsim.TestPrivateKey}, }, } ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA diff --git a/cmd/workflow/delete/delete_test.go b/cmd/workflow/delete/delete_test.go index 0146c0cf..17bb9a2c 100644 --- a/cmd/workflow/delete/delete_test.go +++ b/cmd/workflow/delete/delete_test.go @@ -21,7 +21,7 @@ func TestNonInteractive_WithoutYes_ReturnsError(t *testing.T) { ctx := simulatedEnvironment.NewRuntimeContext() ctx.Settings = &settings.Settings{ User: settings.UserSettings{ - EthPrivateKey: chainsim.TestPrivateKey, + PrivateKeys: map[string]string{settings.EVM.Name: chainsim.TestPrivateKey}, }, } ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA @@ -47,7 +47,7 @@ func TestNonInteractive_WithYes_Proceeds(t *testing.T) { ctx := simulatedEnvironment.NewRuntimeContext() ctx.Settings = &settings.Settings{ User: settings.UserSettings{ - EthPrivateKey: chainsim.TestPrivateKey, + PrivateKeys: map[string]string{settings.EVM.Name: chainsim.TestPrivateKey}, }, } ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA @@ -134,7 +134,7 @@ func TestWorkflowDeleteCommand(t *testing.T) { ctx := simulatedEnvironment.NewRuntimeContext() ctx.Settings = &settings.Settings{ User: settings.UserSettings{ - EthPrivateKey: chainsim.TestPrivateKey, + PrivateKeys: map[string]string{settings.EVM.Name: chainsim.TestPrivateKey}, }, } ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA diff --git a/cmd/workflow/pause/pause_test.go b/cmd/workflow/pause/pause_test.go index 3af6e2f6..89c5af9a 100644 --- a/cmd/workflow/pause/pause_test.go +++ b/cmd/workflow/pause/pause_test.go @@ -20,7 +20,7 @@ func TestNonInteractive_WithoutYes_ReturnsError(t *testing.T) { ctx := simulatedEnvironment.NewRuntimeContext() ctx.Settings = &settings.Settings{ User: settings.UserSettings{ - EthPrivateKey: chainsim.TestPrivateKey, + PrivateKeys: map[string]string{settings.EVM.Name: chainsim.TestPrivateKey}, }, } ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA @@ -46,7 +46,7 @@ func TestNonInteractive_WithYes_PassesGuard(t *testing.T) { ctx := simulatedEnvironment.NewRuntimeContext() ctx.Settings = &settings.Settings{ User: settings.UserSettings{ - EthPrivateKey: chainsim.TestPrivateKey, + PrivateKeys: map[string]string{settings.EVM.Name: chainsim.TestPrivateKey}, }, } ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA @@ -140,7 +140,7 @@ func TestWorkflowPauseCommand(t *testing.T) { ctx := simulatedEnvironment.NewRuntimeContext() ctx.Settings = &settings.Settings{ User: settings.UserSettings{ - EthPrivateKey: chainsim.TestPrivateKey, + PrivateKeys: map[string]string{settings.EVM.Name: chainsim.TestPrivateKey}, }, } ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA diff --git a/cmd/workflow/simulate/chain/aptos/capabilities.go b/cmd/workflow/simulate/chain/aptos/capabilities.go new file mode 100644 index 00000000..308debc1 --- /dev/null +++ b/cmd/workflow/simulate/chain/aptos/capabilities.go @@ -0,0 +1,79 @@ +package aptos + +import ( + "context" + "fmt" + + "github.com/aptos-labs/aptos-go-sdk" + "github.com/aptos-labs/aptos-go-sdk/crypto" + + aptosserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/aptos/server" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink/v2/core/capabilities" + + aptosfakes "github.com/smartcontractkit/chainlink-aptos/fakes" + + "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain" +) + +// AptosChainCapabilities holds the per-selector FakeAptosChain instances +// created for simulation. +type AptosChainCapabilities struct { + AptosChains map[uint64]*aptosfakes.FakeAptosChain +} + +// NewAptosChainCapabilities builds FakeAptosChain instances for every +// (selector -> client) pair, optionally wraps them with LimitedAptosChain, +// and registers each with the capability registry. +func NewAptosChainCapabilities( + ctx context.Context, + lggr logger.Logger, + registry *capabilities.Registry, + clients map[uint64]aptosfakes.AptosClient, + forwarders map[uint64]string, + privateKey *crypto.Ed25519PrivateKey, + dryRunChainWrite bool, + limits chain.Limits, +) (*AptosChainCapabilities, error) { + chains := make(map[uint64]*aptosfakes.FakeAptosChain) + for sel, client := range clients { + fwdStr, ok := forwarders[sel] + if !ok { + lggr.Infow("Forwarder not found for chain", "selector", sel) + continue + } + var fwd aptos.AccountAddress + if err := fwd.ParseStringRelaxed(fwdStr); err != nil { + return nil, fmt.Errorf("parse forwarder for selector %d: %w", sel, err) + } + fc, err := aptosfakes.NewFakeAptosChain(lggr, client, privateKey, fwd, sel, dryRunChainWrite) + if err != nil { + return nil, fmt.Errorf("new FakeAptosChain for selector %d: %w", sel, err) + } + capability := NewLimitedAptosChain(fc, limits) + server := aptosserver.NewClientServer(capability) + if err := registry.Add(ctx, server); err != nil { + return nil, fmt.Errorf("register aptos capability for selector %d: %w", sel, err) + } + chains[sel] = fc + } + return &AptosChainCapabilities{AptosChains: chains}, nil +} + +func (c *AptosChainCapabilities) Start(ctx context.Context) error { + for _, fc := range c.AptosChains { + if err := fc.Start(ctx); err != nil { + return err + } + } + return nil +} + +func (c *AptosChainCapabilities) Close() error { + for _, fc := range c.AptosChains { + if err := fc.Close(); err != nil { + return err + } + } + return nil +} diff --git a/cmd/workflow/simulate/chain/aptos/chaintype.go b/cmd/workflow/simulate/chain/aptos/chaintype.go new file mode 100644 index 00000000..ff084aa1 --- /dev/null +++ b/cmd/workflow/simulate/chain/aptos/chaintype.go @@ -0,0 +1,196 @@ +package aptos + +import ( + "context" + "encoding/hex" + "fmt" + "strings" + + "github.com/aptos-labs/aptos-go-sdk/crypto" + "github.com/rs/zerolog" + "github.com/spf13/viper" + + corekeys "github.com/smartcontractkit/chainlink-common/keystore/corekeys" + "github.com/smartcontractkit/chainlink-common/pkg/services" + + aptosfakes "github.com/smartcontractkit/chainlink-aptos/fakes" + + "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain" + "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +const defaultSentinelAptosSeed = "0000000000000000000000000000000000000000000000000000000000000001" + +func init() { + chain.Register(string(corekeys.Aptos), func(lggr *zerolog.Logger) chain.ChainType { + return &AptosChainType{log: lggr} + }, nil) +} + +// AptosChainType implements chain.ChainType for Aptos. +type AptosChainType struct { + log *zerolog.Logger + aptosChains *AptosChainCapabilities +} + +var _ chain.ChainType = (*AptosChainType)(nil) + +func (ct *AptosChainType) Name() string { return string(corekeys.Aptos) } +func (ct *AptosChainType) SupportedChains() []chain.ChainConfig { return SupportedChains } + +func (ct *AptosChainType) ResolveClients(v *viper.Viper) (chain.ResolvedChains, error) { + clients := make(map[uint64]chain.ChainClient) + forwarders := make(map[uint64]string) + experimental := make(map[uint64]bool) + for _, c := range SupportedChains { + name, err := settings.GetChainNameByChainSelector(c.Selector) + if err != nil { + ct.log.Error().Msgf("Invalid Aptos chain selector %d; skipping", c.Selector) + continue + } + rpcURL, err := settings.GetRpcUrlSettings(v, name) + if err != nil || strings.TrimSpace(rpcURL) == "" { + ct.log.Debug().Msgf("RPC not provided for %s; skipping", name) + continue + } + ct.log.Debug().Msgf("Using RPC for %s: %s", name, chain.RedactURL(rpcURL)) + client, err := aptosfakes.NewAptosClient(rpcURL) + if err != nil { + ui.Warning(fmt.Sprintf("Failed to build Aptos client for %s: %v", name, err)) + continue + } + clients[c.Selector] = client + if strings.TrimSpace(c.Forwarder) != "" { + forwarders[c.Selector] = c.Forwarder + } + } + + expChains, err := settings.GetExperimentalChains(v) + if err != nil { + return chain.ResolvedChains{}, fmt.Errorf("failed to load experimental chains config: %w", err) + } + for _, ec := range expChains { + if !strings.EqualFold(ec.ChainType, ct.Name()) { + continue + } + if ec.ChainSelector == 0 { + return chain.ResolvedChains{}, fmt.Errorf("experimental chain missing chain-selector") + } + if strings.TrimSpace(ec.RPCURL) == "" { + return chain.ResolvedChains{}, fmt.Errorf("experimental aptos chain %d missing rpc-url", ec.ChainSelector) + } + forwarder := strings.TrimSpace(ec.Forwarder) + if forwarder == "" { + return chain.ResolvedChains{}, fmt.Errorf("experimental aptos chain %d missing forwarder", ec.ChainSelector) + } + if _, exists := clients[ec.ChainSelector]; exists { + if forwarders[ec.ChainSelector] != forwarder { + ui.Warning(fmt.Sprintf("Warning: experimental aptos chain %d overrides supported chain forwarder (supported: %s, experimental: %s)\n", + ec.ChainSelector, forwarders[ec.ChainSelector], forwarder)) + forwarders[ec.ChainSelector] = forwarder + } else { + ct.log.Debug().Uint64("chain-selector", ec.ChainSelector).Msg("Experimental chain matches supported chain config") + } + continue + } + ct.log.Debug().Msgf("Using RPC for experimental aptos chain %d: %s", ec.ChainSelector, chain.RedactURL(ec.RPCURL)) + client, err := aptosfakes.NewAptosClient(ec.RPCURL) + if err != nil { + return chain.ResolvedChains{}, fmt.Errorf("failed to create aptos client for experimental chain %d: %w", ec.ChainSelector, err) + } + clients[ec.ChainSelector] = client + forwarders[ec.ChainSelector] = forwarder + experimental[ec.ChainSelector] = true + ui.Dim(fmt.Sprintf("Added experimental aptos chain (chain-selector: %d)\n", ec.ChainSelector)) + } + + return chain.ResolvedChains{Clients: clients, Forwarders: forwarders, ExperimentalSelectors: experimental}, nil +} + +func (ct *AptosChainType) ResolveKey(s *settings.Settings, broadcast bool) (interface{}, error) { + seed := strings.TrimPrefix(strings.ToLower(strings.TrimSpace(s.User.PrivateKey(settings.Aptos))), "0x") + bytes, err := hex.DecodeString(seed) + if err != nil || len(bytes) != 32 { + if broadcast { + if err != nil { + return nil, fmt.Errorf("failed to parse private key, required to broadcast. Please check CRE_APTOS_PRIVATE_KEY in your .env file or system environment: %w", err) + } + return nil, fmt.Errorf("CRE_APTOS_PRIVATE_KEY must be 32 hex bytes (64 chars); got len=%d. Please check CRE_APTOS_PRIVATE_KEY in your .env file or system environment", len(bytes)) + } + bytes, _ = hex.DecodeString(defaultSentinelAptosSeed) + ui.Warning("Using default Aptos private key for chain write simulation. To use your own key, set CRE_APTOS_PRIVATE_KEY in your .env file or system environment.") + } + sentinel, _ := hex.DecodeString(defaultSentinelAptosSeed) + if broadcast && hex.EncodeToString(bytes) == hex.EncodeToString(sentinel) { + return nil, fmt.Errorf("you must configure a valid Aptos private key to perform on-chain writes. Please set CRE_APTOS_PRIVATE_KEY in your .env file or system environment before using the --broadcast flag") + } + k := &crypto.Ed25519PrivateKey{} + if err := k.FromBytes(bytes); err != nil { + return nil, fmt.Errorf("build Ed25519 key: %w", err) + } + return k, nil +} + +func (ct *AptosChainType) ResolveTriggerData(_ context.Context, _ uint64, _ chain.TriggerParams) (interface{}, error) { + return nil, fmt.Errorf("aptos: no trigger surface") +} + +func (ct *AptosChainType) RegisterCapabilities(ctx context.Context, cfg chain.CapabilityConfig) ([]services.Service, error) { + typedClients := make(map[uint64]aptosfakes.AptosClient, len(cfg.Clients)) + for sel, c := range cfg.Clients { + ac, ok := c.(aptosfakes.AptosClient) + if !ok { + return nil, fmt.Errorf("aptos: client for selector %d is not aptosfakes.AptosClient", sel) + } + typedClients[sel] = ac + } + var pk *crypto.Ed25519PrivateKey + if cfg.PrivateKey != nil { + var ok bool + pk, ok = cfg.PrivateKey.(*crypto.Ed25519PrivateKey) + if !ok { + return nil, fmt.Errorf("aptos: private key is not *crypto.Ed25519PrivateKey") + } + } + var lim chain.Limits + if cfg.Limits != nil { + lim = ExtractLimits(cfg.Limits) + } + caps, err := NewAptosChainCapabilities(ctx, cfg.Logger, cfg.Registry, typedClients, cfg.Forwarders, pk, !cfg.Broadcast, lim) + if err != nil { + return nil, err + } + if err := caps.Start(ctx); err != nil { + return nil, fmt.Errorf("aptos: failed to start: %w", err) + } + ct.aptosChains = caps + out := make([]services.Service, 0, len(caps.AptosChains)) + for _, fc := range caps.AptosChains { + out = append(out, fc) + } + return out, nil +} + +func (ct *AptosChainType) ExecuteTrigger(_ context.Context, _ uint64, _ string, _ interface{}) error { + return fmt.Errorf("aptos: no trigger surface") +} + +func (ct *AptosChainType) Supports(selector uint64) bool { + if ct.aptosChains == nil { + return false + } + return ct.aptosChains.AptosChains[selector] != nil +} + +func (ct *AptosChainType) ParseTriggerChainSelector(triggerID string) (uint64, bool) { + return chain.ParseTriggerChainSelector(ct.Name(), triggerID) +} + +func (ct *AptosChainType) RunHealthCheck(resolved chain.ResolvedChains) error { + return RunRPCHealthCheck(resolved.Clients, resolved.ExperimentalSelectors) +} + +func (ct *AptosChainType) CollectCLIInputs(_ *viper.Viper) map[string]string { + return map[string]string{} +} diff --git a/cmd/workflow/simulate/chain/aptos/chaintype_test.go b/cmd/workflow/simulate/chain/aptos/chaintype_test.go new file mode 100644 index 00000000..a2d5bc72 --- /dev/null +++ b/cmd/workflow/simulate/chain/aptos/chaintype_test.go @@ -0,0 +1,165 @@ +package aptos + +import ( + "context" + "io" + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink/v2/core/capabilities" + + "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain" + "github.com/smartcontractkit/cre-cli/internal/settings" +) + +func nopCommonLogger() logger.Logger { return logger.NewWithSync(io.Discard) } + +func newRegistry(t *testing.T) *capabilities.Registry { + t.Helper() + return capabilities.NewRegistry(logger.Test(t)) +} + +func TestResolveKey_SentinelUnderBroadcastFails(t *testing.T) { + t.Parallel() + ct := &AptosChainType{} + s := &settings.Settings{User: settings.UserSettings{PrivateKeys: map[string]string{settings.Aptos.Name: "0000000000000000000000000000000000000000000000000000000000000001"}}} + _, err := ct.ResolveKey(s, true) + require.Error(t, err) +} + +func TestResolveKey_UnparseableUnderBroadcastFails(t *testing.T) { + t.Parallel() + ct := &AptosChainType{} + s := &settings.Settings{User: settings.UserSettings{PrivateKeys: map[string]string{settings.Aptos.Name: "not-hex"}}} + _, err := ct.ResolveKey(s, true) + require.Error(t, err) +} + +func TestResolveKey_ShortKeyUnderBroadcastFails(t *testing.T) { + t.Parallel() + ct := &AptosChainType{} + s := &settings.Settings{User: settings.UserSettings{PrivateKeys: map[string]string{settings.Aptos.Name: "1111"}}} + _, err := ct.ResolveKey(s, true) + require.Error(t, err) +} + +func TestResolveKey_UnparseableNonBroadcastFallsBackToSentinel(t *testing.T) { + t.Parallel() + ct := &AptosChainType{} + s := &settings.Settings{User: settings.UserSettings{PrivateKeys: map[string]string{settings.Aptos.Name: ""}}} + k, err := ct.ResolveKey(s, false) + require.NoError(t, err) + assert.NotNil(t, k) +} + +func TestResolveKey_ValidKeyBroadcast(t *testing.T) { + t.Parallel() + ct := &AptosChainType{} + s := &settings.Settings{User: settings.UserSettings{PrivateKeys: map[string]string{settings.Aptos.Name: "1111111111111111111111111111111111111111111111111111111111111111"}}} + k, err := ct.ResolveKey(s, true) + require.NoError(t, err) + assert.NotNil(t, k) +} + +func TestSupports_False(t *testing.T) { + t.Parallel() + ct := &AptosChainType{} + assert.False(t, ct.Supports(1)) +} + +func TestResolveTriggerData_ReturnsNoTriggerSurface(t *testing.T) { + t.Parallel() + ct := &AptosChainType{} + _, err := ct.ResolveTriggerData(context.Background(), 1, chain.TriggerParams{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "no trigger surface") +} + +func TestExecuteTrigger_ReturnsNoTriggerSurface(t *testing.T) { + t.Parallel() + ct := &AptosChainType{} + err := ct.ExecuteTrigger(context.Background(), 1, "tid", nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "no trigger surface") +} + +func TestRegisterCapabilities_WrongClientType(t *testing.T) { + t.Parallel() + lg := zerolog.Nop() + ct := &AptosChainType{log: &lg} + cfg := chain.CapabilityConfig{ + Clients: map[uint64]chain.ChainClient{1: "not-an-aptos-client"}, + Forwarders: map[uint64]string{1: "0x1"}, + } + _, err := ct.RegisterCapabilities(context.Background(), cfg) + require.Error(t, err) + assert.Contains(t, err.Error(), "client for selector 1 is not aptosfakes.AptosClient") +} + +func TestRegisterCapabilities_WrongPrivateKeyType(t *testing.T) { + t.Parallel() + lg := zerolog.Nop() + ct := &AptosChainType{log: &lg} + cfg := chain.CapabilityConfig{ + Clients: map[uint64]chain.ChainClient{}, + Forwarders: map[uint64]string{}, + PrivateKey: "not-an-ed25519-key", + } + _, err := ct.RegisterCapabilities(context.Background(), cfg) + require.Error(t, err) + assert.Contains(t, err.Error(), "private key is not *crypto.Ed25519PrivateKey") +} + +func TestRegisterCapabilities_NoClients_ConstructsEmpty(t *testing.T) { + t.Parallel() + lg := zerolog.Nop() + ct := &AptosChainType{log: &lg} + cfg := chain.CapabilityConfig{ + Clients: map[uint64]chain.ChainClient{}, + Forwarders: map[uint64]string{}, + Logger: nopCommonLogger(), + Registry: newRegistry(t), + } + srvcs, err := ct.RegisterCapabilities(context.Background(), cfg) + require.NoError(t, err) + assert.Empty(t, srvcs) + assert.False(t, ct.Supports(1)) +} + +func TestRunHealthCheck_PropagatesInvalidClientType(t *testing.T) { + t.Parallel() + ct := &AptosChainType{} + err := ct.RunHealthCheck(chain.ResolvedChains{ + Clients: map[uint64]chain.ChainClient{1: "not-aptos"}, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid client type for Aptos chain type") +} + +func TestRegisteredInFactoryRegistry(t *testing.T) { + t.Parallel() + lg := zerolog.Nop() + chain.Build(&lg) + found := false + for _, n := range chain.Names() { + if n == "aptos" { + found = true + break + } + } + require.True(t, found, "aptos chain type should be registered at init; got %v", chain.Names()) + + ct, err := chain.Get("aptos") + require.NoError(t, err) + require.Equal(t, "aptos", ct.Name()) +} + +func TestCollectCLIInputs_ReturnsEmpty(t *testing.T) { + t.Parallel() + ct := &AptosChainType{} + assert.Empty(t, ct.CollectCLIInputs(nil)) +} diff --git a/cmd/workflow/simulate/chain/aptos/health.go b/cmd/workflow/simulate/chain/aptos/health.go new file mode 100644 index 00000000..54bc9bd7 --- /dev/null +++ b/cmd/workflow/simulate/chain/aptos/health.go @@ -0,0 +1,54 @@ +package aptos + +import ( + "errors" + "fmt" + + aptosfakes "github.com/smartcontractkit/chainlink-aptos/fakes" + + "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain" + "github.com/smartcontractkit/cre-cli/internal/settings" +) + +// RunRPCHealthCheck probes GetChainId() on every configured Aptos client. +// experimentalSelectors identifies chains sourced from experimental-chains config. +func RunRPCHealthCheck(clients map[uint64]chain.ChainClient, experimentalSelectors map[uint64]bool) error { + if len(clients) == 0 { + return fmt.Errorf("check your settings: no Aptos RPC URLs found for supported or experimental chains") + } + var errs []error + for sel, c := range clients { + if c == nil { + errs = append(errs, fmt.Errorf("[%d] nil client", sel)) + continue + } + ac, ok := c.(aptosfakes.AptosClient) + if !ok { + errs = append(errs, fmt.Errorf("[%d] invalid client type for Aptos chain type", sel)) + continue + } + var label string + switch { + case experimentalSelectors[sel]: + label = fmt.Sprintf("experimental chain %d", sel) + default: + if name, err := settings.GetChainNameByChainSelector(sel); err == nil { + label = name + } else { + label = fmt.Sprintf("chain %d", sel) + } + } + chainID, err := ac.GetChainId() + if err != nil { + errs = append(errs, fmt.Errorf("[%s] failed RPC health check: %w", label, err)) + continue + } + if chainID == 0 { + errs = append(errs, fmt.Errorf("[%s] invalid RPC response: zero chain ID", label)) + } + } + if len(errs) > 0 { + return errors.Join(errs...) + } + return nil +} diff --git a/cmd/workflow/simulate/chain/aptos/health_test.go b/cmd/workflow/simulate/chain/aptos/health_test.go new file mode 100644 index 00000000..81673942 --- /dev/null +++ b/cmd/workflow/simulate/chain/aptos/health_test.go @@ -0,0 +1,123 @@ +package aptos + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + chainselectors "github.com/smartcontractkit/chain-selectors" + mocks "github.com/smartcontractkit/chainlink-aptos/relayer/monitor/mocks" + + "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain" +) + +func TestRunRPCHealthCheck_NoClients(t *testing.T) { + t.Parallel() + err := RunRPCHealthCheck(map[uint64]chain.ChainClient{}, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "check your settings: no Aptos RPC URLs") +} + +func TestRunRPCHealthCheck_InvalidClientType(t *testing.T) { + t.Parallel() + err := RunRPCHealthCheck(map[uint64]chain.ChainClient{1: stubNonAptosClient{}}, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid client type") + assert.Contains(t, err.Error(), "[1]") +} + +func TestRunRPCHealthCheck_NilClient(t *testing.T) { + t.Parallel() + err := RunRPCHealthCheck(map[uint64]chain.ChainClient{9: nil}, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "[9] nil client") +} + +func TestRunRPCHealthCheck_Healthy(t *testing.T) { + t.Parallel() + rpc := mocks.NewAptosRpcClient(t) + rpc.EXPECT().GetChainId().Return(uint8(1), nil).Once() + require.NoError(t, RunRPCHealthCheck(map[uint64]chain.ChainClient{1: rpc}, nil)) +} + +func TestRunRPCHealthCheck_ZeroChainID(t *testing.T) { + t.Parallel() + rpc := mocks.NewAptosRpcClient(t) + rpc.EXPECT().GetChainId().Return(uint8(0), nil).Once() + err := RunRPCHealthCheck(map[uint64]chain.ChainClient{7: rpc}, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "zero chain ID") + assert.Contains(t, err.Error(), "[chain 7]") +} + +func TestRunRPCHealthCheck_RPCError(t *testing.T) { + t.Parallel() + rpc := mocks.NewAptosRpcClient(t) + rpc.EXPECT().GetChainId().Return(uint8(0), errors.New("boom")).Once() + err := RunRPCHealthCheck(map[uint64]chain.ChainClient{3: rpc}, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "boom") + assert.Contains(t, err.Error(), "[chain 3]") +} + +func TestRunRPCHealthCheck_NamedChain(t *testing.T) { + t.Parallel() + rpc := mocks.NewAptosRpcClient(t) + rpc.EXPECT().GetChainId().Return(uint8(0), errors.New("unreachable")).Once() + err := RunRPCHealthCheck( + map[uint64]chain.ChainClient{chainselectors.APTOS_TESTNET.Selector: rpc}, + nil, + ) + require.Error(t, err) + assert.Contains(t, err.Error(), "[aptos-testnet]") +} + +func TestRunRPCHealthCheck_ExperimentalLabel(t *testing.T) { + t.Parallel() + rpc := mocks.NewAptosRpcClient(t) + rpc.EXPECT().GetChainId().Return(uint8(0), nil).Once() + err := RunRPCHealthCheck( + map[uint64]chain.ChainClient{42: rpc}, + map[uint64]bool{42: true}, + ) + require.Error(t, err) + assert.Contains(t, err.Error(), "experimental chain 42") +} + +func TestRunRPCHealthCheck_AggregatesMultiple(t *testing.T) { + t.Parallel() + bad := mocks.NewAptosRpcClient(t) + bad.EXPECT().GetChainId().Return(uint8(0), errors.New("net down")).Once() + zero := mocks.NewAptosRpcClient(t) + zero.EXPECT().GetChainId().Return(uint8(0), nil).Once() + err := RunRPCHealthCheck(map[uint64]chain.ChainClient{1: bad, 2: zero}, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "net down") + assert.Contains(t, err.Error(), "zero chain ID") +} + +func TestRunRPCHealthCheck_MixedKnownAndExperimental(t *testing.T) { + t.Parallel() + healthy := mocks.NewAptosRpcClient(t) + healthy.EXPECT().GetChainId().Return(uint8(1), nil).Once() + bad := mocks.NewAptosRpcClient(t) + bad.EXPECT().GetChainId().Return(uint8(0), errors.New("boom")).Once() + + const expSel uint64 = 99999999 + err := RunRPCHealthCheck( + map[uint64]chain.ChainClient{ + chainselectors.APTOS_TESTNET.Selector: healthy, + expSel: bad, + }, + map[uint64]bool{expSel: true}, + ) + require.Error(t, err) + assert.Contains(t, err.Error(), "[experimental chain 99999999]") + assert.Contains(t, err.Error(), "boom") + // healthy named chain must not appear in errors. + assert.NotContains(t, err.Error(), "[aptos-testnet]") +} + +type stubNonAptosClient struct{} diff --git a/cmd/workflow/simulate/chain/aptos/limited_capabilities.go b/cmd/workflow/simulate/chain/aptos/limited_capabilities.go new file mode 100644 index 00000000..30591298 --- /dev/null +++ b/cmd/workflow/simulate/chain/aptos/limited_capabilities.go @@ -0,0 +1,70 @@ +package aptos + +import ( + "context" + "fmt" + + commonCap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" + aptoscappb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/aptos" + aptosserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/aptos/server" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" + + "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain" +) + +// LimitedAptosChain enforces chain-write size + Aptos max_gas_amount. +type LimitedAptosChain struct { + inner aptosserver.ClientCapability + limits chain.Limits +} + +var _ aptosserver.ClientCapability = (*LimitedAptosChain)(nil) + +func NewLimitedAptosChain(inner aptosserver.ClientCapability, limits chain.Limits) *LimitedAptosChain { + return &LimitedAptosChain{inner: inner, limits: limits} +} + +func (l *LimitedAptosChain) WriteReport(ctx context.Context, metadata commonCap.RequestMetadata, input *aptoscappb.WriteReportRequest) (*commonCap.ResponseAndMetadata[*aptoscappb.WriteReportReply], caperrors.Error) { + if input.Report != nil { + if lim := l.limits.ReportSize; lim > 0 && len(input.Report.RawReport) > lim { + return nil, caperrors.NewPublicUserError( + fmt.Errorf("simulation limit exceeded: Aptos chain write report size %d bytes exceeds limit of %d bytes", len(input.Report.RawReport), lim), + caperrors.ResourceExhausted, + ) + } + } + if input.GasConfig != nil { + if gl := l.limits.GasLimit; gl > 0 && input.GasConfig.MaxGasAmount > gl { + return nil, caperrors.NewPublicUserError( + fmt.Errorf("simulation limit exceeded: Aptos max_gas_amount %d exceeds maximum of %d", input.GasConfig.MaxGasAmount, gl), + caperrors.ResourceExhausted, + ) + } + } + return l.inner.WriteReport(ctx, metadata, input) +} + +func (l *LimitedAptosChain) AccountAPTBalance(ctx context.Context, m commonCap.RequestMetadata, i *aptoscappb.AccountAPTBalanceRequest) (*commonCap.ResponseAndMetadata[*aptoscappb.AccountAPTBalanceReply], caperrors.Error) { + return l.inner.AccountAPTBalance(ctx, m, i) +} +func (l *LimitedAptosChain) View(ctx context.Context, m commonCap.RequestMetadata, i *aptoscappb.ViewRequest) (*commonCap.ResponseAndMetadata[*aptoscappb.ViewReply], caperrors.Error) { + return l.inner.View(ctx, m, i) +} +func (l *LimitedAptosChain) TransactionByHash(ctx context.Context, m commonCap.RequestMetadata, i *aptoscappb.TransactionByHashRequest) (*commonCap.ResponseAndMetadata[*aptoscappb.TransactionByHashReply], caperrors.Error) { + return l.inner.TransactionByHash(ctx, m, i) +} +func (l *LimitedAptosChain) AccountTransactions(ctx context.Context, m commonCap.RequestMetadata, i *aptoscappb.AccountTransactionsRequest) (*commonCap.ResponseAndMetadata[*aptoscappb.AccountTransactionsReply], caperrors.Error) { + return l.inner.AccountTransactions(ctx, m, i) +} + +func (l *LimitedAptosChain) ChainSelector() uint64 { return l.inner.ChainSelector() } +func (l *LimitedAptosChain) Start(ctx context.Context) error { return l.inner.Start(ctx) } +func (l *LimitedAptosChain) Close() error { return l.inner.Close() } +func (l *LimitedAptosChain) HealthReport() map[string]error { return l.inner.HealthReport() } +func (l *LimitedAptosChain) Name() string { return l.inner.Name() } +func (l *LimitedAptosChain) Description() string { return l.inner.Description() } +func (l *LimitedAptosChain) Ready() error { return l.inner.Ready() } +func (l *LimitedAptosChain) Initialise(ctx context.Context, deps core.StandardCapabilitiesDependencies) error { + return l.inner.Initialise(ctx, deps) +} diff --git a/cmd/workflow/simulate/chain/aptos/limited_capabilities_test.go b/cmd/workflow/simulate/chain/aptos/limited_capabilities_test.go new file mode 100644 index 00000000..8bbc4ddc --- /dev/null +++ b/cmd/workflow/simulate/chain/aptos/limited_capabilities_test.go @@ -0,0 +1,118 @@ +package aptos + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + commonCap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" + aptoscappb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/aptos" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" + sdk "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + + "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain" +) + +type stubCap struct{ writeCalled bool } + +func (s *stubCap) AccountAPTBalance(context.Context, commonCap.RequestMetadata, *aptoscappb.AccountAPTBalanceRequest) (*commonCap.ResponseAndMetadata[*aptoscappb.AccountAPTBalanceReply], caperrors.Error) { + return nil, nil +} +func (s *stubCap) View(context.Context, commonCap.RequestMetadata, *aptoscappb.ViewRequest) (*commonCap.ResponseAndMetadata[*aptoscappb.ViewReply], caperrors.Error) { + return nil, nil +} +func (s *stubCap) TransactionByHash(context.Context, commonCap.RequestMetadata, *aptoscappb.TransactionByHashRequest) (*commonCap.ResponseAndMetadata[*aptoscappb.TransactionByHashReply], caperrors.Error) { + return nil, nil +} +func (s *stubCap) AccountTransactions(context.Context, commonCap.RequestMetadata, *aptoscappb.AccountTransactionsRequest) (*commonCap.ResponseAndMetadata[*aptoscappb.AccountTransactionsReply], caperrors.Error) { + return nil, nil +} +func (s *stubCap) WriteReport(context.Context, commonCap.RequestMetadata, *aptoscappb.WriteReportRequest) (*commonCap.ResponseAndMetadata[*aptoscappb.WriteReportReply], caperrors.Error) { + s.writeCalled = true + return &commonCap.ResponseAndMetadata[*aptoscappb.WriteReportReply]{Response: &aptoscappb.WriteReportReply{}}, nil +} +func (s *stubCap) ChainSelector() uint64 { return 0 } +func (s *stubCap) Start(context.Context) error { return nil } +func (s *stubCap) Close() error { return nil } +func (s *stubCap) HealthReport() map[string]error { return nil } +func (s *stubCap) Name() string { return "stub" } +func (s *stubCap) Description() string { return "" } +func (s *stubCap) Ready() error { return nil } +func (s *stubCap) Initialise(context.Context, core.StandardCapabilitiesDependencies) error { + return nil +} + +func TestLimitedAptosChain_WriteReport_ReportTooLarge(t *testing.T) { + t.Parallel() + inner := &stubCap{} + l := NewLimitedAptosChain(inner, chain.Limits{ReportSize: 10, GasLimit: 1000}) + _, capErr := l.WriteReport(context.Background(), commonCap.RequestMetadata{}, &aptoscappb.WriteReportRequest{ + Report: &sdk.ReportResponse{RawReport: make([]byte, 11)}, + }) + require.NotNil(t, capErr) + assert.Contains(t, fmt.Sprint(capErr), "Aptos chain write report size 11 bytes exceeds limit of 10 bytes") + assert.False(t, inner.writeCalled) +} + +func TestLimitedAptosChain_WriteReport_MaxGasTooHigh(t *testing.T) { + t.Parallel() + inner := &stubCap{} + l := NewLimitedAptosChain(inner, chain.Limits{ReportSize: 100, GasLimit: 100}) + _, capErr := l.WriteReport(context.Background(), commonCap.RequestMetadata{}, &aptoscappb.WriteReportRequest{ + Report: &sdk.ReportResponse{RawReport: []byte("x")}, + GasConfig: &aptoscappb.GasConfig{MaxGasAmount: 101}, + }) + require.NotNil(t, capErr) + assert.Contains(t, fmt.Sprint(capErr), "Aptos max_gas_amount 101 exceeds maximum of 100") + assert.False(t, inner.writeCalled) +} + +func TestLimitedAptosChain_WriteReport_Delegates(t *testing.T) { + t.Parallel() + inner := &stubCap{} + l := NewLimitedAptosChain(inner, chain.Limits{ReportSize: 100, GasLimit: 1000}) + _, capErr := l.WriteReport(context.Background(), commonCap.RequestMetadata{}, &aptoscappb.WriteReportRequest{ + Report: &sdk.ReportResponse{RawReport: []byte("x")}, + GasConfig: &aptoscappb.GasConfig{MaxGasAmount: 50}, + }) + require.Nil(t, capErr) + assert.True(t, inner.writeCalled) +} + +func TestLimitedAptosChain_WriteReport_ZeroLimitsDelegate(t *testing.T) { + t.Parallel() + inner := &stubCap{} + l := NewLimitedAptosChain(inner, chain.Limits{}) + _, capErr := l.WriteReport(context.Background(), commonCap.RequestMetadata{}, &aptoscappb.WriteReportRequest{ + Report: &sdk.ReportResponse{RawReport: make([]byte, 1_000_000)}, + GasConfig: &aptoscappb.GasConfig{MaxGasAmount: 1_000_000_000}, + }) + require.Nil(t, capErr) + assert.True(t, inner.writeCalled) +} + +func TestLimitedAptosChain_WriteReport_NilGasConfigDelegates(t *testing.T) { + t.Parallel() + inner := &stubCap{} + l := NewLimitedAptosChain(inner, chain.Limits{ReportSize: 100, GasLimit: 1000}) + _, capErr := l.WriteReport(context.Background(), commonCap.RequestMetadata{}, &aptoscappb.WriteReportRequest{ + Report: &sdk.ReportResponse{RawReport: []byte("x")}, + }) + require.Nil(t, capErr) + assert.True(t, inner.writeCalled) +} + +func TestLimitedAptosChain_WriteReport_NilReportDelegates(t *testing.T) { + t.Parallel() + inner := &stubCap{} + l := NewLimitedAptosChain(inner, chain.Limits{ReportSize: 100, GasLimit: 1000}) + _, capErr := l.WriteReport(context.Background(), commonCap.RequestMetadata{}, &aptoscappb.WriteReportRequest{ + GasConfig: &aptoscappb.GasConfig{MaxGasAmount: 50}, + }) + require.Nil(t, capErr) + assert.True(t, inner.writeCalled) +} diff --git a/cmd/workflow/simulate/chain/aptos/limits.go b/cmd/workflow/simulate/chain/aptos/limits.go new file mode 100644 index 00000000..a8e919b6 --- /dev/null +++ b/cmd/workflow/simulate/chain/aptos/limits.go @@ -0,0 +1,14 @@ +package aptos + +import ( + "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" + + "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain" +) + +func ExtractLimits(w *cresettings.Workflows) chain.Limits { + return chain.Limits{ + ReportSize: int(w.ChainWrite.Aptos.ReportSizeLimit.DefaultValue), + GasLimit: w.ChainWrite.Aptos.GasLimit.Default.DefaultValue, + } +} diff --git a/cmd/workflow/simulate/chain/aptos/limits_test.go b/cmd/workflow/simulate/chain/aptos/limits_test.go new file mode 100644 index 00000000..e7ecd506 --- /dev/null +++ b/cmd/workflow/simulate/chain/aptos/limits_test.go @@ -0,0 +1,17 @@ +package aptos + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" +) + +func TestExtractLimitsFromDefault(t *testing.T) { + t.Parallel() + w := cresettings.Default.PerWorkflow + lim := ExtractLimits(&w) + assert.Equal(t, 5_000, lim.ReportSize) + assert.Equal(t, uint64(2_000_000), lim.GasLimit) +} diff --git a/cmd/workflow/simulate/chain/aptos/supported_chains.go b/cmd/workflow/simulate/chain/aptos/supported_chains.go new file mode 100644 index 00000000..1cfa96c6 --- /dev/null +++ b/cmd/workflow/simulate/chain/aptos/supported_chains.go @@ -0,0 +1,18 @@ +package aptos + +import ( + chainselectors "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain" +) + +// placeholderForwarder is used until canonical platform_mock addresses are +// published per network. Users override via experimental-chains config +// (chain-type: aptos). +const placeholderForwarder = "0x0000000000000000000000000000000000000000000000000000000000000000" + +// SupportedChains lists Aptos networks cre-cli simulate can target. +var SupportedChains = []chain.ChainConfig{ + {Selector: chainselectors.APTOS_MAINNET.Selector, Forwarder: placeholderForwarder}, + {Selector: chainselectors.APTOS_TESTNET.Selector, Forwarder: placeholderForwarder}, +} diff --git a/cmd/workflow/simulate/chain/aptos/supported_chains_test.go b/cmd/workflow/simulate/chain/aptos/supported_chains_test.go new file mode 100644 index 00000000..68e8399f --- /dev/null +++ b/cmd/workflow/simulate/chain/aptos/supported_chains_test.go @@ -0,0 +1,81 @@ +package aptos + +import ( + "regexp" + "testing" + + chainselectors "github.com/smartcontractkit/chain-selectors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Aptos forwarders are 32-byte object addresses encoded as 64 hex chars. +var forwarderRe = regexp.MustCompile(`^0x[0-9a-fA-F]{64}$`) + +func TestSupportedChains_AllSelectorsNonZero(t *testing.T) { + t.Parallel() + for i, c := range SupportedChains { + require.NotZerof(t, c.Selector, "index %d has zero selector", i) + } +} + +func TestSupportedChains_AllSelectorsUnique(t *testing.T) { + t.Parallel() + seen := map[uint64]int{} + for i, c := range SupportedChains { + if prev, ok := seen[c.Selector]; ok { + t.Fatalf("duplicate selector %d at indices %d and %d", c.Selector, prev, i) + } + seen[c.Selector] = i + } +} + +func TestSupportedChains_AllForwardersValidHexAddress(t *testing.T) { + t.Parallel() + for _, c := range SupportedChains { + assert.True(t, forwarderRe.MatchString(c.Forwarder), + "selector %d: invalid forwarder hex %q", c.Selector, c.Forwarder) + } +} + +func TestSupportedChains_AllSelectorsResolveToChainName(t *testing.T) { + t.Parallel() + for _, c := range SupportedChains { + info, err := chainselectors.GetSelectorFamily(c.Selector) + require.NoErrorf(t, err, "selector %d missing family", c.Selector) + assert.NotEmpty(t, info) + } +} + +func TestSupportedChains_NoForwarderEmpty(t *testing.T) { + t.Parallel() + for i, c := range SupportedChains { + require.NotEmpty(t, c.Forwarder, "supported chain at index %d has empty forwarder", i) + } +} + +func TestSupportedChains_ReturnedByChainType(t *testing.T) { + t.Parallel() + ct := &AptosChainType{} + ret := ct.SupportedChains() + require.Equal(t, len(SupportedChains), len(ret)) + for i, c := range SupportedChains { + assert.Equal(t, c.Selector, ret[i].Selector, "selector at index %d", i) + assert.Equal(t, c.Forwarder, ret[i].Forwarder, "forwarder at index %d", i) + } +} + +func TestSupportedChains_MainnetAndTestnet(t *testing.T) { + t.Parallel() + var hasMainnet, hasTestnet bool + for _, c := range SupportedChains { + switch c.Selector { + case chainselectors.APTOS_MAINNET.Selector: + hasMainnet = true + case chainselectors.APTOS_TESTNET.Selector: + hasTestnet = true + } + } + assert.True(t, hasMainnet) + assert.True(t, hasTestnet) +} diff --git a/cmd/workflow/simulate/chain/evm/capabilities.go b/cmd/workflow/simulate/chain/evm/capabilities.go index 22f000b1..88348b2a 100644 --- a/cmd/workflow/simulate/chain/evm/capabilities.go +++ b/cmd/workflow/simulate/chain/evm/capabilities.go @@ -3,6 +3,7 @@ package evm import ( "context" "crypto/ecdsa" + "fmt" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" @@ -11,6 +12,8 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink/v2/core/capabilities" "github.com/smartcontractkit/chainlink/v2/core/capabilities/fakes" + + "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain" ) // EVMChainCapabilities holds the EVM chain capability servers created for simulation. @@ -29,7 +32,7 @@ func NewEVMChainCapabilities( forwarders map[uint64]string, privateKey *ecdsa.PrivateKey, dryRunChainWrite bool, - limits EVMChainLimits, + limits chain.Limits, ) (*EVMChainCapabilities, error) { evmChains := make(map[uint64]*fakes.FakeEVMChain) for sel, client := range clients { @@ -48,15 +51,11 @@ func NewEVMChainCapabilities( dryRunChainWrite, ) - // Wrap with limits enforcement if limits are provided - var evmCap evmserver.ClientCapability = evm - if limits != nil { - evmCap = NewLimitedEVMChain(evm, limits) - } + evmCap := NewLimitedEVMChain(evm, limits) evmServer := evmserver.NewClientServer(evmCap) if err := registry.Add(ctx, evmServer); err != nil { - return nil, err + return nil, fmt.Errorf("register evm capability for selector %d: %w", sel, err) } evmChains[sel] = evm diff --git a/cmd/workflow/simulate/chain/evm/chaintype.go b/cmd/workflow/simulate/chain/evm/chaintype.go index 76f8784b..6222524a 100644 --- a/cmd/workflow/simulate/chain/evm/chaintype.go +++ b/cmd/workflow/simulate/chain/evm/chaintype.go @@ -87,6 +87,10 @@ func (ct *EVMChainType) ResolveClients(v *viper.Viper) (chain.ResolvedChains, er } for _, ec := range expChains { + // Empty chain-type falls back to this chain type + if ec.ChainType != "" && !strings.EqualFold(ec.ChainType, ct.Name()) { + continue + } if ec.ChainSelector == 0 { return chain.ResolvedChains{}, fmt.Errorf("experimental chain missing chain-selector") } @@ -151,16 +155,9 @@ func (ct *EVMChainType) RegisterCapabilities(ctx context.Context, cfg chain.Capa dryRun := !cfg.Broadcast - // cfg.Limits is the generic chain.Limits contract. The EVM chain type - // needs the wider EVMChainLimits contract (adds ChainWriteGasLimit). A - // nil cfg.Limits disables enforcement entirely. - var evmLimits EVMChainLimits + var evmLimits chain.Limits if cfg.Limits != nil { - el, ok := cfg.Limits.(EVMChainLimits) - if !ok { - return nil, fmt.Errorf("EVM chain type: limits value does not implement evm.EVMChainLimits (got %T)", cfg.Limits) - } - evmLimits = el + evmLimits = ExtractLimits(cfg.Limits) } evmCaps, err := NewEVMChainCapabilities( @@ -221,7 +218,7 @@ func (ct *EVMChainType) RunHealthCheck(resolved chain.ResolvedChains) error { // is true, an invalid or default-sentinel key is a hard error. Otherwise a // sentinel key is used with a warning so non-broadcast simulations can run. func (ct *EVMChainType) ResolveKey(creSettings *settings.Settings, broadcast bool) (interface{}, error) { - pk, err := crypto.HexToECDSA(creSettings.User.EthPrivateKey) + pk, err := crypto.HexToECDSA(creSettings.User.PrivateKey(settings.EVM)) if err != nil { if broadcast { return nil, fmt.Errorf( diff --git a/cmd/workflow/simulate/chain/evm/chaintype_test.go b/cmd/workflow/simulate/chain/evm/chaintype_test.go index 976e94b6..0a1bd1fa 100644 --- a/cmd/workflow/simulate/chain/evm/chaintype_test.go +++ b/cmd/workflow/simulate/chain/evm/chaintype_test.go @@ -163,7 +163,7 @@ func TestEVMChainType_ResolveKey(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ct := newEVMChainType() - s := &settings.Settings{User: settings.UserSettings{EthPrivateKey: tt.pk}} + s := &settings.Settings{User: settings.UserSettings{PrivateKeys: map[string]string{settings.EVM.Name: tt.pk}}} var got interface{} var err error @@ -261,6 +261,19 @@ func TestEVMChainType_RegisterCapabilities_WrongClientType(t *testing.T) { assert.Contains(t, err.Error(), "client for selector 1 is not *ethclient.Client") } +func TestEVMChainType_RegisterCapabilities_WrongPrivateKeyType(t *testing.T) { + t.Parallel() + ct := newEVMChainType() + cfg := chain.CapabilityConfig{ + Clients: map[uint64]chain.ChainClient{}, + Forwarders: map[uint64]string{}, + PrivateKey: "not-an-ecdsa-key", + } + _, err := ct.RegisterCapabilities(context.Background(), cfg) + require.Error(t, err) + assert.Contains(t, err.Error(), "private key is not *ecdsa.PrivateKey") +} + // With no clients the caps should still construct, no type-assertion error. func TestEVMChainType_RegisterCapabilities_NoClients_ConstructsEmpty(t *testing.T) { t.Parallel() diff --git a/cmd/workflow/simulate/chain/evm/limited_capabilities.go b/cmd/workflow/simulate/chain/evm/limited_capabilities.go index b7b50e02..f46e3282 100644 --- a/cmd/workflow/simulate/chain/evm/limited_capabilities.go +++ b/cmd/workflow/simulate/chain/evm/limited_capabilities.go @@ -13,42 +13,30 @@ import ( "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain" ) -// EVMChainLimits is the EVM-scoped limit contract LimitedEVMChain enforces. -// It extends chain.Limits with EVM-specific accessors (e.g. gas limit) so -// non-EVM chain types cannot accidentally depend on EVM semantics. -type EVMChainLimits interface { - chain.Limits - ChainWriteGasLimit() uint64 -} - // LimitedEVMChain wraps an evmserver.ClientCapability and enforces chain write -// report size and gas limits. +// report size and gas limits. Zero-value chain.Limits disables enforcement. type LimitedEVMChain struct { inner evmserver.ClientCapability - limits EVMChainLimits + limits chain.Limits } var _ evmserver.ClientCapability = (*LimitedEVMChain)(nil) -func NewLimitedEVMChain(inner evmserver.ClientCapability, limits EVMChainLimits) *LimitedEVMChain { +func NewLimitedEVMChain(inner evmserver.ClientCapability, limits chain.Limits) *LimitedEVMChain { return &LimitedEVMChain{inner: inner, limits: limits} } func (l *LimitedEVMChain) WriteReport(ctx context.Context, metadata commonCap.RequestMetadata, input *evmcappb.WriteReportRequest) (*commonCap.ResponseAndMetadata[*evmcappb.WriteReportReply], caperrors.Error) { - // Check report size - reportLimit := l.limits.ChainWriteReportSizeLimit() - if reportLimit > 0 && input.Report != nil && len(input.Report.RawReport) > reportLimit { + if l.limits.ReportSize > 0 && input.Report != nil && len(input.Report.RawReport) > l.limits.ReportSize { return nil, caperrors.NewPublicUserError( - fmt.Errorf("simulation limit exceeded: chain write report size %d bytes exceeds limit of %d bytes", len(input.Report.RawReport), reportLimit), + fmt.Errorf("simulation limit exceeded: chain write report size %d bytes exceeds limit of %d bytes", len(input.Report.RawReport), l.limits.ReportSize), caperrors.ResourceExhausted, ) } - // Check gas limit - gasLimit := l.limits.ChainWriteGasLimit() - if gasLimit > 0 && input.GasConfig != nil && input.GasConfig.GasLimit > gasLimit { + if l.limits.GasLimit > 0 && input.GasConfig != nil && input.GasConfig.GasLimit > l.limits.GasLimit { return nil, caperrors.NewPublicUserError( - fmt.Errorf("simulation limit exceeded: EVM gas limit %d exceeds maximum of %d", input.GasConfig.GasLimit, gasLimit), + fmt.Errorf("simulation limit exceeded: EVM gas limit %d exceeds maximum of %d", input.GasConfig.GasLimit, l.limits.GasLimit), caperrors.ResourceExhausted, ) } diff --git a/cmd/workflow/simulate/chain/evm/limited_capabilities_test.go b/cmd/workflow/simulate/chain/evm/limited_capabilities_test.go index 362a3bb4..8b3eb916 100644 --- a/cmd/workflow/simulate/chain/evm/limited_capabilities_test.go +++ b/cmd/workflow/simulate/chain/evm/limited_capabilities_test.go @@ -13,15 +13,9 @@ import ( evmserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/evm/server" "github.com/smartcontractkit/chainlink-common/pkg/types/core" sdkpb "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" -) - -type stubEVMLimits struct { - reportSizeLimit int - gasLimit uint64 -} -func (s *stubEVMLimits) ChainWriteReportSizeLimit() int { return s.reportSizeLimit } -func (s *stubEVMLimits) ChainWriteGasLimit() uint64 { return s.gasLimit } + "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain" +) type evmCapabilityBaseStub struct{} @@ -94,7 +88,7 @@ func (s *evmClientCapabilityStub) ChainSelector() uint64 { return 0 } func TestLimitedEVMChainWriteReportRejectsOversizedReport(t *testing.T) { t.Parallel() - limits := &stubEVMLimits{reportSizeLimit: 4} + limits := chain.Limits{ReportSize: 4} inner := &evmClientCapabilityStub{} wrapper := NewLimitedEVMChain(inner, limits) @@ -110,7 +104,7 @@ func TestLimitedEVMChainWriteReportRejectsOversizedReport(t *testing.T) { func TestLimitedEVMChainWriteReportRejectsOversizedGasLimit(t *testing.T) { t.Parallel() - limits := &stubEVMLimits{gasLimit: 10} + limits := chain.Limits{GasLimit: 10} inner := &evmClientCapabilityStub{} wrapper := NewLimitedEVMChain(inner, limits) @@ -126,7 +120,7 @@ func TestLimitedEVMChainWriteReportRejectsOversizedGasLimit(t *testing.T) { func TestLimitedEVMChainWriteReportDelegatesOnBoundaryValues(t *testing.T) { t.Parallel() - limits := &stubEVMLimits{reportSizeLimit: 4, gasLimit: 10} + limits := chain.Limits{ReportSize: 4, GasLimit: 10} input := &evmcappb.WriteReportRequest{ Report: &sdkpb.ReportResponse{RawReport: []byte("1234")}, @@ -147,3 +141,43 @@ func TestLimitedEVMChainWriteReportDelegatesOnBoundaryValues(t *testing.T) { assert.Same(t, expectedResp, resp) assert.Equal(t, 1, inner.writeReportCalls) } + +func TestLimitedEVMChainWriteReportZeroLimitsDelegate(t *testing.T) { + t.Parallel() + + inner := &evmClientCapabilityStub{} + wrapper := NewLimitedEVMChain(inner, chain.Limits{}) + + _, err := wrapper.WriteReport(context.Background(), commonCap.RequestMetadata{}, &evmcappb.WriteReportRequest{ + Report: &sdkpb.ReportResponse{RawReport: make([]byte, 1_000_000)}, + GasConfig: &evmcappb.GasConfig{GasLimit: 1_000_000_000}, + }) + require.NoError(t, err) + assert.Equal(t, 1, inner.writeReportCalls) +} + +func TestLimitedEVMChainWriteReportNilGasConfigDelegates(t *testing.T) { + t.Parallel() + + inner := &evmClientCapabilityStub{} + wrapper := NewLimitedEVMChain(inner, chain.Limits{ReportSize: 100, GasLimit: 10}) + + _, err := wrapper.WriteReport(context.Background(), commonCap.RequestMetadata{}, &evmcappb.WriteReportRequest{ + Report: &sdkpb.ReportResponse{RawReport: []byte("x")}, + }) + require.NoError(t, err) + assert.Equal(t, 1, inner.writeReportCalls) +} + +func TestLimitedEVMChainWriteReportNilReportDelegates(t *testing.T) { + t.Parallel() + + inner := &evmClientCapabilityStub{} + wrapper := NewLimitedEVMChain(inner, chain.Limits{ReportSize: 100, GasLimit: 100}) + + _, err := wrapper.WriteReport(context.Background(), commonCap.RequestMetadata{}, &evmcappb.WriteReportRequest{ + GasConfig: &evmcappb.GasConfig{GasLimit: 50}, + }) + require.NoError(t, err) + assert.Equal(t, 1, inner.writeReportCalls) +} diff --git a/cmd/workflow/simulate/chain/evm/limits.go b/cmd/workflow/simulate/chain/evm/limits.go new file mode 100644 index 00000000..155d1f79 --- /dev/null +++ b/cmd/workflow/simulate/chain/evm/limits.go @@ -0,0 +1,14 @@ +package evm + +import ( + "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" + + "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain" +) + +func ExtractLimits(w *cresettings.Workflows) chain.Limits { + return chain.Limits{ + ReportSize: int(w.ChainWrite.EVM.ReportSizeLimit.DefaultValue), + GasLimit: w.ChainWrite.EVM.GasLimit.Default.DefaultValue, + } +} diff --git a/cmd/workflow/simulate/chain/evm/limits_test.go b/cmd/workflow/simulate/chain/evm/limits_test.go new file mode 100644 index 00000000..4df914bc --- /dev/null +++ b/cmd/workflow/simulate/chain/evm/limits_test.go @@ -0,0 +1,17 @@ +package evm + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" +) + +func TestExtractLimitsFromDefault(t *testing.T) { + t.Parallel() + w := cresettings.Default.PerWorkflow + lim := ExtractLimits(&w) + assert.Equal(t, 5_000, lim.ReportSize) + assert.Equal(t, uint64(5_000_000), lim.GasLimit) +} diff --git a/cmd/workflow/simulate/chain/types.go b/cmd/workflow/simulate/chain/types.go index 12f8c1cb..fdc52bab 100644 --- a/cmd/workflow/simulate/chain/types.go +++ b/cmd/workflow/simulate/chain/types.go @@ -2,6 +2,7 @@ package chain import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" "github.com/smartcontractkit/chainlink/v2/core/capabilities" ) @@ -15,14 +16,6 @@ type ChainConfig struct { Forwarder string // chain-type-specific forwarding address } -// Limits exposes the chain-write limits that every chain type's capability -// enforcement layer needs. Chain-type-specific accessors (e.g. EVM gas limit) -// live on chain-type-scoped extension interfaces in the family package so -// non-EVM chain types cannot accidentally depend on EVM semantics. -type Limits interface { - ChainWriteReportSizeLimit() int -} - // ResolvedChains is the result of ChainType.ResolveClients: the RPC clients, // forwarders, and any chain-type-agnostic metadata later interface methods // (e.g. RunHealthCheck) depend on. @@ -35,6 +28,13 @@ type ResolvedChains struct { ExperimentalSelectors map[uint64]bool } +// Limits is the common per-family limits contract enforced by the +// LimitedChain wrappers. +type Limits struct { + ReportSize int + GasLimit uint64 +} + // CapabilityConfig holds everything a chain type needs to register capabilities. type CapabilityConfig struct { Registry *capabilities.Registry @@ -42,7 +42,7 @@ type CapabilityConfig struct { Forwarders map[uint64]string PrivateKey interface{} // chain-type-specific key type; EVM uses *ecdsa.PrivateKey Broadcast bool - Limits Limits // nil disables limit enforcement + Limits *cresettings.Workflows // nil disables enforcement Logger logger.Logger } diff --git a/cmd/workflow/simulate/limits.go b/cmd/workflow/simulate/limits.go index d4b0cfbb..bc05bb23 100644 --- a/cmd/workflow/simulate/limits.go +++ b/cmd/workflow/simulate/limits.go @@ -94,6 +94,12 @@ func applyEngineLimits(cfg *cresettings.Workflows, limits *SimulationLimits) { cfg.HTTPTrigger = src.HTTPTrigger cfg.LogTrigger = src.LogTrigger + //ChainWrite limits - NOTE these are not applied here, but allows flexibility in the future if we want engine to control limits + cfg.ChainWrite.EVM.ReportSizeLimit = src.ChainWrite.EVM.ReportSizeLimit + cfg.ChainWrite.EVM.GasLimit = src.ChainWrite.EVM.GasLimit + cfg.ChainWrite.Aptos.ReportSizeLimit = src.ChainWrite.Aptos.ReportSizeLimit + cfg.ChainWrite.Aptos.GasLimit = src.ChainWrite.Aptos.GasLimit + // NOTE: ChainAllowed is NOT overridden — simulation keeps allow-all } @@ -103,6 +109,7 @@ func disableEngineLimits(cfg *cresettings.Workflows) { maxInt := settings.Setting[int]{DefaultValue: math.MaxInt32} maxSize := settings.Setting[config.Size]{DefaultValue: math.MaxInt32} maxDuration := settings.Setting[time.Duration]{DefaultValue: 24 * time.Hour} + maxGas := settings.Setting[uint64]{DefaultValue: math.MaxUint64} // Execution limits cfg.ExecutionTimeout = maxDuration @@ -149,9 +156,12 @@ func disableEngineLimits(cfg *cresettings.Workflows) { cfg.Consensus.CallLimit = maxInt cfg.Consensus.ObservationSizeLimit = maxSize - // ChainWrite limits + // ChainWrite limits - NOTE these are not applied here, but allows flexibility in the future if we want engine to control limits cfg.ChainWrite.TargetsLimit = maxInt - cfg.ChainWrite.ReportSizeLimit = maxSize + cfg.ChainWrite.EVM.ReportSizeLimit = maxSize + cfg.ChainWrite.EVM.GasLimit.Default = maxGas + cfg.ChainWrite.Aptos.ReportSizeLimit = maxSize + cfg.ChainWrite.Aptos.GasLimit.Default = maxGas // ChainRead limits cfg.ChainRead.CallLimit = maxInt @@ -186,16 +196,26 @@ func (l *SimulationLimits) ConsensusObservationSizeLimit() int { return int(l.Workflows.Consensus.ObservationSizeLimit.DefaultValue) } -// ChainWriteReportSizeLimit returns the chain write report size limit in bytes. -func (l *SimulationLimits) ChainWriteReportSizeLimit() int { - return int(l.Workflows.ChainWrite.ReportSizeLimit.DefaultValue) +// EVMChainWriteReportSizeLimit returns the EVM chain write report size limit in bytes. +func (l *SimulationLimits) EVMChainWriteReportSizeLimit() int { + return int(l.Workflows.ChainWrite.EVM.ReportSizeLimit.DefaultValue) +} + +// AptosChainWriteReportSizeLimit returns the Aptos chain write report size limit in bytes. +func (l *SimulationLimits) AptosChainWriteReportSizeLimit() int { + return int(l.Workflows.ChainWrite.Aptos.ReportSizeLimit.DefaultValue) } -// ChainWriteGasLimit returns the default EVM gas limit. -func (l *SimulationLimits) ChainWriteGasLimit() uint64 { +// EVMChainWriteGasLimit returns the default EVM chain write gas limit. +func (l *SimulationLimits) EVMChainWriteGasLimit() uint64 { return l.Workflows.ChainWrite.EVM.GasLimit.Default.DefaultValue } +// AptosChainWriteGasLimit returns the default Aptos chain write gas limit. +func (l *SimulationLimits) AptosChainWriteGasLimit() uint64 { + return l.Workflows.ChainWrite.Aptos.GasLimit.Default.DefaultValue +} + // WASMBinarySize returns the WASM binary size limit in bytes. func (l *SimulationLimits) WASMBinarySize() int { return int(l.Workflows.WASMBinarySizeLimit.DefaultValue) @@ -210,7 +230,7 @@ func (l *SimulationLimits) WASMCompressedBinarySize() int { func (l *SimulationLimits) LimitsSummary() string { w := &l.Workflows return fmt.Sprintf( - "HTTP: req=%s resp=%s timeout=%s | ConfHTTP: req=%s resp=%s timeout=%s | Consensus obs=%s | ChainWrite report=%s gas=%d | WASM binary=%s compressed=%s", + "HTTP: req=%s resp=%s timeout=%s | ConfHTTP: req=%s resp=%s timeout=%s | Consensus obs=%s | ChainWrite evm_report=%s evm_gas=%d aptos_report=%s aptos_gas=%d | WASM binary=%s compressed=%s", w.HTTPAction.RequestSizeLimit.DefaultValue, w.HTTPAction.ResponseSizeLimit.DefaultValue, w.HTTPAction.ConnectionTimeout.DefaultValue, @@ -218,8 +238,10 @@ func (l *SimulationLimits) LimitsSummary() string { w.ConfidentialHTTP.ResponseSizeLimit.DefaultValue, w.ConfidentialHTTP.ConnectionTimeout.DefaultValue, w.Consensus.ObservationSizeLimit.DefaultValue, - w.ChainWrite.ReportSizeLimit.DefaultValue, + w.ChainWrite.EVM.ReportSizeLimit.DefaultValue, w.ChainWrite.EVM.GasLimit.Default.DefaultValue, + w.ChainWrite.Aptos.ReportSizeLimit.DefaultValue, + w.ChainWrite.Aptos.GasLimit.Default.DefaultValue, w.WASMBinarySizeLimit.DefaultValue, w.WASMCompressedBinarySizeLimit.DefaultValue, ) diff --git a/cmd/workflow/simulate/limits.json b/cmd/workflow/simulate/limits.json index ced46eeb..84f38dcc 100644 --- a/cmd/workflow/simulate/limits.json +++ b/cmd/workflow/simulate/limits.json @@ -32,13 +32,19 @@ }, "ChainWrite": { "TargetsLimit": "10", - "ReportSizeLimit": "5kb", "EVM": { - "TransactionGasLimit": "5000000", + "ReportSizeLimit": "5kb", "GasLimit": { "Default": "5000000", "Values": {} } + }, + "Aptos": { + "ReportSizeLimit": "5kb", + "GasLimit": { + "Default": "2000000", + "Values": {} + } } }, "ChainRead": { diff --git a/cmd/workflow/simulate/limits_test.go b/cmd/workflow/simulate/limits_test.go index 08389fb3..cee61044 100644 --- a/cmd/workflow/simulate/limits_test.go +++ b/cmd/workflow/simulate/limits_test.go @@ -30,8 +30,10 @@ func TestDefaultLimitsAndExportDefaultLimitsJSON(t *testing.T) { assert.Equal(t, 10_000, limits.ConfHTTPRequestSizeLimit()) assert.Equal(t, 100_000, limits.ConfHTTPResponseSizeLimit()) assert.Equal(t, 100_000, limits.ConsensusObservationSizeLimit()) - assert.Equal(t, 5_000, limits.ChainWriteReportSizeLimit()) - assert.Equal(t, uint64(5_000_000), limits.ChainWriteGasLimit()) + assert.Equal(t, 5_000, limits.EVMChainWriteReportSizeLimit()) + assert.Equal(t, 5_000, limits.AptosChainWriteReportSizeLimit()) + assert.Equal(t, uint64(5_000_000), limits.EVMChainWriteGasLimit()) + assert.Equal(t, uint64(2_000_000), limits.AptosChainWriteGasLimit()) assert.Equal(t, 100_000_000, limits.WASMBinarySize()) assert.Equal(t, 20_000_000, limits.WASMCompressedBinarySize()) assert.JSONEq(t, string(defaultLimitsJSON), string(ExportDefaultLimitsJSON())) @@ -46,12 +48,11 @@ func TestLoadLimitsParsesCustomFileAndPreservesDefaultsForUnsetFields(t *testing "ConnectionTimeout": "2s" }, "ChainWrite": { - "ReportSizeLimit": "9kb", - "EVM": { - "GasLimit": { - "Default": "123" - } - } + "EVM": {"ReportSizeLimit": "9kb", "GasLimit": {"Default": "1234567"}}, + "Aptos": {"ReportSizeLimit": "11kb", "GasLimit": {"Default": "7654321"}} + }, + "CRONTrigger": { + "FastestScheduleInterval": "45s" } }`) @@ -60,8 +61,11 @@ func TestLoadLimitsParsesCustomFileAndPreservesDefaultsForUnsetFields(t *testing assert.Equal(t, 7_000, limits.HTTPRequestSizeLimit()) assert.Equal(t, 100_000, limits.HTTPResponseSizeLimit(), "unset values should keep embedded defaults") - assert.Equal(t, 9_000, limits.ChainWriteReportSizeLimit()) - assert.Equal(t, uint64(123), limits.ChainWriteGasLimit()) + assert.Equal(t, 9_000, limits.EVMChainWriteReportSizeLimit()) + assert.Equal(t, 11_000, limits.AptosChainWriteReportSizeLimit()) + assert.Equal(t, uint64(1_234_567), limits.EVMChainWriteGasLimit()) + assert.Equal(t, uint64(7_654_321), limits.AptosChainWriteGasLimit()) + assert.Equal(t, 45*time.Second, limits.Workflows.CRONTrigger.FastestScheduleInterval.DefaultValue) assert.Equal(t, 2*time.Second, limits.Workflows.HTTPAction.ConnectionTimeout.DefaultValue) } @@ -95,7 +99,10 @@ func TestResolveLimitsHandlesAllSupportedModes(t *testing.T) { baseline, err := DefaultLimits() require.NoError(t, err) assert.Equal(t, baseline.HTTPRequestSizeLimit(), defaultLimits.HTTPRequestSizeLimit()) - assert.Equal(t, baseline.ChainWriteGasLimit(), defaultLimits.ChainWriteGasLimit()) + assert.Equal(t, baseline.EVMChainWriteReportSizeLimit(), defaultLimits.EVMChainWriteReportSizeLimit()) + assert.Equal(t, baseline.AptosChainWriteReportSizeLimit(), defaultLimits.AptosChainWriteReportSizeLimit()) + assert.Equal(t, baseline.EVMChainWriteGasLimit(), defaultLimits.EVMChainWriteGasLimit()) + assert.Equal(t, baseline.AptosChainWriteGasLimit(), defaultLimits.AptosChainWriteGasLimit()) path := writeLimitsFile(t, `{"Consensus":{"ObservationSizeLimit":"2kb"}}`) customLimits, err := ResolveLimits(path) @@ -130,6 +137,10 @@ func TestApplyEngineLimitsCopiesSupportedFieldsAndPreservesChainAllowed(t *testi limits.Workflows.LogEventLimit.DefaultValue = 25 limits.Workflows.ChainRead.CallLimit.DefaultValue = 3 limits.Workflows.ChainWrite.TargetsLimit.DefaultValue = 4 + limits.Workflows.ChainWrite.EVM.ReportSizeLimit.DefaultValue = 9_000 + limits.Workflows.ChainWrite.EVM.GasLimit.Default.DefaultValue = 1_234_567 + limits.Workflows.ChainWrite.Aptos.ReportSizeLimit.DefaultValue = 11_000 + limits.Workflows.ChainWrite.Aptos.GasLimit.Default.DefaultValue = 7_654_321 limits.Workflows.Consensus.CallLimit.DefaultValue = 5 limits.Workflows.HTTPAction.CallLimit.DefaultValue = 6 limits.Workflows.ConfidentialHTTP.CallLimit.DefaultValue = 7 @@ -158,6 +169,10 @@ func TestApplyEngineLimitsCopiesSupportedFieldsAndPreservesChainAllowed(t *testi assert.Equal(t, 25, cfg.LogEventLimit.DefaultValue) assert.Equal(t, 3, cfg.ChainRead.CallLimit.DefaultValue) assert.Equal(t, 4, cfg.ChainWrite.TargetsLimit.DefaultValue) + assert.Equal(t, 9_000, int(cfg.ChainWrite.EVM.ReportSizeLimit.DefaultValue)) + assert.Equal(t, uint64(1_234_567), cfg.ChainWrite.EVM.GasLimit.Default.DefaultValue) + assert.Equal(t, 11_000, int(cfg.ChainWrite.Aptos.ReportSizeLimit.DefaultValue)) + assert.Equal(t, uint64(7_654_321), cfg.ChainWrite.Aptos.GasLimit.Default.DefaultValue) assert.Equal(t, 5, cfg.Consensus.CallLimit.DefaultValue) assert.Equal(t, 6, cfg.HTTPAction.CallLimit.DefaultValue) assert.Equal(t, 7, cfg.ConfidentialHTTP.CallLimit.DefaultValue) @@ -173,6 +188,6 @@ func TestSimulationLimitsSummaryIncludesKeyLimitValues(t *testing.T) { assert.Contains(t, summary, "HTTP: req=10kb resp=100kb timeout=10s") assert.Contains(t, summary, "ConfHTTP: req=10kb resp=100kb timeout=10s") assert.Contains(t, summary, "Consensus obs=100kb") - assert.Contains(t, summary, "ChainWrite report=5kb gas=5000000") + assert.Contains(t, summary, "ChainWrite evm_report=5kb evm_gas=5000000 aptos_report=5kb aptos_gas=2000000") assert.Contains(t, summary, "WASM binary=100mb compressed=20mb") } diff --git a/cmd/workflow/simulate/simulate.go b/cmd/workflow/simulate/simulate.go index 68d8d973..1d68f18d 100644 --- a/cmd/workflow/simulate/simulate.go +++ b/cmd/workflow/simulate/simulate.go @@ -31,7 +31,9 @@ import ( cmdcommon "github.com/smartcontractkit/cre-cli/cmd/common" "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain" - _ "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain/evm" // register EVM chain family via package init + _ "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain/aptos" // register Aptos chain family via package init + _ "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain/evm" // register EVM chain family via package init + "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/runtime" @@ -95,7 +97,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { } simulateCmd.Flags().BoolP("engine-logs", "g", false, "Enable non-fatal engine logging") - simulateCmd.Flags().Bool("broadcast", false, "Broadcast transactions to the EVM (default: false)") + simulateCmd.Flags().Bool("broadcast", false, "Broadcast transactions to configured chains (default: false)") simulateCmd.Flags().String("wasm", "", "Path or URL to a pre-built WASM binary (skips compilation)") simulateCmd.Flags().String("config", "", "Override the config file path from workflow.yaml") simulateCmd.Flags().Bool("no-config", false, "Simulate without a config file") @@ -462,11 +464,10 @@ func run( } srvcs = append(srvcs, manualTriggerCaps.ManualCronTrigger, manualTriggerCaps.ManualHTTPTrigger) - // Only set Limits when non-nil to avoid the typed-nil interface trap - // (a nil *SimulationLimits boxed into chain.Limits compares != nil). - var capLimits chain.Limits + // nil capLimits disables enforcement. + var capLimits *cresettings.Workflows if simLimits != nil { - capLimits = simLimits + capLimits = &simLimits.Workflows } // Register chain-type-specific capabilities diff --git a/cmd/workflow/simulate/simulate_test.go b/cmd/workflow/simulate/simulate_test.go index 1d24423a..1ae31d78 100644 --- a/cmd/workflow/simulate/simulate_test.go +++ b/cmd/workflow/simulate/simulate_test.go @@ -73,7 +73,7 @@ func TestBlankWorkflowSimulation(t *testing.T) { Workflow: workflowSettings, User: settings.UserSettings{ TargetName: "staging-settings", - EthPrivateKey: "88888845d8761ca4a8cefb324c89702f12114ffbd0c47222f12aac0ad6538888", + PrivateKeys: map[string]string{settings.EVM.Name: "88888845d8761ca4a8cefb324c89702f12114ffbd0c47222f12aac0ad6538888"}, }, }, } @@ -125,7 +125,7 @@ func createSimulateTestSettings(workflowName, workflowPath, configPath string) * }, }, User: settings.UserSettings{ - EthPrivateKey: "88888845d8761ca4a8cefb324c89702f12114ffbd0c47222f12aac0ad6538888", + PrivateKeys: map[string]string{settings.EVM.Name: "88888845d8761ca4a8cefb324c89702f12114ffbd0c47222f12aac0ad6538888"}, }, } } @@ -446,7 +446,7 @@ func TestSimulateConfigFlagsMutuallyExclusive(t *testing.T) { Viper: viper.New(), Settings: &settings.Settings{ User: settings.UserSettings{ - EthPrivateKey: "88888845d8761ca4a8cefb324c89702f12114ffbd0c47222f12aac0ad6538888", + PrivateKeys: map[string]string{settings.EVM.Name: "88888845d8761ca4a8cefb324c89702f12114ffbd0c47222f12aac0ad6538888"}, }, }, } diff --git a/go.mod b/go.mod index ffb6c346..94f4dc3a 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/test-go/testify v1.1.4 go.uber.org/zap v1.27.1 - golang.org/x/term v0.41.0 + golang.org/x/term v0.42.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 @@ -72,7 +72,7 @@ require ( github.com/VictoriaMetrics/fastcache v1.13.0 // indirect github.com/XSAM/otelsql v0.37.0 // indirect github.com/apache/arrow-go/v18 v18.3.1 // indirect - github.com/aptos-labs/aptos-go-sdk v1.12.1 // indirect + github.com/aptos-labs/aptos-go-sdk v1.12.1 github.com/atombender/go-jsonschema v0.16.1-0.20240916205339-a74cd4e2851c // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/avast/retry-go v3.0.0+incompatible // indirect @@ -310,7 +310,7 @@ require ( github.com/shopspring/decimal v1.4.0 // indirect github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 // indirect github.com/smartcontractkit/ccip-owner-contracts v0.1.0 // indirect - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260407161350-a86b1969da65 // indirect + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260422165416-b56c9c2b5867 github.com/smartcontractkit/chainlink-automation v0.8.1 // indirect github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260417153334-3b564ef614de // indirect github.com/smartcontractkit/chainlink-ccip/chains/evm v0.0.0-20260415165642-49f23e4d76cc // indirect @@ -405,17 +405,17 @@ require ( go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.11.0 // indirect - golang.org/x/crypto v0.49.0 // indirect + golang.org/x/crypto v0.50.0 // indirect golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect - golang.org/x/mod v0.33.0 // indirect + golang.org/x/mod v0.34.0 // indirect golang.org/x/net v0.52.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 // indirect - golang.org/x/text v0.35.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect + golang.org/x/text v0.36.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.42.0 // indirect + golang.org/x/tools v0.43.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gonum.org/v1/gonum v0.17.0 // indirect google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect diff --git a/go.sum b/go.sum index 6fa0e20d..ed6f287d 100644 --- a/go.sum +++ b/go.sum @@ -1307,8 +1307,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.98 h1:fuI7CQ1o5cX64eO4/LvwtfhdpGFH5vnsM/bFHRwEiww= github.com/smartcontractkit/chain-selectors v1.0.98/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260407161350-a86b1969da65 h1:b6+ZvoZxXSj7HywoZ0CfWtC6k47eBSaxNzc2LqtiXBA= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260407161350-a86b1969da65/go.mod h1:BbVsx2VcwSVWkd0C5TcAkQBnFaeYFnogJgUa9BUla18= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260422165416-b56c9c2b5867 h1:DoFHH4hMm1aGNiUQVZGRziMdwGByy4C+Inm5mOlxTYc= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260422165416-b56c9c2b5867/go.mod h1:ZU57FhGIb+m20yysn2fw+vLh3qB5hcgd06RXEUEDBck= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260417153334-3b564ef614de h1:coysmw4zHm6TLOZawoe2h0hHh/25ft+hq9+9mRNkqTs= @@ -1709,8 +1709,8 @@ golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98y golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1747,8 +1747,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1897,10 +1897,10 @@ golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 h1:bTLqdHv7xrGlFbvf5/TXNxy/iUwwdkjhqQTJDjW7aj0= -golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1911,8 +1911,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1926,8 +1926,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1980,8 +1980,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 580b6940..19b99b7c 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -12,13 +12,37 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + corekeys "github.com/smartcontractkit/chainlink-common/keystore/corekeys" + "github.com/smartcontractkit/cre-cli/internal/ui" ) -// sensitive information (not in configuration file) -const ( - EthPrivateKeyEnvVar = "CRE_ETH_PRIVATE_KEY" - CreTargetEnvVar = "CRE_TARGET" +const CreTargetEnvVar = "CRE_TARGET" + +// ChainType describes a chain family and the per-family settings the CLI +// loads from the environment. Add a family by appending to AllChainTypes. +type ChainType struct { + Name string + PrivateKeyEnv string +} + +var ( + EVM = ChainType{ + Name: string(corekeys.EVM), + PrivateKeyEnv: "CRE_ETH_PRIVATE_KEY", + } + Aptos = ChainType{ + Name: string(corekeys.Aptos), + PrivateKeyEnv: "CRE_APTOS_PRIVATE_KEY", + } + + AllChainTypes = []ChainType{EVM, Aptos} +) + +// Backwards-compat aliases; prefer EVM.PrivateKeyEnv / Aptos.PrivateKeyEnv. +var ( + EthPrivateKeyEnvVar = EVM.PrivateKeyEnv + AptosPrivateKeyEnvVar = Aptos.PrivateKeyEnv ) // State tracked by LoadEnv / LoadPublicEnv so downstream code (e.g. build @@ -56,9 +80,16 @@ type Settings struct { // UserSettings stores user-specific configurations. type UserSettings struct { - TargetName string - EthPrivateKey string - EthUrl string + TargetName string + PrivateKeys map[string]string // keyed by ChainType.Name +} + +// PrivateKey returns the signing key for the given chain, or "" if unset. +func (u UserSettings) PrivateKey(f ChainType) string { + if u.PrivateKeys == nil { + return "" + } + return u.PrivateKeys[f.Name] } // New initializes and loads settings from YAML config files and the environment. @@ -101,13 +132,15 @@ func New(logger *zerolog.Logger, v *viper.Viper, cmd *cobra.Command, registryCha return nil, err } - rawPrivKey := v.GetString(EthPrivateKeyEnvVar) - normPrivKey := NormalizeHexKey(rawPrivKey) + privateKeys := make(map[string]string, len(AllChainTypes)) + for _, f := range AllChainTypes { + privateKeys[f.Name] = NormalizeHexKey(v.GetString(f.PrivateKeyEnv)) + } return &Settings{ User: UserSettings{ - EthPrivateKey: normPrivKey, - TargetName: target, + TargetName: target, + PrivateKeys: privateKeys, }, Workflow: workflowSettings, StorageSettings: storageSettings, @@ -163,7 +196,11 @@ func LoadEnv(logger *zerolog.Logger, v *viper.Viper, envPath string) { loadedEnvFilePath = "" loadedEnvVars = nil loadedEnvFilePath, loadedEnvVars = loadEnvFile(logger, envPath) - bindAllVars(v, loadedEnvVars, EthPrivateKeyEnvVar, CreTargetEnvVar) + extras := []string{CreTargetEnvVar} + for _, f := range AllChainTypes { + extras = append(extras, f.PrivateKeyEnv) + } + bindAllVars(v, loadedEnvVars, extras...) } // LoadPublicEnv loads variables from envPath into the process environment diff --git a/internal/settings/settings_generate.go b/internal/settings/settings_generate.go index 1651cbdc..a4fcbdd0 100644 --- a/internal/settings/settings_generate.go +++ b/internal/settings/settings_generate.go @@ -28,9 +28,10 @@ var gitIgnoreTemplateContent string var workflowSettingsTemplateContent string type ProjectEnv struct { - FilePath string - GitHubAPIToken string - EthPrivateKey string + FilePath string + GitHubAPIToken string + EthPrivateKey string + AptosPrivateKey string } func GetDefaultReplacements() map[string]string { @@ -118,8 +119,9 @@ func GenerateProjectEnvFile(workingDirectory string) (string, error) { } replacements := map[string]string{ - "GithubApiToken": "your-github-token", - "EthPrivateKey": "your-eth-private-key", + "GithubApiToken": "your-github-token", + "EthPrivateKey": "your-eth-private-key", + "AptosPrivateKey": "your-aptos-private-key", } if err := GenerateFileFromTemplate(outputPath, ProjectEnvironmentTemplateContent, replacements); err != nil { diff --git a/internal/settings/settings_generate_test.go b/internal/settings/settings_generate_test.go index d612f66e..359428fc 100644 --- a/internal/settings/settings_generate_test.go +++ b/internal/settings/settings_generate_test.go @@ -3,6 +3,7 @@ package settings import ( "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -68,6 +69,21 @@ func TestGetReplacementsWithNetworks(t *testing.T) { assert.Contains(t, repl, "ConfigPathStaging") } +func TestProjectEnvironmentTemplateIncludesAptosPrivateKey(t *testing.T) { + replacements := map[string]string{ + "EthPrivateKey": "eth-key", + "AptosPrivateKey": "aptos-key", + } + + content := ProjectEnvironmentTemplateContent + for key, value := range replacements { + content = strings.ReplaceAll(content, "{{"+key+"}}", value) + } + + assert.Contains(t, content, "CRE_ETH_PRIVATE_KEY=eth-key") + assert.Contains(t, content, "CRE_APTOS_PRIVATE_KEY=aptos-key") +} + func TestPatchProjectRPCs(t *testing.T) { t.Run("patches matching chain URLs", func(t *testing.T) { tmpDir := t.TempDir() diff --git a/internal/settings/settings_get.go b/internal/settings/settings_get.go index 3c778a51..24bf0c31 100644 --- a/internal/settings/settings_get.go +++ b/internal/settings/settings_get.go @@ -38,10 +38,11 @@ type RpcEndpoint struct { Url string `mapstructure:"url" yaml:"url"` } -// ExperimentalChain represents an EVM chain not in official chain-selectors. +// ExperimentalChain represents a chain not in official chain-selectors. // Automatically used by the simulator when present in the target's experimental-chains config. -// The ChainSelector is used as the selector key for EVM clients and forwarders. +// ChainType selects the chain family; empty defaults to "evm" for backward compat. type ExperimentalChain struct { + ChainType string `mapstructure:"chain-type" yaml:"chain-type"` ChainSelector uint64 `mapstructure:"chain-selector" yaml:"chain-selector"` RPCURL string `mapstructure:"rpc-url" yaml:"rpc-url"` Forwarder string `mapstructure:"forwarder" yaml:"forwarder"` @@ -263,15 +264,21 @@ func ChainNameFromSelectorString(raw string) (string, error) { } func GetChainSelectorByChainName(name string) (uint64, error) { - chainID, err := chainSelectors.ChainIdFromName(name) - if err != nil { - return 0, fmt.Errorf("failed to get chain ID from name %q: %w", name, err) - } - - selector, err := chainSelectors.SelectorFromChainId(chainID) - if err != nil { - return 0, fmt.Errorf("failed to get selector from chain ID %d: %w", chainID, err) + switch { + case strings.HasPrefix(name, chainSelectors.FamilyAptos): + for _, c := range chainSelectors.AptosALL { + if c.Name == name { + return c.Selector, nil + } + } + default: + if chainID, err := chainSelectors.ChainIdFromName(name); err == nil { + selector, err := chainSelectors.SelectorFromChainId(chainID) + if err != nil { + return 0, fmt.Errorf("failed to get selector from chain ID %d: %w", chainID, err) + } + return selector, nil + } } - - return selector, nil + return 0, fmt.Errorf("failed to get chain ID from name %q: chain not found", name) } diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index 3b99133f..9ecd7780 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -136,7 +136,7 @@ func TestLoadEnvAndSettings(t *testing.T) { s, err := settings.New(logger, v, cmd, "") require.NoError(t, err) assert.Equal(t, "staging", s.User.TargetName) - assert.Equal(t, "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", s.User.EthPrivateKey) + assert.Equal(t, "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", s.User.PrivateKey(settings.EVM)) } func TestLoadEnvAndSettingsWithWorkflowSettingsFlag(t *testing.T) { @@ -169,7 +169,7 @@ func TestLoadEnvAndSettingsWithWorkflowSettingsFlag(t *testing.T) { s, err := settings.New(logger, v, cmd, "") require.NoError(t, err) assert.Equal(t, "staging", s.User.TargetName) - assert.Equal(t, "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", s.User.EthPrivateKey) + assert.Equal(t, "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", s.User.PrivateKey(settings.EVM)) } func TestInlineEnvTakesPrecedenceOverDotEnv(t *testing.T) { @@ -199,7 +199,7 @@ func TestInlineEnvTakesPrecedenceOverDotEnv(t *testing.T) { s, err := settings.New(logger, v, cmd, "") require.NoError(t, err) assert.Equal(t, "staging", s.User.TargetName) - assert.Equal(t, "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", s.User.EthPrivateKey) + assert.Equal(t, "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", s.User.PrivateKey(settings.EVM)) } func TestLoadEnvAndMergedSettings(t *testing.T) { @@ -241,7 +241,7 @@ func TestLoadEnvAndMergedSettings(t *testing.T) { rpc2 := s.Workflow.RPCs[1] assert.Equal(t, "https://somethingElse.rpc.org", rpc1.Url, "First RPC URL mismatch") assert.Equal(t, "https://something.rpc.org", rpc2.Url, "Second RPC URL mismatch") - assert.Equal(t, "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", s.User.EthPrivateKey) + assert.Equal(t, "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", s.User.PrivateKey(settings.EVM)) } // helper to build a command with optional --broadcast flag and parse args @@ -330,7 +330,7 @@ func TestOffChainDeploymentRegistryUsesDerivedOwnerWithoutPrivateKey(t *testing. require.NoError(t, err) assert.Equal(t, derived, s.Workflow.UserWorkflowSettings.WorkflowOwnerAddress) assert.Equal(t, constants.WorkflowOwnerTypeOrgDerived, s.Workflow.UserWorkflowSettings.WorkflowOwnerType) - assert.Empty(t, s.User.EthPrivateKey) + assert.Empty(t, s.User.PrivateKey(settings.EVM)) } func TestOffChainDeploymentRegistryMissingDerivedOwnerReturnsError(t *testing.T) { diff --git a/internal/settings/template/.env.tpl b/internal/settings/template/.env.tpl index 0f17f640..3a3ba339 100644 --- a/internal/settings/template/.env.tpl +++ b/internal/settings/template/.env.tpl @@ -6,6 +6,9 @@ # Ethereum private key or 1Password reference (e.g. op://vault/item/field) CRE_ETH_PRIVATE_KEY={{EthPrivateKey}} +# Aptos private key or 1Password reference (32-byte Ed25519 seed hex) +CRE_APTOS_PRIVATE_KEY={{AptosPrivateKey}} + # RPC secret keys — referenced in project.yaml via ${VAR_NAME} syntax. # Example: # CRE_SECRET_RPC_SEPOLIA=my-secret-api-key diff --git a/internal/settings/template/project.yaml.tpl b/internal/settings/template/project.yaml.tpl index 8f894a90..8314980d 100644 --- a/internal/settings/template/project.yaml.tpl +++ b/internal/settings/template/project.yaml.tpl @@ -23,10 +23,11 @@ # # Experimental chains (automatically used by the simulator when present): # Use this for chains not yet in official chain-selectors (e.g., hackathons, new chain integrations). -# In your workflow, reference the chain as evm:ChainSelector:@1.0.0 +# In your workflow, reference the chain as :ChainSelector:@1.0.0 # # experimental-chains: -# - chain-selector: 12345 # The chain selector value +# - chain-type: evm # Chain family +# chain-selector: 12345 # The chain selector value # rpc-url: "https://rpc.example.com" # RPC endpoint URL # forwarder: "0x..." # Forwarder contract address on the chain diff --git a/test/multi_command_flows/workflow_private_registry.go b/test/multi_command_flows/workflow_private_registry.go index 5ab8c3df..1cf7af72 100644 --- a/test/multi_command_flows/workflow_private_registry.go +++ b/test/multi_command_flows/workflow_private_registry.go @@ -883,7 +883,7 @@ func RunPrivateRegistryAuthAndSettingsFinalize(t *testing.T, envPath, blankWorkf s, err := settings.New(logger, v, cmd, "") require.NoError(t, err) require.NotNil(t, s) - require.Empty(t, s.User.EthPrivateKey, "CRE_ETH_PRIVATE_KEY must be absent") + require.Empty(t, s.User.PrivateKey(settings.EVM), "CRE_ETH_PRIVATE_KEY must be absent") require.Equal(t, "reg-test", s.Workflow.UserWorkflowSettings.DeploymentRegistry) require.Empty(t, s.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, "owner is deferred until finalize when deployment-registry is set") require.Empty(t, s.Workflow.UserWorkflowSettings.WorkflowOwnerType)