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
12 changes: 12 additions & 0 deletions cmd/common/hash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package common

import (
"crypto/sha256"
"encoding/hex"
)

// HashBytes computes the SHA-256 hash of data and returns it as a hex string.
func HashBytes(data []byte) string {
h := sha256.Sum256(data)
return hex.EncodeToString(h[:])
}
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,7 @@ func isLoadCredentials(cmd *cobra.Command) bool {
"cre update": {},
"cre workflow": {},
"cre workflow build": {},
"cre workflow hash": {},
"cre account": {},
"cre secrets": {},
"cre templates": {},
Expand Down Expand Up @@ -536,6 +537,7 @@ func shouldShowSpinner(cmd *cobra.Command) bool {
"cre update": {},
"cre workflow": {}, // Just shows help
"cre workflow build": {}, // Offline command, no async init
"cre workflow hash": {}, // Offline command, has own spinner
"cre account": {}, // Just shows help
"cre secrets": {}, // Just shows help
"cre templates": {}, // Just shows help
Expand Down
1 change: 1 addition & 0 deletions cmd/workflow/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ func execute(workflowFolder, outputPath string) error {
return fmt.Errorf("failed to compile workflow: %w", err)
}
ui.Success("Workflow compiled successfully")
ui.Dim(fmt.Sprintf("Binary hash: %s", cmdcommon.HashBytes(wasmBytes)))

if err := os.WriteFile(outputPath, wasmBytes, 0666); err != nil { //nolint:gosec
return fmt.Errorf("failed to write WASM binary: %w", err)
Expand Down
4 changes: 4 additions & 0 deletions cmd/workflow/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,10 @@ func (h *handler) Execute(ctx context.Context) error {
return fmt.Errorf("failed to prepare workflow artifact: %w", err)
}

ui.Dim(fmt.Sprintf("Binary hash: %s", cmdcommon.HashBytes(h.workflowArtifact.RawBinaryForID)))
ui.Dim(fmt.Sprintf("Config hash: %s", cmdcommon.HashBytes(h.workflowArtifact.RawConfigForID)))
ui.Dim(fmt.Sprintf("Workflow hash: %s", h.workflowArtifact.WorkflowID))

h.runtimeContext.Workflow.ID = h.workflowArtifact.WorkflowID

h.wg.Wait()
Expand Down
10 changes: 7 additions & 3 deletions cmd/workflow/deploy/prepare.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import (
)

type workflowArtifact struct {
BinaryData []byte
ConfigData []byte
WorkflowID string
BinaryData []byte
ConfigData []byte
WorkflowID string
RawBinaryForID []byte
RawConfigForID []byte
}

func (h *handler) prepareWorkflowBinary() ([]byte, error) {
Expand Down Expand Up @@ -80,6 +82,8 @@ func (h *handler) PrepareWorkflowArtifact() error {
}

h.workflowArtifact.WorkflowID = workflowID
h.workflowArtifact.RawBinaryForID = binaryForID
h.workflowArtifact.RawConfigForID = configData

return nil
}
186 changes: 186 additions & 0 deletions cmd/workflow/hash/hash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package hash

import (
"fmt"
"os"

"github.com/spf13/cobra"

workflowUtils "github.com/smartcontractkit/chainlink-common/pkg/workflows"

cmdcommon "github.com/smartcontractkit/cre-cli/cmd/common"
"github.com/smartcontractkit/cre-cli/internal/ethkeys"
"github.com/smartcontractkit/cre-cli/internal/runtime"
"github.com/smartcontractkit/cre-cli/internal/settings"
"github.com/smartcontractkit/cre-cli/internal/ui"
)

type Inputs struct {
ForUser string
WasmPath string
ConfigPath string
WorkflowName string
WorkflowPath string
OwnerFromSettings string
PrivateKey string
}

func New(runtimeContext *runtime.Context) *cobra.Command {
hashCmd := &cobra.Command{
Use: "hash <workflow-folder-path>",
Short: "Computes and displays workflow hashes",
Long: `Computes the binary hash, config hash, and workflow hash for a workflow. The workflow hash uses the same algorithm as the on-chain workflow ID.`,
Args: cobra.ExactArgs(1),
Example: ` cre workflow hash ./my-workflow
cre workflow hash ./my-workflow --public_key 0x1234...abcd`,
RunE: func(cmd *cobra.Command, args []string) error {
forUser, _ := cmd.Flags().GetString("public_key")

s := runtimeContext.Settings
v := runtimeContext.Viper

rawPrivKey := v.GetString(settings.EthPrivateKeyEnvVar)

inputs := Inputs{
ForUser: forUser,
WasmPath: v.GetString("wasm"),
ConfigPath: cmdcommon.ResolveConfigPath(v, s.Workflow.WorkflowArtifactSettings.ConfigPath),
WorkflowName: s.Workflow.UserWorkflowSettings.WorkflowName,
WorkflowPath: s.Workflow.WorkflowArtifactSettings.WorkflowPath,
OwnerFromSettings: s.Workflow.UserWorkflowSettings.WorkflowOwnerAddress,
PrivateKey: settings.NormalizeHexKey(rawPrivKey),
}

return Execute(inputs)
},
}

hashCmd.Flags().String("public_key", "",
"Owner address to use for computing the workflow hash. "+
"Required when CRE_ETH_PRIVATE_KEY is not set and no workflow-owner-address is configured. "+
"Defaults to the address derived from CRE_ETH_PRIVATE_KEY or the workflow-owner-address in project settings.")
hashCmd.Flags().String("wasm", "", "Path or URL to a pre-built WASM binary (skips compilation)")
hashCmd.Flags().String("config", "", "Override the config file path from workflow.yaml")
hashCmd.Flags().Bool("no-config", false, "Hash without a config file")
hashCmd.Flags().Bool("default-config", false, "Use the config path from workflow.yaml settings (default behavior)")
hashCmd.MarkFlagsMutuallyExclusive("config", "no-config", "default-config")

return hashCmd
}

func Execute(inputs Inputs) error {
rawBinary, err := loadBinary(inputs.WasmPath, inputs.WorkflowPath)
if err != nil {
return err
}

binary, err := cmdcommon.CompressBrotli(rawBinary)
if err != nil {
return fmt.Errorf("failed to compress binary: %w", err)
}

config, err := loadConfig(inputs.ConfigPath)
if err != nil {
return err
}

ownerAddress, err := ResolveOwner(inputs.ForUser, inputs.OwnerFromSettings, inputs.PrivateKey)
if err != nil {
return err
}

binaryHash := cmdcommon.HashBytes(binary)
configHash := cmdcommon.HashBytes(config)

workflowID, err := workflowUtils.GenerateWorkflowIDFromStrings(ownerAddress, inputs.WorkflowName, binary, config, "")
if err != nil {
return fmt.Errorf("failed to generate workflow hash: %w", err)
}

ui.Dim(fmt.Sprintf("Binary hash: %s", binaryHash))
ui.Dim(fmt.Sprintf("Config hash: %s", configHash))
ui.Dim(fmt.Sprintf("Workflow hash: %s", workflowID))

return nil
}

func ResolveOwner(forUser, ownerFromSettings, privateKey string) (string, error) {
if forUser != "" {
return forUser, nil
}

if ownerFromSettings != "" {
return ownerFromSettings, nil
}

if privateKey != "" {
addr, err := ethkeys.DeriveEthAddressFromPrivateKey(privateKey)
if err != nil {
return "", fmt.Errorf("failed to derive owner from private key: %w", err)
}
return addr, nil
}

return "", fmt.Errorf("cannot determine workflow owner: provide --public_key or ensure CRE_ETH_PRIVATE_KEY is set")
}

func loadBinary(wasmFlag, workflowPathFromSettings string) ([]byte, error) {
if wasmFlag != "" {
if cmdcommon.IsURL(wasmFlag) {
ui.Dim("Fetching WASM binary from URL...")
data, err := cmdcommon.FetchURL(wasmFlag)
if err != nil {
return nil, fmt.Errorf("failed to fetch WASM from URL: %w", err)
}
ui.Success("Fetched WASM binary from URL")
return cmdcommon.EnsureRawWasm(data)
}
ui.Dim("Reading pre-built WASM binary...")
data, err := os.ReadFile(wasmFlag)
if err != nil {
return nil, fmt.Errorf("failed to read WASM binary: %w", err)
}
ui.Success(fmt.Sprintf("Loaded WASM binary from %s", wasmFlag))
return cmdcommon.EnsureRawWasm(data)
}

workflowDir, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("workflow directory: %w", err)
}
resolvedWorkflowPath, err := cmdcommon.ResolveWorkflowPath(workflowDir, workflowPathFromSettings)
if err != nil {
return nil, fmt.Errorf("workflow path: %w", err)
}

spinner := ui.NewSpinner()
spinner.Start("Compiling workflow...")
wasmBytes, err := cmdcommon.CompileWorkflowToWasm(resolvedWorkflowPath)
spinner.Stop()
if err != nil {
ui.Error("Build failed:")
return nil, fmt.Errorf("failed to compile workflow: %w", err)
}
ui.Success("Workflow compiled")

return wasmBytes, nil
}

func loadConfig(configPath string) ([]byte, error) {
if configPath == "" {
return nil, nil
}
if cmdcommon.IsURL(configPath) {
ui.Dim("Fetching config from URL...")
data, err := cmdcommon.FetchURL(configPath)
if err != nil {
return nil, fmt.Errorf("failed to fetch config from URL: %w", err)
}
return data, nil
}
data, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
return data, nil
}
Loading
Loading