arnaucube
arnaucube/hermez-nodePublic Self-hosted repository
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}