Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: style, borders, and adds more account state #19

Merged
merged 2 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/lf.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 9 additions & 7 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/algorandfoundation/hack-tui/api"
"github.com/algorandfoundation/hack-tui/internal"
"github.com/algorandfoundation/hack-tui/ui"
"github.com/algorandfoundation/hack-tui/ui/style"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/log"
"github.com/oapi-codegen/oapi-codegen/v2/pkg/securityprovider"
Expand All @@ -32,7 +33,7 @@ var (
rootCmd = &cobra.Command{
Use: "algorun",
Short: "Manage Algorand nodes",
Long: ui.Purple(BANNER) + "\n",
Long: style.Purple(BANNER) + "\n",
CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: true,
},
Expand Down Expand Up @@ -61,7 +62,7 @@ var (
},
ParticipationKeys: partkeys,
}
state.Accounts = internal.AccountsFromState(&state, client)
state.Accounts = internal.AccountsFromState(&state, new(internal.Clock), client)

// Fetch current state
err = state.Status.Fetch(context.Background(), client)
Expand All @@ -73,6 +74,7 @@ var (
p := tea.NewProgram(
m,
tea.WithAltScreen(),
tea.WithFPS(120),
)
go func() {
state.Watch(func(status *internal.StateModel, err error) {
Expand Down Expand Up @@ -112,19 +114,19 @@ func init() {
rootCmd.Version = Version

// Bindings
rootCmd.PersistentFlags().StringVar(&server, "server", "", ui.LightBlue("server address"))
rootCmd.PersistentFlags().StringVar(&token, "token", "", ui.LightBlue("server token"))
rootCmd.PersistentFlags().StringVar(&server, "server", "", style.LightBlue("server address"))
rootCmd.PersistentFlags().StringVar(&token, "token", "", style.LightBlue("server token"))
_ = viper.BindPFlag("server", rootCmd.PersistentFlags().Lookup("server"))
_ = viper.BindPFlag("token", rootCmd.PersistentFlags().Lookup("token"))

// Update Long Text
rootCmd.Long +=
ui.Magenta("Configuration: ") + viper.GetViper().ConfigFileUsed() + "\n" +
ui.LightBlue("Server: ") + viper.GetString("server")
style.Magenta("Configuration: ") + viper.GetViper().ConfigFileUsed() + "\n" +
style.LightBlue("Server: ") + viper.GetString("server")

if viper.GetString("data") != "" {
rootCmd.Long +=
ui.Magenta("\nAlgorand Data: ") + viper.GetString("data")
style.Magenta("\nAlgorand Data: ") + viper.GetString("data")
}

// Add Commands
Expand Down
5 changes: 3 additions & 2 deletions cmd/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"github.com/algorandfoundation/hack-tui/internal"
"github.com/algorandfoundation/hack-tui/ui"
"github.com/algorandfoundation/hack-tui/ui/style"
tea "github.com/charmbracelet/bubbletea"
"github.com/spf13/cobra"
"github.com/spf13/viper"
Expand All @@ -16,10 +17,10 @@ import (
var statusCmd = &cobra.Command{
Use: "status",
Short: "Get the node status",
Long: ui.Purple(BANNER) + "\n" + ui.LightBlue("View the node status"),
Long: style.Purple(BANNER) + "\n" + style.LightBlue("View the node status"),
RunE: func(cmd *cobra.Command, args []string) error {
if viper.GetString("server") == "" {
return errors.New(ui.Magenta("server is required"))
return errors.New(style.Magenta("server is required"))
}

// Get Algod from configuration
Expand Down
43 changes: 19 additions & 24 deletions internal/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ type Account struct {
Keys int
// Expires is the date the participation key will expire
Expires time.Time
// The LastModified round, this only pertains to keys that can be updated
LastModified int
}

// Gets the list of addresses created at genesis from the genesis file
Expand Down Expand Up @@ -92,17 +90,8 @@ func getAddressesFromGenesis(client *api.ClientWithResponses) ([]string, string,
return addresses, rewardsPool, feeSink, nil
}

func isValidStatus(status string) bool {
validStatuses := map[string]bool{
"Online": true,
"Offline": true,
"Not Participating": true,
}
return validStatuses[status]
}

// Get Online Status of Account
func getAccountOnlineStatus(client *api.ClientWithResponses, address string) (string, error) {
func GetAccount(client *api.ClientWithResponses, address string) (api.Account, error) {
var format api.AccountInformationParamsFormat = "json"
r, err := client.AccountInformationWithResponse(
context.Background(),
Expand All @@ -111,23 +100,20 @@ func getAccountOnlineStatus(client *api.ClientWithResponses, address string) (st
Format: &format,
})

var accountInfo api.Account
if err != nil {
return "N/A", err
return accountInfo, err
}

if r.StatusCode() != 200 {
return "N/A", errors.New(fmt.Sprintf("Failed to get account information. Received error code: %d", r.StatusCode()))
return accountInfo, errors.New(fmt.Sprintf("Failed to get account information. Received error code: %d", r.StatusCode()))
}

if r.JSON200 == nil {
return "N/A", errors.New("Failed to get account information. JSON200 is nil")
}

return r.JSON200.Status, nil
return *r.JSON200, nil
}

// AccountsFromParticipationKeys maps an array of api.ParticipationKey to a keyed map of Account
func AccountsFromState(state *StateModel, client *api.ClientWithResponses) map[string]Account {
func AccountsFromState(state *StateModel, t Time, client *api.ClientWithResponses) map[string]Account {
values := make(map[string]Account)
if state == nil || state.ParticipationKeys == nil {
return values
Expand All @@ -136,18 +122,27 @@ func AccountsFromState(state *StateModel, client *api.ClientWithResponses) map[s
val, ok := values[key.Address]
if !ok {

statusOnline, err := getAccountOnlineStatus(client, key.Address)
account, err := GetAccount(client, key.Address)

// TODO: handle error
if err != nil {
// TODO: Logging
panic(err)
}

var expires = t.Now()
if key.EffectiveLastValid != nil {
now := t.Now()
roundDiff := max(0, *key.EffectiveLastValid-int(state.Status.LastRound))
distance := int(state.Metrics.RoundTime) * roundDiff
expires = now.Add(time.Duration(distance))
}

values[key.Address] = Account{
Address: key.Address,
Status: statusOnline,
Balance: 0,
Expires: time.Unix(0, 0),
Status: account.Status,
Balance: account.Amount / 1000000,
Expires: expires,
Keys: 1,
}
} else {
Expand Down
148 changes: 105 additions & 43 deletions internal/accounts_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package internal

import (
"testing"
"time"

"github.com/algorandfoundation/hack-tui/api"
"github.com/oapi-codegen/oapi-codegen/v2/pkg/securityprovider"
"github.com/stretchr/testify/assert"
"testing"
"time"
)

type TestClock struct{}

func (TestClock) Now() time.Time { return time.Time{} }

func Test_AccountsFromState(t *testing.T) {

// Setup elevated client
Expand All @@ -24,67 +27,68 @@ func Test_AccountsFromState(t *testing.T) {
t.Fatal(err)
}

// Test getAccountOnlineStatus

var mapAddressOnlineStatus = make(map[string]string)

var mapAccounts = make(map[string]api.Account)
var onlineAccounts = make([]api.Account, 0)
for _, address := range addresses {
status, err := getAccountOnlineStatus(client, address)
acct, err := GetAccount(client, address)
if err != nil {
t.Fatal(err)
}

assert.True(t, status == "Online" || status == "Offline")
mapAddressOnlineStatus[address] = status
assert.True(t, acct.Status == "Online" || acct.Status == "Offline")
mapAccounts[address] = acct
if acct.Status == "Online" {
onlineAccounts = append(onlineAccounts, acct)
}
}

status, err := getAccountOnlineStatus(client, rewardsPool)
acct, err := GetAccount(client, rewardsPool)
if err != nil {
t.Fatal(err)
}
if status != "Not Participating" {
t.Fatalf("Expected RewardsPool to be 'Not Participating', got %s", status)
if acct.Status != "Not Participating" {
t.Fatalf("Expected RewardsPool to be 'Not Participating', got %s", acct.Status)
}

status, err = getAccountOnlineStatus(client, feeSink)
acct, err = GetAccount(client, feeSink)
if err != nil {
t.Fatal(err)
}
if status != "Not Participating" {
t.Fatalf("Expected FeeSink to be 'Not Participating', got %s", status)
if acct.Status != "Not Participating" {
t.Fatalf("Expected FeeSink to be 'Not Participating', got %s", acct.Status)
}

_, err = getAccountOnlineStatus(client, "invalid_address")
_, err = GetAccount(client, "invalid_address")
if err == nil {
t.Fatal("Expected error for invalid address")
}

// Test AccountFromState
// Test Account from State

// Prepare expected results
// Only include addresses with "Online" status
onlineAddresses := make(map[string]string)
for address, status := range mapAddressOnlineStatus {
if status == "Online" {
onlineAddresses[address] = status
}
}
effectiveFirstValid := 0
effectiveLastValid := 10000

// Create expectedAccounts dynamically from Online accounts, and mocked participation keys
mockedPartKeys := make([]api.ParticipationKey, 0)
expectedAccounts := make(map[string]Account)
for address, status := range onlineAddresses {
expectedAccounts[address] = Account{
Address: address,
Status: status,
Balance: 0,
Expires: time.Unix(0, 0),
Keys: 1,
LastModified: 0,
}

mockedPartKeys = append(mockedPartKeys, api.ParticipationKey{
Address: address,
// Create mockedPart Keys
var mockedPartKeys = []api.ParticipationKey{
{
Address: onlineAccounts[0].Address,
EffectiveFirstValid: &effectiveFirstValid,
EffectiveLastValid: &effectiveLastValid,
Id: "",
Key: api.AccountParticipation{
SelectionParticipationKey: nil,
StateProofKey: nil,
VoteParticipationKey: nil,
VoteFirstValid: 0,
VoteLastValid: 9999999,
VoteKeyDilution: 0,
},
LastBlockProposal: nil,
LastStateProof: nil,
LastVote: nil,
},
{
Address: onlineAccounts[0].Address,
EffectiveFirstValid: nil,
EffectiveLastValid: nil,
Id: "",
Expand All @@ -99,16 +103,74 @@ func Test_AccountsFromState(t *testing.T) {
LastBlockProposal: nil,
LastStateProof: nil,
LastVote: nil,
})
},
{
Address: onlineAccounts[1].Address,
EffectiveFirstValid: &effectiveFirstValid,
EffectiveLastValid: &effectiveLastValid,
Id: "",
Key: api.AccountParticipation{
SelectionParticipationKey: nil,
StateProofKey: nil,
VoteParticipationKey: nil,
VoteFirstValid: 0,
VoteLastValid: 9999999,
VoteKeyDilution: 0,
},
LastBlockProposal: nil,
LastStateProof: nil,
LastVote: nil,
},
}

// Mock StateModel
state := &StateModel{
Metrics: MetricsModel{
Enabled: true,
Window: 100,
RoundTime: time.Duration(2) * time.Second,
TPS: 20,
RX: 1024,
TX: 2048,
},
Status: StatusModel{
State: "WATCHING",
Version: "v0.0.0-test",
Network: "tuinet",
Voting: false,
NeedsUpdate: false,
LastRound: 1337,
},
ParticipationKeys: &mockedPartKeys,
}

// Calculate expiration
clock := new(TestClock)
now := clock.Now()
roundDiff := max(0, effectiveLastValid-int(state.Status.LastRound))
distance := int(state.Metrics.RoundTime) * roundDiff
expires := now.Add(time.Duration(distance))

// Construct expected accounts
expectedAccounts := map[string]Account{
onlineAccounts[0].Address: {
Address: onlineAccounts[0].Address,
Status: onlineAccounts[0].Status,
Balance: onlineAccounts[0].Amount / 1_000_000,
Keys: 2,
Expires: expires,
},
onlineAccounts[1].Address: {
Address: onlineAccounts[1].Address,
Status: onlineAccounts[1].Status,
Balance: onlineAccounts[1].Amount / 1_000_000,
Keys: 1,
Expires: expires,
},
}

// Call AccountsFromState
accounts := AccountsFromState(state, client)
accounts := AccountsFromState(state, clock, client)

// Assert results
assert.Equal(t, expectedAccounts, accounts)
Expand Down
Loading
Loading