diff --git a/api/strategies.go b/api/strategies.go index 49894d9c..c972628d 100644 --- a/api/strategies.go +++ b/api/strategies.go @@ -10,6 +10,7 @@ import ( "strconv" "github.com/ethereum/go-ethereum/common" + gocid "github.com/ipfs/go-cid" queries "github.com/vocdoni/census3/db/sqlc" "github.com/vocdoni/census3/internal" "github.com/vocdoni/census3/lexer" @@ -290,12 +291,37 @@ func (capi *census3API) launchStrategyImport(msg *api.APIdata, ctx *httprouter.H if ipfsCID == "" { return ErrMalformedStrategy.With("no IPFS cID provided") } + // check if the cID is valid + if cid, err := gocid.Decode(ipfsCID); err != nil { + return ErrMalformedStrategy.WithErr(err) + } else { + ipfsCID = cid.String() + } // import the strategy from IPFS in background generating a queueID queueID := capi.queue.Enqueue() go func() { + internalCtx, cancel := context.WithTimeout(context.Background(), getStrategyTimeout) + defer cancel() + // check if the strategy exists in the database using the IPFS URI ipfsURI := fmt.Sprintf("%s%s", capi.downloader.RemoteStorage.URIprefix(), ipfsCID) + exists, err := capi.db.QueriesRO.ExistsStrategyByURI(internalCtx, ipfsURI) + if err != nil { + if ok := capi.queue.Update(queueID, true, nil, err); !ok { + log.Errorf("error updating import strategy queue %s", queueID) + } + return + } + if exists { + err := ErrCantImportStrategy.WithErr(fmt.Errorf("strategy already exists")) + if ok := capi.queue.Update(queueID, true, nil, err); !ok { + log.Errorf("error updating import strategy queue %s", queueID) + } + return + } + // download the strategy from IPFS and import it into the database if it + // does not exist capi.downloader.AddToQueue(ipfsURI, func(_ string, dump []byte) { - strategyID, err := capi.importStrategyDump(dump) + strategyID, err := capi.importStrategyDump(ipfsURI, dump) if err != nil { if ok := capi.queue.Update(queueID, true, nil, err); !ok { log.Errorf("error updating import strategy queue %s", queueID) @@ -316,15 +342,20 @@ func (capi *census3API) launchStrategyImport(msg *api.APIdata, ctx *httprouter.H return ctx.Send(res, api.HTTPstatusOK) } -func (capi *census3API) importStrategyDump(dump []byte) (uint64, error) { +func (capi *census3API) importStrategyDump(ipfsURI string, dump []byte) (uint64, error) { // init the internal context internalCtx, cancel := context.WithTimeout(context.Background(), importStrategyTimeout) defer cancel() - + // decode strategy importedStrategy := GetStrategyResponse{} if err := json.Unmarshal(dump, &importedStrategy); err != nil { return 0, ErrCantImportStrategy.WithErr(err) } + // check if the strategy includes any token + if len(importedStrategy.Tokens) == 0 { + return 0, ErrCantImportStrategy.With("the imported strategy does not include any token") + } + log.Debugw("importing strategy", "strategy", importedStrategy) // init db transaction tx, err := capi.db.RW.BeginTx(internalCtx, nil) if err != nil { @@ -336,11 +367,32 @@ func (capi *census3API) importStrategyDump(dump []byte) (uint64, error) { } }() qtx := capi.db.QueriesRW.WithTx(tx) + // ensure that all the tokens included in the strategy are registered in + // the database + for _, token := range importedStrategy.Tokens { + exists, err := qtx.ExistsTokenByChainIDAndExternalID(internalCtx, + queries.ExistsTokenByChainIDAndExternalIDParams{ + ID: common.HexToAddress(token.ID).Bytes(), + ChainID: token.ChainID, + ExternalID: token.ExternalID, + }) + if err != nil { + return 0, ErrCantGetToken.WithErr(err) + } + if !exists { + return 0, ErrNotFoundToken.Withf("the imported strategy includes a no registered token: %s", token.ID) + } + } + // check the strategy predicate + lx := lexer.NewLexer(strategyoperators.ValidOperatorsTags) + if _, err := lx.Parse(importedStrategy.Predicate); err != nil { + return 0, ErrInvalidStrategyPredicate.With("the imported strategy includes a invalid predicate") + } // create the strategy to get the ID and then create the strategy tokens result, err := qtx.CreateStategy(internalCtx, queries.CreateStategyParams{ Alias: importedStrategy.Alias, Predicate: importedStrategy.Predicate, - Uri: importedStrategy.URI, + Uri: ipfsURI, }) if err != nil { return 0, ErrCantCreateStrategy.WithErr(err) @@ -349,7 +401,7 @@ func (capi *census3API) importStrategyDump(dump []byte) (uint64, error) { if err != nil { return 0, ErrCantCreateStrategy.WithErr(err) } - // iterate over the token included in the predicate and create them in the + // iterate over the token included in the strategy and create them in the // database for symbol, token := range importedStrategy.Tokens { // decode the min balance for the current token if it is provided, @@ -362,7 +414,7 @@ func (capi *census3API) importStrategyDump(dump []byte) (uint64, error) { } // create the strategy token in the database if _, err := qtx.CreateStrategyToken(internalCtx, queries.CreateStrategyTokenParams{ - StrategyID: importedStrategy.ID, + StrategyID: uint64(strategyID), TokenID: common.HexToAddress(token.ID).Bytes(), MinBalance: minBalance.String(), ChainID: token.ChainID, diff --git a/db/queries/strategies.sql b/db/queries/strategies.sql index 0270dea5..5b239802 100644 --- a/db/queries/strategies.sql +++ b/db/queries/strategies.sql @@ -46,6 +46,9 @@ VALUES ( ?, ?, ?, ?, ? ); +-- name: ExistsStrategyByURI :one +SELECT EXISTS(SELECT 1 FROM strategies WHERE uri = ?); + -- name: StrategyTokensByStrategyID :many SELECT st.token_id as id, st.min_balance, t.symbol, t.chain_address, t.chain_id, t.external_id FROM strategy_tokens st diff --git a/db/sqlc/strategies.sql.go b/db/sqlc/strategies.sql.go index a1e91130..5d85c1d2 100644 --- a/db/sqlc/strategies.sql.go +++ b/db/sqlc/strategies.sql.go @@ -86,6 +86,17 @@ func (q *Queries) DeleteStrategyTokensByToken(ctx context.Context, arg DeleteStr return q.db.ExecContext(ctx, deleteStrategyTokensByToken, arg.TokenID, arg.ChainID, arg.ExternalID) } +const existsStrategyByURI = `-- name: ExistsStrategyByURI :one +SELECT EXISTS(SELECT 1 FROM strategies WHERE uri = ?) +` + +func (q *Queries) ExistsStrategyByURI(ctx context.Context, uri string) (bool, error) { + row := q.db.QueryRowContext(ctx, existsStrategyByURI, uri) + var exists bool + err := row.Scan(&exists) + return exists, err +} + const listStrategies = `-- name: ListStrategies :many SELECT id, predicate, alias, uri FROM strategies ORDER BY id diff --git a/go.mod b/go.mod index 4890f4c6..f6a57b8b 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/frankban/quicktest v1.14.6 github.com/google/uuid v1.4.0 github.com/hashicorp/golang-lru/v2 v2.0.7 + github.com/ipfs/go-cid v0.4.1 github.com/mattn/go-sqlite3 v1.14.18 github.com/pressly/goose/v3 v3.10.0 github.com/spf13/pflag v1.0.5 @@ -109,7 +110,6 @@ require ( github.com/ipfs/go-bitfield v1.1.0 // indirect github.com/ipfs/go-block-format v0.2.0 // indirect github.com/ipfs/go-blockservice v0.5.1 // indirect - github.com/ipfs/go-cid v0.4.1 // indirect github.com/ipfs/go-cidutil v0.1.0 // indirect github.com/ipfs/go-datastore v0.6.0 // indirect github.com/ipfs/go-ds-badger v0.3.0 // indirect