From f314498a264d9c183e96e4e94be22463109cf4be Mon Sep 17 00:00:00 2001 From: ToniRamirezM Date: Wed, 21 Oct 2020 12:24:58 +0200 Subject: [PATCH] Coordinators methods for API --- api/api.go | 2 +- api/api_test.go | 69 +++++++++++++++++++++++++++++++++++++++ api/dbtoapistructs.go | 43 +++++++++++++++++++++++- api/handlers.go | 44 +++++++++++++++++++++++++ api/parsers.go | 9 +++++ api/parsers_test.go | 27 +++++++++++++++ api/swagger.yml | 19 +++++++++-- db/historydb/historydb.go | 50 ++++++++++++++++++++++++++++ db/historydb/views.go | 13 ++++++++ db/migrations/0001.sql | 4 +-- 10 files changed, 273 insertions(+), 7 deletions(-) diff --git a/api/api.go b/api/api.go index 36ab971..d610dc5 100644 --- a/api/api.go +++ b/api/api.go @@ -71,7 +71,7 @@ func SetAPIEndpoints( server.GET("/tokens/:id", getToken) server.GET("/recommendedFee", getRecommendedFee) server.GET("/coordinators", getCoordinators) - server.GET("/coordinators/:forgerAddr", getCoordinator) + server.GET("/coordinators/:bidderAddr", getCoordinator) } return nil diff --git a/api/api_test.go b/api/api_test.go index 9c605af..80fc62d 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -41,6 +41,7 @@ type testCommon struct { blocks []common.Block tokens []tokenAPI batches []common.Batch + coordinators []coordinatorAPI usrAddr string usrBjj string accs []common.Account @@ -622,11 +623,26 @@ func TestMain(m *testing.M) { poolTxsToSend = append(poolTxsToSend, genSendTx) poolTxsToReceive = append(poolTxsToReceive, genReceiveTx) } + // Coordinators + const nCoords = 10 + coords := test.GenCoordinators(nCoords, blocks) + err = hdb.AddCoordinators(coords) + if err != nil { + panic(err) + } + fromItem := uint(0) + limit := uint(99999) + coordinators, _, err := hdb.GetCoordinators(&fromItem, &limit, historydb.OrderAsc) + apiCoordinators := coordinatorsToAPI(coordinators) + if err != nil { + panic(err) + } // Set testCommon tc = testCommon{ blocks: blocks, tokens: tokensUSD, batches: batches, + coordinators: apiCoordinators, usrAddr: ethAddrToHez(usrAddr), usrBjj: bjjToString(usrBjj), accs: accs, @@ -1250,6 +1266,59 @@ func assertPoolTx(t *testing.T, expected, actual sendPoolTx) { assert.Equal(t, expected, actual) } +func TestGetCoordinators(t *testing.T) { + endpoint := apiURL + "coordinators" + fetchedCoordinators := []coordinatorAPI{} + + appendIter := func(intr interface{}) { + for i := 0; i < len(intr.(*coordinatorsAPI).Coordinators); i++ { + tmp, err := copystructure.Copy(intr.(*coordinatorsAPI).Coordinators[i]) + if err != nil { + panic(err) + } + fetchedCoordinators = append(fetchedCoordinators, tmp.(coordinatorAPI)) + } + } + + limit := 5 + + path := fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit) + err := doGoodReqPaginated(path, historydb.OrderAsc, &coordinatorsAPI{}, appendIter) + assert.NoError(t, err) + assert.Equal(t, tc.coordinators, fetchedCoordinators) + + // Reverse Order + reversedCoordinators := []coordinatorAPI{} + appendIter = func(intr interface{}) { + for i := 0; i < len(intr.(*coordinatorsAPI).Coordinators); i++ { + tmp, err := copystructure.Copy(intr.(*coordinatorsAPI).Coordinators[i]) + if err != nil { + panic(err) + } + reversedCoordinators = append(reversedCoordinators, tmp.(coordinatorAPI)) + } + } + err = doGoodReqPaginated(path, historydb.OrderDesc, &coordinatorsAPI{}, appendIter) + assert.NoError(t, err) + for i := 0; i < len(fetchedCoordinators); i++ { + assert.Equal(t, reversedCoordinators[i], fetchedCoordinators[len(fetchedCoordinators)-1-i]) + } + + // Test GetCoordinator + path = fmt.Sprintf("%s/%s", endpoint, fetchedCoordinators[2].Forger.String()) + coordinator := coordinatorAPI{} + assert.NoError(t, doGoodReq("GET", path, nil, &coordinator)) + assert.Equal(t, fetchedCoordinators[2], coordinator) + // 400 + path = fmt.Sprintf("%s/0x001", endpoint) + err = doBadReq("GET", path, nil, 400) + assert.NoError(t, err) + // 404 + path = fmt.Sprintf("%s/0xaa942cfcd25ad4d90a62358b0dd84f33b398262a", endpoint) + err = doBadReq("GET", path, nil, 404) + assert.NoError(t, err) +} + func doGoodReqPaginated( path, order string, iterStruct db.Paginationer, diff --git a/api/dbtoapistructs.go b/api/dbtoapistructs.go index 0da96ba..8d6a089 100644 --- a/api/dbtoapistructs.go +++ b/api/dbtoapistructs.go @@ -211,7 +211,6 @@ func historyExitsToAPI(dbExits []historydb.HistoryExit) []exitAPI { } // Tokens - type tokensAPI struct { Tokens []tokenAPI `json:"tokens"` Pagination *db.Pagination `json:"pagination"` @@ -600,3 +599,45 @@ func poolL2TxReadToSend(dbTx *l2db.PoolL2TxRead) *sendPoolTx { } return tx } + +// Coordinators + +type coordinatorAPI struct { + ItemID int `json:"itemId"` + Bidder ethCommon.Address `json:"bidderAddr"` + Forger ethCommon.Address `json:"forgerAddr"` + EthBlockNum int64 `json:"ethereumBlock"` + URL string `json:"URL"` +} + +type coordinatorsAPI struct { + Coordinators []coordinatorAPI `json:"coordinators"` + Pagination *db.Pagination `json:"pagination"` +} + +func (t *coordinatorsAPI) GetPagination() *db.Pagination { + if t.Coordinators[0].ItemID < t.Coordinators[len(t.Coordinators)-1].ItemID { + t.Pagination.FirstReturnedItem = t.Coordinators[0].ItemID + t.Pagination.LastReturnedItem = t.Coordinators[len(t.Coordinators)-1].ItemID + } else { + t.Pagination.LastReturnedItem = t.Coordinators[0].ItemID + t.Pagination.FirstReturnedItem = t.Coordinators[len(t.Coordinators)-1].ItemID + } + return t.Pagination +} + +func (t *coordinatorsAPI) Len() int { return len(t.Coordinators) } + +func coordinatorsToAPI(dbCoordinators []historydb.HistoryCoordinator) []coordinatorAPI { + apiCoordinators := []coordinatorAPI{} + for i := 0; i < len(dbCoordinators); i++ { + apiCoordinators = append(apiCoordinators, coordinatorAPI{ + ItemID: dbCoordinators[i].ItemID, + Bidder: dbCoordinators[i].Bidder, + Forger: dbCoordinators[i].Forger, + EthBlockNum: dbCoordinators[i].EthBlockNum, + URL: dbCoordinators[i].URL, + }) + } + return apiCoordinators +} diff --git a/api/handlers.go b/api/handlers.go index 16134ba..e522623 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -2,6 +2,7 @@ package api import ( "database/sql" + "errors" "net/http" "github.com/gin-gonic/gin" @@ -23,6 +24,11 @@ const ( maxUint32 = 4294967295 ) +var ( + // ErrNillBidderAddr is used when a nil bidderAddr is received in the getCoordinator method + ErrNillBidderAddr = errors.New("biderAddr can not be nil") +) + func postAccountCreationAuth(c *gin.Context) { } @@ -285,11 +291,49 @@ func getRecommendedFee(c *gin.Context) { } func getCoordinators(c *gin.Context) { + // Pagination + fromItem, order, limit, err := parsePagination(c) + if err != nil { + retBadReq(err, c) + return + } + + // Fetch coordinators from historyDB + coordinators, pagination, err := h.GetCoordinators(fromItem, limit, order) + if err != nil { + retSQLErr(err, c) + return + } + // Build succesfull response + apiCoordinators := coordinatorsToAPI(coordinators) + c.JSON(http.StatusOK, &coordinatorsAPI{ + Coordinators: apiCoordinators, + Pagination: pagination, + }) } func getCoordinator(c *gin.Context) { + // Get bidderAddr + const name = "bidderAddr" + bidderAddr, err := parseEthAddr(c, name) + + if err != nil { + retBadReq(err, c) + return + } else if bidderAddr == nil { + retBadReq(ErrNillBidderAddr, c) + return + } + + coordinator, err := h.GetCoordinator(*bidderAddr) + if err != nil { + retSQLErr(err, c) + return + } + apiCoordinator := coordinatorsToAPI([]historydb.HistoryCoordinator{*coordinator}) + c.JSON(http.StatusOK, apiCoordinator[0]) } func retSQLErr(err error, c *gin.Context) { diff --git a/api/parsers.go b/api/parsers.go index 34caffb..821f815 100644 --- a/api/parsers.go +++ b/api/parsers.go @@ -310,3 +310,12 @@ func hezStringToBJJ(bjjStr, name string) (*babyjub.PublicKey, error) { } return bjj, nil } +func parseEthAddr(c paramer, name string) (*ethCommon.Address, error) { + addrStr := c.Param(name) + if addrStr == "" { + return nil, nil + } + var addr ethCommon.Address + err := addr.UnmarshalText([]byte(addrStr)) + return &addr, err +} diff --git a/api/parsers_test.go b/api/parsers_test.go index 8ca069a..edbb78b 100644 --- a/api/parsers_test.go +++ b/api/parsers_test.go @@ -23,6 +23,13 @@ func (qp *queryParser) Query(query string) string { return "" } +func (qp *queryParser) Param(param string) string { + if val, ok := qp.m[param]; ok { + return val + } + return "" +} + func TestParseQueryUint(t *testing.T) { name := "foo" c := &queryParser{} @@ -295,3 +302,23 @@ func TestParseTokenFilters(t *testing.T) { assert.Equal(t, symbolsArray, symbolsParse) assert.Equal(t, nameValue, nameParse) } +func TestParseEthAddr(t *testing.T) { + name := "forgerAddr" + c := &queryParser{} + c.m = make(map[string]string) + ethAddr := ethCommon.BigToAddress(big.NewInt(int64(123456))) + // Default + c.m[name] = "" + res, err := parseEthAddr(c, name) + assert.NoError(t, err) + assert.Nil(t, res) + // Incorrect + c.m[name] = "0x12345678" + _, err = parseEthAddr(c, name) + assert.Error(t, err) + // Correct + c.m[name] = ethAddr.String() + res, err = parseEthAddr(c, name) + assert.NoError(t, err) + assert.Equal(t, ethAddr, *res) +} diff --git a/api/swagger.yml b/api/swagger.yml index 78a396e..37b3bee 100644 --- a/api/swagger.yml +++ b/api/swagger.yml @@ -1162,7 +1162,7 @@ paths: application/json: schema: $ref: '#/components/schemas/Error500' - '/coordinators/{forgerAddr}': + '/coordinators/{bidderAddr}': get: tags: - Hermez status @@ -1170,7 +1170,7 @@ paths: description: Get the information of a coordinator. operationId: getCoordinator parameters: - - name: forgerAddr + - name: bidderAddr in: path description: Coordinator identifier required: true @@ -1807,9 +1807,11 @@ components: Coordinator: type: object properties: + itemId: + $ref: '#/components/schemas/ItemId' forgerAddr: $ref: '#/components/schemas/EthereumAddress' - withdrawAddr: + bidderAddr: $ref: '#/components/schemas/EthereumAddress' URL: $ref: '#/components/schemas/URL' @@ -1818,6 +1820,13 @@ components: - $ref: '#/components/schemas/EthBlockNum' - description: Ethereum block in which the coordinator registered into the network. - example: 5735943738 + additionalProperties: false + required: + - itemId + - forgerAddr + - bidderAddr + - URL + - ethereumBlock Coordinators: type: object properties: @@ -1828,6 +1837,10 @@ components: $ref: '#/components/schemas/Coordinator' pagination: $ref: '#/components/schemas/PaginationInfo' + additionalProperties: false + required: + - coordinators + - pagination Bid: type: object description: Tokens placed in an auction by a coordinator to gain the right to forge batches during a specific slot. diff --git a/db/historydb/historydb.go b/db/historydb/historydb.go index 43ed180..537dd7c 100644 --- a/db/historydb/historydb.go +++ b/db/historydb/historydb.go @@ -915,3 +915,53 @@ func (hdb *HistoryDB) AddBlockSCData(blockData *BlockData) (err error) { return txn.Commit() } + +// GetCoordinator returns a coordinator by its bidderAddr +func (hdb *HistoryDB) GetCoordinator(bidderAddr ethCommon.Address) (*HistoryCoordinator, error) { + coordinator := &HistoryCoordinator{} + err := meddler.QueryRow( + hdb.db, coordinator, `SELECT * FROM coordinator WHERE bidder_addr = $1;`, bidderAddr, + ) + return coordinator, err +} + +// GetCoordinators returns a list of coordinators from the DB and pagination info +func (hdb *HistoryDB) GetCoordinators(fromItem, limit *uint, order string) ([]HistoryCoordinator, *db.Pagination, error) { + var query string + var args []interface{} + queryStr := `SELECT coordinator.*, + COUNT(*) OVER() AS total_items, MIN(coordinator.item_id) OVER() AS first_item, MAX(coordinator.item_id) OVER() AS last_item + FROM coordinator ` + // Apply filters + if fromItem != nil { + queryStr += "WHERE " + if order == OrderAsc { + queryStr += "coordinator.item_id >= ? " + } else { + queryStr += "coordinator.item_id <= ? " + } + args = append(args, fromItem) + } + // pagination + queryStr += "ORDER BY coordinator.item_id " + if order == OrderAsc { + queryStr += " ASC " + } else { + queryStr += " DESC " + } + queryStr += fmt.Sprintf("LIMIT %d;", *limit) + query = hdb.db.Rebind(queryStr) + + coordinators := []*HistoryCoordinator{} + if err := meddler.QueryAll(hdb.db, &coordinators, query, args...); err != nil { + return nil, nil, err + } + if len(coordinators) == 0 { + return nil, nil, sql.ErrNoRows + } + return db.SlicePtrsToSlice(coordinators).([]HistoryCoordinator), &db.Pagination{ + TotalItems: coordinators[0].TotalItems, + FirstItem: coordinators[0].FirstItem, + LastItem: coordinators[0].LastItem, + }, nil +} diff --git a/db/historydb/views.go b/db/historydb/views.go index 814b31a..cdff84c 100644 --- a/db/historydb/views.go +++ b/db/historydb/views.go @@ -118,3 +118,16 @@ type HistoryExit struct { TokenUSD *float64 `meddler:"usd"` TokenUSDUpdate *time.Time `meddler:"usd_update"` } + +// HistoryCoordinator is a representation of a coordinator with additional information +// required by the API +type HistoryCoordinator struct { + ItemID int `meddler:"item_id"` + Bidder ethCommon.Address `meddler:"bidder_addr"` + Forger ethCommon.Address `meddler:"forger_addr"` + EthBlockNum int64 `meddler:"eth_block_num"` + URL string `meddler:"url"` + TotalItems int `meddler:"total_items"` + FirstItem int `meddler:"first_item"` + LastItem int `meddler:"last_item"` +} diff --git a/db/migrations/0001.sql b/db/migrations/0001.sql index 3bf215a..8083d64 100644 --- a/db/migrations/0001.sql +++ b/db/migrations/0001.sql @@ -8,11 +8,11 @@ CREATE TABLE block ( ); CREATE TABLE coordinator ( + item_id SERIAL PRIMARY KEY, bidder_addr BYTEA NOT NULL, forger_addr BYTEA NOT NULL, eth_block_num BIGINT NOT NULL REFERENCES block (eth_block_num) ON DELETE CASCADE, - url VARCHAR(200) NOT NULL, - PRIMARY KEY (bidder_addr, eth_block_num) + url VARCHAR(200) NOT NULL ); CREATE TABLE batch (