Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion cmd/workflow/simulate/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = 2000

// ManualTriggers holds chain-agnostic trigger services used in simulation.
type ManualTriggers struct {
ManualCronTrigger *fakes.ManualCronTriggerService
Expand All @@ -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
Expand Down
72 changes: 22 additions & 50 deletions cmd/workflow/simulate/simulate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -733,11 +733,23 @@ 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, 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)`)
ui.Line()
}
holder.TriggerFunc = func() error {
return manualTriggerCaps.ManualHTTPTrigger.ManualTrigger(ctx, triggerRegistrationID, payload)
}
Expand Down Expand Up @@ -816,7 +828,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)
Expand Down Expand Up @@ -890,22 +902,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{}
Expand All @@ -924,12 +927,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)
Expand Down Expand Up @@ -964,36 +969,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
}
87 changes: 41 additions & 46 deletions cmd/workflow/simulate/simulate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package simulate
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -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")
})
}

Expand Down
2 changes: 1 addition & 1 deletion docs/cre_workflow_simulate.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
33 changes: 17 additions & 16 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -310,24 +310,24 @@ 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
github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260512230622-65f10f4cd305 // indirect
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading