├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── api ├── account.go ├── api.go ├── blocks.go ├── common.go ├── delegations.go ├── historical_state.go ├── meta_data.go ├── proposals.go ├── router.go ├── stats.go ├── transactions.go ├── transfers.go └── validators.go ├── config.example.json ├── config └── config.go ├── dao ├── cache │ └── gocache.go ├── clickhouse │ ├── accounts.go │ ├── balance_updates.go │ ├── blocks.go │ ├── clickhouse.go │ ├── delegations.go │ ├── historical_states.go │ ├── history_proposals.go │ ├── jailers.go │ ├── migrations │ │ ├── 001_blocks.down.sql │ │ ├── 001_blocks.up.sql │ │ ├── 002_delegations.down.sql │ │ ├── 002_delegations.up.sql │ │ ├── 003_transactions.down.sql │ │ ├── 003_transactions.up.sql │ │ ├── 004_transfers.down.sql │ │ ├── 004_transfers.up.sql │ │ ├── 005_delegator_rewards.down.sql │ │ ├── 005_delegator_rewards.up.sql │ │ ├── 006_validator_rewards.down.sql │ │ ├── 006_validator_rewards.up.sql │ │ ├── 007_history_proposals.down.sql │ │ ├── 007_history_proposals.up.sql │ │ ├── 008_proposal_deposits.down.sql │ │ ├── 008_proposal_deposits.up.sql │ │ ├── 009_proposal_votes.down.sql │ │ ├── 009_proposal_votes.up.sql │ │ ├── 010_historical_states.down.sql │ │ ├── 010_historical_states.up.sql │ │ ├── 011_balance_updates.down.sql │ │ ├── 011_balance_updates.up.sql │ │ ├── 012_jailers.down.sql │ │ ├── 012_jailers.up.sql │ │ ├── 013_stats.down.sql │ │ ├── 013_stats.up.sql │ │ ├── 014_missed_blocks.down.sql │ │ ├── 014_missed_blocks.up.sql │ │ ├── 015_account_txs.down.sql │ │ └── 015_account_txs.up.sql │ ├── missed_blocks.go │ ├── proposal_deposits.go │ ├── proposal_votes.go │ ├── rewards.go │ ├── stats.go │ ├── transactions.go │ └── transfers.go ├── dao.go ├── derrors │ └── dao_errors.go ├── filters │ ├── accounts.go │ ├── agg.go │ ├── balance_updates.go │ ├── blocks.go │ ├── delegations.go │ ├── historical_state.go │ ├── history_proposals.go │ ├── missed_blocks.go │ ├── proposal_deposits.go │ ├── proposal_votes.go │ ├── proposals.go │ ├── states.go │ ├── time_range.go │ ├── transactions.go │ └── voting_power.go └── mysql │ ├── accounts.go │ ├── main.go │ ├── migrations │ └── init.sql │ ├── parsers.go │ ├── proposals.go │ └── validators.go ├── dmodels ├── account.go ├── account_tx.go ├── balance_update.go ├── block.go ├── delegation.go ├── delegator_reward.go ├── historical_state.go ├── history_proposal.go ├── jailer.go ├── missed_block.go ├── parser.go ├── proposal.go ├── proposal_deposit.go ├── proposal_vote.go ├── range_state.go ├── stat.go ├── time.go ├── time_test.go ├── transaction.go ├── transfer.go ├── validator.go ├── validator_delegator.go ├── validator_reward.go └── validator_value.go ├── docker-compose.example.yml ├── docker ├── .env.example ├── .gitignore └── clickhouse-users.xml.example ├── go.mod ├── go.sum ├── log └── log.go ├── main.go ├── resources ├── static │ ├── api.yaml │ ├── swagger-ui-bundle.js │ ├── swagger-ui-standalone-preset.js │ └── swagger-ui.css └── templates │ └── swagger.html ├── services ├── accounts.go ├── blocks.go ├── cmc │ └── cmc.go ├── coingecko │ └── coingecko.go ├── delegations.go ├── grafana.go ├── helpers │ ├── codding.go │ └── public_key.go ├── historical_states.go ├── meta_data.go ├── modules │ └── modules.go ├── node │ └── api.go ├── parser │ └── hub3 │ │ ├── api.go │ │ ├── genesis.go │ │ └── parser.go ├── proposals.go ├── range_states.go ├── scheduler │ └── scheduler.go ├── services.go ├── stats.go ├── transactions.go ├── transfers.go └── validators.go └── smodels ├── accounts.go ├── agg_data.go ├── balance.go ├── block.go ├── fee_range.go ├── historical_state.go ├── meta_data.go ├── paginatable.go ├── pie_item.go ├── proposal_chart_data.go ├── proposal_vote.go ├── tx.go ├── validator.go └── validator_blocks_stat.go /.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 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14 2 | WORKDIR /app 3 | COPY . /app 4 | RUN go build -o app . 5 | CMD ["./app"] 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /dao/clickhouse/migrations/001_blocks.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS blocks; 2 | -------------------------------------------------------------------------------- /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/002_delegations.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS delegations; 2 | -------------------------------------------------------------------------------- /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/003_transactions.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS transactions; 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /dao/clickhouse/migrations/004_transfers.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS transfers; 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /dao/clickhouse/migrations/005_delegator_rewards.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS delegator_rewards; 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /dao/clickhouse/migrations/006_validator_rewards.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS validator_rewards; 2 | -------------------------------------------------------------------------------- /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/clickhouse/migrations/007_history_proposals.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS history_proposals; 2 | -------------------------------------------------------------------------------- /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); -------------------------------------------------------------------------------- /dao/clickhouse/migrations/008_proposal_deposits.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS proposal_deposits; 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /dao/clickhouse/migrations/009_proposal_votes.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS proposal_votes; 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /dao/clickhouse/migrations/010_historical_states.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS historical_states; 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /dao/clickhouse/migrations/011_balance_updates.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS balance_updates; 2 | -------------------------------------------------------------------------------- /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/012_jailers.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS jailers; 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /dao/clickhouse/migrations/013_stats.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS stats; 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /dao/clickhouse/migrations/014_missed_blocks.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS missed_blocks; 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /dao/clickhouse/migrations/015_account_txs.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS account_txs; 2 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /dao/derrors/dao_errors.go: -------------------------------------------------------------------------------- 1 | package derrors 2 | 3 | const ( 4 | ErrNotFound = "ERR_NOT_FOUND" 5 | ErrDuplicate = "ERR_DUPLICATE" 6 | ) 7 | -------------------------------------------------------------------------------- /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/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/filters/balance_updates.go: -------------------------------------------------------------------------------- 1 | package filters 2 | 3 | type BalanceUpdates struct { 4 | Limit uint64 5 | Offset uint64 6 | } 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /dao/filters/historical_state.go: -------------------------------------------------------------------------------- 1 | package filters 2 | 3 | type HistoricalState struct { 4 | Limit uint64 5 | Offset uint64 6 | } 7 | -------------------------------------------------------------------------------- /dao/filters/history_proposals.go: -------------------------------------------------------------------------------- 1 | package filters 2 | 3 | type HistoryProposals struct { 4 | ID []uint64 5 | Limit uint64 6 | Offset uint64 7 | } 8 | -------------------------------------------------------------------------------- /dao/filters/missed_blocks.go: -------------------------------------------------------------------------------- 1 | package filters 2 | 3 | type MissedBlocks struct { 4 | Validators []string 5 | } 6 | -------------------------------------------------------------------------------- /dao/filters/proposal_deposits.go: -------------------------------------------------------------------------------- 1 | package filters 2 | 3 | type ProposalDeposits struct { 4 | ProposalID []uint64 `schema:"proposal_id"` 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/filters/voting_power.go: -------------------------------------------------------------------------------- 1 | package filters 2 | 3 | type VotingPower struct { 4 | TimeRange 5 | Delegators []string 6 | Validators []string 7 | } 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docker/.env.example: -------------------------------------------------------------------------------- 1 | DB_NAME=cosmos 2 | DB_USER=root 3 | DB_PASSWORD=secret 4 | DB_HOST=mysql 5 | -------------------------------------------------------------------------------- /docker/.gitignore: -------------------------------------------------------------------------------- 1 | clickhouse-users.xml 2 | .env 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /smodels/paginatable.go: -------------------------------------------------------------------------------- 1 | package smodels 2 | 3 | type PaginatableResponse struct { 4 | Items interface{} `json:"items"` 5 | Total uint64 `json:"total"` 6 | } 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------