mirror of
https://github.com/arnaucube/hermez-node.git
synced 2026-02-07 03:16:45 +01:00
Coordinators methods for API
This commit is contained in:
@@ -71,7 +71,7 @@ func SetAPIEndpoints(
|
|||||||
server.GET("/tokens/:id", getToken)
|
server.GET("/tokens/:id", getToken)
|
||||||
server.GET("/recommendedFee", getRecommendedFee)
|
server.GET("/recommendedFee", getRecommendedFee)
|
||||||
server.GET("/coordinators", getCoordinators)
|
server.GET("/coordinators", getCoordinators)
|
||||||
server.GET("/coordinators/:forgerAddr", getCoordinator)
|
server.GET("/coordinators/:bidderAddr", getCoordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ type testCommon struct {
|
|||||||
blocks []common.Block
|
blocks []common.Block
|
||||||
tokens []tokenAPI
|
tokens []tokenAPI
|
||||||
batches []common.Batch
|
batches []common.Batch
|
||||||
|
coordinators []coordinatorAPI
|
||||||
usrAddr string
|
usrAddr string
|
||||||
usrBjj string
|
usrBjj string
|
||||||
accs []common.Account
|
accs []common.Account
|
||||||
@@ -622,11 +623,26 @@ func TestMain(m *testing.M) {
|
|||||||
poolTxsToSend = append(poolTxsToSend, genSendTx)
|
poolTxsToSend = append(poolTxsToSend, genSendTx)
|
||||||
poolTxsToReceive = append(poolTxsToReceive, genReceiveTx)
|
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
|
// Set testCommon
|
||||||
tc = testCommon{
|
tc = testCommon{
|
||||||
blocks: blocks,
|
blocks: blocks,
|
||||||
tokens: tokensUSD,
|
tokens: tokensUSD,
|
||||||
batches: batches,
|
batches: batches,
|
||||||
|
coordinators: apiCoordinators,
|
||||||
usrAddr: ethAddrToHez(usrAddr),
|
usrAddr: ethAddrToHez(usrAddr),
|
||||||
usrBjj: bjjToString(usrBjj),
|
usrBjj: bjjToString(usrBjj),
|
||||||
accs: accs,
|
accs: accs,
|
||||||
@@ -1250,6 +1266,59 @@ func assertPoolTx(t *testing.T, expected, actual sendPoolTx) {
|
|||||||
assert.Equal(t, expected, actual)
|
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(
|
func doGoodReqPaginated(
|
||||||
path, order string,
|
path, order string,
|
||||||
iterStruct db.Paginationer,
|
iterStruct db.Paginationer,
|
||||||
|
|||||||
@@ -211,7 +211,6 @@ func historyExitsToAPI(dbExits []historydb.HistoryExit) []exitAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tokens
|
// Tokens
|
||||||
|
|
||||||
type tokensAPI struct {
|
type tokensAPI struct {
|
||||||
Tokens []tokenAPI `json:"tokens"`
|
Tokens []tokenAPI `json:"tokens"`
|
||||||
Pagination *db.Pagination `json:"pagination"`
|
Pagination *db.Pagination `json:"pagination"`
|
||||||
@@ -600,3 +599,45 @@ func poolL2TxReadToSend(dbTx *l2db.PoolL2TxRead) *sendPoolTx {
|
|||||||
}
|
}
|
||||||
return tx
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -23,6 +24,11 @@ const (
|
|||||||
maxUint32 = 4294967295
|
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) {
|
func postAccountCreationAuth(c *gin.Context) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -285,11 +291,49 @@ func getRecommendedFee(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getCoordinators(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) {
|
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) {
|
func retSQLErr(err error, c *gin.Context) {
|
||||||
|
|||||||
@@ -310,3 +310,12 @@ func hezStringToBJJ(bjjStr, name string) (*babyjub.PublicKey, error) {
|
|||||||
}
|
}
|
||||||
return bjj, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ func (qp *queryParser) Query(query string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qp *queryParser) Param(param string) string {
|
||||||
|
if val, ok := qp.m[param]; ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseQueryUint(t *testing.T) {
|
func TestParseQueryUint(t *testing.T) {
|
||||||
name := "foo"
|
name := "foo"
|
||||||
c := &queryParser{}
|
c := &queryParser{}
|
||||||
@@ -295,3 +302,23 @@ func TestParseTokenFilters(t *testing.T) {
|
|||||||
assert.Equal(t, symbolsArray, symbolsParse)
|
assert.Equal(t, symbolsArray, symbolsParse)
|
||||||
assert.Equal(t, nameValue, nameParse)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1162,7 +1162,7 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/Error500'
|
$ref: '#/components/schemas/Error500'
|
||||||
'/coordinators/{forgerAddr}':
|
'/coordinators/{bidderAddr}':
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- Hermez status
|
- Hermez status
|
||||||
@@ -1170,7 +1170,7 @@ paths:
|
|||||||
description: Get the information of a coordinator.
|
description: Get the information of a coordinator.
|
||||||
operationId: getCoordinator
|
operationId: getCoordinator
|
||||||
parameters:
|
parameters:
|
||||||
- name: forgerAddr
|
- name: bidderAddr
|
||||||
in: path
|
in: path
|
||||||
description: Coordinator identifier
|
description: Coordinator identifier
|
||||||
required: true
|
required: true
|
||||||
@@ -1807,9 +1807,11 @@ components:
|
|||||||
Coordinator:
|
Coordinator:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
itemId:
|
||||||
|
$ref: '#/components/schemas/ItemId'
|
||||||
forgerAddr:
|
forgerAddr:
|
||||||
$ref: '#/components/schemas/EthereumAddress'
|
$ref: '#/components/schemas/EthereumAddress'
|
||||||
withdrawAddr:
|
bidderAddr:
|
||||||
$ref: '#/components/schemas/EthereumAddress'
|
$ref: '#/components/schemas/EthereumAddress'
|
||||||
URL:
|
URL:
|
||||||
$ref: '#/components/schemas/URL'
|
$ref: '#/components/schemas/URL'
|
||||||
@@ -1818,6 +1820,13 @@ components:
|
|||||||
- $ref: '#/components/schemas/EthBlockNum'
|
- $ref: '#/components/schemas/EthBlockNum'
|
||||||
- description: Ethereum block in which the coordinator registered into the network.
|
- description: Ethereum block in which the coordinator registered into the network.
|
||||||
- example: 5735943738
|
- example: 5735943738
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- itemId
|
||||||
|
- forgerAddr
|
||||||
|
- bidderAddr
|
||||||
|
- URL
|
||||||
|
- ethereumBlock
|
||||||
Coordinators:
|
Coordinators:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -1828,6 +1837,10 @@ components:
|
|||||||
$ref: '#/components/schemas/Coordinator'
|
$ref: '#/components/schemas/Coordinator'
|
||||||
pagination:
|
pagination:
|
||||||
$ref: '#/components/schemas/PaginationInfo'
|
$ref: '#/components/schemas/PaginationInfo'
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- coordinators
|
||||||
|
- pagination
|
||||||
Bid:
|
Bid:
|
||||||
type: object
|
type: object
|
||||||
description: Tokens placed in an auction by a coordinator to gain the right to forge batches during a specific slot.
|
description: Tokens placed in an auction by a coordinator to gain the right to forge batches during a specific slot.
|
||||||
|
|||||||
@@ -915,3 +915,53 @@ func (hdb *HistoryDB) AddBlockSCData(blockData *BlockData) (err error) {
|
|||||||
|
|
||||||
return txn.Commit()
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -118,3 +118,16 @@ type HistoryExit struct {
|
|||||||
TokenUSD *float64 `meddler:"usd"`
|
TokenUSD *float64 `meddler:"usd"`
|
||||||
TokenUSDUpdate *time.Time `meddler:"usd_update"`
|
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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ CREATE TABLE block (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE coordinator (
|
CREATE TABLE coordinator (
|
||||||
|
item_id SERIAL PRIMARY KEY,
|
||||||
bidder_addr BYTEA NOT NULL,
|
bidder_addr BYTEA NOT NULL,
|
||||||
forger_addr BYTEA NOT NULL,
|
forger_addr BYTEA NOT NULL,
|
||||||
eth_block_num BIGINT NOT NULL REFERENCES block (eth_block_num) ON DELETE CASCADE,
|
eth_block_num BIGINT NOT NULL REFERENCES block (eth_block_num) ON DELETE CASCADE,
|
||||||
url VARCHAR(200) NOT NULL,
|
url VARCHAR(200) NOT NULL
|
||||||
PRIMARY KEY (bidder_addr, eth_block_num)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE batch (
|
CREATE TABLE batch (
|
||||||
|
|||||||
Reference in New Issue
Block a user