package api
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ecdsa"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"math/big"
|
|
"testing"
|
|
"time"
|
|
|
|
ethCrypto "github.com/ethereum/go-ethereum/crypto"
|
|
"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"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// 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"`
|
|
}
|
|
|
|
type testPoolTxsResponse struct {
|
|
Txs []testPoolTxReceive `json:"transactions"`
|
|
}
|
|
|
|
// 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 genTestPoolTxs(
|
|
poolTxs []common.PoolL2Tx,
|
|
tokens []historydb.TokenWithUSD,
|
|
accs []common.Account,
|
|
) (poolTxsToSend []testPoolTxSend, poolTxsToReceive []testPoolTxReceive) {
|
|
poolTxsToSend = []testPoolTxSend{}
|
|
poolTxsToReceive = []testPoolTxReceive{}
|
|
for _, poolTx := range poolTxs {
|
|
// 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.FromIdx, accs)
|
|
fromAddr := ethAddrToHez(fromAcc.EthAddr)
|
|
genReceiveTx.FromEthAddr = &fromAddr
|
|
fromBjj := bjjToString(fromAcc.BJJ)
|
|
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 != common.EmptyBJJComp {
|
|
toBJJ := bjjToString(poolTx.ToBJJ)
|
|
genSendTx.ToBJJ = &toBJJ
|
|
genReceiveTx.ToBJJ = &toBJJ
|
|
} else if poolTx.ToIdx > 255 {
|
|
acc := getAccountByIdx(poolTx.ToIdx, accs)
|
|
bjj := bjjToString(acc.BJJ)
|
|
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 != common.EmptyBJJComp {
|
|
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)
|
|
require.NoError(t, err)
|
|
jsonTxReader := bytes.NewReader(jsonTxBytes)
|
|
require.NoError(
|
|
t, doGoodReq(
|
|
"POST",
|
|
endpoint,
|
|
jsonTxReader, &fetchedTxID,
|
|
),
|
|
)
|
|
assert.Equal(t, tx.TxID, fetchedTxID)
|
|
}
|
|
// 400
|
|
// Wrong fee
|
|
badTx := tc.poolTxsToSend[0]
|
|
badTx.Amount = "99950000000000000"
|
|
badTx.Fee = 255
|
|
jsonTxBytes, err := json.Marshal(badTx)
|
|
require.NoError(t, err)
|
|
jsonTxReader := bytes.NewReader(jsonTxBytes)
|
|
err = doBadReq("POST", endpoint, jsonTxReader, 400)
|
|
require.NoError(t, err)
|
|
// Wrong signature
|
|
badTx = tc.poolTxsToSend[0]
|
|
badTx.FromIdx = "hez:foo:1000"
|
|
jsonTxBytes, err = json.Marshal(badTx)
|
|
require.NoError(t, err)
|
|
jsonTxReader = bytes.NewReader(jsonTxBytes)
|
|
err = doBadReq("POST", endpoint, jsonTxReader, 400)
|
|
require.NoError(t, err)
|
|
// Wrong to
|
|
badTx = tc.poolTxsToSend[0]
|
|
ethAddr := "hez:0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
|
|
badTx.ToEthAddr = ðAddr
|
|
badTx.ToIdx = nil
|
|
jsonTxBytes, err = json.Marshal(badTx)
|
|
require.NoError(t, err)
|
|
jsonTxReader = bytes.NewReader(jsonTxBytes)
|
|
err = doBadReq("POST", endpoint, jsonTxReader, 400)
|
|
require.NoError(t, err)
|
|
// Wrong rq
|
|
badTx = tc.poolTxsToSend[0]
|
|
rqFromIdx := "hez:foo:30"
|
|
badTx.RqFromIdx = &rqFromIdx
|
|
jsonTxBytes, err = json.Marshal(badTx)
|
|
require.NoError(t, err)
|
|
jsonTxReader = bytes.NewReader(jsonTxBytes)
|
|
err = doBadReq("POST", endpoint, jsonTxReader, 400)
|
|
require.NoError(t, err)
|
|
// GET
|
|
// get by idx
|
|
fetchedTxs := testPoolTxsResponse{}
|
|
require.NoError(t, doGoodReq(
|
|
"GET",
|
|
endpoint+"?fromAccountIndex=hez:ETH:263",
|
|
nil, &fetchedTxs))
|
|
assert.Equal(t, 1, len(fetchedTxs.Txs))
|
|
assert.Equal(t, "hez:ETH:263", fetchedTxs.Txs[0].FromIdx)
|
|
// get by state
|
|
require.NoError(t, doGoodReq(
|
|
"GET",
|
|
endpoint+"?state=pend",
|
|
nil, &fetchedTxs))
|
|
assert.Equal(t, 4, len(fetchedTxs.Txs))
|
|
for _, v := range fetchedTxs.Txs {
|
|
assert.Equal(t, common.PoolL2TxStatePending, v.State)
|
|
}
|
|
|
|
// GET
|
|
endpoint += "/"
|
|
for _, tx := range tc.poolTxsToReceive {
|
|
fetchedTx := testPoolTxReceive{}
|
|
require.NoError(
|
|
t, doGoodReq(
|
|
"GET",
|
|
endpoint+tx.TxID.String(),
|
|
nil, &fetchedTx,
|
|
),
|
|
)
|
|
assertPoolTx(t, tx, fetchedTx)
|
|
}
|
|
// 400, due invalid TxID
|
|
err = doBadReq("GET", endpoint+"0xG2241b6f2b1dd772dba391f4a1a3407c7c21f598d86e2585a14e616fb4a255f823", nil, 400)
|
|
require.NoError(t, err)
|
|
// 404, due nonexistent TxID in DB
|
|
err = doBadReq("GET", endpoint+"0x02241b6f2b1dd772dba391f4a1a3407c7c21f598d86e2585a14e616fb4a255f823", nil, 404)
|
|
require.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)
|
|
}
|
|
|
|
// TestAllTosNull test that the API doesn't accept txs with all the TOs set to null (to eth, to bjj, to idx)
|
|
func TestAllTosNull(t *testing.T) {
|
|
// Generate account:
|
|
// Ethereum private key
|
|
var key ecdsa.PrivateKey
|
|
key.D = big.NewInt(int64(4444)) // only for testing
|
|
key.PublicKey.X, key.PublicKey.Y = ethCrypto.S256().ScalarBaseMult(key.D.Bytes())
|
|
key.Curve = ethCrypto.S256()
|
|
addr := ethCrypto.PubkeyToAddress(key.PublicKey)
|
|
// BJJ private key
|
|
var sk babyjub.PrivateKey
|
|
var iBytes [8]byte
|
|
binary.LittleEndian.PutUint64(iBytes[:], 4444)
|
|
copy(sk[:], iBytes[:]) // only for testing
|
|
account := common.Account{
|
|
Idx: 4444,
|
|
TokenID: 0,
|
|
BatchNum: 1,
|
|
BJJ: sk.Public().Compress(),
|
|
EthAddr: addr,
|
|
Nonce: 0,
|
|
Balance: big.NewInt(1000000),
|
|
}
|
|
// Add account to history DB (required to verify signature)
|
|
err := api.h.AddAccounts([]common.Account{account})
|
|
assert.NoError(t, err)
|
|
// Genrate tx with all tos set to nil (to eth, to bjj, to idx)
|
|
tx := common.PoolL2Tx{
|
|
FromIdx: account.Idx,
|
|
TokenID: account.TokenID,
|
|
Amount: big.NewInt(1000),
|
|
Fee: 200,
|
|
Nonce: 0,
|
|
}
|
|
// Set idx and type manually, and check that the function doesn't allow it
|
|
_, err = common.NewPoolL2Tx(&tx)
|
|
assert.Error(t, err)
|
|
tx.Type = common.TxTypeTransfer
|
|
var txID common.TxID
|
|
txIDRaw, err := hex.DecodeString("02e66e24f7f25272906647c8fd1d7fe8acf3cf3e9b38ffc9f94bbb5090dc275073")
|
|
assert.NoError(t, err)
|
|
copy(txID[:], txIDRaw)
|
|
tx.TxID = txID
|
|
// Sign tx
|
|
toSign, err := tx.HashToSign(0)
|
|
assert.NoError(t, err)
|
|
sig := sk.SignPoseidon(toSign)
|
|
tx.Signature = sig.Compress()
|
|
// Transform common.PoolL2Tx ==> testPoolTxSend
|
|
txToSend := testPoolTxSend{
|
|
TxID: tx.TxID,
|
|
Type: tx.Type,
|
|
TokenID: tx.TokenID,
|
|
FromIdx: idxToHez(tx.FromIdx, "ETH"),
|
|
Amount: tx.Amount.String(),
|
|
Fee: tx.Fee,
|
|
Nonce: tx.Nonce,
|
|
Signature: tx.Signature,
|
|
}
|
|
// Send tx to the API
|
|
jsonTxBytes, err := json.Marshal(txToSend)
|
|
require.NoError(t, err)
|
|
jsonTxReader := bytes.NewReader(jsonTxBytes)
|
|
err = doBadReq("POST", apiURL+"transactions-pool", jsonTxReader, 400)
|
|
require.NoError(t, err)
|
|
// Clean historyDB: the added account shouldn't be there for other tests
|
|
_, err = api.h.DB().DB.Exec("delete from account where idx = 4444")
|
|
assert.NoError(t, err)
|
|
}
|