This repository contains a modern TUI wallet for the DERO blockchain built with Bubble Tea.
# Build the TUI wallet binary
go build -o derotui ./cmd/
# Run all tests
go test -v ./...
# Format code
go fmt ./...
# Static analysis
go vet ./...cmd/
└── main.go # Entry point, CLI parsing, daemon connectivity setup
internal/
├── config/
│ └── config.go # User config (~/.derotui.json) for last wallet
├── ui/
│ ├── app.go # Main application model, page routing, message handling
│ ├── mouse.go # MouseablePage interface definition
│ ├── components/
│ │ └── input.go # Styled text input wrapper
│ ├── pages/
│ │ ├── dashboard.go # Main dashboard with balance, activity, quick actions
│ │ ├── daemon.go # Daemon connection input page
│ │ ├── history.go # Transaction history (custom color-coded table)
│ │ ├── integratedaddr.go # Integrated address (payment request) generation
│ │ ├── keyinput.go # Hex key input/display (restore & view)
│ │ ├── network.go # Network selection (Mainnet/Testnet/Simulator)
│ │ ├── password.go # Password input (unlock/create)
│ │ ├── qrcode.go # QR code display page
│ │ ├── seed.go # Seed phrase input/display (restore & view)
│ │ ├── send.go # Send DERO form with Payment ID support
│ │ ├── txdetails.go # Transaction details with copy keybindings
│ │ ├── welcome.go # Welcome page with slash command menu
│ │ └── xswd.go # XSWD authorization and permission dialogs
│ └── styles/
│ └── styles.go # Lipgloss styles, theme colors, ASCII art
└── wallet/
├── wallet.go # Wallet wrapper around DERO walletapi
└── types.go # Type definitions (WalletInfo, TransactionInfo, etc.)
- Primary Color: Neon purple (#BF40FF)
- Background: Black (#000000)
- Card Background: Dark purple (#1E1B2E)
- Accent colors:
- Green (#10B981) for success/synced
- Orange (#F59E0B) for warning/syncing
- Cyan (#06B6D4) for simulator indicator
- Red (#EF4444) for errors
- Slash command menu: Type "/" to open command autocomplete
/open- Open an existing wallet (file picker)/create- Create a new wallet/restore- Restore wallet (submenu: From Seed, From Key)/connect- Connect to a daemon/exit- Exit the application
- Menu opens upward above the input field
- Daemon status display: Shows "Block: " and "Daemon: "
- Block number is green when synced, yellow when syncing
- Daemon address shown in green when connected
- Stores last opened wallet path in
~/.derotui.json - On startup, automatically prompts for password if a previous wallet exists
- Shows wallet file path in the password prompt
- Supports multiple wallet networks (Mainnet, Testnet, Simulator) saved per-wallet
- Balance display: Large balance with DERO symbol (◆)
- Recent Activity: Shows last 5 transactions
- Paginated Quick Actions (2 pages):
- Page 1 - Core:
[S]Send DERO[C]Copy Address[Y]QR Code[H]History[G]Register Wallet (shown only when unregistered)
- Page 2 - Advanced:
[V]View Seed[K]View Hex Key[P]Change Password[R]Payment Request (Integrated Address)[X]XSWD Server Toggle
- Page 1 - Core:
- Address display at bottom (truncated to 50 chars)
- Footer: Shows all keyboard shortcuts with page indicator
- Flash messages: Success/error notifications (green/red)
- Display mode: Electrum-style card grid with 25 cards (5x5)
- Each card shows: word number (gray) + word (neon purple, bold)
- Card background: dark purple (#1E1B2E)
- Gap between cards: 1 character
- Input mode: Textarea for entering 25-word seed
- Real-time validation: Words validated against BIP39 wordlist as you type
- Error persistence: Errors stay visible until user starts typing
- Copy to clipboard: Press
Cto copy seed phrase
- Display mode: Shows 64-character hex key split into two lines
- Input mode: Text input for 64-character hex key
- Validation: Checks length and hex characters
- Copy to clipboard: Press
Cto copy hex key
- Displays wallet receive address as QR code
- Uses Unicode half-block characters for compact rendering
- Shows full address in text below QR code
- Copy: Press
Cto copy address
- Auto-detect local daemon on startup (Mainnet, Testnet, Simulator)
- Multi-daemon detection: Prompts user to select if multiple daemons detected
- Keep_Connectivity() background goroutine maintains connection
- Periodic status refresh every 5 seconds
- Default ports: Mainnet 10102, Testnet 40402, Simulator 20000
- Placeholder shows appropriate localhost:port for guidance
- Auto-append port if user doesn't specify one
- Custom table implementation (not using bubbles table)
- Color coding:
- Green text for MINED/IN transactions
- Red text for OUT transactions
- Full-row selection: Purple background + yellow text across all columns
- Manual scroll: Cursor + offset management
- Columns: Date, Type, Amount, Fee, Confirmations
- Mouse support: Click to select, double-click for details
- Keyboard: Arrow keys to navigate, Enter for details
- Section-based layout with headers (
── Section Name ─────────) - Sections: Overview, Block, Transaction, Payment ID (conditional)
- Fields: Type, Status, Amount, Fee, Burn, Height, Topo Height, Timestamp, TxID, Block Hash, Sender, Destination, Proof, Message, Payment ID/Port
- Word wrapping: Long messages are wrapped to fit width (52 chars visible per line)
- Truncation: Hashes/addresses truncated at 52 chars with
... - Copy keybindings:
[T]Copy TxID[B]Copy Block Hash[H]Copy Height[S]Copy Sender[D]Copy Destination[P]Copy Proof[M]Copy Message[I]Copy Destination Port
- Mouse support: Click on any value row to copy
- DERO HE uses
RPC_DESTINATION_PORT(uint64) instead of legacy 64-char hex Payment IDs - Integrated addresses (
deroi...) embed port and comment in arguments - Send form recipient accepts plain usernames (no
@) and resolves them through daemon name service - Send form: Payment ID field validates as uint64 numeric (max 20 digits)
- Integrated address detection: Auto-fills Payment ID and Message from address
- Visual indicator: Shows "✓ (from address)" when port extracted from integrated address
- Max address length: 300 characters (integrated addresses can be up to 297-298 chars)
- Shown when opening a wallet with unknown network
- Options: Mainnet (dero1...), Testnet (deto1...), Simulator (deto1...)
- Network selection is saved for future wallet opens
- Each wallet remembers its network in
~/.derotui.json - Colors: Mainnet = green, Testnet = orange, Simulator = cyan
- Open: File picker for .db files
- Create: Generates new wallet, displays seed phrase
- Restore from Seed: 25-word seed phrase input with validation
- Restore from Key: 64-character hex key input
- Online mode: Calls
SetOnlineMode()to start sync loop - Balance sync: Uses DERO's encrypted balance system
- Password change: In-app password changing via dashboard
- Generate: Create integrated addresses with embedded destination port and optional comment
- QR Display: Show QR code for generated integrated address
- Copy: Copy integrated address to clipboard
- Auto-fill: When sending to integrated addresses, destination port and comment are auto-extracted
- WebSocket Protocol: Allows dApps to connect on port 44326 via
/xswdendpoint - User Control: Toggle server on/off from dashboard (key
X) - App Authorization: Dialog shows connecting app's name, description, and URL for user approval
- Permission System: Per-method permission requests with options:
- Allow (this request only)
- Deny (this request only)
- Always Allow (all future requests for this method)
- Always Deny (all future requests for this method)
- Callback Bridge: XSWD callbacks block on channels while TUI displays dialogs, responses sent back through channels
Two categories of selection across the app:
A. List/Menu Selection
- Selected: Neon purple
▸prefix + white bold text - Unselected: No prefix + muted gray text
- Used in: Welcome command menu, Restore submenu, Network selection
B. Form Element Focus
- Border color changes from
ColorBordertoColorPrimary - Used in: Send form fields, input boxes
C. Table Row Selection
- Full-row purple background + yellow text
- Used in: Transaction history
type Model struct {
page Page // Current page enum
wallet *wallet.Wallet // Wallet instance
// ... page models and state
}const (
PageWelcome Page = iota // 0
PageFilePicker // 1
PagePassword // 2
PageNetwork // 3 - Network selection
PageSeed // 4
PageKeyInput // 5
PageQRCode // 6 - QR code display
PageMain // 7 - Dashboard
PageSend // 8 - Send form
PageHistory // 9 - Transaction history
PageTxDetails // 10 - Transaction details
PageDaemon // 11 - Daemon connection
PageIntegratedAddr // 12 - Generate integrated address
PageXSWDAuth // 13 - XSWD app authorization
PageXSWDPerm // 14 - XSWD permission request
)tickMsg- Periodic updates (5 second interval)daemonStatusMsg- Daemon connection statusstartupCheckMsg- Last wallet check on startupwalletOpenedMsg- Wallet open resultwalletCreatedMsg- Wallet creation resultwalletRestoredMsg- Wallet restore resulttransferResultMsg- Transfer resultpasswordChangedMsg- Password change resultwalletDaemonConnectedMsg- Async daemon connection resultnetworkRequiredMsg- Wallet needs network selectiondaemonConnectMsg- Daemon connection attempt result
S- Send DEROC- Copy addressY- QR CodeR- Payment Request (Integrated Address)V- View seed phraseK- View hex keyH- HistoryP- Change passwordX- Toggle XSWD serverG- Register wallet (when unregistered)←/→orh/l- Switch action pagesEsc- Close wallet, return to welcomeQ- Quit applicationCtrl+C- Force quit
Esc- Close details, return to historyT- Copy TxIDB- Copy Block HashH- Copy Block HeightS- Copy SenderD- Copy DestinationP- Copy ProofM- Copy MessageI- Copy Destination Port
- All pages implement
HandleMouse(msg tea.MouseMsg, width, height int) bool - Dashboard: Click quick actions, click page indicators, click transactions
- History: Click to select, double-click for details
- Transaction Details: Click rows to copy
- Send form: Click fields, buttons
- QR Code: Click anywhere to close
- Network selection: Click options
- Welcome: Click menu items
- Daemon selection: Double-click to confirm
- XSWD dialogs: Click buttons
All source files should include:
// Copyright 2017-2026 DERO Project. All rights reserved.Use grouped import blocks:
import (
"fmt"
"strings"
"github.com/atotto/clipboard"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/deroproject/dero-wallet-cli/internal/ui/styles"
)Each page/component follows this pattern:
type PageModel struct {
// state fields
}
func NewPage() PageModel { ... }
func (p PageModel) Init() tea.Cmd { ... }
func (p PageModel) Update(msg tea.Msg) (PageModel, tea.Cmd) { ... }
func (p PageModel) View() string { ... }- Store
currentErrorat start of Update - Clear error on key press (user typing)
- Preserve error for non-KeyMsg (cursor blink, etc.)
- Use
styles.TitleStyle,styles.MutedStyle, etc. from styles package - Use
lipgloss.JoinVertical()for composing views - Use
styles.BoxStylefor bordered containers - CRITICAL:
lipgloss.Width()and centering functions don't properly handle ANSI escape codes. The escape codes are counted as part of string length, causing truncation and misalignment.
Reliable pattern for centered/styled text:
- Calculate visible text length manually
- Pad with plain spaces to target width
- Apply lipgloss styles to the already-padded string
- Concatenate with
strings.Joinor+(NOTJoinHorizontalfor multi-colored segments)
--wallet-file <file> Wallet file path
--password <password> Wallet password
--offline Run in offline mode
--offline-datafile <file> Offline data file (default "getoutputs.bin")
--rpc-server Enable RPC server
--rpc-bind <addr> RPC bind address (default "127.0.0.1:20209")
--rpc-login <user:pass> RPC credentials
--allow-rpc-password-change Allow RPC password change via RPC
--testnet Use testnet
--simulator Connect to simulator (port 20000)
--daemon-address <addr> Daemon address (default "localhost:10102")
--socks-proxy <ip:port> SOCKS proxy
--generate-new-wallet Generate new wallet
--restore-deterministic-wallet Restore from seed
--electrum-seed <seed> Recovery seed
--unlocked Keep wallet unlocked
--debug Enable debug loggingwalletapi.Open_Encrypted_Wallet()- Open existing walletwalletapi.Create_Encrypted_Wallet_Random()- Create new walletwalletapi.Create_Encrypted_Wallet_From_Recovery_Words()- Restore from seedwalletapi.Create_Encrypted_Wallet()- Restore from keywallet.SetDaemonAddress()- Set daemon endpointwallet.SetOnlineMode()- Enable sync loopwallet.Get_Balance()- Get wallet balancewallet.GetSeed()- Get seed phrasewallet.Get_Keys()- Get private keyswalletapi.Connect()- Establish WebSocket connectionwalletapi.Keep_Connectivity()- Background connection maintenance
walletapi.Daemon_Endpoint_Active- Active daemon address used by Keep_Connectivitywalletapi.Connected- Connection status flag
- Type
/to open command menu/open- Open an existing wallet/create- Create a new wallet/restore- Restore wallet (submenu: From Seed, From Key)/connect- Connect to a daemon/exit- Exit the application
- Arrow keys to navigate menu
- Enter to select
- Type command name for autocomplete
- Page 1 - Core:
S- Send DEROC- Copy addressY- QR CodeH- HistoryG- Register wallet (when unregistered)
- Page 2 - Advanced:
V- View seed phraseK- View hex keyP- Change passwordR- Payment Request (Integrated Address)X- Toggle XSWD server
←/→orh/l- Switch action pages- Mouse click on page indicators or quick actions
Escto close walletQto quit
- Arrow keys to navigate
- Enter to view details
- Mouse click to select
- Mouse double-click for details
- Esc to go back
T- Copy TxIDB- Copy Block HashH- Copy Block HeightS- Copy SenderD- Copy DestinationP- Copy ProofM- Copy MessageI- Copy Destination Port (Payment ID)Escto go back- Mouse click on any value row to copy
- Tab/Shift+Tab to switch fields
- Arrow keys for ring size selection
- Enter to send (when valid)
- Esc to cancel
- Tab to switch fields
- Enter to generate
Yto show QR codeCto copy integrated address- Esc to close
A- Accept connectionR- Reject connection- Tab to switch between buttons
- Enter to confirm
- Esc to reject
1- Allow (this request only)2- Deny (this request only)3- Always Allow (all future requests for this method)4- Always Deny (all future requests for this method)- ↑/↓ to navigate
- Enter to confirm
- Esc to deny
Enterto confirmEscto cancel/go backTabto switch fields (password creation)Cto copy (seed/key/QR display)
- Never log passwords or seeds
- Wallet files handled securely via DERO's encrypted wallet API
- Password input uses masked characters
- Seed/key display includes warning about not sharing
- Confirmation required for sensitive operations
When using lipgloss.Width() or lipgloss.Center() on strings containing ANSI escape codes (styled/colored text), the escape codes are included in the character count. This causes:
- Incorrect width calculations
- Truncation of visible text
- Misalignment of columns
Solution for the Seed Card Grid:
// Calculate visible text length manually
visibleLen := len(word)
// Pad with plain spaces
padding := strings.Repeat(" ", cardWidth-visibleLen)
// Apply styles to padded string
styledWord := lipgloss.NewStyle().Foreground(ColorPrimary).Bold(true).Render(word + padding)The transaction history uses a custom implementation rather than bubbles/table to achieve:
- Full-row selection highlighting (purple background, yellow text)
- Color-coded transaction types (green for IN, red for OUT)
- Manual column alignment without lipgloss.Width on styled text
- Control over scroll behavior and cursor position
DERO wallet API pads payloads with zeros to 144 bytes. CBOR decoder fails with "extraneous data" error. Fix: Trim trailing zeros from payload before CBOR unmarshaling:
payload = bytes.TrimRight(payload, "\x00")Integrated addresses embed arguments (RPC_DESTINATION_PORT, RPC_COMMENT) in the address itself. When parsing:
- Detect with
addr.IsIntegratedAddress() - Extract arguments with
addr.Arguments - Validate with
arguments.Validate_Arguments() - Copy ALL embedded arguments (not just destination port)