Skip to content
Open
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: 9 additions & 0 deletions .github/workflows/pull-request-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,18 @@ jobs:
contents: read
actions: read
steps:
- name: Checkout the repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2

- name: ci-lint-misc
uses: smartcontractkit/.github/actions/ci-lint-misc@01d931b0455a754d12e7143cc54a5a3521a8f6f6 # ci-lint-misc@0.1.4

- name: Verify install.sh embedded release public key
run: bash install/check_embedded_key.sh

- name: Verify install.sh linux asset suffix helpers
run: bash install/linux_asset_suffix_test.sh

ci-test-unit:
runs-on: ubuntu-latest
permissions:
Expand Down
12 changes: 11 additions & 1 deletion cmd/client/eth_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/smartcontractkit/chainlink-testing-framework/seth"

"github.com/smartcontractkit/cre-cli/internal/constants"
crpc "github.com/smartcontractkit/cre-cli/internal/rpc"
"github.com/smartcontractkit/cre-cli/internal/settings"
)

Expand Down Expand Up @@ -76,6 +77,15 @@ func NewEthClientFromEnv(v *viper.Viper, l *zerolog.Logger, ethUrl string) (*set
// check configuration file then use default value
sethConfigPath := v.GetString(settings.SethConfigPathSettingName)

cleartextOpts := crpc.CleartextPolicyOptions{
AllowInsecure: v.GetBool(settings.Flags.AllowInsecureRPC.Name),
}
if warnMsg, blockErr := crpc.EvaluateCleartextRPC(ethUrl, cleartextOpts); blockErr != nil {
return nil, blockErr
} else if warnMsg != "" {
l.Warn().Str("url", crpc.RedactURL(ethUrl)).Msg(warnMsg)
}

ethChainID, err := getChainID(ethUrl)
if err != nil {
return nil, fmt.Errorf("failed to get chain ID: %w", err)
Expand All @@ -97,7 +107,7 @@ func NewEthClientFromEnv(v *viper.Viper, l *zerolog.Logger, ethUrl string) (*set
if err != nil {
return nil, fmt.Errorf("failed to create Seth client: %w", err)
}
l.Debug().Int64("ChainID", client.ChainID).Str("URL", client.URL).Msg("Connected to a RPC node")
l.Debug().Int64("ChainID", client.ChainID).Str("URL", crpc.RedactURL(client.URL)).Msg("Connected to a RPC node")

l.Debug().Msg("Loading contract interfaces")
err = LoadContracts(l, client)
Expand Down
36 changes: 36 additions & 0 deletions cmd/client/eth_client_cleartext_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package client_test

import (
"testing"

"github.com/rs/zerolog"
"github.com/spf13/viper"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/cre-cli/cmd/client"
"github.com/smartcontractkit/cre-cli/internal/settings"
)

func TestNewEthClientFromEnvBlocksRemoteCleartext(t *testing.T) {
t.Parallel()

v := viper.New()
v.Set(settings.Flags.AllowInsecureRPC.Name, false)

logger := zerolog.Nop()
_, err := client.NewEthClientFromEnv(v, &logger, "http://rpc.example.com")
require.Error(t, err)
require.Contains(t, err.Error(), "--allow-insecure-rpc")
}

func TestNewEthClientFromEnvAllowsCleartextWithOptIn(t *testing.T) {
t.Parallel()

v := viper.New()
v.Set(settings.Flags.AllowInsecureRPC.Name, true)

logger := zerolog.Nop()
_, err := client.NewEthClientFromEnv(v, &logger, "http://rpc.example.com")
require.Error(t, err)
require.NotContains(t, err.Error(), "--allow-insecure-rpc")
}
2 changes: 1 addition & 1 deletion cmd/common/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func TestCompileWorkflowToWasm_TS_Success(t *testing.T) {
mainPath := filepath.Join(dir, "main.ts")
require.NoError(t, os.WriteFile(mainPath, []byte(`export async function main() { return "ok"; }
`), 0600))
require.NoError(t, os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"name":"test","dependencies":{"@chainlink/cre-sdk":"^1.9.0"}}
require.NoError(t, os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"name":"test","dependencies":{"@chainlink/cre-sdk":"1.17.0-alpha.solana-log-trigger.1"}}
`), 0600))
install := exec.Command("bun", "install")
install.Dir = dir
Expand Down
14 changes: 9 additions & 5 deletions cmd/creinit/creinit.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package creinit
import (
"fmt"
"maps"
"net/url"
"os"
"path/filepath"
"strings"
Expand All @@ -14,6 +13,7 @@ import (
"golang.org/x/term"

"github.com/smartcontractkit/cre-cli/internal/constants"
"github.com/smartcontractkit/cre-cli/internal/rpc"
"github.com/smartcontractkit/cre-cli/internal/runtime"
"github.com/smartcontractkit/cre-cli/internal/settings"
"github.com/smartcontractkit/cre-cli/internal/templateconfig"
Expand Down Expand Up @@ -348,10 +348,14 @@ func (h *handler) Execute(inputs Inputs) error {
maps.Copy(networkRPCs, inputs.RpcURLs)
// Validate any provided RPC URLs
for chain, rpcURL := range networkRPCs {
if rpcURL != "" {
if u, parseErr := url.Parse(rpcURL); parseErr != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" {
return fmt.Errorf("invalid RPC URL for %s: must be a valid http/https URL", chain)
}
if rpcURL == "" {
continue
}
if err := rpc.IsValidURL(rpcURL); err != nil {
return fmt.Errorf("invalid RPC URL for %s: %w", chain, err)
}
if _, blockErr := rpc.EvaluateCleartextRPC(rpcURL, rpc.CleartextPolicyOptions{}); blockErr != nil {
return fmt.Errorf("invalid RPC URL for %s: %w", chain, blockErr)
}
}

Expand Down
21 changes: 3 additions & 18 deletions cmd/creinit/wizard.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package creinit
import (
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"slices"
Expand All @@ -15,6 +14,7 @@ import (
"github.com/charmbracelet/lipgloss"

"github.com/smartcontractkit/cre-cli/internal/constants"
"github.com/smartcontractkit/cre-cli/internal/rpc"
"github.com/smartcontractkit/cre-cli/internal/templaterepo"
"github.com/smartcontractkit/cre-cli/internal/ui"
"github.com/smartcontractkit/cre-cli/internal/validation"
Expand Down Expand Up @@ -707,7 +707,7 @@ func (m wizardModel) handleEnter(msgs ...tea.Msg) (tea.Model, tea.Cmd) {
network := m.networks[m.rpcCursor]

if value != "" {
if err := validateRpcURL(value); err != nil {
if err := rpc.IsValidURL(value); err != nil {
m.err = fmt.Sprintf("Invalid URL for %s: %s", network, err.Error())
return m, nil
}
Expand Down Expand Up @@ -907,7 +907,7 @@ func (m wizardModel) View() string {
b.WriteString("\n")
// Real-time validation hint for RPC URL
if v := strings.TrimSpace(m.rpcInputs[i].Value()); v != "" {
if err := validateRpcURL(v); err != nil {
if err := rpc.IsValidURL(v); err != nil {
b.WriteString(m.warnStyle.Render(" " + err.Error()))
b.WriteString("\n")
}
Expand Down Expand Up @@ -1007,18 +1007,3 @@ func MissingNetworks(template *templaterepo.TemplateSummary, flagRpcURLs map[str
}
return missing
}

// validateRpcURL validates that a URL is a valid HTTP/HTTPS URL.
func validateRpcURL(rawURL string) error {
u, err := url.Parse(rawURL)
if err != nil {
return fmt.Errorf("invalid URL format")
}
if u.Scheme != "http" && u.Scheme != "https" {
return fmt.Errorf("URL must start with http:// or https://")
}
if u.Host == "" {
return fmt.Errorf("URL must have a host")
}
return nil
}
124 changes: 124 additions & 0 deletions cmd/execution/events/events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package events

import (
"context"
"fmt"

"github.com/spf13/cobra"

"github.com/smartcontractkit/cre-cli/internal/client/graphqlclient"
"github.com/smartcontractkit/cre-cli/internal/client/workflowdataclient"
"github.com/smartcontractkit/cre-cli/internal/credentials"
"github.com/smartcontractkit/cre-cli/internal/runtime"
"github.com/smartcontractkit/cre-cli/internal/ui"
"github.com/smartcontractkit/cre-cli/internal/workflowresolve"
)

// Inputs holds resolved and validated flag/arg values for execution events.
type Inputs struct {
ExecutionRef string
CapabilityID *string
Status *string
OutputFormat string
}

func resolveInputs(executionRef, capabilityID, status, outputFormat string, jsonFlag bool) (Inputs, error) {
outputFormat, err := workflowresolve.ResolveOutputFormat(outputFormat, jsonFlag)
if err != nil {
return Inputs{}, err
}
in := Inputs{
ExecutionRef: executionRef,
OutputFormat: outputFormat,
}
if capabilityID != "" {
in.CapabilityID = &capabilityID
}
if status != "" {
in.Status = &status
}
return in, nil
}

// Handler fetches and renders execution capability events.
type Handler struct {
credentials *credentials.Credentials
wdc *workflowdataclient.Client
}

// NewHandler builds a Handler with a real WorkflowDataClient.
func NewHandler(ctx *runtime.Context) *Handler {
gql := graphqlclient.New(ctx.Credentials, ctx.EnvironmentSet, ctx.Logger)
wdc := workflowdataclient.New(gql, ctx.Logger)
return &Handler{credentials: ctx.Credentials, wdc: wdc}
}

// NewHandlerWithClient builds a Handler with a pre-built client (for testing).
func NewHandlerWithClient(ctx *runtime.Context, wdc *workflowdataclient.Client) *Handler {
return &Handler{credentials: ctx.Credentials, wdc: wdc}
}

// Execute fetches and renders execution events.
func (h *Handler) Execute(ctx context.Context, inputs Inputs) error {
if h.credentials == nil {
return fmt.Errorf("credentials not available — run `cre login` and retry")
}

spinner := ui.NewSpinner()
spinner.Start("Fetching execution events...")

uuid, err := workflowresolve.ResolveExecutionUUID(ctx, h.wdc, inputs.ExecutionRef)
if err != nil {
spinner.Stop()
return err
}

events, err := h.wdc.ListExecutionEvents(ctx, workflowdataclient.ListEventsInput{
ExecutionUUID: uuid,
CapabilityID: inputs.CapabilityID,
Status: inputs.Status,
})
spinner.Stop()
if err != nil {
return err
}

if inputs.OutputFormat == workflowresolve.OutputFormatJSON {
return workflowresolve.PrintEventsJSON(events)
}
workflowresolve.PrintEventsTable(events)
return nil
}

// New returns the cobra command.
func New(runtimeContext *runtime.Context) *cobra.Command {
var capabilityID string
var status string
var outputFormat string
var jsonFlag bool

cmd := &cobra.Command{
Use: "events <execution-uuid>",
Short: "Show the node/capability event timeline for an execution",
Long: `Fetch and display the ordered sequence of capability events for a workflow
execution, including per-event status, method, duration, and any errors.`,
Example: "cre execution events 7f3d8a12-b1c2-4d3e-9f0a-1b2c3d4e5f6g\n" +
" cre execution events 7f3d8a12-b1c2-4d3e-9f0a-1b2c3d4e5f6g --capability fetch-price\n" +
" cre execution events 7f3d8a12-b1c2-4d3e-9f0a-1b2c3d4e5f6g --status FAILURE\n" +
" cre execution events 7f3d8a12-b1c2-4d3e-9f0a-1b2c3d4e5f6g --output json",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
inputs, err := resolveInputs(args[0], capabilityID, status, outputFormat, jsonFlag)
if err != nil {
return err
}
return NewHandler(runtimeContext).Execute(cmd.Context(), inputs)
},
}

cmd.Flags().StringVar(&capabilityID, "capability", "", "Filter events to a specific capability ID")
cmd.Flags().StringVar(&status, "status", "", "Filter events by status (e.g. FAILURE)")
cmd.Flags().StringVar(&outputFormat, "output", "", `Output format: "json" prints a JSON array to stdout`)
cmd.Flags().BoolVar(&jsonFlag, "json", false, "Output as JSON (shorthand for --output=json)")
return cmd
}
Loading