From 9b299bf9f72df0c4e8f18305eb1a91cc92e12faa Mon Sep 17 00:00:00 2001 From: cordt-sei <165932662+cordt-sei@users.noreply.github.com> Date: Mon, 2 Sep 2024 00:35:07 -0600 Subject: [PATCH] Add Sei blockchain (#56) **Summary**: This revamps handling of Cosmos SDK-based chains and greatly simplifying the addition of new chains. The changes simplify the codebase, reduce redundancy, and make it easier to manage and extend. ### Key Updates: 1. **Centralized Cosmos SDK Logic**: - Modified the `FetchCosmosSDKNakaCoeff` function in `cosmos.go` to be exportable and handle all Cosmos SDK-based chains. 2. **New Chain Added**: - Added support for the **Sei** network. - Updated `chain.go` to include Sei in the list of supported chains. 3. **Streamlined Existing Chains**: - Refactored `agoric.go`, `osmosis.go`, `regen.go`, `sei.go`, and `stargaze.go` to use this shared function, cleaning up much redundant code. - Included pool data fetching to ensure more accurate Nakamoto coefficient calculations, since the previous method would not have included any for unbonded validators. [correct me if this seems off] ### Testing and Validation: - **Manual Testing**: Ran tests on each refactored chain to ensure the Nakamoto coefficient calculations are accurate and that everything works as expected. - **Validation**: Checked logs to confirm the refactored code runs smoothly and handles errors correctly. ### Looking Ahead: - **Automated Testing**: May be worth adding automated tests for the `FetchCosmosSDKNakaCoeff` function to ensure it continues to work well across different chains. It should work universally as-is, but this may change in the future. - **Further Simplification**: Could look into whether other non-Cosmos SDK chains could benefit from a similar centralized approach. Overall this should make the Nakamoto coefficient calculator easier to manage and extend, while keeping all the current features intact. Here's the image you requested also. ![image](https://github.com/user-attachments/assets/4810b55f-fbab-4909-924c-e5b998790092) --------- Co-authored-by: Cordtus --- README.md | 1 + core/chains/agoric.go | 67 +-------------------- core/chains/chain.go | 27 ++++++++- core/chains/cosmos.go | 129 +++++++++++++++++++++++++++++++--------- core/chains/juno.go | 63 +------------------- core/chains/osmosis.go | 79 +----------------------- core/chains/regen.go | 85 +------------------------- core/chains/sei.go | 8 +++ core/chains/stargaze.go | 6 +- 9 files changed, 150 insertions(+), 315 deletions(-) create mode 100644 core/chains/sei.go diff --git a/README.md b/README.md index e622469..e5682a8 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ NOTE: You can get your API Key by signing up [here](https://www.validators.app/u 20. [MultiversX](https://multiversx.com/) 21. [Polkadot](https://polkadot.network/) 22. [Aptos](https://aptosfoundation.org/) +23. [Sei](https://sei.io/) ### Notes diff --git a/core/chains/agoric.go b/core/chains/agoric.go index 3a3b03a..15d92ef 100644 --- a/core/chains/agoric.go +++ b/core/chains/agoric.go @@ -1,69 +1,8 @@ package chains -import ( - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "sort" - "strconv" - - utils "github.com/xenowits/nakamoto-coefficient-calculator/core/utils" -) - -type AgoricResponse struct { - Result []struct { - Tokens string `json:"tokens"` - } `json:"result"` -} - -type AgoricErrorResponse struct { - Id int `json:"id"` - Jsonrpc string `json:"jsonrpc"` - Error string `json:"error"` -} - func Agoric() (int, error) { - votingPowers := make([]int64, 0, 200) - - url := fmt.Sprintf("https://lcd-agoric.keplr.app/staking/validators") - resp, err := http.Get(url) - if err != nil { - errBody, _ := io.ReadAll(resp.Body) - var errResp AgoricErrorResponse - json.Unmarshal(errBody, &errResp) - log.Println(errResp.Error) - return 0, err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return 0, err - } - - var response AgoricResponse - err = json.Unmarshal(body, &response) - if err != nil { - return 0, err - } - - // loop through the validators voting powers - for _, ele := range response.Result { - val, _ := strconv.Atoi(ele.Tokens) - votingPowers = append(votingPowers, int64(val)) - } - - // need to sort the powers in descending order since they are in random order - sort.Slice(votingPowers, func(i, j int) bool { return votingPowers[i] > votingPowers[j] }) - - totalVotingPower := utils.CalculateTotalVotingPower(votingPowers) - fmt.Println("Total voting power:", totalVotingPower) - - // // now we're ready to calculate the nakomoto coefficient - nakamotoCoefficient := utils.CalcNakamotoCoefficient(totalVotingPower, votingPowers) - fmt.Println("The Nakamoto coefficient for agoric is", nakamotoCoefficient) + validatorURL := "https://main.api.agoric.net/cosmos/staking/v1beta1/validators?page.offset=1&pagination.limit=100&status=BOND_STATUS_BONDED" + stakingPoolURL := "https://main.api.agoric.net/cosmos/staking/v1beta1/pool" - return nakamotoCoefficient, nil + return FetchCosmosSDKNakaCoeff("agoric", validatorURL, stakingPoolURL) } diff --git a/core/chains/chain.go b/core/chains/chain.go index fe9f195..55be02b 100644 --- a/core/chains/chain.go +++ b/core/chains/chain.go @@ -39,6 +39,7 @@ const ( PLS Token = "PLS" REGEN Token = "REGEN" RUNE Token = "RUNE" + SEI Token = "SEI" SOL Token = "SOL" STARS Token = "STARS" SUI Token = "SUI" @@ -84,6 +85,8 @@ func (t Token) ChainName() string { return "Regen Network" case RUNE: return "Thorchain" + case SEI: + return "Sei" case SOL: return "Solana" case STARS: @@ -97,7 +100,7 @@ func (t Token) ChainName() string { } } -var Tokens = []Token{ALGO, APT, ATOM, AVAX, BLD, BNB, DOT, EGLD, GRT, HBAR, JUNO, MATIC, MINA, NEAR, OSMO, PLS, REGEN, RUNE, SOL, STARS, SUI, TIA} +var Tokens = []Token{ALGO, APT, ATOM, AVAX, BLD, BNB, DOT, EGLD, GRT, HBAR, JUNO, MATIC, MINA, NEAR, OSMO, PLS, REGEN, RUNE, SEI, SOL, STARS, SUI, TIA} // NewState returns a new fresh state. func NewState() ChainState { @@ -111,7 +114,7 @@ func RefreshChainState(prevState ChainState) ChainState { for _, token := range Tokens { currVal, err := newValues(token) if err != nil { - log.Println("failed to update chain info", token, err) + log.Println("Failed to update chain info:", token, err) continue } @@ -130,6 +133,8 @@ func newValues(token Token) (int, error) { err error ) + log.Printf("Calculating Nakamoto coefficient for %s", token.ChainName()) + switch token { case ALGO: currVal, err = Algorand() @@ -167,16 +172,32 @@ func newValues(token Token) (int, error) { currVal, err = Regen() case RUNE: currVal, err = Thorchain() + case SEI: + log.Println("Attempting to calculate Sei Nakamoto coefficient...") + currVal, err = Sei() + if err != nil { + log.Printf("Error calculating Sei Nakamoto coefficient: %v", err) + } case SOL: currVal, err = Solana() case STARS: + log.Println("Attempting to calculate Stargaze Nakamoto coefficient...") currVal, err = Stargaze() + if err != nil { + log.Printf("Error calculating Stargaze Nakamoto coefficient: %v", err) + } case SUI: currVal, err = Sui() case TIA: currVal, err = Celestia() default: - return 0, fmt.Errorf("chain not found %s", token) + return 0, fmt.Errorf("chain not found: %s", token) + } + + if err != nil { + log.Printf("Error in chain %s: %v", token.ChainName(), err) + } else { + log.Printf("Successfully calculated Nakamoto coefficient for %s: %d", token.ChainName(), currVal) } return currVal, err diff --git a/core/chains/cosmos.go b/core/chains/cosmos.go index 67b47d6..3459e77 100644 --- a/core/chains/cosmos.go +++ b/core/chains/cosmos.go @@ -18,82 +18,155 @@ import ( const BONDED = "BOND_STATUS_BONDED" -type cosmosResponse struct { +func Cosmos() (int, error) { + validatorDataURL := "https://proxy.atomscan.com/cosmoshub-lcd/cosmos/staking/v1beta1/validators?page.offset=1&pagination.limit=500&status=BOND_STATUS_BONDED" + stakingPoolURL := "https://proxy.atomscan.com/cosmoshub-lcd/cosmos/staking/v1beta1/pool" + + return FetchCosmosSDKNakaCoeff("cosmos", validatorDataURL, stakingPoolURL) +} + +type cosmosValidatorData struct { Validators []struct { - Status string `json:"status"` - Tokens string `json:"tokens"` + 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"` } `json:"validators"` } -func Cosmos() (int, error) { - url := "https://proxy.atomscan.com/cosmoshub-lcd/cosmos/staking/v1beta1/validators?page.offset=1&pagination.limit=500&status=BOND_STATUS_BONDED" - return fetchCosmosSDKNakaCoeff("cosmos", url) +type cosmosStakingPoolData struct { + Pool struct { + NotBondedTokens string `json:"not_bonded_tokens"` + BondedTokens string `json:"bonded_tokens"` + } `json:"pool"` } -// fetchCosmosSDKNakaCoeff returns the nakamoto coefficient for the provided cosmos SDK-based chain using the provided url. -func fetchCosmosSDKNakaCoeff(chainName, url string) (int, error) { +// fetchCosmosSDKNakaCoeff returns the nakamoto coefficient for a given cosmos SDK-based chain through REST API. +func FetchCosmosSDKNakaCoeff(chainName, validatorURL, poolURL string) (int, error) { var ( votingPowers []big.Int - response cosmosResponse + validators cosmosValidatorData + pool cosmosStakingPoolData err error ) - response, err = fetch(url) + log.Printf("Fetching data for %s", chainName) + + // Fetch the validator data + validators, err = fetchValidatorData(validatorURL) if err != nil { - return 0, fmt.Errorf("failed to fetch data for %s: %w", chainName, err) + return 0, fmt.Errorf("failed to fetch validator data for %s: %w", chainName, err) + } + + // Fetch the staking pool data to get the total bonded tokens + pool, err = fetchStakingPoolData(poolURL) + if err != nil { + return 0, fmt.Errorf("failed to fetch pool data for %s: %w", chainName, err) + } + + // Convert the bonded tokens from the pool response + totalVotingPower, ok := new(big.Int).SetString(pool.Pool.BondedTokens, 10) + if !ok { + return 0, errors.New("failed to convert bonded tokens to big.Int") } - // loop through the validators voting powers - for _, ele := range response.Validators { + // Loop through the validators' voting powers + for _, ele := range validators.Validators { if ele.Status != BONDED { continue } - val, _ := strconv.Atoi(ele.Tokens) + val, err := strconv.Atoi(ele.Tokens) + if err != nil { + log.Printf("Error parsing token value for %s: %s", chainName, ele.Tokens) + continue + } votingPowers = append(votingPowers, *big.NewInt(int64(val))) } - // Sort the powers in descending order since they maybe in random order + // Summarize voting powers for logging + log.Printf("Voting powers for %s: %d validators with a total voting power of %s", chainName, len(votingPowers), totalVotingPower.String()) + + if len(votingPowers) == 0 { + return 0, fmt.Errorf("no valid voting powers found for %s", chainName) + } + + // Sort the powers in descending order since they may be in random order sort.Slice(votingPowers, func(i, j int) bool { - res := (&votingPowers[i]).Cmp(&votingPowers[j]) - return res == 1 + return votingPowers[i].Cmp(&votingPowers[j]) > 0 }) - totalVotingPower := utils.CalculateTotalVotingPowerBigNums(votingPowers) - fmt.Println("Total voting power:", totalVotingPower) - - // Now we're ready to calculate the nakamoto coefficient + // Calculate the Nakamoto coefficient nakamotoCoefficient := utils.CalcNakamotoCoefficientBigNums(totalVotingPower, votingPowers) - fmt.Printf("The Nakamoto coefficient for %s is %d\n", chainName, nakamotoCoefficient) + log.Printf("The Nakamoto coefficient for %s is %d", chainName, nakamotoCoefficient) return nakamotoCoefficient, nil } -func fetch(url string) (cosmosResponse, error) { + +// Fetches data on active validator set +func fetchValidatorData(url string) (cosmosValidatorData, error) { + ctx, cancelFunc := context.WithTimeout(context.Background(), 10*time.Second) + defer cancelFunc() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + log.Println(err) + return cosmosValidatorData{}, errors.New("create get request for cosmos validators") + } + + resp, err := new(http.Client).Do(req) + if err != nil { + log.Println(err) + return cosmosValidatorData{}, errors.New("get request unsuccessful for cosmos validators") + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return cosmosValidatorData{}, err + } + + var response cosmosValidatorData + err = json.Unmarshal(body, &response) + if err != nil { + return cosmosValidatorData{}, err + } + + return response, nil +} + +// Fetches staking pool data incl bonded and not_bonded tokens +func fetchStakingPoolData(url string) (cosmosStakingPoolData, error) { ctx, cancelFunc := context.WithTimeout(context.Background(), 10*time.Second) defer cancelFunc() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { log.Println(err) - return cosmosResponse{}, errors.New("create get request for cosmos") + return cosmosStakingPoolData{}, errors.New("create get request for cosmos pool") } resp, err := new(http.Client).Do(req) if err != nil { log.Println(err) - return cosmosResponse{}, errors.New("get request unsuccessful for cosmos") + return cosmosStakingPoolData{}, errors.New("get request unsuccessful for cosmos pool") } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - return cosmosResponse{}, err + return cosmosStakingPoolData{}, err } - var response cosmosResponse + var response cosmosStakingPoolData err = json.Unmarshal(body, &response) if err != nil { - return cosmosResponse{}, nil + return cosmosStakingPoolData{}, err } return response, nil diff --git a/core/chains/juno.go b/core/chains/juno.go index c9e5d76..f5df93b 100644 --- a/core/chains/juno.go +++ b/core/chains/juno.go @@ -1,65 +1,8 @@ package chains -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "sort" - - utils "github.com/xenowits/nakamoto-coefficient-calculator/core/utils" -) - -const BOND_STATUS_BONDED = "BOND_STATUS_BONDED" -const JunoValidatorsUrl = "https://validators.cosmos.directory/chains/juno" - -type JunoValidators struct { - Name string `json:"name"` - Validators []struct { - Status string `json:""` - Tokens int64 `json:",string"` - Jailed bool `json:""` - } `json:"validators"` -} - func Juno() (int, error) { - var votingPowers []int64 - - resp, err := http.Get(JunoValidatorsUrl) - if err != nil { - return 0, err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return 0, err - } - - var response JunoValidators - err = json.Unmarshal(body, &response) - if err != nil { - return 0, err - } - - // loop through the validators voting powers - for _, ele := range response.Validators { - if ele.Jailed || ele.Status != BOND_STATUS_BONDED { - continue - } - - votingPowers = append(votingPowers, ele.Tokens) - } - - // need to sort the powers in descending order since they are in random order - sort.Slice(votingPowers, func(i, j int) bool { return votingPowers[i] > votingPowers[j] }) - - totalVotingPower := utils.CalculateTotalVotingPower(votingPowers) - fmt.Println("Total voting power:", totalVotingPower) - - // now we're ready to calculate the nakomoto coefficient - nakamotoCoefficient := utils.CalcNakamotoCoefficient(totalVotingPower, votingPowers) - fmt.Println("The Nakamoto coefficient for juno is", nakamotoCoefficient) + validatorsURL := "https://api.juno.basementnodes.ca/cosmos/staking/v1beta1/validators?page.offset=1&pagination.limit=100&status=BOND_STATUS_BONDED" + stakingPoolURL := "https://api.juno.basementnodes.ca/cosmos/staking/v1beta1/pool" - return nakamotoCoefficient, nil + return FetchCosmosSDKNakaCoeff("juno", validatorsURL, stakingPoolURL) } diff --git a/core/chains/osmosis.go b/core/chains/osmosis.go index 80dcdd1..f41413c 100644 --- a/core/chains/osmosis.go +++ b/core/chains/osmosis.go @@ -1,81 +1,8 @@ package chains -import ( - "encoding/json" - "fmt" - utils "github.com/xenowits/nakamoto-coefficient-calculator/core/utils" - "io/ioutil" - "log" - "net/http" - "sort" - "strconv" -) - -type OsmosisResponse struct { - Height string `json:"height"` - Result []struct { - OperatorAddress string `json:"operator_address"` - ConsensusPubkey struct { - Type string `json:"type"` - Value string `json:"value"` - } `json:"consensus_pubkey"` - Tokens string `json:"tokens"` - 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"` - } `json:"result"` -} - -type OsmosisErrorResponse struct { - Id int `json:"id"` - Jsonrpc string `json:"jsonrpc"` - Error string `json:"error"` -} - func Osmosis() (int, error) { - votingPowers := make([]int64, 0, 200) - - url := fmt.Sprintf("https://lcd-osmosis.keplr.app/staking/validators") - resp, err := http.Get(url) - if err != nil { - errBody, _ := ioutil.ReadAll(resp.Body) - var errResp OsmosisErrorResponse - json.Unmarshal(errBody, &errResp) - log.Println(errResp.Error) - return 0, err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return 0, err - } - - var response OsmosisResponse - err = json.Unmarshal(body, &response) - if err != nil { - return 0, err - } - - // loop through the validators voting powers - for _, ele := range response.Result { - val, _ := strconv.Atoi(ele.Tokens) - votingPowers = append(votingPowers, int64(val)) - } - - // need to sort the powers in descending order since they are in random order - sort.Slice(votingPowers, func(i, j int) bool { return votingPowers[i] > votingPowers[j] }) - - totalVotingPower := utils.CalculateTotalVotingPower(votingPowers) - fmt.Println("Total voting power:", totalVotingPower) - - // // now we're ready to calculate the nakomoto coefficient - nakamotoCoefficient := utils.CalcNakamotoCoefficient(totalVotingPower, votingPowers) - fmt.Println("The Nakamoto coefficient for osmosiszone is", nakamotoCoefficient) + validatorURL := "https://rest.osmosis.goldenratiostaking.net/cosmos/staking/v1beta1/validators?page.offset=1&pagination.limit=500&status=BOND_STATUS_BONDED" + stakingPoolURL := "https://rest.osmosis.goldenratiostaking.net/cosmos/staking/v1beta1/pool" - return nakamotoCoefficient, nil + return FetchCosmosSDKNakaCoeff("osmosis", validatorURL, stakingPoolURL) } diff --git a/core/chains/regen.go b/core/chains/regen.go index 7f1edd4..7c0079c 100644 --- a/core/chains/regen.go +++ b/core/chains/regen.go @@ -1,87 +1,8 @@ package chains -import ( - "encoding/json" - "fmt" - "github.com/xenowits/nakamoto-coefficient-calculator/core/utils" - "io" - "log" - "math/big" - "net/http" - "sort" - "strconv" -) - -type RegenResponse struct { - Data []struct { - Tokens interface{} `json:"tokens"` - } `json:"data"` -} - func Regen() (int, error) { - const url = "https://api.regen.aneka.io/validators/details/all" - - resp, err := http.Get(url) - if err != nil { - log.Println(err) - return 0, err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return 0, err - } - - var response RegenResponse - err = json.Unmarshal(body, &response) - if err != nil { - return 0, err - } - - // Loop through the validators voting powers. - var votingPowers []big.Int - for _, ele := range response.Data { - tokens, err := getInt(ele.Tokens) - if err != nil { - fmt.Printf("regen error, ignoring: %s\n", err.Error()) - continue - } - votingPowers = append(votingPowers, *big.NewInt(tokens)) - } - - // Need to sort the powers in descending order since they maybe in random order. - sort.Slice(votingPowers, func(i, j int) bool { - res := (&votingPowers[i]).Cmp(&votingPowers[j]) - if res == 1 { - return true - } - return false - }) - - totalVotingPower := utils.CalculateTotalVotingPowerBigNums(votingPowers) - fmt.Println("Total voting power:", new(big.Float).SetInt(totalVotingPower)) - - // now we're ready to calculate the nakamoto coefficient - nakamotoCoefficient := utils.CalcNakamotoCoefficientBigNums(totalVotingPower, votingPowers) - fmt.Println("The Nakamoto coefficient for regen network is", nakamotoCoefficient) - - return nakamotoCoefficient, nil -} + validatorURL := "https://regen.api.m.stavr.tech/cosmos/staking/v1beta1/validators?page.offset=1&pagination.limit=100&status=BOND_STATUS_BONDED" + poolURL := "https://regen.api.m.stavr.tech/cosmos/staking/v1beta1/pool" -// getInt returns an int64 or an error. The weird interface is included since terra responses contain -// a mix of both integer and strings for token values. -func getInt(v interface{}) (int64, error) { - switch v := v.(type) { - case float64: - return int64(v), nil - case string: - c, err := strconv.Atoi(v) - if err != nil { - return 0, err - } - return int64(c), nil - default: - return 0, fmt.Errorf("conversion to int from %T not supported", v) - } + return FetchCosmosSDKNakaCoeff("regen", validatorURL, poolURL) } diff --git a/core/chains/sei.go b/core/chains/sei.go new file mode 100644 index 0000000..0ac1594 --- /dev/null +++ b/core/chains/sei.go @@ -0,0 +1,8 @@ +package chains + +func Sei() (int, error) { + validatorsURL := "https://rest.sei-apis.com/cosmos/staking/v1beta1/validators?page.offset=1&pagination.limit=100&status=BOND_STATUS_BONDED" + stakingPoolURL := "https://rest.sei-apis.com/cosmos/staking/v1beta1/pool" + + return FetchCosmosSDKNakaCoeff("sei", validatorsURL, stakingPoolURL) +} diff --git a/core/chains/stargaze.go b/core/chains/stargaze.go index ac507ae..6025374 100644 --- a/core/chains/stargaze.go +++ b/core/chains/stargaze.go @@ -1,6 +1,8 @@ package chains func Stargaze() (int, error) { - url := "https://rest.stargaze-apis.com/cosmos/staking/v1beta1/validators?page.offset=1&pagination.limit=500&status=BOND_STATUS_BONDED" - return fetchCosmosSDKNakaCoeff("stargaze", url) + validatorsURL := "https://rest.stargaze-apis.com/cosmos/staking/v1beta1/validators?page.offset=1&pagination.limit=500&status=BOND_STATUS_BONDED" + stakingPoolURL := "https://rest.stargaze-apis.com/cosmos/staking/v1beta1/pool" + + return FetchCosmosSDKNakaCoeff("stargaze", validatorsURL, stakingPoolURL) }