hermez-node/api/txshistory_test.go
api/txshistory_test.goRaw
1package api
2
3import (
4 "fmt"
5 "math"
6 "math/big"
7 "testing"
8 "time"
9
10 "github.com/hermeznetwork/hermez-node/apitypes"
11 "github.com/hermeznetwork/hermez-node/common"
12 "github.com/hermeznetwork/hermez-node/db"
13 "github.com/hermeznetwork/hermez-node/db/historydb"
14 "github.com/hermeznetwork/hermez-node/test"
15 "github.com/mitchellh/copystructure"
16 "github.com/stretchr/testify/assert"
17 "github.com/stretchr/testify/require"
18)
19
20type testL1Info struct {
21 ToForgeL1TxsNum *int64 `json:"toForgeL1TransactionsNum"`
22 UserOrigin bool `json:"userOrigin"`
23 LoadAmount string `json:"loadAmount"`
24 HistoricLoadAmountUSD *float64 `json:"historicLoadAmountUSD"`
25 EthBlockNum int64 `json:"ethereumBlockNum"`
26}
27
28type testL2Info struct {
29 Fee common.FeeSelector `json:"fee"`
30 HistoricFeeUSD *float64 `json:"historicFeeUSD"`
31 Nonce common.Nonce `json:"nonce"`
32}
33
34type testTx struct {
35 IsL1 string `json:"L1orL2"`
36 TxID common.TxID `json:"id"`
37 ItemID int `json:"itemId"`
38 Type common.TxType `json:"type"`
39 Position int `json:"position"`
40 FromIdx *string `json:"fromAccountIndex"`
41 FromEthAddr *string `json:"fromHezEthereumAddress"`
42 FromBJJ *string `json:"fromBJJ"`
43 ToIdx string `json:"toAccountIndex"`
44 ToEthAddr *string `json:"toHezEthereumAddress"`
45 ToBJJ *string `json:"toBJJ"`
46 Amount string `json:"amount"`
47 BatchNum *common.BatchNum `json:"batchNum"`
48 HistoricUSD *float64 `json:"historicUSD"`
49 Timestamp time.Time `json:"timestamp"`
50 L1Info *testL1Info `json:"L1Info"`
51 L2Info *testL2Info `json:"L2Info"`
52 Token historydb.TokenWithUSD `json:"token"`
53}
54
55type testTxsResponse struct {
56 Txs []testTx `json:"transactions"`
57 Pagination *db.Pagination `json:"pagination"`
58}
59
60func (t testTxsResponse) GetPagination() *db.Pagination {
61 if t.Txs[0].ItemID < t.Txs[len(t.Txs)-1].ItemID {
62 t.Pagination.FirstReturnedItem = t.Txs[0].ItemID
63 t.Pagination.LastReturnedItem = t.Txs[len(t.Txs)-1].ItemID
64 } else {
65 t.Pagination.LastReturnedItem = t.Txs[0].ItemID
66 t.Pagination.FirstReturnedItem = t.Txs[len(t.Txs)-1].ItemID
67 }
68 return t.Pagination
69}
70
71func (t testTxsResponse) Len() int {
72 return len(t.Txs)
73}
74
75// TxSortFields represents the fields needed to sort L1 and L2 transactions
76type txSortFields struct {
77 BatchNum *common.BatchNum
78 Position int
79}
80
81// TxSortFielder is a interface that allows sorting L1 and L2 transactions in a combined way
82type txSortFielder interface {
83 SortFields() txSortFields
84 L1() *common.L1Tx
85 L2() *common.L2Tx
86}
87
88// TxsSort array of TxSortFielder
89type txsSort []txSortFielder
90
91func (t txsSort) Len() int { return len(t) }
92func (t txsSort) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
93func (t txsSort) Less(i, j int) bool {
94 // i not forged yet
95 isf := t[i].SortFields()
96 jsf := t[j].SortFields()
97 if isf.BatchNum == nil {
98 if jsf.BatchNum != nil { // j is already forged
99 return false
100 }
101 // Both aren't forged, is i in a smaller position?
102 return isf.Position < jsf.Position
103 }
104 // i is forged
105 if jsf.BatchNum == nil {
106 return false // j is not forged
107 }
108 // Both are forged
109 if *isf.BatchNum == *jsf.BatchNum {
110 // At the same batch, is i in a smaller position?
111 return isf.Position < jsf.Position
112 }
113 // At different batches, is i in a smaller batch?
114 return *isf.BatchNum < *jsf.BatchNum
115}
116
117type wrappedL1 common.L1Tx
118
119// SortFields implements TxSortFielder
120func (tx *wrappedL1) SortFields() txSortFields {
121 return txSortFields{
122 BatchNum: tx.BatchNum,
123 Position: tx.Position,
124 }
125}
126
127// L1 implements TxSortFielder
128func (tx *wrappedL1) L1() *common.L1Tx {
129 l1tx := common.L1Tx(*tx)
130 return &l1tx
131}
132
133// L2 implements TxSortFielder
134func (tx *wrappedL1) L2() *common.L2Tx { return nil }
135
136type wrappedL2 common.L2Tx
137
138// SortFields implements TxSortFielder
139func (tx *wrappedL2) SortFields() txSortFields {
140 return txSortFields{
141 BatchNum: &tx.BatchNum,
142 Position: tx.Position,
143 }
144}
145
146// L1 implements TxSortFielder
147func (tx *wrappedL2) L1() *common.L1Tx { return nil }
148
149// L2 implements TxSortFielder
150func (tx *wrappedL2) L2() *common.L2Tx {
151 l2tx := common.L2Tx(*tx)
152 return &l2tx
153}
154
155func genTestTxs(
156 genericTxs []txSortFielder,
157 usrIdxs []string,
158 accs []common.Account,
159 tokens []historydb.TokenWithUSD,
160 blocks []common.Block,
161) (usrTxs []testTx, allTxs []testTx) {
162 usrTxs = []testTx{}
163 allTxs = []testTx{}
164 isUsrTx := func(tx testTx) bool {
165 for _, idx := range usrIdxs {
166 if tx.FromIdx != nil && *tx.FromIdx == idx {
167 return true
168 }
169 if tx.ToIdx == idx {
170 return true
171 }
172 }
173 return false
174 }
175 for _, genericTx := range genericTxs {
176 l1 := genericTx.L1()
177 l2 := genericTx.L2()
178 if l1 != nil { // L1Tx to testTx
179 token := getTokenByID(l1.TokenID, tokens)
180 // l1.FromEthAddr and l1.FromBJJ can't be nil
181 fromEthAddr := string(apitypes.NewHezEthAddr(l1.FromEthAddr))
182 fromBJJ := string(apitypes.NewHezBJJ(l1.FromBJJ))
183 tx := testTx{
184 IsL1: "L1",
185 TxID: l1.TxID,
186 Type: l1.Type,
187 Position: l1.Position,
188 FromEthAddr: &fromEthAddr,
189 FromBJJ: &fromBJJ,
190 ToIdx: idxToHez(l1.ToIdx, token.Symbol),
191 Amount: l1.Amount.String(),
192 BatchNum: l1.BatchNum,
193 Timestamp: getTimestamp(l1.EthBlockNum, blocks),
194 L1Info: &testL1Info{
195 ToForgeL1TxsNum: l1.ToForgeL1TxsNum,
196 UserOrigin: l1.UserOrigin,
197 LoadAmount: l1.LoadAmount.String(),
198 EthBlockNum: l1.EthBlockNum,
199 },
200 Token: token,
201 }
202 // If FromIdx is not nil
203 if l1.FromIdx != 0 {
204 idxStr := idxToHez(l1.FromIdx, token.Symbol)
205 tx.FromIdx = &idxStr
206 }
207 // If tx has a normal ToIdx (>255), set FromEthAddr and FromBJJ
208 if l1.ToIdx >= common.UserThreshold {
209 // find account
210 for _, acc := range accs {
211 if l1.ToIdx == acc.Idx {
212 toEthAddr := string(apitypes.NewHezEthAddr(acc.EthAddr))
213 tx.ToEthAddr = &toEthAddr
214 toBJJ := string(apitypes.NewHezBJJ(acc.PublicKey))
215 tx.ToBJJ = &toBJJ
216 break
217 }
218 }
219 }
220 // If the token has USD value setted
221 if token.USD != nil {
222 af := new(big.Float).SetInt(l1.Amount)
223 amountFloat, _ := af.Float64()
224 usd := *token.USD * amountFloat / math.Pow(10, float64(token.Decimals))
225 tx.HistoricUSD = &usd
226 laf := new(big.Float).SetInt(l1.LoadAmount)
227 loadAmountFloat, _ := laf.Float64()
228 loadUSD := *token.USD * loadAmountFloat / math.Pow(10, float64(token.Decimals))
229 tx.L1Info.HistoricLoadAmountUSD = &loadUSD
230 }
231 allTxs = append(allTxs, tx)
232 if isUsrTx(tx) {
233 usrTxs = append(usrTxs, tx)
234 }
235 } else { // L2Tx to testTx
236 token := getTokenByIdx(l2.FromIdx, tokens, accs)
237 // l1.FromIdx can't be nil
238 fromIdx := idxToHez(l2.FromIdx, token.Symbol)
239 tx := testTx{
240 IsL1: "L2",
241 TxID: l2.TxID,
242 Type: l2.Type,
243 Position: l2.Position,
244 ToIdx: idxToHez(l2.ToIdx, token.Symbol),
245 FromIdx: &fromIdx,
246 Amount: l2.Amount.String(),
247 BatchNum: &l2.BatchNum,
248 Timestamp: getTimestamp(l2.EthBlockNum, blocks),
249 L2Info: &testL2Info{
250 Nonce: l2.Nonce,
251 Fee: l2.Fee,
252 },
253 Token: token,
254 }
255 // If FromIdx is not nil
256 if l2.FromIdx != 0 {
257 idxStr := idxToHez(l2.FromIdx, token.Symbol)
258 tx.FromIdx = &idxStr
259 }
260 // Set FromEthAddr and FromBJJ (FromIdx it's always >255)
261 for _, acc := range accs {
262 if l2.ToIdx == acc.Idx {
263 fromEthAddr := string(apitypes.NewHezEthAddr(acc.EthAddr))
264 tx.FromEthAddr = &fromEthAddr
265 fromBJJ := string(apitypes.NewHezBJJ(acc.PublicKey))
266 tx.FromBJJ = &fromBJJ
267 break
268 }
269 }
270 // If tx has a normal ToIdx (>255), set FromEthAddr and FromBJJ
271 if l2.ToIdx >= common.UserThreshold {
272 // find account
273 for _, acc := range accs {
274 if l2.ToIdx == acc.Idx {
275 toEthAddr := string(apitypes.NewHezEthAddr(acc.EthAddr))
276 tx.ToEthAddr = &toEthAddr
277 toBJJ := string(apitypes.NewHezBJJ(acc.PublicKey))
278 tx.ToBJJ = &toBJJ
279 break
280 }
281 }
282 }
283 // If the token has USD value setted
284 if token.USD != nil {
285 af := new(big.Float).SetInt(l2.Amount)
286 amountFloat, _ := af.Float64()
287 usd := *token.USD * amountFloat / math.Pow(10, float64(token.Decimals))
288 tx.HistoricUSD = &usd
289 feeUSD := usd * l2.Fee.Percentage()
290 tx.HistoricUSD = &usd
291 tx.L2Info.HistoricFeeUSD = &feeUSD
292 }
293 allTxs = append(allTxs, tx)
294 if isUsrTx(tx) {
295 usrTxs = append(usrTxs, tx)
296 }
297 }
298 }
299 return usrTxs, allTxs
300}
301
302func TestGetHistoryTxs(t *testing.T) {
303 endpoint := apiURL + "transactions-history"
304 fetchedTxs := []testTx{}
305 appendIter := func(intr interface{}) {
306 for i := 0; i < len(intr.(*testTxsResponse).Txs); i++ {
307 tmp, err := copystructure.Copy(intr.(*testTxsResponse).Txs[i])
308 if err != nil {
309 panic(err)
310 }
311 fetchedTxs = append(fetchedTxs, tmp.(testTx))
312 }
313 }
314 // Get all (no filters)
315 limit := 8
316 path := fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit)
317 err := doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
318 assert.NoError(t, err)
319 assertTxs(t, tc.allTxs, fetchedTxs)
320 // Uncomment once tx generation for tests is fixed
321 // // Get by ethAddr
322 // fetchedTxs = []testTx{}
323 // limit = 7
324 // path = fmt.Sprintf(
325 // "%s?hermezEthereumAddress=%s&limit=%d&fromItem=",
326 // endpoint, tc.usrAddr, limit,
327 // )
328 // err = doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
329 // assert.NoError(t, err)
330 // assertTxs(t, tc.usrTxs, fetchedTxs)
331 // // Get by bjj
332 // fetchedTxs = []testTx{}
333 // limit = 6
334 // path = fmt.Sprintf(
335 // "%s?BJJ=%s&limit=%d&fromItem=",
336 // endpoint, tc.usrBjj, limit,
337 // )
338 // err = doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
339 // assert.NoError(t, err)
340 // assertTxs(t, tc.usrTxs, fetchedTxs)
341 // Get by tokenID
342 fetchedTxs = []testTx{}
343 limit = 5
344 tokenID := tc.allTxs[0].Token.TokenID
345 path = fmt.Sprintf(
346 "%s?tokenId=%d&limit=%d&fromItem=",
347 endpoint, tokenID, limit,
348 )
349 err = doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
350 assert.NoError(t, err)
351 tokenIDTxs := []testTx{}
352 for i := 0; i < len(tc.allTxs); i++ {
353 if tc.allTxs[i].Token.TokenID == tokenID {
354 tokenIDTxs = append(tokenIDTxs, tc.allTxs[i])
355 }
356 }
357 assertTxs(t, tokenIDTxs, fetchedTxs)
358 // // idx
359 // fetchedTxs = []testTx{}
360 // limit = 4
361 idx := tc.allTxs[0].ToIdx
362 // path = fmt.Sprintf(
363 // "%s?accountIndex=%s&limit=%d&fromItem=",
364 // endpoint, idx, limit,
365 // )
366 // err = doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
367 // assert.NoError(t, err)
368 // idxTxs := []testTx{}
369 // for i := 0; i < len(tc.allTxs); i++ {
370 // if (tc.allTxs[i].FromIdx != nil && (*tc.allTxs[i].FromIdx)[6:] == idx[6:]) ||
371 // tc.allTxs[i].ToIdx[6:] == idx[6:] {
372 // idxTxs = append(idxTxs, tc.allTxs[i])
373 // }
374 // }
375 // assertHistoryTxAPIs(t, idxTxs, fetchedTxs)
376 // batchNum
377 fetchedTxs = []testTx{}
378 limit = 3
379 batchNum := tc.allTxs[0].BatchNum
380 path = fmt.Sprintf(
381 "%s?batchNum=%d&limit=%d&fromItem=",
382 endpoint, *batchNum, limit,
383 )
384 err = doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
385 assert.NoError(t, err)
386 batchNumTxs := []testTx{}
387 for i := 0; i < len(tc.allTxs); i++ {
388 if tc.allTxs[i].BatchNum != nil &&
389 *tc.allTxs[i].BatchNum == *batchNum {
390 batchNumTxs = append(batchNumTxs, tc.allTxs[i])
391 }
392 }
393 assertTxs(t, batchNumTxs, fetchedTxs)
394 // type
395 txTypes := []common.TxType{
396 // Uncomment once test gen is fixed
397 // common.TxTypeExit,
398 // common.TxTypeTransfer,
399 // common.TxTypeDeposit,
400 common.TxTypeCreateAccountDeposit,
401 // common.TxTypeCreateAccountDepositTransfer,
402 // common.TxTypeDepositTransfer,
403 common.TxTypeForceTransfer,
404 // common.TxTypeForceExit,
405 // common.TxTypeTransferToEthAddr,
406 // common.TxTypeTransferToBJJ,
407 }
408 for _, txType := range txTypes {
409 fetchedTxs = []testTx{}
410 limit = 2
411 path = fmt.Sprintf(
412 "%s?type=%s&limit=%d&fromItem=",
413 endpoint, txType, limit,
414 )
415 err = doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
416 assert.NoError(t, err)
417 txTypeTxs := []testTx{}
418 for i := 0; i < len(tc.allTxs); i++ {
419 if tc.allTxs[i].Type == txType {
420 txTypeTxs = append(txTypeTxs, tc.allTxs[i])
421 }
422 }
423 assertTxs(t, txTypeTxs, fetchedTxs)
424 }
425 // Multiple filters
426 fetchedTxs = []testTx{}
427 limit = 1
428 path = fmt.Sprintf(
429 "%s?batchNum=%d&tokenId=%d&limit=%d&fromItem=",
430 endpoint, *batchNum, tokenID, limit,
431 )
432 err = doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
433 assert.NoError(t, err)
434 mixedTxs := []testTx{}
435 for i := 0; i < len(tc.allTxs); i++ {
436 if tc.allTxs[i].BatchNum != nil {
437 if *tc.allTxs[i].BatchNum == *batchNum && tc.allTxs[i].Token.TokenID == tokenID {
438 mixedTxs = append(mixedTxs, tc.allTxs[i])
439 }
440 }
441 }
442 assertTxs(t, mixedTxs, fetchedTxs)
443 // All, in reverse order
444 fetchedTxs = []testTx{}
445 limit = 5
446 path = fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit)
447 err = doGoodReqPaginated(path, historydb.OrderDesc, &testTxsResponse{}, appendIter)
448 assert.NoError(t, err)
449 flipedTxs := []testTx{}
450 for i := 0; i < len(tc.allTxs); i++ {
451 flipedTxs = append(flipedTxs, tc.allTxs[len(tc.allTxs)-1-i])
452 }
453 assertTxs(t, flipedTxs, fetchedTxs)
454 // 400
455 path = fmt.Sprintf(
456 "%s?accountIndex=%s&hermezEthereumAddress=%s",
457 endpoint, idx, tc.usrAddr,
458 )
459 err = doBadReq("GET", path, nil, 400)
460 assert.NoError(t, err)
461 path = fmt.Sprintf("%s?tokenId=X", endpoint)
462 err = doBadReq("GET", path, nil, 400)
463 assert.NoError(t, err)
464 // 404
465 path = fmt.Sprintf("%s?batchNum=999999", endpoint)
466 err = doBadReq("GET", path, nil, 404)
467 assert.NoError(t, err)
468 path = fmt.Sprintf("%s?limit=1000&fromItem=999999", endpoint)
469 err = doBadReq("GET", path, nil, 404)
470 assert.NoError(t, err)
471}
472
473func TestGetHistoryTx(t *testing.T) {
474 // Get all txs by their ID
475 endpoint := apiURL + "transactions-history/"
476 fetchedTxs := []testTx{}
477 for _, tx := range tc.allTxs {
478 fetchedTx := testTx{}
479 err := doGoodReq("GET", endpoint+tx.TxID.String(), nil, &fetchedTx)
480 assert.NoError(t, err)
481 fetchedTxs = append(fetchedTxs, fetchedTx)
482 }
483 assertTxs(t, tc.allTxs, fetchedTxs)
484 // 400
485 err := doBadReq("GET", endpoint+"0x001", nil, 400)
486 assert.NoError(t, err)
487 // 404
488 err = doBadReq("GET", endpoint+"0x00000000000001e240004700", nil, 404)
489 assert.NoError(t, err)
490}
491
492func assertTxs(t *testing.T, expected, actual []testTx) {
493 require.Equal(t, len(expected), len(actual))
494 for i := 0; i < len(actual); i++ { //nolint len(actual) won't change within the loop
495 actual[i].ItemID = 0
496 actual[i].Token.ItemID = 0
497 assert.Equal(t, expected[i].Timestamp.Unix(), actual[i].Timestamp.Unix())
498 expected[i].Timestamp = actual[i].Timestamp
499 if expected[i].Token.USDUpdate == nil {
500 assert.Equal(t, expected[i].Token.USDUpdate, actual[i].Token.USDUpdate)
501 } else {
502 assert.Equal(t, expected[i].Token.USDUpdate.Unix(), actual[i].Token.USDUpdate.Unix())
503 expected[i].Token.USDUpdate = actual[i].Token.USDUpdate
504 }
505 test.AssertUSD(t, expected[i].HistoricUSD, actual[i].HistoricUSD)
506 if expected[i].L2Info != nil {
507 test.AssertUSD(t, expected[i].L2Info.HistoricFeeUSD, actual[i].L2Info.HistoricFeeUSD)
508 } else {
509 test.AssertUSD(t, expected[i].L1Info.HistoricLoadAmountUSD, actual[i].L1Info.HistoricLoadAmountUSD)
510 }
511 assert.Equal(t, expected[i], actual[i])
512 }
513}