From 3623e8e7d334e49ecde03b413026b2bd96544141 Mon Sep 17 00:00:00 2001 From: Dmitry Kazharski Date: Sun, 27 Oct 2024 10:59:18 +0300 Subject: [PATCH] Token Price. --- CHANGELOG.md | 5 ++ go.mod | 3 +- go.sum | 6 +- internal/app.go | 13 +++++ internal/config/app.go | 1 + internal/config/zerion.go | 6 ++ internal/dao/models.go | 1 + internal/dao/token_price_worker.go | 76 +++++++++++++++++++++++++ pkg/sdk/zerion/client.go | 90 ++++++++++++++++++++++++++++++ pkg/sdk/zerion/models.go | 21 +++++++ resources/V15_dao_fungible_id.sql | 2 + 11 files changed, 221 insertions(+), 3 deletions(-) create mode 100644 internal/config/zerion.go create mode 100644 internal/dao/token_price_worker.go create mode 100644 pkg/sdk/zerion/client.go create mode 100644 pkg/sdk/zerion/models.go create mode 100644 resources/V15_dao_fungible_id.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index a132ddc..cd6926f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.3.2] - 2024-10-27 + +### Added +- Calculation of token price + ## [0.3.1] - 2024-10-18 ### Added diff --git a/go.mod b/go.mod index 2268f17..a2a4f39 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/goverland-labs/goverland-core-storage/protocol v0.0.0 github.com/goverland-labs/goverland-datasource-snapshot/protocol v0.6.2 github.com/goverland-labs/goverland-helpers-ens-resolver/protocol v0.1.0 - github.com/goverland-labs/goverland-platform-events v0.3.6 + github.com/goverland-labs/goverland-platform-events v0.3.7 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/lib/pq v1.10.9 @@ -25,6 +25,7 @@ require ( github.com/s-larionov/process-manager v0.0.1 github.com/shopspring/decimal v1.3.1 github.com/stretchr/testify v1.9.0 + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa golang.org/x/sync v0.7.0 google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.1 diff --git a/go.sum b/go.sum index 3904f85..0dece5f 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,8 @@ github.com/goverland-labs/goverland-datasource-snapshot/protocol v0.6.2 h1:MlshV github.com/goverland-labs/goverland-datasource-snapshot/protocol v0.6.2/go.mod h1:YEfWXRljVwjMnbPbolcatOwY6+QtjcKy1Tc4oLWCPA0= github.com/goverland-labs/goverland-helpers-ens-resolver/protocol v0.1.0 h1:Gc0aRk6jL9zJV2Ce5h+bsIl49OJHn3m27IPVeYOJTrE= github.com/goverland-labs/goverland-helpers-ens-resolver/protocol v0.1.0/go.mod h1:jyJGoBmFVY0o6b/3PNy5+bHtKqff6m/kw0eq4g0knBc= -github.com/goverland-labs/goverland-platform-events v0.3.6 h1:vpGehpJYNfKsNvufNySNiH5bSi18YNd/SFzmjRnGEi4= -github.com/goverland-labs/goverland-platform-events v0.3.6/go.mod h1:0/131HTR3cue1cDBVIoJ/iwgA+8f5MDQC8mUiqnouzE= +github.com/goverland-labs/goverland-platform-events v0.3.7 h1:CZJ1TGwayrc2cx05TOWLuvqmCtYul5MRNJUzf16cPuQ= +github.com/goverland-labs/goverland-platform-events v0.3.7/go.mod h1:0/131HTR3cue1cDBVIoJ/iwgA+8f5MDQC8mUiqnouzE= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= @@ -155,6 +155,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= diff --git a/internal/app.go b/internal/app.go index bcba280..d42d76b 100644 --- a/internal/app.go +++ b/internal/app.go @@ -3,6 +3,7 @@ package internal import ( "fmt" "log" + "net/http" "os" "os/signal" "syscall" @@ -33,6 +34,7 @@ import ( "github.com/goverland-labs/goverland-core-storage/pkg/grpcsrv" "github.com/goverland-labs/goverland-core-storage/pkg/health" "github.com/goverland-labs/goverland-core-storage/pkg/prometheus" + zerionsdk "github.com/goverland-labs/goverland-core-storage/pkg/sdk/zerion" ) type Application struct { @@ -63,6 +65,8 @@ type Application struct { eventsRepo *events.Repo statsService *stats.Service + + zerionClient *zerionsdk.Client } func NewApplication(cfg config.App) (*Application, error) { @@ -161,6 +165,8 @@ func (a *Application) initServices() error { return err } + a.initZerionAPI() + err = a.initEnsResolver(pb) if err != nil { return fmt.Errorf("init dao: %w", err) @@ -237,6 +243,7 @@ func (a *Application) initDao(nc *nats.Conn, pb *natsclient.Publisher) error { pcw := dao.NewPopularCategoryWorker(service) avw := dao.NewActiveVotesWorker(service) rw := dao.NewRecommendationWorker(service) + tpw := dao.NewTokenPriceWorker(service, a.zerionClient) a.manager.AddWorker(process.NewCallbackWorker("dao-new-category-process-worker", cw.ProcessNew)) a.manager.AddWorker(process.NewCallbackWorker("dao-new-category-outdated-worker", cw.RemoveOutdated)) a.manager.AddWorker(process.NewCallbackWorker("dao-new-voters-worker", mc.ProcessNew)) @@ -244,6 +251,7 @@ func (a *Application) initDao(nc *nats.Conn, pb *natsclient.Publisher) error { a.manager.AddWorker(process.NewCallbackWorker("dao-active-votes-worker", avw.Process)) a.manager.AddWorker(process.NewCallbackWorker("top-dao-cache-worker", topDAOCache.Start)) a.manager.AddWorker(process.NewCallbackWorker("dao-recommendations", rw.Process)) + a.manager.AddWorker(process.NewCallbackWorker("token-price", tpw.Process)) return nil } @@ -375,3 +383,8 @@ func (a *Application) registerShutdown() { a.manager.AwaitAll() } + +func (a *Application) initZerionAPI() { + zc := zerionsdk.NewClient(a.cfg.Zerion.BaseURL, a.cfg.Zerion.Key, http.DefaultClient) + a.zerionClient = zc +} diff --git a/internal/config/app.go b/internal/config/app.go index e15dc59..e41f4e3 100644 --- a/internal/config/app.go +++ b/internal/config/app.go @@ -7,4 +7,5 @@ type App struct { Nats Nats DB DB InternalAPI InternalAPI + Zerion Zerion } diff --git a/internal/config/zerion.go b/internal/config/zerion.go new file mode 100644 index 0000000..7c3f2db --- /dev/null +++ b/internal/config/zerion.go @@ -0,0 +1,6 @@ +package config + +type Zerion struct { + BaseURL string `env:"ZERION_API_BASE_URL" require:"true"` + Key string `env:"ZERION_API_KEY" require:"true"` +} diff --git a/internal/dao/models.go b/internal/dao/models.go index 4f4103e..1d5224b 100644 --- a/internal/dao/models.go +++ b/internal/dao/models.go @@ -112,6 +112,7 @@ type Dao struct { // ActiveProposalsIDs the list of active proposals identifiers ActiveProposalsIDs []string `gorm:"serializer:json"` Verified bool + FungibleId string } func convertToCoreEvent(dao Dao) events.DaoPayload { diff --git a/internal/dao/token_price_worker.go b/internal/dao/token_price_worker.go new file mode 100644 index 0000000..87a1aee --- /dev/null +++ b/internal/dao/token_price_worker.go @@ -0,0 +1,76 @@ +package dao + +import ( + "context" + "github.com/google/uuid" + "github.com/goverland-labs/goverland-core-storage/pkg/sdk/zerion" + coreevents "github.com/goverland-labs/goverland-platform-events/events/core" + "golang.org/x/exp/maps" + "strings" + "time" + + "github.com/rs/zerolog/log" +) + +const ( + tokenPriceCheckDelay = 24 * time.Hour +) + +type TokenPriceWorker struct { + service *Service + zerionClient *zerion.Client +} + +func NewTokenPriceWorker(s *Service, z *zerion.Client) *TokenPriceWorker { + return &TokenPriceWorker{ + service: s, + zerionClient: z, + } +} + +func (w *TokenPriceWorker) Process(ctx context.Context) error { + for { + filters := []Filter{VerifiedFilter{}} + + list, err := w.service.GetByFilters(filters) + if err != nil { + log.Error().Err(err).Msg("getTokenPrice") + } + fm := make(map[string]uuid.UUID) + for _, d := range list.Daos { + if d.FungibleId != "" { + fm[d.FungibleId] = d.ID + } + } + if len(fm) > 0 { + l, err := w.zerionClient.GetTokenPrices(strings.Join(maps.Keys(fm), ",")) + if err != nil { + log.Error().Err(err).Msg("zerion client error") + } + if err := w.service.events.PublishJSON(ctx, coreevents.DaoTokenPriceUpdated, convertToCorePaylod(l.List, fm)); err != nil { + log.Error().Err(err).Msgf("publish token prices event") + } + + } + select { + case <-ctx.Done(): + return nil + case <-time.After(tokenPriceCheckDelay): + } + } +} + +func convertToCorePaylod(list []zerion.FungibleData, fungiblesMap map[string]uuid.UUID) coreevents.TokenPricesPayload { + res := make(coreevents.TokenPricesPayload, 0, len(list)) + for i := range list { + daoId, exist := fungiblesMap[list[i].ID] + if exist { + res = append(res, coreevents.TokenPricePayload{ + DaoID: daoId, + Price: list[i].Attributes.MarketData.Price, + }) + } + } + + return res +} diff --git a/pkg/sdk/zerion/client.go b/pkg/sdk/zerion/client.go new file mode 100644 index 0000000..0765560 --- /dev/null +++ b/pkg/sdk/zerion/client.go @@ -0,0 +1,90 @@ +package zerion + +import ( + "encoding/json" + "fmt" + "github.com/rs/zerolog/log" + "io" + "net/http" +) + +type ( + Client struct { + client *http.Client + apiURL string + authKey string + } +) + +func NewClient(apiURL, authKey string, client *http.Client) *Client { + return &Client{ + client: client, + apiURL: apiURL, + authKey: authKey, + } +} + +func (c *Client) GetTokenPrices(ids string) (*FungibleList, error) { + req, err := c.buildRequest( + http.MethodGet, + "fungibles/", + "fungibles-list", + map[string]string{ + "currency": "usd", + "filter[fungible_ids]": ids, + }, + ) + log.Info().Msgf("request %v", req) + if err != nil { + return nil, fmt.Errorf("build request: %w", err) + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("request do: %w", err) + } + + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("read body: %w", err) + } + + var list FungibleList + if err = json.Unmarshal(body, &list); err != nil { + return nil, fmt.Errorf("unmarshal body: %w", err) + } + + return &list, nil +} + +func (c *Client) buildRequest(method, subURL, alias string, params map[string]string) (*http.Request, error) { + req, err := http.NewRequest( + method, + fmt.Sprintf("%s/%s", c.apiURL, subURL), + nil, + ) + if err != nil { + return nil, fmt.Errorf("build request: %w", err) + } + + q := req.URL.Query() + for k, v := range params { + q.Add(k, v) + } + req.URL.RawQuery = q.Encode() + + req.Header.Add("alias", alias) + + return c.withAuth(req), nil +} + +func (c *Client) withAuth(req *http.Request) *http.Request { + if c.authKey == "" { + return req + } + + req.Header.Add("Authorization", fmt.Sprintf("Basic %s", c.authKey)) + + return req +} diff --git a/pkg/sdk/zerion/models.go b/pkg/sdk/zerion/models.go new file mode 100644 index 0000000..eefdbdf --- /dev/null +++ b/pkg/sdk/zerion/models.go @@ -0,0 +1,21 @@ +package zerion + +type ( + MarketData struct { + Price float64 `json:"price"` + } + + Attributes struct { + Symbol string `json:"symbol"` + MarketData MarketData `json:"market_data"` + } + + FungibleData struct { + ID string `json:"id"` + Attributes Attributes `json:"attributes"` + } + + FungibleList struct { + List []FungibleData `json:"data"` + } +) diff --git a/resources/V15_dao_fungible_id.sql b/resources/V15_dao_fungible_id.sql new file mode 100644 index 0000000..c30a2a4 --- /dev/null +++ b/resources/V15_dao_fungible_id.sql @@ -0,0 +1,2 @@ +ALTER TABLE daos + ADD COLUMN IF NOT EXISTS fungible_id text;