Add GET histroy-transactions endpoint

This commit is contained in:
Arnau B
2020-09-21 00:11:20 +02:00
parent 35a558f6c9
commit 85fe885265
21 changed files with 4114 additions and 88 deletions

19
api/README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff