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/historydb"
13 "github.com/hermeznetwork/hermez-node/test"
14 "github.com/mitchellh/copystructure"
15 "github.com/stretchr/testify/assert"
16 "github.com/stretchr/testify/require"
17)
18
19type testL1Info struct {
20 ToForgeL1TxsNum *int64 `json:"toForgeL1TransactionsNum"`
21 UserOrigin bool `json:"userOrigin"`
22 LoadAmount string `json:"loadAmount"`
23 HistoricLoadAmountUSD *float64 `json:"historicLoadAmountUSD"`
24 EthBlockNum int64 `json:"ethereumBlockNum"`
25}
26
27type testL2Info struct {
28 Fee common.FeeSelector `json:"fee"`
29 HistoricFeeUSD *float64 `json:"historicFeeUSD"`
30 Nonce common.Nonce `json:"nonce"`
31}
32
33type testTx struct {
34 IsL1 string `json:"L1orL2"`
35 TxID common.TxID `json:"id"`
36 ItemID uint64 `json:"itemId"`
37 Type common.TxType `json:"type"`
38 Position int `json:"position"`
39 FromIdx *string `json:"fromAccountIndex"`
40 FromEthAddr *string `json:"fromHezEthereumAddress"`
41 FromBJJ *string `json:"fromBJJ"`
42 ToIdx string `json:"toAccountIndex"`
43 ToEthAddr *string `json:"toHezEthereumAddress"`
44 ToBJJ *string `json:"toBJJ"`
45 Amount string `json:"amount"`
46 BatchNum *common.BatchNum `json:"batchNum"`
47 HistoricUSD *float64 `json:"historicUSD"`
48 Timestamp time.Time `json:"timestamp"`
49 L1Info *testL1Info `json:"L1Info"`
50 L2Info *testL2Info `json:"L2Info"`
51 Token historydb.TokenWithUSD `json:"token"`
52}
53
54type testTxsResponse struct {
55 Txs []testTx `json:"transactions"`
56 PendingItems uint64 `json:"pendingItems"`
57}
58
59func (t testTxsResponse) GetPending() (pendingItems, lastItemID uint64) {
60 pendingItems = t.PendingItems
61 lastItemID = t.Txs[len(t.Txs)-1].ItemID
62 return pendingItems, lastItemID
63}
64
65func (t testTxsResponse) Len() int {
66 return len(t.Txs)
67}
68
69// TxSortFields represents the fields needed to sort L1 and L2 transactions
70type txSortFields struct {
71 BatchNum *common.BatchNum
72 Position int
73}
74
75// TxSortFielder is a interface that allows sorting L1 and L2 transactions in a combined way
76type txSortFielder interface {
77 SortFields() txSortFields
78 L1() *common.L1Tx
79 L2() *common.L2Tx
80}
81
82// TxsSort array of TxSortFielder
83type txsSort []txSortFielder
84
85func (t txsSort) Len() int { return len(t) }
86func (t txsSort) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
87func (t txsSort) Less(i, j int) bool {
88 // i not forged yet
89 isf := t[i].SortFields()
90 jsf := t[j].SortFields()
91 if isf.BatchNum == nil {
92 if jsf.BatchNum != nil { // j is already forged
93 return false
94 }
95 // Both aren't forged, is i in a smaller position?
96 return isf.Position < jsf.Position
97 }
98 // i is forged
99 if jsf.BatchNum == nil {
100 return false // j is not forged
101 }
102 // Both are forged
103 if *isf.BatchNum == *jsf.BatchNum {
104 // At the same batch, is i in a smaller position?
105 return isf.Position < jsf.Position
106 }
107 // At different batches, is i in a smaller batch?
108 return *isf.BatchNum < *jsf.BatchNum
109}
110
111type wrappedL1 common.L1Tx
112
113// SortFields implements TxSortFielder
114func (tx *wrappedL1) SortFields() txSortFields {
115 return txSortFields{
116 BatchNum: tx.BatchNum,
117 Position: tx.Position,
118 }
119}
120
121// L1 implements TxSortFielder
122func (tx *wrappedL1) L1() *common.L1Tx {
123 l1tx := common.L1Tx(*tx)
124 return &l1tx
125}
126
127// L2 implements TxSortFielder
128func (tx *wrappedL1) L2() *common.L2Tx { return nil }
129
130type wrappedL2 common.L2Tx
131
132// SortFields implements TxSortFielder
133func (tx *wrappedL2) SortFields() txSortFields {
134 return txSortFields{
135 BatchNum: &tx.BatchNum,
136 Position: tx.Position,
137 }
138}
139
140// L1 implements TxSortFielder
141func (tx *wrappedL2) L1() *common.L1Tx { return nil }
142
143// L2 implements TxSortFielder
144func (tx *wrappedL2) L2() *common.L2Tx {
145 l2tx := common.L2Tx(*tx)
146 return &l2tx
147}
148
149func genTestTxs(
150 genericTxs []txSortFielder,
151 usrIdxs []string,
152 accs []common.Account,
153 tokens []historydb.TokenWithUSD,
154 blocks []common.Block,
155) (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 // assertHistoryTxAPIs(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}