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