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