Skip to content

Commit

Permalink
Merge branch 'v1.2.0' into chore/bootstrap-offer-fast-catchup-conditi…
Browse files Browse the repository at this point in the history
…onally

# Conflicts:
#	cmd/bootstrap.go
#	cmd/root.go
PhearZero committed Jan 21, 2025

Verified

This commit was signed with the committer’s verified signature.
dchassin David P. Chassin
2 parents 21b6db5 + a808fe2 commit 96999f4
Showing 29 changed files with 292 additions and 150 deletions.
Binary file removed .DS_Store
Binary file not shown.
19 changes: 19 additions & 0 deletions .decisions/2-Release-Cycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# ℹ️ Overview

The release cycle is largely based on the internal Algorand Foundation's engineering compass.
It has been tailored to work with a cli based tool with binary artifacts

## ✅ Decisions

- **SHOULD** use conventional commits for consistency
- **SHOULD** use automated release tools like semantic-release
- **SHOULD** build for arm/amd64 on darwin and linux
- **SHOULD** have checks on code quality and regressions
- **SHOULD** present a manual page and generated reference material
- **SHOULD** provide handcrafted guides and documentation

## 🔨 Deliverables

- automated CI/CD release actions
- installer features for consumers
- marketing landing page with documentation site
17 changes: 17 additions & 0 deletions .decisions/3-Node-Management.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# ℹ️ Overview

Node Management has many aspects which this decision makes concrete.

## ✅ Decisions

- **SHOULD** include install/upgrade commands
- **SHOULD** include start/stop commands
- **SHOULD** include catchup commands
- **SHOULD** include bootstrap command

## 🔨 Deliverables

- Use package managers for installation and upgrades (brew, dnf, apt-get)
- Use native supervisors for algod orchestration (launchd, systemd)
- Bootstrap concept which ties several components together (install, start, fast-catchup, launch TUI)
- Limited amount of configurations for the initial release
4 changes: 3 additions & 1 deletion .decisions/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Decisions for this Project

- [1. GoLang/Charm TUI](1-GoLang-Charm.md)
- [1. GoLang/Charm TUI](1-GoLang-Charm.md)
- [2. Release Cycle](2-Release-Cycle.md)
- [3. Node Management](3-Node-Management.md)
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.DS_Store
nodekit
coverage
bin
37 changes: 37 additions & 0 deletions .tapes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Overview

Includes various [vhs](https://github.com/charmbracelet/vhs) tapes for the project.
Useful for creating consistent demos and guides when the TUI updates


## Get Started

Install vhs

```bash
go install github.com/charmbracelet/vhs@latest
```

Copy the default `tui.tape` and name it appropriately

```bash
cp ./tui.tape ./my-demo.tape
```

Edit the tape with your favorite editor.
Then you can run the vhs tape

(Make sure to update the output file)

```bash
vhs ./my-demo.tape
```

### Theme

Example theme that uses some of the official Algorand Foundation brand guides

```
Set Theme { "name": "Whimsy", "black": "#2D2DFI", "red": "#ef6487", "green": "#5eca89", "yellow": "#fdd877", "blue": "#65aef7", "magenta": "#aa7ff0", "cyan": "#43c1be", "white": "#ffffff", "brightBlack": "#535178", "brightRed": "#ef6487", "brightGreen": "#5eca89", "brightYellow": "#fdd877", "brightBlue": "#65aef7", "brightMagenta": "#aa7ff0", "brightCyan": "#43c1be", "brightWhite": "#ffffff", "background": "#001324", "foreground": "#b3b0d6", "selection": "#3d3c58", "cursor": "#b3b0d6" }
```

25 changes: 25 additions & 0 deletions .tapes/tui.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Output ../assets/tapes/tui.gif

Require nodekit

Set Framerate 30
Set Margin 1

Set Theme { "name": "Whimsy", "black": "#2D2DFI", "red": "#ef6487", "green": "#5eca89", "yellow": "#fdd877", "blue": "#65aef7", "magenta": "#aa7ff0", "cyan": "#43c1be", "white": "#ffffff", "brightBlack": "#535178", "brightRed": "#ef6487", "brightGreen": "#5eca89", "brightYellow": "#fdd877", "brightBlue": "#65aef7", "brightMagenta": "#aa7ff0", "brightCyan": "#43c1be", "brightWhite": "#ffffff", "background": "#001324", "foreground": "#b3b0d6", "selection": "#3d3c58", "cursor": "#b3b0d6" }

Set Shell "bash"
Set FontSize 10
Set Width 1280
Set Height 640

Type "nodekit" Sleep 500ms Enter

Sleep 3s Enter

Sleep 500ms Down Sleep 1s Enter

Sleep 1s Type "o"

Sleep 5s

Ctrl+C
31 changes: 8 additions & 23 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -22,28 +22,8 @@ Build the project
make build
```

Optionally, run a sandboxed participation node


```bash
docker compose up
```

Create a configuration file for the participation node in the root directory of the project (.nodekit.yaml)

```yaml
algod-endpoint: http://localhost:8080
algod-token: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
```
Launch the TUI
> [!NOTE]
> If you skipped the docker container or config file, try running `./bin/nodekit` standalone,
> which will detect your algorand data directory from the `ALGORAND_DATA` environment variable that works for `goal`.
> Otherwise, provide the `--algod-endpoint` and `--algod-token` arguments so that it can find your node.
> Note that nodekit requires the admin algod token.
Launch the TUI.
See the [full documentation](https://nodekit.run/guides/getting-started/) for a complete guide

```bash
./bin/nodekit
@@ -52,7 +32,7 @@ Launch the TUI
# 📂 Folder Structure

```bash
├── api # Generated API Client
├── api # Generated API Client and Hand Crafted HTTPInterface
├── cmd # Command Controller
├── internal # Data Models/Fetch Wrappers
└── ui # BubbleTea Interfaces
@@ -142,3 +122,8 @@ The full command for reference
```bash
oapi-codegen -config generate.yaml https://raw.githubusercontent.com/algorand/go-algorand/v3.26.0-stable/daemon/algod/api/algod.oas3.yml
```

# Submitting Changes

This project follows [GitHub flow](https://githubflow.github.io/).
Create a fork and submit changes directly to the `main` branch.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 Algorand Foundation

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Binary file added assets/tapes/tui.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions cmd/bootstrap.go
Original file line number Diff line number Diff line change
@@ -66,7 +66,7 @@ var bootstrapCmd = &cobra.Command{
if err != nil {
log.Fatal(err)
}
err = runTUI(RootCmd, dir)
err = runTUI(RootCmd, dir, false)
if err != nil {
log.Fatal(err)
}
@@ -176,6 +176,6 @@ var bootstrapCmd = &cobra.Command{

}

return runTUI(RootCmd, dataDir)
return runTUI(RootCmd, dataDir, false)

Check warning on line 179 in cmd/bootstrap.go

Codecov / codecov/patch

cmd/bootstrap.go#L179

Added line #L179 was not covered by tests
},
}
10 changes: 7 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -24,6 +24,9 @@ var (

NeedsUpgrade = false

// whether the user forbids incentive eligibility fees to be set
IncentivesDisabled = false

// algodEndpoint defines the URI address of the Algorand node, including the protocol (http/https), for client communication.
algodData string

@@ -53,7 +56,7 @@ var (
},
Run: func(cmd *cobra.Command, args []string) {
log.SetOutput(cmd.OutOrStdout())
err := runTUI(cmd, algodData)
err := runTUI(cmd, algodData, IncentivesDisabled)

Check warning on line 59 in cmd/root.go

Codecov / codecov/patch

cmd/root.go#L59

Added line #L59 was not covered by tests
if err != nil {
log.Fatal(err)
}
@@ -90,6 +93,7 @@ func NeedsToBeStopped(cmd *cobra.Command, args []string) {
// init initializes the application, setting up logging, commands, and version information.
func init() {
log.SetReportTimestamp(false)
RootCmd.Flags().BoolVarP(&IncentivesDisabled, "no-incentives", "n", false, style.LightBlue("Disable setting incentive eligibility fees"))
RootCmd.SetVersionTemplate(fmt.Sprintf("nodekit-%s-%s@{{.Version}}\n", runtime.GOARCH, runtime.GOOS))
// Add Commands
if runtime.GOOS != "windows" {
@@ -112,7 +116,7 @@ func Execute(version string, needsUpgrade bool) error {
return RootCmd.Execute()
}

func runTUI(cmd *cobra.Command, dataDir string) error {
func runTUI(cmd *cobra.Command, dataDir string, incentivesFlag bool) error {
if cmd == nil {
return fmt.Errorf("cmd is nil")
}

Check warning on line 122 in cmd/root.go

Codecov / codecov/patch

cmd/root.go#L119-L122

Added lines #L119 - L122 were not covered by tests
@@ -124,7 +128,7 @@ func runTUI(cmd *cobra.Command, dataDir string) error {
cobra.CheckErr(err)

// Fetch the state and handle any creation errors
state, stateResponse, err := algod.NewStateModel(ctx, client, httpPkg)
state, stateResponse, err := algod.NewStateModel(ctx, client, httpPkg, incentivesFlag)
utils.WithInvalidResponsesExplanations(err, stateResponse, cmd.UsageString())
cobra.CheckErr(err)

4 changes: 2 additions & 2 deletions cmd/utils/flags.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package utils

import (
"fmt"
"github.com/algorandfoundation/nodekit/api"
"github.com/algorandfoundation/nodekit/cmd/utils/explanations"
"github.com/algorandfoundation/nodekit/internal/algod"
@@ -20,8 +21,7 @@ func WithInvalidResponsesExplanations(err error, response api.ResponseInterface,
}
if response.StatusCode() > 300 {
log.Fatal(
style.Red.Render("failed to get status: error code %d")+"\n\n"+explanations.TokenNotAdmin+"\n"+postFix,
response.StatusCode())
style.Red.Render(fmt.Sprintf("failed to get status: error code %d", response.StatusCode())) + "\n\n" + explanations.TokenNotAdmin + "\n" + postFix)
}
}

12 changes: 10 additions & 2 deletions internal/algod/mac/mac.go
Original file line number Diff line number Diff line change
@@ -113,11 +113,19 @@ func Upgrade(force bool) error {
if !system.CmdExists("brew") {
return errors.New("homebrew is not installed")
}

return system.RunAll(system.CmdsList{
err := system.RunAll(system.CmdsList{
{"brew", "--prefix", "algorand", "--installed"},
{"brew", "update"},
{"brew", "upgrade", "algorand", "--formula"},
})
if err != nil {
return err
}
err = Stop(false)
if err != nil {
return err
}
return Start(false)
}

// Start algorand with launchd
10 changes: 7 additions & 3 deletions internal/algod/participation/participation.go
Original file line number Diff line number Diff line change
@@ -167,7 +167,7 @@ func GetOnlineShortLink(http api.HttpPkgInterface, part OnlineShortLinkBody) (Sh
if err != nil {
return response, err
}
res, err := http.Post("http://b.nodekit.run/online", "applicaiton/json", bytes.NewReader(data))
res, err := http.Post("http://b.nodekit.run/online", "application/json", bytes.NewReader(data))
if err != nil {
return response, err
}
@@ -219,6 +219,10 @@ func GetOfflineShortLink(http api.HttpPkgInterface, offline OfflineShortLinkBody

// ToShortLink generates a shortened URL string using the unique
// identifier from the provided ShortLinkResponse.
func ToShortLink(link ShortLinkResponse) string {
return fmt.Sprintf("https://b.nodekit.run/%s", link.Id)
func ToShortLink(link ShortLinkResponse, incentiveEligibleFee bool) string {
suffix := ""
if incentiveEligibleFee {
suffix = "i"
}
return fmt.Sprintf("https://b.nodekit.run/%s%s", link.Id, suffix)
}
34 changes: 20 additions & 14 deletions internal/algod/state.go
Original file line number Diff line number Diff line change
@@ -38,6 +38,9 @@ type StateModel struct {
// TODO: handle contexts instead of adding it to state (skill-issue zero)
Watching bool

// Whether user has disabled automatically applying incentive eligibility fees
IncentivesDisabled bool

// Client provides an interface for interacting with API endpoints,
// enabling various node operations and data retrieval.
Client api.ClientWithResponsesInterface
@@ -53,7 +56,7 @@ type StateModel struct {

// NewStateModel initializes and returns a new StateModel instance
// along with an API response and potential error.
func NewStateModel(ctx context.Context, client api.ClientWithResponsesInterface, httpPkg api.HttpPkgInterface) (*StateModel, api.ResponseInterface, error) {
func NewStateModel(ctx context.Context, client api.ClientWithResponsesInterface, httpPkg api.HttpPkgInterface, incentivesDisabled bool) (*StateModel, api.ResponseInterface, error) {
// Preload the node status
status, response, err := NewStatus(ctx, client, httpPkg)
if err != nil {
@@ -79,6 +82,8 @@ func NewStateModel(ctx context.Context, client api.ClientWithResponsesInterface,
Client: client,
HttpPkg: httpPkg,
Context: ctx,

IncentivesDisabled: incentivesDisabled,
}, partkeysResponse, nil
}

@@ -116,6 +121,7 @@ func (s *StateModel) Watch(cb func(model *StateModel, err error), ctx context.Co
if !s.Watching {
break
}

// Abort on Fast-Catchup
if s.Status.State == FastCatchupState {
// Update current render
@@ -132,6 +138,9 @@ func (s *StateModel) Watch(cb func(model *StateModel, err error), ctx context.Co
cb(s, nil)
continue
}
// Fetch Keys
s.UpdateKeys(ctx, t)
cb(s, nil)

// Wait for the next block
s.Status, _, err = s.Status.Wait(ctx)
@@ -140,9 +149,6 @@ func (s *StateModel) Watch(cb func(model *StateModel, err error), ctx context.Co
continue
}

// Fetch Keys
s.UpdateKeys(ctx, t)

if s.Status.State == SyncingState {
cb(s, nil)
continue
@@ -176,18 +182,18 @@ func (s *StateModel) UpdateKeys(ctx context.Context, t system.Time) {
if err == nil {
s.Admin = true
s.Accounts = ParticipationKeysToAccounts(s.ParticipationKeys)

// For each account, update the data from the RPC endpoint
for _, acct := range s.Accounts {
// For each account, update the data from the RPC endpoint
if s.Status.State == StableState {
// Skip eon errors
rpcAcct, err := GetAccount(s.Client, acct.Address)
if err != nil {
continue
}

s.Accounts[acct.Address] = s.Accounts[acct.Address].Merge(rpcAcct)
s.Accounts[acct.Address] = s.Accounts[acct.Address].UpdateExpiredTime(t, s.ParticipationKeys, int(s.Status.LastRound), s.Metrics.RoundTime)
// Skip eon errors
rpcAcct, err := GetAccount(s.Client, acct.Address)
if err != nil {
continue
}

s.Accounts[acct.Address] = s.Accounts[acct.Address].Merge(rpcAcct)
s.Accounts[acct.Address] = s.Accounts[acct.Address].UpdateExpiredTime(t, s.ParticipationKeys, int(s.Status.LastRound), s.Metrics.RoundTime)
}

}
}
Loading

0 comments on commit 96999f4

Please sign in to comment.