Refactor api txs

This commit is contained in:
Arnau B
2020-10-26 16:47:16 +01:00
parent decc4996ee
commit a329d894d2
18 changed files with 1452 additions and 1272 deletions

View File

@@ -8,7 +8,6 @@ import (
"fmt"
"io"
"io/ioutil"
"math"
"math/big"
"net/http"
"os"
@@ -45,103 +44,26 @@ type testCommon struct {
usrAddr string
usrBjj string
accs []common.Account
usrTxs []historyTxAPI
allTxs []historyTxAPI
usrTxs []testTx
allTxs []testTx
exits []exitAPI
usrExits []exitAPI
poolTxsToSend []receivedPoolTx
poolTxsToReceive []sendPoolTx
poolTxsToSend []testPoolTxSend
poolTxsToReceive []testPoolTxReceive
auths []accountCreationAuthAPI
router *swagger.Router
}
// 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
}
var tc testCommon
var config configAPI
// TestMain initializes the API server, and fill HistoryDB and StateDB with fake data,
// emulating the task of the synchronizer in order to have data to be returned
// by the API endpoints that will be tested
func TestMain(m *testing.M) {
// Init swagger
// Initializations
// Swagger
router := swagger.NewRouter().WithSwaggerFromFile("./swagger.yml")
// Init DBs
// HistoryDB
pass := os.Getenv("POSTGRES_PASS")
database, err := db.InitSQLDB(5432, "localhost", "hermez", pass, "hermez")
@@ -170,7 +92,7 @@ func TestMain(m *testing.M) {
// L2DB
l2DB := l2db.NewL2DB(database, 10, 100, 24*time.Hour)
test.CleanL2DB(l2DB.DB())
// Config (smart contract constants)
config.RollupConstants.ExchangeMultiplier = eth.RollupConstExchangeMultiplier
config.RollupConstants.ExitIdx = eth.RollupConstExitIDx
config.RollupConstants.ReservedIdx = eth.RollupConstReservedIDx
@@ -213,7 +135,7 @@ func TestMain(m *testing.M) {
config.AuctionConstants = auctionConstants
config.WDelayerConstants = wdelayerConstants
// Init API
// API
api := gin.Default()
if err := SetAPIEndpoints(
true,
@@ -235,7 +157,7 @@ func TestMain(m *testing.M) {
}
}()
// Populate DBs
// Fill HistoryDB and StateDB with fake data
// Clean DB
err = h.Reorg(0)
if err != nil {
@@ -302,6 +224,17 @@ func TestMain(m *testing.M) {
panic(err)
}
}
// helper to vinculate user related resources
usrIdxs := []string{}
for _, acc := range accs {
if acc.EthAddr == usrAddr || acc.PublicKey == usrBjj {
for _, token := range tokens {
if token.TokenID == acc.TokenID {
usrIdxs = append(usrIdxs, idxToHez(acc.Idx, token.Symbol))
}
}
}
}
// Gen exits and add them to DB
const totalExits = 40
exits := test.GenExitTree(totalExits, batches, accs)
@@ -309,15 +242,17 @@ func TestMain(m *testing.M) {
if err != nil {
panic(err)
}
// Gen L1Txs and add them to DB
// L1 and L2 txs need to be sorted in a combined way
// Gen L1Txs
const totalL1Txs = 40
const userL1Txs = 4
usrL1Txs, othrL1Txs := test.GenL1Txs(256, totalL1Txs, userL1Txs, &usrAddr, accs, tokens, blocks, batches)
// Gen L2Txs and add them to DB
// Gen L2Txs
const totalL2Txs = 20
const userL2Txs = 4
usrL2Txs, othrL2Txs := test.GenL2Txs(256+totalL1Txs, totalL2Txs, userL2Txs, &usrAddr, accs, tokens, blocks, batches)
// Order txs
// Sort txs
sortedTxs := []txSortFielder{}
for i := 0; i < len(usrL1Txs); i++ {
wL1 := wrappedL1(usrL1Txs[i])
@@ -336,161 +271,30 @@ func TestMain(m *testing.M) {
sortedTxs = append(sortedTxs, &wL2)
}
sort.Sort(txsSort(sortedTxs))
// Add txs to DB and prepare them for test commons
usrTxs := []historyTxAPI{}
allTxs := []historyTxAPI{}
getTimestamp := func(blockNum int64) time.Time {
for i := 0; i < len(blocks); i++ {
if blocks[i].EthBlockNum == blockNum {
return blocks[i].Timestamp
}
}
panic("timesamp not found")
}
getToken := func(id common.TokenID) historydb.TokenWithUSD {
for i := 0; i < len(tokensUSD); i++ {
if tokensUSD[i].TokenID == id {
return tokensUSD[i]
}
}
panic("token not found")
}
getTokenByIdx := func(idx common.Idx) historydb.TokenWithUSD {
for _, acc := range accs {
if idx == acc.Idx {
return getToken(acc.TokenID)
}
}
panic("token not found")
}
usrIdxs := []string{}
for _, acc := range accs {
if acc.EthAddr == usrAddr || acc.PublicKey == usrBjj {
for _, token := range tokens {
if token.TokenID == acc.TokenID {
usrIdxs = append(usrIdxs, idxToHez(acc.Idx, token.Symbol))
}
}
}
}
isUsrTx := func(tx historyTxAPI) bool {
for _, idx := range usrIdxs {
if tx.FromIdx != nil && *tx.FromIdx == idx {
return true
}
if tx.ToIdx == idx {
return true
}
}
return false
}
// Store txs to DB
for _, genericTx := range sortedTxs {
l1 := genericTx.L1()
l2 := genericTx.L2()
if l1 != nil {
// Add L1 tx to DB
err = h.AddL1Txs([]common.L1Tx{*l1})
if err != nil {
panic(err)
}
// L1Tx ==> historyTxAPI
token := getToken(l1.TokenID)
tx := historyTxAPI{
IsL1: "L1",
TxID: l1.TxID,
Type: l1.Type,
Position: l1.Position,
ToIdx: idxToHez(l1.ToIdx, token.Symbol),
Amount: l1.Amount.String(),
BatchNum: l1.BatchNum,
Timestamp: getTimestamp(l1.EthBlockNum),
L1Info: &l1Info{
ToForgeL1TxsNum: l1.ToForgeL1TxsNum,
UserOrigin: l1.UserOrigin,
FromEthAddr: ethAddrToHez(l1.FromEthAddr),
FromBJJ: bjjToString(l1.FromBJJ),
LoadAmount: l1.LoadAmount.String(),
EthBlockNum: l1.EthBlockNum,
},
Token: token,
}
if l1.FromIdx != 0 {
idxStr := idxToHez(l1.FromIdx, token.Symbol)
tx.FromIdx = &idxStr
}
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 {
// Add L2 tx to DB
} else if l2 != nil {
err = h.AddL2Txs([]common.L2Tx{*l2})
if err != nil {
panic(err)
}
// L2Tx ==> historyTxAPI
var tokenID common.TokenID
found := false
for _, acc := range accs {
if acc.Idx == l2.FromIdx {
found = true
tokenID = acc.TokenID
break
}
}
if !found {
panic("tokenID not found")
}
token := getToken(tokenID)
tx := historyTxAPI{
IsL1: "L2",
TxID: l2.TxID,
Type: l2.Type,
Position: l2.Position,
ToIdx: idxToHez(l2.ToIdx, token.Symbol),
Amount: l2.Amount.String(),
BatchNum: &l2.BatchNum,
Timestamp: getTimestamp(l2.EthBlockNum),
L2Info: &l2Info{
Nonce: l2.Nonce,
Fee: l2.Fee,
},
Token: token,
}
if l2.FromIdx != 0 {
idxStr := idxToHez(l2.FromIdx, token.Symbol)
tx.FromIdx = &idxStr
}
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)
}
} else {
panic("should be l1 or l2")
}
}
// Transform exits to API
exitsToAPIExits := func(exits []common.ExitInfo, accs []common.Account, tokens []common.Token) []exitAPI {
historyExits := []historydb.HistoryExit{}
for _, exit := range exits {
token := getTokenByIdx(exit.AccountIdx)
token := getTokenByIdx(exit.AccountIdx, tokensUSD, accs)
historyExits = append(historyExits, historydb.HistoryExit{
BatchNum: exit.BatchNum,
AccountIdx: exit.AccountIdx,
@@ -521,109 +325,7 @@ func TestMain(m *testing.M) {
}
}
}
// Prepare pool Txs
// Generate common.PoolL2Tx
// WARNING: this should be replaced once transakcio is ready
poolTxs := []common.PoolL2Tx{}
amount := new(big.Int)
amount, ok := amount.SetString("100000000000000", 10)
if !ok {
panic("bad amount")
}
poolTx := common.PoolL2Tx{
FromIdx: accs[0].Idx,
ToIdx: accs[1].Idx,
Amount: amount,
TokenID: accs[0].TokenID,
Nonce: 6,
}
if _, err := common.NewPoolL2Tx(&poolTx); err != nil {
panic(err)
}
h, err := poolTx.HashToSign()
if err != nil {
panic(err)
}
poolTx.Signature = privK.SignPoseidon(h).Compress()
poolTxs = append(poolTxs, poolTx)
// Transform to API formats
poolTxsToSend := []receivedPoolTx{}
poolTxsToReceive := []sendPoolTx{}
for _, poolTx := range poolTxs {
// common.PoolL2Tx ==> receivedPoolTx
token := getToken(poolTx.TokenID)
genSendTx := receivedPoolTx{
TxID: poolTx.TxID,
Type: poolTx.Type,
TokenID: poolTx.TokenID,
FromIdx: idxToHez(poolTx.FromIdx, token.Symbol),
Amount: poolTx.Amount.String(),
Fee: poolTx.Fee,
Nonce: poolTx.Nonce,
Signature: poolTx.Signature,
RqFee: &poolTx.RqFee,
RqNonce: &poolTx.RqNonce,
}
// common.PoolL2Tx ==> receivedPoolTx
genReceiveTx := sendPoolTx{
TxID: poolTx.TxID,
Type: poolTx.Type,
FromIdx: idxToHez(poolTx.FromIdx, token.Symbol),
Amount: poolTx.Amount.String(),
Fee: poolTx.Fee,
Nonce: poolTx.Nonce,
State: poolTx.State,
Signature: poolTx.Signature,
Timestamp: poolTx.Timestamp,
// BatchNum: poolTx.BatchNum,
RqFee: &poolTx.RqFee,
RqNonce: &poolTx.RqNonce,
Token: token,
}
if poolTx.ToIdx != 0 {
toIdx := idxToHez(poolTx.ToIdx, token.Symbol)
genSendTx.ToIdx = &toIdx
genReceiveTx.ToIdx = &toIdx
}
if poolTx.ToEthAddr != common.EmptyAddr {
toEth := ethAddrToHez(poolTx.ToEthAddr)
genSendTx.ToEthAddr = &toEth
genReceiveTx.ToEthAddr = &toEth
}
if poolTx.ToBJJ != nil {
toBJJ := bjjToString(poolTx.ToBJJ)
genSendTx.ToBJJ = &toBJJ
genReceiveTx.ToBJJ = &toBJJ
}
if poolTx.RqFromIdx != 0 {
rqFromIdx := idxToHez(poolTx.RqFromIdx, token.Symbol)
genSendTx.RqFromIdx = &rqFromIdx
genReceiveTx.RqFromIdx = &rqFromIdx
genSendTx.RqTokenID = &token.TokenID
genReceiveTx.RqTokenID = &token.TokenID
rqAmount := poolTx.RqAmount.String()
genSendTx.RqAmount = &rqAmount
genReceiveTx.RqAmount = &rqAmount
if poolTx.RqToIdx != 0 {
rqToIdx := idxToHez(poolTx.RqToIdx, token.Symbol)
genSendTx.RqToIdx = &rqToIdx
genReceiveTx.RqToIdx = &rqToIdx
}
if poolTx.RqToEthAddr != common.EmptyAddr {
rqToEth := ethAddrToHez(poolTx.RqToEthAddr)
genSendTx.RqToEthAddr = &rqToEth
genReceiveTx.RqToEthAddr = &rqToEth
}
if poolTx.RqToBJJ != nil {
rqToBJJ := bjjToString(poolTx.RqToBJJ)
genSendTx.RqToBJJ = &rqToBJJ
genReceiveTx.RqToBJJ = &rqToBJJ
}
}
poolTxsToSend = append(poolTxsToSend, genSendTx)
poolTxsToReceive = append(poolTxsToReceive, genReceiveTx)
}
// Coordinators
const nCoords = 10
coords := test.GenCoordinators(nCoords, blocks)
@@ -647,6 +349,8 @@ func TestMain(m *testing.M) {
apiAuths = append(apiAuths, *apiAuth)
}
// Set testCommon
usrTxs, allTxs := genTestTxs(sortedTxs, usrIdxs, accs, tokensUSD, blocks)
poolTxsToSend, poolTxsToReceive := genTestPoolTx(accs, []babyjub.PrivateKey{privK}, tokensUSD) // NOTE: pool txs are not inserted to the DB here. In the test they will be posted and getted.
tc = testCommon{
blocks: blocks,
tokens: tokensUSD,
@@ -667,8 +371,8 @@ func TestMain(m *testing.M) {
// Fake server
if os.Getenv("FAKE_SERVER") == "yes" {
for {
log.Info("Running fake server until ^C is received")
time.Sleep(10 * time.Second)
log.Info("Running fake server at " + apiURL + " until ^C is received")
time.Sleep(30 * time.Second)
}
}
// Run tests
@@ -686,217 +390,6 @@ func TestMain(m *testing.M) {
os.Exit(result)
}
func TestGetHistoryTxs(t *testing.T) {
endpoint := apiURL + "transactions-history"
fetchedTxs := []historyTxAPI{}
appendIter := func(intr interface{}) {
for i := 0; i < len(intr.(*historyTxsAPI).Txs); i++ {
tmp, err := copystructure.Copy(intr.(*historyTxsAPI).Txs[i])
if err != nil {
panic(err)
}
fetchedTxs = append(fetchedTxs, tmp.(historyTxAPI))
}
}
// Get all (no filters)
limit := 8
path := fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit)
err := doGoodReqPaginated(path, historydb.OrderAsc, &historyTxsAPI{}, appendIter)
assert.NoError(t, err)
assertHistoryTxAPIs(t, tc.allTxs, fetchedTxs)
// Uncomment once tx generation for tests is fixed
// // Get by ethAddr
// fetchedTxs = []historyTxAPI{}
// limit = 7
// path = fmt.Sprintf(
// "%s?hermezEthereumAddress=%s&limit=%d&fromItem=",
// endpoint, tc.usrAddr, limit,
// )
// err = doGoodReqPaginated(path, historydb.OrderAsc, &historyTxsAPI{}, appendIter)
// assert.NoError(t, err)
// assertHistoryTxAPIs(t, tc.usrTxs, fetchedTxs)
// // Get by bjj
// fetchedTxs = []historyTxAPI{}
// limit = 6
// path = fmt.Sprintf(
// "%s?BJJ=%s&limit=%d&fromItem=",
// endpoint, tc.usrBjj, limit,
// )
// err = doGoodReqPaginated(path, historydb.OrderAsc, &historyTxsAPI{}, appendIter)
// assert.NoError(t, err)
// assertHistoryTxAPIs(t, tc.usrTxs, fetchedTxs)
// Get by tokenID
fetchedTxs = []historyTxAPI{}
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, &historyTxsAPI{}, appendIter)
assert.NoError(t, err)
tokenIDTxs := []historyTxAPI{}
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 = []historyTxAPI{}
limit = 4
idx := tc.allTxs[0].ToIdx
path = fmt.Sprintf(
"%s?accountIndex=%s&limit=%d&fromItem=",
endpoint, idx, limit,
)
err = doGoodReqPaginated(path, historydb.OrderAsc, &historyTxsAPI{}, appendIter)
assert.NoError(t, err)
idxTxs := []historyTxAPI{}
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 = []historyTxAPI{}
limit = 3
batchNum := tc.allTxs[0].BatchNum
path = fmt.Sprintf(
"%s?batchNum=%d&limit=%d&fromItem=",
endpoint, *batchNum, limit,
)
err = doGoodReqPaginated(path, historydb.OrderAsc, &historyTxsAPI{}, appendIter)
assert.NoError(t, err)
batchNumTxs := []historyTxAPI{}
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 = []historyTxAPI{}
limit = 2
path = fmt.Sprintf(
"%s?type=%s&limit=%d&fromItem=",
endpoint, txType, limit,
)
err = doGoodReqPaginated(path, historydb.OrderAsc, &historyTxsAPI{}, appendIter)
assert.NoError(t, err)
txTypeTxs := []historyTxAPI{}
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 = []historyTxAPI{}
limit = 1
path = fmt.Sprintf(
"%s?batchNum=%d&tokenId=%d&limit=%d&fromItem=",
endpoint, *batchNum, tokenID, limit,
)
err = doGoodReqPaginated(path, historydb.OrderAsc, &historyTxsAPI{}, appendIter)
assert.NoError(t, err)
mixedTxs := []historyTxAPI{}
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 = []historyTxAPI{}
limit = 5
path = fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit)
err = doGoodReqPaginated(path, historydb.OrderDesc, &historyTxsAPI{}, appendIter)
assert.NoError(t, err)
flipedTxs := []historyTxAPI{}
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 := []historyTxAPI{}
for _, tx := range tc.allTxs {
fetchedTx := historyTxAPI{}
assert.NoError(t, doGoodReq("GET", endpoint+tx.TxID.String(), nil, &fetchedTx))
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 []historyTxAPI) {
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
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])
}
}
func TestGetExits(t *testing.T) {
endpoint := apiURL + "exits"
fetchedExits := []exitAPI{}
@@ -1078,89 +571,6 @@ func TestGetConfig(t *testing.T) {
assert.Equal(t, cg, &configTest)
}
func TestPoolTxs(t *testing.T) {
// POST
endpoint := apiURL + "transactions-pool"
fetchedTxID := common.TxID{}
for _, tx := range tc.poolTxsToSend {
jsonTxBytes, err := json.Marshal(tx)
assert.NoError(t, err)
jsonTxReader := bytes.NewReader(jsonTxBytes)
assert.NoError(
t, doGoodReq(
"POST",
endpoint,
jsonTxReader, &fetchedTxID,
),
)
assert.Equal(t, tx.TxID, fetchedTxID)
}
// 400
// Wrong signature
badTx := tc.poolTxsToSend[0]
badTx.FromIdx = "hez:foo:1000"
jsonTxBytes, err := json.Marshal(badTx)
assert.NoError(t, err)
jsonTxReader := bytes.NewReader(jsonTxBytes)
err = doBadReq("POST", endpoint, jsonTxReader, 400)
assert.NoError(t, err)
// Wrong to
badTx = tc.poolTxsToSend[0]
ethAddr := "hez:0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
badTx.ToEthAddr = &ethAddr
badTx.ToIdx = nil
jsonTxBytes, err = json.Marshal(badTx)
assert.NoError(t, err)
jsonTxReader = bytes.NewReader(jsonTxBytes)
err = doBadReq("POST", endpoint, jsonTxReader, 400)
assert.NoError(t, err)
// Wrong rq
badTx = tc.poolTxsToSend[0]
rqFromIdx := "hez:foo:30"
badTx.RqFromIdx = &rqFromIdx
jsonTxBytes, err = json.Marshal(badTx)
assert.NoError(t, err)
jsonTxReader = bytes.NewReader(jsonTxBytes)
err = doBadReq("POST", endpoint, jsonTxReader, 400)
assert.NoError(t, err)
// GET
endpoint += "/"
for _, tx := range tc.poolTxsToReceive {
fetchedTx := sendPoolTx{}
assert.NoError(
t, doGoodReq(
"GET",
endpoint+tx.TxID.String(),
nil, &fetchedTx,
),
)
assertPoolTx(t, tx, fetchedTx)
}
// 400
err = doBadReq("GET", endpoint+"0xG20000000156660000000090", nil, 400)
assert.NoError(t, err)
// 404
err = doBadReq("GET", endpoint+"0x020000000156660000000090", nil, 404)
assert.NoError(t, err)
}
func assertPoolTx(t *testing.T, expected, actual sendPoolTx) {
// state should be pending
assert.Equal(t, common.PoolL2TxStatePending, actual.State)
expected.State = actual.State
// timestamp should be very close to now
assert.Less(t, time.Now().UTC().Unix()-3, actual.Timestamp.Unix())
expected.Timestamp = actual.Timestamp
// token timestamp
if expected.Token.USDUpdate == nil {
assert.Equal(t, expected.Token.USDUpdate, actual.Token.USDUpdate)
} else {
assert.Equal(t, expected.Token.USDUpdate.Unix(), actual.Token.USDUpdate.Unix())
expected.Token.USDUpdate = actual.Token.USDUpdate
}
assert.Equal(t, expected, actual)
}
func TestAccountCreationAuth(t *testing.T) {
// POST
endpoint := apiURL + "account-creation-authorization"
@@ -1368,3 +778,41 @@ func doBadReq(method, path string, reqBody io.Reader, expectedResponseCode int)
responseValidationInput = responseValidationInput.SetBodyBytes(body)
return swagger.ValidateResponse(ctx, responseValidationInput)
}
// test helpers
func getTimestamp(blockNum int64, blocks []common.Block) time.Time {
for i := 0; i < len(blocks); i++ {
if blocks[i].EthBlockNum == blockNum {
return blocks[i].Timestamp
}
}
panic("timesamp not found")
}
func getTokenByID(id common.TokenID, tokens []historydb.TokenWithUSD) historydb.TokenWithUSD {
for i := 0; i < len(tokens); i++ {
if tokens[i].TokenID == id {
return tokens[i]
}
}
panic("token not found")
}
func getTokenByIdx(idx common.Idx, tokens []historydb.TokenWithUSD, accs []common.Account) historydb.TokenWithUSD {
for _, acc := range accs {
if idx == acc.Idx {
return getTokenByID(acc.TokenID, tokens)
}
}
panic("token not found")
}
func getAccountByIdx(idx common.Idx, accs []common.Account) *common.Account {
for _, acc := range accs {
if acc.Idx == idx {
return &acc
}
}
panic("account not found")
}

View File

@@ -85,7 +85,7 @@ func getBatch(c *gin.Context) {
type fullBatch struct {
Batch *historydb.BatchAPI
Txs []historyTxAPI
Txs []historydb.TxAPI
}
func getFullBatch(c *gin.Context) {
@@ -107,7 +107,7 @@ func getFullBatch(c *gin.Context) {
}
// Fetch txs from historyDB
// TODO
txs := []historyTxAPI{}
txs := []historydb.TxAPI{}
// JSON response
c.JSON(http.StatusOK, fullBatch{
Batch: batch,

View File

@@ -4,7 +4,6 @@ import (
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"math/big"
"strconv"
"strings"
@@ -14,7 +13,6 @@ import (
"github.com/hermeznetwork/hermez-node/common"
"github.com/hermeznetwork/hermez-node/db"
"github.com/hermeznetwork/hermez-node/db/historydb"
"github.com/hermeznetwork/hermez-node/db/l2db"
"github.com/hermeznetwork/hermez-node/eth"
"github.com/iden3/go-iden3-crypto/babyjub"
)
@@ -46,113 +44,6 @@ func idxToHez(idx common.Idx, tokenSymbol string) string {
return "hez:" + tokenSymbol + ":" + strconv.Itoa(int(idx))
}
// History Tx
type historyTxsAPI struct {
Txs []historyTxAPI `json:"transactions"`
Pagination *db.Pagination `json:"pagination"`
}
func (htx *historyTxsAPI) GetPagination() *db.Pagination {
if htx.Txs[0].ItemID < htx.Txs[len(htx.Txs)-1].ItemID {
htx.Pagination.FirstReturnedItem = htx.Txs[0].ItemID
htx.Pagination.LastReturnedItem = htx.Txs[len(htx.Txs)-1].ItemID
} else {
htx.Pagination.LastReturnedItem = htx.Txs[0].ItemID
htx.Pagination.FirstReturnedItem = htx.Txs[len(htx.Txs)-1].ItemID
}
return htx.Pagination
}
func (htx *historyTxsAPI) Len() int { return len(htx.Txs) }
type l1Info struct {
ToForgeL1TxsNum *int64 `json:"toForgeL1TransactionsNum"`
UserOrigin bool `json:"userOrigin"`
FromEthAddr string `json:"fromHezEthereumAddress"`
FromBJJ string `json:"fromBJJ"`
LoadAmount string `json:"loadAmount"`
HistoricLoadAmountUSD *float64 `json:"historicLoadAmountUSD"`
EthBlockNum int64 `json:"ethereumBlockNum"`
}
type l2Info struct {
Fee common.FeeSelector `json:"fee"`
HistoricFeeUSD *float64 `json:"historicFeeUSD"`
Nonce common.Nonce `json:"nonce"`
}
type historyTxAPI 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"`
ToIdx string `json:"toAccountIndex"`
Amount string `json:"amount"`
BatchNum *common.BatchNum `json:"batchNum"`
HistoricUSD *float64 `json:"historicUSD"`
Timestamp time.Time `json:"timestamp"`
L1Info *l1Info `json:"L1Info"`
L2Info *l2Info `json:"L2Info"`
Token historydb.TokenWithUSD `json:"token"`
}
func historyTxsToAPI(dbTxs []historydb.HistoryTx) []historyTxAPI {
apiTxs := []historyTxAPI{}
for i := 0; i < len(dbTxs); i++ {
apiTx := historyTxAPI{
TxID: dbTxs[i].TxID,
ItemID: dbTxs[i].ItemID,
Type: dbTxs[i].Type,
Position: dbTxs[i].Position,
ToIdx: idxToHez(dbTxs[i].ToIdx, dbTxs[i].TokenSymbol),
Amount: dbTxs[i].Amount.String(),
HistoricUSD: dbTxs[i].HistoricUSD,
BatchNum: dbTxs[i].BatchNum,
Timestamp: dbTxs[i].Timestamp,
Token: historydb.TokenWithUSD{
TokenID: dbTxs[i].TokenID,
EthBlockNum: dbTxs[i].TokenEthBlockNum,
EthAddr: dbTxs[i].TokenEthAddr,
Name: dbTxs[i].TokenName,
Symbol: dbTxs[i].TokenSymbol,
Decimals: dbTxs[i].TokenDecimals,
USD: dbTxs[i].TokenUSD,
USDUpdate: dbTxs[i].TokenUSDUpdate,
},
L1Info: nil,
L2Info: nil,
}
if dbTxs[i].FromIdx != nil {
fromIdx := new(string)
*fromIdx = idxToHez(*dbTxs[i].FromIdx, dbTxs[i].TokenSymbol)
apiTx.FromIdx = fromIdx
}
if dbTxs[i].IsL1 {
apiTx.IsL1 = "L1"
apiTx.L1Info = &l1Info{
ToForgeL1TxsNum: dbTxs[i].ToForgeL1TxsNum,
UserOrigin: *dbTxs[i].UserOrigin,
FromEthAddr: ethAddrToHez(*dbTxs[i].FromEthAddr),
FromBJJ: bjjToString(dbTxs[i].FromBJJ),
LoadAmount: dbTxs[i].LoadAmount.String(),
HistoricLoadAmountUSD: dbTxs[i].HistoricLoadAmountUSD,
EthBlockNum: dbTxs[i].EthBlockNum,
}
} else {
apiTx.IsL1 = "L2"
apiTx.L2Info = &l2Info{
Fee: *dbTxs[i].Fee,
HistoricFeeUSD: dbTxs[i].HistoricFeeUSD,
Nonce: *dbTxs[i].Nonce,
}
}
apiTxs = append(apiTxs, apiTx)
}
return apiTxs
}
// Exit
type exitsAPI struct {
@@ -262,322 +153,6 @@ type configAPI struct {
WDelayerConstants eth.WDelayerConstants `json:"withdrawalDelayer"`
}
// PoolL2Tx
type receivedPoolTx struct {
TxID common.TxID `json:"id" binding:"required"`
Type common.TxType `json:"type" binding:"required"`
TokenID common.TokenID `json:"tokenId"`
FromIdx string `json:"fromAccountIndex" binding:"required"`
ToIdx *string `json:"toAccountIndex"`
ToEthAddr *string `json:"toHezEthereumAddress"`
ToBJJ *string `json:"toBjj"`
Amount string `json:"amount" binding:"required"`
Fee common.FeeSelector `json:"fee"`
Nonce common.Nonce `json:"nonce"`
Signature babyjub.SignatureComp `json:"signature" binding:"required"`
RqFromIdx *string `json:"requestFromAccountIndex"`
RqToIdx *string `json:"requestToAccountIndex"`
RqToEthAddr *string `json:"requestToHezEthereumAddress"`
RqToBJJ *string `json:"requestToBjj"`
RqTokenID *common.TokenID `json:"requestTokenId"`
RqAmount *string `json:"requestAmount"`
RqFee *common.FeeSelector `json:"requestFee"`
RqNonce *common.Nonce `json:"requestNonce"`
}
func (tx *receivedPoolTx) toDBWritePoolL2Tx() (*l2db.PoolL2TxWrite, error) {
amount := new(big.Int)
amount.SetString(tx.Amount, 10)
txw := &l2db.PoolL2TxWrite{
TxID: tx.TxID,
TokenID: tx.TokenID,
Amount: amount,
Fee: tx.Fee,
Nonce: tx.Nonce,
State: common.PoolL2TxStatePending,
Signature: tx.Signature,
RqTokenID: tx.RqTokenID,
RqFee: tx.RqFee,
RqNonce: tx.RqNonce,
Type: tx.Type,
}
// Check FromIdx (required)
fidx, err := stringToIdx(tx.FromIdx, "fromAccountIndex")
if err != nil {
return nil, err
}
if fidx == nil {
return nil, errors.New("invalid fromAccountIndex")
}
// Set FromIdx
txw.FromIdx = common.Idx(*fidx)
// Set AmountFloat
f := new(big.Float).SetInt(amount)
amountF, _ := f.Float64()
txw.AmountFloat = amountF
if amountF < 0 {
return nil, errors.New("amount must be positive")
}
// Check "to" fields, only one of: ToIdx, ToEthAddr, ToBJJ
if tx.ToIdx != nil { // Case: Tx with ToIdx setted
// Set ToIdx
tidxUint, err := stringToIdx(*tx.ToIdx, "toAccountIndex")
if err != nil || tidxUint == nil {
return nil, errors.New("invalid toAccountIndex")
}
tidx := common.Idx(*tidxUint)
txw.ToIdx = &tidx
} else if tx.ToBJJ != nil { // Case: Tx with ToBJJ setted
// tx.ToEthAddr must be equal to ethAddrWhenBJJLower or ethAddrWhenBJJUpper
if tx.ToEthAddr != nil {
toEthAddr, err := hezStringToEthAddr(*tx.ToEthAddr, "toHezEthereumAddress")
if err != nil || *toEthAddr != common.FFAddr {
return nil, fmt.Errorf("if toBjj is setted, toHezEthereumAddress must be hez:%s", common.FFAddr.Hex())
}
} else {
return nil, fmt.Errorf("if toBjj is setted, toHezEthereumAddress must be hez:%s and toAccountIndex must be null", common.FFAddr.Hex())
}
// Set ToEthAddr and ToBJJ
toBJJ, err := hezStringToBJJ(*tx.ToBJJ, "toBjj")
if err != nil || toBJJ == nil {
return nil, errors.New("invalid toBjj")
}
txw.ToBJJ = toBJJ
txw.ToEthAddr = &common.FFAddr
} else if tx.ToEthAddr != nil { // Case: Tx with ToEthAddr setted
// Set ToEthAddr
toEthAddr, err := hezStringToEthAddr(*tx.ToEthAddr, "toHezEthereumAddress")
if err != nil || toEthAddr == nil {
return nil, errors.New("invalid toHezEthereumAddress")
}
txw.ToEthAddr = toEthAddr
} else {
return nil, errors.New("one of toAccountIndex, toHezEthereumAddress or toBjj must be setted")
}
// Check "rq" fields
if tx.RqFromIdx != nil {
// check and set RqFromIdx
rqfidxUint, err := stringToIdx(tx.FromIdx, "requestFromAccountIndex")
if err != nil || rqfidxUint == nil {
return nil, errors.New("invalid requestFromAccountIndex")
}
// Set RqFromIdx
rqfidx := common.Idx(*rqfidxUint)
txw.RqFromIdx = &rqfidx
// Case: RqTx with RqToIdx setted
if tx.RqToIdx != nil {
// Set ToIdx
tidxUint, err := stringToIdx(*tx.RqToIdx, "requestToAccountIndex")
if err != nil || tidxUint == nil {
return nil, errors.New("invalid requestToAccountIndex")
}
tidx := common.Idx(*tidxUint)
txw.ToIdx = &tidx
} else if tx.RqToBJJ != nil { // Case: Tx with ToBJJ setted
// tx.ToEthAddr must be equal to ethAddrWhenBJJLower or ethAddrWhenBJJUpper
if tx.RqToEthAddr != nil {
rqEthAddr, err := hezStringToEthAddr(*tx.RqToEthAddr, "")
if err != nil || *rqEthAddr != common.FFAddr {
return nil, fmt.Errorf("if requestToBjj is setted, requestToHezEthereumAddress must be hez:%s", common.FFAddr.Hex())
}
} else {
return nil, fmt.Errorf("if requestToBjj is setted, toHezEthereumAddress must be hez:%s and requestToAccountIndex must be null", common.FFAddr.Hex())
}
// Set ToEthAddr and ToBJJ
rqToBJJ, err := hezStringToBJJ(*tx.RqToBJJ, "requestToBjj")
if err != nil || rqToBJJ == nil {
return nil, errors.New("invalid requestToBjj")
}
txw.RqToBJJ = rqToBJJ
txw.RqToEthAddr = &common.FFAddr
} else if tx.RqToEthAddr != nil { // Case: Tx with ToEthAddr setted
// Set ToEthAddr
rqToEthAddr, err := hezStringToEthAddr(*tx.ToEthAddr, "requestToHezEthereumAddress")
if err != nil || rqToEthAddr == nil {
return nil, errors.New("invalid requestToHezEthereumAddress")
}
txw.RqToEthAddr = rqToEthAddr
} else {
return nil, errors.New("one of requestToAccountIndex, requestToHezEthereumAddress or requestToBjj must be setted")
}
if tx.RqAmount == nil {
return nil, errors.New("requestAmount must be provided if other request fields are setted")
}
rqAmount := new(big.Int)
rqAmount.SetString(*tx.RqAmount, 10)
txw.RqAmount = rqAmount
} else if tx.RqToIdx != nil && tx.RqToEthAddr != nil && tx.RqToBJJ != nil &&
tx.RqTokenID != nil && tx.RqAmount != nil && tx.RqNonce != nil && tx.RqFee != nil {
// if tx.RqToIdx is not setted, tx.Rq* must be null as well
return nil, errors.New("if requestFromAccountIndex is setted, the rest of request fields must be null as well")
}
return txw, validatePoolL2TxWrite(txw)
}
func validatePoolL2TxWrite(txw *l2db.PoolL2TxWrite) error {
poolTx := common.PoolL2Tx{
TxID: txw.TxID,
FromIdx: txw.FromIdx,
ToBJJ: txw.ToBJJ,
TokenID: txw.TokenID,
Amount: txw.Amount,
Fee: txw.Fee,
Nonce: txw.Nonce,
State: txw.State,
Signature: txw.Signature,
RqToBJJ: txw.RqToBJJ,
RqAmount: txw.RqAmount,
Type: txw.Type,
}
// ToIdx
if txw.ToIdx != nil {
poolTx.ToIdx = *txw.ToIdx
}
// ToEthAddr
if txw.ToEthAddr == nil {
poolTx.ToEthAddr = common.EmptyAddr
} else {
poolTx.ToEthAddr = *txw.ToEthAddr
}
// RqFromIdx
if txw.RqFromIdx != nil {
poolTx.RqFromIdx = *txw.RqFromIdx
}
// RqToIdx
if txw.RqToIdx != nil {
poolTx.RqToIdx = *txw.RqToIdx
}
// RqToEthAddr
if txw.RqToEthAddr == nil {
poolTx.RqToEthAddr = common.EmptyAddr
} else {
poolTx.RqToEthAddr = *txw.RqToEthAddr
}
// RqTokenID
if txw.RqTokenID != nil {
poolTx.RqTokenID = *txw.RqTokenID
}
// RqFee
if txw.RqFee != nil {
poolTx.RqFee = *txw.RqFee
}
// RqNonce
if txw.RqNonce != nil {
poolTx.RqNonce = *txw.RqNonce
}
// Check type and id
_, err := common.NewPoolL2Tx(&poolTx)
if err != nil {
return err
}
// Check signature
// Get public key
account, err := s.GetAccount(poolTx.FromIdx)
if err != nil {
return err
}
if !poolTx.VerifySignature(account.PublicKey) {
return errors.New("wrong signature")
}
return nil
}
type sendPoolTx struct {
TxID common.TxID `json:"id"`
Type common.TxType `json:"type"`
FromIdx string `json:"fromAccountIndex"`
ToIdx *string `json:"toAccountIndex"`
ToEthAddr *string `json:"toHezEthereumAddress"`
ToBJJ *string `json:"toBjj"`
Amount string `json:"amount"`
Fee common.FeeSelector `json:"fee"`
Nonce common.Nonce `json:"nonce"`
State common.PoolL2TxState `json:"state"`
Signature babyjub.SignatureComp `json:"signature"`
Timestamp time.Time `json:"timestamp"`
BatchNum *common.BatchNum `json:"batchNum"`
RqFromIdx *string `json:"requestFromAccountIndex"`
RqToIdx *string `json:"requestToAccountIndex"`
RqToEthAddr *string `json:"requestToHezEthereumAddress"`
RqToBJJ *string `json:"requestToBJJ"`
RqTokenID *common.TokenID `json:"requestTokenId"`
RqAmount *string `json:"requestAmount"`
RqFee *common.FeeSelector `json:"requestFee"`
RqNonce *common.Nonce `json:"requestNonce"`
Token historydb.TokenWithUSD `json:"token"`
}
func poolL2TxReadToSend(dbTx *l2db.PoolL2TxRead) *sendPoolTx {
tx := &sendPoolTx{
TxID: dbTx.TxID,
Type: dbTx.Type,
FromIdx: idxToHez(dbTx.FromIdx, dbTx.TokenSymbol),
Amount: dbTx.Amount.String(),
Fee: dbTx.Fee,
Nonce: dbTx.Nonce,
State: dbTx.State,
Signature: dbTx.Signature,
Timestamp: dbTx.Timestamp,
BatchNum: dbTx.BatchNum,
RqTokenID: dbTx.RqTokenID,
RqFee: dbTx.RqFee,
RqNonce: dbTx.RqNonce,
Token: historydb.TokenWithUSD{
TokenID: dbTx.TokenID,
EthBlockNum: dbTx.TokenEthBlockNum,
EthAddr: dbTx.TokenEthAddr,
Name: dbTx.TokenName,
Symbol: dbTx.TokenSymbol,
Decimals: dbTx.TokenDecimals,
USD: dbTx.TokenUSD,
USDUpdate: dbTx.TokenUSDUpdate,
},
}
// ToIdx
if dbTx.ToIdx != nil {
toIdx := idxToHez(*dbTx.ToIdx, dbTx.TokenSymbol)
tx.ToIdx = &toIdx
}
// ToEthAddr
if dbTx.ToEthAddr != nil {
toEth := ethAddrToHez(*dbTx.ToEthAddr)
tx.ToEthAddr = &toEth
}
// ToBJJ
if dbTx.ToBJJ != nil {
toBJJ := bjjToString(dbTx.ToBJJ)
tx.ToBJJ = &toBJJ
}
// RqFromIdx
if dbTx.RqFromIdx != nil {
rqFromIdx := idxToHez(*dbTx.RqFromIdx, dbTx.TokenSymbol)
tx.RqFromIdx = &rqFromIdx
}
// RqToIdx
if dbTx.RqToIdx != nil {
rqToIdx := idxToHez(*dbTx.RqToIdx, dbTx.TokenSymbol)
tx.RqToIdx = &rqToIdx
}
// RqToEthAddr
if dbTx.RqToEthAddr != nil {
rqToEth := ethAddrToHez(*dbTx.RqToEthAddr)
tx.RqToEthAddr = &rqToEth
}
// RqToBJJ
if dbTx.RqToBJJ != nil {
rqToBJJ := bjjToString(dbTx.RqToBJJ)
tx.RqToBJJ = &rqToBJJ
}
// RqAmount
if dbTx.RqAmount != nil {
rqAmount := dbTx.RqAmount.String()
tx.RqAmount = &rqAmount
}
return tx
}
// AccountCreationAuth
type accountCreationAuthAPI struct {

View File

@@ -68,46 +68,6 @@ func getAccountCreationAuth(c *gin.Context) {
c.JSON(http.StatusOK, apiAuth)
}
func postPoolTx(c *gin.Context) {
// Parse body
var receivedTx receivedPoolTx
if err := c.ShouldBindJSON(&receivedTx); err != nil {
retBadReq(err, c)
return
}
// Transform from received to insert format and validate
writeTx, err := receivedTx.toDBWritePoolL2Tx()
if err != nil {
retBadReq(err, c)
return
}
// Insert to DB
if err := l2.AddTx(writeTx); err != nil {
retSQLErr(err, c)
return
}
// Return TxID
c.JSON(http.StatusOK, writeTx.TxID.String())
}
func getPoolTx(c *gin.Context) {
// Get TxID
txID, err := parseParamTxID(c)
if err != nil {
retBadReq(err, c)
return
}
// Fetch tx from l2DB
dbTx, err := l2.GetTx(txID)
if err != nil {
retSQLErr(err, c)
return
}
apiTx := poolL2TxReadToSend(dbTx)
// Build succesfull response
c.JSON(http.StatusOK, apiTx)
}
func getAccounts(c *gin.Context) {
}
@@ -177,24 +137,6 @@ func getExit(c *gin.Context) {
c.JSON(http.StatusOK, apiExits[0])
}
func getHistoryTx(c *gin.Context) {
// Get TxID
txID, err := parseParamTxID(c)
if err != nil {
retBadReq(err, c)
return
}
// Fetch tx from historyDB
tx, err := h.GetHistoryTx(txID)
if err != nil {
retSQLErr(err, c)
return
}
apiTxs := historyTxsToAPI([]historydb.HistoryTx{*tx})
// Build succesfull response
c.JSON(http.StatusOK, apiTxs[0])
}
func getSlots(c *gin.Context) {
}

View File

@@ -1314,6 +1314,18 @@ components:
$ref: '#/components/schemas/TransactionTypeL2'
fromAccountIndex:
$ref: '#/components/schemas/AccountIndex'
fromHezEthereumAddress:
type: string
description: "Address of an Etherum account linked to the Hermez network."
pattern: "^hez:0x[a-fA-F0-9]{40}$"
example: "hez:0xaa942cfcd25ad4d90a62358b0dd84f33b398262a"
nullable: true
fromBJJ:
type: string
description: "BabyJubJub public key, encoded as base64 URL (RFC 4648), which result in 33 bytes. The padding byte is replaced by a sum of the encoded bytes."
pattern: "^hez:[A-Za-z0-9_-]{44}$"
example: "hez:9CK9fjQdMUTGm8KDvGLy3MB-vnP0NCcGX7Uh7OO6KRJm"
nullable: true
toAccountIndex:
type: string
description: >-
@@ -1416,6 +1428,8 @@ components:
- id
- type
- fromAccountIndex
- fromHezEthereumAddress
- fromBJJ
- toAccountIndex
- toHezEthereumAddress
- toBjj
@@ -1588,10 +1602,34 @@ components:
The identifier is built using: `hez:` + `token symbol:` + `index`
example: "hez:DAI:4444"
nullable: true
fromHezEthereumAddress:
type: string
description: "Address of an Etherum account linked to the Hermez network."
pattern: "^hez:0x[a-fA-F0-9]{40}$"
example: "hez:0xaa942cfcd25ad4d90a62358b0dd84f33b398262a"
nullable: true
fromBJJ:
type: string
description: "BabyJubJub public key, encoded as base64 URL (RFC 4648), which result in 33 bytes. The padding byte is replaced by a sum of the encoded bytes."
pattern: "^hez:[A-Za-z0-9_-]{44}$"
example: "hez:9CK9fjQdMUTGm8KDvGLy3MB-vnP0NCcGX7Uh7OO6KRJm"
nullable: true
toAccountIndex:
allOf:
- $ref: '#/components/schemas/AccountIndex'
- example: "hez:DAI:672"
toHezEthereumAddress:
type: string
description: "Address of an Etherum account linked to the Hermez network."
pattern: "^hez:0x[a-fA-F0-9]{40}$"
example: "hez:0xaa942cfcd25ad4d90a62358b0dd84f33b398262a"
nullable: true
toBJJ:
type: string
description: "BabyJubJub public key, encoded as base64 URL (RFC 4648), which result in 33 bytes. The padding byte is replaced by a sum of the encoded bytes."
pattern: "^hez:[A-Za-z0-9_-]{44}$"
example: "hez:f1J78_6uqTyjX6jrVCqN4RFeRBnWQAGl477ZFtOnH6Sm"
nullable: true
amount:
allOf:
- $ref: '#/components/schemas/BigInt'
@@ -1625,10 +1663,6 @@ components:
userOrigin:
type: boolean
description: True if the transaction was sent by a user. False if it was sent by a coordinator.
fromHezEthereumAddress:
$ref: '#/components/schemas/HezEthereumAddress'
fromBJJ:
$ref: '#/components/schemas/BJJ'
loadAmount:
allOf:
- $ref: '#/components/schemas/BigInt'
@@ -1647,8 +1681,6 @@ components:
required:
- toForgeL1TransactionsNum
- userOrigin
- fromHezEthereumAddress
- fromBJJ
- loadAmount
- historicLoadAmountUSD
- ethereumBlockNum
@@ -1680,7 +1712,11 @@ components:
- type
- position
- fromAccountIndex
- fromHezEthereumAddress
- fromBJJ
- toAccountIndex
- toHezEthereumAddress
- toBJJ
- amount
- batchNum
- historicUSD

View File

@@ -4,6 +4,8 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/hermeznetwork/hermez-node/db"
"github.com/hermeznetwork/hermez-node/db/historydb"
)
func getHistoryTxs(c *gin.Context) {
@@ -42,9 +44,29 @@ func getHistoryTxs(c *gin.Context) {
}
// Build succesfull response
apiTxs := historyTxsToAPI(txs)
c.JSON(http.StatusOK, &historyTxsAPI{
Txs: apiTxs,
type txsResponse struct {
Txs []historydb.TxAPI `json:"transactions"`
Pagination *db.Pagination `json:"pagination"`
}
c.JSON(http.StatusOK, &txsResponse{
Txs: txs,
Pagination: pagination,
})
}
func getHistoryTx(c *gin.Context) {
// Get TxID
txID, err := parseParamTxID(c)
if err != nil {
retBadReq(err, c)
return
}
// Fetch tx from historyDB
tx, err := h.GetHistoryTx(txID)
if err != nil {
retSQLErr(err, c)
return
}
// Build succesfull response
c.JSON(http.StatusOK, tx)
}

507
api/txshistory_test.go Normal file
View File

@@ -0,0 +1,507 @@
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])
}
}

171
api/txspool.go Normal file
View File

@@ -0,0 +1,171 @@
package api
import (
"errors"
"math/big"
"net/http"
ethCommon "github.com/ethereum/go-ethereum/common"
"github.com/gin-gonic/gin"
"github.com/hermeznetwork/hermez-node/apitypes"
"github.com/hermeznetwork/hermez-node/common"
"github.com/hermeznetwork/hermez-node/db/l2db"
"github.com/iden3/go-iden3-crypto/babyjub"
)
func postPoolTx(c *gin.Context) {
// Parse body
var receivedTx receivedPoolTx
if err := c.ShouldBindJSON(&receivedTx); err != nil {
retBadReq(err, c)
return
}
// Transform from received to insert format and validate
writeTx := receivedTx.toPoolL2TxWrite()
if err := verifyPoolL2TxWrite(writeTx); err != nil {
retBadReq(err, c)
return
}
// Insert to DB
if err := l2.AddTx(writeTx); err != nil {
retSQLErr(err, c)
return
}
// Return TxID
c.JSON(http.StatusOK, writeTx.TxID.String())
}
func getPoolTx(c *gin.Context) {
// Get TxID
txID, err := parseParamTxID(c)
if err != nil {
retBadReq(err, c)
return
}
// Fetch tx from l2DB
tx, err := l2.GetTxAPI(txID)
if err != nil {
retSQLErr(err, c)
return
}
// Build succesfull response
c.JSON(http.StatusOK, tx)
}
type receivedPoolTx struct {
TxID common.TxID `json:"id" binding:"required"`
Type common.TxType `json:"type" binding:"required"`
TokenID common.TokenID `json:"tokenId"`
FromIdx apitypes.StrHezIdx `json:"fromAccountIndex" binding:"required"`
ToIdx *apitypes.StrHezIdx `json:"toAccountIndex"`
ToEthAddr *apitypes.StrHezEthAddr `json:"toHezEthereumAddress"`
ToBJJ *apitypes.StrHezBJJ `json:"toBjj"`
Amount apitypes.StrBigInt `json:"amount" binding:"required"`
Fee common.FeeSelector `json:"fee"`
Nonce common.Nonce `json:"nonce"`
Signature babyjub.SignatureComp `json:"signature" binding:"required"`
RqFromIdx *apitypes.StrHezIdx `json:"requestFromAccountIndex"`
RqToIdx *apitypes.StrHezIdx `json:"requestToAccountIndex"`
RqToEthAddr *apitypes.StrHezEthAddr `json:"requestToHezEthereumAddress"`
RqToBJJ *apitypes.StrHezBJJ `json:"requestToBjj"`
RqTokenID *common.TokenID `json:"requestTokenId"`
RqAmount *apitypes.StrBigInt `json:"requestAmount"`
RqFee *common.FeeSelector `json:"requestFee"`
RqNonce *common.Nonce `json:"requestNonce"`
}
func (tx *receivedPoolTx) toPoolL2TxWrite() *l2db.PoolL2TxWrite {
f := new(big.Float).SetInt((*big.Int)(&tx.Amount))
amountF, _ := f.Float64()
return &l2db.PoolL2TxWrite{
TxID: tx.TxID,
FromIdx: common.Idx(tx.FromIdx),
ToIdx: (*common.Idx)(tx.ToIdx),
ToEthAddr: (*ethCommon.Address)(tx.ToEthAddr),
ToBJJ: (*babyjub.PublicKey)(tx.ToBJJ),
TokenID: tx.TokenID,
Amount: (*big.Int)(&tx.Amount),
AmountFloat: amountF,
Fee: tx.Fee,
Nonce: tx.Nonce,
State: common.PoolL2TxStatePending,
Signature: tx.Signature,
RqFromIdx: (*common.Idx)(tx.RqFromIdx),
RqToIdx: (*common.Idx)(tx.RqToIdx),
RqToEthAddr: (*ethCommon.Address)(tx.RqToEthAddr),
RqToBJJ: (*babyjub.PublicKey)(tx.RqToBJJ),
RqTokenID: tx.RqTokenID,
RqAmount: (*big.Int)(tx.RqAmount),
RqFee: tx.RqFee,
RqNonce: tx.RqNonce,
Type: tx.Type,
}
}
func verifyPoolL2TxWrite(txw *l2db.PoolL2TxWrite) error {
poolTx := common.PoolL2Tx{
TxID: txw.TxID,
FromIdx: txw.FromIdx,
ToBJJ: txw.ToBJJ,
TokenID: txw.TokenID,
Amount: txw.Amount,
Fee: txw.Fee,
Nonce: txw.Nonce,
// State: txw.State,
Signature: txw.Signature,
RqToBJJ: txw.RqToBJJ,
RqAmount: txw.RqAmount,
Type: txw.Type,
}
// ToIdx
if txw.ToIdx != nil {
poolTx.ToIdx = *txw.ToIdx
}
// ToEthAddr
if txw.ToEthAddr == nil {
poolTx.ToEthAddr = common.EmptyAddr
} else {
poolTx.ToEthAddr = *txw.ToEthAddr
}
// RqFromIdx
if txw.RqFromIdx != nil {
poolTx.RqFromIdx = *txw.RqFromIdx
}
// RqToIdx
if txw.RqToIdx != nil {
poolTx.RqToIdx = *txw.RqToIdx
}
// RqToEthAddr
if txw.RqToEthAddr == nil {
poolTx.RqToEthAddr = common.EmptyAddr
} else {
poolTx.RqToEthAddr = *txw.RqToEthAddr
}
// RqTokenID
if txw.RqTokenID != nil {
poolTx.RqTokenID = *txw.RqTokenID
}
// RqFee
if txw.RqFee != nil {
poolTx.RqFee = *txw.RqFee
}
// RqNonce
if txw.RqNonce != nil {
poolTx.RqNonce = *txw.RqNonce
}
// Check type and id
_, err := common.NewPoolL2Tx(&poolTx)
if err != nil {
return err
}
// Get public key
account, err := s.GetAccount(poolTx.FromIdx)
if err != nil {
return err
}
// Check signature
if !poolTx.VerifySignature(account.PublicKey) {
return errors.New("wrong signature")
}
return nil
}

272
api/txspool_test.go Normal file
View File

@@ -0,0 +1,272 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"math/big"
"testing"
"time"
"github.com/hermeznetwork/hermez-node/common"
"github.com/hermeznetwork/hermez-node/db/historydb"
"github.com/iden3/go-iden3-crypto/babyjub"
"github.com/stretchr/testify/assert"
)
// testPoolTxReceive is a struct to be used to assert the response
// of GET /transactions-pool/:id
type testPoolTxReceive struct {
TxID common.TxID `json:"id"`
Type common.TxType `json:"type"`
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"`
Fee common.FeeSelector `json:"fee"`
Nonce common.Nonce `json:"nonce"`
State common.PoolL2TxState `json:"state"`
Signature babyjub.SignatureComp `json:"signature"`
RqFromIdx *string `json:"requestFromAccountIndex"`
RqToIdx *string `json:"requestToAccountIndex"`
RqToEthAddr *string `json:"requestToHezEthereumAddress"`
RqToBJJ *string `json:"requestToBJJ"`
RqTokenID *common.TokenID `json:"requestTokenId"`
RqAmount *string `json:"requestAmount"`
RqFee *common.FeeSelector `json:"requestFee"`
RqNonce *common.Nonce `json:"requestNonce"`
BatchNum *common.BatchNum `json:"batchNum"`
Timestamp time.Time `json:"timestamp"`
Token historydb.TokenWithUSD `json:"token"`
}
// testPoolTxSend is a struct to be used as a JSON body
// when testing POST /transactions-pool
type testPoolTxSend struct {
TxID common.TxID `json:"id" binding:"required"`
Type common.TxType `json:"type" binding:"required"`
TokenID common.TokenID `json:"tokenId"`
FromIdx string `json:"fromAccountIndex" binding:"required"`
ToIdx *string `json:"toAccountIndex"`
ToEthAddr *string `json:"toHezEthereumAddress"`
ToBJJ *string `json:"toBjj"`
Amount string `json:"amount" binding:"required"`
Fee common.FeeSelector `json:"fee"`
Nonce common.Nonce `json:"nonce"`
Signature babyjub.SignatureComp `json:"signature" binding:"required"`
RqFromIdx *string `json:"requestFromAccountIndex"`
RqToIdx *string `json:"requestToAccountIndex"`
RqToEthAddr *string `json:"requestToHezEthereumAddress"`
RqToBJJ *string `json:"requestToBjj"`
RqTokenID *common.TokenID `json:"requestTokenId"`
RqAmount *string `json:"requestAmount"`
RqFee *common.FeeSelector `json:"requestFee"`
RqNonce *common.Nonce `json:"requestNonce"`
}
func genTestPoolTx(accs []common.Account, privKs []babyjub.PrivateKey, tokens []historydb.TokenWithUSD) (poolTxsToSend []testPoolTxSend, poolTxsToReceive []testPoolTxReceive) {
// Generate common.PoolL2Tx
// WARNING: this should be replaced once til is ready
poolTxs := []common.PoolL2Tx{}
amount := new(big.Int)
amount, ok := amount.SetString("100000000000000", 10)
if !ok {
panic("bad amount")
}
poolTx := common.PoolL2Tx{
FromIdx: accs[0].Idx,
ToIdx: accs[1].Idx,
Amount: amount,
TokenID: accs[0].TokenID,
Nonce: 6,
}
if _, err := common.NewPoolL2Tx(&poolTx); err != nil {
panic(err)
}
h, err := poolTx.HashToSign()
if err != nil {
panic(err)
}
poolTx.Signature = privKs[0].SignPoseidon(h).Compress()
poolTxs = append(poolTxs, poolTx)
// Transform to API formats
poolTxsToSend = []testPoolTxSend{}
poolTxsToReceive = []testPoolTxReceive{}
for _, poolTx := range poolTxs {
fmt.Println(poolTx)
// common.PoolL2Tx ==> testPoolTxSend
token := getTokenByID(poolTx.TokenID, tokens)
genSendTx := testPoolTxSend{
TxID: poolTx.TxID,
Type: poolTx.Type,
TokenID: poolTx.TokenID,
FromIdx: idxToHez(poolTx.FromIdx, token.Symbol),
Amount: poolTx.Amount.String(),
Fee: poolTx.Fee,
Nonce: poolTx.Nonce,
Signature: poolTx.Signature,
RqFee: &poolTx.RqFee,
RqNonce: &poolTx.RqNonce,
}
// common.PoolL2Tx ==> testPoolTxReceive
genReceiveTx := testPoolTxReceive{
TxID: poolTx.TxID,
Type: poolTx.Type,
FromIdx: idxToHez(poolTx.FromIdx, token.Symbol),
Amount: poolTx.Amount.String(),
Fee: poolTx.Fee,
Nonce: poolTx.Nonce,
State: poolTx.State,
Signature: poolTx.Signature,
Timestamp: poolTx.Timestamp,
// BatchNum: poolTx.BatchNum,
RqFee: &poolTx.RqFee,
RqNonce: &poolTx.RqNonce,
Token: token,
}
fromAcc := getAccountByIdx(poolTx.ToIdx, accs)
fromAddr := ethAddrToHez(fromAcc.EthAddr)
genReceiveTx.FromEthAddr = &fromAddr
fromBjj := bjjToString(fromAcc.PublicKey)
genReceiveTx.FromBJJ = &fromBjj
if poolTx.ToIdx != 0 {
toIdx := idxToHez(poolTx.ToIdx, token.Symbol)
genSendTx.ToIdx = &toIdx
genReceiveTx.ToIdx = &toIdx
}
if poolTx.ToEthAddr != common.EmptyAddr {
toEth := ethAddrToHez(poolTx.ToEthAddr)
genSendTx.ToEthAddr = &toEth
genReceiveTx.ToEthAddr = &toEth
} else if poolTx.ToIdx > 255 {
acc := getAccountByIdx(poolTx.ToIdx, accs)
addr := ethAddrToHez(acc.EthAddr)
genReceiveTx.ToEthAddr = &addr
}
if poolTx.ToBJJ != nil {
toBJJ := bjjToString(poolTx.ToBJJ)
genSendTx.ToBJJ = &toBJJ
genReceiveTx.ToBJJ = &toBJJ
} else if poolTx.ToIdx > 255 {
acc := getAccountByIdx(poolTx.ToIdx, accs)
bjj := bjjToString(acc.PublicKey)
genReceiveTx.ToBJJ = &bjj
}
if poolTx.RqFromIdx != 0 {
rqFromIdx := idxToHez(poolTx.RqFromIdx, token.Symbol)
genSendTx.RqFromIdx = &rqFromIdx
genReceiveTx.RqFromIdx = &rqFromIdx
genSendTx.RqTokenID = &token.TokenID
genReceiveTx.RqTokenID = &token.TokenID
rqAmount := poolTx.RqAmount.String()
genSendTx.RqAmount = &rqAmount
genReceiveTx.RqAmount = &rqAmount
if poolTx.RqToIdx != 0 {
rqToIdx := idxToHez(poolTx.RqToIdx, token.Symbol)
genSendTx.RqToIdx = &rqToIdx
genReceiveTx.RqToIdx = &rqToIdx
}
if poolTx.RqToEthAddr != common.EmptyAddr {
rqToEth := ethAddrToHez(poolTx.RqToEthAddr)
genSendTx.RqToEthAddr = &rqToEth
genReceiveTx.RqToEthAddr = &rqToEth
}
if poolTx.RqToBJJ != nil {
rqToBJJ := bjjToString(poolTx.RqToBJJ)
genSendTx.RqToBJJ = &rqToBJJ
genReceiveTx.RqToBJJ = &rqToBJJ
}
}
poolTxsToSend = append(poolTxsToSend, genSendTx)
poolTxsToReceive = append(poolTxsToReceive, genReceiveTx)
}
return poolTxsToSend, poolTxsToReceive
}
func TestPoolTxs(t *testing.T) {
// POST
endpoint := apiURL + "transactions-pool"
fetchedTxID := common.TxID{}
for _, tx := range tc.poolTxsToSend {
jsonTxBytes, err := json.Marshal(tx)
assert.NoError(t, err)
jsonTxReader := bytes.NewReader(jsonTxBytes)
assert.NoError(
t, doGoodReq(
"POST",
endpoint,
jsonTxReader, &fetchedTxID,
),
)
assert.Equal(t, tx.TxID, fetchedTxID)
}
// 400
// Wrong signature
badTx := tc.poolTxsToSend[0]
badTx.FromIdx = "hez:foo:1000"
jsonTxBytes, err := json.Marshal(badTx)
assert.NoError(t, err)
jsonTxReader := bytes.NewReader(jsonTxBytes)
err = doBadReq("POST", endpoint, jsonTxReader, 400)
assert.NoError(t, err)
// Wrong to
badTx = tc.poolTxsToSend[0]
ethAddr := "hez:0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
badTx.ToEthAddr = &ethAddr
badTx.ToIdx = nil
jsonTxBytes, err = json.Marshal(badTx)
assert.NoError(t, err)
jsonTxReader = bytes.NewReader(jsonTxBytes)
err = doBadReq("POST", endpoint, jsonTxReader, 400)
assert.NoError(t, err)
// Wrong rq
badTx = tc.poolTxsToSend[0]
rqFromIdx := "hez:foo:30"
badTx.RqFromIdx = &rqFromIdx
jsonTxBytes, err = json.Marshal(badTx)
assert.NoError(t, err)
jsonTxReader = bytes.NewReader(jsonTxBytes)
err = doBadReq("POST", endpoint, jsonTxReader, 400)
assert.NoError(t, err)
// GET
endpoint += "/"
for _, tx := range tc.poolTxsToReceive {
fetchedTx := testPoolTxReceive{}
assert.NoError(
t, doGoodReq(
"GET",
endpoint+tx.TxID.String(),
nil, &fetchedTx,
),
)
assertPoolTx(t, tx, fetchedTx)
}
// 400
err = doBadReq("GET", endpoint+"0xG20000000156660000000090", nil, 400)
assert.NoError(t, err)
// 404
err = doBadReq("GET", endpoint+"0x020000000156660000000090", nil, 404)
assert.NoError(t, err)
}
func assertPoolTx(t *testing.T, expected, actual testPoolTxReceive) {
// state should be pending
assert.Equal(t, common.PoolL2TxStatePending, actual.State)
expected.State = actual.State
actual.Token.ItemID = 0
// timestamp should be very close to now
assert.Less(t, time.Now().UTC().Unix()-3, actual.Timestamp.Unix())
expected.Timestamp = actual.Timestamp
// token timestamp
if expected.Token.USDUpdate == nil {
assert.Equal(t, expected.Token.USDUpdate, actual.Token.USDUpdate)
} else {
assert.Equal(t, expected.Token.USDUpdate.Unix(), actual.Token.USDUpdate.Unix())
expected.Token.USDUpdate = actual.Token.USDUpdate
}
assert.Equal(t, expected, actual)
}