From 3206e4ed034090c506cc394dda3f84eae57c6d74 Mon Sep 17 00:00:00 2001 From: Tarcisio Ferraz Date: Thu, 4 Jun 2026 10:18:40 -0300 Subject: [PATCH 1/5] simplify local gateway --- cmd/workflow/simulate/capabilities.go | 9 ++- cmd/workflow/simulate/simulate.go | 70 ++++++--------------- cmd/workflow/simulate/simulate_test.go | 87 ++++++++++++-------------- docs/cre_workflow_simulate.md | 2 +- 4 files changed, 70 insertions(+), 98 deletions(-) diff --git a/cmd/workflow/simulate/capabilities.go b/cmd/workflow/simulate/capabilities.go index ed5dc92b..dbe37de3 100644 --- a/cmd/workflow/simulate/capabilities.go +++ b/cmd/workflow/simulate/capabilities.go @@ -16,8 +16,13 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink/v2/core/capabilities" "github.com/smartcontractkit/chainlink/v2/core/capabilities/fakes" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/fakes/gateway" ) +// httpTriggerServerPort is the port on which the local HTTP server listens +// when no --http-payload flag is supplied and the user chooses to POST the payload. +const httpTriggerServerPort = 9090 + // ManualTriggers holds chain-agnostic trigger services used in simulation. type ManualTriggers struct { ManualCronTrigger *fakes.ManualCronTriggerService @@ -36,7 +41,9 @@ func NewManualTriggerCapabilities(ctx context.Context, lggr logger.Logger, regis return nil, err } - manualHTTPTrigger := fakes.NewManualHTTPTriggerService(lggr) + manualHTTPTrigger := fakes.NewManualHTTPTriggerService(lggr, gateway.Config{ + Port: httpTriggerServerPort, + }) manualHTTPTriggerServer := httptrigger.NewHTTPServer(manualHTTPTrigger) if err := registry.Add(ctx, manualHTTPTriggerServer); err != nil { return nil, err diff --git a/cmd/workflow/simulate/simulate.go b/cmd/workflow/simulate/simulate.go index fe95ed2b..2c64ec6c 100644 --- a/cmd/workflow/simulate/simulate.go +++ b/cmd/workflow/simulate/simulate.go @@ -60,7 +60,7 @@ type Inputs struct { // Non-interactive mode options NonInteractive bool `validate:"-"` TriggerIndex int `validate:"-"` - HTTPPayload string `validate:"-"` // JSON string or @/path/to/file.json + HTTPPayload string `validate:"-"` // JSON string or /path/to/file.json ChainTypeInputs map[string]string `validate:"-"` // CLI-supplied chain-type-specific trigger inputs // Limits enforcement LimitsPath string `validate:"-"` // "default" or path to custom limits JSON @@ -103,7 +103,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { simulateCmd.MarkFlagsMutuallyExclusive("config", "no-config", "default-config") // Non-interactive trigger selection flags simulateCmd.Flags().Int("trigger-index", -1, "Index of the trigger to run (0-based)") - simulateCmd.Flags().String("http-payload", "", "HTTP trigger payload as JSON string or path to JSON file (with or without @ prefix)") + simulateCmd.Flags().String("http-payload", "", "HTTP trigger payload as JSON string or path to JSON file") // Register chain-type-specific CLI flags (e.g., --evm-tx-hash). chain.RegisterAllCLIFlags(simulateCmd) @@ -733,11 +733,21 @@ func makeBeforeStartInteractive(holder *TriggerInfoAndBeforeStart, inputs Inputs return manualTriggerCaps.ManualCronTrigger.ManualTrigger(ctx, triggerRegistrationID, skipWaitSignal) } case "http-trigger@1.0.0-alpha": - payload, err := getHTTPTriggerPayload(inputs.InvocationDir) + payload, err := getHTTPTriggerPayloadFromInput(inputs.InvocationDir, inputs.HTTPPayload) if err != nil { ui.Error(fmt.Sprintf("Failed to get HTTP trigger payload: %v", err)) os.Exit(1) } + if payload == nil { + ui.Line() + ui.Step("No input detected for http-trigger. Supply the payload using one of:") + ui.Dim("1. POST JSON to the local trigger server:") + ui.Dim(fmt.Sprintf(` listening at http://localhost:%d`, httpTriggerServerPort)) + ui.Dim("2. Re-run with --http-payload flag:") + ui.Dim(` --http-payload '{"key":"value"}' (inline JSON)`) + ui.Dim(` --http-payload ./payload.json (path to a JSON file)`) + ui.Line() + } holder.TriggerFunc = func() error { return manualTriggerCaps.ManualHTTPTrigger.ManualTrigger(ctx, triggerRegistrationID, payload) } @@ -816,7 +826,7 @@ func makeBeforeStartNonInteractive(holder *TriggerInfoAndBeforeStart, inputs Inp ui.Error("--http-payload is required for http-trigger@1.0.0-alpha in non-interactive mode") os.Exit(1) } - payload, err := getHTTPTriggerPayloadFromInput(inputs.HTTPPayload, inputs.InvocationDir) + payload, err := getHTTPTriggerPayloadFromInput(inputs.InvocationDir, inputs.HTTPPayload) if err != nil { ui.Error(fmt.Sprintf("Failed to parse HTTP trigger payload: %v", err)) os.Exit(1) @@ -890,22 +900,13 @@ func cleanupBeholder() error { return nil } -// getHTTPTriggerPayload prompts user for HTTP trigger data. Relative paths are +// getHTTPTriggerPayloadFromInput prompts user for HTTP trigger data. Relative paths are // resolved against invocationDir so file references work from where the user ran // the command even after SetExecutionContext switches cwd to the workflow dir. -func getHTTPTriggerPayload(invocationDir string) (*httptypedapi.Payload, error) { - ui.Line() - input, err := ui.Input("HTTP Trigger Configuration", - ui.WithInputDescription("Enter a file path or JSON directly for the HTTP trigger"), - ui.WithPlaceholder(`{"key": "value"} or ./payload.json`), - ) - if err != nil { - return nil, fmt.Errorf("HTTP trigger input cancelled: %w", err) - } - +func getHTTPTriggerPayloadFromInput(invocationDir, input string) (*httptypedapi.Payload, error) { input = strings.TrimSpace(input) if input == "" { - return nil, fmt.Errorf("empty input provided") + return nil, nil } var jsonData map[string]interface{} @@ -924,12 +925,14 @@ func getHTTPTriggerPayload(invocationDir string) (*httptypedapi.Payload, error) return nil, fmt.Errorf("failed to parse JSON from file %s: %w", resolvedPath, err) } ui.Success(fmt.Sprintf("Loaded JSON from file: %s", resolvedPath)) - } else { + } else if strings.HasPrefix(input, "{") { // Treat as direct JSON input if err := json.Unmarshal([]byte(input), &jsonData); err != nil { return nil, fmt.Errorf("failed to parse JSON: %w", err) } ui.Success("Parsed JSON input successfully") + } else { + return nil, fmt.Errorf("invalid JSON input: %s", input) } jsonDataBytes, err := json.Marshal(jsonData) @@ -964,36 +967,3 @@ func resolvePathFromInvocation(path, invocationDir string) string { } return filepath.Join(invocationDir, path) } - -// getHTTPTriggerPayloadFromInput builds an HTTP trigger payload from a JSON string or a file path -// (optionally prefixed with '@'). invocationDir is used to resolve relative paths against the -// directory where the user invoked the CLI rather than the current working directory. -func getHTTPTriggerPayloadFromInput(input, invocationDir string) (*httptypedapi.Payload, error) { - trimmed := strings.TrimSpace(input) - if trimmed == "" { - return nil, fmt.Errorf("empty http payload input") - } - - var raw []byte - if strings.HasPrefix(trimmed, "@") { - path := resolvePathFromInvocation(strings.TrimPrefix(trimmed, "@"), invocationDir) - data, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("failed to read file %s: %w", path, err) - } - raw = data - } else { - resolvedPath := resolvePathFromInvocation(trimmed, invocationDir) - if _, err := os.Stat(resolvedPath); err == nil { - data, err := os.ReadFile(resolvedPath) - if err != nil { - return nil, fmt.Errorf("failed to read file %s: %w", resolvedPath, err) - } - raw = data - } else { - raw = []byte(trimmed) - } - } - - return &httptypedapi.Payload{Input: raw}, nil -} diff --git a/cmd/workflow/simulate/simulate_test.go b/cmd/workflow/simulate/simulate_test.go index 4879b8f3..2ee833cb 100644 --- a/cmd/workflow/simulate/simulate_test.go +++ b/cmd/workflow/simulate/simulate_test.go @@ -3,6 +3,7 @@ package simulate import ( "context" "encoding/base64" + "encoding/json" "fmt" "io" "os" @@ -351,79 +352,73 @@ func TestGetHTTPTriggerPayloadFromInput(t *testing.T) { payloadFile := filepath.Join(tmpDir, "payload.json") require.NoError(t, os.WriteFile(payloadFile, []byte(payloadJSON), 0600)) - t.Run("empty input returns error", func(t *testing.T) { + t.Run("empty input returns nil payload and no error", func(t *testing.T) { t.Parallel() - _, err := getHTTPTriggerPayloadFromInput("", "") - require.Error(t, err) - assert.Contains(t, err.Error(), "empty http payload input") - }) - - t.Run("whitespace-only input returns error", func(t *testing.T) { - t.Parallel() - _, err := getHTTPTriggerPayloadFromInput(" ", "") - require.Error(t, err) - assert.Contains(t, err.Error(), "empty http payload input") - }) - - t.Run("at-prefix with absolute file path reads file", func(t *testing.T) { - t.Parallel() - payload, err := getHTTPTriggerPayloadFromInput("@"+payloadFile, "") + payload, err := getHTTPTriggerPayloadFromInput("", "") require.NoError(t, err) - assert.Equal(t, []byte(payloadJSON), payload.Input) + require.Nil(t, payload) }) - t.Run("at-prefix with relative path resolved against invocationDir", func(t *testing.T) { + t.Run("whitespace-only input returns nil payload and no error", func(t *testing.T) { t.Parallel() - payload, err := getHTTPTriggerPayloadFromInput("@payload.json", tmpDir) + payload, err := getHTTPTriggerPayloadFromInput(" ", " ") require.NoError(t, err) - assert.Equal(t, []byte(payloadJSON), payload.Input) + require.Nil(t, payload) }) - t.Run("at-prefix with nonexistent file returns error", func(t *testing.T) { + t.Run("absolute file path reads and parses JSON", func(t *testing.T) { t.Parallel() - _, err := getHTTPTriggerPayloadFromInput("@/nonexistent/no-such-file.json", "") - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to read file") + payload, err := getHTTPTriggerPayloadFromInput("", payloadFile) + require.NoError(t, err) + require.NotNil(t, payload) + var got map[string]interface{} + require.NoError(t, json.Unmarshal(payload.Input, &got)) + assert.Equal(t, "GET", got["method"]) + assert.Equal(t, "/hello", got["path"]) }) - t.Run("absolute file path without at-prefix reads file", func(t *testing.T) { + t.Run("relative path resolved against invocationDir reads and parses JSON", func(t *testing.T) { t.Parallel() - payload, err := getHTTPTriggerPayloadFromInput(payloadFile, "") + payload, err := getHTTPTriggerPayloadFromInput(tmpDir, "payload.json") require.NoError(t, err) - assert.Equal(t, []byte(payloadJSON), payload.Input) + require.NotNil(t, payload) + var got map[string]interface{} + require.NoError(t, json.Unmarshal(payload.Input, &got)) + assert.Equal(t, "GET", got["method"]) + assert.Equal(t, "/hello", got["path"]) }) - t.Run("relative file path resolved against invocationDir reads file", func(t *testing.T) { + t.Run("nonexistent file path returns invalid JSON error", func(t *testing.T) { t.Parallel() - payload, err := getHTTPTriggerPayloadFromInput("payload.json", tmpDir) - require.NoError(t, err) - assert.Equal(t, []byte(payloadJSON), payload.Input) + _, err := getHTTPTriggerPayloadFromInput("", "/nonexistent/no-such-file.json") + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid JSON input") }) - t.Run("inline JSON string used as raw bytes", func(t *testing.T) { + t.Run("inline JSON string parsed as payload", func(t *testing.T) { t.Parallel() inlineJSON := `{"method":"POST","path":"/api"}` - payload, err := getHTTPTriggerPayloadFromInput(inlineJSON, "") + payload, err := getHTTPTriggerPayloadFromInput("", inlineJSON) require.NoError(t, err) - assert.Equal(t, []byte(inlineJSON), payload.Input) + require.NotNil(t, payload) + var got map[string]interface{} + require.NoError(t, json.Unmarshal(payload.Input, &got)) + assert.Equal(t, "POST", got["method"]) + assert.Equal(t, "/api", got["path"]) }) - t.Run("nonexistent relative path with empty invocationDir treated as raw bytes", func(t *testing.T) { + t.Run("non-JSON non-file input returns error", func(t *testing.T) { t.Parallel() - // A path that doesn't exist is treated as raw bytes (no error). - input := "no-such-file-or-json" - payload, err := getHTTPTriggerPayloadFromInput(input, "") - require.NoError(t, err) - assert.Equal(t, []byte(input), payload.Input) + _, err := getHTTPTriggerPayloadFromInput("", "no-such-file-or-json") + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid JSON input") }) - t.Run("relative path not found in invocationDir treated as raw bytes", func(t *testing.T) { + t.Run("relative path not found in invocationDir returns error", func(t *testing.T) { t.Parallel() - // A relative path that resolves to a nonexistent file is used as raw bytes. - input := "does-not-exist.json" - payload, err := getHTTPTriggerPayloadFromInput(input, tmpDir) - require.NoError(t, err) - assert.Equal(t, []byte(input), payload.Input) + _, err := getHTTPTriggerPayloadFromInput(tmpDir, "does-not-exist.json") + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid JSON input") }) } diff --git a/docs/cre_workflow_simulate.md b/docs/cre_workflow_simulate.md index af5bd1fa..61e7d039 100644 --- a/docs/cre_workflow_simulate.md +++ b/docs/cre_workflow_simulate.md @@ -26,7 +26,7 @@ cre workflow simulate ./my-workflow --evm-event-index int EVM trigger log index (0-based) (default -1) --evm-tx-hash string EVM trigger transaction hash (0x...) -h, --help help for simulate - --http-payload string HTTP trigger payload as JSON string or path to JSON file (with or without @ prefix) + --http-payload string HTTP trigger payload as JSON string or path to JSON file --limits string Production limits to enforce during simulation: 'default' for prod defaults, path to a limits JSON file (e.g. from 'cre workflow limits export'), or 'none' to disable (default "default") --no-config Simulate without a config file --skip-type-checks Skip TypeScript project typecheck during compilation (passes --skip-type-checks to cre-compile) From 9c65e6e71bd7a1ab12bac1001e7cc2262d741ec5 Mon Sep 17 00:00:00 2001 From: Tarcisio Ferraz Date: Thu, 4 Jun 2026 10:48:21 -0300 Subject: [PATCH 2/5] update mods --- go.mod | 33 ++++++++++++++++--------------- go.sum | 62 ++++++++++++++++++++++++++++++---------------------------- 2 files changed, 49 insertions(+), 46 deletions(-) diff --git a/go.mod b/go.mod index a1b4b70d..8cee450c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/smartcontractkit/cre-cli -go 1.26.2 +go 1.26.4 require ( github.com/BurntSushi/toml v1.5.0 @@ -28,15 +28,15 @@ require ( github.com/machinebox/graphql v0.2.2 github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.34.0 - github.com/smartcontractkit/chain-selectors v1.0.100 - github.com/smartcontractkit/chainlink-common v0.11.2-0.20260520194751-11a4f360f4e2 + github.com/smartcontractkit/chain-selectors v1.0.101 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260602101458-208ae6ddea43 github.com/smartcontractkit/chainlink-common/keystore v1.1.0 github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260512150409-b4068bf735e6 - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260522145417-85c85baa73cf - github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260323124644-faea187e6997 + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260526195338-adcf8013a1b7 + github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260528221400-84746b70eeeb github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5 github.com/smartcontractkit/chainlink/deployment v0.0.0-20260521170940-67f9a4b233f8 - github.com/smartcontractkit/chainlink/v2 v2.29.1-cre-beta.0.0.20260521170940-67f9a4b233f8 + github.com/smartcontractkit/chainlink/v2 v2.29.1-cre-beta.0.0.20260604131613-86f8d41557b9 github.com/smartcontractkit/cre-sdk-go v1.11.0 github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm v1.0.0-beta.12 github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana v0.1.0-beta.1 @@ -150,7 +150,7 @@ require ( github.com/digital-asset/dazl-client/v8 v8.9.0 // indirect github.com/docker/go-connections v0.7.0 // indirect github.com/dominikbraun/graph v0.23.0 // indirect - github.com/doyensec/safeurl v0.2.1 // indirect + github.com/doyensec/safeurl v0.2.3 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.7.0 // indirect github.com/emicklei/dot v1.6.2 // indirect @@ -310,15 +310,15 @@ require ( github.com/smartcontractkit/chainlink-automation v0.8.1 // indirect github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260415165642-49f23e4d76cc // indirect github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260511195239-0f6e1b177fc7 // indirect - github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260520194751-11a4f360f4e2 // indirect + github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260601211238-9f526774fef0 // indirect github.com/smartcontractkit/chainlink-data-streams v0.1.15-0.20260522094612-5f9f748bd87a // indirect github.com/smartcontractkit/chainlink-deployments-framework v0.105.0 // indirect - github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260521145337-fdf89453516c // indirect + github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260527175653-b78bae59d823 // indirect github.com/smartcontractkit/chainlink-evm/contracts/cre/gobindings v0.0.0-20260403151002-2c91155b5501 // indirect github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c // indirect github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c // indirect - github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260508154216-3ed6f623098f // indirect - github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4 // indirect + github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260521164805-26d78d5e1243 // indirect + github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243 // indirect github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 // indirect github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20260512230622-65f10f4cd305 // indirect @@ -326,8 +326,8 @@ require ( github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260331131315-f08a616d8dcd // indirect github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0 // indirect github.com/smartcontractkit/chainlink-protos/svr v1.2.0 // indirect - github.com/smartcontractkit/chainlink-sui v0.0.0-20260429183453-39df0198aed8 // indirect - github.com/smartcontractkit/chainlink-ton v1.0.5-0.20260520103847-15ca4de9dba9 // indirect + github.com/smartcontractkit/chainlink-sui v0.0.0-20260527160341-aa3adc0abf67 // indirect + github.com/smartcontractkit/chainlink-ton v1.0.5-0.20260601214705-1ab0adfd785f // indirect github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20260408092456-3c6369888d4a // indirect github.com/smartcontractkit/cld-changesets v0.4.0 // indirect github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad // indirect @@ -373,6 +373,7 @@ require ( go.etcd.io/bbolt v1.4.2 // indirect go.mongodb.org/mongo-driver v1.17.9 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect @@ -400,12 +401,12 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect golang.org/x/arch v0.11.0 // indirect - golang.org/x/crypto v0.51.0 // indirect + golang.org/x/crypto v0.52.0 // indirect golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect - golang.org/x/net v0.54.0 // indirect + golang.org/x/net v0.55.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.44.0 // indirect + golang.org/x/sys v0.45.0 // indirect golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.45.0 // indirect diff --git a/go.sum b/go.sum index 798e43aa..262d4f69 100644 --- a/go.sum +++ b/go.sum @@ -380,8 +380,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= -github.com/doyensec/safeurl v0.2.1 h1:DY15JorEfQsnpBWhBkVQIkaif2jfxCC14PIuGDsjDVs= -github.com/doyensec/safeurl v0.2.1/go.mod h1:wzSXqC/6Z410qHz23jtBWT+wQ8yTxcY0p8bZH/4EZIg= +github.com/doyensec/safeurl v0.2.3 h1:KJZHxTUMI17yUSy5umKmDLtzYBUxN6MkdSIyRI81DvY= +github.com/doyensec/safeurl v0.2.3/go.mod h1:3H0cgRpPYPSpgxRRn5yGD35Ns/LgGX/BVWSBbzUqXtY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dvsekhvalnov/jose2go v1.7.0 h1:bnQc8+GMnidJZA8zc6lLEAb4xNrIqHwO+9TzqvtQZPo= @@ -1142,8 +1142,8 @@ github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9LsA7vTMPv+0n7ClhSFnZFAk= github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= -github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= -github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= +github.com/smartcontractkit/chain-selectors v1.0.101 h1:TF4ma9h3QeyIZ8XoEmgI5lrUvZfzHAz8tfR0pV0+GCA= +github.com/smartcontractkit/chain-selectors v1.0.101/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb h1:6UjHnVanvb+6yJuefhyVlfv6YKFGMeZY5jv+a7Sexyo= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260507123701-77fc93b573bb/go.mod h1:FEm5fvIQe5O8Qdx6GvQcXsk7rDFpmYdIWXea5i4tpjw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= @@ -1160,18 +1160,18 @@ github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260 github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260511195239-0f6e1b177fc7/go.mod h1:67YbnoglYD61Pz/jTVCgav9wFq7S35OU8UyQSvPllRw= github.com/smartcontractkit/chainlink-ccv v0.0.2-0.20260428133800-3b1484e8b1fd h1:IMopuENFVS63AerRELdfWo6o60UNUidcldJOxJLmk24= github.com/smartcontractkit/chainlink-ccv v0.0.2-0.20260428133800-3b1484e8b1fd/go.mod h1:SBN8Urnh5sQvrQRbSo1Nr8coWatHg8LZoPw3R/42sho= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260520194751-11a4f360f4e2 h1:Ne11+eg/uuVJ5duEfr4ec+1EoeZt/dHS9IFIGDdTr00= -github.com/smartcontractkit/chainlink-common v0.11.2-0.20260520194751-11a4f360f4e2/go.mod h1:Pu4czYGiGRAJo+a1M3ZXY+wEyItMe9wtJCVp0pBgAzg= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260602101458-208ae6ddea43 h1:SsdcAiY6/MDPV93o2P6JRaooIA088OZFTT3ohOKSS3U= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260602101458-208ae6ddea43/go.mod h1:6jgqiFXFJHqjkvFFmuf8gvoUFa6Ygx/D1tKnIL+CCF8= github.com/smartcontractkit/chainlink-common/keystore v1.1.0 h1:2wzySccgk2fpWusPKO0bpeAZzfSU9eq6CS5U+JwYaVo= github.com/smartcontractkit/chainlink-common/keystore v1.1.0/go.mod h1:6JexOOhPhknQ0QMuppFIlOpm6wCp54yZMxai+tWugwY= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260520194751-11a4f360f4e2 h1:22H/CQy2L1unVJ2KEViEqvM8evJLSIqJxEdfDeXB4o8= -github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260520194751-11a4f360f4e2/go.mod h1:HmUyH2oD9m+GRpKq7q3vuRnm1F2Uczf/Nd1v3ipMSK8= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260601211238-9f526774fef0 h1:NExKM/D0HneOq/N5LGTbkV4VOa0UHCvfTNEb4GqYpto= +github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20260601211238-9f526774fef0/go.mod h1:HmUyH2oD9m+GRpKq7q3vuRnm1F2Uczf/Nd1v3ipMSK8= github.com/smartcontractkit/chainlink-data-streams v0.1.15-0.20260522094612-5f9f748bd87a h1:8bIqv4r7SgDWkXL2Qz/Ijw+YjZY1uroIte3E2v2keVk= github.com/smartcontractkit/chainlink-data-streams v0.1.15-0.20260522094612-5f9f748bd87a/go.mod h1:dF5JiHWueHjYguUUUrFeb03MkcDqha/tssEkqTkgzp4= github.com/smartcontractkit/chainlink-deployments-framework v0.105.0 h1:Vp4EwkvxcBzgahIZdbWyCExDXLha93cS63xvwd2xwx8= github.com/smartcontractkit/chainlink-deployments-framework v0.105.0/go.mod h1:xFLOOpIz7vqqno4YngHZlF9MKqk8rnvQa9adVElUXaE= -github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260521145337-fdf89453516c h1:Udi8eopVcBCcr3+M3Jzbe3SksQdl1HxMaBMvD1XJh90= -github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260521145337-fdf89453516c/go.mod h1:pTPpE5TQD0re8MJ9mWx5VjmXXPXF/2ZYAdS5KcBcc/c= +github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260527175653-b78bae59d823 h1:TsRkbimQWtBnDqT5IdUXuy9kS4NU3xN54CpaLeRNHLw= +github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260527175653-b78bae59d823/go.mod h1:3ifVi4ueXLo5+pSVpvmaJBbCYhTFVx9qxp6bIU11QQc= github.com/smartcontractkit/chainlink-evm/contracts/cre/gobindings v0.0.0-20260403151002-2c91155b5501 h1:QJiXTG9CmaQAuMRn5JGi+Jhji7fSkehVnKpjc8oNJJY= github.com/smartcontractkit/chainlink-evm/contracts/cre/gobindings v0.0.0-20260403151002-2c91155b5501/go.mod h1:4cT1BeNF8DAn6In9zr3LayVCv1KzFeuxT7zcuNkfIb0= github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260512150409-b4068bf735e6 h1:JFo7C3FilwhfwGBLAyj2umbL+P4QxGmVi/b8yt9kqvI= @@ -1182,10 +1182,10 @@ github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-202604231355 github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:HcwehCao5k5C2NGuKJUVoX/AYtoH6njGFiV44dBOcY4= github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c h1:0c+bCKo47vy/ItRtGa3S/vCpE5LRlgXpGnVKQX8TgjE= github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260423135514-5b1a7565a99c/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260508154216-3ed6f623098f h1:C6eGGsdTVyB3mtz1VF/o9Znshwrr/VkKsLYGinOVM/k= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260508154216-3ed6f623098f/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= -github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4 h1:nXU0s4WAVU2cAR76Ke7h9z55NuEtRq1WvT4wVEs7jwk= -github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260505202410-b350dca113b4/go.mod h1:7ketk4ischPQW/JQgmyHz6zdzLUJv1VC29SiSgosydQ= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260521164805-26d78d5e1243 h1:vaFBupfFfImQgqOeuC7Muk2GflbYP6Gpi0Y/TLroFU8= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260521164805-26d78d5e1243/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= +github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243 h1:71PGTkjdFZ0JrloEC2Fs8eHl1b1gmUuH+bq7q23usKk= +github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260521164805-26d78d5e1243/go.mod h1:7ketk4ischPQW/JQgmyHz6zdzLUJv1VC29SiSgosydQ= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/committee-verifier v0.0.0-20251211142334-5c3421fe2c8d h1:VYoBBNnQpZ5p+enPTl8SkKBRaubqyGpO0ul3B1np++I= @@ -1196,8 +1196,8 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0. github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:ATjAPIVJibHRcIfiG47rEQkUIOoYa6KDvWj3zwCAw6g= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d h1:AJy55QJ/pBhXkZjc7N+ATnWfxrcjq9BI9DmdtdjwDUQ= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260522145417-85c85baa73cf h1:9nKluBQ0GBgnOokB8FCU1dmgZXDh22u9UPPMWFdKaYE= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260522145417-85c85baa73cf/go.mod h1:vTFHTCbLui4Vn8fTmAadfE3rdnvfrDwOmMujmW857D0= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260526195338-adcf8013a1b7 h1:iljEJss3WOwcsMkWy72Yn2zvjw7Gyxc+RXL7r8YKM6g= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260526195338-adcf8013a1b7/go.mod h1:vTFHTCbLui4Vn8fTmAadfE3rdnvfrDwOmMujmW857D0= github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36 h1:SG+wAsNyAcA6Kk19ljuxi3HK9Ll2lpHik8OKoY4x7A0= github.com/smartcontractkit/chainlink-protos/data-feeds v0.1.1-0.20260501174546-2e8846986b36/go.mod h1:vL1bDgPSJjV0EqHYs4dDlR+EEE0cJchgvGLYXhwIjXY= github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY= @@ -1218,24 +1218,24 @@ github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0 h1:B7itmjy+C github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0/go.mod h1:h6kqaGajbNRrezm56zhx03p0mVmmA2xxj7E/M4ytLUA= github.com/smartcontractkit/chainlink-protos/svr v1.2.0 h1:7jjgqRgORQS/ikL3z0ZgJy95pzjhR9LuU1TVWg4BZ78= github.com/smartcontractkit/chainlink-protos/svr v1.2.0/go.mod h1:TcOliTQU6r59DwG4lo3U+mFM9WWyBHGuFkkxQpvSujo= -github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260323124644-faea187e6997 h1:W0HKHO8eE8BckTRnhSdqjHKbJcnk068nEWYnWRu6tJY= -github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260323124644-faea187e6997/go.mod h1:GTpDgyK0OObf7jpch6p8N281KxN92wbB8serZhU9yRc= -github.com/smartcontractkit/chainlink-sui v0.0.0-20260429183453-39df0198aed8 h1:sWpTYRucOQQ/wXbKj52UE59JMMEq2Aq5g+sMdjYzfRM= -github.com/smartcontractkit/chainlink-sui v0.0.0-20260429183453-39df0198aed8/go.mod h1:k1HSbHyPaQWPOj6lXDIAe04EuwbC5ge1nK+cpG2E8hE= +github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260528221400-84746b70eeeb h1:mlN8zK1UzDIBYtKSILQ4gci9MFwo42QFtGV1tWddMyk= +github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260528221400-84746b70eeeb/go.mod h1:GTpDgyK0OObf7jpch6p8N281KxN92wbB8serZhU9yRc= +github.com/smartcontractkit/chainlink-sui v0.0.0-20260527160341-aa3adc0abf67 h1:NNvPOgvf5vbOYVLxLST+5E88iPOAnpmzZGPihEx8DFc= +github.com/smartcontractkit/chainlink-sui v0.0.0-20260527160341-aa3adc0abf67/go.mod h1:k1HSbHyPaQWPOj6lXDIAe04EuwbC5ge1nK+cpG2E8hE= github.com/smartcontractkit/chainlink-testing-framework/framework v0.16.1 h1:wZd5hIQRcQaq3FgW1lg/4ilk68Id6cxYKFNU9iTnugs= github.com/smartcontractkit/chainlink-testing-framework/framework v0.16.1/go.mod h1:wxgGfrJpzIdC1wyMJEGOfN4H4yPQTZD/DdrMRBxA0io= github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5 h1:RwZXxdIAOyjp6cwc9Quxgr38k8r7ACz+Lxh9o/A6oH0= github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5/go.mod h1:kHYJnZUqiPF7/xN5273prV+srrLJkS77GbBXHLKQpx0= -github.com/smartcontractkit/chainlink-ton v1.0.5-0.20260520103847-15ca4de9dba9 h1:KFu4Hj88Bx7hftWpDnam8TcdYHX8ga1oW5aT7SfP4CQ= -github.com/smartcontractkit/chainlink-ton v1.0.5-0.20260520103847-15ca4de9dba9/go.mod h1:4e/rmLNsaA39KZYQ9BvBbyf2fMsYtf7Da/FX9YEwgtw= +github.com/smartcontractkit/chainlink-ton v1.0.5-0.20260601214705-1ab0adfd785f h1:v6cRPj4xB05vMID3awiLWNiVPtCRAmiovPdagHXpCjE= +github.com/smartcontractkit/chainlink-ton v1.0.5-0.20260601214705-1ab0adfd785f/go.mod h1:4e/rmLNsaA39KZYQ9BvBbyf2fMsYtf7Da/FX9YEwgtw= github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20260408092456-3c6369888d4a h1:Xu8iBnBQEibWIXTCwKYf8okXjFtzJ0KochjL03h+T40= github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20260408092456-3c6369888d4a/go.mod h1:1eaXR+Fe6TlpP+CKXozfYlFM8QgN/N5C7OMvTRWNT8I= github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014143056-a0c6328c91e9 h1:/Q1gD5gI0glBMztVH9XUVci3aOy8h+qTDV6o42MsqMM= github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014143056-a0c6328c91e9/go.mod h1:ea1LESxlSSOgc2zZBqf1RTkXTMthHaspdqUHd7W4lF0= github.com/smartcontractkit/chainlink/deployment v0.0.0-20260521170940-67f9a4b233f8 h1:xPDpOmxTlT2RW+pPUElGa0/y02V/MAHIPD8DEtEBLfE= github.com/smartcontractkit/chainlink/deployment v0.0.0-20260521170940-67f9a4b233f8/go.mod h1:WL7W/YQO5pQ1Nexm4lvd/SztM2OzbhaIhJKyrfMU8QQ= -github.com/smartcontractkit/chainlink/v2 v2.29.1-cre-beta.0.0.20260521170940-67f9a4b233f8 h1:naZxYMHsgi+JVChySLtaqAfgRM3Az2+HpvIzWNFcPRo= -github.com/smartcontractkit/chainlink/v2 v2.29.1-cre-beta.0.0.20260521170940-67f9a4b233f8/go.mod h1:8Ppr/wQrc9GVwQow3Tw2JsNjNjqMhZTyU32cm5OFMzo= +github.com/smartcontractkit/chainlink/v2 v2.29.1-cre-beta.0.0.20260604131613-86f8d41557b9 h1:vutlOpTlnDcV7YMheZQ/fz7RTPcsOpLiE6QNAxSahoo= +github.com/smartcontractkit/chainlink/v2 v2.29.1-cre-beta.0.0.20260604131613-86f8d41557b9/go.mod h1:+K9jjB9i1VStV17o5WRUFiH9aPfQ5zAcvI9VgbOPiak= github.com/smartcontractkit/cld-changesets v0.4.0 h1:S6yNRj6FssyyKbxLHTbC9X9U4qsph17xiiBBT6DGyNE= github.com/smartcontractkit/cld-changesets v0.4.0/go.mod h1:4wOfnbSP8Ior/75QWLbtDntamSA/81SYcXzctBSx9CY= github.com/smartcontractkit/cre-sdk-go v1.11.0 h1:E3MG0j8O9qDv6lDz71HPD3/WRKh/PX2/hfxO1+9YL2w= @@ -1412,6 +1412,8 @@ go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5 go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 h1:w3zlHYETbDwXyWHZlyyR58ZC39XGi8rAhkBgUgJ9d5w= +go.opentelemetry.io/contrib/bridges/prometheus v0.68.0/go.mod h1:GR/mClR2nn7vE8RLwxKjoBNg+QtgdDhRzxVa93koy5o= go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.49.0 h1:1f31+6grJmV3X4lxcEvUy13i5/kfDw1nJZwhd8mA4tg= go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.49.0/go.mod h1:1P/02zM3OwkX9uki+Wmxw3a5GVb6KUXRsa7m7bOC9Fg= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= @@ -1514,8 +1516,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.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw= golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= @@ -1567,8 +1569,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= -golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1647,8 +1649,8 @@ 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.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= 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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= From 8d6dc0fb3d29aa84c9c7ea2d3f45336bf6ced603 Mon Sep 17 00:00:00 2001 From: Tarcisio Ferraz Date: Thu, 4 Jun 2026 11:00:21 -0300 Subject: [PATCH 3/5] update docs --- cmd/workflow/simulate/capabilities.go | 2 +- cmd/workflow/simulate/simulate.go | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/workflow/simulate/capabilities.go b/cmd/workflow/simulate/capabilities.go index dbe37de3..ee5b516e 100644 --- a/cmd/workflow/simulate/capabilities.go +++ b/cmd/workflow/simulate/capabilities.go @@ -21,7 +21,7 @@ import ( // httpTriggerServerPort is the port on which the local HTTP server listens // when no --http-payload flag is supplied and the user chooses to POST the payload. -const httpTriggerServerPort = 9090 +const httpTriggerServerPort = 2000 // ManualTriggers holds chain-agnostic trigger services used in simulation. type ManualTriggers struct { diff --git a/cmd/workflow/simulate/simulate.go b/cmd/workflow/simulate/simulate.go index 2c64ec6c..b7d815cb 100644 --- a/cmd/workflow/simulate/simulate.go +++ b/cmd/workflow/simulate/simulate.go @@ -741,8 +741,10 @@ func makeBeforeStartInteractive(holder *TriggerInfoAndBeforeStart, inputs Inputs if payload == nil { ui.Line() ui.Step("No input detected for http-trigger. Supply the payload using one of:") - ui.Dim("1. POST JSON to the local trigger server:") - ui.Dim(fmt.Sprintf(` listening at http://localhost:%d`, httpTriggerServerPort)) + ui.Dim("1. POST JSON to the local trigger server, example:") + ui.Dim(fmt.Sprintf(` curl -X POST http://localhost:%d/trigger \`, httpTriggerServerPort)) + ui.Dim(" -H 'Content-Type: application/json' \\") + ui.Dim(" -d '{\"input\":{\"key\":\"value\"}}'") ui.Dim("2. Re-run with --http-payload flag:") ui.Dim(` --http-payload '{"key":"value"}' (inline JSON)`) ui.Dim(` --http-payload ./payload.json (path to a JSON file)`) From 06cabca9b484117dbe72416c5482a469cb58cfac Mon Sep 17 00:00:00 2001 From: Tarcisio Ferraz Date: Mon, 8 Jun 2026 17:39:01 -0300 Subject: [PATCH 4/5] Add waiting prompt for interactive HTTP trigger --- cmd/workflow/simulate/simulate.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/workflow/simulate/simulate.go b/cmd/workflow/simulate/simulate.go index b7d815cb..19494293 100644 --- a/cmd/workflow/simulate/simulate.go +++ b/cmd/workflow/simulate/simulate.go @@ -751,6 +751,9 @@ func makeBeforeStartInteractive(holder *TriggerInfoAndBeforeStart, inputs Inputs ui.Line() } holder.TriggerFunc = func() error { + if payload == nil { + ui.Step(fmt.Sprintf("Waiting for HTTP request to start execution (listening on http://localhost:%d/trigger)...", httpTriggerServerPort)) + } return manualTriggerCaps.ManualHTTPTrigger.ManualTrigger(ctx, triggerRegistrationID, payload) } default: From 688c0229d6bd8b27842d457031c0f25b06f398eb Mon Sep 17 00:00:00 2001 From: Tarcisio Ferraz Date: Tue, 9 Jun 2026 10:20:05 -0300 Subject: [PATCH 5/5] keep alive --- cmd/workflow/simulate/capabilities.go | 7 +- cmd/workflow/simulate/manual_http_trigger.go | 170 +++++++++++++ cmd/workflow/simulate/simulate.go | 241 +++++++++++++++++-- cmd/workflow/simulate/simulate_test.go | 60 +++++ docs/cre_workflow_simulate.md | 1 + 5 files changed, 454 insertions(+), 25 deletions(-) create mode 100644 cmd/workflow/simulate/manual_http_trigger.go diff --git a/cmd/workflow/simulate/capabilities.go b/cmd/workflow/simulate/capabilities.go index ee5b516e..87c8a220 100644 --- a/cmd/workflow/simulate/capabilities.go +++ b/cmd/workflow/simulate/capabilities.go @@ -16,7 +16,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink/v2/core/capabilities" "github.com/smartcontractkit/chainlink/v2/core/capabilities/fakes" - "github.com/smartcontractkit/chainlink/v2/core/capabilities/fakes/gateway" ) // httpTriggerServerPort is the port on which the local HTTP server listens @@ -26,7 +25,7 @@ const httpTriggerServerPort = 2000 // ManualTriggers holds chain-agnostic trigger services used in simulation. type ManualTriggers struct { ManualCronTrigger *fakes.ManualCronTriggerService - ManualHTTPTrigger *fakes.ManualHTTPTriggerService + ManualHTTPTrigger *ManualHTTPTriggerService } // NewManualTriggerCapabilities creates and registers cron and HTTP trigger capabilities. @@ -41,9 +40,7 @@ func NewManualTriggerCapabilities(ctx context.Context, lggr logger.Logger, regis return nil, err } - manualHTTPTrigger := fakes.NewManualHTTPTriggerService(lggr, gateway.Config{ - Port: httpTriggerServerPort, - }) + manualHTTPTrigger := NewManualHTTPTriggerService(lggr) manualHTTPTriggerServer := httptrigger.NewHTTPServer(manualHTTPTrigger) if err := registry.Add(ctx, manualHTTPTriggerServer); err != nil { return nil, err diff --git a/cmd/workflow/simulate/manual_http_trigger.go b/cmd/workflow/simulate/manual_http_trigger.go new file mode 100644 index 00000000..e76d03e5 --- /dev/null +++ b/cmd/workflow/simulate/manual_http_trigger.go @@ -0,0 +1,170 @@ +package simulate + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" + httptypedapi "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/triggers/http" + httpserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/triggers/http/server" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" + "github.com/smartcontractkit/chainlink/v2/core/services/workflows/events" +) + +var _ services.Service = (*ManualHTTPTriggerService)(nil) +var _ httpserver.HTTPCapability = (*ManualHTTPTriggerService)(nil) + +const manualHTTPTriggerServiceName = "HttpTriggerService" +const manualHTTPTriggerID = "http-trigger@1.0.0-alpha" + +var manualHTTPTriggerInfo = capabilities.MustNewCapabilityInfo( + manualHTTPTriggerID, + capabilities.CapabilityTypeTrigger, + "A trigger that uses an HTTP request to run a workflow.", +) + +type ManualHTTPTriggerService struct { + capabilities.CapabilityInfo + + lggr logger.Logger + + mu sync.RWMutex + callbackCh map[string]chan capabilities.TriggerAndId[*httptypedapi.Payload] + workflowIDs map[string]string + inputs map[string]*httptypedapi.Config + eventSeq uint64 +} + +func NewManualHTTPTriggerService(parentLggr logger.Logger) *ManualHTTPTriggerService { + return &ManualHTTPTriggerService{ + CapabilityInfo: manualHTTPTriggerInfo, + lggr: logger.Named(parentLggr, "HTTPTriggerService"), + callbackCh: make(map[string]chan capabilities.TriggerAndId[*httptypedapi.Payload]), + workflowIDs: make(map[string]string), + inputs: make(map[string]*httptypedapi.Config), + } +} + +func (f *ManualHTTPTriggerService) RegisterTrigger(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *httptypedapi.Config) (<-chan capabilities.TriggerAndId[*httptypedapi.Payload], caperrors.Error) { + f.mu.Lock() + defer f.mu.Unlock() + + f.inputs[triggerID] = input + f.callbackCh[triggerID] = make(chan capabilities.TriggerAndId[*httptypedapi.Payload], 1) + f.workflowIDs[triggerID] = metadata.WorkflowID + return f.callbackCh[triggerID], nil +} + +func (f *ManualHTTPTriggerService) UnregisterTrigger(ctx context.Context, triggerID string, metadata capabilities.RequestMetadata, input *httptypedapi.Config) caperrors.Error { + return nil +} + +func (f *ManualHTTPTriggerService) AckEvent(ctx context.Context, triggerID string, eventID string, method string) caperrors.Error { + return nil +} + +func (f *ManualHTTPTriggerService) Initialise(ctx context.Context, dependencies core.StandardCapabilitiesDependencies) error { + f.lggr.Debugf("Initialising %s", manualHTTPTriggerServiceName) + return f.Start(ctx) +} + +func (f *ManualHTTPTriggerService) ManualTrigger(ctx context.Context, triggerID string, payload *httptypedapi.Payload) error { + f.mu.RLock() + workflowID, workflowExists := f.workflowIDs[triggerID] + input := f.inputs[triggerID] + callbackCh := f.callbackCh[triggerID] + f.mu.RUnlock() + + if !workflowExists { + f.lggr.Errorw("workflowID not found for triggerID", "triggerID", triggerID) + workflowID = "unknownWorkflow" + } + if input == nil { + f.lggr.Errorw("input is nil or not found for triggerID", "triggerID", triggerID) + return fmt.Errorf("input not found for triggerID") + } + if callbackCh == nil { + return fmt.Errorf("callback channel not found for triggerID") + } + + if payload == nil { + var err error + payload, err = f.listenForTriggerPayload(ctx) + if err != nil { + return fmt.Errorf("gateway: %w", err) + } + } + + triggerEvent := f.createManualTriggerEvent(payload) + workflowExecutionID, err := events.GenerateExecutionID(workflowID, triggerEvent.Id) + if err != nil { + f.lggr.Errorw("failed to generate execution ID", "err", err) + workflowExecutionID = "" + } + if err := events.EmitTriggerExecutionStarted(ctx, map[string]string{}, triggerEvent.Id, workflowExecutionID); err != nil { + f.lggr.Errorw("failed to emit trigger execution started event", "err", err) + } + + select { + case callbackCh <- triggerEvent: + return nil + case <-ctx.Done(): + f.lggr.Debug("ManualTrigger cancelled due to context cancellation") + return ctx.Err() + } +} + +func (f *ManualHTTPTriggerService) listenForTriggerPayload(ctx context.Context) (*httptypedapi.Payload, error) { + payloadCh, closeServer, err := startHTTPKeepAlivePayloadServer(ctx, httpTriggerServerPort) + if err != nil { + return nil, err + } + defer closeServer() + + select { + case payload := <-payloadCh: + return payload, nil + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +func (f *ManualHTTPTriggerService) createManualTriggerEvent(payload *httptypedapi.Payload) capabilities.TriggerAndId[*httptypedapi.Payload] { + seq := atomic.AddUint64(&f.eventSeq, 1) + return capabilities.TriggerAndId[*httptypedapi.Payload]{ + Trigger: payload, + Id: fmt.Sprintf("manual-http-trigger-%d-%d", time.Now().UnixNano(), seq), + } +} + +func (f *ManualHTTPTriggerService) Start(ctx context.Context) error { + f.lggr.Debug("Starting HTTP Trigger Capability") + return nil +} + +func (f *ManualHTTPTriggerService) Close() error { + f.lggr.Debug("Closing HTTP Trigger Capability") + return nil +} + +func (f *ManualHTTPTriggerService) HealthReport() map[string]error { + return map[string]error{f.Name(): nil} +} + +func (f *ManualHTTPTriggerService) Name() string { + return f.lggr.Name() +} + +func (f *ManualHTTPTriggerService) Description() string { + return "Manual HTTP Trigger Service" +} + +func (f *ManualHTTPTriggerService) Ready() error { + return nil +} diff --git a/cmd/workflow/simulate/simulate.go b/cmd/workflow/simulate/simulate.go index 19494293..999cb17d 100644 --- a/cmd/workflow/simulate/simulate.go +++ b/cmd/workflow/simulate/simulate.go @@ -5,6 +5,9 @@ import ( "encoding/json" "errors" "fmt" + "io" + "net" + "net/http" "os" "os/signal" "path/filepath" @@ -62,6 +65,9 @@ type Inputs struct { TriggerIndex int `validate:"-"` HTTPPayload string `validate:"-"` // JSON string or /path/to/file.json ChainTypeInputs map[string]string `validate:"-"` // CLI-supplied chain-type-specific trigger inputs + // KeepAlive keeps the HTTP trigger server running after each execution so it can + // process additional requests until the user interrupts (ctrl-C). + KeepAlive bool `validate:"-"` // Limits enforcement LimitsPath string `validate:"-"` // "default" or path to custom limits JSON // SkipTypeChecks passes --skip-type-checks to cre-compile for TypeScript workflows. @@ -104,6 +110,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { // Non-interactive trigger selection flags simulateCmd.Flags().Int("trigger-index", -1, "Index of the trigger to run (0-based)") simulateCmd.Flags().String("http-payload", "", "HTTP trigger payload as JSON string or path to JSON file") + simulateCmd.Flags().Bool("keep-alive", false, "Keep the simulator running after each execution and accept additional HTTP trigger requests (http-trigger only)") // Register chain-type-specific CLI flags (e.g., --evm-tx-hash). chain.RegisterAllCLIFlags(simulateCmd) @@ -197,6 +204,7 @@ func (h *handler) ResolveInputs(v *viper.Viper, creSettings *settings.Settings) TriggerIndex: v.GetInt("trigger-index"), HTTPPayload: v.GetString("http-payload"), ChainTypeInputs: chain.CollectAllCLIInputs(v), + KeepAlive: v.GetBool("keep-alive"), LimitsPath: v.GetString("limits"), SkipTypeChecks: v.GetBool(cmdcommon.SkipTypeChecksCLIFlag), InvocationDir: h.runtimeContext.InvocationDir, @@ -429,9 +437,10 @@ func run( return fmt.Errorf("failed to create engine logger: %w", err) } - // Channels to coordinate blocking + // Channels to coordinate blocking. executionFinishedCh is buffered so multiple + // runs (keep-alive mode) can each signal completion without blocking the engine. initializedCh := make(chan struct{}) - executionFinishedCh := make(chan struct{}) + executionFinishedCh := make(chan struct{}, 1) var manualTriggerCaps *ManualTriggers simulatorInitialize := func(ctx context.Context, cfg simulator.RunnerConfig) (*capabilities.Registry, []services.Service) { @@ -551,20 +560,47 @@ func run( simLogger.Error("Trigger to run not selected") os.Exit(1) } - simLogger.Info("Running trigger", "trigger", triggerInfoAndBeforeStart.TriggerToRun.GetId()) - err := triggerInfoAndBeforeStart.TriggerFunc() - if err != nil { - simLogger.Error("Failed to run trigger", "trigger", triggerInfoAndBeforeStart.TriggerToRun.GetId(), "error", err) - os.Exit(1) + + keepAlive := inputs.KeepAlive && triggerInfoAndBeforeStart.TriggerToRun.GetId() == "http-trigger@1.0.0-alpha" + if inputs.KeepAlive && !keepAlive { + ui.Warning("--keep-alive only applies to http-trigger; ignoring for this trigger type") + } + if keepAlive { + runHTTPKeepAlive(ctx, inputs, triggerInfoAndBeforeStart, executionFinishedCh, simLogger) + return } - select { - case <-executionFinishedCh: - simLogger.Info("Execution finished signal received") - case <-ctx.Done(): - simLogger.Info("Received interrupt signal, stopping execution") - case <-time.After(WorkflowExecutionTimeout): - simLogger.Warn("Timeout waiting for execution to finish") + for iteration := 0; ; iteration++ { + if iteration > 0 { + ui.Line() + ui.Step(fmt.Sprintf("Keep-alive: ready for next request (run #%d)", iteration+1)) + // Drain any stale completion signal so we wait for the new run's result. + select { + case <-executionFinishedCh: + default: + } + } + + simLogger.Info("Running trigger", "trigger", triggerInfoAndBeforeStart.TriggerToRun.GetId()) + err := triggerInfoAndBeforeStart.TriggerFunc() + if err != nil { + simLogger.Error("Failed to run trigger", "trigger", triggerInfoAndBeforeStart.TriggerToRun.GetId(), "error", err) + os.Exit(1) + } + + select { + case <-executionFinishedCh: + simLogger.Info("Execution finished signal received") + case <-ctx.Done(): + simLogger.Info("Received interrupt signal, stopping execution") + return + case <-time.After(WorkflowExecutionTimeout): + simLogger.Warn("Timeout waiting for execution to finish") + } + + if !keepAlive { + return + } } } simulatorCleanup := func(ctx context.Context, cfg simulator.RunnerConfig, registry *capabilities.Registry, services []services.Service) { @@ -645,7 +681,10 @@ func run( ui.Error("Execution resulted in an error being returned: " + r.Error) } ui.Line() - close(executionFinishedCh) + select { + case executionFinishedCh <- struct{}{}: + default: + } }, }, WorkflowSettingsCfgFn: func(cfg *cresettings.Workflows) { @@ -666,10 +705,157 @@ func run( return nil } +func runHTTPKeepAlive(ctx context.Context, inputs Inputs, triggerInfo *TriggerInfoAndBeforeStart, executionFinishedCh <-chan struct{}, simLogger *SimulationLogger) { + if triggerInfo.TriggerWithPayload == nil { + simLogger.Error("HTTP trigger payload function not initialized") + os.Exit(1) + } + + payloadCh, closeServer, err := startHTTPKeepAlivePayloadServer(ctx, httpTriggerServerPort) + if err != nil { + ui.Error(fmt.Sprintf("Failed to start HTTP trigger server: %v", err)) + os.Exit(1) + } + defer closeServer() + + runPayload := func(payload *httptypedapi.Payload) bool { + simLogger.Info("Running trigger", "trigger", triggerInfo.TriggerToRun.GetId()) + if err := triggerInfo.TriggerWithPayload(payload); err != nil { + simLogger.Error("Failed to run trigger", "trigger", triggerInfo.TriggerToRun.GetId(), "error", err) + os.Exit(1) + } + + select { + case <-executionFinishedCh: + simLogger.Info("Execution finished signal received") + case <-ctx.Done(): + simLogger.Info("Received interrupt signal, stopping execution") + return false + case <-time.After(WorkflowExecutionTimeout): + simLogger.Warn("Timeout waiting for execution to finish") + } + + return true + } + + iteration := 0 + if strings.TrimSpace(inputs.HTTPPayload) != "" { + if triggerInfo.TriggerFunc == nil { + simLogger.Error("Trigger function not initialized") + os.Exit(1) + } + simLogger.Info("Running trigger", "trigger", triggerInfo.TriggerToRun.GetId()) + if err := triggerInfo.TriggerFunc(); err != nil { + simLogger.Error("Failed to run trigger", "trigger", triggerInfo.TriggerToRun.GetId(), "error", err) + os.Exit(1) + } + select { + case <-executionFinishedCh: + simLogger.Info("Execution finished signal received") + case <-ctx.Done(): + simLogger.Info("Received interrupt signal, stopping execution") + return + case <-time.After(WorkflowExecutionTimeout): + simLogger.Warn("Timeout waiting for execution to finish") + } + iteration = 1 + } + + for { + if iteration > 0 { + ui.Line() + ui.Step(fmt.Sprintf("Keep-alive: ready for next request (run #%d)", iteration+1)) + } + ui.Step(fmt.Sprintf("Waiting for HTTP request to start execution (listening on http://localhost:%d/trigger)...", httpTriggerServerPort)) + + var payload *httptypedapi.Payload + select { + case payload = <-payloadCh: + case <-ctx.Done(): + simLogger.Info("Received interrupt signal, stopping execution") + return + } + + if !runPayload(payload) { + return + } + iteration++ + } +} + +func startHTTPKeepAlivePayloadServer(ctx context.Context, port int) (<-chan *httptypedapi.Payload, func(), error) { + payloadCh := make(chan *httptypedapi.Payload, 16) + mux := http.NewServeMux() + mux.HandleFunc("/trigger", func(w http.ResponseWriter, r *http.Request) { + input, err := parseHTTPTriggerRequest(r) + if err != nil { + http.Error(w, fmt.Sprintf("error processing request: %v", err), http.StatusBadRequest) + return + } + + select { + case payloadCh <- &httptypedapi.Payload{Input: input}: + w.WriteHeader(http.StatusOK) + default: + http.Error(w, "trigger queue is full", http.StatusTooManyRequests) + } + }) + + server := &http.Server{ + Handler: mux, + ReadHeaderTimeout: time.Second, + } + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + return nil, nil, err + } + + done := make(chan struct{}) + go func() { + defer close(done) + if err := server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { + // Nothing to do here: startup errors are handled synchronously by + // net.Listen above, and shutdown uses server.Close(). + return + } + }() + go func() { + <-ctx.Done() + _ = server.Close() + }() + + closeServer := func() { + _ = server.Close() + <-done + } + return payloadCh, closeServer, nil +} + +func parseHTTPTriggerRequest(req *http.Request) ([]byte, error) { + if req.Method != http.MethodPost { + return nil, errors.New("gateway expects POST request") + } + + body, err := io.ReadAll(req.Body) + if err != nil { + return nil, fmt.Errorf("failed to read request body: %w", err) + } + + var rpcRequest struct { + Input json.RawMessage `json:"input"` + } + if err := json.Unmarshal(body, &rpcRequest); err != nil { + return nil, fmt.Errorf("failed to parse request body: %w", err) + } + + return rpcRequest.Input, nil +} + type TriggerInfoAndBeforeStart struct { - TriggerFunc func() error - TriggerToRun *pb.TriggerSubscription - BeforeStart func(ctx context.Context, cfg simulator.RunnerConfig, registry *capabilities.Registry, services []services.Service, triggerSub []*pb.TriggerSubscription) + TriggerFunc func() error + TriggerWithPayload func(*httptypedapi.Payload) error + TriggerToRun *pb.TriggerSubscription + BeforeStart func(ctx context.Context, cfg simulator.RunnerConfig, registry *capabilities.Registry, services []services.Service, triggerSub []*pb.TriggerSubscription) } // makeBeforeStartInteractive builds the interactive BeforeStart closure @@ -751,9 +937,16 @@ func makeBeforeStartInteractive(holder *TriggerInfoAndBeforeStart, inputs Inputs ui.Line() } holder.TriggerFunc = func() error { - if payload == nil { + // Consume any inline payload on the first call; subsequent calls + // (keep-alive mode) listen on the local HTTP server. + p := payload + payload = nil + if p == nil { ui.Step(fmt.Sprintf("Waiting for HTTP request to start execution (listening on http://localhost:%d/trigger)...", httpTriggerServerPort)) } + return manualTriggerCaps.ManualHTTPTrigger.ManualTrigger(ctx, triggerRegistrationID, p) + } + holder.TriggerWithPayload = func(payload *httptypedapi.Payload) error { return manualTriggerCaps.ManualHTTPTrigger.ManualTrigger(ctx, triggerRegistrationID, payload) } default: @@ -827,7 +1020,7 @@ func makeBeforeStartNonInteractive(holder *TriggerInfoAndBeforeStart, inputs Inp return manualTriggerCaps.ManualCronTrigger.ManualTrigger(ctx, triggerRegistrationID, skipWaitSignal) } case "http-trigger@1.0.0-alpha": - if strings.TrimSpace(inputs.HTTPPayload) == "" { + if strings.TrimSpace(inputs.HTTPPayload) == "" && !inputs.KeepAlive { ui.Error("--http-payload is required for http-trigger@1.0.0-alpha in non-interactive mode") os.Exit(1) } @@ -837,6 +1030,14 @@ func makeBeforeStartNonInteractive(holder *TriggerInfoAndBeforeStart, inputs Inp os.Exit(1) } holder.TriggerFunc = func() error { + p := payload + payload = nil + if p == nil { + ui.Step(fmt.Sprintf("Waiting for HTTP request to start execution (listening on http://localhost:%d/trigger)...", httpTriggerServerPort)) + } + return manualTriggerCaps.ManualHTTPTrigger.ManualTrigger(ctx, triggerRegistrationID, p) + } + holder.TriggerWithPayload = func(payload *httptypedapi.Payload) error { return manualTriggerCaps.ManualHTTPTrigger.ManualTrigger(ctx, triggerRegistrationID, payload) } default: diff --git a/cmd/workflow/simulate/simulate_test.go b/cmd/workflow/simulate/simulate_test.go index 2ee833cb..ab9fcfe6 100644 --- a/cmd/workflow/simulate/simulate_test.go +++ b/cmd/workflow/simulate/simulate_test.go @@ -1,11 +1,14 @@ package simulate import ( + "bytes" "context" "encoding/base64" "encoding/json" "fmt" "io" + "net" + "net/http" "os" "path/filepath" rt "runtime" @@ -29,6 +32,19 @@ import ( "github.com/smartcontractkit/cre-cli/internal/testutil" ) +func freeTCPPort(t *testing.T) int { + t.Helper() + + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer listener.Close() + + addr, ok := listener.Addr().(*net.TCPAddr) + require.True(t, ok) + require.Greater(t, addr.Port, 0) + return addr.Port +} + // TestBlankWorkflowSimulation validates that the simulator can successfully // run a blank workflow from end to end in a non-interactive mode. func TestBlankWorkflowSimulation(t *testing.T) { @@ -493,6 +509,50 @@ func TestNonInteractiveCronTriggerDoesNotBlockOnSchedule(t *testing.T) { } } +func TestHTTPKeepAlivePayloadServerAcceptsMultipleRequests(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + port := freeTCPPort(t) + payloadCh, closeServer, err := startHTTPKeepAlivePayloadServer(ctx, port) + require.NoError(t, err) + t.Cleanup(closeServer) + + for _, input := range []string{`{"key":"first"}`, `{"key":"second"}`} { + body := []byte(fmt.Sprintf(`{"input":%s}`, input)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("http://127.0.0.1:%d/trigger", port), bytes.NewReader(body)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + require.NoError(t, resp.Body.Close()) + } + + for _, want := range [][]byte{[]byte(`{"key":"first"}`), []byte(`{"key":"second"}`)} { + select { + case payload := <-payloadCh: + require.Equal(t, want, payload.Input) + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for queued HTTP trigger payload") + } + } +} + +func TestManualHTTPTriggerEventsHaveUniqueIDs(t *testing.T) { + t.Parallel() + + svc := NewManualHTTPTriggerService(logger.Test(t)) + first := svc.createManualTriggerEvent(nil) + second := svc.createManualTriggerEvent(nil) + + require.NotEmpty(t, first.Id) + require.NotEqual(t, first.Id, second.Id) +} + func TestSimulateConfigFlagsMutuallyExclusive(t *testing.T) { t.Parallel() diff --git a/docs/cre_workflow_simulate.md b/docs/cre_workflow_simulate.md index 61e7d039..4f7c9ff3 100644 --- a/docs/cre_workflow_simulate.md +++ b/docs/cre_workflow_simulate.md @@ -27,6 +27,7 @@ cre workflow simulate ./my-workflow --evm-tx-hash string EVM trigger transaction hash (0x...) -h, --help help for simulate --http-payload string HTTP trigger payload as JSON string or path to JSON file + --keep-alive Keep the simulator running after each execution and accept additional HTTP trigger requests (http-trigger only) --limits string Production limits to enforce during simulation: 'default' for prod defaults, path to a limits JSON file (e.g. from 'cre workflow limits export'), or 'none' to disable (default "default") --no-config Simulate without a config file --skip-type-checks Skip TypeScript project typecheck during compilation (passes --skip-type-checks to cre-compile)