diff --git a/.env.example b/.env.example index c7af1b79..2ca0a78d 100644 --- a/.env.example +++ b/.env.example @@ -36,4 +36,7 @@ VOCDONI_WEB3=https://mainnet.optimism.io,https://optimism.llamarpc.com,https://o # VOCDONI_AIRSTACKBLOCKCHAINS="ethereum,polygon,base" # VOCDONI_AIRSTACKSUPPORTAPIENDPOINT= # VOCDONI_AIRSTACKMAXHOLDERS=10000 -# VOCDOIN_AIRSTACKTOKENWHITELIST="" \ No newline at end of file +# VOCDOIN_AIRSTACKTOKENWHITELIST="" + +# VOCDONI_CENSUS3ENDPOINT="" +# VOCDONI_CENSUS3APIKEY="" \ No newline at end of file diff --git a/census3/census3.go b/census3/census3.go new file mode 100644 index 00000000..ba599296 --- /dev/null +++ b/census3/census3.go @@ -0,0 +1,151 @@ +package census3 + +import ( + "fmt" + "net/url" + "time" + + "github.com/google/uuid" + c3types "github.com/vocdoni/census3/api" + c3client "github.com/vocdoni/census3/apiclient" +) + +const ( + // maxRetries is the maximum number of retries for a request. + maxRetries = 3 + // retryTime is the time to wait between retries. + retryTime = time.Second * 2 +) + +// Client wraps a client for the Census3 API and other revelant data. +type Client struct { + c *c3client.HTTPclient +} + +// NewClient creates a new client for the Census3 API. +func NewClient(endpoint string, bearerToken string) (*Client, error) { + bt, err := uuid.Parse(bearerToken) + if err != nil { + return nil, fmt.Errorf("invalid bearer token: %v", err) + } + addr, err := url.Parse(endpoint) + if err != nil { + return nil, fmt.Errorf("invalid endpoint: %v", err) + } + httpClient, err := c3client.NewHTTPclient(addr, &bt) + if err != nil { + return nil, fmt.Errorf("error creating HTTP client: %v", err) + } + c3 := &Client{ + c: httpClient, + } + return c3, nil +} + +// reqFunc is a function type that encapsulates an API call +type reqFunc[T any] func() (T, error) + +// requestWithRetry handles the retry logic for a request +func requestWithRetry[T any](fn reqFunc[T]) (T, error) { + var result T + for i := 0; i < maxRetries; i++ { + result, err := fn() + if err != nil { + time.Sleep(retryTime) + continue + } + return result, nil + } + return result, fmt.Errorf("failed after %d retries", maxRetries) +} + +// SupportedChains returns the information of the Census3 endpoint supported chains. +func (c3 *Client) SupportedChains() ([]c3types.SupportedChain, error) { + return requestWithRetry(func() ([]c3types.SupportedChain, error) { + info, err := c3.c.Info() + if err != nil { + return nil, fmt.Errorf("failed to get supported chains: %w", err) + } + return info.SupportedChains, nil + }) +} + +// Tokens returns the list of tokens registered in the Census3 endpoint. +func (c3 *Client) Tokens() ([]*c3types.TokenListItem, error) { + return requestWithRetry(func() ([]*c3types.TokenListItem, error) { + tokens, err := c3.c.Tokens(-1, "", "") + if err != nil { + return nil, fmt.Errorf("failed to get tokens: %w", err) + } + return tokens, nil + + }) +} + +// Token returns the token with the given ID. +func (c3 *Client) Token(tokenID, externalID string, chainID uint64) (*c3types.Token, error) { + return requestWithRetry(func() (*c3types.Token, error) { + token, err := c3.c.Token(tokenID, chainID, externalID) + if err != nil { + return nil, fmt.Errorf("failed to get token: %w", err) + } + return token, nil + }) +} + +// SupportedTokens returns the list of tokens supported by the Census3 endpoint. +func (c3 *Client) SupportedTokens() ([]string, error) { + return requestWithRetry(func() ([]string, error) { + tokens, err := c3.c.TokenTypes() + if err != nil { + return nil, fmt.Errorf("failed to get supported tokens: %w", err) + } + return tokens, nil + }) +} + +// Strategies returns the list of strategies registered in the Census3 endpoint. +func (c3 *Client) Strategies() ([]*c3types.Strategy, error) { + return requestWithRetry(func() ([]*c3types.Strategy, error) { + strategies, err := c3.c.Strategies(-1, "", "") + if err != nil { + return nil, fmt.Errorf("failed to get strategies: %w", err) + } + return strategies, nil + }) +} + +// Strategy returns the strategy with the given ID. +func (c3 *Client) Strategy(strategyID uint64) (*c3types.Strategy, error) { + return requestWithRetry(func() (*c3types.Strategy, error) { + strategy, err := c3.c.Strategy(strategyID) + if err != nil { + return nil, fmt.Errorf("failed to get strategy: %w", err) + } + return strategy, nil + }) +} + +// StrategyHolders returns the list of holders for the given strategy. +// The returned map has the holder's address as key and the holder's balance as a string encoded big.Int +func (c3 *Client) StrategyHolders(strategyID uint64) (map[string]string, error) { + return requestWithRetry(func() (map[string]string, error) { + holders, err := c3.c.HoldersByStrategy(strategyID) + if err != nil { + return nil, fmt.Errorf("failed to get strategy holders: %w", err) + } + return holders.Holders, nil + }) +} + +// Census returns the census with the given ID. +func (c3 *Client) Census(censusID uint64) (*c3types.Census, error) { + return requestWithRetry(func() (*c3types.Census, error) { + census, err := c3.c.Census(censusID) + if err != nil { + return nil, fmt.Errorf("failed to get census: %w", err) + } + return census, nil + + }) +} diff --git a/go.mod b/go.mod index 237f6c5c..6d5f5758 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.18.2 + github.com/vocdoni/census3 v0.1.4-0.20240411135514-e434723b7840 github.com/zeebo/blake3 v0.2.3 go.mongodb.org/mongo-driver v1.14.0 go.vocdoni.io/dvote v1.10.2-0.20240313095944-f5790a5af0ed @@ -21,6 +22,7 @@ require ( require ( bazil.org/fuse v0.0.0-20200407214033-5883e5a4b512 // indirect contrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect + git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9 // indirect github.com/766b/chi-prometheus v0.0.0-20211217152057-87afa9aa2ca8 // indirect github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect github.com/DataDog/zstd v1.5.2 // indirect diff --git a/go.sum b/go.sum index 3058c9f5..c3849637 100644 --- a/go.sum +++ b/go.sum @@ -46,6 +46,9 @@ dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBr dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +git.sr.ht/~sircmpwn/getopt v0.0.0-20191230200459-23622cc906b3/go.mod h1:wMEGFFFNuPos7vHmWXfszqImLppbc0wEhh6JBfJIUgw= +git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9 h1:Ahny8Ud1LjVMMAlt8utUFKhhxJtwBAualvsbc/Sk7cE= +git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9/go.mod h1:BVJwbDfVjCjoFiKrhkei6NdGcZYpkDkdyCdg1ukytRA= github.com/766b/chi-prometheus v0.0.0-20211217152057-87afa9aa2ca8 h1:hK1G69lDhhrGqJbRA5i1rmT2KI/W77MSdr7hEGHqWdQ= github.com/766b/chi-prometheus v0.0.0-20211217152057-87afa9aa2ca8/go.mod h1:X/LhbmoBoRu8TxoGIOIraVNhfz3hhikJoaelrOuhdPY= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= @@ -1545,6 +1548,8 @@ github.com/vektah/gqlparser/v2 v2.5.1 h1:ZGu+bquAY23jsxDRcYpWjttRZrUz07LbiY77gUO github.com/vektah/gqlparser/v2 v2.5.1/go.mod h1:mPgqFBu/woKTVYWyNk8cO3kh4S/f4aRFZrvOnp3hmCs= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/vocdoni/census3 v0.1.4-0.20240411135514-e434723b7840 h1:1iw2/8sSZAyXioxG7y4bf7D59DVrFtfYuVu1UNoocps= +github.com/vocdoni/census3 v0.1.4-0.20240411135514-e434723b7840/go.mod h1:Ml5LlJQLbbd5GYttVCKu4kN2mjvrdLSxXfjoc/okNRY= github.com/vocdoni/storage-proofs-eth-go v0.1.6 h1:mF6VNGudgCjjgjxs5Z2GGMM2tS79+xFBWcr7BnPuhAY= github.com/vocdoni/storage-proofs-eth-go v0.1.6/go.mod h1:aoD9pKg8kDFQ5I6b3obGPTwjC+DsaW2h4wt2UQEjQrg= github.com/wangjia184/sortedset v0.0.0-20160527075905-f5d03557ba30/go.mod h1:YkocrP2K2tcw938x9gCOmT5G5eCD6jsTz0SZuyAqwIE= diff --git a/handler.go b/handler.go index 51e287cc..327c9d31 100644 --- a/handler.go +++ b/handler.go @@ -15,6 +15,7 @@ import ( "github.com/google/uuid" lru "github.com/hashicorp/golang-lru/v2" "github.com/vocdoni/vote-frame/airstack" + "github.com/vocdoni/vote-frame/census3" "github.com/vocdoni/vote-frame/farcasterapi" "github.com/vocdoni/vote-frame/imageframe" "github.com/vocdoni/vote-frame/mongo" @@ -38,6 +39,7 @@ type vocdoniHandler struct { electionLRU *lru.Cache[string, *api.Election] fcapi farcasterapi.API airstack *airstack.Airstack + census3 *census3.Client censusCreationMap sync.Map addAuthTokenFunc func(uint64, string) @@ -53,6 +55,7 @@ func NewVocdoniHandler( fcapi farcasterapi.API, token *uuid.UUID, airstack *airstack.Airstack, + census3 *census3.Client, ) (*vocdoniHandler, error) { // Get the vocdoni account if accountPrivKey == "" { @@ -89,6 +92,7 @@ func NewVocdoniHandler( db: db, fcapi: fcapi, airstack: airstack, + census3: census3, electionLRU: func() *lru.Cache[string, *api.Election] { lru, err := lru.New[string, *api.Election](100) if err != nil { diff --git a/main.go b/main.go index f3ff2091..16ddfdfd 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( flag "github.com/spf13/pflag" "github.com/spf13/viper" "github.com/vocdoni/vote-frame/airstack" + "github.com/vocdoni/vote-frame/census3" "github.com/vocdoni/vote-frame/discover" "github.com/vocdoni/vote-frame/farcasterapi" "github.com/vocdoni/vote-frame/farcasterapi/hub" @@ -81,6 +82,10 @@ func main() { flag.String("airstackSupportAPIEndpoint", "", "Airstack support API endpoint") flag.String("airstackTokenWhitelist", "", "Airstack token whitelist") + // Census3 flags + flag.String("census3APIEndpoint", "https://census3.vocdoni.net", "The Census3 API endpoint to use") + flag.String("census3APIToken", "", "The Census3 API token to use") + // Limited features flags flag.Int32("featureNotificationReputation", 15, "Reputation threshold to enable the notification feature") @@ -134,6 +139,10 @@ func main() { airstackSupportAPIEndpoint := viper.GetString("airstackSupportAPIEndpoint") airstackTokenWhitelist := viper.GetString("airstackTokenWhitelist") + // census3 vars + census3APIEndpoint := viper.GetString("census3APIEndpoint") + census3APIToken := viper.GetString("census3APIToken") + // limited features vars featureNotificationReputation := uint32(viper.GetInt32("featureNotificationReputation")) @@ -264,9 +273,29 @@ func main() { } } + // Create Census3 client + var c3 *census3.Client + if census3APIEndpoint != "" { + c3, err = census3.NewClient(census3APIEndpoint, census3APIToken) + if err != nil { + log.Fatal(err) + } + } + // Create the Vocdoni handler apiTokenUUID := uuid.MustParse(apiToken) - handler, err := NewVocdoniHandler(apiEndpoint, vocdoniPrivKey, censusInfo, webAppDir, db, mainCtx, neynarcli, &apiTokenUUID, as) + handler, err := NewVocdoniHandler( + apiEndpoint, + vocdoniPrivKey, + censusInfo, + webAppDir, + db, + mainCtx, + neynarcli, + &apiTokenUUID, + as, + c3, + ) if err != nil { log.Fatal(err) }