diff --git a/api/api.go b/api/api.go index 03e7fa6..8e7f793 100644 --- a/api/api.go +++ b/api/api.go @@ -62,6 +62,7 @@ func SetAPIEndpoints( server.GET("/batches/:batchNum", getBatch) server.GET("/full-batches/:batchNum", getFullBatch) server.GET("/slots", getSlots) + server.GET("/slots/:slotNum", getSlot) server.GET("/bids", getBids) server.GET("/next-forgers", getNextForgers) server.GET("/state", getState) diff --git a/api/api_test.go b/api/api_test.go index 8f6ead7..e9ea583 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -49,6 +49,8 @@ type testCommon struct { auths []testAuth router *swagger.Router bids []testBid + slots []testSlot + auctionVars common.AuctionVariables } var tc testCommon @@ -122,6 +124,8 @@ func TestMain(m *testing.M) { if err != nil { panic(err) } + lastBlockNum := blocks[nBlocks-1].EthBlockNum + // Gen tokens and add them to DB const nTokens = 10 tokens, ethToken := test.GenTokens(nTokens, blocks) @@ -258,12 +262,26 @@ func TestMain(m *testing.M) { } // Bids - const nBids = 10 + const nBids = 20 bids := test.GenBids(nBids, blocks, coords) err = hdb.AddBids(bids) if err != nil { panic(err) } + testBids := genTestBids(blocks, coordinators, bids) + + // Vars + auctionVars := common.AuctionVariables{ + BootCoordinator: ethCommon.HexToAddress("0x1111111111111111111111111111111111111111"), + ClosedAuctionSlots: uint16(2), + OpenAuctionSlots: uint16(5), + } + err = hdb.AddAuctionVars(&auctionVars) + if err != nil { + panic(err) + } + + const nSlots = 20 // Set testCommon usrTxs, allTxs := genTestTxs(sortedTxs, usrIdxs, accs, tokensUSD, blocks) @@ -287,7 +305,9 @@ func TestMain(m *testing.M) { poolTxsToReceive: poolTxsToReceive, auths: genTestAuths(test.GenAuths(5)), router: router, - bids: genTestBids(blocks, coordinators, bids), + bids: testBids, + slots: genTestSlots(nSlots, lastBlockNum, testBids, auctionVars), + auctionVars: auctionVars, } // Fake server @@ -317,15 +337,18 @@ func doGoodReqPaginated( iterStruct db.Paginationer, appendIter func(res interface{}), ) error { - next := 0 + next := -1 for { // Call API to get this iteration items iterPath := path - if next == 0 && order == historydb.OrderDesc { + if next == -1 && order == historydb.OrderDesc { // Fetch first item in reverse order iterPath += "99999" } else { // Fetch from next item or 0 if it's ascending order + if next == -1 { + next = 0 + } iterPath += strconv.Itoa(next) } if err := doGoodReq("GET", iterPath+"&order="+order, nil, iterStruct); err != nil { diff --git a/api/bids_test.go b/api/bids_test.go index 2517fbe..3c18f59 100644 --- a/api/bids_test.go +++ b/api/bids_test.go @@ -108,7 +108,7 @@ func TestGetBids(t *testing.T) { // slotNum, in reverse order fetchedBids = []testBid{} path = fmt.Sprintf("%s?slotNum=%d&limit=%d&fromItem=", endpoint, slotNum, limit) - err = doGoodReqPaginated(path, historydb.OrderAsc, &testBidsResponse{}, appendIter) + err = doGoodReqPaginated(path, historydb.OrderDesc, &testBidsResponse{}, appendIter) assert.NoError(t, err) flippedBids := []testBid{} for i := len(slotNumBids) - 1; i >= 0; i-- { diff --git a/api/handlers.go b/api/handlers.go index f107024..d3edc05 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -21,6 +21,9 @@ const ( // 2^32 -1 maxUint32 = 4294967295 + + // 2^64 /2 -1 + maxInt64 = 9223372036854775807 ) var ( @@ -36,10 +39,6 @@ func getAccount(c *gin.Context) { } -func getSlots(c *gin.Context) { - -} - func getNextForgers(c *gin.Context) { } diff --git a/api/parsers.go b/api/parsers.go index 6e63f9f..36e34f2 100644 --- a/api/parsers.go +++ b/api/parsers.go @@ -54,6 +54,11 @@ func parseQueryUint(name string, dflt *uint, min, max uint, c querier) (*uint, e return stringToUint(str, name, dflt, min, max) } +func parseQueryInt64(name string, dflt *int64, min, max int64, c querier) (*int64, error) { //nolint:SA4009 res may be not overwriten + str := c.Query(name) + return stringToInt64(str, name, dflt, min, max) +} + func parseQueryBool(name string, dflt *bool, c querier) (*bool, error) { //nolint:SA4009 res may be not overwriten str := c.Query(name) if str == "" { @@ -69,7 +74,7 @@ func parseQueryBool(name string, dflt *bool, c querier) (*bool, error) { //nolin *res = false return res, nil } - return nil, fmt.Errorf("Inavlid %s. Must be eithe true or false", name) + return nil, fmt.Errorf("Invalid %s. Must be eithe true or false", name) } func parseQueryHezEthAddr(c querier) (*ethCommon.Address, error) { @@ -198,8 +203,8 @@ func parseTokenFilters(c querier) ([]common.TokenID, []string, string, error) { return tokensIDs, symbols, nameStr, nil } -func parseBidFilters(c querier) (*uint, *ethCommon.Address, error) { - slotNum, err := parseQueryUint("slotNum", nil, 0, maxUint32, c) +func parseBidFilters(c querier) (*int64, *ethCommon.Address, error) { + slotNum, err := parseQueryInt64("slotNum", nil, 0, maxInt64, c) if err != nil { return nil, nil, err } @@ -210,6 +215,26 @@ func parseBidFilters(c querier) (*uint, *ethCommon.Address, error) { return slotNum, bidderAddr, nil } +func parseSlotFilters(c querier) (*int64, *int64, *ethCommon.Address, *bool, error) { + minSlotNum, err := parseQueryInt64("minSlotNum", nil, 0, maxInt64, c) + if err != nil { + return nil, nil, nil, nil, err + } + maxSlotNum, err := parseQueryInt64("maxSlotNum", nil, 0, maxInt64, c) + if err != nil { + return nil, nil, nil, nil, err + } + wonByEthereumAddress, err := parseQueryEthAddr("wonByEthereumAddress", c) + if err != nil { + return nil, nil, nil, nil, err + } + finishedAuction, err := parseQueryBool("finishedAuction", nil, c) + if err != nil { + return nil, nil, nil, nil, err + } + return minSlotNum, maxSlotNum, wonByEthereumAddress, finishedAuction, nil +} + // Param parsers type paramer interface { @@ -240,6 +265,11 @@ func parseParamUint(name string, dflt *uint, min, max uint, c paramer) (*uint, e return stringToUint(str, name, dflt, min, max) } +func parseParamInt64(name string, dflt *int64, min, max int64, c paramer) (*int64, error) { //nolint:SA4009 res may be not overwriten + str := c.Param(name) + return stringToInt64(str, name, dflt, min, max) +} + func stringToIdx(idxStr, name string) (*common.Idx, error) { if idxStr == "" { return nil, nil @@ -261,7 +291,7 @@ func stringToUint(uintStr, name string, dflt *uint, min, max uint) (*uint, error resInt, err := strconv.Atoi(uintStr) if err != nil || resInt < 0 || resInt < int(min) || resInt > int(max) { return nil, fmt.Errorf( - "Inavlid %s. Must be an integer within the range [%d, %d]", + "Invalid %s. Must be an integer within the range [%d, %d]", name, min, max) } res := uint(resInt) @@ -270,6 +300,20 @@ func stringToUint(uintStr, name string, dflt *uint, min, max uint) (*uint, error return dflt, nil } +func stringToInt64(uintStr, name string, dflt *int64, min, max int64) (*int64, error) { + if uintStr != "" { + resInt, err := strconv.Atoi(uintStr) + if err != nil || resInt < 0 || resInt < int(min) || resInt > int(max) { + return nil, fmt.Errorf( + "Invalid %s. Must be an integer within the range [%d, %d]", + name, min, max) + } + res := int64(resInt) + return &res, nil + } + return dflt, nil +} + func hezStringToEthAddr(addrStr, name string) (*ethCommon.Address, error) { if addrStr == "" { return nil, nil diff --git a/api/slots.go b/api/slots.go new file mode 100644 index 0000000..66dda9c --- /dev/null +++ b/api/slots.go @@ -0,0 +1,346 @@ +package api + +import ( + "database/sql" + "errors" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/hermeznetwork/hermez-node/common" + "github.com/hermeznetwork/hermez-node/db" + "github.com/hermeznetwork/hermez-node/db/historydb" +) + +// SlotAPI is a repesentation of a slot information +type SlotAPI struct { + ItemID int `json:"itemId"` + SlotNum int64 `json:"slotNum"` + FirstBlock int64 `json:"firstBlock"` + LastBlock int64 `json:"lastBlock"` + OpenAuction bool `json:"openAuction"` + WinnerBid *historydb.BidAPI `json:"winnerBid"` + TotalItems int `json:"-"` + FirstItem int `json:"-"` + LastItem int `json:"-"` +} + +func getFirstLastBlock(slotNum int64) (int64, int64) { + genesisBlock := cg.AuctionConstants.GenesisBlockNum + blocksPerSlot := int64(cg.AuctionConstants.BlocksPerSlot) + firstBlock := slotNum*blocksPerSlot + genesisBlock + lastBlock := (slotNum+1)*blocksPerSlot + genesisBlock - 1 + return firstBlock, lastBlock +} + +func getCurrentSlot(currentBlock int64) int64 { + genesisBlock := cg.AuctionConstants.GenesisBlockNum + blocksPerSlot := int64(cg.AuctionConstants.BlocksPerSlot) + currentSlot := (currentBlock - genesisBlock) / blocksPerSlot + return currentSlot +} + +func isOpenAuction(currentBlock, slotNum int64, auctionVars common.AuctionVariables) bool { + currentSlot := getCurrentSlot(currentBlock) + closedAuctionSlots := currentSlot + int64(auctionVars.ClosedAuctionSlots) + openAuctionSlots := int64(auctionVars.OpenAuctionSlots) + if slotNum > closedAuctionSlots && slotNum <= (closedAuctionSlots+openAuctionSlots) { + return true + } + return false +} + +func getPagination(totalItems int, minSlotNum, maxSlotNum *int64) *db.Pagination { + // itemID is slotNum + firstItem := *minSlotNum + lastItem := *maxSlotNum + pagination := &db.Pagination{ + TotalItems: int(totalItems), + FirstItem: int(firstItem), + LastItem: int(lastItem), + } + return pagination +} + +func newSlotAPI(slotNum, currentBlockNum int64, bid *historydb.BidAPI, auctionVars *common.AuctionVariables) SlotAPI { + firstBlock, lastBlock := getFirstLastBlock(slotNum) + openAuction := isOpenAuction(currentBlockNum, slotNum, *auctionVars) + slot := SlotAPI{ + ItemID: int(slotNum), + SlotNum: slotNum, + FirstBlock: firstBlock, + LastBlock: lastBlock, + OpenAuction: openAuction, + WinnerBid: bid, + } + return slot +} + +func newSlotsAPIFromWinnerBids(fromItem *uint, order string, bids []historydb.BidAPI, currentBlockNum int64, auctionVars *common.AuctionVariables) (slots []SlotAPI) { + for i := range bids { + slotNum := bids[i].SlotNum + slot := newSlotAPI(slotNum, currentBlockNum, &bids[i], auctionVars) + if order == historydb.OrderAsc { + if slot.ItemID >= int(*fromItem) { + slots = append(slots, slot) + } + } else { + if slot.ItemID <= int(*fromItem) { + slots = append(slots, slot) + } + } + } + return slots +} + +func addEmptySlot(slots []SlotAPI, slotNum int64, currentBlockNum int64, auctionVars *common.AuctionVariables, fromItem *uint, order string) ([]SlotAPI, error) { + emptySlot := newSlotAPI(slotNum, currentBlockNum, nil, auctionVars) + if order == historydb.OrderAsc { + if emptySlot.ItemID >= int(*fromItem) { + slots = append(slots, emptySlot) + } + } else { + if emptySlot.ItemID <= int(*fromItem) { + slots = append([]SlotAPI{emptySlot}, slots...) + } + } + return slots, nil +} + +func getSlot(c *gin.Context) { + slotNumUint, err := parseParamUint("slotNum", nil, 0, maxUint32, c) + if err != nil { + retBadReq(err, c) + return + } + currentBlock, err := h.GetLastBlock() + if err != nil { + retBadReq(err, c) + return + } + auctionVars, err := h.GetAuctionVars() + if err != nil { + retBadReq(err, c) + return + } + + slotNum := int64(*slotNumUint) + bid, err := h.GetBestBidAPI(&slotNum) + if err != nil && err != sql.ErrNoRows { + retSQLErr(err, c) + return + } + + var slot SlotAPI + if err == sql.ErrNoRows { + slot = newSlotAPI(slotNum, currentBlock.EthBlockNum, nil, auctionVars) + } else { + slot = newSlotAPI(bid.SlotNum, currentBlock.EthBlockNum, &bid, auctionVars) + } + + // JSON response + c.JSON(http.StatusOK, slot) +} + +func getLimits(minSlotNum, maxSlotNum *int64, fromItem, limit *uint, order string) (int64, int64) { + var minLim, maxLim int64 + if fromItem != nil { + if order == historydb.OrderAsc { + if int64(*fromItem) > *minSlotNum { + minLim = int64(*fromItem) + } else { + minLim = *minSlotNum + } + if (minLim + int64(*limit-1)) < *maxSlotNum { + maxLim = minLim + int64(*limit-1) + } else { + maxLim = *maxSlotNum + } + } else { + if int64(*fromItem) < *maxSlotNum { + maxLim = int64(*fromItem) + } else { + maxLim = *maxSlotNum + } + if (maxLim - int64(*limit-1)) < *minSlotNum { + minLim = *minSlotNum + } else { + minLim = maxLim - int64(*limit-1) + } + } + } + return minLim, maxLim +} + +func getLimitsWithAddr(minSlotNum, maxSlotNum *int64, fromItem, limit *uint, order string) (int64, int64) { + var minLim, maxLim int64 + if fromItem != nil { + if order == historydb.OrderAsc { + maxLim = *maxSlotNum + if int64(*fromItem) > *minSlotNum { + minLim = int64(*fromItem) + } else { + minLim = *minSlotNum + } + } else { + minLim = *minSlotNum + if int64(*fromItem) < *maxSlotNum { + maxLim = int64(*fromItem) + } else { + maxLim = *maxSlotNum + } + } + } + return minLim, maxLim +} + +func getSlots(c *gin.Context) { + var slots []SlotAPI + minSlotNumDflt := int64(0) + + // Get filters + minSlotNum, maxSlotNum, wonByEthereumAddress, finishedAuction, err := parseSlotFilters(c) + if err != nil { + retBadReq(err, c) + return + } + + // Pagination + fromItem, order, limit, err := parsePagination(c) + if err != nil { + retBadReq(err, c) + return + } + + currentBlock, err := h.GetLastBlock() + if err != nil { + retBadReq(err, c) + return + } + auctionVars, err := h.GetAuctionVars() + if err != nil { + retBadReq(err, c) + return + } + + // Check filters + if maxSlotNum == nil && finishedAuction == nil { + retBadReq(errors.New("It is necessary to add maxSlotNum filter"), c) + return + } else if finishedAuction != nil { + if maxSlotNum == nil && !*finishedAuction { + retBadReq(errors.New("It is necessary to add maxSlotNum filter"), c) + return + } else if *finishedAuction { + currentBlock, err := h.GetLastBlock() + if err != nil { + retBadReq(err, c) + return + } + currentSlot := getCurrentSlot(currentBlock.EthBlockNum) + auctionVars, err := h.GetAuctionVars() + if err != nil { + retBadReq(err, c) + return + } + closedAuctionSlots := currentSlot + int64(auctionVars.ClosedAuctionSlots) + if maxSlotNum == nil { + maxSlotNum = &closedAuctionSlots + } else if closedAuctionSlots < *maxSlotNum { + maxSlotNum = &closedAuctionSlots + } + } + } else if maxSlotNum != nil && minSlotNum != nil { + if *minSlotNum > *maxSlotNum { + retBadReq(errors.New("It is necessary to add valid filter (minSlotNum <= maxSlotNum)"), c) + return + } + } + if minSlotNum == nil { + minSlotNum = &minSlotNumDflt + } + + // Get bids and pagination according to filters + var slotMinLim, slotMaxLim int64 + var bids []historydb.BidAPI + var pag *db.Pagination + totalItems := 0 + if wonByEthereumAddress == nil { + slotMinLim, slotMaxLim = getLimits(minSlotNum, maxSlotNum, fromItem, limit, order) + // Get best bids in range maxSlotNum - minSlotNum + bids, _, err = h.GetBestBidsAPI(&slotMinLim, &slotMaxLim, wonByEthereumAddress, nil, order) + if err != nil && err != sql.ErrNoRows { + retSQLErr(err, c) + return + } + totalItems = int(*maxSlotNum) - int(*minSlotNum) + 1 + } else { + slotMinLim, slotMaxLim = getLimitsWithAddr(minSlotNum, maxSlotNum, fromItem, limit, order) + bids, pag, err = h.GetBestBidsAPI(&slotMinLim, &slotMaxLim, wonByEthereumAddress, limit, order) + if err != nil && err != sql.ErrNoRows { + retSQLErr(err, c) + return + } + if len(bids) > 0 { + totalItems = pag.TotalItems + *maxSlotNum = int64(pag.LastItem) + *minSlotNum = int64(pag.FirstItem) + } + } + + // Build the slot information with previous bids + var slotsBids []SlotAPI + if len(bids) > 0 { + slotsBids = newSlotsAPIFromWinnerBids(fromItem, order, bids, currentBlock.EthBlockNum, auctionVars) + if err != nil { + retBadReq(err, c) + return + } + } + + // Build the other slots + if wonByEthereumAddress == nil { + // Build hte information of the slots with bids or not + for i := slotMinLim; i <= slotMaxLim; i++ { + found := false + for j := range slotsBids { + if slotsBids[j].SlotNum == i { + found = true + if order == historydb.OrderAsc { + if slotsBids[j].ItemID >= int(*fromItem) { + slots = append(slots, slotsBids[j]) + } + } else { + if slotsBids[j].ItemID <= int(*fromItem) { + slots = append([]SlotAPI{slotsBids[j]}, slots...) + } + } + break + } + } + if !found { + slots, err = addEmptySlot(slots, i, currentBlock.EthBlockNum, auctionVars, fromItem, order) + if err != nil { + retBadReq(err, c) + return + } + } + } + } else if len(slotsBids) > 0 { + slots = slotsBids + } + + if len(slots) == 0 { + retSQLErr(sql.ErrNoRows, c) + return + } + + // Build succesfull response + type slotsResponse struct { + Slots []SlotAPI `json:"slots"` + Pagination *db.Pagination `json:"pagination"` + } + c.JSON(http.StatusOK, &slotsResponse{ + Slots: slots, + Pagination: getPagination(totalItems, minSlotNum, maxSlotNum), + }) +} diff --git a/api/slots_test.go b/api/slots_test.go new file mode 100644 index 0000000..0eb52d7 --- /dev/null +++ b/api/slots_test.go @@ -0,0 +1,305 @@ +package api + +import ( + "fmt" + "strconv" + "testing" + + "github.com/hermeznetwork/hermez-node/common" + "github.com/hermeznetwork/hermez-node/db" + "github.com/hermeznetwork/hermez-node/db/historydb" + "github.com/mitchellh/copystructure" + "github.com/stretchr/testify/assert" +) + +type testSlot struct { + ItemID int `json:"itemId"` + SlotNum int64 `json:"slotNum"` + FirstBlock int64 `json:"firstBlock"` + LastBlock int64 `json:"lastBlock"` + OpenAuction bool `json:"openAuction"` + WinnerBid *testBid `json:"winnerBid"` +} + +type testSlotsResponse struct { + Slots []testSlot `json:"slots"` + Pagination *db.Pagination `json:"pagination"` +} + +func (t testSlotsResponse) GetPagination() *db.Pagination { + if t.Slots[0].ItemID < t.Slots[len(t.Slots)-1].ItemID { + t.Pagination.FirstReturnedItem = int(t.Slots[0].ItemID) + t.Pagination.LastReturnedItem = int(t.Slots[len(t.Slots)-1].ItemID) + } else { + t.Pagination.LastReturnedItem = int(t.Slots[0].ItemID) + t.Pagination.FirstReturnedItem = int(t.Slots[len(t.Slots)-1].ItemID) + } + return t.Pagination +} + +func (t testSlotsResponse) Len() int { + return len(t.Slots) +} + +func genTestSlots(nSlots int, lastBlockNum int64, bids []testBid, auctionVars common.AuctionVariables) []testSlot { + tSlots := []testSlot{} + bestBids := make(map[int64]testBid) + // It's assumed that bids for each slot will be received in increasing order + for i := range bids { + bestBids[bids[i].SlotNum] = bids[i] + } + + for i := int64(0); i < int64(nSlots); i++ { + bid, ok := bestBids[i] + firstBlock, lastBlock := getFirstLastBlock(int64(i)) + tSlot := testSlot{ + SlotNum: int64(i), + FirstBlock: firstBlock, + LastBlock: lastBlock, + OpenAuction: isOpenAuction(lastBlockNum, int64(i), auctionVars), + } + if ok { + tSlot.WinnerBid = &bid + } + tSlots = append(tSlots, tSlot) + } + return tSlots +} + +func getEmptyTestSlot(slotNum int64) testSlot { + firstBlock, lastBlock := getFirstLastBlock(slotNum) + slot := testSlot{ + SlotNum: slotNum, + FirstBlock: firstBlock, + LastBlock: lastBlock, + OpenAuction: false, + WinnerBid: nil, + } + return slot +} + +func TestGetSlot(t *testing.T) { + endpoint := apiURL + "slots/" + for _, slot := range tc.slots { + fetchedSlot := testSlot{} + assert.NoError( + t, doGoodReq( + "GET", + endpoint+strconv.Itoa(int(slot.SlotNum)), + nil, &fetchedSlot, + ), + ) + assertSlot(t, slot, fetchedSlot) + } + + // Slot with WinnerBid == nil + slotNum := int64(15) + fetchedSlot := testSlot{} + assert.NoError( + t, doGoodReq( + "GET", + endpoint+strconv.Itoa(int(slotNum)), + nil, &fetchedSlot, + ), + ) + emptySlot := getEmptyTestSlot(slotNum) + assertSlot(t, emptySlot, fetchedSlot) + + // Invalid slotNum + path := endpoint + strconv.Itoa(-2) + err := doBadReq("GET", path, nil, 400) + assert.NoError(t, err) +} + +func TestGetSlots(t *testing.T) { + endpoint := apiURL + "slots" + fetchedSlots := []testSlot{} + appendIter := func(intr interface{}) { + for i := 0; i < len(intr.(*testSlotsResponse).Slots); i++ { + tmp, err := copystructure.Copy(intr.(*testSlotsResponse).Slots[i]) + if err != nil { + panic(err) + } + fetchedSlots = append(fetchedSlots, tmp.(testSlot)) + } + } + // All slots with maxSlotNum filter + maxSlotNum := tc.slots[len(tc.slots)-1].SlotNum + 5 + limit := 1 + path := fmt.Sprintf("%s?maxSlotNum=%d&limit=%d&fromItem=", endpoint, maxSlotNum, limit) + err := doGoodReqPaginated(path, historydb.OrderAsc, &testSlotsResponse{}, appendIter) + assert.NoError(t, err) + allSlots := tc.slots + for i := tc.slots[len(tc.slots)-1].SlotNum; i < maxSlotNum; i++ { + emptySlot := getEmptyTestSlot(i + 1) + allSlots = append(allSlots, emptySlot) + } + assertSlots(t, allSlots, fetchedSlots) + + // All slots with maxSlotNum filter, in reverse order + fetchedSlots = []testSlot{} + limit = 3 + path = fmt.Sprintf("%s?maxSlotNum=%d&limit=%d&fromItem=", endpoint, maxSlotNum, limit) + err = doGoodReqPaginated(path, historydb.OrderDesc, &testSlotsResponse{}, appendIter) + assert.NoError(t, err) + + flippedAllSlots := []testSlot{} + for i := len(allSlots) - 1; i >= 0; i-- { + flippedAllSlots = append(flippedAllSlots, allSlots[i]) + } + assertSlots(t, flippedAllSlots, fetchedSlots) + + // maxSlotNum & wonByEthereumAddress + fetchedSlots = []testSlot{} + limit = 1 + bidderAddr := tc.coordinators[2].Bidder + path = fmt.Sprintf("%s?maxSlotNum=%d&wonByEthereumAddress=%s&limit=%d&fromItem=", endpoint, maxSlotNum, bidderAddr.String(), limit) + err = doGoodReqPaginated(path, historydb.OrderAsc, &testSlotsResponse{}, appendIter) + assert.NoError(t, err) + bidderAddressSlots := []testSlot{} + for i := 0; i < len(tc.slots); i++ { + if tc.slots[i].WinnerBid != nil { + if tc.slots[i].WinnerBid.Bidder == bidderAddr { + bidderAddressSlots = append(bidderAddressSlots, tc.slots[i]) + } + } + } + assertSlots(t, bidderAddressSlots, fetchedSlots) + + // maxSlotNum & wonByEthereumAddress, in reverse order + fetchedSlots = []testSlot{} + limit = 1 + path = fmt.Sprintf("%s?maxSlotNum=%d&wonByEthereumAddress=%s&limit=%d&fromItem=", endpoint, maxSlotNum, bidderAddr.String(), limit) + err = doGoodReqPaginated(path, historydb.OrderDesc, &testSlotsResponse{}, appendIter) + assert.NoError(t, err) + flippedBidderAddressSlots := []testSlot{} + for i := len(bidderAddressSlots) - 1; i >= 0; i-- { + flippedBidderAddressSlots = append(flippedBidderAddressSlots, bidderAddressSlots[i]) + } + assertSlots(t, flippedBidderAddressSlots, fetchedSlots) + + // finishedAuction + fetchedSlots = []testSlot{} + limit = 15 + path = fmt.Sprintf("%s?finishedAuction=%t&limit=%d&fromItem=", endpoint, true, limit) + err = doGoodReqPaginated(path, historydb.OrderAsc, &testSlotsResponse{}, appendIter) + assert.NoError(t, err) + + currentSlot := getCurrentSlot(tc.blocks[len(tc.blocks)-1].EthBlockNum) + finishedAuctionSlots := []testSlot{} + for i := 0; i < len(tc.slots); i++ { + finishAuction := currentSlot + int64(tc.auctionVars.ClosedAuctionSlots) + if tc.slots[i].SlotNum <= finishAuction { + finishedAuctionSlots = append(finishedAuctionSlots, tc.slots[i]) + } else { + break + } + } + assertSlots(t, finishedAuctionSlots, fetchedSlots) + + //minSlot + maxSlot + limit = 10 + minSlotNum := tc.slots[3].SlotNum + maxSlotNum = tc.slots[len(tc.slots)-1].SlotNum - 1 + fetchedSlots = []testSlot{} + path = fmt.Sprintf("%s?maxSlotNum=%d&minSlotNum=%d&limit=%d&fromItem=", endpoint, maxSlotNum, minSlotNum, limit) + err = doGoodReqPaginated(path, historydb.OrderAsc, &testSlotsResponse{}, appendIter) + assert.NoError(t, err) + minMaxBatchNumSlots := []testSlot{} + for i := 0; i < len(tc.slots); i++ { + if tc.slots[i].SlotNum >= minSlotNum && tc.slots[i].SlotNum <= maxSlotNum { + minMaxBatchNumSlots = append(minMaxBatchNumSlots, tc.slots[i]) + } + } + assertSlots(t, minMaxBatchNumSlots, fetchedSlots) + + //minSlot + maxSlot + limit = 15 + minSlotNum = tc.slots[0].SlotNum + maxSlotNum = tc.slots[0].SlotNum + fetchedSlots = []testSlot{} + path = fmt.Sprintf("%s?maxSlotNum=%d&minSlotNum=%d&limit=%d&fromItem=", endpoint, maxSlotNum, minSlotNum, limit) + err = doGoodReqPaginated(path, historydb.OrderAsc, &testSlotsResponse{}, appendIter) + assert.NoError(t, err) + minMaxBatchNumSlots = []testSlot{} + for i := 0; i < len(tc.slots); i++ { + if tc.slots[i].SlotNum >= minSlotNum && tc.slots[i].SlotNum <= maxSlotNum { + minMaxBatchNumSlots = append(minMaxBatchNumSlots, tc.slots[i]) + } + } + assertSlots(t, minMaxBatchNumSlots, fetchedSlots) + + // Only empty Slots + limit = 2 + minSlotNum = tc.slots[len(tc.slots)-1].SlotNum + 1 + maxSlotNum = tc.slots[len(tc.slots)-1].SlotNum + 5 + fetchedSlots = []testSlot{} + path = fmt.Sprintf("%s?maxSlotNum=%d&minSlotNum=%d&limit=%d&fromItem=", endpoint, maxSlotNum, minSlotNum, limit) + err = doGoodReqPaginated(path, historydb.OrderAsc, &testSlotsResponse{}, appendIter) + assert.NoError(t, err) + emptySlots := []testSlot{} + for i := 0; i < len(allSlots); i++ { + if allSlots[i].SlotNum >= minSlotNum && allSlots[i].SlotNum <= maxSlotNum { + emptySlots = append(emptySlots, allSlots[i]) + } + } + assertSlots(t, emptySlots, fetchedSlots) + + // Only empty Slots, in reverse order + limit = 4 + minSlotNum = tc.slots[len(tc.slots)-1].SlotNum + 1 + maxSlotNum = tc.slots[len(tc.slots)-1].SlotNum + 5 + fetchedSlots = []testSlot{} + path = fmt.Sprintf("%s?maxSlotNum=%d&minSlotNum=%d&limit=%d&fromItem=", endpoint, maxSlotNum, minSlotNum, limit) + err = doGoodReqPaginated(path, historydb.OrderDesc, &testSlotsResponse{}, appendIter) + assert.NoError(t, err) + flippedEmptySlots := []testSlot{} + for i := 0; i < len(flippedAllSlots); i++ { + if flippedAllSlots[i].SlotNum >= minSlotNum && flippedAllSlots[i].SlotNum <= maxSlotNum { + flippedEmptySlots = append(flippedEmptySlots, flippedAllSlots[i]) + } + } + assertSlots(t, flippedEmptySlots, fetchedSlots) + + // 400 + // No filters + path = fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit) + err = doBadReq("GET", path, nil, 400) + assert.NoError(t, err) + // Invalid maxSlotNum + path = fmt.Sprintf("%s?maxSlotNum=%d", endpoint, -2) + err = doBadReq("GET", path, nil, 400) + assert.NoError(t, err) + // Invalid wonByEthereumAddress + path = fmt.Sprintf("%s?maxSlotNum=%d&wonByEthereumAddress=%s", endpoint, maxSlotNum, "0xG0000001") + err = doBadReq("GET", path, nil, 400) + assert.NoError(t, err) + // Invalid minSlotNum / maxSlotNum (minSlotNum > maxSlotNum) + maxSlotNum = tc.slots[1].SlotNum + minSlotNum = tc.slots[4].SlotNum + path = fmt.Sprintf("%s?maxSlotNum=%d&minSlotNum=%d&limit=%d&fromItem=", endpoint, maxSlotNum, minSlotNum, limit) + err = doBadReq("GET", path, nil, 400) + assert.NoError(t, err) + // 404 + maxSlotNum = tc.slots[1].SlotNum + path = fmt.Sprintf("%s?maxSlotNum=%d&wonByEthereumAddress=%s&limit=%d&fromItem=", endpoint, maxSlotNum, tc.coordinators[3].Bidder.String(), limit) + err = doBadReq("GET", path, nil, 404) + assert.NoError(t, err) +} + +func assertSlots(t *testing.T, expected, actual []testSlot) { + assert.Equal(t, len(expected), len(actual)) + for i := 0; i < len(expected); i++ { + assertSlot(t, expected[i], actual[i]) + } +} + +func assertSlot(t *testing.T, expected, actual testSlot) { + if actual.WinnerBid != nil { + assert.Equal(t, expected.WinnerBid.Timestamp.Unix(), actual.WinnerBid.Timestamp.Unix()) + expected.WinnerBid.Timestamp = actual.WinnerBid.Timestamp + actual.WinnerBid.ItemID = expected.WinnerBid.ItemID + } + actual.ItemID = expected.ItemID + assert.Equal(t, expected, actual) +} diff --git a/api/swagger.yml b/api/swagger.yml index e19fce6..b0c7015 100644 --- a/api/swagger.yml +++ b/api/swagger.yml @@ -774,19 +774,19 @@ paths: - name: minSlotNum in: query required: false - description: Only include batches with `slotNum < minSlotNum`. + description: Only include slots with `slotNum >= minSlotNum`. By default, `minSlotNum = 0`. schema: $ref: '#/components/schemas/SlotNum' - name: maxSlothNum in: query required: false - description: Only include batches with `slotNum > maxSlotNum`. + description: Only include slots with `slotNum <= maxSlotNum`. schema: $ref: '#/components/schemas/SlotNum' - name: wonByEthereumAddress in: query required: false - description: Only include slots won by a coordinator whose `forgerAddr == wonByEthereumAddress`. + description: Only include slots won by a coordinator whose `bidderAddr == wonByEthereumAddress`. schema: $ref: '#/components/schemas/EthereumAddress' - name: finishedAuction @@ -2171,6 +2171,8 @@ components: type: object description: Slot information. properties: + itemId: + $ref: '#/components/schemas/ItemId' slotNum: $ref: '#/components/schemas/SlotNum' firstBlock: @@ -2183,25 +2185,67 @@ components: - $ref: '#/components/schemas/EthBlockNum' - description: Block in which the slot ended or will end - example: 4475934 - closedAuction: + openAuction: type: boolean - description: Whether the auction of the slot has finished or not. - winner: - allOf: - - $ref: '#/components/schemas/Coordinator' - - description: Coordinator who won the auction. Only applicable if the auction is closed. - - nullable: true - - example: null + description: Whether the auction of the slot is open or not. + winnerBid: + type: object + description: The winning bid of the auction. If openAuction == true, is the current winner. If the auction is closed because it has already been finalized, the bid is the final winner. If the winnerBid is null, it is because no coordinator has bid for that slot. + nullable: true + properties: + itemId: + $ref: '#/components/schemas/ItemId' + bidderAddr: + $ref: '#/components/schemas/EthereumAddress' + forgerAddr: + $ref: '#/components/schemas/EthereumAddress' + slotNum: + $ref: '#/components/schemas/SlotNum' + URL: + $ref: '#/components/schemas/URL' + bidValue: + type: string + description: BigInt is an integer encoded as a string for numbers that are very large. + example: "8708856933496328593" + pattern: "^\\d+$" + ethereumBlockNum: + $ref: '#/components/schemas/EthBlockNum' + timestamp: + type: string + format: date-time + additionalProperties: false + require: + - bidderAddr + - forgerAddr + - slotNum + - URL + - bidValue + - ethereumBlockNum + - timestamp + additionalProperties: false + require: + - slotNum + - firstBlock + - lastBlock + - openAuction + - winner + - winnerBid Slots: type: object properties: - nextForgers: + slots: type: array description: List of slots. items: - $ref: '#/components/schemas/Slot' + allOf: + - $ref: '#/components/schemas/Slot' + - description: Last synchronized Etherum block. pagination: $ref: '#/components/schemas/PaginationInfo' + additionalProperties: false + require: + - slots + - pagination NextForger: type: object description: Coordinator information along with the scheduled forging period diff --git a/db/historydb/historydb.go b/db/historydb/historydb.go index 5542395..71e0aa8 100644 --- a/db/historydb/historydb.go +++ b/db/historydb/historydb.go @@ -10,7 +10,6 @@ import ( ethCommon "github.com/ethereum/go-ethereum/common" "github.com/hermeznetwork/hermez-node/common" "github.com/hermeznetwork/hermez-node/db" - "github.com/hermeznetwork/hermez-node/log" "github.com/iden3/go-iden3-crypto/babyjub" "github.com/jmoiron/sqlx" @@ -257,7 +256,7 @@ func (hdb *HistoryDB) GetBatchesAPI( } queryStr += fmt.Sprintf("LIMIT %d;", *limit) query = hdb.db.Rebind(queryStr) - log.Debug(query) + // log.Debug(query) batchPtrs := []*BatchAPI{} if err := meddler.QueryAll(hdb.db, &batchPtrs, query, args...); err != nil { return nil, nil, err @@ -355,8 +354,67 @@ func (hdb *HistoryDB) GetAllBids() ([]common.Bid, error) { return db.SlicePtrsToSlice(bids).([]common.Bid), err } +// GetBestBidAPI returns the best bid in specific slot by slotNum +func (hdb *HistoryDB) GetBestBidAPI(slotNum *int64) (BidAPI, error) { + bid := &BidAPI{} + err := meddler.QueryRow( + hdb.db, bid, `SELECT bid.*, block.timestamp, coordinator.forger_addr, coordinator.url + FROM bid INNER JOIN block ON bid.eth_block_num = block.eth_block_num + INNER JOIN coordinator ON bid.bidder_addr = coordinator.bidder_addr + WHERE slot_num = $1 ORDER BY item_id DESC LIMIT 1;`, slotNum, + ) + return *bid, err +} + +// GetBestBidsAPI returns the best bid in specific slot by slotNum +func (hdb *HistoryDB) GetBestBidsAPI(minSlotNum, maxSlotNum *int64, bidderAddr *ethCommon.Address, limit *uint, order string) ([]BidAPI, *db.Pagination, error) { + var query string + var args []interface{} + queryStr := `SELECT b.*, block.timestamp, coordinator.forger_addr, coordinator.url, + COUNT(*) OVER() AS total_items, MIN(b.slot_num) OVER() AS first_item, + MAX(b.slot_num) OVER() AS last_item FROM ( + SELECT slot_num, MAX(item_id) as maxitem + FROM bid GROUP BY slot_num + ) + AS x INNER JOIN bid AS b ON b.item_id = x.maxitem + INNER JOIN block ON b.eth_block_num = block.eth_block_num + INNER JOIN coordinator ON b.bidder_addr = coordinator.bidder_addr + WHERE (b.slot_num >= ? AND b.slot_num <= ?)` + args = append(args, minSlotNum) + args = append(args, maxSlotNum) + // Apply filters + if bidderAddr != nil { + queryStr += " AND b.bidder_addr = ? " + args = append(args, bidderAddr) + } + queryStr += " ORDER BY b.slot_num " + if order == OrderAsc { + queryStr += "ASC " + } else { + queryStr += "DESC " + } + if limit != nil { + queryStr += fmt.Sprintf("LIMIT %d;", *limit) + } + query = hdb.db.Rebind(queryStr) + bidPtrs := []*BidAPI{} + if err := meddler.QueryAll(hdb.db, &bidPtrs, query, args...); err != nil { + return nil, nil, err + } + // log.Debug(query) + bids := db.SlicePtrsToSlice(bidPtrs).([]BidAPI) + if len(bids) == 0 { + return nil, nil, sql.ErrNoRows + } + return bids, &db.Pagination{ + TotalItems: bids[0].TotalItems, + FirstItem: bids[0].FirstItem, + LastItem: bids[0].LastItem, + }, nil +} + // GetBidsAPI return the bids applying the given filters -func (hdb *HistoryDB) GetBidsAPI(slotNum *uint, forgerAddr *ethCommon.Address, fromItem, limit *uint, order string) ([]BidAPI, *db.Pagination, error) { +func (hdb *HistoryDB) GetBidsAPI(slotNum *int64, forgerAddr *ethCommon.Address, fromItem, limit *uint, order string) ([]BidAPI, *db.Pagination, error) { var query string var args []interface{} queryStr := `SELECT bid.*, block.timestamp, coordinator.forger_addr, coordinator.url, @@ -863,7 +921,7 @@ func (hdb *HistoryDB) GetHistoryTxs( } queryStr += fmt.Sprintf("LIMIT %d;", *limit) query = hdb.db.Rebind(queryStr) - log.Debug(query) + // log.Debug(query) txsPtrs := []*TxAPI{} if err := meddler.QueryAll(hdb.db, &txsPtrs, query, args...); err != nil { return nil, nil, err @@ -1320,3 +1378,17 @@ func (hdb *HistoryDB) GetCoordinatorsAPI(fromItem, limit *uint, order string) ([ LastItem: coordinators[0].LastItem, }, nil } + +// AddAuctionVars insert auction vars into the DB +func (hdb *HistoryDB) AddAuctionVars(auctionVars *common.AuctionVariables) error { + return meddler.Insert(hdb.db, "auction_vars", auctionVars) +} + +// GetAuctionVars returns auction variables +func (hdb *HistoryDB) GetAuctionVars() (*common.AuctionVariables, error) { + auctionVars := &common.AuctionVariables{} + err := meddler.QueryRow( + hdb.db, auctionVars, `SELECT * FROM auction_vars;`, + ) + return auctionVars, err +} diff --git a/test/historydb.go b/test/historydb.go index ef5c73c..1a2a6bc 100644 --- a/test/historydb.go +++ b/test/historydb.go @@ -329,9 +329,15 @@ func GenCoordinators(nCoords int, blocks []common.Block) []common.Coordinator { // GenBids generates bids. WARNING: This is meant for DB/API testing, and may not be fully consistent with the protocol. func GenBids(nBids int, blocks []common.Block, coords []common.Coordinator) []common.Bid { bids := []common.Bid{} - for i := 0; i < nBids; i++ { + for i := 0; i < nBids*2; i = i + 2 { //nolint:gomnd + var slotNum int64 + if i < nBids { + slotNum = int64(i) + } else { + slotNum = int64(i - nBids) + } bids = append(bids, common.Bid{ - SlotNum: int64(i), + SlotNum: slotNum, BidValue: big.NewInt(int64(i)), EthBlockNum: blocks[i%len(blocks)].EthBlockNum, Bidder: coords[i%len(blocks)].Bidder,