package txmodel import ( "fmt" "strings" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/tx" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" ) const maxWidth = 100 var ( red = lipgloss.AdaptiveColor{Light: "#FE5F86", Dark: "#FE5F86"} indigo = lipgloss.AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"} green = lipgloss.AdaptiveColor{Light: "#02BA84", Dark: "#02BF87"} ) type Styles struct { Base, HeaderText, Status, StatusHeader, Highlight, ErrorHeaderText, Help lipgloss.Style } func NewStyles(lg *lipgloss.Renderer) *Styles { s := Styles{} s.Base = lg.NewStyle(). Padding(1, 2, 0, 1) s.HeaderText = lg.NewStyle(). Foreground(indigo). Bold(true). Padding(0, 1, 0, 1) s.Status = lg.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(indigo). PaddingLeft(1). MarginTop(1) s.StatusHeader = lg.NewStyle(). Foreground(green). Bold(true) s.Highlight = lg.NewStyle(). Foreground(lipgloss.Color("212")) s.ErrorHeaderText = s.HeaderText. Foreground(red) s.Help = lg.NewStyle(). Foreground(lipgloss.Color("240")) return &s } type state int const ( statusNormal state = iota stateDone ) type Model struct { state state lg *lipgloss.Renderer styles *Styles form *huh.Form width int message *tx.TxBody } func NewModel() Model { m := Model{width: maxWidth} m.lg = lipgloss.DefaultRenderer() m.styles = NewStyles(m.lg) m.form = huh.NewForm( huh.NewGroup( huh.NewInput(). Key("from"). Title("From Address"). Placeholder("cosmos1..."). Validate(func(s string) error { if !strings.HasPrefix(s, "cosmos1") { return fmt.Errorf("invalid address format") } return nil }), huh.NewInput(). Key("to"). Title("To Address"). Placeholder("cosmos1..."). Validate(func(s string) error { if !strings.HasPrefix(s, "cosmos1") { return fmt.Errorf("invalid address format") } return nil }), huh.NewInput(). Key("amount"). Title("Amount"). Placeholder("100"). Validate(func(s string) error { if _, err := sdk.ParseCoinNormalized(s + "atom"); err != nil { return fmt.Errorf("invalid coin amount") } return nil }), huh.NewSelect[string](). Key("denom"). Title("Denom"). Options(huh.NewOptions("atom", "osmo", "usnr", "snr")...), huh.NewInput(). Key("memo"). Title("Memo"). Placeholder("Optional"), huh.NewConfirm(). Key("done"). Title("Ready to convert?"). Validate(func(v bool) error { if !v { return fmt.Errorf("Please confirm when you're ready to convert") } return nil }). Affirmative("Yes, convert!"). Negative("Not yet"), ), ). WithWidth(60). WithShowHelp(false). WithShowErrors(false) return m } func (m Model) Init() tea.Cmd { return m.form.Init() } func min(x, y int) int { if x > y { return y } return x } func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.width = min(msg.Width, maxWidth) - m.styles.Base.GetHorizontalFrameSize() case tea.KeyMsg: switch msg.String() { case "esc", "ctrl+c", "q": return m, tea.Quit } } var cmds []tea.Cmd form, cmd := m.form.Update(msg) if f, ok := form.(*huh.Form); ok { m.form = f cmds = append(cmds, cmd) } if m.form.State == huh.StateCompleted { m.buildMessage() cmds = append(cmds, tea.Quit) } return m, tea.Batch(cmds...) } func (m Model) View() string { s := m.styles switch m.form.State { case huh.StateCompleted: pklCode := m.generatePkl() messageView := m.getMessageView() var b strings.Builder fmt.Fprintf(&b, "Final Tx:\n\n%s\n\n%s", pklCode, messageView) return s.Status.Margin(0, 1).Padding(1, 2).Width(80).Render(b.String()) + "\n\n" default: var schemaType string if m.form.GetString("schemaType") != "" { schemaType = "Schema Type: " + m.form.GetString("schemaType") } v := strings.TrimSuffix(m.form.View(), "\n\n") form := m.lg.NewStyle().Margin(1, 0).Render(v) var status string { preview := "(Preview will appear here)" if m.form.GetString("schema") != "" { preview = m.generatePkl() } const statusWidth = 40 statusMarginLeft := m.width - statusWidth - lipgloss.Width(form) - s.Status.GetMarginRight() status = s.Status. Height(lipgloss.Height(form)). Width(statusWidth). MarginLeft(statusMarginLeft). Render(s.StatusHeader.Render("Pkl Preview") + "\n" + schemaType + "\n\n" + preview) } errors := m.form.Errors() header := m.appBoundaryView("Sonr TX Builder") if len(errors) > 0 { header = m.appErrorBoundaryView(m.errorView()) } body := lipgloss.JoinHorizontal(lipgloss.Top, form, status) footer := m.appBoundaryView(m.form.Help().ShortHelpView(m.form.KeyBinds())) if len(errors) > 0 { footer = m.appErrorBoundaryView("") } return s.Base.Render(header + "\n" + body + "\n\n" + footer) } } func (m Model) errorView() string { var s string for _, err := range m.form.Errors() { s += err.Error() } return s } func (m Model) appBoundaryView(text string) string { return lipgloss.PlaceHorizontal( m.width, lipgloss.Left, m.styles.HeaderText.Render(text), lipgloss.WithWhitespaceChars("="), lipgloss.WithWhitespaceForeground(indigo), ) } func (m Model) appErrorBoundaryView(text string) string { return lipgloss.PlaceHorizontal( m.width, lipgloss.Left, m.styles.ErrorHeaderText.Render(text), lipgloss.WithWhitespaceChars("="), lipgloss.WithWhitespaceForeground(red), ) } func (m Model) generatePkl() string { schemaType := m.form.GetString("schemaType") schema := m.form.GetString("schema") // This is a placeholder for the actual conversion logic // In a real implementation, you would parse the schema and generate Pkl code return fmt.Sprintf("// Converted from %s\n\nclass ConvertedSchema {\n // TODO: Implement conversion from %s\n // Original schema:\n /*\n%s\n */\n}", schemaType, schemaType, schema) } func (m *Model) buildMessage() { from := m.form.GetString("from") to := m.form.GetString("to") amount := m.form.GetString("amount") denom := m.form.GetString("denom") memo := m.form.GetString("memo") coin, _ := sdk.ParseCoinNormalized(fmt.Sprintf("%s%s", amount, denom)) sendMsg := &banktypes.MsgSend{ FromAddress: from, ToAddress: to, Amount: sdk.NewCoins(coin), } anyMsg, _ := codectypes.NewAnyWithValue(sendMsg) m.message = &tx.TxBody{ Messages: []*codectypes.Any{anyMsg}, Memo: memo, } } func (m Model) getMessageView() string { if m.message == nil { return "Current Message: None" } interfaceRegistry := codectypes.NewInterfaceRegistry() marshaler := codec.NewProtoCodec(interfaceRegistry) jsonBytes, _ := marshaler.MarshalJSON(m.message) return fmt.Sprintf("Current Message:\n%s", string(jsonBytes)) } func RunBuildTxnTUI() (*tx.TxBody, error) { m := NewModel() p := tea.NewProgram(m) finalModel, err := p.Run() if err != nil { return nil, fmt.Errorf("failed to run program: %w", err) } finalM, ok := finalModel.(Model) if !ok || finalM.message == nil { return nil, fmt.Errorf("form not completed") } return finalM.message, nil }