Skip to content

Commit

Permalink
refactor: account state updates
Browse files Browse the repository at this point in the history
  • Loading branch information
PhearZero committed Dec 4, 2024
1 parent 0e2a4cf commit afe3da8
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 150 deletions.
4 changes: 2 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ var (
Client: client,
Context: ctx,
}
state.Accounts = internal.AccountsFromState(&state, new(internal.Clock), client)

state.Accounts, err = internal.AccountsFromState(&state, new(internal.Clock), client)
cobra.CheckErr(err)
// Fetch current state
err = state.Status.Fetch(ctx, client, new(internal.HttpPkg))
cobra.CheckErr(err)
Expand Down
146 changes: 95 additions & 51 deletions internal/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type Account struct {
Participation *api.AccountParticipation
// IncentiveEligible determines the minimum fee
IncentiveEligible bool
// NonResidentKey finds an online account that is missing locally
NonResidentKey bool
// Account Address is the algorand encoded address
Address string
// Status is the Online/Offline/"NotParticipating" status of the account
Expand All @@ -30,7 +32,7 @@ type Account struct {
Expires *time.Time
}

// Get Online Status of Account
// GetAccount status of api.Account
func GetAccount(client api.ClientWithResponsesInterface, address string) (api.Account, error) {
var format api.AccountInformationParamsFormat = "json"
r, err := client.AccountInformationWithResponse(
Expand All @@ -53,74 +55,116 @@ func GetAccount(client api.ClientWithResponsesInterface, address string) (api.Ac
}

// GetExpiresTime calculates and returns the expiration time for a participation key based on the current account state.
func GetExpiresTime(t Time, key api.ParticipationKey, state *StateModel) *time.Time {
func GetExpiresTime(t Time, lastRound int, roundTime time.Duration, account Account) *time.Time {
now := t.Now()
var expires time.Time
if state.Accounts[key.Address].Status == "Online" &&
state.Accounts[key.Address].Participation != nil &&
bytes.Equal(*state.Accounts[key.Address].Participation.StateProofKey, *key.Key.StateProofKey) &&
state.Status.LastRound != 0 &&
state.Metrics.RoundTime != 0 {
roundDiff := max(0, key.Key.VoteLastValid-int(state.Status.LastRound))
distance := int(state.Metrics.RoundTime) * roundDiff
if account.Status == "Online" &&
account.Participation != nil &&
lastRound != 0 &&
roundTime != 0 {
roundDiff := max(0, account.Participation.VoteLastValid-int(lastRound))
distance := int(roundTime) * roundDiff
expires = now.Add(time.Duration(distance))
return &expires
}
return nil
}

// AccountsFromParticipationKeys maps an array of api.ParticipationKey to a keyed map of Account
func AccountsFromState(state *StateModel, t Time, client api.ClientWithResponsesInterface) map[string]Account {
values := make(map[string]Account)
if state == nil || state.ParticipationKeys == nil {
return values
// ParticipationKeysToAccounts converts a slice of ParticipationKey objects into a map of Account objects.
// The keys parameter is a slice of pointers to ParticipationKey instances.
// The prev parameter is an optional map that allows merging of existing accounts with new ones.
// Returns a map where each key is an address from a ParticipationKey, and the value is a corresponding Account.
func ParticipationKeysToAccounts(keys *[]api.ParticipationKey) map[string]Account {
// Allow merging of existing accounts
var accounts = make(map[string]Account)

// Must have keys to process
if keys == nil {
return accounts
}

Check warning on line 84 in internal/accounts.go

View check run for this annotation

Codecov / codecov/patch

internal/accounts.go#L83-L84

Added lines #L83 - L84 were not covered by tests
for _, key := range *state.ParticipationKeys {
val, ok := values[key.Address]
if !ok {
var account = api.Account{
Address: key.Address,
Status: "Unknown",
IncentiveEligible: nil,
Amount: 0,
}
if state.Status.State != SyncingState {
var err error
account, err = GetAccount(client, key.Address)
// TODO: handle error
if err != nil {
// TODO: Logging
panic(err)
}
}

// Check for eligibility
var incentiveEligible = false
if account.IncentiveEligible == nil {
incentiveEligible = false
} else {
incentiveEligible = *account.IncentiveEligible
}
values[key.Address] = Account{
Participation: account.Participation,
// Add missing Accounts
for _, key := range *keys {
if _, ok := accounts[key.Address]; !ok {
accounts[key.Address] = Account{
Participation: nil,
IncentiveEligible: false,
Address: key.Address,
Status: account.Status,
Balance: account.Amount / 1000000,
Expires: GetExpiresTime(t, key, state),
IncentiveEligible: incentiveEligible,
Status: "Unknown",
Balance: 0,
Keys: 1,
Expires: nil,
}
} else {
val.Keys++
if val.Participation != nil &&
bytes.Equal(*val.Participation.StateProofKey, *key.Key.StateProofKey) {
val.Expires = GetExpiresTime(t, key, state)
acct := accounts[key.Address]
acct.Keys++
accounts[key.Address] = acct
}
}
return accounts
}

func UpdateAccountFromRPC(account Account, rpcAccount api.Account) Account {
account.Status = rpcAccount.Status
account.Balance = rpcAccount.Amount / 1000000
account.Participation = rpcAccount.Participation

var incentiveEligible = false
if rpcAccount.IncentiveEligible == nil {
incentiveEligible = false
} else {
incentiveEligible = *rpcAccount.IncentiveEligible
}

account.IncentiveEligible = incentiveEligible

return account
}

func IsParticipationKeyActive(part api.ParticipationKey, account api.AccountParticipation) bool {
var equal = false
if bytes.Equal(part.Key.VoteParticipationKey, account.VoteParticipationKey) &&
part.Key.VoteLastValid == account.VoteLastValid &&
part.Key.VoteFirstValid == account.VoteFirstValid {
equal = true
}
return equal
}

func UpdateAccountExpiredTime(t Time, account Account, state *StateModel) Account {
var nonResidentKey = true
for _, key := range *state.ParticipationKeys {
// We have the key locally, update the residency
if key.Address == account.Address && account.Participation != nil && IsParticipationKeyActive(key, *account.Participation) {
nonResidentKey = false
}
}
account.NonResidentKey = nonResidentKey
account.Expires = GetExpiresTime(t, int(state.Status.LastRound), state.Metrics.RoundTime, account)
return account
}

// AccountsFromState maps an array of api.ParticipationKey to a keyed map of Account
func AccountsFromState(state *StateModel, t Time, client api.ClientWithResponsesInterface) (map[string]Account, error) {
if state == nil {
return make(map[string]Account), nil
}

Check warning on line 151 in internal/accounts.go

View check run for this annotation

Codecov / codecov/patch

internal/accounts.go#L150-L151

Added lines #L150 - L151 were not covered by tests

accounts := ParticipationKeysToAccounts(state.ParticipationKeys)

for _, acct := range accounts {
// For each account, update the data from the RPC endpoint
if state.Status.State != SyncingState {
rpcAcct, err := GetAccount(client, acct.Address)
if err != nil {
return nil, err

Check warning on line 160 in internal/accounts.go

View check run for this annotation

Codecov / codecov/patch

internal/accounts.go#L160

Added line #L160 was not covered by tests
}
values[key.Address] = val
accounts[acct.Address] = UpdateAccountFromRPC(acct, rpcAcct)
accounts[acct.Address] = UpdateAccountExpiredTime(t, accounts[acct.Address], state)
}
}

return values
return accounts, nil
}

func ValidateAddress(address string) bool {
Expand Down
96 changes: 16 additions & 80 deletions internal/accounts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,66 +62,6 @@ func Test_AccountsFromState(t *testing.T) {
t.Fatal("Expected error for invalid address")
}

// Test Account from State

effectiveFirstValid := 0
effectiveLastValid := 10000
lastProposedRound := 1336
// 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: &lastProposedRound,
LastStateProof: nil,
LastVote: nil,
},
{
Address: onlineAccounts[0].Address,
EffectiveFirstValid: nil,
EffectiveLastValid: nil,
Id: "",
Key: api.AccountParticipation{
SelectionParticipationKey: nil,
StateProofKey: nil,
VoteParticipationKey: nil,
VoteFirstValid: 0,
VoteLastValid: 9999999,
VoteKeyDilution: 0,
},
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: &lastProposedRound,
LastStateProof: nil,
LastVote: nil,
},
}

// Mock StateModel
state := &StateModel{
Metrics: MetricsModel{
Expand All @@ -140,39 +80,35 @@ func Test_AccountsFromState(t *testing.T) {
NeedsUpdate: false,
LastRound: 1337,
},
ParticipationKeys: &mockedPartKeys,
ParticipationKeys: &mock.Keys,
}

// Calculate expiration
clock := new(mock.Clock)
now := clock.Now()
roundDiff := max(0, effectiveLastValid-int(state.Status.LastRound))
roundDiff := max(0, mock.Keys[0].Key.VoteLastValid-int(state.Status.LastRound))
distance := int(state.Metrics.RoundTime) * roundDiff
expires := now.Add(time.Duration(distance))

tClient := test.GetClient(false)
acct, _ = GetAccount(tClient, "ABC")
// Construct expected accounts
expectedAccounts := map[string]Account{
onlineAccounts[0].Address: {
Participation: onlineAccounts[0].Participation,
Address: onlineAccounts[0].Address,
Status: onlineAccounts[0].Status,
Balance: onlineAccounts[0].Amount / 1_000_000,
Keys: 2,
Expires: expires,
},
onlineAccounts[1].Address: {
Participation: onlineAccounts[1].Participation,
Address: onlineAccounts[1].Address,
Status: onlineAccounts[1].Status,
Balance: onlineAccounts[1].Amount / 1_000_000,
Keys: 1,
Expires: expires,
"ABC": {
Participation: acct.Participation,
Address: acct.Address,
Status: acct.Status,
IncentiveEligible: true,
Balance: acct.Amount / 1_000_000,
Keys: 2,
Expires: &expires,
},
}

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

accounts, err := AccountsFromState(state, clock, tClient)
if err != nil {
t.Fatal(err)
}
// Assert results
assert.Equal(t, expectedAccounts, accounts)

Expand Down
11 changes: 8 additions & 3 deletions internal/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,10 @@ func (s *StateModel) UpdateMetricsFromRPC(ctx context.Context, client api.Client
s.Metrics.LastRX = res["algod_network_received_bytes_total"]
}
}
func (s *StateModel) UpdateAccounts() {
s.Accounts = AccountsFromState(s, new(Clock), s.Client)
func (s *StateModel) UpdateAccounts() error {
var err error
s.Accounts, err = AccountsFromState(s, new(Clock), s.Client)
return err
}

func (s *StateModel) UpdateKeys() {
Expand All @@ -137,6 +139,9 @@ func (s *StateModel) UpdateKeys() {
}
if err == nil {
s.Admin = true
s.UpdateAccounts()
err = s.UpdateAccounts()
if err != nil {
// TODO: Handle error
}

Check warning on line 145 in internal/state.go

View check run for this annotation

Codecov / codecov/patch

internal/state.go#L144-L145

Added lines #L144 - L145 were not covered by tests
}
}
12 changes: 12 additions & 0 deletions internal/test/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,18 @@ func (c *Client) DeleteParticipationKeyByIDWithResponse(ctx context.Context, par
return &res, nil
}

func (c *Client) AccountInformationWithResponse(ctx context.Context, address string, params *api.AccountInformationParams, reqEditors ...api.RequestEditorFn) (*api.AccountInformationResponse, error) {
httpResponse := http.Response{StatusCode: 200}
return &api.AccountInformationResponse{
Body: nil,
HTTPResponse: &httpResponse,
JSON200: &mock.ABCAccount,
JSON400: nil,
JSON401: nil,
JSON500: nil,
}, nil
}

func (c *Client) GenerateParticipationKeysWithResponse(ctx context.Context, address string, params *api.GenerateParticipationKeysParams, reqEditors ...api.RequestEditorFn) (*api.GenerateParticipationKeysResponse, error) {
mock.Keys = append(mock.Keys, api.ParticipationKey{
Address: "ABC",
Expand Down
Loading

0 comments on commit afe3da8

Please sign in to comment.