├── docker
├── .gitignore
├── .env.example
└── clickhouse-users.xml.example
├── dao
├── clickhouse
│ ├── migrations
│ │ ├── 001_blocks.down.sql
│ │ ├── 013_stats.down.sql
│ │ ├── 012_jailers.down.sql
│ │ ├── 002_delegations.down.sql
│ │ ├── 004_transfers.down.sql
│ │ ├── 015_account_txs.down.sql
│ │ ├── 003_transactions.down.sql
│ │ ├── 014_missed_blocks.down.sql
│ │ ├── 009_proposal_votes.down.sql
│ │ ├── 011_balance_updates.down.sql
│ │ ├── 005_delegator_rewards.down.sql
│ │ ├── 006_validator_rewards.down.sql
│ │ ├── 007_history_proposals.down.sql
│ │ ├── 008_proposal_deposits.down.sql
│ │ ├── 010_historical_states.down.sql
│ │ ├── 015_account_txs.up.sql
│ │ ├── 012_jailers.up.sql
│ │ ├── 013_stats.up.sql
│ │ ├── 001_blocks.up.sql
│ │ ├── 014_missed_blocks.up.sql
│ │ ├── 008_proposal_deposits.up.sql
│ │ ├── 006_validator_rewards.up.sql
│ │ ├── 002_delegations.up.sql
│ │ ├── 009_proposal_votes.up.sql
│ │ ├── 005_delegator_rewards.up.sql
│ │ ├── 011_balance_updates.up.sql
│ │ ├── 004_transfers.up.sql
│ │ ├── 003_transactions.up.sql
│ │ ├── 007_history_proposals.up.sql
│ │ └── 010_historical_states.up.sql
│ ├── balance_updates.go
│ ├── jailers.go
│ ├── missed_blocks.go
│ ├── proposal_deposits.go
│ ├── history_proposals.go
│ ├── rewards.go
│ ├── accounts.go
│ ├── stats.go
│ ├── historical_states.go
│ ├── transfers.go
│ ├── proposal_votes.go
│ ├── clickhouse.go
│ ├── transactions.go
│ ├── blocks.go
│ └── delegations.go
├── filters
│ ├── missed_blocks.go
│ ├── balance_updates.go
│ ├── historical_state.go
│ ├── proposal_deposits.go
│ ├── voting_power.go
│ ├── history_proposals.go
│ ├── proposals.go
│ ├── blocks.go
│ ├── transactions.go
│ ├── states.go
│ ├── proposal_votes.go
│ ├── accounts.go
│ ├── delegations.go
│ ├── time_range.go
│ └── agg.go
├── derrors
│ └── dao_errors.go
├── cache
│ └── gocache.go
├── mysql
│ ├── parsers.go
│ ├── validators.go
│ ├── accounts.go
│ ├── proposals.go
│ ├── main.go
│ └── migrations
│ │ └── init.sql
└── dao.go
├── Dockerfile
├── smodels
├── paginatable.go
├── proposal_vote.go
├── agg_data.go
├── balance.go
├── validator_blocks_stat.go
├── accounts.go
├── pie_item.go
├── historical_state.go
├── fee_range.go
├── meta_data.go
├── proposal_chart_data.go
├── block.go
├── validator.go
└── tx.go
├── .gitignore
├── dmodels
├── validator_value.go
├── account_tx.go
├── parser.go
├── jailer.go
├── block.go
├── missed_block.go
├── validator_delegator.go
├── account.go
├── validator_reward.go
├── proposal_vote.go
├── delegation.go
├── balance_update.go
├── delegator_reward.go
├── transfer.go
├── proposal_deposit.go
├── time_test.go
├── transaction.go
├── history_proposal.go
├── range_state.go
├── historical_state.go
├── validator.go
├── stat.go
├── proposal.go
└── time.go
├── api
├── transfers.go
├── meta_data.go
├── historical_state.go
├── account.go
├── router.go
├── stats.go
├── common.go
├── transactions.go
├── blocks.go
├── proposals.go
├── delegations.go
├── validators.go
└── api.go
├── services
├── helpers
│ ├── codding.go
│ └── public_key.go
├── transfers.go
├── grafana.go
├── coingecko
│ └── coingecko.go
├── modules
│ └── modules.go
├── meta_data.go
├── cmc
│ └── cmc.go
├── accounts.go
├── transactions.go
├── delegations.go
├── historical_states.go
├── blocks.go
├── services.go
├── parser
│ └── hub3
│ │ ├── genesis.go
│ │ └── api.go
├── scheduler
│ └── scheduler.go
└── range_states.go
├── config.example.json
├── README.md
├── log
└── log.go
├── docker-compose.example.yml
├── go.mod
├── resources
└── templates
│ └── swagger.html
├── main.go
└── config
└── config.go
/docker/.gitignore:
--------------------------------------------------------------------------------
1 | clickhouse-users.xml
2 | .env
3 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/001_blocks.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS blocks;
2 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/013_stats.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS stats;
2 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/012_jailers.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS jailers;
2 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/002_delegations.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS delegations;
2 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/004_transfers.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS transfers;
2 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/015_account_txs.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS account_txs;
2 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/003_transactions.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS transactions;
2 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/014_missed_blocks.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS missed_blocks;
2 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/009_proposal_votes.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS proposal_votes;
2 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/011_balance_updates.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS balance_updates;
2 |
--------------------------------------------------------------------------------
/docker/.env.example:
--------------------------------------------------------------------------------
1 | DB_NAME=cosmos
2 | DB_USER=root
3 | DB_PASSWORD=secret
4 | DB_HOST=mysql
5 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/005_delegator_rewards.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS delegator_rewards;
2 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/006_validator_rewards.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS validator_rewards;
2 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/007_history_proposals.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS history_proposals;
2 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/008_proposal_deposits.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS proposal_deposits;
2 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/010_historical_states.down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS historical_states;
2 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.14
2 | WORKDIR /app
3 | COPY . /app
4 | RUN go build -o app .
5 | CMD ["./app"]
6 |
--------------------------------------------------------------------------------
/dao/filters/missed_blocks.go:
--------------------------------------------------------------------------------
1 | package filters
2 |
3 | type MissedBlocks struct {
4 | Validators []string
5 | }
6 |
--------------------------------------------------------------------------------
/dao/filters/balance_updates.go:
--------------------------------------------------------------------------------
1 | package filters
2 |
3 | type BalanceUpdates struct {
4 | Limit uint64
5 | Offset uint64
6 | }
7 |
--------------------------------------------------------------------------------
/dao/filters/historical_state.go:
--------------------------------------------------------------------------------
1 | package filters
2 |
3 | type HistoricalState struct {
4 | Limit uint64
5 | Offset uint64
6 | }
7 |
--------------------------------------------------------------------------------
/dao/derrors/dao_errors.go:
--------------------------------------------------------------------------------
1 | package derrors
2 |
3 | const (
4 | ErrNotFound = "ERR_NOT_FOUND"
5 | ErrDuplicate = "ERR_DUPLICATE"
6 | )
7 |
--------------------------------------------------------------------------------
/dao/filters/proposal_deposits.go:
--------------------------------------------------------------------------------
1 | package filters
2 |
3 | type ProposalDeposits struct {
4 | ProposalID []uint64 `schema:"proposal_id"`
5 | }
6 |
--------------------------------------------------------------------------------
/dao/filters/voting_power.go:
--------------------------------------------------------------------------------
1 | package filters
2 |
3 | type VotingPower struct {
4 | TimeRange
5 | Delegators []string
6 | Validators []string
7 | }
8 |
--------------------------------------------------------------------------------
/dao/filters/history_proposals.go:
--------------------------------------------------------------------------------
1 | package filters
2 |
3 | type HistoryProposals struct {
4 | ID []uint64
5 | Limit uint64
6 | Offset uint64
7 | }
8 |
--------------------------------------------------------------------------------
/smodels/paginatable.go:
--------------------------------------------------------------------------------
1 | package smodels
2 |
3 | type PaginatableResponse struct {
4 | Items interface{} `json:"items"`
5 | Total uint64 `json:"total"`
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | vendor/**/
3 | cosmoscan-api
4 | config.json
5 | config.json.*
6 | backups
7 | ./clickhouse
8 | ./mysql
9 | .env
10 | .cache
11 | docker-compose.yml
12 |
--------------------------------------------------------------------------------
/dao/filters/proposals.go:
--------------------------------------------------------------------------------
1 | package filters
2 |
3 | type Proposals struct {
4 | ID []uint64 `schema:"id"`
5 | Limit uint64 `schema:"limit"`
6 | Offset uint64 `schema:"offset"`
7 | }
8 |
--------------------------------------------------------------------------------
/dmodels/validator_value.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | type ValidatorValue struct {
4 | Validator string `db:"validator" json:"validator"`
5 | Value uint64 `db:"value" json:"value"`
6 | }
7 |
--------------------------------------------------------------------------------
/dmodels/account_tx.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | const AccountTxsTable = "account_txs"
4 |
5 | type AccountTx struct {
6 | Account string `db:"atx_account"`
7 | TxHash string `db:"atx_tx_hash"`
8 | }
9 |
--------------------------------------------------------------------------------
/dao/filters/blocks.go:
--------------------------------------------------------------------------------
1 | package filters
2 |
3 | type Blocks struct {
4 | Limit uint64 `schema:"limit"`
5 | Offset uint64 `schema:"offset"`
6 | }
7 |
8 | type BlocksProposed struct {
9 | Proposers []string
10 | }
11 |
--------------------------------------------------------------------------------
/dmodels/parser.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | const ParsersTable = "parsers"
4 |
5 | type Parser struct {
6 | ID uint64 `db:"par_id"`
7 | Title string `db:"par_title"`
8 | Height uint64 `db:"par_height"`
9 | }
10 |
--------------------------------------------------------------------------------
/api/transfers.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "net/http"
5 | )
6 |
7 | func (api *API) GetAggTransfersVolume(w http.ResponseWriter, r *http.Request) {
8 | api.aggHandler(w, r, api.svc.GetAggTransfersVolume)
9 | }
10 |
--------------------------------------------------------------------------------
/dao/filters/transactions.go:
--------------------------------------------------------------------------------
1 | package filters
2 |
3 | type Transactions struct {
4 | Height uint64 `schema:"height"`
5 | Address string `schema:"address"`
6 | Limit uint64 `schema:"limit"`
7 | Offset uint64 `schema:"offset"`
8 | }
9 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/015_account_txs.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS account_txs
2 | (
3 | atx_account FixedString(45),
4 | atx_tx_hash FixedString(64)
5 | ) ENGINE ReplacingMergeTree() ORDER BY (atx_account, atx_tx_hash);
6 |
7 |
--------------------------------------------------------------------------------
/dao/filters/states.go:
--------------------------------------------------------------------------------
1 | package filters
2 |
3 | import "github.com/everstake/cosmoscan-api/dmodels"
4 |
5 | type Stats struct {
6 | Titles []string `schema:"-"`
7 | To dmodels.Time `schema:"to"`
8 | From dmodels.Time `schema:"-"`
9 | }
10 |
--------------------------------------------------------------------------------
/dao/filters/proposal_votes.go:
--------------------------------------------------------------------------------
1 | package filters
2 |
3 | type ProposalVotes struct {
4 | ProposalID uint64 `schema:"proposal_id"`
5 | Voters []string `schema:"voters"`
6 | Limit uint64 `schema:"limit"`
7 | Offset uint64 `schema:"offset"`
8 | }
9 |
--------------------------------------------------------------------------------
/smodels/proposal_vote.go:
--------------------------------------------------------------------------------
1 | package smodels
2 |
3 | import "github.com/everstake/cosmoscan-api/dmodels"
4 |
5 | type ProposalVote struct {
6 | Title string `json:"title"`
7 | IsValidator bool `json:"is_validator"`
8 | dmodels.ProposalVote
9 | }
10 |
--------------------------------------------------------------------------------
/dmodels/jailer.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | import "time"
4 |
5 | const JailersTable = "jailers"
6 |
7 | type Jailer struct {
8 | ID string `db:"jlr_id"`
9 | Address string `db:"jlr_address"`
10 | CreatedAt time.Time `db:"jlr_created_at"`
11 | }
12 |
--------------------------------------------------------------------------------
/smodels/agg_data.go:
--------------------------------------------------------------------------------
1 | package smodels
2 |
3 | import (
4 | "github.com/everstake/cosmoscan-api/dmodels"
5 | "github.com/shopspring/decimal"
6 | )
7 |
8 | type AggItem struct {
9 | Time dmodels.Time `db:"time" json:"time"`
10 | Value decimal.Decimal `db:"value" json:"value"`
11 | }
12 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/012_jailers.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS jailers
2 | (
3 | jlr_id FixedString(40),
4 | jlr_address String,
5 | jlr_created_at DateTime
6 | ) ENGINE ReplacingMergeTree()
7 | PARTITION BY toYYYYMMDD(jlr_created_at)
8 | ORDER BY (jlr_id);
9 |
--------------------------------------------------------------------------------
/smodels/balance.go:
--------------------------------------------------------------------------------
1 | package smodels
2 |
3 | import "github.com/shopspring/decimal"
4 |
5 | type Balance struct {
6 | SelfDelegated decimal.Decimal `json:"self_delegated"`
7 | OtherDelegated decimal.Decimal `json:"other_delegated"`
8 | Available decimal.Decimal `json:"available"`
9 | }
10 |
--------------------------------------------------------------------------------
/dmodels/block.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | import "time"
4 |
5 | const BlocksTable = "blocks"
6 |
7 | type Block struct {
8 | ID uint64 `db:"blk_id"`
9 | Hash string `db:"blk_hash"`
10 | Proposer string `db:"blk_proposer"`
11 | CreatedAt time.Time `db:"blk_created_at"`
12 | }
13 |
--------------------------------------------------------------------------------
/dao/filters/accounts.go:
--------------------------------------------------------------------------------
1 | package filters
2 |
3 | import (
4 | "github.com/shopspring/decimal"
5 | "time"
6 | )
7 |
8 | type Accounts struct {
9 | LtTotalAmount decimal.Decimal
10 | GtTotalAmount decimal.Decimal
11 | }
12 |
13 | type ActiveAccounts struct {
14 | From time.Time
15 | To time.Time
16 | }
17 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/013_stats.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS stats
2 | (
3 | stt_id FixedString(42),
4 | stt_title String,
5 | stt_value String,
6 | stt_created_at DateTime
7 | ) ENGINE ReplacingMergeTree()
8 | PARTITION BY toYYYYMM(stt_created_at)
9 | ORDER BY (stt_id);
10 |
--------------------------------------------------------------------------------
/smodels/validator_blocks_stat.go:
--------------------------------------------------------------------------------
1 | package smodels
2 |
3 | import "github.com/shopspring/decimal"
4 |
5 | type ValidatorBlocksStat struct {
6 | Proposed uint64 `json:"proposed"`
7 | MissedValidations uint64 `json:"missed_validations"`
8 | Revenue decimal.Decimal `json:"revenue"`
9 | }
10 |
--------------------------------------------------------------------------------
/dmodels/missed_block.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | import "time"
4 |
5 | const MissedBlocks = "missed_blocks"
6 |
7 | type MissedBlock struct {
8 | ID string `db:"mib_id"`
9 | Height uint64 `db:"mib_height"`
10 | Validator string `db:"mib_validator"`
11 | CreatedAt time.Time `db:"mib_created_at"`
12 | }
13 |
--------------------------------------------------------------------------------
/dmodels/validator_delegator.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | import "github.com/shopspring/decimal"
4 |
5 | type ValidatorDelegator struct {
6 | Delegator string `json:"delegator"`
7 | Amount decimal.Decimal `json:"amount"`
8 | Since Time `json:"since"`
9 | Delta decimal.Decimal `json:"delta"`
10 | }
11 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/001_blocks.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS blocks
2 | (
3 | blk_id UInt64,
4 | blk_hash FixedString(64),
5 | blk_proposer FixedString(40),
6 | blk_created_at DateTime
7 | ) ENGINE ReplacingMergeTree()
8 | PARTITION BY toYYYYMMDD(blk_created_at)
9 | ORDER BY (blk_id);
10 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/014_missed_blocks.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS missed_blocks
2 | (
3 | mib_id FixedString(40),
4 | mib_height UInt64,
5 | mib_validator FixedString(40),
6 | mib_created_at DateTime
7 | ) ENGINE ReplacingMergeTree()
8 | PARTITION BY toYYYYMM(mib_created_at)
9 | ORDER BY (mib_id);
10 |
11 |
--------------------------------------------------------------------------------
/smodels/accounts.go:
--------------------------------------------------------------------------------
1 | package smodels
2 |
3 | import "github.com/shopspring/decimal"
4 |
5 | type Account struct {
6 | Address string `json:"address"`
7 | Balance decimal.Decimal `json:"balance"`
8 | Delegated decimal.Decimal `json:"delegated"`
9 | Unbonding decimal.Decimal `json:"unbonding"`
10 | StakeReward decimal.Decimal `json:"stake_reward"`
11 | }
12 |
--------------------------------------------------------------------------------
/smodels/pie_item.go:
--------------------------------------------------------------------------------
1 | package smodels
2 |
3 | import "github.com/shopspring/decimal"
4 |
5 | type (
6 | Pie struct {
7 | Parts []PiePart `json:"parts"`
8 | Total decimal.Decimal `json:"total"`
9 | }
10 | PiePart struct {
11 | Label string `json:"label"`
12 | Title string `json:"title"`
13 | Value decimal.Decimal `json:"value"`
14 | }
15 | )
16 |
--------------------------------------------------------------------------------
/api/meta_data.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/everstake/cosmoscan-api/log"
5 | "net/http"
6 | )
7 |
8 | func (api *API) GetMetaData(w http.ResponseWriter, r *http.Request) {
9 | resp, err := api.svc.GetMetaData()
10 | if err != nil {
11 | log.Error("API GetMetaData: svc.GetMetaData: %s", err.Error())
12 | jsonError(w)
13 | return
14 | }
15 | jsonData(w, resp)
16 | }
17 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/008_proposal_deposits.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS proposal_deposits
2 | (
3 | prd_id FixedString(40),
4 | prd_proposal_id UInt64,
5 | prd_depositor String,
6 | prd_amount Decimal128(18),
7 | prd_created_at DateTime
8 | ) ENGINE ReplacingMergeTree()
9 | PARTITION BY toYYYYMMDD(prd_created_at)
10 | ORDER BY (prd_id);
11 |
12 |
--------------------------------------------------------------------------------
/smodels/historical_state.go:
--------------------------------------------------------------------------------
1 | package smodels
2 |
3 | import "github.com/everstake/cosmoscan-api/dmodels"
4 |
5 | type HistoricalState struct {
6 | Current dmodels.HistoricalState `json:"current"`
7 | PriceAgg []AggItem `json:"price_agg"`
8 | MarketCapAgg []AggItem `json:"market_cap_agg"`
9 | StakedRatioAgg []AggItem `json:"staked_ratio"`
10 | }
11 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/006_validator_rewards.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS validator_rewards
2 | (
3 | var_id FixedString(40),
4 | var_tx_hash FixedString(64),
5 | var_address FixedString(52),
6 | var_amount Decimal128(18),
7 | var_created_at DateTime
8 | ) ENGINE ReplacingMergeTree()
9 | PARTITION BY toYYYYMMDD(var_created_at)
10 | ORDER BY (var_id);
11 |
--------------------------------------------------------------------------------
/dao/filters/delegations.go:
--------------------------------------------------------------------------------
1 | package filters
2 |
3 | type Delegators struct {
4 | TimeRange
5 | Validators []string `schema:"validators"`
6 | }
7 |
8 | type DelegationsAgg struct {
9 | Agg
10 | Validators []string `schema:"validators"`
11 | }
12 |
13 | type ValidatorDelegators struct {
14 | Validator string `json:"-"`
15 | Limit uint64 `schema:"limit"`
16 | Offset uint64 `schema:"offset"`
17 | }
18 |
--------------------------------------------------------------------------------
/services/helpers/codding.go:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/hex"
6 | "fmt"
7 | )
8 |
9 | func B64ToHex(b64Str string) (hexStr string, err error) {
10 | bts, err := base64.StdEncoding.DecodeString(b64Str)
11 | if err != nil {
12 | return hexStr, fmt.Errorf("base64.StdEncoding.DecodeString: %s", err.Error())
13 | }
14 | return hex.EncodeToString(bts), nil
15 | }
16 |
--------------------------------------------------------------------------------
/smodels/fee_range.go:
--------------------------------------------------------------------------------
1 | package smodels
2 |
3 | import "github.com/shopspring/decimal"
4 |
5 | type FeeRange struct {
6 | From decimal.Decimal `json:"from"`
7 | To decimal.Decimal `json:"to"`
8 | Validators []FeeRangeValidator `json:"validators"`
9 | }
10 |
11 | type FeeRangeValidator struct {
12 | Validator string `json:"validator"`
13 | Fee decimal.Decimal `json:"fee"`
14 | }
15 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/002_delegations.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS delegations
2 | (
3 | dlg_id FixedString(40),
4 | dlg_tx_hash FixedString(64),
5 | dlg_delegator FixedString(45),
6 | dlg_validator FixedString(52),
7 | dlg_amount Decimal128(18),
8 | dlg_created_at DateTime
9 | ) ENGINE ReplacingMergeTree()
10 | PARTITION BY toYYYYMMDD(dlg_created_at)
11 | ORDER BY (dlg_id);
12 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/009_proposal_votes.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS proposal_votes
2 | (
3 | prv_id FixedString(40),
4 | prv_proposal_id UInt64,
5 | prv_tx_hash FixedString(64),
6 | prv_voter String,
7 | prv_option String,
8 | prv_created_at DateTime
9 | ) ENGINE ReplacingMergeTree()
10 | PARTITION BY toYYYYMMDD(prv_created_at)
11 | ORDER BY (prv_id);
12 |
--------------------------------------------------------------------------------
/api/historical_state.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/everstake/cosmoscan-api/log"
5 | "net/http"
6 | )
7 |
8 | func (api *API) GetHistoricalState(w http.ResponseWriter, r *http.Request) {
9 | resp, err := api.svc.GetHistoricalState()
10 | if err != nil {
11 | log.Error("API GetHistoricalState: svc.GetHistoricalState: %s", err.Error())
12 | jsonError(w)
13 | return
14 | }
15 | jsonData(w, resp)
16 | }
17 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/005_delegator_rewards.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS delegator_rewards
2 | (
3 | der_id FixedString(40),
4 | der_tx_hash FixedString(64),
5 | der_delegator FixedString(45),
6 | der_validator FixedString(52),
7 | der_amount Decimal128(18),
8 | der_created_at DateTime
9 | ) ENGINE ReplacingMergeTree()
10 | PARTITION BY toYYYYMMDD(der_created_at)
11 | ORDER BY (der_id);
12 |
--------------------------------------------------------------------------------
/dmodels/account.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | import (
4 | "github.com/shopspring/decimal"
5 | "time"
6 | )
7 |
8 | const AccountsTable = "accounts"
9 |
10 | type Account struct {
11 | Address string `db:"acc_address"`
12 | Balance decimal.Decimal `db:"acc_balance"`
13 | Stake decimal.Decimal `db:"acc_stake"`
14 | Unbonding decimal.Decimal `db:"acc_unbonding"`
15 | CreatedAt time.Time `db:"acc_created_at"`
16 | }
17 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/011_balance_updates.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS balance_updates
2 | (
3 | bau_id FixedString(40),
4 | bau_address FixedString(45),
5 | bau_balance Decimal(20, 8),
6 | bau_stake Decimal(20, 8),
7 | bau_unbonding Decimal(20, 8),
8 | bau_created_at DateTime
9 | ) ENGINE ReplacingMergeTree()
10 | PARTITION BY toYYYYMMDD(bau_created_at)
11 | ORDER BY (bau_id);
12 |
13 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/004_transfers.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS transfers
2 | (
3 | trf_id FixedString(40),
4 | trf_tx_hash FixedString(64),
5 | trf_from FixedString(65),
6 | trf_to FixedString(65),
7 | trf_amount Decimal128(18),
8 | trf_created_at DateTime,
9 | trf_currency String
10 | ) ENGINE=ReplacingMergeTree()
11 | PARTITION BY toYYYYMMDD(trf_created_at)
12 | ORDER BY (trf_id);
13 |
--------------------------------------------------------------------------------
/dmodels/validator_reward.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | import (
4 | "github.com/shopspring/decimal"
5 | "time"
6 | )
7 |
8 | const ValidatorRewardsTable = "validator_rewards"
9 |
10 | type ValidatorReward struct {
11 | ID string `db:"var_id"`
12 | TxHash string `db:"var_tx_hash"`
13 | Address string `db:"var_address"`
14 | Amount decimal.Decimal `db:"var_amount"`
15 | CreatedAt time.Time `db:"var_created_at"`
16 | }
17 |
--------------------------------------------------------------------------------
/dmodels/proposal_vote.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | const ProposalVotesTable = "proposal_votes"
4 |
5 | type ProposalVote struct {
6 | ID string `db:"prv_id" json:"-"`
7 | ProposalID uint64 `db:"prv_proposal_id" json:"proposal_id"`
8 | Voter string `db:"prv_voter" json:"voter"`
9 | TxHash string `db:"prv_tx_hash" json:"tx_hash"`
10 | Option string `db:"prv_option" json:"option"`
11 | CreatedAt Time `db:"prv_created_at" json:"created_at"`
12 | }
13 |
--------------------------------------------------------------------------------
/services/transfers.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "fmt"
5 | "github.com/everstake/cosmoscan-api/dao/filters"
6 | "github.com/everstake/cosmoscan-api/smodels"
7 | )
8 |
9 | func (s *ServiceFacade) GetAggTransfersVolume(filter filters.Agg) (items []smodels.AggItem, err error) {
10 | items, err = s.dao.GetAggTransfersVolume(filter)
11 | if err != nil {
12 | return nil, fmt.Errorf("dao.GetAggTransfersVolume: %s", err.Error())
13 | }
14 | return items, nil
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/dmodels/delegation.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | import (
4 | "github.com/shopspring/decimal"
5 | "time"
6 | )
7 |
8 | const DelegationsTable = "delegations"
9 |
10 | type Delegation struct {
11 | ID string `db:"dlg_id"`
12 | TxHash string `db:"dlg_tx_hash"`
13 | Delegator string `db:"dlg_delegator"`
14 | Validator string `db:"dlg_validator"`
15 | Amount decimal.Decimal `db:"dlg_amount"`
16 | CreatedAt time.Time `db:"dlg_created_at"`
17 | }
18 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/003_transactions.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS transactions
2 | (
3 | trn_hash FixedString(64),
4 | trn_block_id UInt64,
5 | trn_status UInt8,
6 | trn_height UInt64,
7 | trn_messages UInt32,
8 | trn_fee Decimal128(18),
9 | trn_gas_used UInt64,
10 | trn_gas_wanted UInt64,
11 | trn_created_at DateTime
12 | ) ENGINE ReplacingMergeTree()
13 | PARTITION BY toYYYYMMDD(trn_created_at)
14 | ORDER BY (trn_hash);
15 |
--------------------------------------------------------------------------------
/dmodels/balance_update.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | import (
4 | "github.com/shopspring/decimal"
5 | "time"
6 | )
7 |
8 | const BalanceUpdatesTable = "balance_updates"
9 |
10 | type BalanceUpdate struct {
11 | ID string `db:"bau_id"`
12 | Address string `db:"bau_address"`
13 | Stake decimal.Decimal `db:"bau_stake"`
14 | Balance decimal.Decimal `db:"bau_balance"`
15 | Unbonding decimal.Decimal `db:"bau_unbonding"`
16 | CreatedAt time.Time `db:"bau_created_at"`
17 | }
18 |
--------------------------------------------------------------------------------
/dmodels/delegator_reward.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | import (
4 | "github.com/shopspring/decimal"
5 | "time"
6 | )
7 |
8 | const DelegatorRewardsTable = "delegator_rewards"
9 |
10 | type DelegatorReward struct {
11 | ID string `db:"der_id"`
12 | TxHash string `db:"der_tx_hash"`
13 | Delegator string `db:"der_delegator"`
14 | Validator string `db:"der_validator"`
15 | Amount decimal.Decimal `db:"der_amount"`
16 | CreatedAt time.Time `db:"der_created_at"`
17 | }
18 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/007_history_proposals.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS history_proposals
2 | (
3 | hpr_id UInt64,
4 | hpr_tx_hash String,
5 | hpr_title String,
6 | hpr_description String,
7 | hpr_recipient String,
8 | hpr_amount Decimal128(18),
9 | hpr_init_deposit Decimal128(18),
10 | hpr_proposer String,
11 | hpr_created_at DateTime
12 | ) ENGINE ReplacingMergeTree()
13 | PARTITION BY toYYYYMMDD(hpr_created_at)
14 | ORDER BY (hpr_id);
--------------------------------------------------------------------------------
/dmodels/transfer.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | import (
4 | "github.com/shopspring/decimal"
5 | "time"
6 | )
7 |
8 | const TransfersTable = "transfers"
9 |
10 | type Transfer struct {
11 | ID string `db:"trf_id"`
12 | TxHash string `db:"trf_tx_hash"`
13 | From string `db:"trf_from"`
14 | To string `db:"trf_to"`
15 | Amount decimal.Decimal `db:"trf_amount"`
16 | Currency string `db:"trf_currency"`
17 | CreatedAt time.Time `db:"trf_created_at"`
18 | }
19 |
--------------------------------------------------------------------------------
/dao/cache/gocache.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "github.com/patrickmn/go-cache"
5 | "time"
6 | )
7 |
8 | type Cache struct {
9 | cache *cache.Cache
10 | }
11 |
12 | func New() *Cache {
13 | return &Cache{
14 | cache: cache.New(5*time.Minute, 10*time.Minute),
15 | }
16 | }
17 |
18 | func (c *Cache) CacheSet(key string, data interface{}, duration time.Duration) {
19 | c.cache.Set(key, data, duration)
20 | }
21 |
22 | func (c *Cache) CacheGet(key string) (data interface{}, found bool) {
23 | return c.cache.Get(key)
24 | }
25 |
--------------------------------------------------------------------------------
/dmodels/proposal_deposit.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | import (
4 | "github.com/shopspring/decimal"
5 | )
6 |
7 | const ProposalDepositsTable = "proposal_deposits"
8 |
9 | type ProposalDeposit struct {
10 | ID string `db:"prd_id" json:"-"`
11 | ProposalID uint64 `db:"prd_proposal_id" json:"proposal_id"`
12 | Depositor string `db:"prd_depositor" json:"depositor"`
13 | Amount decimal.Decimal `db:"prd_amount" json:"amount"`
14 | CreatedAt Time `db:"prd_created_at" json:"created_at"`
15 | }
16 |
--------------------------------------------------------------------------------
/api/account.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/everstake/cosmoscan-api/log"
5 | "github.com/gorilla/mux"
6 | "net/http"
7 | )
8 |
9 | func (api *API) GetAccount(w http.ResponseWriter, r *http.Request) {
10 | address, ok := mux.Vars(r)["address"]
11 | if !ok || address == "" {
12 | jsonBadRequest(w, "invalid address")
13 | return
14 | }
15 | resp, err := api.svc.GetAccount(address)
16 | if err != nil {
17 | log.Error("API GetAccount: svc.GetAccount: %s", err.Error())
18 | jsonError(w)
19 | return
20 | }
21 | jsonData(w, resp)
22 | }
23 |
--------------------------------------------------------------------------------
/smodels/meta_data.go:
--------------------------------------------------------------------------------
1 | package smodels
2 |
3 | import "github.com/shopspring/decimal"
4 |
5 | type MetaData struct {
6 | Height uint64 `json:"height"`
7 | LatestValidator string `json:"latest_validator"`
8 | LatestProposal MetaDataProposal `json:"latest_proposal"`
9 | ValidatorAvgFee decimal.Decimal `json:"validator_avg_fee"`
10 | BlockTime float64 `json:"block_time"`
11 | CurrentPrice decimal.Decimal `json:"current_price"`
12 | }
13 |
14 | type MetaDataProposal struct {
15 | Name string `json:"name"`
16 | ID uint64 `json:"id"`
17 | }
18 |
--------------------------------------------------------------------------------
/dmodels/time_test.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | import (
4 | "encoding/json"
5 | "reflect"
6 | "testing"
7 | "time"
8 | )
9 |
10 | type testTime struct {
11 | T Time `json:"t"`
12 | }
13 |
14 | func TestTime(t *testing.T) {
15 | tm := time.Unix(1565885014, 0)
16 | s1 := testTime{T: Time{tm}}
17 | b, err := json.Marshal(s1)
18 | if err != nil {
19 | t.Error(err)
20 | return
21 | }
22 | var s2 testTime
23 | err = json.Unmarshal(b, &s2)
24 | if err != nil {
25 | t.Error(err)
26 | return
27 | }
28 | if !reflect.DeepEqual(s1, s2) {
29 | t.Error("not equal", s1, s2)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/dmodels/transaction.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | import (
4 | "github.com/shopspring/decimal"
5 | "time"
6 | )
7 |
8 | const TransactionsTable = "transactions"
9 |
10 | type Transaction struct {
11 | Hash string `db:"trn_hash"`
12 | Status bool `db:"trn_status"`
13 | Height uint64 `db:"trn_height"`
14 | Messages uint64 `db:"trn_messages"`
15 | Fee decimal.Decimal `db:"trn_fee"`
16 | GasUsed uint64 `db:"trn_gas_used"`
17 | GasWanted uint64 `db:"trn_gas_wanted"`
18 | CreatedAt time.Time `db:"trn_created_at"`
19 | }
20 |
--------------------------------------------------------------------------------
/dao/filters/time_range.go:
--------------------------------------------------------------------------------
1 | package filters
2 |
3 | import (
4 | "github.com/Masterminds/squirrel"
5 | "github.com/everstake/cosmoscan-api/dmodels"
6 | )
7 |
8 | type TimeRange struct {
9 | From dmodels.Time `schema:"from"`
10 | To dmodels.Time `schema:"to"`
11 | }
12 |
13 | func (filter *TimeRange) Query(timeColumn string, q squirrel.SelectBuilder) squirrel.SelectBuilder {
14 | if !filter.From.IsZero() {
15 | q = q.Where(squirrel.GtOrEq{timeColumn: filter.From.Time})
16 | }
17 | if !filter.To.IsZero() {
18 | q = q.Where(squirrel.LtOrEq{timeColumn: filter.To.Time})
19 | }
20 | return q
21 | }
22 |
--------------------------------------------------------------------------------
/smodels/proposal_chart_data.go:
--------------------------------------------------------------------------------
1 | package smodels
2 |
3 | import "github.com/shopspring/decimal"
4 |
5 | type ProposalChartData struct {
6 | ProposalID uint64 `json:"proposal_id"`
7 | VotersTotal uint64 `json:"voters_total"`
8 | ValidatorsTotal uint64 `json:"validators_total"`
9 | Turnout decimal.Decimal `json:"turnout"`
10 | YesPercent decimal.Decimal `json:"yes_percent"`
11 | NoPercent decimal.Decimal `json:"no_percent"`
12 | NoWithVetoPercent decimal.Decimal `json:"no_with_veto_percent"`
13 | AbstainPercent decimal.Decimal `json:"abstain_percent"`
14 | }
15 |
--------------------------------------------------------------------------------
/config.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "api": {
3 | "port": "8080",
4 | "allowed_hosts": [
5 | "http://localhost:8000"
6 | ]
7 | },
8 | "mysql": {
9 | "host": "localhost",
10 | "port": "3306",
11 | "db": "cosmoscan",
12 | "user": "root",
13 | "password": "secret"
14 | },
15 | "clickhouse": {
16 | "protocol": "http",
17 | "host": "localhost",
18 | "port": 8123,
19 | "user": "default",
20 | "password": "",
21 | "database": "cosmoshub3"
22 | },
23 | "parser": {
24 | "node": "https://api.cosmos.network",
25 | "batch": 500,
26 | "fetchers": 5
27 | },
28 | "cmc_key": ""
29 | }
30 |
--------------------------------------------------------------------------------
/dmodels/history_proposal.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | import (
4 | "github.com/shopspring/decimal"
5 | "time"
6 | )
7 |
8 | const HistoryProposalsTable = "history_proposals"
9 |
10 | type HistoryProposal struct {
11 | ID uint64 `db:"hpr_id"`
12 | TxHash string `db:"hpr_tx_hash"`
13 | Title string `db:"hpr_title"`
14 | Description string `db:"hpr_description"`
15 | Recipient string `db:"hpr_recipient"`
16 | Amount decimal.Decimal `db:"hpr_amount"`
17 | InitDeposit decimal.Decimal `db:"hpr_init_deposit"`
18 | Proposer string `db:"hpr_proposer"`
19 | CreatedAt time.Time `db:"hpr_created_at"`
20 | }
21 |
--------------------------------------------------------------------------------
/dao/clickhouse/migrations/010_historical_states.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS historical_states
2 | (
3 | his_price Decimal(18, 8) default 0,
4 | his_market_cap Decimal(18, 2) default 0,
5 | his_circulating_supply Decimal(18, 2) default 0,
6 | his_trading_volume Decimal(18, 2) default 0,
7 | his_staked_ratio Decimal(4, 2) default 0,
8 | his_inflation_rate Decimal(4, 2) default 0,
9 | his_transactions_count UInt64 default 0,
10 | his_community_pool Decimal(18, 2) default 0,
11 | his_top_20_weight Decimal(4, 2) default 0,
12 | his_created_at DateTime
13 | ) ENGINE ReplacingMergeTree()
14 | PARTITION BY toYYYYMMDD(his_created_at)
15 | ORDER BY (his_created_at);
16 |
17 |
--------------------------------------------------------------------------------
/smodels/block.go:
--------------------------------------------------------------------------------
1 | package smodels
2 |
3 | import "github.com/everstake/cosmoscan-api/dmodels"
4 |
5 | type (
6 | Block struct {
7 | Height uint64 `json:"height"`
8 | Hash string `json:"hash"`
9 | TotalTxs uint64 `json:"total_txs"`
10 | ChainID string `json:"chain_id"`
11 | Proposer string `json:"proposer"`
12 | ProposerAddress string `json:"proposer_address"`
13 | Txs []TxItem `json:"txs"`
14 | CreatedAt dmodels.Time `json:"created_at"`
15 | }
16 | BlockItem struct {
17 | Height uint64 `json:"height"`
18 | Hash string `json:"hash"`
19 | Proposer string `json:"proposer"`
20 | ProposerAddress string `json:"proposer_address"`
21 | CreatedAt dmodels.Time `json:"created_at"`
22 | }
23 | )
24 |
--------------------------------------------------------------------------------
/smodels/validator.go:
--------------------------------------------------------------------------------
1 | package smodels
2 |
3 | import "github.com/shopspring/decimal"
4 |
5 | type Validator struct {
6 | Title string `json:"title"`
7 | Website string `json:"website"`
8 | OperatorAddress string `json:"operator_address"`
9 | AccAddress string `json:"acc_address"`
10 | ConsAddress string `json:"cons_address"`
11 | PercentPower decimal.Decimal `json:"percent_power"`
12 | Power decimal.Decimal `json:"power"`
13 | SelfStake decimal.Decimal `json:"self_stake"`
14 | Fee decimal.Decimal `json:"fee"`
15 | BlocksProposed uint64 `json:"blocks_proposed"`
16 | Delegators uint64 `json:"delegators"`
17 | Power24Change decimal.Decimal `json:"power_24_change"`
18 | GovernanceVotes uint64 `json:"governance_votes"`
19 | }
20 |
--------------------------------------------------------------------------------
/services/grafana.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "net/http"
8 | )
9 |
10 | type nodeSize struct {
11 | Size float64 `json:"dir_size_gb"`
12 | }
13 |
14 | func (s *ServiceFacade) GetSizeOfNode() (size float64, err error) {
15 | // not public, available only for internal everstake services
16 | url := "http://s175.everstake.one:8060/monitoring"
17 | resp, err := http.Get(url)
18 | if err != nil {
19 | return size, fmt.Errorf("http.Get: %s", err.Error())
20 | }
21 | defer resp.Body.Close()
22 | data, err := ioutil.ReadAll(resp.Body)
23 | if err != nil {
24 | return size, fmt.Errorf("ioutil.ReadAll: %s", err.Error())
25 | }
26 | var nSize nodeSize
27 | err = json.Unmarshal(data, &nSize)
28 | if err != nil {
29 | return size, fmt.Errorf("json.Unmarshal: %s", err.Error())
30 | }
31 | return nSize.Size, nil
32 | }
33 |
--------------------------------------------------------------------------------
/dmodels/range_state.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | import "time"
4 |
5 | const (
6 | RangeStateTotalStakingBalance = "total_staking_balance"
7 | RangeStateNumberDelegators = "number_delegators"
8 | RangeStateNumberMultiDelegators = "number_multi_delegators"
9 | RangeStateTransfersVolume = "transfer_volume"
10 | RangeStateFeeVolume = "fee_volume"
11 | RangeStateHighestFee = "highest_fee"
12 | RangeStateUndelegationVolume = "undelegation_volume"
13 | RangeStateBlockDelay = "block_delay"
14 | )
15 |
16 | const RangeStatesTable = "range_states"
17 |
18 | type RangeState struct {
19 | Title string `db:"rst_title"`
20 | Value1d string `db:"rst_value_1d"`
21 | Value7d string `db:"rst_value_7d"`
22 | Value30d string `db:"rst_value_30d"`
23 | Value90d string `db:"rst_value_90d"`
24 | UpdatedAt time.Time `db:"rst_updated_at"`
25 | }
26 |
--------------------------------------------------------------------------------
/dao/mysql/parsers.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "github.com/Masterminds/squirrel"
5 | "github.com/everstake/cosmoscan-api/dmodels"
6 | )
7 |
8 | func (m DB) GetParsers() (parsers []dmodels.Parser, err error) {
9 | q := squirrel.Select("*").From(dmodels.ParsersTable)
10 | err = m.find(&parsers, q)
11 | if err != nil {
12 | return nil, err
13 | }
14 | return parsers, nil
15 | }
16 |
17 | func (m DB) GetParser(title string) (parser dmodels.Parser, err error) {
18 | q := squirrel.Select("*").From(dmodels.ParsersTable).
19 | Where(squirrel.Eq{"par_title": title})
20 | err = m.first(&parser, q)
21 | return parser, err
22 | }
23 |
24 | func (m DB) UpdateParser(parser dmodels.Parser) error {
25 | q := squirrel.Update(dmodels.ParsersTable).
26 | Where(squirrel.Eq{"par_id": parser.ID}).
27 | SetMap(map[string]interface{}{
28 | "par_height": parser.Height,
29 | })
30 | return m.update(q)
31 | }
32 |
--------------------------------------------------------------------------------
/api/router.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/gorilla/mux"
5 | "github.com/urfave/negroni"
6 | "net/http"
7 | )
8 |
9 | // Route stores an API route data
10 | type Route struct {
11 | Path string
12 | Method string
13 | Func func(http.ResponseWriter, *http.Request)
14 | Middleware []negroni.HandlerFunc
15 | }
16 |
17 | // HandleActions is used to handle all given routes
18 | func HandleActions(router *mux.Router, wrapper *negroni.Negroni, prefix string, routes []*Route) {
19 | for _, r := range routes {
20 | w := wrapper.With()
21 | for _, m := range r.Middleware {
22 | w.Use(m)
23 | }
24 |
25 | w.Use(negroni.Wrap(http.HandlerFunc(r.Func)))
26 | router.Handle(prefix+r.Path, w).Methods(r.Method, "OPTIONS")
27 | }
28 | }
29 |
30 | func getParamsFromVars(r *http.Request) map[string][]string {
31 | mp := make(map[string][]string, 0)
32 | for k, v := range mux.Vars(r) {
33 | mp[k] = []string{v}
34 | }
35 | return mp
36 | }
37 |
--------------------------------------------------------------------------------
/dmodels/historical_state.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | import (
4 | "github.com/shopspring/decimal"
5 | )
6 |
7 | const HistoricalStates = "historical_states"
8 |
9 | type HistoricalState struct {
10 | Price decimal.Decimal `db:"his_price" json:"price"`
11 | MarketCap decimal.Decimal `db:"his_market_cap" json:"market_cap"`
12 | CirculatingSupply decimal.Decimal `db:"his_circulating_supply" json:"circulating_supply"`
13 | TradingVolume decimal.Decimal `db:"his_trading_volume" json:"trading_volume"`
14 | StakedRatio decimal.Decimal `db:"his_staked_ratio" json:"staked_ratio"`
15 | InflationRate decimal.Decimal `db:"his_inflation_rate" json:"inflation_rate"`
16 | TransactionsCount uint64 `db:"his_transactions_count" json:"transactions_count"`
17 | CommunityPool decimal.Decimal `db:"his_community_pool" json:"community_pool"`
18 | Top20Weight decimal.Decimal `db:"his_top_20_weight" json:"top20_weight"`
19 | CreatedAt Time `db:"his_created_at" json:"created_at"`
20 | }
21 |
--------------------------------------------------------------------------------
/services/coingecko/coingecko.go:
--------------------------------------------------------------------------------
1 | package coingecko
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "github.com/shopspring/decimal"
7 | coingecko "github.com/superoo7/go-gecko/v3"
8 | "net/http"
9 | "time"
10 | )
11 |
12 | const (
13 | coinID = "cosmos"
14 | )
15 |
16 | type CoinGecko struct {
17 | client *coingecko.Client
18 | }
19 |
20 | func NewGecko() *CoinGecko {
21 | httpClient := &http.Client{
22 | Timeout: time.Second * 10,
23 | }
24 | return &CoinGecko{
25 | client: coingecko.NewClient(httpClient),
26 | }
27 | }
28 |
29 | func (g CoinGecko) GetMarketData() (price, volume24h decimal.Decimal, err error) {
30 | data, err := g.client.CoinsID(coinID, false, true, true, false, false, false)
31 | if err != nil {
32 | return price, volume24h, fmt.Errorf("client.CoinsID: %s", err.Error())
33 | }
34 | if data.MarketData.MarketCap == nil {
35 | return price, volume24h, errors.New("MarketData.MarketCap is nil")
36 | }
37 |
38 | return decimal.NewFromFloat(data.MarketData.CurrentPrice["usd"]), decimal.NewFromFloat(data.MarketData.TotalVolume["usd"]), nil
39 | }
--------------------------------------------------------------------------------
/dmodels/validator.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | import (
4 | "github.com/shopspring/decimal"
5 | "time"
6 | )
7 |
8 | const ValidatorsTable = "validators"
9 |
10 | type Validator struct {
11 | Address string `db:"val_address"`
12 | OperatorAddress string `db:"val_operator_address"`
13 | ConsAddress string `db:"val_cons_address"`
14 | ConsPubKey string `db:"val_cons_pub_key"`
15 | Name string `db:"val_name"`
16 | Description string `db:"val_description"`
17 | Commission decimal.Decimal `db:"val_commission"`
18 | MinCommission decimal.Decimal `db:"val_min_commission"`
19 | MaxCommission decimal.Decimal `db:"val_max_commission"`
20 | SelfDelegations decimal.Decimal `db:"val_self_delegations"`
21 | Delegations decimal.Decimal `db:"val_delegations"`
22 | VotingPower decimal.Decimal `db:"val_voting_power"`
23 | Website string `db:"val_website"`
24 | Jailed bool `db:"val_jailed"`
25 | CreatedAt time.Time `db:"val_created_at"`
26 | }
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cosmoscan API (backend)
2 |
3 | Website: https://cosmoscan.net
4 | Frontend repo: https://github.com/everstake/cosmoscan-front
5 |
6 | Cosmoscan is the first data and statistics explorer for the Cosmos network. It provides information oт the overall network operations, governance details, validators and much more. This is still an MVP, so if you have any suggestions, please reach out.
7 |
8 | Dependency:
9 | - Clickhouse
10 | - Mysql
11 | - Cosmos node
12 | - Golang
13 |
14 | ## How to run ?
15 | At first you need to configure the config.json file.
16 | ```sh
17 | cp config.example.json config.json
18 | ```
19 | Next step you need to build and run application.
20 | #### Docker-compose way:
21 | ```sh
22 | cp docker-compose.example.yml docker-compose.yml
23 | cp docker/.env.example .env
24 | cp docker/clickhouse-users.xml.example docker/clickhouse-users.xml
25 | ```
26 | > don`t forget set your passwords
27 | ```sh
28 | docker-compose build && docker-compose up -d
29 | ```
30 | #### Native way:
31 | > at first setup your dependency and set passwords
32 | ```sh
33 | go build && ./cosmoscan-api
34 | ```
35 |
--------------------------------------------------------------------------------
/smodels/tx.go:
--------------------------------------------------------------------------------
1 | package smodels
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/everstake/cosmoscan-api/dmodels"
6 | "github.com/shopspring/decimal"
7 | )
8 |
9 | type (
10 | TxItem struct {
11 | Hash string `json:"hash"`
12 | Status bool `json:"status"`
13 | Fee decimal.Decimal `json:"fee"`
14 | Height uint64 `json:"height"`
15 | Messages uint64 `json:"messages"`
16 | CreatedAt dmodels.Time `json:"created_at"`
17 | }
18 | Tx struct {
19 | Hash string `json:"hash"`
20 | Type string `json:"type"`
21 | Status bool `json:"status"`
22 | Fee decimal.Decimal `json:"fee"`
23 | Height uint64 `json:"height"`
24 | GasUsed uint64 `json:"gas_used"`
25 | GasWanted uint64 `json:"gas_wanted"`
26 | Memo string `json:"memo"`
27 | CreatedAt dmodels.Time `json:"created_at"`
28 | Messages []Message `json:"messages"`
29 | }
30 | Message struct {
31 | Type string `json:"type"`
32 | Body json.RawMessage `json:"body"`
33 | }
34 | )
35 |
--------------------------------------------------------------------------------
/api/stats.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/everstake/cosmoscan-api/dao/filters"
5 | "github.com/everstake/cosmoscan-api/log"
6 | "net/http"
7 | )
8 |
9 | func (api *API) GetNetworkStats(w http.ResponseWriter, r *http.Request) {
10 | var filter filters.Stats
11 | err := api.queryDecoder.Decode(&filter, r.URL.Query())
12 | if err != nil {
13 | log.Debug("API Decode: %s", err.Error())
14 | jsonBadRequest(w, "")
15 | return
16 | }
17 | resp, err := api.svc.GetNetworkStates(filter)
18 | if err != nil {
19 | log.Error("API GetNetworkStats: svc.GetNetworkStates: %s", err.Error())
20 | jsonError(w)
21 | return
22 | }
23 | jsonData(w, resp)
24 | }
25 |
26 | func (api *API) GetAggValidators33Power(w http.ResponseWriter, r *http.Request) {
27 | api.aggHandler(w, r, api.svc.GetAggValidators33Power)
28 | }
29 |
30 | func (api *API) GetAggWhaleAccounts(w http.ResponseWriter, r *http.Request) {
31 | api.aggHandler(w, r, api.svc.GetAggWhaleAccounts)
32 | }
33 |
34 | func (api *API) GetAggBondedRatio(w http.ResponseWriter, r *http.Request) {
35 | api.aggHandler(w, r, api.svc.GetAggBondedRatio)
36 | }
37 |
--------------------------------------------------------------------------------
/log/log.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "time"
8 | )
9 |
10 | const (
11 | debugLvl = "debug"
12 | warningLvl = "warn "
13 | errorLvl = "error"
14 | infoLvl = "info "
15 | )
16 |
17 | func Debug(format string, args ...interface{}) {
18 | fmt.Println(wrapper(format, debugLvl, args...))
19 | }
20 |
21 | func Warn(format string, args ...interface{}) {
22 | fmt.Println(wrapper(format, warningLvl, args...))
23 | }
24 |
25 | func Error(format string, args ...interface{}) {
26 | fmt.Println(wrapper(format, errorLvl, args...))
27 | }
28 |
29 | func Info(format string, args ...interface{}) {
30 | fmt.Println(wrapper(format, infoLvl, args...))
31 | }
32 | func Fatal(format string, args ...interface{}) {
33 | fmt.Println(wrapper(format, infoLvl, args...))
34 | log.Fatal()
35 | os.Exit(0)
36 | }
37 |
38 | func wrapper(txt string, lvl string, args ...interface{}) string {
39 | if len(args) > 0 {
40 | txt = fmt.Sprintf(txt, args...)
41 | }
42 | return fmt.Sprintf("[%s %s] %s", lvl, timeForLog(), txt)
43 | }
44 |
45 | func timeForLog() string {
46 | return time.Now().Format("2006.01.02 15:04:05")
47 | }
48 |
--------------------------------------------------------------------------------
/dmodels/stat.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | import (
4 | "github.com/shopspring/decimal"
5 | "time"
6 | )
7 |
8 | const StatsTable = "stats"
9 |
10 | const (
11 | StatsTotalStakingBalance = "total_staking_balance"
12 | StatsNumberDelegators = "number_delegators"
13 | StatsTotalDelegators = "total_delegators"
14 | StatsNumberMultiDelegators = "number_multi_delegators"
15 | StatsTransfersVolume = "transfer_volume"
16 | StatsFeeVolume = "fee_volume"
17 | StatsHighestFee = "highest_fee"
18 | StatsUndelegationVolume = "undelegation_volume"
19 | StatsBlockDelay = "block_delay"
20 | StatsNetworkSize = "network_size"
21 | StatsTotalAccounts = "total_accounts"
22 | StatsTotalWhaleAccounts = "total_whale_accounts"
23 | StatsTotalSmallAccounts = "total_small_accounts"
24 | StatsTotalJailers = "total_jailers"
25 | StatsValidatorsWith33Power = "validators_with_33_power"
26 | )
27 |
28 | type Stat struct {
29 | ID string `db:"stt_id"`
30 | Title string `db:"stt_title"`
31 | Value decimal.Decimal `db:"stt_value"`
32 | CreatedAt time.Time `db:"stt_created_at"`
33 | }
34 |
--------------------------------------------------------------------------------
/dao/clickhouse/balance_updates.go:
--------------------------------------------------------------------------------
1 | package clickhouse
2 |
3 | import (
4 | "fmt"
5 | "github.com/Masterminds/squirrel"
6 | "github.com/everstake/cosmoscan-api/dao/filters"
7 | "github.com/everstake/cosmoscan-api/dmodels"
8 | )
9 |
10 | func (db DB) CreateBalanceUpdates(updates []dmodels.BalanceUpdate) error {
11 | if len(updates) == 0 {
12 | return nil
13 | }
14 | q := squirrel.Insert(dmodels.BalanceUpdatesTable).Columns("bau_id", "bau_address", "bau_stake", "bau_balance", "bau_unbonding", "bau_created_at")
15 | for _, update := range updates {
16 | if update.ID == "" {
17 | return fmt.Errorf("field ProposalID can not be empty")
18 | }
19 | if update.CreatedAt.IsZero() {
20 | return fmt.Errorf("field CreatedAt can not be 0")
21 | }
22 | q = q.Values(update.ID, update.Address, update.Stake, update.Balance, update.Unbonding, update.CreatedAt)
23 | }
24 | return db.Insert(q)
25 | }
26 |
27 | func (db DB) GetBalanceUpdate(filter filters.BalanceUpdates) (updates []dmodels.BalanceUpdate, err error) {
28 | q := squirrel.Select("*").From(dmodels.BalanceUpdatesTable).OrderBy("bau_created_at desc")
29 | if filter.Limit != 0 {
30 | q = q.Limit(filter.Limit)
31 | }
32 | if filter.Offset != 0 {
33 | q = q.Offset(filter.Offset)
34 | }
35 | err = db.Find(&updates, q)
36 | return updates, err
37 | }
38 |
--------------------------------------------------------------------------------
/dao/clickhouse/jailers.go:
--------------------------------------------------------------------------------
1 | package clickhouse
2 |
3 | import (
4 | "fmt"
5 | "github.com/Masterminds/squirrel"
6 | "github.com/everstake/cosmoscan-api/dmodels"
7 | )
8 |
9 | func (db DB) CreateJailers(jailers []dmodels.Jailer) error {
10 | if len(jailers) == 0 {
11 | return nil
12 | }
13 | q := squirrel.Insert(dmodels.JailersTable).Columns("jlr_id", "jlr_address", "jlr_created_at")
14 | for _, jailer := range jailers {
15 | if jailer.ID == "" {
16 | return fmt.Errorf("field ProposalID can not be empty")
17 | }
18 | if jailer.Address == "" {
19 | return fmt.Errorf("field Address can not be empty")
20 | }
21 | if jailer.CreatedAt.IsZero() {
22 | return fmt.Errorf("field CreatedAt can not be zero")
23 | }
24 | q = q.Values(jailer.ID, jailer.Address, jailer.CreatedAt)
25 | }
26 | return db.Insert(q)
27 | }
28 |
29 | func (db DB) GetJailersTotal() (total uint64, err error) {
30 | q := squirrel.Select("count(*) as total").From(dmodels.JailersTable)
31 | err = db.FindFirst(&total, q)
32 | return total, err
33 | }
34 |
35 | func (db DB) GetMostJailedValidators() (items []dmodels.ValidatorValue, err error) {
36 | q := squirrel.Select("count() as value", "jlr_address as validator").
37 | From(dmodels.JailersTable).
38 | GroupBy("validator").
39 | OrderBy("value desc")
40 | err = db.Find(&items, q)
41 | return items, err
42 | }
43 |
--------------------------------------------------------------------------------
/api/common.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/everstake/cosmoscan-api/config"
5 | "github.com/everstake/cosmoscan-api/dao/filters"
6 | "github.com/everstake/cosmoscan-api/log"
7 | "github.com/everstake/cosmoscan-api/smodels"
8 | "net/http"
9 | "reflect"
10 | "runtime"
11 | )
12 |
13 | func (api *API) Index(w http.ResponseWriter, r *http.Request) {
14 | jsonData(w, map[string]string{
15 | "service": config.ServiceName,
16 | })
17 | }
18 |
19 | func (api *API) Health(w http.ResponseWriter, r *http.Request) {
20 | jsonData(w, map[string]bool{
21 | "status": true,
22 | })
23 | }
24 |
25 | func (api *API) aggHandler(w http.ResponseWriter, r *http.Request, action func(filters.Agg) ([]smodels.AggItem, error)) {
26 | method := runtime.FuncForPC(reflect.ValueOf(action).Pointer()).Name()
27 | var filter filters.Agg
28 | err := api.queryDecoder.Decode(&filter, r.URL.Query())
29 | if err != nil {
30 | log.Debug("API %s: Decode: %s", method, err.Error())
31 | jsonBadRequest(w, "")
32 | return
33 | }
34 | err = filter.Validate()
35 | if err != nil {
36 | log.Debug("API %s: Validate: %s", method, err.Error())
37 | jsonBadRequest(w, err.Error())
38 | return
39 | }
40 | resp, err := action(filter)
41 | if err != nil {
42 | log.Error("API %s: %s", method, err.Error())
43 | jsonError(w)
44 | return
45 | }
46 | jsonData(w, resp)
47 | }
48 |
--------------------------------------------------------------------------------
/dao/clickhouse/missed_blocks.go:
--------------------------------------------------------------------------------
1 | package clickhouse
2 |
3 | import (
4 | "fmt"
5 | "github.com/Masterminds/squirrel"
6 | "github.com/everstake/cosmoscan-api/dao/filters"
7 | "github.com/everstake/cosmoscan-api/dmodels"
8 | )
9 |
10 | func (db DB) CreateMissedBlocks(blocks []dmodels.MissedBlock) error {
11 | if len(blocks) == 0 {
12 | return nil
13 | }
14 | q := squirrel.Insert(dmodels.MissedBlocks).Columns("mib_id", "mib_height", "mib_validator", "mib_created_at")
15 | for _, block := range blocks {
16 | if block.ID == "" {
17 | return fmt.Errorf("field ProposalID can not be empty")
18 | }
19 | if block.Height == 0 {
20 | return fmt.Errorf("field ProposalID can not be zero")
21 | }
22 | if block.Validator == "" {
23 | return fmt.Errorf("field Validator can not be empty")
24 | }
25 | if block.CreatedAt.IsZero() {
26 | return fmt.Errorf("field CreatedAt can not be 0")
27 | }
28 | q = q.Values(block.ID, block.Height, block.Validator, block.CreatedAt)
29 | }
30 | return db.Insert(q)
31 | }
32 |
33 | func (db DB) GetMissedBlocksCount(filter filters.MissedBlocks) (total uint64, err error) {
34 | q := squirrel.Select("count(*) as total").From(dmodels.MissedBlocks)
35 | if len(filter.Validators) != 0 {
36 | q = q.Where(squirrel.Eq{"mib_validator": filter.Validators})
37 | }
38 | err = db.FindFirst(&total, q)
39 | return total, err
40 | }
41 |
--------------------------------------------------------------------------------
/dao/clickhouse/proposal_deposits.go:
--------------------------------------------------------------------------------
1 | package clickhouse
2 |
3 | import (
4 | "fmt"
5 | "github.com/Masterminds/squirrel"
6 | "github.com/everstake/cosmoscan-api/dao/filters"
7 | "github.com/everstake/cosmoscan-api/dmodels"
8 | )
9 |
10 | func (db DB) CreateProposalDeposits(deposits []dmodels.ProposalDeposit) error {
11 | if len(deposits) == 0 {
12 | return nil
13 | }
14 | q := squirrel.Insert(dmodels.ProposalDepositsTable).Columns("prd_id", "prd_proposal_id", "prd_depositor", "prd_amount", "prd_created_at")
15 | for _, deposit := range deposits {
16 | if deposit.ID == "" {
17 | return fmt.Errorf("field ProposalID can not be empty")
18 | }
19 | if deposit.ProposalID == 0 {
20 | return fmt.Errorf("field ProposalID can not be zero")
21 | }
22 | if deposit.CreatedAt.IsZero() {
23 | return fmt.Errorf("field CreatedAt can not be zero")
24 | }
25 | q = q.Values(deposit.ID, deposit.ProposalID, deposit.Depositor, deposit.Amount, deposit.CreatedAt)
26 | }
27 | return db.Insert(q)
28 | }
29 |
30 | func (db DB) GetProposalDeposits(filter filters.ProposalDeposits) (deposits []dmodels.ProposalDeposit, err error) {
31 | q := squirrel.Select("*").From(dmodels.ProposalDepositsTable)
32 | if len(filter.ProposalID) != 0 {
33 | q = q.Where(squirrel.Eq{"prd_proposal_id": filter.ProposalID})
34 | }
35 | err = db.Find(&deposits, q)
36 | return deposits, err
37 | }
38 |
--------------------------------------------------------------------------------
/services/helpers/public_key.go:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import (
4 | "encoding/base64"
5 | "fmt"
6 | "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
7 | "github.com/cosmos/cosmos-sdk/types"
8 | "github.com/tendermint/tendermint/crypto/ed25519"
9 | )
10 |
11 | func GetHexAddressFromBase64PK(key string) (address string, err error) {
12 | decodedKey, err := base64.StdEncoding.DecodeString(key)
13 | if err != nil {
14 | return address, fmt.Errorf("base64.DecodeString: %s", err.Error())
15 | }
16 | if len(decodedKey) != 32 {
17 | return address, fmt.Errorf("wrong key format")
18 | }
19 | pub := ed25519.PubKey(decodedKey)
20 | return pub.Address().String(), nil
21 | }
22 |
23 | func GetBech32FromBase64PK(pkB64 string, pkType string) (address string, err error) {
24 | decodedKey, err := base64.StdEncoding.DecodeString(pkB64)
25 | if err != nil {
26 | return address, fmt.Errorf("base64.DecodeString: %s", err.Error())
27 | }
28 | var hexAddress string
29 | switch pkType {
30 | case "/cosmos.crypto.secp256k1.PubKey":
31 | pk := secp256k1.PubKey{Key: decodedKey}
32 | hexAddress = pk.Address().String()
33 | default:
34 | return address, fmt.Errorf("%s - unknown PK type", pkType)
35 | }
36 | addr, err := types.AccAddressFromHex(hexAddress)
37 | if err != nil {
38 | return address, fmt.Errorf("types.AccAddressFromHex: %s", err.Error())
39 | }
40 | return addr.String(), nil
41 | }
42 |
--------------------------------------------------------------------------------
/docker-compose.example.yml:
--------------------------------------------------------------------------------
1 | version: "3.7"
2 | services:
3 | go:
4 | restart: always
5 | ports:
6 | - "6000:6000"
7 | container_name: cosmoscan-api
8 | build: .
9 | volumes:
10 | - ./:/cosmoscan-api
11 | - ~/.ssh:/root/.ssh
12 | logging:
13 | driver: "json-file"
14 | options:
15 | max-size: "100m"
16 | deploy:
17 | resources:
18 | limits:
19 | memory: 5000M
20 | reservations:
21 | memory: 2000M
22 |
23 | clickhouse:
24 | restart: always
25 | ports:
26 | - "9000:9000"
27 | - "8123:8123"
28 | container_name: cosmoscan-clickhouse
29 | image: yandex/clickhouse-server:19.14.3.3
30 | volumes:
31 | - ./docker/clickhouse-users.xml:/etc/clickhouse-server/users.xml
32 | - ./clickhouse:/var/lib/clickhouse
33 | deploy:
34 | resources:
35 | limits:
36 | memory: 10000M
37 | reservations:
38 | memory: 2000M
39 | mysql:
40 | restart: always
41 | ports:
42 | - "3306:3306"
43 | container_name: cosmoscan-mysql
44 | image: mysql:8
45 | volumes:
46 | - ./mysql:/var/lib/mysql
47 | env_file:
48 | - ./docker/.env:./.env
49 | environment:
50 | MYSQL_DATABASE: "${DB_NAME}"
51 | MYSQL_USER: "${DB_USER}"
52 | MYSQL_ROOT_PASSWORD: "${DB_PASSWORD}"
53 | deploy:
54 | resources:
55 | limits:
56 | memory: 5000M
57 | reservations:
58 | memory: 2000M
59 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/everstake/cosmoscan-api
2 |
3 | go 1.14
4 |
5 | replace github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1
6 |
7 | require (
8 | github.com/Masterminds/squirrel v1.4.0
9 | github.com/Workiva/go-datastructures v1.0.53 // indirect
10 | github.com/adlio/schema v1.1.14 // indirect
11 | github.com/cosmos/cosmos-sdk v0.44.3
12 | github.com/go-kit/kit v0.12.0 // indirect
13 | github.com/go-sql-driver/mysql v1.5.0
14 | github.com/golang-migrate/migrate/v4 v4.11.0
15 | github.com/gorilla/mux v1.8.0
16 | github.com/gorilla/schema v1.1.0
17 | github.com/hashicorp/go-multierror v1.1.1 // indirect
18 | github.com/jmoiron/sqlx v1.2.0
19 | github.com/mailru/go-clickhouse v1.3.0
20 | github.com/onsi/gomega v1.16.0 // indirect
21 | github.com/patrickmn/go-cache v2.1.0+incompatible
22 | github.com/rogpeppe/go-internal v1.6.2 // indirect
23 | github.com/rs/cors v1.8.0
24 | github.com/rs/zerolog v1.26.0 // indirect
25 | github.com/rubenv/sql-migrate v0.0.0-20200429072036-ae26b214fa43
26 | github.com/shopspring/decimal v1.2.0
27 | github.com/spf13/viper v1.9.0 // indirect
28 | github.com/superoo7/go-gecko v1.0.0
29 | github.com/tendermint/tendermint v0.34.14
30 | github.com/urfave/negroni v1.0.0
31 | go.uber.org/zap v1.19.1
32 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
33 | golang.org/x/mod v0.5.0 // indirect
34 | golang.org/x/sys v0.0.0-20211013075003-97ac67df715c // indirect
35 | google.golang.org/grpc v1.41.0 // indirect
36 | )
37 |
--------------------------------------------------------------------------------
/resources/templates/swagger.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Cosmoscan API
7 |
8 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/everstake/cosmoscan-api/api"
5 | "github.com/everstake/cosmoscan-api/config"
6 | "github.com/everstake/cosmoscan-api/dao"
7 | "github.com/everstake/cosmoscan-api/log"
8 | "github.com/everstake/cosmoscan-api/services"
9 | "github.com/everstake/cosmoscan-api/services/modules"
10 | "github.com/everstake/cosmoscan-api/services/parser/hub3"
11 | "github.com/everstake/cosmoscan-api/services/scheduler"
12 | "os"
13 | "os/signal"
14 | "time"
15 | )
16 |
17 | func main() {
18 | err := os.Setenv("TZ", "UTC")
19 | if err != nil {
20 | log.Fatal("os.Setenv (TZ): %s", err.Error())
21 | }
22 |
23 | cfg := config.GetConfig()
24 | d, err := dao.NewDAO(cfg)
25 | if err != nil {
26 | log.Fatal("dao.NewDAO: %s", err.Error())
27 | }
28 |
29 | s, err := services.NewServices(d, cfg)
30 | if err != nil {
31 | log.Fatal("services.NewServices: %s", err.Error())
32 | }
33 |
34 | prs := hub3.NewParser(cfg, d)
35 |
36 | apiServer := api.NewAPI(cfg, s, d)
37 |
38 | sch := scheduler.NewScheduler()
39 |
40 | sch.AddProcessWithInterval(s.UpdateValidatorsMap, time.Minute*10)
41 | sch.AddProcessWithInterval(s.UpdateProposals, time.Minute*15)
42 | sch.AddProcessWithInterval(s.UpdateValidators, time.Minute*15)
43 | sch.EveryDayAt(s.MakeUpdateBalances, 1, 0)
44 | sch.EveryDayAt(s.MakeStats, 2, 0)
45 |
46 | go s.KeepHistoricalState()
47 |
48 | g := modules.NewGroup(apiServer, sch, prs)
49 | g.Run()
50 |
51 | interrupt := make(chan os.Signal)
52 | signal.Notify(interrupt, os.Interrupt, os.Kill)
53 |
54 | <-interrupt
55 | g.Stop()
56 |
57 | os.Exit(0)
58 | }
59 |
--------------------------------------------------------------------------------
/api/transactions.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/everstake/cosmoscan-api/dao/filters"
5 | "github.com/everstake/cosmoscan-api/log"
6 | "github.com/gorilla/mux"
7 | "net/http"
8 | )
9 |
10 | func (api *API) GetAggTransactionsFee(w http.ResponseWriter, r *http.Request) {
11 | api.aggHandler(w, r, api.svc.GetAggTransactionsFee)
12 | }
13 |
14 | func (api *API) GetAggOperationsCount(w http.ResponseWriter, r *http.Request) {
15 | api.aggHandler(w, r, api.svc.GetAggOperationsCount)
16 | }
17 |
18 | func (api *API) GetAvgOperationsPerBlock(w http.ResponseWriter, r *http.Request) {
19 | api.aggHandler(w, r, api.svc.GetAvgOperationsPerBlock)
20 | }
21 |
22 | func (api *API) GetTransaction(w http.ResponseWriter, r *http.Request) {
23 | hash, ok := mux.Vars(r)["hash"]
24 | if !ok || hash == "" {
25 | jsonBadRequest(w, "invalid hash")
26 | return
27 | }
28 | resp, err := api.svc.GetTransaction(hash)
29 | if err != nil {
30 | log.Error("API GetTransaction: svc.GetTransaction: %s", err.Error())
31 | jsonError(w)
32 | return
33 | }
34 | jsonData(w, resp)
35 | }
36 |
37 | func (api *API) GetTransactions(w http.ResponseWriter, r *http.Request) {
38 | var filter filters.Transactions
39 | err := api.queryDecoder.Decode(&filter, r.URL.Query())
40 | if err != nil {
41 | log.Debug("API Decode: %s", err.Error())
42 | jsonBadRequest(w, "")
43 | return
44 | }
45 | if filter.Limit == 0 || filter.Limit > 100 {
46 | filter.Limit = 100
47 | }
48 | resp, err := api.svc.GetTransactions(filter)
49 | if err != nil {
50 | log.Error("API GetTransactions: svc.GetTransactions: %s", err.Error())
51 | jsonError(w)
52 | return
53 | }
54 | jsonData(w, resp)
55 | }
56 |
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "log"
7 | "path/filepath"
8 | )
9 |
10 | const (
11 | ServiceName = "cosmoscan-api"
12 | configPath = "./config.json"
13 | Currency = "atom"
14 | )
15 |
16 | type (
17 | Config struct {
18 | API API `json:"api"`
19 | Mysql Mysql `json:"mysql"`
20 | Clickhouse Clickhouse `json:"clickhouse"`
21 | Parser Parser `json:"parser"`
22 | CMCKey string `json:"cmc_key"`
23 | }
24 | Parser struct {
25 | Node string `json:"node"`
26 | Batch uint64 `json:"batch"`
27 | Fetchers uint64 `json:"fetchers"`
28 | }
29 | API struct {
30 | Port string `json:"port"`
31 | AllowedHosts []string `json:"allowed_hosts"`
32 | }
33 | Mysql struct {
34 | Host string `json:"host"`
35 | Port string `json:"port"`
36 | DB string `json:"db"`
37 | User string `json:"user"`
38 | Password string `json:"password"`
39 | }
40 | Clickhouse struct {
41 | Protocol string `json:"protocol"`
42 | Host string `json:"host"`
43 | Port uint `json:"port"`
44 | User string `json:"user"`
45 | Password string `json:"password"`
46 | Database string `json:"database"`
47 | }
48 | )
49 |
50 | func GetConfig() Config {
51 | path, _ := filepath.Abs(configPath)
52 | file, err := ioutil.ReadFile(path)
53 | if err != nil {
54 | log.Fatalln("Invalid config path : "+configPath, err)
55 | }
56 | var config Config
57 | err = json.Unmarshal(file, &config)
58 | if err != nil {
59 | log.Fatalln("Failed unmarshal config ", err)
60 | }
61 | return config
62 | }
63 |
--------------------------------------------------------------------------------
/dao/clickhouse/history_proposals.go:
--------------------------------------------------------------------------------
1 | package clickhouse
2 |
3 | import (
4 | "fmt"
5 | "github.com/Masterminds/squirrel"
6 | "github.com/everstake/cosmoscan-api/dao/filters"
7 | "github.com/everstake/cosmoscan-api/dmodels"
8 | )
9 |
10 | func (db DB) CreateHistoryProposals(proposals []dmodels.HistoryProposal) error {
11 | if len(proposals) == 0 {
12 | return nil
13 | }
14 | q := squirrel.Insert(dmodels.HistoryProposalsTable).Columns(
15 | "hpr_id",
16 | "hpr_tx_hash",
17 | "hpr_title",
18 | "hpr_description",
19 | "hpr_recipient",
20 | "hpr_amount",
21 | "hpr_init_deposit",
22 | "hpr_proposer",
23 | "hpr_created_at",
24 | )
25 | for _, proposal := range proposals {
26 | if proposal.ID == 0 {
27 | return fmt.Errorf("field ProposalID can not be 0")
28 | }
29 | if proposal.CreatedAt.IsZero() {
30 | return fmt.Errorf("field CreatedAt can not be zero")
31 | }
32 | q = q.Values(
33 | proposal.ID,
34 | proposal.TxHash,
35 | proposal.Title,
36 | proposal.Description,
37 | proposal.Recipient,
38 | proposal.Amount,
39 | proposal.InitDeposit,
40 | proposal.Proposer,
41 | proposal.CreatedAt,
42 | )
43 | }
44 | return db.Insert(q)
45 | }
46 |
47 | func (db DB) GetHistoryProposals(filter filters.HistoryProposals) (proposals []dmodels.HistoryProposal, err error) {
48 | q := squirrel.Select("*").From(dmodels.HistoryProposalsTable).OrderBy("hpr_created_at desc")
49 | if len(filter.ID) != 0 {
50 | q = q.Where(squirrel.Eq{"hpr_id": filter.ID})
51 | }
52 | if filter.Limit != 0 {
53 | q = q.Limit(filter.Limit)
54 | }
55 | if filter.Offset != 0 {
56 | q = q.Offset(filter.Offset)
57 | }
58 | err = db.Find(&proposals, q)
59 | return proposals, err
60 | }
61 |
--------------------------------------------------------------------------------
/api/blocks.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/everstake/cosmoscan-api/dao/filters"
5 | "github.com/everstake/cosmoscan-api/log"
6 | "github.com/gorilla/mux"
7 | "net/http"
8 | "strconv"
9 | )
10 |
11 | func (api *API) GetAggBlocksCount(w http.ResponseWriter, r *http.Request) {
12 | api.aggHandler(w, r, api.svc.GetAggBlocksCount)
13 | }
14 |
15 | func (api *API) GetAggBlocksDelay(w http.ResponseWriter, r *http.Request) {
16 | api.aggHandler(w, r, api.svc.GetAggBlocksDelay)
17 | }
18 |
19 | func (api *API) GetAggUniqBlockValidators(w http.ResponseWriter, r *http.Request) {
20 | api.aggHandler(w, r, api.svc.GetAggUniqBlockValidators)
21 | }
22 |
23 | func (api *API) GetBlock(w http.ResponseWriter, r *http.Request) {
24 | heightStr, ok := mux.Vars(r)["height"]
25 | if !ok || heightStr == "" {
26 | jsonBadRequest(w, "invalid address")
27 | return
28 | }
29 | height, err := strconv.ParseUint(heightStr, 10, 64)
30 | if err != nil {
31 | jsonBadRequest(w, "invalid height")
32 | return
33 | }
34 | resp, err := api.svc.GetBlock(height)
35 | if err != nil {
36 | log.Error("API GetValidator: svc.GetBlock: %s", err.Error())
37 | jsonError(w)
38 | return
39 | }
40 | jsonData(w, resp)
41 | }
42 |
43 | func (api *API) GetBlocks(w http.ResponseWriter, r *http.Request) {
44 | var filter filters.Blocks
45 | err := api.queryDecoder.Decode(&filter, r.URL.Query())
46 | if err != nil {
47 | log.Debug("API Decode: %s", err.Error())
48 | jsonBadRequest(w, "")
49 | return
50 | }
51 | if filter.Limit == 0 || filter.Limit > 100 {
52 | filter.Limit = 100
53 | }
54 | resp, err := api.svc.GetBlocks(filter)
55 | if err != nil {
56 | log.Error("API GetBlocks: svc.GetBlocks: %s", err.Error())
57 | jsonError(w)
58 | return
59 | }
60 | jsonData(w, resp)
61 | }
62 |
--------------------------------------------------------------------------------
/dmodels/proposal.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/shopspring/decimal"
6 | )
7 |
8 | const ProposalsTable = "proposals"
9 |
10 | type Proposal struct {
11 | ID uint64 `db:"pro_id" json:"id"`
12 | TxHash string `db:"pro_tx_hash" json:"tx_hash"`
13 | Type string `db:"pro_type" json:"type"`
14 | Proposer string `db:"pro_proposer" json:"proposer"`
15 | ProposerAddress string `db:"pro_proposer_address" json:"proposer_address"`
16 | Title string `db:"pro_title" json:"title"`
17 | Description string `db:"pro_description" json:"description"`
18 | Status string `db:"pro_status" json:"status"`
19 | VotesYes decimal.Decimal `db:"pro_votes_yes" json:"votes_yes"`
20 | VotesAbstain decimal.Decimal `db:"pro_votes_abstain" json:"votes_abstain"`
21 | VotesNo decimal.Decimal `db:"pro_votes_no" json:"votes_no"`
22 | VotesNoWithVeto decimal.Decimal `db:"pro_votes_no_with_veto" json:"votes_no_with_veto"`
23 | SubmitTime Time `db:"pro_submit_time" json:"submit_time"`
24 | DepositEndTime Time `db:"pro_deposit_end_time" json:"deposit_end_time"`
25 | TotalDeposits decimal.Decimal `db:"pro_total_deposits" json:"total_deposits"`
26 | VotingStartTime Time `db:"pro_voting_start_time" json:"voting_start_time"`
27 | VotingEndTime Time `db:"pro_voting_end_time" json:"voting_end_time"`
28 | Voters uint64 `db:"pro_voters" json:"voters"`
29 | ParticipationRate decimal.Decimal `db:"pro_participation_rate" json:"participation_rate"`
30 | Turnout decimal.Decimal `db:"pro_turnout" json:"turnout"`
31 | Activity json.RawMessage `db:"pro_activity" json:"activity"`
32 | }
33 |
--------------------------------------------------------------------------------
/services/modules/modules.go:
--------------------------------------------------------------------------------
1 | package modules
2 |
3 | import (
4 | "fmt"
5 | "github.com/everstake/cosmoscan-api/log"
6 | "os"
7 | "sync"
8 | "time"
9 | )
10 |
11 | var gracefulTimeout = time.Second * 5
12 |
13 | type Module interface {
14 | Run() error
15 | Stop() error
16 | Title() string
17 | }
18 |
19 | type Group struct {
20 | modules []Module
21 | }
22 |
23 | type errResp struct {
24 | err error
25 | module string
26 | }
27 |
28 | func NewGroup(module ...Module) *Group {
29 | return &Group{
30 | modules: module,
31 | }
32 | }
33 |
34 | func (g *Group) Run() {
35 | errors := make(chan errResp, len(g.modules))
36 | for _, m := range g.modules {
37 | go func(m Module) {
38 | err := m.Run()
39 | errResp := errResp{
40 | err: err,
41 | module: m.Title(),
42 | }
43 | errors <- errResp
44 | }(m)
45 | }
46 | // handle errors
47 | go func() {
48 | for {
49 | err := <-errors
50 | if err.err != nil {
51 | log.Error("Module [%s] return error: %s", err.module, err.err)
52 | g.Stop()
53 | os.Exit(0)
54 | }
55 | log.Info("Module [%s] finish work", err.module)
56 | }
57 | }()
58 | }
59 |
60 | func (g *Group) Stop() {
61 | wg := &sync.WaitGroup{}
62 | wg.Add(len(g.modules))
63 | for _, m := range g.modules {
64 | go func(m Module) {
65 | err := stopModule(m)
66 | if err != nil {
67 | log.Error("Module [%s] stopped with error: %s", m.Title(), err.Error())
68 | }
69 | wg.Done()
70 | }(m)
71 | }
72 | wg.Wait()
73 | log.Info("All modules was stopped")
74 | }
75 |
76 | func stopModule(m Module) error {
77 | if m == nil {
78 | return nil
79 | }
80 | result := make(chan error)
81 | go func() {
82 | result <- m.Stop()
83 | }()
84 | select {
85 | case err := <-result:
86 | return err
87 | case <-time.After(gracefulTimeout):
88 | return fmt.Errorf("stoped by timeout")
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/dao/clickhouse/rewards.go:
--------------------------------------------------------------------------------
1 | package clickhouse
2 |
3 | import (
4 | "fmt"
5 | "github.com/Masterminds/squirrel"
6 | "github.com/everstake/cosmoscan-api/dmodels"
7 | )
8 |
9 | func (db DB) CreateDelegatorRewards(rewards []dmodels.DelegatorReward) error {
10 | if len(rewards) == 0 {
11 | return nil
12 | }
13 | q := squirrel.Insert(dmodels.DelegatorRewardsTable).Columns("der_id", "der_tx_hash", "der_delegator", "der_validator", "der_amount", "der_created_at")
14 | for _, reward := range rewards {
15 | if reward.ID == "" {
16 | return fmt.Errorf("field ProposalID can not be empty")
17 | }
18 | if reward.TxHash == "" {
19 | return fmt.Errorf("field TxHash can not be empty")
20 | }
21 | if reward.Delegator == "" {
22 | return fmt.Errorf("field Delegator can not be empty")
23 | }
24 | if reward.Validator == "" {
25 | return fmt.Errorf("field Validator can not be empty")
26 | }
27 | if reward.CreatedAt.IsZero() {
28 | return fmt.Errorf("field CreatedAt can not be zero")
29 | }
30 | q = q.Values(reward.ID, reward.TxHash, reward.Delegator, reward.Validator, reward.Amount, reward.CreatedAt)
31 | }
32 | return db.Insert(q)
33 | }
34 |
35 | func (db DB) CreateValidatorRewards(rewards []dmodels.ValidatorReward) error {
36 | if len(rewards) == 0 {
37 | return nil
38 | }
39 | q := squirrel.Insert(dmodels.ValidatorRewardsTable).Columns("var_id", "var_tx_hash", "var_address", "var_amount", "var_created_at")
40 | for _, reward := range rewards {
41 | if reward.ID == "" {
42 | return fmt.Errorf("field ProposalID can not be empty")
43 | }
44 | if reward.TxHash == "" {
45 | return fmt.Errorf("field TxHash can not be empty")
46 | }
47 | if reward.Address == "" {
48 | return fmt.Errorf("field Address can not be empty")
49 | }
50 | if reward.CreatedAt.IsZero() {
51 | return fmt.Errorf("field CreatedAt can not be zero")
52 | }
53 | q = q.Values(reward.ID, reward.TxHash, reward.Address, reward.Amount, reward.CreatedAt)
54 | }
55 | return db.Insert(q)
56 | }
57 |
--------------------------------------------------------------------------------
/dao/clickhouse/accounts.go:
--------------------------------------------------------------------------------
1 | package clickhouse
2 |
3 | import (
4 | "fmt"
5 | "github.com/Masterminds/squirrel"
6 | "github.com/everstake/cosmoscan-api/dao/filters"
7 | "github.com/everstake/cosmoscan-api/dmodels"
8 | )
9 |
10 | func (db DB) GetActiveAccounts(filter filters.ActiveAccounts) (addresses []string, err error) {
11 | var items = []struct {
12 | field string
13 | table string
14 | dateField string
15 | }{
16 | {field: "dlg_delegator", table: dmodels.DelegationsTable, dateField: "dlg_created_at"},
17 | {field: "trf_from", table: dmodels.TransfersTable, dateField: "trf_created_at"},
18 | {field: "trf_to", table: dmodels.TransfersTable, dateField: "trf_created_at"},
19 | {field: "der_delegator", table: dmodels.DelegatorRewardsTable, dateField: "der_created_at"},
20 | }
21 |
22 | var qs []squirrel.SelectBuilder
23 | for _, item := range items {
24 | q := squirrel.Select(fmt.Sprintf("DISTINCT %s as address", item.field)).
25 | From(item.table)
26 | if !filter.From.IsZero() {
27 | q = q.Where(squirrel.GtOrEq{item.dateField: filter.From})
28 | }
29 | if !filter.To.IsZero() {
30 | q = q.Where(squirrel.LtOrEq{item.dateField: filter.To})
31 | }
32 | qs = append(qs, q)
33 | }
34 |
35 | q := qs[0]
36 |
37 | for i := 1; i < len(qs); i++ {
38 | sql, args, _ := qs[i].ToSql()
39 | q = qs[i].Suffix("UNION ALL "+sql, args...)
40 | }
41 |
42 | query := squirrel.Select("DISTINCT t.address").FromSelect(q, "t")
43 |
44 | err = db.Find(&addresses, query)
45 | return addresses, err
46 | }
47 |
48 | func (db DB) CreateAccountTxs(accountTxs []dmodels.AccountTx) error {
49 | if len(accountTxs) == 0 {
50 | return nil
51 | }
52 | q := squirrel.Insert(dmodels.AccountTxsTable).Columns("atx_account", "atx_tx_hash")
53 | for _, acc := range accountTxs {
54 | if acc.Account == "" {
55 | return fmt.Errorf("field Account can not beempty")
56 | }
57 | if acc.TxHash == "" {
58 | return fmt.Errorf("hash can not be empty")
59 | }
60 | q = q.Values(acc.Account, acc.TxHash)
61 | }
62 | return db.Insert(q)
63 | }
64 |
--------------------------------------------------------------------------------
/dao/clickhouse/stats.go:
--------------------------------------------------------------------------------
1 | package clickhouse
2 |
3 | import (
4 | "fmt"
5 | "github.com/Masterminds/squirrel"
6 | "github.com/everstake/cosmoscan-api/dao/filters"
7 | "github.com/everstake/cosmoscan-api/dmodels"
8 | "github.com/everstake/cosmoscan-api/smodels"
9 | )
10 |
11 | func (db DB) CreateStats(stats []dmodels.Stat) (err error) {
12 | if len(stats) == 0 {
13 | return nil
14 | }
15 | q := squirrel.Insert(dmodels.StatsTable).Columns("stt_id", "stt_title", "stt_value", "stt_created_at")
16 | for _, stat := range stats {
17 | if stat.ID == "" {
18 | return fmt.Errorf("field ProposalID can not be empty")
19 | }
20 | if stat.Title == "" {
21 | return fmt.Errorf("field Title can not be empty")
22 | }
23 | if stat.CreatedAt.IsZero() {
24 | return fmt.Errorf("field CreatedAt can not be zero")
25 | }
26 | q = q.Values(stat.ID, stat.Title, stat.Value, stat.CreatedAt)
27 | }
28 | return db.Insert(q)
29 | }
30 |
31 | func (db DB) GetStats(filter filters.Stats) (stats []dmodels.Stat, err error) {
32 | q := squirrel.Select("*").From(dmodels.StatsTable).OrderBy("stt_created_at")
33 | if !filter.From.IsZero() {
34 | q = q.Where(squirrel.GtOrEq{"stt_created_at": filter.From})
35 | }
36 | if !filter.To.IsZero() {
37 | q = q.Where(squirrel.LtOrEq{"stt_created_at": filter.To})
38 | }
39 | if len(filter.Titles) != 0 {
40 | q = q.Where(squirrel.Eq{"stt_title": filter.Titles})
41 | }
42 | err = db.Find(&stats, q)
43 | return stats, err
44 | }
45 |
46 | func (db DB) GetAggValidators33Power(filter filters.Agg) (items []smodels.AggItem, err error) {
47 | q := filter.BuildQuery("max(stt_value)", "stt_created_at", dmodels.StatsTable).
48 | Where(squirrel.Eq{"stt_title": dmodels.StatsValidatorsWith33Power})
49 | err = db.Find(&items, q)
50 | return items, err
51 | }
52 |
53 | func (db DB) GetAggWhaleAccounts(filter filters.Agg) (items []smodels.AggItem, err error) {
54 | q := filter.BuildQuery("max(stt_value)", "stt_created_at", dmodels.StatsTable).
55 | Where(squirrel.Eq{"stt_title": dmodels.StatsTotalWhaleAccounts})
56 | err = db.Find(&items, q)
57 | return items, err
58 | }
59 |
--------------------------------------------------------------------------------
/api/proposals.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/everstake/cosmoscan-api/dao/filters"
5 | "github.com/everstake/cosmoscan-api/log"
6 | "net/http"
7 | )
8 |
9 | func (api *API) GetProposals(w http.ResponseWriter, r *http.Request) {
10 | var filter filters.Proposals
11 | err := api.queryDecoder.Decode(&filter, r.URL.Query())
12 | if err != nil {
13 | log.Debug("API Decode: %s", err.Error())
14 | jsonBadRequest(w, "")
15 | return
16 | }
17 | resp, err := api.svc.GetProposals(filter)
18 | if err != nil {
19 | log.Error("API GetProposals: svc.GetProposals: %s", err.Error())
20 | jsonError(w)
21 | return
22 | }
23 | jsonData(w, resp)
24 | }
25 |
26 | func (api *API) GetProposalVotes(w http.ResponseWriter, r *http.Request) {
27 | var filter filters.ProposalVotes
28 | err := api.queryDecoder.Decode(&filter, r.URL.Query())
29 | if err != nil {
30 | log.Debug("API Decode: %s", err.Error())
31 | jsonBadRequest(w, "")
32 | return
33 | }
34 | //if filter.ProposalID == 0 {
35 | // log.Debug("API GetProposalVotes: proposal_id necessary")
36 | // jsonBadRequest(w, "proposal_id necessary")
37 | // return
38 | //}
39 | resp, err := api.svc.GetProposalVotes(filter)
40 | if err != nil {
41 | log.Error("API GetProposalVotes: svc.GetProposalVotes: %s", err.Error())
42 | jsonError(w)
43 | return
44 | }
45 | jsonData(w, resp)
46 | }
47 |
48 | func (api *API) GetProposalDeposits(w http.ResponseWriter, r *http.Request) {
49 | var filter filters.ProposalDeposits
50 | err := api.queryDecoder.Decode(&filter, r.URL.Query())
51 | if err != nil {
52 | log.Debug("API Decode: %s", err.Error())
53 | jsonBadRequest(w, "")
54 | return
55 | }
56 | resp, err := api.svc.GetProposalDeposits(filter)
57 | if err != nil {
58 | log.Error("API GetProposalDeposits: svc.GetProposalDeposits: %s", err.Error())
59 | jsonError(w)
60 | return
61 | }
62 | jsonData(w, resp)
63 | }
64 |
65 | func (api *API) GetProposalChartData(w http.ResponseWriter, r *http.Request) {
66 | resp, err := api.svc.GetProposalsChartData()
67 | if err != nil {
68 | log.Error("API GetProposalsChartData: svc.GetProposalsChartData: %s", err.Error())
69 | jsonError(w)
70 | return
71 | }
72 | jsonData(w, resp)
73 | }
74 |
--------------------------------------------------------------------------------
/dao/clickhouse/historical_states.go:
--------------------------------------------------------------------------------
1 | package clickhouse
2 |
3 | import (
4 | "fmt"
5 | "github.com/Masterminds/squirrel"
6 | "github.com/everstake/cosmoscan-api/dao/filters"
7 | "github.com/everstake/cosmoscan-api/dmodels"
8 | "github.com/everstake/cosmoscan-api/smodels"
9 | )
10 |
11 | func (db DB) CreateHistoricalStates(states []dmodels.HistoricalState) error {
12 | if len(states) == 0 {
13 | return nil
14 | }
15 | q := squirrel.Insert(dmodels.HistoricalStates).Columns(
16 | "his_price",
17 | "his_market_cap",
18 | "his_circulating_supply",
19 | "his_trading_volume",
20 | "his_staked_ratio",
21 | "his_inflation_rate",
22 | "his_transactions_count",
23 | "his_community_pool",
24 | "his_top_20_weight",
25 | "his_created_at",
26 | )
27 | for _, state := range states {
28 | q = q.Values(
29 | state.Price,
30 | state.MarketCap,
31 | state.CirculatingSupply,
32 | state.TradingVolume,
33 | state.StakedRatio,
34 | state.InflationRate,
35 | state.TransactionsCount,
36 | state.CommunityPool,
37 | state.Top20Weight,
38 | state.CreatedAt,
39 | )
40 | }
41 | return db.Insert(q)
42 | }
43 |
44 | func (db DB) GetHistoricalStates(filter filters.HistoricalState) (states []dmodels.HistoricalState, err error) {
45 | q := squirrel.Select("*").From(dmodels.HistoricalStates).OrderBy("his_created_at desc")
46 | if filter.Limit != 0 {
47 | q = q.Limit(filter.Limit)
48 | }
49 | if filter.Offset != 0 {
50 | q = q.Limit(filter.Offset)
51 | }
52 | err = db.Find(&states, q)
53 | return states, err
54 | }
55 |
56 | func (db DB) GetAggHistoricalStatesByField(filter filters.Agg, field string) (items []smodels.AggItem, err error) {
57 | q := squirrel.Select(
58 | fmt.Sprintf("avg(%s) AS value", field),
59 | fmt.Sprintf("toDateTime(%s(his_created_at)) AS time", filter.AggFunc()),
60 | ).From(dmodels.HistoricalStates).
61 | GroupBy("time").
62 | OrderBy("time")
63 | if !filter.From.IsZero() {
64 | q = q.Where(squirrel.GtOrEq{"his_created_at": filter.From.Time})
65 | }
66 | if !filter.To.IsZero() {
67 | q = q.Where(squirrel.LtOrEq{"his_created_at": filter.To.Time})
68 | }
69 | err = db.Find(&items, q)
70 | if err != nil {
71 | return nil, err
72 | }
73 | return items, nil
74 | }
75 |
--------------------------------------------------------------------------------
/dao/clickhouse/transfers.go:
--------------------------------------------------------------------------------
1 | package clickhouse
2 |
3 | import (
4 | "fmt"
5 | "github.com/Masterminds/squirrel"
6 | "github.com/everstake/cosmoscan-api/config"
7 | "github.com/everstake/cosmoscan-api/dao/filters"
8 | "github.com/everstake/cosmoscan-api/dmodels"
9 | "github.com/everstake/cosmoscan-api/smodels"
10 | "github.com/shopspring/decimal"
11 | )
12 |
13 | func (db DB) CreateTransfers(transfers []dmodels.Transfer) error {
14 | if len(transfers) == 0 {
15 | return nil
16 | }
17 | q := squirrel.Insert(dmodels.TransfersTable).Columns("trf_id", "trf_tx_hash", "trf_from", "trf_to", "trf_amount", "trf_created_at", "trf_currency")
18 | for _, transfer := range transfers {
19 | if transfer.ID == "" {
20 | return fmt.Errorf("field ProposalID can not be empty")
21 | }
22 | if transfer.TxHash == "" {
23 | return fmt.Errorf("field TxHash can not be empty")
24 | }
25 | if transfer.CreatedAt.IsZero() {
26 | return fmt.Errorf("field CreatedAt can not be zero")
27 | }
28 | q = q.Values(transfer.ID, transfer.TxHash, transfer.From, transfer.To, transfer.Amount, transfer.CreatedAt, transfer.Currency)
29 | }
30 | return db.Insert(q)
31 | }
32 |
33 | func (db DB) GetAggTransfersVolume(filter filters.Agg) (items []smodels.AggItem, err error) {
34 | q := squirrel.Select(
35 | "sum(trf_amount) AS value",
36 | fmt.Sprintf("toDateTime(%s(trf_created_at)) AS time", filter.AggFunc()),
37 | ).From(dmodels.TransfersTable).
38 | Where("notEmpty(trf_from)").
39 | Where(squirrel.Eq{"trf_currency": config.Currency}).
40 | GroupBy("time").
41 | OrderBy("time")
42 | if !filter.From.IsZero() {
43 | q = q.Where(squirrel.GtOrEq{"trf_created_at": filter.From.Time})
44 | }
45 | if !filter.To.IsZero() {
46 | q = q.Where(squirrel.LtOrEq{"trf_created_at": filter.To.Time})
47 | }
48 | err = db.Find(&items, q)
49 | return items, err
50 | }
51 |
52 | func (db DB) GetTransferVolume(filter filters.TimeRange) (total decimal.Decimal, err error) {
53 | q := squirrel.Select("sum(trf_amount) as total").
54 | From(dmodels.TransfersTable).
55 | Where("notEmpty(trf_from)").
56 | Where(squirrel.Eq{"trf_currency": config.Currency})
57 | q = filter.Query("trf_created_at", q)
58 | err = db.FindFirst(&total, q)
59 | return total, err
60 | }
61 |
--------------------------------------------------------------------------------
/services/meta_data.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "fmt"
5 | "github.com/everstake/cosmoscan-api/dao/filters"
6 | "github.com/everstake/cosmoscan-api/services/helpers"
7 | "github.com/everstake/cosmoscan-api/services/node"
8 | "github.com/everstake/cosmoscan-api/smodels"
9 | "github.com/shopspring/decimal"
10 | )
11 |
12 | func (s *ServiceFacade) GetMetaData() (meta smodels.MetaData, err error) {
13 | states, err := s.dao.GetHistoricalStates(filters.HistoricalState{Limit: 1})
14 | if err != nil {
15 | return meta, fmt.Errorf("dao.GetHistoricalStates: %s", err.Error())
16 | }
17 | if len(states) != 0 {
18 | state := states[0]
19 | meta.CurrentPrice = state.Price
20 | }
21 | blocks, err := s.dao.GetBlocks(filters.Blocks{Limit: 2})
22 | if err != nil {
23 | return meta, fmt.Errorf("dao.GetBlocks: %s", err.Error())
24 | }
25 | if len(blocks) == 2 {
26 | meta.BlockTime = blocks[0].CreatedAt.Sub(blocks[1].CreatedAt).Seconds()
27 | meta.Height = blocks[0].ID
28 | }
29 | var proposer string
30 | if len(blocks) > 0 {
31 | proposer = blocks[0].Proposer
32 | }
33 |
34 | data, found := s.dao.CacheGet(validatorsMapCacheKey)
35 | if found {
36 | validators := data.(map[string]node.Validator)
37 | avgFee := decimal.Zero
38 | for _, validator := range validators {
39 | avgFee = avgFee.Add(validator.Commission.CommissionRates.Rate)
40 | }
41 | if len(validators) > 0 {
42 | meta.ValidatorAvgFee = avgFee.Div(decimal.New(int64(len(validators)), 0)).Mul(decimal.New(100, 0))
43 | }
44 | for _, validator := range validators {
45 | consAddress, err := helpers.GetHexAddressFromBase64PK(validator.ConsensusPubkey.Key)
46 | if err != nil {
47 | return meta, fmt.Errorf("helpers.GetHexAddressFromBase64PK(%s): %s", validator.ConsensusPubkey.Key, err.Error())
48 | }
49 | if consAddress == proposer {
50 | meta.LatestValidator = validator.Description.Moniker
51 | break
52 | }
53 | }
54 | }
55 | proposals, err := s.dao.GetProposals(filters.Proposals{Limit: 1})
56 | if err != nil {
57 | return meta, fmt.Errorf("dao.GetProposals: %s", err.Error())
58 | }
59 | if len(proposals) != 0 {
60 | meta.LatestProposal = smodels.MetaDataProposal{
61 | Name: proposals[0].Title,
62 | ID: proposals[0].ID,
63 | }
64 | }
65 | return meta, nil
66 | }
67 |
--------------------------------------------------------------------------------
/dao/filters/agg.go:
--------------------------------------------------------------------------------
1 | package filters
2 |
3 | import (
4 | "fmt"
5 | "github.com/Masterminds/squirrel"
6 | "github.com/everstake/cosmoscan-api/dmodels"
7 | "time"
8 | )
9 |
10 | const (
11 | AggByHour = "hour"
12 | AggByDay = "day"
13 | AggByWeek = "week"
14 | AggByMonth = "month"
15 | )
16 |
17 | type Agg struct {
18 | By string `schema:"by"`
19 | From dmodels.Time `schema:"from"`
20 | To dmodels.Time `schema:"to"`
21 | }
22 |
23 | var aggLimits = map[string]struct {
24 | defaultRange time.Duration
25 | maxRange time.Duration
26 | }{
27 | AggByHour: {
28 | defaultRange: time.Hour * 24,
29 | maxRange: time.Hour * 24 * 7,
30 | },
31 | AggByDay: {
32 | defaultRange: time.Hour * 24 * 30,
33 | maxRange: time.Hour * 24 * 30 * 2,
34 | },
35 | AggByWeek: {
36 | defaultRange: time.Hour * 24 * 40,
37 | maxRange: time.Hour * 24 * 40 * 3,
38 | },
39 | AggByMonth: {
40 | defaultRange: time.Hour * 24 * 365,
41 | maxRange: time.Hour * 24 * 365 * 2,
42 | },
43 | }
44 |
45 | func (agg *Agg) Validate() error {
46 | limit, ok := aggLimits[agg.By]
47 | if !ok {
48 | return fmt.Errorf("not found `by` param")
49 | }
50 | if agg.From.IsZero() {
51 | agg.From = dmodels.NewTime(time.Now().Add(-limit.defaultRange))
52 | agg.To = dmodels.NewTime(time.Now())
53 | } else {
54 | d := agg.To.Sub(agg.From.Time)
55 | if d > limit.maxRange {
56 | return fmt.Errorf("over max limit range")
57 | }
58 | }
59 | return nil
60 | }
61 |
62 | func (agg *Agg) AggFunc() string {
63 | switch agg.By {
64 | case AggByHour:
65 | return "toStartOfHour"
66 | case AggByDay:
67 | return "toStartOfDay"
68 | case AggByWeek:
69 | return "toStartOfWeek"
70 | case AggByMonth:
71 | return "toStartOfMonth"
72 | default:
73 | return "toStartOfDay"
74 | }
75 | }
76 |
77 | func (agg *Agg) BuildQuery(aggValue string, timeColumn string, table string) squirrel.SelectBuilder {
78 | q := squirrel.Select(
79 | fmt.Sprintf("%s as value", aggValue),
80 | fmt.Sprintf("toDateTime(%s(%s)) AS time", agg.AggFunc(), timeColumn),
81 | ).From(table).
82 | GroupBy("time").
83 | OrderBy("time")
84 | if !agg.From.IsZero() {
85 | q = q.Where(squirrel.GtOrEq{timeColumn: agg.From.Time})
86 | }
87 | if !agg.To.IsZero() {
88 | q = q.Where(squirrel.LtOrEq{timeColumn: agg.To.Time})
89 | }
90 | return q
91 | }
92 |
--------------------------------------------------------------------------------
/dao/mysql/validators.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "fmt"
5 | "github.com/Masterminds/squirrel"
6 | "github.com/everstake/cosmoscan-api/dmodels"
7 | )
8 |
9 | func (m DB) CreateValidators(validators []dmodels.Validator) error {
10 | if len(validators) == 0 {
11 | return nil
12 | }
13 | q := squirrel.Insert(dmodels.ValidatorsTable).Columns(
14 | "val_cons_address",
15 | "val_address",
16 | "val_operator_address",
17 | "val_cons_pub_key",
18 | "val_name",
19 | "val_description",
20 | "val_commission",
21 | "val_min_commission",
22 | "val_max_commission",
23 | "val_self_delegations",
24 | "val_delegations",
25 | "val_voting_power",
26 | "val_website",
27 | "val_jailed",
28 | "val_created_at",
29 | )
30 | for _, validator := range validators {
31 | if validator.ConsAddress == "" {
32 | return fmt.Errorf("ConsAddress is empty")
33 | }
34 | q = q.Values(
35 | validator.ConsAddress,
36 | validator.Address,
37 | validator.OperatorAddress,
38 | validator.ConsPubKey,
39 | validator.Name,
40 | validator.Description,
41 | validator.Commission,
42 | validator.MinCommission,
43 | validator.MaxCommission,
44 | validator.SelfDelegations,
45 | validator.Delegations,
46 | validator.VotingPower,
47 | validator.Website,
48 | validator.Jailed,
49 | validator.CreatedAt,
50 | )
51 | }
52 | _, err := m.insert(q)
53 | return err
54 | }
55 |
56 | func (m DB) UpdateValidators(validator dmodels.Validator) error {
57 | q := squirrel.Update(dmodels.ValidatorsTable).
58 | Where(squirrel.Eq{"val_cons_address": validator.ConsAddress}).
59 | SetMap(map[string]interface{}{
60 | "val_address": validator.Address,
61 | "val_operator_address": validator.OperatorAddress,
62 | "val_cons_pub_key": validator.ConsPubKey,
63 | "val_name": validator.Name,
64 | "val_description": validator.Description,
65 | "val_commission": validator.Commission,
66 | "val_min_commission": validator.MinCommission,
67 | "val_max_commission": validator.MaxCommission,
68 | "val_self_delegations": validator.SelfDelegations,
69 | "val_delegations": validator.Delegations,
70 | "val_voting_power": validator.VotingPower,
71 | "val_website": validator.Website,
72 | "val_jailed": validator.Jailed,
73 | })
74 | return m.update(q)
75 | }
76 |
--------------------------------------------------------------------------------
/dao/clickhouse/proposal_votes.go:
--------------------------------------------------------------------------------
1 | package clickhouse
2 |
3 | import (
4 | "fmt"
5 | "github.com/Masterminds/squirrel"
6 | "github.com/everstake/cosmoscan-api/dao/filters"
7 | "github.com/everstake/cosmoscan-api/dmodels"
8 | "github.com/everstake/cosmoscan-api/smodels"
9 | )
10 |
11 | func (db DB) CreateProposalVotes(votes []dmodels.ProposalVote) error {
12 | if len(votes) == 0 {
13 | return nil
14 | }
15 | q := squirrel.Insert(dmodels.ProposalVotesTable).Columns("prv_id", "prv_proposal_id", "prv_voter", "prv_tx_hash", "prv_option", "prv_created_at")
16 | for _, vote := range votes {
17 | if vote.ID == "" {
18 | return fmt.Errorf("field ProposalID can not be empty")
19 | }
20 | if vote.ProposalID == 0 {
21 | return fmt.Errorf("field ProposalID can not be zero")
22 | }
23 | if vote.Voter == "" {
24 | return fmt.Errorf("field Voter can not be empty")
25 | }
26 | if vote.TxHash == "" {
27 | return fmt.Errorf("field TxHash can not be empty")
28 | }
29 | if vote.CreatedAt.IsZero() {
30 | return fmt.Errorf("field CreatedAt can not be zero")
31 | }
32 | q = q.Values(vote.ID, vote.ProposalID, vote.Voter, vote.TxHash, vote.Option, vote.CreatedAt)
33 | }
34 | return db.Insert(q)
35 | }
36 |
37 | func (db DB) GetProposalVotes(filter filters.ProposalVotes) (votes []dmodels.ProposalVote, err error) {
38 | q := squirrel.Select("*").From(dmodels.ProposalVotesTable).OrderBy("prv_created_at")
39 | if filter.ProposalID != 0 {
40 | q = q.Where(squirrel.Eq{"prv_proposal_id": filter.ProposalID})
41 | }
42 | if len(filter.Voters) != 0 {
43 | q = q.Where(squirrel.Eq{"prv_voter": filter.Voters})
44 | }
45 | if filter.Limit != 0 {
46 | q = q.Limit(filter.Limit)
47 | }
48 | if filter.Offset != 0 {
49 | q = q.Offset(filter.Offset)
50 | }
51 | err = db.Find(&votes, q)
52 | return votes, err
53 | }
54 |
55 | func (db DB) GetAggProposalVotes(filter filters.Agg, id []uint64) (items []smodels.AggItem, err error) {
56 | q := filter.BuildQuery("toDecimal64(count(*), 0)", "prv_created_at", dmodels.ProposalVotesTable)
57 | if len(id) != 0 {
58 | q = q.Where(squirrel.Eq{"prv_proposal_id": id})
59 | }
60 | err = db.Find(&items, q)
61 | return items, err
62 | }
63 |
64 | func (db DB) GetTotalVotesByAddress(address string) (total uint64, err error) {
65 | q := squirrel.Select("count(distinct prv_proposal_id) as total").
66 | From(dmodels.ProposalVotesTable).
67 | Where(squirrel.Eq{"prv_voter": address})
68 | err = db.FindFirst(&total, q)
69 | return total, err
70 | }
71 |
--------------------------------------------------------------------------------
/services/cmc/cmc.go:
--------------------------------------------------------------------------------
1 | package cmc
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/everstake/cosmoscan-api/config"
7 | "github.com/shopspring/decimal"
8 | "io/ioutil"
9 | "net/http"
10 | )
11 |
12 | const apiURL = "https://pro-api.coinmarketcap.com"
13 |
14 | type (
15 | CMC struct {
16 | cfg config.Config
17 | client *http.Client
18 | }
19 | CurrenciesResponse struct {
20 | Status struct {
21 | ErrorCode int `json:"error_code"`
22 | ErrorMessage string `json:"error_message,omitempty"`
23 | } `json:"status"`
24 | Data []Currency `json:"data"`
25 | }
26 | Currency struct {
27 | CirculatingSupply decimal.Decimal `json:"circulating_supply"`
28 | CMCRank int `json:"cmc_rank"`
29 | TotalSupply decimal.Decimal `json:"total_supply"`
30 | Symbol string `json:"symbol"`
31 | Quote map[string]struct {
32 | MarketCap decimal.Decimal `json:"market_cap"`
33 | PercentChange1h decimal.Decimal `json:"percent_change_1h"`
34 | PercentChange7d decimal.Decimal `json:"percent_change_7d"`
35 | PercentChange24h decimal.Decimal `json:"percent_change_24h"`
36 | Price decimal.Decimal `json:"price"`
37 | Volume24h decimal.Decimal `json:"volume_24h"`
38 | } `json:"quote"`
39 | }
40 | )
41 |
42 | func NewCMC(cfg config.Config) *CMC {
43 | return &CMC{
44 | client: &http.Client{},
45 | cfg: cfg,
46 | }
47 | }
48 |
49 | func (cmc *CMC) request(endpoint string, data interface{}) error {
50 | url := fmt.Sprintf("%s%s", apiURL, endpoint)
51 | req, err := http.NewRequest(http.MethodGet, url, nil)
52 | if err != nil {
53 | return fmt.Errorf("http.NewRequest: %s", err.Error())
54 | }
55 | req.Header.Set("Accepts", "application/json")
56 | req.Header.Set("X-CMC_PRO_API_KEY", cmc.cfg.CMCKey)
57 | resp, err := cmc.client.Do(req)
58 | if err != nil {
59 | return fmt.Errorf("client.Do: %s", err.Error())
60 | }
61 | d, err := ioutil.ReadAll(resp.Body)
62 | if err != nil {
63 | return fmt.Errorf("ioutil.ReadAll: %s", err.Error())
64 | }
65 | err = json.Unmarshal(d, data)
66 | if err != nil {
67 | return fmt.Errorf("json.Unmarshal: %s", err.Error())
68 | }
69 | return nil
70 | }
71 |
72 | func (cmc *CMC) GetCurrencies() (currencies []Currency, err error) {
73 | var currencyResp CurrenciesResponse
74 | err = cmc.request("/v1/cryptocurrency/listings/latest", ¤cyResp)
75 | if currencyResp.Status.ErrorCode != 0 {
76 | return nil, fmt.Errorf("error code: %d, msg: %s", currencyResp.Status.ErrorCode, currencyResp.Status.ErrorMessage)
77 | }
78 | return currencyResp.Data, err
79 | }
80 |
--------------------------------------------------------------------------------
/dao/mysql/accounts.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "fmt"
5 | "github.com/Masterminds/squirrel"
6 | "github.com/everstake/cosmoscan-api/dao/filters"
7 | "github.com/everstake/cosmoscan-api/dmodels"
8 | )
9 |
10 | func (m DB) CreateAccounts(accounts []dmodels.Account) error {
11 | if len(accounts) == 0 {
12 | return nil
13 | }
14 | q := squirrel.Insert(dmodels.AccountsTable).Columns(
15 | "acc_address",
16 | "acc_balance",
17 | "acc_stake",
18 | "acc_unbonding",
19 | "acc_created_at",
20 | )
21 | for _, account := range accounts {
22 | if account.Address == "" {
23 | return fmt.Errorf("field Address is empty")
24 | }
25 | if account.CreatedAt.IsZero() {
26 | return fmt.Errorf("field CreatedAt is empty")
27 | }
28 | q = q.Values(
29 | account.Address,
30 | account.Balance,
31 | account.Stake,
32 | account.Unbonding,
33 | account.CreatedAt,
34 | )
35 | }
36 | q = q.Suffix("ON DUPLICATE KEY UPDATE acc_address=acc_address")
37 | _, err := m.insert(q)
38 | return err
39 | }
40 |
41 | func (m DB) UpdateAccount(account dmodels.Account) error {
42 | q := squirrel.Update(dmodels.AccountsTable).
43 | Where(squirrel.Eq{"acc_address": account.Address}).
44 | SetMap(map[string]interface{}{
45 | "acc_balance": account.Balance,
46 | "acc_stake": account.Stake,
47 | "acc_unbonding": account.Unbonding,
48 | })
49 | return m.update(q)
50 | }
51 |
52 | func (m DB) GetAccounts(filter filters.Accounts) (accounts []dmodels.Account, err error) {
53 | q := squirrel.Select("*").From(dmodels.AccountsTable)
54 | if !filter.GtTotalAmount.IsZero() {
55 | q = q.Where(squirrel.Gt{"acc_balance + acc_stake": filter.GtTotalAmount})
56 | }
57 | if !filter.LtTotalAmount.IsZero() {
58 | q = q.Where(squirrel.Lt{"acc_balance + acc_stake": filter.LtTotalAmount})
59 | }
60 | err = m.find(&accounts, q)
61 | return accounts, err
62 | }
63 |
64 | func (m DB) GetAccountsTotal(filter filters.Accounts) (total uint64, err error) {
65 | q := squirrel.Select("count(*) as total").From(dmodels.AccountsTable)
66 | if !filter.GtTotalAmount.IsZero() {
67 | q = q.Where(squirrel.Gt{"acc_balance + acc_stake + acc_unbonding": filter.GtTotalAmount})
68 | }
69 | if !filter.LtTotalAmount.IsZero() {
70 | q = q.Where(squirrel.Lt{"acc_balance + acc_stake + acc_unbonding": filter.LtTotalAmount})
71 | }
72 | err = m.first(&total, q)
73 | return total, err
74 | }
75 |
76 | func (m DB) GetAccount(address string) (account dmodels.Account, err error) {
77 | q := squirrel.Select("*").From(dmodels.AccountsTable).Where(squirrel.Eq{"acc_address": address})
78 | err = m.first(&account, q)
79 | return account, err
80 | }
81 |
--------------------------------------------------------------------------------
/dmodels/time.go:
--------------------------------------------------------------------------------
1 | package dmodels
2 |
3 | import (
4 | "database/sql/driver"
5 | "fmt"
6 | "strconv"
7 | "strings"
8 | "time"
9 | )
10 |
11 | type Time struct {
12 | time.Time
13 | }
14 |
15 | func NewTime(t time.Time) Time {
16 | return Time{Time: t}
17 | }
18 |
19 | func (t Time) MarshalJSON() ([]byte, error) {
20 | return []byte(strconv.FormatInt(t.Unix(), 10)), nil
21 | }
22 |
23 | func (t *Time) UnmarshalJSON(data []byte) error {
24 | str := string(data)
25 | str = strings.Trim(str, `"`)
26 | timestamp, err := strconv.ParseInt(str, 10, 64)
27 | if err != nil {
28 | return err
29 | }
30 | t.Time = time.Unix(timestamp, 0)
31 | return nil
32 | }
33 |
34 | const timeFormat = "2006-01-02 15:04:05.999999"
35 |
36 | // Scan implements the Scanner interface.
37 | // The value type must be time.Time or string / []byte (formatted time-string),
38 | // otherwise Scan fails.
39 | func (t *Time) Scan(value interface{}) (err error) {
40 | if value == nil {
41 | return fmt.Errorf("invalid value")
42 | }
43 |
44 | switch v := value.(type) {
45 | case time.Time:
46 | t.Time = v
47 | return
48 | case []byte:
49 | t.Time, err = parseDateTime(string(v), time.UTC)
50 | if err != nil {
51 | return err
52 | }
53 | case string:
54 | t.Time, err = parseDateTime(v, time.UTC)
55 | if err != nil {
56 | return err
57 | }
58 | }
59 | return fmt.Errorf("can't convert %T to time.Time", value)
60 | }
61 |
62 | // Value implements the driver Valuer interface.
63 | func (t Time) Value() (driver.Value, error) {
64 | return t.Time, nil
65 | }
66 |
67 | func parseDateTime(str string, loc *time.Location) (t time.Time, err error) {
68 | base := "0000-00-00 00:00:00.0000000"
69 | switch len(str) {
70 | case 10, 19, 21, 22, 23, 24, 25, 26: // up to "YYYY-MM-DD HH:MM:SS.MMMMMM"
71 | if str == base[:len(str)] {
72 | return
73 | }
74 | t, err = time.Parse(timeFormat[:len(str)], str)
75 | default:
76 | err = fmt.Errorf("invalid time string: %s", str)
77 | return
78 | }
79 |
80 | // Adjust location
81 | if err == nil && loc != time.UTC {
82 | y, mo, d := t.Date()
83 | h, mi, s := t.Clock()
84 | t, err = time.Date(y, mo, d, h, mi, s, t.Nanosecond(), loc), nil
85 | }
86 |
87 | return
88 | }
89 |
90 | func (t Time) MarshalBinary() ([]byte, error) {
91 | return t.Time.MarshalBinary()
92 | }
93 |
94 | func (t *Time) UnmarshalBinary(data []byte) error {
95 | return t.Time.UnmarshalBinary(data)
96 | }
97 |
98 | // IsZero returns true for null strings (omitempty support)
99 | func (t Time) IsZero() bool {
100 | return t.Time.IsZero()
101 | }
102 |
--------------------------------------------------------------------------------
/dao/clickhouse/clickhouse.go:
--------------------------------------------------------------------------------
1 | package clickhouse
2 |
3 | import (
4 | "database/sql"
5 | "fmt"
6 | "github.com/Masterminds/squirrel"
7 | "github.com/everstake/cosmoscan-api/config"
8 | "github.com/golang-migrate/migrate/v4"
9 | goclickhouse "github.com/golang-migrate/migrate/v4/database/clickhouse"
10 | _ "github.com/golang-migrate/migrate/v4/source/file"
11 | "github.com/jmoiron/sqlx"
12 | _ "github.com/mailru/go-clickhouse"
13 | "strings"
14 | )
15 |
16 | const migrationsPath = "./dao/clickhouse/migrations"
17 |
18 | type DB struct {
19 | conn *sqlx.DB
20 | }
21 |
22 | func NewDB(cfg config.Clickhouse) (*DB, error) {
23 | conn, err := sql.Open("clickhouse", makeSource(cfg))
24 | if err != nil {
25 | return nil, fmt.Errorf("can`t make connection: %s", err.Error())
26 | }
27 | //err = makeMigration(conn, migrationsPath, cfg.Database)
28 | //if err != nil {
29 | // return nil, fmt.Errorf("can`t make makeMigration: %s", err.Error())
30 | //}
31 | return &DB{
32 | conn: sqlx.NewDb(conn, "clickhouse"),
33 | }, nil
34 | }
35 |
36 | func (db *DB) Find(dest interface{}, b squirrel.SelectBuilder) error {
37 | q, params, err := b.ToSql()
38 | if err != nil {
39 | return err
40 | }
41 | err = db.conn.Select(dest, q, params...)
42 | if err == sql.ErrNoRows {
43 | return nil
44 | }
45 | if err != nil {
46 | return err
47 | }
48 | return nil
49 | }
50 |
51 | func (db *DB) FindFirst(dest interface{}, b squirrel.SelectBuilder) error {
52 | q, params, err := b.ToSql()
53 | if err != nil {
54 | return err
55 | }
56 | err = db.conn.Get(dest, q, params...)
57 | if err != nil {
58 | return err
59 | }
60 | return nil
61 | }
62 |
63 | func (db *DB) Insert(b squirrel.InsertBuilder) error {
64 | q, params, err := b.ToSql()
65 | if err != nil {
66 | return err
67 | }
68 | _, err = db.conn.Exec(q, params...)
69 | if err != nil {
70 | return err
71 | }
72 | return nil
73 | }
74 |
75 | func makeSource(cfg config.Clickhouse) string {
76 | return fmt.Sprintf("%s://%s:%d/%s?password=%s&user=%s",
77 | strings.Trim(cfg.Protocol, "://"),
78 | strings.Trim(cfg.Host, "/"),
79 | cfg.Port,
80 | cfg.Database,
81 | cfg.Password,
82 | cfg.User,
83 | )
84 | }
85 |
86 | func makeMigration(conn *sql.DB, migrationDir string, dbName string) error {
87 | driver, err := goclickhouse.WithInstance(conn, &goclickhouse.Config{})
88 | if err != nil {
89 | return fmt.Errorf("clickhouse.WithInstance: %s", err.Error())
90 | }
91 | mg, err := migrate.NewWithDatabaseInstance(
92 | fmt.Sprintf("file://%s", migrationDir),
93 | dbName, driver)
94 | if err != nil {
95 | return fmt.Errorf("migrate.NewWithDatabaseInstance: %s", err.Error())
96 | }
97 | if err := mg.Up(); err != nil {
98 | if err != migrate.ErrNoChange {
99 | return err
100 | }
101 | }
102 | return nil
103 | }
104 |
--------------------------------------------------------------------------------
/services/accounts.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/everstake/cosmoscan-api/dao/filters"
7 | "github.com/everstake/cosmoscan-api/dmodels"
8 | "github.com/everstake/cosmoscan-api/log"
9 | "github.com/everstake/cosmoscan-api/smodels"
10 | "time"
11 | )
12 |
13 | func (s *ServiceFacade) MakeUpdateBalances() {
14 | tn := time.Now()
15 | accounts, err := s.dao.GetAccounts(filters.Accounts{})
16 | if err != nil {
17 | log.Error("MakeUpdateBalances: dao.GetAccounts: %s", err.Error())
18 | return
19 | }
20 | ctx, cancel := context.WithCancel(context.Background())
21 | defer cancel()
22 | fetchers := 5
23 | accountsCh := make(chan dmodels.Account)
24 | for i := 0; i < fetchers; i++ {
25 | go func() {
26 | for {
27 | select {
28 | case acc := <-accountsCh:
29 | for {
30 | err := s.updateAccount(acc)
31 | if err != nil {
32 | log.Warn("MakeSmartUpdateBalances: updateAccount: %s", err.Error())
33 | time.After(time.Second * 2)
34 | continue
35 | }
36 | break
37 | }
38 | case <-ctx.Done():
39 | return
40 | }
41 | }
42 | }()
43 | }
44 | for _, acc := range accounts {
45 | accountsCh <- acc
46 | }
47 | <-time.After(time.Second * 5)
48 | log.Info("MakeUpdateBalances finished, duration: %s", time.Now().Sub(tn))
49 | }
50 |
51 | func (s *ServiceFacade) updateAccount(account dmodels.Account) error {
52 | balance, err := s.node.GetBalance(account.Address)
53 | if err != nil {
54 | return fmt.Errorf("node.GetBalance: %s", err.Error())
55 | }
56 | stake, err := s.node.GetStake(account.Address)
57 | if err != nil {
58 | return fmt.Errorf("node.GetStake: %s", err.Error())
59 | }
60 | if balance.Equal(account.Balance) && stake.Equal(account.Stake) {
61 | return nil
62 | }
63 | unbonding, err := s.node.GetUnbonding(account.Address)
64 | if err != nil {
65 | return fmt.Errorf("node.GetUnbonding: %s", err.Error())
66 | }
67 | account.Balance = balance
68 | account.Stake = stake
69 | account.Unbonding = unbonding
70 | err = s.dao.UpdateAccount(account)
71 | if err != nil {
72 | return fmt.Errorf("dao.UpdateAccount: %s", err.Error())
73 | }
74 | return nil
75 | }
76 |
77 | func (s *ServiceFacade) GetAccount(address string) (account smodels.Account, err error) {
78 | balance, err := s.node.GetBalance(address)
79 | if err != nil {
80 | return account, fmt.Errorf("node.GetBalance: %s", err.Error())
81 | }
82 | stake, err := s.node.GetStake(address)
83 | if err != nil {
84 | return account, fmt.Errorf("node.GetStake: %s", err.Error())
85 | }
86 | unbonding, err := s.node.GetUnbonding(address)
87 | if err != nil {
88 | return account, fmt.Errorf("node.GetUnbonding: %s", err.Error())
89 | }
90 | rewards, err := s.node.GetStakeRewards(address)
91 | if err != nil {
92 | return account, fmt.Errorf("node.GetStakeRewards: %s", err.Error())
93 | }
94 | return smodels.Account{
95 | Address: address,
96 | Balance: balance,
97 | Delegated: stake,
98 | Unbonding: unbonding,
99 | StakeReward: rewards,
100 | }, nil
101 | }
102 |
--------------------------------------------------------------------------------
/api/delegations.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/everstake/cosmoscan-api/dao/filters"
5 | "github.com/everstake/cosmoscan-api/log"
6 | "github.com/gorilla/mux"
7 | "net/http"
8 | )
9 |
10 | func (api *API) GetAggDelegationsVolume(w http.ResponseWriter, r *http.Request) {
11 | var filter filters.DelegationsAgg
12 | err := api.queryDecoder.Decode(&filter, r.URL.Query())
13 | if err != nil {
14 | log.Debug("API Decode: %s", err.Error())
15 | jsonBadRequest(w, "")
16 | return
17 | }
18 | resp, err := api.svc.GetAggDelegationsVolume(filter)
19 | if err != nil {
20 | log.Error("API GetAggDelegationsVolume: svc.GetAggDelegationsVolume: %s", err.Error())
21 | jsonError(w)
22 | return
23 | }
24 | jsonData(w, resp)
25 | }
26 |
27 | func (api *API) GetAggUndelegationsVolume(w http.ResponseWriter, r *http.Request) {
28 | api.aggHandler(w, r, api.svc.GetAggUndelegationsVolume)
29 | }
30 |
31 | func (api *API) GetAggUnbondingVolume(w http.ResponseWriter, r *http.Request) {
32 | api.aggHandler(w, r, api.svc.GetAggUnbondingVolume)
33 | }
34 |
35 | func (api *API) GetStakingPie(w http.ResponseWriter, r *http.Request) {
36 | resp, err := api.svc.GetStakingPie()
37 | if err != nil {
38 | log.Error("API GetStakingPie: svc.GetStakingPie: %s", err.Error())
39 | jsonError(w)
40 | return
41 | }
42 | jsonData(w, resp)
43 | }
44 |
45 | func (api *API) GetValidatorDelegationsAgg(w http.ResponseWriter, r *http.Request) {
46 | address, ok := mux.Vars(r)["address"]
47 | if !ok || address == "" {
48 | jsonBadRequest(w, "invalid address")
49 | return
50 | }
51 | resp, err := api.svc.GetValidatorDelegationsAgg(address)
52 | if err != nil {
53 | log.Error("API GetValidatorDelegationsAgg: svc.GetValidatorDelegationsAgg: %s", err.Error())
54 | jsonError(w)
55 | return
56 | }
57 | jsonData(w, resp)
58 | }
59 |
60 | func (api *API) GetValidatorDelegatorsAgg(w http.ResponseWriter, r *http.Request) {
61 | address, ok := mux.Vars(r)["address"]
62 | if !ok || address == "" {
63 | jsonBadRequest(w, "invalid address")
64 | return
65 | }
66 | resp, err := api.svc.GetValidatorDelegatorsAgg(address)
67 | if err != nil {
68 | log.Error("API GetValidatorDelegatorsAgg: svc.GetValidatorDelegatorsAgg: %s", err.Error())
69 | jsonError(w)
70 | return
71 | }
72 | jsonData(w, resp)
73 | }
74 |
75 | func (api *API) GetValidatorDelegators(w http.ResponseWriter, r *http.Request) {
76 | address, ok := mux.Vars(r)["address"]
77 | if !ok || address == "" {
78 | jsonBadRequest(w, "invalid address")
79 | return
80 | }
81 | var filter filters.ValidatorDelegators
82 | err := api.queryDecoder.Decode(&filter, r.URL.Query())
83 | if err != nil {
84 | log.Debug("API Decode: %s", err.Error())
85 | jsonBadRequest(w, "")
86 | return
87 | }
88 | if filter.Limit > 20 || filter.Limit == 0 {
89 | filter.Limit = 20
90 | }
91 | filter.Validator = address
92 | resp, err := api.svc.GetValidatorDelegators(filter)
93 | if err != nil {
94 | log.Error("API GetValidatorDelegators: svc.GetValidatorDelegators: %s", err.Error())
95 | jsonError(w)
96 | return
97 | }
98 | jsonData(w, resp)
99 | }
100 |
--------------------------------------------------------------------------------
/api/validators.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/everstake/cosmoscan-api/log"
5 | "github.com/gorilla/mux"
6 | "net/http"
7 | )
8 |
9 | func (api *API) GetTopProposedBlocksValidators(w http.ResponseWriter, r *http.Request) {
10 | resp, err := api.svc.GetTopProposedBlocksValidators()
11 | if err != nil {
12 | log.Error("API GetTopProposedBlocksValidators: svc.GetTopProposedBlocksValidators: %s", err.Error())
13 | jsonError(w)
14 | return
15 | }
16 | jsonData(w, resp)
17 |
18 | }
19 |
20 | func (api *API) GetMostJailedValidators(w http.ResponseWriter, r *http.Request) {
21 | resp, err := api.svc.GetMostJailedValidators()
22 | if err != nil {
23 | log.Error("API GetMostJailedValidators: svc.GetMostJailedValidators: %s", err.Error())
24 | jsonError(w)
25 | return
26 | }
27 | jsonData(w, resp)
28 |
29 | }
30 |
31 | func (api *API) GetFeeRanges(w http.ResponseWriter, r *http.Request) {
32 | resp, err := api.svc.GetFeeRanges()
33 | if err != nil {
34 | log.Error("API GetFeeRanges: svc.GetFeeRanges: %s", err.Error())
35 | jsonError(w)
36 | return
37 | }
38 | jsonData(w, resp)
39 |
40 | }
41 |
42 | func (api *API) GetValidators(w http.ResponseWriter, r *http.Request) {
43 | resp, err := api.svc.GetValidators()
44 | if err != nil {
45 | log.Error("API GetValidators: svc.GetValidators: %s", err.Error())
46 | jsonError(w)
47 | return
48 | }
49 | jsonData(w, resp)
50 |
51 | }
52 |
53 | func (api *API) GetValidatorsDelegatorsTotal(w http.ResponseWriter, r *http.Request) {
54 | resp, err := api.svc.GetValidatorsDelegatorsTotal()
55 | if err != nil {
56 | log.Error("API GetValidatorsDelegatorsTotal: svc.GetValidatorsDelegatorsTotal: %s", err.Error())
57 | jsonError(w)
58 | return
59 | }
60 | jsonData(w, resp)
61 | }
62 |
63 | func (api *API) GetValidator(w http.ResponseWriter, r *http.Request) {
64 | address, ok := mux.Vars(r)["address"]
65 | if !ok || address == "" {
66 | jsonBadRequest(w, "invalid address")
67 | return
68 | }
69 | resp, err := api.svc.GetValidator(address)
70 | if err != nil {
71 | log.Error("API GetValidator: svc.GetValidator: %s", err.Error())
72 | jsonError(w)
73 | return
74 | }
75 | jsonData(w, resp)
76 | }
77 |
78 | func (api *API) GetValidatorBalance(w http.ResponseWriter, r *http.Request) {
79 | address, ok := mux.Vars(r)["address"]
80 | if !ok || address == "" {
81 | jsonBadRequest(w, "invalid address")
82 | return
83 | }
84 | resp, err := api.svc.GetValidatorBalance(address)
85 | if err != nil {
86 | log.Error("API GetValidatorBalance: svc.GetValidatorBalance: %s", err.Error())
87 | jsonError(w)
88 | return
89 | }
90 | jsonData(w, resp)
91 | }
92 |
93 | func (api *API) GetValidatorBlocksStat(w http.ResponseWriter, r *http.Request) {
94 | address, ok := mux.Vars(r)["address"]
95 | if !ok || address == "" {
96 | jsonBadRequest(w, "invalid address")
97 | return
98 | }
99 | resp, err := api.svc.GetValidatorBlocksStat(address)
100 | if err != nil {
101 | log.Error("API GetValidatorBlocksStat: svc.GetValidatorBlocksStat: %s", err.Error())
102 | jsonError(w)
103 | return
104 | }
105 | jsonData(w, resp)
106 | }
107 |
--------------------------------------------------------------------------------
/dao/mysql/proposals.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "fmt"
5 | "github.com/Masterminds/squirrel"
6 | "github.com/everstake/cosmoscan-api/dao/filters"
7 | "github.com/everstake/cosmoscan-api/dmodels"
8 | )
9 |
10 | func (m DB) CreateProposals(proposals []dmodels.Proposal) error {
11 | if len(proposals) == 0 {
12 | return nil
13 | }
14 | q := squirrel.Insert(dmodels.ProposalsTable).Columns(
15 | "pro_id",
16 | "pro_tx_hash",
17 | "pro_proposer",
18 | "pro_proposer_address",
19 | "pro_type",
20 | "pro_title",
21 | "pro_description",
22 | "pro_status",
23 | "pro_votes_yes",
24 | "pro_votes_abstain",
25 | "pro_votes_no",
26 | "pro_votes_no_with_veto",
27 | "pro_submit_time",
28 | "pro_deposit_end_time",
29 | "pro_total_deposits",
30 | "pro_voting_start_time",
31 | "pro_voting_end_time",
32 | "pro_voters",
33 | "pro_participation_rate",
34 | "pro_turnout",
35 | "pro_activity",
36 | )
37 | for _, p := range proposals {
38 | if p.ID == 0 {
39 | return fmt.Errorf("invalid ProposalID")
40 | }
41 |
42 | q = q.Values(
43 | p.ID,
44 | p.TxHash,
45 | p.Proposer,
46 | p.ProposerAddress,
47 | p.Type,
48 | p.Title,
49 | p.Description,
50 | p.Status,
51 | p.VotesYes,
52 | p.VotesAbstain,
53 | p.VotesNo,
54 | p.VotesNoWithVeto,
55 | p.SubmitTime,
56 | p.DepositEndTime,
57 | p.TotalDeposits,
58 | p.VotingStartTime.Time,
59 | p.VotingEndTime.Time,
60 | p.Voters,
61 | p.ParticipationRate,
62 | p.Turnout,
63 | p.Activity,
64 | )
65 | }
66 | _, err := m.insert(q)
67 | return err
68 | }
69 |
70 | func (m DB) GetProposals(filter filters.Proposals) (proposals []dmodels.Proposal, err error) {
71 | q := squirrel.Select("*").From(dmodels.ProposalsTable).OrderBy("pro_id desc")
72 | if len(filter.ID) != 0 {
73 | q = q.Where(squirrel.Eq{"pro_id": filter.ID})
74 | }
75 | if filter.Limit != 0 {
76 | q = q.Limit(filter.Limit)
77 | }
78 | if filter.Offset != 0 {
79 | q = q.Limit(filter.Offset)
80 | }
81 | err = m.find(&proposals, q)
82 | return proposals, err
83 | }
84 |
85 | func (m DB) UpdateProposal(proposal dmodels.Proposal) error {
86 | mp := map[string]interface{}{
87 | "pro_proposer": proposal.Proposer,
88 | "pro_proposer_address": proposal.ProposerAddress,
89 | "pro_tx_hash": proposal.TxHash,
90 | "pro_type": proposal.Type,
91 | "pro_title": proposal.Title,
92 | "pro_description": proposal.Description,
93 | "pro_status": proposal.Status,
94 | "pro_votes_yes": proposal.VotesYes,
95 | "pro_votes_abstain": proposal.VotesAbstain,
96 | "pro_votes_no": proposal.VotesNo,
97 | "pro_votes_no_with_veto": proposal.VotesNoWithVeto,
98 | "pro_submit_time": proposal.SubmitTime,
99 | "pro_deposit_end_time": proposal.DepositEndTime,
100 | "pro_voting_start_time": proposal.VotingStartTime,
101 | "pro_voting_end_time": proposal.VotingEndTime,
102 | "pro_total_deposits": proposal.TotalDeposits,
103 | "pro_voters": proposal.Voters,
104 | "pro_participation_rate": proposal.ParticipationRate,
105 | "pro_turnout": proposal.Turnout,
106 | "pro_activity": proposal.Activity,
107 | }
108 | q := squirrel.Update(dmodels.ProposalsTable).
109 | Where(squirrel.Eq{"pro_id": proposal.ID}).
110 | SetMap(mp)
111 | return m.update(q)
112 | }
113 |
--------------------------------------------------------------------------------
/services/transactions.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/everstake/cosmoscan-api/dao/filters"
7 | "github.com/everstake/cosmoscan-api/dmodels"
8 | "github.com/everstake/cosmoscan-api/log"
9 | "github.com/everstake/cosmoscan-api/services/node"
10 | "github.com/everstake/cosmoscan-api/smodels"
11 | "github.com/shopspring/decimal"
12 | "strings"
13 | )
14 |
15 | func (s *ServiceFacade) GetAggTransactionsFee(filter filters.Agg) (items []smodels.AggItem, err error) {
16 | items, err = s.dao.GetAggTransactionsFee(filter)
17 | if err != nil {
18 | return nil, fmt.Errorf("dao.GetAggTransactionsFee: %s", err.Error())
19 | }
20 | return items, nil
21 | }
22 |
23 | func (s *ServiceFacade) GetAggOperationsCount(filter filters.Agg) (items []smodels.AggItem, err error) {
24 | items, err = s.dao.GetAggOperationsCount(filter)
25 | if err != nil {
26 | return nil, fmt.Errorf("dao.GetAggOperationsCount: %s", err.Error())
27 | }
28 | return items, nil
29 | }
30 |
31 | func (s *ServiceFacade) GetAvgOperationsPerBlock(filter filters.Agg) (items []smodels.AggItem, err error) {
32 | items, err = s.dao.GetAvgOperationsPerBlock(filter)
33 | if err != nil {
34 | return nil, fmt.Errorf("dao.GetAvgOperationsPerBlock: %s", err.Error())
35 | }
36 | return items, nil
37 | }
38 |
39 | type baseMsg struct {
40 | Type string `json:"@type"`
41 | }
42 |
43 | func (s *ServiceFacade) GetTransaction(hash string) (tx smodels.Tx, err error) {
44 | dTx, err := s.node.GetTransaction(hash)
45 | if err != nil {
46 | return tx, fmt.Errorf("node.GetTransaction: %s", err.Error())
47 | }
48 | var fee decimal.Decimal
49 | for _, a := range dTx.Tx.AuthInfo.Fee.Amount {
50 | if a.Denom == node.MainUnit {
51 | fee = fee.Add(a.Amount)
52 | }
53 | }
54 | var msgs []smodels.Message
55 | for _, m := range dTx.Tx.Body.Messages {
56 | var bm baseMsg
57 | err = json.Unmarshal(m, &bm)
58 | if err != nil {
59 | log.Warn("GetTransaction: parse baseMsg: %s", err.Error())
60 | continue
61 | }
62 | parts := strings.Split(bm.Type, ".")
63 | t := strings.Trim(parts[len(parts)-1], "Msg")
64 | msgs = append(msgs, smodels.Message{Type: t, Body: m})
65 | }
66 | success := dTx.TxResponse.Code == 0
67 | fee = node.Precision(fee)
68 | return smodels.Tx{
69 | Hash: dTx.TxResponse.Txhash,
70 | Type: dTx.Tx.Type,
71 | Status: success,
72 | Fee: fee,
73 | Height: dTx.TxResponse.Height,
74 | GasUsed: dTx.TxResponse.GasUsed,
75 | GasWanted: dTx.TxResponse.GasWanted,
76 | Memo: dTx.Tx.Body.Memo,
77 | CreatedAt: dmodels.NewTime(dTx.TxResponse.Timestamp),
78 | Messages: msgs,
79 | }, nil
80 | }
81 |
82 | func (s *ServiceFacade) GetTransactions(filter filters.Transactions) (resp smodels.PaginatableResponse, err error) {
83 | dTxs, err := s.dao.GetTransactions(filter)
84 | if err != nil {
85 | return resp, fmt.Errorf("dao.GetTransactions: %s", err.Error())
86 | }
87 | total, err := s.dao.GetTransactionsCount(filter)
88 | if err != nil {
89 | return resp, fmt.Errorf("dao.GetTransactionsCount: %s", err.Error())
90 | }
91 | var txs []smodels.TxItem
92 | for _, tx := range dTxs {
93 | txs = append(txs, smodels.TxItem{
94 | Hash: tx.Hash,
95 | Status: tx.Status,
96 | Fee: tx.Fee,
97 | Height: tx.Height,
98 | Messages: tx.Messages,
99 | CreatedAt: dmodels.NewTime(tx.CreatedAt),
100 | })
101 | }
102 | return smodels.PaginatableResponse{
103 | Items: txs,
104 | Total: total,
105 | }, nil
106 | }
107 |
--------------------------------------------------------------------------------
/services/delegations.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "fmt"
5 | "github.com/everstake/cosmoscan-api/dao/filters"
6 | "github.com/everstake/cosmoscan-api/dmodels"
7 | "github.com/everstake/cosmoscan-api/smodels"
8 | "github.com/shopspring/decimal"
9 | "time"
10 | )
11 |
12 | func (s *ServiceFacade) GetAggDelegationsVolume(filter filters.DelegationsAgg) (items []smodels.AggItem, err error) {
13 | items, err = s.dao.GetAggDelegationsVolume(filter)
14 | if err != nil {
15 | return nil, fmt.Errorf("dao.GetAggDelegationsVolume: %s", err.Error())
16 | }
17 | return items, nil
18 | }
19 |
20 | func (s *ServiceFacade) GetAggUndelegationsVolume(filter filters.Agg) (items []smodels.AggItem, err error) {
21 | items, err = s.dao.GetAggUndelegationsVolume(filter)
22 | if err != nil {
23 | return nil, fmt.Errorf("dao.GetAggUndelegationsVolume: %s", err.Error())
24 | }
25 | return items, nil
26 | }
27 |
28 | func (s *ServiceFacade) GetAggUnbondingVolume(filter filters.Agg) (items []smodels.AggItem, err error) {
29 | undelegationItems, err := s.dao.GetAggUndelegationsVolume(filter)
30 | if err != nil {
31 | return nil, fmt.Errorf("dao.GetAggUndelegationsVolume: %s", err.Error())
32 | }
33 | items = make([]smodels.AggItem, len(undelegationItems))
34 | for i, item := range undelegationItems {
35 | total, err := s.dao.GetUndelegationsVolume(filters.TimeRange{
36 | From: dmodels.NewTime(item.Time.Add(-time.Hour * 24 * 21)),
37 | To: item.Time,
38 | })
39 | if err != nil {
40 | return nil, fmt.Errorf("dao.GetUndelegationsVolume: %s", err.Error())
41 | }
42 | items[i] = smodels.AggItem{
43 | Time: item.Time,
44 | Value: total,
45 | }
46 | }
47 | return items, nil
48 | }
49 |
50 | func (s *ServiceFacade) GetValidatorDelegationsAgg(validatorAddress string) (items []smodels.AggItem, err error) {
51 | validator, err := s.GetValidator(validatorAddress)
52 | if err != nil {
53 | return nil, fmt.Errorf("GetValidator: %s", err.Error())
54 | }
55 | items, err = s.dao.GetAggDelegationsAndUndelegationsVolume(filters.DelegationsAgg{
56 | Agg: filters.Agg{
57 | By: filters.AggByDay,
58 | From: dmodels.NewTime(time.Now().Add(-time.Hour * 24 * 30)),
59 | To: dmodels.NewTime(time.Now()),
60 | },
61 | Validators: []string{validatorAddress},
62 | })
63 | if err != nil {
64 | return nil, fmt.Errorf("dao.GetAggDelegationsVolume: %s", err.Error())
65 | }
66 | powerValue := validator.Power
67 | for i := len(items) - 1; i >= 0; i-- {
68 | v := items[i].Value
69 | items[i].Value = powerValue
70 | powerValue = items[i].Value.Sub(v)
71 | }
72 | return items, nil
73 | }
74 |
75 | func (s *ServiceFacade) GetValidatorDelegatorsAgg(validatorAddress string) (items []smodels.AggItem, err error) {
76 | for i := 29; i >= 0; i-- {
77 | y, m, d := time.Now().Add(-time.Hour * 24 * time.Duration(i)).Date()
78 | date := time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
79 | total, err := s.dao.GetDelegatorsTotal(filters.Delegators{
80 | TimeRange: filters.TimeRange{
81 | To: dmodels.NewTime(date),
82 | },
83 | Validators: []string{validatorAddress},
84 | })
85 | if err != nil {
86 | return nil, fmt.Errorf("dao.GetDelegatorsTotal: %s", err.Error())
87 | }
88 | items = append(items, smodels.AggItem{
89 | Time: dmodels.NewTime(date),
90 | Value: decimal.NewFromInt(int64(total)),
91 | })
92 | }
93 | return items, nil
94 | }
95 |
96 | func (s *ServiceFacade) GetValidatorDelegators(filter filters.ValidatorDelegators) (resp smodels.PaginatableResponse, err error) {
97 | items, err := s.dao.GetValidatorDelegators(filter)
98 | if err != nil {
99 | return resp, fmt.Errorf("dao.GetValidatorDelegators: %s", err.Error())
100 | }
101 | total, err := s.dao.GetValidatorDelegatorsTotal(filter)
102 | if err != nil {
103 | return resp, fmt.Errorf("dao.GetValidatorDelegatorsTotal: %s", err.Error())
104 | }
105 | return smodels.PaginatableResponse{
106 | Items: items,
107 | Total: total,
108 | }, nil
109 | }
110 |
--------------------------------------------------------------------------------
/dao/mysql/main.go:
--------------------------------------------------------------------------------
1 | package mysql
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "github.com/Masterminds/squirrel"
7 | "github.com/everstake/cosmoscan-api/config"
8 | "github.com/everstake/cosmoscan-api/dao/derrors"
9 | "github.com/everstake/cosmoscan-api/log"
10 | "github.com/go-sql-driver/mysql"
11 | _ "github.com/go-sql-driver/mysql"
12 | "github.com/jmoiron/sqlx"
13 | migrate "github.com/rubenv/sql-migrate"
14 | "os"
15 | "path/filepath"
16 | "time"
17 | )
18 |
19 | const migrationsDir = "./dao/mysql/migrations"
20 |
21 | type DB struct {
22 | config config.Mysql
23 | db *sqlx.DB
24 | }
25 |
26 | func NewDB(cfg config.Mysql) (*DB, error) {
27 | m := &DB{
28 | config: cfg,
29 | }
30 | m.tryOpenConnection()
31 | err := m.migrate()
32 | if err != nil {
33 | return nil, err
34 | }
35 | return m, nil
36 | }
37 |
38 | func (m *DB) tryOpenConnection() {
39 | for {
40 | err := m.openConnection()
41 | if err != nil {
42 | log.Error("cant open connection to mysql: %s", err.Error())
43 | } else {
44 | log.Info("mysql connection success")
45 | return
46 | }
47 | time.Sleep(time.Second)
48 | }
49 | }
50 |
51 | func (m *DB) openConnection() error {
52 | source := fmt.Sprintf(
53 | "%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&multiStatements=true&parseTime=true",
54 | m.config.User,
55 | m.config.Password,
56 | m.config.Host,
57 | m.config.Port,
58 | m.config.DB,
59 | )
60 | var err error
61 | m.db, err = sqlx.Connect("mysql", source)
62 | if err != nil {
63 | return err
64 | }
65 | err = m.db.Ping()
66 | if err != nil {
67 | return err
68 | }
69 | return nil
70 | }
71 |
72 | func (m DB) find(dest interface{}, sb squirrel.SelectBuilder) error {
73 | sql, args, err := sb.ToSql()
74 | if err != nil {
75 | return err
76 | }
77 | err = m.db.Select(dest, sql, args...)
78 | if err != nil {
79 | return err
80 | }
81 | return nil
82 | }
83 |
84 | func (m DB) first(dest interface{}, sb squirrel.SelectBuilder) error {
85 | sql, args, err := sb.ToSql()
86 | if err != nil {
87 | return err
88 | }
89 | err = m.db.Get(dest, sql, args...)
90 | if err != nil {
91 | if err.Error() == "sql: no rows in result set" {
92 | return errors.New(derrors.ErrNotFound)
93 | }
94 | return err
95 | }
96 | return nil
97 | }
98 |
99 | func (m DB) insert(sb squirrel.InsertBuilder) (id uint64, err error) {
100 | sql, args, err := sb.ToSql()
101 | if err != nil {
102 | return id, err
103 | }
104 | result, err := m.db.Exec(sql, args...)
105 | if err != nil {
106 | mErr, ok := err.(*mysql.MySQLError)
107 | if ok && mErr.Number == 1062 {
108 | return 0, errors.New(derrors.ErrDuplicate)
109 | }
110 | return id, err
111 | }
112 | lastID, err := result.LastInsertId()
113 | if err != nil {
114 | return id, err
115 | }
116 | return uint64(lastID), nil
117 | }
118 |
119 | func (m DB) update(sb squirrel.UpdateBuilder) (err error) {
120 | sql, args, err := sb.ToSql()
121 | if err != nil {
122 | return err
123 | }
124 | _, err = m.db.Exec(sql, args...)
125 | if err != nil {
126 | return err
127 | }
128 | return nil
129 | }
130 |
131 | func (m DB) migrate() error {
132 | ex, err := os.Executable()
133 | if err != nil {
134 | return err
135 | }
136 | dir := filepath.Join(filepath.Dir(ex), migrationsDir)
137 | if _, err := os.Stat(dir); os.IsNotExist(err) {
138 | dir = migrationsDir
139 | if _, err := os.Stat(migrationsDir); os.IsNotExist(err) {
140 | return errors.New("Migrations dir does not exist: " + dir)
141 | }
142 | }
143 | migrations := &migrate.FileMigrationSource{
144 | Dir: dir,
145 | }
146 | _, err = migrate.Exec(m.db.DB, "mysql", migrations, migrate.Up)
147 | return err
148 | }
149 |
150 | func field(table string, column string, alias ...string) string {
151 | s := fmt.Sprintf("%s.%s", table, column)
152 | if len(alias) == 1 {
153 | return fmt.Sprintf("%s as %s", s, alias)
154 | }
155 | return s
156 | }
157 |
158 | func joiner(rightTable string, leftTable string, field string) string {
159 | return fmt.Sprintf("%s ON %s.%s = %s.%s", rightTable, leftTable, field, rightTable, field)
160 | }
161 |
--------------------------------------------------------------------------------
/dao/clickhouse/transactions.go:
--------------------------------------------------------------------------------
1 | package clickhouse
2 |
3 | import (
4 | "fmt"
5 | "github.com/Masterminds/squirrel"
6 | "github.com/everstake/cosmoscan-api/dao/filters"
7 | "github.com/everstake/cosmoscan-api/dmodels"
8 | "github.com/everstake/cosmoscan-api/smodels"
9 | "github.com/shopspring/decimal"
10 | )
11 |
12 | func (db DB) CreateTransactions(transactions []dmodels.Transaction) error {
13 | if len(transactions) == 0 {
14 | return nil
15 | }
16 | q := squirrel.Insert(dmodels.TransactionsTable).Columns(
17 | "trn_hash",
18 | "trn_status",
19 | "trn_height",
20 | "trn_messages",
21 | "trn_fee",
22 | "trn_gas_used",
23 | "trn_gas_wanted",
24 | "trn_created_at",
25 | )
26 | for _, tx := range transactions {
27 | if tx.Hash == "" {
28 | return fmt.Errorf("field Hash can not be empty")
29 | }
30 | if tx.Height == 0 {
31 | return fmt.Errorf("field Height can not be 0")
32 | }
33 | if tx.CreatedAt.IsZero() {
34 | return fmt.Errorf("field CreatedAt can not be zero")
35 | }
36 | q = q.Values(
37 | tx.Hash,
38 | tx.Status,
39 | tx.Height,
40 | tx.Messages,
41 | tx.Fee,
42 | tx.GasUsed,
43 | tx.GasWanted,
44 | tx.CreatedAt,
45 | )
46 | }
47 | return db.Insert(q)
48 | }
49 |
50 | func (db DB) GetAggTransactionsFee(filter filters.Agg) (items []smodels.AggItem, err error) {
51 | q := filter.BuildQuery("sum(trn_fee)", "trn_created_at", dmodels.TransactionsTable)
52 | err = db.Find(&items, q)
53 | return items, err
54 | }
55 |
56 | func (db DB) GetAggOperationsCount(filter filters.Agg) (items []smodels.AggItem, err error) {
57 | q := filter.BuildQuery("toDecimal64(sum(trn_messages), 0)", "trn_created_at", dmodels.TransactionsTable)
58 | err = db.Find(&items, q)
59 | return items, err
60 | }
61 |
62 | func (db DB) GetTransactionsFeeVolume(filter filters.TimeRange) (total decimal.Decimal, err error) {
63 | q := squirrel.Select("sum(trn_fee) as total").From(dmodels.TransactionsTable)
64 | q = filter.Query("trn_created_at", q)
65 | err = db.FindFirst(&total, q)
66 | return total, err
67 | }
68 |
69 | func (db DB) GetTransactionsHighestFee(filter filters.TimeRange) (total decimal.Decimal, err error) {
70 | q := squirrel.Select("max(trn_fee) as total").From(dmodels.TransactionsTable)
71 | q = filter.Query("trn_created_at", q)
72 | err = db.FindFirst(&total, q)
73 | return total, err
74 | }
75 |
76 | func (db DB) GetAvgOperationsPerBlock(filter filters.Agg) (items []smodels.AggItem, err error) {
77 | // approximate number of blocks by `period`
78 | blocks := 12000
79 | switch filter.By {
80 | case filters.AggByHour:
81 | blocks = 500
82 | case filters.AggByWeek:
83 | blocks = 84000
84 | case filters.AggByMonth:
85 | blocks = 360000
86 | }
87 | aggValue := fmt.Sprintf("toDecimal64(sum(trn_messages) / %d, 4)", blocks)
88 | q := filter.BuildQuery(aggValue, "trn_created_at", dmodels.TransactionsTable)
89 | err = db.Find(&items, q)
90 | return items, err
91 | }
92 |
93 | func (db DB) GetTransactions(filter filters.Transactions) (items []dmodels.Transaction, err error) {
94 | q := squirrel.Select("transactions.*").From(dmodels.TransactionsTable).OrderBy("transactions.trn_created_at desc")
95 | if filter.Height != 0 {
96 | q = q.Where(squirrel.Eq{"transactions.trn_height": filter.Height})
97 | }
98 | if filter.Address != "" {
99 | q = q.LeftJoin(fmt.Sprintf("account_txs ON account_txs.atx_tx_hash = transactions.trn_hash")).
100 | Where(squirrel.Eq{"account_txs.atx_account": filter.Address})
101 | }
102 | if filter.Limit != 0 {
103 | q = q.Limit(filter.Limit)
104 | }
105 | if filter.Offset != 0 {
106 | q = q.Offset(filter.Offset)
107 | }
108 | err = db.Find(&items, q)
109 | return items, err
110 | }
111 |
112 | func (db DB) GetTransactionsCount(filter filters.Transactions) (total uint64, err error) {
113 | q := squirrel.Select("count(*)").From(dmodels.TransactionsTable)
114 | if filter.Height != 0 {
115 | q = q.Where(squirrel.Eq{"transactions.trn_height": filter.Height})
116 | }
117 | if filter.Address != "" {
118 | q = q.LeftJoin(fmt.Sprintf("account_txs ON account_txs.atx_tx_hash = transactions.trn_hash")).
119 | Where(squirrel.Eq{"account_txs.atx_account": filter.Address})
120 | }
121 | err = db.FindFirst(&total, q)
122 | return total, err
123 | }
124 |
--------------------------------------------------------------------------------
/dao/clickhouse/blocks.go:
--------------------------------------------------------------------------------
1 | package clickhouse
2 |
3 | import (
4 | "fmt"
5 | "github.com/Masterminds/squirrel"
6 | "github.com/everstake/cosmoscan-api/dao/filters"
7 | "github.com/everstake/cosmoscan-api/dmodels"
8 | "github.com/everstake/cosmoscan-api/smodels"
9 | )
10 |
11 | func (db DB) CreateBlocks(blocks []dmodels.Block) error {
12 | if len(blocks) == 0 {
13 | return nil
14 | }
15 | q := squirrel.Insert(dmodels.BlocksTable).Columns("blk_id", "blk_hash", "blk_proposer", "blk_created_at")
16 | for _, block := range blocks {
17 | if block.ID == 0 {
18 | return fmt.Errorf("field ProposalID can not be 0")
19 | }
20 | if block.Hash == "" {
21 | return fmt.Errorf("hash can not be empty")
22 | }
23 | if block.Proposer == "" {
24 | return fmt.Errorf("proposer can not be empty")
25 | }
26 | if block.CreatedAt.IsZero() {
27 | return fmt.Errorf("field CreatedAt can not be 0")
28 | }
29 | q = q.Values(block.ID, block.Hash, block.Proposer, block.CreatedAt)
30 | }
31 | return db.Insert(q)
32 | }
33 |
34 | func (db DB) GetBlocks(filter filters.Blocks) (blocks []dmodels.Block, err error) {
35 | q := squirrel.Select("*").From(dmodels.BlocksTable).OrderBy("blk_id desc")
36 | if filter.Limit != 0 {
37 | q = q.Limit(filter.Limit)
38 | }
39 | if filter.Offset != 0 {
40 | q = q.Offset(filter.Offset)
41 | }
42 | err = db.Find(&blocks, q)
43 | return blocks, err
44 | }
45 |
46 | func (db DB) GetBlocksCount(filter filters.Blocks) (total uint64, err error) {
47 | q := squirrel.Select("count(*)").From(dmodels.BlocksTable)
48 | err = db.FindFirst(&total, q)
49 | return total, err
50 | }
51 |
52 | func (db DB) GetAggBlocksCount(filter filters.Agg) (items []smodels.AggItem, err error) {
53 | q := filter.BuildQuery("toDecimal64(count(blk_id), 0)", "blk_created_at", dmodels.BlocksTable)
54 | err = db.Find(&items, q)
55 | return items, err
56 | }
57 |
58 | func (db DB) GetAggBlocksDelay(filter filters.Agg) (items []smodels.AggItem, err error) {
59 | q := squirrel.Select(
60 | "avg(toUnixTimestamp(b1.blk_created_at) - toUnixTimestamp(b2.blk_created_at)) as value",
61 | fmt.Sprintf("toDateTime(%s(b1.blk_created_at)) AS time", filter.AggFunc()),
62 | ).From(fmt.Sprintf("%s as b1", dmodels.BlocksTable)).
63 | JoinClause("JOIN blocks as b2 ON b1.blk_id = toUInt64(plus(b2.blk_id, 1))").
64 | Where(squirrel.Gt{"b1.blk_id": 2}).
65 | GroupBy("time").
66 | OrderBy("time")
67 |
68 | if !filter.From.IsZero() {
69 | q = q.Where(squirrel.GtOrEq{"time": filter.From.Time})
70 | }
71 | if !filter.To.IsZero() {
72 | q = q.Where(squirrel.LtOrEq{"time": filter.To.Time})
73 | }
74 | err = db.Find(&items, q)
75 | return items, err
76 | }
77 |
78 | func (db DB) GetAggUniqBlockValidators(filter filters.Agg) (items []smodels.AggItem, err error) {
79 | q := filter.BuildQuery("toDecimal64(count(DISTINCT blk_proposer), 0)", "blk_created_at", dmodels.BlocksTable)
80 | err = db.Find(&items, q)
81 | return items, err
82 | }
83 |
84 | func (db DB) GetAvgBlocksDelay(filter filters.TimeRange) (delay float64, err error) {
85 | q := squirrel.Select(
86 | "avg(toUnixTimestamp(b1.blk_created_at) - toUnixTimestamp(b2.blk_created_at)) as delay",
87 | ).From(fmt.Sprintf("%s as b1", dmodels.BlocksTable)).
88 | JoinClause("JOIN blocks as b2 ON b1.blk_id = toUInt64(plus(b2.blk_id, 1))").
89 | Where(squirrel.Gt{"b1.blk_id": 2})
90 | if !filter.From.IsZero() {
91 | q = q.Where(squirrel.GtOrEq{"b1.blk_created_at": filter.From.Time})
92 | }
93 | if !filter.To.IsZero() {
94 | q = q.Where(squirrel.LtOrEq{"b1.blk_created_at": filter.To.Time})
95 | }
96 | err = db.FindFirst(&delay, q)
97 | return delay, err
98 | }
99 |
100 | func (db DB) GetProposedBlocksTotal(filter filters.BlocksProposed) (total uint64, err error) {
101 | q := squirrel.Select("count(*) as total").From(dmodels.BlocksTable)
102 | if len(filter.Proposers) != 0 {
103 | q = q.Where(squirrel.Eq{"blk_proposer": filter.Proposers})
104 | }
105 | err = db.FindFirst(&total, q)
106 | return total, err
107 | }
108 |
109 | func (db DB) GetTopProposedBlocksValidators() (items []dmodels.ValidatorValue, err error) {
110 | q := squirrel.Select("count(*) as value", "blk_proposer as validator").
111 | From(dmodels.BlocksTable).
112 | GroupBy("validator").
113 | OrderBy("value desc")
114 | err = db.Find(&items, q)
115 | return items, err
116 | }
117 |
--------------------------------------------------------------------------------
/services/historical_states.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "fmt"
5 | "github.com/everstake/cosmoscan-api/dao/filters"
6 | "github.com/everstake/cosmoscan-api/dmodels"
7 | "github.com/everstake/cosmoscan-api/log"
8 | "github.com/everstake/cosmoscan-api/services/node"
9 | "github.com/everstake/cosmoscan-api/smodels"
10 | "github.com/shopspring/decimal"
11 | "sort"
12 | "time"
13 | )
14 |
15 | func (s ServiceFacade) KeepHistoricalState() {
16 | for {
17 | states, err := s.dao.GetHistoricalStates(filters.HistoricalState{Limit: 1})
18 | if err != nil {
19 | log.Error("KeepHistoricalState: dao.GetHistoricalStates: %s", err.Error())
20 | <-time.After(time.Second * 10)
21 | continue
22 | }
23 | tn := time.Now()
24 | if len(states) != 0 {
25 | lastState := states[0]
26 | if tn.Sub(lastState.CreatedAt.Time) < time.Hour {
27 | point := lastState.CreatedAt.Time.Add(time.Hour)
28 | <-time.After(point.Sub(tn))
29 | }
30 | }
31 | state, err := s.makeState()
32 | if err != nil {
33 | log.Error("KeepHistoricalState: makeState: %s", err.Error())
34 | <-time.After(time.Minute * 10)
35 | continue
36 | }
37 | for {
38 | if err = s.dao.CreateHistoricalStates([]dmodels.HistoricalState{state}); err == nil {
39 | break
40 | }
41 | log.Error("KeepHistoricalState: dao.CreateHistoricalStates: %s", err.Error())
42 | <-time.After(time.Second * 10)
43 | }
44 | <-time.After(time.Minute)
45 | }
46 | }
47 |
48 |
49 | func (s ServiceFacade) Test() (state dmodels.HistoricalState, err error) {
50 | return s.makeState()
51 | }
52 |
53 | func (s ServiceFacade) makeState() (state dmodels.HistoricalState, err error) {
54 | state.InflationRate, err = s.node.GetInflation()
55 | if err != nil {
56 | return state, fmt.Errorf("node.GetInflation: %s", err.Error())
57 | }
58 | state.InflationRate = state.InflationRate.Truncate(2)
59 | state.CommunityPool, err = s.node.GetCommunityPoolAmount()
60 | if err != nil {
61 | return state, fmt.Errorf("node.GetCommunityPoolAmount: %s", err.Error())
62 | }
63 | state.CommunityPool = state.CommunityPool.Truncate(2)
64 | totalSupply, err := s.node.GetTotalSupply()
65 | if err != nil {
66 | return state, fmt.Errorf("node.GetTotalSupply: %s", err.Error())
67 | }
68 | stakingPool, err := s.node.GetStakingPool()
69 | if err != nil {
70 | return state, fmt.Errorf("node.GetStakingPool: %s", err.Error())
71 | }
72 | if !totalSupply.IsZero() {
73 | state.StakedRatio = stakingPool.Pool.BondedTokens.Div(totalSupply).Mul(decimal.New(100, 0)).Truncate(2)
74 | }
75 | validators, err := s.node.GetValidators()
76 | if err != nil {
77 | return state, fmt.Errorf("node.GetValidators: %s", err.Error())
78 | }
79 | sort.Slice(validators, func(i, j int) bool {
80 | return validators[i].DelegatorShares.GreaterThan(validators[j].DelegatorShares)
81 | })
82 | if len(validators) >= 20 {
83 | top20Stake := decimal.Zero
84 | for i := 0; i < 20; i++ {
85 | top20Stake = top20Stake.Add(validators[i].DelegatorShares)
86 | }
87 | top20Stake = top20Stake.Div(node.PrecisionDiv)
88 | if !stakingPool.Pool.BondedTokens.IsZero() {
89 | state.Top20Weight = top20Stake.Div(stakingPool.Pool.BondedTokens).Mul(decimal.New(100, 0)).Truncate(2)
90 | }
91 | }
92 |
93 | state.CirculatingSupply = totalSupply.Truncate(2)
94 |
95 | state.Price, state.TradingVolume, err = s.cm.GetMarketData()
96 | if err != nil {
97 | return state, fmt.Errorf("cm.GetMarketData: %s", err.Error())
98 | }
99 | state.MarketCap = state.CirculatingSupply.Mul(state.Price).Truncate(2)
100 |
101 | if state.Price.IsZero() {
102 | return state, fmt.Errorf("cmc not found currency")
103 | }
104 | state.CreatedAt = dmodels.NewTime(time.Now())
105 | return state, nil
106 | }
107 |
108 | func (s *ServiceFacade) GetHistoricalState() (state smodels.HistoricalState, err error) {
109 | models, err := s.dao.GetHistoricalStates(filters.HistoricalState{Limit: 1})
110 | if err != nil {
111 | return state, fmt.Errorf("dao.GetHistoricalStates: %s", err.Error())
112 | }
113 | if len(models) == 0 {
114 | return state, fmt.Errorf("not found any states")
115 | }
116 | state.Current = models[0]
117 | state.PriceAgg, err = s.dao.GetAggHistoricalStatesByField(filters.Agg{
118 | By: filters.AggByHour,
119 | From: dmodels.NewTime(time.Now().Add(-time.Hour * 24)),
120 | }, "his_price")
121 | if err != nil {
122 | return state, fmt.Errorf("dao.GetAggHistoricalStatesByField: %s", err.Error())
123 | }
124 | state.MarketCapAgg, err = s.dao.GetAggHistoricalStatesByField(filters.Agg{
125 | By: filters.AggByHour,
126 | From: dmodels.NewTime(time.Now().Add(-time.Hour * 24)),
127 | }, "his_market_cap")
128 | if err != nil {
129 | return state, fmt.Errorf("dao.GetAggHistoricalStatesByField: %s", err.Error())
130 | }
131 | state.StakedRatioAgg, err = s.dao.GetAggHistoricalStatesByField(filters.Agg{
132 | By: filters.AggByDay,
133 | From: dmodels.NewTime(time.Now().Add(-time.Hour * 24 * 30)),
134 | }, "his_staked_ratio")
135 | if err != nil {
136 | return state, fmt.Errorf("dao.GetAggHistoricalStatesByField: %s", err.Error())
137 | }
138 | return state, nil
139 | }
140 |
--------------------------------------------------------------------------------
/services/blocks.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "fmt"
5 | "github.com/everstake/cosmoscan-api/dao/filters"
6 | "github.com/everstake/cosmoscan-api/dmodels"
7 | "github.com/everstake/cosmoscan-api/services/helpers"
8 | "github.com/everstake/cosmoscan-api/smodels"
9 | "github.com/shopspring/decimal"
10 | "strings"
11 | )
12 |
13 | const topProposedBlocksValidatorsKey = "topProposedBlocksValidatorsKey"
14 | const rewardPerBlock = 4.0
15 |
16 | func (s *ServiceFacade) GetAggBlocksCount(filter filters.Agg) (items []smodels.AggItem, err error) {
17 | items, err = s.dao.GetAggBlocksCount(filter)
18 | if err != nil {
19 | return nil, fmt.Errorf("dao.GetAggBlocksCount: %s", err.Error())
20 | }
21 | return items, nil
22 | }
23 |
24 | func (s *ServiceFacade) GetAggBlocksDelay(filter filters.Agg) (items []smodels.AggItem, err error) {
25 | items, err = s.dao.GetAggBlocksDelay(filter)
26 | if err != nil {
27 | return nil, fmt.Errorf("dao.GetAggBlocksDelay: %s", err.Error())
28 | }
29 | return items, nil
30 | }
31 |
32 | func (s *ServiceFacade) GetAggUniqBlockValidators(filter filters.Agg) (items []smodels.AggItem, err error) {
33 | items, err = s.dao.GetAggUniqBlockValidators(filter)
34 | if err != nil {
35 | return nil, fmt.Errorf("dao.GetAggUniqBlockValidators: %s", err.Error())
36 | }
37 | return items, nil
38 | }
39 |
40 | func (s *ServiceFacade) GetValidatorBlocksStat(validatorAddress string) (stat smodels.ValidatorBlocksStat, err error) {
41 | validator, err := s.GetValidator(validatorAddress)
42 | if err != nil {
43 | return stat, fmt.Errorf("GetValidator: %s", err.Error())
44 | }
45 | stat.Proposed, err = s.dao.GetProposedBlocksTotal(filters.BlocksProposed{
46 | Proposers: []string{validator.ConsAddress},
47 | })
48 | if err != nil {
49 | return stat, fmt.Errorf("dao.GetProposedBlocksTotal: %s", err.Error())
50 | }
51 | stat.MissedValidations, err = s.dao.GetMissedBlocksCount(filters.MissedBlocks{
52 | Validators: []string{validator.ConsAddress},
53 | })
54 | if err != nil {
55 | return stat, fmt.Errorf("dao.GetMissedBlocksCount: %s", err.Error())
56 | }
57 | stat.Revenue = decimal.NewFromFloat(rewardPerBlock).Mul(decimal.NewFromInt(int64(stat.Proposed)))
58 | return stat, nil
59 | }
60 |
61 | func (s *ServiceFacade) GetBlock(height uint64) (block smodels.Block, err error) {
62 | dBlock, err := s.node.GetBlock(height)
63 | if err != nil {
64 | return block, fmt.Errorf("node.GetBlock: %s", err.Error())
65 | }
66 | validators, err := s.getConsensusValidatorMap()
67 | if err != nil {
68 | return block, fmt.Errorf("s.getConsensusValidatorMap: %s", err.Error())
69 | }
70 | proposerKey, err := helpers.B64ToHex(dBlock.Block.Header.ProposerAddress)
71 | if err != nil {
72 | return block, fmt.Errorf("helpers.B64ToHex: %s", err.Error())
73 | }
74 | hashHex, err := helpers.B64ToHex(dBlock.BlockID.Hash)
75 | if err != nil {
76 | return block, fmt.Errorf("helpers.B64ToHex: %s", err.Error())
77 | }
78 | var proposer, proposerAddress string
79 | validator, ok := validators[strings.ToUpper(proposerKey)]
80 | if ok {
81 | proposer = validator.Description.Moniker
82 | proposerAddress = validator.OperatorAddress
83 | }
84 | dTxs, err := s.dao.GetTransactions(filters.Transactions{Height: height})
85 | if err != nil {
86 | return block, fmt.Errorf("dao.GetTransactions: %s", err.Error())
87 | }
88 | var txs []smodels.TxItem
89 | for _, tx := range dTxs {
90 | txs = append(txs, smodels.TxItem{
91 | Hash: tx.Hash,
92 | Status: tx.Status,
93 | Fee: tx.Fee,
94 | Height: tx.Height,
95 | Messages: tx.Messages,
96 | CreatedAt: dmodels.NewTime(tx.CreatedAt),
97 | })
98 | }
99 | return smodels.Block{
100 | Height: dBlock.Block.Header.Height,
101 | Hash: strings.ToUpper(hashHex),
102 | TotalTxs: uint64(len(dBlock.Block.Data.Txs)),
103 | ChainID: dBlock.Block.Header.ChainID,
104 | Proposer: proposer,
105 | ProposerAddress: proposerAddress,
106 | Txs: txs,
107 | CreatedAt: dmodels.NewTime(dBlock.Block.Header.Time),
108 | }, nil
109 | }
110 |
111 | func (s *ServiceFacade) GetBlocks(filter filters.Blocks) (resp smodels.PaginatableResponse, err error) {
112 | dBlocks, err := s.dao.GetBlocks(filter)
113 | if err != nil {
114 | return resp, fmt.Errorf("dao.GetBlocks: %s", err.Error())
115 | }
116 | total, err := s.dao.GetBlocksCount(filter)
117 | if err != nil {
118 | return resp, fmt.Errorf("dao.GetBlocksCount: %s", err.Error())
119 | }
120 | validators, err := s.getConsensusValidatorMap()
121 | if err != nil {
122 | return resp, fmt.Errorf("s.getConsensusValidatorMap: %s", err.Error())
123 | }
124 | var blocks []smodels.BlockItem
125 | for _, b := range dBlocks {
126 | var proposer, proposerAddress string
127 | validator, ok := validators[b.Proposer]
128 | if ok {
129 | proposer = validator.Description.Moniker
130 | proposerAddress = validator.OperatorAddress
131 | }
132 | blocks = append(blocks, smodels.BlockItem{
133 | Height: b.ID,
134 | Hash: b.Hash,
135 | Proposer: proposer,
136 | ProposerAddress: proposerAddress,
137 | CreatedAt: dmodels.NewTime(b.CreatedAt),
138 | })
139 | }
140 | return smodels.PaginatableResponse{
141 | Items: blocks,
142 | Total: total,
143 | }, nil
144 | }
145 |
--------------------------------------------------------------------------------
/services/services.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "github.com/everstake/cosmoscan-api/config"
5 | "github.com/everstake/cosmoscan-api/dao"
6 | "github.com/everstake/cosmoscan-api/dao/filters"
7 | "github.com/everstake/cosmoscan-api/dmodels"
8 | "github.com/everstake/cosmoscan-api/services/coingecko"
9 | "github.com/everstake/cosmoscan-api/services/node"
10 | "github.com/everstake/cosmoscan-api/smodels"
11 | "github.com/shopspring/decimal"
12 | )
13 |
14 | type (
15 | Services interface {
16 | KeepHistoricalState()
17 | UpdateValidatorsMap()
18 | GetValidatorMap() (map[string]node.Validator, error)
19 | GetMetaData() (meta smodels.MetaData, err error)
20 | GetAggTransactionsFee(filter filters.Agg) (items []smodels.AggItem, err error)
21 | GetAggOperationsCount(filter filters.Agg) (items []smodels.AggItem, err error)
22 | GetAggTransfersVolume(filter filters.Agg) (items []smodels.AggItem, err error)
23 | GetHistoricalState() (state smodels.HistoricalState, err error)
24 | GetAggBlocksCount(filter filters.Agg) (items []smodels.AggItem, err error)
25 | GetAggBlocksDelay(filter filters.Agg) (items []smodels.AggItem, err error)
26 | GetAggUniqBlockValidators(filter filters.Agg) (items []smodels.AggItem, err error)
27 | GetAggDelegationsVolume(filter filters.DelegationsAgg) (items []smodels.AggItem, err error)
28 | GetAggUndelegationsVolume(filter filters.Agg) (items []smodels.AggItem, err error)
29 | GetNetworkStates(filter filters.Stats) (map[string][]decimal.Decimal, error)
30 | GetStakingPie() (pie smodels.Pie, err error)
31 | MakeUpdateBalances()
32 | GetSizeOfNode() (size float64, err error)
33 | MakeStats()
34 | UpdateProposals()
35 | GetProposals(filter filters.Proposals) (proposals []dmodels.Proposal, err error)
36 | GetProposalVotes(filter filters.ProposalVotes) (items []smodels.ProposalVote, err error)
37 | GetProposalDeposits(filter filters.ProposalDeposits) (deposits []dmodels.ProposalDeposit, err error)
38 | GetProposalsChartData() (items []smodels.ProposalChartData, err error)
39 | GetAggValidators33Power(filter filters.Agg) (items []smodels.AggItem, err error)
40 | GetValidators() (validators []smodels.Validator, err error)
41 | UpdateValidators()
42 | GetAvgOperationsPerBlock(filter filters.Agg) (items []smodels.AggItem, err error)
43 | GetAggWhaleAccounts(filter filters.Agg) (items []smodels.AggItem, err error)
44 | GetTopProposedBlocksValidators() (items []dmodels.ValidatorValue, err error)
45 | GetMostJailedValidators() (items []dmodels.ValidatorValue, err error)
46 | GetFeeRanges() (items []smodels.FeeRange, err error)
47 | GetValidatorsDelegatorsTotal() (values []dmodels.ValidatorValue, err error)
48 | GetValidator(address string) (validator smodels.Validator, err error)
49 | GetValidatorBalance(valAddress string) (balance smodels.Balance, err error)
50 | GetValidatorDelegationsAgg(validatorAddress string) (items []smodels.AggItem, err error)
51 | GetValidatorDelegatorsAgg(validatorAddress string) (items []smodels.AggItem, err error)
52 | GetValidatorBlocksStat(validatorAddress string) (stat smodels.ValidatorBlocksStat, err error)
53 | GetValidatorDelegators(filter filters.ValidatorDelegators) (resp smodels.PaginatableResponse, err error)
54 | GetAggBondedRatio(filter filters.Agg) (items []smodels.AggItem, err error)
55 | GetAggUnbondingVolume(filter filters.Agg) (items []smodels.AggItem, err error)
56 | Test() (state dmodels.HistoricalState, err error)
57 | GetBlock(height uint64) (block smodels.Block, err error)
58 | GetBlocks(filter filters.Blocks) (resp smodels.PaginatableResponse, err error)
59 | GetTransaction(hash string) (tx smodels.Tx, err error)
60 | GetTransactions(filter filters.Transactions) (resp smodels.PaginatableResponse, err error)
61 | GetAccount(address string) (account smodels.Account, err error)
62 | }
63 | CryptoMarket interface {
64 | GetMarketData() (price, volume24h decimal.Decimal, err error)
65 | }
66 | Node interface {
67 | GetCommunityPoolAmount() (amount decimal.Decimal, err error)
68 | GetValidators() (items []node.Validator, err error)
69 | GetInflation() (amount decimal.Decimal, err error)
70 | GetTotalSupply() (amount decimal.Decimal, err error)
71 | GetStakingPool() (sp node.StakingPool, err error)
72 | GetBalance(address string) (amount decimal.Decimal, err error)
73 | GetStake(address string) (amount decimal.Decimal, err error)
74 | GetUnbonding(address string) (amount decimal.Decimal, err error)
75 | GetProposals() (proposals node.ProposalsResult, err error)
76 | GetDelegatorValidatorStake(delegator string, validator string) (amount decimal.Decimal, err error)
77 | ProposalTallyResult(id uint64) (result node.ProposalTallyResult, err error)
78 | GetBlock(id uint64) (result node.Block, err error)
79 | GetTransaction(hash string) (result node.TxResult, err error)
80 | GetBalances(address string) (result node.AmountResult, err error)
81 | GetStakeRewards(address string) (amount decimal.Decimal, err error)
82 | }
83 |
84 | ServiceFacade struct {
85 | dao dao.DAO
86 | cfg config.Config
87 | cm CryptoMarket
88 | node Node
89 | }
90 | )
91 |
92 | func NewServices(d dao.DAO, cfg config.Config) (svc Services, err error) {
93 | return &ServiceFacade{
94 | dao: d,
95 | cfg: cfg,
96 | cm: coingecko.NewGecko(),
97 | node: node.NewAPI(cfg),
98 | }, nil
99 | }
100 |
--------------------------------------------------------------------------------
/dao/mysql/migrations/init.sql:
--------------------------------------------------------------------------------
1 | -- +migrate Up
2 | create table parsers
3 | (
4 | par_id int auto_increment
5 | primary key,
6 | par_title varchar(255) not null,
7 | par_height int default 0 not null,
8 | constraint parsers_par_title_uindex
9 | unique (par_title)
10 | ) ENGINE = InnoDB
11 | DEFAULT CHARSET = utf8mb4
12 | COLLATE = utf8mb4_general_ci;
13 |
14 | insert into parsers (par_id, par_title, par_height)
15 | VALUES (1, 'hub3', 0);
16 |
17 | create table validators
18 | (
19 | val_cons_address varchar(255) not null
20 | primary key,
21 | val_address varchar(255) default '' not null,
22 | val_operator_address varchar(255) default '' not null,
23 | val_cons_pub_key varchar(255) default '' not null,
24 | val_name varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci default '' not null,
25 | val_description text not null,
26 | val_commission decimal(8, 4) default 0.0000 not null,
27 | val_min_commission decimal(8, 4) default 0.0000 not null,
28 | val_max_commission decimal(8, 4) default 0.0000 not null,
29 | val_self_delegations decimal(20, 8) not null,
30 | val_delegations decimal(20, 8) default 0.00000000 not null,
31 | val_voting_power decimal(20, 8) default 0.00000000 not null,
32 | val_website varchar(255) default '' not null,
33 | val_jailed tinyint(1) default 0 null,
34 | val_created_at timestamp default CURRENT_TIMESTAMP not null,
35 | constraint validators_val_cons_address_uindex
36 | unique (val_cons_address)
37 | ) ENGINE = InnoDB
38 | DEFAULT CHARSET = utf8mb4
39 | COLLATE = utf8mb4_general_ci;
40 |
41 | create table accounts
42 | (
43 | acc_address varchar(255) not null
44 | primary key,
45 | acc_balance decimal(20, 8) default 0.00000000 not null,
46 | acc_stake decimal(20, 8) default 0.00000000 not null,
47 | acc_unbonding decimal(20, 8) default 0.00000000 not null,
48 | acc_created_at timestamp default CURRENT_TIMESTAMP not null
49 | ) ENGINE = InnoDB
50 | DEFAULT CHARSET = utf8mb4
51 | COLLATE = utf8mb4_general_ci;
52 |
53 | create index accounts_acc_created_at_index
54 | on accounts (acc_created_at);
55 |
56 |
57 | create table proposals
58 | (
59 | pro_id int not null
60 | primary key,
61 | pro_tx_hash varchar(255) not null,
62 | pro_proposer varchar(255) not null,
63 | pro_proposer_address varchar(255) not null,
64 | pro_type varchar(255) not null,
65 | pro_title varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci not null,
66 | pro_description text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci null,
67 | pro_status varchar(255) null,
68 | pro_votes_yes decimal(20, 8) default 0.00000000 not null,
69 | pro_votes_abstain decimal(20, 8) default 0.00000000 not null,
70 | pro_votes_no decimal(20, 8) default 0.00000000 not null,
71 | pro_votes_no_with_veto decimal(20, 8) not null,
72 | pro_submit_time datetime not null,
73 | pro_deposit_end_time datetime not null,
74 | pro_total_deposits decimal(20, 8) default 0.00000000 not null,
75 | pro_voting_start_time datetime default '2000-01-01 00:00:00' not null,
76 | pro_voting_end_time datetime default '2000-01-01 00:00:00' not null,
77 | pro_voters int default 0 not null,
78 | pro_participation_rate decimal(5, 2) default 0 not null,
79 | pro_turnout decimal(20, 8) default 0.00000000 not null,
80 | pro_activity json not null
81 | ) ENGINE = InnoDB
82 | DEFAULT CHARSET = utf8mb4
83 | COLLATE = utf8mb4_general_ci;
84 |
85 |
86 | -- +migrate Down
87 | drop table parsers;
88 | drop table validators;
89 | drop table accounts;
90 |
--------------------------------------------------------------------------------
/services/parser/hub3/genesis.go:
--------------------------------------------------------------------------------
1 | package hub3
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/everstake/cosmoscan-api/dmodels"
7 | "github.com/shopspring/decimal"
8 | "io/ioutil"
9 | "net/http"
10 | "time"
11 | )
12 |
13 | const genesisJson = "https://raw.githubusercontent.com/cosmos/launch/master/genesis.json"
14 | const saveGenesisBatch = 100
15 |
16 | type Genesis struct {
17 | AppState struct {
18 | Accounts []struct {
19 | Address string `json:"address"`
20 | Coins []Amount `json:"coins"`
21 | } `json:"accounts"`
22 | Distribution struct {
23 | DelegatorStartingInfos []struct {
24 | StartingInfo struct {
25 | DelegatorAddress string `json:"delegator_address"`
26 | StartingInfo struct {
27 | Stake decimal.Decimal `json:"stake"`
28 | } `json:"starting_info"`
29 | ValidatorAddress string `json:"validator_address"`
30 | } `json:"starting_info"`
31 | } `json:"delegator_starting_infos"`
32 | } `json:"distribution"`
33 | Staking struct {
34 | Delegations []struct {
35 | DelegatorAddress string `json:"delegator_address"`
36 | Shares decimal.Decimal `json:"shares"`
37 | ValidatorAddress string `json:"validator_address"`
38 | } `json:"delegations"`
39 | Redelegations []struct {
40 | DelegatorAddress string `json:"delegator_address"`
41 | Entries []struct {
42 | SharesDst decimal.Decimal `json:"shares_dst"`
43 | } `json:"entries"`
44 | ValidatorDstAddress string `json:"validator_dst_address"`
45 | ValidatorSrcAddress string `json:"validator_src_address"`
46 | } `json:"redelegations"`
47 | } `json:"staking"`
48 | } `json:"app_state"`
49 | GenesisTime time.Time `json:"genesis_time"`
50 | Validators []struct {
51 | Address string `json:"address"`
52 | Name string `json:"name"`
53 | Power decimal.Decimal `json:"power"`
54 | } `json:"validators"`
55 | }
56 |
57 | func GetGenesisState() (state Genesis, err error) {
58 | resp, err := http.Get(genesisJson)
59 | if err != nil {
60 | return state, fmt.Errorf("http.Get: %s", err.Error())
61 | }
62 | data, err := ioutil.ReadAll(resp.Body)
63 | if err != nil {
64 | return state, fmt.Errorf("ioutil.ReadAll: %s", err.Error())
65 | }
66 | err = json.Unmarshal(data, &state)
67 | if err != nil {
68 | return state, fmt.Errorf("json.Unmarshal: %s", err.Error())
69 | }
70 |
71 | return state, nil
72 | }
73 |
74 | func ShowGenesisStructure() {
75 | resp, _ := http.Get(genesisJson)
76 | data, _ := ioutil.ReadAll(resp.Body)
77 | var value interface{}
78 | _ = json.Unmarshal(data, &value)
79 | printStruct(value, 0)
80 | }
81 |
82 | func printStruct(field interface{}, i int) {
83 | mp, ok := field.(map[string]interface{})
84 | if ok {
85 | if len(mp) > 50 {
86 | return
87 | }
88 | for title, f := range mp {
89 | var str string
90 | for k := 0; k < i; k++ {
91 | str = str + " "
92 | }
93 | fmt.Println(str + title)
94 | printStruct(f, i+1)
95 | }
96 | }
97 | }
98 |
99 | func (p *Parser) parseGenesisState() error {
100 | state, err := GetGenesisState()
101 | if err != nil {
102 | return fmt.Errorf("getGenesisState: %s", err.Error())
103 | }
104 | t, err := time.Parse("2006-01-02", "2019-12-11")
105 | if err != nil {
106 | return fmt.Errorf("time.Parse: %s", err.Error())
107 | }
108 | var (
109 | delegations []dmodels.Delegation
110 | accounts []dmodels.Account
111 | )
112 | for i, delegation := range state.AppState.Staking.Delegations {
113 | delegations = append(delegations, dmodels.Delegation{
114 | ID: makeHash(fmt.Sprintf("delegations.%d", i)),
115 | TxHash: "genesis",
116 | Delegator: delegation.DelegatorAddress,
117 | Validator: delegation.ValidatorAddress,
118 | Amount: delegation.Shares.Div(precisionDiv),
119 | CreatedAt: t,
120 | })
121 | }
122 | for i, delegation := range state.AppState.Staking.Redelegations {
123 | amount := decimal.Zero
124 | for _, entry := range delegation.Entries {
125 | amount = amount.Add(entry.SharesDst)
126 | }
127 | // ignore undelegation
128 | delegations = append(delegations, dmodels.Delegation{
129 | ID: makeHash(fmt.Sprintf("redelegations.%d", i)),
130 | TxHash: "genesis",
131 | Delegator: delegation.DelegatorAddress,
132 | Validator: delegation.ValidatorDstAddress,
133 | Amount: amount.Div(precisionDiv),
134 | CreatedAt: t,
135 | })
136 | }
137 | accountDelegation := make(map[string]decimal.Decimal)
138 | for _, delegation := range delegations {
139 | accountDelegation[delegation.Delegator] = accountDelegation[delegation.Delegator].Add(delegation.Amount)
140 | }
141 | for _, account := range state.AppState.Accounts {
142 | amount, _ := calculateAtomAmount(account.Coins)
143 | accounts = append(accounts, dmodels.Account{
144 | Address: account.Address,
145 | Balance: amount,
146 | Stake: accountDelegation[account.Address],
147 | CreatedAt: t,
148 | })
149 | }
150 |
151 | for i := 0; i < len(accounts); i += saveGenesisBatch {
152 | endOfPart := i + saveGenesisBatch
153 | if i+saveGenesisBatch > len(accounts) {
154 | endOfPart = len(accounts)
155 | }
156 | err := p.dao.CreateAccounts(accounts[i:endOfPart])
157 | if err != nil {
158 | return fmt.Errorf("dao.CreateAccounts: %s", err.Error())
159 | }
160 | }
161 |
162 | for i := 0; i < len(delegations); i += saveGenesisBatch {
163 | endOfPart := i + saveGenesisBatch
164 | if i+saveGenesisBatch > len(delegations) {
165 | endOfPart = len(delegations)
166 | }
167 | err := p.dao.CreateDelegations(delegations[i:endOfPart])
168 | if err != nil {
169 | return fmt.Errorf("dao.CreateDelegations: %s", err.Error())
170 | }
171 | }
172 |
173 | return nil
174 | }
175 |
--------------------------------------------------------------------------------
/docker/clickhouse-users.xml.example:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 10000000000
9 |
10 |
11 | 0
12 |
13 |
21 | random
22 |
23 |
24 |
25 |
26 | 1
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
47 | 42a9798b99d4afcec9995e47a1d246b98ebc96be7a732323eee39d924006ee1d
48 |
49 |
69 |
70 | ::/0
71 |
72 |
73 |
74 | default
75 |
76 |
77 | default
78 |
79 |
80 |
81 |
82 |
83 |
84 | a = 1
85 |
86 |
87 |
88 |
89 | a + b < 1 or c - d > 5
90 |
91 |
92 |
93 |
94 | c = 1
95 |
96 |
97 |
98 |
99 |
100 |
101 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | 3600
120 |
121 |
122 | 0
123 | 0
124 | 0
125 | 0
126 | 0
127 |
128 |
129 |
130 |
--------------------------------------------------------------------------------
/services/scheduler/scheduler.go:
--------------------------------------------------------------------------------
1 | package scheduler
2 |
3 | import (
4 | "context"
5 | "github.com/everstake/cosmoscan-api/log"
6 | "reflect"
7 | "runtime"
8 | "strings"
9 | "sync"
10 | "time"
11 | )
12 |
13 | const (
14 | intervalRunType runType = "interval"
15 | periodRunType runType = "period"
16 | everyDayRunType runType = "every_day"
17 | everyMonthRunType runType = "every_month"
18 | )
19 |
20 | type (
21 | runType string
22 | Process func()
23 | task struct {
24 | runType runType
25 | process Process
26 | duration time.Duration
27 | atTime atTime
28 | }
29 | atTime struct {
30 | day int
31 | hours int
32 | minutes int
33 | }
34 | Scheduler struct {
35 | tskCh chan task
36 | wg *sync.WaitGroup
37 | ctx context.Context
38 | cancel context.CancelFunc
39 | mu *sync.RWMutex
40 | tasks []task
41 | alreadyRun bool
42 | }
43 | )
44 |
45 | func NewScheduler() *Scheduler {
46 | ctx, cancel := context.WithCancel(context.Background())
47 | return &Scheduler{
48 | ctx: ctx,
49 | cancel: cancel,
50 | tskCh: make(chan task),
51 | wg: &sync.WaitGroup{},
52 | mu: &sync.RWMutex{},
53 | tasks: make([]task, 0),
54 | }
55 | }
56 |
57 | func (sch *Scheduler) AddProcessWithInterval(process Process, interval time.Duration) {
58 | tsk := task{
59 | runType: intervalRunType,
60 | process: process,
61 | duration: interval,
62 | }
63 | sch.addTask(tsk)
64 | }
65 |
66 | func (sch *Scheduler) AddProcessWithPeriod(process Process, period time.Duration) {
67 | tsk := task{
68 | runType: periodRunType,
69 | process: process,
70 | duration: period,
71 | }
72 | sch.addTask(tsk)
73 | }
74 |
75 | func (sch *Scheduler) EveryDayAt(process Process, hours int, minutes int) {
76 | tsk := task{
77 | runType: everyDayRunType,
78 | process: process,
79 | atTime: atTime{
80 | hours: hours,
81 | minutes: minutes,
82 | },
83 | }
84 | sch.addTask(tsk)
85 | }
86 |
87 | func (sch *Scheduler) EveryMonthAt(process Process, day int, hours int, minutes int) {
88 | tsk := task{
89 | runType: everyMonthRunType,
90 | process: process,
91 | atTime: atTime{
92 | day: day,
93 | hours: hours,
94 | minutes: minutes,
95 | },
96 | }
97 | sch.addTask(tsk)
98 | }
99 |
100 | func (sch *Scheduler) Run() error {
101 | sch.markAsAlreadyRun()
102 | for _, t := range sch.tasks {
103 | sch.runTask(t)
104 | }
105 | for {
106 | select {
107 | case <-sch.ctx.Done():
108 | return nil
109 | case t := <-sch.tskCh:
110 | sch.runTask(t)
111 | }
112 | }
113 | }
114 |
115 | func (sch *Scheduler) Stop() error {
116 | sch.cancel()
117 | sch.wg.Wait()
118 | return nil
119 | }
120 |
121 | func (sch *Scheduler) Title() string {
122 | return "Scheduler"
123 | }
124 |
125 | func (sch *Scheduler) runTask(t task) {
126 | switch t.runType {
127 | case intervalRunType:
128 | go func() {
129 | runByInterval(sch.ctx, t.process, t.duration)
130 | sch.wg.Done()
131 | }()
132 | case periodRunType:
133 | go func() {
134 | runByPeriod(sch.ctx, t.process, t.duration)
135 | sch.wg.Done()
136 | }()
137 | case everyDayRunType:
138 | go func() {
139 | runEveryDayAt(sch.ctx, t.process, t.atTime)
140 | sch.wg.Done()
141 | }()
142 | case everyMonthRunType:
143 | go func() {
144 | runEveryMonthAt(sch.ctx, t.process, t.atTime)
145 | sch.wg.Done()
146 | }()
147 | }
148 | log.Debug("Scheduler run process %s", t.process.GetName())
149 | }
150 |
151 | func (sch *Scheduler) addTask(tsk task) {
152 | sch.wg.Add(1)
153 | if !sch.isAlreadyRun() {
154 | sch.mu.Lock()
155 | sch.tasks = append(sch.tasks, tsk)
156 | sch.mu.Unlock()
157 | return
158 | }
159 | sch.tskCh <- tsk
160 | }
161 |
162 | func (sch *Scheduler) isAlreadyRun() bool {
163 | sch.mu.RLock()
164 | defer sch.mu.RUnlock()
165 | return sch.alreadyRun
166 | }
167 |
168 | func (sch *Scheduler) markAsAlreadyRun() {
169 | sch.mu.Lock()
170 | sch.alreadyRun = true
171 | sch.mu.Unlock()
172 | }
173 |
174 | func runByInterval(ctx context.Context, process Process, interval time.Duration) {
175 | if interval == 0 {
176 | log.Error("Scheduler: interval is zero, process %s", process.GetName())
177 | return
178 | }
179 | for {
180 | process()
181 | select {
182 | case <-ctx.Done():
183 | return
184 | case <-time.After(interval):
185 | continue
186 | }
187 | }
188 | }
189 |
190 | func runByPeriod(ctx context.Context, process Process, period time.Duration) {
191 | if period == 0 {
192 | log.Error("Scheduler: period is zero, process %s", process.GetName())
193 | return
194 | }
195 | periodCh := time.After(period)
196 | for {
197 | periodCh = time.After(period)
198 | process()
199 | select {
200 | case <-ctx.Done():
201 | return
202 | case <-periodCh:
203 | continue
204 | }
205 | }
206 | }
207 |
208 | func runEveryDayAt(ctx context.Context, process Process, a atTime) {
209 | for {
210 | now := time.Now()
211 | year, month, day := now.Date()
212 | today := time.Date(year, month, day, a.hours, a.minutes, 0, 0, time.Local)
213 | var duration time.Duration
214 | if today.After(now) {
215 | duration = today.Sub(now)
216 | } else {
217 | tomorrow := today.Add(time.Hour * 24)
218 | duration = tomorrow.Sub(now)
219 | }
220 | next := time.After(duration)
221 | select {
222 | case <-ctx.Done():
223 | return
224 | case <-next:
225 | process()
226 | }
227 | }
228 | }
229 |
230 | func runEveryMonthAt(ctx context.Context, process Process, a atTime) {
231 | for {
232 | now := time.Now()
233 | year, month, _ := now.Date()
234 | timeInCurrentMonth := time.Date(year, month, a.day, a.hours, a.minutes, 0, 0, time.Local)
235 | var duration time.Duration
236 | if timeInCurrentMonth.After(now) {
237 | duration = timeInCurrentMonth.Sub(now)
238 | } else {
239 | nextMonth := timeInCurrentMonth.AddDate(0, 1, 0)
240 | duration = nextMonth.Sub(now)
241 | }
242 | next := time.After(duration)
243 | select {
244 | case <-ctx.Done():
245 | return
246 | case <-next:
247 | process()
248 | }
249 | }
250 | }
251 |
252 | func (p Process) GetName() string {
253 | path := runtime.FuncForPC(reflect.ValueOf(p).Pointer()).Name()
254 | if path == "" {
255 | return path
256 | }
257 | parts := strings.Split(path, ".")
258 | if len(path) == 0 {
259 | return ""
260 | }
261 | return parts[len(parts)-1]
262 | }
263 |
--------------------------------------------------------------------------------
/dao/clickhouse/delegations.go:
--------------------------------------------------------------------------------
1 | package clickhouse
2 |
3 | import (
4 | "fmt"
5 | "github.com/Masterminds/squirrel"
6 | "github.com/everstake/cosmoscan-api/dao/filters"
7 | "github.com/everstake/cosmoscan-api/dmodels"
8 | "github.com/everstake/cosmoscan-api/smodels"
9 | "github.com/shopspring/decimal"
10 | )
11 |
12 | func (db DB) CreateDelegations(delegations []dmodels.Delegation) error {
13 | if len(delegations) == 0 {
14 | return nil
15 | }
16 | q := squirrel.Insert(dmodels.DelegationsTable).Columns("dlg_id", "dlg_tx_hash", "dlg_delegator", "dlg_validator", "dlg_amount", "dlg_created_at")
17 | for _, delegation := range delegations {
18 | if delegation.ID == "" {
19 | return fmt.Errorf("field ProposalID can not be empty")
20 | }
21 | if delegation.TxHash == "" {
22 | return fmt.Errorf("field TxHash can not be empty")
23 | }
24 | if delegation.Delegator == "" {
25 | return fmt.Errorf("field Delegator can not be empty")
26 | }
27 | if delegation.Validator == "" {
28 | return fmt.Errorf("field Validator can not be empty")
29 | }
30 | if delegation.CreatedAt.IsZero() {
31 | return fmt.Errorf("field CreatedAt can not be zero")
32 | }
33 | q = q.Values(delegation.ID, delegation.TxHash, delegation.Delegator, delegation.Validator, delegation.Amount, delegation.CreatedAt)
34 | }
35 | return db.Insert(q)
36 | }
37 |
38 | func (db DB) GetAggDelegationsAndUndelegationsVolume(filter filters.DelegationsAgg) (items []smodels.AggItem, err error) {
39 | q := filter.BuildQuery("sum(dlg_amount)", "dlg_created_at", dmodels.DelegationsTable)
40 | if len(filter.Validators) != 0 {
41 | q = q.Where(squirrel.Eq{"dlg_validator": filter.Validators})
42 | }
43 | err = db.Find(&items, q)
44 | return items, err
45 | }
46 |
47 | func (db DB) GetAggDelegationsVolume(filter filters.DelegationsAgg) (items []smodels.AggItem, err error) {
48 | q := filter.BuildQuery("sum(dlg_amount)", "dlg_created_at", dmodels.DelegationsTable)
49 | if len(filter.Validators) != 0 {
50 | q = q.Where(squirrel.Eq{"dlg_validator": filter.Validators})
51 | }
52 | q = q.Where(squirrel.Gt{"dlg_amount": 0})
53 | err = db.Find(&items, q)
54 | return items, err
55 | }
56 |
57 | func (db DB) GetAggUndelegationsVolume(filter filters.Agg) (items []smodels.AggItem, err error) {
58 | q := filter.BuildQuery("sum(abs(dlg_amount))", "dlg_created_at", dmodels.DelegationsTable)
59 | q = q.Where(squirrel.Lt{"dlg_amount": 0})
60 | err = db.Find(&items, q)
61 | return items, err
62 | }
63 |
64 | func (db DB) GetDelegatorsTotal(filter filters.Delegators) (total uint64, err error) {
65 | q1 := squirrel.Select("dlg_delegator as delegator", "sum(dlg_amount) as amount").
66 | From(dmodels.DelegationsTable).GroupBy("dlg_delegator").
67 | Having(squirrel.Gt{"amount": 0})
68 | if len(filter.Validators) != 0 {
69 | q1 = q1.Where(squirrel.Eq{"dlg_validator": filter.Validators})
70 | }
71 | q1 = filter.Query("dlg_created_at", q1)
72 | q := squirrel.Select("count() as total").FromSelect(q1, "t")
73 | err = db.FindFirst(&total, q)
74 | return total, err
75 | }
76 |
77 | func (db DB) GetMultiDelegatorsTotal(filter filters.TimeRange) (total uint64, err error) {
78 | q1 := squirrel.Select("dlg_delegator as delegator", "sum(dlg_amount) as amount", "count(DISTINCT dlg_validator) as validators_count").
79 | From(dmodels.DelegationsTable).GroupBy("dlg_delegator").
80 | Having(squirrel.Gt{"amount": 0}).Having(squirrel.Gt{"validators_count": 1})
81 | q1 = filter.Query("dlg_created_at", q1)
82 | q := squirrel.Select("count() as total").FromSelect(q1, "t")
83 | err = db.FindFirst(&total, q)
84 | return total, err
85 | }
86 |
87 | func (db DB) GetUndelegationsVolume(filter filters.TimeRange) (total decimal.Decimal, err error) {
88 | q := squirrel.Select("sum(abs(dlg_amount)) as total").
89 | From(dmodels.DelegationsTable).
90 | Where(squirrel.Lt{"dlg_amount": 0})
91 | q = filter.Query("dlg_created_at", q)
92 | err = db.FindFirst(&total, q)
93 | return total, err
94 | }
95 |
96 | func (db DB) GetVotingPower(filter filters.VotingPower) (volume decimal.Decimal, err error) {
97 | q := squirrel.Select("sum(dlg_amount) as volume").From(dmodels.DelegationsTable)
98 | q = filter.Query("dlg_created_at", q)
99 | if len(filter.Delegators) != 0 {
100 | q = q.Where(squirrel.Eq{"dlg_delegator": filter.Delegators})
101 | }
102 | if len(filter.Validators) != 0 {
103 | q = q.Where(squirrel.Eq{"dlg_validator": filter.Validators})
104 | }
105 | err = db.FindFirst(&volume, q)
106 | return volume, err
107 | }
108 |
109 | func (db DB) GetValidatorsDelegatorsTotal() (values []dmodels.ValidatorValue, err error) {
110 | q1 := squirrel.Select("sum(dlg_amount) as volume", "dlg_delegator", "dlg_validator").
111 | From(dmodels.DelegationsTable).
112 | GroupBy("dlg_delegator", "dlg_validator").
113 | Having(squirrel.Gt{"volume": 0})
114 | q := squirrel.Select("count(dlg_validator) as value, t.dlg_validator as validator").
115 | FromSelect(q1, "t").GroupBy("dlg_validator").OrderBy("value desc")
116 | err = db.Find(&values, q)
117 | return values, err
118 | }
119 |
120 | func (db DB) GetValidatorDelegators(filter filters.ValidatorDelegators) (items []dmodels.ValidatorDelegator, err error) {
121 | query := `SELECT * FROM
122 | (SELECT dlg_delegator as delegator, sum(dlg_amount) as amount, min(dlg_created_at) as since
123 | FROM delegations
124 | WHERE dlg_validator = ?
125 | GROUP BY dlg_delegator
126 | HAVING amount > 0 ORDER BY amount DESC) as t1
127 | ANY LEFT JOIN (
128 | SELECT sum(dlg_amount) as delta, dlg_delegator as delegator
129 | FROM delegations
130 | WHERE dlg_validator = ? and dlg_created_at > yesterday()
131 | GROUP BY dlg_delegator
132 | ) as t2 USING (delegator)`
133 | if filter.Limit != 0 {
134 | query = fmt.Sprintf("%s LIMIT %d", query, filter.Limit)
135 | }
136 | if filter.Offset != 0 {
137 | query = fmt.Sprintf("%s OFFSET %d", query, filter.Offset)
138 | }
139 | q, args, err := squirrel.Expr(query, filter.Validator, filter.Validator).ToSql()
140 | if err != nil {
141 | return nil, err
142 | }
143 | err = db.conn.Select(&items, q, args...)
144 | return items, err
145 | }
146 |
147 | func (db DB) GetValidatorDelegatorsTotal(filter filters.ValidatorDelegators) (total uint64, err error) {
148 | q1 := squirrel.Select("sum(dlg_amount) as amount").
149 | From(dmodels.DelegationsTable).
150 | Where(squirrel.Eq{"dlg_validator": filter.Validator}).
151 | GroupBy("dlg_delegator").
152 | Having(squirrel.Gt{"amount": 0})
153 | q := squirrel.Select("count(*) as total").FromSelect(q1, "t")
154 | err = db.FindFirst(&total, q)
155 | return total, err
156 | }
157 |
--------------------------------------------------------------------------------
/dao/dao.go:
--------------------------------------------------------------------------------
1 | package dao
2 |
3 | import (
4 | "fmt"
5 | "github.com/everstake/cosmoscan-api/config"
6 | "github.com/everstake/cosmoscan-api/dao/cache"
7 | "github.com/everstake/cosmoscan-api/dao/clickhouse"
8 | "github.com/everstake/cosmoscan-api/dao/filters"
9 | "github.com/everstake/cosmoscan-api/dao/mysql"
10 | "github.com/everstake/cosmoscan-api/dmodels"
11 | "github.com/everstake/cosmoscan-api/smodels"
12 | "github.com/shopspring/decimal"
13 | "time"
14 | )
15 |
16 | type (
17 | DAO interface {
18 | Mysql
19 | Clickhouse
20 | Cache
21 | }
22 | Mysql interface {
23 | GetParsers() (parsers []dmodels.Parser, err error)
24 | GetParser(title string) (parser dmodels.Parser, err error)
25 | UpdateParser(parser dmodels.Parser) error
26 | CreateValidators(validators []dmodels.Validator) error
27 | UpdateValidators(validator dmodels.Validator) error
28 | CreateAccounts(accounts []dmodels.Account) error
29 | UpdateAccount(account dmodels.Account) error
30 | GetAccount(address string) (account dmodels.Account, err error)
31 | GetAccounts(filter filters.Accounts) (accounts []dmodels.Account, err error)
32 | GetAccountsTotal(filter filters.Accounts) (total uint64, err error)
33 | CreateProposals(proposals []dmodels.Proposal) error
34 | GetProposals(filter filters.Proposals) (proposals []dmodels.Proposal, err error)
35 | UpdateProposal(proposal dmodels.Proposal) error
36 | }
37 | Clickhouse interface {
38 | CreateBlocks(blocks []dmodels.Block) error
39 | GetBlocks(filter filters.Blocks) (blocks []dmodels.Block, err error)
40 | GetBlocksCount(filter filters.Blocks) (total uint64, err error)
41 | GetTransactions(filter filters.Transactions) (items []dmodels.Transaction, err error)
42 | GetTransactionsCount(filter filters.Transactions) (total uint64, err error)
43 | GetAggBlocksCount(filter filters.Agg) (items []smodels.AggItem, err error)
44 | GetAggBlocksDelay(filter filters.Agg) (items []smodels.AggItem, err error)
45 | GetAvgBlocksDelay(filter filters.TimeRange) (delay float64, err error)
46 | GetAggUniqBlockValidators(filter filters.Agg) (items []smodels.AggItem, err error)
47 | CreateTransactions(transactions []dmodels.Transaction) error
48 | GetAggOperationsCount(filter filters.Agg) (items []smodels.AggItem, err error)
49 | GetAggTransactionsFee(filter filters.Agg) (items []smodels.AggItem, err error)
50 | GetTransactionsFeeVolume(filter filters.TimeRange) (total decimal.Decimal, err error)
51 | GetTransactionsHighestFee(filter filters.TimeRange) (total decimal.Decimal, err error)
52 | GetAggTransfersVolume(filter filters.Agg) (items []smodels.AggItem, err error)
53 | CreateTransfers(transfers []dmodels.Transfer) error
54 | GetTransferVolume(filter filters.TimeRange) (total decimal.Decimal, err error)
55 | CreateDelegations(delegations []dmodels.Delegation) error
56 | GetAggDelegationsAndUndelegationsVolume(filter filters.DelegationsAgg) (items []smodels.AggItem, err error)
57 | GetAggDelegationsVolume(filter filters.DelegationsAgg) (items []smodels.AggItem, err error)
58 | GetUndelegationsVolume(filter filters.TimeRange) (total decimal.Decimal, err error)
59 | GetDelegatorsTotal(filter filters.Delegators) (total uint64, err error)
60 | GetMultiDelegatorsTotal(filter filters.TimeRange) (total uint64, err error)
61 | GetAggUndelegationsVolume(filter filters.Agg) (items []smodels.AggItem, err error)
62 | CreateDelegatorRewards(rewards []dmodels.DelegatorReward) error
63 | CreateValidatorRewards(rewards []dmodels.ValidatorReward) error
64 | CreateProposalDeposits(deposits []dmodels.ProposalDeposit) error
65 | GetProposalDeposits(filter filters.ProposalDeposits) (deposits []dmodels.ProposalDeposit, err error)
66 | CreateProposalVotes(votes []dmodels.ProposalVote) error
67 | GetProposalVotes(filter filters.ProposalVotes) (votes []dmodels.ProposalVote, err error)
68 | GetAggProposalVotes(filter filters.Agg, id []uint64) (items []smodels.AggItem, err error)
69 | GetTotalVotesByAddress(address string) (total uint64, err error)
70 | CreateHistoricalStates(states []dmodels.HistoricalState) error
71 | GetHistoricalStates(state filters.HistoricalState) (states []dmodels.HistoricalState, err error)
72 | GetAggHistoricalStatesByField(filter filters.Agg, field string) (items []smodels.AggItem, err error)
73 | GetActiveAccounts(filter filters.ActiveAccounts) (addresses []string, err error)
74 | CreateBalanceUpdates(updates []dmodels.BalanceUpdate) error
75 | GetBalanceUpdate(filter filters.BalanceUpdates) (updates []dmodels.BalanceUpdate, err error)
76 | CreateJailers(jailers []dmodels.Jailer) error
77 | GetJailersTotal() (total uint64, err error)
78 | CreateStats(stats []dmodels.Stat) (err error)
79 | GetStats(filter filters.Stats) (stats []dmodels.Stat, err error)
80 | CreateHistoryProposals(proposals []dmodels.HistoryProposal) error
81 | GetHistoryProposals(filter filters.HistoryProposals) (proposals []dmodels.HistoryProposal, err error)
82 | GetAggValidators33Power(filter filters.Agg) (items []smodels.AggItem, err error)
83 | GetAggWhaleAccounts(filter filters.Agg) (items []smodels.AggItem, err error)
84 | GetProposedBlocksTotal(filter filters.BlocksProposed) (total uint64, err error)
85 | GetVotingPower(filter filters.VotingPower) (volume decimal.Decimal, err error)
86 | GetAvgOperationsPerBlock(filter filters.Agg) (items []smodels.AggItem, err error)
87 | CreateMissedBlocks(blocks []dmodels.MissedBlock) error
88 | GetTopProposedBlocksValidators() (items []dmodels.ValidatorValue, err error)
89 | GetMostJailedValidators() (items []dmodels.ValidatorValue, err error)
90 | GetValidatorsDelegatorsTotal() (values []dmodels.ValidatorValue, err error)
91 | GetMissedBlocksCount(filter filters.MissedBlocks) (total uint64, err error)
92 | GetValidatorDelegators(filter filters.ValidatorDelegators) (items []dmodels.ValidatorDelegator, err error)
93 | GetValidatorDelegatorsTotal(filter filters.ValidatorDelegators) (total uint64, err error)
94 | CreateAccountTxs(accountTxs []dmodels.AccountTx) error
95 | }
96 |
97 | Cache interface {
98 | CacheSet(key string, data interface{}, duration time.Duration)
99 | CacheGet(key string) (data interface{}, found bool)
100 | }
101 |
102 | daoImpl struct {
103 | Mysql
104 | Clickhouse
105 | Cache
106 | }
107 | )
108 |
109 | func NewDAO(cfg config.Config) (DAO, error) {
110 | mysqlDB, err := mysql.NewDB(cfg.Mysql)
111 | if err != nil {
112 | return nil, fmt.Errorf("mysql.NewDB: %s", err.Error())
113 | }
114 | ch, err := clickhouse.NewDB(cfg.Clickhouse)
115 | if err != nil {
116 | return nil, fmt.Errorf("clickhouse.NewDB: %s", err.Error())
117 | }
118 | return daoImpl{
119 | Mysql: mysqlDB,
120 | Clickhouse: ch,
121 | Cache: cache.New(),
122 | }, nil
123 | }
124 |
--------------------------------------------------------------------------------
/services/range_states.go:
--------------------------------------------------------------------------------
1 | package services
2 | // todo delete
3 | //
4 | //import (
5 | // "fmt"
6 | // "github.com/everstake/cosmoscan-api/dao/filters"
7 | // "github.com/everstake/cosmoscan-api/dmodels"
8 | // "github.com/everstake/cosmoscan-api/log"
9 | // "github.com/everstake/cosmoscan-api/smodels"
10 | // "time"
11 | //)
12 | //
13 | //type multiValue struct {
14 | // Value1d string
15 | // Value7d string
16 | // Value30d string
17 | // Value90d string
18 | //}
19 | //
20 | //func (v *multiValue) setValue(i int, s string) {
21 | // switch i {
22 | // case 0:
23 | // v.Value1d = s
24 | // case 1:
25 | // v.Value7d = s
26 | // case 2:
27 | // v.Value30d = s
28 | // case 3:
29 | // v.Value90d = s
30 | // }
31 | //}
32 | //
33 | //func (s *ServiceFacade) KeepRangeStates() {
34 | // ranges := []time.Duration{time.Hour * 24, time.Hour * 24 * 7, time.Hour * 24 * 30, time.Hour * 24 * 90}
35 | // states := []struct {
36 | // title string
37 | // cacheDuration time.Duration
38 | // fetcher func() (multiValue, error)
39 | // }{
40 | // {
41 | // title: dmodels.RangeStateNumberDelegators,
42 | // cacheDuration: time.Minute * 5,
43 | // fetcher: func() (value multiValue, err error) {
44 | // t := time.Now()
45 | // for i, r := range ranges {
46 | // total, err := s.dao.GetDelegatorsTotal(filters.TimeRange{From: dmodels.NewTime(t.Add(-r))})
47 | // if err != nil {
48 | // return value, fmt.Errorf("dao.GetDelegatorsTotal: %s", err.Error())
49 | // }
50 | // value.setValue(i, fmt.Sprintf("%d", total))
51 | // }
52 | // return value, nil
53 | // },
54 | // },
55 | // {
56 | // title: dmodels.RangeStateNumberMultiDelegators,
57 | // cacheDuration: time.Minute * 5,
58 | // fetcher: func() (value multiValue, err error) {
59 | // t := time.Now()
60 | // for i, r := range ranges {
61 | // total, err := s.dao.GetMultiDelegatorsTotal(filters.TimeRange{From: dmodels.NewTime(t.Add(-r))})
62 | // if err != nil {
63 | // return value, fmt.Errorf("dao.GetMultiDelegatorsTotal: %s", err.Error())
64 | // }
65 | // value.setValue(i, fmt.Sprintf("%d", total))
66 | // }
67 | // return value, nil
68 | // },
69 | // },
70 | // {
71 | // title: dmodels.RangeStateTransfersVolume,
72 | // cacheDuration: time.Minute * 5,
73 | // fetcher: func() (value multiValue, err error) {
74 | // t := time.Now()
75 | // for i, r := range ranges {
76 | // total, err := s.dao.GetTransferVolume(filters.TimeRange{From: dmodels.NewTime(t.Add(-r))})
77 | // if err != nil {
78 | // return value, fmt.Errorf("dao.GetTransferVolume: %s", err.Error())
79 | // }
80 | // value.setValue(i, total.String())
81 | // }
82 | // return value, nil
83 | // },
84 | // },
85 | // {
86 | // title: dmodels.RangeStateFeeVolume,
87 | // cacheDuration: time.Minute * 5,
88 | // fetcher: func() (value multiValue, err error) {
89 | // t := time.Now()
90 | // for i, r := range ranges {
91 | // total, err := s.dao.GetTransactionsFeeVolume(filters.TimeRange{From: dmodels.NewTime(t.Add(-r))})
92 | // if err != nil {
93 | // return value, fmt.Errorf("dao.GetTransactionsFeeVolume: %s", err.Error())
94 | // }
95 | // value.setValue(i, total.String())
96 | // }
97 | // return value, nil
98 | // },
99 | // },
100 | // {
101 | // title: dmodels.RangeStateHighestFee,
102 | // cacheDuration: time.Minute * 5,
103 | // fetcher: func() (value multiValue, err error) {
104 | // t := time.Now()
105 | // for i, r := range ranges {
106 | // total, err := s.dao.GetTransactionsHighestFee(filters.TimeRange{From: dmodels.NewTime(t.Add(-r))})
107 | // if err != nil {
108 | // return value, fmt.Errorf("dao.GetTransactionsHighestFee: %s", err.Error())
109 | // }
110 | // value.setValue(i, total.String())
111 | // }
112 | // return value, nil
113 | // },
114 | // },
115 | // {
116 | // title: dmodels.RangeStateUndelegationVolume,
117 | // cacheDuration: time.Minute * 5,
118 | // fetcher: func() (value multiValue, err error) {
119 | // t := time.Now()
120 | // for i, r := range ranges {
121 | // total, err := s.dao.GetUndelegationsVolume(filters.TimeRange{From: dmodels.NewTime(t.Add(-r))})
122 | // if err != nil {
123 | // return value, fmt.Errorf("dao.GetUndelegationsVolume: %s", err.Error())
124 | // }
125 | // value.setValue(i, total.String())
126 | // }
127 | // return value, nil
128 | // },
129 | // },
130 | // {
131 | // title: dmodels.RangeStateBlockDelay,
132 | // cacheDuration: time.Minute * 5,
133 | // fetcher: func() (value multiValue, err error) {
134 | // t := time.Now()
135 | // for i, r := range ranges {
136 | // total, err := s.dao.GetAvgBlocksDelay(filters.TimeRange{From: dmodels.NewTime(t.Add(-r))})
137 | // if err != nil {
138 | // return value, fmt.Errorf("dao.GetAvgBlocksDelay: %s", err.Error())
139 | // }
140 | // value.setValue(i, fmt.Sprintf("%f", total))
141 | // }
142 | // return value, nil
143 | // },
144 | // },
145 | // }
146 | //
147 | // items, err := s.dao.GetRangeStates(nil)
148 | // if err != nil {
149 | // log.Error("KeepRangeStates: dao.GetRangeStates: %s", err.Error())
150 | // return
151 | // }
152 | // mp := make(map[string]dmodels.RangeState)
153 | // for _, item := range items {
154 | // mp[item.Title] = item
155 | // }
156 | //
157 | // for {
158 | // for _, state := range states {
159 | // item, found := mp[state.title]
160 | // if found {
161 | // if time.Now().Sub(item.UpdatedAt) < state.cacheDuration {
162 | // continue
163 | // }
164 | // }
165 | // values, err := state.fetcher()
166 | // if err != nil {
167 | // log.Error("KeepRangeStates: %s", err.Error())
168 | // continue
169 | // }
170 | // model := dmodels.RangeState{
171 | // Title: state.title,
172 | // Value1d: values.Value1d,
173 | // Value7d: values.Value7d,
174 | // Value30d: values.Value30d,
175 | // Value90d: values.Value90d,
176 | // UpdatedAt: time.Now(),
177 | // }
178 | // if found {
179 | // err = s.dao.UpdateRangeState(model)
180 | // if err != nil {
181 | // log.Error("KeepRangeStates: UpdateRangeState %s", err.Error())
182 | // }
183 | // } else {
184 | // err = s.dao.CreateRangeState(model)
185 | // if err != nil {
186 | // log.Error("KeepRangeStates: CreateRangeState (%s) %s", state.title, err.Error())
187 | // }
188 | // }
189 | // if err != nil {
190 | // mp[state.title] = model
191 | // }
192 | // }
193 | //
194 | // <-time.After(time.Second * 5)
195 | // }
196 | //}
197 | //
198 | //func (s *ServiceFacade) GetNetworkStates() (states map[string]smodels.RangeState, err error) {
199 | // models, err := s.dao.GetRangeStates([]string{
200 | // dmodels.RangeStateNumberDelegators,
201 | // dmodels.RangeStateNumberMultiDelegators,
202 | // dmodels.RangeStateTransfersVolume,
203 | // dmodels.RangeStateFeeVolume,
204 | // dmodels.RangeStateHighestFee,
205 | // dmodels.RangeStateUndelegationVolume,
206 | // dmodels.RangeStateBlockDelay,
207 | // })
208 | // if err != nil {
209 | // return nil, fmt.Errorf("dao.GetRangeStates: %s", err.Error())
210 | // }
211 | // states = make(map[string]smodels.RangeState)
212 | // for _, model := range models {
213 | // states[model.Title] = smodels.RangeState{
214 | // D1: model.Value1d,
215 | // D7: model.Value7d,
216 | // D30: model.Value30d,
217 | // D90: model.Value90d,
218 | // }
219 | // }
220 | // return states, nil
221 | //}
222 |
--------------------------------------------------------------------------------
/api/api.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/everstake/cosmoscan-api/config"
7 | "github.com/everstake/cosmoscan-api/dao"
8 | "github.com/everstake/cosmoscan-api/dmodels"
9 | "github.com/everstake/cosmoscan-api/log"
10 | "github.com/everstake/cosmoscan-api/services"
11 | "github.com/gorilla/mux"
12 | "github.com/gorilla/schema"
13 | "github.com/rs/cors"
14 | "github.com/urfave/negroni"
15 | "go.uber.org/zap"
16 | "io/ioutil"
17 | "net/http"
18 | "reflect"
19 | "strconv"
20 | "time"
21 | )
22 |
23 | type API struct {
24 | dao dao.DAO
25 | cfg config.Config
26 | svc services.Services
27 | router *mux.Router
28 | queryDecoder *schema.Decoder
29 | }
30 |
31 | type errResponse struct {
32 | Error string `json:"error"`
33 | Msg string `json:"msg"`
34 | }
35 |
36 | func NewAPI(cfg config.Config, svc services.Services, dao dao.DAO) *API {
37 | sd := schema.NewDecoder()
38 | sd.IgnoreUnknownKeys(true)
39 | sd.RegisterConverter(dmodels.Time{}, func(s string) reflect.Value {
40 | timestamp, err := strconv.ParseInt(s, 10, 64)
41 | if err != nil {
42 | return reflect.Value{}
43 | }
44 | t := dmodels.NewTime(time.Unix(timestamp, 0))
45 | return reflect.ValueOf(t)
46 | })
47 | return &API{
48 | cfg: cfg,
49 | dao: dao,
50 | svc: svc,
51 | queryDecoder: sd,
52 | }
53 | }
54 |
55 | func (api *API) Title() string {
56 | return "API"
57 | }
58 |
59 | func (api *API) Run() error {
60 | api.router = mux.NewRouter()
61 | api.loadRoutes()
62 |
63 | http.Handle("/", api.router)
64 | log.Info("Listen API server on %s port", api.cfg.API.Port)
65 | err := http.ListenAndServe(fmt.Sprintf(":%s", api.cfg.API.Port), nil)
66 | if err != nil {
67 | return err
68 | }
69 | return nil
70 | }
71 |
72 | func (api *API) Stop() error {
73 | return nil
74 | }
75 |
76 | func (api *API) loadRoutes() {
77 |
78 | api.router = mux.NewRouter()
79 |
80 | api.router.
81 | PathPrefix("/static").
82 | Handler(http.StripPrefix("/static", http.FileServer(http.Dir("./resources/static"))))
83 |
84 | wrapper := negroni.New()
85 |
86 | wrapper.Use(cors.New(cors.Options{
87 | AllowedOrigins: api.cfg.API.AllowedHosts,
88 | AllowCredentials: true,
89 | AllowedMethods: []string{"POST", "GET", "OPTIONS", "PUT", "DELETE"},
90 | AllowedHeaders: []string{"Accept", "Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization", "X-User-Env", "Sec-Fetch-Mode"},
91 | }))
92 |
93 | // public
94 | HandleActions(api.router, wrapper, "", []*Route{
95 | {Path: "/", Method: http.MethodGet, Func: api.Index},
96 | {Path: "/health", Method: http.MethodGet, Func: api.Health},
97 | {Path: "/api", Method: http.MethodGet, Func: api.GetSwaggerAPI},
98 |
99 | {Path: "/meta", Method: http.MethodGet, Func: api.GetMetaData},
100 | {Path: "/historical-state", Method: http.MethodGet, Func: api.GetHistoricalState},
101 | {Path: "/transactions/fee/agg", Method: http.MethodGet, Func: api.GetAggTransactionsFee},
102 | {Path: "/transfers/volume/agg", Method: http.MethodGet, Func: api.GetAggTransfersVolume},
103 | {Path: "/operations/count/agg", Method: http.MethodGet, Func: api.GetAggOperationsCount},
104 | {Path: "/blocks/count/agg", Method: http.MethodGet, Func: api.GetAggBlocksCount},
105 | {Path: "/blocks/delay/agg", Method: http.MethodGet, Func: api.GetAggBlocksDelay},
106 | {Path: "/blocks/validators/uniq/agg", Method: http.MethodGet, Func: api.GetAggUniqBlockValidators},
107 | {Path: "/blocks/operations/agg", Method: http.MethodGet, Func: api.GetAvgOperationsPerBlock},
108 | {Path: "/delegations/volume/agg", Method: http.MethodGet, Func: api.GetAggDelegationsVolume},
109 | {Path: "/undelegations/volume/agg", Method: http.MethodGet, Func: api.GetAggUndelegationsVolume},
110 | {Path: "/unbonding/volume/agg", Method: http.MethodGet, Func: api.GetAggUnbondingVolume},
111 | {Path: "/bonded-ratio/agg", Method: http.MethodGet, Func: api.GetAggBondedRatio},
112 | {Path: "/network/stats", Method: http.MethodGet, Func: api.GetNetworkStats},
113 | {Path: "/staking/pie", Method: http.MethodGet, Func: api.GetStakingPie},
114 | {Path: "/proposals", Method: http.MethodGet, Func: api.GetProposals},
115 | {Path: "/proposals/votes", Method: http.MethodGet, Func: api.GetProposalVotes},
116 | {Path: "/proposals/deposits", Method: http.MethodGet, Func: api.GetProposalDeposits},
117 | {Path: "/proposals/chart", Method: http.MethodGet, Func: api.GetProposalChartData},
118 | {Path: "/validators", Method: http.MethodGet, Func: api.GetValidators},
119 | {Path: "/validators/33power/agg", Method: http.MethodGet, Func: api.GetAggValidators33Power},
120 | {Path: "/validators/top/proposed", Method: http.MethodGet, Func: api.GetTopProposedBlocksValidators},
121 | {Path: "/validators/top/jailed", Method: http.MethodGet, Func: api.GetMostJailedValidators},
122 | {Path: "/validators/fee/ranges", Method: http.MethodGet, Func: api.GetFeeRanges},
123 | {Path: "/validators/delegators/total", Method: http.MethodGet, Func: api.GetValidatorsDelegatorsTotal},
124 | {Path: "/accounts/whale/agg", Method: http.MethodGet, Func: api.GetAggWhaleAccounts},
125 | {Path: "/validator/{address}/balance", Method: http.MethodGet, Func: api.GetValidatorBalance},
126 | {Path: "/validator/{address}/delegations/agg", Method: http.MethodGet, Func: api.GetValidatorDelegationsAgg},
127 | {Path: "/validator/{address}/delegators/agg", Method: http.MethodGet, Func: api.GetValidatorDelegatorsAgg},
128 | {Path: "/validator/{address}/blocks/stats", Method: http.MethodGet, Func: api.GetValidatorBlocksStat},
129 | {Path: "/validator/{address}", Method: http.MethodGet, Func: api.GetValidator},
130 | {Path: "/validator/{address}/delegators", Method: http.MethodGet, Func: api.GetValidatorDelegators},
131 | {Path: "/blocks", Method: http.MethodGet, Func: api.GetBlocks},
132 | {Path: "/block/{height}", Method: http.MethodGet, Func: api.GetBlock},
133 | {Path: "/transactions", Method: http.MethodGet, Func: api.GetTransactions},
134 | {Path: "/transaction/{hash}", Method: http.MethodGet, Func: api.GetTransaction},
135 | {Path: "/account/{address}", Method: http.MethodGet, Func: api.GetAccount},
136 | })
137 |
138 | }
139 |
140 | func jsonData(writer http.ResponseWriter, data interface{}) {
141 | bytes, err := json.Marshal(data)
142 | if err != nil {
143 | writer.WriteHeader(500)
144 | writer.Write([]byte("can`t marshal json"))
145 | return
146 | }
147 | writer.Header().Set("Content-Type", "application/json")
148 | writer.Write(bytes)
149 | }
150 |
151 | func jsonError(writer http.ResponseWriter) {
152 | writer.WriteHeader(500)
153 | bytes, err := json.Marshal(errResponse{
154 | Error: "service_error",
155 | })
156 | if err != nil {
157 | writer.Write([]byte("can`t marshal json"))
158 | return
159 | }
160 | writer.Header().Set("Content-Type", "application/json")
161 | writer.Write(bytes)
162 | }
163 |
164 | func jsonBadRequest(writer http.ResponseWriter, msg string) {
165 | bytes, err := json.Marshal(errResponse{
166 | Error: "bad_request",
167 | Msg: msg,
168 | })
169 | if err != nil {
170 | writer.WriteHeader(500)
171 | writer.Write([]byte("can`t marshal json"))
172 | return
173 | }
174 | writer.Header().Set("Content-Type", "application/json")
175 | writer.WriteHeader(400)
176 | writer.Write(bytes)
177 | }
178 |
179 | func (api *API) GetSwaggerAPI(w http.ResponseWriter, r *http.Request) {
180 | body, err := ioutil.ReadFile("./resources/templates/swagger.html")
181 | if err != nil {
182 | log.Error("GetSwaggerAPI: ReadFile", zap.Error(err))
183 | return
184 | }
185 | _, err = w.Write(body)
186 | if err != nil {
187 | log.Error("GetSwaggerAPI: Write", zap.Error(err))
188 | return
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/services/parser/hub3/api.go:
--------------------------------------------------------------------------------
1 | package hub3
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/shopspring/decimal"
7 | "io/ioutil"
8 | "net/http"
9 | "net/url"
10 | "time"
11 | )
12 |
13 | const (
14 | SendMsg = "/cosmos.bank.v1beta1.MsgSend"
15 | MultiSendMsg = "/cosmos.bank.v1beta1.MsgMultiSend"
16 | DelegateMsg = "/cosmos.staking.v1beta1.MsgDelegate"
17 | UndelegateMsg = "/cosmos.staking.v1beta1.MsgUndelegate"
18 | BeginRedelegateMsg = "/cosmos.staking.v1beta1.MsgBeginRedelegate"
19 | WithdrawDelegationRewardMsg = "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward"
20 | WithdrawValidatorCommissionMsg = "/cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission"
21 | SubmitProposalMsg = "/cosmos.gov.v1beta1.MsgSubmitProposal"
22 | DepositMsg = "/cosmos.gov.v1beta1.MsgDeposit"
23 | VoteMsg = "/cosmos.gov.v1beta1.MsgVote"
24 | UnJailMsg = "/cosmos.slashing.v1beta1.MsgUnjail"
25 | )
26 |
27 | type (
28 | API struct {
29 | address string
30 | client *http.Client
31 | }
32 |
33 | Block struct {
34 | BlockID struct {
35 | Hash string `json:"hash"`
36 | Parts struct {
37 | Total int `json:"total"`
38 | Hash string `json:"hash"`
39 | } `json:"parts"`
40 | } `json:"block_id"`
41 | Block struct {
42 | Header struct {
43 | Version struct {
44 | Block uint64 `json:"block,string"`
45 | } `json:"version"`
46 | ChainID string `json:"chain_id"`
47 | Height uint64 `json:"height,string"`
48 | Time time.Time `json:"time"`
49 | LastBlockID struct {
50 | Hash string `json:"hash"`
51 | Parts struct {
52 | Total int `json:"total"`
53 | Hash string `json:"hash"`
54 | } `json:"parts"`
55 | } `json:"last_block_id"`
56 | ProposerAddress string `json:"proposer_address"`
57 | } `json:"header"`
58 | Data struct {
59 | Txs []string `json:"txs"`
60 | } `json:"data"`
61 | Evidence struct {
62 | Evidence []interface{} `json:"evidence"`
63 | } `json:"evidence"`
64 | LastCommit struct {
65 | Height string `json:"height"`
66 | Round int `json:"round"`
67 | BlockID struct {
68 | Hash string `json:"hash"`
69 | Parts struct {
70 | Total int `json:"total"`
71 | Hash string `json:"hash"`
72 | } `json:"parts"`
73 | } `json:"block_id"`
74 | Signatures []struct {
75 | ValidatorAddress string `json:"validator_address"`
76 | }
77 | } `json:"last_commit"`
78 | } `json:"block"`
79 | }
80 |
81 | Tx struct {
82 | Tx struct {
83 | Body struct {
84 | Messages []json.RawMessage `json:"messages"`
85 | Memo string `json:"memo"`
86 | } `json:"body"`
87 | AuthInfo struct {
88 | SignerInfos []struct {
89 | PublicKey struct {
90 | Type string `json:"@type"`
91 | Key string `json:"key"`
92 | } `json:"public_key"`
93 | } `json:"signer_infos"`
94 | Fee struct {
95 | Amount []Amount `json:"amount"`
96 | GasLimit uint64 `json:"gas_limit,string"`
97 | Payer string `json:"payer"`
98 | Granter string `json:"granter"`
99 | } `json:"fee"`
100 | Signatures []string `json:"signatures"`
101 | } `json:"auth_info"`
102 | } `json:"tx"`
103 | TxResponse struct {
104 | Height uint64 `json:"height,string"`
105 | Hash string `json:"txhash"`
106 | Data string `json:"data"`
107 | RawLog string `json:"raw_log"`
108 | Code int64 `json:"code"`
109 | Logs []struct {
110 | Events []struct {
111 | Type string `json:"type"`
112 | Attributes []struct {
113 | Key string `json:"key"`
114 | Value string `json:"value"`
115 | } `json:"attributes"`
116 | } `json:"events"`
117 | } `json:"logs"`
118 | GasWanted uint64 `json:"gas_wanted,string"`
119 | GasUsed uint64 `json:"gas_used,string"`
120 | Tx struct {
121 | Type string `json:"@type"`
122 | Body struct {
123 | Messages []json.RawMessage `json:"messages"`
124 | Memo string `json:"memo"`
125 | } `json:"body"`
126 | } `json:"tx"`
127 | Timestamp time.Time `json:"timestamp"`
128 | } `json:"tx_response"`
129 | }
130 |
131 | BaseMsg struct {
132 | Type string `json:"@type"`
133 | }
134 | Amount struct {
135 | Denom string `json:"denom"`
136 | Amount decimal.Decimal `json:"amount"`
137 | }
138 |
139 | MsgSend struct {
140 | FromAddress string `json:"from_address,omitempty"`
141 | ToAddress string `json:"to_address,omitempty"`
142 | Amount []Amount `json:"amount"`
143 | }
144 | MsgMultiSendValue struct {
145 | Inputs []struct {
146 | Address string `json:"address"`
147 | Coins []Amount `json:"coins"`
148 | } `json:"inputs"`
149 | Outputs []struct {
150 | Address string `json:"address"`
151 | Coins []Amount `json:"coins"`
152 | } `json:"outputs"`
153 | }
154 | MsgDelegate struct {
155 | DelegatorAddress string `json:"delegator_address"`
156 | ValidatorAddress string `json:"validator_address"`
157 | Amount Amount `json:"amount"`
158 | }
159 | MsgUndelegate struct {
160 | DelegatorAddress string `json:"delegator_address"`
161 | ValidatorAddress string `json:"validator_address"`
162 | Amount Amount `json:"amount"`
163 | }
164 | MsgBeginRedelegate struct {
165 | DelegatorAddress string `json:"delegator_address"`
166 | ValidatorSrcAddress string `json:"validator_src_address"`
167 | ValidatorDstAddress string `json:"validator_dst_address"`
168 | Amount Amount `json:"amount"`
169 | }
170 | MsgWithdrawDelegationReward struct {
171 | DelegatorAddress string `json:"delegator_address"`
172 | ValidatorAddress string `json:"validator_address"`
173 | }
174 | MsgWithdrawDelegationRewardsAll struct {
175 | DelegatorAddress string `json:"delegator_address"`
176 | }
177 | MsgWithdrawValidatorCommission struct {
178 | ValidatorAddress string `json:"validator_address"`
179 | }
180 | MsgSubmitProposal struct {
181 | Content struct {
182 | Type string `json:"type"`
183 | Value struct {
184 | Title string `json:"title"`
185 | Description string `json:"description"`
186 | Recipient string `json:"recipient"`
187 | Amount []Amount `json:"amount"`
188 | } `json:"value"`
189 | } `json:"content"`
190 | InitialDeposit []Amount `json:"initial_deposit"`
191 | Proposer string `json:"proposer"`
192 | }
193 | MsgDeposit struct {
194 | ProposalID uint64 `json:"proposal_id,string"`
195 | Depositor string `json:"depositor" `
196 | Amount []Amount `json:"amount" `
197 | }
198 | MsgVote struct {
199 | ProposalID uint64 `json:"proposal_id,string"`
200 | Voter string `json:"voter"`
201 | Option string `json:"option"`
202 | }
203 | MsgUnjail struct {
204 | ValidatorAddr string `json:"validator_addr"`
205 | }
206 |
207 | TxsFilter struct {
208 | Limit uint64
209 | Page uint64
210 | Height uint64
211 | MinHeight uint64
212 | MaxHeight uint64
213 | }
214 |
215 | Validatorsets struct {
216 | Validators []struct {
217 | Address string `json:"address"`
218 | PubKey struct {
219 | Type string `json:"@type"`
220 | Key string `json:"key"`
221 | } `json:"pub_key"`
222 | VotingPower decimal.Decimal `json:"voting_power"`
223 | } `json:"validators"`
224 | }
225 | )
226 |
227 | func NewAPI(address string) *API {
228 | return &API{
229 | address: address,
230 | client: &http.Client{
231 | Timeout: time.Minute,
232 | },
233 | }
234 | }
235 |
236 | func (api *API) GetBlock(height uint64) (block Block, err error) {
237 | endpoint := fmt.Sprintf("blocks/%d", height)
238 | err = api.get(endpoint, nil, &block)
239 | return block, err
240 | }
241 |
242 | func (api *API) GetLatestBlock() (block Block, err error) {
243 | err = api.get("blocks/latest", nil, &block)
244 | return block, err
245 | }
246 |
247 | func (api *API) GetTx(hash string) (tx Tx, err error) {
248 | endpoint := fmt.Sprintf("cosmos/tx/v1beta1/txs/%s", hash)
249 | err = api.get(endpoint, nil, &tx)
250 | return tx, err
251 | }
252 |
253 | func (api *API) get(endpoint string, params map[string]string, result interface{}) error {
254 | fullURL := fmt.Sprintf("%s/%s", api.address, endpoint)
255 | if len(params) != 0 {
256 | values := url.Values{}
257 | for key, value := range params {
258 | values.Add(key, value)
259 | }
260 | fullURL = fmt.Sprintf("%s?%s", fullURL, values.Encode())
261 | }
262 | resp, err := api.client.Get(fullURL)
263 | if err != nil {
264 | return fmt.Errorf("client.Get: %s", err.Error())
265 | }
266 | defer resp.Body.Close()
267 | if resp.StatusCode != http.StatusOK {
268 | d, _ := ioutil.ReadAll(resp.Body)
269 | text := string(d)
270 | if len(text) > 150 {
271 | text = text[:150]
272 | }
273 | return fmt.Errorf("bad status: %d, %s", resp.StatusCode, text)
274 | }
275 | data, err := ioutil.ReadAll(resp.Body)
276 | if err != nil {
277 | return fmt.Errorf("ioutil.ReadAll: %s", err.Error())
278 | }
279 | err = json.Unmarshal(data, result)
280 | if err != nil {
281 | return fmt.Errorf("json.Unmarshal: %s", err.Error())
282 | }
283 | return nil
284 | }
285 |
286 | func (api *API) GetValidatorset(height uint64) (set Validatorsets, err error) {
287 | err = api.get(fmt.Sprintf("cosmos/base/tendermint/v1beta1/validatorsets/%d", height), nil, &set)
288 | return set, err
289 | }
290 |
--------------------------------------------------------------------------------