mirror of
https://github.com/arnaucube/hermez-node.git
synced 2026-02-06 19:06:42 +01:00
Add GET histroy-transactions endpoint
This commit is contained in:
19
api/README.md
Normal file
19
api/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Hermez API
|
||||
|
||||
Easy to deploy and scale API for Hermez operators.
|
||||
You will need to have [docker](https://docs.docker.com/engine/install/) and [docker-compose](https://docs.docker.com/compose/install/) installed on your machine in order to use this repo.
|
||||
|
||||
## Documentation
|
||||
|
||||
As of now the documentation is not hosted anywhere, but you can easily do it yourself by running `./run.sh doc` and then [opening the documentation in your browser](http://localhost:8001)
|
||||
|
||||
## Mock Up
|
||||
|
||||
To use a mock up of the endpoints in the API run `./run.sh doc` (UI + mock up server) or `./run.sh mock` (only mock up server). You can play with the mocked up endpoints using the [web UI](http://localhost:8001), importing `swagger.yml` into Postman or using your preferred language and using `http://loclahost:4010` as base URL.
|
||||
|
||||
## Editor
|
||||
|
||||
It is recomended to edit `swagger.yml` using a dedicated editor as they provide spec validation and real time visualization. Of course you can use your prefered editor. To use the editor run `./run.sh editor` and then [opening the editor in your browser](http://localhost:8002).
|
||||
**Keep in mind that you will need to manually save the file otherwise you will lose the changes** you made once you close your browser seshion or stop the server.
|
||||
|
||||
**Note:** Your browser may cache the swagger definition, so in order to see updated changes it may be needed to refresh the page without cache (Ctrl + Shift + R).
|
||||
77
api/api.go
Normal file
77
api/api.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hermeznetwork/hermez-node/db/historydb"
|
||||
"github.com/hermeznetwork/hermez-node/db/l2db"
|
||||
"github.com/hermeznetwork/hermez-node/db/statedb"
|
||||
)
|
||||
|
||||
var h *historydb.HistoryDB
|
||||
var s *statedb.StateDB // Not 100% sure if this is needed
|
||||
var l2 *l2db.L2DB
|
||||
|
||||
// SetAPIEndpoints sets the endpoints and the appropriate handlers, but doesn't start the server
|
||||
func SetAPIEndpoints(
|
||||
coordinatorEndpoints, explorerEndpoints bool,
|
||||
server *gin.Engine,
|
||||
hdb *historydb.HistoryDB,
|
||||
sdb *statedb.StateDB,
|
||||
l2db *l2db.L2DB,
|
||||
) error {
|
||||
// Check input
|
||||
// TODO: is stateDB only needed for explorer endpoints or for both?
|
||||
if coordinatorEndpoints && l2db == nil {
|
||||
return errors.New("cannot serve Coordinator endpoints without L2DB")
|
||||
}
|
||||
if explorerEndpoints && hdb == nil {
|
||||
return errors.New("cannot serve Explorer endpoints without HistoryDB")
|
||||
}
|
||||
|
||||
h = hdb
|
||||
s = sdb
|
||||
l2 = l2db
|
||||
|
||||
// tmp
|
||||
fmt.Println(h, s, l2)
|
||||
// Add coordinator endpoints
|
||||
if coordinatorEndpoints {
|
||||
// Account
|
||||
server.POST("/account-creation-authorization", postAccountCreationAuth)
|
||||
server.GET("/account-creation-authorization/:hermezEthereumAddress", getAccountCreationAuth)
|
||||
// Transaction
|
||||
server.POST("/transactions-pool", postPoolTx)
|
||||
server.POST("/transactions-pool/:id", getPoolTx)
|
||||
}
|
||||
|
||||
// Add explorer endpoints
|
||||
if explorerEndpoints {
|
||||
// Account
|
||||
server.GET("/accounts", getAccounts)
|
||||
server.GET("/accounts/:hermezEthereumAddress/:accountIndex", getAccount)
|
||||
server.GET("/exits", getExits)
|
||||
server.GET("/exits/:batchNum/:accountIndex", getExit)
|
||||
// Transaction
|
||||
server.GET("/transactions-history", getHistoryTxs)
|
||||
server.GET("/transactions-history/:id", getHistoryTx)
|
||||
// Status
|
||||
server.GET("/batches", getBatches)
|
||||
server.GET("/batches/:batchNum", getBatch)
|
||||
server.GET("/full-batches/:batchNum", getFullBatch)
|
||||
server.GET("/slots", getSlots)
|
||||
server.GET("/bids", getBids)
|
||||
server.GET("/next-forgers", getNextForgers)
|
||||
server.GET("/state", getState)
|
||||
server.GET("/config", getConfig)
|
||||
server.GET("/tokens", getTokens)
|
||||
server.GET("/tokens/:id", getToken)
|
||||
server.GET("/recommendedFee", getRecommendedFee)
|
||||
server.GET("/coordinators", getCoordinators)
|
||||
server.GET("/coordinators/:forgerAddr", getCoordinator)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
621
api/api_test.go
Normal file
621
api/api_test.go
Normal file
@@ -0,0 +1,621 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
ethCommon "github.com/ethereum/go-ethereum/common"
|
||||
swagger "github.com/getkin/kin-openapi/openapi3filter"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hermeznetwork/hermez-node/common"
|
||||
"github.com/hermeznetwork/hermez-node/db/historydb"
|
||||
"github.com/hermeznetwork/hermez-node/db/l2db"
|
||||
"github.com/hermeznetwork/hermez-node/db/statedb"
|
||||
"github.com/hermeznetwork/hermez-node/log"
|
||||
"github.com/hermeznetwork/hermez-node/test"
|
||||
"github.com/iden3/go-iden3-crypto/babyjub"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const apiPort = ":4010"
|
||||
const apiURL = "http://localhost" + apiPort + "/"
|
||||
|
||||
type testCommon struct {
|
||||
blocks []common.Block
|
||||
tokens []common.Token
|
||||
batches []common.Batch
|
||||
usrAddr string
|
||||
usrBjj string
|
||||
accs []common.Account
|
||||
usrTxs historyTxAPIs
|
||||
othrTxs historyTxAPIs
|
||||
allTxs historyTxAPIs
|
||||
router *swagger.Router
|
||||
}
|
||||
|
||||
type historyTxAPIs []historyTxAPI
|
||||
|
||||
func (h historyTxAPIs) Len() int { return len(h) }
|
||||
func (h historyTxAPIs) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
|
||||
func (h historyTxAPIs) Less(i, j int) bool {
|
||||
// i not forged yet
|
||||
if h[i].BatchNum == nil {
|
||||
if h[j].BatchNum != nil { // j is already forged
|
||||
return false
|
||||
}
|
||||
// Both aren't forged, is i in a smaller position?
|
||||
return h[i].Position < h[j].Position
|
||||
}
|
||||
// i is forged
|
||||
if h[j].BatchNum == nil {
|
||||
return true // j is not forged
|
||||
}
|
||||
// Both are forged
|
||||
if *h[i].BatchNum == *h[j].BatchNum {
|
||||
// At the same batch, is i in a smaller position?
|
||||
return h[i].Position < h[j].Position
|
||||
}
|
||||
// At different batches, is i in a smaller batch?
|
||||
return *h[i].BatchNum < *h[j].BatchNum
|
||||
}
|
||||
|
||||
var tc testCommon
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// Init swagger
|
||||
router := swagger.NewRouter().WithSwaggerFromFile("./swagger.yml")
|
||||
// Init DBs
|
||||
pass := os.Getenv("POSTGRES_PASS")
|
||||
hdb, err := historydb.NewHistoryDB(5432, "localhost", "hermez", pass, "history")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
dir, err := ioutil.TempDir("", "tmpdb")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
sdb, err := statedb.NewStateDB(dir, false, 0)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
l2db, err := l2db.NewL2DB(5432, "localhost", "hermez", pass, "l2", 10, 512, 24*time.Hour)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Init API
|
||||
api := gin.Default()
|
||||
if err := SetAPIEndpoints(
|
||||
true,
|
||||
true,
|
||||
api,
|
||||
hdb,
|
||||
sdb,
|
||||
l2db,
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Start server
|
||||
server := &http.Server{Addr: apiPort, Handler: api}
|
||||
go func() {
|
||||
if err := server.ListenAndServe(); err != nil &&
|
||||
err != http.ErrServerClosed {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
// Populate DBs
|
||||
// Clean DB
|
||||
err = h.Reorg(0)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Gen blocks and add them to DB
|
||||
const nBlocks = 5
|
||||
blocks := test.GenBlocks(1, nBlocks+1)
|
||||
err = h.AddBlocks(blocks)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Gen tokens and add them to DB
|
||||
const nTokens = 10
|
||||
tokens := test.GenTokens(nTokens, blocks)
|
||||
err = h.AddTokens(tokens)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Gen batches and add them to DB
|
||||
const nBatches = 10
|
||||
batches := test.GenBatches(nBatches, blocks)
|
||||
err = h.AddBatches(batches)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Gen accounts and add them to DB
|
||||
const totalAccounts = 40
|
||||
const userAccounts = 4
|
||||
usrAddr := ethCommon.BigToAddress(big.NewInt(4896847))
|
||||
privK := babyjub.NewRandPrivKey()
|
||||
usrBjj := privK.Public()
|
||||
accs := test.GenAccounts(totalAccounts, userAccounts, tokens, &usrAddr, usrBjj, batches)
|
||||
err = h.AddAccounts(accs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Gen L1Txs and add them to DB
|
||||
const totalL1Txs = 40
|
||||
const userL1Txs = 4
|
||||
usrL1Txs, othrL1Txs := test.GenL1Txs(0, totalL1Txs, userL1Txs, &usrAddr, accs, tokens, blocks, batches)
|
||||
var l1Txs []common.L1Tx
|
||||
l1Txs = append(l1Txs, usrL1Txs...)
|
||||
l1Txs = append(l1Txs, othrL1Txs...)
|
||||
err = h.AddL1Txs(l1Txs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Gen L2Txs and add them to DB
|
||||
const totalL2Txs = 20
|
||||
const userL2Txs = 4
|
||||
usrL2Txs, othrL2Txs := test.GenL2Txs(totalL1Txs, totalL2Txs, userL2Txs, &usrAddr, accs, tokens, blocks, batches)
|
||||
var l2Txs []common.L2Tx
|
||||
l2Txs = append(l2Txs, usrL2Txs...)
|
||||
l2Txs = append(l2Txs, othrL2Txs...)
|
||||
err = h.AddL2Txs(l2Txs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Set test commons
|
||||
txsToAPITxs := func(l1Txs []common.L1Tx, l2Txs []common.L2Tx, blocks []common.Block, tokens []common.Token) historyTxAPIs {
|
||||
// Transform L1Txs and L2Txs to generic Txs
|
||||
genericTxs := []*common.Tx{}
|
||||
for _, l1tx := range l1Txs {
|
||||
genericTxs = append(genericTxs, l1tx.Tx())
|
||||
}
|
||||
for _, l2tx := range l2Txs {
|
||||
genericTxs = append(genericTxs, l2tx.Tx())
|
||||
}
|
||||
// Transform generic Txs to HistoryTx
|
||||
historyTxs := []*historydb.HistoryTx{}
|
||||
for _, genericTx := range genericTxs {
|
||||
// find timestamp
|
||||
var timestamp time.Time
|
||||
for i := 0; i < len(blocks); i++ {
|
||||
if blocks[i].EthBlockNum == genericTx.EthBlockNum {
|
||||
timestamp = blocks[i].Timestamp
|
||||
break
|
||||
}
|
||||
}
|
||||
// find token
|
||||
token := common.Token{}
|
||||
for i := 0; i < len(tokens); i++ {
|
||||
if tokens[i].TokenID == genericTx.TokenID {
|
||||
token = tokens[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
historyTxs = append(historyTxs, &historydb.HistoryTx{
|
||||
IsL1: genericTx.IsL1,
|
||||
TxID: genericTx.TxID,
|
||||
Type: genericTx.Type,
|
||||
Position: genericTx.Position,
|
||||
FromIdx: genericTx.FromIdx,
|
||||
ToIdx: genericTx.ToIdx,
|
||||
Amount: genericTx.Amount,
|
||||
AmountFloat: genericTx.AmountFloat,
|
||||
TokenID: genericTx.TokenID,
|
||||
USD: token.USD * genericTx.AmountFloat,
|
||||
BatchNum: genericTx.BatchNum,
|
||||
EthBlockNum: genericTx.EthBlockNum,
|
||||
ToForgeL1TxsNum: genericTx.ToForgeL1TxsNum,
|
||||
UserOrigin: genericTx.UserOrigin,
|
||||
FromEthAddr: genericTx.FromEthAddr,
|
||||
FromBJJ: genericTx.FromBJJ,
|
||||
LoadAmount: genericTx.LoadAmount,
|
||||
LoadAmountFloat: genericTx.LoadAmountFloat,
|
||||
LoadAmountUSD: token.USD * genericTx.LoadAmountFloat,
|
||||
Fee: genericTx.Fee,
|
||||
FeeUSD: genericTx.Fee.Percentage() * token.USD * genericTx.AmountFloat,
|
||||
Nonce: genericTx.Nonce,
|
||||
Timestamp: timestamp,
|
||||
TokenSymbol: token.Symbol,
|
||||
CurrentUSD: token.USD * genericTx.AmountFloat,
|
||||
USDUpdate: token.USDUpdate,
|
||||
})
|
||||
}
|
||||
return historyTxAPIs(historyTxsToAPI(historyTxs))
|
||||
}
|
||||
usrTxs := txsToAPITxs(usrL1Txs, usrL2Txs, blocks, tokens)
|
||||
sort.Sort(usrTxs)
|
||||
othrTxs := txsToAPITxs(othrL1Txs, othrL2Txs, blocks, tokens)
|
||||
sort.Sort(othrTxs)
|
||||
allTxs := append(usrTxs, othrTxs...)
|
||||
sort.Sort(allTxs)
|
||||
tc = testCommon{
|
||||
blocks: blocks,
|
||||
tokens: tokens,
|
||||
batches: batches,
|
||||
usrAddr: "hez:" + usrAddr.String(),
|
||||
usrBjj: bjjToString(usrBjj),
|
||||
accs: accs,
|
||||
usrTxs: usrTxs,
|
||||
othrTxs: othrTxs,
|
||||
allTxs: allTxs,
|
||||
router: router,
|
||||
}
|
||||
// Run tests
|
||||
result := m.Run()
|
||||
// Stop server
|
||||
if err := server.Shutdown(context.Background()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := h.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := l2.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
os.Exit(result)
|
||||
}
|
||||
|
||||
func TestGetHistoryTxs(t *testing.T) {
|
||||
endpoint := apiURL + "transactions-history"
|
||||
fetchedTxs := historyTxAPIs{}
|
||||
appendIter := func(intr interface{}) {
|
||||
for i := 0; i < len(intr.(*historyTxsAPI).Txs); i++ {
|
||||
tmp := &historyTxAPI{}
|
||||
if err := copier.Copy(tmp, &intr.(*historyTxsAPI).Txs[i]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fetchedTxs = append(fetchedTxs, *tmp)
|
||||
}
|
||||
}
|
||||
// Get all (no filters)
|
||||
limit := 8
|
||||
path := fmt.Sprintf("%s?limit=%d&offset=", endpoint, limit)
|
||||
err := doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
|
||||
assert.NoError(t, err)
|
||||
assertHistoryTxAPIs(t, tc.allTxs, fetchedTxs)
|
||||
// Get by ethAddr
|
||||
fetchedTxs = historyTxAPIs{}
|
||||
limit = 7
|
||||
path = fmt.Sprintf(
|
||||
"%s?hermezEthereumAddress=%s&limit=%d&offset=",
|
||||
endpoint, tc.usrAddr, limit,
|
||||
)
|
||||
err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
|
||||
assert.NoError(t, err)
|
||||
assertHistoryTxAPIs(t, tc.usrTxs, fetchedTxs)
|
||||
// Get by bjj
|
||||
fetchedTxs = historyTxAPIs{}
|
||||
limit = 6
|
||||
path = fmt.Sprintf(
|
||||
"%s?BJJ=%s&limit=%d&offset=",
|
||||
endpoint, tc.usrBjj, limit,
|
||||
)
|
||||
err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
|
||||
assert.NoError(t, err)
|
||||
assertHistoryTxAPIs(t, tc.usrTxs, fetchedTxs)
|
||||
// Get by tokenID
|
||||
fetchedTxs = historyTxAPIs{}
|
||||
limit = 5
|
||||
tokenID := tc.allTxs[0].TokenID
|
||||
path = fmt.Sprintf(
|
||||
"%s?tokenId=%d&limit=%d&offset=",
|
||||
endpoint, tokenID, limit,
|
||||
)
|
||||
err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
|
||||
assert.NoError(t, err)
|
||||
tokenIDTxs := historyTxAPIs{}
|
||||
for i := 0; i < len(tc.allTxs); i++ {
|
||||
if tc.allTxs[i].TokenID == tokenID {
|
||||
tokenIDTxs = append(tokenIDTxs, tc.allTxs[i])
|
||||
}
|
||||
}
|
||||
assertHistoryTxAPIs(t, tokenIDTxs, fetchedTxs)
|
||||
// idx
|
||||
fetchedTxs = historyTxAPIs{}
|
||||
limit = 4
|
||||
idx := tc.allTxs[0].FromIdx
|
||||
path = fmt.Sprintf(
|
||||
"%s?accountIndex=%s&limit=%d&offset=",
|
||||
endpoint, idx, limit,
|
||||
)
|
||||
err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
|
||||
assert.NoError(t, err)
|
||||
idxTxs := historyTxAPIs{}
|
||||
for i := 0; i < len(tc.allTxs); i++ {
|
||||
if tc.allTxs[i].FromIdx == idx {
|
||||
idxTxs = append(idxTxs, tc.allTxs[i])
|
||||
}
|
||||
}
|
||||
assertHistoryTxAPIs(t, idxTxs, fetchedTxs)
|
||||
// batchNum
|
||||
fetchedTxs = historyTxAPIs{}
|
||||
limit = 3
|
||||
batchNum := tc.allTxs[0].BatchNum
|
||||
path = fmt.Sprintf(
|
||||
"%s?batchNum=%d&limit=%d&offset=",
|
||||
endpoint, *batchNum, limit,
|
||||
)
|
||||
err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
|
||||
assert.NoError(t, err)
|
||||
batchNumTxs := historyTxAPIs{}
|
||||
for i := 0; i < len(tc.allTxs); i++ {
|
||||
if tc.allTxs[i].BatchNum != nil &&
|
||||
*tc.allTxs[i].BatchNum == *batchNum {
|
||||
batchNumTxs = append(batchNumTxs, tc.allTxs[i])
|
||||
}
|
||||
}
|
||||
assertHistoryTxAPIs(t, batchNumTxs, fetchedTxs)
|
||||
// type
|
||||
txTypes := []common.TxType{
|
||||
common.TxTypeExit,
|
||||
common.TxTypeWithdrawn,
|
||||
common.TxTypeTransfer,
|
||||
common.TxTypeDeposit,
|
||||
common.TxTypeCreateAccountDeposit,
|
||||
common.TxTypeCreateAccountDepositTransfer,
|
||||
common.TxTypeDepositTransfer,
|
||||
common.TxTypeForceTransfer,
|
||||
common.TxTypeForceExit,
|
||||
common.TxTypeTransferToEthAddr,
|
||||
common.TxTypeTransferToBJJ,
|
||||
}
|
||||
for _, txType := range txTypes {
|
||||
fetchedTxs = historyTxAPIs{}
|
||||
limit = 2
|
||||
path = fmt.Sprintf(
|
||||
"%s?type=%s&limit=%d&offset=",
|
||||
endpoint, txType, limit,
|
||||
)
|
||||
err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
|
||||
assert.NoError(t, err)
|
||||
txTypeTxs := historyTxAPIs{}
|
||||
for i := 0; i < len(tc.allTxs); i++ {
|
||||
if tc.allTxs[i].Type == txType {
|
||||
txTypeTxs = append(txTypeTxs, tc.allTxs[i])
|
||||
}
|
||||
}
|
||||
assertHistoryTxAPIs(t, txTypeTxs, fetchedTxs)
|
||||
}
|
||||
// Multiple filters
|
||||
fetchedTxs = historyTxAPIs{}
|
||||
limit = 1
|
||||
path = fmt.Sprintf(
|
||||
"%s?batchNum=%d&tokeId=%d&limit=%d&offset=",
|
||||
endpoint, *batchNum, tokenID, limit,
|
||||
)
|
||||
err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
|
||||
assert.NoError(t, err)
|
||||
mixedTxs := historyTxAPIs{}
|
||||
for i := 0; i < len(tc.allTxs); i++ {
|
||||
if tc.allTxs[i].BatchNum != nil {
|
||||
if *tc.allTxs[i].BatchNum == *batchNum && tc.allTxs[i].TokenID == tokenID {
|
||||
mixedTxs = append(mixedTxs, tc.allTxs[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
assertHistoryTxAPIs(t, mixedTxs, fetchedTxs)
|
||||
// All, in reverse order
|
||||
fetchedTxs = historyTxAPIs{}
|
||||
limit = 5
|
||||
path = fmt.Sprintf("%s?", endpoint)
|
||||
appendIterRev := func(intr interface{}) {
|
||||
tmpAll := historyTxAPIs{}
|
||||
for i := 0; i < len(intr.(*historyTxsAPI).Txs); i++ {
|
||||
tmpItem := &historyTxAPI{}
|
||||
if err := copier.Copy(tmpItem, &intr.(*historyTxsAPI).Txs[i]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tmpAll = append(tmpAll, *tmpItem)
|
||||
}
|
||||
fetchedTxs = append(tmpAll, fetchedTxs...)
|
||||
}
|
||||
err = doGoodReqPaginatedReverse(path, &historyTxsAPI{}, appendIterRev, limit)
|
||||
assert.NoError(t, err)
|
||||
assertHistoryTxAPIs(t, tc.allTxs, fetchedTxs)
|
||||
// 400
|
||||
path = fmt.Sprintf(
|
||||
"%s?accountIndex=%s&hermezEthereumAddress=%s",
|
||||
endpoint, idx, tc.usrAddr,
|
||||
)
|
||||
err = doBadReq("GET", path, nil, 400)
|
||||
assert.NoError(t, err)
|
||||
path = fmt.Sprintf("%s?tokenId=X", endpoint)
|
||||
err = doBadReq("GET", path, nil, 400)
|
||||
assert.NoError(t, err)
|
||||
// 404
|
||||
path = fmt.Sprintf("%s?batchNum=999999", endpoint)
|
||||
err = doBadReq("GET", path, nil, 404)
|
||||
assert.NoError(t, err)
|
||||
path = fmt.Sprintf("%s?limit=1000&offset=1000", endpoint)
|
||||
err = doBadReq("GET", path, nil, 404)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func assertHistoryTxAPIs(t *testing.T, expected, actual historyTxAPIs) {
|
||||
assert.Equal(t, len(expected), len(actual))
|
||||
for i := 0; i < len(actual); i++ { //nolint len(actual) won't change within the loop
|
||||
assert.Equal(t, expected[i].Timestamp.Unix(), actual[i].Timestamp.Unix())
|
||||
expected[i].Timestamp = actual[i].Timestamp
|
||||
assert.Equal(t, expected[i].USDUpdate.Unix(), actual[i].USDUpdate.Unix())
|
||||
expected[i].USDUpdate = actual[i].USDUpdate
|
||||
if expected[i].L2Info != nil {
|
||||
if expected[i].L2Info.FeeUSD > actual[i].L2Info.FeeUSD {
|
||||
assert.Less(t, 0.999, actual[i].L2Info.FeeUSD/expected[i].L2Info.FeeUSD)
|
||||
} else if expected[i].L2Info.FeeUSD < actual[i].L2Info.FeeUSD {
|
||||
assert.Less(t, 0.999, expected[i].L2Info.FeeUSD/actual[i].L2Info.FeeUSD)
|
||||
}
|
||||
expected[i].L2Info.FeeUSD = actual[i].L2Info.FeeUSD
|
||||
}
|
||||
assert.Equal(t, expected[i], actual[i])
|
||||
}
|
||||
}
|
||||
|
||||
func doGoodReqPaginated(
|
||||
path string,
|
||||
iterStruct paginationer,
|
||||
appendIter func(res interface{}),
|
||||
) error {
|
||||
next := 0
|
||||
for {
|
||||
// Call API to get this iteration items
|
||||
if err := doGoodReq("GET", path+strconv.Itoa(next), nil, iterStruct); err != nil {
|
||||
return err
|
||||
}
|
||||
appendIter(iterStruct)
|
||||
// Keep iterating?
|
||||
pag := iterStruct.GetPagination()
|
||||
if pag.LastReturnedItem == pag.TotalItems-1 { // No
|
||||
break
|
||||
} else { // Yes
|
||||
next = int(pag.LastReturnedItem + 1)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func doGoodReqPaginatedReverse(
|
||||
path string,
|
||||
iterStruct paginationer,
|
||||
appendIter func(res interface{}),
|
||||
limit int,
|
||||
) error {
|
||||
next := 0
|
||||
first := true
|
||||
for {
|
||||
// Call API to get this iteration items
|
||||
if first {
|
||||
first = false
|
||||
pagQuery := fmt.Sprintf("last=true&limit=%d", limit)
|
||||
if err := doGoodReq("GET", path+pagQuery, nil, iterStruct); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
pagQuery := fmt.Sprintf("offset=%d&limit=%d", next, limit)
|
||||
if err := doGoodReq("GET", path+pagQuery, nil, iterStruct); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
appendIter(iterStruct)
|
||||
// Keep iterating?
|
||||
pag := iterStruct.GetPagination()
|
||||
if iterStruct.Len() == pag.TotalItems || pag.LastReturnedItem-iterStruct.Len() == -1 { // No
|
||||
break
|
||||
} else { // Yes
|
||||
prevOffset := next
|
||||
next = pag.LastReturnedItem - iterStruct.Len() - limit + 1
|
||||
if next < 0 {
|
||||
next = 0
|
||||
limit = prevOffset
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func doGoodReq(method, path string, reqBody io.Reader, returnStruct interface{}) error {
|
||||
ctx := context.Background()
|
||||
client := &http.Client{}
|
||||
httpReq, _ := http.NewRequest(method, path, reqBody)
|
||||
route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Validate request against swagger spec
|
||||
requestValidationInput := &swagger.RequestValidationInput{
|
||||
Request: httpReq,
|
||||
PathParams: pathParams,
|
||||
Route: route,
|
||||
}
|
||||
if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
|
||||
return err
|
||||
}
|
||||
// Do API call
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Body == nil {
|
||||
return errors.New("Nil body")
|
||||
}
|
||||
//nolint
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("%d response: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
// Unmarshal body into return struct
|
||||
if err := json.Unmarshal(body, returnStruct); err != nil {
|
||||
return err
|
||||
}
|
||||
// Validate response against swagger spec
|
||||
responseValidationInput := &swagger.ResponseValidationInput{
|
||||
RequestValidationInput: requestValidationInput,
|
||||
Status: resp.StatusCode,
|
||||
Header: resp.Header,
|
||||
}
|
||||
responseValidationInput = responseValidationInput.SetBodyBytes(body)
|
||||
return swagger.ValidateResponse(ctx, responseValidationInput)
|
||||
}
|
||||
|
||||
func doBadReq(method, path string, reqBody io.Reader, expectedResponseCode int) error {
|
||||
ctx := context.Background()
|
||||
client := &http.Client{}
|
||||
httpReq, _ := http.NewRequest(method, path, reqBody)
|
||||
route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Validate request against swagger spec
|
||||
requestValidationInput := &swagger.RequestValidationInput{
|
||||
Request: httpReq,
|
||||
PathParams: pathParams,
|
||||
Route: route,
|
||||
}
|
||||
if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
|
||||
if expectedResponseCode != 400 {
|
||||
return err
|
||||
}
|
||||
log.Warn("The request does not match the API spec")
|
||||
}
|
||||
// Do API call
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Body == nil {
|
||||
return errors.New("Nil body")
|
||||
}
|
||||
//nolint
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != expectedResponseCode {
|
||||
return fmt.Errorf("Unexpected response code: %d", resp.StatusCode)
|
||||
}
|
||||
// Validate response against swagger spec
|
||||
responseValidationInput := &swagger.ResponseValidationInput{
|
||||
RequestValidationInput: requestValidationInput,
|
||||
Status: resp.StatusCode,
|
||||
Header: resp.Header,
|
||||
}
|
||||
responseValidationInput = responseValidationInput.SetBodyBytes(body)
|
||||
return swagger.ValidateResponse(ctx, responseValidationInput)
|
||||
}
|
||||
130
api/dbtoapistructs.go
Normal file
130
api/dbtoapistructs.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/hermeznetwork/hermez-node/common"
|
||||
"github.com/hermeznetwork/hermez-node/db/historydb"
|
||||
"github.com/iden3/go-iden3-crypto/babyjub"
|
||||
)
|
||||
|
||||
// Commons of the API
|
||||
|
||||
type pagination struct {
|
||||
TotalItems int `json:"totalItems"`
|
||||
LastReturnedItem int `json:"lastReturnedItem"`
|
||||
}
|
||||
|
||||
type paginationer interface {
|
||||
GetPagination() pagination
|
||||
Len() int
|
||||
}
|
||||
|
||||
type errorMsg struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
// History Tx related
|
||||
|
||||
type historyTxsAPI struct {
|
||||
Txs []historyTxAPI `json:"transactions"`
|
||||
Pagination pagination `json:"pagination"`
|
||||
}
|
||||
|
||||
func (htx *historyTxsAPI) GetPagination() pagination { return htx.Pagination }
|
||||
func (htx *historyTxsAPI) Len() int { return len(htx.Txs) }
|
||||
|
||||
type l1Info struct {
|
||||
ToForgeL1TxsNum int64 `json:"toForgeL1TransactionsNum"`
|
||||
UserOrigin bool `json:"userOrigin"`
|
||||
FromEthAddr string `json:"fromEthereumAddress"`
|
||||
FromBJJ string `json:"fromBJJ"`
|
||||
LoadAmount string `json:"loadAmount"`
|
||||
LoadAmountUSD float64 `json:"loadAmountUSD"`
|
||||
EthBlockNum int64 `json:"ethereumBlockNum"`
|
||||
}
|
||||
|
||||
type l2Info struct {
|
||||
Fee common.FeeSelector `json:"fee"`
|
||||
FeeUSD float64 `json:"feeUSD"`
|
||||
Nonce common.Nonce `json:"nonce"`
|
||||
}
|
||||
|
||||
type historyTxAPI struct {
|
||||
IsL1 string `json:"L1orL2"`
|
||||
TxID common.TxID `json:"id"`
|
||||
Type common.TxType `json:"type"`
|
||||
Position int `json:"position"`
|
||||
FromIdx string `json:"fromAccountIndex"`
|
||||
ToIdx string `json:"toAccountIndex"`
|
||||
Amount string `json:"amount"`
|
||||
BatchNum *common.BatchNum `json:"batchNum"`
|
||||
TokenID common.TokenID `json:"tokenId"`
|
||||
TokenSymbol string `json:"tokenSymbol"`
|
||||
USD float64 `json:"historicUSD"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
CurrentUSD float64 `json:"currentUSD"`
|
||||
USDUpdate time.Time `json:"fiatUpdate"`
|
||||
L1Info *l1Info `json:"L1Info"`
|
||||
L2Info *l2Info `json:"L2Info"`
|
||||
}
|
||||
|
||||
func historyTxsToAPI(dbTxs []*historydb.HistoryTx) []historyTxAPI {
|
||||
apiTxs := []historyTxAPI{}
|
||||
for i := 0; i < len(dbTxs); i++ {
|
||||
apiTx := historyTxAPI{
|
||||
TxID: dbTxs[i].TxID,
|
||||
Type: dbTxs[i].Type,
|
||||
Position: dbTxs[i].Position,
|
||||
FromIdx: "hez:" + dbTxs[i].TokenSymbol + ":" + strconv.Itoa(int(dbTxs[i].FromIdx)),
|
||||
ToIdx: "hez:" + dbTxs[i].TokenSymbol + ":" + strconv.Itoa(int(dbTxs[i].ToIdx)),
|
||||
Amount: dbTxs[i].Amount.String(),
|
||||
TokenID: dbTxs[i].TokenID,
|
||||
USD: dbTxs[i].USD,
|
||||
BatchNum: nil,
|
||||
Timestamp: dbTxs[i].Timestamp,
|
||||
TokenSymbol: dbTxs[i].TokenSymbol,
|
||||
CurrentUSD: dbTxs[i].CurrentUSD,
|
||||
USDUpdate: dbTxs[i].USDUpdate,
|
||||
L1Info: nil,
|
||||
L2Info: nil,
|
||||
}
|
||||
bn := dbTxs[i].BatchNum
|
||||
if dbTxs[i].BatchNum != 0 {
|
||||
apiTx.BatchNum = &bn
|
||||
}
|
||||
if dbTxs[i].IsL1 {
|
||||
apiTx.IsL1 = "L1"
|
||||
apiTx.L1Info = &l1Info{
|
||||
ToForgeL1TxsNum: dbTxs[i].ToForgeL1TxsNum,
|
||||
UserOrigin: dbTxs[i].UserOrigin,
|
||||
FromEthAddr: "hez:" + dbTxs[i].FromEthAddr.String(),
|
||||
FromBJJ: bjjToString(dbTxs[i].FromBJJ),
|
||||
LoadAmount: dbTxs[i].LoadAmount.String(),
|
||||
LoadAmountUSD: dbTxs[i].LoadAmountUSD,
|
||||
EthBlockNum: dbTxs[i].EthBlockNum,
|
||||
}
|
||||
} else {
|
||||
apiTx.IsL1 = "L2"
|
||||
apiTx.L2Info = &l2Info{
|
||||
Fee: dbTxs[i].Fee,
|
||||
FeeUSD: dbTxs[i].FeeUSD,
|
||||
Nonce: dbTxs[i].Nonce,
|
||||
}
|
||||
}
|
||||
apiTxs = append(apiTxs, apiTx)
|
||||
}
|
||||
return apiTxs
|
||||
}
|
||||
|
||||
func bjjToString(bjj *babyjub.PublicKey) string {
|
||||
pkComp := [32]byte(bjj.Compress())
|
||||
sum := pkComp[0]
|
||||
for i := 1; i < len(pkComp); i++ {
|
||||
sum += pkComp[i]
|
||||
}
|
||||
bjjSum := append(pkComp[:], sum)
|
||||
return "hez:" + base64.RawURLEncoding.EncodeToString(bjjSum)
|
||||
}
|
||||
32
api/docker-compose.yml
Normal file
32
api/docker-compose.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
version: "3"
|
||||
services:
|
||||
hermez-api-doc:
|
||||
container_name: hermez-api-doc
|
||||
image: swaggerapi/swagger-ui
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8001:8080
|
||||
volumes:
|
||||
- .:/spec
|
||||
environment:
|
||||
- SWAGGER_JSON=/spec/swagger.yml
|
||||
hermez-api-mock:
|
||||
container_name: hermez-api-mock
|
||||
image: stoplight/prism
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 4010:4010
|
||||
volumes:
|
||||
- .:/spec
|
||||
command: mock -h 0.0.0.0 "/spec/swagger.yml"
|
||||
#docker run -d -p 80:8080 -e URL=/foo/swagger.json -v /bar:/usr/share/nginx/html/foo swaggerapi/swagger-editor
|
||||
hermez-api-editor:
|
||||
container_name: hermez-api-editor
|
||||
image: swaggerapi/swagger-editor
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8002:8080
|
||||
volumes:
|
||||
- .:/spec
|
||||
environment:
|
||||
- SWAGGER_FILE=/spec/swagger.yml
|
||||
204
api/handlers.go
Normal file
204
api/handlers.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// maxLimit is the max permited items to be returned in paginated responses
|
||||
const maxLimit uint = 2049
|
||||
|
||||
// dfltLast indicates how paginated endpoints use the query param last if not provided
|
||||
const dfltLast = false
|
||||
|
||||
// dfltLimit indicates the limit of returned items in paginated responses if the query param limit is not provided
|
||||
const dfltLimit uint = 20
|
||||
|
||||
// 2^32 -1
|
||||
const maxUint32 = 4294967295
|
||||
|
||||
func postAccountCreationAuth(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func getAccountCreationAuth(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func postPoolTx(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func getPoolTx(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func getAccounts(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func getAccount(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func getExits(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func getExit(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func getHistoryTxs(c *gin.Context) {
|
||||
// Get query parameters
|
||||
// TokenID
|
||||
tokenID, err := parseQueryUint("tokenId", nil, 0, maxUint32, c)
|
||||
if err != nil {
|
||||
retBadReq(err, c)
|
||||
return
|
||||
}
|
||||
// Hez Eth addr
|
||||
addr, err := parseQueryHezEthAddr(c)
|
||||
if err != nil {
|
||||
retBadReq(err, c)
|
||||
return
|
||||
}
|
||||
// BJJ
|
||||
bjj, err := parseQueryBJJ(c)
|
||||
if err != nil {
|
||||
retBadReq(err, c)
|
||||
return
|
||||
}
|
||||
if addr != nil && bjj != nil {
|
||||
retBadReq(errors.New("bjj and hermezEthereumAddress params are incompatible"), c)
|
||||
return
|
||||
}
|
||||
// Idx
|
||||
idx, err := parseIdx(c)
|
||||
if err != nil {
|
||||
retBadReq(err, c)
|
||||
return
|
||||
}
|
||||
if idx != nil && (addr != nil || bjj != nil || tokenID != nil) {
|
||||
retBadReq(errors.New("accountIndex is incompatible with BJJ, hermezEthereumAddress and tokenId"), c)
|
||||
return
|
||||
}
|
||||
// BatchNum
|
||||
batchNum, err := parseQueryUint("batchNum", nil, 0, maxUint32, c)
|
||||
if err != nil {
|
||||
retBadReq(err, c)
|
||||
return
|
||||
}
|
||||
// TxType
|
||||
txType, err := parseQueryTxType(c)
|
||||
if err != nil {
|
||||
retBadReq(err, c)
|
||||
return
|
||||
}
|
||||
// Pagination
|
||||
offset, last, limit, err := parsePagination(c)
|
||||
if err != nil {
|
||||
retBadReq(err, c)
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch txs from historyDB
|
||||
txs, totalItems, err := h.GetHistoryTxs(
|
||||
addr, bjj, tokenID, idx, batchNum, txType, offset, limit, *last,
|
||||
)
|
||||
if err != nil {
|
||||
retSQLErr(err, c)
|
||||
return
|
||||
}
|
||||
|
||||
// Build succesfull response
|
||||
apiTxs := historyTxsToAPI(txs)
|
||||
lastRet := int(*offset) + len(apiTxs) - 1
|
||||
if *last {
|
||||
lastRet = totalItems - 1
|
||||
}
|
||||
c.JSON(http.StatusOK, &historyTxsAPI{
|
||||
Txs: apiTxs,
|
||||
Pagination: pagination{
|
||||
TotalItems: totalItems,
|
||||
LastReturnedItem: lastRet,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func getHistoryTx(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func getBatches(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func getBatch(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func getFullBatch(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func getSlots(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func getBids(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func getNextForgers(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func getState(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func getConfig(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func getTokens(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func getToken(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func getRecommendedFee(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func getCoordinators(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func getCoordinator(c *gin.Context) {
|
||||
|
||||
}
|
||||
|
||||
func retSQLErr(err error, c *gin.Context) {
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, errorMsg{
|
||||
Message: err.Error(),
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, errorMsg{
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func retBadReq(err error, c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, errorMsg{
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
204
api/parsers.go
Normal file
204
api/parsers.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
ethCommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/hermeznetwork/hermez-node/common"
|
||||
"github.com/iden3/go-iden3-crypto/babyjub"
|
||||
)
|
||||
|
||||
type querier interface {
|
||||
Query(string) string
|
||||
}
|
||||
|
||||
func parsePagination(c querier) (*uint, *bool, *uint, error) {
|
||||
// Offset
|
||||
offset := new(uint)
|
||||
*offset = 0
|
||||
offset, err := parseQueryUint("offset", offset, 0, maxUint32, c)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
// Last
|
||||
last := new(bool)
|
||||
*last = dfltLast
|
||||
last, err = parseQueryBool("last", last, c)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
if *last && (offset != nil && *offset > 0) {
|
||||
return nil, nil, nil, errors.New(
|
||||
"last and offset are incompatible, provide only one of them",
|
||||
)
|
||||
}
|
||||
// Limit
|
||||
limit := new(uint)
|
||||
*limit = dfltLimit
|
||||
limit, err = parseQueryUint("limit", limit, 1, maxLimit, c)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return offset, last, limit, nil
|
||||
}
|
||||
|
||||
func parseQueryUint(name string, dflt *uint, min, max uint, c querier) (*uint, error) { //nolint:SA4009 res may be not overwriten
|
||||
str := c.Query(name)
|
||||
if str != "" {
|
||||
resInt, err := strconv.Atoi(str)
|
||||
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]",
|
||||
name, min, max)
|
||||
}
|
||||
res := uint(resInt)
|
||||
return &res, nil
|
||||
}
|
||||
return dflt, nil
|
||||
}
|
||||
|
||||
func parseQueryBool(name string, dflt *bool, c querier) (*bool, error) { //nolint:SA4009 res may be not overwriten
|
||||
str := c.Query(name)
|
||||
if str == "" {
|
||||
return dflt, nil
|
||||
}
|
||||
if str == "true" {
|
||||
res := new(bool)
|
||||
*res = true
|
||||
return res, nil
|
||||
}
|
||||
if str == "false" {
|
||||
res := new(bool)
|
||||
*res = false
|
||||
return res, nil
|
||||
}
|
||||
return nil, fmt.Errorf("Inavlid %s. Must be eithe true or false", name)
|
||||
}
|
||||
|
||||
func parseQueryHezEthAddr(c querier) (*ethCommon.Address, error) {
|
||||
const name = "hermezEthereumAddress"
|
||||
addrStr := c.Query(name)
|
||||
if addrStr == "" {
|
||||
return nil, nil
|
||||
}
|
||||
splitted := strings.Split(addrStr, "hez:")
|
||||
if len(splitted) != 2 || len(splitted[1]) != 42 {
|
||||
return nil, fmt.Errorf(
|
||||
"Invalid %s, must follow this regex: ^hez:0x[a-fA-F0-9]{40}$", name)
|
||||
}
|
||||
var addr ethCommon.Address
|
||||
err := addr.UnmarshalText([]byte(splitted[1]))
|
||||
return &addr, err
|
||||
}
|
||||
|
||||
func parseQueryBJJ(c querier) (*babyjub.PublicKey, error) {
|
||||
const name = "BJJ"
|
||||
const decodedLen = 33
|
||||
bjjStr := c.Query(name)
|
||||
if bjjStr == "" {
|
||||
return nil, nil
|
||||
}
|
||||
splitted := strings.Split(bjjStr, "hez:")
|
||||
if len(splitted) != 2 || len(splitted[1]) != 44 {
|
||||
return nil, fmt.Errorf(
|
||||
"Invalid %s, must follow this regex: ^hez:[A-Za-z0-9+/=]{44}$",
|
||||
name)
|
||||
}
|
||||
decoded, err := base64.RawURLEncoding.DecodeString(splitted[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"Invalid %s, error decoding base64 string: %s",
|
||||
name, err.Error())
|
||||
}
|
||||
if len(decoded) != decodedLen {
|
||||
return nil, fmt.Errorf(
|
||||
"invalid %s, error decoding base64 string: unexpected byte array length",
|
||||
name)
|
||||
}
|
||||
bjjBytes := [decodedLen - 1]byte{}
|
||||
copy(bjjBytes[:decodedLen-1], decoded[:decodedLen-1])
|
||||
sum := bjjBytes[0]
|
||||
for i := 1; i < len(bjjBytes); i++ {
|
||||
sum += bjjBytes[i]
|
||||
}
|
||||
if decoded[decodedLen-1] != sum {
|
||||
return nil, fmt.Errorf("invalid %s, checksum failed",
|
||||
name)
|
||||
}
|
||||
bjjComp := babyjub.PublicKeyComp(bjjBytes)
|
||||
bjj, err := bjjComp.Decompress()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"invalid %s, error decompressing public key: %s",
|
||||
name, err.Error())
|
||||
}
|
||||
return bjj, nil
|
||||
}
|
||||
|
||||
func parseQueryTxType(c querier) (*common.TxType, error) {
|
||||
const name = "type"
|
||||
typeStr := c.Query(name)
|
||||
if typeStr == "" {
|
||||
return nil, nil
|
||||
}
|
||||
switch common.TxType(typeStr) {
|
||||
case common.TxTypeExit:
|
||||
ret := common.TxTypeExit
|
||||
return &ret, nil
|
||||
case common.TxTypeWithdrawn:
|
||||
ret := common.TxTypeWithdrawn
|
||||
return &ret, nil
|
||||
case common.TxTypeTransfer:
|
||||
ret := common.TxTypeTransfer
|
||||
return &ret, nil
|
||||
case common.TxTypeDeposit:
|
||||
ret := common.TxTypeDeposit
|
||||
return &ret, nil
|
||||
case common.TxTypeCreateAccountDeposit:
|
||||
ret := common.TxTypeCreateAccountDeposit
|
||||
return &ret, nil
|
||||
case common.TxTypeCreateAccountDepositTransfer:
|
||||
ret := common.TxTypeCreateAccountDepositTransfer
|
||||
return &ret, nil
|
||||
case common.TxTypeDepositTransfer:
|
||||
ret := common.TxTypeDepositTransfer
|
||||
return &ret, nil
|
||||
case common.TxTypeForceTransfer:
|
||||
ret := common.TxTypeForceTransfer
|
||||
return &ret, nil
|
||||
case common.TxTypeForceExit:
|
||||
ret := common.TxTypeForceExit
|
||||
return &ret, nil
|
||||
case common.TxTypeTransferToEthAddr:
|
||||
ret := common.TxTypeTransferToEthAddr
|
||||
return &ret, nil
|
||||
case common.TxTypeTransferToBJJ:
|
||||
ret := common.TxTypeTransferToBJJ
|
||||
return &ret, nil
|
||||
}
|
||||
return nil, fmt.Errorf(
|
||||
"invalid %s, %s is not a valid option. Check the valid options in the docmentation",
|
||||
name, typeStr,
|
||||
)
|
||||
}
|
||||
|
||||
func parseIdx(c querier) (*uint, error) {
|
||||
const name = "accountIndex"
|
||||
addrStr := c.Query(name)
|
||||
if addrStr == "" {
|
||||
return nil, nil
|
||||
}
|
||||
splitted := strings.Split(addrStr, ":")
|
||||
const expectedLen = 3
|
||||
if len(splitted) != expectedLen {
|
||||
return nil, fmt.Errorf(
|
||||
"invalid %s, must follow this: hez:<tokenSymbol>:index", name)
|
||||
}
|
||||
idxInt, err := strconv.Atoi(splitted[2])
|
||||
idx := uint(idxInt)
|
||||
return &idx, err
|
||||
}
|
||||
282
api/parsers_test.go
Normal file
282
api/parsers_test.go
Normal file
@@ -0,0 +1,282 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
ethCommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/hermeznetwork/hermez-node/common"
|
||||
"github.com/iden3/go-iden3-crypto/babyjub"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type queryParser struct {
|
||||
m map[string]string
|
||||
}
|
||||
|
||||
func (qp *queryParser) Query(query string) string {
|
||||
if val, ok := qp.m[query]; ok {
|
||||
return val
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestParseQueryUint(t *testing.T) {
|
||||
name := "foo"
|
||||
c := &queryParser{}
|
||||
c.m = make(map[string]string)
|
||||
var min uint = 1
|
||||
var max uint = 10
|
||||
var dflt *uint
|
||||
// Not uint
|
||||
c.m[name] = "-1"
|
||||
_, err := parseQueryUint(name, dflt, min, max, c)
|
||||
assert.Error(t, err)
|
||||
c.m[name] = "a"
|
||||
_, err = parseQueryUint(name, dflt, min, max, c)
|
||||
assert.Error(t, err)
|
||||
c.m[name] = "0.1"
|
||||
_, err = parseQueryUint(name, dflt, min, max, c)
|
||||
assert.Error(t, err)
|
||||
c.m[name] = "1.0"
|
||||
_, err = parseQueryUint(name, dflt, min, max, c)
|
||||
assert.Error(t, err)
|
||||
// Out of range
|
||||
c.m[name] = strconv.Itoa(int(min) - 1)
|
||||
_, err = parseQueryUint(name, dflt, min, max, c)
|
||||
assert.Error(t, err)
|
||||
c.m[name] = strconv.Itoa(int(max) + 1)
|
||||
_, err = parseQueryUint(name, dflt, min, max, c)
|
||||
assert.Error(t, err)
|
||||
// Default nil
|
||||
c.m[name] = ""
|
||||
res, err := parseQueryUint(name, dflt, min, max, c)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, res)
|
||||
// Default not nil
|
||||
dflt = new(uint)
|
||||
*dflt = uint(min)
|
||||
res, err = parseQueryUint(name, dflt, min, max, c)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, uint(min), *res)
|
||||
// Correct
|
||||
c.m[name] = strconv.Itoa(int(max))
|
||||
res, err = parseQueryUint(name, res, min, max, c)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, uint(max), *res)
|
||||
}
|
||||
|
||||
func TestParseQueryBool(t *testing.T) {
|
||||
name := "foo"
|
||||
c := &queryParser{}
|
||||
c.m = make(map[string]string)
|
||||
var dflt *bool
|
||||
// Not bool
|
||||
c.m[name] = "x"
|
||||
_, err := parseQueryBool(name, dflt, c)
|
||||
assert.Error(t, err)
|
||||
c.m[name] = "False"
|
||||
_, err = parseQueryBool(name, dflt, c)
|
||||
assert.Error(t, err)
|
||||
c.m[name] = "0"
|
||||
_, err = parseQueryBool(name, dflt, c)
|
||||
assert.Error(t, err)
|
||||
c.m[name] = "1"
|
||||
_, err = parseQueryBool(name, dflt, c)
|
||||
assert.Error(t, err)
|
||||
// Default nil
|
||||
c.m[name] = ""
|
||||
res, err := parseQueryBool(name, dflt, c)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, res)
|
||||
// Default not nil
|
||||
dflt = new(bool)
|
||||
*dflt = true
|
||||
res, err = parseQueryBool(name, dflt, c)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, *res)
|
||||
// Correct
|
||||
c.m[name] = "false"
|
||||
res, err = parseQueryBool(name, dflt, c)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, *res)
|
||||
c.m[name] = "true"
|
||||
res, err = parseQueryBool(name, dflt, c)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, *res)
|
||||
}
|
||||
|
||||
func TestParsePagination(t *testing.T) {
|
||||
c := &queryParser{}
|
||||
c.m = make(map[string]string)
|
||||
// Offset out of range
|
||||
c.m["offset"] = "-1"
|
||||
_, _, _, err := parsePagination(c)
|
||||
assert.Error(t, err)
|
||||
c.m["offset"] = strconv.Itoa(maxUint32 + 1)
|
||||
_, _, _, err = parsePagination(c)
|
||||
assert.Error(t, err)
|
||||
c.m["offset"] = ""
|
||||
// Limit out of range
|
||||
c.m["limit"] = "0"
|
||||
_, _, _, err = parsePagination(c)
|
||||
assert.Error(t, err)
|
||||
c.m["limit"] = strconv.Itoa(int(maxLimit) + 1)
|
||||
_, _, _, err = parsePagination(c)
|
||||
assert.Error(t, err)
|
||||
c.m["limit"] = ""
|
||||
// Last and offset
|
||||
c.m["offset"] = "1"
|
||||
c.m["last"] = "true"
|
||||
_, _, _, err = parsePagination(c)
|
||||
assert.Error(t, err)
|
||||
// Default
|
||||
c.m["offset"] = ""
|
||||
c.m["last"] = ""
|
||||
c.m["limit"] = ""
|
||||
offset, last, limit, err := parsePagination(c)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, int(*offset))
|
||||
assert.Equal(t, dfltLast, *last)
|
||||
assert.Equal(t, dfltLimit, *limit)
|
||||
// Correct
|
||||
c.m["offset"] = ""
|
||||
c.m["last"] = "true"
|
||||
c.m["limit"] = "25"
|
||||
offset, last, limit, err = parsePagination(c)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, int(*offset))
|
||||
assert.True(t, *last)
|
||||
assert.Equal(t, 25, int(*limit))
|
||||
c.m["offset"] = "25"
|
||||
c.m["last"] = "false"
|
||||
c.m["limit"] = "50"
|
||||
offset, last, limit, err = parsePagination(c)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 25, int(*offset))
|
||||
assert.False(t, *last)
|
||||
assert.Equal(t, 50, int(*limit))
|
||||
}
|
||||
|
||||
func TestParseQueryHezEthAddr(t *testing.T) {
|
||||
name := "hermezEthereumAddress"
|
||||
c := &queryParser{}
|
||||
c.m = make(map[string]string)
|
||||
ethAddr := ethCommon.BigToAddress(big.NewInt(int64(347683)))
|
||||
// Not HEZ Eth addr
|
||||
c.m[name] = "hez:0xf"
|
||||
_, err := parseQueryHezEthAddr(c)
|
||||
assert.Error(t, err)
|
||||
c.m[name] = ethAddr.String()
|
||||
_, err = parseQueryHezEthAddr(c)
|
||||
assert.Error(t, err)
|
||||
c.m[name] = "hez:0xXX942cfcd25ad4d90a62358b0dd84f33b39826XX"
|
||||
_, err = parseQueryHezEthAddr(c)
|
||||
assert.Error(t, err)
|
||||
// Default
|
||||
c.m[name] = ""
|
||||
res, err := parseQueryHezEthAddr(c)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, res)
|
||||
// Correct
|
||||
c.m[name] = "hez:" + ethAddr.String()
|
||||
res, err = parseQueryHezEthAddr(c)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ethAddr, *res)
|
||||
}
|
||||
|
||||
func TestParseQueryBJJ(t *testing.T) {
|
||||
name := "BJJ"
|
||||
c := &queryParser{}
|
||||
c.m = make(map[string]string)
|
||||
privK := babyjub.NewRandPrivKey()
|
||||
pubK := privK.Public()
|
||||
pkComp := [32]byte(pubK.Compress())
|
||||
// Not HEZ Eth addr
|
||||
c.m[name] = "hez:abcd"
|
||||
_, err := parseQueryBJJ(c)
|
||||
assert.Error(t, err)
|
||||
c.m[name] = pubK.String()
|
||||
_, err = parseQueryBJJ(c)
|
||||
assert.Error(t, err)
|
||||
// Wrong checksum
|
||||
bjjSum := append(pkComp[:], byte(1))
|
||||
c.m[name] = "hez:" + base64.RawStdEncoding.EncodeToString(bjjSum)
|
||||
_, err = parseQueryBJJ(c)
|
||||
assert.Error(t, err)
|
||||
// Default
|
||||
c.m[name] = ""
|
||||
res, err := parseQueryBJJ(c)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, res)
|
||||
// Correct
|
||||
c.m[name] = bjjToString(pubK)
|
||||
res, err = parseQueryBJJ(c)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, *pubK, *res)
|
||||
}
|
||||
|
||||
func TestParseQueryTxType(t *testing.T) {
|
||||
name := "type"
|
||||
c := &queryParser{}
|
||||
c.m = make(map[string]string)
|
||||
// Incorrect values
|
||||
c.m[name] = "deposit"
|
||||
_, err := parseQueryTxType(c)
|
||||
assert.Error(t, err)
|
||||
c.m[name] = "1"
|
||||
_, err = parseQueryTxType(c)
|
||||
assert.Error(t, err)
|
||||
// Default
|
||||
c.m[name] = ""
|
||||
res, err := parseQueryTxType(c)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, res)
|
||||
// Correct values
|
||||
c.m[name] = string(common.TxTypeExit)
|
||||
res, err = parseQueryTxType(c)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, common.TxTypeExit, *res)
|
||||
c.m[name] = string(common.TxTypeWithdrawn)
|
||||
res, err = parseQueryTxType(c)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, common.TxTypeWithdrawn, *res)
|
||||
c.m[name] = string(common.TxTypeTransfer)
|
||||
res, err = parseQueryTxType(c)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, common.TxTypeTransfer, *res)
|
||||
c.m[name] = string(common.TxTypeDeposit)
|
||||
res, err = parseQueryTxType(c)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, common.TxTypeDeposit, *res)
|
||||
c.m[name] = string(common.TxTypeCreateAccountDeposit)
|
||||
res, err = parseQueryTxType(c)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, common.TxTypeCreateAccountDeposit, *res)
|
||||
c.m[name] = string(common.TxTypeCreateAccountDepositTransfer)
|
||||
res, err = parseQueryTxType(c)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, common.TxTypeCreateAccountDepositTransfer, *res)
|
||||
c.m[name] = string(common.TxTypeDepositTransfer)
|
||||
res, err = parseQueryTxType(c)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, common.TxTypeDepositTransfer, *res)
|
||||
c.m[name] = string(common.TxTypeForceTransfer)
|
||||
res, err = parseQueryTxType(c)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, common.TxTypeForceTransfer, *res)
|
||||
c.m[name] = string(common.TxTypeForceExit)
|
||||
res, err = parseQueryTxType(c)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, common.TxTypeForceExit, *res)
|
||||
c.m[name] = string(common.TxTypeTransferToEthAddr)
|
||||
res, err = parseQueryTxType(c)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, common.TxTypeTransferToEthAddr, *res)
|
||||
c.m[name] = string(common.TxTypeTransferToBJJ)
|
||||
res, err = parseQueryTxType(c)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, common.TxTypeTransferToBJJ, *res)
|
||||
}
|
||||
29
api/run.sh
Executable file
29
api/run.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
USAGE="Available options:
|
||||
doc Start documentation UI at http://loclahost:8001 and the mock up server at http://loclahost:4010
|
||||
mock Start the mock up server at http://loclahost:4010
|
||||
editor Start the documentation editor at http://loclahost:8002
|
||||
stop Stop all runing services started using this script
|
||||
help display this message"
|
||||
|
||||
case "$1" in
|
||||
doc)
|
||||
sudo docker-compose up -d hermez-api-doc hermez-api-mock && echo "\n\nStarted documentation UI at http://localhost:8001 and mockup server at http://localhost:4010"
|
||||
;;
|
||||
mock)
|
||||
sudo docker-compose up -d hermez-api-mock && echo "\n\nStarted mockup server at http://localhost:4010"
|
||||
;;
|
||||
editor)
|
||||
sudo docker-compose up -d hermez-api-editor hermez-api-mock && echo "\n\nStarted spec editor at http://localhost:8002 and mockup server at http://localhost:4010"
|
||||
;;
|
||||
stop)
|
||||
sudo docker-compose rm -sf && echo "\n\nStopped all the services initialized by this script"
|
||||
;;
|
||||
help)
|
||||
echo "$USAGE"
|
||||
;;
|
||||
*)
|
||||
echo "Invalid option.\n\n$USAGE"
|
||||
;;
|
||||
esac
|
||||
2266
api/swagger.yml
Normal file
2266
api/swagger.yml
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user