@ -0,0 +1,82 @@ |
|||||
|
package api |
||||
|
|
||||
|
import ( |
||||
|
"net/http" |
||||
|
|
||||
|
"github.com/gin-gonic/gin" |
||||
|
"github.com/hermeznetwork/hermez-node/apitypes" |
||||
|
"github.com/hermeznetwork/hermez-node/db" |
||||
|
"github.com/hermeznetwork/hermez-node/db/historydb" |
||||
|
) |
||||
|
|
||||
|
func getAccount(c *gin.Context) { |
||||
|
// Get Addr
|
||||
|
idx, err := parseParamIdx(c) |
||||
|
if err != nil { |
||||
|
retBadReq(err, c) |
||||
|
return |
||||
|
} |
||||
|
apiAccount, err := h.GetAccountAPI(*idx) |
||||
|
if err != nil { |
||||
|
retSQLErr(err, c) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Get balance from stateDB
|
||||
|
account, err := s.GetAccount(*idx) |
||||
|
if err != nil { |
||||
|
retSQLErr(err, c) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
apiAccount.Balance = apitypes.NewBigIntStr(account.Balance) |
||||
|
|
||||
|
c.JSON(http.StatusOK, apiAccount) |
||||
|
} |
||||
|
|
||||
|
func getAccounts(c *gin.Context) { |
||||
|
// Account filters
|
||||
|
tokenIDs, addr, bjj, err := parseAccountFilters(c) |
||||
|
if err != nil { |
||||
|
retBadReq(err, c) |
||||
|
return |
||||
|
} |
||||
|
// Pagination
|
||||
|
fromItem, order, limit, err := parsePagination(c) |
||||
|
if err != nil { |
||||
|
retBadReq(err, c) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Fetch Accounts from historyDB
|
||||
|
apiAccounts, pagination, err := h.GetAccountsAPI(tokenIDs, addr, bjj, fromItem, limit, order) |
||||
|
if err != nil { |
||||
|
retSQLErr(err, c) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Get balances from stateDB
|
||||
|
for x, apiAccount := range apiAccounts { |
||||
|
idx, err := stringToIdx(string(apiAccount.Idx), "Account Idx") |
||||
|
if err != nil { |
||||
|
retSQLErr(err, c) |
||||
|
return |
||||
|
} |
||||
|
account, err := s.GetAccount(*idx) |
||||
|
if err != nil { |
||||
|
retSQLErr(err, c) |
||||
|
return |
||||
|
} |
||||
|
apiAccounts[x].Balance = apitypes.NewBigIntStr(account.Balance) |
||||
|
} |
||||
|
|
||||
|
// Build succesfull response
|
||||
|
type accountResponse struct { |
||||
|
Accounts []historydb.AccountAPI `json:"accounts"` |
||||
|
Pagination *db.Pagination `json:"pagination"` |
||||
|
} |
||||
|
c.JSON(http.StatusOK, &accountResponse{ |
||||
|
Accounts: apiAccounts, |
||||
|
Pagination: pagination, |
||||
|
}) |
||||
|
} |
@ -0,0 +1,166 @@ |
|||||
|
package api |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"strconv" |
||||
|
"testing" |
||||
|
|
||||
|
"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/mitchellh/copystructure" |
||||
|
"github.com/stretchr/testify/assert" |
||||
|
) |
||||
|
|
||||
|
type testAccount struct { |
||||
|
ItemID int `json:"itemId"` |
||||
|
Idx apitypes.HezIdx `json:"accountIndex"` |
||||
|
BatchNum common.BatchNum `json:"batchNum"` |
||||
|
PublicKey apitypes.HezBJJ `json:"bjj"` |
||||
|
EthAddr apitypes.HezEthAddr `json:"hezEthereumAddress"` |
||||
|
Nonce common.Nonce `json:"nonce"` |
||||
|
Balance *apitypes.BigIntStr `json:"balance"` |
||||
|
Token historydb.TokenWithUSD `json:"token"` |
||||
|
} |
||||
|
|
||||
|
type testAccountsResponse struct { |
||||
|
Accounts []testAccount `json:"accounts"` |
||||
|
Pagination *db.Pagination `json:"pagination"` |
||||
|
} |
||||
|
|
||||
|
func genTestAccounts(accounts []common.Account, tokens []historydb.TokenWithUSD) []testAccount { |
||||
|
tAccounts := []testAccount{} |
||||
|
for x, account := range accounts { |
||||
|
token := getTokenByID(account.TokenID, tokens) |
||||
|
tAccount := testAccount{ |
||||
|
ItemID: x + 1, |
||||
|
Idx: apitypes.HezIdx(idxToHez(account.Idx, token.Symbol)), |
||||
|
PublicKey: apitypes.NewHezBJJ(account.PublicKey), |
||||
|
EthAddr: apitypes.NewHezEthAddr(account.EthAddr), |
||||
|
Nonce: account.Nonce, |
||||
|
Balance: apitypes.NewBigIntStr(account.Balance), |
||||
|
Token: token, |
||||
|
} |
||||
|
tAccounts = append(tAccounts, tAccount) |
||||
|
} |
||||
|
return tAccounts |
||||
|
} |
||||
|
|
||||
|
func (t *testAccountsResponse) GetPagination() *db.Pagination { |
||||
|
if t.Accounts[0].ItemID < t.Accounts[len(t.Accounts)-1].ItemID { |
||||
|
t.Pagination.FirstReturnedItem = t.Accounts[0].ItemID |
||||
|
t.Pagination.LastReturnedItem = t.Accounts[len(t.Accounts)-1].ItemID |
||||
|
} else { |
||||
|
t.Pagination.LastReturnedItem = t.Accounts[0].ItemID |
||||
|
t.Pagination.FirstReturnedItem = t.Accounts[len(t.Accounts)-1].ItemID |
||||
|
} |
||||
|
return t.Pagination |
||||
|
} |
||||
|
|
||||
|
func (t *testAccountsResponse) Len() int { return len(t.Accounts) } |
||||
|
|
||||
|
func TestGetAccounts(t *testing.T) { |
||||
|
endpoint := apiURL + "accounts" |
||||
|
fetchedAccounts := []testAccount{} |
||||
|
|
||||
|
appendIter := func(intr interface{}) { |
||||
|
for i := 0; i < len(intr.(*testAccountsResponse).Accounts); i++ { |
||||
|
tmp, err := copystructure.Copy(intr.(*testAccountsResponse).Accounts[i]) |
||||
|
if err != nil { |
||||
|
panic(err) |
||||
|
} |
||||
|
fetchedAccounts = append(fetchedAccounts, tmp.(testAccount)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
limit := 5 |
||||
|
stringIds := strconv.Itoa(int(tc.tokens[2].TokenID)) + "," + strconv.Itoa(int(tc.tokens[5].TokenID)) + "," + strconv.Itoa(int(tc.tokens[6].TokenID)) |
||||
|
|
||||
|
// Filter by BJJ
|
||||
|
path := fmt.Sprintf("%s?BJJ=%s&limit=%d&fromItem=", endpoint, tc.accounts[0].PublicKey, limit) |
||||
|
err := doGoodReqPaginated(path, historydb.OrderAsc, &testAccountsResponse{}, appendIter) |
||||
|
assert.NoError(t, err) |
||||
|
assert.Greater(t, len(fetchedAccounts), 0) |
||||
|
assert.LessOrEqual(t, len(fetchedAccounts), len(tc.accounts)) |
||||
|
fetchedAccounts = []testAccount{} |
||||
|
// Filter by ethAddr
|
||||
|
path = fmt.Sprintf("%s?hermezEthereumAddress=%s&limit=%d&fromItem=", endpoint, tc.accounts[0].EthAddr, limit) |
||||
|
err = doGoodReqPaginated(path, historydb.OrderAsc, &testAccountsResponse{}, appendIter) |
||||
|
assert.NoError(t, err) |
||||
|
assert.Greater(t, len(fetchedAccounts), 0) |
||||
|
assert.LessOrEqual(t, len(fetchedAccounts), len(tc.accounts)) |
||||
|
fetchedAccounts = []testAccount{} |
||||
|
// both filters (incompatible)
|
||||
|
path = fmt.Sprintf("%s?hermezEthereumAddress=%s&BJJ=%s&limit=%d&fromItem=", endpoint, tc.accounts[0].EthAddr, tc.accounts[0].PublicKey, limit) |
||||
|
err = doBadReq("GET", path, nil, 400) |
||||
|
assert.NoError(t, err) |
||||
|
fetchedAccounts = []testAccount{} |
||||
|
// Filter by token IDs
|
||||
|
path = fmt.Sprintf("%s?tokenIds=%s&limit=%d&fromItem=", endpoint, stringIds, limit) |
||||
|
err = doGoodReqPaginated(path, historydb.OrderAsc, &testAccountsResponse{}, appendIter) |
||||
|
assert.NoError(t, err) |
||||
|
assert.Greater(t, len(fetchedAccounts), 0) |
||||
|
assert.LessOrEqual(t, len(fetchedAccounts), len(tc.accounts)) |
||||
|
fetchedAccounts = []testAccount{} |
||||
|
// Token Ids + bjj
|
||||
|
path = fmt.Sprintf("%s?tokenIds=%s&BJJ=%s&limit=%d&fromItem=", endpoint, stringIds, tc.accounts[0].PublicKey, limit) |
||||
|
err = doGoodReqPaginated(path, historydb.OrderAsc, &testAccountsResponse{}, appendIter) |
||||
|
assert.NoError(t, err) |
||||
|
assert.Greater(t, len(fetchedAccounts), 0) |
||||
|
assert.LessOrEqual(t, len(fetchedAccounts), len(tc.accounts)) |
||||
|
fetchedAccounts = []testAccount{} |
||||
|
// No filters (checks response content)
|
||||
|
path = fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit) |
||||
|
err = doGoodReqPaginated(path, historydb.OrderAsc, &testAccountsResponse{}, appendIter) |
||||
|
assert.NoError(t, err) |
||||
|
assert.Equal(t, len(tc.accounts), len(fetchedAccounts)) |
||||
|
for i := 0; i < len(fetchedAccounts); i++ { |
||||
|
fetchedAccounts[i].Token.ItemID = 0 |
||||
|
if tc.accounts[i].Token.USDUpdate != nil { |
||||
|
assert.Less(t, fetchedAccounts[i].Token.USDUpdate.Unix()-3, tc.accounts[i].Token.USDUpdate.Unix()) |
||||
|
fetchedAccounts[i].Token.USDUpdate = tc.accounts[i].Token.USDUpdate |
||||
|
} |
||||
|
assert.Equal(t, tc.accounts[i], fetchedAccounts[i]) |
||||
|
} |
||||
|
|
||||
|
// No filters Reverse Order (checks response content)
|
||||
|
reversedAccounts := []testAccount{} |
||||
|
appendIter = func(intr interface{}) { |
||||
|
for i := 0; i < len(intr.(*testAccountsResponse).Accounts); i++ { |
||||
|
tmp, err := copystructure.Copy(intr.(*testAccountsResponse).Accounts[i]) |
||||
|
if err != nil { |
||||
|
panic(err) |
||||
|
} |
||||
|
reversedAccounts = append(reversedAccounts, tmp.(testAccount)) |
||||
|
} |
||||
|
} |
||||
|
err = doGoodReqPaginated(path, historydb.OrderDesc, &testAccountsResponse{}, appendIter) |
||||
|
assert.NoError(t, err) |
||||
|
assert.Equal(t, len(reversedAccounts), len(fetchedAccounts)) |
||||
|
for i := 0; i < len(fetchedAccounts); i++ { |
||||
|
reversedAccounts[i].Token.ItemID = 0 |
||||
|
fetchedAccounts[len(fetchedAccounts)-1-i].Token.ItemID = 0 |
||||
|
if reversedAccounts[i].Token.USDUpdate != nil { |
||||
|
assert.Less(t, fetchedAccounts[len(fetchedAccounts)-1-i].Token.USDUpdate.Unix()-3, reversedAccounts[i].Token.USDUpdate.Unix()) |
||||
|
fetchedAccounts[len(fetchedAccounts)-1-i].Token.USDUpdate = reversedAccounts[i].Token.USDUpdate |
||||
|
} |
||||
|
assert.Equal(t, reversedAccounts[i], fetchedAccounts[len(fetchedAccounts)-1-i]) |
||||
|
} |
||||
|
|
||||
|
// Test GetAccount
|
||||
|
path = fmt.Sprintf("%s/%s", endpoint, fetchedAccounts[2].Idx) |
||||
|
account := testAccount{} |
||||
|
assert.NoError(t, doGoodReq("GET", path, nil, &account)) |
||||
|
account.Token.ItemID = 0 |
||||
|
assert.Equal(t, fetchedAccounts[2], account) |
||||
|
|
||||
|
// 400
|
||||
|
path = fmt.Sprintf("%s/hez:12345", endpoint) |
||||
|
err = doBadReq("GET", path, nil, 400) |
||||
|
assert.NoError(t, err) |
||||
|
// 404
|
||||
|
path = fmt.Sprintf("%s/hez:10:12345", endpoint) |
||||
|
err = doBadReq("GET", path, nil, 404) |
||||
|
assert.NoError(t, err) |
||||
|
} |