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