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(genericTxs []txSortFielder, usrIdxs []string, accs []common.Account, tokens []historydb.TokenWithUSD, blocks []common.Block) (usrTxs []testTx, allTxs []testTx) {
156 usrTxs = []testTx{}
157 allTxs = []testTx{}
158 isUsrTx := func(tx testTx) bool {
159 for _, idx := range usrIdxs {
160 if tx.FromIdx != nil && *tx.FromIdx == idx {
161 return true
162 }
163 if tx.ToIdx == idx {
164 return true
165 }
166 }
167 return false
168 }
169 for _, genericTx := range genericTxs {
170 l1 := genericTx.L1()
171 l2 := genericTx.L2()
172 if l1 != nil { // L1Tx to testTx
173 token := getTokenByID(l1.TokenID, tokens)
174 // l1.FromEthAddr and l1.FromBJJ can't be nil
175 fromEthAddr := string(apitypes.NewHezEthAddr(l1.FromEthAddr))
176 fromBJJ := string(apitypes.NewHezBJJ(l1.FromBJJ))
177 tx := testTx{
178 IsL1: "L1",
179 TxID: l1.TxID,
180 Type: l1.Type,
181 Position: l1.Position,
182 FromEthAddr: &fromEthAddr,
183 FromBJJ: &fromBJJ,
184 ToIdx: idxToHez(l1.ToIdx, token.Symbol),
185 Amount: l1.Amount.String(),
186 BatchNum: l1.BatchNum,
187 Timestamp: getTimestamp(l1.EthBlockNum, blocks),
188 L1Info: &testL1Info{
189 ToForgeL1TxsNum: l1.ToForgeL1TxsNum,
190 UserOrigin: l1.UserOrigin,
191 LoadAmount: l1.LoadAmount.String(),
192 EthBlockNum: l1.EthBlockNum,
193 },
194 Token: token,
195 }
196 // If FromIdx is not nil
197 if l1.FromIdx != 0 {
198 idxStr := idxToHez(l1.FromIdx, token.Symbol)
199 tx.FromIdx = &idxStr
200 }
201 // If tx has a normal ToIdx (>255), set FromEthAddr and FromBJJ
202 if l1.ToIdx >= common.UserThreshold {
203 // find account
204 for _, acc := range accs {
205 if l1.ToIdx == acc.Idx {
206 toEthAddr := string(apitypes.NewHezEthAddr(acc.EthAddr))
207 tx.ToEthAddr = &toEthAddr
208 toBJJ := string(apitypes.NewHezBJJ(acc.PublicKey))
209 tx.ToBJJ = &toBJJ
210 break
211 }
212 }
213 }
214 // If the token has USD value setted
215 if token.USD != nil {
216 af := new(big.Float).SetInt(l1.Amount)
217 amountFloat, _ := af.Float64()
218 usd := *token.USD * amountFloat / math.Pow(10, float64(token.Decimals))
219 tx.HistoricUSD = &usd
220 laf := new(big.Float).SetInt(l1.LoadAmount)
221 loadAmountFloat, _ := laf.Float64()
222 loadUSD := *token.USD * loadAmountFloat / math.Pow(10, float64(token.Decimals))
223 tx.L1Info.HistoricLoadAmountUSD = &loadUSD
224 }
225 allTxs = append(allTxs, tx)
226 if isUsrTx(tx) {
227 usrTxs = append(usrTxs, tx)
228 }
229 } else { // L2Tx to testTx
230 token := getTokenByIdx(l2.FromIdx, tokens, accs)
231 // l1.FromIdx can't be nil
232 fromIdx := idxToHez(l2.FromIdx, token.Symbol)
233 tx := testTx{
234 IsL1: "L2",
235 TxID: l2.TxID,
236 Type: l2.Type,
237 Position: l2.Position,
238 ToIdx: idxToHez(l2.ToIdx, token.Symbol),
239 FromIdx: &fromIdx,
240 Amount: l2.Amount.String(),
241 BatchNum: &l2.BatchNum,
242 Timestamp: getTimestamp(l2.EthBlockNum, blocks),
243 L2Info: &testL2Info{
244 Nonce: l2.Nonce,
245 Fee: l2.Fee,
246 },
247 Token: token,
248 }
249 // If FromIdx is not nil
250 if l2.FromIdx != 0 {
251 idxStr := idxToHez(l2.FromIdx, token.Symbol)
252 tx.FromIdx = &idxStr
253 }
254 // Set FromEthAddr and FromBJJ (FromIdx it's always >255)
255 for _, acc := range accs {
256 if l2.ToIdx == acc.Idx {
257 fromEthAddr := string(apitypes.NewHezEthAddr(acc.EthAddr))
258 tx.FromEthAddr = &fromEthAddr
259 fromBJJ := string(apitypes.NewHezBJJ(acc.PublicKey))
260 tx.FromBJJ = &fromBJJ
261 break
262 }
263 }
264 // If tx has a normal ToIdx (>255), set FromEthAddr and FromBJJ
265 if l2.ToIdx >= common.UserThreshold {
266 // find account
267 for _, acc := range accs {
268 if l2.ToIdx == acc.Idx {
269 toEthAddr := string(apitypes.NewHezEthAddr(acc.EthAddr))
270 tx.ToEthAddr = &toEthAddr
271 toBJJ := string(apitypes.NewHezBJJ(acc.PublicKey))
272 tx.ToBJJ = &toBJJ
273 break
274 }
275 }
276 }
277 // If the token has USD value setted
278 if token.USD != nil {
279 af := new(big.Float).SetInt(l2.Amount)
280 amountFloat, _ := af.Float64()
281 usd := *token.USD * amountFloat / math.Pow(10, float64(token.Decimals))
282 tx.HistoricUSD = &usd
283 feeUSD := usd * l2.Fee.Percentage()
284 tx.HistoricUSD = &usd
285 tx.L2Info.HistoricFeeUSD = &feeUSD
286 }
287 allTxs = append(allTxs, tx)
288 if isUsrTx(tx) {
289 usrTxs = append(usrTxs, tx)
290 }
291 }
292 }
293 return usrTxs, allTxs
294}
295
296func TestGetHistoryTxs(t *testing.T) {
297 endpoint := apiURL + "transactions-history"
298 fetchedTxs := []testTx{}
299 appendIter := func(intr interface{}) {
300 for i := 0; i < len(intr.(*testTxsResponse).Txs); i++ {
301 tmp, err := copystructure.Copy(intr.(*testTxsResponse).Txs[i])
302 if err != nil {
303 panic(err)
304 }
305 fetchedTxs = append(fetchedTxs, tmp.(testTx))
306 }
307 }
308 // Get all (no filters)
309 limit := 8
310 path := fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit)
311 err := doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
312 assert.NoError(t, err)
313 assertTxs(t, tc.allTxs, fetchedTxs)
314 // Uncomment once tx generation for tests is fixed
315 // // Get by ethAddr
316 // fetchedTxs = []testTx{}
317 // limit = 7
318 // path = fmt.Sprintf(
319 // "%s?hermezEthereumAddress=%s&limit=%d&fromItem=",
320 // endpoint, tc.usrAddr, limit,
321 // )
322 // err = doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
323 // assert.NoError(t, err)
324 // assertTxs(t, tc.usrTxs, fetchedTxs)
325 // // Get by bjj
326 // fetchedTxs = []testTx{}
327 // limit = 6
328 // path = fmt.Sprintf(
329 // "%s?BJJ=%s&limit=%d&fromItem=",
330 // endpoint, tc.usrBjj, limit,
331 // )
332 // err = doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
333 // assert.NoError(t, err)
334 // assertTxs(t, tc.usrTxs, fetchedTxs)
335 // Get by tokenID
336 fetchedTxs = []testTx{}
337 limit = 5
338 tokenID := tc.allTxs[0].Token.TokenID
339 path = fmt.Sprintf(
340 "%s?tokenId=%d&limit=%d&fromItem=",
341 endpoint, tokenID, limit,
342 )
343 err = doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
344 assert.NoError(t, err)
345 tokenIDTxs := []testTx{}
346 for i := 0; i < len(tc.allTxs); i++ {
347 if tc.allTxs[i].Token.TokenID == tokenID {
348 tokenIDTxs = append(tokenIDTxs, tc.allTxs[i])
349 }
350 }
351 assertTxs(t, tokenIDTxs, fetchedTxs)
352 // idx
353 fetchedTxs = []testTx{}
354 limit = 4
355 idx := tc.allTxs[0].ToIdx
356 path = fmt.Sprintf(
357 "%s?accountIndex=%s&limit=%d&fromItem=",
358 endpoint, idx, limit,
359 )
360 err = doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
361 assert.NoError(t, err)
362 idxTxs := []testTx{}
363 for i := 0; i < len(tc.allTxs); i++ {
364 if (tc.allTxs[i].FromIdx != nil && (*tc.allTxs[i].FromIdx)[6:] == idx[6:]) ||
365 tc.allTxs[i].ToIdx[6:] == idx[6:] {
366 idxTxs = append(idxTxs, tc.allTxs[i])
367 }
368 }
369 assertTxs(t, idxTxs, fetchedTxs)
370 // batchNum
371 fetchedTxs = []testTx{}
372 limit = 3
373 batchNum := tc.allTxs[0].BatchNum
374 path = fmt.Sprintf(
375 "%s?batchNum=%d&limit=%d&fromItem=",
376 endpoint, *batchNum, limit,
377 )
378 err = doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
379 assert.NoError(t, err)
380 batchNumTxs := []testTx{}
381 for i := 0; i < len(tc.allTxs); i++ {
382 if tc.allTxs[i].BatchNum != nil &&
383 *tc.allTxs[i].BatchNum == *batchNum {
384 batchNumTxs = append(batchNumTxs, tc.allTxs[i])
385 }
386 }
387 assertTxs(t, batchNumTxs, fetchedTxs)
388 // type
389 txTypes := []common.TxType{
390 // Uncomment once test gen is fixed
391 // common.TxTypeExit,
392 // common.TxTypeTransfer,
393 // common.TxTypeDeposit,
394 common.TxTypeCreateAccountDeposit,
395 // common.TxTypeCreateAccountDepositTransfer,
396 // common.TxTypeDepositTransfer,
397 common.TxTypeForceTransfer,
398 // common.TxTypeForceExit,
399 // common.TxTypeTransferToEthAddr,
400 // common.TxTypeTransferToBJJ,
401 }
402 for _, txType := range txTypes {
403 fetchedTxs = []testTx{}
404 limit = 2
405 path = fmt.Sprintf(
406 "%s?type=%s&limit=%d&fromItem=",
407 endpoint, txType, limit,
408 )
409 err = doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
410 assert.NoError(t, err)
411 txTypeTxs := []testTx{}
412 for i := 0; i < len(tc.allTxs); i++ {
413 if tc.allTxs[i].Type == txType {
414 txTypeTxs = append(txTypeTxs, tc.allTxs[i])
415 }
416 }
417 assertTxs(t, txTypeTxs, fetchedTxs)
418 }
419 // Multiple filters
420 fetchedTxs = []testTx{}
421 limit = 1
422 path = fmt.Sprintf(
423 "%s?batchNum=%d&tokenId=%d&limit=%d&fromItem=",
424 endpoint, *batchNum, tokenID, limit,
425 )
426 err = doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
427 assert.NoError(t, err)
428 mixedTxs := []testTx{}
429 for i := 0; i < len(tc.allTxs); i++ {
430 if tc.allTxs[i].BatchNum != nil {
431 if *tc.allTxs[i].BatchNum == *batchNum && tc.allTxs[i].Token.TokenID == tokenID {
432 mixedTxs = append(mixedTxs, tc.allTxs[i])
433 }
434 }
435 }
436 assertTxs(t, mixedTxs, fetchedTxs)
437 // All, in reverse order
438 fetchedTxs = []testTx{}
439 limit = 5
440 path = fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit)
441 err = doGoodReqPaginated(path, historydb.OrderDesc, &testTxsResponse{}, appendIter)
442 assert.NoError(t, err)
443 flipedTxs := []testTx{}
444 for i := 0; i < len(tc.allTxs); i++ {
445 flipedTxs = append(flipedTxs, tc.allTxs[len(tc.allTxs)-1-i])
446 }
447 assertTxs(t, flipedTxs, fetchedTxs)
448 // 400
449 path = fmt.Sprintf(
450 "%s?accountIndex=%s&hermezEthereumAddress=%s",
451 endpoint, idx, tc.usrAddr,
452 )
453 err = doBadReq("GET", path, nil, 400)
454 assert.NoError(t, err)
455 path = fmt.Sprintf("%s?tokenId=X", endpoint)
456 err = doBadReq("GET", path, nil, 400)
457 assert.NoError(t, err)
458 // 404
459 path = fmt.Sprintf("%s?batchNum=999999", endpoint)
460 err = doBadReq("GET", path, nil, 404)
461 assert.NoError(t, err)
462 path = fmt.Sprintf("%s?limit=1000&fromItem=999999", endpoint)
463 err = doBadReq("GET", path, nil, 404)
464 assert.NoError(t, err)
465}
466
467func TestGetHistoryTx(t *testing.T) {
468 // Get all txs by their ID
469 endpoint := apiURL + "transactions-history/"
470 fetchedTxs := []testTx{}
471 for _, tx := range tc.allTxs {
472 fetchedTx := testTx{}
473 err := doGoodReq("GET", endpoint+tx.TxID.String(), nil, &fetchedTx)
474 assert.NoError(t, err)
475 fetchedTxs = append(fetchedTxs, fetchedTx)
476 }
477 assertTxs(t, tc.allTxs, fetchedTxs)
478 // 400
479 err := doBadReq("GET", endpoint+"0x001", nil, 400)
480 assert.NoError(t, err)
481 // 404
482 err = doBadReq("GET", endpoint+"0x00000000000001e240004700", nil, 404)
483 assert.NoError(t, err)
484}
485
486func assertTxs(t *testing.T, expected, actual []testTx) {
487 require.Equal(t, len(expected), len(actual))
488 for i := 0; i < len(actual); i++ { //nolint len(actual) won't change within the loop
489 actual[i].ItemID = 0
490 actual[i].Token.ItemID = 0
491 assert.Equal(t, expected[i].Timestamp.Unix(), actual[i].Timestamp.Unix())
492 expected[i].Timestamp = actual[i].Timestamp
493 if expected[i].Token.USDUpdate == nil {
494 assert.Equal(t, expected[i].Token.USDUpdate, actual[i].Token.USDUpdate)
495 } else {
496 assert.Equal(t, expected[i].Token.USDUpdate.Unix(), actual[i].Token.USDUpdate.Unix())
497 expected[i].Token.USDUpdate = actual[i].Token.USDUpdate
498 }
499 test.AssertUSD(t, expected[i].HistoricUSD, actual[i].HistoricUSD)
500 if expected[i].L2Info != nil {
501 test.AssertUSD(t, expected[i].L2Info.HistoricFeeUSD, actual[i].L2Info.HistoricFeeUSD)
502 } else {
503 test.AssertUSD(t, expected[i].L1Info.HistoricLoadAmountUSD, actual[i].L1Info.HistoricLoadAmountUSD)
504 }
505 assert.Equal(t, expected[i], actual[i])
506 }
507}