├── .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 |