Browse Source

API add slots endpoints

feature/sql-semaphore1
laisolizq 4 years ago
parent
commit
5012b82914
10 changed files with 872 additions and 32 deletions
  1. +1
    -0
      api/api.go
  2. +27
    -4
      api/api_test.go
  3. +1
    -1
      api/bids_test.go
  4. +3
    -4
      api/handlers.go
  5. +48
    -4
      api/parsers.go
  6. +346
    -0
      api/slots.go
  7. +305
    -0
      api/slots_test.go
  8. +57
    -13
      api/swagger.yml
  9. +76
    -4
      db/historydb/historydb.go
  10. +8
    -2
      test/historydb.go

+ 1
- 0
api/api.go

@ -62,6 +62,7 @@ func SetAPIEndpoints(
server.GET("/batches/:batchNum", getBatch) server.GET("/batches/:batchNum", getBatch)
server.GET("/full-batches/:batchNum", getFullBatch) server.GET("/full-batches/:batchNum", getFullBatch)
server.GET("/slots", getSlots) server.GET("/slots", getSlots)
server.GET("/slots/:slotNum", getSlot)
server.GET("/bids", getBids) server.GET("/bids", getBids)
server.GET("/next-forgers", getNextForgers) server.GET("/next-forgers", getNextForgers)
server.GET("/state", getState) server.GET("/state", getState)

+ 27
- 4
api/api_test.go

@ -49,6 +49,8 @@ type testCommon struct {
auths []testAuth auths []testAuth
router *swagger.Router router *swagger.Router
bids []testBid bids []testBid
slots []testSlot
auctionVars common.AuctionVariables
} }
var tc testCommon var tc testCommon
@ -122,6 +124,8 @@ func TestMain(m *testing.M) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
lastBlockNum := blocks[nBlocks-1].EthBlockNum
// Gen tokens and add them to DB // Gen tokens and add them to DB
const nTokens = 10 const nTokens = 10
tokens, ethToken := test.GenTokens(nTokens, blocks) tokens, ethToken := test.GenTokens(nTokens, blocks)
@ -258,12 +262,26 @@ func TestMain(m *testing.M) {
} }
// Bids // Bids
const nBids = 10
const nBids = 20
bids := test.GenBids(nBids, blocks, coords) bids := test.GenBids(nBids, blocks, coords)
err = hdb.AddBids(bids) err = hdb.AddBids(bids)
if err != nil { if err != nil {
panic(err) 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 // Set testCommon
usrTxs, allTxs := genTestTxs(sortedTxs, usrIdxs, accs, tokensUSD, blocks) usrTxs, allTxs := genTestTxs(sortedTxs, usrIdxs, accs, tokensUSD, blocks)
@ -287,7 +305,9 @@ func TestMain(m *testing.M) {
poolTxsToReceive: poolTxsToReceive, poolTxsToReceive: poolTxsToReceive,
auths: genTestAuths(test.GenAuths(5)), auths: genTestAuths(test.GenAuths(5)),
router: router, router: router,
bids: genTestBids(blocks, coordinators, bids),
bids: testBids,
slots: genTestSlots(nSlots, lastBlockNum, testBids, auctionVars),
auctionVars: auctionVars,
} }
// Fake server // Fake server
@ -317,15 +337,18 @@ func doGoodReqPaginated(
iterStruct db.Paginationer, iterStruct db.Paginationer,
appendIter func(res interface{}), appendIter func(res interface{}),
) error { ) error {
next := 0
next := -1
for { for {
// Call API to get this iteration items // Call API to get this iteration items
iterPath := path iterPath := path
if next == 0 && order == historydb.OrderDesc {
if next == -1 && order == historydb.OrderDesc {
// Fetch first item in reverse order // Fetch first item in reverse order
iterPath += "99999" iterPath += "99999"
} else { } else {
// Fetch from next item or 0 if it's ascending order // Fetch from next item or 0 if it's ascending order
if next == -1 {
next = 0
}
iterPath += strconv.Itoa(next) iterPath += strconv.Itoa(next)
} }
if err := doGoodReq("GET", iterPath+"&order="+order, nil, iterStruct); err != nil { if err := doGoodReq("GET", iterPath+"&order="+order, nil, iterStruct); err != nil {

+ 1
- 1
api/bids_test.go

@ -108,7 +108,7 @@ func TestGetBids(t *testing.T) {
// slotNum, in reverse order // slotNum, in reverse order
fetchedBids = []testBid{} fetchedBids = []testBid{}
path = fmt.Sprintf("%s?slotNum=%d&limit=%d&fromItem=", endpoint, slotNum, limit) 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) assert.NoError(t, err)
flippedBids := []testBid{} flippedBids := []testBid{}
for i := len(slotNumBids) - 1; i >= 0; i-- { for i := len(slotNumBids) - 1; i >= 0; i-- {

+ 3
- 4
api/handlers.go

@ -21,6 +21,9 @@ const (
// 2^32 -1 // 2^32 -1
maxUint32 = 4294967295 maxUint32 = 4294967295
// 2^64 /2 -1
maxInt64 = 9223372036854775807
) )
var ( var (
@ -36,10 +39,6 @@ func getAccount(c *gin.Context) {
} }
func getSlots(c *gin.Context) {
}
func getNextForgers(c *gin.Context) { func getNextForgers(c *gin.Context) {
} }

+ 48
- 4
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) 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 func parseQueryBool(name string, dflt *bool, c querier) (*bool, error) { //nolint:SA4009 res may be not overwriten
str := c.Query(name) str := c.Query(name)
if str == "" { if str == "" {
@ -69,7 +74,7 @@ func parseQueryBool(name string, dflt *bool, c querier) (*bool, error) { //nolin
*res = false *res = false
return res, nil 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) { 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 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 { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -210,6 +215,26 @@ func parseBidFilters(c querier) (*uint, *ethCommon.Address, error) {
return slotNum, bidderAddr, nil 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 // Param parsers
type paramer interface { 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) 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) { func stringToIdx(idxStr, name string) (*common.Idx, error) {
if idxStr == "" { if idxStr == "" {
return nil, nil return nil, nil
@ -261,7 +291,7 @@ func stringToUint(uintStr, name string, dflt *uint, min, max uint) (*uint, error
resInt, err := strconv.Atoi(uintStr) resInt, err := strconv.Atoi(uintStr)
if err != nil || resInt < 0 || resInt < int(min) || resInt > int(max) { if err != nil || resInt < 0 || resInt < int(min) || resInt > int(max) {
return nil, fmt.Errorf( 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) name, min, max)
} }
res := uint(resInt) res := uint(resInt)
@ -270,6 +300,20 @@ func stringToUint(uintStr, name string, dflt *uint, min, max uint) (*uint, error
return dflt, nil 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) { func hezStringToEthAddr(addrStr, name string) (*ethCommon.Address, error) {
if addrStr == "" { if addrStr == "" {
return nil, nil return nil, nil

+ 346
- 0
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),
})
}

+ 305
- 0
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)
}

+ 57
- 13
api/swagger.yml

@ -774,19 +774,19 @@ paths:
- name: minSlotNum - name: minSlotNum
in: query in: query
required: false required: false
description: Only include batches with `slotNum < minSlotNum`.
description: Only include slots with `slotNum >= minSlotNum`. By default, `minSlotNum = 0`.
schema: schema:
$ref: '#/components/schemas/SlotNum' $ref: '#/components/schemas/SlotNum'
- name: maxSlothNum - name: maxSlothNum
in: query in: query
required: false required: false
description: Only include batches with `slotNum > maxSlotNum`.
description: Only include slots with `slotNum <= maxSlotNum`.
schema: schema:
$ref: '#/components/schemas/SlotNum' $ref: '#/components/schemas/SlotNum'
- name: wonByEthereumAddress - name: wonByEthereumAddress
in: query in: query
required: false 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: schema:
$ref: '#/components/schemas/EthereumAddress' $ref: '#/components/schemas/EthereumAddress'
- name: finishedAuction - name: finishedAuction
@ -2171,6 +2171,8 @@ components:
type: object type: object
description: Slot information. description: Slot information.
properties: properties:
itemId:
$ref: '#/components/schemas/ItemId'
slotNum: slotNum:
$ref: '#/components/schemas/SlotNum' $ref: '#/components/schemas/SlotNum'
firstBlock: firstBlock:
@ -2183,25 +2185,67 @@ components:
- $ref: '#/components/schemas/EthBlockNum' - $ref: '#/components/schemas/EthBlockNum'
- description: Block in which the slot ended or will end - description: Block in which the slot ended or will end
- example: 4475934 - example: 4475934
closedAuction:
openAuction:
type: boolean 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: Slots:
type: object type: object
properties: properties:
nextForgers:
slots:
type: array type: array
description: List of slots. description: List of slots.
items: items:
$ref: '#/components/schemas/Slot'
allOf:
- $ref: '#/components/schemas/Slot'
- description: Last synchronized Etherum block.
pagination: pagination:
$ref: '#/components/schemas/PaginationInfo' $ref: '#/components/schemas/PaginationInfo'
additionalProperties: false
require:
- slots
- pagination
NextForger: NextForger:
type: object type: object
description: Coordinator information along with the scheduled forging period description: Coordinator information along with the scheduled forging period

+ 76
- 4
db/historydb/historydb.go

@ -10,7 +10,6 @@ import (
ethCommon "github.com/ethereum/go-ethereum/common" ethCommon "github.com/ethereum/go-ethereum/common"
"github.com/hermeznetwork/hermez-node/common" "github.com/hermeznetwork/hermez-node/common"
"github.com/hermeznetwork/hermez-node/db" "github.com/hermeznetwork/hermez-node/db"
"github.com/hermeznetwork/hermez-node/log"
"github.com/iden3/go-iden3-crypto/babyjub" "github.com/iden3/go-iden3-crypto/babyjub"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
@ -257,7 +256,7 @@ func (hdb *HistoryDB) GetBatchesAPI(
} }
queryStr += fmt.Sprintf("LIMIT %d;", *limit) queryStr += fmt.Sprintf("LIMIT %d;", *limit)
query = hdb.db.Rebind(queryStr) query = hdb.db.Rebind(queryStr)
log.Debug(query)
// log.Debug(query)
batchPtrs := []*BatchAPI{} batchPtrs := []*BatchAPI{}
if err := meddler.QueryAll(hdb.db, &batchPtrs, query, args...); err != nil { if err := meddler.QueryAll(hdb.db, &batchPtrs, query, args...); err != nil {
return nil, nil, err return nil, nil, err
@ -355,8 +354,67 @@ func (hdb *HistoryDB) GetAllBids() ([]common.Bid, error) {
return db.SlicePtrsToSlice(bids).([]common.Bid), err 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 // 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 query string
var args []interface{} var args []interface{}
queryStr := `SELECT bid.*, block.timestamp, coordinator.forger_addr, coordinator.url, queryStr := `SELECT bid.*, block.timestamp, coordinator.forger_addr, coordinator.url,
@ -863,7 +921,7 @@ func (hdb *HistoryDB) GetHistoryTxs(
} }
queryStr += fmt.Sprintf("LIMIT %d;", *limit) queryStr += fmt.Sprintf("LIMIT %d;", *limit)
query = hdb.db.Rebind(queryStr) query = hdb.db.Rebind(queryStr)
log.Debug(query)
// log.Debug(query)
txsPtrs := []*TxAPI{} txsPtrs := []*TxAPI{}
if err := meddler.QueryAll(hdb.db, &txsPtrs, query, args...); err != nil { if err := meddler.QueryAll(hdb.db, &txsPtrs, query, args...); err != nil {
return nil, nil, err return nil, nil, err
@ -1320,3 +1378,17 @@ func (hdb *HistoryDB) GetCoordinatorsAPI(fromItem, limit *uint, order string) ([
LastItem: coordinators[0].LastItem, LastItem: coordinators[0].LastItem,
}, nil }, 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
}

+ 8
- 2
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. // 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 { func GenBids(nBids int, blocks []common.Block, coords []common.Coordinator) []common.Bid {
bids := []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{ bids = append(bids, common.Bid{
SlotNum: int64(i),
SlotNum: slotNum,
BidValue: big.NewInt(int64(i)), BidValue: big.NewInt(int64(i)),
EthBlockNum: blocks[i%len(blocks)].EthBlockNum, EthBlockNum: blocks[i%len(blocks)].EthBlockNum,
Bidder: coords[i%len(blocks)].Bidder, Bidder: coords[i%len(blocks)].Bidder,

Loading…
Cancel
Save