Skip to content

Commit

Permalink
initial implementation of basic strategies with support for 'AND' and…
Browse files Browse the repository at this point in the history
… 'OR' operators, based on SQL queries and set operations
  • Loading branch information
lucasmenendez committed Sep 21, 2023
1 parent 94fd2ff commit a6a9fd4
Show file tree
Hide file tree
Showing 7 changed files with 548 additions and 65 deletions.
15 changes: 15 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,21 @@ Returns the information of the census that are in the creation queue.
| 500 | `error getting census information` | 5009 |
| 500 | `error encoding census queue item` | 5022 |

- ⚠️ possible error values inside the body:

<small>The request could response `OK 200` and at the same time includes an error because it is an error of the enqueued process and not of the request processing).</small>

| HTTP Status | Message | Internal error |
|:---:|:---|:---:|
| 404 | `no token holders found` | 4004 |
| 404 | `no strategy found with the ID provided` | 4005 |
| 400 | `no tokens found for the strategy provided` | 4010 |
| 409 | `census already exists` | 4012 |
| 400 | `the predicate provided is not valid` | 4015 |
| 204 | `strategy has not registered holders` | 4017 |
| 500 | `error creating the census tree on the census database` | 5001 |
| 500 | `error evaluating strategy predicate` | 5026 |

### GET `/census/strategy/{strategyID}`
Returns a list of censusID for the strategy provided.

Expand Down
107 changes: 75 additions & 32 deletions api/censuses.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/vocdoni/census3/census"
queries "github.com/vocdoni/census3/db/sqlc"
"github.com/vocdoni/census3/lexer"
"go.vocdoni.io/dvote/httprouter"
api "go.vocdoni.io/dvote/httprouter/apirest"
"go.vocdoni.io/dvote/log"
Expand Down Expand Up @@ -124,67 +125,109 @@ func (capi *census3API) createAndPublishCensus(req *CreateCensusRequest, qID str
}()
qtx := capi.db.QueriesRW.WithTx(tx)

strategyTokens, err := qtx.TokensByStrategyID(internalCtx, req.StrategyID)
strategy, err := qtx.StrategyByID(internalCtx, req.StrategyID)
if err != nil {
if errors.Is(sql.ErrNoRows, err) {
return 0, ErrNoStrategyTokens.WithErr(err)
return 0, ErrNotFoundStrategy.WithErr(err)
}
return 0, ErrCantCreateCensus.WithErr(err)
}
if len(strategyTokens) == 0 {
return 0, ErrNoStrategyTokens.WithErr(err)
if strategy.Predicate == "" {
return 0, ErrInvalidStrategyPredicate.With("empty predicate")
}

// compute the new censusId and censusType
newCensusID := census.InnerCensusID(req.BlockNumber, req.StrategyID, req.Anonymous)
// check if the census already exists
_, err = qtx.CensusByID(internalCtx, newCensusID)
if err != nil {
if !errors.Is(sql.ErrNoRows, err) {
return 0, ErrCantCreateCensus.WithErr(err)
}
} else {
return 0, ErrCensusAlreadyExists.Withf("census %d already exists", newCensusID)
}

censusType := census.DefaultCensusType
if req.Anonymous {
censusType = census.AnonymousCensusType
}
// get holders associated to every strategy token, create a map to avoid
// duplicates and count the sum of the balances to get the weight of the
// census
// init some variables to get computed in the following steps
censusWeight := new(big.Int)
strategyHolders := map[common.Address]*big.Int{}
for _, token := range strategyTokens {
holders, err := qtx.TokenHoldersByTokenID(internalCtx, token.ID)
// parse the predicate
lx := lexer.NewLexer(ValidOperatorsTags)
validPredicate, err := lx.Parse(strategy.Predicate)
if err != nil {
return 0, ErrInvalidStrategyPredicate.WithErr(err)
}
// if the current predicate is a literal, just query about its holders. If
// it is a complex predicate, create a evaluator and evaluate the predicate
if validPredicate.IsLiteral() {
// get the strategy holders from the database
holders, err := qtx.TokenHoldersByStrategyID(internalCtx, req.StrategyID)
if err != nil {
if errors.Is(sql.ErrNoRows, err) {
continue
return 0, ErrNoStrategyHolders.WithErr(err)
}
return 0, ErrCantGetTokenHolders.WithErr(err)
return 0, ErrCantCreateCensus.WithErr(err)
}
if len(holders) == 0 {
return 0, ErrNoStrategyHolders
}
// parse holders addresses and balances
for _, holder := range holders {
holderAddr := common.BytesToAddress(holder.ID)
holderAddr := common.BytesToAddress(holder.HolderID)
holderBalance := new(big.Int).SetBytes(holder.Balance)
if _, exists := strategyHolders[holderAddr]; !exists {
strategyHolders[holderAddr] = holderBalance
censusWeight = new(big.Int).Add(censusWeight, holderBalance)
}
}
} else {
// get strategy tokens from the database
strategyTokens, err := qtx.TokensByStrategyID(internalCtx, req.StrategyID)
if err != nil {
if errors.Is(sql.ErrNoRows, err) {
return 0, ErrNoStrategyTokens.WithErr(err)
}
return 0, ErrCantCreateCensus.WithErr(err)
}
if len(strategyTokens) == 0 {
return 0, ErrNoStrategyTokens
}
// parse token information
tokensInfo := map[string]*StrategyToken{}
for _, token := range strategyTokens {
tokensInfo[token.Symbol.String] = &StrategyToken{
ID: common.BytesToAddress(token.ID).String(),
ChainID: token.ChainID,
MinBalance: new(big.Int).SetBytes(token.MinBalance).String(),
}
}
// init the operators and the predicate evaluator
operators := InitOperators(capi.db.QueriesRO, tokensInfo)
eval := lexer.NewEval[[]string](operators.Map())
// execute the evaluation of the predicate
res, err := eval.EvalToken(validPredicate)
if err != nil {
return 0, ErrEvalStrategyPredicate.WithErr(err)
}
// parse the evaluation results
for _, strAddress := range res {
strategyHolders[common.HexToAddress(strAddress)] = big.NewInt(1)
censusWeight = new(big.Int).Add(censusWeight, big.NewInt(1))
}
}
// if no holders found, return an error
if len(strategyHolders) == 0 {
log.Errorf("no holders for strategy '%d'", req.StrategyID)
return 0, ErrNotFoundTokenHolders.With("no holders for strategy")
}

// compute the new censusId and censusType
newCensusID := census.InnerCensusID(req.BlockNumber, req.StrategyID, req.Anonymous)
// check if the census already exists
_, err = qtx.CensusByID(internalCtx, newCensusID)
if err != nil {
if !errors.Is(sql.ErrNoRows, err) {
return 0, ErrCantCreateCensus.WithErr(err)
}
} else {
return 0, ErrCensusAlreadyExists.Withf("census %d already exists", newCensusID)
}
// check the censusType
censusType := census.DefaultCensusType
if req.Anonymous {
censusType = census.AnonymousCensusType
}
// create a census tree and publish on IPFS
def := census.NewCensusDefinition(newCensusID, req.StrategyID, strategyHolders, req.Anonymous)
newCensus, err := capi.censusDB.CreateAndPublish(def)
if err != nil {
return 0, ErrCantCreateCensus.WithErr(err)
}

// save the new census in the SQL database
sqlURI := &sql.NullString{}
if err := sqlURI.Scan(newCensus.URI); err != nil {
Expand Down
10 changes: 10 additions & 0 deletions api/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ var (
HTTPstatus: apirest.HTTPstatusBadRequest,
Err: fmt.Errorf("the predicate includes tokens that are not included in the request"),
}
ErrNoStrategyHolders = apirest.APIerror{
Code: 4017,
HTTPstatus: apirest.HTTPstatusNoContent,
Err: fmt.Errorf("strategy has not registered holders"),
}
ErrCantCreateToken = apirest.APIerror{
Code: 5000,
HTTPstatus: apirest.HTTPstatusInternalErr,
Expand Down Expand Up @@ -223,4 +228,9 @@ var (
HTTPstatus: apirest.HTTPstatusInternalErr,
Err: fmt.Errorf("error creating strategy"),
}
ErrEvalStrategyPredicate = apirest.APIerror{
Code: 5026,
HTTPstatus: apirest.HTTPstatusInternalErr,
Err: fmt.Errorf("error evaluating strategy predicate"),
}
)
Loading

0 comments on commit a6a9fd4

Please sign in to comment.