package api
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"math/big"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hermeznetwork/hermez-node/apitypes"
|
|
"github.com/hermeznetwork/hermez-node/common"
|
|
"github.com/hermeznetwork/hermez-node/db"
|
|
"github.com/hermeznetwork/hermez-node/db/historydb"
|
|
"github.com/hermeznetwork/hermez-node/test"
|
|
"github.com/mitchellh/copystructure"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type testL1Info struct {
|
|
ToForgeL1TxsNum *int64 `json:"toForgeL1TransactionsNum"`
|
|
UserOrigin bool `json:"userOrigin"`
|
|
LoadAmount string `json:"loadAmount"`
|
|
HistoricLoadAmountUSD *float64 `json:"historicLoadAmountUSD"`
|
|
EthBlockNum int64 `json:"ethereumBlockNum"`
|
|
}
|
|
|
|
type testL2Info struct {
|
|
Fee common.FeeSelector `json:"fee"`
|
|
HistoricFeeUSD *float64 `json:"historicFeeUSD"`
|
|
Nonce common.Nonce `json:"nonce"`
|
|
}
|
|
|
|
type testTx struct {
|
|
IsL1 string `json:"L1orL2"`
|
|
TxID common.TxID `json:"id"`
|
|
ItemID int `json:"itemId"`
|
|
Type common.TxType `json:"type"`
|
|
Position int `json:"position"`
|
|
FromIdx *string `json:"fromAccountIndex"`
|
|
FromEthAddr *string `json:"fromHezEthereumAddress"`
|
|
FromBJJ *string `json:"fromBJJ"`
|
|
ToIdx string `json:"toAccountIndex"`
|
|
ToEthAddr *string `json:"toHezEthereumAddress"`
|
|
ToBJJ *string `json:"toBJJ"`
|
|
Amount string `json:"amount"`
|
|
BatchNum *common.BatchNum `json:"batchNum"`
|
|
HistoricUSD *float64 `json:"historicUSD"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
L1Info *testL1Info `json:"L1Info"`
|
|
L2Info *testL2Info `json:"L2Info"`
|
|
Token historydb.TokenWithUSD `json:"token"`
|
|
}
|
|
|
|
type testTxsResponse struct {
|
|
Txs []testTx `json:"transactions"`
|
|
Pagination *db.Pagination `json:"pagination"`
|
|
}
|
|
|
|
func (t testTxsResponse) GetPagination() *db.Pagination {
|
|
if t.Txs[0].ItemID < t.Txs[len(t.Txs)-1].ItemID {
|
|
t.Pagination.FirstReturnedItem = t.Txs[0].ItemID
|
|
t.Pagination.LastReturnedItem = t.Txs[len(t.Txs)-1].ItemID
|
|
} else {
|
|
t.Pagination.LastReturnedItem = t.Txs[0].ItemID
|
|
t.Pagination.FirstReturnedItem = t.Txs[len(t.Txs)-1].ItemID
|
|
}
|
|
return t.Pagination
|
|
}
|
|
|
|
func (t testTxsResponse) Len() int {
|
|
return len(t.Txs)
|
|
}
|
|
|
|
// TxSortFields represents the fields needed to sort L1 and L2 transactions
|
|
type txSortFields struct {
|
|
BatchNum *common.BatchNum
|
|
Position int
|
|
}
|
|
|
|
// TxSortFielder is a interface that allows sorting L1 and L2 transactions in a combined way
|
|
type txSortFielder interface {
|
|
SortFields() txSortFields
|
|
L1() *common.L1Tx
|
|
L2() *common.L2Tx
|
|
}
|
|
|
|
// TxsSort array of TxSortFielder
|
|
type txsSort []txSortFielder
|
|
|
|
func (t txsSort) Len() int { return len(t) }
|
|
func (t txsSort) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
|
|
func (t txsSort) Less(i, j int) bool {
|
|
// i not forged yet
|
|
isf := t[i].SortFields()
|
|
jsf := t[j].SortFields()
|
|
if isf.BatchNum == nil {
|
|
if jsf.BatchNum != nil { // j is already forged
|
|
return false
|
|
}
|
|
// Both aren't forged, is i in a smaller position?
|
|
return isf.Position < jsf.Position
|
|
}
|
|
// i is forged
|
|
if jsf.BatchNum == nil {
|
|
return false // j is not forged
|
|
}
|
|
// Both are forged
|
|
if *isf.BatchNum == *jsf.BatchNum {
|
|
// At the same batch, is i in a smaller position?
|
|
return isf.Position < jsf.Position
|
|
}
|
|
// At different batches, is i in a smaller batch?
|
|
return *isf.BatchNum < *jsf.BatchNum
|
|
}
|
|
|
|
type wrappedL1 common.L1Tx
|
|
|
|
// SortFields implements TxSortFielder
|
|
func (tx *wrappedL1) SortFields() txSortFields {
|
|
return txSortFields{
|
|
BatchNum: tx.BatchNum,
|
|
Position: tx.Position,
|
|
}
|
|
}
|
|
|
|
// L1 implements TxSortFielder
|
|
func (tx *wrappedL1) L1() *common.L1Tx {
|
|
l1tx := common.L1Tx(*tx)
|
|
return &l1tx
|
|
}
|
|
|
|
// L2 implements TxSortFielder
|
|
func (tx *wrappedL1) L2() *common.L2Tx { return nil }
|
|
|
|
type wrappedL2 common.L2Tx
|
|
|
|
// SortFields implements TxSortFielder
|
|
func (tx *wrappedL2) SortFields() txSortFields {
|
|
return txSortFields{
|
|
BatchNum: &tx.BatchNum,
|
|
Position: tx.Position,
|
|
}
|
|
}
|
|
|
|
// L1 implements TxSortFielder
|
|
func (tx *wrappedL2) L1() *common.L1Tx { return nil }
|
|
|
|
// L2 implements TxSortFielder
|
|
func (tx *wrappedL2) L2() *common.L2Tx {
|
|
l2tx := common.L2Tx(*tx)
|
|
return &l2tx
|
|
}
|
|
|
|
func genTestTxs(genericTxs []txSortFielder, usrIdxs []string, accs []common.Account, tokens []historydb.TokenWithUSD, blocks []common.Block) (usrTxs []testTx, allTxs []testTx) {
|
|
usrTxs = []testTx{}
|
|
allTxs = []testTx{}
|
|
isUsrTx := func(tx testTx) bool {
|
|
for _, idx := range usrIdxs {
|
|
if tx.FromIdx != nil && *tx.FromIdx == idx {
|
|
return true
|
|
}
|
|
if tx.ToIdx == idx {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
for _, genericTx := range genericTxs {
|
|
l1 := genericTx.L1()
|
|
l2 := genericTx.L2()
|
|
if l1 != nil { // L1Tx to testTx
|
|
token := getTokenByID(l1.TokenID, tokens)
|
|
// l1.FromEthAddr and l1.FromBJJ can't be nil
|
|
fromEthAddr := string(apitypes.NewHezEthAddr(l1.FromEthAddr))
|
|
fromBJJ := string(apitypes.NewHezBJJ(l1.FromBJJ))
|
|
tx := testTx{
|
|
IsL1: "L1",
|
|
TxID: l1.TxID,
|
|
Type: l1.Type,
|
|
Position: l1.Position,
|
|
FromEthAddr: &fromEthAddr,
|
|
FromBJJ: &fromBJJ,
|
|
ToIdx: idxToHez(l1.ToIdx, token.Symbol),
|
|
Amount: l1.Amount.String(),
|
|
BatchNum: l1.BatchNum,
|
|
Timestamp: getTimestamp(l1.EthBlockNum, blocks),
|
|
L1Info: &testL1Info{
|
|
ToForgeL1TxsNum: l1.ToForgeL1TxsNum,
|
|
UserOrigin: l1.UserOrigin,
|
|
LoadAmount: l1.LoadAmount.String(),
|
|
EthBlockNum: l1.EthBlockNum,
|
|
},
|
|
Token: token,
|
|
}
|
|
// If FromIdx is not nil
|
|
if l1.FromIdx != 0 {
|
|
idxStr := idxToHez(l1.FromIdx, token.Symbol)
|
|
tx.FromIdx = &idxStr
|
|
}
|
|
// If tx has a normal ToIdx (>255), set FromEthAddr and FromBJJ
|
|
if l1.ToIdx >= common.UserThreshold {
|
|
// find account
|
|
for _, acc := range accs {
|
|
if l1.ToIdx == acc.Idx {
|
|
toEthAddr := string(apitypes.NewHezEthAddr(acc.EthAddr))
|
|
tx.ToEthAddr = &toEthAddr
|
|
toBJJ := string(apitypes.NewHezBJJ(acc.PublicKey))
|
|
tx.ToBJJ = &toBJJ
|
|
break
|
|
}
|
|
}
|
|
}
|
|
// If the token has USD value setted
|
|
if token.USD != nil {
|
|
af := new(big.Float).SetInt(l1.Amount)
|
|
amountFloat, _ := af.Float64()
|
|
usd := *token.USD * amountFloat / math.Pow(10, float64(token.Decimals))
|
|
tx.HistoricUSD = &usd
|
|
laf := new(big.Float).SetInt(l1.LoadAmount)
|
|
loadAmountFloat, _ := laf.Float64()
|
|
loadUSD := *token.USD * loadAmountFloat / math.Pow(10, float64(token.Decimals))
|
|
tx.L1Info.HistoricLoadAmountUSD = &loadUSD
|
|
}
|
|
allTxs = append(allTxs, tx)
|
|
if isUsrTx(tx) {
|
|
usrTxs = append(usrTxs, tx)
|
|
}
|
|
} else { // L2Tx to testTx
|
|
token := getTokenByIdx(l2.FromIdx, tokens, accs)
|
|
// l1.FromIdx can't be nil
|
|
fromIdx := idxToHez(l2.FromIdx, token.Symbol)
|
|
tx := testTx{
|
|
IsL1: "L2",
|
|
TxID: l2.TxID,
|
|
Type: l2.Type,
|
|
Position: l2.Position,
|
|
ToIdx: idxToHez(l2.ToIdx, token.Symbol),
|
|
FromIdx: &fromIdx,
|
|
Amount: l2.Amount.String(),
|
|
BatchNum: &l2.BatchNum,
|
|
Timestamp: getTimestamp(l2.EthBlockNum, blocks),
|
|
L2Info: &testL2Info{
|
|
Nonce: l2.Nonce,
|
|
Fee: l2.Fee,
|
|
},
|
|
Token: token,
|
|
}
|
|
// If FromIdx is not nil
|
|
if l2.FromIdx != 0 {
|
|
idxStr := idxToHez(l2.FromIdx, token.Symbol)
|
|
tx.FromIdx = &idxStr
|
|
}
|
|
// Set FromEthAddr and FromBJJ (FromIdx it's always >255)
|
|
for _, acc := range accs {
|
|
if l2.ToIdx == acc.Idx {
|
|
fromEthAddr := string(apitypes.NewHezEthAddr(acc.EthAddr))
|
|
tx.FromEthAddr = &fromEthAddr
|
|
fromBJJ := string(apitypes.NewHezBJJ(acc.PublicKey))
|
|
tx.FromBJJ = &fromBJJ
|
|
break
|
|
}
|
|
}
|
|
// If tx has a normal ToIdx (>255), set FromEthAddr and FromBJJ
|
|
if l2.ToIdx >= common.UserThreshold {
|
|
// find account
|
|
for _, acc := range accs {
|
|
if l2.ToIdx == acc.Idx {
|
|
toEthAddr := string(apitypes.NewHezEthAddr(acc.EthAddr))
|
|
tx.ToEthAddr = &toEthAddr
|
|
toBJJ := string(apitypes.NewHezBJJ(acc.PublicKey))
|
|
tx.ToBJJ = &toBJJ
|
|
break
|
|
}
|
|
}
|
|
}
|
|
// If the token has USD value setted
|
|
if token.USD != nil {
|
|
af := new(big.Float).SetInt(l2.Amount)
|
|
amountFloat, _ := af.Float64()
|
|
usd := *token.USD * amountFloat / math.Pow(10, float64(token.Decimals))
|
|
tx.HistoricUSD = &usd
|
|
feeUSD := usd * l2.Fee.Percentage()
|
|
tx.HistoricUSD = &usd
|
|
tx.L2Info.HistoricFeeUSD = &feeUSD
|
|
}
|
|
allTxs = append(allTxs, tx)
|
|
if isUsrTx(tx) {
|
|
usrTxs = append(usrTxs, tx)
|
|
}
|
|
}
|
|
}
|
|
return usrTxs, allTxs
|
|
}
|
|
|
|
func TestGetHistoryTxs(t *testing.T) {
|
|
endpoint := apiURL + "transactions-history"
|
|
fetchedTxs := []testTx{}
|
|
appendIter := func(intr interface{}) {
|
|
for i := 0; i < len(intr.(*testTxsResponse).Txs); i++ {
|
|
tmp, err := copystructure.Copy(intr.(*testTxsResponse).Txs[i])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
fetchedTxs = append(fetchedTxs, tmp.(testTx))
|
|
}
|
|
}
|
|
// Get all (no filters)
|
|
limit := 8
|
|
path := fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit)
|
|
err := doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
|
|
assert.NoError(t, err)
|
|
assertHistoryTxAPIs(t, tc.allTxs, fetchedTxs)
|
|
// Uncomment once tx generation for tests is fixed
|
|
// // Get by ethAddr
|
|
// fetchedTxs = []testTx{}
|
|
// limit = 7
|
|
// path = fmt.Sprintf(
|
|
// "%s?hermezEthereumAddress=%s&limit=%d&fromItem=",
|
|
// endpoint, tc.usrAddr, limit,
|
|
// )
|
|
// err = doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
|
|
// assert.NoError(t, err)
|
|
// assertHistoryTxAPIs(t, tc.usrTxs, fetchedTxs)
|
|
// // Get by bjj
|
|
// fetchedTxs = []testTx{}
|
|
// limit = 6
|
|
// path = fmt.Sprintf(
|
|
// "%s?BJJ=%s&limit=%d&fromItem=",
|
|
// endpoint, tc.usrBjj, limit,
|
|
// )
|
|
// err = doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
|
|
// assert.NoError(t, err)
|
|
// assertHistoryTxAPIs(t, tc.usrTxs, fetchedTxs)
|
|
// Get by tokenID
|
|
fetchedTxs = []testTx{}
|
|
limit = 5
|
|
tokenID := tc.allTxs[0].Token.TokenID
|
|
path = fmt.Sprintf(
|
|
"%s?tokenId=%d&limit=%d&fromItem=",
|
|
endpoint, tokenID, limit,
|
|
)
|
|
err = doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
|
|
assert.NoError(t, err)
|
|
tokenIDTxs := []testTx{}
|
|
for i := 0; i < len(tc.allTxs); i++ {
|
|
if tc.allTxs[i].Token.TokenID == tokenID {
|
|
tokenIDTxs = append(tokenIDTxs, tc.allTxs[i])
|
|
}
|
|
}
|
|
assertHistoryTxAPIs(t, tokenIDTxs, fetchedTxs)
|
|
// idx
|
|
fetchedTxs = []testTx{}
|
|
limit = 4
|
|
idx := tc.allTxs[0].ToIdx
|
|
path = fmt.Sprintf(
|
|
"%s?accountIndex=%s&limit=%d&fromItem=",
|
|
endpoint, idx, limit,
|
|
)
|
|
err = doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
|
|
assert.NoError(t, err)
|
|
idxTxs := []testTx{}
|
|
for i := 0; i < len(tc.allTxs); i++ {
|
|
if (tc.allTxs[i].FromIdx != nil && (*tc.allTxs[i].FromIdx)[6:] == idx[6:]) ||
|
|
tc.allTxs[i].ToIdx[6:] == idx[6:] {
|
|
idxTxs = append(idxTxs, tc.allTxs[i])
|
|
}
|
|
}
|
|
assertHistoryTxAPIs(t, idxTxs, fetchedTxs)
|
|
// batchNum
|
|
fetchedTxs = []testTx{}
|
|
limit = 3
|
|
batchNum := tc.allTxs[0].BatchNum
|
|
path = fmt.Sprintf(
|
|
"%s?batchNum=%d&limit=%d&fromItem=",
|
|
endpoint, *batchNum, limit,
|
|
)
|
|
err = doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
|
|
assert.NoError(t, err)
|
|
batchNumTxs := []testTx{}
|
|
for i := 0; i < len(tc.allTxs); i++ {
|
|
if tc.allTxs[i].BatchNum != nil &&
|
|
*tc.allTxs[i].BatchNum == *batchNum {
|
|
batchNumTxs = append(batchNumTxs, tc.allTxs[i])
|
|
}
|
|
}
|
|
assertHistoryTxAPIs(t, batchNumTxs, fetchedTxs)
|
|
// type
|
|
txTypes := []common.TxType{
|
|
// Uncomment once test gen is fixed
|
|
// common.TxTypeExit,
|
|
// common.TxTypeTransfer,
|
|
// common.TxTypeDeposit,
|
|
common.TxTypeCreateAccountDeposit,
|
|
// common.TxTypeCreateAccountDepositTransfer,
|
|
// common.TxTypeDepositTransfer,
|
|
common.TxTypeForceTransfer,
|
|
// common.TxTypeForceExit,
|
|
// common.TxTypeTransferToEthAddr,
|
|
// common.TxTypeTransferToBJJ,
|
|
}
|
|
for _, txType := range txTypes {
|
|
fetchedTxs = []testTx{}
|
|
limit = 2
|
|
path = fmt.Sprintf(
|
|
"%s?type=%s&limit=%d&fromItem=",
|
|
endpoint, txType, limit,
|
|
)
|
|
err = doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
|
|
assert.NoError(t, err)
|
|
txTypeTxs := []testTx{}
|
|
for i := 0; i < len(tc.allTxs); i++ {
|
|
if tc.allTxs[i].Type == txType {
|
|
txTypeTxs = append(txTypeTxs, tc.allTxs[i])
|
|
}
|
|
}
|
|
assertHistoryTxAPIs(t, txTypeTxs, fetchedTxs)
|
|
}
|
|
// Multiple filters
|
|
fetchedTxs = []testTx{}
|
|
limit = 1
|
|
path = fmt.Sprintf(
|
|
"%s?batchNum=%d&tokenId=%d&limit=%d&fromItem=",
|
|
endpoint, *batchNum, tokenID, limit,
|
|
)
|
|
err = doGoodReqPaginated(path, historydb.OrderAsc, &testTxsResponse{}, appendIter)
|
|
assert.NoError(t, err)
|
|
mixedTxs := []testTx{}
|
|
for i := 0; i < len(tc.allTxs); i++ {
|
|
if tc.allTxs[i].BatchNum != nil {
|
|
if *tc.allTxs[i].BatchNum == *batchNum && tc.allTxs[i].Token.TokenID == tokenID {
|
|
mixedTxs = append(mixedTxs, tc.allTxs[i])
|
|
}
|
|
}
|
|
}
|
|
assertHistoryTxAPIs(t, mixedTxs, fetchedTxs)
|
|
// All, in reverse order
|
|
fetchedTxs = []testTx{}
|
|
limit = 5
|
|
path = fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit)
|
|
err = doGoodReqPaginated(path, historydb.OrderDesc, &testTxsResponse{}, appendIter)
|
|
assert.NoError(t, err)
|
|
flipedTxs := []testTx{}
|
|
for i := 0; i < len(tc.allTxs); i++ {
|
|
flipedTxs = append(flipedTxs, tc.allTxs[len(tc.allTxs)-1-i])
|
|
}
|
|
assertHistoryTxAPIs(t, flipedTxs, fetchedTxs)
|
|
// 400
|
|
path = fmt.Sprintf(
|
|
"%s?accountIndex=%s&hermezEthereumAddress=%s",
|
|
endpoint, idx, tc.usrAddr,
|
|
)
|
|
err = doBadReq("GET", path, nil, 400)
|
|
assert.NoError(t, err)
|
|
path = fmt.Sprintf("%s?tokenId=X", endpoint)
|
|
err = doBadReq("GET", path, nil, 400)
|
|
assert.NoError(t, err)
|
|
// 404
|
|
path = fmt.Sprintf("%s?batchNum=999999", endpoint)
|
|
err = doBadReq("GET", path, nil, 404)
|
|
assert.NoError(t, err)
|
|
path = fmt.Sprintf("%s?limit=1000&fromItem=999999", endpoint)
|
|
err = doBadReq("GET", path, nil, 404)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestGetHistoryTx(t *testing.T) {
|
|
// Get all txs by their ID
|
|
endpoint := apiURL + "transactions-history/"
|
|
fetchedTxs := []testTx{}
|
|
for _, tx := range tc.allTxs {
|
|
fetchedTx := testTx{}
|
|
err := doGoodReq("GET", endpoint+tx.TxID.String(), nil, &fetchedTx)
|
|
assert.NoError(t, err)
|
|
fetchedTxs = append(fetchedTxs, fetchedTx)
|
|
}
|
|
assertHistoryTxAPIs(t, tc.allTxs, fetchedTxs)
|
|
// 400
|
|
err := doBadReq("GET", endpoint+"0x001", nil, 400)
|
|
assert.NoError(t, err)
|
|
// 404
|
|
err = doBadReq("GET", endpoint+"0x00000000000001e240004700", nil, 404)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func assertHistoryTxAPIs(t *testing.T, expected, actual []testTx) {
|
|
require.Equal(t, len(expected), len(actual))
|
|
for i := 0; i < len(actual); i++ { //nolint len(actual) won't change within the loop
|
|
actual[i].ItemID = 0
|
|
actual[i].Token.ItemID = 0
|
|
assert.Equal(t, expected[i].Timestamp.Unix(), actual[i].Timestamp.Unix())
|
|
expected[i].Timestamp = actual[i].Timestamp
|
|
if expected[i].Token.USDUpdate == nil {
|
|
assert.Equal(t, expected[i].Token.USDUpdate, actual[i].Token.USDUpdate)
|
|
} else {
|
|
assert.Equal(t, expected[i].Token.USDUpdate.Unix(), actual[i].Token.USDUpdate.Unix())
|
|
expected[i].Token.USDUpdate = actual[i].Token.USDUpdate
|
|
}
|
|
test.AssertUSD(t, expected[i].HistoricUSD, actual[i].HistoricUSD)
|
|
if expected[i].L2Info != nil {
|
|
test.AssertUSD(t, expected[i].L2Info.HistoricFeeUSD, actual[i].L2Info.HistoricFeeUSD)
|
|
} else {
|
|
test.AssertUSD(t, expected[i].L1Info.HistoricLoadAmountUSD, actual[i].L1Info.HistoricLoadAmountUSD)
|
|
}
|
|
assert.Equal(t, expected[i], actual[i])
|
|
}
|
|
}
|