Skip to content

Commit

Permalink
feat(world-cli): Add commands for running evm and da layer (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
jerargus authored Dec 1, 2023
1 parent 730c8b0 commit e2696f7
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 39 deletions.
21 changes: 0 additions & 21 deletions cmd/world/cardinal/cardinal.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
package cardinal

import (
"errors"

"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"pkg.world.dev/world-cli/common/config"
"pkg.world.dev/world-cli/common/dependency"
"pkg.world.dev/world-cli/tea/style"
)

func init() {
// Register subcommands - `world cardinal [subcommand]`
BaseCmd.AddCommand(createCmd, startCmd, devCmd, restartCmd, purgeCmd, stopCmd)
BaseCmd.Flags().String("config", "", "a toml encoded config file")
}

// BaseCmd is the base command for the cardinal subcommand
Expand All @@ -38,20 +34,3 @@ var BaseCmd = &cobra.Command{
}
},
}

func getConfig(cmd *cobra.Command) (cfg config.Config, err error) {
if !cmd.Flags().Changed("config") {
// The config flag was not set. Attempt to find the config via environment variables or in the local directory
return config.LoadConfig("")
}
// The config flag was explicitly set
configFile, err := cmd.Flags().GetString("config")
if err != nil {
return cfg, err
}
if configFile == "" {
return cfg, errors.New("config cannot be empty")
}
return config.LoadConfig(configFile)

}
3 changes: 2 additions & 1 deletion cmd/world/cardinal/restart.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cardinal

import (
"github.com/spf13/cobra"
"pkg.world.dev/world-cli/common/config"
"pkg.world.dev/world-cli/common/tea_cmd"
)

Expand All @@ -20,7 +21,7 @@ This will restart the following Docker services:
- Cardinal (Core game logic)
- Nakama (Relay)`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := getConfig(cmd)
cfg, err := config.GetConfig(cmd)
if err != nil {
return err
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/world/cardinal/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"

"github.com/spf13/cobra"
"pkg.world.dev/world-cli/common/config"
"pkg.world.dev/world-cli/common/tea_cmd"
)

Expand Down Expand Up @@ -35,7 +36,7 @@ This will start the following Docker services and its dependencies:
- Nakama (Relay)
- Redis (Cardinal dependency)`,
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := getConfig(cmd)
cfg, err := config.GetConfig(cmd)
if err != nil {
return err
}
Expand Down
217 changes: 217 additions & 0 deletions cmd/world/evm/evm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package evm

import (
"bytes"
"errors"
"fmt"
"os/exec"
"strings"
"time"

"github.com/spf13/cobra"
"pkg.world.dev/world-cli/common/config"
"pkg.world.dev/world-cli/common/tea_cmd"
)

func EVMCmds() *cobra.Command {
evmRootCmd := &cobra.Command{
Use: "evm",
Short: "EVM base shard commands.",
Long: "Commands for provisioning the EVM Base Shard.",
}
evmRootCmd.AddGroup(&cobra.Group{
ID: "EVM",
Title: "EVM Base Shard Commands",
})
evmRootCmd.AddCommand(
StartEVM(),
StopAll(),
)
return evmRootCmd
}

const (
FlagUseDevDA = "dev"
FlagDAAuthToken = "da-auth-token"
EnvDAAuthToken = "DA_AUTH_TOKEN"
EnvDABaseURL = "DA_BASE_URL"
EnvDANamespaceID = "DA_NAMESPACE_ID"

daService = tea_cmd.DockerServiceDA
)

var (
// Docker compose seems to replace the hyphen with an underscore. This could be properly fixed by removing the hyphen
// from celesta-devnet, or by investigating the docker compose documentation.
daContainer = strings.ReplaceAll(string(daService), "-", "_")
)

func services(s ...tea_cmd.DockerService) []tea_cmd.DockerService {
return s
}

// validateDevDALayer starts a locally running version of the DA layer, and replaces the DA_AUTH_TOKEN configuration
// variable with the token from the locally running container.
func validateDevDALayer(cfg config.Config) error {
cfg.Build = true
cfg.Debug = false
cfg.Detach = true
cfg.Timeout = -1
fmt.Println("starting DA docker service for dev mode...")
if err := tea_cmd.DockerStart(cfg, services(daService)); err != nil {
return fmt.Errorf("error starting %s docker container: %w", daService, err)
}

if err := blockUntilContainerIsRunning(daContainer, 10*time.Second); err != nil {
return err
}
fmt.Println("started DA service...")

daToken, err := getDAToken()
if err != nil {
return err
}
envOverrides := map[string]string{
EnvDAAuthToken: daToken,
EnvDABaseURL: fmt.Sprintf("http://%s:26658", daService),
EnvDANamespaceID: "67480c4a88c4d12935d4",
}
for key, value := range envOverrides {
fmt.Printf("overriding config value %q to %q\n", key, value)
cfg.DockerEnv[key] = value
}
return nil
}

// validateProdDALayer assumes the DA layer is running somewhere else and validates the required world.toml variables are
// non-empty.
func validateProdDALayer(cfg config.Config) error {
requiredEnvVariables := []string{
EnvDAAuthToken,
EnvDABaseURL,
EnvDANamespaceID,
}
var errs []error
for _, env := range requiredEnvVariables {
if len(cfg.DockerEnv[env]) > 0 {
continue
}
errs = append(errs, fmt.Errorf("missing %q", env))
}
if len(errs) > 0 {
// Prepend an error describing the overall problem
errs = append([]error{
fmt.Errorf("the [evm] section of your config is missing some required variables"),
}, errs...)
return errors.Join(errs...)
}
return nil
}

func validateDALayer(cmd *cobra.Command, cfg config.Config) error {
devDA, err := cmd.Flags().GetBool(FlagUseDevDA)
if err != nil {
return err
}
if devDA {
return validateDevDALayer(cfg)
}
return validateProdDALayer(cfg)
}

func StartEVM() *cobra.Command {
cmd := &cobra.Command{
Use: "start",
Short: "Start the EVM base shard. Use --da-auth-token to pass in an auth token directly.",
Long: "Start the EVM base shard. Requires connection to celestia DA.",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := config.GetConfig(cmd)
if err != nil {
return err
}

if err = validateDALayer(cmd, cfg); err != nil {
return err
}

daToken, err := cmd.Flags().GetString(FlagDAAuthToken)
if err != nil {
return err
}
if daToken != "" {
cfg.DockerEnv[EnvDAAuthToken] = daToken
}

cfg.Build = true
cfg.Debug = false
cfg.Detach = false
cfg.Timeout = 0

err = tea_cmd.DockerStart(cfg, services(tea_cmd.DockerServiceEVM))
if err != nil {
fmt.Errorf("error starting %s docker container: %w", tea_cmd.DockerServiceEVM, err)
}
return nil
},
}
cmd.Flags().String(FlagDAAuthToken, "", "DA Auth Token that allows the rollup to communicate with the Celestia client.")
cmd.Flags().Bool(FlagUseDevDA, false, "Use a locally running DA layer")
return cmd
}

func StopAll() *cobra.Command {
cmd := &cobra.Command{
Use: "stop",
Short: "Stop the EVM base shard and DA layer client.",
Long: "Stop the EVM base shard and data availability layer client if they are running.",
RunE: func(cmd *cobra.Command, args []string) error {
return tea_cmd.DockerStop(services(tea_cmd.DockerServiceEVM, tea_cmd.DockerServiceDA))
},
}
return cmd
}

func getDAToken() (token string, err error) {
// Create a new command
maxRetries := 10
cmdString := fmt.Sprintf("docker exec %s celestia bridge --node.store /home/celestia/bridge/ auth admin", daContainer)
cmdParts := strings.Split(cmdString, " ")
for retry := 0; retry < maxRetries; retry++ {
fmt.Println("attempting to get DA token...")

cmd := exec.Command(cmdParts[0], cmdParts[1:]...)
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Println("failed to get da token")
fmt.Printf("%d/%d retrying...\n", retry+1, maxRetries)
time.Sleep(2 * time.Second)
continue
}

if bytes.Contains(output, []byte("\n")) {
return "", fmt.Errorf("da token should be a single line. got %v", string(output))
}
if len(output) == 0 {
return "", fmt.Errorf("got empty DA token")
}
return string(output), nil
}
return "", fmt.Errorf("timed out while getting DA token")

}

func blockUntilContainerIsRunning(targetContainer string, timeout time.Duration) error {
timeoutAt := time.Now().Add(timeout)
cmdString := "docker container inspect -f '{{.State.Running}}' " + targetContainer
// This string will be returned by the above command when the container is running
runningOutput := "'true'\n"
cmdParts := strings.Split(cmdString, " ")
for time.Now().Before(timeoutAt) {
output, err := exec.Command(cmdParts[0], cmdParts[1:]...).CombinedOutput()
if err == nil && string(output) == runningOutput {
return nil
}
time.Sleep(250 * time.Millisecond)
}
return fmt.Errorf("timeout while waiting for %q to start", targetContainer)
}
9 changes: 8 additions & 1 deletion cmd/world/root/root.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package root

import (
"os"

"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"os"
"pkg.world.dev/world-cli/cmd/world/cardinal"
"pkg.world.dev/world-cli/cmd/world/evm"
"pkg.world.dev/world-cli/common/config"
"pkg.world.dev/world-cli/tea/style"
)

Expand All @@ -21,6 +24,10 @@ func init() {

// Register subcommands
rootCmd.AddCommand(cardinal.BaseCmd)

rootCmd.AddCommand(evm.EVMCmds())

config.AddConfigFlag(rootCmd)
}

// rootCmd represents the base command
Expand Down
25 changes: 24 additions & 1 deletion common/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ import (

"github.com/pelletier/go-toml"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)

const (
WorldCLIConfigFileEnvVariable = "WORLD_CLI_CONFIG_FILE"
WorldCLIConfigFilename = "world.toml"

flagForConfigFile = "config"
)

var (
Expand All @@ -32,7 +35,27 @@ type Config struct {
DockerEnv map[string]string
}

func LoadConfig(filename string) (Config, error) {
func AddConfigFlag(cmd *cobra.Command) {
cmd.Flags().String(flagForConfigFile, "", "a toml encoded config file")
}

func GetConfig(cmd *cobra.Command) (cfg Config, err error) {
if !cmd.Flags().Changed(flagForConfigFile) {
// The config flag was not set. Attempt to find the config via environment variables or in the local directory
return loadConfig("")
}
// The config flag was explicitly set
configFile, err := cmd.Flags().GetString(flagForConfigFile)
if err != nil {
return cfg, err
}
if configFile == "" {
return cfg, errors.New("config cannot be empty")
}
return loadConfig(configFile)
}

func loadConfig(filename string) (Config, error) {
if filename != "" {
return loadConfigFromFile(filename)
}
Expand Down
Loading

0 comments on commit e2696f7

Please sign in to comment.