@ -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). |
@ -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 |
|||
} |
@ -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) |
|||
} |
@ -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) |
|||
} |
@ -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 |
@ -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(), |
|||
}) |
|||
} |
@ -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 |
|||
} |
@ -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) |
|||
} |
@ -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 |
@ -0,0 +1,46 @@ |
|||
package historydb |
|||
|
|||
import ( |
|||
"math/big" |
|||
"time" |
|||
|
|||
ethCommon "github.com/ethereum/go-ethereum/common" |
|||
"github.com/hermeznetwork/hermez-node/common" |
|||
"github.com/iden3/go-iden3-crypto/babyjub" |
|||
) |
|||
|
|||
// HistoryTx is a representation of a generic Tx with additional information
|
|||
// required by the API, and extracted by joining block and token tables
|
|||
type HistoryTx struct { |
|||
// Generic
|
|||
IsL1 bool `meddler:"is_l1"` |
|||
TxID common.TxID `meddler:"id"` |
|||
Type common.TxType `meddler:"type"` |
|||
Position int `meddler:"position"` |
|||
FromIdx common.Idx `meddler:"from_idx"` |
|||
ToIdx common.Idx `meddler:"to_idx"` |
|||
Amount *big.Int `meddler:"amount,bigint"` |
|||
AmountFloat float64 `meddler:"amount_f"` |
|||
TokenID common.TokenID `meddler:"token_id"` |
|||
USD float64 `meddler:"amount_usd,zeroisnull"` |
|||
BatchNum common.BatchNum `meddler:"batch_num,zeroisnull"` // batchNum in which this tx was forged. If the tx is L2, this must be != 0
|
|||
EthBlockNum int64 `meddler:"eth_block_num"` // Ethereum Block Number in which this L1Tx was added to the queue
|
|||
// L1
|
|||
ToForgeL1TxsNum int64 `meddler:"to_forge_l1_txs_num"` // toForgeL1TxsNum in which the tx was forged / will be forged
|
|||
UserOrigin bool `meddler:"user_origin"` // true if the tx was originated by a user, false if it was aoriginated by a coordinator. Note that this differ from the spec for implementation simplification purpposes
|
|||
FromEthAddr ethCommon.Address `meddler:"from_eth_addr"` |
|||
FromBJJ *babyjub.PublicKey `meddler:"from_bjj"` |
|||
LoadAmount *big.Int `meddler:"load_amount,bigintnull"` |
|||
LoadAmountFloat float64 `meddler:"load_amount_f"` |
|||
LoadAmountUSD float64 `meddler:"load_amount_usd,zeroisnull"` |
|||
// L2
|
|||
Fee common.FeeSelector `meddler:"fee,zeroisnull"` |
|||
FeeUSD float64 `meddler:"fee_usd,zeroisnull"` |
|||
Nonce common.Nonce `meddler:"nonce,zeroisnull"` |
|||
// API extras
|
|||
Timestamp time.Time `meddler:"timestamp,utctime"` |
|||
TokenSymbol string `meddler:"symbol"` |
|||
CurrentUSD float64 `meddler:"current_usd"` |
|||
USDUpdate time.Time `meddler:"usd_update,utctime"` |
|||
TotalItems int `meddler:"total_items"` |
|||
} |