├── VERSION ├── tools ├── market │ ├── tickers.go │ └── market.go ├── filter.go ├── recovery │ └── recovery.go ├── cache │ ├── cache_item.go │ └── cache.go ├── http.go ├── metrics │ └── block.go └── pagination.go ├── helpers ├── error.go ├── main.go ├── math.go └── minter.go ├── .deploy ├── templates │ ├── serviceaccount.yaml │ ├── configmap.yaml │ ├── service.yaml │ ├── ingress.yaml │ ├── _helpers.tpl │ └── deployment.yaml ├── Chart.yaml ├── .helmignore └── values-default.yaml ├── .env.prod ├── api ├── v2 │ ├── status │ │ └── routes.go │ ├── checks │ │ ├── routes.go │ │ └── handlers.go │ ├── statistics │ │ ├── routes.go │ │ └── handler.go │ ├── transactions │ │ └── routes.go │ ├── blocks │ │ ├── routes.go │ │ └── handler.go │ ├── coins │ │ └── routes.go │ ├── validators │ │ ├── routes.go │ │ └── helpers.go │ ├── addresses │ │ ├── routes.go │ │ └── utils.go │ ├── routes.go │ └── pools │ │ ├── routes.go │ │ └── proxy.go ├── validators │ ├── minter_hash.go │ ├── minter_pub_key.go │ ├── timestamp.go │ ├── minter_address.go │ └── minter_address_test.go └── api.go ├── address ├── types.go ├── repository.go ├── resource.go └── service.go ├── invalid_transaction ├── data_resources │ ├── redeem_check.go │ ├── lock_stake.go │ ├── set_halt_block.go │ ├── edit_coin_owner.go │ ├── multisend.go │ ├── edit_commission.go │ ├── burn_token.go │ ├── edit_candidate_public_key.go │ ├── mint_token.go │ ├── send.go │ ├── set_candidate.go │ ├── unbond.go │ ├── edit_multisig_owners.go │ ├── delegate.go │ ├── lock.go │ ├── create_multisig.go │ ├── edit_candidate.go │ ├── move_stake.go │ ├── sell_all_coin.go │ ├── add_liquidity.go │ ├── declare_candidacy.go │ ├── buy_coin.go │ ├── sell_coin.go │ ├── create_token.go │ ├── recreate_token.go │ ├── create_coin.go │ ├── recreate_coin.go │ ├── remove_liquidity.go │ └── coin.go └── repository.go ├── transaction ├── data_resources │ ├── lock_stake.go │ ├── set_halt_block.go │ ├── edit_coin_owner.go │ ├── edit_commission.go │ ├── edit_candidate_public_key.go │ ├── burn_token.go │ ├── mint_token.go │ ├── send.go │ ├── set_candidate.go │ ├── unbond.go │ ├── delegate.go │ ├── edit_multisig_owners.go │ ├── lock.go │ ├── edit_candidate.go │ ├── move_stake.go │ ├── declare_candidacy.go │ ├── create_multisig.go │ ├── create_token.go │ ├── recreate_token.go │ ├── multisend.go │ ├── sell_coin.go │ ├── buy_coin.go │ ├── sell_all_coin.go │ ├── add_liquidity.go │ ├── create_coin.go │ ├── recreate_coin.go │ ├── remove_liquidity.go │ ├── redeem_check.go │ └── coin.go ├── data_models │ └── check.go └── types.go ├── order ├── types.go ├── helpers.go ├── models.go ├── repository.go ├── resource.go └── filters.go ├── Dockerfile ├── unbond ├── models.go ├── resource.go └── repository.go ├── .gitignore ├── aggregated_reward ├── filter.go └── resource.go ├── events ├── filters.go └── resource.go ├── blocks ├── filters.go ├── repository.go └── resource.go ├── coingecko ├── service.go └── api.go ├── chart ├── filters.go ├── tx_resource.go └── reward_resource.go ├── check ├── filters.go ├── repository.go └── resource.go ├── core ├── config │ └── config.go ├── ws │ ├── ws.go │ └── blocks.go └── enviroment.go ├── pool ├── helpers.go └── models.go ├── hub └── client.go ├── balance ├── resource.go └── service.go ├── stake ├── resource.go ├── service.go └── repository.go ├── delegation └── resource.go ├── database └── main.go ├── slash ├── resource.go └── repository.go ├── resource ├── main.go └── pagination.go ├── .github └── workflows │ ├── stage.yml │ ├── mainnet.yml │ ├── taconet.yml │ ├── testnet.yml │ └── do-mainnet.yml ├── reward └── repository.go ├── README.md ├── errors └── main.go ├── services └── validator.go ├── cmd └── explorer.go ├── coins └── resource.go ├── validator └── repository.go └── go.mod /VERSION: -------------------------------------------------------------------------------- 1 | v2.0.0 -------------------------------------------------------------------------------- /tools/market/tickers.go: -------------------------------------------------------------------------------- 1 | package market 2 | 3 | const USDTicker = "USD" 4 | -------------------------------------------------------------------------------- /tools/filter.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import "github.com/go-pg/pg/v10/orm" 4 | 5 | type Filter interface { 6 | Filter(q *orm.Query) (*orm.Query, error) 7 | } 8 | -------------------------------------------------------------------------------- /helpers/error.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | func CheckErr(err error) { 8 | if err != nil { 9 | log.Panic(err) 10 | } 11 | } 12 | 13 | func CheckErrBool(ok bool) { 14 | if !ok { 15 | log.Panic(ok) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.deploy/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "chart.serviceAccountName" . }} 6 | labels: 7 | {{ include "chart.labels" . | nindent 4 }} 8 | {{- end -}} 9 | -------------------------------------------------------------------------------- /.env.prod: -------------------------------------------------------------------------------- 1 | EXPLORER_DEBUG= 2 | APP_BASE_COIN= 3 | CENTRIFUGO_LINK= 4 | CENTRIFUGO_BLOCK_CHANNEL= 5 | DB_HOST= 6 | DB_PORT= 7 | DB_POOL_SIZE= 8 | DB_NAME= 9 | DB_USER= 10 | DB_PASSWORD= 11 | POSTGRES_SSL_ENABLED= 12 | EXPLORER_PORT= 13 | MARKET_HOST= 14 | POSTGRES_SSL_ENABLED= -------------------------------------------------------------------------------- /.deploy/Chart.yaml: -------------------------------------------------------------------------------- 1 | version: 0.1.1 2 | apiVersion: v2 3 | 4 | name: minter-explorer-api 5 | description: A Helm chart for Minter Explorer Api 6 | 7 | appVersion: latest 8 | 9 | home: https://github.com/MinterTeam/minter-explorer-api 10 | keywords: 11 | - minter-explorer-api 12 | -------------------------------------------------------------------------------- /api/v2/status/routes.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | // ApplyRoutes applies router to the gin Engine 6 | func ApplyRoutes(r *gin.RouterGroup) { 7 | r.GET("/status", GetStatus) 8 | r.GET("/status-page", GetStatusPage) 9 | r.GET("/info", GetInfo) 10 | } 11 | -------------------------------------------------------------------------------- /api/v2/checks/routes.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | // ApplyRoutes applies router to the gin Engine 6 | func ApplyRoutes(r *gin.RouterGroup) { 7 | checks := r.Group("/checks") 8 | { 9 | checks.GET("", GetChecks) 10 | checks.GET("/:raw", GetCheck) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /api/v2/statistics/routes.go: -------------------------------------------------------------------------------- 1 | package statistics 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | // ApplyRoutes applies router to the gin Engine 6 | func ApplyRoutes(r *gin.RouterGroup) { 7 | statistics := r.Group("/statistics") 8 | { 9 | statistics.GET("/transactions", GetTransactions) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.deploy/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "chart.fullname" . }} 5 | labels: 6 | {{- include "chart.labels" . | nindent 4 }} 7 | data: 8 | .env: |- 9 | {{ range $key, $value := .Values.env }} 10 | {{ $key }}={{ $value | quote }} 11 | {{- end -}} -------------------------------------------------------------------------------- /tools/recovery/recovery.go: -------------------------------------------------------------------------------- 1 | package recovery 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | "runtime/debug" 6 | ) 7 | 8 | func SafeGo(f func()) { 9 | defer func() { 10 | if err := recover(); err != nil { 11 | log.WithField("stacktrace", string(debug.Stack())).Error(err) 12 | } 13 | }() 14 | 15 | f() 16 | } 17 | -------------------------------------------------------------------------------- /address/types.go: -------------------------------------------------------------------------------- 1 | package address 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 5 | "math/big" 6 | ) 7 | 8 | type Balance struct { 9 | Model *models.Address 10 | TotalBalanceSum *big.Int 11 | TotalBalanceSumUSD *big.Float 12 | StakeBalanceSum *big.Int 13 | StakeBalanceSumUSD *big.Float 14 | } 15 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/redeem_check.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | ) 6 | 7 | type RedeemCheck struct { 8 | } 9 | 10 | func (RedeemCheck) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 11 | return RedeemCheck{} 12 | } 13 | -------------------------------------------------------------------------------- /api/v2/transactions/routes.go: -------------------------------------------------------------------------------- 1 | package transactions 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | // ApplyRoutes applies router to the gin Engine 6 | func ApplyRoutes(r *gin.RouterGroup) { 7 | transactions := r.Group("/transactions") 8 | { 9 | transactions.GET("", GetTransactions) 10 | transactions.GET("/:hash", GetTransaction) 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /api/v2/blocks/routes.go: -------------------------------------------------------------------------------- 1 | package blocks 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | // ApplyRoutes applies router to the gin Engine 8 | func ApplyRoutes(r *gin.RouterGroup) { 9 | blocks := r.Group("/blocks") 10 | { 11 | blocks.GET("", GetBlocks) 12 | blocks.GET("/:height", GetBlock) 13 | blocks.GET("/:height/transactions", GetBlockTransactions) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /api/validators/minter_hash.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "github.com/go-playground/validator/v10" 5 | "regexp" 6 | ) 7 | 8 | func MinterTxHash(fl validator.FieldLevel) bool { 9 | return isValidMinterHash(fl.Field().Interface().(string)) 10 | } 11 | 12 | func isValidMinterHash(hash string) bool { 13 | return regexp.MustCompile("^Mt([A-Fa-f0-9]{64})$").MatchString(hash) 14 | } 15 | -------------------------------------------------------------------------------- /transaction/data_resources/lock_stake.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | ) 6 | 7 | type LockStakeData struct{} 8 | 9 | func (LockStakeData) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 10 | //data := txData.(*api_pb.LockStakeData) 11 | return LockStakeData{} 12 | } 13 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/lock_stake.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | ) 6 | 7 | type LockStakeData struct{} 8 | 9 | func (LockStakeData) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 10 | //data := txData.(*api_pb.LockStakeData) 11 | return LockStakeData{} 12 | } 13 | -------------------------------------------------------------------------------- /api/validators/minter_pub_key.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "github.com/go-playground/validator/v10" 5 | "regexp" 6 | ) 7 | 8 | func MinterPublicKey(fl validator.FieldLevel) bool { 9 | return isValidMinterPublicKey(fl.Field().Interface().(string)) 10 | } 11 | 12 | func isValidMinterPublicKey(publicKey string) bool { 13 | return regexp.MustCompile("^Mp([A-Fa-f0-9]{64})$").MatchString(publicKey) 14 | } 15 | -------------------------------------------------------------------------------- /api/v2/coins/routes.go: -------------------------------------------------------------------------------- 1 | package coins 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | // ApplyRoutes applies router to the gin Engine 8 | func ApplyRoutes(r *gin.RouterGroup) { 9 | coins := r.Group("/coins") 10 | { 11 | coins.GET("", GetCoins) 12 | coins.GET("symbol/:symbol", GetCoinBySymbol) 13 | coins.GET("id/:id", GetCoinByID) 14 | coins.GET("oracle/verified", GetOracleVerifiedCoins) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /order/types.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | type Type string 4 | type Status string 5 | 6 | const ( 7 | OrderTypeBuy Type = "buy" 8 | OrderTypeSell Type = "sell" 9 | 10 | OrderStatusActive Status = "active" 11 | OrderStatusPartiallyFilled Status = "partially_filled" 12 | OrderStatusFilled Status = "filled" 13 | OrderStatusCanceled Status = "canceled" 14 | OrderStatusExpired Status = "expired" 15 | ) 16 | -------------------------------------------------------------------------------- /.deploy/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "chart.fullname" . }} 5 | labels: 6 | {{- include "chart.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "chart.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /transaction/data_models/check.go: -------------------------------------------------------------------------------- 1 | package data_models 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 5 | "math/big" 6 | ) 7 | 8 | type Check struct { 9 | RawCheck string 10 | Proof string 11 | Check CheckData 12 | } 13 | 14 | type CheckData struct { 15 | Coin models.Coin 16 | GasCoin models.Coin 17 | Nonce []byte 18 | Value *big.Int 19 | Sender string 20 | DueBlock uint64 21 | } 22 | -------------------------------------------------------------------------------- /.deploy/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-alpine as builder 2 | 3 | WORKDIR /app 4 | COPY ./ /app 5 | RUN apk add --no-cache make gcc musl-dev linux-headers 6 | RUN go mod tidy 7 | RUN go build -o ./builds/linux/explorer ./cmd/explorer.go 8 | 9 | FROM alpine:3.17 10 | 11 | COPY --from=builder /app/builds/linux/explorer /usr/bin/explorer 12 | RUN addgroup minteruser && adduser -D -h /minter -G minteruser minteruser 13 | USER minteruser 14 | WORKDIR /minter 15 | ENTRYPOINT ["/usr/bin/explorer"] 16 | CMD ["explorer"] -------------------------------------------------------------------------------- /tools/cache/cache_item.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type CacheItem struct { 8 | value interface{} // cached value 9 | btl *uint64 // expiration block id 10 | ttl *time.Time // expiration time 11 | } 12 | 13 | func (c *CacheItem) IsExpired(currentBlock uint64) bool { 14 | if c.btl != nil && currentBlock <= *c.btl { 15 | return false 16 | } 17 | 18 | if c.ttl != nil && time.Now().Before(*c.ttl) { 19 | return false 20 | } 21 | 22 | return true 23 | } 24 | -------------------------------------------------------------------------------- /unbond/models.go: -------------------------------------------------------------------------------- 1 | package unbond 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 5 | "time" 6 | ) 7 | 8 | type UnbondMoveStake struct { 9 | BlockId uint 10 | CoinId uint 11 | ValidatorId uint 12 | ToValidatorId *uint 13 | Value string 14 | CreatedAt time.Time 15 | MinterAddress string 16 | Address *models.Address 17 | Coin *models.Coin 18 | FromValidator *models.Validator 19 | ToValidator *models.Validator 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | builds/ 17 | vendor/ 18 | tmp/ 19 | media/ 20 | 21 | config.json 22 | values-prod.yaml 23 | values.*.yaml 24 | .env 25 | go.sum 26 | *.private.* 27 | *.tmp.* 28 | -------------------------------------------------------------------------------- /aggregated_reward/filter.go: -------------------------------------------------------------------------------- 1 | package aggregated_reward 2 | 3 | import "github.com/go-pg/pg/v10/orm" 4 | 5 | type SelectFilter struct { 6 | Address string 7 | StartTime *string 8 | EndTime *string 9 | } 10 | 11 | func (f SelectFilter) Filter(q *orm.Query) (*orm.Query, error) { 12 | if f.StartTime != nil { 13 | q = q.Where("time_id >= ?", f.StartTime) 14 | } 15 | 16 | if f.EndTime != nil { 17 | q = q.Where("time_id <= ?", f.EndTime) 18 | } 19 | 20 | return q.Where("address.address = ?", f.Address), nil 21 | } 22 | -------------------------------------------------------------------------------- /events/filters.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/blocks" 5 | "github.com/go-pg/pg/v10/orm" 6 | ) 7 | 8 | type SelectFilter struct { 9 | Address string 10 | StartBlock *string 11 | EndBlock *string 12 | } 13 | 14 | func (f SelectFilter) Filter(q *orm.Query) (*orm.Query, error) { 15 | blocksRange := blocks.RangeSelectFilter{StartBlock: f.StartBlock, EndBlock: f.EndBlock} 16 | 17 | return q.Where("address.address = ?", f.Address).Apply(blocksRange.Filter), nil 18 | } 19 | -------------------------------------------------------------------------------- /blocks/filters.go: -------------------------------------------------------------------------------- 1 | package blocks 2 | 3 | import ( 4 | "github.com/go-pg/pg/v10/orm" 5 | ) 6 | 7 | // TODO: replace string to int 8 | type RangeSelectFilter struct { 9 | Prefix string 10 | StartBlock *string 11 | EndBlock *string 12 | } 13 | 14 | func (f RangeSelectFilter) Filter(q *orm.Query) (*orm.Query, error) { 15 | if f.StartBlock != nil { 16 | q = q.Where(f.Prefix+"block_id >= ?", f.StartBlock) 17 | } 18 | 19 | if f.EndBlock != nil { 20 | q = q.Where(f.Prefix+"block_id <= ?", f.EndBlock) 21 | } 22 | 23 | return q, nil 24 | } 25 | -------------------------------------------------------------------------------- /api/validators/timestamp.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "github.com/go-playground/validator/v10" 5 | "time" 6 | ) 7 | 8 | func Timestamp(fl validator.FieldLevel) bool { 9 | timestamp := fl.Field().Interface().(string) 10 | _, err := time.Parse("2006-01-02", timestamp) 11 | if err == nil { 12 | return true 13 | } 14 | 15 | _, err = time.Parse("2006-01-02 15:04:05", timestamp) 16 | if err == nil { 17 | return true 18 | } 19 | 20 | _, err = time.Parse(time.RFC3339, timestamp) 21 | if err == nil { 22 | return true 23 | } 24 | 25 | return false 26 | } 27 | -------------------------------------------------------------------------------- /coingecko/service.go: -------------------------------------------------------------------------------- 1 | package coingecko 2 | 3 | import "strings" 4 | 5 | type CoinGeckoService struct { 6 | api *Api 7 | } 8 | 9 | func NewService(host string) *CoinGeckoService { 10 | return &CoinGeckoService{ 11 | api: NewApi(host), 12 | } 13 | } 14 | 15 | func (s *CoinGeckoService) GetPrice(from string, to string) (float64, float64, error) { 16 | from, to = strings.ToLower(from), strings.ToLower(to) 17 | resp, err := s.api.GetSimplePrice(from, to) 18 | if err != nil { 19 | return 0, 0, err 20 | } 21 | 22 | return resp[from][to], resp[from][to+"_24h_change"], nil 23 | } 24 | -------------------------------------------------------------------------------- /chart/filters.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import "github.com/go-pg/pg/v10/orm" 4 | 5 | type SelectFilter struct { 6 | Scale string 7 | StartTime *string 8 | EndTime *string 9 | } 10 | 11 | func (f SelectFilter) Filter(q *orm.Query) (*orm.Query, error) { 12 | if f.StartTime != nil { 13 | q = q.Where("block.created_at >= ?", f.StartTime) 14 | } 15 | 16 | if f.EndTime != nil { 17 | q = q.Where("block.created_at <= ?", f.EndTime) 18 | } 19 | 20 | return q.Relation("Block._").ColumnExpr("date_trunc(?, block.created_at) as time", f.Scale).Group("time").Order("time"), nil 21 | } 22 | -------------------------------------------------------------------------------- /transaction/data_resources/set_halt_block.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 6 | ) 7 | 8 | type SetHaltBlock struct { 9 | Height uint64 `json:"height"` 10 | PubKey string `json:"pub_key"` 11 | } 12 | 13 | func (SetHaltBlock) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 14 | data := txData.(*api_pb.SetHaltBlockData) 15 | 16 | return SetHaltBlock{ 17 | Height: data.GetHeight(), 18 | PubKey: data.GetPubKey(), 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/set_halt_block.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 6 | ) 7 | 8 | type SetHaltBlock struct { 9 | Height uint64 `json:"height"` 10 | PubKey string `json:"pub_key"` 11 | } 12 | 13 | func (SetHaltBlock) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 14 | data := txData.(*api_pb.SetHaltBlockData) 15 | 16 | return SetHaltBlock{ 17 | Height: data.GetHeight(), 18 | PubKey: data.GetPubKey(), 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /check/filters.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/go-pg/pg/v10/orm" 6 | ) 7 | 8 | type SelectFilter struct { 9 | FromAddress string 10 | ToAddress string 11 | } 12 | 13 | func (f SelectFilter) Filter(q *orm.Query) (*orm.Query, error) { 14 | if len(f.FromAddress) != 0 { 15 | q = q.Where(`"from_address"."address" = ?`, helpers.RemoveMinterPrefix(f.FromAddress)) 16 | } 17 | 18 | if len(f.ToAddress) != 0 { 19 | q = q.Where(`"to_address"."address" = ?`, helpers.RemoveMinterPrefix(f.ToAddress)) 20 | } 21 | 22 | return q, nil 23 | } 24 | -------------------------------------------------------------------------------- /transaction/data_resources/edit_coin_owner.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 6 | ) 7 | 8 | type EditCoinOwner struct { 9 | Symbol string `json:"symbol"` 10 | NewOwner string `json:"new_owner"` 11 | } 12 | 13 | func (EditCoinOwner) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 14 | data := txData.(*api_pb.EditCoinOwnerData) 15 | 16 | return EditCoinOwner{ 17 | Symbol: data.GetSymbol(), 18 | NewOwner: data.GetNewOwner(), 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /api/validators/minter_address.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "github.com/go-playground/validator/v10" 5 | "regexp" 6 | ) 7 | 8 | func MinterAddress(fl validator.FieldLevel) bool { 9 | if data, ok := fl.Field().Interface().([]string); ok { 10 | for _, address := range data { 11 | if !isValidMinterAddress(address) { 12 | return false 13 | } 14 | } 15 | 16 | return true 17 | } 18 | 19 | return isValidMinterAddress(fl.Field().Interface().(string)) 20 | } 21 | 22 | func isValidMinterAddress(address string) bool { 23 | return regexp.MustCompile("^Mx([A-Fa-f0-9]{40})$").MatchString(address) 24 | } 25 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/edit_coin_owner.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 6 | ) 7 | 8 | type EditCoinOwner struct { 9 | Symbol string `json:"symbol"` 10 | NewOwner string `json:"new_owner"` 11 | } 12 | 13 | func (EditCoinOwner) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 14 | data := txData.(*api_pb.EditCoinOwnerData) 15 | 16 | return EditCoinOwner{ 17 | Symbol: data.GetSymbol(), 18 | NewOwner: data.GetNewOwner(), 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/multisend.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 6 | ) 7 | 8 | type Multisend struct { 9 | List []Send `json:"list"` 10 | } 11 | 12 | func (Multisend) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 13 | data := txData.(*api_pb.MultiSendData) 14 | 15 | list := make([]Send, len(data.List)) 16 | for key, item := range data.List { 17 | list[key] = Send{}.Transform(item).(Send) 18 | } 19 | 20 | return Multisend{list} 21 | } 22 | -------------------------------------------------------------------------------- /chart/tx_resource.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | "github.com/MinterTeam/minter-explorer-api/v2/transaction" 6 | "time" 7 | ) 8 | 9 | type TransactionResource struct { 10 | Date string `json:"date"` 11 | TxCount uint64 `json:"transaction_count"` 12 | } 13 | 14 | func (TransactionResource) Transform(model resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 15 | data := model.(transaction.TxCountChartData) 16 | 17 | return TransactionResource{ 18 | Date: data.Time.Format(time.RFC3339), 19 | TxCount: data.Count, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /order/helpers.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import "github.com/MinterTeam/minter-explorer-extender/v2/models" 4 | 5 | var ( 6 | statuses = map[models.OrderType]Status{ 7 | models.OrderTypeActive: OrderStatusActive, 8 | models.OrderTypeNew: OrderStatusActive, 9 | models.OrderTypePartiallyFilled: OrderStatusPartiallyFilled, 10 | models.OrderTypeFilled: OrderStatusFilled, 11 | models.OrderTypeCanceled: OrderStatusCanceled, 12 | models.OrderTypeExpired: OrderStatusExpired, 13 | } 14 | ) 15 | 16 | func MakeOrderStatus(status models.OrderType) Status { 17 | return statuses[status] 18 | } 19 | -------------------------------------------------------------------------------- /transaction/data_resources/edit_commission.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 6 | ) 7 | 8 | type EditCandidateCommission struct { 9 | PubKey string `json:"pub_key"` 10 | Commission uint64 `json:"commission"` 11 | } 12 | 13 | func (EditCandidateCommission) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 14 | data := txData.(*api_pb.EditCandidateCommission) 15 | 16 | return EditCandidateCommission{ 17 | PubKey: data.PubKey, 18 | Commission: data.Commission, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /api/v2/validators/routes.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | // ApplyRoutes applies router to the gin Engine 6 | func ApplyRoutes(r *gin.RouterGroup) { 7 | validators := r.Group("/validators") 8 | { 9 | validators.GET("", GetValidators) 10 | validators.GET("/meta", GetValidatorsMeta) 11 | validators.GET("/:publicKey", GetValidator) 12 | validators.GET("/:publicKey/stakes", GetValidatorStakes) 13 | validators.GET("/:publicKey/transactions", GetValidatorTransactions) 14 | validators.GET("/:publicKey/events/bans", GetValidatorBans) 15 | validators.GET("/:publicKey/events/slashes", GetValidatorSlashes) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/edit_commission.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 6 | ) 7 | 8 | type EditCandidateCommission struct { 9 | PubKey string `json:"pub_key"` 10 | Commission uint64 `json:"commission"` 11 | } 12 | 13 | func (EditCandidateCommission) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 14 | data := txData.(*api_pb.EditCandidateCommission) 15 | 16 | return EditCandidateCommission{ 17 | PubKey: data.PubKey, 18 | Commission: data.Commission, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /transaction/data_resources/edit_candidate_public_key.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 6 | ) 7 | 8 | type EditCandidatePublicKey struct { 9 | PubKey string `json:"pub_key"` 10 | NewPubKey string `json:"new_pub_key"` 11 | } 12 | 13 | func (EditCandidatePublicKey) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 14 | data := txData.(*api_pb.EditCandidatePublicKeyData) 15 | 16 | return EditCandidatePublicKey{ 17 | PubKey: data.PubKey, 18 | NewPubKey: data.NewPubKey, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /transaction/data_resources/burn_token.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type BurnToken struct { 10 | Coin Coin `json:"coin"` 11 | Value string `json:"value"` 12 | } 13 | 14 | func (BurnToken) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 15 | data := txData.(*api_pb.BurnTokenData) 16 | 17 | return BurnToken{ 18 | Coin: new(Coin).Transform(data.Coin), 19 | Value: helpers.PipStr2Bip(data.Value), 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /transaction/data_resources/mint_token.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type MintToken struct { 10 | Coin Coin `json:"coin"` 11 | Value string `json:"value"` 12 | } 13 | 14 | func (MintToken) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 15 | data := txData.(*api_pb.MintTokenData) 16 | 17 | return MintToken{ 18 | Coin: new(Coin).Transform(data.Coin), 19 | Value: helpers.PipStr2Bip(data.Value), 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /chart/reward_resource.go: -------------------------------------------------------------------------------- 1 | package chart 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/minter-explorer-api/v2/reward" 7 | "time" 8 | ) 9 | 10 | type RewardResource struct { 11 | Time string `json:"time"` 12 | Amount string `json:"amount"` 13 | } 14 | 15 | func (RewardResource) Transform(model resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 16 | data := model.(reward.ChartData) 17 | 18 | return RewardResource{ 19 | Time: data.Time.Format(time.RFC3339), 20 | Amount: helpers.PipStr2Bip(data.Amount), 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/burn_token.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type BurnToken struct { 10 | Coin Coin `json:"coin"` 11 | Value string `json:"value"` 12 | } 13 | 14 | func (BurnToken) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 15 | data := txData.(*api_pb.BurnTokenData) 16 | 17 | return BurnToken{ 18 | Coin: new(Coin).Transform(data.Coin), 19 | Value: helpers.PipStr2Bip(data.Value), 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/edit_candidate_public_key.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 6 | ) 7 | 8 | type EditCandidatePublicKey struct { 9 | PubKey string `json:"pub_key"` 10 | NewPubKey string `json:"new_pub_key"` 11 | } 12 | 13 | func (EditCandidatePublicKey) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 14 | data := txData.(*api_pb.EditCandidatePublicKeyData) 15 | 16 | return EditCandidatePublicKey{ 17 | PubKey: data.PubKey, 18 | NewPubKey: data.NewPubKey, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/mint_token.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type MintToken struct { 10 | Coin Coin `json:"coin"` 11 | Value string `json:"value"` 12 | } 13 | 14 | func (MintToken) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 15 | data := txData.(*api_pb.MintTokenData) 16 | 17 | return MintToken{ 18 | Coin: new(Coin).Transform(data.Coin), 19 | Value: helpers.PipStr2Bip(data.Value), 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /transaction/data_resources/send.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type Send struct { 10 | Coin Coin `json:"coin"` 11 | To string `json:"to"` 12 | Value string `json:"value"` 13 | } 14 | 15 | func (Send) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 16 | data := txData.(*api_pb.SendData) 17 | 18 | return Send{ 19 | Coin: new(Coin).Transform(data.Coin), 20 | To: data.To, 21 | Value: helpers.PipStr2Bip(data.Value), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/send.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type Send struct { 10 | Coin Coin `json:"coin"` 11 | To string `json:"to"` 12 | Value string `json:"value"` 13 | } 14 | 15 | func (Send) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 16 | data := txData.(*api_pb.SendData) 17 | 18 | return Send{ 19 | Coin: new(Coin).Transform(data.Coin), 20 | To: data.To, 21 | Value: helpers.PipStr2Bip(data.Value), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /transaction/data_resources/set_candidate.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 6 | ) 7 | 8 | type SetCandidate struct { 9 | PubKey string `json:"pub_key"` 10 | } 11 | 12 | func (SetCandidate) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 13 | var pubkey string 14 | 15 | if data, ok := txData.(*api_pb.SetCandidateOnData); ok { 16 | pubkey = data.PubKey 17 | } 18 | 19 | if data, ok := txData.(*api_pb.SetCandidateOffData); ok { 20 | pubkey = data.PubKey 21 | } 22 | 23 | return SetCandidate{ 24 | PubKey: pubkey, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/set_candidate.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 6 | ) 7 | 8 | type SetCandidate struct { 9 | PubKey string `json:"pub_key"` 10 | } 11 | 12 | func (SetCandidate) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 13 | var pubkey string 14 | 15 | if data, ok := txData.(*api_pb.SetCandidateOnData); ok { 16 | pubkey = data.PubKey 17 | } 18 | 19 | if data, ok := txData.(*api_pb.SetCandidateOffData); ok { 20 | pubkey = data.PubKey 21 | } 22 | 23 | return SetCandidate{ 24 | PubKey: pubkey, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /transaction/data_resources/unbond.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type Unbond struct { 10 | PubKey string `json:"pub_key"` 11 | Coin Coin `json:"coin"` 12 | Value string `json:"value"` 13 | } 14 | 15 | func (Unbond) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 16 | data := txData.(*api_pb.UnbondData) 17 | 18 | return Unbond{ 19 | PubKey: data.PubKey, 20 | Coin: new(Coin).Transform(data.Coin), 21 | Value: helpers.PipStr2Bip(data.Value), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/unbond.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type Unbond struct { 10 | PubKey string `json:"pub_key"` 11 | Coin Coin `json:"coin"` 12 | Value string `json:"value"` 13 | } 14 | 15 | func (Unbond) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 16 | data := txData.(*api_pb.UnbondData) 17 | 18 | return Unbond{ 19 | PubKey: data.PubKey, 20 | Coin: new(Coin).Transform(data.Coin), 21 | Value: helpers.PipStr2Bip(data.Value), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /transaction/data_resources/delegate.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type Delegate struct { 10 | PubKey string `json:"pub_key"` 11 | Coin Coin `json:"coin"` 12 | Value string `json:"value"` 13 | } 14 | 15 | func (Delegate) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 16 | data := txData.(*api_pb.DelegateData) 17 | 18 | return Delegate{ 19 | PubKey: data.PubKey, 20 | Coin: new(Coin).Transform(data.Coin), 21 | Value: helpers.PipStr2Bip(data.Value), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /transaction/data_resources/edit_multisig_owners.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 6 | ) 7 | 8 | type EditMultisigData struct { 9 | Threshold uint64 `json:"threshold"` 10 | Weights []uint64 `json:"weights"` 11 | Addresses []string `json:"addresses"` 12 | } 13 | 14 | func (EditMultisigData) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 15 | data := txData.(*api_pb.EditMultisigData) 16 | 17 | return EditMultisigData{ 18 | Threshold: data.GetThreshold(), 19 | Weights: data.GetWeights(), 20 | Addresses: data.GetAddresses(), 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const ( 4 | DefaultStatisticsScale = "day" 5 | DefaultStatisticsDayDelta = -14 6 | NetworkActivePeriod = 90 7 | SlowBlocksMaxTimeInSec = 6 8 | DefaultPaginationLimit = 50 9 | MaxPaginationOffset = 5000000 10 | MaxPaginationLimit = 150 11 | MarketPriceUpdatePeriodInMin = 2 12 | MaxDelegatorCount = 1000 13 | ) 14 | 15 | var ( 16 | BaseCoinSymbol string 17 | PriceCoinId uint64 = 1993 // TODO: move to env 18 | ) 19 | 20 | const ( 21 | MUSD = uint64(2024) 22 | ) 23 | 24 | var ( 25 | StableCoinIds = []uint64{1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000} 26 | ) 27 | 28 | var ( 29 | SwapRouterProxyUrl string 30 | ) 31 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/edit_multisig_owners.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 6 | ) 7 | 8 | type EditMultisigData struct { 9 | Threshold uint64 `json:"threshold"` 10 | Weights []uint64 `json:"weights"` 11 | Addresses []string `json:"addresses"` 12 | } 13 | 14 | func (EditMultisigData) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 15 | data := txData.(*api_pb.EditMultisigData) 16 | 17 | return EditMultisigData{ 18 | Threshold: data.GetThreshold(), 19 | Weights: data.GetWeights(), 20 | Addresses: data.GetAddresses(), 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/delegate.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type Delegate struct { 10 | PubKey string `json:"pub_key"` 11 | Coin Coin `json:"coin"` 12 | Value string `json:"value"` 13 | } 14 | 15 | func (Delegate) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 16 | data := txData.(*api_pb.DelegateData) 17 | 18 | return Delegate{ 19 | PubKey: data.PubKey, 20 | Coin: new(Coin).Transform(data.Coin), 21 | Value: helpers.PipStr2Bip(data.Value), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pool/helpers.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 6 | "math/big" 7 | ) 8 | 9 | func GetPoolChainFromStr(chainStr string) (chain []SwapChain) { 10 | json.Unmarshal([]byte(chainStr), &chain) 11 | return chain 12 | } 13 | 14 | func getVolumeInBip(price *big.Float, volume string) *big.Float { 15 | firstCoinBaseVolume := helpers.Pip2Bip(helpers.StringToBigInt(volume)) 16 | return new(big.Float).Mul(firstCoinBaseVolume, price) 17 | } 18 | 19 | func computePrice(reserve1, reserve2 string) *big.Float { 20 | return new(big.Float).Quo( 21 | helpers.Pip2Bip(helpers.StringToBigInt(reserve1)), 22 | helpers.Pip2Bip(helpers.StringToBigInt(reserve2)), 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /transaction/data_resources/lock.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type LockData struct { 10 | DueBlock uint64 `json:"due_block"` 11 | Coin Coin `json:"coin"` 12 | Value string `json:"value"` 13 | } 14 | 15 | func (LockData) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 16 | data := txData.(*api_pb.LockData) 17 | 18 | return LockData{ 19 | DueBlock: data.DueBlock, 20 | Coin: new(Coin).Transform(data.Coin), 21 | Value: helpers.PipStr2Bip(data.Value), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/lock.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type LockData struct { 10 | DueBlock uint64 `json:"due_block"` 11 | Coin Coin `json:"coin"` 12 | Value string `json:"value"` 13 | } 14 | 15 | func (LockData) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 16 | data := txData.(*api_pb.LockData) 17 | 18 | return LockData{ 19 | DueBlock: data.DueBlock, 20 | Coin: new(Coin).Transform(data.Coin), 21 | Value: helpers.PipStr2Bip(data.Value), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /coingecko/api.go: -------------------------------------------------------------------------------- 1 | package coingecko 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MinterTeam/minter-explorer-api/v2/tools" 6 | ) 7 | 8 | type Api struct { 9 | client *tools.HttpClient 10 | } 11 | 12 | func NewApi(host string) *Api { 13 | return &Api{ 14 | client: tools.NewHttpClient(host), 15 | } 16 | } 17 | 18 | type tickerData map[string]float64 19 | type SimplePriceResponse map[string]tickerData 20 | 21 | func (api *Api) GetSimplePrice(from string, to string) (SimplePriceResponse, error) { 22 | response := make(SimplePriceResponse) 23 | url := fmt.Sprintf("/api/v3/simple/price?include_24hr_change=true&ids=%s&vs_currencies=%s", from, to) 24 | if err := api.client.Get(url, &response); err != nil { 25 | return nil, err 26 | } 27 | 28 | return response, nil 29 | } 30 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/create_multisig.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 6 | ) 7 | 8 | type CreateMultisig struct { 9 | Threshold uint64 `json:"threshold"` 10 | Weights []uint64 `json:"weights"` 11 | Addresses []string `json:"addresses"` 12 | MultisigAddress string `json:"multisig_address"` 13 | } 14 | 15 | func (CreateMultisig) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 16 | data := txData.(*api_pb.CreateMultisigData) 17 | 18 | return CreateMultisig{ 19 | Threshold: data.Threshold, 20 | Weights: data.Weights, 21 | Addresses: data.Addresses, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /api/v2/validators/helpers.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import "github.com/MinterTeam/minter-explorer-api/v2/core" 4 | 5 | // Get IDs of active validators 6 | func getActiveValidatorIDs(explorer *core.Explorer) []uint { 7 | return explorer.Cache.Get("active_validators", func() interface{} { 8 | return explorer.ValidatorRepository.GetActiveValidatorIds() 9 | }, CacheBlocksCount).([]uint) 10 | } 11 | 12 | // Get total stake of active validators 13 | func getTotalStakeByActiveValidators(explorer *core.Explorer, validators []uint) string { 14 | return explorer.Cache.Get("validators_total_stake", func() interface{} { 15 | if len(validators) == 0 { 16 | return "0" 17 | } 18 | 19 | return explorer.ValidatorRepository.GetTotalStakeByActiveValidators(validators) 20 | }, CacheBlocksCount).(string) 21 | } 22 | -------------------------------------------------------------------------------- /transaction/data_resources/edit_candidate.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 6 | ) 7 | 8 | type EditCandidate struct { 9 | PubKey string `json:"pub_key"` 10 | RewardAddress string `json:"reward_address"` 11 | OwnerAddress string `json:"owner_address"` 12 | ControlAddress string `json:"control_address"` 13 | } 14 | 15 | func (EditCandidate) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 16 | data := txData.(*api_pb.EditCandidateData) 17 | 18 | return EditCandidate{ 19 | PubKey: data.PubKey, 20 | RewardAddress: data.RewardAddress, 21 | OwnerAddress: data.OwnerAddress, 22 | ControlAddress: data.ControlAddress, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/edit_candidate.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 6 | ) 7 | 8 | type EditCandidate struct { 9 | PubKey string `json:"pub_key"` 10 | RewardAddress string `json:"reward_address"` 11 | OwnerAddress string `json:"owner_address"` 12 | ControlAddress string `json:"control_address"` 13 | } 14 | 15 | func (EditCandidate) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 16 | data := txData.(*api_pb.EditCandidateData) 17 | 18 | return EditCandidate{ 19 | PubKey: data.PubKey, 20 | RewardAddress: data.RewardAddress, 21 | OwnerAddress: data.OwnerAddress, 22 | ControlAddress: data.ControlAddress, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /transaction/data_resources/move_stake.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type MoveStake struct { 10 | FromPubKey string `json:"from_pub_key"` 11 | ToPubKey string `json:"to_pub_key"` 12 | Coin Coin `json:"coin"` 13 | Value string `json:"value"` 14 | } 15 | 16 | func (MoveStake) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 17 | data := txData.(*api_pb.MoveStakeData) 18 | 19 | return MoveStake{ 20 | FromPubKey: data.FromPubKey, 21 | ToPubKey: data.ToPubKey, 22 | Coin: new(Coin).Transform(data.Coin), 23 | Value: helpers.PipStr2Bip(data.Value), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/move_stake.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type MoveStake struct { 10 | FromPubKey string `json:"from_pub_key"` 11 | ToPubKey string `json:"to_pub_key"` 12 | Coin Coin `json:"coin"` 13 | Value string `json:"value"` 14 | } 15 | 16 | func (MoveStake) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 17 | data := txData.(*api_pb.MoveStakeData) 18 | 19 | return MoveStake{ 20 | FromPubKey: data.FromPubKey, 21 | ToPubKey: data.ToPubKey, 22 | Coin: new(Coin).Transform(data.Coin), 23 | Value: helpers.PipStr2Bip(data.Value), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tools/http.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | type HttpClient struct { 11 | host string 12 | client *http.Client 13 | } 14 | 15 | func NewHttpClient(host string) *HttpClient { 16 | return &HttpClient{ 17 | host: host, 18 | client: &http.Client{ 19 | Timeout: time.Minute, 20 | }, 21 | } 22 | } 23 | 24 | func (c *HttpClient) Get(url string, response interface{}) error { 25 | req, err := http.NewRequest("GET", c.host+"/"+url, nil) 26 | resp, err := c.client.Do(req) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | defer resp.Body.Close() 32 | 33 | body, err := ioutil.ReadAll(resp.Body) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | err = json.Unmarshal(body, response) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/sell_all_coin.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type SellAllCoin struct { 10 | CoinToSell Coin `json:"coin_to_sell"` 11 | CoinToBuy Coin `json:"coin_to_buy"` 12 | MinimumValueToBuy string `json:"minimum_value_to_buy"` 13 | } 14 | 15 | func (SellAllCoin) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 16 | data := txData.(*api_pb.SellAllCoinData) 17 | 18 | return SellAllCoin{ 19 | CoinToSell: new(Coin).Transform(data.CoinToSell), 20 | CoinToBuy: new(Coin).Transform(data.CoinToBuy), 21 | MinimumValueToBuy: helpers.PipStr2Bip(data.MinimumValueToBuy), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /hub/client.go: -------------------------------------------------------------------------------- 1 | package hub 2 | 3 | import "github.com/go-resty/resty/v2" 4 | 5 | type Client struct { 6 | http *resty.Client 7 | } 8 | 9 | func NewClient() *Client { 10 | return &Client{ 11 | http: resty.New().SetHostURL("https://hub-api.minter.network"), 12 | } 13 | } 14 | 15 | type OracleCoinsResponse struct { 16 | List struct { 17 | TokenInfos []struct { 18 | Id string `json:"id"` 19 | Denom string `json:"denom"` 20 | ChainId string `json:"chain_id"` 21 | ExternalTokenId string `json:"external_token_id"` 22 | ExternalDecimals string `json:"external_decimals"` 23 | Commission string `json:"commission"` 24 | } `json:"token_infos"` 25 | } `json:"list"` 26 | } 27 | 28 | func (c *Client) GetOracleCoins() (*OracleCoinsResponse, error) { 29 | resp := new(OracleCoinsResponse) 30 | _, err := c.http.R().SetResult(resp).Get("/mhub2/v1/token_infos") 31 | return resp, err 32 | } 33 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/add_liquidity.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type AddLiquidity struct { 10 | Coin0 Coin `json:"coin0"` 11 | Coin1 Coin `json:"coin1"` 12 | Volume0 string `json:"volume0"` 13 | MaximumVolume1 string `json:"maximum_volume1"` 14 | } 15 | 16 | func (AddLiquidity) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 17 | data := txData.(*api_pb.AddLiquidityData) 18 | 19 | return AddLiquidity{ 20 | Coin0: new(Coin).Transform(data.Coin0), 21 | Coin1: new(Coin).Transform(data.Coin1), 22 | Volume0: helpers.PipStr2Bip(data.Volume0), 23 | MaximumVolume1: helpers.PipStr2Bip(data.MaximumVolume1), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /transaction/data_resources/declare_candidacy.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type DeclareCandidacy struct { 10 | Address string `json:"address"` 11 | PubKey string `json:"pub_key"` 12 | Commission uint64 `json:"commission"` 13 | Coin Coin `json:"coin"` 14 | Stake string `json:"stake"` 15 | } 16 | 17 | func (DeclareCandidacy) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 18 | data := txData.(*api_pb.DeclareCandidacyData) 19 | 20 | return DeclareCandidacy{ 21 | Address: data.Address, 22 | PubKey: data.PubKey, 23 | Commission: data.Commission, 24 | Coin: new(Coin).Transform(data.Coin), 25 | Stake: helpers.PipStr2Bip(data.Stake), 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/declare_candidacy.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type DeclareCandidacy struct { 10 | Address string `json:"address"` 11 | PubKey string `json:"pub_key"` 12 | Commission uint64 `json:"commission"` 13 | Coin Coin `json:"coin"` 14 | Stake string `json:"stake"` 15 | } 16 | 17 | func (DeclareCandidacy) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 18 | data := txData.(*api_pb.DeclareCandidacyData) 19 | 20 | return DeclareCandidacy{ 21 | Address: data.Address, 22 | PubKey: data.PubKey, 23 | Commission: data.Commission, 24 | Coin: new(Coin).Transform(data.Coin), 25 | Stake: helpers.PipStr2Bip(data.Stake), 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /api/v2/addresses/routes.go: -------------------------------------------------------------------------------- 1 | package addresses 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | // ApplyRoutes applies router to the gin Engine 6 | func ApplyRoutes(r *gin.RouterGroup) { 7 | addresses := r.Group("/addresses") 8 | { 9 | addresses.GET("", GetAddresses) 10 | addresses.GET("/:address", GetAddress) 11 | addresses.GET("/:address/locks", GetLocks) 12 | addresses.GET("/:address/orders", GetLimitOrders) 13 | addresses.GET("/:address/transactions", GetTransactions) 14 | addresses.GET("/:address/events/unbonds", GetUnbonds) 15 | addresses.GET("/:address/events/slashes", GetSlashes) 16 | addresses.GET("/:address/events/bans", GetBans) 17 | addresses.GET("/:address/delegations", GetDelegations) 18 | addresses.GET("/:address/delegations/locked", GetLockedDelegations) 19 | addresses.GET("/:address/statistics/rewards", GetRewardsStatistics) 20 | addresses.GET("/:address/events/rewards/aggregated", GetAggregatedRewards) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /transaction/data_resources/create_multisig.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type CreateMultisig struct { 10 | Threshold uint64 `json:"threshold"` 11 | Weights []uint64 `json:"weights"` 12 | Addresses []string `json:"addresses"` 13 | MultisigAddress string `json:"multisig_address"` 14 | } 15 | 16 | func (CreateMultisig) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 17 | data := txData.(*api_pb.CreateMultisigData) 18 | model := params[0].(models.Transaction) 19 | 20 | return CreateMultisig{ 21 | Threshold: data.Threshold, 22 | Weights: data.Weights, 23 | Addresses: data.Addresses, 24 | MultisigAddress: `Mx` + model.Tags["tx.created_multisig"], 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/buy_coin.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type BuyCoin struct { 10 | CoinToBuy Coin `json:"coin_to_buy"` 11 | CoinToSell Coin `json:"coin_to_sell"` 12 | ValueToBuy string `json:"value_to_buy"` 13 | MaximumValueToSell string `json:"maximum_value_to_sell"` 14 | } 15 | 16 | func (BuyCoin) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 17 | data := txData.(*api_pb.BuyCoinData) 18 | 19 | return BuyCoin{ 20 | CoinToBuy: new(Coin).Transform(data.CoinToBuy), 21 | CoinToSell: new(Coin).Transform(data.CoinToSell), 22 | ValueToBuy: helpers.PipStr2Bip(data.ValueToBuy), 23 | MaximumValueToSell: helpers.PipStr2Bip(data.MaximumValueToSell), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/sell_coin.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type SellCoin struct { 10 | CoinToSell Coin `json:"coin_to_sell"` 11 | CoinToBuy Coin `json:"coin_to_buy"` 12 | ValueToSell string `json:"value_to_sell"` 13 | MinimumValueToBuy string `json:"minimum_value_to_buy"` 14 | } 15 | 16 | func (SellCoin) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 17 | data := txData.(*api_pb.SellCoinData) 18 | 19 | return SellCoin{ 20 | CoinToSell: new(Coin).Transform(data.CoinToSell), 21 | CoinToBuy: new(Coin).Transform(data.CoinToBuy), 22 | ValueToSell: helpers.PipStr2Bip(data.ValueToSell), 23 | MinimumValueToBuy: helpers.PipStr2Bip(data.MinimumValueToBuy), 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tools/market/market.go: -------------------------------------------------------------------------------- 1 | package market 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/core/config" 5 | "time" 6 | ) 7 | 8 | type Service struct { 9 | api ApiService 10 | PriceChange PriceChange 11 | basecoin string 12 | } 13 | 14 | type ApiService interface { 15 | GetPrice(from string, to string) (float64, float64, error) 16 | } 17 | 18 | type PriceChange struct { 19 | Price float64 20 | Change float64 21 | } 22 | 23 | func NewService(api ApiService, basecoin string) *Service { 24 | return &Service{ 25 | api: api, 26 | basecoin: basecoin, 27 | PriceChange: PriceChange{Price: 0, Change: 0}, 28 | } 29 | } 30 | 31 | func (s *Service) Run() { 32 | for { 33 | price, change, err := s.api.GetPrice(s.basecoin, USDTicker) 34 | if err == nil { 35 | s.PriceChange = PriceChange{ 36 | Price: price, 37 | Change: change, 38 | } 39 | } 40 | 41 | time.Sleep(config.MarketPriceUpdatePeriodInMin * time.Minute) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /helpers/main.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "reflect" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | type ChannelData struct { 10 | Value interface{} 11 | Error error 12 | } 13 | 14 | func NewChannelData(value interface{}, err error) ChannelData { 15 | return ChannelData{ 16 | Value: value, 17 | Error: err, 18 | } 19 | } 20 | 21 | func InArray(needle interface{}, haystack interface{}) bool { 22 | switch reflect.TypeOf(haystack).Kind() { 23 | case reflect.Slice: 24 | s := reflect.ValueOf(haystack) 25 | for i := 0; i < s.Len(); i++ { 26 | if reflect.DeepEqual(needle, s.Index(i).Interface()) == true { 27 | return true 28 | } 29 | } 30 | } 31 | 32 | return false 33 | } 34 | 35 | func StartOfTheDay(t time.Time) time.Time { 36 | return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC) 37 | } 38 | 39 | func StrToUint64(str string) uint64 { 40 | val, _ := strconv.ParseUint(str, 10, 64) 41 | return val 42 | } 43 | 44 | func StrPointer(str string) *string { 45 | return &str 46 | } 47 | -------------------------------------------------------------------------------- /transaction/data_resources/create_token.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type CreateToken struct { 10 | Name string `json:"name"` 11 | Symbol string `json:"symbol"` 12 | InitialAmount string `json:"initial_amount"` 13 | MaxSupply string `json:"max_supply"` 14 | Mintable bool `json:"mintable"` 15 | Burnable bool `json:"burnable"` 16 | } 17 | 18 | func (CreateToken) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 19 | data := txData.(*api_pb.CreateTokenData) 20 | 21 | return CreateToken{ 22 | Name: data.Name, 23 | Symbol: data.Symbol, 24 | Mintable: data.Mintable, 25 | Burnable: data.Burnable, 26 | InitialAmount: helpers.PipStr2Bip(data.InitialAmount), 27 | MaxSupply: helpers.PipStr2Bip(data.MaxSupply), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/create_token.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type CreateToken struct { 10 | Name string `json:"name"` 11 | Symbol string `json:"symbol"` 12 | InitialAmount string `json:"initial_amount"` 13 | MaxSupply string `json:"max_supply"` 14 | Mintable bool `json:"mintable"` 15 | Burnable bool `json:"burnable"` 16 | } 17 | 18 | func (CreateToken) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 19 | data := txData.(*api_pb.CreateTokenData) 20 | 21 | return CreateToken{ 22 | Name: data.Name, 23 | Symbol: data.Symbol, 24 | Mintable: data.Mintable, 25 | Burnable: data.Burnable, 26 | InitialAmount: helpers.PipStr2Bip(data.InitialAmount), 27 | MaxSupply: helpers.PipStr2Bip(data.MaxSupply), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /balance/resource.go: -------------------------------------------------------------------------------- 1 | package balance 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/coins" 5 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 6 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 7 | "github.com/MinterTeam/minter-explorer-api/v2/services" 8 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 9 | ) 10 | 11 | type Resource struct { 12 | Coin resource.Interface `json:"coin"` 13 | Amount string `json:"amount"` 14 | BipAmount string `json:"bip_amount"` 15 | } 16 | 17 | func (Resource) Transform(model resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 18 | balance := model.(models.Balance) 19 | bipAmount := services.Swap.EstimateInBip(balance.Coin, helpers.StringToBigInt(balance.Value)) 20 | 21 | return Resource{ 22 | Coin: new(coins.IdResource).Transform(*balance.Coin, coins.Params{IsTypeRequired: true}), 23 | Amount: helpers.PipStr2Bip(balance.Value), 24 | BipAmount: helpers.Pip2BipStr(bipAmount), 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /transaction/data_resources/recreate_token.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type RecreateToken struct { 10 | Name string `json:"name"` 11 | Symbol string `json:"symbol"` 12 | InitialAmount string `json:"initial_amount"` 13 | MaxSupply string `json:"max_supply"` 14 | Mintable bool `json:"mintable"` 15 | Burnable bool `json:"burnable"` 16 | } 17 | 18 | func (RecreateToken) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 19 | data := txData.(*api_pb.RecreateTokenData) 20 | 21 | return RecreateToken{ 22 | Name: data.Name, 23 | Symbol: data.Symbol, 24 | Mintable: data.Mintable, 25 | Burnable: data.Burnable, 26 | InitialAmount: helpers.PipStr2Bip(data.InitialAmount), 27 | MaxSupply: helpers.PipStr2Bip(data.MaxSupply), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/recreate_token.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type RecreateToken struct { 10 | Name string `json:"name"` 11 | Symbol string `json:"symbol"` 12 | InitialAmount string `json:"initial_amount"` 13 | MaxSupply string `json:"max_supply"` 14 | Mintable bool `json:"mintable"` 15 | Burnable bool `json:"burnable"` 16 | } 17 | 18 | func (RecreateToken) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 19 | data := txData.(*api_pb.RecreateTokenData) 20 | 21 | return RecreateToken{ 22 | Name: data.Name, 23 | Symbol: data.Symbol, 24 | Mintable: data.Mintable, 25 | Burnable: data.Burnable, 26 | InitialAmount: helpers.PipStr2Bip(data.InitialAmount), 27 | MaxSupply: helpers.PipStr2Bip(data.MaxSupply), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /stake/resource.go: -------------------------------------------------------------------------------- 1 | package stake 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/coins" 5 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 6 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 7 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 8 | ) 9 | 10 | type Resource struct { 11 | Coin resource.Interface `json:"coin"` 12 | Address string `json:"address"` 13 | Value string `json:"value"` 14 | BipValue string `json:"bip_value"` 15 | IsWaitlisted bool `json:"is_waitlisted"` 16 | } 17 | 18 | func (Resource) Transform(model resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 19 | stake := model.(models.Stake) 20 | 21 | return Resource{ 22 | Coin: new(coins.IdResource).Transform(*stake.Coin), 23 | Address: stake.OwnerAddress.GetAddress(), 24 | Value: helpers.PipStr2Bip(stake.Value), 25 | BipValue: helpers.PipStr2Bip(stake.BipValue), 26 | IsWaitlisted: stake.IsKicked, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /check/repository.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/tools" 5 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 6 | "github.com/go-pg/pg/v10" 7 | ) 8 | 9 | type Repository struct { 10 | db *pg.DB 11 | } 12 | 13 | func NewRepository(db *pg.DB) *Repository { 14 | return &Repository{db} 15 | } 16 | 17 | func (r *Repository) GetByRawCheck(raw string) (models.Check, error) { 18 | var check models.Check 19 | 20 | err := r.db.Model(&check). 21 | Relation("FromAddress"). 22 | Relation("ToAddress"). 23 | Relation("Transaction"). 24 | Where(`"check"."data" = ?`, raw). 25 | Select() 26 | 27 | return check, err 28 | } 29 | 30 | func (r *Repository) GetListByFilter(filter SelectFilter, pagination *tools.Pagination) (checks []models.Check, err error) { 31 | pagination.Total, err = r.db.Model(&checks). 32 | Relation("FromAddress"). 33 | Relation("ToAddress"). 34 | Relation("Transaction"). 35 | Apply(filter.Filter). 36 | Apply(pagination.Filter). 37 | SelectAndCount() 38 | 39 | return checks, err 40 | } 41 | -------------------------------------------------------------------------------- /api/validators/minter_address_test.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type tCase struct { 8 | Address string 9 | Valid bool 10 | } 11 | 12 | func TestValidateMinterAddress(t *testing.T) { 13 | tCases := []tCase{ 14 | { 15 | Address: "Mxce542add0391b893d58c5fad21339f0f312cfa30", 16 | Valid: true, 17 | }, 18 | { 19 | Address: "MxCE542ADD0391B893D58C5FAD21339F0F312CFA30", 20 | Valid: true, 21 | }, 22 | { 23 | Address: "MxCE542ADD0391B893D58C5FAD21339F0F312CFA301", 24 | Valid: false, 25 | }, 26 | { 27 | Address: "MXCE542ADD0391B893D58C5FAD21339F0F312CFA30", 28 | Valid: false, 29 | }, 30 | { 31 | Address: "MxHE542ADD0391B893D58C5FAD21339F0F312CFA30", 32 | Valid: false, 33 | }, 34 | { 35 | Address: "Mxce542add0391b893d58c5fad21339f0f312cfa3", 36 | Valid: false, 37 | }, 38 | } 39 | 40 | for _, c := range tCases { 41 | if isValidMinterAddress(c.Address) != c.Valid { 42 | t.Fatalf("Address validation failed. For %s expected %t, got %t", c.Address, c.Valid, !c.Valid) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /core/ws/ws.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/core" 5 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 6 | "github.com/centrifugal/centrifuge-go" 7 | ) 8 | 9 | type ExtenderWsClient struct { 10 | client *centrifuge.Client 11 | } 12 | 13 | // create new extender connection 14 | func NewExtenderWsClient(explorer *core.Explorer) *ExtenderWsClient { 15 | c := centrifuge.New(explorer.Environment.WsServer, centrifuge.DefaultConfig()) 16 | 17 | err := c.Connect() 18 | helpers.CheckErr(err) 19 | 20 | return &ExtenderWsClient{c} 21 | } 22 | 23 | // create new subscription to channel 24 | func (e *ExtenderWsClient) CreateSubscription(channel string) *centrifuge.Subscription { 25 | sub, err := e.client.NewSubscription(channel) 26 | helpers.CheckErr(err) 27 | 28 | return sub 29 | } 30 | 31 | // subscribe to channel 32 | func (e ExtenderWsClient) Subscribe(sub *centrifuge.Subscription) { 33 | err := sub.Subscribe() 34 | helpers.CheckErr(err) 35 | } 36 | 37 | // close ws connection 38 | func (e *ExtenderWsClient) Close() { 39 | err := e.client.Close() 40 | helpers.CheckErr(err) 41 | } 42 | -------------------------------------------------------------------------------- /transaction/data_resources/multisend.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 7 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 8 | ) 9 | 10 | type Multisend struct { 11 | List []Send `json:"list"` 12 | } 13 | 14 | func (Multisend) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 15 | data := txData.(*api_pb.MultiSendData) 16 | 17 | list := make([]Send, len(data.List)) 18 | for key, item := range data.List { 19 | list[key] = Send{}.Transform(item).(Send) 20 | } 21 | 22 | return Multisend{list} 23 | } 24 | 25 | func (Multisend) TransformByTxOutput(txData resource.ItemInterface) resource.Interface { 26 | data := txData.(*models.TransactionOutput) 27 | coin := &api_pb.Coin{ 28 | Id: uint64(data.Coin.ID), 29 | Symbol: data.Coin.Symbol, 30 | } 31 | 32 | return Send{ 33 | Coin: new(Coin).Transform(coin), 34 | To: data.ToAddress.GetAddress(), 35 | Value: helpers.PipStr2Bip(data.Value), 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /delegation/resource.go: -------------------------------------------------------------------------------- 1 | package delegation 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/coins" 5 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 6 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 7 | "github.com/MinterTeam/minter-explorer-api/v2/validator" 8 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 9 | ) 10 | 11 | type Resource struct { 12 | Coin resource.Interface `json:"coin"` 13 | Value string `json:"value"` 14 | BipValue string `json:"bip_value"` 15 | Validator resource.Interface `json:"validator"` 16 | IsWaitlisted bool `json:"is_waitlisted"` 17 | } 18 | 19 | func (resource Resource) Transform(model resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 20 | stake := model.(models.Stake) 21 | 22 | return Resource{ 23 | Coin: new(coins.IdResource).Transform(*stake.Coin), 24 | Value: helpers.PipStr2Bip(stake.Value), 25 | BipValue: helpers.PipStr2Bip(stake.BipValue), 26 | Validator: new(validator.Resource).Transform(*stake.Validator), 27 | IsWaitlisted: stake.IsKicked, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/create_coin.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type CreateCoin struct { 10 | Name string `json:"name"` 11 | Symbol string `json:"symbol"` 12 | InitialAmount string `json:"initial_amount"` 13 | InitialReserve string `json:"initial_reserve"` 14 | ConstantReserveRatio uint64 `json:"constant_reserve_ratio"` 15 | MaxSupply string `json:"max_supply"` 16 | } 17 | 18 | func (CreateCoin) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 19 | data := txData.(*api_pb.CreateCoinData) 20 | 21 | return CreateCoin{ 22 | Name: data.Name, 23 | Symbol: data.Symbol, 24 | InitialAmount: helpers.PipStr2Bip(data.InitialAmount), 25 | InitialReserve: helpers.PipStr2Bip(data.InitialReserve), 26 | ConstantReserveRatio: data.ConstantReserveRatio, 27 | MaxSupply: helpers.PipStr2Bip(data.MaxSupply), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /aggregated_reward/resource.go: -------------------------------------------------------------------------------- 1 | package aggregated_reward 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/minter-explorer-api/v2/validator" 7 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 8 | "time" 9 | ) 10 | 11 | type Resource struct { 12 | TimeID string `json:"time_id"` 13 | Timestamp string `json:"timestamp"` 14 | Role string `json:"role"` 15 | Amount string `json:"amount"` 16 | Address string `json:"address"` 17 | Validator resource.Interface `json:"validator"` 18 | } 19 | 20 | func (Resource) Transform(model resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 21 | reward := model.(models.AggregatedReward) 22 | 23 | return Resource{ 24 | TimeID: reward.TimeID.Format(time.RFC3339), 25 | Timestamp: reward.TimeID.Format(time.RFC3339), 26 | Role: reward.Role, 27 | Amount: helpers.PipStr2Bip(reward.Amount), 28 | Address: reward.Address.GetAddress(), 29 | Validator: new(validator.Resource).Transform(*reward.Validator), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/recreate_coin.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type RecreateCoin struct { 10 | Name string `json:"name"` 11 | Symbol string `json:"symbol"` 12 | InitialAmount string `json:"initial_amount"` 13 | InitialReserve string `json:"initial_reserve"` 14 | ConstantReserveRatio uint64 `json:"constant_reserve_ratio"` 15 | MaxSupply string `json:"max_supply"` 16 | } 17 | 18 | func (RecreateCoin) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 19 | data := txData.(*api_pb.RecreateCoinData) 20 | 21 | return RecreateCoin{ 22 | Name: data.Name, 23 | Symbol: data.Symbol, 24 | InitialAmount: helpers.PipStr2Bip(data.InitialAmount), 25 | InitialReserve: helpers.PipStr2Bip(data.InitialReserve), 26 | ConstantReserveRatio: data.ConstantReserveRatio, 27 | MaxSupply: helpers.PipStr2Bip(data.MaxSupply), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /transaction/data_resources/sell_coin.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 7 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 8 | ) 9 | 10 | type SellCoin struct { 11 | CoinToSell Coin `json:"coin_to_sell"` 12 | CoinToBuy Coin `json:"coin_to_buy"` 13 | ValueToSell string `json:"value_to_sell"` 14 | ValueToBuy string `json:"value_to_buy"` 15 | MinimumValueToBuy string `json:"minimum_value_to_buy"` 16 | } 17 | 18 | func (SellCoin) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 19 | data := txData.(*api_pb.SellCoinData) 20 | model := params[0].(models.Transaction) 21 | 22 | return SellCoin{ 23 | CoinToSell: new(Coin).Transform(data.CoinToSell), 24 | CoinToBuy: new(Coin).Transform(data.CoinToBuy), 25 | ValueToSell: helpers.PipStr2Bip(data.ValueToSell), 26 | ValueToBuy: helpers.PipStr2Bip(model.Tags["tx.return"]), 27 | MinimumValueToBuy: helpers.PipStr2Bip(data.MinimumValueToBuy), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/remove_liquidity.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 7 | ) 8 | 9 | type RemoveLiquidity struct { 10 | Coin0 Coin `json:"coin0"` 11 | Coin1 Coin `json:"coin1"` 12 | PoolToken Coin `json:"pool_token"` 13 | Liquidity string `json:"liquidity"` 14 | MinimumVolume0 string `json:"minimum_volume0"` 15 | MinimumVolume1 string `json:"minimum_volume1"` 16 | Volume0 string `json:"volume0"` 17 | Volume1 string `json:"volume1"` 18 | } 19 | 20 | func (RemoveLiquidity) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 21 | data := txData.(*api_pb.RemoveLiquidityData) 22 | 23 | return RemoveLiquidity{ 24 | Coin0: new(Coin).Transform(data.Coin0), 25 | Coin1: new(Coin).Transform(data.Coin1), 26 | Liquidity: helpers.PipStr2Bip(data.Liquidity), 27 | MinimumVolume0: helpers.PipStr2Bip(data.MinimumVolume0), 28 | MinimumVolume1: helpers.PipStr2Bip(data.MinimumVolume1), 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /transaction/data_resources/buy_coin.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 7 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 8 | ) 9 | 10 | type BuyCoin struct { 11 | CoinToBuy Coin `json:"coin_to_buy"` 12 | CoinToSell Coin `json:"coin_to_sell"` 13 | ValueToBuy string `json:"value_to_buy"` 14 | ValueToSell string `json:"value_to_sell"` 15 | MaximumValueToSell string `json:"maximum_value_to_sell"` 16 | } 17 | 18 | func (BuyCoin) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 19 | data := txData.(*api_pb.BuyCoinData) 20 | model := params[0].(models.Transaction) 21 | 22 | return BuyCoin{ 23 | CoinToBuy: new(Coin).Transform(data.CoinToBuy), 24 | CoinToSell: new(Coin).Transform(data.CoinToSell), 25 | ValueToBuy: helpers.PipStr2Bip(data.ValueToBuy), 26 | ValueToSell: helpers.PipStr2Bip(model.Tags["tx.return"]), 27 | MaximumValueToSell: helpers.PipStr2Bip(data.MaximumValueToSell), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /api/v2/routes.go: -------------------------------------------------------------------------------- 1 | package apiV2 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/api/v2/addresses" 5 | "github.com/MinterTeam/minter-explorer-api/v2/api/v2/blocks" 6 | "github.com/MinterTeam/minter-explorer-api/v2/api/v2/checks" 7 | "github.com/MinterTeam/minter-explorer-api/v2/api/v2/coins" 8 | "github.com/MinterTeam/minter-explorer-api/v2/api/v2/pools" 9 | "github.com/MinterTeam/minter-explorer-api/v2/api/v2/statistics" 10 | "github.com/MinterTeam/minter-explorer-api/v2/api/v2/status" 11 | "github.com/MinterTeam/minter-explorer-api/v2/api/v2/transactions" 12 | "github.com/MinterTeam/minter-explorer-api/v2/api/v2/validators" 13 | "github.com/gin-gonic/gin" 14 | ) 15 | 16 | // ApplyRoutes applies router to the gin Engine 17 | func ApplyRoutes(r *gin.RouterGroup) { 18 | v2 := r.Group("/v2") 19 | { 20 | blocks.ApplyRoutes(v2) 21 | coins.ApplyRoutes(v2) 22 | addresses.ApplyRoutes(v2) 23 | transactions.ApplyRoutes(v2) 24 | validators.ApplyRoutes(v2) 25 | statistics.ApplyRoutes(v2) 26 | status.ApplyRoutes(v2) 27 | checks.ApplyRoutes(v2) 28 | pools.ApplyRoutes(v2) 29 | } 30 | 31 | // alias from v2 32 | r.GET("/v1/info", status.GetInfo) 33 | r.GET("/v2/check", status.HealthCheck) 34 | } 35 | -------------------------------------------------------------------------------- /transaction/data_resources/sell_all_coin.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 7 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 8 | ) 9 | 10 | type SellAllCoin struct { 11 | CoinToSell Coin `json:"coin_to_sell"` 12 | CoinToBuy Coin `json:"coin_to_buy"` 13 | ValueToSell string `json:"value_to_sell"` 14 | ValueToBuy string `json:"value_to_buy"` 15 | MinimumValueToBuy string `json:"minimum_value_to_buy"` 16 | } 17 | 18 | func (SellAllCoin) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 19 | data := txData.(*api_pb.SellAllCoinData) 20 | model := params[0].(models.Transaction) 21 | 22 | return SellAllCoin{ 23 | CoinToSell: new(Coin).Transform(data.CoinToSell), 24 | CoinToBuy: new(Coin).Transform(data.CoinToBuy), 25 | ValueToSell: helpers.PipStr2Bip(model.Tags["tx.sell_amount"]), 26 | ValueToBuy: helpers.PipStr2Bip(model.Tags["tx.return"]), 27 | MinimumValueToBuy: helpers.PipStr2Bip(data.MinimumValueToBuy), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /order/models.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import "github.com/MinterTeam/minter-explorer-extender/v2/models" 4 | 5 | type OrderTransaction struct { 6 | tableName struct{} `pg:"orders"` 7 | Id uint64 `json:"id"` 8 | AddressId uint64 `json:"address_id"` 9 | LiquidityPoolId uint64 `json:"liquidity_pool_id"` 10 | CoinSellId uint64 `json:"coin_sell_id" pg:",use_zero"` 11 | CoinSellVolume string `json:"coin_sell_volume"` 12 | CoinBuyId uint64 `json:"coin_buy_id" pg:",use_zero"` 13 | CoinBuyVolume string `json:"coin_buy_volume"` 14 | CreatedAtBlock uint64 `json:"created_at_block"` 15 | Price string `json:"price"` 16 | IsCanceled bool 17 | Status models.OrderType `json:"status"` 18 | Address *models.Address `json:"address" pg:"rel:has-one,fk:address_id"` 19 | LiquidityPool *models.LiquidityPool `json:"liquidity_pool" pg:"rel:has-one,fk:liquidity_pool_id"` 20 | CoinSell *models.Coin `json:"coin_sell" pg:"rel:has-one,fk:coin_sell_id"` 21 | CoinBuy *models.Coin `json:"coin_buy" pg:"rel:has-one,fk:coin_buy_id"` 22 | Transaction *models.Transaction `json:"transaction"` 23 | } 24 | -------------------------------------------------------------------------------- /tools/metrics/block.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/blocks" 5 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 6 | "github.com/prometheus/client_golang/prometheus" 7 | "time" 8 | ) 9 | 10 | type LastBlockMetric struct { 11 | id prometheus.Gauge 12 | time prometheus.Gauge 13 | } 14 | 15 | // Constructor for prometheus metrics 16 | func NewLastBlockMetric() *LastBlockMetric { 17 | prometheusLastBlockIdMetric := prometheus.NewGauge( 18 | prometheus.GaugeOpts{ 19 | Name: "explorer_last_block_id", 20 | }, 21 | ) 22 | 23 | prometheusLastBlockTimeMetric := prometheus.NewGauge( 24 | prometheus.GaugeOpts{ 25 | Name: "explorer_last_block_time", 26 | }, 27 | ) 28 | 29 | prometheus.MustRegister(prometheusLastBlockIdMetric) 30 | prometheus.MustRegister(prometheusLastBlockTimeMetric) 31 | 32 | return &LastBlockMetric{ 33 | id: prometheusLastBlockIdMetric, 34 | time: prometheusLastBlockTimeMetric, 35 | } 36 | } 37 | 38 | // Update last block for prometheus metric 39 | func (m *LastBlockMetric) OnNewBlock(block blocks.Resource) { 40 | blockTime, err := time.Parse(time.RFC3339, block.Timestamp) 41 | helpers.CheckErr(err) 42 | 43 | m.id.Set(float64(block.ID)) 44 | m.time.Set(float64(blockTime.Unix())) 45 | } 46 | -------------------------------------------------------------------------------- /.deploy/values-default.yaml: -------------------------------------------------------------------------------- 1 | replicaCount: 1 2 | 3 | image: 4 | repository: minterteam/explorer-api 5 | pullPolicy: IfNotPresent 6 | 7 | nameOverride: "" 8 | fullnameOverride: "" 9 | 10 | serviceAccount: 11 | create: true 12 | 13 | podSecurityContext: {} 14 | 15 | securityContext: {} 16 | 17 | service: 18 | port: 8080 19 | 20 | #ingress: 21 | # enabled: false 22 | # domain: [] 23 | # tls: [] 24 | 25 | ingress: 26 | enabled: true 27 | annotations: 28 | cert-manager.io/cluster-issuer: letsencrypt-prod 29 | certmanager.k8s.io/cluster-issuer: letsencrypt-prod 30 | kubernetes.io/ingress.class: nginx 31 | kubernetes.io/tls-acme: "true" 32 | path: / 33 | hosts: 34 | - explorer-api.minter.network 35 | tls: 36 | secret: explorer-api 37 | hosts: 38 | - explorer-api.minter.network 39 | 40 | 41 | resources: {} 42 | 43 | nodeSelector: {} 44 | 45 | tolerations: [] 46 | 47 | affinity: {} 48 | 49 | env: 50 | EXPLORER_DEBUG: "false" 51 | APP_BASE_COIN: "BIP" 52 | CENTRIFUGO_LINK: "" 53 | CENTRIFUGO_BLOCK_CHANNEL: "" 54 | DB_HOST: "" 55 | DB_PORT: "" 56 | DB_POOL_SIZE: "" 57 | DB_NAME: "" 58 | DB_USER: "" 59 | DB_PASSWORD: "" 60 | EXPLORER_PORT: "" 61 | MARKET_HOST: "https://api.coingecko.com" 62 | POSTGRES_SSL_ENABLED: "false" 63 | -------------------------------------------------------------------------------- /core/ws/blocks.go: -------------------------------------------------------------------------------- 1 | package ws 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/MinterTeam/minter-explorer-api/v2/blocks" 6 | "github.com/MinterTeam/minter-explorer-api/v2/errors" 7 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 8 | "github.com/centrifugal/centrifuge-go" 9 | ) 10 | 11 | type BlocksChannelHandler struct { 12 | Subscribers []NewBlockSubscriber 13 | } 14 | 15 | type NewBlockSubscriber interface { 16 | OnNewBlock(blocks.Resource) 17 | } 18 | 19 | // Constructor for blocks event channel handler 20 | func NewBlocksChannelHandler() *BlocksChannelHandler { 21 | return &BlocksChannelHandler{ 22 | Subscribers: make([]NewBlockSubscriber, 0), 23 | } 24 | } 25 | 26 | // Add new subscriber for channel 27 | func (b *BlocksChannelHandler) AddSubscriber(sub NewBlockSubscriber) { 28 | b.Subscribers = append(b.Subscribers, sub) 29 | } 30 | 31 | // Handle new block from ws and publish him to all subscribers 32 | func (b *BlocksChannelHandler) OnPublish(sub *centrifuge.Subscription, e centrifuge.PublishEvent) { 33 | var block blocks.Resource 34 | err := json.Unmarshal(e.Data, &block) 35 | helpers.CheckErr(err) 36 | 37 | for _, sub := range b.Subscribers { 38 | go func(sub NewBlockSubscriber) { 39 | defer errors.Recovery() 40 | sub.OnNewBlock(block) 41 | }(sub) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /database/main.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "github.com/MinterTeam/minter-explorer-api/v2/core" 8 | "github.com/go-pg/pg/v10" 9 | "os" 10 | ) 11 | 12 | func Connect(env *core.Environment) *pg.DB { 13 | options := &pg.Options{ 14 | User: env.DbUser, 15 | Password: env.DbPassword, 16 | Database: env.DbName, 17 | PoolSize: 20, 18 | Addr: fmt.Sprintf("%s:%s", env.DbHost, env.DbPort), 19 | } 20 | 21 | if os.Getenv("POSTGRES_SSL_ENABLED") == "true" { 22 | options.TLSConfig = &tls.Config{ 23 | InsecureSkipVerify: true, 24 | } 25 | } 26 | 27 | db := pg.Connect(options) 28 | if db == nil { 29 | panic("Could not connect to database") 30 | } 31 | 32 | db.AddQueryHook(dbLogger{}) 33 | 34 | return db 35 | } 36 | 37 | func Close(db *pg.DB) { 38 | err := db.Close() 39 | if err != nil { 40 | panic(fmt.Sprintf("Could not close connection to database: %s", err)) 41 | } 42 | } 43 | 44 | type dbLogger struct{} 45 | 46 | func (d dbLogger) BeforeQuery(ctx context.Context, q *pg.QueryEvent) (context.Context, error) { 47 | return ctx, nil 48 | } 49 | 50 | func (d dbLogger) AfterQuery(ctx context.Context, q *pg.QueryEvent) error { 51 | if q.Err != nil { 52 | sql, _ := q.FormattedQuery() 53 | fmt.Println(string(sql[:])) 54 | } 55 | 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /slash/resource.go: -------------------------------------------------------------------------------- 1 | package slash 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/coins" 5 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 6 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 7 | "github.com/MinterTeam/minter-explorer-api/v2/validator" 8 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 9 | "time" 10 | ) 11 | 12 | type Resource struct { 13 | BlockID uint64 `json:"height"` 14 | Coin resource.Interface `json:"coin"` 15 | Amount string `json:"amount"` 16 | Address string `json:"address"` 17 | Timestamp string `json:"timestamp"` 18 | Validator resource.Interface `json:"validator,omitempty"` 19 | } 20 | 21 | func (Resource) Transform(model resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 22 | slash := model.(models.Slash) 23 | 24 | slashResource := Resource{ 25 | BlockID: slash.BlockID, 26 | Coin: new(coins.IdResource).Transform(*slash.Coin), 27 | Amount: helpers.PipStr2Bip(slash.Amount), 28 | Address: slash.Address.GetAddress(), 29 | Timestamp: slash.Block.CreatedAt.Format(time.RFC3339), 30 | } 31 | 32 | if slash.Validator != nil { 33 | slashResource.Validator = new(validator.Resource).Transform(*slash.Validator) 34 | } 35 | 36 | return slashResource 37 | } 38 | -------------------------------------------------------------------------------- /invalid_transaction/data_resources/coin.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/coins" 5 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 6 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 7 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 8 | ) 9 | 10 | type Coin struct { 11 | ID uint64 `json:"id"` 12 | Symbol string `json:"symbol"` 13 | } 14 | 15 | func (Coin) Transform(data *api_pb.Coin) Coin { 16 | coin, err := coins.GlobalRepository.FindByID(uint(data.GetId())) 17 | helpers.CheckErr(err) 18 | 19 | return Coin{ 20 | ID: data.GetId(), 21 | Symbol: coin.GetSymbol(), 22 | } 23 | } 24 | 25 | type PoolCoin struct { 26 | ID uint64 `json:"id"` 27 | Symbol string `json:"symbol"` 28 | } 29 | 30 | func (PoolCoin) Transform(data *api_pb.Coin) PoolCoin { 31 | return PoolCoin{ 32 | ID: data.GetId(), 33 | Symbol: data.GetSymbol(), 34 | } 35 | } 36 | 37 | 38 | func (PoolCoin) TransformCollection(data []*api_pb.Coin, model models.InvalidTransaction) []PoolCoin { 39 | poolCoins := make([]PoolCoin, len(data)) 40 | 41 | for i, coin := range data { 42 | // todo: refactor 43 | coinModel, err := coins.GlobalRepository.FindByID(uint(coin.Id)) 44 | helpers.CheckErr(err) 45 | coin.Symbol = coinModel.GetSymbol() 46 | poolCoins[i] = new(PoolCoin).Transform(coin) 47 | } 48 | 49 | return poolCoins 50 | } 51 | -------------------------------------------------------------------------------- /address/repository.go: -------------------------------------------------------------------------------- 1 | package address 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 6 | "github.com/go-pg/pg/v10" 7 | ) 8 | 9 | type Repository struct { 10 | db *pg.DB 11 | } 12 | 13 | func NewRepository(db *pg.DB) *Repository { 14 | return &Repository{ 15 | db: db, 16 | } 17 | } 18 | 19 | // Get address model by address 20 | func (r Repository) GetByAddress(minterAddress string) *models.Address { 21 | var address models.Address 22 | 23 | err := r.db.Model(&address). 24 | Relation("Balances"). 25 | Relation("Balances.Coin"). 26 | Where("address = ?", minterAddress). 27 | Select() 28 | 29 | if err != nil { 30 | return nil 31 | } 32 | 33 | return &address 34 | } 35 | 36 | // Get list of addresses models 37 | func (r Repository) GetByAddresses(minterAddresses []string) []*models.Address { 38 | var addresses []*models.Address 39 | 40 | err := r.db.Model(&addresses). 41 | Relation("Balances"). 42 | Relation("Balances.Coin"). 43 | WhereIn("address IN (?)", minterAddresses). 44 | Select() 45 | 46 | helpers.CheckErr(err) 47 | 48 | return addresses 49 | } 50 | 51 | func (r Repository) GetNonZeroAddressesCount() (count uint64, err error) { 52 | err = r.db.Model(new(models.Balance)). 53 | ColumnExpr("count (DISTINCT address_id)"). 54 | Select(&count) 55 | 56 | return count, err 57 | } 58 | -------------------------------------------------------------------------------- /resource/main.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | type ItemInterface interface{} 8 | type ParamInterface interface{} 9 | type ParamsInterface []ParamInterface 10 | type Interface interface { 11 | Transform(model ItemInterface, params ...ParamInterface) Interface 12 | } 13 | 14 | func TransformCollection(collection interface{}, resource Interface) []Interface { 15 | models := makeItemsFromModelsCollection(collection) 16 | result := make([]Interface, len(models)) 17 | 18 | for i := range models { 19 | result[i] = resource.Transform(models[i]) 20 | } 21 | 22 | return result 23 | } 24 | 25 | func TransformCollectionWithCallback(collection interface{}, resource Interface, callbackFunc func(model ParamInterface) ParamsInterface) []Interface { 26 | models := makeItemsFromModelsCollection(collection) 27 | result := make([]Interface, len(models)) 28 | 29 | for i := range models { 30 | result[i] = resource.Transform(models[i], callbackFunc(models[i])...) 31 | } 32 | 33 | return result 34 | } 35 | 36 | func makeItemsFromModelsCollection(collection interface{}) []ItemInterface { 37 | val := reflect.ValueOf(collection) 38 | 39 | models := make([]ItemInterface, val.Len()) 40 | for i := 0; i < val.Len(); i++ { 41 | if val.Index(i).Kind() == reflect.Ptr { 42 | models[i] = val.Index(i).Elem().Interface() 43 | } else { 44 | models[i] = val.Index(i).Interface() 45 | } 46 | } 47 | 48 | return models 49 | } 50 | -------------------------------------------------------------------------------- /transaction/data_resources/add_liquidity.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 7 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 8 | ) 9 | 10 | type AddLiquidity struct { 11 | Coin0 Coin `json:"coin0"` 12 | Coin1 Coin `json:"coin1"` 13 | PoolToken Coin `json:"pool_token"` 14 | Volume0 string `json:"volume0"` 15 | MaximumVolume1 string `json:"maximum_volume1"` 16 | Volume1 string `json:"volume1"` 17 | Liquidity string `json:"liquidity"` 18 | } 19 | 20 | func (AddLiquidity) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 21 | data, model := txData.(*api_pb.AddLiquidityData), params[0].(models.Transaction) 22 | 23 | return AddLiquidity{ 24 | Coin0: new(Coin).Transform(data.Coin0), 25 | Coin1: new(Coin).Transform(data.Coin1), 26 | Volume0: helpers.PipStr2Bip(data.Volume0), 27 | MaximumVolume1: helpers.PipStr2Bip(data.MaximumVolume1), 28 | Volume1: helpers.PipStr2Bip(model.Tags["tx.volume1"]), 29 | Liquidity: helpers.PipStr2Bip(model.Tags["tx.liquidity"]), 30 | PoolToken: Coin{ 31 | ID: helpers.StrToUint64(model.Tags["tx.pool_token_id"]), 32 | Symbol: model.Tags["tx.pool_token"], 33 | }, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /transaction/data_resources/create_coin.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 7 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 8 | "strconv" 9 | ) 10 | 11 | type CreateCoin struct { 12 | CreatedCoinID uint64 `json:"created_coin_id"` 13 | Name string `json:"name"` 14 | Symbol string `json:"symbol"` 15 | InitialAmount string `json:"initial_amount"` 16 | InitialReserve string `json:"initial_reserve"` 17 | ConstantReserveRatio uint64 `json:"constant_reserve_ratio"` 18 | MaxSupply string `json:"max_supply"` 19 | } 20 | 21 | func (CreateCoin) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 22 | data, model := txData.(*api_pb.CreateCoinData), params[0].(models.Transaction) 23 | coinID, _ := strconv.ParseUint(model.Tags["tx.coin_id"], 10, 64) 24 | 25 | return CreateCoin{ 26 | CreatedCoinID: coinID, 27 | Name: data.Name, 28 | Symbol: data.Symbol, 29 | InitialAmount: helpers.PipStr2Bip(data.InitialAmount), 30 | InitialReserve: helpers.PipStr2Bip(data.InitialReserve), 31 | ConstantReserveRatio: data.ConstantReserveRatio, 32 | MaxSupply: helpers.PipStr2Bip(data.MaxSupply), 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /transaction/data_resources/recreate_coin.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 7 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 8 | "strconv" 9 | ) 10 | 11 | type RecreateCoin struct { 12 | CreatedCoinID uint64 `json:"created_coin_id"` 13 | Name string `json:"name"` 14 | Symbol string `json:"symbol"` 15 | InitialAmount string `json:"initial_amount"` 16 | InitialReserve string `json:"initial_reserve"` 17 | ConstantReserveRatio uint64 `json:"constant_reserve_ratio"` 18 | MaxSupply string `json:"max_supply"` 19 | } 20 | 21 | func (RecreateCoin) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 22 | data, model := txData.(*api_pb.RecreateCoinData), params[0].(models.Transaction) 23 | coinID, _ := strconv.ParseUint(model.Tags["tx.coin_id"], 10, 64) 24 | 25 | return RecreateCoin{ 26 | CreatedCoinID: coinID, 27 | Name: data.Name, 28 | Symbol: data.Symbol, 29 | InitialAmount: helpers.PipStr2Bip(data.InitialAmount), 30 | InitialReserve: helpers.PipStr2Bip(data.InitialReserve), 31 | ConstantReserveRatio: data.ConstantReserveRatio, 32 | MaxSupply: helpers.PipStr2Bip(data.MaxSupply), 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /events/resource.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 5 | "github.com/MinterTeam/minter-explorer-api/v2/validator" 6 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 7 | "time" 8 | ) 9 | 10 | type BanResource struct { 11 | FromBlockId uint64 `json:"height"` 12 | FromTimestamp string `json:"timestamp"` 13 | ToBlockId uint64 `json:"to_block_id"` 14 | } 15 | 16 | func (BanResource) Transform(model resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 17 | ban := model.(models.ValidatorBan) 18 | 19 | return BanResource{ 20 | FromBlockId: ban.BlockId, 21 | FromTimestamp: ban.Block.CreatedAt.Format(time.RFC3339), 22 | ToBlockId: ban.ToBlockId, 23 | } 24 | } 25 | 26 | type AddressBanResource struct { 27 | FromBlockId uint64 `json:"height"` 28 | FromTimestamp string `json:"timestamp"` 29 | ToBlockId uint64 `json:"to_block_id"` 30 | Validator resource.Interface `json:"validator"` 31 | } 32 | 33 | func (AddressBanResource) Transform(model resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 34 | ban := model.(models.ValidatorBan) 35 | 36 | return AddressBanResource{ 37 | FromBlockId: ban.BlockId, 38 | FromTimestamp: ban.Block.CreatedAt.Format(time.RFC3339), 39 | ToBlockId: ban.ToBlockId, 40 | Validator: new(validator.Resource).Transform(*ban.Validator), 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /core/enviroment.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/core/config" 5 | "log" 6 | "os" 7 | "strconv" 8 | ) 9 | 10 | type Environment struct { 11 | DbName string 12 | DbUser string 13 | DbPassword string 14 | DbPoolSize int 15 | DbHost string 16 | DbPort string 17 | Basecoin string 18 | ServerPort string 19 | IsDebug bool 20 | WsServer string 21 | WsBlocksChannel string 22 | MarketHost string 23 | } 24 | 25 | func NewEnvironment() *Environment { 26 | dbPoolSize, err := strconv.ParseInt(os.Getenv("DB_POOL_SIZE"), 10, 64) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | 31 | config.BaseCoinSymbol = os.Getenv("APP_BASE_COIN") 32 | config.SwapRouterProxyUrl = os.Getenv("SWAP_ROUTER_PROXY_URL") 33 | 34 | env := Environment{ 35 | DbName: os.Getenv("DB_NAME"), 36 | DbUser: os.Getenv("DB_USER"), 37 | DbPassword: os.Getenv("DB_PASSWORD"), 38 | DbPort: os.Getenv("DB_PORT"), 39 | DbPoolSize: int(dbPoolSize), 40 | DbHost: os.Getenv("DB_HOST"), 41 | Basecoin: os.Getenv("APP_BASE_COIN"), 42 | ServerPort: os.Getenv("EXPLORER_PORT"), 43 | IsDebug: os.Getenv("EXPLORER_DEBUG") == "1", 44 | WsServer: os.Getenv("CENTRIFUGO_LINK"), 45 | WsBlocksChannel: os.Getenv("CENTRIFUGO_BLOCK_CHANNEL"), 46 | MarketHost: os.Getenv("MARKET_HOST"), 47 | } 48 | 49 | return &env 50 | } 51 | -------------------------------------------------------------------------------- /api/v2/pools/routes.go: -------------------------------------------------------------------------------- 1 | package pools 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | // ApplyRoutes applies router to the gin Engine 6 | func ApplyRoutes(r *gin.RouterGroup) { 7 | pools := r.Group("/pools") 8 | { 9 | pools.GET("", GetSwapPools) 10 | pools.GET("/all", GetAllSwapPools) 11 | pools.GET("/token/:token", GetSwapPool) 12 | pools.GET("/token/:token/transactions", GetSwapPoolTransactions) 13 | pools.GET("/token/:token/providers", GetSwapPoolProviders) 14 | pools.GET("/token/:token/providers/:address", GetSwapPoolProvider) 15 | pools.GET("/token/:token/stats/volume", GetSwapPoolTradesVolume) 16 | pools.GET("/token/:token/orders", GetSwapPoolOrders) 17 | pools.GET("/coins/:coin0/:coin1", GetSwapPool) 18 | pools.GET("/coins/:coin0/:coin1/route", ProxySwapPoolRoute) 19 | pools.GET("/coins/:coin0/:coin1/estimate", EstimateSwap) 20 | pools.GET("/coins/:coin0/:coin1/transactions", GetSwapPoolTransactions) 21 | pools.GET("/coins/:coin0/:coin1/providers", GetSwapPoolProviders) 22 | pools.GET("/coins/:coin0/:coin1/stats/volume", GetSwapPoolTradesVolume) 23 | pools.GET("/coins/:coin0/:coin1/providers/:address", GetSwapPoolProvider) 24 | pools.GET("/coins/:coin0/:coin1/orders", GetSwapPoolOrders) 25 | pools.GET("/orders/:orderId", GetSwapPoolOrder) 26 | pools.GET("/providers/:address", GetSwapPoolsByProvider) 27 | pools.GET("/list/coins", GetCoinsList) 28 | pools.GET("/list/coins/:coin", GetCoinPossibleSwaps) 29 | pools.GET("/list/cmc", GetSwapPoolsList) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /invalid_transaction/repository.go: -------------------------------------------------------------------------------- 1 | package invalid_transaction 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/tools" 5 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 6 | "github.com/go-pg/pg/v10" 7 | ) 8 | 9 | type Repository struct { 10 | db *pg.DB 11 | } 12 | 13 | func NewRepository(db *pg.DB) *Repository { 14 | return &Repository{ 15 | db: db, 16 | } 17 | } 18 | 19 | // GetTxByHash get invalid transaction by hash 20 | func (r Repository) GetTxByHash(hash string) *models.InvalidTransaction { 21 | var transaction models.InvalidTransaction 22 | 23 | err := r.db.Model(&transaction). 24 | Relation("FromAddress"). 25 | Relation("GasCoin"). 26 | Where("hash = ?", hash). 27 | Select() 28 | 29 | if err != nil { 30 | return nil 31 | } 32 | 33 | return &transaction 34 | } 35 | 36 | // GetPaginatedByAddress get invalid transactions by address 37 | func (r Repository) GetPaginatedByAddress(address string, pagination *tools.Pagination) (txs []*models.InvalidTransaction, err error) { 38 | pagination.Total, err = r.db.Model(&txs). 39 | Relation("FromAddress"). 40 | Relation("GasCoin"). 41 | Join(`INNER JOIN addresses ON addresses.id = "invalid_transaction"."from_address_id"`). 42 | Where("addresses.address = ?", address). 43 | Where("gas_coin_id is not null"). 44 | Apply(pagination.Filter). 45 | Order("invalid_transaction.id DESC"). 46 | SelectAndCount() 47 | 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return txs, nil 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/stage.yml: -------------------------------------------------------------------------------- 1 | name: Stage 2 | 3 | on: 4 | push: 5 | branches: [ stage ] 6 | 7 | jobs: 8 | docker: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v1 13 | with: 14 | fetch-depth: 5 15 | 16 | - name: Version 17 | uses: pCYSl5EDgo/cat@master 18 | id: version 19 | with: 20 | path: VERSION 21 | 22 | - name: Build & Push to Docker Hub 23 | uses: opspresso/action-docker@master 24 | env: 25 | USERNAME: ${{ secrets.DOCKER_HUB_USER }} 26 | PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} 27 | IMAGE_NAME: "minterteam/explorer-api" 28 | LATEST: "true" 29 | VERSION: "${{ steps.version.outputs.text }}" 30 | TAG_NAME: ${{ github.sha }} 31 | DOCKER_BUILD_ARGS: --build-arg VERSION=${{ steps.version.outputs.text }} 32 | 33 | - name: Deploy 34 | uses: wahyd4/kubectl-helm-action@master 35 | env: 36 | KUBE_CONFIG_DATA: ${{ secrets.KUBE_STAGE_CONFIG_DATA }} 37 | with: 38 | args: helm upgrade -i explorer-api helm/ --reuse-values --atomic --timeout 100s --set image.tag=$GITHUB_SHA 39 | 40 | - name: notification 41 | if: cancelled() == false 42 | uses: xinthink/action-telegram@v1.1 43 | with: 44 | botToken: ${{ secrets.TELEGRAM_TOKEN }} 45 | chatId: ${{ secrets.TELEGRAM_TO }} 46 | jobStatus: ${{ job.status }} 47 | skipSuccess: false 48 | -------------------------------------------------------------------------------- /pool/models.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "math/big" 5 | "time" 6 | ) 7 | 8 | type SwapChain struct { 9 | PoolId int `json:"pool_id"` 10 | CoinIn int `json:"coin_in"` 11 | ValueIn string `json:"value_in"` 12 | CoinOut int `json:"coin_out"` 13 | ValueOut string `json:"value_out"` 14 | Details struct { 15 | AmountIn string `json:"amount_in"` 16 | AmountOut string `json:"amount_out"` 17 | CommissionAmountIn string `json:"commission_amount_in"` 18 | CommissionAmountOut string `json:"commission_amount_out"` 19 | Orders []struct { 20 | Buy string `json:"buy"` 21 | Sell string `json:"sell"` 22 | Seller string `json:"seller"` 23 | Id int `json:"id"` 24 | } `json:"orders"` 25 | } `json:"details"` 26 | Sellers []struct { 27 | Seller string `json:"seller"` 28 | Value string `json:"value"` 29 | } `json:"sellers"` 30 | } 31 | 32 | type tradeVolume struct { 33 | PoolId uint64 34 | Date time.Time 35 | FirstCoinVolume string 36 | SecondCoinVolume string 37 | } 38 | 39 | type TradeVolume struct { 40 | Date time.Time 41 | FirstCoinVolume string 42 | SecondCoinVolume string 43 | BipVolume *big.Float 44 | } 45 | 46 | type TradeVolumes struct { 47 | Day TradeVolume 48 | Month TradeVolume 49 | } 50 | 51 | type CoinTradingVolume struct { 52 | Volume string 53 | CoinId uint64 54 | } 55 | 56 | type TradeType int 57 | 58 | const ( 59 | TradeTypeExactInput TradeType = 0 60 | TradeTypeExactOutput TradeType = 1 61 | ) 62 | -------------------------------------------------------------------------------- /transaction/data_resources/remove_liquidity.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 7 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 8 | ) 9 | 10 | type RemoveLiquidity struct { 11 | Coin0 Coin `json:"coin0"` 12 | Coin1 Coin `json:"coin1"` 13 | PoolToken Coin `json:"pool_token"` 14 | Liquidity string `json:"liquidity"` 15 | MinimumVolume0 string `json:"minimum_volume0"` 16 | MinimumVolume1 string `json:"minimum_volume1"` 17 | Volume0 string `json:"volume0"` 18 | Volume1 string `json:"volume1"` 19 | } 20 | 21 | func (RemoveLiquidity) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 22 | data, model := txData.(*api_pb.RemoveLiquidityData), params[0].(models.Transaction) 23 | 24 | return RemoveLiquidity{ 25 | Coin0: new(Coin).Transform(data.Coin0), 26 | Coin1: new(Coin).Transform(data.Coin1), 27 | Liquidity: helpers.PipStr2Bip(data.Liquidity), 28 | MinimumVolume0: helpers.PipStr2Bip(data.MinimumVolume0), 29 | MinimumVolume1: helpers.PipStr2Bip(data.MinimumVolume1), 30 | Volume0: helpers.PipStr2Bip(model.Tags["tx.volume0"]), 31 | Volume1: helpers.PipStr2Bip(model.Tags["tx.volume1"]), 32 | PoolToken: Coin{ 33 | Symbol: model.Tags["tx.pool_token"], 34 | ID: helpers.StrToUint64(model.Tags["tx.pool_token_id"]), 35 | }, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.deploy/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: {{ include "chart.fullname" . }} 5 | annotations: 6 | kubernetes.io/ingress.class: nginx 7 | cert-manager.io/cluster-issuer: letsencrypt-prod 8 | nginx.ingress.kubernetes.io/enable-cors: "true" 9 | spec: 10 | rules: 11 | - host: "{{ .Values.ingress.host }}" 12 | http: 13 | paths: 14 | - backend: 15 | service: 16 | name: "{{ include "chart.fullname" . }}" 17 | port: 18 | number: {{.Values.service.port}} 19 | path: / 20 | pathType: ImplementationSpecific 21 | - host: "{{ .Values.ingress.host2 }}" 22 | http: 23 | paths: 24 | - backend: 25 | service: 26 | name: "{{ include "chart.fullname" . }}" 27 | port: 28 | number: {{.Values.service.port}} 29 | path: / 30 | pathType: ImplementationSpecific 31 | - host: "{{ .Values.ingress.host3 }}" 32 | http: 33 | paths: 34 | - backend: 35 | service: 36 | name: "{{ include "chart.fullname" . }}" 37 | port: 38 | number: {{.Values.service.port}} 39 | path: / 40 | pathType: ImplementationSpecific 41 | 42 | tls: 43 | - hosts: 44 | - "{{ .Values.ingress.host }}" 45 | - "{{ .Values.ingress.host2 }}" 46 | - "{{ .Values.ingress.host3 }}" 47 | secretName: "{{ .Values.ingress.host }}" 48 | -------------------------------------------------------------------------------- /slash/repository.go: -------------------------------------------------------------------------------- 1 | package slash 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/events" 5 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 6 | "github.com/MinterTeam/minter-explorer-api/v2/tools" 7 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 8 | "github.com/go-pg/pg/v10" 9 | ) 10 | 11 | type Repository struct { 12 | db *pg.DB 13 | } 14 | 15 | func NewRepository(db *pg.DB) *Repository { 16 | return &Repository{ 17 | db: db, 18 | } 19 | } 20 | 21 | func (r Repository) GetPaginatedByAddress(filter events.SelectFilter, pagination *tools.Pagination) []models.Slash { 22 | var slashes []models.Slash 23 | var err error 24 | 25 | pagination.Total, err = r.db.Model(&slashes). 26 | Relation("Coin"). 27 | Relation("Address.address"). 28 | Relation("Block.created_at"). 29 | Relation("Validator"). 30 | Apply(filter.Filter). 31 | Apply(pagination.Filter). 32 | Order("block_id DESC"). 33 | SelectAndCount() 34 | 35 | helpers.CheckErr(err) 36 | 37 | return slashes 38 | } 39 | 40 | func (r Repository) GetPaginatedByValidator(validator *models.Validator, pagination *tools.Pagination) ([]models.Slash, error) { 41 | var slashes []models.Slash 42 | var err error 43 | 44 | pagination.Total, err = r.db.Model(&slashes). 45 | Relation("Coin"). 46 | Relation("Address.address"). 47 | Relation("Block.created_at"). 48 | Where("validator_id = ?", validator.ID). 49 | Apply(pagination.Filter). 50 | Order("block_id DESC"). 51 | SelectAndCount() 52 | 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | return slashes, nil 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/mainnet.yml: -------------------------------------------------------------------------------- 1 | name: MainNet 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | docker: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v1 13 | with: 14 | fetch-depth: 5 15 | 16 | - name: Version 17 | uses: pCYSl5EDgo/cat@master 18 | id: version 19 | with: 20 | path: VERSION 21 | 22 | - name: Build & Push to Docker Hub 23 | uses: opspresso/action-docker@v0.2.13 24 | env: 25 | USERNAME: ${{ secrets.DOCKER_HUB_USER }} 26 | PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} 27 | IMAGE_NAME: "minterteam/explorer-api" 28 | LATEST: "true" 29 | VERSION: "${{ steps.version.outputs.text }}" 30 | TAG_NAME: ${{ github.sha }} 31 | DOCKER_BUILD_ARGS: --build-arg VERSION=${{ steps.version.outputs.text }} 32 | 33 | - name: deploy to production cluster 34 | uses: wahyd4/kubectl-helm-action@v0.1.2 35 | env: 36 | KUBE_CONFIG_DATA: ${{ secrets.KUBE_PROD_CONFIG_DATA }} 37 | TAG_NAME: ${{ github.sha }} 38 | with: 39 | args: helm upgrade -n mainnet -i api .deploy/ --atomic --timeout 100s --reuse-values --set image.tag=$TAG_NAME 40 | 41 | - name: notification 42 | if: cancelled() == false 43 | uses: xinthink/action-telegram@v1.1 44 | with: 45 | botToken: ${{ secrets.TELEGRAM_CI_TOKEN }} 46 | chatId: ${{ secrets.TELEGRAM_CI_PROD_TO }} 47 | jobStatus: ${{ job.status }} 48 | skipSuccess: false 49 | -------------------------------------------------------------------------------- /transaction/data_resources/redeem_check.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "encoding/base64" 5 | "github.com/MinterTeam/minter-explorer-api/v2/coins" 6 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 7 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 8 | dataModels "github.com/MinterTeam/minter-explorer-api/v2/transaction/data_models" 9 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 10 | ) 11 | 12 | type RedeemCheck struct { 13 | RawCheck string `json:"raw_check"` 14 | Proof string `json:"proof"` 15 | Check CheckData `json:"check"` 16 | } 17 | 18 | type CheckData struct { 19 | Coin resource.Interface `json:"coin"` 20 | GasCoin resource.Interface `json:"gas_coin"` 21 | Nonce string `json:"nonce"` 22 | Value string `json:"value"` 23 | Sender string `json:"sender"` 24 | DueBlock uint64 `json:"due_block"` 25 | } 26 | 27 | func (RedeemCheck) Transform(txData resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 28 | tx := params[0].(models.Transaction) 29 | model := tx.IData.(dataModels.Check) 30 | 31 | return RedeemCheck{ 32 | RawCheck: model.RawCheck, 33 | Proof: model.Proof, 34 | Check: CheckData{ 35 | Coin: new(coins.IdResource).Transform(model.Check.Coin), 36 | GasCoin: new(coins.IdResource).Transform(model.Check.GasCoin), 37 | Nonce: base64.StdEncoding.EncodeToString(model.Check.Nonce), 38 | Value: helpers.PipStr2Bip(model.Check.Value.String()), 39 | Sender: model.Check.Sender, 40 | DueBlock: model.Check.DueBlock, 41 | }, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /stake/service.go: -------------------------------------------------------------------------------- 1 | package stake 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 6 | "github.com/MinterTeam/minter-go-node/formula" 7 | "math/big" 8 | ) 9 | 10 | type Service struct { 11 | repository *Repository 12 | } 13 | 14 | func NewService(repository *Repository) *Service { 15 | return &Service{ 16 | repository: repository, 17 | } 18 | } 19 | 20 | func (s *Service) PrepareStakesModels(stakes []models.Stake) ([]models.Stake, error) { 21 | for i, stake := range stakes { 22 | if stake.IsKicked { 23 | bipValue, err := s.calculateBipValue(stake, stake.Coin) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | stakes[i].BipValue = bipValue.String() 29 | } 30 | } 31 | 32 | return stakes, nil 33 | } 34 | 35 | func (s *Service) calculateBipValue(stake models.Stake, coin *models.Coin) (*big.Int, error) { 36 | if coin.ID == 0 { 37 | return helpers.StringToBigInt(stake.Value), nil 38 | } 39 | 40 | totalStakeStr, err := s.repository.GetSumValueByCoin(coin.ID) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | totalStake := helpers.StringToBigInt(totalStakeStr) 46 | stakeValue := helpers.StringToBigInt(stake.Value) 47 | coinVolume := helpers.StringToBigInt(coin.Volume) 48 | coinReserve := helpers.StringToBigInt(coin.Reserve) 49 | 50 | freeFloat := new(big.Int).Sub(coinVolume, totalStake) 51 | freeFloatInBip := formula.CalculateSaleReturn(coinVolume, coinReserve, coin.Crr, freeFloat) 52 | delegatedBipValue := new(big.Int).Sub(coinReserve, freeFloatInBip) 53 | 54 | return new(big.Int).Div(new(big.Int).Mul(stakeValue, delegatedBipValue), totalStake), nil 55 | } 56 | -------------------------------------------------------------------------------- /reward/repository.go: -------------------------------------------------------------------------------- 1 | package reward 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/aggregated_reward" 5 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 6 | "github.com/MinterTeam/minter-explorer-api/v2/tools" 7 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 8 | "github.com/go-pg/pg/v10" 9 | "time" 10 | ) 11 | 12 | type Repository struct { 13 | db *pg.DB 14 | } 15 | 16 | func NewRepository(db *pg.DB) *Repository { 17 | return &Repository{ 18 | db: db, 19 | } 20 | } 21 | 22 | type ChartData struct { 23 | Time time.Time `json:"time"` 24 | Amount string `json:"amount"` 25 | } 26 | 27 | func (r Repository) GetAggregatedChartData(filter aggregated_reward.SelectFilter) []ChartData { 28 | var rewards models.AggregatedReward 29 | var chartData []ChartData 30 | 31 | err := r.db.Model(&rewards). 32 | Relation("Address._"). 33 | ColumnExpr("date_trunc('day', time_id) as time"). 34 | ColumnExpr("SUM(amount) as amount"). 35 | Group("time"). 36 | Order("time"). 37 | Apply(filter.Filter). 38 | Select(&chartData) 39 | 40 | helpers.CheckErr(err) 41 | 42 | return chartData 43 | } 44 | 45 | func (r Repository) GetPaginatedAggregatedByAddress(filter aggregated_reward.SelectFilter, pagination *tools.Pagination) []models.AggregatedReward { 46 | var rewards []models.AggregatedReward 47 | var err error 48 | 49 | // get rewards 50 | pagination.Total, err = r.db.Model(&rewards). 51 | Relation("Address.address"). 52 | Relation("Validator"). 53 | Apply(filter.Filter). 54 | Apply(pagination.Filter). 55 | Order("time_id DESC"). 56 | Order("amount"). 57 | SelectAndCount() 58 | 59 | helpers.CheckErr(err) 60 | 61 | return rewards 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minter Explorer api 2 | 3 | The official repository of Minter Explorer API service. 4 | 5 | ## API Docs 6 | https://app.swaggerhub.com/apis-docs/GrKamil/minter-explorer_api 7 | 8 | ## Related services: 9 | - [explorer-extender](https://github.com/MinterTeam/minter-explorer-extender) 10 | - [explorer-gate](https://github.com/MinterTeam/explorer-gate) 11 | - [explorer-validators](https://github.com/MinterTeam/minter-explorer-validators) - API for validators meta 12 | - [explorer-tools](https://github.com/MinterTeam/minter-explorer-tools) - common packages 13 | - [explorer-genesis-uploader](https://github.com/MinterTeam/explorer-genesis-uploader) 14 | 15 | ## BUILD 16 | 17 | - `go mod download` 18 | 19 | - `go build ./cmd/explorer.go` 20 | 21 | ## USE 22 | 23 | ### Requirement 24 | 25 | - PostgresSQL 26 | 27 | - Centrifugo (WebSocket server) [GitHub](https://github.com/centrifugal/centrifugo) 28 | 29 | - Explorer Extender [GitHub](https://github.com/MinterTeam/minter-explorer-extender) 30 | 31 | ### Setup 32 | 33 | - be sure [explorer-extender](https://github.com/MinterTeam/minter-explorer-extender) has been installed and work correctly 34 | 35 | - build and move the compiled file to the directory e.g. `/opt/minter/explorer` 36 | 37 | - copy .env file in extender's directory and fill with own values 38 | 39 | #### Run 40 | 41 | ./explorer 42 | 43 | ### Env file 44 | 45 | ``` 46 | EXPLORER_DEBUG=1 47 | APP_BASE_COIN=BIP 48 | CENTRIFUGO_LINK=wss://exporer-rtm.minter.network/connection/websocket 49 | CENTRIFUGO_BLOCK_CHANNEL=blocks 50 | DB_HOST=localhost 51 | DB_PORT=5432 52 | DB_POOL_SIZE=20 53 | DB_NAME=explorer 54 | DB_USER=minter 55 | DB_PASSWORD=password 56 | EXPLORER_PORT=8080 57 | MARKET_HOST=https://api.coingecko.com 58 | ``` -------------------------------------------------------------------------------- /transaction/types.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 6 | ) 7 | 8 | type Transaction interface { 9 | GetTags() map[string]string 10 | GetType() uint8 11 | GetData() json.RawMessage 12 | SetData(interface{}) 13 | SetCommissionPriceCoin(c models.Coin) 14 | GetModel() interface{} 15 | } 16 | 17 | type ValidTx struct { 18 | tx *models.Transaction 19 | } 20 | 21 | func NewValidTx(tx *models.Transaction) *ValidTx { 22 | return &ValidTx{tx} 23 | } 24 | 25 | func (v *ValidTx) GetTags() map[string]string { 26 | return v.tx.Tags 27 | } 28 | 29 | func (v *ValidTx) GetType() uint8 { 30 | return v.tx.Type 31 | } 32 | 33 | func (v *ValidTx) GetData() json.RawMessage { 34 | return v.tx.Data 35 | } 36 | 37 | func (v *ValidTx) SetData(value interface{}) { 38 | v.tx.IData = value 39 | } 40 | 41 | func (v *ValidTx) SetCommissionPriceCoin(coin models.Coin) { 42 | v.tx.CommissionPriceCoin = coin 43 | } 44 | 45 | func (v *ValidTx) GetModel() interface{} { 46 | return v.tx 47 | } 48 | 49 | type invalidTx struct { 50 | tx *models.InvalidTransaction 51 | } 52 | 53 | func NewInvalidTx(tx *models.InvalidTransaction) *invalidTx { 54 | return &invalidTx{tx} 55 | } 56 | 57 | func (i *invalidTx) GetTags() map[string]string { 58 | return i.tx.Tags 59 | } 60 | 61 | func (i *invalidTx) GetType() uint8 { 62 | return i.tx.Type 63 | } 64 | 65 | func (i *invalidTx) SetCommissionPriceCoin(coin models.Coin) { 66 | i.tx.CommissionPriceCoin = coin 67 | } 68 | 69 | func (i *invalidTx) GetData() json.RawMessage { 70 | // no data field for this model 71 | return nil 72 | } 73 | 74 | func (i *invalidTx) SetData(value interface{}) { 75 | // no data field for this model 76 | } 77 | 78 | func (i *invalidTx) GetModel() interface{} { 79 | return i.tx 80 | } 81 | -------------------------------------------------------------------------------- /balance/service.go: -------------------------------------------------------------------------------- 1 | package balance 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/pool" 6 | "github.com/MinterTeam/minter-explorer-api/v2/services" 7 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 8 | "github.com/MinterTeam/minter-go-node/formula" 9 | "math/big" 10 | ) 11 | 12 | type Service struct { 13 | baseCoin string 14 | poolService *pool.Service 15 | swapService *services.SwapService 16 | } 17 | 18 | func NewService(baseCoin string, poolService *pool.Service, swapService *services.SwapService) *Service { 19 | return &Service{baseCoin, poolService, swapService} 20 | } 21 | 22 | func (s *Service) GetTotalBalance(address *models.Address) *big.Int { 23 | sum := big.NewInt(0) 24 | for _, balance := range address.Balances { 25 | sum = sum.Add(sum, s.swapService.EstimateInBip(balance.Coin, helpers.StringToBigInt(balance.Value))) 26 | } 27 | 28 | return sum 29 | } 30 | 31 | func (s *Service) GetStakeBalance(stakes []models.Stake) *big.Int { 32 | sum := big.NewInt(0) 33 | 34 | for _, stake := range stakes { 35 | if stake.IsKicked || stake.Coin.Crr == 0 { 36 | continue 37 | } 38 | 39 | // just add base coin to sum 40 | if stake.CoinID == 0 { 41 | sum = sum.Add(sum, helpers.StringToBigInt(stake.Value)) 42 | continue 43 | } 44 | 45 | // calculate the sale return value for custom coin 46 | sum = sum.Add(sum, formula.CalculateSaleReturn( 47 | helpers.StringToBigInt(stake.Coin.Volume), 48 | helpers.StringToBigInt(stake.Coin.Reserve), 49 | uint(stake.Coin.Crr), 50 | helpers.StringToBigInt(stake.Value), 51 | )) 52 | } 53 | 54 | return sum 55 | } 56 | 57 | func (s *Service) GetTotalBalanceInUSD(sumInBasecoin *big.Int) *big.Float { 58 | return new(big.Float).Mul(new(big.Float).SetInt(sumInBasecoin), s.poolService.GetCoinPrice(0)) 59 | } 60 | -------------------------------------------------------------------------------- /transaction/data_resources/coin.go: -------------------------------------------------------------------------------- 1 | package data_resources 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/coins" 5 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 6 | "github.com/MinterTeam/minter-explorer-api/v2/pool" 7 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 8 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 9 | ) 10 | 11 | type Coin struct { 12 | ID uint64 `json:"id"` 13 | Symbol string `json:"symbol"` 14 | } 15 | 16 | func (Coin) Transform(data *api_pb.Coin) Coin { 17 | coin, err := coins.GlobalRepository.FindByID(uint(data.GetId())) 18 | helpers.CheckErr(err) 19 | 20 | return Coin{ 21 | ID: data.GetId(), 22 | Symbol: coin.GetSymbol(), 23 | } 24 | } 25 | 26 | type PoolCoin struct { 27 | ID uint64 `json:"id"` 28 | Symbol string `json:"symbol"` 29 | Amount string `json:"amount"` 30 | } 31 | 32 | func (PoolCoin) Transform(data *api_pb.Coin, pipAmount string) PoolCoin { 33 | return PoolCoin{ 34 | ID: data.GetId(), 35 | Symbol: data.GetSymbol(), 36 | Amount: helpers.PipStr2Bip(pipAmount), 37 | } 38 | } 39 | 40 | func (PoolCoin) TransformCollection(data []*api_pb.Coin, model models.Transaction) []PoolCoin { 41 | pools := pool.GetPoolChainFromStr(model.Tags["tx.pools"]) 42 | poolCoins := make([]PoolCoin, len(data)) 43 | 44 | for i, coin := range data { 45 | pool := pools[0] 46 | if len(pools) > 1 { 47 | if i == len(pools) { 48 | pool = pools[i-1] 49 | } else { 50 | pool = pools[i] 51 | } 52 | } 53 | 54 | amount := pool.ValueIn 55 | if i == len(pools) { 56 | amount = pool.ValueOut 57 | } 58 | 59 | // todo: refactor 60 | coinModel, err := coins.GlobalRepository.FindByID(uint(coin.Id)) 61 | helpers.CheckErr(err) 62 | coin.Symbol = coinModel.GetSymbol() 63 | 64 | poolCoins[i] = new(PoolCoin).Transform(coin, amount) 65 | } 66 | 67 | return poolCoins 68 | } 69 | -------------------------------------------------------------------------------- /order/repository.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/tools" 5 | "github.com/MinterTeam/minter-go-sdk/v2/transaction" 6 | "github.com/go-pg/pg/v10" 7 | ) 8 | 9 | type Repository struct { 10 | db *pg.DB 11 | } 12 | 13 | func NewRepository(db *pg.DB) *Repository { 14 | return &Repository{db} 15 | } 16 | 17 | func (r *Repository) GetListPaginated(pagination *tools.Pagination, filters ...tools.Filter) (orders []OrderTransaction, err error) { 18 | q := r.db.Model(&orders). 19 | Relation("Address"). 20 | Relation("CoinSell"). 21 | Relation("CoinBuy"). 22 | Column("coin_sell_volume", "coin_buy_volume", "created_at_block", "status", "liquidity_pool_id", "price"). 23 | ColumnExpr(`"order_transaction".id AS "id"`). 24 | ColumnExpr(`transactions.data AS "transaction__data"`). 25 | Join(`JOIN transactions ON (transactions.tags->>'tx.order_id')::int = "order_transaction".id and transactions.type = ?`, transaction.TypeAddLimitOrder). 26 | Apply(pagination.Filter) 27 | 28 | for _, f := range filters { 29 | q = q.Apply(f.Filter) 30 | } 31 | 32 | q = q.Order("id DESC") 33 | 34 | pagination.Total, err = q.SelectAndCount() 35 | return 36 | } 37 | 38 | func (r *Repository) FindById(id uint64) (OrderTransaction, error) { 39 | var order OrderTransaction 40 | err := r.db.Model(&order). 41 | Relation("Address"). 42 | Relation("CoinSell"). 43 | Relation("CoinBuy"). 44 | Column("coin_sell_volume", "coin_buy_volume", "created_at_block", "status", "liquidity_pool_id", "price"). 45 | ColumnExpr(`"order_transaction".id AS "id"`). 46 | ColumnExpr(`transactions.data AS "transaction__data"`). 47 | Join(`JOIN transactions ON (transactions.tags->>'tx.order_id')::int = "order_transaction".id and transactions.type = ?`, transaction.TypeAddLimitOrder). 48 | Where(`"order_transaction".id = ?`, id). 49 | Select() 50 | return order, err 51 | } 52 | -------------------------------------------------------------------------------- /errors/main.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "github.com/go-playground/validator/v10" 7 | log "github.com/sirupsen/logrus" 8 | "net/http" 9 | "runtime/debug" 10 | ) 11 | 12 | type Error struct { 13 | Message string `json:"message"` 14 | Fields map[string]string `json:"fields,omitempty"` 15 | } 16 | 17 | type Response struct { 18 | Error Error `json:"error"` 19 | } 20 | 21 | // Return error response 22 | func SetErrorResponse(statusCode int, text string, c *gin.Context) { 23 | c.JSON(statusCode, Response{ 24 | Error: Error{ 25 | Message: text, 26 | }, 27 | }) 28 | } 29 | 30 | // Returns validation errors 31 | func SetValidationErrorResponse(err error, c *gin.Context) { 32 | errs := err.(validator.ValidationErrors) 33 | 34 | errorFieldsList := make(map[string]string) 35 | for _, err := range errs { 36 | errorFieldsList[err.Field()] = validationErrorToText(err) 37 | } 38 | 39 | c.JSON(http.StatusUnprocessableEntity, Response{ 40 | Error{ 41 | Message: "Validation failed.", 42 | Fields: errorFieldsList, 43 | }, 44 | }) 45 | } 46 | 47 | // Get validation error field message 48 | func validationErrorToText(e validator.FieldError) string { 49 | field := e.Field() 50 | tag := e.Tag() 51 | 52 | switch tag { 53 | case "numeric": 54 | return fmt.Sprintf("The %s must be a number", field) 55 | case "max": 56 | return fmt.Sprintf("The length of field %s must be less than %s", field, e.Param()) 57 | case "required": 58 | return fmt.Sprintf("The field %s is required", field) 59 | case "oneof": 60 | return fmt.Sprintf("The field %s can have the next values: %s", field, e.Param()) 61 | default: 62 | return fmt.Sprintf("%s is not valid", field) 63 | } 64 | } 65 | 66 | func Recovery() { 67 | if err := recover(); err != nil { 68 | log.WithField("stacktrace", string(debug.Stack())).Error(err) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/taconet.yml: -------------------------------------------------------------------------------- 1 | name: M2 2 | 3 | on: 4 | push: 5 | branches: [ torronet ] 6 | 7 | jobs: 8 | docker: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v1 13 | with: 14 | fetch-depth: 5 15 | 16 | - name: Version 17 | uses: pCYSl5EDgo/cat@master 18 | id: version 19 | with: 20 | path: VERSION 21 | 22 | - name: Build & Push to Docker Hub 23 | uses: opspresso/action-docker@master 24 | env: 25 | USERNAME: ${{ secrets.DOCKER_HUB_USER }} 26 | PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} 27 | IMAGE_NAME: "minterteam/explorer-api" 28 | LATEST: "true" 29 | VERSION: "${{ steps.version.outputs.text }}" 30 | TAG_NAME: ${{ github.sha }} 31 | DOCKER_BUILD_ARGS: --build-arg VERSION=${{ steps.version.outputs.text }} 32 | 33 | - name: DigitalOcean Kubernetes 34 | uses: matootie/dokube@master 35 | with: 36 | personalAccessToken: ${{ secrets.DIGITALOCEAN_TOKEN }} 37 | clusterName: k8s-prod-do 38 | 39 | - name: Install helm 40 | run: wget https://get.helm.sh/helm-v3.3.4-linux-amd64.tar.gz &> /dev/null && tar xvzf helm-v3.3.4-linux-amd64.tar.gz && chmod 777 linux-amd64/helm && ./linux-amd64/helm ls -n taconet 41 | 42 | - name: Deploy to DO 43 | env: 44 | TAG_NAME: ${{ github.sha }} 45 | run: ./linux-amd64/helm upgrade -n toronet -i api helm/ --atomic --timeout 100s --set image.tag=$TAG_NAME --reuse-values 46 | 47 | - name: notification 48 | if: cancelled() == false 49 | uses: xinthink/action-telegram@v1.1 50 | with: 51 | botToken: ${{ secrets.TELEGRAM_TOKEN }} 52 | chatId: ${{ secrets.TELEGRAM_TO }} 53 | jobStatus: ${{ job.status }} 54 | skipSuccess: false -------------------------------------------------------------------------------- /services/validator.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/core/config" 5 | "github.com/MinterTeam/minter-explorer-api/v2/stake" 6 | "github.com/MinterTeam/minter-explorer-api/v2/tools/cache" 7 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | type ValidatorService struct { 12 | stakeRepo *stake.Repository 13 | cache *cache.ExplorerCache 14 | } 15 | 16 | type StakeRepository interface { 17 | GetMinStakes() ([]models.Stake, error) 18 | } 19 | 20 | func NewValidatorService(stakeRepo *stake.Repository, cache *cache.ExplorerCache) *ValidatorService { 21 | return &ValidatorService{ 22 | stakeRepo: stakeRepo, 23 | cache: cache, 24 | } 25 | } 26 | 27 | const ( 28 | minStakeCacheTime = 720 29 | ) 30 | 31 | type ValidatorsMinStake map[uint]string 32 | 33 | func (s *ValidatorService) GetMinStakesByValidator(validator *models.Validator) string { 34 | if len(validator.Stakes) < config.MaxDelegatorCount { 35 | return "0" 36 | } 37 | 38 | var stakes ValidatorsMinStake 39 | 40 | if s.cache.GetLastBlock().ID%120 == 0 { 41 | stakes = s.getMinStakes() 42 | s.cache.Store("min_stakes", stakes, minStakeCacheTime) 43 | } else { 44 | stakes = s.cache.Get("min_stakes", func() interface{} { 45 | return s.getMinStakes() 46 | }, minStakeCacheTime).(ValidatorsMinStake) 47 | } 48 | 49 | if bipStake, ok := stakes[validator.ID]; ok { 50 | return bipStake 51 | } 52 | 53 | return "0" 54 | } 55 | 56 | func (s *ValidatorService) getMinStakes() ValidatorsMinStake { 57 | stakes, err := s.stakeRepo.GetMinStakes() 58 | if err != nil { 59 | log.Errorf("failed to get min stakes: %s", err) 60 | return nil 61 | } 62 | 63 | minStakes := make(ValidatorsMinStake, len(stakes)) 64 | for _, v := range stakes { 65 | minStakes[v.ValidatorID] = v.BipValue 66 | } 67 | 68 | return minStakes 69 | } 70 | -------------------------------------------------------------------------------- /check/resource.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "encoding/base64" 5 | "github.com/MinterTeam/minter-explorer-api/v2/coins" 6 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 7 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 8 | "github.com/MinterTeam/minter-explorer-api/v2/transaction/data_models" 9 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 10 | "time" 11 | ) 12 | 13 | type Resource struct { 14 | TxHash string `json:"tx_hash"` 15 | BlockID uint64 `json:"block_id"` 16 | Timestamp string `json:"timestamp"` 17 | FromAddress string `json:"address_from"` 18 | ToAddress string `json:"address_to"` 19 | Coin resource.Interface `json:"coin"` 20 | GasCoin resource.Interface `json:"gas_coin"` 21 | Nonce string `json:"nonce"` 22 | Value string `json:"value"` 23 | DueBlock uint64 `json:"due_block"` 24 | RawCheck string `json:"raw_check"` 25 | } 26 | 27 | type Params struct { 28 | CheckData *data_models.CheckData 29 | } 30 | 31 | func (r Resource) Transform(model resource.ItemInterface, resourceParams ...resource.ParamInterface) resource.Interface { 32 | c := model.(models.Check) 33 | data := resourceParams[0].(Params).CheckData 34 | 35 | return Resource{ 36 | TxHash: c.Transaction.GetHash(), 37 | BlockID: c.Transaction.BlockID, 38 | Timestamp: c.Transaction.CreatedAt.Format(time.RFC3339), 39 | FromAddress: c.ToAddress.GetAddress(), 40 | ToAddress: c.FromAddress.GetAddress(), 41 | Coin: new(coins.IdResource).Transform(data.Coin), 42 | GasCoin: new(coins.IdResource).Transform(data.GasCoin), 43 | Nonce: base64.StdEncoding.EncodeToString(data.Nonce), 44 | Value: helpers.PipStr2Bip(data.Value.String()), 45 | DueBlock: data.DueBlock, 46 | RawCheck: `Mc` + c.Data, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/testnet.yml: -------------------------------------------------------------------------------- 1 | name: TestsNet 2 | 3 | on: 4 | push: 5 | branches: [ testnet ] 6 | 7 | jobs: 8 | docker: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v1 13 | with: 14 | fetch-depth: 5 15 | 16 | - name: Version 17 | uses: pCYSl5EDgo/cat@master 18 | id: version 19 | with: 20 | path: VERSION 21 | 22 | - name: Build & Push to Docker Hub 23 | uses: opspresso/action-docker@master 24 | env: 25 | USERNAME: ${{ secrets.DOCKER_HUB_USER }} 26 | PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} 27 | IMAGE_NAME: "minterteam/explorer-api" 28 | LATEST: "true" 29 | VERSION: "${{ steps.version.outputs.text }}" 30 | TAG_NAME: ${{ github.sha }} 31 | DOCKER_BUILD_ARGS: --build-arg VERSION=${{ steps.version.outputs.text }} 32 | 33 | - name: DigitalOcean Kubernetes 34 | uses: matootie/dokube@master 35 | with: 36 | personalAccessToken: ${{ secrets.DIGITALOCEAN_TOKEN }} 37 | clusterName: k8s-prod-do 38 | 39 | - name: Install helm 40 | run: wget https://get.helm.sh/helm-v3.3.4-linux-amd64.tar.gz &> /dev/null && tar xvzf helm-v3.3.4-linux-amd64.tar.gz && chmod 777 linux-amd64/helm && ./linux-amd64/helm ls -n testnet-reserved 41 | 42 | - name: Deploy to DO 43 | env: 44 | TAG_NAME: ${{ github.sha }} 45 | run: ./linux-amd64/helm upgrade -n testnet-reserved -i api .deploy/ --atomic --timeout 100s --set image.tag=$TAG_NAME --reuse-values 46 | 47 | - name: notification 48 | if: cancelled() == false 49 | uses: xinthink/action-telegram@v1.1 50 | with: 51 | botToken: ${{ secrets.TELEGRAM_TOKEN }} 52 | chatId: ${{ secrets.TELEGRAM_TO }} 53 | jobStatus: ${{ job.status }} 54 | skipSuccess: false -------------------------------------------------------------------------------- /.github/workflows/do-mainnet.yml: -------------------------------------------------------------------------------- 1 | name: MainNet-DO 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | docker: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v1 13 | with: 14 | fetch-depth: 5 15 | 16 | - name: Version 17 | uses: pCYSl5EDgo/cat@master 18 | id: version 19 | with: 20 | path: VERSION 21 | 22 | - name: Build & Push to Docker Hub 23 | uses: opspresso/action-docker@v0.2.13 24 | env: 25 | USERNAME: ${{ secrets.DOCKER_HUB_USER }} 26 | PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} 27 | IMAGE_NAME: "minterteam/explorer-api" 28 | LATEST: "true" 29 | VERSION: "${{ steps.version.outputs.text }}" 30 | TAG_NAME: ${{ github.sha }} 31 | DOCKER_BUILD_ARGS: --build-arg VERSION=${{ steps.version.outputs.text }} 32 | 33 | - name: DigitalOcean Kubernetes 34 | uses: matootie/dokube@master 35 | with: 36 | personalAccessToken: ${{ secrets.DIGITALOCEAN_TOKEN }} 37 | clusterName: k8s-prod-do 38 | 39 | - name: Install helm 40 | run: wget https://get.helm.sh/helm-v3.3.4-linux-amd64.tar.gz &> /dev/null && tar xvzf helm-v3.3.4-linux-amd64.tar.gz && chmod 777 linux-amd64/helm && ./linux-amd64/helm ls -n mainnet-reserved 41 | 42 | - name: Deploy to reserv 43 | env: 44 | TAG_NAME: ${{ github.sha }} 45 | run: ./linux-amd64/helm upgrade -n mainnet-reserved api .deploy/ --atomic --timeout 100s --set image.tag=$TAG_NAME --reuse-values 46 | 47 | - name: notification 48 | if: cancelled() == false 49 | uses: xinthink/action-telegram@v1.1 50 | with: 51 | botToken: ${{ secrets.TELEGRAM_CI_TOKEN }} 52 | chatId: ${{ secrets.TELEGRAM_CI_PROD_TO }} 53 | jobStatus: ${{ job.status }} 54 | skipSuccess: false 55 | -------------------------------------------------------------------------------- /api/v2/statistics/handler.go: -------------------------------------------------------------------------------- 1 | package statistics 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/chart" 5 | "github.com/MinterTeam/minter-explorer-api/v2/core" 6 | "github.com/MinterTeam/minter-explorer-api/v2/core/config" 7 | "github.com/MinterTeam/minter-explorer-api/v2/errors" 8 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 9 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 10 | "github.com/gin-gonic/gin" 11 | "net/http" 12 | "time" 13 | ) 14 | 15 | type GetTransactionsRequest struct { 16 | Scale *string `form:"scale" binding:"omitempty,eq=minute|eq=hour|eq=day"` 17 | StartTime *string `form:"start_time" binding:"omitempty,timestamp"` 18 | EndTime *string `form:"end_time" binding:"omitempty,timestamp"` 19 | } 20 | 21 | // statistics cache time 22 | const CacheTime = time.Duration(600) 23 | 24 | func GetTransactions(c *gin.Context) { 25 | explorer := c.MustGet("explorer").(*core.Explorer) 26 | 27 | var request GetTransactionsRequest 28 | if err := c.ShouldBindQuery(&request); err != nil { 29 | errors.SetValidationErrorResponse(err, c) 30 | return 31 | } 32 | 33 | // set default scale 34 | scale := config.DefaultStatisticsScale 35 | if request.Scale != nil { 36 | scale = *request.Scale 37 | } 38 | 39 | // set default start time 40 | startTime := helpers.StartOfTheDay(time.Now().AddDate(0, 0, config.DefaultStatisticsDayDelta)).Format("2006-01-02 15:04:05") 41 | if request.StartTime != nil { 42 | startTime = *request.StartTime 43 | } 44 | 45 | // cache request without query parameters 46 | txs := explorer.Cache.ExecuteOrGet("tx_statistics", func() interface{} { 47 | return explorer.TransactionRepository.GetTxCountChartDataByFilter(chart.SelectFilter{ 48 | Scale: scale, 49 | StartTime: &startTime, 50 | EndTime: request.EndTime, 51 | }) 52 | }, CacheTime, len(c.Request.URL.Query()) != 0) 53 | 54 | c.JSON(http.StatusOK, gin.H{ 55 | "data": resource.TransformCollection(txs, chart.TransactionResource{}), 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /cmd/explorer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/api" 5 | "github.com/MinterTeam/minter-explorer-api/v2/core" 6 | "github.com/MinterTeam/minter-explorer-api/v2/core/ws" 7 | "github.com/MinterTeam/minter-explorer-api/v2/database" 8 | "github.com/MinterTeam/minter-explorer-api/v2/tools/metrics" 9 | "github.com/MinterTeam/minter-explorer-api/v2/tools/recovery" 10 | "github.com/joho/godotenv" 11 | log "github.com/sirupsen/logrus" 12 | "os" 13 | "time" 14 | ) 15 | 16 | func main() { 17 | // Init Logger 18 | log.SetOutput(os.Stdout) 19 | log.SetReportCaller(true) 20 | log.SetLevel(log.DebugLevel) 21 | log.SetFormatter(&log.TextFormatter{ 22 | FullTimestamp: true, 23 | TimestampFormat: time.RFC3339Nano, 24 | }) 25 | 26 | err := godotenv.Load() 27 | if err != nil { 28 | log.Fatal(".env file not found") 29 | } 30 | 31 | // init environment 32 | env := core.NewEnvironment() 33 | 34 | // connect to database 35 | db := database.Connect(env) 36 | defer database.Close(db) 37 | 38 | // create explorer 39 | explorer := core.NewExplorer(db, env) 40 | 41 | // run market price update 42 | go recovery.SafeGo(explorer.MarketService.Run) 43 | go recovery.SafeGo(explorer.PoolService.RunPoolUpdater) 44 | go recovery.SafeGo(explorer.PoolService.RunTradingVolumeUpdater) 45 | 46 | // create ws extender 47 | extender := ws.NewExtenderWsClient(explorer) 48 | defer extender.Close() 49 | 50 | // create ws channel handler 51 | blocksChannelHandler := ws.NewBlocksChannelHandler() 52 | blocksChannelHandler.AddSubscriber(explorer.Cache) 53 | blocksChannelHandler.AddSubscriber(metrics.NewLastBlockMetric()) 54 | blocksChannelHandler.AddSubscriber(explorer.PoolService) 55 | blocksChannelHandler.AddSubscriber(explorer.CoinRepository) 56 | 57 | // subscribe to channel and add cache handler 58 | sub := extender.CreateSubscription(explorer.Environment.WsBlocksChannel) 59 | sub.OnPublish(blocksChannelHandler) 60 | extender.Subscribe(sub) 61 | 62 | api.Run(db, explorer) 63 | } 64 | -------------------------------------------------------------------------------- /.deploy/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "chart.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "chart.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "chart.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "chart.labels" -}} 38 | helm.sh/chart: {{ include "chart.chart" . }} 39 | {{ include "chart.selectorLabels" . }} 40 | {{- if .Chart.AppVersion }} 41 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 42 | {{- end }} 43 | app.kubernetes.io/managed-by: {{ .Release.Service }} 44 | {{- end -}} 45 | 46 | {{/* 47 | Selector labels 48 | */}} 49 | {{- define "chart.selectorLabels" -}} 50 | app.kubernetes.io/name: {{ include "chart.name" . }} 51 | app.kubernetes.io/instance: {{ .Release.Name }} 52 | {{- end -}} 53 | 54 | {{/* 55 | Create the name of the service account to use 56 | */}} 57 | {{- define "chart.serviceAccountName" -}} 58 | {{- if .Values.serviceAccount.create -}} 59 | {{ default (include "chart.fullname" .) .Values.serviceAccount.name }} 60 | {{- else -}} 61 | {{ default "default" .Values.serviceAccount.name }} 62 | {{- end -}} 63 | {{- end -}} 64 | -------------------------------------------------------------------------------- /order/resource.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/coins" 5 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 6 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 7 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 8 | "google.golang.org/protobuf/encoding/protojson" 9 | "google.golang.org/protobuf/proto" 10 | "reflect" 11 | ) 12 | 13 | type Resource struct { 14 | Id uint64 `json:"id"` 15 | Address string `json:"address"` 16 | PoolId uint64 `json:"pool_id"` 17 | InitialCoinToBuyVolume string `json:"initial_coin_to_buy_volume"` 18 | CoinToBuyVolume string `json:"coin_to_buy_volume"` 19 | InitialCoinToSellVolume string `json:"initial_coin_to_sell_volume"` 20 | CoinToSellVolume string `json:"coin_to_sell_volume"` 21 | CoinToSell resource.Interface `json:"coin_to_sell"` 22 | CoinToBuy resource.Interface `json:"coin_to_buy"` 23 | Height uint64 `json:"height"` 24 | Status Status `json:"status"` 25 | } 26 | 27 | func (Resource) Transform(model resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 28 | order := model.(OrderTransaction) 29 | 30 | txData := reflect.New(reflect.TypeOf(new(api_pb.AddLimitOrderData)).Elem()).Interface().(proto.Message) 31 | protojson.Unmarshal(order.Transaction.Data, txData) 32 | limitOrder := txData.(*api_pb.AddLimitOrderData) 33 | 34 | return Resource{ 35 | Id: order.Id, 36 | Address: order.Address.GetAddress(), 37 | PoolId: order.LiquidityPoolId, 38 | CoinToSell: new(coins.IdResource).Transform(*order.CoinSell), 39 | CoinToBuy: new(coins.IdResource).Transform(*order.CoinBuy), 40 | CoinToBuyVolume: helpers.PipStr2Bip(order.CoinBuyVolume), 41 | CoinToSellVolume: helpers.PipStr2Bip(order.CoinSellVolume), 42 | InitialCoinToBuyVolume: helpers.PipStr2Bip(limitOrder.ValueToBuy), 43 | InitialCoinToSellVolume: helpers.PipStr2Bip(limitOrder.ValueToSell), 44 | Height: order.CreatedAtBlock, 45 | Status: MakeOrderStatus(order.Status), 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.deploy/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "chart.fullname" . }} 5 | labels: 6 | {{- include "chart.labels" . | nindent 4 }} 7 | spec: 8 | replicas: {{ .Values.replicaCount }} 9 | selector: 10 | matchLabels: 11 | {{- include "chart.selectorLabels" . | nindent 6 }} 12 | template: 13 | metadata: 14 | labels: 15 | {{- include "chart.selectorLabels" . | nindent 8 }} 16 | spec: 17 | {{- with .Values.imagePullSecrets }} 18 | imagePullSecrets: 19 | {{- toYaml . | nindent 8 }} 20 | {{- end }} 21 | serviceAccountName: {{ include "chart.serviceAccountName" . }} 22 | securityContext: 23 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 24 | containers: 25 | - name: {{ .Chart.Name }} 26 | securityContext: {{- toYaml .Values.securityContext | nindent 12 }} 27 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 28 | imagePullPolicy: {{ .Values.image.pullPolicy }} 29 | volumeMounts: 30 | - name: {{ include "chart.fullname" . }} 31 | mountPath: {{ .Values.mountpath }} 32 | readOnly: true 33 | ports: 34 | - name: http 35 | containerPort: {{.Values.service.port}} 36 | protocol: TCP 37 | - name: pprof 38 | containerPort: 6060 39 | protocol: TCP 40 | livenessProbe: 41 | failureThreshold: 5 42 | periodSeconds: 20 43 | timeoutSeconds: 5 44 | httpGet: 45 | path: /api/v2/check 46 | port: http 47 | readinessProbe: 48 | httpGet: 49 | path: /api/v2/check 50 | port: http 51 | resources: 52 | {{- toYaml .Values.resources | nindent 12 }} 53 | {{- with .Values.nodeSelector }} 54 | nodeSelector: 55 | {{- toYaml . | nindent 8 }} 56 | {{- end }} 57 | {{- with .Values.affinity }} 58 | affinity: 59 | {{- toYaml . | nindent 8 }} 60 | {{- end }} 61 | {{- with .Values.tolerations }} 62 | tolerations: 63 | {{- toYaml . | nindent 8 }} 64 | {{- end }} 65 | volumes: 66 | - name: {{ include "chart.fullname" . }} 67 | configMap: 68 | name: {{ include "chart.fullname" . }} 69 | -------------------------------------------------------------------------------- /helpers/math.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "math" 5 | "math/big" 6 | "time" 7 | ) 8 | 9 | // default amount of pips in 1 bip 10 | var pipInBip = big.NewFloat(1000000000000000000) 11 | 12 | var feeDefaultMultiplier = big.NewInt(100000000000000000) 13 | 14 | // default amount of unit in one bip 15 | const unitInBip = 10 16 | 17 | func PipStr2Bip(value string) string { 18 | if value == "" { 19 | value = "0" 20 | } 21 | 22 | floatValue, err := new(big.Float).SetPrec(500).SetString(value) 23 | CheckErrBool(err) 24 | 25 | return new(big.Float).SetPrec(500).Quo(floatValue, pipInBip).Text('f', 18) 26 | } 27 | 28 | func Pip2Bip(pip *big.Int) *big.Float { 29 | return new(big.Float).Quo(new(big.Float).SetInt(pip), pipInBip) 30 | } 31 | 32 | func Pip2BipStr(pip *big.Int) string { 33 | return Bip2Str(Pip2Bip(pip)) 34 | } 35 | 36 | func Bip2Str(bip *big.Float) string { 37 | return bip.Text('f', 18) 38 | } 39 | 40 | func Bip2Pip(bip *big.Float) *big.Int { 41 | value, _ := bip.Mul(bip, pipInBip).Int(nil) 42 | return value 43 | } 44 | 45 | func Fee2Bip(value uint64) string { 46 | return PipStr2Bip(new(big.Int).Mul(feeDefaultMultiplier, new(big.Int).SetUint64(value)).String()) 47 | } 48 | 49 | func CalculatePercent(part string, total string) string { 50 | v1, err := new(big.Float).SetString(part) 51 | CheckErrBool(err) 52 | 53 | v2, err := new(big.Float).SetString(total) 54 | CheckErrBool(err) 55 | 56 | v1 = new(big.Float).Mul(v1, big.NewFloat(100)) 57 | 58 | return new(big.Float).Quo(v1, v2).String() 59 | } 60 | 61 | func Round(value float64, precision int) float64 { 62 | return math.Round(value*math.Pow10(precision)) / math.Pow10(precision) 63 | } 64 | 65 | func Nano2Seconds(nano uint64) float64 { 66 | return float64(nano) / float64(time.Second) 67 | } 68 | 69 | func Unit2Bip(units float64) float64 { 70 | return units / unitInBip 71 | } 72 | 73 | func Seconds2Nano(sec int) float64 { 74 | return float64(sec) * float64(time.Second) 75 | } 76 | 77 | func StringToBigInt(string string) *big.Int { 78 | if len(string) == 0 { 79 | return big.NewInt(0) 80 | } 81 | 82 | bInt, err := new(big.Int).SetString(string, 10) 83 | CheckErrBool(err) 84 | 85 | return bInt 86 | } 87 | 88 | func StrToBigFloat(string string) *big.Float { 89 | if len(string) == 0 { 90 | return big.NewFloat(0) 91 | } 92 | 93 | bFloat, err := new(big.Float).SetString(string) 94 | CheckErrBool(err) 95 | 96 | return bFloat 97 | } 98 | -------------------------------------------------------------------------------- /blocks/repository.go: -------------------------------------------------------------------------------- 1 | package blocks 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/core/config" 5 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 6 | "github.com/MinterTeam/minter-explorer-api/v2/tools" 7 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 8 | "github.com/go-pg/pg/v10" 9 | "github.com/go-pg/pg/v10/orm" 10 | "time" 11 | ) 12 | 13 | type Repository struct { 14 | db *pg.DB 15 | } 16 | 17 | func NewRepository(db *pg.DB) *Repository { 18 | orm.RegisterTable((*models.BlockValidator)(nil)) 19 | 20 | return &Repository{ 21 | db: db, 22 | } 23 | } 24 | 25 | // Get block by height (id) 26 | func (r Repository) GetById(id uint64) *models.Block { 27 | var block models.Block 28 | 29 | err := r.db.Model(&block). 30 | Relation("BlockValidators"). 31 | Relation("BlockValidators.Validator"). 32 | Where("block.id = ?", id). 33 | Select() 34 | 35 | if err != nil { 36 | return nil 37 | } 38 | 39 | return &block 40 | } 41 | 42 | // Get paginated list of blocks 43 | func (r Repository) GetPaginated(pagination *tools.Pagination) []models.Block { 44 | var blocks []models.Block 45 | var err error 46 | 47 | pagination.Total, err = r.db.Model(&blocks). 48 | Relation("BlockValidators"). 49 | Apply(pagination.Filter). 50 | Order("id DESC"). 51 | SelectAndCount() 52 | 53 | helpers.CheckErr(err) 54 | 55 | return blocks 56 | } 57 | 58 | // Get last block 59 | func (r Repository) GetLastBlock() models.Block { 60 | var block models.Block 61 | r.db.Model(&block).Last() 62 | return block 63 | } 64 | 65 | // Get average block time 66 | func (r Repository) GetAverageBlockTime() float64 { 67 | var block models.Block 68 | var blockTime float64 69 | 70 | err := r.db.Model(&block). 71 | ColumnExpr("AVG(block_time) / ?", time.Second). 72 | Where("created_at >= ?", time.Now().AddDate(0, 0, -1).Format(time.RFC3339)). 73 | Select(&blockTime) 74 | 75 | helpers.CheckErr(err) 76 | 77 | return blockTime 78 | } 79 | 80 | // Get sum of delta slow time 81 | func (r Repository) GetSumSlowBlocksTimeBy24h() float64 { 82 | var block models.Block 83 | var sum float64 84 | 85 | err := r.db.Model(&block). 86 | ColumnExpr("SUM(block_time - ?) / ?", helpers.Seconds2Nano(config.SlowBlocksMaxTimeInSec), helpers.Seconds2Nano(1)). 87 | Where("block_time >= ?", helpers.Seconds2Nano(config.SlowBlocksMaxTimeInSec)). 88 | Where("created_at >= ?", time.Now().AddDate(0, 0, -1).Format(time.RFC3339)). 89 | Select(&sum) 90 | 91 | helpers.CheckErr(err) 92 | 93 | return sum 94 | } 95 | -------------------------------------------------------------------------------- /api/v2/addresses/utils.go: -------------------------------------------------------------------------------- 1 | package addresses 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/events" 5 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 6 | "github.com/MinterTeam/minter-explorer-api/v2/tools" 7 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // GetAddressFromRequestUri Get minter address from current request uri 12 | func GetAddressFromRequestUri(c *gin.Context) (*string, error) { 13 | var request GetAddressRequest 14 | if err := c.ShouldBindUri(&request); err != nil { 15 | return nil, err 16 | } 17 | 18 | minterAddress := helpers.RemoveMinterPrefix(request.Address) 19 | return &minterAddress, nil 20 | } 21 | 22 | // Return model address with zero base coin 23 | func makeEmptyAddressModel(minterAddress string, baseCoin string) *models.Address { 24 | return &models.Address{ 25 | Address: minterAddress, 26 | Balances: []*models.Balance{{ 27 | Coin: &models.Coin{ 28 | Symbol: baseCoin, 29 | Type: models.CoinTypeBase, 30 | }, 31 | Value: "0", 32 | }}, 33 | } 34 | } 35 | 36 | // Check that array of address models contain exact minter address 37 | func isModelsContainAddress(minterAddress string, models []*models.Address) bool { 38 | for _, item := range models { 39 | if item.Address == minterAddress { 40 | return true 41 | } 42 | } 43 | 44 | return false 45 | } 46 | 47 | func extendModelWithBaseSymbolBalance(model *models.Address, minterAddress, baseCoin string) *models.Address { 48 | // if model not found 49 | if model == nil || len(model.Balances) == 0 { 50 | return makeEmptyAddressModel(minterAddress, baseCoin) 51 | } 52 | 53 | isBaseSymbolExists := false 54 | for _, b := range model.Balances { 55 | if b.CoinID == 0 { 56 | isBaseSymbolExists = true 57 | } 58 | } 59 | 60 | if !isBaseSymbolExists { 61 | model.Balances = append(model.Balances, &models.Balance{ 62 | Value: "0", 63 | Coin: &models.Coin{Symbol: baseCoin, Type: models.CoinTypeBase}, 64 | }) 65 | } 66 | 67 | return model 68 | } 69 | 70 | func prepareEventsRequest(c *gin.Context) (*events.SelectFilter, *tools.Pagination, error) { 71 | minterAddress, err := GetAddressFromRequestUri(c) 72 | if err != nil { 73 | return nil, nil, err 74 | } 75 | 76 | var requestQuery FilterQueryRequest 77 | if err := c.ShouldBindQuery(&requestQuery); err != nil { 78 | return nil, nil, err 79 | } 80 | 81 | pagination := tools.NewPagination(c.Request) 82 | 83 | return &events.SelectFilter{ 84 | Address: *minterAddress, 85 | StartBlock: requestQuery.StartBlock, 86 | EndBlock: requestQuery.EndBlock, 87 | }, &pagination, nil 88 | } 89 | -------------------------------------------------------------------------------- /api/v2/checks/handlers.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/check" 5 | "github.com/MinterTeam/minter-explorer-api/v2/core" 6 | "github.com/MinterTeam/minter-explorer-api/v2/errors" 7 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 8 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 9 | "github.com/MinterTeam/minter-explorer-api/v2/tools" 10 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 11 | "github.com/gin-gonic/gin" 12 | "net/http" 13 | ) 14 | 15 | type GetCheckRequest struct { 16 | RawCheck string `uri:"raw"` 17 | } 18 | 19 | type GetChecksRequest struct { 20 | FromAddress string `form:"address_to" binding:"omitempty,minterAddress"` 21 | ToAddress string `form:"address_from" binding:"omitempty,minterAddress"` 22 | } 23 | 24 | // Get check information 25 | func GetCheck(c *gin.Context) { 26 | explorer := c.MustGet("explorer").(*core.Explorer) 27 | 28 | // validate request 29 | var req GetCheckRequest 30 | if err := c.ShouldBindUri(&req); err != nil { 31 | errors.SetValidationErrorResponse(err, c) 32 | return 33 | } 34 | 35 | minterCheck, err := explorer.CheckRepository.GetByRawCheck(helpers.RemoveMinterPrefix(req.RawCheck)) 36 | if err != nil { 37 | errors.SetErrorResponse(http.StatusNotFound, "Check not found.", c) 38 | return 39 | } 40 | 41 | checkData, err := explorer.TransactionService.TransformBaseCheckToModel(req.RawCheck) 42 | helpers.CheckErr(err) 43 | 44 | c.JSON(http.StatusOK, gin.H{ 45 | "data": new(check.Resource).Transform(minterCheck, check.Params{CheckData: checkData}), 46 | }) 47 | } 48 | 49 | // Get redeemed checks by filter 50 | func GetChecks(c *gin.Context) { 51 | explorer := c.MustGet("explorer").(*core.Explorer) 52 | 53 | // validate request 54 | var req GetChecksRequest 55 | if err := c.ShouldBindQuery(&req); err != nil { 56 | errors.SetValidationErrorResponse(err, c) 57 | return 58 | } 59 | 60 | pagination := tools.NewPagination(c.Request) 61 | filter := check.SelectFilter{FromAddress: req.FromAddress, ToAddress: req.ToAddress} 62 | checks, err := explorer.CheckRepository.GetListByFilter(filter, &pagination) 63 | helpers.CheckErr(err) 64 | 65 | // add params to each model resource 66 | resourceCallback := func(model resource.ParamInterface) resource.ParamsInterface { 67 | checkData, err := explorer.TransactionService.TransformBaseCheckToModel(`Mc` + model.(models.Check).Data) 68 | helpers.CheckErr(err) 69 | return resource.ParamsInterface{check.Params{CheckData: checkData}} 70 | } 71 | 72 | c.JSON(http.StatusOK, resource.TransformPaginatedCollectionWithCallback(checks, check.Resource{}, pagination, resourceCallback)) 73 | } 74 | -------------------------------------------------------------------------------- /order/filters.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 5 | "github.com/go-pg/pg/v10/orm" 6 | ) 7 | 8 | type AddressFilter struct { 9 | Address string 10 | } 11 | 12 | func NewAddressFilter(address string) AddressFilter { 13 | return AddressFilter{address} 14 | } 15 | 16 | func (f AddressFilter) Filter(q *orm.Query) (*orm.Query, error) { 17 | if len(f.Address) == 0 { 18 | return q, nil 19 | } 20 | 21 | return q.Where(`"address"."address" = ?`, f.Address), nil 22 | } 23 | 24 | // ------------------------------ 25 | 26 | type TypeFilter struct { 27 | Type Type 28 | pool models.LiquidityPool 29 | coinId uint64 30 | } 31 | 32 | func NewTypeFilter(f string, pool models.LiquidityPool, coinId uint64) TypeFilter { 33 | return TypeFilter{Type(f), pool, coinId} 34 | } 35 | 36 | func (f TypeFilter) Filter(q *orm.Query) (*orm.Query, error) { 37 | if f.Type == OrderTypeBuy { 38 | q = q.Where("coin_buy_id = ?", f.coinId).OrderExpr("price desc") 39 | } 40 | 41 | if f.Type == OrderTypeSell { 42 | q = q.Where("coin_sell_id = ?", f.coinId).OrderExpr("price desc") 43 | } 44 | 45 | if len(f.Type) == 0 { 46 | return q.OrderExpr(`"id" desc`), nil 47 | } 48 | 49 | return q, nil 50 | } 51 | 52 | // ------------------------------ 53 | 54 | type PoolFilter struct { 55 | Pool models.LiquidityPool 56 | } 57 | 58 | func NewPoolFilter(p models.LiquidityPool) PoolFilter { 59 | return PoolFilter{p} 60 | } 61 | 62 | func (f PoolFilter) Filter(q *orm.Query) (*orm.Query, error) { 63 | return q.Where("liquidity_pool_id = ?", f.Pool.Id), nil 64 | } 65 | 66 | // ------------------------------ 67 | 68 | type StatusFilter struct { 69 | status Status 70 | } 71 | 72 | func NewStatusFilter(status string) StatusFilter { 73 | return StatusFilter{Status(status)} 74 | } 75 | 76 | func (f StatusFilter) Filter(q *orm.Query) (*orm.Query, error) { 77 | if len(f.status) == 0 { 78 | return q, nil 79 | } 80 | 81 | if f.status == OrderStatusActive { 82 | return q.Where(`"status" in (?, ?)`, models.OrderTypeActive, models.OrderTypeNew), nil 83 | } 84 | 85 | if f.status == OrderStatusCanceled { 86 | return q.Where(`"status" = ?`, models.OrderTypeCanceled), nil 87 | } 88 | 89 | if f.status == OrderStatusExpired { 90 | return q.Where(`"status" = ?`, models.OrderTypeExpired), nil 91 | } 92 | 93 | if f.status == OrderStatusFilled { 94 | return q.Where(`"status" = ?`, models.OrderTypeFilled), nil 95 | } 96 | 97 | if f.status == OrderStatusPartiallyFilled { 98 | return q.Where(`"status" = ?`, models.OrderTypePartiallyFilled), nil 99 | } 100 | 101 | return q, nil 102 | } 103 | -------------------------------------------------------------------------------- /coins/resource.go: -------------------------------------------------------------------------------- 1 | package coins 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 7 | "math/big" 8 | ) 9 | 10 | type Resource struct { 11 | ID uint `json:"id"` 12 | Crr uint `json:"crr"` 13 | Volume string `json:"volume"` 14 | Reserve string `json:"reserve_balance"` 15 | MaxSupply string `json:"max_supply"` 16 | Name string `json:"name"` 17 | Symbol string `json:"symbol"` 18 | OwnerAddress *string `json:"owner_address"` 19 | Burnable bool `json:"burnable"` 20 | Mintable bool `json:"mintable"` 21 | Type string `json:"type"` 22 | TradingVolume24h string `json:"trading_volume_24h"` 23 | TradingVolume1mo string `json:"trading_volume_1mo"` 24 | PriceUsd string `json:"price_usd"` 25 | } 26 | 27 | type Params struct { 28 | TradingVolume24h string 29 | TradingVolume1mo string 30 | PriceUsd *big.Float 31 | IsTypeRequired bool 32 | } 33 | 34 | func (Resource) Transform(model resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 35 | coin := model.(models.Coin) 36 | 37 | ownerAddress := new(string) 38 | if coin.OwnerAddressId != 0 { 39 | *ownerAddress = coin.OwnerAddress.GetAddress() 40 | } else { 41 | ownerAddress = nil 42 | } 43 | 44 | return Resource{ 45 | ID: coin.ID, 46 | Crr: coin.Crr, 47 | Volume: helpers.PipStr2Bip(coin.Volume), 48 | Reserve: helpers.PipStr2Bip(coin.Reserve), 49 | MaxSupply: helpers.PipStr2Bip(coin.MaxSupply), 50 | Name: coin.Name, 51 | Symbol: coin.GetSymbol(), 52 | OwnerAddress: ownerAddress, 53 | Burnable: coin.Burnable, 54 | Mintable: coin.Mintable, 55 | Type: helpers.GetCoinType(coin.Type), 56 | TradingVolume24h: helpers.PipStr2Bip(params[0].(Params).TradingVolume24h), 57 | TradingVolume1mo: helpers.PipStr2Bip(params[0].(Params).TradingVolume1mo), 58 | PriceUsd: params[0].(Params).PriceUsd.String(), 59 | } 60 | } 61 | 62 | type IdResource struct { 63 | ID uint32 `json:"id"` 64 | Type string `json:"type,omitempty"` 65 | Symbol string `json:"symbol"` 66 | } 67 | 68 | func (IdResource) Transform(model resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 69 | coin := model.(models.Coin) 70 | 71 | r := IdResource{ 72 | ID: uint32(coin.ID), 73 | Symbol: coin.GetSymbol(), 74 | } 75 | 76 | if len(params) > 0 { 77 | r.Type = helpers.GetCoinType(coin.Type) 78 | } 79 | 80 | return r 81 | } 82 | -------------------------------------------------------------------------------- /address/resource.go: -------------------------------------------------------------------------------- 1 | package address 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/balance" 5 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 6 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 7 | "github.com/MinterTeam/minter-explorer-api/v2/transaction/data_resources" 8 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 9 | "github.com/MinterTeam/node-grpc-gateway/api_pb" 10 | "math/big" 11 | ) 12 | 13 | type Resource struct { 14 | Address string `json:"address"` 15 | Balances []resource.Interface `json:"balances"` 16 | TotalBalanceSum *string `json:"total_balance_sum,omitempty"` 17 | TotalBalanceSumUSD *string `json:"total_balance_sum_usd,omitempty"` 18 | StakeBalanceSum *string `json:"stake_balance_sum,omitempty"` 19 | StakeBalanceSumUSD *string `json:"stake_balance_sum_usd,omitempty"` 20 | } 21 | 22 | type Params struct { 23 | TotalBalanceSum *big.Int 24 | TotalBalanceSumUSD *big.Float 25 | StakeBalanceSum *big.Int 26 | StakeBalanceSumUSD *big.Float 27 | } 28 | 29 | func (r Resource) Transform(model resource.ItemInterface, resourceParams ...resource.ParamInterface) resource.Interface { 30 | address := model.(models.Address) 31 | 32 | r.Address = address.GetAddress() 33 | r.Balances = resource.TransformCollection(address.Balances, balance.Resource{}) 34 | 35 | if len(resourceParams) > 0 { 36 | if params, ok := resourceParams[0].(Params); ok { 37 | r.transformParams(params) 38 | } 39 | } 40 | 41 | return r 42 | } 43 | 44 | // prepare total address balance 45 | func (r *Resource) transformParams(params Params) { 46 | sum := helpers.PipStr2Bip(params.TotalBalanceSum.String()) 47 | usd := helpers.PipStr2Bip(params.TotalBalanceSumUSD.String()) 48 | 49 | stakeSum := helpers.PipStr2Bip(params.StakeBalanceSum.String()) 50 | stakeSumUSD := helpers.PipStr2Bip(params.StakeBalanceSumUSD.String()) 51 | 52 | r.TotalBalanceSum, r.TotalBalanceSumUSD = &sum, &usd 53 | r.StakeBalanceSum, r.StakeBalanceSumUSD = &stakeSum, &stakeSumUSD 54 | } 55 | 56 | type LockedTokenResource struct { 57 | Coin data_resources.Coin `json:"coin"` 58 | DueBlock uint64 `json:"due_block"` 59 | Value string `json:"value"` 60 | StartBlock uint64 `json:"start_block"` 61 | } 62 | 63 | func (r LockedTokenResource) Transform(model resource.ItemInterface, resourceParams ...resource.ParamInterface) resource.Interface { 64 | m := model.(models.Transaction) 65 | data := m.IData.(*api_pb.LockData) 66 | 67 | return LockedTokenResource{ 68 | Coin: new(data_resources.Coin).Transform(data.Coin), 69 | DueBlock: data.DueBlock, 70 | Value: helpers.PipStr2Bip(data.Value), 71 | StartBlock: m.BlockID, 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /address/service.go: -------------------------------------------------------------------------------- 1 | package address 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/balance" 5 | "github.com/MinterTeam/minter-explorer-api/v2/core/config" 6 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 7 | ) 8 | 9 | type Service struct { 10 | rp *Repository 11 | stakeRepo StakeRepository 12 | balanceService *balance.Service 13 | } 14 | 15 | type StakeRepository interface { 16 | GetAllByAddress(address string) ([]models.Stake, error) 17 | } 18 | 19 | func NewService(rp *Repository, stakeRepo StakeRepository, balanceService *balance.Service) *Service { 20 | return &Service{ 21 | rp: rp, 22 | stakeRepo: stakeRepo, 23 | balanceService: balanceService, 24 | } 25 | } 26 | 27 | func (s *Service) GetBalance(minterAddress string, isTotal bool) (*Balance, error) { 28 | model := s.getModelByAddress(minterAddress) 29 | if isTotal { 30 | return s.GetTotalBalance(model) 31 | } 32 | 33 | return &Balance{ 34 | Model: model, 35 | }, nil 36 | } 37 | 38 | func (s *Service) GetTotalBalance(model *models.Address) (*Balance, error) { 39 | stakes, err := s.stakeRepo.GetAllByAddress(model.Address) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | // compute available balance from address balances 45 | totalBalanceSum := s.balanceService.GetTotalBalance(model) 46 | totalBalanceSumUSD := s.balanceService.GetTotalBalanceInUSD(totalBalanceSum) 47 | stakeBalanceSum := s.balanceService.GetStakeBalance(stakes) 48 | stakeBalanceSumUSD := s.balanceService.GetTotalBalanceInUSD(stakeBalanceSum) 49 | 50 | return &Balance{ 51 | Model: model, 52 | TotalBalanceSum: totalBalanceSum, 53 | TotalBalanceSumUSD: totalBalanceSumUSD, 54 | StakeBalanceSum: stakeBalanceSum, 55 | StakeBalanceSumUSD: stakeBalanceSumUSD, 56 | }, nil 57 | } 58 | 59 | func (s *Service) getModelByAddress(address string) *models.Address { 60 | model := s.rp.GetByAddress(address) 61 | 62 | // if model not found 63 | if model == nil || len(model.Balances) == 0 { 64 | return s.makeEmptyAddressModel(address, config.BaseCoinSymbol) 65 | } 66 | 67 | isBaseSymbolExists := false 68 | for _, b := range model.Balances { 69 | if b.CoinID == 0 { 70 | isBaseSymbolExists = true 71 | } 72 | } 73 | 74 | if !isBaseSymbolExists { 75 | model.Balances = append(model.Balances, &models.Balance{ 76 | Value: "0", 77 | Coin: &models.Coin{Symbol: config.BaseCoinSymbol, Type: models.CoinTypeBase}, 78 | }) 79 | } 80 | 81 | return model 82 | } 83 | 84 | func (s *Service) makeEmptyAddressModel(minterAddress string, baseCoin string) *models.Address { 85 | return &models.Address{ 86 | Address: minterAddress, 87 | Balances: []*models.Balance{{ 88 | Coin: &models.Coin{ 89 | Symbol: baseCoin, 90 | Type: models.CoinTypeBase, 91 | }, 92 | Value: "0", 93 | }}, 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tools/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/blocks" 5 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | type ExplorerCache struct { 11 | lastBlock blocks.Resource 12 | items *sync.Map 13 | } 14 | 15 | // cache constructor 16 | func NewCache(lastBlock models.Block) *ExplorerCache { 17 | cache := &ExplorerCache{ 18 | lastBlock: new(blocks.Resource).Transform(lastBlock).(blocks.Resource), 19 | items: new(sync.Map), 20 | } 21 | 22 | return cache 23 | } 24 | 25 | // create new cache item 26 | func (c *ExplorerCache) newCacheItem(value interface{}, ttl interface{}) *CacheItem { 27 | switch t := ttl.(type) { 28 | case time.Duration: 29 | ttl := time.Now().Add(t * time.Second) 30 | return &CacheItem{value: value, ttl: &ttl} 31 | case int: 32 | ttl := c.lastBlock.ID + uint64(t) 33 | return &CacheItem{value: value, btl: &ttl} 34 | } 35 | 36 | panic("Invalid cache ttl type.") 37 | } 38 | 39 | // get or store value from cache 40 | func (c *ExplorerCache) Get(key interface{}, callback func() interface{}, ttl interface{}) interface{} { 41 | v, ok := c.items.Load(key) 42 | if ok { 43 | item := v.(*CacheItem) 44 | if !item.IsExpired(c.lastBlock.ID) { 45 | return item.value 46 | } 47 | } 48 | 49 | return c.Store(key, callback(), ttl) 50 | } 51 | 52 | // get or store value from cache 53 | func (c *ExplorerCache) ExecuteOrGet(key interface{}, callback func() interface{}, ttl interface{}, executeRequired bool) interface{} { 54 | if executeRequired { 55 | return callback() 56 | } 57 | 58 | v, ok := c.items.Load(key) 59 | if ok { 60 | item := v.(*CacheItem) 61 | if !item.IsExpired(c.lastBlock.ID) { 62 | return item.value 63 | } 64 | } 65 | 66 | return c.Store(key, callback(), ttl) 67 | } 68 | 69 | // save value to cache 70 | func (c *ExplorerCache) Store(key interface{}, value interface{}, ttl interface{}) interface{} { 71 | c.items.Store(key, c.newCacheItem(value, ttl)) 72 | return value 73 | } 74 | 75 | // loop for checking items expiration 76 | func (c *ExplorerCache) ExpirationCheck() { 77 | c.items.Range(func(key, value interface{}) bool { 78 | item := value.(*CacheItem) 79 | if item.IsExpired(c.lastBlock.ID) { 80 | c.items.Delete(key) 81 | } 82 | 83 | return true 84 | }) 85 | } 86 | 87 | // set new last block id 88 | func (c *ExplorerCache) SetLastBlock(block blocks.Resource) { 89 | c.lastBlock = block 90 | // clean expired items 91 | go c.ExpirationCheck() 92 | } 93 | 94 | // Get latest explorer block 95 | func (c *ExplorerCache) GetLastBlock() blocks.Resource { 96 | return c.lastBlock 97 | } 98 | 99 | // update last block id by ws data 100 | func (c *ExplorerCache) OnNewBlock(block blocks.Resource) { 101 | c.SetLastBlock(block) 102 | } 103 | -------------------------------------------------------------------------------- /blocks/resource.go: -------------------------------------------------------------------------------- 1 | package blocks 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 6 | "github.com/MinterTeam/minter-explorer-api/v2/validator" 7 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 8 | "time" 9 | ) 10 | 11 | type Resource struct { 12 | ID uint64 `json:"height"` 13 | Size uint64 `json:"size"` 14 | NumTxs uint32 `json:"transaction_count"` 15 | BlockTime float64 `json:"block_time"` 16 | Timestamp string `json:"timestamp"` 17 | BlockReward string `json:"reward"` 18 | Hash string `json:"hash"` 19 | ValidatorsCount int `json:"validators_count"` 20 | } 21 | 22 | func (Resource) Transform(model resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 23 | block := model.(models.Block) 24 | 25 | return Resource{ 26 | ID: block.ID, 27 | Size: block.Size, 28 | NumTxs: block.NumTxs, 29 | BlockTime: helpers.Nano2Seconds(block.BlockTime), 30 | Timestamp: block.CreatedAt.Format(time.RFC3339), 31 | BlockReward: helpers.PipStr2Bip(block.BlockReward), 32 | Hash: block.GetHash(), 33 | ValidatorsCount: len(block.BlockValidators), 34 | } 35 | } 36 | 37 | type ResourceDetailed struct { 38 | ID uint64 `json:"height"` 39 | Size uint64 `json:"size"` 40 | NumTxs uint32 `json:"transaction_count"` 41 | BlockTime float64 `json:"block_time"` 42 | Timestamp string `json:"timestamp"` 43 | BlockReward string `json:"reward"` 44 | Hash string `json:"hash"` 45 | Validators []resource.Interface `json:"validators"` 46 | } 47 | 48 | // Transform lastBlockId - uint64 pointer to the last block height, optional field. 49 | func (ResourceDetailed) Transform(model resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 50 | block := model.(models.Block) 51 | 52 | return ResourceDetailed{ 53 | ID: block.ID, 54 | Size: block.Size, 55 | NumTxs: block.NumTxs, 56 | BlockTime: helpers.Nano2Seconds(block.BlockTime), 57 | Timestamp: block.CreatedAt.Format(time.RFC3339), 58 | BlockReward: helpers.PipStr2Bip(block.BlockReward), 59 | Hash: block.GetHash(), 60 | Validators: resource.TransformCollection(block.BlockValidators, ValidatorResource{}), 61 | } 62 | } 63 | 64 | type ValidatorResource struct { 65 | Validator resource.Interface `json:"validator"` 66 | Signed bool `json:"signed"` 67 | } 68 | 69 | func (ValidatorResource) Transform(model resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 70 | blockValidator := model.(models.BlockValidator) 71 | 72 | return ValidatorResource{ 73 | Signed: blockValidator.Signed, 74 | Validator: new(validator.Resource).Transform(blockValidator.Validator), 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /helpers/minter.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/core/config" 5 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 6 | validatorTypes "github.com/MinterTeam/minter-explorer-extender/v2/validator" 7 | "math" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | func RemoveMinterPrefix(raw string) string { 13 | if len(raw) < 2 { 14 | return raw 15 | } 16 | 17 | return raw[2:] 18 | } 19 | 20 | const firstReward = 333 21 | const blocksPerReward = 200000 22 | const premineValue = 200 * 1000000 // in the mainnet the real value is 200 * 1000000 23 | const startHeight = 5000001 + 4150000 24 | 25 | func CalculateEmission(blockId uint64) uint64 { 26 | blockId += startHeight 27 | sum := uint64(premineValue) 28 | reward := firstReward 29 | high := int(math.Ceil(float64(blockId) / float64(blocksPerReward))) 30 | 31 | var i int 32 | for i = 1; i < high; i++ { 33 | sum += uint64(blocksPerReward * reward) 34 | reward -= 1 35 | } 36 | 37 | sum += (blockId % uint64(blocksPerReward)) * uint64(reward) 38 | 39 | return sum 40 | } 41 | 42 | func GetSymbolAndVersionFromStr(symbol string) (string, *uint64) { 43 | items := strings.Split(symbol, "-") 44 | baseSymbol := items[0] 45 | 46 | if baseSymbol == "LP" { 47 | return symbol, nil 48 | } 49 | 50 | if len(items) == 2 { 51 | version, _ := strconv.ParseUint(items[1], 10, 64) 52 | return baseSymbol, &version 53 | } 54 | 55 | return baseSymbol, nil 56 | } 57 | 58 | func GetSymbolAndDefaultVersionFromStr(symbol string) (string, uint64) { 59 | symbol, version := GetSymbolAndVersionFromStr(symbol) 60 | if version == nil { 61 | return symbol, 0 62 | } 63 | 64 | return symbol, *version 65 | } 66 | 67 | func GetCoinType(coinType models.CoinType) string { 68 | switch coinType { 69 | case models.CoinTypeBase: 70 | return "coin" 71 | case models.CoinTypeToken: 72 | return "token" 73 | case models.CoinTypePoolToken: 74 | return "pool_token" 75 | } 76 | 77 | return "" 78 | } 79 | 80 | func GetPoolIdFromToken(token string) uint { 81 | id, _ := strconv.ParseUint(token[2:], 10, 64) 82 | return uint(id) 83 | } 84 | 85 | func GetTokenContractAndChain(tokenContract *models.TokenContract) (contract string, chain string) { 86 | // for BIP and HUB 87 | if tokenContract.CoinId == 0 || tokenContract.CoinId == 1902 { 88 | return tokenContract.Eth, "minter" 89 | } 90 | 91 | contract = tokenContract.Bsc 92 | if len(tokenContract.Eth) != 0 { 93 | return tokenContract.Eth, "ethereum" 94 | } 95 | 96 | return contract, "bsc" 97 | } 98 | 99 | func GetUnbondStartHeight(unbondBlockId uint, lockType string) uint { 100 | if config.BaseCoinSymbol == "BIP" { 101 | if lockType == "unbond" { 102 | return unbondBlockId - validatorTypes.UnbondBlockCount 103 | } 104 | return unbondBlockId - validatorTypes.MoveStakeBlockCount 105 | } 106 | 107 | if lockType == "unbond" { 108 | return unbondBlockId - validatorTypes.UnbondBlockCountTestnet 109 | } 110 | return unbondBlockId - validatorTypes.MoveStakeBlockCountTestnet 111 | } 112 | -------------------------------------------------------------------------------- /resource/pagination.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/errors" 5 | "github.com/MinterTeam/minter-explorer-api/v2/tools" 6 | "sync" 7 | ) 8 | 9 | type PaginationResource struct { 10 | Data []Interface `json:"data"` 11 | Links PaginationLinksResource `json:"links"` 12 | Meta PaginationMetaResource `json:"meta"` 13 | } 14 | 15 | type PaginationLinksResource struct { 16 | First *string `json:"first"` 17 | Last *string `json:"last"` 18 | Prev *string `json:"prev"` 19 | Next *string `json:"next"` 20 | } 21 | 22 | type PaginationMetaResource struct { 23 | CurrentPage int `json:"current_page"` 24 | LastPage int `json:"last_page"` 25 | Path string `json:"path"` 26 | PerPage int `json:"per_page"` 27 | Total int `json:"total"` 28 | Additional map[string]interface{} `json:"additional,omitempty"` 29 | } 30 | 31 | func TransformPaginatedCollection(collection interface{}, resource Interface, pagination tools.Pagination) PaginationResource { 32 | return transformPaginatedCollection(collection, resource, pagination, nil) 33 | } 34 | 35 | func TransformPaginatedCollectionWithCallback(collection interface{}, resource Interface, pagination tools.Pagination, callbackFunc func(model ParamInterface) ParamsInterface) PaginationResource { 36 | models := makeItemsFromModelsCollection(collection) 37 | result := make([]Interface, len(models)) 38 | 39 | wg := new(sync.WaitGroup) 40 | for i := range models { 41 | wg.Add(1) 42 | go func(i int, wg *sync.WaitGroup) { 43 | defer wg.Done() 44 | defer errors.Recovery() 45 | result[i] = resource.Transform(models[i], callbackFunc(models[i])...) 46 | }(i, wg) 47 | } 48 | wg.Wait() 49 | 50 | return PaginationResource{ 51 | Data: result, 52 | Links: PaginationLinksResource{ 53 | First: pagination.GetFirstPageLink(), 54 | Last: pagination.GetLastPageLink(), 55 | Prev: pagination.GetPrevPageLink(), 56 | Next: pagination.GetNextPageLink(), 57 | }, 58 | Meta: PaginationMetaResource{ 59 | CurrentPage: pagination.GetCurrentPage(), 60 | LastPage: pagination.GetLastPage(), 61 | Path: pagination.GetPath(), 62 | PerPage: pagination.GetPerPage(), 63 | Total: pagination.Total, 64 | }, 65 | } 66 | } 67 | 68 | func transformPaginatedCollection(collection interface{}, resource Interface, pagination tools.Pagination, additional map[string]interface{}) PaginationResource { 69 | result := TransformCollection(collection, resource) 70 | 71 | return PaginationResource{ 72 | Data: result, 73 | Links: PaginationLinksResource{ 74 | First: pagination.GetFirstPageLink(), 75 | Last: pagination.GetLastPageLink(), 76 | Prev: pagination.GetPrevPageLink(), 77 | Next: pagination.GetNextPageLink(), 78 | }, 79 | Meta: PaginationMetaResource{ 80 | CurrentPage: pagination.GetCurrentPage(), 81 | LastPage: pagination.GetLastPage(), 82 | Path: pagination.GetPath(), 83 | PerPage: pagination.GetPerPage(), 84 | Total: pagination.Total, 85 | Additional: additional, 86 | }, 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tools/pagination.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MinterTeam/minter-explorer-api/v2/core/config" 6 | "github.com/go-pg/pg/v10/orm" 7 | "github.com/go-pg/urlstruct" 8 | "math" 9 | "net/http" 10 | "strconv" 11 | ) 12 | 13 | type Pagination struct { 14 | Pager *urlstruct.Pager 15 | Request *http.Request 16 | RequestURL string 17 | Total int 18 | } 19 | 20 | func NewPagination(request *http.Request) Pagination { 21 | query := request.URL.Query() 22 | if query.Get("limit") == "" { 23 | query.Set("limit", strconv.FormatInt(config.DefaultPaginationLimit, 10)) 24 | } 25 | 26 | limit, _ := strconv.ParseUint(query.Get("limit"), 10, 64) 27 | if limit > config.MaxPaginationLimit { 28 | query.Set("limit", strconv.FormatInt(config.MaxPaginationLimit, 10)) 29 | } 30 | 31 | pager := urlstruct.NewPager(query) 32 | pager.MaxOffset = config.MaxPaginationOffset 33 | pager.MaxLimit = config.MaxPaginationLimit 34 | 35 | return Pagination{ 36 | Pager: pager, 37 | Request: request, 38 | RequestURL: fmt.Sprintf("https://%s%s", request.Host, request.URL.Path), // TODO: fix request url 39 | } 40 | } 41 | 42 | func (pagination Pagination) Filter(query *orm.Query) (*orm.Query, error) { 43 | return query.Limit(pagination.Pager.GetLimit()).Offset(pagination.Pager.GetOffset()), nil 44 | } 45 | 46 | func (pagination Pagination) GetNextPageLink() *string { 47 | if pagination.GetLastPage() == 0 || pagination.GetLastPage() == pagination.GetCurrentPage() { 48 | return nil 49 | } 50 | 51 | nextPage := strconv.Itoa(pagination.GetCurrentPage() + 1) 52 | query := pagination.Request.URL.Query() 53 | query.Set("page", nextPage) 54 | 55 | link := fmt.Sprintf("%s?%s", pagination.RequestURL, query.Encode()) 56 | return &link 57 | } 58 | 59 | func (pagination Pagination) GetLastPageLink() *string { 60 | lastPage := "1" 61 | if pagination.GetLastPage() != 0 { 62 | lastPage = strconv.Itoa(pagination.GetLastPage()) 63 | } 64 | 65 | query := pagination.Request.URL.Query() 66 | query.Set("page", lastPage) 67 | 68 | link := fmt.Sprintf("%s?%s", pagination.RequestURL, query.Encode()) 69 | return &link 70 | } 71 | 72 | func (pagination Pagination) GetPrevPageLink() *string { 73 | if pagination.GetCurrentPage() == 1 { 74 | return nil 75 | } 76 | 77 | prevPage := strconv.Itoa(pagination.GetCurrentPage() - 1) 78 | query := pagination.Request.URL.Query() 79 | query.Set("page", prevPage) 80 | 81 | link := fmt.Sprintf("%s?%s", pagination.RequestURL, query.Encode()) 82 | return &link 83 | } 84 | 85 | func (pagination Pagination) GetFirstPageLink() *string { 86 | query := pagination.Request.URL.Query() 87 | query.Set("page", "1") 88 | 89 | link := fmt.Sprintf("%s?%s", pagination.RequestURL, query.Encode()) 90 | return &link 91 | } 92 | 93 | func (pagination Pagination) GetPath() string { 94 | return pagination.RequestURL 95 | } 96 | 97 | func (pagination Pagination) GetLastPage() int { 98 | return int(math.Ceil(float64(pagination.Total) / float64(pagination.Pager.Limit))) 99 | } 100 | 101 | func (pagination Pagination) GetCurrentPage() int { 102 | return pagination.Pager.GetPage() 103 | } 104 | 105 | func (pagination Pagination) GetPerPage() int { 106 | return pagination.Pager.GetLimit() 107 | } 108 | -------------------------------------------------------------------------------- /unbond/resource.go: -------------------------------------------------------------------------------- 1 | package unbond 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/coins" 5 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 6 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 7 | "github.com/MinterTeam/minter-explorer-api/v2/validator" 8 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 9 | "time" 10 | ) 11 | 12 | type Resource struct { 13 | Coin resource.Interface `json:"coin"` 14 | Address string `json:"address"` 15 | Value string `json:"value"` 16 | Validator resource.Interface `json:"validator"` 17 | ToValidator resource.Interface `json:"to_validator,omitempty"` 18 | BlockID uint `json:"end_height"` 19 | StartBlockID uint `json:"start_height"` 20 | CreatedAt string `json:"created_at"` 21 | Type string `json:"type"` 22 | } 23 | 24 | const ( 25 | typeUnbond = "unbond" 26 | typeMoveStake = "move_stake" 27 | ) 28 | 29 | func (Resource) Transform(model resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 30 | unbond := model.(UnbondMoveStake) 31 | 32 | if unbond.ToValidator == nil { 33 | return Resource{ 34 | Coin: new(coins.IdResource).Transform(*unbond.Coin), 35 | Address: unbond.Address.GetAddress(), 36 | Value: helpers.PipStr2Bip(unbond.Value), 37 | Validator: new(validator.Resource).Transform(*unbond.FromValidator), 38 | BlockID: unbond.BlockId, 39 | StartBlockID: helpers.GetUnbondStartHeight(unbond.BlockId, typeUnbond), 40 | CreatedAt: unbond.CreatedAt.Format(time.RFC3339), 41 | Type: typeUnbond, 42 | } 43 | } 44 | 45 | return Resource{ 46 | Coin: new(coins.IdResource).Transform(*unbond.Coin), 47 | Address: unbond.Address.GetAddress(), 48 | Value: helpers.PipStr2Bip(unbond.Value), 49 | Validator: new(validator.Resource).Transform(*unbond.FromValidator), 50 | ToValidator: new(validator.Resource).Transform(*unbond.ToValidator), 51 | BlockID: unbond.BlockId, 52 | StartBlockID: helpers.GetUnbondStartHeight(unbond.BlockId, typeMoveStake), 53 | CreatedAt: unbond.CreatedAt.Format(time.RFC3339), 54 | Type: typeMoveStake, 55 | } 56 | } 57 | 58 | type EventResource struct { 59 | Coin resource.Interface `json:"coin"` 60 | Address string `json:"address"` 61 | Value string `json:"value"` 62 | Validator resource.Interface `json:"validator"` 63 | ToValidator resource.Interface `json:"to_validator,omitempty"` 64 | BlockID uint `json:"end_height"` 65 | StartBlockID uint `json:"start_height"` 66 | CreatedAt string `json:"created_at"` 67 | Type string `json:"type"` 68 | } 69 | 70 | func (EventResource) Transform(model resource.ItemInterface, params ...resource.ParamInterface) resource.Interface { 71 | unbond := model.(models.Unbond) 72 | 73 | return EventResource{ 74 | Coin: new(coins.IdResource).Transform(*unbond.Coin), 75 | Address: unbond.Address.GetAddress(), 76 | Value: helpers.PipStr2Bip(unbond.Value), 77 | Validator: new(validator.Resource).Transform(*unbond.Validator), 78 | BlockID: unbond.BlockId, 79 | StartBlockID: helpers.GetUnbondStartHeight(unbond.BlockId, typeUnbond), 80 | //CreatedAt: unbond.CreatedAt.Format(time.RFC3339), 81 | Type: typeUnbond, 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /unbond/repository.go: -------------------------------------------------------------------------------- 1 | package unbond 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/events" 5 | "github.com/MinterTeam/minter-explorer-api/v2/tools" 6 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 7 | "github.com/go-pg/pg/v10" 8 | ) 9 | 10 | type Repository struct { 11 | db *pg.DB 12 | } 13 | 14 | func NewRepository(db *pg.DB) *Repository { 15 | return &Repository{db} 16 | } 17 | 18 | func (r *Repository) GetListByAddress(filter *events.SelectFilter, pagination *tools.Pagination) ([]UnbondMoveStake, error) { 19 | var unbonds []UnbondMoveStake 20 | var err error 21 | 22 | allCoins := r.db.Model(new(models.Unbond)). 23 | ColumnExpr("block_id, coin_id, validator_id, null as to_validator_id, value, created_at, address.address as minter_address"). 24 | Join("JOIN addresses as address ON address.id = unbond.address_id"). 25 | Apply(filter.Filter). 26 | UnionAll(r.db.Model(new(models.MovedStake)). 27 | ColumnExpr("block_id, coin_id, from_validator_id as validator_id, to_validator_id, value, created_at, address.address as minter_address"). 28 | Join("JOIN addresses as address ON address.id = moved_stake.address_id"). 29 | Apply(filter.Filter)) 30 | 31 | pagination.Total, err = r.db.Model(). 32 | With("data", allCoins). 33 | Table("data"). 34 | Join("JOIN coins on coins.id = data.coin_id"). 35 | Join("JOIN validators on validators.id = data.validator_id"). 36 | Join("LEFT JOIN validators as toValidators on toValidators.id = data.to_validator_id"). 37 | ColumnExpr("data.block_id, data.coin_id, data.validator_id, data.to_validator_id, data.value, data.created_at"). 38 | ColumnExpr("coins.id as coin__id"). 39 | ColumnExpr("coins.symbol as coin__symbol"). 40 | ColumnExpr("validators.public_key as from_validator__public_key"). 41 | ColumnExpr("validators.name as from_validator__name"). 42 | ColumnExpr("validators.description as from_validator__description"). 43 | ColumnExpr("validators.icon_url as from_validator__icon_url"). 44 | ColumnExpr("validators.site_url as from_validator__site_url"). 45 | ColumnExpr("validators.status as from_validator__status"). 46 | ColumnExpr("validators.commission as from_validator__commission"). 47 | ColumnExpr("toValidators.public_key as to_validator__public_key"). 48 | ColumnExpr("toValidators.name as to_validator__name"). 49 | ColumnExpr("toValidators.description as to_validator__description"). 50 | ColumnExpr("toValidators.icon_url as to_validator__icon_url"). 51 | ColumnExpr("toValidators.site_url as to_validator__site_url"). 52 | ColumnExpr("toValidators.status as to_validator__status"). 53 | ColumnExpr("toValidators.commission as to_validator__commission"). 54 | ColumnExpr("data.minter_address as address__address"). 55 | OrderExpr("data.block_id desc"). 56 | Apply(pagination.Filter). 57 | SelectAndCount(&unbonds) 58 | 59 | return unbonds, err 60 | } 61 | 62 | func (r *Repository) GetListAsEventsByAddress(filter *events.SelectFilter, lastBlockId uint64, pagination *tools.Pagination) (unbonds []models.Unbond, err error) { 63 | pagination.Total, err = r.db.Model(&unbonds). 64 | Relation("Coin"). 65 | Relation("Validator"). 66 | ColumnExpr("unbond.block_id, unbond.value, address.address as address__address"). 67 | Join("JOIN addresses as address ON address.id = unbond.address_id"). 68 | Apply(filter.Filter). 69 | Apply(pagination.Filter). 70 | Where("unbond.block_id <= ?", lastBlockId). 71 | Order("unbond.block_id desc"). 72 | SelectAndCount() 73 | 74 | return unbonds, err 75 | } 76 | -------------------------------------------------------------------------------- /api/v2/pools/proxy.go: -------------------------------------------------------------------------------- 1 | package pools 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MinterTeam/minter-explorer-api/v2/coins" 6 | "github.com/MinterTeam/minter-explorer-api/v2/core" 7 | "github.com/MinterTeam/minter-explorer-api/v2/core/config" 8 | "github.com/MinterTeam/minter-explorer-api/v2/errors" 9 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 10 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 11 | "github.com/gin-gonic/gin" 12 | "github.com/go-resty/resty/v2" 13 | "net/http" 14 | "strconv" 15 | ) 16 | 17 | type swapRouterResponse struct { 18 | Path []string `json:"path"` 19 | Result string `json:"result"` 20 | } 21 | 22 | type swapRouterErrorResponse struct { 23 | Error struct { 24 | Message string `json:"message"` 25 | } `json:"error"` 26 | } 27 | 28 | type routeResource struct { 29 | SwapType string `json:"swap_type"` 30 | AmountIn string `json:"amount_in"` 31 | AmountOut string `json:"amount_out"` 32 | Coins []resource.Interface `json:"coins"` 33 | } 34 | 35 | func ProxySwapPoolRoute(c *gin.Context) { 36 | explorer := c.MustGet("explorer").(*core.Explorer) 37 | 38 | // validate request 39 | var req FindSwapPoolRouteRequest 40 | if err := c.ShouldBindUri(&req); err != nil { 41 | errors.SetValidationErrorResponse(err, c) 42 | return 43 | } 44 | 45 | if req.Coin0 == req.Coin1 { 46 | errors.SetErrorResponse(http.StatusNotFound, "Route path not exists.", c) 47 | return 48 | } 49 | 50 | var reqQuery FindSwapPoolRouteRequestQuery 51 | if err := c.ShouldBindQuery(&reqQuery); err != nil { 52 | errors.SetValidationErrorResponse(err, c) 53 | return 54 | } 55 | 56 | coinFrom, coinTo, err := req.GetCoins(explorer) 57 | if err != nil { 58 | errors.SetErrorResponse(http.StatusNotFound, "Coins not found.", c) 59 | return 60 | } 61 | 62 | coin0 := strconv.FormatUint(uint64(coinFrom.ID), 10) 63 | coin1 := strconv.FormatUint(uint64(coinTo.ID), 10) 64 | 65 | resp, err := proxySwapPoolRouteRequest(coin0, coin1, reqQuery.Amount, reqQuery.TradeType) 66 | if err != nil { 67 | errors.SetErrorResponse(http.StatusNotFound, "Route path not exists.", c) 68 | return 69 | } 70 | 71 | if resp.IsError() { 72 | c.JSON(resp.StatusCode(), resp.Error()) 73 | return 74 | } 75 | 76 | data := resp.Result().(*swapRouterResponse) 77 | path := make([]resource.Interface, len(data.Path)) 78 | for i, cidStr := range data.Path { 79 | cid, _ := strconv.ParseUint(cidStr, 10, 64) 80 | coin, _ := explorer.CoinRepository.FindByID(uint(cid)) 81 | path[i] = new(coins.IdResource).Transform(coin) 82 | } 83 | 84 | outputAmount := helpers.PipStr2Bip(data.Result) 85 | inputAmount := helpers.PipStr2Bip(reqQuery.Amount) 86 | 87 | if reqQuery.TradeType == "output" { 88 | inputAmount = helpers.PipStr2Bip(data.Result) 89 | outputAmount = helpers.PipStr2Bip(reqQuery.Amount) 90 | } 91 | 92 | c.JSON(resp.StatusCode(), routeResource{ 93 | SwapType: "pool", 94 | AmountIn: inputAmount, 95 | AmountOut: outputAmount, 96 | Coins: path, 97 | }) 98 | } 99 | 100 | func proxySwapPoolRouteRequest(coin0, coin1, amount, tradeType string) (*resty.Response, error) { 101 | // todo: move host url to config 102 | return resty.New().R(). 103 | SetError(&swapRouterErrorResponse{}). 104 | SetResult(&swapRouterResponse{}). 105 | Get(fmt.Sprintf("%s/v2/best_trade/%s/%s/%s/%s?max_depth=4", config.SwapRouterProxyUrl, coin0, coin1, tradeType, amount)) 106 | } 107 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/api/v2" 5 | "github.com/MinterTeam/minter-explorer-api/v2/api/validators" 6 | "github.com/MinterTeam/minter-explorer-api/v2/core" 7 | "github.com/MinterTeam/minter-explorer-api/v2/errors" 8 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 9 | "github.com/gin-gonic/gin" 10 | "github.com/gin-gonic/gin/binding" 11 | "github.com/go-pg/pg/v10" 12 | "github.com/go-playground/validator/v10" 13 | "github.com/zsais/go-gin-prometheus" 14 | "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" 15 | "golang.org/x/time/rate" 16 | "net/http" 17 | "sync" 18 | ) 19 | 20 | // Run API 21 | func Run(db *pg.DB, explorer *core.Explorer) { 22 | router := SetupRouter(db, explorer) 23 | appAddress := ":" + explorer.Environment.ServerPort 24 | err := router.Run(appAddress) 25 | helpers.CheckErr(err) 26 | } 27 | 28 | // Setup router 29 | func SetupRouter(db *pg.DB, explorer *core.Explorer) *gin.Engine { 30 | // Set release mode 31 | if !explorer.Environment.IsDebug { 32 | gin.SetMode(gin.ReleaseMode) 33 | } 34 | 35 | router := gin.New() 36 | router.Use(gin.Recovery()) 37 | //router.Use(gin.Logger()) 38 | 39 | // metrics 40 | p := ginprometheus.NewPrometheus("gin") 41 | p.ReqCntURLLabelMappingFn = func(c *gin.Context) string { return "" } // do not save stats for all routes 42 | p.Use(router) 43 | 44 | // router.Use(cors.Default()) // CORS 45 | router.Use(gin.ErrorLogger()) // print all errors 46 | router.Use(apiMiddleware(db, explorer)) // init global context 47 | router.Use(otelgin.Middleware("ExplorerApi")) // metrics 48 | //router.Use(throttle(&sync.Map{})) // rate limiter 49 | 50 | // Default handler 404 51 | router.NoRoute(func(c *gin.Context) { 52 | errors.SetErrorResponse(http.StatusNotFound, "Resource not found.", c) 53 | }) 54 | 55 | // Create base api prefix 56 | api := router.Group("/api") 57 | { 58 | // apply routes of version 2.0 59 | apiV2.ApplyRoutes(api) 60 | } 61 | 62 | // Register validator for api requests 63 | registerApiValidators() 64 | 65 | return router 66 | } 67 | 68 | // Add necessary services to global context 69 | func apiMiddleware(db *pg.DB, explorer *core.Explorer) gin.HandlerFunc { 70 | return func(c *gin.Context) { 71 | c.Set("db", db) 72 | c.Set("explorer", explorer) 73 | c.Next() 74 | } 75 | } 76 | 77 | // Register request validators 78 | func registerApiValidators() { 79 | 80 | if v, ok := binding.Validator.Engine().(*validator.Validate); ok { 81 | err := v.RegisterValidation("minterAddress", validators.MinterAddress) 82 | helpers.CheckErr(err) 83 | 84 | err = v.RegisterValidation("minterTxHash", validators.MinterTxHash) 85 | helpers.CheckErr(err) 86 | 87 | err = v.RegisterValidation("minterPubKey", validators.MinterPublicKey) 88 | helpers.CheckErr(err) 89 | 90 | err = v.RegisterValidation("timestamp", validators.Timestamp) 91 | helpers.CheckErr(err) 92 | } 93 | } 94 | 95 | func throttle(ipMap *sync.Map) gin.HandlerFunc { 96 | return func(c *gin.Context) { 97 | limiter, ok := ipMap.Load(c.ClientIP()) 98 | if !ok { 99 | limiter = rate.NewLimiter(10, 10) 100 | ipMap.Store(c.ClientIP(), limiter) 101 | } 102 | 103 | if !limiter.(*rate.Limiter).Allow() { 104 | errors.SetErrorResponse(http.StatusTooManyRequests, "Too many requests", c) 105 | c.Abort() 106 | } else { 107 | c.Next() 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /stake/repository.go: -------------------------------------------------------------------------------- 1 | package stake 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/tools" 6 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 7 | "github.com/go-pg/pg/v10" 8 | "math/big" 9 | ) 10 | 11 | type Repository struct { 12 | db *pg.DB 13 | } 14 | 15 | func NewRepository(db *pg.DB) *Repository { 16 | return &Repository{ 17 | db: db, 18 | } 19 | } 20 | 21 | // Get list of stakes by Minter address 22 | func (r Repository) GetAllByAddress(address string) ([]models.Stake, error) { 23 | var stakes []models.Stake 24 | 25 | err := r.db.Model(&stakes). 26 | Relation("Coin"). 27 | Relation("Validator"). 28 | Relation("OwnerAddress._"). 29 | Where("owner_address.address = ?", address). 30 | Order("bip_value DESC"). 31 | Select() 32 | 33 | return stakes, err 34 | } 35 | 36 | // Get total delegated bip value 37 | func (r Repository) GetSumInBipValue() (string, error) { 38 | var sum string 39 | 40 | err := r.db.Model(&models.Stake{}). 41 | Where("is_kicked = false"). 42 | ColumnExpr("SUM(bip_value)"). 43 | Select(&sum) 44 | 45 | return sum, err 46 | } 47 | 48 | // Get total delegated sum by address 49 | func (r Repository) GetSumInBipValueByAddress(address string) (string, error) { 50 | var sum string 51 | err := r.db.Model(&models.Stake{}). 52 | Relation("OwnerAddress._"). 53 | ColumnExpr("SUM(bip_value)"). 54 | Where("is_kicked = false"). 55 | Where("owner_address.address = ?", address). 56 | Select(&sum) 57 | 58 | return sum, err 59 | } 60 | 61 | // Get paginated list of stakes by validator 62 | func (r Repository) GetPaginatedByValidator( 63 | validator models.Validator, 64 | pagination *tools.Pagination, 65 | ) ([]models.Stake, error) { 66 | var stakes []models.Stake 67 | var err error 68 | 69 | pagination.Total, err = r.db.Model(&stakes). 70 | Relation("Coin"). 71 | Relation("OwnerAddress.address"). 72 | Where("validator_id = ?", validator.ID). 73 | Order("bip_value DESC"). 74 | Apply(pagination.Filter). 75 | SelectAndCount() 76 | 77 | return stakes, err 78 | } 79 | 80 | func (r Repository) GetMinStakes() ([]models.Stake, error) { 81 | var stakes []models.Stake 82 | 83 | err := r.db.Model(&stakes). 84 | ColumnExpr("min(bip_value) as bip_value"). 85 | Column("validator_id"). 86 | Where("bip_value != 0"). 87 | Where("is_kicked = false"). 88 | Group("validator_id"). 89 | Select() 90 | 91 | return stakes, err 92 | } 93 | 94 | func (r Repository) GetSumValueByCoin(coinID uint) (string, error) { 95 | var sum string 96 | 97 | err := r.db.Model(new(models.Stake)). 98 | ColumnExpr("SUM(value)"). 99 | Where("coin_id = ?", coinID). 100 | Select(&sum) 101 | 102 | return sum, err 103 | } 104 | 105 | func (r Repository) GetDelegatorsCount() (count uint64, err error) { 106 | err = r.db.Model(new(models.Stake)). 107 | ColumnExpr("count (DISTINCT owner_address_id)"). 108 | Select(&count) 109 | 110 | return count, err 111 | } 112 | 113 | func (r Repository) GetAddressValidatorIds(address string) (ids []uint64, err error) { 114 | err = r.db.Model(new(models.Stake)). 115 | Relation("OwnerAddress._"). 116 | ColumnExpr("DISTINCT validator_id"). 117 | Where("owner_address.address = ?", address). 118 | Select(&ids) 119 | 120 | return ids, err 121 | } 122 | 123 | func (r Repository) GetTotalStakeLocked() (*big.Int, error) { 124 | var tsl string 125 | 126 | err := r.db.Model(new(models.Stake)). 127 | ColumnExpr("sum(bip_value)"). 128 | Where(`exists (select 1 from transactions where type = 37 and from_address_id = "stake"."owner_address_id")`). 129 | Select(&tsl) 130 | 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | return helpers.StringToBigInt(tsl), err 136 | } 137 | -------------------------------------------------------------------------------- /api/v2/blocks/handler.go: -------------------------------------------------------------------------------- 1 | package blocks 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/MinterTeam/minter-explorer-api/v2/blocks" 8 | "github.com/MinterTeam/minter-explorer-api/v2/core" 9 | "github.com/MinterTeam/minter-explorer-api/v2/core/config" 10 | "github.com/MinterTeam/minter-explorer-api/v2/errors" 11 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 12 | "github.com/MinterTeam/minter-explorer-api/v2/resource" 13 | "github.com/MinterTeam/minter-explorer-api/v2/tools" 14 | "github.com/MinterTeam/minter-explorer-api/v2/transaction" 15 | "github.com/gin-gonic/gin" 16 | ) 17 | 18 | // TODO: replace string to int 19 | type GetBlockRequest struct { 20 | ID string `uri:"height" binding:"numeric"` 21 | } 22 | 23 | // TODO: replace string to int 24 | type GetBlocksRequest struct { 25 | Page string `form:"page" binding:"omitempty,numeric"` 26 | } 27 | 28 | // Blocks cache helpers 29 | const CacheBlocksCount = 1 30 | 31 | type CacheBlocksData struct { 32 | Blocks resource.PaginationResource 33 | } 34 | 35 | // Get list of blocks 36 | func GetBlocks(c *gin.Context) { 37 | explorer := c.MustGet("explorer").(*core.Explorer) 38 | 39 | // fetch blocks 40 | pagination := tools.NewPagination(c.Request) 41 | getBlocks := func() resource.PaginationResource { 42 | blockModels := explorer.BlockRepository.GetPaginated(&pagination) 43 | return resource.TransformPaginatedCollection(blockModels, blocks.Resource{}, pagination) 44 | } 45 | 46 | // cache last blocks 47 | var bresource resource.PaginationResource 48 | if pagination.GetCurrentPage() == 1 && pagination.GetPerPage() == config.DefaultPaginationLimit { 49 | cached := explorer.Cache.Get("blocks", func() interface{} { 50 | return CacheBlocksData{getBlocks()} 51 | }, CacheBlocksCount).(CacheBlocksData) 52 | 53 | bresource = cached.Blocks 54 | } else { 55 | bresource = getBlocks() 56 | } 57 | 58 | c.JSON(http.StatusOK, bresource) 59 | } 60 | 61 | // Get block detail 62 | func GetBlock(c *gin.Context) { 63 | explorer := c.MustGet("explorer").(*core.Explorer) 64 | 65 | // validate request 66 | var request GetBlockRequest 67 | err := c.ShouldBindUri(&request) 68 | if err != nil { 69 | errors.SetValidationErrorResponse(err, c) 70 | return 71 | } 72 | 73 | // parse to uint64 74 | blockId, err := strconv.ParseUint(request.ID, 10, 64) 75 | helpers.CheckErr(err) 76 | 77 | // fetch block by height 78 | block := explorer.BlockRepository.GetById(blockId) 79 | 80 | // check block to existing 81 | if block == nil { 82 | errors.SetErrorResponse(http.StatusNotFound, "Block not found.", c) 83 | return 84 | } 85 | 86 | c.JSON(http.StatusOK, gin.H{ 87 | "data": new(blocks.ResourceDetailed).Transform(*block), 88 | }) 89 | } 90 | 91 | // Get list of transactions by block height 92 | func GetBlockTransactions(c *gin.Context) { 93 | explorer := c.MustGet("explorer").(*core.Explorer) 94 | 95 | // validate request 96 | var request GetBlockRequest 97 | err := c.ShouldBindUri(&request) 98 | if err != nil { 99 | errors.SetValidationErrorResponse(err, c) 100 | return 101 | } 102 | 103 | // validate request query 104 | var requestQuery GetBlocksRequest 105 | err = c.ShouldBindQuery(&requestQuery) 106 | if err != nil { 107 | errors.SetValidationErrorResponse(err, c) 108 | return 109 | } 110 | 111 | // parse to uint64 112 | blockId, err := strconv.ParseUint(request.ID, 10, 64) 113 | helpers.CheckErr(err) 114 | 115 | // fetch data 116 | pagination := tools.NewPagination(c.Request) 117 | txs := explorer.TransactionRepository.GetPaginatedTxsByFilter(transaction.BlockFilter{ 118 | BlockId: blockId, 119 | }, &pagination) 120 | 121 | txs, err = explorer.TransactionService.PrepareTransactionsModel(txs) 122 | helpers.CheckErr(err) 123 | 124 | c.JSON(http.StatusOK, resource.TransformPaginatedCollection(txs, transaction.Resource{}, pagination)) 125 | } 126 | -------------------------------------------------------------------------------- /validator/repository.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "github.com/MinterTeam/minter-explorer-api/v2/helpers" 5 | "github.com/MinterTeam/minter-explorer-api/v2/tools" 6 | "github.com/MinterTeam/minter-explorer-extender/v2/models" 7 | "github.com/go-pg/pg/v10" 8 | ) 9 | 10 | type Repository struct { 11 | db *pg.DB 12 | } 13 | 14 | func NewRepository(db *pg.DB) *Repository { 15 | return &Repository{ 16 | db: db, 17 | } 18 | } 19 | 20 | func (r Repository) GetByPublicKey(publicKey string) *models.Validator { 21 | var validator models.Validator 22 | 23 | err := r.db.Model(&validator). 24 | Relation("Stakes"). 25 | Relation("OwnerAddress.address"). 26 | Relation("RewardAddress.address"). 27 | Relation("ControlAddress.address"). 28 | Join("JOIN validator_public_keys ON validator_public_keys.validator_id = validator.id"). 29 | Where("validator_public_keys.key = ?", publicKey). 30 | Select() 31 | 32 | if err != nil { 33 | return nil 34 | } 35 | 36 | return &validator 37 | } 38 | 39 | func (r Repository) GetTotalStakeByActiveValidators(ids []uint) string { 40 | var total string 41 | 42 | // get total stake of active validators 43 | err := r.db.Model((*models.Validator)(nil)). 44 | ColumnExpr("SUM(total_stake)"). 45 | Where("id IN (?)", pg.In(ids)). 46 | Select(&total) 47 | 48 | helpers.CheckErr(err) 49 | 50 | return total 51 | } 52 | 53 | func (r Repository) GetActiveValidatorIds() []uint { 54 | var blockValidator models.BlockValidator 55 | var lastBlock models.Block 56 | var ids []uint 57 | 58 | lastBlockQuery := r.db.Model(&lastBlock). 59 | Column("id"). 60 | Order("id DESC"). 61 | Limit(1) 62 | 63 | // get active validators by last block 64 | err := r.db.Model(&blockValidator). 65 | Column("validator_id"). 66 | Where("block_id = (?)", lastBlockQuery). 67 | Select(&ids) 68 | 69 | helpers.CheckErr(err) 70 | 71 | return ids 72 | } 73 | 74 | // Get active candidates count 75 | func (r Repository) GetActiveCandidatesCount() int { 76 | var validator models.Validator 77 | 78 | count, err := r.db.Model(&validator). 79 | Where("status = ?", models.ValidatorStatusReady). 80 | Count() 81 | 82 | helpers.CheckErr(err) 83 | 84 | return count 85 | } 86 | 87 | // GetValidatorsAndStakes Get validators and stakes 88 | func (r Repository) GetValidatorsAndStakes() []models.Validator { 89 | var validators []models.Validator 90 | 91 | err := r.db.Model(&validators). 92 | Relation("Stakes"). 93 | Relation("OwnerAddress.address"). 94 | Relation("RewardAddress.address"). 95 | Relation("ControlAddress.address"). 96 | Where("status is not null"). 97 | Order("total_stake desc"). 98 | Select() 99 | 100 | helpers.CheckErr(err) 101 | 102 | return validators 103 | } 104 | 105 | // GetValidators Get validators 106 | func (r Repository) GetValidators() []models.Validator { 107 | var validators []models.Validator 108 | 109 | err := r.db.Model(&validators). 110 | Where("status is not null"). 111 | Order("total_stake desc"). 112 | Select() 113 | 114 | helpers.CheckErr(err) 115 | 116 | return validators 117 | } 118 | 119 | // Get validator bans 120 | func (r Repository) GetBans(validator *models.Validator, pagination *tools.Pagination) (bans []models.ValidatorBan, err error) { 121 | pagination.Total, err = r.db.Model(&bans). 122 | Relation("Block"). 123 | Where("validator_id = ?", validator.ID). 124 | Apply(pagination.Filter). 125 | Order("block_id DESC"). 126 | SelectAndCount() 127 | 128 | return bans, err 129 | } 130 | 131 | // Get bans of validator list 132 | func (r Repository) GetBansByValidatorIds(validatorIds []uint64, pagination *tools.Pagination) ([]models.ValidatorBan, error) { 133 | var bans []models.ValidatorBan 134 | 135 | err := r.db.Model(&bans). 136 | Relation("Block"). 137 | Relation("Validator"). 138 | Where(`validator_id in (?)`, pg.In(validatorIds)). 139 | Order("block_id DESC"). 140 | Select() 141 | 142 | return bans, err 143 | } 144 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/MinterTeam/minter-explorer-api/v2 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/MinterTeam/explorer-sdk v0.1.1-0.20211108095047-4421b691e285 7 | github.com/MinterTeam/minter-explorer-extender/v2 v2.14.15-0.20220405120513-da4acb2cbc97 8 | github.com/MinterTeam/minter-go-node v1.0.5 9 | github.com/MinterTeam/minter-go-sdk/v2 v2.4.1-0.20220222111347-67fbebbdf760 10 | github.com/MinterTeam/node-grpc-gateway v1.5.2-0.20220222111111-70a755dd46ee 11 | github.com/centrifugal/centrifuge-go v0.3.0 12 | github.com/gin-gonic/gin v1.7.7 13 | github.com/go-pg/pg/v10 v10.10.6 14 | github.com/go-pg/urlstruct v0.2.8 15 | github.com/go-playground/validator/v10 v10.4.1 16 | github.com/go-resty/resty/v2 v2.7.0 17 | github.com/joho/godotenv v1.4.0 18 | github.com/prometheus/client_golang v1.12.1 19 | github.com/sirupsen/logrus v1.8.1 20 | github.com/zsais/go-gin-prometheus v0.1.0 21 | go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.22.0 22 | golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba 23 | google.golang.org/protobuf v1.27.1 24 | ) 25 | 26 | require ( 27 | github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect 28 | github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect 29 | github.com/MinterTeam/minter-explorer-tools/v4 v4.2.3 // indirect 30 | github.com/beorn7/perks v1.0.1 // indirect 31 | github.com/btcsuite/btcd v0.22.0-beta // indirect 32 | github.com/centrifugal/protocol v0.1.0 // indirect 33 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 34 | github.com/codemodus/kace v0.5.1 // indirect 35 | github.com/ethereum/go-ethereum v1.10.13 // indirect 36 | github.com/gin-contrib/sse v0.1.0 // indirect 37 | github.com/go-pg/zerochecker v0.2.0 // indirect 38 | github.com/go-playground/locales v0.13.0 // indirect 39 | github.com/go-playground/universal-translator v0.17.0 // indirect 40 | github.com/gogo/protobuf v1.3.2 // indirect 41 | github.com/golang/protobuf v1.5.2 // indirect 42 | github.com/gorilla/websocket v1.4.2 // indirect 43 | github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect 44 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.1 // indirect 45 | github.com/jinzhu/inflection v1.0.0 // indirect 46 | github.com/jpillora/backoff v1.0.0 // indirect 47 | github.com/json-iterator/go v1.1.12 // indirect 48 | github.com/leodido/go-urn v1.2.0 // indirect 49 | github.com/mattn/go-isatty v0.0.12 // indirect 50 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 51 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 52 | github.com/modern-go/reflect2 v1.0.2 // indirect 53 | github.com/prometheus/client_model v0.2.0 // indirect 54 | github.com/prometheus/common v0.32.1 // indirect 55 | github.com/prometheus/procfs v0.7.3 // indirect 56 | github.com/starwander/GoFibonacciHeap v0.0.0-20190508061137-ba2e4f01000a // indirect 57 | github.com/starwander/goraph v0.0.0-20200325033650-cb8f0beb44cc // indirect 58 | github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect 59 | github.com/tyler-smith/go-bip32 v1.0.0 // indirect 60 | github.com/tyler-smith/go-bip39 v1.1.0 // indirect 61 | github.com/ugorji/go/codec v1.1.7 // indirect 62 | github.com/vmihailenco/bufpool v0.1.11 // indirect 63 | github.com/vmihailenco/msgpack/v5 v5.3.4 // indirect 64 | github.com/vmihailenco/tagparser v0.1.2 // indirect 65 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 66 | go.opentelemetry.io/contrib v0.22.0 // indirect 67 | go.opentelemetry.io/otel v1.0.0-RC2 // indirect 68 | go.opentelemetry.io/otel/trace v1.0.0-RC2 // indirect 69 | golang.org/x/crypto v0.0.0-20211202192323-5770296d904e // indirect 70 | golang.org/x/net v0.0.0-20211208012354-db4efeb81f4b // indirect 71 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect 72 | golang.org/x/text v0.3.7 // indirect 73 | google.golang.org/genproto v0.0.0-20211207154714-918901c715cf // indirect 74 | google.golang.org/grpc v1.44.0 // indirect 75 | gopkg.in/yaml.v2 v2.4.0 // indirect 76 | mellium.im/sasl v0.2.1 // indirect 77 | ) 78 | --------------------------------------------------------------------------------