From a329d894d2d87913addf0b881e411df6aebfa7a0 Mon Sep 17 00:00:00 2001 From: Arnau B Date: Mon, 26 Oct 2020 16:47:16 +0100 Subject: [PATCH] Refactor api txs --- api/api_test.go | 704 ++++---------------------------------- api/batch.go | 4 +- api/dbtoapistructs.go | 425 ----------------------- api/handlers.go | 58 ---- api/swagger.yml | 48 ++- api/txshistory.go | 28 +- api/txshistory_test.go | 507 +++++++++++++++++++++++++++ api/txspool.go | 171 +++++++++ api/txspool_test.go | 272 +++++++++++++++ apitypes/apitypes.go | 74 +++- apitypes/apitypes_test.go | 49 +++ db/historydb/historydb.go | 28 +- db/historydb/views.go | 95 +++-- db/l2db/l2db.go | 35 +- db/l2db/l2db_test.go | 79 +---- db/l2db/views.go | 70 +++- db/migrations/0001.sql | 76 +++- go.sum | 1 + 18 files changed, 1452 insertions(+), 1272 deletions(-) create mode 100644 api/txshistory_test.go create mode 100644 api/txspool.go create mode 100644 api/txspool_test.go diff --git a/api/api_test.go b/api/api_test.go index a3bac19..42b2252 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -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 = ðAddr - 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") +} diff --git a/api/batch.go b/api/batch.go index 954bb61..72031a4 100644 --- a/api/batch.go +++ b/api/batch.go @@ -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, diff --git a/api/dbtoapistructs.go b/api/dbtoapistructs.go index 4a8cd70..1eee777 100644 --- a/api/dbtoapistructs.go +++ b/api/dbtoapistructs.go @@ -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 { diff --git a/api/handlers.go b/api/handlers.go index ed60d2c..2bd598b 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -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) { } diff --git a/api/swagger.yml b/api/swagger.yml index 5cc0490..30515d7 100644 --- a/api/swagger.yml +++ b/api/swagger.yml @@ -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 diff --git a/api/txshistory.go b/api/txshistory.go index 02c949c..0a6a0e6 100644 --- a/api/txshistory.go +++ b/api/txshistory.go @@ -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) +} diff --git a/api/txshistory_test.go b/api/txshistory_test.go new file mode 100644 index 0000000..1d3cb73 --- /dev/null +++ b/api/txshistory_test.go @@ -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]) + } +} diff --git a/api/txspool.go b/api/txspool.go new file mode 100644 index 0000000..8a43ee3 --- /dev/null +++ b/api/txspool.go @@ -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 +} diff --git a/api/txspool_test.go b/api/txspool_test.go new file mode 100644 index 0000000..826bd90 --- /dev/null +++ b/api/txspool_test.go @@ -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 = ðAddr + 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) +} diff --git a/apitypes/apitypes.go b/apitypes/apitypes.go index 284a0d9..a97ab98 100644 --- a/apitypes/apitypes.go +++ b/apitypes/apitypes.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "math/big" + "strconv" "strings" ethCommon "github.com/ethereum/go-ethereum/common" @@ -71,6 +72,19 @@ func (b BigIntStr) Value() (driver.Value, error) { return base64.StdEncoding.EncodeToString(bigInt.Bytes()), nil } +// StrBigInt is used to unmarshal BigIntStr directly into an alias of big.Int +type StrBigInt big.Int + +// UnmarshalText unmarshals a StrBigInt +func (s *StrBigInt) UnmarshalText(text []byte) error { + bi, ok := (*big.Int)(s).SetString(string(text), 10) + if !ok { + return fmt.Errorf("could not unmarshal %s into a StrBigInt", text) + } + *s = StrBigInt(*bi) + return nil +} + // CollectedFees is used to retrieve common.batch.CollectedFee from the DB type CollectedFees map[common.TokenID]BigIntStr @@ -127,6 +141,20 @@ func (a HezEthAddr) Value() (driver.Value, error) { return ethAddr.Value() } +// StrHezEthAddr is used to unmarshal HezEthAddr directly into an alias of ethCommon.Address +type StrHezEthAddr ethCommon.Address + +// UnmarshalText unmarshals a StrHezEthAddr +func (s *StrHezEthAddr) UnmarshalText(text []byte) error { + withoutHez := strings.TrimPrefix(string(text), "hez:") + var addr ethCommon.Address + if err := addr.UnmarshalText([]byte(withoutHez)); err != nil { + return err + } + *s = StrHezEthAddr(addr) + return nil +} + // HezBJJ is used to scan/value *babyjub.PublicKey directly into strings that follow the BJJ public key hez fotmat (^hez:[A-Za-z0-9_-]{44}$) from/to sql DBs. // It assumes that *babyjub.PublicKey are inserted/fetched to/from the DB using the default Scan/Value interface type HezBJJ string @@ -143,12 +171,11 @@ func NewHezBJJ(bjj *babyjub.PublicKey) HezBJJ { return HezBJJ("hez:" + base64.RawURLEncoding.EncodeToString(bjjSum)) } -// ToBJJ returns a *babyjub.PublicKey created from HezBJJ -func (b HezBJJ) ToBJJ() (*babyjub.PublicKey, error) { +func hezStrToBJJ(s string) (*babyjub.PublicKey, error) { const decodedLen = 33 const encodedLen = 44 formatErr := errors.New("invalid BJJ format. Must follow this regex: ^hez:[A-Za-z0-9_-]{44}$") - encoded := strings.TrimPrefix(string(b), "hez:") + encoded := strings.TrimPrefix(s, "hez:") if len(encoded) != encodedLen { return nil, formatErr } @@ -172,6 +199,11 @@ func (b HezBJJ) ToBJJ() (*babyjub.PublicKey, error) { return bjjComp.Decompress() } +// ToBJJ returns a *babyjub.PublicKey created from HezBJJ +func (b HezBJJ) ToBJJ() (*babyjub.PublicKey, error) { + return hezStrToBJJ(string(b)) +} + // Scan implements Scanner for database/sql func (b *HezBJJ) Scan(src interface{}) error { bjj := &babyjub.PublicKey{} @@ -193,3 +225,39 @@ func (b HezBJJ) Value() (driver.Value, error) { } return bjj.Value() } + +// StrHezBJJ is used to unmarshal HezBJJ directly into an alias of babyjub.PublicKey +type StrHezBJJ babyjub.PublicKey + +// UnmarshalText unmarshals a StrHezBJJ +func (s *StrHezBJJ) UnmarshalText(text []byte) error { + bjj, err := hezStrToBJJ(string(text)) + if err != nil { + return err + } + *s = StrHezBJJ(*bjj) + return nil +} + +// HezIdx is used to value common.Idx directly into strings that follow the Idx key hez fotmat (hez:tokenSymbol:idx) to sql DBs. +// Note that this can only be used to insert to DB since there is no way to automaticaly read from the DB since it needs the tokenSymbol +type HezIdx string + +// StrHezIdx is used to unmarshal HezIdx directly into an alias of common.Idx +type StrHezIdx common.Idx + +// UnmarshalText unmarshals a StrHezIdx +func (s *StrHezIdx) UnmarshalText(text []byte) error { + withoutHez := strings.TrimPrefix(string(text), "hez:") + splitted := strings.Split(withoutHez, ":") + const expectedLen = 2 + if len(splitted) != expectedLen { + return fmt.Errorf("can not unmarshal %s into StrHezIdx", text) + } + idxInt, err := strconv.Atoi(splitted[1]) + if err != nil { + return err + } + *s = StrHezIdx(common.Idx(idxInt)) + return nil +} diff --git a/apitypes/apitypes_test.go b/apitypes/apitypes_test.go index 69aeba1..2ce058d 100644 --- a/apitypes/apitypes_test.go +++ b/apitypes/apitypes_test.go @@ -2,12 +2,14 @@ package apitypes import ( "database/sql" + "encoding/json" "io/ioutil" "math/big" "os" "testing" ethCommon "github.com/ethereum/go-ethereum/common" + "github.com/hermeznetwork/hermez-node/common" dbUtils "github.com/hermeznetwork/hermez-node/db" "github.com/iden3/go-iden3-crypto/babyjub" _ "github.com/mattn/go-sqlite3" //nolint sqlite driver @@ -123,6 +125,53 @@ func TestBigIntStrScannerValuer(t *testing.T) { assert.Nil(t, toMeddlerNil.I) } +func TestStrBigInt(t *testing.T) { + type testStrBigInt struct { + I StrBigInt + } + from := []byte(`{"I":"4"}`) + to := &testStrBigInt{} + assert.NoError(t, json.Unmarshal(from, to)) + assert.Equal(t, big.NewInt(4), (*big.Int)(&to.I)) +} + +func TestStrHezEthAddr(t *testing.T) { + type testStrHezEthAddr struct { + I StrHezEthAddr + } + withoutHez := "0xaa942cfcd25ad4d90a62358b0dd84f33b398262a" + from := []byte(`{"I":"hez:` + withoutHez + `"}`) + var addr ethCommon.Address + if err := addr.UnmarshalText([]byte(withoutHez)); err != nil { + panic(err) + } + to := &testStrHezEthAddr{} + assert.NoError(t, json.Unmarshal(from, to)) + assert.Equal(t, addr, ethCommon.Address(to.I)) +} + +func TestStrHezBJJ(t *testing.T) { + type testStrHezBJJ struct { + I StrHezBJJ + } + priv := babyjub.NewRandPrivKey() + hezBjj := NewHezBJJ(priv.Public()) + from := []byte(`{"I":"` + hezBjj + `"}`) + to := &testStrHezBJJ{} + assert.NoError(t, json.Unmarshal(from, to)) + assert.Equal(t, priv.Public(), (*babyjub.PublicKey)(&to.I)) +} + +func TestStrHezIdx(t *testing.T) { + type testStrHezIdx struct { + I StrHezIdx + } + from := []byte(`{"I":"hez:foo:4"}`) + to := &testStrHezIdx{} + assert.NoError(t, json.Unmarshal(from, to)) + assert.Equal(t, common.Idx(4), common.Idx(to.I)) +} + func TestHezEthAddr(t *testing.T) { // Clean DB _, err := db.Exec("delete from test") diff --git a/db/historydb/historydb.go b/db/historydb/historydb.go index 5b1aef0..ca8b435 100644 --- a/db/historydb/historydb.go +++ b/db/historydb/historydb.go @@ -651,15 +651,16 @@ func (hdb *HistoryDB) addTxs(d meddler.DB, txs []txWrite) error { // } // GetHistoryTx returns a tx from the DB given a TxID -func (hdb *HistoryDB) GetHistoryTx(txID common.TxID) (*HistoryTx, error) { - tx := &HistoryTx{} +func (hdb *HistoryDB) GetHistoryTx(txID common.TxID) (*TxAPI, error) { + tx := &TxAPI{} err := meddler.QueryRow( hdb.db, tx, `SELECT tx.item_id, tx.is_l1, tx.id, tx.type, tx.position, - tx.from_idx, tx.to_idx, tx.amount, tx.token_id, tx.amount_usd, + hez_idx(tx.from_idx, token.symbol) AS from_idx, tx.from_eth_addr, tx.from_bjj, + hez_idx(tx.to_idx, token.symbol) AS to_idx, tx.to_eth_addr, tx.to_bjj, + tx.amount, tx.token_id, tx.amount_usd, tx.batch_num, tx.eth_block_num, tx.to_forge_l1_txs_num, tx.user_origin, - tx.from_eth_addr, tx.from_bjj, tx.load_amount, - tx.load_amount_usd, tx.fee, tx.fee_usd, tx.nonce, - token.token_id, token.eth_block_num AS token_block, + tx.load_amount, tx.load_amount_usd, tx.fee, tx.fee_usd, tx.nonce, + token.token_id, token.item_id AS token_item_id, token.eth_block_num AS token_block, token.eth_addr, token.name, token.symbol, token.decimals, token.usd, token.usd_update, block.timestamp FROM tx INNER JOIN token ON tx.token_id = token.token_id @@ -675,18 +676,19 @@ func (hdb *HistoryDB) GetHistoryTxs( ethAddr *ethCommon.Address, bjj *babyjub.PublicKey, tokenID *common.TokenID, idx *common.Idx, batchNum *uint, txType *common.TxType, fromItem, limit *uint, order string, -) ([]HistoryTx, *db.Pagination, error) { +) ([]TxAPI, *db.Pagination, error) { if ethAddr != nil && bjj != nil { return nil, nil, errors.New("ethAddr and bjj are incompatible") } var query string var args []interface{} queryStr := `SELECT tx.item_id, tx.is_l1, tx.id, tx.type, tx.position, - tx.from_idx, tx.to_idx, tx.amount, tx.token_id, tx.amount_usd, + hez_idx(tx.from_idx, token.symbol) AS from_idx, tx.from_eth_addr, tx.from_bjj, + hez_idx(tx.to_idx, token.symbol) AS to_idx, tx.to_eth_addr, tx.to_bjj, + tx.amount, tx.token_id, tx.amount_usd, tx.batch_num, tx.eth_block_num, tx.to_forge_l1_txs_num, tx.user_origin, - tx.from_eth_addr, tx.from_bjj, tx.load_amount, - tx.load_amount_usd, tx.fee, tx.fee_usd, tx.nonce, - token.token_id, token.eth_block_num AS token_block, + tx.load_amount, tx.load_amount_usd, tx.fee, tx.fee_usd, tx.nonce, + token.token_id, token.item_id AS token_item_id, token.eth_block_num AS token_block, token.eth_addr, token.name, token.symbol, token.decimals, token.usd, token.usd_update, block.timestamp, count(*) OVER() AS total_items, MIN(tx.item_id) OVER() AS first_item, MAX(tx.item_id) OVER() AS last_item @@ -783,11 +785,11 @@ func (hdb *HistoryDB) GetHistoryTxs( queryStr += fmt.Sprintf("LIMIT %d;", *limit) query = hdb.db.Rebind(queryStr) log.Debug(query) - txsPtrs := []*HistoryTx{} + txsPtrs := []*TxAPI{} if err := meddler.QueryAll(hdb.db, &txsPtrs, query, args...); err != nil { return nil, nil, err } - txs := db.SlicePtrsToSlice(txsPtrs).([]HistoryTx) + txs := db.SlicePtrsToSlice(txsPtrs).([]TxAPI) if len(txs) == 0 { return nil, nil, sql.ErrNoRows } diff --git a/db/historydb/views.go b/db/historydb/views.go index e42988e..5cb57cd 100644 --- a/db/historydb/views.go +++ b/db/historydb/views.go @@ -1,6 +1,7 @@ package historydb import ( + "encoding/json" "math/big" "time" @@ -11,29 +12,30 @@ import ( "github.com/iden3/go-merkletree" ) -// HistoryTx is a representation of a generic Tx with additional information +// TxAPI is a representation of a generic Tx with additional information // required by the API, and extracted by joining block and token tables -type HistoryTx struct { +type TxAPI struct { // Generic - IsL1 bool `meddler:"is_l1"` - TxID common.TxID `meddler:"id"` - ItemID int `meddler:"item_id"` - Type common.TxType `meddler:"type"` - Position int `meddler:"position"` - FromIdx *common.Idx `meddler:"from_idx"` - ToIdx common.Idx `meddler:"to_idx"` - Amount *big.Int `meddler:"amount,bigint"` - HistoricUSD *float64 `meddler:"amount_usd"` - BatchNum *common.BatchNum `meddler:"batch_num"` // batchNum in which this tx was forged. If the tx is L2, this must be != 0 - EthBlockNum int64 `meddler:"eth_block_num"` // Ethereum Block Number in which this L1Tx was added to the queue + IsL1 bool `meddler:"is_l1"` + TxID common.TxID `meddler:"id"` + ItemID int `meddler:"item_id"` + Type common.TxType `meddler:"type"` + Position int `meddler:"position"` + FromIdx *apitypes.HezIdx `meddler:"from_idx"` + FromEthAddr *apitypes.HezEthAddr `meddler:"from_eth_addr"` + FromBJJ *apitypes.HezBJJ `meddler:"from_bjj"` + ToIdx apitypes.HezIdx `meddler:"to_idx"` + ToEthAddr *apitypes.HezEthAddr `meddler:"to_eth_addr"` + ToBJJ *apitypes.HezBJJ `meddler:"to_bjj"` + Amount apitypes.BigIntStr `meddler:"amount"` + HistoricUSD *float64 `meddler:"amount_usd"` + BatchNum *common.BatchNum `meddler:"batch_num"` // batchNum in which this tx was forged. If the tx is L2, this must be != 0 + EthBlockNum int64 `meddler:"eth_block_num"` // Ethereum Block Number in which this L1Tx was added to the queue // L1 - ToForgeL1TxsNum *int64 `meddler:"to_forge_l1_txs_num"` // toForgeL1TxsNum in which the tx was forged / will be forged - UserOrigin *bool `meddler:"user_origin"` // true if the tx was originated by a user, false if it was aoriginated by a coordinator. Note that this differ from the spec for implementation simplification purpposes - FromEthAddr *ethCommon.Address `meddler:"from_eth_addr"` - FromBJJ *babyjub.PublicKey `meddler:"from_bjj"` - LoadAmount *big.Int `meddler:"load_amount,bigintnull"` - // LoadAmountFloat *float64 `meddler:"load_amount_f"` - HistoricLoadAmountUSD *float64 `meddler:"load_amount_usd"` + ToForgeL1TxsNum *int64 `meddler:"to_forge_l1_txs_num"` // toForgeL1TxsNum in which the tx was forged / will be forged + UserOrigin *bool `meddler:"user_origin"` // true if the tx was originated by a user, false if it was aoriginated by a coordinator. Note that this differ from the spec for implementation simplification purpposes + LoadAmount *apitypes.BigIntStr `meddler:"load_amount"` + HistoricLoadAmountUSD *float64 `meddler:"load_amount_usd"` // L2 Fee *common.FeeSelector `meddler:"fee"` HistoricFeeUSD *float64 `meddler:"fee_usd"` @@ -44,6 +46,7 @@ type HistoryTx struct { FirstItem int `meddler:"first_item"` LastItem int `meddler:"last_item"` TokenID common.TokenID `meddler:"token_id"` + TokenItemID int `meddler:"token_item_id"` TokenEthBlockNum int64 `meddler:"token_block"` TokenEthAddr ethCommon.Address `meddler:"eth_addr"` TokenName string `meddler:"name"` @@ -53,6 +56,58 @@ type HistoryTx struct { TokenUSDUpdate *time.Time `meddler:"usd_update"` } +// MarshalJSON is used to neast some of the fields of TxAPI +// without the need of auxiliar structs +func (tx TxAPI) MarshalJSON() ([]byte, error) { + jsonTx := map[string]interface{}{ + "id": tx.TxID, + "itemId": tx.ItemID, + "type": tx.Type, + "position": tx.Position, + "fromAccountIndex": tx.FromIdx, + "fromHezEthereumAddress": tx.FromEthAddr, + "fromBJJ": tx.FromBJJ, + "toAccountIndex": tx.ToIdx, + "toHezEthereumAddress": tx.ToEthAddr, + "toBJJ": tx.ToBJJ, + "amount": tx.Amount, + "batchNum": tx.BatchNum, + "historicUSD": tx.HistoricUSD, + "timestamp": tx.Timestamp, + "L1Info": nil, + "L2Info": nil, + "token": map[string]interface{}{ + "id": tx.TokenID, + "itemId": tx.TokenItemID, + "ethereumBlockNum": tx.TokenEthBlockNum, + "ethereumAddress": tx.TokenEthAddr, + "name": tx.TokenName, + "symbol": tx.TokenSymbol, + "decimals": tx.TokenDecimals, + "USD": tx.TokenUSD, + "fiatUpdate": tx.TokenUSDUpdate, + }, + } + if tx.IsL1 { + jsonTx["L1orL2"] = "L1" + jsonTx["L1Info"] = map[string]interface{}{ + "toForgeL1TransactionsNum": tx.ToForgeL1TxsNum, + "userOrigin": tx.UserOrigin, + "loadAmount": tx.LoadAmount, + "historicLoadAmountUSD": tx.HistoricLoadAmountUSD, + "ethereumBlockNum": tx.EthBlockNum, + } + } else { + jsonTx["L1orL2"] = "L2" + jsonTx["L2Info"] = map[string]interface{}{ + "fee": tx.Fee, + "historicFeeUSD": tx.HistoricFeeUSD, + "nonce": tx.Nonce, + } + } + return json.Marshal(jsonTx) +} + // txWrite is an representatiion that merges common.L1Tx and common.L2Tx // in order to perform inserts into tx table type txWrite struct { diff --git a/db/l2db/l2db.go b/db/l2db/l2db.go index 252e06d..9891040 100644 --- a/db/l2db/l2db.go +++ b/db/l2db/l2db.go @@ -114,30 +114,41 @@ func (l2db *L2DB) AddTxTest(tx *common.PoolL2Tx) error { return meddler.Insert(l2db.db, "tx_pool", insertTx) } -// selectPoolTxRead select part of queries to get PoolL2TxRead -const selectPoolTxRead = `SELECT tx_pool.tx_id, tx_pool.from_idx, tx_pool.to_idx, tx_pool.to_eth_addr, +// selectPoolTxAPI select part of queries to get PoolL2TxRead +const selectPoolTxAPI = `SELECT tx_pool.tx_id, hez_idx(tx_pool.from_idx, token.symbol) AS from_idx, tx_pool.from_eth_addr, +tx_pool.from_bjj, hez_idx(tx_pool.to_idx, token.symbol) AS to_idx, tx_pool.to_eth_addr, tx_pool.to_bjj, tx_pool.token_id, tx_pool.amount, tx_pool.fee, tx_pool.nonce, -tx_pool.state, tx_pool.signature, tx_pool.timestamp, tx_pool.batch_num, tx_pool.rq_from_idx, -tx_pool.rq_to_idx, tx_pool.rq_to_eth_addr, tx_pool.rq_to_bjj, tx_pool.rq_token_id, tx_pool.rq_amount, +tx_pool.state, tx_pool.signature, tx_pool.timestamp, tx_pool.batch_num, hez_idx(tx_pool.rq_from_idx, token.symbol) AS rq_from_idx, +hez_idx(tx_pool.rq_to_idx, token.symbol) AS rq_to_idx, tx_pool.rq_to_eth_addr, tx_pool.rq_to_bjj, tx_pool.rq_token_id, tx_pool.rq_amount, tx_pool.rq_fee, tx_pool.rq_nonce, tx_pool.tx_type, -token.eth_block_num, token.eth_addr, token.name, token.symbol, token.decimals, token.usd, token.usd_update +token.item_id AS token_item_id, token.eth_block_num, token.eth_addr, token.name, token.symbol, token.decimals, token.usd, token.usd_update FROM tx_pool INNER JOIN token ON tx_pool.token_id = token.token_id ` // selectPoolTxCommon select part of queries to get common.PoolL2Tx -const selectPoolTxCommon = `SELECT tx_pool.tx_id, tx_pool.from_idx, tx_pool.to_idx, tx_pool.to_eth_addr, +const selectPoolTxCommon = `SELECT tx_pool.tx_id, from_idx, to_idx, tx_pool.to_eth_addr, tx_pool.to_bjj, tx_pool.token_id, tx_pool.amount, tx_pool.fee, tx_pool.nonce, -tx_pool.state, tx_pool.signature, tx_pool.timestamp, tx_pool.rq_from_idx, -tx_pool.rq_to_idx, tx_pool.rq_to_eth_addr, tx_pool.rq_to_bjj, tx_pool.rq_token_id, tx_pool.rq_amount, +tx_pool.state, tx_pool.signature, tx_pool.timestamp, rq_from_idx, +rq_to_idx, tx_pool.rq_to_eth_addr, tx_pool.rq_to_bjj, tx_pool.rq_token_id, tx_pool.rq_amount, tx_pool.rq_fee, tx_pool.rq_nonce, tx_pool.tx_type, fee_percentage(tx_pool.fee::NUMERIC) * token.usd * tx_pool.amount_f AS fee_usd, token.usd_update FROM tx_pool INNER JOIN token ON tx_pool.token_id = token.token_id ` -// GetTx return the specified Tx -func (l2db *L2DB) GetTx(txID common.TxID) (*PoolL2TxRead, error) { - tx := new(PoolL2TxRead) +// GetTx return the specified Tx in common.PoolL2Tx format +func (l2db *L2DB) GetTx(txID common.TxID) (*common.PoolL2Tx, error) { + tx := new(common.PoolL2Tx) return tx, meddler.QueryRow( l2db.db, tx, - selectPoolTxRead+"WHERE tx_id = $1;", + selectPoolTxCommon+"WHERE tx_id = $1;", + txID, + ) +} + +// GetTxAPI return the specified Tx in PoolTxAPI format +func (l2db *L2DB) GetTxAPI(txID common.TxID) (*PoolTxAPI, error) { + tx := new(PoolTxAPI) + return tx, meddler.QueryRow( + l2db.db, tx, + selectPoolTxAPI+"WHERE tx_id = $1;", txID, ) } diff --git a/db/l2db/l2db_test.go b/db/l2db/l2db_test.go index c4124b0..8bc2a3f 100644 --- a/db/l2db/l2db_test.go +++ b/db/l2db/l2db_test.go @@ -6,7 +6,6 @@ import ( "testing" "time" - ethCommon "github.com/ethereum/go-ethereum/common" "github.com/hermeznetwork/hermez-node/common" dbUtils "github.com/hermeznetwork/hermez-node/db" "github.com/hermeznetwork/hermez-node/db/historydb" @@ -94,81 +93,11 @@ func TestAddTxTest(t *testing.T) { assert.NoError(t, err) fetchedTx, err := l2DB.GetTx(tx.TxID) assert.NoError(t, err) - assertReadTx(t, commonToRead(tx, tokens), fetchedTx) + // assertReadTx(t, commonToRead(tx, tokens), fetchedTx) + assertTx(t, tx, fetchedTx) } } -func commonToRead(commonTx *common.PoolL2Tx, tokens []common.Token) *PoolL2TxRead { - readTx := &PoolL2TxRead{ - TxID: commonTx.TxID, - FromIdx: commonTx.FromIdx, - ToBJJ: commonTx.ToBJJ, - Amount: commonTx.Amount, - Fee: commonTx.Fee, - Nonce: commonTx.Nonce, - State: commonTx.State, - Signature: commonTx.Signature, - RqToBJJ: commonTx.RqToBJJ, - RqAmount: commonTx.RqAmount, - Type: commonTx.Type, - Timestamp: commonTx.Timestamp, - TokenID: commonTx.TokenID, - } - // token related fields - // find token - token := historydb.TokenWithUSD{} - for _, tkn := range tokensUSD { - if tkn.TokenID == readTx.TokenID { - token = tkn - break - } - } - // set token related fields - readTx.TokenEthBlockNum = token.EthBlockNum - readTx.TokenEthAddr = token.EthAddr - readTx.TokenName = token.Name - readTx.TokenSymbol = token.Symbol - readTx.TokenDecimals = token.Decimals - readTx.TokenUSD = token.USD - readTx.TokenUSDUpdate = token.USDUpdate - // nullable fields - if commonTx.ToIdx != 0 { - readTx.ToIdx = &commonTx.ToIdx - } - nilAddr := ethCommon.BigToAddress(big.NewInt(0)) - if commonTx.ToEthAddr != nilAddr { - readTx.ToEthAddr = &commonTx.ToEthAddr - } - if commonTx.RqFromIdx != 0 { - readTx.RqFromIdx = &commonTx.RqFromIdx - } - if commonTx.RqToIdx != 0 { // if true, all Rq... fields must be different to nil - readTx.RqToIdx = &commonTx.RqToIdx - readTx.RqTokenID = &commonTx.RqTokenID - readTx.RqFee = &commonTx.RqFee - readTx.RqNonce = &commonTx.RqNonce - } - if commonTx.RqToEthAddr != nilAddr { - readTx.RqToEthAddr = &commonTx.RqToEthAddr - } - return readTx -} - -func assertReadTx(t *testing.T, expected, actual *PoolL2TxRead) { - // Check that timestamp has been set within the last 3 seconds - assert.Less(t, time.Now().UTC().Unix()-3, actual.Timestamp.Unix()) - assert.GreaterOrEqual(t, time.Now().UTC().Unix(), actual.Timestamp.Unix()) - expected.Timestamp = actual.Timestamp - // Check token related stuff - if expected.TokenUSDUpdate != nil { - // Check that TokenUSDUpdate has been set within the last 3 seconds - assert.Less(t, time.Now().UTC().Unix()-3, actual.TokenUSDUpdate.Unix()) - assert.GreaterOrEqual(t, time.Now().UTC().Unix(), actual.TokenUSDUpdate.Unix()) - expected.TokenUSDUpdate = actual.TokenUSDUpdate - } - assert.Equal(t, expected, actual) -} - func assertTx(t *testing.T, expected, actual *common.PoolL2Tx) { // Check that timestamp has been set within the last 3 seconds assert.Less(t, time.Now().UTC().Unix()-3, actual.Timestamp.Unix()) @@ -400,13 +329,13 @@ func TestReorg(t *testing.T) { err := l2DB.Reorg(lastValidBatch) assert.NoError(t, err) for _, id := range reorgedTxIDs { - tx, err := l2DB.GetTx(id) + tx, err := l2DB.GetTxAPI(id) assert.NoError(t, err) assert.Nil(t, tx.BatchNum) assert.Equal(t, common.PoolL2TxStatePending, tx.State) } for _, id := range nonReorgedTxIDs { - fetchedTx, err := l2DB.GetTx(id) + fetchedTx, err := l2DB.GetTxAPI(id) assert.NoError(t, err) assert.Equal(t, lastValidBatch, *fetchedTx.BatchNum) } diff --git a/db/l2db/views.go b/db/l2db/views.go index ce327e7..c398030 100644 --- a/db/l2db/views.go +++ b/db/l2db/views.go @@ -1,10 +1,12 @@ package l2db import ( + "encoding/json" "math/big" "time" ethCommon "github.com/ethereum/go-ethereum/common" + "github.com/hermeznetwork/hermez-node/apitypes" "github.com/hermeznetwork/hermez-node/common" "github.com/iden3/go-iden3-crypto/babyjub" ) @@ -34,24 +36,26 @@ type PoolL2TxWrite struct { Type common.TxType `meddler:"tx_type"` } -// PoolL2TxRead represents a L2 Tx pool with extra metadata used by the API -type PoolL2TxRead struct { +// PoolTxAPI represents a L2 Tx pool with extra metadata used by the API +type PoolTxAPI struct { TxID common.TxID `meddler:"tx_id"` - FromIdx common.Idx `meddler:"from_idx"` - ToIdx *common.Idx `meddler:"to_idx"` - ToEthAddr *ethCommon.Address `meddler:"to_eth_addr"` - ToBJJ *babyjub.PublicKey `meddler:"to_bjj"` - Amount *big.Int `meddler:"amount,bigint"` + FromIdx apitypes.HezIdx `meddler:"from_idx"` + FromEthAddr *apitypes.HezEthAddr `meddler:"from_eth_addr"` + FromBJJ *apitypes.HezBJJ `meddler:"from_bjj"` + ToIdx *apitypes.HezIdx `meddler:"to_idx"` + ToEthAddr *apitypes.HezEthAddr `meddler:"to_eth_addr"` + ToBJJ *apitypes.HezBJJ `meddler:"to_bjj"` + Amount apitypes.BigIntStr `meddler:"amount"` Fee common.FeeSelector `meddler:"fee"` Nonce common.Nonce `meddler:"nonce"` State common.PoolL2TxState `meddler:"state"` Signature babyjub.SignatureComp `meddler:"signature"` - RqFromIdx *common.Idx `meddler:"rq_from_idx"` - RqToIdx *common.Idx `meddler:"rq_to_idx"` - RqToEthAddr *ethCommon.Address `meddler:"rq_to_eth_addr"` - RqToBJJ *babyjub.PublicKey `meddler:"rq_to_bjj"` + RqFromIdx *apitypes.HezIdx `meddler:"rq_from_idx"` + RqToIdx *apitypes.HezIdx `meddler:"rq_to_idx"` + RqToEthAddr *apitypes.HezEthAddr `meddler:"rq_to_eth_addr"` + RqToBJJ *apitypes.HezBJJ `meddler:"rq_to_bjj"` RqTokenID *common.TokenID `meddler:"rq_token_id"` - RqAmount *big.Int `meddler:"rq_amount,bigintnull"` + RqAmount *apitypes.BigIntStr `meddler:"rq_amount"` RqFee *common.FeeSelector `meddler:"rq_fee"` RqNonce *common.Nonce `meddler:"rq_nonce"` Type common.TxType `meddler:"tx_type"` @@ -60,6 +64,7 @@ type PoolL2TxRead struct { Timestamp time.Time `meddler:"timestamp,utctime"` TotalItems int `meddler:"total_items"` TokenID common.TokenID `meddler:"token_id"` + TokenItemID int `meddler:"token_item_id"` TokenEthBlockNum int64 `meddler:"eth_block_num"` TokenEthAddr ethCommon.Address `meddler:"eth_addr"` TokenName string `meddler:"name"` @@ -68,3 +73,44 @@ type PoolL2TxRead struct { TokenUSD *float64 `meddler:"usd"` TokenUSDUpdate *time.Time `meddler:"usd_update"` } + +// MarshalJSON is used to neast some of the fields of PoolTxAPI +// without the need of auxiliar structs +func (tx PoolTxAPI) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "id": tx.TxID, + "type": tx.Type, + "fromAccountIndex": tx.FromIdx, + "fromHezEthereumAddress": tx.FromEthAddr, + "fromBJJ": tx.FromBJJ, + "toAccountIndex": tx.ToIdx, + "toHezEthereumAddress": tx.ToEthAddr, + "toBjj": tx.ToBJJ, + "amount": tx.Amount, + "fee": tx.Fee, + "nonce": tx.Nonce, + "state": tx.State, + "signature": tx.Signature, + "timestamp": tx.Timestamp, + "batchNum": tx.BatchNum, + "requestFromAccountIndex": tx.RqFromIdx, + "requestToAccountIndex": tx.RqToIdx, + "requestToHezEthereumAddress": tx.RqToEthAddr, + "requestToBJJ": tx.RqToBJJ, + "requestTokenId": tx.RqTokenID, + "requestAmount": tx.RqAmount, + "requestFee": tx.RqFee, + "requestNonce": tx.RqNonce, + "token": map[string]interface{}{ + "id": tx.TokenID, + "itemId": tx.TokenItemID, + "ethereumBlockNum": tx.TokenEthBlockNum, + "ethereumAddress": tx.TokenEthAddr, + "name": tx.TokenName, + "symbol": tx.TokenSymbol, + "decimals": tx.TokenDecimals, + "USD": tx.TokenUSD, + "fiatUpdate": tx.TokenUSDUpdate, + }, + }) +} diff --git a/db/migrations/0001.sql b/db/migrations/0001.sql index fa6646a..8f8847a 100644 --- a/db/migrations/0001.sql +++ b/db/migrations/0001.sql @@ -50,6 +50,22 @@ CREATE TABLE token ( usd_update TIMESTAMP WITHOUT TIME ZONE ); + +-- +migrate StatementBegin +CREATE FUNCTION hez_idx(BIGINT, VARCHAR) + RETURNS VARCHAR +AS +$BODY$ +BEGIN + IF $1 = 1 THEN + RETURN 'hez:EXIT:1'; + END IF; + RETURN 'hez:' || $2 || ':' || $1; +END; +$BODY$ +LANGUAGE plpgsql; +-- +migrate StatementEnd + CREATE TABLE account ( idx BIGINT PRIMARY KEY, token_id INT NOT NULL REFERENCES token (token_id) ON DELETE CASCADE, @@ -96,7 +112,11 @@ CREATE TABLE tx ( type VARCHAR(40) NOT NULL, position INT NOT NULL, from_idx BIGINT, + from_eth_addr BYTEA, + from_bjj BYTEA, to_idx BIGINT NOT NULL, + to_eth_addr BYTEA, + to_bjj BYTEA, amount BYTEA NOT NULL, amount_f NUMERIC NOT NULL, token_id INT NOT NULL REFERENCES token (token_id), @@ -106,8 +126,6 @@ CREATE TABLE tx ( -- L1 to_forge_l1_txs_num BIGINT, user_origin BOOLEAN, - from_eth_addr BYTEA, - from_bjj BYTEA, load_amount BYTEA, load_amount_f NUMERIC, load_amount_usd NUMERIC, @@ -398,28 +416,31 @@ DECLARE _usd_update TIMESTAMP; _tx_timestamp TIMESTAMP; BEGIN - -- Validate L1/L2 constrains - IF NEW.is_l1 AND (( -- L1 mandatory fields - NEW.user_origin IS NULL OR + IF NEW.is_l1 THEN + -- Validate + IF NEW.user_origin IS NULL OR NEW.from_eth_addr IS NULL OR NEW.from_bjj IS NULL OR NEW.load_amount IS NULL OR - NEW.load_amount_f IS NULL - ) OR (NOT NEW.user_origin AND NEW.batch_num IS NULL)) THEN -- If is Coordinator L1, must include batch_num - RAISE EXCEPTION 'Invalid L1 tx.'; - ELSIF NOT NEW.is_l1 THEN - IF NEW.fee IS NULL THEN - NEW.fee = (SELECT 0); + NEW.load_amount_f IS NULL OR + (NOT NEW.user_origin AND NEW.batch_num IS NULL) THEN -- If is Coordinator L1, must include batch_num + RAISE EXCEPTION 'Invalid L1 tx.'; END IF; + ELSE + -- Validate IF NEW.batch_num IS NULL OR NEW.nonce IS NULL THEN RAISE EXCEPTION 'Invalid L2 tx.'; END IF; - END IF; - -- If is L2, add token_id - IF NOT NEW.is_l1 THEN + -- Set fee if it's null + IF NEW.fee IS NULL THEN + NEW.fee = (SELECT 0); + END IF; + -- Set token_id NEW."token_id" = (SELECT token_id FROM account WHERE idx = NEW."from_idx"); + -- Set from_{eth_addr,bjj} + SELECT INTO NEW."from_eth_addr", NEW."from_bjj" eth_addr, bjj FROM account WHERE idx = NEW.from_idx; END IF; - -- Set value_usd + -- Set USD related SELECT INTO _value, _usd_update, _tx_timestamp usd / POWER(10, decimals), usd_update, timestamp FROM token INNER JOIN block on token.eth_block_num = block.eth_block_num WHERE token_id = NEW.token_id; IF _tx_timestamp - interval '24 hours' < _usd_update AND _tx_timestamp + interval '24 hours' > _usd_update THEN @@ -430,6 +451,10 @@ BEGIN NEW."load_amount_usd" = (SELECT _value * NEW.load_amount_f); END IF; END IF; + -- Set to_{eth_addr,bjj} + IF NEW.to_idx > 255 THEN + SELECT INTO NEW."to_eth_addr", NEW."to_bjj" eth_addr, bjj FROM account WHERE idx = NEW.to_idx; + END IF; RETURN NEW; END; $BODY$ @@ -487,6 +512,8 @@ CREATE TABLE consensus_vars ( CREATE TABLE tx_pool ( tx_id BYTEA PRIMARY KEY, from_idx BIGINT NOT NULL, + from_eth_addr BYTEA, + from_bjj BYTEA, to_idx BIGINT, to_eth_addr BYTEA, to_bjj BYTEA, @@ -510,6 +537,25 @@ CREATE TABLE tx_pool ( tx_type VARCHAR(40) NOT NULL ); +-- +migrate StatementBegin +CREATE FUNCTION set_pool_tx() + RETURNS TRIGGER +AS +$BODY$ +BEGIN + SELECT INTO NEW."from_eth_addr", NEW."from_bjj" eth_addr, bjj FROM account WHERE idx = NEW."from_idx"; + -- Set to_{eth_addr,bjj} + IF NEW.to_idx > 255 THEN + SELECT INTO NEW."to_eth_addr", NEW."to_bjj" eth_addr, bjj FROM account WHERE idx = NEW."to_idx"; + END IF; + RETURN NEW; +END; +$BODY$ +LANGUAGE plpgsql; +-- +migrate StatementEnd +CREATE TRIGGER trigger_set_pool_tx BEFORE INSERT ON tx_pool +FOR EACH ROW EXECUTE PROCEDURE set_pool_tx(); + CREATE TABLE account_creation_auth ( eth_addr BYTEA PRIMARY KEY, bjj BYTEA NOT NULL, diff --git a/go.sum b/go.sum index e0062fb..ac9d477 100644 --- a/go.sum +++ b/go.sum @@ -160,6 +160,7 @@ github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqG github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/getkin/kin-openapi v0.22.0 h1:J5IFyKd/5yuB6AZAgwK0CMBKnabWcmkowtsl6bRkz4s= github.com/getkin/kin-openapi v0.22.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw= +github.com/getkin/kin-openapi v0.23.0 h1:RKtVNKk8kxcTIWEswgZ3Olvn1RxWOJ0zz8cP3d9aHIA= github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9/go.mod h1:106OIgooyS7OzLDOpUGgm9fA3bQENb/cFSyyBmMoJDs=