diff --git a/internal/algod/participation/participation.go b/internal/algod/participation/participation.go index c7ba3402..b9b8d2cc 100644 --- a/internal/algod/participation/participation.go +++ b/internal/algod/participation/participation.go @@ -102,6 +102,43 @@ func GenerateKeys( } } +type Diff struct { + VoteFirstValid bool + VoteLastValid bool + VoteKeyDilution bool + VoteParticipationKey bool + SelectionParticipationKey bool + StateProofKey bool +} + +func boolToInt(input bool) int { + if input { + return 1 + } + return 0 +} +func HasChanged(part api.ParticipationKey, account *api.AccountParticipation) (Diff, bool, int) { + diff := Diff{ + VoteFirstValid: account.VoteFirstValid != part.Key.VoteFirstValid, + VoteLastValid: account.VoteLastValid != part.Key.VoteLastValid, + VoteKeyDilution: account.VoteKeyDilution != part.Key.VoteKeyDilution, + VoteParticipationKey: !bytes.Equal(account.VoteParticipationKey, part.Key.VoteParticipationKey), + SelectionParticipationKey: !bytes.Equal(account.SelectionParticipationKey, part.Key.SelectionParticipationKey), + StateProofKey: !bytes.Equal(*account.StateProofKey, *part.Key.StateProofKey), + } + + // Count matches + fvMatch := boolToInt(diff.VoteFirstValid) + lvMatch := boolToInt(diff.VoteLastValid) + kdMatch := boolToInt(diff.VoteKeyDilution) + selMatch := boolToInt(diff.SelectionParticipationKey) + votMatch := boolToInt(diff.VoteParticipationKey) + spkMatch := boolToInt(diff.StateProofKey) + matchCount := fvMatch + lvMatch + kdMatch + selMatch + votMatch + spkMatch + + return diff, diff.VoteFirstValid || diff.VoteLastValid || diff.VoteKeyDilution || diff.VoteParticipationKey || diff.SelectionParticipationKey || diff.StateProofKey, matchCount +} + // Delete remove a key from the node func Delete(ctx context.Context, client api.ClientWithResponsesInterface, participationId string) error { deletion, err := client.DeleteParticipationKeyByIDWithResponse(ctx, participationId) diff --git a/ui/modal/controller.go b/ui/modal/controller.go index b9b05d16..aa9384e6 100644 --- a/ui/modal/controller.go +++ b/ui/modal/controller.go @@ -1,7 +1,6 @@ package modal import ( - "bytes" "fmt" "github.com/algorandfoundation/nodekit/internal/algod" "github.com/algorandfoundation/nodekit/internal/algod/participation" @@ -32,7 +31,7 @@ func boolToInt(input bool) int { } // HandleMessage processes the given message, updates the ViewModel state, and returns any commands to execute. -func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) { +func (m *ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) { var ( cmd tea.Cmd cmds []tea.Cmd @@ -76,66 +75,72 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) { m.borderColor = "7" m.controls = "" m.title = "Fast Catchup" + // Return early, skip any checks + m.exceptionModal, cmd = m.exceptionModal.HandleMessage(msg) + return m, cmd + } + + // Get the existing account from the state + acct, ok := msg.Accounts[m.Address] + + // Handle suspensions + if ok { + if acct.Participation != nil && acct.Status == "Offline" { + m.SetSuspended(true) + } + } + + // We found the account, and we are on one of the modals + if ok && m.Type == app.TransactionModal || m.Type == app.InfoModal { + // Make sure the transaction modal is set to the current address + if m.transactionModal.Participation != nil && m.transactionModal.Participation.Address == acct.Address { + // Actual State + isOnline := acct.Status == "Online" + isActive := acct.Participation != nil + + // Derived State + isValid := isOnline && isActive + diff, isDifferent, count := participation.HasChanged(*m.transactionModal.Participation, acct.Participation) + + // The account is valid and we registered + if isValid && !isDifferent && m.Type == app.TransactionModal && !m.transactionModal.Active { + m.SetActive(true) + m.infoModal.Prefix = "Successfully registered online!\n" + m.HasPrefix = true + m.SetType(app.InfoModal) + // For the love of all that is good, please lets refactor this. Preferably with a daemon + } else if isValid && isDifferent && count != 6 && (m.Type == app.InfoModal || (m.Type == app.TransactionModal && !m.transactionModal.Active)) { + // It is online, has a participation key but not the one we are looking at AND all the keys are not different + // (AND it's the info modal (this case we are checking on enter) OR we are waiting to register a key, and we made a mistake + + // You know it's getting bad when the plugin recommendation is Grazie + // TODO: refactor this beast to have isolated state from the modal controller - } else if m.Type == app.TransactionModal && m.transactionModal.Participation != nil { - // Get the existing account from the state - acct, ok := msg.Accounts[m.Address] - // If the previous state is not active - if ok { - if !m.transactionModal.Active { - if acct.Participation != nil && acct.Status == "Online" { - // comparing values to detect corrupted/non-resident keys - fvMatch := boolToInt(acct.Participation.VoteFirstValid == m.transactionModal.Participation.Key.VoteFirstValid) - lvMatch := boolToInt(acct.Participation.VoteLastValid == m.transactionModal.Participation.Key.VoteLastValid) - kdMatch := boolToInt(acct.Participation.VoteKeyDilution == m.transactionModal.Participation.Key.VoteKeyDilution) - selMatch := boolToInt(bytes.Equal(acct.Participation.SelectionParticipationKey, m.transactionModal.Participation.Key.SelectionParticipationKey)) - votMatch := boolToInt(bytes.Equal(acct.Participation.VoteParticipationKey, m.transactionModal.Participation.Key.VoteParticipationKey)) - spkMatch := boolToInt(bytes.Equal(*acct.Participation.StateProofKey, *m.transactionModal.Participation.Key.StateProofKey)) - matchCount := fvMatch + lvMatch + kdMatch + selMatch + votMatch + spkMatch - if matchCount == 6 { - m.SetActive(true) - m.infoModal.Active = true - m.infoModal.Prefix = "Successfully registered online!\n" - m.HasPrefix = true - m.SetType(app.InfoModal) - } else if matchCount >= 4 { - // We use 4 as the "non resident key" threshold here - // because it would be valid to re-reg with a key that has the same fv / lv / kd - // but it would trigger the non resident condition - // TOOD: refactor this beast to have {previous state} -> compare with next state - m.SetActive(true) - m.infoModal.Active = true - m.infoModal.Prefix = "***WARNING***\nRegistered online but keys do not fully match\nCheck your registered keys carefully against the node keys\n\n" - if fvMatch == 0 { - m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: Vote First Valid\n" - } - if lvMatch == 0 { - m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: Vote Last Valid\n" - } - if kdMatch == 0 { - m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: Vote Key Dilution\n" - } - if votMatch == 0 { - m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: Vote Key\n" - } - if selMatch == 0 { - m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: Selection Key\n" - } - if spkMatch == 0 { - m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: State Proof Key\n" - } - m.HasPrefix = true - m.SetType(app.InfoModal) + // Ahh yes, classic "Set Active to the inverse then only navigate when there is no prefix" + // This is the closest thing we have to state, between this and the transaction modal state it works + m.SetActive(false) + if m.infoModal.Prefix == "" { + m.infoModal.Prefix = "***WARNING***\nRegistered online but keys do not fully match\nCheck your registered keys carefully against the node keys\n\n" + if diff.VoteFirstValid { + m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: Vote First Valid\n" } - } - } else { - // TODO: This includes suspended keys, where Status == offline but .Participation is set - // Detect and display this - if acct.Participation == nil { - m.SetActive(false) - m.SetType(app.InfoModal) - } else { - m.SetSuspended() + if diff.VoteLastValid { + m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: Vote Last Valid\n" + } + if diff.VoteKeyDilution { + m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: Vote Key Dilution\n" + } + if diff.VoteParticipationKey { + m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: Vote Key\n" + } + if diff.SelectionParticipationKey { + m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: Selection Key\n" + } + if diff.StateProofKey { + m.infoModal.Prefix = m.infoModal.Prefix + "Mismatched: State Proof Key\n" + } + m.HasPrefix = true + m.SetType(app.InfoModal) } } @@ -223,7 +228,7 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) { cmds = append(cmds, cmd) m.exceptionModal, cmd = m.exceptionModal.HandleMessage(modalMsg) cmds = append(cmds, cmd) - return &m, tea.Batch(cmds...) + return m, tea.Batch(cmds...) } // Only trigger modal commands when they are active @@ -242,7 +247,7 @@ func (m ViewModel) HandleMessage(msg tea.Msg) (*ViewModel, tea.Cmd) { } cmds = append(cmds, cmd) - return &m, tea.Batch(cmds...) + return m, tea.Batch(cmds...) } // Update processes the given message, updates the ViewModel state, and returns the updated model and accompanying commands. diff --git a/ui/modal/model.go b/ui/modal/model.go index 347dc608..c1af3060 100644 --- a/ui/modal/model.go +++ b/ui/modal/model.go @@ -70,12 +70,10 @@ func (m *ViewModel) SetActive(active bool) { } // SetSuspended sets the active state to false both infoModal and transactionModal, and sets suspended state to true, also for both modals. -func (m *ViewModel) SetSuspended() { - m.infoModal.Active = false - m.infoModal.Suspended = true +func (m *ViewModel) SetSuspended(sus bool) { + m.infoModal.Suspended = sus m.infoModal.UpdateState() - m.transactionModal.Active = false - m.transactionModal.Suspended = true + m.transactionModal.Suspended = sus m.transactionModal.UpdateState() }