Skip to content

Commit

Permalink
Add Sei blockchain (#56)
Browse files Browse the repository at this point in the history
**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 <[email protected]>
  • Loading branch information
cordt-sei and Cordtus authored Sep 2, 2024
1 parent cdf1db9 commit 9b299bf
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 315 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
67 changes: 3 additions & 64 deletions core/chains/agoric.go
Original file line number Diff line number Diff line change
@@ -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)
}
27 changes: 24 additions & 3 deletions core/chains/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand All @@ -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 {
Expand All @@ -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
}

Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down
129 changes: 101 additions & 28 deletions core/chains/cosmos.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 9b299bf

Please sign in to comment.