Add GET histroy-transactions endpointfeature/sql-semaphore1
@ -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"` |
||||
|
} |