diff --git a/api/accountcreationauths.go b/api/accountcreationauths.go new file mode 100644 index 0000000..0eb6706 --- /dev/null +++ b/api/accountcreationauths.go @@ -0,0 +1,68 @@ +package api + +import ( + "errors" + "net/http" + "time" + + 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/iden3/go-iden3-crypto/babyjub" +) + +func postAccountCreationAuth(c *gin.Context) { + // Parse body + var apiAuth receivedAuth + if err := c.ShouldBindJSON(&apiAuth); err != nil { + retBadReq(err, c) + return + } + // API to common + verify signature + commonAuth := accountCreationAuthAPIToCommon(&apiAuth) + if !commonAuth.VerifySignature() { + retBadReq(errors.New("invalid signature"), c) + return + } + // Insert to DB + if err := l2.AddAccountCreationAuth(commonAuth); err != nil { + retSQLErr(err, c) + return + } + // Return OK + c.Status(http.StatusOK) +} + +func getAccountCreationAuth(c *gin.Context) { + // Get hezEthereumAddress + addr, err := parseParamHezEthAddr(c) + if err != nil { + retBadReq(err, c) + return + } + // Fetch auth from l2DB + auth, err := l2.GetAccountCreationAuthAPI(*addr) + if err != nil { + retSQLErr(err, c) + return + } + // Build succesfull response + c.JSON(http.StatusOK, auth) +} + +type receivedAuth struct { + EthAddr apitypes.StrHezEthAddr `json:"hezEthereumAddress" binding:"required"` + BJJ apitypes.StrHezBJJ `json:"bjj" binding:"required"` + Signature apitypes.EthSignature `json:"signature" binding:"required"` + Timestamp time.Time `json:"timestamp"` +} + +func accountCreationAuthAPIToCommon(apiAuth *receivedAuth) *common.AccountCreationAuth { + return &common.AccountCreationAuth{ + EthAddr: ethCommon.Address(apiAuth.EthAddr), + BJJ: (*babyjub.PublicKey)(&apiAuth.BJJ), + Signature: []byte(apiAuth.Signature), + Timestamp: apiAuth.Timestamp, + } +} diff --git a/api/accountcreationauths_test.go b/api/accountcreationauths_test.go new file mode 100644 index 0000000..145ee44 --- /dev/null +++ b/api/accountcreationauths_test.go @@ -0,0 +1,99 @@ +package api + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "math/big" + "testing" + "time" + + ethCommon "github.com/ethereum/go-ethereum/common" + "github.com/hermeznetwork/hermez-node/common" + "github.com/stretchr/testify/assert" +) + +type testAuth struct { + EthAddr string `json:"hezEthereumAddress" binding:"required"` + BJJ string `json:"bjj" binding:"required"` + Signature string `json:"signature" binding:"required"` + Timestamp time.Time `json:"timestamp"` +} + +func genTestAuths(auths []*common.AccountCreationAuth) []testAuth { + testAuths := []testAuth{} + for _, auth := range auths { + testAuths = append(testAuths, testAuth{ + EthAddr: ethAddrToHez(auth.EthAddr), + BJJ: bjjToString(auth.BJJ), + Signature: "0x" + hex.EncodeToString(auth.Signature), + Timestamp: auth.Timestamp, + }) + } + return testAuths +} + +func TestAccountCreationAuth(t *testing.T) { + // POST + endpoint := apiURL + "account-creation-authorization" + for _, auth := range tc.auths { + jsonAuthBytes, err := json.Marshal(auth) + assert.NoError(t, err) + jsonAuthReader := bytes.NewReader(jsonAuthBytes) + fmt.Println(string(jsonAuthBytes)) + assert.NoError( + t, doGoodReq( + "POST", + endpoint, + jsonAuthReader, nil, + ), + ) + } + // GET + endpoint += "/" + for _, auth := range tc.auths { + fetchedAuth := testAuth{} + assert.NoError( + t, doGoodReq( + "GET", + endpoint+auth.EthAddr, + nil, &fetchedAuth, + ), + ) + assertAuth(t, auth, fetchedAuth) + } + // POST + // 400 + // Wrong addr + badAuth := tc.auths[0] + badAuth.EthAddr = ethAddrToHez(ethCommon.BigToAddress(big.NewInt(1))) + jsonAuthBytes, err := json.Marshal(badAuth) + assert.NoError(t, err) + jsonAuthReader := bytes.NewReader(jsonAuthBytes) + err = doBadReq("POST", endpoint, jsonAuthReader, 400) + assert.NoError(t, err) + // Wrong signature + badAuth = tc.auths[0] + badAuth.Signature = badAuth.Signature[:len(badAuth.Signature)-1] + badAuth.Signature += "F" + jsonAuthBytes, err = json.Marshal(badAuth) + assert.NoError(t, err) + jsonAuthReader = bytes.NewReader(jsonAuthBytes) + err = doBadReq("POST", endpoint, jsonAuthReader, 400) + assert.NoError(t, err) + // GET + // 400 + err = doBadReq("GET", endpoint+"hez:0xFooBar", nil, 400) + assert.NoError(t, err) + // 404 + err = doBadReq("GET", endpoint+"hez:0x0000000000000000000000000000000000000001", nil, 404) + assert.NoError(t, err) +} + +func assertAuth(t *testing.T, expected, actual testAuth) { + // timestamp should be very close to now + assert.Less(t, time.Now().UTC().Unix()-3, actual.Timestamp.Unix()) + expected.Timestamp = actual.Timestamp + assert.Equal(t, expected, actual) +} diff --git a/api/api_test.go b/api/api_test.go index 49640fe..d824809 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -1,7 +1,6 @@ package api import ( - "bytes" "context" "encoding/json" "errors" @@ -28,9 +27,7 @@ import ( "github.com/hermeznetwork/hermez-node/log" "github.com/hermeznetwork/hermez-node/test" "github.com/iden3/go-iden3-crypto/babyjub" - "github.com/mitchellh/copystructure" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) const apiPort = ":4010" @@ -47,11 +44,11 @@ type testCommon struct { accs []common.Account usrTxs []testTx allTxs []testTx - exits []exitAPI - usrExits []exitAPI + exits []testExit + usrExits []testExit poolTxsToSend []testPoolTxSend poolTxsToReceive []testPoolTxReceive - auths []accountCreationAuthAPI + auths []testAuth router *swagger.Router bids []testBid } @@ -287,42 +284,6 @@ func TestMain(m *testing.M) { } } - // 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, tokensUSD, accs) - historyExits = append(historyExits, historydb.HistoryExit{ - BatchNum: exit.BatchNum, - AccountIdx: exit.AccountIdx, - MerkleProof: exit.MerkleProof, - Balance: exit.Balance, - InstantWithdrawn: exit.InstantWithdrawn, - DelayedWithdrawRequest: exit.DelayedWithdrawRequest, - DelayedWithdrawn: exit.DelayedWithdrawn, - TokenID: token.TokenID, - TokenEthBlockNum: token.EthBlockNum, - TokenEthAddr: token.EthAddr, - TokenName: token.Name, - TokenSymbol: token.Symbol, - TokenDecimals: token.Decimals, - TokenUSD: token.USD, - TokenUSDUpdate: token.USDUpdate, - }) - } - return historyExitsToAPI(historyExits) - } - apiExits := exitsToAPIExits(exits, accs, tokens) - // sort.Sort(apiExits) - usrExits := []exitAPI{} - for _, exit := range apiExits { - for _, idx := range usrIdxs { - if idx == exit.AccountIdx { - usrExits = append(usrExits, exit) - } - } - } - // Coordinators const nCoords = 10 coords := test.GenCoordinators(nCoords, blocks) @@ -336,15 +297,6 @@ func TestMain(m *testing.M) { if err != nil { panic(err) } - // Account creation auth - const nAuths = 5 - auths := test.GenAuths(nAuths) - // Transform auths to API format - apiAuths := []accountCreationAuthAPI{} - for _, auth := range auths { - apiAuth := accountCreationAuthToAPI(auth) - apiAuths = append(apiAuths, *apiAuth) - } // Bids const nBids = 10 @@ -358,6 +310,7 @@ func TestMain(m *testing.M) { 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. testBatches, fullBatches := genTestBatches(blocks, batches, allTxs) + usrExits, allExits := genTestExits(exits, tokensUSD, accs, usrIdxs) tc = testCommon{ blocks: blocks, tokens: tokensUSD, @@ -369,11 +322,11 @@ func TestMain(m *testing.M) { accs: accs, usrTxs: usrTxs, allTxs: allTxs, - exits: apiExits, + exits: allExits, usrExits: usrExits, poolTxsToSend: poolTxsToSend, poolTxsToReceive: poolTxsToReceive, - auths: apiAuths, + auths: genTestAuths(test.GenAuths(5)), router: router, bids: genTestBids(blocks, coordinators, bids), } @@ -400,179 +353,6 @@ func TestMain(m *testing.M) { os.Exit(result) } -func TestGetExits(t *testing.T) { - endpoint := apiURL + "exits" - fetchedExits := []exitAPI{} - appendIter := func(intr interface{}) { - for i := 0; i < len(intr.(*exitsAPI).Exits); i++ { - tmp, err := copystructure.Copy(intr.(*exitsAPI).Exits[i]) - if err != nil { - panic(err) - } - fetchedExits = append(fetchedExits, tmp.(exitAPI)) - } - } - // Get all (no filters) - limit := 8 - path := fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit) - err := doGoodReqPaginated(path, historydb.OrderAsc, &exitsAPI{}, appendIter) - assert.NoError(t, err) - assertExitAPIs(t, tc.exits, fetchedExits) - - // Get by ethAddr - fetchedExits = []exitAPI{} - limit = 7 - path = fmt.Sprintf( - "%s?hermezEthereumAddress=%s&limit=%d&fromItem=", - endpoint, tc.usrAddr, limit, - ) - err = doGoodReqPaginated(path, historydb.OrderAsc, &exitsAPI{}, appendIter) - assert.NoError(t, err) - assertExitAPIs(t, tc.usrExits, fetchedExits) - // Get by bjj - fetchedExits = []exitAPI{} - limit = 6 - path = fmt.Sprintf( - "%s?BJJ=%s&limit=%d&fromItem=", - endpoint, tc.usrBjj, limit, - ) - err = doGoodReqPaginated(path, historydb.OrderAsc, &exitsAPI{}, appendIter) - assert.NoError(t, err) - assertExitAPIs(t, tc.usrExits, fetchedExits) - // Get by tokenID - fetchedExits = []exitAPI{} - limit = 5 - tokenID := tc.exits[0].Token.TokenID - path = fmt.Sprintf( - "%s?tokenId=%d&limit=%d&fromItem=", - endpoint, tokenID, limit, - ) - err = doGoodReqPaginated(path, historydb.OrderAsc, &exitsAPI{}, appendIter) - assert.NoError(t, err) - tokenIDExits := []exitAPI{} - for i := 0; i < len(tc.exits); i++ { - if tc.exits[i].Token.TokenID == tokenID { - tokenIDExits = append(tokenIDExits, tc.exits[i]) - } - } - assertExitAPIs(t, tokenIDExits, fetchedExits) - // idx - fetchedExits = []exitAPI{} - limit = 4 - idx := tc.exits[0].AccountIdx - path = fmt.Sprintf( - "%s?accountIndex=%s&limit=%d&fromItem=", - endpoint, idx, limit, - ) - err = doGoodReqPaginated(path, historydb.OrderAsc, &exitsAPI{}, appendIter) - assert.NoError(t, err) - idxExits := []exitAPI{} - for i := 0; i < len(tc.exits); i++ { - if tc.exits[i].AccountIdx[6:] == idx[6:] { - idxExits = append(idxExits, tc.exits[i]) - } - } - assertExitAPIs(t, idxExits, fetchedExits) - // batchNum - fetchedExits = []exitAPI{} - limit = 3 - batchNum := tc.exits[0].BatchNum - path = fmt.Sprintf( - "%s?batchNum=%d&limit=%d&fromItem=", - endpoint, batchNum, limit, - ) - err = doGoodReqPaginated(path, historydb.OrderAsc, &exitsAPI{}, appendIter) - assert.NoError(t, err) - batchNumExits := []exitAPI{} - for i := 0; i < len(tc.exits); i++ { - if tc.exits[i].BatchNum == batchNum { - batchNumExits = append(batchNumExits, tc.exits[i]) - } - } - assertExitAPIs(t, batchNumExits, fetchedExits) - // Multiple filters - fetchedExits = []exitAPI{} - limit = 1 - path = fmt.Sprintf( - "%s?batchNum=%d&tokeId=%d&limit=%d&fromItem=", - endpoint, batchNum, tokenID, limit, - ) - err = doGoodReqPaginated(path, historydb.OrderAsc, &exitsAPI{}, appendIter) - assert.NoError(t, err) - mixedExits := []exitAPI{} - flipedExits := []exitAPI{} - for i := 0; i < len(tc.exits); i++ { - if tc.exits[i].BatchNum == batchNum && tc.exits[i].Token.TokenID == tokenID { - mixedExits = append(mixedExits, tc.exits[i]) - } - flipedExits = append(flipedExits, tc.exits[len(tc.exits)-1-i]) - } - assertExitAPIs(t, mixedExits, fetchedExits) - // All, in reverse order - fetchedExits = []exitAPI{} - limit = 5 - path = fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit) - err = doGoodReqPaginated(path, historydb.OrderDesc, &exitsAPI{}, appendIter) - assert.NoError(t, err) - assertExitAPIs(t, flipedExits, fetchedExits) - // 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 TestGetExit(t *testing.T) { - // Get all txs by their ID - endpoint := apiURL + "exits/" - fetchedExits := []exitAPI{} - for _, exit := range tc.exits { - fetchedExit := exitAPI{} - assert.NoError( - t, doGoodReq( - "GET", - fmt.Sprintf("%s%d/%s", endpoint, exit.BatchNum, exit.AccountIdx), - nil, &fetchedExit, - ), - ) - fetchedExits = append(fetchedExits, fetchedExit) - } - assertExitAPIs(t, tc.exits, fetchedExits) - // 400 - err := doBadReq("GET", endpoint+"1/haz:BOOM:1", nil, 400) - assert.NoError(t, err) - err = doBadReq("GET", endpoint+"-1/hez:BOOM:1", nil, 400) - assert.NoError(t, err) - // 404 - err = doBadReq("GET", endpoint+"494/hez:XXX:1", nil, 404) - assert.NoError(t, err) -} - -func assertExitAPIs(t *testing.T, expected, actual []exitAPI) { - 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 - 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 - } - assert.Equal(t, expected[i], actual[i]) - } -} func TestGetConfig(t *testing.T) { endpoint := apiURL + "config" var configTest configAPI @@ -581,70 +361,6 @@ func TestGetConfig(t *testing.T) { assert.Equal(t, cg, &configTest) } -func TestAccountCreationAuth(t *testing.T) { - // POST - endpoint := apiURL + "account-creation-authorization" - for _, auth := range tc.auths { - jsonAuthBytes, err := json.Marshal(auth) - assert.NoError(t, err) - jsonAuthReader := bytes.NewReader(jsonAuthBytes) - fmt.Println(string(jsonAuthBytes)) - assert.NoError( - t, doGoodReq( - "POST", - endpoint, - jsonAuthReader, nil, - ), - ) - } - // GET - endpoint += "/" - for _, auth := range tc.auths { - fetchedAuth := accountCreationAuthAPI{} - assert.NoError( - t, doGoodReq( - "GET", - endpoint+auth.EthAddr, - nil, &fetchedAuth, - ), - ) - assertAuth(t, auth, fetchedAuth) - } - // POST - // 400 - // Wrong addr - badAuth := tc.auths[0] - badAuth.EthAddr = ethAddrToHez(ethCommon.BigToAddress(big.NewInt(1))) - jsonAuthBytes, err := json.Marshal(badAuth) - assert.NoError(t, err) - jsonAuthReader := bytes.NewReader(jsonAuthBytes) - err = doBadReq("POST", endpoint, jsonAuthReader, 400) - assert.NoError(t, err) - // Wrong signature - badAuth = tc.auths[0] - badAuth.Signature = badAuth.Signature[:len(badAuth.Signature)-1] - badAuth.Signature += "F" - jsonAuthBytes, err = json.Marshal(badAuth) - assert.NoError(t, err) - jsonAuthReader = bytes.NewReader(jsonAuthBytes) - err = doBadReq("POST", endpoint, jsonAuthReader, 400) - assert.NoError(t, err) - // GET - // 400 - err = doBadReq("GET", endpoint+"hez:0xFooBar", nil, 400) - assert.NoError(t, err) - // 404 - err = doBadReq("GET", endpoint+"hez:0x0000000000000000000000000000000000000001", nil, 404) - assert.NoError(t, err) -} - -func assertAuth(t *testing.T, expected, actual accountCreationAuthAPI) { - // timestamp should be very close to now - assert.Less(t, time.Now().UTC().Unix()-3, actual.Timestamp.Unix()) - expected.Timestamp = actual.Timestamp - assert.Equal(t, expected, actual) -} - func doGoodReqPaginated( path, order string, iterStruct db.Paginationer, diff --git a/api/dbtoapistructs.go b/api/dbtoapistructs.go index 1eee777..3f1db97 100644 --- a/api/dbtoapistructs.go +++ b/api/dbtoapistructs.go @@ -2,17 +2,11 @@ package api import ( "encoding/base64" - "encoding/hex" - "errors" "math/big" "strconv" - "strings" - "time" ethCommon "github.com/ethereum/go-ethereum/common" "github.com/hermeznetwork/hermez-node/common" - "github.com/hermeznetwork/hermez-node/db" - "github.com/hermeznetwork/hermez-node/db/historydb" "github.com/hermeznetwork/hermez-node/eth" "github.com/iden3/go-iden3-crypto/babyjub" ) @@ -44,89 +38,6 @@ func idxToHez(idx common.Idx, tokenSymbol string) string { return "hez:" + tokenSymbol + ":" + strconv.Itoa(int(idx)) } -// Exit - -type exitsAPI struct { - Exits []exitAPI `json:"exits"` - Pagination *db.Pagination `json:"pagination"` -} - -func (e *exitsAPI) GetPagination() *db.Pagination { - if e.Exits[0].ItemID < e.Exits[len(e.Exits)-1].ItemID { - e.Pagination.FirstReturnedItem = e.Exits[0].ItemID - e.Pagination.LastReturnedItem = e.Exits[len(e.Exits)-1].ItemID - } else { - e.Pagination.LastReturnedItem = e.Exits[0].ItemID - e.Pagination.FirstReturnedItem = e.Exits[len(e.Exits)-1].ItemID - } - return e.Pagination -} -func (e *exitsAPI) Len() int { return len(e.Exits) } - -type merkleProofAPI struct { - Root string - Siblings []string - OldKey string - OldValue string - IsOld0 bool - Key string - Value string - Fnc int -} - -type exitAPI struct { - ItemID int `json:"itemId"` - BatchNum common.BatchNum `json:"batchNum"` - AccountIdx string `json:"accountIndex"` - MerkleProof merkleProofAPI `json:"merkleProof"` - Balance string `json:"balance"` - InstantWithdrawn *int64 `json:"instantWithdrawn"` - DelayedWithdrawRequest *int64 `json:"delayedWithdrawRequest"` - DelayedWithdrawn *int64 `json:"delayedWithdrawn"` - Token historydb.TokenWithUSD `json:"token"` -} - -func historyExitsToAPI(dbExits []historydb.HistoryExit) []exitAPI { - apiExits := []exitAPI{} - for i := 0; i < len(dbExits); i++ { - exit := exitAPI{ - ItemID: dbExits[i].ItemID, - BatchNum: dbExits[i].BatchNum, - AccountIdx: idxToHez(dbExits[i].AccountIdx, dbExits[i].TokenSymbol), - MerkleProof: merkleProofAPI{ - Root: dbExits[i].MerkleProof.Root.String(), - OldKey: dbExits[i].MerkleProof.OldKey.String(), - OldValue: dbExits[i].MerkleProof.OldValue.String(), - IsOld0: dbExits[i].MerkleProof.IsOld0, - Key: dbExits[i].MerkleProof.Key.String(), - Value: dbExits[i].MerkleProof.Value.String(), - Fnc: dbExits[i].MerkleProof.Fnc, - }, - Balance: dbExits[i].Balance.String(), - InstantWithdrawn: dbExits[i].InstantWithdrawn, - DelayedWithdrawRequest: dbExits[i].DelayedWithdrawRequest, - DelayedWithdrawn: dbExits[i].DelayedWithdrawn, - Token: historydb.TokenWithUSD{ - TokenID: dbExits[i].TokenID, - EthBlockNum: dbExits[i].TokenEthBlockNum, - EthAddr: dbExits[i].TokenEthAddr, - Name: dbExits[i].TokenName, - Symbol: dbExits[i].TokenSymbol, - Decimals: dbExits[i].TokenDecimals, - USD: dbExits[i].TokenUSD, - USDUpdate: dbExits[i].TokenUSDUpdate, - }, - } - siblings := []string{} - for j := 0; j < len(dbExits[i].MerkleProof.Siblings); j++ { - siblings = append(siblings, dbExits[i].MerkleProof.Siblings[j].String()) - } - exit.MerkleProof.Siblings = siblings - apiExits = append(apiExits, exit) - } - return apiExits -} - // Config type rollupConstants struct { @@ -152,47 +63,3 @@ type configAPI struct { AuctionConstants eth.AuctionConstants `json:"auction"` WDelayerConstants eth.WDelayerConstants `json:"withdrawalDelayer"` } - -// AccountCreationAuth - -type accountCreationAuthAPI struct { - EthAddr string `json:"hezEthereumAddress" binding:"required"` - BJJ string `json:"bjj" binding:"required"` - Signature string `json:"signature" binding:"required"` - Timestamp time.Time `json:"timestamp"` -} - -func accountCreationAuthToAPI(dbAuth *common.AccountCreationAuth) *accountCreationAuthAPI { - return &accountCreationAuthAPI{ - EthAddr: ethAddrToHez(dbAuth.EthAddr), - BJJ: bjjToString(dbAuth.BJJ), - Signature: "0x" + hex.EncodeToString(dbAuth.Signature), - Timestamp: dbAuth.Timestamp, - } -} - -func accountCreationAuthAPIToCommon(apiAuth *accountCreationAuthAPI) (*common.AccountCreationAuth, error) { - ethAddr, err := hezStringToEthAddr(apiAuth.EthAddr, "hezEthereumAddress") - if err != nil { - return nil, err - } - bjj, err := hezStringToBJJ(apiAuth.BJJ, "bjj") - if err != nil { - return nil, err - } - without0x := strings.TrimPrefix(apiAuth.Signature, "0x") - s, err := hex.DecodeString(without0x) - if err != nil { - return nil, err - } - auth := &common.AccountCreationAuth{ - EthAddr: *ethAddr, - BJJ: bjj, - Signature: s, - Timestamp: apiAuth.Timestamp, - } - if !auth.VerifySignature() { - return nil, errors.New("invalid signature") - } - return auth, nil -} diff --git a/api/exits.go b/api/exits.go new file mode 100644 index 0000000..76f64d6 --- /dev/null +++ b/api/exits.go @@ -0,0 +1,72 @@ +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/hermeznetwork/hermez-node/db" + "github.com/hermeznetwork/hermez-node/db/historydb" +) + +func getExits(c *gin.Context) { + // Get query parameters + // Account filters + tokenID, addr, bjj, idx, err := parseAccountFilters(c) + if err != nil { + retBadReq(err, c) + return + } + // BatchNum + batchNum, err := parseQueryUint("batchNum", nil, 0, maxUint32, c) + if err != nil { + retBadReq(err, c) + return + } + // Pagination + fromItem, order, limit, err := parsePagination(c) + if err != nil { + retBadReq(err, c) + return + } + + // Fetch exits from historyDB + exits, pagination, err := h.GetExitsAPI( + addr, bjj, tokenID, idx, batchNum, fromItem, limit, order, + ) + if err != nil { + retSQLErr(err, c) + return + } + + // Build succesfull response + type exitsResponse struct { + Exits []historydb.ExitAPI `json:"exits"` + Pagination *db.Pagination `json:"pagination"` + } + c.JSON(http.StatusOK, &exitsResponse{ + Exits: exits, + Pagination: pagination, + }) +} + +func getExit(c *gin.Context) { + // Get batchNum and accountIndex + batchNum, err := parseParamUint("batchNum", nil, 0, maxUint32, c) + if err != nil { + retBadReq(err, c) + return + } + idx, err := parseParamIdx(c) + if err != nil { + retBadReq(err, c) + return + } + // Fetch tx from historyDB + exit, err := h.GetExitAPI(batchNum, idx) + if err != nil { + retSQLErr(err, c) + return + } + // Build succesfull response + c.JSON(http.StatusOK, exit) +} diff --git a/api/exits_test.go b/api/exits_test.go new file mode 100644 index 0000000..5cbf82a --- /dev/null +++ b/api/exits_test.go @@ -0,0 +1,276 @@ +package api + +import ( + "fmt" + "testing" + + "github.com/hermeznetwork/hermez-node/common" + "github.com/hermeznetwork/hermez-node/db" + "github.com/hermeznetwork/hermez-node/db/historydb" + "github.com/mitchellh/copystructure" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testCVP struct { + Root string + Siblings []string + OldKey string + OldValue string + IsOld0 bool + Key string + Value string + Fnc int +} + +type testExit struct { + ItemID int `json:"itemId"` + BatchNum common.BatchNum `json:"batchNum"` + AccountIdx string `json:"accountIndex"` + MerkleProof testCVP `json:"merkleProof"` + Balance string `json:"balance"` + InstantWithdrawn *int64 `json:"instantWithdrawn"` + DelayedWithdrawRequest *int64 `json:"delayedWithdrawRequest"` + DelayedWithdrawn *int64 `json:"delayedWithdrawn"` + Token historydb.TokenWithUSD `json:"token"` +} + +type testExitsResponse struct { + Exits []testExit `json:"exits"` + Pagination *db.Pagination `json:"pagination"` +} + +func (t *testExitsResponse) GetPagination() *db.Pagination { + if t.Exits[0].ItemID < t.Exits[len(t.Exits)-1].ItemID { + t.Pagination.FirstReturnedItem = t.Exits[0].ItemID + t.Pagination.LastReturnedItem = t.Exits[len(t.Exits)-1].ItemID + } else { + t.Pagination.LastReturnedItem = t.Exits[0].ItemID + t.Pagination.FirstReturnedItem = t.Exits[len(t.Exits)-1].ItemID + } + return t.Pagination +} + +func (t *testExitsResponse) Len() int { + return len(t.Exits) +} + +func genTestExits( + commonExits []common.ExitInfo, + tokens []historydb.TokenWithUSD, + accs []common.Account, + usrIdxs []string, +) (usrExits, allExits []testExit) { + allExits = []testExit{} + for _, exit := range commonExits { + token := getTokenByIdx(exit.AccountIdx, tokens, accs) + siblings := []string{} + for i := 0; i < len(exit.MerkleProof.Siblings); i++ { + siblings = append(siblings, exit.MerkleProof.Siblings[i].String()) + } + allExits = append(allExits, testExit{ + BatchNum: exit.BatchNum, + AccountIdx: idxToHez(exit.AccountIdx, token.Symbol), + MerkleProof: testCVP{ + Root: exit.MerkleProof.Root.String(), + Siblings: siblings, + OldKey: exit.MerkleProof.OldKey.String(), + OldValue: exit.MerkleProof.OldValue.String(), + IsOld0: exit.MerkleProof.IsOld0, + Key: exit.MerkleProof.Key.String(), + Value: exit.MerkleProof.Value.String(), + Fnc: exit.MerkleProof.Fnc, + }, + Balance: exit.Balance.String(), + InstantWithdrawn: exit.InstantWithdrawn, + DelayedWithdrawRequest: exit.DelayedWithdrawRequest, + DelayedWithdrawn: exit.DelayedWithdrawn, + Token: token, + }) + } + usrExits = []testExit{} + for _, exit := range allExits { + for _, idx := range usrIdxs { + if idx == exit.AccountIdx { + usrExits = append(usrExits, exit) + break + } + } + } + return usrExits, allExits +} + +func TestGetExits(t *testing.T) { + endpoint := apiURL + "exits" + fetchedExits := []testExit{} + appendIter := func(intr interface{}) { + for i := 0; i < len(intr.(*testExitsResponse).Exits); i++ { + tmp, err := copystructure.Copy(intr.(*testExitsResponse).Exits[i]) + if err != nil { + panic(err) + } + fetchedExits = append(fetchedExits, tmp.(testExit)) + } + } + // Get all (no filters) + limit := 8 + path := fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit) + err := doGoodReqPaginated(path, historydb.OrderAsc, &testExitsResponse{}, appendIter) + assert.NoError(t, err) + assertExitAPIs(t, tc.exits, fetchedExits) + + // Get by ethAddr + fetchedExits = []testExit{} + limit = 7 + path = fmt.Sprintf( + "%s?hermezEthereumAddress=%s&limit=%d&fromItem=", + endpoint, tc.usrAddr, limit, + ) + err = doGoodReqPaginated(path, historydb.OrderAsc, &testExitsResponse{}, appendIter) + assert.NoError(t, err) + assertExitAPIs(t, tc.usrExits, fetchedExits) + // Get by bjj + fetchedExits = []testExit{} + limit = 6 + path = fmt.Sprintf( + "%s?BJJ=%s&limit=%d&fromItem=", + endpoint, tc.usrBjj, limit, + ) + err = doGoodReqPaginated(path, historydb.OrderAsc, &testExitsResponse{}, appendIter) + assert.NoError(t, err) + assertExitAPIs(t, tc.usrExits, fetchedExits) + // Get by tokenID + fetchedExits = []testExit{} + limit = 5 + tokenID := tc.exits[0].Token.TokenID + path = fmt.Sprintf( + "%s?tokenId=%d&limit=%d&fromItem=", + endpoint, tokenID, limit, + ) + err = doGoodReqPaginated(path, historydb.OrderAsc, &testExitsResponse{}, appendIter) + assert.NoError(t, err) + tokenIDExits := []testExit{} + for i := 0; i < len(tc.exits); i++ { + if tc.exits[i].Token.TokenID == tokenID { + tokenIDExits = append(tokenIDExits, tc.exits[i]) + } + } + assertExitAPIs(t, tokenIDExits, fetchedExits) + // idx + fetchedExits = []testExit{} + limit = 4 + idx := tc.exits[0].AccountIdx + path = fmt.Sprintf( + "%s?accountIndex=%s&limit=%d&fromItem=", + endpoint, idx, limit, + ) + err = doGoodReqPaginated(path, historydb.OrderAsc, &testExitsResponse{}, appendIter) + assert.NoError(t, err) + idxExits := []testExit{} + for i := 0; i < len(tc.exits); i++ { + if tc.exits[i].AccountIdx[6:] == idx[6:] { + idxExits = append(idxExits, tc.exits[i]) + } + } + assertExitAPIs(t, idxExits, fetchedExits) + // batchNum + fetchedExits = []testExit{} + limit = 3 + batchNum := tc.exits[0].BatchNum + path = fmt.Sprintf( + "%s?batchNum=%d&limit=%d&fromItem=", + endpoint, batchNum, limit, + ) + err = doGoodReqPaginated(path, historydb.OrderAsc, &testExitsResponse{}, appendIter) + assert.NoError(t, err) + batchNumExits := []testExit{} + for i := 0; i < len(tc.exits); i++ { + if tc.exits[i].BatchNum == batchNum { + batchNumExits = append(batchNumExits, tc.exits[i]) + } + } + assertExitAPIs(t, batchNumExits, fetchedExits) + // Multiple filters + fetchedExits = []testExit{} + limit = 1 + path = fmt.Sprintf( + "%s?batchNum=%d&tokeId=%d&limit=%d&fromItem=", + endpoint, batchNum, tokenID, limit, + ) + err = doGoodReqPaginated(path, historydb.OrderAsc, &testExitsResponse{}, appendIter) + assert.NoError(t, err) + mixedExits := []testExit{} + flipedExits := []testExit{} + for i := 0; i < len(tc.exits); i++ { + if tc.exits[i].BatchNum == batchNum && tc.exits[i].Token.TokenID == tokenID { + mixedExits = append(mixedExits, tc.exits[i]) + } + flipedExits = append(flipedExits, tc.exits[len(tc.exits)-1-i]) + } + assertExitAPIs(t, mixedExits, fetchedExits) + // All, in reverse order + fetchedExits = []testExit{} + limit = 5 + path = fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit) + err = doGoodReqPaginated(path, historydb.OrderDesc, &testExitsResponse{}, appendIter) + assert.NoError(t, err) + assertExitAPIs(t, flipedExits, fetchedExits) + // 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 TestGetExit(t *testing.T) { + // Get all txs by their ID + endpoint := apiURL + "exits/" + fetchedExits := []testExit{} + for _, exit := range tc.exits { + fetchedExit := testExit{} + assert.NoError( + t, doGoodReq( + "GET", + fmt.Sprintf("%s%d/%s", endpoint, exit.BatchNum, exit.AccountIdx), + nil, &fetchedExit, + ), + ) + fetchedExits = append(fetchedExits, fetchedExit) + } + assertExitAPIs(t, tc.exits, fetchedExits) + // 400 + err := doBadReq("GET", endpoint+"1/haz:BOOM:1", nil, 400) + assert.NoError(t, err) + err = doBadReq("GET", endpoint+"-1/hez:BOOM:1", nil, 400) + assert.NoError(t, err) + // 404 + err = doBadReq("GET", endpoint+"494/hez:XXX:1", nil, 404) + assert.NoError(t, err) +} + +func assertExitAPIs(t *testing.T, expected, actual []testExit) { + 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 + 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 + } + assert.Equal(t, expected[i], actual[i]) + } +} diff --git a/api/handlers.go b/api/handlers.go index d4da8ef..60b59ff 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -28,46 +28,6 @@ var ( ErrNillBidderAddr = errors.New("biderAddr can not be nil") ) -func postAccountCreationAuth(c *gin.Context) { - // Parse body - var apiAuth accountCreationAuthAPI - if err := c.ShouldBindJSON(&apiAuth); err != nil { - retBadReq(err, c) - return - } - // API to common + verify signature - dbAuth, err := accountCreationAuthAPIToCommon(&apiAuth) - if err != nil { - retBadReq(err, c) - return - } - // Insert to DB - if err := l2.AddAccountCreationAuth(dbAuth); err != nil { - retSQLErr(err, c) - return - } - // Return OK - c.Status(http.StatusOK) -} - -func getAccountCreationAuth(c *gin.Context) { - // Get hezEthereumAddress - addr, err := parseParamHezEthAddr(c) - if err != nil { - retBadReq(err, c) - return - } - // Fetch auth from l2DB - dbAuth, err := l2.GetAccountCreationAuth(*addr) - if err != nil { - retSQLErr(err, c) - return - } - apiAuth := accountCreationAuthToAPI(dbAuth) - // Build succesfull response - c.JSON(http.StatusOK, apiAuth) -} - func getAccounts(c *gin.Context) { } @@ -76,67 +36,6 @@ func getAccount(c *gin.Context) { } -func getExits(c *gin.Context) { - // Get query parameters - // Account filters - tokenID, addr, bjj, idx, err := parseAccountFilters(c) - if err != nil { - retBadReq(err, c) - return - } - // BatchNum - batchNum, err := parseQueryUint("batchNum", nil, 0, maxUint32, c) - if err != nil { - retBadReq(err, c) - return - } - // Pagination - fromItem, order, limit, err := parsePagination(c) - if err != nil { - retBadReq(err, c) - return - } - - // Fetch exits from historyDB - exits, pagination, err := h.GetExits( - addr, bjj, tokenID, idx, batchNum, fromItem, limit, order, - ) - if err != nil { - retSQLErr(err, c) - return - } - - // Build succesfull response - apiExits := historyExitsToAPI(exits) - c.JSON(http.StatusOK, &exitsAPI{ - Exits: apiExits, - Pagination: pagination, - }) -} - -func getExit(c *gin.Context) { - // Get batchNum and accountIndex - batchNum, err := parseParamUint("batchNum", nil, 0, maxUint32, c) - if err != nil { - retBadReq(err, c) - return - } - idx, err := parseParamIdx(c) - if err != nil { - retBadReq(err, c) - return - } - // Fetch tx from historyDB - exit, err := h.GetExit(batchNum, idx) - if err != nil { - retSQLErr(err, c) - return - } - apiExits := historyExitsToAPI([]historydb.HistoryExit{*exit}) - // Build succesfull response - c.JSON(http.StatusOK, apiExits[0]) -} - func getSlots(c *gin.Context) { } diff --git a/api/txshistory_test.go b/api/txshistory_test.go index 31e5c3c..3d02bb0 100644 --- a/api/txshistory_test.go +++ b/api/txshistory_test.go @@ -152,7 +152,13 @@ func (tx *wrappedL2) L2() *common.L2Tx { return &l2tx } -func genTestTxs(genericTxs []txSortFielder, usrIdxs []string, accs []common.Account, tokens []historydb.TokenWithUSD, blocks []common.Block) (usrTxs []testTx, allTxs []testTx) { +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 { diff --git a/apitypes/apitypes.go b/apitypes/apitypes.go index a97ab98..65d6499 100644 --- a/apitypes/apitypes.go +++ b/apitypes/apitypes.go @@ -3,6 +3,7 @@ package apitypes import ( "database/sql/driver" "encoding/base64" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -261,3 +262,49 @@ func (s *StrHezIdx) UnmarshalText(text []byte) error { *s = StrHezIdx(common.Idx(idxInt)) return nil } + +// EthSignature is used to scan/value []byte representing an Ethereum signature directly into strings from/to sql DBs. +type EthSignature string + +// NewEthSignature creates a *EthSignature from []byte +// If the provided signature is nil the returned *EthSignature will also be nil +func NewEthSignature(signature []byte) *EthSignature { + if signature == nil { + return nil + } + ethSignature := EthSignature("0x" + hex.EncodeToString(signature)) + return ðSignature +} + +// Scan implements Scanner for database/sql +func (e *EthSignature) Scan(src interface{}) error { + if srcStr, ok := src.(string); ok { + // src is a string + *e = *(NewEthSignature([]byte(srcStr))) + return nil + } else if srcBytes, ok := src.([]byte); ok { + // src is []byte + *e = *(NewEthSignature(srcBytes)) + return nil + } else { + // unexpected src + return fmt.Errorf("can't scan %T into apitypes.EthSignature", src) + } +} + +// Value implements valuer for database/sql +func (e EthSignature) Value() (driver.Value, error) { + without0x := strings.TrimPrefix(string(e), "0x") + return hex.DecodeString(without0x) +} + +// UnmarshalText unmarshals a StrEthSignature +func (e *EthSignature) UnmarshalText(text []byte) error { + without0x := strings.TrimPrefix(string(text), "0x") + signature, err := hex.DecodeString(without0x) + if err != nil { + return err + } + *e = EthSignature([]byte(signature)) + return nil +} diff --git a/apitypes/apitypes_test.go b/apitypes/apitypes_test.go index 2ce058d..020fc70 100644 --- a/apitypes/apitypes_test.go +++ b/apitypes/apitypes_test.go @@ -332,3 +332,82 @@ func TestHezBJJ(t *testing.T) { assert.NoError(t, err) assert.Nil(t, toBJJNil.I) } + +func TestEthSignature(t *testing.T) { + // Clean DB + _, err := db.Exec("delete from test") + assert.NoError(t, err) + // Example structs + type ethSignStruct struct { + I []byte `meddler:"i"` + } + type hezEthSignStruct struct { + I EthSignature `meddler:"i"` + } + type hezEthSignStructNil struct { + I *EthSignature `meddler:"i"` + } + + // Not nil case + // Insert into DB using []byte Scan/Value + s := "someRandomFooForYou" + fromEth := ethSignStruct{ + I: []byte(s), + } + err = meddler.Insert(db, "test", &fromEth) + assert.NoError(t, err) + // Read from DB using EthSignature Scan/Value + toHezEth := hezEthSignStruct{} + err = meddler.QueryRow(db, &toHezEth, "select * from test") + assert.NoError(t, err) + assert.Equal(t, NewEthSignature(fromEth.I), &toHezEth.I) + // Clean DB + _, err = db.Exec("delete from test") + assert.NoError(t, err) + // Insert into DB using EthSignature Scan/Value + fromHezEth := hezEthSignStruct{ + I: *NewEthSignature([]byte(s)), + } + err = meddler.Insert(db, "test", &fromHezEth) + assert.NoError(t, err) + // Read from DB using []byte Scan/Value + toEth := ethSignStruct{} + err = meddler.QueryRow(db, &toEth, "select * from test") + assert.NoError(t, err) + assert.Equal(t, &fromHezEth.I, NewEthSignature(toEth.I)) + + // Nil case + // Clean DB + _, err = db.Exec("delete from test") + assert.NoError(t, err) + // Insert into DB using []byte Scan/Value + fromEthNil := ethSignStruct{ + I: nil, + } + err = meddler.Insert(db, "test", &fromEthNil) + assert.NoError(t, err) + // Read from DB using EthSignature Scan/Value + foo := EthSignature("foo") + toHezEthNil := hezEthSignStructNil{ + I: &foo, // check that this will be set to nil, not because of not being initialized + } + err = meddler.QueryRow(db, &toHezEthNil, "select * from test") + assert.NoError(t, err) + assert.Nil(t, toHezEthNil.I) + // Clean DB + _, err = db.Exec("delete from test") + assert.NoError(t, err) + // Insert into DB using EthSignature Scan/Value + fromHezEthNil := hezEthSignStructNil{ + I: nil, + } + err = meddler.Insert(db, "test", &fromHezEthNil) + assert.NoError(t, err) + // Read from DB using []byte Scan/Value + toEthNil := ethSignStruct{ + I: []byte(s), // check that this will be set to nil, not because of not being initialized + } + err = meddler.QueryRow(db, &toEthNil, "select * from test") + assert.NoError(t, err) + assert.Nil(t, toEthNil.I) +} diff --git a/db/historydb/historydb.go b/db/historydb/historydb.go index aba4a3f..0c5d1c8 100644 --- a/db/historydb/historydb.go +++ b/db/historydb/historydb.go @@ -891,12 +891,17 @@ func (hdb *HistoryDB) GetAllExits() ([]common.ExitInfo, error) { return db.SlicePtrsToSlice(exits).([]common.ExitInfo), err } -// GetExit returns a exit from the DB -func (hdb *HistoryDB) GetExit(batchNum *uint, idx *common.Idx) (*HistoryExit, error) { - exit := &HistoryExit{} +// GetExitAPI returns a exit from the DB +func (hdb *HistoryDB) GetExitAPI(batchNum *uint, idx *common.Idx) (*ExitAPI, error) { + exit := &ExitAPI{} err := meddler.QueryRow( - hdb.db, exit, `SELECT exit_tree.*, token.token_id, token.eth_block_num AS token_block, - token.eth_addr, token.name, token.symbol, token.decimals, token.usd, token.usd_update + hdb.db, exit, `SELECT exit_tree.item_id, exit_tree.batch_num, + hez_idx(exit_tree.account_idx, token.symbol) AS account_idx, + exit_tree.merkle_proof, exit_tree.balance, exit_tree.instant_withdrawn, + exit_tree.delayed_withdraw_request, exit_tree.delayed_withdrawn, + 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 FROM exit_tree INNER JOIN account ON exit_tree.account_idx = account.idx INNER JOIN token ON account.token_id = token.token_id WHERE exit_tree.batch_num = $1 AND exit_tree.account_idx = $2;`, batchNum, idx, @@ -904,20 +909,25 @@ func (hdb *HistoryDB) GetExit(batchNum *uint, idx *common.Idx) (*HistoryExit, er return exit, err } -// GetExits returns a list of exits from the DB and pagination info -func (hdb *HistoryDB) GetExits( +// GetExitsAPI returns a list of exits from the DB and pagination info +func (hdb *HistoryDB) GetExitsAPI( ethAddr *ethCommon.Address, bjj *babyjub.PublicKey, tokenID *common.TokenID, idx *common.Idx, batchNum *uint, fromItem, limit *uint, order string, -) ([]HistoryExit, *db.Pagination, error) { +) ([]ExitAPI, *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 exit_tree.*, token.token_id, token.eth_block_num AS token_block, - token.eth_addr, token.name, token.symbol, token.decimals, token.usd, - token.usd_update, COUNT(*) OVER() AS total_items, MIN(exit_tree.item_id) OVER() AS first_item, MAX(exit_tree.item_id) OVER() AS last_item + queryStr := `SELECT exit_tree.item_id, exit_tree.batch_num, + hez_idx(exit_tree.account_idx, token.symbol) AS account_idx, + exit_tree.merkle_proof, exit_tree.balance, exit_tree.instant_withdrawn, + exit_tree.delayed_withdraw_request, exit_tree.delayed_withdrawn, + 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, COUNT(*) OVER() AS total_items, + MIN(exit_tree.item_id) OVER() AS first_item, MAX(exit_tree.item_id) OVER() AS last_item FROM exit_tree INNER JOIN account ON exit_tree.account_idx = account.idx INNER JOIN token ON account.token_id = token.token_id ` // Apply filters @@ -989,14 +999,14 @@ func (hdb *HistoryDB) GetExits( queryStr += fmt.Sprintf("LIMIT %d;", *limit) query = hdb.db.Rebind(queryStr) // log.Debug(query) - exits := []*HistoryExit{} + exits := []*ExitAPI{} if err := meddler.QueryAll(hdb.db, &exits, query, args...); err != nil { return nil, nil, err } if len(exits) == 0 { return nil, nil, sql.ErrNoRows } - return db.SlicePtrsToSlice(exits).([]HistoryExit), &db.Pagination{ + return db.SlicePtrsToSlice(exits).([]ExitAPI), &db.Pagination{ TotalItems: exits[0].TotalItems, FirstItem: exits[0].FirstItem, LastItem: exits[0].LastItem, diff --git a/db/historydb/views.go b/db/historydb/views.go index 80c0180..dd23a00 100644 --- a/db/historydb/views.go +++ b/db/historydb/views.go @@ -151,14 +151,14 @@ type TokenWithUSD struct { LastItem int `json:"-" meddler:"last_item"` } -// HistoryExit is a representation of a exit with additional information +// ExitAPI is a representation of a exit with additional information // required by the API, and extracted by joining token table -type HistoryExit struct { +type ExitAPI struct { ItemID int `meddler:"item_id"` BatchNum common.BatchNum `meddler:"batch_num"` - AccountIdx common.Idx `meddler:"account_idx"` + AccountIdx apitypes.HezIdx `meddler:"account_idx"` MerkleProof *merkletree.CircomVerifierProof `meddler:"merkle_proof,json"` - Balance *big.Int `meddler:"balance,bigint"` + Balance apitypes.BigIntStr `meddler:"balance"` InstantWithdrawn *int64 `meddler:"instant_withdrawn"` DelayedWithdrawRequest *int64 `meddler:"delayed_withdraw_request"` DelayedWithdrawn *int64 `meddler:"delayed_withdrawn"` @@ -166,6 +166,7 @@ type HistoryExit 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"` @@ -175,6 +176,45 @@ type HistoryExit struct { TokenUSDUpdate *time.Time `meddler:"usd_update"` } +// MarshalJSON is used to neast some of the fields of ExitAPI +// without the need of auxiliar structs +func (e ExitAPI) MarshalJSON() ([]byte, error) { + siblings := []string{} + for i := 0; i < len(e.MerkleProof.Siblings); i++ { + siblings = append(siblings, e.MerkleProof.Siblings[i].String()) + } + return json.Marshal(map[string]interface{}{ + "itemId": e.ItemID, + "batchNum": e.BatchNum, + "accountIndex": e.AccountIdx, + "merkleProof": map[string]interface{}{ + "Root": e.MerkleProof.Root.String(), + "Siblings": siblings, + "OldKey": e.MerkleProof.OldKey.String(), + "OldValue": e.MerkleProof.OldValue.String(), + "IsOld0": e.MerkleProof.IsOld0, + "Key": e.MerkleProof.Key.String(), + "Value": e.MerkleProof.Value.String(), + "Fnc": e.MerkleProof.Fnc, + }, + "balance": e.Balance, + "instantWithdrawn": e.InstantWithdrawn, + "delayedWithdrawRequest": e.DelayedWithdrawRequest, + "delayedWithdrawn": e.DelayedWithdrawn, + "token": map[string]interface{}{ + "id": e.TokenID, + "itemId": e.TokenItemID, + "ethereumBlockNum": e.TokenEthBlockNum, + "ethereumAddress": e.TokenEthAddr, + "name": e.TokenName, + "symbol": e.TokenSymbol, + "decimals": e.TokenDecimals, + "USD": e.TokenUSD, + "fiatUpdate": e.TokenUSDUpdate, + }, + }) +} + // CoordinatorAPI is a representation of a coordinator with additional information // required by the API type CoordinatorAPI struct { @@ -198,16 +238,15 @@ type BatchAPI struct { Timestamp time.Time `json:"timestamp" meddler:"timestamp,utctime"` ForgerAddr ethCommon.Address `json:"forgerAddr" meddler:"forger_addr"` CollectedFees apitypes.CollectedFees `json:"collectedFees" meddler:"fees_collected,json"` - // CollectedFees map[common.TokenID]*big.Int `json:"collectedFees" meddler:"fees_collected,json"` - TotalFeesUSD *float64 `json:"historicTotalCollectedFeesUSD" meddler:"total_fees_usd"` - StateRoot apitypes.BigIntStr `json:"stateRoot" meddler:"state_root"` - NumAccounts int `json:"numAccounts" meddler:"num_accounts"` - ExitRoot apitypes.BigIntStr `json:"exitRoot" meddler:"exit_root"` - ForgeL1TxsNum *int64 `json:"forgeL1TransactionsNum" meddler:"forge_l1_txs_num"` - SlotNum int64 `json:"slotNum" meddler:"slot_num"` - TotalItems int `json:"-" meddler:"total_items"` - FirstItem int `json:"-" meddler:"first_item"` - LastItem int `json:"-" meddler:"last_item"` + TotalFeesUSD *float64 `json:"historicTotalCollectedFeesUSD" meddler:"total_fees_usd"` + StateRoot apitypes.BigIntStr `json:"stateRoot" meddler:"state_root"` + NumAccounts int `json:"numAccounts" meddler:"num_accounts"` + ExitRoot apitypes.BigIntStr `json:"exitRoot" meddler:"exit_root"` + ForgeL1TxsNum *int64 `json:"forgeL1TransactionsNum" meddler:"forge_l1_txs_num"` + SlotNum int64 `json:"slotNum" meddler:"slot_num"` + TotalItems int `json:"-" meddler:"total_items"` + FirstItem int `json:"-" meddler:"first_item"` + LastItem int `json:"-" meddler:"last_item"` } // Network define status of the network diff --git a/db/l2db/l2db.go b/db/l2db/l2db.go index 9891040..970bb9d 100644 --- a/db/l2db/l2db.go +++ b/db/l2db/l2db.go @@ -55,7 +55,7 @@ func (l2db *L2DB) AddAccountCreationAuth(auth *common.AccountCreationAuth) error return err } -// GetAccountCreationAuth returns an account creation authorization into the DB +// GetAccountCreationAuth returns an account creation authorization from the DB func (l2db *L2DB) GetAccountCreationAuth(addr ethCommon.Address) (*common.AccountCreationAuth, error) { auth := new(common.AccountCreationAuth) return auth, meddler.QueryRow( @@ -65,6 +65,16 @@ func (l2db *L2DB) GetAccountCreationAuth(addr ethCommon.Address) (*common.Accoun ) } +// GetAccountCreationAuthAPI returns an account creation authorization from the DB +func (l2db *L2DB) GetAccountCreationAuthAPI(addr ethCommon.Address) (*AccountCreationAuthAPI, error) { + auth := new(AccountCreationAuthAPI) + return auth, meddler.QueryRow( + l2db.db, auth, + "SELECT * FROM account_creation_auth WHERE eth_addr = $1;", + addr, + ) +} + // AddTx inserts a tx to the pool func (l2db *L2DB) AddTx(tx *PoolL2TxWrite) error { return meddler.Insert(l2db.db, "tx_pool", tx) diff --git a/db/l2db/views.go b/db/l2db/views.go index c398030..29263aa 100644 --- a/db/l2db/views.go +++ b/db/l2db/views.go @@ -114,3 +114,11 @@ func (tx PoolTxAPI) MarshalJSON() ([]byte, error) { }, }) } + +// AccountCreationAuthAPI represents an account creation auth in the expected format by the API +type AccountCreationAuthAPI struct { + EthAddr apitypes.HezEthAddr `json:"hezEthereumAddress" meddler:"eth_addr" ` + BJJ apitypes.HezBJJ `json:"bjj" meddler:"bjj" ` + Signature apitypes.EthSignature `json:"signature" meddler:"signature" ` + Timestamp time.Time `json:"timestamp" meddler:"timestamp,utctime"` +}