diff --git a/client/cosmos/api/account.go b/client/cosmos/api/account.go new file mode 100644 index 0000000..4e787de --- /dev/null +++ b/client/cosmos/api/account.go @@ -0,0 +1,43 @@ +package api + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/informalsystems/stakooler/client/cosmos/model" +) + +func GetAuth(account *model.Account) (model.AuthResponse, error) { + var authResponse model.AuthResponse + + url := account.Chain.LCD + "/cosmos/auth/v1beta1/accounts/" + account.Address + method := "GET" + + client := &http.Client{} + req, err := http.NewRequest(method, url, nil) + + if err != nil { + fmt.Println(err) + return authResponse, err + } + res, err := client.Do(req) + if err != nil { + fmt.Println(err) + return authResponse, err + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + fmt.Println(err) + return authResponse, err + } + err = json.Unmarshal(body, &authResponse) + if err != nil { + fmt.Println(err) + return authResponse, err + } + return authResponse, nil +} diff --git a/client/cosmos/api/bank.go b/client/cosmos/api/bank.go index d640ab6..20ef154 100644 --- a/client/cosmos/api/bank.go +++ b/client/cosmos/api/bank.go @@ -9,32 +9,8 @@ import ( "strings" ) -type BalancesResponse struct { - Balances []struct { - Denom string `json:"denom"` - Amount string `json:"amount"` - } `json:"balances"` - Pagination struct { - NextKey interface{} `json:"next_key"` - Total string `json:"total"` - } `json:"pagination"` -} - -type DenomMetadataResponse struct { - Metadata struct { - Description string `json:"description"` - DenomUnits []struct { - Denom string `json:"denom"` - Exponent int `json:"exponent"` - Aliases []string `json:"aliases"` - } `json:"denom_units"` - Base string `json:"base"` - Display string `json:"display"` - } `json:"metadata"` -} - -func GetBalances(account *model.Account) (BalancesResponse, error) { - var balanceResponse BalancesResponse +func GetBalances(account *model.Account) (model.BalancesResponse, error) { + var balanceResponse model.BalancesResponse url := account.Chain.LCD + "/cosmos/bank/v1beta1/balances/" + account.Address method := "GET" @@ -66,8 +42,8 @@ func GetBalances(account *model.Account) (BalancesResponse, error) { return balanceResponse, nil } -func GetDenomMetadata(account *model.Account, denom string) (DenomMetadataResponse, error) { - var denomMetadata DenomMetadataResponse +func GetDenomMetadata(account *model.Account, denom string) (model.DenomMetadataResponse, error) { + var denomMetadata model.DenomMetadataResponse url := account.Chain.LCD + "/cosmos/bank/v1beta1/denoms_metadata/" + denom method := "GET" @@ -100,7 +76,7 @@ func GetDenomMetadata(account *model.Account, denom string) (DenomMetadataRespon return denomMetadata, nil } -func (metadata *DenomMetadataResponse) GetExponent() int { +func GetExponent(metadata *model.DenomMetadataResponse) int { exponent := 0 for _, d := range metadata.Metadata.DenomUnits { if strings.ToUpper(d.Denom) == strings.ToUpper(metadata.Metadata.Display) { @@ -108,4 +84,4 @@ func (metadata *DenomMetadataResponse) GetExponent() int { } } return exponent -} \ No newline at end of file +} diff --git a/client/cosmos/api/distribution.go b/client/cosmos/api/distribution.go index c85b1a8..e92b39c 100644 --- a/client/cosmos/api/distribution.go +++ b/client/cosmos/api/distribution.go @@ -95,4 +95,4 @@ func GetCommissions(account *model.Account, validator string) (CommissionRespons return response, err } return response, nil -} \ No newline at end of file +} diff --git a/client/cosmos/api/staking.go b/client/cosmos/api/staking.go index 9dac255..be807d9 100644 --- a/client/cosmos/api/staking.go +++ b/client/cosmos/api/staking.go @@ -3,114 +3,14 @@ package api import ( "encoding/json" "fmt" - "github.com/informalsystems/stakooler/client/cosmos/model" "io" "net/http" - "time" -) - -type Delegations struct { - DelegationResponses []struct { - Delegation struct { - DelegatorAddress string `json:"delegator_address"` - ValidatorAddress string `json:"validator_address"` - Shares string `json:"shares"` - } `json:"delegation"` - Balance struct { - Denom string `json:"denom"` - Amount string `json:"amount"` - } `json:"balance"` - } `json:"delegation_responses"` - Pagination struct { - NextKey interface{} `json:"next_key"` - Total string `json:"total"` - } `json:"pagination"` -} -type Unbondings struct { - UnbondingResponses []struct { - DelegatorAddress string `json:"delegator_address"` - ValidatorAddress string `json:"validator_address"` - Entries []struct { - CreationHeight string `json:"creation_height"` - CompletionTime time.Time `json:"completion_time"` - InitialBalance string `json:"initial_balance"` - Balance string `json:"balance"` - } `json:"entries"` - } `json:"unbonding_responses"` - Pagination struct { - NextKey interface{} `json:"next_key"` - Total string `json:"total"` - } `json:"pagination"` -} - -type Params struct { - ParamsResponse struct { - UnbondingTime string `json:"unbonding_time"` - MaxValidators int `json:"max_validators"` - MaxEntries int `json:"max_entries"` - HistoricalEntries int `json:"historical_entries"` - BondDenom string `json:"bond_denom"` - MinCommissionRate string `json:"min_commission_rate"` - } `json:"params"` -} - -type ValidatorSet struct { - BlockHeight string `json:"block_height"` - Validators []struct { - Address string `json:"address"` - PubKey struct { - Type string `json:"@type"` - Key string `json:"key"` - } `json:"pub_key"` - VotingPower string `json:"voting_power"` - ProposerPriority string `json:"proposer_priority"` - } `json:"validators"` - Pagination struct { - NextKey interface{} `json:"next_key"` - Total string `json:"total"` - } `json:"pagination"` -} - -type Validators struct { - BlockHeight string `json:"block_height,omitempty"` - ValidatorsResponse []struct { - OperatorAddress string `json:"operator_address"` - ConsensusPubkey struct { - Type string `json:"@type"` - Key string `json:"key"` - } `json:"consensus_pubkey"` - Jailed bool `json:"jailed"` - Status string `json:"status"` - Tokens string `json:"tokens"` - DelegatorShares string `json:"delegator_shares"` - Description struct { - Moniker string `json:"moniker"` - Identity string `json:"identity"` - Website string `json:"website"` - SecurityContact string `json:"security_contact"` - Details string `json:"details"` - } `json:"description"` - UnbondingHeight string `json:"unbonding_height"` - UnbondingTime time.Time `json:"unbonding_time"` - Commission struct { - CommissionRates struct { - Rate string `json:"rate"` - MaxRate string `json:"max_rate"` - MaxChangeRate string `json:"max_change_rate"` - } `json:"commission_rates"` - UpdateTime time.Time `json:"update_time"` - } `json:"commission"` - MinSelfDelegation string `json:"min_self_delegation"` - } `json:"validators"` - Pagination struct { - NextKey interface{} `json:"next_key"` - Total string `json:"total"` - } `json:"pagination"` -} + "github.com/informalsystems/stakooler/client/cosmos/model" +) -func GetDelegations(account *model.Account) (Delegations, error) { - var delegations Delegations +func GetDelegations(account *model.Account) (model.Delegations, error) { + var delegations model.Delegations url := account.Chain.LCD + "/cosmos/staking/v1beta1/delegations/" + account.Address method := "GET" @@ -142,8 +42,8 @@ func GetDelegations(account *model.Account) (Delegations, error) { return delegations, nil } -func GetUnbondings(account *model.Account) (Unbondings, error) { - var unbondings Unbondings +func GetUnbondings(account *model.Account) (model.Unbondings, error) { + var unbondings model.Unbondings url := account.Chain.LCD + "/cosmos/staking/v1beta1/delegators/" + account.Address + "/unbonding_delegations" method := "GET" @@ -175,8 +75,8 @@ func GetUnbondings(account *model.Account) (Unbondings, error) { return unbondings, nil } -func GetStakingParams(chainEndpoint string) (Params, error) { - var params Params +func GetStakingParams(chainEndpoint string) (model.Params, error) { + var params model.Params url := chainEndpoint + "/cosmos/staking/v1beta1/params" method := "GET" @@ -239,8 +139,8 @@ func GetStakingParams(chainEndpoint string) (Params, error) { // return valset, nil //} -func GetChainValidators(validator *model.Validator) (Validators, error) { - var validators Validators +func GetChainValidators(validator *model.Validator) (model.Validators, error) { + var validators model.Validators url := validator.Chain.LCD + "/cosmos/staking/v1beta1/validators?pagination.limit=1000&pagination.count_total=true&status=BOND_STATUS_BONDED" method := "GET" @@ -274,8 +174,8 @@ func GetChainValidators(validator *model.Validator) (Validators, error) { return validators, nil } -func GetValidatorUnbondings(validator *model.Validator) (Unbondings, error) { - var unbondings Unbondings +func GetValidatorUnbondings(validator *model.Validator) (model.Unbondings, error) { + var unbondings model.Unbondings url := validator.Chain.LCD + "/cosmos/staking/v1beta1/validators/" + validator.ValoperAddress + "/unbonding_delegations" method := "GET" @@ -306,8 +206,8 @@ func GetValidatorUnbondings(validator *model.Validator) (Unbondings, error) { return unbondings, nil } -func GetValidatorDelegations(validator *model.Validator) (Delegations, error) { - var delegations Delegations +func GetValidatorDelegations(validator *model.Validator) (model.Delegations, error) { + var delegations model.Delegations url := validator.Chain.LCD + "/cosmos/staking/v1beta1/validators/" + validator.ValoperAddress + "/delegations?pagination.limit=15000&pagination.count_total=true" method := "GET" diff --git a/client/cosmos/model/account.go b/client/cosmos/model/account.go index d5fb5c2..f707ca7 100644 --- a/client/cosmos/model/account.go +++ b/client/cosmos/model/account.go @@ -1,6 +1,8 @@ package model -import "time" +import ( + "time" +) type Accounts struct { Entries []*Account @@ -10,17 +12,54 @@ type Account struct { Name string Address string Chain Chain + BlockTime time.Time + BlockHeight string TokensEntry []TokenEntry } type TokenEntry struct { - DisplayName string - Denom string - BlockTime time.Time - BlockHeight string - Balance float64 - Reward float64 - Delegation float64 - Unbonding float64 - Commission float64 + DisplayName string + Denom string + Balance float64 + Reward float64 + Delegation float64 + Unbonding float64 + Commission float64 + Vesting float64 + DelegatedVesting float64 +} + +type AuthResponse struct { + Account struct { + Type string `json:"@type"` + BaseVestingAccount struct { + BaseAccount struct { + Address string `json:"address,omitempty"` + PubKey string `json:"public_key,omitempty"` + AccountNumber string `json:"account_number,omitempty"` + Sequence string `json:"sequence,omitempty"` + } + OriginalVesting []struct { + Denom string `json:"denom"` + Amount string `json:"amount"` + } `json:"original_vesting"` + DelegatedFree []struct { + Denom string `json:"denom"` + Amount string `json:"amount"` + } `json:"delegated_free"` + DelegatedVesting []struct { + Denom string `json:"denom"` + Amount string `json:"amount"` + } `json:"delegated_vesting"` + EndTime string `json:"end_time"` + } `json:"base_vesting_account"` + StartTime string `json:"start_time"` + VestingPeriods []struct { + Length string `json:"length"` + Amount []struct { + Denom string `json:"denom"` + Amount string `json:"amount"` + } `json:"amount"` + } `json:"vesting_periods"` + } `json:"account"` } diff --git a/client/cosmos/model/bank.go b/client/cosmos/model/bank.go new file mode 100644 index 0000000..cb90de3 --- /dev/null +++ b/client/cosmos/model/bank.go @@ -0,0 +1,25 @@ +package model + +type BalancesResponse struct { + Balances []struct { + Denom string `json:"denom"` + Amount string `json:"amount"` + } `json:"balances"` + Pagination struct { + NextKey interface{} `json:"next_key"` + Total string `json:"total"` + } `json:"pagination"` +} + +type DenomMetadataResponse struct { + Metadata struct { + Description string `json:"description"` + DenomUnits []struct { + Denom string `json:"denom"` + Exponent int `json:"exponent"` + Aliases []string `json:"aliases"` + } `json:"denom_units"` + Base string `json:"base"` + Display string `json:"display"` + } `json:"metadata"` +} diff --git a/client/cosmos/model/config.go b/client/cosmos/model/config.go index 96471fa..9d44063 100644 --- a/client/cosmos/model/config.go +++ b/client/cosmos/model/config.go @@ -8,7 +8,7 @@ type ZabbixConfig struct { type Config struct { Accounts Accounts - Validators Validators + Validators ValidatorList Chains Chains Zabbix ZabbixConfig } diff --git a/client/cosmos/model/staking.go b/client/cosmos/model/staking.go new file mode 100644 index 0000000..749fe50 --- /dev/null +++ b/client/cosmos/model/staking.go @@ -0,0 +1,103 @@ +package model + +import "time" + +type Delegations struct { + DelegationResponses []struct { + Delegation struct { + DelegatorAddress string `json:"delegator_address"` + ValidatorAddress string `json:"validator_address"` + Shares string `json:"shares"` + } `json:"delegation"` + Balance struct { + Denom string `json:"denom"` + Amount string `json:"amount"` + } `json:"balance"` + } `json:"delegation_responses"` + Pagination struct { + NextKey interface{} `json:"next_key"` + Total string `json:"total"` + } `json:"pagination"` +} + +type Unbondings struct { + UnbondingResponses []struct { + DelegatorAddress string `json:"delegator_address"` + ValidatorAddress string `json:"validator_address"` + Entries []struct { + CreationHeight string `json:"creation_height"` + CompletionTime time.Time `json:"completion_time"` + InitialBalance string `json:"initial_balance"` + Balance string `json:"balance"` + } `json:"entries"` + } `json:"unbonding_responses"` + Pagination struct { + NextKey interface{} `json:"next_key"` + Total string `json:"total"` + } `json:"pagination"` +} + +type Params struct { + ParamsResponse struct { + UnbondingTime string `json:"unbonding_time"` + MaxValidators int `json:"max_validators"` + MaxEntries int `json:"max_entries"` + HistoricalEntries int `json:"historical_entries"` + BondDenom string `json:"bond_denom"` + MinCommissionRate string `json:"min_commission_rate"` + } `json:"params"` +} + +type ValidatorSet struct { + BlockHeight string `json:"block_height"` + Validators []struct { + Address string `json:"address"` + PubKey struct { + Type string `json:"@type"` + Key string `json:"key"` + } `json:"pub_key"` + VotingPower string `json:"voting_power"` + ProposerPriority string `json:"proposer_priority"` + } `json:"validators"` + Pagination struct { + NextKey interface{} `json:"next_key"` + Total string `json:"total"` + } `json:"pagination"` +} + +type Validators struct { + BlockHeight string `json:"block_height,omitempty"` + ValidatorsResponse []struct { + OperatorAddress string `json:"operator_address"` + ConsensusPubkey struct { + Type string `json:"@type"` + Key string `json:"key"` + } `json:"consensus_pubkey"` + Jailed bool `json:"jailed"` + Status string `json:"status"` + Tokens string `json:"tokens"` + DelegatorShares string `json:"delegator_shares"` + Description struct { + Moniker string `json:"moniker"` + Identity string `json:"identity"` + Website string `json:"website"` + SecurityContact string `json:"security_contact"` + Details string `json:"details"` + } `json:"description"` + UnbondingHeight string `json:"unbonding_height"` + UnbondingTime time.Time `json:"unbonding_time"` + Commission struct { + CommissionRates struct { + Rate string `json:"rate"` + MaxRate string `json:"max_rate"` + MaxChangeRate string `json:"max_change_rate"` + } `json:"commission_rates"` + UpdateTime time.Time `json:"update_time"` + } `json:"commission"` + MinSelfDelegation string `json:"min_self_delegation"` + } `json:"validators"` + Pagination struct { + NextKey interface{} `json:"next_key"` + Total string `json:"total"` + } `json:"pagination"` +} diff --git a/client/cosmos/model/validator.go b/client/cosmos/model/validator.go index 628eb68..12d5052 100644 --- a/client/cosmos/model/validator.go +++ b/client/cosmos/model/validator.go @@ -1,10 +1,8 @@ package model -import ( - "time" -) +import "time" -type Validators struct { +type ValidatorList struct { Entries []*Validator } diff --git a/client/cosmos/querier/account.go b/client/cosmos/querier/account.go index 58469a2..058cfd8 100644 --- a/client/cosmos/querier/account.go +++ b/client/cosmos/querier/account.go @@ -3,15 +3,15 @@ package querier import ( "errors" "fmt" + "math" + "strconv" + "strings" + "github.com/cosmos/cosmos-sdk/types/bech32" "github.com/informalsystems/stakooler/client/cosmos/api" "github.com/informalsystems/stakooler/client/cosmos/api/osmosis" "github.com/informalsystems/stakooler/client/cosmos/api/sifchain" "github.com/informalsystems/stakooler/client/cosmos/model" - "github.com/schollz/progressbar/v3" - "math" - "strconv" - "strings" ) const zeroAmount = 0.00000 @@ -21,178 +21,274 @@ type TokenDetail struct { Precision int } -func LoadTokenInfo(account *model.Account, bar *progressbar.ProgressBar) error { - - var tokens []model.TokenEntry - - // Get Latest Block Information - // Use the same block information for all the entries - blockResponse, err := api.GetLatestBlock(account.Chain) +func LoadAuthData(account *model.Account) error { + var authResponse model.AuthResponse + authResponse, err := api.GetAuth(account) if err != nil { - return errors.New(fmt.Sprintf("failed to get latest block: %s", err)) + return errors.New(fmt.Sprintf("failed to get auth info: %s", err)) + } + for _, value := range authResponse.Account.BaseVestingAccount.OriginalVesting { + metadata := GetDenomMetadata(value.Denom, *account) + amount, err2 := strconv.ParseFloat(value.Amount, 1) + if err2 != nil { + return errors.New(fmt.Sprintf("error converting rewards amount: %s", err2)) + } else { + if amount > zeroAmount { + convertedAmount := amount / math.Pow10(metadata.Precision) + foundToken := false + for j := range account.TokensEntry { + if strings.ToLower(account.TokensEntry[j].Denom) == strings.ToLower(value.Denom) { + account.TokensEntry[j].Vesting += convertedAmount + foundToken = true + } + } + // If there were no tokens of this denom yet, create one + if !foundToken { + account.TokensEntry = append(account.TokensEntry, model.TokenEntry{ + DisplayName: metadata.Symbol, + Denom: value.Denom, + Vesting: convertedAmount, + }) + } + } + } + } + + for _, value := range authResponse.Account.BaseVestingAccount.DelegatedVesting { + metadata := GetDenomMetadata(value.Denom, *account) + amount, err2 := strconv.ParseFloat(value.Amount, 1) + if err2 != nil { + return errors.New(fmt.Sprintf("error converting rewards amount: %s", err2)) + } else { + if amount > zeroAmount { + convertedAmount := amount / math.Pow10(metadata.Precision) + foundToken := false + for j := range account.TokensEntry { + if strings.ToLower(account.TokensEntry[j].Denom) == strings.ToLower(value.Denom) { + account.TokensEntry[j].DelegatedVesting += convertedAmount + foundToken = true + } + } + // If there were no tokens of this denom yet, create one + if !foundToken { + account.TokensEntry = append(account.TokensEntry, model.TokenEntry{ + DisplayName: metadata.Symbol, + Denom: value.Denom, + DelegatedVesting: convertedAmount, + }) + } + } + } } - bar.Add(1) - // Get Balances + return nil +} + +func LoadBankBalances(account *model.Account) error { + balancesResponse, err := api.GetBalances(account) if err != nil { return errors.New(fmt.Sprintf("failed to get balances: %s", err)) } - bar.Add(1) for i := range balancesResponse.Balances { // Skip liquidity pools and IBC tokens balance := balancesResponse.Balances[i] if !strings.HasPrefix(strings.ToUpper(balance.Denom), "GAMM/POOL/") && !strings.HasPrefix(strings.ToUpper(balance.Denom), "IBC/") { - metadata := GetTokenMetadata(balance.Denom, *account) - token := model.TokenEntry{} - token.DisplayName = metadata.Symbol - token.Denom = balance.Denom - token.BlockTime = blockResponse.Block.Header.Time - token.BlockHeight = blockResponse.Block.Header.Height - amount, err := strconv.ParseFloat(balance.Amount, 1) - if err != nil { - return errors.New(fmt.Sprintf("error converting balance amount: %s", err)) + metadata := GetDenomMetadata(balance.Denom, *account) + amount, err2 := strconv.ParseFloat(balance.Amount, 1) + if err2 != nil { + return errors.New(fmt.Sprintf("error converting balance amount: %s", err2)) } else { + var convertedAmount float64 if amount > zeroAmount { - convertedAmount := amount / math.Pow10(metadata.Precision) - token.Balance = convertedAmount - tokens = append(tokens, token) + convertedAmount = amount / math.Pow10(metadata.Precision) + } else { + convertedAmount = zeroAmount + } + foundToken := false + for j := range account.TokensEntry { + if strings.ToLower(account.TokensEntry[j].Denom) == strings.ToLower(balance.Denom) { + account.TokensEntry[j].Balance += convertedAmount + foundToken = true + } + } + // If there were no tokens of this denom yet, create one + if !foundToken { + account.TokensEntry = append(account.TokensEntry, model.TokenEntry{ + DisplayName: metadata.Symbol, + Denom: balance.Denom, + Balance: convertedAmount, + }) } } } } - // Get Rewards + return nil +} + +func LoadDistributionData(account *model.Account) error { + rewardsResponse, err := api.GetRewards(account) if err != nil { return errors.New(fmt.Sprintf("failed to get rewards: %s", err)) } - bar.Add(1) - totalAmount := 0.0 for i := range rewardsResponse.Total { reward := rewardsResponse.Total[i] - metadata := GetTokenMetadata(reward.Denom, *account) - amount, err := strconv.ParseFloat(reward.Amount, 1) - if err != nil { - return errors.New(fmt.Sprintf("error converting rewards amount: %s", err)) + metadata := GetDenomMetadata(reward.Denom, *account) + amount, err2 := strconv.ParseFloat(reward.Amount, 1) + if err2 != nil { + return errors.New(fmt.Sprintf("error converting rewards amount: %s", err2)) } else { if amount > zeroAmount { convertedAmount := amount / math.Pow10(metadata.Precision) - totalAmount += convertedAmount - for i := range tokens { - if strings.ToLower(tokens[i].Denom) == strings.ToLower(reward.Denom) { - tokens[i].Reward = totalAmount + foundToken := false + for j := range account.TokensEntry { + if strings.ToLower(account.TokensEntry[j].Denom) == strings.ToLower(reward.Denom) { + account.TokensEntry[j].Reward += convertedAmount + foundToken = true + } + } + // If there were no tokens of this denom yet, create one + if !foundToken { + account.TokensEntry = append(account.TokensEntry, model.TokenEntry{ + DisplayName: metadata.Symbol, + Denom: reward.Denom, + Reward: convertedAmount, + }) + } + } + } + } + + validator, err := GetValidatorAccount(account) + if err != nil { + return errors.New("cannot retrieve validator account") + } else { + commissions, err2 := api.GetCommissions(account, validator) + if err2 != nil { + return errors.New(fmt.Sprintf("Failed to get commissions: %s", err2)) + } else { + for i := range commissions.Commissions.Commission { + commission := commissions.Commissions.Commission[i] + metadata := GetDenomMetadata(commission.Denom, *account) + amount, err3 := strconv.ParseFloat(commission.Amount, 1) + if err3 != nil { + return errors.New(fmt.Sprintf("error converting commission amount: %s", err3)) + } else { + if amount > zeroAmount { + convertedAmount := amount / math.Pow10(metadata.Precision) + foundToken := false + for j := range account.TokensEntry { + if strings.ToLower(account.TokensEntry[j].Denom) == strings.ToLower(commission.Denom) { + account.TokensEntry[j].Commission += convertedAmount + foundToken = true + } + } + // If there were no tokens of this denom yet, create one + if !foundToken { + account.TokensEntry = append(account.TokensEntry, model.TokenEntry{ + DisplayName: metadata.Symbol, + Denom: commission.Denom, + Commission: convertedAmount, + }) + } } } } } } - // Get Delegations + return nil +} + +func LoadStakingData(account *model.Account) error { delegations, err := api.GetDelegations(account) if err != nil { return errors.New(fmt.Sprintf("failed to get delegations: %s", err)) } - bar.Add(1) - totalAmount = 0.0 + params, err := api.GetStakingParams(account.Chain.LCD) + if err != nil { + return errors.New(fmt.Sprintf("failed to get staking params: %s", err)) + } + + metadata := GetDenomMetadata(params.ParamsResponse.BondDenom, *account) + for i := range delegations.DelegationResponses { delegation := delegations.DelegationResponses[i] - metadata := GetTokenMetadata(delegation.Balance.Denom, *account) - amount, err := strconv.ParseFloat(delegation.Balance.Amount, 1) - if err != nil { - return errors.New(fmt.Sprintf("error converting delegation amount: %s", err)) + amount, err2 := strconv.ParseFloat(delegation.Balance.Amount, 1) + if err2 != nil { + return errors.New(fmt.Sprintf("error converting delegation amount: %s", err2)) } else { if amount > zeroAmount { convertedAmount := amount / math.Pow10(metadata.Precision) - totalAmount += convertedAmount - for i := range tokens { - if strings.ToLower(tokens[i].Denom) == strings.ToLower(delegation.Balance.Denom) { - tokens[i].Delegation = totalAmount + foundToken := false + for j := range account.TokensEntry { + if strings.ToLower(account.TokensEntry[j].Denom) == strings.ToLower(params.ParamsResponse.BondDenom) { + account.TokensEntry[j].Delegation += convertedAmount + foundToken = true } } + + if !foundToken { + account.TokensEntry = append(account.TokensEntry, model.TokenEntry{ + DisplayName: metadata.Symbol, + Denom: params.ParamsResponse.BondDenom, + Delegation: convertedAmount, + }) + } } } } - //Get Unbondings unbondings, err := api.GetUnbondings(account) if err != nil { return errors.New(fmt.Sprintf("failed to get unbondings: %s", err)) } - bar.Add(1) - totalAmount = 0.0 for i := range unbondings.UnbondingResponses { unbonding := unbondings.UnbondingResponses[i] - for i := range unbonding.Entries { - params, err := api.GetStakingParams(account.Chain.LCD) - if err != nil { - return errors.New(fmt.Sprintf("failed to get staking params: %s", err)) - } - metadata := GetTokenMetadata(params.ParamsResponse.BondDenom, *account) - amount, err := strconv.ParseFloat(unbonding.Entries[i].Balance, 1) - if err != nil { - return errors.New(fmt.Sprintf("error converting unbonding amount: %s", err)) + for j := range unbonding.Entries { + amount, err2 := strconv.ParseFloat(unbonding.Entries[j].Balance, 1) + if err2 != nil { + return errors.New(fmt.Sprintf("error converting unbonding amount: %s", err2)) } else { if amount > zeroAmount { convertedAmount := amount / math.Pow10(metadata.Precision) - totalAmount += convertedAmount - for i := range tokens { - if strings.ToLower(tokens[i].Denom) == strings.ToLower(params.ParamsResponse.BondDenom) { - tokens[i].Unbonding = totalAmount + foundToken := false + for k := range account.TokensEntry { + if strings.ToLower(account.TokensEntry[k].Denom) == strings.ToLower(params.ParamsResponse.BondDenom) { + account.TokensEntry[k].Unbonding += convertedAmount + foundToken = true } } - } - } - } - } - // Get commissions - totalAmount = 0.0 - validator, err := GetValidatorAccount(account) - if err != nil { - return errors.New("cannot retrieve validator account") - } else { - commissions, err := api.GetCommissions(account, validator) - bar.Add(1) - if err != nil { - return errors.New(fmt.Sprintf("Failed to get commissions: %s", err)) - } else { - for i := range commissions.Commissions.Commission { - commission := commissions.Commissions.Commission[i] - metadata := GetTokenMetadata(commission.Denom, *account) - amount, err := strconv.ParseFloat(commission.Amount, 1) - if err != nil { - return errors.New(fmt.Sprintf("error converting commission amount: %s", err)) - } else { - if amount > zeroAmount { - convertedAmount := amount / math.Pow10(metadata.Precision) - totalAmount += convertedAmount - for i := range tokens { - if strings.ToLower(tokens[i].Denom) == strings.ToLower(commission.Denom) { - tokens[i].Commission = totalAmount - } - } + if !foundToken { + account.TokensEntry = append(account.TokensEntry, model.TokenEntry{ + DisplayName: metadata.Symbol, + Denom: params.ParamsResponse.BondDenom, + Unbonding: convertedAmount, + }) } } } } } - account.TokensEntry = tokens + return nil } -// GetTokenMetadata This function checks if the denom is for a chain (e.g. Osmosis or Sifchain) +// GetDenomMetadata This function checks if the denom is for a chain (e.g. Osmosis or Sifchain) // that keeps an asset list or registry for their denominations for the IBC denoms // or the liquidity pools. The function returns the UI friendly name and the exponent // used by the denom. If there are any errors just return the denom and 0 for // the precision exponent -func GetTokenMetadata(denom string, account model.Account) TokenDetail { +func GetDenomMetadata(denom string, account model.Account) TokenDetail { symbol := denom precision := 0 bech32Prefix, _, _ := bech32.DecodeAndConvert(account.Address) @@ -220,7 +316,7 @@ func GetTokenMetadata(denom string, account model.Account) TokenDetail { precision = 6 } else { symbol = denomMetadata.Metadata.Display - precision = denomMetadata.GetExponent() + precision = api.GetExponent(&denomMetadata) } } diff --git a/client/display/csv.go b/client/display/csv.go index c0d0954..9f3b305 100644 --- a/client/display/csv.go +++ b/client/display/csv.go @@ -16,7 +16,7 @@ func WriteAccountsCSV(accounts *model.Accounts) { w := csv.NewWriter(os.Stdout) defer w.Flush() - header := []string{"account_name", "account_address", "chain_id", "block_height", "block_time", "token", "balance", "rewards", "staked", "unbonding", "commissions", "total"} + header := []string{"account_name", "account_address", "chain_id", "block_height", "block_time", "token", "balance", "rewards", "staked", "unbonding", "commissions", "original_vesting", "delegated_vesting", "total"} if err := w.Write(header); err != nil { log.Fatalln("error writing record to file", err) } @@ -27,7 +27,7 @@ func WriteAccountsCSV(accounts *model.Accounts) { // In case there is no token information if len(entries) == 0 { record := []string{ - accounts.Entries[acctIdx].Name, accounts.Entries[acctIdx].Address, "na", "na", "na", "na", "na", "na", + accounts.Entries[acctIdx].Name, accounts.Entries[acctIdx].Address, "na", "na", "na", "na", "na", "na", "na", "na", "na", "na", "na", "na", } if err := w.Write(record); err != nil { @@ -40,14 +40,16 @@ func WriteAccountsCSV(accounts *model.Accounts) { accounts.Entries[acctIdx].Name, accounts.Entries[acctIdx].Address, accounts.Entries[acctIdx].Chain.ID, - accounts.Entries[acctIdx].TokensEntry[i].BlockHeight, - accounts.Entries[acctIdx].TokensEntry[i].BlockTime.Format(time.RFC3339Nano), + accounts.Entries[acctIdx].BlockHeight, + accounts.Entries[acctIdx].BlockTime.Format(time.RFC3339Nano), accounts.Entries[acctIdx].TokensEntry[i].DisplayName, fmt.Sprintf("%f", accounts.Entries[acctIdx].TokensEntry[i].Balance), fmt.Sprintf("%f", accounts.Entries[acctIdx].TokensEntry[i].Reward), fmt.Sprintf("%f", accounts.Entries[acctIdx].TokensEntry[i].Delegation), fmt.Sprintf("%f", accounts.Entries[acctIdx].TokensEntry[i].Unbonding), fmt.Sprintf("%f", accounts.Entries[acctIdx].TokensEntry[i].Commission), + fmt.Sprintf("%f", accounts.Entries[acctIdx].TokensEntry[i].Vesting), + fmt.Sprintf("%f", accounts.Entries[acctIdx].TokensEntry[i].DelegatedVesting), fmt.Sprintf("%f", total), } if err := w.Write(record); err != nil { @@ -58,7 +60,7 @@ func WriteAccountsCSV(accounts *model.Accounts) { } } -func WriteValidatorCSV(validators *model.Validators) { +func WriteValidatorCSV(validators *model.ValidatorList) { // Outputs to Stdout w := csv.NewWriter(os.Stdout) diff --git a/client/display/table.go b/client/display/table.go index 9649654..fb8adca 100644 --- a/client/display/table.go +++ b/client/display/table.go @@ -17,14 +17,14 @@ func PrintAccountDetailsTable(accounts *model.Accounts) { t.SetOutputMirror(os.Stdout) t.SetTitle(strings.ToUpper("Accounts - Details")) t.SetCaption(fmt.Sprintf("Retrieved information for %d accounts", len(accounts.Entries))) - t.AppendHeader(table.Row{"Name", "Account", "Token", "Balance", "Rewards", "Staked", "Unbonding", "Commissions", "Total"}) + t.AppendHeader(table.Row{"Name", "Account", "Token", "Balance", "Rewards", "Staked", "Unbonding", "Commissions", "Original Vesting", "Delegated Vesting", "Total"}) for acctIdx := range accounts.Entries { entries := accounts.Entries[acctIdx].TokensEntry // To store the keys in slice in sorted order for i := range accounts.Entries[acctIdx].TokensEntry { - total := entries[i].Balance + entries[i].Reward + entries[i].Delegation + entries[i].Unbonding + entries[i].Commission + total := entries[i].Vesting - entries[i].DelegatedVesting + entries[i].Balance + entries[i].Reward + entries[i].Delegation + entries[i].Unbonding + entries[i].Commission if i == 0 { t.AppendRow([]interface{}{ accounts.Entries[acctIdx].Name, @@ -35,6 +35,8 @@ func PrintAccountDetailsTable(accounts *model.Accounts) { FilterZeroValue(accounts.Entries[acctIdx].TokensEntry[i].Delegation), FilterZeroValue(accounts.Entries[acctIdx].TokensEntry[i].Unbonding), FilterZeroValue(accounts.Entries[acctIdx].TokensEntry[i].Commission), + FilterZeroValue(accounts.Entries[acctIdx].TokensEntry[i].Vesting), + FilterZeroValue(accounts.Entries[acctIdx].TokensEntry[i].DelegatedVesting), FilterZeroValue(total), }) } else { @@ -47,6 +49,8 @@ func PrintAccountDetailsTable(accounts *model.Accounts) { FilterZeroValue(accounts.Entries[acctIdx].TokensEntry[i].Delegation), FilterZeroValue(accounts.Entries[acctIdx].TokensEntry[i].Unbonding), FilterZeroValue(accounts.Entries[acctIdx].TokensEntry[i].Commission), + FilterZeroValue(accounts.Entries[acctIdx].TokensEntry[i].Vesting), + FilterZeroValue(accounts.Entries[acctIdx].TokensEntry[i].DelegatedVesting), FilterZeroValue(total), }) } @@ -64,13 +68,15 @@ func PrintAccountDetailsTable(accounts *model.Accounts) { {Name: "Staked", Align: text.AlignRight, AlignHeader: text.AlignCenter}, {Name: "Unbonding", Align: text.AlignRight, AlignHeader: text.AlignCenter}, {Name: "Commissions", Align: text.AlignRight, AlignHeader: text.AlignCenter}, + {Name: "Original Vesting", Align: text.AlignRight, AlignHeader: text.AlignCenter}, + {Name: "Delegated Vesting", Align: text.AlignRight, AlignHeader: text.AlignCenter}, {Name: "Total", Align: text.AlignRight, AlignHeader: text.AlignCenter}, }) t.Render() return } -func PrintValidatorStasTable(validators *model.Validators) { +func PrintValidatorStasTable(validators *model.ValidatorList) { t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.SetTitle(strings.ToUpper("Validator - Statistics")) diff --git a/client/display/zabbix.go b/client/display/zabbix.go index ff1cea9..529c2a0 100644 --- a/client/display/zabbix.go +++ b/client/display/zabbix.go @@ -35,15 +35,15 @@ func ZbxAccountsDetails(config *model.Config) { if chain.ID == account.Chain.ID { var metrics []*sender.Metric for _, token := range account.TokensEntry { - metrics = append(metrics, sender.NewMetric(account.Chain.ID, "account.address.["+account.Address+"]", account.Address, token.BlockTime.Unix())) - metrics = append(metrics, sender.NewMetric(account.Chain.ID, "account.balance.["+account.Address+"]", fmt.Sprintf("%.2f", token.Balance), token.BlockTime.Unix())) - metrics = append(metrics, sender.NewMetric(account.Chain.ID, "account.height.["+account.Address+"]", token.BlockHeight, token.BlockTime.Unix())) - metrics = append(metrics, sender.NewMetric(account.Chain.ID, "account.commission.["+account.Address+"]", fmt.Sprintf("%.2f", token.Commission), token.BlockTime.Unix())) - metrics = append(metrics, sender.NewMetric(account.Chain.ID, "account.denom.["+account.Address+"]", token.DisplayName, token.BlockTime.Unix())) - metrics = append(metrics, sender.NewMetric(account.Chain.ID, "account.name.["+account.Address+"]", account.Name, token.BlockTime.Unix())) - metrics = append(metrics, sender.NewMetric(account.Chain.ID, "account.rewards.["+account.Address+"]", fmt.Sprintf("%.2f", token.Reward), token.BlockTime.Unix())) - metrics = append(metrics, sender.NewMetric(account.Chain.ID, "account.staked.["+account.Address+"]", fmt.Sprintf("%.2f", token.Delegation), token.BlockTime.Unix())) - metrics = append(metrics, sender.NewMetric(account.Chain.ID, "account.unbonding.["+account.Address+"]", fmt.Sprintf("%.2f", token.Unbonding), token.BlockTime.Unix())) + metrics = append(metrics, sender.NewMetric(account.Chain.ID, "account.address.["+account.Address+"]", account.Address, account.BlockTime.Unix())) + metrics = append(metrics, sender.NewMetric(account.Chain.ID, "account.balance.["+account.Address+"]", fmt.Sprintf("%.2f", token.Balance), account.BlockTime.Unix())) + metrics = append(metrics, sender.NewMetric(account.Chain.ID, "account.height.["+account.Address+"]", account.BlockHeight, account.BlockTime.Unix())) + metrics = append(metrics, sender.NewMetric(account.Chain.ID, "account.commission.["+account.Address+"]", fmt.Sprintf("%.2f", token.Commission), account.BlockTime.Unix())) + metrics = append(metrics, sender.NewMetric(account.Chain.ID, "account.denom.["+account.Address+"]", token.DisplayName, account.BlockTime.Unix())) + metrics = append(metrics, sender.NewMetric(account.Chain.ID, "account.name.["+account.Address+"]", account.Name, account.BlockTime.Unix())) + metrics = append(metrics, sender.NewMetric(account.Chain.ID, "account.rewards.["+account.Address+"]", fmt.Sprintf("%.2f", token.Reward), account.BlockTime.Unix())) + metrics = append(metrics, sender.NewMetric(account.Chain.ID, "account.staked.["+account.Address+"]", fmt.Sprintf("%.2f", token.Delegation), account.BlockTime.Unix())) + metrics = append(metrics, sender.NewMetric(account.Chain.ID, "account.unbonding.["+account.Address+"]", fmt.Sprintf("%.2f", token.Unbonding), account.BlockTime.Unix())) } diff --git a/cmd/accounts_details.go b/cmd/accounts_details.go index 21f77e8..124f90a 100644 --- a/cmd/accounts_details.go +++ b/cmd/accounts_details.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "github.com/informalsystems/stakooler/client/cosmos/api" "github.com/informalsystems/stakooler/client/cosmos/querier" "github.com/informalsystems/stakooler/client/display" "github.com/informalsystems/stakooler/config" @@ -56,16 +57,38 @@ It shows tokens balance, rewards, delegation and unbonding values per account`, bar.Describe(fmt.Sprintf("Getting account %s details", acct.Address)) } - err := querier.LoadTokenInfo(acct, bar) + // Get latest block information to include in the account + blockInfo, err := api.GetLatestBlock(acct.Chain) if err != nil { - bar.Describe(fmt.Sprintf("failed to retrieve %s details: %s", acct.Address, err)) - //os.Exit(1) - } else { - // Don't show this if csv option enabled - if barEnabled { - bar.Describe(fmt.Sprintf("Got account %s details", acct.Address)) - } + bar.Describe(fmt.Sprintf("failed to get latest block: %s", err)) } + bar.Add(1) + + acct.BlockHeight = blockInfo.Block.Header.Height + acct.BlockTime = blockInfo.Block.Header.Time + + err = querier.LoadAuthData(acct) + if err != nil { + bar.Describe(err.Error()) + } + + err = querier.LoadBankBalances(acct) + if err != nil { + bar.Describe(err.Error()) + } + bar.Add(1) + + err = querier.LoadDistributionData(acct) + if err != nil { + bar.Describe(err.Error()) + } + bar.Add(1) + + err = querier.LoadStakingData(acct) + if err != nil { + bar.Describe(err.Error()) + } + bar.Add(1) } // Hide bar diff --git a/config/config.go b/config/config.go index e15e412..a21cd6f 100644 --- a/config/config.go +++ b/config/config.go @@ -57,7 +57,7 @@ func LoadConfig(configPath string) (model.Config, error) { config := model.Config{} accounts := model.Accounts{} chains := model.Chains{} - validators := model.Validators{} + validators := model.ValidatorList{} err := viper.ReadInConfig() // Find and read the config file diff --git a/go.mod b/go.mod index a8956b2..081cb81 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.18 // indirect @@ -35,6 +36,7 @@ require ( golang.org/x/crypto v0.7.0 // indirect golang.org/x/sys v0.7.0 // indirect golang.org/x/term v0.7.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index e386d4e..e79a2b1 100644 --- a/go.sum +++ b/go.sum @@ -1945,7 +1945,6 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -2464,7 +2463,6 @@ github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= @@ -3954,7 +3952,6 @@ gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=