From 233ecc47046dc6eaad2ed39158f1d513c37da1be Mon Sep 17 00:00:00 2001 From: Arnau B Date: Fri, 30 Oct 2020 12:55:30 +0100 Subject: [PATCH] Refactor api exits --- api/accountcreationauths.go | 8 +- api/api_test.go | 218 +--------------------------- api/dbtoapistructs.go | 85 ----------- api/exits.go | 72 ++++++++++ api/exits_test.go | 276 ++++++++++++++++++++++++++++++++++++ api/handlers.go | 61 -------- api/txshistory_test.go | 8 +- apitypes/apitypes.go | 27 ++-- db/historydb/historydb.go | 36 +++-- db/historydb/views.go | 67 +++++++-- db/l2db/views.go | 1 + 11 files changed, 452 insertions(+), 407 deletions(-) create mode 100644 api/exits.go create mode 100644 api/exits_test.go diff --git a/api/accountcreationauths.go b/api/accountcreationauths.go index 90e36ce..0eb6706 100644 --- a/api/accountcreationauths.go +++ b/api/accountcreationauths.go @@ -52,10 +52,10 @@ func getAccountCreationAuth(c *gin.Context) { } type receivedAuth struct { - EthAddr apitypes.StrHezEthAddr `json:"hezEthereumAddress" binding:"required"` - BJJ apitypes.StrHezBJJ `json:"bjj" binding:"required"` - Signature apitypes.StrEthSignature `json:"signature" binding:"required"` - Timestamp time.Time `json:"timestamp"` + EthAddr apitypes.StrHezEthAddr `json:"hezEthereumAddress" binding:"required"` + BJJ apitypes.StrHezBJJ `json:"bjj" binding:"required"` + Signature apitypes.EthSignature `json:"signature" binding:"required"` + Timestamp time.Time `json:"timestamp"` } func accountCreationAuthAPIToCommon(apiAuth *receivedAuth) *common.AccountCreationAuth { diff --git a/api/api_test.go b/api/api_test.go index 568c1de..d824809 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -27,9 +27,7 @@ import ( "github.com/hermeznetwork/hermez-node/log" "github.com/hermeznetwork/hermez-node/test" "github.com/iden3/go-iden3-crypto/babyjub" - "github.com/mitchellh/copystructure" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) const apiPort = ":4010" @@ -46,8 +44,8 @@ type testCommon struct { accs []common.Account usrTxs []testTx allTxs []testTx - exits []exitAPI - usrExits []exitAPI + exits []testExit + usrExits []testExit poolTxsToSend []testPoolTxSend poolTxsToReceive []testPoolTxReceive auths []testAuth @@ -286,42 +284,6 @@ func TestMain(m *testing.M) { } } - // Transform exits to API - exitsToAPIExits := func(exits []common.ExitInfo, accs []common.Account, tokens []common.Token) []exitAPI { - historyExits := []historydb.HistoryExit{} - for _, exit := range exits { - token := getTokenByIdx(exit.AccountIdx, tokensUSD, accs) - historyExits = append(historyExits, historydb.HistoryExit{ - BatchNum: exit.BatchNum, - AccountIdx: exit.AccountIdx, - MerkleProof: exit.MerkleProof, - Balance: exit.Balance, - InstantWithdrawn: exit.InstantWithdrawn, - DelayedWithdrawRequest: exit.DelayedWithdrawRequest, - DelayedWithdrawn: exit.DelayedWithdrawn, - TokenID: token.TokenID, - TokenEthBlockNum: token.EthBlockNum, - TokenEthAddr: token.EthAddr, - TokenName: token.Name, - TokenSymbol: token.Symbol, - TokenDecimals: token.Decimals, - TokenUSD: token.USD, - TokenUSDUpdate: token.USDUpdate, - }) - } - return historyExitsToAPI(historyExits) - } - apiExits := exitsToAPIExits(exits, accs, tokens) - // sort.Sort(apiExits) - usrExits := []exitAPI{} - for _, exit := range apiExits { - for _, idx := range usrIdxs { - if idx == exit.AccountIdx { - usrExits = append(usrExits, exit) - } - } - } - // Coordinators const nCoords = 10 coords := test.GenCoordinators(nCoords, blocks) @@ -348,6 +310,7 @@ func TestMain(m *testing.M) { usrTxs, allTxs := genTestTxs(sortedTxs, usrIdxs, accs, tokensUSD, blocks) poolTxsToSend, poolTxsToReceive := genTestPoolTx(accs, []babyjub.PrivateKey{privK}, tokensUSD) // NOTE: pool txs are not inserted to the DB here. In the test they will be posted and getted. testBatches, fullBatches := genTestBatches(blocks, batches, allTxs) + usrExits, allExits := genTestExits(exits, tokensUSD, accs, usrIdxs) tc = testCommon{ blocks: blocks, tokens: tokensUSD, @@ -359,7 +322,7 @@ func TestMain(m *testing.M) { accs: accs, usrTxs: usrTxs, allTxs: allTxs, - exits: apiExits, + exits: allExits, usrExits: usrExits, poolTxsToSend: poolTxsToSend, poolTxsToReceive: poolTxsToReceive, @@ -390,179 +353,6 @@ func TestMain(m *testing.M) { os.Exit(result) } -func TestGetExits(t *testing.T) { - endpoint := apiURL + "exits" - fetchedExits := []exitAPI{} - appendIter := func(intr interface{}) { - for i := 0; i < len(intr.(*exitsAPI).Exits); i++ { - tmp, err := copystructure.Copy(intr.(*exitsAPI).Exits[i]) - if err != nil { - panic(err) - } - fetchedExits = append(fetchedExits, tmp.(exitAPI)) - } - } - // Get all (no filters) - limit := 8 - path := fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit) - err := doGoodReqPaginated(path, historydb.OrderAsc, &exitsAPI{}, appendIter) - assert.NoError(t, err) - assertExitAPIs(t, tc.exits, fetchedExits) - - // Get by ethAddr - fetchedExits = []exitAPI{} - limit = 7 - path = fmt.Sprintf( - "%s?hermezEthereumAddress=%s&limit=%d&fromItem=", - endpoint, tc.usrAddr, limit, - ) - err = doGoodReqPaginated(path, historydb.OrderAsc, &exitsAPI{}, appendIter) - assert.NoError(t, err) - assertExitAPIs(t, tc.usrExits, fetchedExits) - // Get by bjj - fetchedExits = []exitAPI{} - limit = 6 - path = fmt.Sprintf( - "%s?BJJ=%s&limit=%d&fromItem=", - endpoint, tc.usrBjj, limit, - ) - err = doGoodReqPaginated(path, historydb.OrderAsc, &exitsAPI{}, appendIter) - assert.NoError(t, err) - assertExitAPIs(t, tc.usrExits, fetchedExits) - // Get by tokenID - fetchedExits = []exitAPI{} - limit = 5 - tokenID := tc.exits[0].Token.TokenID - path = fmt.Sprintf( - "%s?tokenId=%d&limit=%d&fromItem=", - endpoint, tokenID, limit, - ) - err = doGoodReqPaginated(path, historydb.OrderAsc, &exitsAPI{}, appendIter) - assert.NoError(t, err) - tokenIDExits := []exitAPI{} - for i := 0; i < len(tc.exits); i++ { - if tc.exits[i].Token.TokenID == tokenID { - tokenIDExits = append(tokenIDExits, tc.exits[i]) - } - } - assertExitAPIs(t, tokenIDExits, fetchedExits) - // idx - fetchedExits = []exitAPI{} - limit = 4 - idx := tc.exits[0].AccountIdx - path = fmt.Sprintf( - "%s?accountIndex=%s&limit=%d&fromItem=", - endpoint, idx, limit, - ) - err = doGoodReqPaginated(path, historydb.OrderAsc, &exitsAPI{}, appendIter) - assert.NoError(t, err) - idxExits := []exitAPI{} - for i := 0; i < len(tc.exits); i++ { - if tc.exits[i].AccountIdx[6:] == idx[6:] { - idxExits = append(idxExits, tc.exits[i]) - } - } - assertExitAPIs(t, idxExits, fetchedExits) - // batchNum - fetchedExits = []exitAPI{} - limit = 3 - batchNum := tc.exits[0].BatchNum - path = fmt.Sprintf( - "%s?batchNum=%d&limit=%d&fromItem=", - endpoint, batchNum, limit, - ) - err = doGoodReqPaginated(path, historydb.OrderAsc, &exitsAPI{}, appendIter) - assert.NoError(t, err) - batchNumExits := []exitAPI{} - for i := 0; i < len(tc.exits); i++ { - if tc.exits[i].BatchNum == batchNum { - batchNumExits = append(batchNumExits, tc.exits[i]) - } - } - assertExitAPIs(t, batchNumExits, fetchedExits) - // Multiple filters - fetchedExits = []exitAPI{} - limit = 1 - path = fmt.Sprintf( - "%s?batchNum=%d&tokeId=%d&limit=%d&fromItem=", - endpoint, batchNum, tokenID, limit, - ) - err = doGoodReqPaginated(path, historydb.OrderAsc, &exitsAPI{}, appendIter) - assert.NoError(t, err) - mixedExits := []exitAPI{} - flipedExits := []exitAPI{} - for i := 0; i < len(tc.exits); i++ { - if tc.exits[i].BatchNum == batchNum && tc.exits[i].Token.TokenID == tokenID { - mixedExits = append(mixedExits, tc.exits[i]) - } - flipedExits = append(flipedExits, tc.exits[len(tc.exits)-1-i]) - } - assertExitAPIs(t, mixedExits, fetchedExits) - // All, in reverse order - fetchedExits = []exitAPI{} - limit = 5 - path = fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit) - err = doGoodReqPaginated(path, historydb.OrderDesc, &exitsAPI{}, appendIter) - assert.NoError(t, err) - assertExitAPIs(t, flipedExits, fetchedExits) - // 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&fromItem=999999", endpoint) - err = doBadReq("GET", path, nil, 404) - assert.NoError(t, err) -} - -func TestGetExit(t *testing.T) { - // Get all txs by their ID - endpoint := apiURL + "exits/" - fetchedExits := []exitAPI{} - for _, exit := range tc.exits { - fetchedExit := exitAPI{} - assert.NoError( - t, doGoodReq( - "GET", - fmt.Sprintf("%s%d/%s", endpoint, exit.BatchNum, exit.AccountIdx), - nil, &fetchedExit, - ), - ) - fetchedExits = append(fetchedExits, fetchedExit) - } - assertExitAPIs(t, tc.exits, fetchedExits) - // 400 - err := doBadReq("GET", endpoint+"1/haz:BOOM:1", nil, 400) - assert.NoError(t, err) - err = doBadReq("GET", endpoint+"-1/hez:BOOM:1", nil, 400) - assert.NoError(t, err) - // 404 - err = doBadReq("GET", endpoint+"494/hez:XXX:1", nil, 404) - assert.NoError(t, err) -} - -func assertExitAPIs(t *testing.T, expected, actual []exitAPI) { - require.Equal(t, len(expected), len(actual)) - for i := 0; i < len(actual); i++ { //nolint len(actual) won't change within the loop - actual[i].ItemID = 0 - if expected[i].Token.USDUpdate == nil { - assert.Equal(t, expected[i].Token.USDUpdate, actual[i].Token.USDUpdate) - } else { - assert.Equal(t, expected[i].Token.USDUpdate.Unix(), actual[i].Token.USDUpdate.Unix()) - expected[i].Token.USDUpdate = actual[i].Token.USDUpdate - } - assert.Equal(t, expected[i], actual[i]) - } -} func TestGetConfig(t *testing.T) { endpoint := apiURL + "config" var configTest configAPI diff --git a/api/dbtoapistructs.go b/api/dbtoapistructs.go index fc47bdd..3f1db97 100644 --- a/api/dbtoapistructs.go +++ b/api/dbtoapistructs.go @@ -7,8 +7,6 @@ import ( ethCommon "github.com/ethereum/go-ethereum/common" "github.com/hermeznetwork/hermez-node/common" - "github.com/hermeznetwork/hermez-node/db" - "github.com/hermeznetwork/hermez-node/db/historydb" "github.com/hermeznetwork/hermez-node/eth" "github.com/iden3/go-iden3-crypto/babyjub" ) @@ -40,89 +38,6 @@ func idxToHez(idx common.Idx, tokenSymbol string) string { return "hez:" + tokenSymbol + ":" + strconv.Itoa(int(idx)) } -// Exit - -type exitsAPI struct { - Exits []exitAPI `json:"exits"` - Pagination *db.Pagination `json:"pagination"` -} - -func (e *exitsAPI) GetPagination() *db.Pagination { - if e.Exits[0].ItemID < e.Exits[len(e.Exits)-1].ItemID { - e.Pagination.FirstReturnedItem = e.Exits[0].ItemID - e.Pagination.LastReturnedItem = e.Exits[len(e.Exits)-1].ItemID - } else { - e.Pagination.LastReturnedItem = e.Exits[0].ItemID - e.Pagination.FirstReturnedItem = e.Exits[len(e.Exits)-1].ItemID - } - return e.Pagination -} -func (e *exitsAPI) Len() int { return len(e.Exits) } - -type merkleProofAPI struct { - Root string - Siblings []string - OldKey string - OldValue string - IsOld0 bool - Key string - Value string - Fnc int -} - -type exitAPI struct { - ItemID int `json:"itemId"` - BatchNum common.BatchNum `json:"batchNum"` - AccountIdx string `json:"accountIndex"` - MerkleProof merkleProofAPI `json:"merkleProof"` - Balance string `json:"balance"` - InstantWithdrawn *int64 `json:"instantWithdrawn"` - DelayedWithdrawRequest *int64 `json:"delayedWithdrawRequest"` - DelayedWithdrawn *int64 `json:"delayedWithdrawn"` - Token historydb.TokenWithUSD `json:"token"` -} - -func historyExitsToAPI(dbExits []historydb.HistoryExit) []exitAPI { - apiExits := []exitAPI{} - for i := 0; i < len(dbExits); i++ { - exit := exitAPI{ - ItemID: dbExits[i].ItemID, - BatchNum: dbExits[i].BatchNum, - AccountIdx: idxToHez(dbExits[i].AccountIdx, dbExits[i].TokenSymbol), - MerkleProof: merkleProofAPI{ - Root: dbExits[i].MerkleProof.Root.String(), - OldKey: dbExits[i].MerkleProof.OldKey.String(), - OldValue: dbExits[i].MerkleProof.OldValue.String(), - IsOld0: dbExits[i].MerkleProof.IsOld0, - Key: dbExits[i].MerkleProof.Key.String(), - Value: dbExits[i].MerkleProof.Value.String(), - Fnc: dbExits[i].MerkleProof.Fnc, - }, - Balance: dbExits[i].Balance.String(), - InstantWithdrawn: dbExits[i].InstantWithdrawn, - DelayedWithdrawRequest: dbExits[i].DelayedWithdrawRequest, - DelayedWithdrawn: dbExits[i].DelayedWithdrawn, - Token: historydb.TokenWithUSD{ - TokenID: dbExits[i].TokenID, - EthBlockNum: dbExits[i].TokenEthBlockNum, - EthAddr: dbExits[i].TokenEthAddr, - Name: dbExits[i].TokenName, - Symbol: dbExits[i].TokenSymbol, - Decimals: dbExits[i].TokenDecimals, - USD: dbExits[i].TokenUSD, - USDUpdate: dbExits[i].TokenUSDUpdate, - }, - } - siblings := []string{} - for j := 0; j < len(dbExits[i].MerkleProof.Siblings); j++ { - siblings = append(siblings, dbExits[i].MerkleProof.Siblings[j].String()) - } - exit.MerkleProof.Siblings = siblings - apiExits = append(apiExits, exit) - } - return apiExits -} - // Config type rollupConstants struct { diff --git a/api/exits.go b/api/exits.go new file mode 100644 index 0000000..76f64d6 --- /dev/null +++ b/api/exits.go @@ -0,0 +1,72 @@ +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/hermeznetwork/hermez-node/db" + "github.com/hermeznetwork/hermez-node/db/historydb" +) + +func getExits(c *gin.Context) { + // Get query parameters + // Account filters + tokenID, addr, bjj, idx, err := parseAccountFilters(c) + if err != nil { + retBadReq(err, c) + return + } + // BatchNum + batchNum, err := parseQueryUint("batchNum", nil, 0, maxUint32, c) + if err != nil { + retBadReq(err, c) + return + } + // Pagination + fromItem, order, limit, err := parsePagination(c) + if err != nil { + retBadReq(err, c) + return + } + + // Fetch exits from historyDB + exits, pagination, err := h.GetExitsAPI( + addr, bjj, tokenID, idx, batchNum, fromItem, limit, order, + ) + if err != nil { + retSQLErr(err, c) + return + } + + // Build succesfull response + type exitsResponse struct { + Exits []historydb.ExitAPI `json:"exits"` + Pagination *db.Pagination `json:"pagination"` + } + c.JSON(http.StatusOK, &exitsResponse{ + Exits: exits, + Pagination: pagination, + }) +} + +func getExit(c *gin.Context) { + // Get batchNum and accountIndex + batchNum, err := parseParamUint("batchNum", nil, 0, maxUint32, c) + if err != nil { + retBadReq(err, c) + return + } + idx, err := parseParamIdx(c) + if err != nil { + retBadReq(err, c) + return + } + // Fetch tx from historyDB + exit, err := h.GetExitAPI(batchNum, idx) + if err != nil { + retSQLErr(err, c) + return + } + // Build succesfull response + c.JSON(http.StatusOK, exit) +} diff --git a/api/exits_test.go b/api/exits_test.go new file mode 100644 index 0000000..5cbf82a --- /dev/null +++ b/api/exits_test.go @@ -0,0 +1,276 @@ +package api + +import ( + "fmt" + "testing" + + "github.com/hermeznetwork/hermez-node/common" + "github.com/hermeznetwork/hermez-node/db" + "github.com/hermeznetwork/hermez-node/db/historydb" + "github.com/mitchellh/copystructure" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testCVP struct { + Root string + Siblings []string + OldKey string + OldValue string + IsOld0 bool + Key string + Value string + Fnc int +} + +type testExit struct { + ItemID int `json:"itemId"` + BatchNum common.BatchNum `json:"batchNum"` + AccountIdx string `json:"accountIndex"` + MerkleProof testCVP `json:"merkleProof"` + Balance string `json:"balance"` + InstantWithdrawn *int64 `json:"instantWithdrawn"` + DelayedWithdrawRequest *int64 `json:"delayedWithdrawRequest"` + DelayedWithdrawn *int64 `json:"delayedWithdrawn"` + Token historydb.TokenWithUSD `json:"token"` +} + +type testExitsResponse struct { + Exits []testExit `json:"exits"` + Pagination *db.Pagination `json:"pagination"` +} + +func (t *testExitsResponse) GetPagination() *db.Pagination { + if t.Exits[0].ItemID < t.Exits[len(t.Exits)-1].ItemID { + t.Pagination.FirstReturnedItem = t.Exits[0].ItemID + t.Pagination.LastReturnedItem = t.Exits[len(t.Exits)-1].ItemID + } else { + t.Pagination.LastReturnedItem = t.Exits[0].ItemID + t.Pagination.FirstReturnedItem = t.Exits[len(t.Exits)-1].ItemID + } + return t.Pagination +} + +func (t *testExitsResponse) Len() int { + return len(t.Exits) +} + +func genTestExits( + commonExits []common.ExitInfo, + tokens []historydb.TokenWithUSD, + accs []common.Account, + usrIdxs []string, +) (usrExits, allExits []testExit) { + allExits = []testExit{} + for _, exit := range commonExits { + token := getTokenByIdx(exit.AccountIdx, tokens, accs) + siblings := []string{} + for i := 0; i < len(exit.MerkleProof.Siblings); i++ { + siblings = append(siblings, exit.MerkleProof.Siblings[i].String()) + } + allExits = append(allExits, testExit{ + BatchNum: exit.BatchNum, + AccountIdx: idxToHez(exit.AccountIdx, token.Symbol), + MerkleProof: testCVP{ + Root: exit.MerkleProof.Root.String(), + Siblings: siblings, + OldKey: exit.MerkleProof.OldKey.String(), + OldValue: exit.MerkleProof.OldValue.String(), + IsOld0: exit.MerkleProof.IsOld0, + Key: exit.MerkleProof.Key.String(), + Value: exit.MerkleProof.Value.String(), + Fnc: exit.MerkleProof.Fnc, + }, + Balance: exit.Balance.String(), + InstantWithdrawn: exit.InstantWithdrawn, + DelayedWithdrawRequest: exit.DelayedWithdrawRequest, + DelayedWithdrawn: exit.DelayedWithdrawn, + Token: token, + }) + } + usrExits = []testExit{} + for _, exit := range allExits { + for _, idx := range usrIdxs { + if idx == exit.AccountIdx { + usrExits = append(usrExits, exit) + break + } + } + } + return usrExits, allExits +} + +func TestGetExits(t *testing.T) { + endpoint := apiURL + "exits" + fetchedExits := []testExit{} + appendIter := func(intr interface{}) { + for i := 0; i < len(intr.(*testExitsResponse).Exits); i++ { + tmp, err := copystructure.Copy(intr.(*testExitsResponse).Exits[i]) + if err != nil { + panic(err) + } + fetchedExits = append(fetchedExits, tmp.(testExit)) + } + } + // Get all (no filters) + limit := 8 + path := fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit) + err := doGoodReqPaginated(path, historydb.OrderAsc, &testExitsResponse{}, appendIter) + assert.NoError(t, err) + assertExitAPIs(t, tc.exits, fetchedExits) + + // Get by ethAddr + fetchedExits = []testExit{} + limit = 7 + path = fmt.Sprintf( + "%s?hermezEthereumAddress=%s&limit=%d&fromItem=", + endpoint, tc.usrAddr, limit, + ) + err = doGoodReqPaginated(path, historydb.OrderAsc, &testExitsResponse{}, appendIter) + assert.NoError(t, err) + assertExitAPIs(t, tc.usrExits, fetchedExits) + // Get by bjj + fetchedExits = []testExit{} + limit = 6 + path = fmt.Sprintf( + "%s?BJJ=%s&limit=%d&fromItem=", + endpoint, tc.usrBjj, limit, + ) + err = doGoodReqPaginated(path, historydb.OrderAsc, &testExitsResponse{}, appendIter) + assert.NoError(t, err) + assertExitAPIs(t, tc.usrExits, fetchedExits) + // Get by tokenID + fetchedExits = []testExit{} + limit = 5 + tokenID := tc.exits[0].Token.TokenID + path = fmt.Sprintf( + "%s?tokenId=%d&limit=%d&fromItem=", + endpoint, tokenID, limit, + ) + err = doGoodReqPaginated(path, historydb.OrderAsc, &testExitsResponse{}, appendIter) + assert.NoError(t, err) + tokenIDExits := []testExit{} + for i := 0; i < len(tc.exits); i++ { + if tc.exits[i].Token.TokenID == tokenID { + tokenIDExits = append(tokenIDExits, tc.exits[i]) + } + } + assertExitAPIs(t, tokenIDExits, fetchedExits) + // idx + fetchedExits = []testExit{} + limit = 4 + idx := tc.exits[0].AccountIdx + path = fmt.Sprintf( + "%s?accountIndex=%s&limit=%d&fromItem=", + endpoint, idx, limit, + ) + err = doGoodReqPaginated(path, historydb.OrderAsc, &testExitsResponse{}, appendIter) + assert.NoError(t, err) + idxExits := []testExit{} + for i := 0; i < len(tc.exits); i++ { + if tc.exits[i].AccountIdx[6:] == idx[6:] { + idxExits = append(idxExits, tc.exits[i]) + } + } + assertExitAPIs(t, idxExits, fetchedExits) + // batchNum + fetchedExits = []testExit{} + limit = 3 + batchNum := tc.exits[0].BatchNum + path = fmt.Sprintf( + "%s?batchNum=%d&limit=%d&fromItem=", + endpoint, batchNum, limit, + ) + err = doGoodReqPaginated(path, historydb.OrderAsc, &testExitsResponse{}, appendIter) + assert.NoError(t, err) + batchNumExits := []testExit{} + for i := 0; i < len(tc.exits); i++ { + if tc.exits[i].BatchNum == batchNum { + batchNumExits = append(batchNumExits, tc.exits[i]) + } + } + assertExitAPIs(t, batchNumExits, fetchedExits) + // Multiple filters + fetchedExits = []testExit{} + limit = 1 + path = fmt.Sprintf( + "%s?batchNum=%d&tokeId=%d&limit=%d&fromItem=", + endpoint, batchNum, tokenID, limit, + ) + err = doGoodReqPaginated(path, historydb.OrderAsc, &testExitsResponse{}, appendIter) + assert.NoError(t, err) + mixedExits := []testExit{} + flipedExits := []testExit{} + for i := 0; i < len(tc.exits); i++ { + if tc.exits[i].BatchNum == batchNum && tc.exits[i].Token.TokenID == tokenID { + mixedExits = append(mixedExits, tc.exits[i]) + } + flipedExits = append(flipedExits, tc.exits[len(tc.exits)-1-i]) + } + assertExitAPIs(t, mixedExits, fetchedExits) + // All, in reverse order + fetchedExits = []testExit{} + limit = 5 + path = fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit) + err = doGoodReqPaginated(path, historydb.OrderDesc, &testExitsResponse{}, appendIter) + assert.NoError(t, err) + assertExitAPIs(t, flipedExits, fetchedExits) + // 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&fromItem=999999", endpoint) + err = doBadReq("GET", path, nil, 404) + assert.NoError(t, err) +} + +func TestGetExit(t *testing.T) { + // Get all txs by their ID + endpoint := apiURL + "exits/" + fetchedExits := []testExit{} + for _, exit := range tc.exits { + fetchedExit := testExit{} + assert.NoError( + t, doGoodReq( + "GET", + fmt.Sprintf("%s%d/%s", endpoint, exit.BatchNum, exit.AccountIdx), + nil, &fetchedExit, + ), + ) + fetchedExits = append(fetchedExits, fetchedExit) + } + assertExitAPIs(t, tc.exits, fetchedExits) + // 400 + err := doBadReq("GET", endpoint+"1/haz:BOOM:1", nil, 400) + assert.NoError(t, err) + err = doBadReq("GET", endpoint+"-1/hez:BOOM:1", nil, 400) + assert.NoError(t, err) + // 404 + err = doBadReq("GET", endpoint+"494/hez:XXX:1", nil, 404) + assert.NoError(t, err) +} + +func assertExitAPIs(t *testing.T, expected, actual []testExit) { + require.Equal(t, len(expected), len(actual)) + for i := 0; i < len(actual); i++ { //nolint len(actual) won't change within the loop + actual[i].ItemID = 0 + actual[i].Token.ItemID = 0 + if expected[i].Token.USDUpdate == nil { + assert.Equal(t, expected[i].Token.USDUpdate, actual[i].Token.USDUpdate) + } else { + assert.Equal(t, expected[i].Token.USDUpdate.Unix(), actual[i].Token.USDUpdate.Unix()) + expected[i].Token.USDUpdate = actual[i].Token.USDUpdate + } + assert.Equal(t, expected[i], actual[i]) + } +} diff --git a/api/handlers.go b/api/handlers.go index 6415463..60b59ff 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -36,67 +36,6 @@ func getAccount(c *gin.Context) { } -func getExits(c *gin.Context) { - // Get query parameters - // Account filters - tokenID, addr, bjj, idx, err := parseAccountFilters(c) - if err != nil { - retBadReq(err, c) - return - } - // BatchNum - batchNum, err := parseQueryUint("batchNum", nil, 0, maxUint32, c) - if err != nil { - retBadReq(err, c) - return - } - // Pagination - fromItem, order, limit, err := parsePagination(c) - if err != nil { - retBadReq(err, c) - return - } - - // Fetch exits from historyDB - exits, pagination, err := h.GetExits( - addr, bjj, tokenID, idx, batchNum, fromItem, limit, order, - ) - if err != nil { - retSQLErr(err, c) - return - } - - // Build succesfull response - apiExits := historyExitsToAPI(exits) - c.JSON(http.StatusOK, &exitsAPI{ - Exits: apiExits, - Pagination: pagination, - }) -} - -func getExit(c *gin.Context) { - // Get batchNum and accountIndex - batchNum, err := parseParamUint("batchNum", nil, 0, maxUint32, c) - if err != nil { - retBadReq(err, c) - return - } - idx, err := parseParamIdx(c) - if err != nil { - retBadReq(err, c) - return - } - // Fetch tx from historyDB - exit, err := h.GetExit(batchNum, idx) - if err != nil { - retSQLErr(err, c) - return - } - apiExits := historyExitsToAPI([]historydb.HistoryExit{*exit}) - // Build succesfull response - c.JSON(http.StatusOK, apiExits[0]) -} - func getSlots(c *gin.Context) { } diff --git a/api/txshistory_test.go b/api/txshistory_test.go index 31e5c3c..3d02bb0 100644 --- a/api/txshistory_test.go +++ b/api/txshistory_test.go @@ -152,7 +152,13 @@ func (tx *wrappedL2) L2() *common.L2Tx { return &l2tx } -func genTestTxs(genericTxs []txSortFielder, usrIdxs []string, accs []common.Account, tokens []historydb.TokenWithUSD, blocks []common.Block) (usrTxs []testTx, allTxs []testTx) { +func genTestTxs( + genericTxs []txSortFielder, + usrIdxs []string, + accs []common.Account, + tokens []historydb.TokenWithUSD, + blocks []common.Block, +) (usrTxs []testTx, allTxs []testTx) { usrTxs = []testTx{} allTxs = []testTx{} isUsrTx := func(tx testTx) bool { diff --git a/apitypes/apitypes.go b/apitypes/apitypes.go index 6d90291..65d6499 100644 --- a/apitypes/apitypes.go +++ b/apitypes/apitypes.go @@ -156,20 +156,6 @@ func (s *StrHezEthAddr) UnmarshalText(text []byte) error { return nil } -// StrEthSignature is used to unmarshal EthSignature directly into an alias of []byte -type StrEthSignature []byte - -// UnmarshalText unmarshals a StrEthSignature -func (s *StrEthSignature) UnmarshalText(text []byte) error { - without0x := strings.TrimPrefix(string(text), "0x") - signature, err := hex.DecodeString(without0x) - if err != nil { - return err - } - *s = signature - return nil -} - // HezBJJ is used to scan/value *babyjub.PublicKey directly into strings that follow the BJJ public key hez fotmat (^hez:[A-Za-z0-9_-]{44}$) from/to sql DBs. // It assumes that *babyjub.PublicKey are inserted/fetched to/from the DB using the default Scan/Value interface type HezBJJ string @@ -277,7 +263,7 @@ func (s *StrHezIdx) UnmarshalText(text []byte) error { return nil } -// EthSignature is used to scan/value []byte representing an Ethereum signatue directly into strings from/to sql DBs. +// EthSignature is used to scan/value []byte representing an Ethereum signature directly into strings from/to sql DBs. type EthSignature string // NewEthSignature creates a *EthSignature from []byte @@ -311,3 +297,14 @@ func (e EthSignature) Value() (driver.Value, error) { without0x := strings.TrimPrefix(string(e), "0x") return hex.DecodeString(without0x) } + +// UnmarshalText unmarshals a StrEthSignature +func (e *EthSignature) UnmarshalText(text []byte) error { + without0x := strings.TrimPrefix(string(text), "0x") + signature, err := hex.DecodeString(without0x) + if err != nil { + return err + } + *e = EthSignature([]byte(signature)) + return nil +} diff --git a/db/historydb/historydb.go b/db/historydb/historydb.go index aba4a3f..0c5d1c8 100644 --- a/db/historydb/historydb.go +++ b/db/historydb/historydb.go @@ -891,12 +891,17 @@ func (hdb *HistoryDB) GetAllExits() ([]common.ExitInfo, error) { return db.SlicePtrsToSlice(exits).([]common.ExitInfo), err } -// GetExit returns a exit from the DB -func (hdb *HistoryDB) GetExit(batchNum *uint, idx *common.Idx) (*HistoryExit, error) { - exit := &HistoryExit{} +// GetExitAPI returns a exit from the DB +func (hdb *HistoryDB) GetExitAPI(batchNum *uint, idx *common.Idx) (*ExitAPI, error) { + exit := &ExitAPI{} err := meddler.QueryRow( - hdb.db, exit, `SELECT exit_tree.*, token.token_id, token.eth_block_num AS token_block, - token.eth_addr, token.name, token.symbol, token.decimals, token.usd, token.usd_update + hdb.db, exit, `SELECT exit_tree.item_id, exit_tree.batch_num, + hez_idx(exit_tree.account_idx, token.symbol) AS account_idx, + exit_tree.merkle_proof, exit_tree.balance, exit_tree.instant_withdrawn, + exit_tree.delayed_withdraw_request, exit_tree.delayed_withdrawn, + token.token_id, token.item_id AS token_item_id, + token.eth_block_num AS token_block, token.eth_addr, token.name, token.symbol, + token.decimals, token.usd, token.usd_update FROM exit_tree INNER JOIN account ON exit_tree.account_idx = account.idx INNER JOIN token ON account.token_id = token.token_id WHERE exit_tree.batch_num = $1 AND exit_tree.account_idx = $2;`, batchNum, idx, @@ -904,20 +909,25 @@ func (hdb *HistoryDB) GetExit(batchNum *uint, idx *common.Idx) (*HistoryExit, er return exit, err } -// GetExits returns a list of exits from the DB and pagination info -func (hdb *HistoryDB) GetExits( +// GetExitsAPI returns a list of exits from the DB and pagination info +func (hdb *HistoryDB) GetExitsAPI( ethAddr *ethCommon.Address, bjj *babyjub.PublicKey, tokenID *common.TokenID, idx *common.Idx, batchNum *uint, fromItem, limit *uint, order string, -) ([]HistoryExit, *db.Pagination, error) { +) ([]ExitAPI, *db.Pagination, error) { if ethAddr != nil && bjj != nil { return nil, nil, errors.New("ethAddr and bjj are incompatible") } var query string var args []interface{} - queryStr := `SELECT exit_tree.*, token.token_id, token.eth_block_num AS token_block, - token.eth_addr, token.name, token.symbol, token.decimals, token.usd, - token.usd_update, COUNT(*) OVER() AS total_items, MIN(exit_tree.item_id) OVER() AS first_item, MAX(exit_tree.item_id) OVER() AS last_item + queryStr := `SELECT exit_tree.item_id, exit_tree.batch_num, + hez_idx(exit_tree.account_idx, token.symbol) AS account_idx, + exit_tree.merkle_proof, exit_tree.balance, exit_tree.instant_withdrawn, + exit_tree.delayed_withdraw_request, exit_tree.delayed_withdrawn, + token.token_id, token.item_id AS token_item_id, + token.eth_block_num AS token_block, token.eth_addr, token.name, token.symbol, + token.decimals, token.usd, token.usd_update, COUNT(*) OVER() AS total_items, + MIN(exit_tree.item_id) OVER() AS first_item, MAX(exit_tree.item_id) OVER() AS last_item FROM exit_tree INNER JOIN account ON exit_tree.account_idx = account.idx INNER JOIN token ON account.token_id = token.token_id ` // Apply filters @@ -989,14 +999,14 @@ func (hdb *HistoryDB) GetExits( queryStr += fmt.Sprintf("LIMIT %d;", *limit) query = hdb.db.Rebind(queryStr) // log.Debug(query) - exits := []*HistoryExit{} + exits := []*ExitAPI{} if err := meddler.QueryAll(hdb.db, &exits, query, args...); err != nil { return nil, nil, err } if len(exits) == 0 { return nil, nil, sql.ErrNoRows } - return db.SlicePtrsToSlice(exits).([]HistoryExit), &db.Pagination{ + return db.SlicePtrsToSlice(exits).([]ExitAPI), &db.Pagination{ TotalItems: exits[0].TotalItems, FirstItem: exits[0].FirstItem, LastItem: exits[0].LastItem, diff --git a/db/historydb/views.go b/db/historydb/views.go index 80c0180..dd23a00 100644 --- a/db/historydb/views.go +++ b/db/historydb/views.go @@ -151,14 +151,14 @@ type TokenWithUSD struct { LastItem int `json:"-" meddler:"last_item"` } -// HistoryExit is a representation of a exit with additional information +// ExitAPI is a representation of a exit with additional information // required by the API, and extracted by joining token table -type HistoryExit struct { +type ExitAPI struct { ItemID int `meddler:"item_id"` BatchNum common.BatchNum `meddler:"batch_num"` - AccountIdx common.Idx `meddler:"account_idx"` + AccountIdx apitypes.HezIdx `meddler:"account_idx"` MerkleProof *merkletree.CircomVerifierProof `meddler:"merkle_proof,json"` - Balance *big.Int `meddler:"balance,bigint"` + Balance apitypes.BigIntStr `meddler:"balance"` InstantWithdrawn *int64 `meddler:"instant_withdrawn"` DelayedWithdrawRequest *int64 `meddler:"delayed_withdraw_request"` DelayedWithdrawn *int64 `meddler:"delayed_withdrawn"` @@ -166,6 +166,7 @@ type HistoryExit struct { FirstItem int `meddler:"first_item"` LastItem int `meddler:"last_item"` TokenID common.TokenID `meddler:"token_id"` + TokenItemID int `meddler:"token_item_id"` TokenEthBlockNum int64 `meddler:"token_block"` TokenEthAddr ethCommon.Address `meddler:"eth_addr"` TokenName string `meddler:"name"` @@ -175,6 +176,45 @@ type HistoryExit struct { TokenUSDUpdate *time.Time `meddler:"usd_update"` } +// MarshalJSON is used to neast some of the fields of ExitAPI +// without the need of auxiliar structs +func (e ExitAPI) MarshalJSON() ([]byte, error) { + siblings := []string{} + for i := 0; i < len(e.MerkleProof.Siblings); i++ { + siblings = append(siblings, e.MerkleProof.Siblings[i].String()) + } + return json.Marshal(map[string]interface{}{ + "itemId": e.ItemID, + "batchNum": e.BatchNum, + "accountIndex": e.AccountIdx, + "merkleProof": map[string]interface{}{ + "Root": e.MerkleProof.Root.String(), + "Siblings": siblings, + "OldKey": e.MerkleProof.OldKey.String(), + "OldValue": e.MerkleProof.OldValue.String(), + "IsOld0": e.MerkleProof.IsOld0, + "Key": e.MerkleProof.Key.String(), + "Value": e.MerkleProof.Value.String(), + "Fnc": e.MerkleProof.Fnc, + }, + "balance": e.Balance, + "instantWithdrawn": e.InstantWithdrawn, + "delayedWithdrawRequest": e.DelayedWithdrawRequest, + "delayedWithdrawn": e.DelayedWithdrawn, + "token": map[string]interface{}{ + "id": e.TokenID, + "itemId": e.TokenItemID, + "ethereumBlockNum": e.TokenEthBlockNum, + "ethereumAddress": e.TokenEthAddr, + "name": e.TokenName, + "symbol": e.TokenSymbol, + "decimals": e.TokenDecimals, + "USD": e.TokenUSD, + "fiatUpdate": e.TokenUSDUpdate, + }, + }) +} + // CoordinatorAPI is a representation of a coordinator with additional information // required by the API type CoordinatorAPI struct { @@ -198,16 +238,15 @@ type BatchAPI struct { Timestamp time.Time `json:"timestamp" meddler:"timestamp,utctime"` ForgerAddr ethCommon.Address `json:"forgerAddr" meddler:"forger_addr"` CollectedFees apitypes.CollectedFees `json:"collectedFees" meddler:"fees_collected,json"` - // CollectedFees map[common.TokenID]*big.Int `json:"collectedFees" meddler:"fees_collected,json"` - TotalFeesUSD *float64 `json:"historicTotalCollectedFeesUSD" meddler:"total_fees_usd"` - StateRoot apitypes.BigIntStr `json:"stateRoot" meddler:"state_root"` - NumAccounts int `json:"numAccounts" meddler:"num_accounts"` - ExitRoot apitypes.BigIntStr `json:"exitRoot" meddler:"exit_root"` - ForgeL1TxsNum *int64 `json:"forgeL1TransactionsNum" meddler:"forge_l1_txs_num"` - SlotNum int64 `json:"slotNum" meddler:"slot_num"` - TotalItems int `json:"-" meddler:"total_items"` - FirstItem int `json:"-" meddler:"first_item"` - LastItem int `json:"-" meddler:"last_item"` + TotalFeesUSD *float64 `json:"historicTotalCollectedFeesUSD" meddler:"total_fees_usd"` + StateRoot apitypes.BigIntStr `json:"stateRoot" meddler:"state_root"` + NumAccounts int `json:"numAccounts" meddler:"num_accounts"` + ExitRoot apitypes.BigIntStr `json:"exitRoot" meddler:"exit_root"` + ForgeL1TxsNum *int64 `json:"forgeL1TransactionsNum" meddler:"forge_l1_txs_num"` + SlotNum int64 `json:"slotNum" meddler:"slot_num"` + TotalItems int `json:"-" meddler:"total_items"` + FirstItem int `json:"-" meddler:"first_item"` + LastItem int `json:"-" meddler:"last_item"` } // Network define status of the network diff --git a/db/l2db/views.go b/db/l2db/views.go index b86c34e..29263aa 100644 --- a/db/l2db/views.go +++ b/db/l2db/views.go @@ -115,6 +115,7 @@ func (tx PoolTxAPI) MarshalJSON() ([]byte, error) { }) } +// AccountCreationAuthAPI represents an account creation auth in the expected format by the API type AccountCreationAuthAPI struct { EthAddr apitypes.HezEthAddr `json:"hezEthereumAddress" meddler:"eth_addr" ` BJJ apitypes.HezBJJ `json:"bjj" meddler:"bjj" `