Implement tx pool endpoints

This commit is contained in:
Arnau B
2020-10-14 17:38:34 +02:00
parent efed84f1e8
commit 006bbf3321
20 changed files with 990 additions and 399 deletions

View File

@@ -12,8 +12,8 @@ import (
var h *historydb.HistoryDB
var cg *configAPI
// var s *statedb.StateDB // Not 100% sure if this is needed
// var l2 *l2db.L2DB
var s *statedb.StateDB
var l2 *l2db.L2DB
// SetAPIEndpoints sets the endpoints and the appropriate handlers, but doesn't start the server
func SetAPIEndpoints(
@@ -35,8 +35,8 @@ func SetAPIEndpoints(
h = hdb
cg = config
// s = sdb
// l2 = l2db
s = sdb
l2 = l2db
// Add coordinator endpoints
if coordinatorEndpoints {
@@ -45,7 +45,7 @@ func SetAPIEndpoints(
server.GET("/account-creation-authorization/:hermezEthereumAddress", getAccountCreationAuth)
// Transaction
server.POST("/transactions-pool", postPoolTx)
server.POST("/transactions-pool/:id", getPoolTx)
server.GET("/transactions-pool/:id", getPoolTx)
}
// Add explorer endpoints

View File

@@ -1,6 +1,7 @@
package api
import (
"bytes"
"context"
"encoding/json"
"errors"
@@ -37,17 +38,19 @@ const apiPort = ":4010"
const apiURL = "http://localhost" + apiPort + "/"
type testCommon struct {
blocks []common.Block
tokens []tokenAPI
batches []common.Batch
usrAddr string
usrBjj string
accs []common.Account
usrTxs []historyTxAPI
allTxs []historyTxAPI
exits []exitAPI
usrExits []exitAPI
router *swagger.Router
blocks []common.Block
tokens []tokenAPI
batches []common.Batch
usrAddr string
usrBjj string
accs []common.Account
usrTxs []historyTxAPI
allTxs []historyTxAPI
exits []exitAPI
usrExits []exitAPI
poolTxsToSend []receivedPoolTx
poolTxsToReceive []sendPoolTx
router *swagger.Router
}
// TxSortFields represents the fields needed to sort L1 and L2 transactions
@@ -281,7 +284,7 @@ func TestMain(m *testing.M) {
if err != nil {
panic(err)
}
// Gen accounts and add them to DB
// Gen accounts and add them to HistoryDB and StateDB
const totalAccounts = 40
const userAccounts = 4
usrAddr := ethCommon.BigToAddress(big.NewInt(4896847))
@@ -292,6 +295,11 @@ func TestMain(m *testing.M) {
if err != nil {
panic(err)
}
for i := 0; i < len(accs); i++ {
if _, err := s.CreateAccount(accs[i].Idx, &accs[i]); err != nil {
panic(err)
}
}
// Gen exits and add them to DB
const totalExits = 40
exits := test.GenExitTree(totalExits, batches, accs)
@@ -387,7 +395,7 @@ func TestMain(m *testing.M) {
token := getToken(l1.TokenID)
tx := historyTxAPI{
IsL1: "L1",
TxID: l1.TxID.String(),
TxID: l1.TxID,
Type: l1.Type,
Position: l1.Position,
ToIdx: idxToHez(l1.ToIdx, token.Symbol),
@@ -444,7 +452,7 @@ func TestMain(m *testing.M) {
token := getToken(tokenID)
tx := historyTxAPI{
IsL1: "L2",
TxID: l2.TxID.String(),
TxID: l2.TxID,
Type: l2.Type,
Position: l2.Position,
ToIdx: idxToHez(l2.ToIdx, token.Symbol),
@@ -511,18 +519,124 @@ 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)
}
// Set testCommon
tc = testCommon{
blocks: blocks,
tokens: tokensUSD,
batches: batches,
usrAddr: ethAddrToHez(usrAddr),
usrBjj: bjjToString(usrBjj),
accs: accs,
usrTxs: usrTxs,
allTxs: allTxs,
exits: apiExits,
usrExits: usrExits,
router: router,
blocks: blocks,
tokens: tokensUSD,
batches: batches,
usrAddr: ethAddrToHez(usrAddr),
usrBjj: bjjToString(usrBjj),
accs: accs,
usrTxs: usrTxs,
allTxs: allTxs,
exits: apiExits,
usrExits: usrExits,
poolTxsToSend: poolTxsToSend,
poolTxsToReceive: poolTxsToReceive,
router: router,
}
// Run tests
result := m.Run()
@@ -533,6 +647,9 @@ func TestMain(m *testing.M) {
if err := database.Close(); err != nil {
panic(err)
}
if err := os.RemoveAll(dir); err != nil {
panic(err)
}
os.Exit(result)
}
@@ -713,7 +830,7 @@ func TestGetHistoryTx(t *testing.T) {
fetchedTxs := []historyTxAPI{}
for _, tx := range tc.allTxs {
fetchedTx := historyTxAPI{}
assert.NoError(t, doGoodReq("GET", endpoint+tx.TxID, nil, &fetchedTx))
assert.NoError(t, doGoodReq("GET", endpoint+tx.TxID.String(), nil, &fetchedTx))
fetchedTxs = append(fetchedTxs, fetchedTx)
}
assertHistoryTxAPIs(t, tc.allTxs, fetchedTxs)
@@ -876,7 +993,7 @@ func TestGetExits(t *testing.T) {
path = fmt.Sprintf("%s?batchNum=999999", endpoint)
err = doBadReq("GET", path, nil, 404)
assert.NoError(t, err)
path = fmt.Sprintf("%s?limit=1000&fromItem=1000", endpoint)
path = fmt.Sprintf("%s?limit=1000&fromItem=999999", endpoint)
err = doBadReq("GET", path, nil, 404)
assert.NoError(t, err)
}
@@ -1049,6 +1166,90 @@ 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)
fmt.Println(string(jsonTxBytes))
assert.NoError(
t, doGoodReq(
"POST",
endpoint,
jsonTxReader, &fetchedTxID,
),
)
assert.Equal(t, tx.TxID, fetchedTxID)
}
// 400
// Wrong signature
badTx := tc.poolTxsToSend[0]
badTx.FromIdx = "hez:foo:1000"
jsonTxBytes, err := json.Marshal(badTx)
assert.NoError(t, err)
jsonTxReader := bytes.NewReader(jsonTxBytes)
err = doBadReq("POST", endpoint, jsonTxReader, 400)
assert.NoError(t, err)
// Wrong to
badTx = tc.poolTxsToSend[0]
ethAddr := "hez:0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
badTx.ToEthAddr = &ethAddr
badTx.ToIdx = nil
jsonTxBytes, err = json.Marshal(badTx)
assert.NoError(t, err)
jsonTxReader = bytes.NewReader(jsonTxBytes)
err = doBadReq("POST", endpoint, jsonTxReader, 400)
assert.NoError(t, err)
// Wrong rq
badTx = tc.poolTxsToSend[0]
rqFromIdx := "hez:foo:30"
badTx.RqFromIdx = &rqFromIdx
jsonTxBytes, err = json.Marshal(badTx)
assert.NoError(t, err)
jsonTxReader = bytes.NewReader(jsonTxBytes)
err = doBadReq("POST", endpoint, jsonTxReader, 400)
assert.NoError(t, err)
// GET
endpoint += "/"
for _, tx := range tc.poolTxsToReceive {
fetchedTx := sendPoolTx{}
assert.NoError(
t, doGoodReq(
"GET",
endpoint+tx.TxID.String(),
nil, &fetchedTx,
),
)
assertPoolTx(t, tx, fetchedTx)
}
// 400
err = doBadReq("GET", endpoint+"0xG20000000156660000000090", nil, 400)
assert.NoError(t, err)
// 404
err = doBadReq("GET", endpoint+"0x020000000156660000000090", nil, 404)
assert.NoError(t, err)
}
func assertPoolTx(t *testing.T, expected, actual sendPoolTx) {
// state should be pending
assert.Equal(t, common.PoolL2TxStatePending, actual.State)
expected.State = actual.State
// timestamp should be very close to now
assert.Less(t, time.Now().UTC().Unix()-3, actual.Timestamp.Unix())
expected.Timestamp = actual.Timestamp
// token timestamp
if expected.Token.USDUpdate == nil {
assert.Equal(t, expected.Token.USDUpdate, actual.Token.USDUpdate)
} else {
assert.Equal(t, expected.Token.USDUpdate.Unix(), actual.Token.USDUpdate.Unix())
expected.Token.USDUpdate = actual.Token.USDUpdate
}
assert.Equal(t, expected, actual)
}
func doGoodReqPaginated(
path, order string,
iterStruct db.Paginationer,
@@ -1091,7 +1292,13 @@ func doGoodReqPaginated(
func doGoodReq(method, path string, reqBody io.Reader, returnStruct interface{}) error {
ctx := context.Background()
client := &http.Client{}
httpReq, _ := http.NewRequest(method, path, reqBody)
httpReq, err := http.NewRequest(method, path, reqBody)
if err != nil {
return err
}
if reqBody != nil {
httpReq.Header.Add("Content-Type", "application/json")
}
route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
if err != nil {
return err
@@ -1171,7 +1378,7 @@ func doBadReq(method, path string, reqBody io.Reader, expectedResponseCode int)
return err
}
if resp.StatusCode != expectedResponseCode {
return fmt.Errorf("Unexpected response code: %d", resp.StatusCode)
return fmt.Errorf("Unexpected response code: %d. Body: %s", resp.StatusCode, string(body))
}
// Validate response against swagger spec
responseValidationInput := &swagger.ResponseValidationInput{

View File

@@ -2,6 +2,8 @@ package api
import (
"encoding/base64"
"errors"
"fmt"
"math/big"
"strconv"
"time"
@@ -10,11 +12,14 @@ 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"
"github.com/iden3/go-merkletree"
)
const exitIdx = "hez:EXIT:1"
type errorMsg struct {
Message string
}
@@ -34,6 +39,9 @@ func ethAddrToHez(addr ethCommon.Address) string {
}
func idxToHez(idx common.Idx, tokenSymbol string) string {
if idx == 1 {
return exitIdx
}
return "hez:" + tokenSymbol + ":" + strconv.Itoa(int(idx))
}
@@ -74,7 +82,7 @@ type l2Info struct {
type historyTxAPI struct {
IsL1 string `json:"L1orL2"`
TxID string `json:"id"`
TxID common.TxID `json:"id"`
ItemID int `json:"itemId"`
Type common.TxType `json:"type"`
Position int `json:"position"`
@@ -93,7 +101,7 @@ func historyTxsToAPI(dbTxs []historydb.HistoryTx) []historyTxAPI {
apiTxs := []historyTxAPI{}
for i := 0; i < len(dbTxs); i++ {
apiTx := historyTxAPI{
TxID: dbTxs[i].TxID.String(),
TxID: dbTxs[i].TxID,
ItemID: dbTxs[i].ItemID,
Type: dbTxs[i].Type,
Position: dbTxs[i].Position,
@@ -276,3 +284,319 @@ type configAPI struct {
AuctionConstants eth.AuctionConstants `json:"auction"`
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 tokenAPI `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: tokenAPI{
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
}

View File

@@ -28,15 +28,46 @@ func postAccountCreationAuth(c *gin.Context) {
}
func getAccountCreationAuth(c *gin.Context) {
}
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) {

View File

@@ -75,61 +75,16 @@ func parseQueryBool(name string, dflt *bool, c querier) (*bool, error) { //nolin
func parseQueryHezEthAddr(c querier) (*ethCommon.Address, error) {
const name = "hermezEthereumAddress"
addrStr := c.Query(name)
if addrStr == "" {
return nil, nil
}
splitted := strings.Split(addrStr, "hez:")
if len(splitted) != 2 || len(splitted[1]) != 42 {
return nil, fmt.Errorf(
"Invalid %s, must follow this regex: ^hez:0x[a-fA-F0-9]{40}$", name)
}
var addr ethCommon.Address
err := addr.UnmarshalText([]byte(splitted[1]))
return &addr, err
return hezStringToEthAddr(addrStr, name)
}
func parseQueryBJJ(c querier) (*babyjub.PublicKey, error) {
const name = "BJJ"
const decodedLen = 33
bjjStr := c.Query(name)
if bjjStr == "" {
return nil, nil
}
splitted := strings.Split(bjjStr, "hez:")
if len(splitted) != 2 || len(splitted[1]) != 44 {
return nil, fmt.Errorf(
"Invalid %s, must follow this regex: ^hez:[A-Za-z0-9+/=]{44}$",
name)
}
decoded, err := base64.RawURLEncoding.DecodeString(splitted[1])
if err != nil {
return nil, fmt.Errorf(
"Invalid %s, error decoding base64 string: %s",
name, err.Error())
}
if len(decoded) != decodedLen {
return nil, fmt.Errorf(
"invalid %s, error decoding base64 string: unexpected byte array length",
name)
}
bjjBytes := [decodedLen - 1]byte{}
copy(bjjBytes[:decodedLen-1], decoded[:decodedLen-1])
sum := bjjBytes[0]
for i := 1; i < len(bjjBytes); i++ {
sum += bjjBytes[i]
}
if decoded[decodedLen-1] != sum {
return nil, fmt.Errorf("invalid %s, checksum failed",
name)
}
bjjComp := babyjub.PublicKeyComp(bjjBytes)
bjj, err := bjjComp.Decompress()
if err != nil {
return nil, fmt.Errorf(
"invalid %s, error decompressing public key: %s",
name, err.Error())
}
return bjj, nil
return hezStringToBJJ(bjjStr, name)
}
func parseQueryTxType(c querier) (*common.TxType, error) {
@@ -302,3 +257,56 @@ func stringToUint(uintStr, name string, dflt *uint, min, max uint) (*uint, error
}
return dflt, nil
}
func hezStringToEthAddr(addrStr, name string) (*ethCommon.Address, error) {
if addrStr == "" {
return nil, nil
}
splitted := strings.Split(addrStr, "hez:")
if len(splitted) != 2 || len(splitted[1]) != 42 {
return nil, fmt.Errorf(
"Invalid %s, must follow this regex: ^hez:0x[a-fA-F0-9]{40}$", name)
}
var addr ethCommon.Address
err := addr.UnmarshalText([]byte(splitted[1]))
return &addr, err
}
func hezStringToBJJ(bjjStr, name string) (*babyjub.PublicKey, error) {
const decodedLen = 33
splitted := strings.Split(bjjStr, "hez:")
if len(splitted) != 2 || len(splitted[1]) != 44 {
return nil, fmt.Errorf(
"Invalid %s, must follow this regex: ^hez:[A-Za-z0-9+/=]{44}$",
name)
}
decoded, err := base64.RawURLEncoding.DecodeString(splitted[1])
if err != nil {
return nil, fmt.Errorf(
"Invalid %s, error decoding base64 string: %s",
name, err.Error())
}
if len(decoded) != decodedLen {
return nil, fmt.Errorf(
"invalid %s, error decoding base64 string: unexpected byte array length",
name)
}
bjjBytes := [decodedLen - 1]byte{}
copy(bjjBytes[:decodedLen-1], decoded[:decodedLen-1])
sum := bjjBytes[0]
for i := 1; i < len(bjjBytes); i++ {
sum += bjjBytes[i]
}
if decoded[decodedLen-1] != sum {
return nil, fmt.Errorf("invalid %s, checksum failed",
name)
}
bjjComp := babyjub.PublicKeyComp(bjjBytes)
bjj, err := bjjComp.Decompress()
if err != nil {
return nil, fmt.Errorf(
"invalid %s, error decompressing public key: %s",
name, err.Error())
}
return bjj, nil
}

View File

@@ -340,7 +340,7 @@ paths:
tags:
- Account
summary: Get specific exit information.
description: Get exit information form a specific exit tree and account. This information is required to perform a withdraw.
description: Get exit information form a specific exit tree and account. This information is required to perform a withdraw. Exits are identified with accounIndex and batchNum since every batch that has exits has a different exit tree.
operationId: getExit
parameters:
- name: batchNum
@@ -614,6 +614,12 @@ paths:
description: Include only `batchNum > maxBatchNum` batches.
schema:
type: number
- name: slotNum
in: query
required: false
description: Include only batches that were forged within the specified slot.
schema:
$ref: '#/components/schemas/SlotNum'
- name: forgerAddr
in: query
required: false
@@ -941,40 +947,12 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
'/next-forgers':
get:
tags:
- Hermez status
summary: Get next coordinators to forge.
description: >-
Return a list of the coordinators that will forge in the next slots.
The response includes the coordinator that is currently forging, and the ones that have won the upcomming slots whose auctions are closed.
operationId: getNextForgers
responses:
'200':
description: Successful operation.
content:
application/json:
schema:
$ref: '#/components/schemas/NextForgers'
'400':
description: Bad request.
content:
application/json:
schema:
$ref: '#/components/schemas/Error400'
'500':
description: Internal server error.
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
'/state':
get:
tags:
- Hermez status
summary: Return global statistics and metrics of the network.
description: Return global statistics and metrics of the network.
summary: Return information that represents the current state of the network.
description: Return information that represents the current state of the network. It also includes metrics and statistics.
operationId: getState
responses:
'200':
@@ -1133,27 +1111,6 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error500'
'/recommended-fee':
get:
tags:
- Hermez status
summary: Get recommended fee in USD.
description: >-
Get recommended fee in USD. Recommended price to pay according to the status of the destination account.
operationId: getFee
responses:
'200':
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/RecommendedFee'
'500':
description: Internal server error.
content:
application/json:
schema:
$ref: '#/components/schemas/Error500'
'/coordinators':
get:
tags:
@@ -1251,6 +1208,7 @@ components:
description: Position of the item in the DB. This is useful for pagination, but has nothing to do with the protocol.
PostPoolL2Transaction:
type: object
description: L2 transaction to be posted.
properties:
id:
$ref: '#/components/schemas/TransactionId'
@@ -1264,18 +1222,21 @@ components:
type: string
description: >-
Identifier of the destination account. It references the position where the account is inside the state Merkle tree.
The identifier is built using: `hez:` + `token symbol:` + `index`
The identifier is built using: `hez:` + `token symbol:` + `index`. If this is provided, toHezEthereumAddress and toBjj
must be null. To perform an exit the value hez:EXIT:1 must be used.
example: null
nullable: true
toHezEthereumAddress:
type: string
description: "Address of an Etherum account linked to the Hermez network."
description: "Address of an Etherum account linked to the Hermez network. If this is provided, toAccountIndex and toBjj must be null."
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."
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.
If this is prvided, toAccountIndex must be null and toHezEthereumAddress must be hez:0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF.
pattern: "^hez:[A-Za-z0-9_-]{44}$"
example: null
nullable: true
@@ -1309,9 +1270,9 @@ components:
pattern: "^hez:0x[a-fA-F0-9]{40}$"
example: null
nullable: true
requestToBJJ:
requestToBjj:
type: string
description: References the `toBJJ` of the requested transaction.
description: References the `toBjj` of the requested transaction.
pattern: "^hez:[A-Za-z0-9_-]{44}$"
example: null
nullable: true
@@ -1340,22 +1301,10 @@ components:
- type
- tokenId
- fromAccountIndex
- toAccountIndex
- toHezAccountIndex
- toHezEthereumAddress
- toBjj
- amount
- fee
- nonce
- signature
- requestFromAccountIndex
- requestToAccountIndex
- requestToHezEthereumAddress
- requestToBJJ
- requestTokenId
- requestAmount
- requestFee
- requestNonce
PoolL2Transaction:
type: object
properties:
@@ -1405,63 +1354,87 @@ components:
description: Moment in which the transaction was added to the pool.
format: date-time
batchNum:
allOf:
- $ref: '#/components/schemas/BatchNum'
- nullable: true
- example: 5432
type: integer
description: Identifier of a batch. Every new forged batch increments by one the batchNum, starting at 0.
minimum: 0
maximum: 4294967295
nullable: true
example: null
requestFromAccountIndex:
allOf:
- $ref: '#/components/schemas/AccountIndex'
- nullable: true
- example: "hez:0xaa942cfcd25ad4d90a62358b0dd84f33b398262a"
type: string
description: >-
Identifier of an account. It references the position where the account is inside the state Merkle tree.
The identifier is built using: `hez:` + `token symbol:` + `index`
nullable: true
example: null
requestToAccountIndex:
allOf:
- $ref: '#/components/schemas/AccountIndex'
- nullable: true
- example: "hez:DAI:33"
type: string
description: >-
Identifier of an account. It references the position where the account is inside the state Merkle tree.
The identifier is built using: `hez:` + `token symbol:` + `index`
nullable: true
example: null
requestToHezEthereumAddress:
allOf:
- $ref: '#/components/schemas/HezEthereumAddress'
- nullable: true
- example: "hez:0xbb942cfcd25ad4d90a62358b0dd84f33b3982699"
type: string
description: "Address of an Etherum account linked to the Hermez network."
pattern: "^hez:0x[a-fA-F0-9]{40}$"
nullable: true
example: null
requestToBJJ:
allOf:
- $ref: '#/components/schemas/BJJ'
- nullable: true
- example: "hez:HVrB8xQHAYt9QTpPUsj3RGOzDmrCI4IgrYslTeTqo6Ix"
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}$"
nullable: true
example: null
requestTokenId:
allOf:
- $ref: '#/components/schemas/TokenId'
- nullable: true
- example: 4444
type: integer
description: References the `tokenId` of the requested transaction.
example: null
nullable: true
requestAmount:
allOf:
- $ref: '#/components/schemas/BigInt'
- description: Amount of tokens to be sent.
- example: "700000000000000000"
- nullable: true
type: string
description: BigInt is an integer encoded as a string for numbers that are very large.
nullable: true
example: null
requestFee:
allOf:
- $ref: '#/components/schemas/FeeSelector'
- nullable: true
- example: 8
type: integer
description: Index of the fee type to select, more info [here](https://idocs.hermez.io/#/spec/zkrollup/fee-table?id=transaction-fee-table).
minimum: 0
maximum: 256
nullable: true
example: null
requestNonce:
allOf:
- $ref: '#/components/schemas/Nonce'
- nullable: true
- example: 6
type: integer
description: Number that can only be used once per account. Increments by one with each transaction.
minimum: 0
maximum: 1.84467440737096e+19
nullable: true
example: null
token:
$ref: '#/components/schemas/Token'
required:
- fromAccountIndex
- toHezAccountIndex
- toHezEthereumAddress
- toBjj
- tokenId
- amount
- fee
- nonce
- signature
- id
- type
- fromAccountIndex
- toAccountIndex
- toHezEthereumAddress
- toBjj
- amount
- fee
- nonce
- state
- signature
- timestamp
- batchNum
- requestFromAccountIndex
- requestToAccountIndex
- requestToHezEthereumAddress
- requestToBJJ
- requestTokenId
- requestAmount
- requestFee
- requestNonce
- token
TransactionId:
type: string
description: Identifier for transactions. Used for any kind of transaction (both L1 and L2). More info on how the identifiers are built [here](https://idocs.hermez.io/#/spec/architecture/db/README?id=txid)
@@ -1514,7 +1487,7 @@ components:
description: Identifier of a token registered in the network.
minimum: 0
maximum: 4294967295
example: 4444
example: 98765
BigInt:
type: string
description: BigInt is an integer encoded as a string for numbers that are very large.
@@ -1776,9 +1749,6 @@ components:
$ref: '#/components/schemas/EthereumAddress'
collectedFees:
$ref: '#/components/schemas/CollectedFees'
totalCollectedFeesUSD:
type: number
description: Value in USD of the collected tokens by the forger in concept of fees. This is calculated at the moment the batch is forged, with the conversion rates at that time.
historicTotalCollectedFeesUSD:
type: number
description: Sum of the all the fees collected, in USD, at the moment the batch was forged.
@@ -2124,11 +2094,6 @@ components:
- description: Coordinator who won the auction. Only applicable if the auction is closed.
- nullable: true
- example: null
batchNums:
type: array
description: List of batch numbers that were forged during the slot
items:
$ref: '#/components/schemas/BatchNum'
Slots:
type: object
properties:
@@ -2212,6 +2177,10 @@ components:
type: number
description: Average fee percentage paid for L2 transactions in the last 24 hours.
example: 1.54
nextForgers:
$ref: '#/components/schemas/NextForgers'
recommendedFee:
$ref: '#/components/schemas/RecommendedFee'
governance:
type: object
description: Network setings that are updatable by the governance.