diff --git a/.gitignore b/.gitignore index 6f44cfc..e7fd6da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .idea .env dev/dev-data -migrations/*.env +migrations/*prod* dist/ diff --git a/cmd/server.go b/cmd/server.go index 016645b..9f81bc5 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -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 } diff --git a/go.mod b/go.mod index 7c97728..263a089 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 64c933d..343a03e 100644 --- a/go.sum +++ b/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= diff --git a/internal/dashboard/api.go b/internal/dashboard/api.go index a763929..4664588 100644 --- a/internal/dashboard/api.go +++ b/internal/dashboard/api.go @@ -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) } diff --git a/internal/dashboard/charts.go b/internal/dashboard/charts.go index 781bbc8..d338ffb 100644 --- a/internal/dashboard/charts.go +++ b/internal/dashboard/charts.go @@ -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) +} diff --git a/internal/public/api.go b/internal/public/api.go index 81619b5..ce46aeb 100644 --- a/internal/public/api.go +++ b/internal/public/api.go @@ -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) } diff --git a/internal/public/balances.go b/internal/public/balances.go index c06b030..3de7d6e 100644 --- a/internal/public/balances.go +++ b/internal/public/balances.go @@ -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 } diff --git a/internal/public/tokens.go b/internal/public/tokens.go index 33328df..af97c0d 100644 --- a/internal/public/tokens.go +++ b/internal/public/tokens.go @@ -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) +} diff --git a/queries/dashboard.sql b/queries/dashboard.sql index c731487..c12c16f 100644 --- a/queries/dashboard.sql +++ b/queries/dashboard.sql @@ -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; \ No newline at end of file +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; diff --git a/queries/public.sql b/queries/public.sql index 9711ea7..1144277 100644 --- a/queries/public.sql +++ b/queries/public.sql @@ -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; \ No newline at end of file +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;