hermez-node/api/api_test.go
api/api_test.goRaw
1package api
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io"
9 "io/ioutil"
10 "math/big"
11 "net/http"
12 "os"
13 "sort"
14 "strconv"
15 "testing"
16 "time"
17
18 ethCommon "github.com/ethereum/go-ethereum/common"
19 swagger "github.com/getkin/kin-openapi/openapi3filter"
20 "github.com/gin-gonic/gin"
21 "github.com/hermeznetwork/hermez-node/common"
22 "github.com/hermeznetwork/hermez-node/db/historydb"
23 "github.com/hermeznetwork/hermez-node/db/l2db"
24 "github.com/hermeznetwork/hermez-node/db/statedb"
25 "github.com/hermeznetwork/hermez-node/log"
26 "github.com/hermeznetwork/hermez-node/test"
27 "github.com/iden3/go-iden3-crypto/babyjub"
28 "github.com/jinzhu/copier"
29 "github.com/stretchr/testify/assert"
30)
31
32const apiPort = ":4010"
33const apiURL = "http://localhost" + apiPort + "/"
34
35type testCommon struct {
36 blocks []common.Block
37 tokens []common.Token
38 batches []common.Batch
39 usrAddr string
40 usrBjj string
41 accs []common.Account
42 usrTxs historyTxAPIs
43 othrTxs historyTxAPIs
44 allTxs historyTxAPIs
45 router *swagger.Router
46}
47
48type historyTxAPIs []historyTxAPI
49
50func (h historyTxAPIs) Len() int { return len(h) }
51func (h historyTxAPIs) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
52func (h historyTxAPIs) Less(i, j int) bool {
53 // i not forged yet
54 if h[i].BatchNum == nil {
55 if h[j].BatchNum != nil { // j is already forged
56 return false
57 }
58 // Both aren't forged, is i in a smaller position?
59 return h[i].Position < h[j].Position
60 }
61 // i is forged
62 if h[j].BatchNum == nil {
63 return true // j is not forged
64 }
65 // Both are forged
66 if *h[i].BatchNum == *h[j].BatchNum {
67 // At the same batch, is i in a smaller position?
68 return h[i].Position < h[j].Position
69 }
70 // At different batches, is i in a smaller batch?
71 return *h[i].BatchNum < *h[j].BatchNum
72}
73
74var tc testCommon
75
76func TestMain(m *testing.M) {
77 // Init swagger
78 router := swagger.NewRouter().WithSwaggerFromFile("./swagger.yml")
79 // Init DBs
80 pass := os.Getenv("POSTGRES_PASS")
81 hdb, err := historydb.NewHistoryDB(5432, "localhost", "hermez", pass, "history")
82 if err != nil {
83 panic(err)
84 }
85 // Reset DB
86 err = hdb.Reorg(-1)
87 if err != nil {
88 panic(err)
89 }
90 dir, err := ioutil.TempDir("", "tmpdb")
91 if err != nil {
92 panic(err)
93 }
94 sdb, err := statedb.NewStateDB(dir, false, 0)
95 if err != nil {
96 panic(err)
97 }
98 l2db, err := l2db.NewL2DB(5432, "localhost", "hermez", pass, "l2", 10, 512, 24*time.Hour)
99 if err != nil {
100 panic(err)
101 }
102 test.CleanL2DB(l2db.DB())
103 // Init API
104 api := gin.Default()
105 if err := SetAPIEndpoints(
106 true,
107 true,
108 api,
109 hdb,
110 sdb,
111 l2db,
112 ); err != nil {
113 panic(err)
114 }
115 // Start server
116 server := &http.Server{Addr: apiPort, Handler: api}
117 go func() {
118 if err := server.ListenAndServe(); err != nil &&
119 err != http.ErrServerClosed {
120 panic(err)
121 }
122 }()
123 // Populate DBs
124 // Clean DB
125 err = h.Reorg(0)
126 if err != nil {
127 panic(err)
128 }
129 // Gen blocks and add them to DB
130 const nBlocks = 5
131 blocks := test.GenBlocks(1, nBlocks+1)
132 err = h.AddBlocks(blocks)
133 if err != nil {
134 panic(err)
135 }
136 // Gen tokens and add them to DB
137 const nTokens = 10
138 tokens := test.GenTokens(nTokens, blocks)
139 err = h.AddTokens(tokens)
140 if err != nil {
141 panic(err)
142 }
143 // Gen batches and add them to DB
144 const nBatches = 10
145 batches := test.GenBatches(nBatches, blocks)
146 err = h.AddBatches(batches)
147 if err != nil {
148 panic(err)
149 }
150 // Gen accounts and add them to DB
151 const totalAccounts = 40
152 const userAccounts = 4
153 usrAddr := ethCommon.BigToAddress(big.NewInt(4896847))
154 privK := babyjub.NewRandPrivKey()
155 usrBjj := privK.Public()
156 accs := test.GenAccounts(totalAccounts, userAccounts, tokens, &usrAddr, usrBjj, batches)
157 err = h.AddAccounts(accs)
158 if err != nil {
159 panic(err)
160 }
161 // Gen L1Txs and add them to DB
162 const totalL1Txs = 40
163 const userL1Txs = 4
164 usrL1Txs, othrL1Txs := test.GenL1Txs(0, totalL1Txs, userL1Txs, &usrAddr, accs, tokens, blocks, batches)
165 var l1Txs []common.L1Tx
166 l1Txs = append(l1Txs, usrL1Txs...)
167 l1Txs = append(l1Txs, othrL1Txs...)
168 err = h.AddL1Txs(l1Txs)
169 if err != nil {
170 panic(err)
171 }
172 // Gen L2Txs and add them to DB
173 const totalL2Txs = 20
174 const userL2Txs = 4
175 usrL2Txs, othrL2Txs := test.GenL2Txs(totalL1Txs, totalL2Txs, userL2Txs, &usrAddr, accs, tokens, blocks, batches)
176 var l2Txs []common.L2Tx
177 l2Txs = append(l2Txs, usrL2Txs...)
178 l2Txs = append(l2Txs, othrL2Txs...)
179 err = h.AddL2Txs(l2Txs)
180 if err != nil {
181 panic(err)
182 }
183
184 // Set test commons
185 txsToAPITxs := func(l1Txs []common.L1Tx, l2Txs []common.L2Tx, blocks []common.Block, tokens []common.Token) historyTxAPIs {
186 // Transform L1Txs and L2Txs to generic Txs
187 genericTxs := []*common.Tx{}
188 for _, l1tx := range l1Txs {
189 genericTxs = append(genericTxs, l1tx.Tx())
190 }
191 for _, l2tx := range l2Txs {
192 genericTxs = append(genericTxs, l2tx.Tx())
193 }
194 // Transform generic Txs to HistoryTx
195 historyTxs := []*historydb.HistoryTx{}
196 for _, genericTx := range genericTxs {
197 // find timestamp
198 var timestamp time.Time
199 for i := 0; i < len(blocks); i++ {
200 if blocks[i].EthBlockNum == genericTx.EthBlockNum {
201 timestamp = blocks[i].Timestamp
202 break
203 }
204 }
205 // find token
206 token := common.Token{}
207 for i := 0; i < len(tokens); i++ {
208 if tokens[i].TokenID == genericTx.TokenID {
209 token = tokens[i]
210 break
211 }
212 }
213 historyTxs = append(historyTxs, &historydb.HistoryTx{
214 IsL1: genericTx.IsL1,
215 TxID: genericTx.TxID,
216 Type: genericTx.Type,
217 Position: genericTx.Position,
218 FromIdx: genericTx.FromIdx,
219 ToIdx: genericTx.ToIdx,
220 Amount: genericTx.Amount,
221 AmountFloat: genericTx.AmountFloat,
222 TokenID: genericTx.TokenID,
223 USD: token.USD * genericTx.AmountFloat,
224 BatchNum: genericTx.BatchNum,
225 EthBlockNum: genericTx.EthBlockNum,
226 ToForgeL1TxsNum: genericTx.ToForgeL1TxsNum,
227 UserOrigin: genericTx.UserOrigin,
228 FromEthAddr: genericTx.FromEthAddr,
229 FromBJJ: genericTx.FromBJJ,
230 LoadAmount: genericTx.LoadAmount,
231 LoadAmountFloat: genericTx.LoadAmountFloat,
232 LoadAmountUSD: token.USD * genericTx.LoadAmountFloat,
233 Fee: genericTx.Fee,
234 FeeUSD: genericTx.Fee.Percentage() * token.USD * genericTx.AmountFloat,
235 Nonce: genericTx.Nonce,
236 Timestamp: timestamp,
237 TokenSymbol: token.Symbol,
238 CurrentUSD: token.USD * genericTx.AmountFloat,
239 USDUpdate: token.USDUpdate,
240 })
241 }
242 return historyTxAPIs(historyTxsToAPI(historyTxs))
243 }
244 usrTxs := txsToAPITxs(usrL1Txs, usrL2Txs, blocks, tokens)
245 sort.Sort(usrTxs)
246 othrTxs := txsToAPITxs(othrL1Txs, othrL2Txs, blocks, tokens)
247 sort.Sort(othrTxs)
248 allTxs := append(usrTxs, othrTxs...)
249 sort.Sort(allTxs)
250 tc = testCommon{
251 blocks: blocks,
252 tokens: tokens,
253 batches: batches,
254 usrAddr: "hez:" + usrAddr.String(),
255 usrBjj: bjjToString(usrBjj),
256 accs: accs,
257 usrTxs: usrTxs,
258 othrTxs: othrTxs,
259 allTxs: allTxs,
260 router: router,
261 }
262 // Run tests
263 result := m.Run()
264 // Stop server
265 if err := server.Shutdown(context.Background()); err != nil {
266 panic(err)
267 }
268 if err := h.Close(); err != nil {
269 panic(err)
270 }
271 if err := l2.Close(); err != nil {
272 panic(err)
273 }
274 os.Exit(result)
275}
276
277func TestGetHistoryTxs(t *testing.T) {
278 endpoint := apiURL + "transactions-history"
279 fetchedTxs := historyTxAPIs{}
280 appendIter := func(intr interface{}) {
281 for i := 0; i < len(intr.(*historyTxsAPI).Txs); i++ {
282 tmp := &historyTxAPI{}
283 if err := copier.Copy(tmp, &intr.(*historyTxsAPI).Txs[i]); err != nil {
284 panic(err)
285 }
286 fetchedTxs = append(fetchedTxs, *tmp)
287 }
288 }
289 // Get all (no filters)
290 limit := 8
291 path := fmt.Sprintf("%s?limit=%d&offset=", endpoint, limit)
292 err := doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
293 assert.NoError(t, err)
294 assertHistoryTxAPIs(t, tc.allTxs, fetchedTxs)
295 // Get by ethAddr
296 fetchedTxs = historyTxAPIs{}
297 limit = 7
298 path = fmt.Sprintf(
299 "%s?hermezEthereumAddress=%s&limit=%d&offset=",
300 endpoint, tc.usrAddr, limit,
301 )
302 err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
303 assert.NoError(t, err)
304 assertHistoryTxAPIs(t, tc.usrTxs, fetchedTxs)
305 // Get by bjj
306 fetchedTxs = historyTxAPIs{}
307 limit = 6
308 path = fmt.Sprintf(
309 "%s?BJJ=%s&limit=%d&offset=",
310 endpoint, tc.usrBjj, limit,
311 )
312 err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
313 assert.NoError(t, err)
314 assertHistoryTxAPIs(t, tc.usrTxs, fetchedTxs)
315 // Get by tokenID
316 fetchedTxs = historyTxAPIs{}
317 limit = 5
318 tokenID := tc.allTxs[0].TokenID
319 path = fmt.Sprintf(
320 "%s?tokenId=%d&limit=%d&offset=",
321 endpoint, tokenID, limit,
322 )
323 err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
324 assert.NoError(t, err)
325 tokenIDTxs := historyTxAPIs{}
326 for i := 0; i < len(tc.allTxs); i++ {
327 if tc.allTxs[i].TokenID == tokenID {
328 tokenIDTxs = append(tokenIDTxs, tc.allTxs[i])
329 }
330 }
331 assertHistoryTxAPIs(t, tokenIDTxs, fetchedTxs)
332 // idx
333 fetchedTxs = historyTxAPIs{}
334 limit = 4
335 idx := tc.allTxs[0].FromIdx
336 path = fmt.Sprintf(
337 "%s?accountIndex=%s&limit=%d&offset=",
338 endpoint, idx, limit,
339 )
340 err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
341 assert.NoError(t, err)
342 idxTxs := historyTxAPIs{}
343 for i := 0; i < len(tc.allTxs); i++ {
344 if tc.allTxs[i].FromIdx == idx {
345 idxTxs = append(idxTxs, tc.allTxs[i])
346 }
347 }
348 assertHistoryTxAPIs(t, idxTxs, fetchedTxs)
349 // batchNum
350 fetchedTxs = historyTxAPIs{}
351 limit = 3
352 batchNum := tc.allTxs[0].BatchNum
353 path = fmt.Sprintf(
354 "%s?batchNum=%d&limit=%d&offset=",
355 endpoint, *batchNum, limit,
356 )
357 err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
358 assert.NoError(t, err)
359 batchNumTxs := historyTxAPIs{}
360 for i := 0; i < len(tc.allTxs); i++ {
361 if tc.allTxs[i].BatchNum != nil &&
362 *tc.allTxs[i].BatchNum == *batchNum {
363 batchNumTxs = append(batchNumTxs, tc.allTxs[i])
364 }
365 }
366 assertHistoryTxAPIs(t, batchNumTxs, fetchedTxs)
367 // type
368 txTypes := []common.TxType{
369 common.TxTypeExit,
370 common.TxTypeWithdrawn,
371 common.TxTypeTransfer,
372 common.TxTypeDeposit,
373 common.TxTypeCreateAccountDeposit,
374 common.TxTypeCreateAccountDepositTransfer,
375 common.TxTypeDepositTransfer,
376 common.TxTypeForceTransfer,
377 common.TxTypeForceExit,
378 common.TxTypeTransferToEthAddr,
379 common.TxTypeTransferToBJJ,
380 }
381 for _, txType := range txTypes {
382 fetchedTxs = historyTxAPIs{}
383 limit = 2
384 path = fmt.Sprintf(
385 "%s?type=%s&limit=%d&offset=",
386 endpoint, txType, limit,
387 )
388 err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
389 assert.NoError(t, err)
390 txTypeTxs := historyTxAPIs{}
391 for i := 0; i < len(tc.allTxs); i++ {
392 if tc.allTxs[i].Type == txType {
393 txTypeTxs = append(txTypeTxs, tc.allTxs[i])
394 }
395 }
396 assertHistoryTxAPIs(t, txTypeTxs, fetchedTxs)
397 }
398 // Multiple filters
399 fetchedTxs = historyTxAPIs{}
400 limit = 1
401 path = fmt.Sprintf(
402 "%s?batchNum=%d&tokeId=%d&limit=%d&offset=",
403 endpoint, *batchNum, tokenID, limit,
404 )
405 err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
406 assert.NoError(t, err)
407 mixedTxs := historyTxAPIs{}
408 for i := 0; i < len(tc.allTxs); i++ {
409 if tc.allTxs[i].BatchNum != nil {
410 if *tc.allTxs[i].BatchNum == *batchNum && tc.allTxs[i].TokenID == tokenID {
411 mixedTxs = append(mixedTxs, tc.allTxs[i])
412 }
413 }
414 }
415 assertHistoryTxAPIs(t, mixedTxs, fetchedTxs)
416 // All, in reverse order
417 fetchedTxs = historyTxAPIs{}
418 limit = 5
419 path = fmt.Sprintf("%s?", endpoint)
420 appendIterRev := func(intr interface{}) {
421 tmpAll := historyTxAPIs{}
422 for i := 0; i < len(intr.(*historyTxsAPI).Txs); i++ {
423 tmpItem := &historyTxAPI{}
424 if err := copier.Copy(tmpItem, &intr.(*historyTxsAPI).Txs[i]); err != nil {
425 panic(err)
426 }
427 tmpAll = append(tmpAll, *tmpItem)
428 }
429 fetchedTxs = append(tmpAll, fetchedTxs...)
430 }
431 err = doGoodReqPaginatedReverse(path, &historyTxsAPI{}, appendIterRev, limit)
432 assert.NoError(t, err)
433 assertHistoryTxAPIs(t, tc.allTxs, fetchedTxs)
434 // 400
435 path = fmt.Sprintf(
436 "%s?accountIndex=%s&hermezEthereumAddress=%s",
437 endpoint, idx, tc.usrAddr,
438 )
439 err = doBadReq("GET", path, nil, 400)
440 assert.NoError(t, err)
441 path = fmt.Sprintf("%s?tokenId=X", endpoint)
442 err = doBadReq("GET", path, nil, 400)
443 assert.NoError(t, err)
444 // 404
445 path = fmt.Sprintf("%s?batchNum=999999", endpoint)
446 err = doBadReq("GET", path, nil, 404)
447 assert.NoError(t, err)
448 path = fmt.Sprintf("%s?limit=1000&offset=1000", endpoint)
449 err = doBadReq("GET", path, nil, 404)
450 assert.NoError(t, err)
451}
452
453func assertHistoryTxAPIs(t *testing.T, expected, actual historyTxAPIs) {
454 assert.Equal(t, len(expected), len(actual))
455 for i := 0; i < len(actual); i++ { //nolint len(actual) won't change within the loop
456 assert.Equal(t, expected[i].Timestamp.Unix(), actual[i].Timestamp.Unix())
457 expected[i].Timestamp = actual[i].Timestamp
458 assert.Equal(t, expected[i].USDUpdate.Unix(), actual[i].USDUpdate.Unix())
459 expected[i].USDUpdate = actual[i].USDUpdate
460 if expected[i].L2Info != nil {
461 if expected[i].L2Info.FeeUSD > actual[i].L2Info.FeeUSD {
462 assert.Less(t, 0.999, actual[i].L2Info.FeeUSD/expected[i].L2Info.FeeUSD)
463 } else if expected[i].L2Info.FeeUSD < actual[i].L2Info.FeeUSD {
464 assert.Less(t, 0.999, expected[i].L2Info.FeeUSD/actual[i].L2Info.FeeUSD)
465 }
466 expected[i].L2Info.FeeUSD = actual[i].L2Info.FeeUSD
467 }
468 assert.Equal(t, expected[i], actual[i])
469 }
470}
471
472func doGoodReqPaginated(
473 path string,
474 iterStruct paginationer,
475 appendIter func(res interface{}),
476) error {
477 next := 0
478 for {
479 // Call API to get this iteration items
480 if err := doGoodReq("GET", path+strconv.Itoa(next), nil, iterStruct); err != nil {
481 return err
482 }
483 appendIter(iterStruct)
484 // Keep iterating?
485 pag := iterStruct.GetPagination()
486 if pag.LastReturnedItem == pag.TotalItems-1 { // No
487 break
488 } else { // Yes
489 next = int(pag.LastReturnedItem + 1)
490 }
491 }
492 return nil
493}
494
495func doGoodReqPaginatedReverse(
496 path string,
497 iterStruct paginationer,
498 appendIter func(res interface{}),
499 limit int,
500) error {
501 next := 0
502 first := true
503 for {
504 // Call API to get this iteration items
505 if first {
506 first = false
507 pagQuery := fmt.Sprintf("last=true&limit=%d", limit)
508 if err := doGoodReq("GET", path+pagQuery, nil, iterStruct); err != nil {
509 return err
510 }
511 } else {
512 pagQuery := fmt.Sprintf("offset=%d&limit=%d", next, limit)
513 if err := doGoodReq("GET", path+pagQuery, nil, iterStruct); err != nil {
514 return err
515 }
516 }
517 appendIter(iterStruct)
518 // Keep iterating?
519 pag := iterStruct.GetPagination()
520 if iterStruct.Len() == pag.TotalItems || pag.LastReturnedItem-iterStruct.Len() == -1 { // No
521 break
522 } else { // Yes
523 prevOffset := next
524 next = pag.LastReturnedItem - iterStruct.Len() - limit + 1
525 if next < 0 {
526 next = 0
527 limit = prevOffset
528 }
529 }
530 }
531 return nil
532}
533
534func doGoodReq(method, path string, reqBody io.Reader, returnStruct interface{}) error {
535 ctx := context.Background()
536 client := &http.Client{}
537 httpReq, _ := http.NewRequest(method, path, reqBody)
538 route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
539 if err != nil {
540 return err
541 }
542 // Validate request against swagger spec
543 requestValidationInput := &swagger.RequestValidationInput{
544 Request: httpReq,
545 PathParams: pathParams,
546 Route: route,
547 }
548 if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
549 return err
550 }
551 // Do API call
552 resp, err := client.Do(httpReq)
553 if err != nil {
554 return err
555 }
556 if resp.Body == nil {
557 return errors.New("Nil body")
558 }
559 //nolint
560 defer resp.Body.Close()
561 body, err := ioutil.ReadAll(resp.Body)
562 if err != nil {
563 return err
564 }
565 if resp.StatusCode != 200 {
566 return fmt.Errorf("%d response: %s", resp.StatusCode, string(body))
567 }
568 // Unmarshal body into return struct
569 if err := json.Unmarshal(body, returnStruct); err != nil {
570 return err
571 }
572 // Validate response against swagger spec
573 responseValidationInput := &swagger.ResponseValidationInput{
574 RequestValidationInput: requestValidationInput,
575 Status: resp.StatusCode,
576 Header: resp.Header,
577 }
578 responseValidationInput = responseValidationInput.SetBodyBytes(body)
579 return swagger.ValidateResponse(ctx, responseValidationInput)
580}
581
582func doBadReq(method, path string, reqBody io.Reader, expectedResponseCode int) error {
583 ctx := context.Background()
584 client := &http.Client{}
585 httpReq, _ := http.NewRequest(method, path, reqBody)
586 route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
587 if err != nil {
588 return err
589 }
590 // Validate request against swagger spec
591 requestValidationInput := &swagger.RequestValidationInput{
592 Request: httpReq,
593 PathParams: pathParams,
594 Route: route,
595 }
596 if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
597 if expectedResponseCode != 400 {
598 return err
599 }
600 log.Warn("The request does not match the API spec")
601 }
602 // Do API call
603 resp, err := client.Do(httpReq)
604 if err != nil {
605 return err
606 }
607 if resp.Body == nil {
608 return errors.New("Nil body")
609 }
610 //nolint
611 defer resp.Body.Close()
612 body, err := ioutil.ReadAll(resp.Body)
613 if err != nil {
614 return err
615 }
616 if resp.StatusCode != expectedResponseCode {
617 return fmt.Errorf("Unexpected response code: %d", resp.StatusCode)
618 }
619 // Validate response against swagger spec
620 responseValidationInput := &swagger.ResponseValidationInput{
621 RequestValidationInput: requestValidationInput,
622 Status: resp.StatusCode,
623 Header: resp.Header,
624 }
625 responseValidationInput = responseValidationInput.SetBodyBytes(body)
626 return swagger.ValidateResponse(ctx, responseValidationInput)
627}