diff --git a/models/opinit_bots/bots.go b/models/opinit_bots/bots.go index bcb5f782..ab490287 100644 --- a/models/opinit_bots/bots.go +++ b/models/opinit_bots/bots.go @@ -1,8 +1,13 @@ package opinit_bots import ( + "fmt" + "os" "os/exec" + "path/filepath" "strings" + + "github.com/initia-labs/weave/common" ) const ( @@ -94,9 +99,17 @@ var BotInfos = []BotInfo{ // CheckIfKeysExist checks the output of `initiad keys list` and sets IsNotExist for missing keys func CheckIfKeysExist(botInfos []BotInfo) []BotInfo { - cmd := exec.Command(AppName, "keys", "list", "weave-dummy") + userHome, err := os.UserHomeDir() + if err != nil { + panic(err) + } + binaryPath := filepath.Join(userHome, common.WeaveDataDirectory, fmt.Sprintf("opinitd@%s", OpinitBotBinaryVersion), AppName) + cmd := exec.Command(binaryPath, "keys", "list", "weave-dummy") outputBytes, err := cmd.Output() if err != nil { + for i := range botInfos { + botInfos[i].IsNotExist = true + } return botInfos } output := string(outputBytes) diff --git a/models/opinit_bots/config.go b/models/opinit_bots/config.go index 4addec4c..fab26ecf 100644 --- a/models/opinit_bots/config.go +++ b/models/opinit_bots/config.go @@ -7,13 +7,13 @@ type NodeConfig struct { } type ChallengerConfig struct { - Version int `json:"version"` - ListenAddress string `json:"listen_address"` - L1Node NodeConfig `json:"l1_node"` - L2Node NodeConfig `json:"l2_node"` - L1StartHeight int `json:"l1_start_height"` - L2StartHeight int `json:"l2_start_height"` - DisableAutoSetL1Height bool `json:"disable_auto_set_l1_height"` + Version int `json:"version"` + Server ServerConfig `json:"server"` + L1Node NodeConfig `json:"l1_node"` + L2Node NodeConfig `json:"l2_node"` + L1StartHeight int `json:"l1_start_height"` + L2StartHeight int `json:"l2_start_height"` + DisableAutoSetL1Height bool `json:"disable_auto_set_l1_height"` } type NodeSettings struct { @@ -25,18 +25,29 @@ type NodeSettings struct { TxTimeout int `json:"tx_timeout"` } +type ServerConfig struct { + Address string `json:"address"` + AllowOrigins string `json:"allow_origins"` + AllowHeaders string `json:"allow_headers"` + AllowMethods string `json:"allow_methods"` +} + type ExecutorConfig struct { - Version int `json:"version"` - ListenAddress string `json:"listen_address"` - L1Node NodeSettings `json:"l1_node"` - L2Node NodeSettings `json:"l2_node"` - DANode NodeSettings `json:"da_node"` - OutputSubmitter string `json:"output_submitter"` - BridgeExecutor string `json:"bridge_executor"` - BatchSubmitterEnabled bool `json:"enable_batch_submitter"` - MaxChunks int `json:"max_chunks"` - MaxChunkSize int `json:"max_chunk_size"` - MaxSubmissionTime int `json:"max_submission_time"` - L2StartHeight int `json:"l2_start_height"` - BatchStartHeight int `json:"batch_start_height"` + Version int `json:"version"` + Server ServerConfig `json:"server"` + L1Node NodeSettings `json:"l1_node"` + L2Node NodeSettings `json:"l2_node"` + DANode NodeSettings `json:"da_node"` + BridgeExecutor string `json:"bridge_executor"` + OracleBridgeExecutor string `json:"oracle_bridge_executor"` + DisableOutputSubmitter bool `json:"disable_output_submitter"` + DisableBatchSubmitter bool `json:"disable_batch_submitter"` + MaxChunks int `json:"max_chunks"` + MaxChunkSize int `json:"max_chunk_size"` + MaxSubmissionTime int `json:"max_submission_time"` + DisableAutoSetL1Height bool `json:"disable_auto_set_l1_height"` + L1StartHeight int `json:"l1_start_height"` + L2StartHeight int `json:"l2_start_height"` + BatchStartHeight int `json:"batch_start_height"` + DisableDeleteFutureWithdrawal bool `json:"disable_delete_future_withdrawal"` } diff --git a/models/opinit_bots/init.go b/models/opinit_bots/init.go index 683bc232..b55d3b00 100644 --- a/models/opinit_bots/init.go +++ b/models/opinit_bots/init.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/initia-labs/weave/io" "os" "path/filepath" "strings" @@ -14,6 +13,8 @@ import ( "github.com/initia-labs/weave/common" weavecontext "github.com/initia-labs/weave/context" + "github.com/initia-labs/weave/cosmosutils" + "github.com/initia-labs/weave/io" "github.com/initia-labs/weave/registry" "github.com/initia-labs/weave/service" "github.com/initia-labs/weave/styles" @@ -238,12 +239,55 @@ func (m *OPInitBotInitSelector) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if selected != nil { state := weavecontext.PushPageAndGetState[OPInitBotsState](m) state.weave.PushPreviousResponse(styles.RenderPreviousResponse(styles.ArrowSeparator, m.GetQuestion(), []string{"bot"}, string(*selected))) - m.Ctx = weavecontext.SetCurrentState(m.Ctx, state) switch *selected { case ExecutorOPInitBotInitOption: - return OPInitBotInitSelectExecutor(m.Ctx), cmd + state.InitExecutorBot = true + keyNames := make(map[string]bool) + keyNames[BridgeExecutorKeyName] = true + keyNames[OutputSubmitterKeyName] = true + keyNames[BatchSubmitterKeyName] = true + keyNames[OracleBridgeExecutorKeyName] = true + + finished := true + + state.BotInfos = CheckIfKeysExist(BotInfos) + for idx, botInfo := range state.BotInfos { + if keyNames[botInfo.KeyName] && botInfo.IsNotExist { + state.BotInfos[idx].IsSetup = true + finished = false + } else { + state.BotInfos[idx].IsSetup = false + } + } + if finished { + return OPInitBotInitSelectExecutor(weavecontext.SetCurrentState(m.Ctx, state)), cmd + } + + state.isSetupMissingKey = true + return NextUpdateOpinitBotKey(weavecontext.SetCurrentState(m.Ctx, state)) case ChallengerOPInitBotInitOption: - return OPInitBotInitSelectChallenger(m.Ctx), cmd + state.InitChallengerBot = true + keyNames := make(map[string]bool) + keyNames[ChallengerKeyName] = true + + finished := true + + state.BotInfos = CheckIfKeysExist(BotInfos) + for idx, botInfo := range state.BotInfos { + fmt.Println(botInfo.KeyName, keyNames[botInfo.KeyName], botInfo.IsNotExist) + if keyNames[botInfo.KeyName] && botInfo.IsNotExist { + state.BotInfos[idx].IsSetup = true + finished = false + } else { + state.BotInfos[idx].IsSetup = false + } + } + if finished { + return OPInitBotInitSelectChallenger(weavecontext.SetCurrentState(m.Ctx, state)), cmd + } + + state.isSetupMissingKey = true + return NextUpdateOpinitBotKey(weavecontext.SetCurrentState(m.Ctx, state)) } } return m, cmd @@ -754,8 +798,13 @@ func WaitStartingInitBot(ctx context.Context) tea.Cmd { version := registry.MustGetOPInitBotsSpecVersion(state.botConfig["l1_node.chain_id"]) config := ExecutorConfig{ - Version: version, - ListenAddress: configMap["listen_address"], + Version: version, + Server: ServerConfig{ + Address: configMap["listen_address"], + AllowOrigins: "*", + AllowHeaders: "Origin, Content-Type, Accept", + AllowMethods: "GET", + }, L1Node: NodeSettings{ ChainID: configMap["l1_node.chain_id"], RPCAddress: configMap["l1_node.rpc_address"], @@ -780,14 +829,17 @@ func WaitStartingInitBot(ctx context.Context) tea.Cmd { GasAdjustment: 1.5, TxTimeout: 60, }, - OutputSubmitter: OutputSubmitterKeyName, - BridgeExecutor: BridgeExecutorKeyName, - BatchSubmitterEnabled: true, - MaxChunks: 5000, - MaxChunkSize: 300000, - MaxSubmissionTime: 3600, - L2StartHeight: 0, - BatchStartHeight: 0, + BridgeExecutor: BridgeExecutorKeyName, + OracleBridgeExecutor: OracleBridgeExecutorKeyName, + MaxChunks: 5000, + MaxChunkSize: 300000, + MaxSubmissionTime: 3600, + L2StartHeight: 0, + BatchStartHeight: 0, + DisableDeleteFutureWithdrawal: false, + DisableAutoSetL1Height: false, + DisableBatchSubmitter: false, + DisableOutputSubmitter: false, } configBz, err := json.MarshalIndent(config, "", " ") if err != nil { @@ -814,8 +866,13 @@ func WaitStartingInitBot(ctx context.Context) tea.Cmd { version := registry.MustGetOPInitBotsSpecVersion(state.botConfig["l1_node.chain_id"]) config := ChallengerConfig{ - Version: version, - ListenAddress: configMap["listen_address"], + Version: version, + Server: ServerConfig{ + Address: configMap["listen_address"], + AllowOrigins: "*", + AllowHeaders: "Origin, Content-Type, Accept", + AllowMethods: "GET", + }, L1Node: NodeConfig{ ChainID: configMap["l1_node.chain_id"], RPCAddress: configMap["l1_node.rpc_address"], @@ -888,3 +945,96 @@ func (m *OPinitBotSuccessful) View() string { return state.weave.Render() + styles.RenderPrompt(fmt.Sprintf("OPInit bot setup successfully. Config file is saved at %s. Feel free to modify it as needed.", filepath.Join(weavecontext.GetOPInitHome(m.Ctx), fmt.Sprintf("%s.json", botConfigFileName))), []string{}, styles.Completed) + "\n" + styles.RenderPrompt("You can start the bot by running `weave opinit-bots start "+botConfigFileName+"`", []string{}, styles.Completed) + "\n" } + +// SetupOPInitBotsMissingKey handles the loading and setup of OPInit bots +type SetupOPInitBotsMissingKey struct { + weavecontext.BaseModel + loading ui.Loading +} + +// NewSetupOPInitBots initializes a new SetupOPInitBots with context +func NewSetupOPInitBotsMissingKey(ctx context.Context) *SetupOPInitBotsMissingKey { + return &SetupOPInitBotsMissingKey{ + BaseModel: weavecontext.BaseModel{Ctx: ctx, CannotBack: true}, + loading: ui.NewLoading("Downloading binary and adding keys...", WaitSetupOPInitBotsMissingKey(ctx)), + } +} + +func (m *SetupOPInitBotsMissingKey) Init() tea.Cmd { + return m.loading.Init() +} + +func (m *SetupOPInitBotsMissingKey) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + loader, cmd := m.loading.Update(msg) + m.loading = loader + if m.loading.Completing { + switch msg := msg.(type) { + case tea.KeyMsg: + if msg.String() == "enter" { + state := weavecontext.GetCurrentState[OPInitBotsState](m.loading.EndContext) + if state.InitExecutorBot { + return OPInitBotInitSelectExecutor(m.loading.EndContext), nil + } else if state.InitChallengerBot { + return OPInitBotInitSelectChallenger(m.loading.EndContext), nil + } + } + } + } + return m, cmd +} + +func (m *SetupOPInitBotsMissingKey) View() string { + state := weavecontext.GetCurrentState[OPInitBotsState](m.Ctx) + if len(state.SetupOpinitResponses) > 0 { + mnemonicText := "" + for _, botName := range BotNames { + if res, ok := state.SetupOpinitResponses[botName]; ok { + keyInfo := strings.Split(res, "\n") + address := strings.Split(keyInfo[0], ": ") + mnemonicText += renderMnemonic(string(botName), address[1], keyInfo[1]) + } + } + + return state.weave.Render() + "\n" + styles.BoldUnderlineText("Important", styles.Yellow) + "\n" + + styles.Text("Write down these mnemonic phrases and store them in a safe place. \nIt is the only way to recover your system keys.", styles.Yellow) + "\n\n" + + mnemonicText + "\nPress enter to go next step\n" + } + return state.weave.Render() + "\n" +} + +func WaitSetupOPInitBotsMissingKey(ctx context.Context) tea.Cmd { + return func() tea.Msg { + state := weavecontext.GetCurrentState[OPInitBotsState](ctx) + userHome, err := os.UserHomeDir() + if err != nil { + return ui.ErrorLoading{Err: fmt.Errorf("failed to get user home directory: %v", err)} + } + + binaryPath := filepath.Join(userHome, common.WeaveDataDirectory, fmt.Sprintf("opinitd@%s", OpinitBotBinaryVersion), AppName) + + opInitHome := weavecontext.GetOPInitHome(ctx) + for _, info := range state.BotInfos { + if info.Mnemonic != "" { + res, err := cosmosutils.OPInitRecoverKeyFromMnemonic(binaryPath, info.KeyName, info.Mnemonic, info.DALayer == string(CelestiaLayerOption), opInitHome) + if err != nil { + return ui.ErrorLoading{Err: fmt.Errorf("failed to recover key from mnemonic: %v", err)} + } + state.SetupOpinitResponses[info.BotName] = res + continue + } + if info.IsGenerateKey { + res, err := cosmosutils.OPInitAddOrReplace(binaryPath, info.KeyName, info.DALayer == string(CelestiaLayerOption), opInitHome) + if err != nil { + return ui.ErrorLoading{Err: fmt.Errorf("failed to add or replace key: %v", err)} + + } + state.SetupOpinitResponses[info.BotName] = res + continue + } + } + + return ui.EndLoading{ + Ctx: ctx, + } + } +} diff --git a/models/opinit_bots/setup.go b/models/opinit_bots/setup.go index cddeea5e..6834cb6d 100644 --- a/models/opinit_bots/setup.go +++ b/models/opinit_bots/setup.go @@ -253,6 +253,11 @@ func NextUpdateOpinitBotKey(ctx context.Context) (tea.Model, tea.Cmd) { return NewRecoverKeySelector(ctx, idx), nil } } + if state.isSetupMissingKey { + model := NewSetupOPInitBotsMissingKey(ctx) + return model, model.Init() + } + model := NewSetupOPInitBots(ctx) return model, model.Init() } @@ -715,10 +720,12 @@ func (m *TerminalState) View() string { state := weavecontext.GetCurrentState[OPInitBotsState](m.Ctx) if len(state.SetupOpinitResponses) > 0 { mnemonicText := "" - for botName, res := range state.SetupOpinitResponses { - keyInfo := strings.Split(res, "\n") - address := strings.Split(keyInfo[0], ": ") - mnemonicText += renderMnemonic(string(botName), address[1], keyInfo[1]) + for _, botName := range BotNames { + if res, ok := state.SetupOpinitResponses[botName]; ok { + keyInfo := strings.Split(res, "\n") + address := strings.Split(keyInfo[0], ": ") + mnemonicText += renderMnemonic(string(botName), address[1], keyInfo[1]) + } } return state.weave.Render() + "\n" + styles.RenderPrompt("Download binary and add keys successfully.", []string{}, styles.Completed) + "\n\n" + diff --git a/models/opinit_bots/setup_test.go b/models/opinit_bots/setup_test.go index 951031c1..768d382c 100644 --- a/models/opinit_bots/setup_test.go +++ b/models/opinit_bots/setup_test.go @@ -21,6 +21,7 @@ func TestProcessingMinitiaConfig_Update_AddKeys(t *testing.T) { {BotName: OutputSubmitter}, {BotName: BatchSubmitter}, {BotName: Challenger}, + {BotName: OracleBridgeExecutor}, } state.MinitiaConfig = &types.MinitiaConfig{ SystemKeys: &types.SystemKeys{ @@ -65,6 +66,7 @@ func TestProcessingMinitiaConfig_Update_SkipKeys(t *testing.T) { {BotName: OutputSubmitter}, {BotName: BatchSubmitter}, {BotName: Challenger}, + {BotName: OracleBridgeExecutor}, } ctx = weavecontext.SetCurrentState(ctx, state) @@ -84,6 +86,7 @@ func TestProcessingMinitiaConfig_Update_SkipKeys(t *testing.T) { assert.Empty(t, state.BotInfos[1].Mnemonic) assert.Empty(t, state.BotInfos[2].Mnemonic) assert.Empty(t, state.BotInfos[3].Mnemonic) + assert.Empty(t, state.BotInfos[4].Mnemonic) } else { t.Errorf("Expected model to be of type *SetupBotCheckbox, but got %T", nextModel) } @@ -99,6 +102,7 @@ func TestSetupBotCheckbox_SelectBots(t *testing.T) { {BotName: OutputSubmitter}, {BotName: BatchSubmitter}, {BotName: Challenger}, + {BotName: OracleBridgeExecutor}, } ctx = weavecontext.SetCurrentState(ctx, state) @@ -123,6 +127,7 @@ func TestSetupBotCheckbox_SelectBots(t *testing.T) { assert.False(t, state.BotInfos[1].IsSetup) // OutputSubmitter assert.True(t, state.BotInfos[2].IsSetup) // BatchSubmitter assert.False(t, state.BotInfos[3].IsSetup) // Challenger + assert.False(t, state.BotInfos[4].IsSetup) // Challenger } else { t.Errorf("Expected model to transition to *NextUpdateOpinitBotKey, but got %T", nextModel) } @@ -138,6 +143,7 @@ func TestSetupBotCheckbox_NoSelection(t *testing.T) { {BotName: OutputSubmitter}, {BotName: BatchSubmitter}, {BotName: Challenger}, + {BotName: OracleBridgeExecutor}, } ctx = weavecontext.SetCurrentState(ctx, state) diff --git a/models/opinit_bots/state.go b/models/opinit_bots/state.go index 748e76b7..09925a49 100644 --- a/models/opinit_bots/state.go +++ b/models/opinit_bots/state.go @@ -24,6 +24,7 @@ type OPInitBotsState struct { daIsCelestia bool dbPath string isDeleteDB bool + isSetupMissingKey bool } // NewOPInitBotsState initializes OPInitBotsState with default values @@ -34,6 +35,7 @@ func NewOPInitBotsState() OPInitBotsState { weave: types.NewWeaveState(), MinitiaConfig: nil, botConfig: make(map[string]string), + isSetupMissingKey: false, } } @@ -57,8 +59,13 @@ func (state OPInitBotsState) Clone() OPInitBotsState { botConfig: make(map[string]string), dbPath: state.dbPath, isDeleteDB: state.isDeleteDB, + isSetupMissingKey: state.isSetupMissingKey, } + if state.MinitiaConfig != nil { + clone.MinitiaConfig = state.MinitiaConfig.Clone() + } + clone.weave = state.weave.Clone() // Copy slice data copy(clone.BotInfos, state.BotInfos) diff --git a/types/minitia_config.go b/types/minitia_config.go index 1fcbe453..b905419c 100644 --- a/types/minitia_config.go +++ b/types/minitia_config.go @@ -76,3 +76,74 @@ type Artifacts struct { ExecutorL1MonitorHeight string `json:"EXECUTOR_L1_MONITOR_HEIGHT"` ExecutorL2MonitorHeight string `json:"EXECUTOR_L2_MONITOR_HEIGHT"` } + +// Clone returns a deep copy of MinitiaConfig. +// Returns nil if the receiver is nil. +func (m *MinitiaConfig) Clone() *MinitiaConfig { + if m == nil { + return nil + } + + clone := &MinitiaConfig{ + L1Config: nil, + L2Config: nil, + OpBridge: nil, + SystemKeys: nil, + GenesisAccounts: nil, + } + + if m.L1Config != nil { + clone.L1Config = &L1Config{ + ChainID: m.L1Config.ChainID, + RpcUrl: m.L1Config.RpcUrl, + GasPrices: m.L1Config.GasPrices, + } + } + // Similar deep copy for other fields... + if m.L2Config != nil { + clone.L2Config = &L2Config{ + ChainID: m.L2Config.ChainID, + Denom: m.L2Config.Denom, + Moniker: m.L2Config.Moniker, + BridgeID: m.L2Config.BridgeID, + } + } + + if m.OpBridge != nil { + clone.OpBridge = &OpBridge{ + OutputSubmissionInterval: m.OpBridge.OutputSubmissionInterval, + OutputFinalizationPeriod: m.OpBridge.OutputFinalizationPeriod, + OutputSubmissionStartHeight: m.OpBridge.OutputSubmissionStartHeight, + BatchSubmissionTarget: m.OpBridge.BatchSubmissionTarget, + EnableOracle: m.OpBridge.EnableOracle, + } + } + + if m.SystemKeys != nil { + clone.SystemKeys = &SystemKeys{ + Validator: cloneSystemAccount(m.SystemKeys.Validator), + BridgeExecutor: cloneSystemAccount(m.SystemKeys.BridgeExecutor), + OutputSubmitter: cloneSystemAccount(m.SystemKeys.OutputSubmitter), + BatchSubmitter: cloneSystemAccount(m.SystemKeys.BatchSubmitter), + Challenger: cloneSystemAccount(m.SystemKeys.Challenger), + } + } + if m.GenesisAccounts != nil { + accs := make(GenesisAccounts, len(*m.GenesisAccounts)) + copy(accs, *m.GenesisAccounts) + clone.GenesisAccounts = &accs + } + return clone +} + +func cloneSystemAccount(acc *SystemAccount) *SystemAccount { + if acc == nil { + return nil + } + return &SystemAccount{ + L1Address: acc.L1Address, + L2Address: acc.L2Address, + DAAddress: acc.DAAddress, + Mnemonic: acc.Mnemonic, + } +}