mirror of
				https://github.com/GrassrootsEconomics/cic-dw.git
				synced 2025-11-04 08:08:22 +01:00 
			
		
		
		
	add: (feat) individual token stats
- volume chart - token holders - token tx count * minor refactors
This commit is contained in:
		
							parent
							
								
									f00e97cc94
								
							
						
					
					
						commit
						7bda44c169
					
				
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -1,6 +1,6 @@
 | 
			
		||||
.idea
 | 
			
		||||
.env
 | 
			
		||||
dev/dev-data
 | 
			
		||||
migrations/*.env
 | 
			
		||||
migrations/*prod*
 | 
			
		||||
 | 
			
		||||
dist/
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@ func initHTTPServer() *echo.Echo {
 | 
			
		||||
	}))
 | 
			
		||||
 | 
			
		||||
	dashboard.InitDashboardApi(server, db, preparedQueries.dashboard)
 | 
			
		||||
	public.InitPublicApi(server, db, batchBalance, preparedQueries.public)
 | 
			
		||||
	public.InitPublicApi(server, db, batchBalance, cicnetClient, preparedQueries.public)
 | 
			
		||||
 | 
			
		||||
	return server
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							@ -6,7 +6,7 @@ require (
 | 
			
		||||
	github.com/ethereum/go-ethereum v1.10.17
 | 
			
		||||
	github.com/georgysavva/scany v0.3.0
 | 
			
		||||
	github.com/golang-module/carbon/v2 v2.1.6
 | 
			
		||||
	github.com/grassrootseconomics/cic-go v1.4.1
 | 
			
		||||
	github.com/grassrootseconomics/cic-go v1.5.0
 | 
			
		||||
	github.com/hibiken/asynq v0.23.0
 | 
			
		||||
	github.com/jackc/pgx/v4 v4.16.1
 | 
			
		||||
	github.com/knadh/koanf v1.4.1
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
									
									
									
									
								
							@ -228,6 +228,8 @@ github.com/grassrootseconomics/cic-go v1.4.0 h1:ydGp9pVrAhpq45KUSkPOHjP1PyGlBsRC
 | 
			
		||||
github.com/grassrootseconomics/cic-go v1.4.0/go.mod h1:cQcLMsuhCirTVO5ccG37S4pGS1vnkSepoi+eYZvdOEY=
 | 
			
		||||
github.com/grassrootseconomics/cic-go v1.4.1 h1:fFthl73ZJydubPOP48nMtDIl0TgvjOXGUMBYn4JXIsI=
 | 
			
		||||
github.com/grassrootseconomics/cic-go v1.4.1/go.mod h1:cQcLMsuhCirTVO5ccG37S4pGS1vnkSepoi+eYZvdOEY=
 | 
			
		||||
github.com/grassrootseconomics/cic-go v1.5.0 h1:XnOPxMq3hFwdSwse749dfO1biPH5KjsJk+gbm1bcCz0=
 | 
			
		||||
github.com/grassrootseconomics/cic-go v1.5.0/go.mod h1:cQcLMsuhCirTVO5ccG37S4pGS1vnkSepoi+eYZvdOEY=
 | 
			
		||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 | 
			
		||||
github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0=
 | 
			
		||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
 | 
			
		||||
 | 
			
		||||
@ -26,4 +26,6 @@ func InitDashboardApi(e *echo.Echo, db *pgxpool.Pool, queries goyesql.Queries) {
 | 
			
		||||
 | 
			
		||||
	g.GET("/new-registrations", handleNewRegistrations)
 | 
			
		||||
	g.GET("/transactions-count", handleTransactionsCount)
 | 
			
		||||
	g.GET("/token-transactions-count/:address", handleTokenTransactionsCount)
 | 
			
		||||
	g.GET("/token-volume/:address", handleTokenVolume)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,10 +2,11 @@ package dashboard
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"github.com/georgysavva/scany/pgxscan"
 | 
			
		||||
	"github.com/labstack/echo/v4"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/georgysavva/scany/pgxscan"
 | 
			
		||||
	"github.com/labstack/echo/v4"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type lineChartRes struct {
 | 
			
		||||
@ -15,7 +16,8 @@ type lineChartRes struct {
 | 
			
		||||
 | 
			
		||||
func handleNewRegistrations(c echo.Context) error {
 | 
			
		||||
	var (
 | 
			
		||||
		api  = c.Get("api").(*api)
 | 
			
		||||
		api = c.Get("api").(*api)
 | 
			
		||||
 | 
			
		||||
		data []lineChartRes
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
@ -35,7 +37,8 @@ func handleNewRegistrations(c echo.Context) error {
 | 
			
		||||
 | 
			
		||||
func handleTransactionsCount(c echo.Context) error {
 | 
			
		||||
	var (
 | 
			
		||||
		api  = c.Get("api").(*api)
 | 
			
		||||
		api = c.Get("api").(*api)
 | 
			
		||||
 | 
			
		||||
		data []lineChartRes
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
@ -52,3 +55,48 @@ func handleTransactionsCount(c echo.Context) error {
 | 
			
		||||
 | 
			
		||||
	return c.JSON(http.StatusOK, data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func handleTokenTransactionsCount(c echo.Context) error {
 | 
			
		||||
	var (
 | 
			
		||||
		api   = c.Get("api").(*api)
 | 
			
		||||
		token = c.Param("address")
 | 
			
		||||
 | 
			
		||||
		data []lineChartRes
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	from, to := parseDateRange(c.QueryParams())
 | 
			
		||||
 | 
			
		||||
	rows, err := api.db.Query(context.Background(), api.q["token-transactions-count"], from, to, token)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := pgxscan.ScanAll(&data, rows); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return c.JSON(http.StatusOK, data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func handleTokenVolume(c echo.Context) error {
 | 
			
		||||
	var (
 | 
			
		||||
		api   = c.Get("api").(*api)
 | 
			
		||||
		token = c.Param("address")
 | 
			
		||||
 | 
			
		||||
		data []lineChartRes
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	from, to := parseDateRange(c.QueryParams())
 | 
			
		||||
 | 
			
		||||
	rows, err := api.db.Query(context.Background(), api.q["token-volume"], from, to, token)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := pgxscan.ScanAll(&data, rows); err != nil {
 | 
			
		||||
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return c.JSON(http.StatusOK, data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ package public
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	batch_balance "github.com/grassrootseconomics/cic-go/batch_balance"
 | 
			
		||||
	cic_net "github.com/grassrootseconomics/cic-go/net"
 | 
			
		||||
	"github.com/jackc/pgx/v4/pgxpool"
 | 
			
		||||
	"github.com/labstack/echo/v4"
 | 
			
		||||
	"github.com/nleof/goyesql"
 | 
			
		||||
@ -10,10 +11,11 @@ import (
 | 
			
		||||
type api struct {
 | 
			
		||||
	db *pgxpool.Pool
 | 
			
		||||
	q  goyesql.Queries
 | 
			
		||||
	c  *batch_balance.BatchBalance
 | 
			
		||||
	bb *batch_balance.BatchBalance
 | 
			
		||||
	cn *cic_net.CicNet
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func InitPublicApi(e *echo.Echo, db *pgxpool.Pool, batchBalance *batch_balance.BatchBalance, queries goyesql.Queries) {
 | 
			
		||||
func InitPublicApi(e *echo.Echo, db *pgxpool.Pool, batchBalance *batch_balance.BatchBalance, cicnet *cic_net.CicNet, queries goyesql.Queries) {
 | 
			
		||||
	g := e.Group("/public")
 | 
			
		||||
 | 
			
		||||
	g.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
 | 
			
		||||
@ -21,7 +23,8 @@ func InitPublicApi(e *echo.Echo, db *pgxpool.Pool, batchBalance *batch_balance.B
 | 
			
		||||
			c.Set("api", &api{
 | 
			
		||||
				db: db,
 | 
			
		||||
				q:  queries,
 | 
			
		||||
				c:  batchBalance,
 | 
			
		||||
				cn: cicnet,
 | 
			
		||||
				bb: batchBalance,
 | 
			
		||||
			})
 | 
			
		||||
			return next(c)
 | 
			
		||||
		}
 | 
			
		||||
@ -32,4 +35,6 @@ func InitPublicApi(e *echo.Echo, db *pgxpool.Pool, batchBalance *batch_balance.B
 | 
			
		||||
	g.GET("/balances/:address", handleBalancesQuery)
 | 
			
		||||
	g.GET("/tokens-count", handleTokensCountQuery)
 | 
			
		||||
	g.GET("/tokens", handleTokenListQuery)
 | 
			
		||||
	g.GET("/token/:address", handleTokenInfo)
 | 
			
		||||
	g.GET("/token-summary/:address", handleTokenSummary)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -48,7 +48,7 @@ func handleBalancesQuery(c echo.Context) error {
 | 
			
		||||
		tokenAddresses = append(tokenAddresses, w3.A(address.Checksum(rowData.TokenAddress)))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	balances, err := api.c.TokensBalance(context.Background(), w3.A(address.Checksum(qAddress)), tokenAddresses)
 | 
			
		||||
	balances, err := api.bb.TokensBalance(context.Background(), w3.A(address.Checksum(qAddress)), tokenAddresses)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,14 @@
 | 
			
		||||
package public
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"cic-dw/pkg/address"
 | 
			
		||||
	"cic-dw/pkg/pagination"
 | 
			
		||||
	"context"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/georgysavva/scany/pgxscan"
 | 
			
		||||
	"github.com/labstack/echo/v4"
 | 
			
		||||
	"github.com/lmittmann/w3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type tokensRes struct {
 | 
			
		||||
@ -20,10 +22,23 @@ type tokenCountRes struct {
 | 
			
		||||
	Count int `db:"count" json:"count"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TokenInfoRes struct {
 | 
			
		||||
	IsDemurrage bool   `json:"is_demurrage"`
 | 
			
		||||
	Name        string `json:"token_name"`
 | 
			
		||||
	Symbol      string `json:"token_symbol"`
 | 
			
		||||
	TotalSupply int64  `json:"token_total_supply"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type tokenSummaryRes struct {
 | 
			
		||||
	TotalHolders      int64 `db:"count" json:"token_holders"`
 | 
			
		||||
	TotalTransactions int64 `db:"count" json:"token_transactions"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func handleTokenListQuery(c echo.Context) error {
 | 
			
		||||
	var (
 | 
			
		||||
		api = c.Get("api").(*api)
 | 
			
		||||
		pg  = pagination.GetPagination(c.QueryParams())
 | 
			
		||||
 | 
			
		||||
		res []tokensRes
 | 
			
		||||
		q   string
 | 
			
		||||
	)
 | 
			
		||||
@ -49,6 +64,7 @@ func handleTokenListQuery(c echo.Context) error {
 | 
			
		||||
func handleTokensCountQuery(c echo.Context) error {
 | 
			
		||||
	var (
 | 
			
		||||
		api = c.Get("api").(*api)
 | 
			
		||||
 | 
			
		||||
		res tokenCountRes
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
@ -63,3 +79,61 @@ func handleTokensCountQuery(c echo.Context) error {
 | 
			
		||||
 | 
			
		||||
	return c.JSON(http.StatusOK, res)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func handleTokenInfo(c echo.Context) error {
 | 
			
		||||
	var (
 | 
			
		||||
		api          = c.Get("api").(*api)
 | 
			
		||||
		tokenAddress = c.Param("address")
 | 
			
		||||
		rCtx         = context.Background()
 | 
			
		||||
 | 
			
		||||
		res TokenInfoRes
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	_, err := api.cn.DemurrageTokenInfo(rCtx, w3.A(address.Checksum(tokenAddress)))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		res.IsDemurrage = false
 | 
			
		||||
	} else {
 | 
			
		||||
		res.IsDemurrage = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tokenInfo, err := api.cn.ERC20TokenInfo(rCtx, w3.A(address.Checksum(tokenAddress)))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res.Name = tokenInfo.Name
 | 
			
		||||
	res.Symbol = tokenInfo.Symbol
 | 
			
		||||
	res.TotalSupply = tokenInfo.TotalSupply.Int64() / 1000000
 | 
			
		||||
 | 
			
		||||
	return c.JSON(http.StatusOK, res)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func handleTokenSummary(c echo.Context) error {
 | 
			
		||||
	var (
 | 
			
		||||
		api   = c.Get("api").(*api)
 | 
			
		||||
		token = c.Param("address")
 | 
			
		||||
 | 
			
		||||
		data tokenSummaryRes
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	uniqueTokenHoldersrRow, err := api.db.Query(context.Background(), api.q["unique-token-holders"], token)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := pgxscan.ScanOne(&data.TotalHolders, uniqueTokenHoldersrRow); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tokenTxRow, err := api.db.Query(context.Background(), api.q["all-time-token-transactions-count"], token)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := pgxscan.ScanOne(&data.TotalTransactions, tokenTxRow); err != nil {
 | 
			
		||||
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return c.JSON(http.StatusOK, data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,47 @@ exclude AS (
 | 
			
		||||
SELECT date_range.day AS x, COUNT(transactions.id) AS y
 | 
			
		||||
FROM date_range
 | 
			
		||||
LEFT JOIN transactions ON date_range.day = CAST(transactions.date_block AS date)
 | 
			
		||||
WHERE transactions.sender_address NOT IN (SELECT sys_address FROM exclude) AND transactions.recipient_address NOT IN (SELECT sys_address FROM exclude)
 | 
			
		||||
AND transactions.sender_address NOT IN (SELECT sys_address FROM exclude) AND transactions.recipient_address NOT IN (SELECT sys_address FROM exclude)
 | 
			
		||||
AND transactions.success = true
 | 
			
		||||
GROUP BY date_range.day
 | 
			
		||||
ORDER BY date_range.day
 | 
			
		||||
LIMIT 730;
 | 
			
		||||
LIMIT 730;
 | 
			
		||||
 | 
			
		||||
-- name: token-transactions-count
 | 
			
		||||
-- This query gets transactions for a specific token for a given date range
 | 
			
		||||
WITH date_range AS (
 | 
			
		||||
    SELECT day::date FROM generate_series($1, $2, INTERVAL '1 day') day
 | 
			
		||||
),
 | 
			
		||||
exclude AS (
 | 
			
		||||
    SELECT sys_address FROM sys_accounts WHERE sys_address IS NOT NULL
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
SELECT date_range.day AS x, COUNT(transactions.id) AS y
 | 
			
		||||
FROM date_range
 | 
			
		||||
LEFT JOIN transactions ON date_range.day = CAST(transactions.date_block AS date)
 | 
			
		||||
AND transactions.sender_address NOT IN (SELECT sys_address FROM exclude) AND transactions.recipient_address NOT IN (SELECT sys_address FROM exclude)
 | 
			
		||||
AND transactions.token_address = $3
 | 
			
		||||
AND transactions.success = true
 | 
			
		||||
GROUP BY date_range.day
 | 
			
		||||
ORDER BY date_range.day
 | 
			
		||||
LIMIT 730;
 | 
			
		||||
 | 
			
		||||
--name: token-volume
 | 
			
		||||
-- This query rteurns daily token volume
 | 
			
		||||
-- Assumes erc20 token is 6 decimals
 | 
			
		||||
WITH date_range AS (
 | 
			
		||||
    SELECT day::date FROM generate_series($1, $2, INTERVAL '1 day') day
 | 
			
		||||
),
 | 
			
		||||
exclude AS (
 | 
			
		||||
    SELECT sys_address FROM sys_accounts WHERE sys_address IS NOT NULL
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
SELECT date_range.day AS x, COALESCE(SUM(transactions.tx_value / 1000000), 0) AS y
 | 
			
		||||
FROM date_range
 | 
			
		||||
LEFT JOIN transactions ON date_range.day = CAST(transactions.date_block AS date)
 | 
			
		||||
AND transactions.sender_address NOT IN (SELECT sys_address FROM exclude) AND transactions.recipient_address NOT IN (SELECT sys_address FROM exclude)
 | 
			
		||||
AND transactions.token_address = $3
 | 
			
		||||
AND transactions.success = true
 | 
			
		||||
GROUP BY date_range.day
 | 
			
		||||
ORDER BY date_range.day
 | 
			
		||||
LIMIT 730;
 | 
			
		||||
 | 
			
		||||
@ -16,4 +16,33 @@ WHERE tokens.id < $1 ORDER BY tokens.id ASC LIMIT $2;
 | 
			
		||||
 | 
			
		||||
-- name: tokens-count
 | 
			
		||||
-- Return total record count from individual i= tables/views
 | 
			
		||||
SELECT COUNT(*) FROM tokens;
 | 
			
		||||
SELECT COUNT(*) FROM tokens;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
--name: unique-token-holders
 | 
			
		||||
-- Returns the unique token holders based on seen transactions
 | 
			
		||||
WITH unique_holders AS (
 | 
			
		||||
	SELECT sender_address AS holding_address FROM transactions
 | 
			
		||||
  	WHERE token_address = $1
 | 
			
		||||
  	UNION
 | 
			
		||||
  	SELECT recipient_address AS holding_address FROM transactions
 | 
			
		||||
  	WHERE token_address = $1
 | 
			
		||||
),
 | 
			
		||||
exclude AS (
 | 
			
		||||
    SELECT sys_address FROM sys_accounts WHERE sys_address IS NOT NULL
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
SELECT COUNT(holding_address) FROM unique_holders
 | 
			
		||||
WHERE holding_address NOT IN (SELECT sys_address FROM exclude);
 | 
			
		||||
 | 
			
		||||
--name: all-time-token-transactions-count
 | 
			
		||||
-- Returns transactions of individual tokens
 | 
			
		||||
WITH exclude AS (
 | 
			
		||||
    SELECT sys_address FROM sys_accounts WHERE sys_address IS NOT NULL
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
SELECT COUNT(*) FROM transactions
 | 
			
		||||
WHERE token_address = $1
 | 
			
		||||
AND transactions.sender_address NOT IN (SELECT sys_address FROM exclude)
 | 
			
		||||
AND transactions.recipient_address NOT IN (SELECT sys_address FROM exclude)
 | 
			
		||||
AND transactions.success = true;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user