Merge pull request #179 from hermeznetwork/feature/api-exits

Impl exit edpoint and refactor pagination
This commit is contained in:
arnau
2020-10-14 11:16:03 +02:00
committed by GitHub
13 changed files with 1387 additions and 584 deletions

View File

@@ -18,6 +18,13 @@ import (
"github.com/russross/meddler"
)
const (
// OrderAsc indicates ascending order when using pagination
OrderAsc = "ASC"
// OrderDesc indicates descending order when using pagination
OrderDesc = "DESC"
)
// TODO(Edu): Document here how HistoryDB is kept consistent
// HistoryDB persist the historic of the rollup
@@ -445,25 +452,47 @@ func (hdb *HistoryDB) addTxs(d meddler.DB, txs []txWrite) error {
// return db.SlicePtrsToSlice(txs).([]common.Tx), err
// }
// GetHistoryTx returns a tx from the DB given a TxID
func (hdb *HistoryDB) GetHistoryTx(txID common.TxID) (*HistoryTx, error) {
tx := &HistoryTx{}
err := meddler.QueryRow(
hdb.db, tx, `SELECT tx.item_id, tx.is_l1, tx.id, tx.type, tx.position,
tx.from_idx, tx.to_idx, tx.amount, tx.token_id, tx.amount_usd,
tx.batch_num, tx.eth_block_num, tx.to_forge_l1_txs_num, tx.user_origin,
tx.from_eth_addr, tx.from_bjj, tx.load_amount,
tx.load_amount_usd, tx.fee, tx.fee_usd, tx.nonce,
token.token_id, token.eth_block_num AS token_block,
token.eth_addr, token.name, token.symbol, token.decimals, token.usd,
token.usd_update, block.timestamp
FROM tx INNER JOIN token ON tx.token_id = token.token_id
INNER JOIN block ON tx.eth_block_num = block.eth_block_num
WHERE tx.id = $1;`, txID,
)
return tx, err
}
// GetHistoryTxs returns a list of txs from the DB using the HistoryTx struct
// and pagination info
func (hdb *HistoryDB) GetHistoryTxs(
ethAddr *ethCommon.Address, bjj *babyjub.PublicKey,
tokenID, idx, batchNum *uint, txType *common.TxType,
offset, limit *uint, last bool,
) ([]HistoryTx, int, error) {
tokenID *common.TokenID, idx *common.Idx, batchNum *uint, txType *common.TxType,
fromItem, limit *uint, order string,
) ([]HistoryTx, *db.Pagination, error) {
if ethAddr != nil && bjj != nil {
return nil, 0, errors.New("ethAddr and bjj are incompatible")
return nil, nil, errors.New("ethAddr and bjj are incompatible")
}
var query string
var args []interface{}
queryStr := `SELECT tx.is_l1, tx.id, tx.type, tx.position, tx.from_idx, tx.to_idx,
tx.amount, tx.token_id, tx.batch_num, tx.eth_block_num, tx.to_forge_l1_txs_num,
tx.user_origin, tx.from_eth_addr, tx.from_bjj, tx.load_amount, tx.fee, tx.nonce,
queryStr := `SELECT tx.item_id, tx.is_l1, tx.id, tx.type, tx.position,
tx.from_idx, tx.to_idx, tx.amount, tx.token_id, tx.amount_usd,
tx.batch_num, tx.eth_block_num, tx.to_forge_l1_txs_num, tx.user_origin,
tx.from_eth_addr, tx.from_bjj, tx.load_amount,
tx.load_amount_usd, tx.fee, tx.fee_usd, tx.nonce,
token.token_id, token.eth_block_num AS token_block,
token.eth_addr, token.name, token.symbol, token.decimals, token.usd,
token.usd_update, block.timestamp, count(*) OVER() AS total_items
FROM tx
INNER JOIN token ON tx.token_id = token.token_id
token.usd_update, block.timestamp, count(*) OVER() AS total_items,
MIN(tx.item_id) OVER() AS first_item, MAX(tx.item_id) OVER() AS last_item
FROM tx INNER JOIN token ON tx.token_id = token.token_id
INNER JOIN block ON tx.eth_block_num = block.eth_block_num `
// Apply filters
nextIsAnd := false
@@ -523,33 +552,164 @@ func (hdb *HistoryDB) GetHistoryTxs(
}
queryStr += "tx.type = ? "
args = append(args, txType)
nextIsAnd = true
}
if fromItem != nil {
if nextIsAnd {
queryStr += "AND "
} else {
queryStr += "WHERE "
}
if order == OrderAsc {
queryStr += "tx.item_id >= ? "
} else {
queryStr += "tx.item_id <= ? "
}
args = append(args, fromItem)
nextIsAnd = true
}
if nextIsAnd {
queryStr += "AND "
} else {
queryStr += "WHERE "
}
queryStr += "tx.batch_num IS NOT NULL "
// pagination
queryStr += "ORDER BY tx.item_id "
if order == OrderAsc {
queryStr += " ASC "
} else {
queryStr += " DESC "
}
queryStr += fmt.Sprintf("LIMIT %d;", *limit)
query = hdb.db.Rebind(queryStr)
log.Debug(query)
txsPtrs := []*HistoryTx{}
if err := meddler.QueryAll(hdb.db, &txsPtrs, query, args...); err != nil {
return nil, nil, err
}
txs := db.SlicePtrsToSlice(txsPtrs).([]HistoryTx)
if len(txs) == 0 {
return nil, nil, sql.ErrNoRows
}
return txs, &db.Pagination{
TotalItems: txs[0].TotalItems,
FirstItem: txs[0].FirstItem,
LastItem: txs[0].LastItem,
}, nil
}
// GetExit returns a exit from the DB
func (hdb *HistoryDB) GetExit(batchNum *uint, idx *common.Idx) (*HistoryExit, error) {
exit := &HistoryExit{}
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
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,
)
return exit, err
}
// GetExits returns a list of exits from the DB and pagination info
func (hdb *HistoryDB) GetExits(
ethAddr *ethCommon.Address, bjj *babyjub.PublicKey,
tokenID *common.TokenID, idx *common.Idx, batchNum *uint,
fromItem, limit *uint, order string,
) ([]HistoryExit, *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
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
nextIsAnd := false
// ethAddr filter
if ethAddr != nil {
queryStr += "WHERE account.eth_addr = ? "
nextIsAnd = true
args = append(args, ethAddr)
} else if bjj != nil { // bjj filter
queryStr += "WHERE account.bjj = ? "
nextIsAnd = true
args = append(args, bjj)
}
// tokenID filter
if tokenID != nil {
if nextIsAnd {
queryStr += "AND "
} else {
queryStr += "WHERE "
}
queryStr += "account.token_id = ? "
args = append(args, tokenID)
nextIsAnd = true
}
// idx filter
if idx != nil {
if nextIsAnd {
queryStr += "AND "
} else {
queryStr += "WHERE "
}
queryStr += "exit_tree.account_idx = ? "
args = append(args, idx)
nextIsAnd = true
}
// batchNum filter
if batchNum != nil {
if nextIsAnd {
queryStr += "AND "
} else {
queryStr += "WHERE "
}
queryStr += "exit_tree.batch_num = ? "
args = append(args, batchNum)
nextIsAnd = true
}
if fromItem != nil {
if nextIsAnd {
queryStr += "AND "
} else {
queryStr += "WHERE "
}
if order == OrderAsc {
queryStr += "exit_tree.item_id >= ? "
} else {
queryStr += "exit_tree.item_id <= ? "
}
args = append(args, fromItem)
// nextIsAnd = true
}
// pagination
if last {
queryStr += "ORDER BY (batch_num, position) DESC NULLS FIRST "
queryStr += "ORDER BY exit_tree.item_id "
if order == OrderAsc {
queryStr += " ASC "
} else {
queryStr += "ORDER BY (batch_num, position) ASC NULLS LAST "
queryStr += fmt.Sprintf("OFFSET %d ", *offset)
queryStr += " DESC "
}
queryStr += fmt.Sprintf("LIMIT %d;", *limit)
query = hdb.db.Rebind(queryStr)
// log.Debug(query)
txsPtrs := []*HistoryTx{}
if err := meddler.QueryAll(hdb.db, &txsPtrs, query, args...); err != nil {
return nil, 0, err
exits := []*HistoryExit{}
if err := meddler.QueryAll(hdb.db, &exits, query, args...); err != nil {
return nil, nil, err
}
txs := db.SlicePtrsToSlice(txsPtrs).([]HistoryTx)
if len(txs) == 0 {
return nil, 0, sql.ErrNoRows
} else if last {
tmp := []HistoryTx{}
for i := len(txs) - 1; i >= 0; i-- {
tmp = append(tmp, txs[i])
}
txs = tmp
if len(exits) == 0 {
return nil, nil, sql.ErrNoRows
}
return txs, txs[0].TotalItems, nil
return db.SlicePtrsToSlice(exits).([]HistoryExit), &db.Pagination{
TotalItems: exits[0].TotalItems,
FirstItem: exits[0].FirstItem,
LastItem: exits[0].LastItem,
}, nil
}
// // GetTx returns a tx from the DB

View File

@@ -213,6 +213,7 @@ func TestTxs(t *testing.T) {
/*
Uncomment once the transaction generation is fixed
!! test that batches that forge user L1s !!
!! Missing tests to check that historic USD is not set if USDUpdate is too old (24h) !!
// Generate fake L1 txs
@@ -333,9 +334,14 @@ func TestExitTree(t *testing.T) {
blocks := setTestBlocks(0, 10)
batches := test.GenBatches(nBatches, blocks)
err := historyDB.AddBatches(batches)
const nTokens = 50
tokens := test.GenTokens(nTokens, blocks)
assert.NoError(t, historyDB.AddTokens(tokens))
assert.NoError(t, err)
exitTree := test.GenExitTree(nBatches)
const nAccounts = 3
accs := test.GenAccounts(nAccounts, 0, tokens, nil, nil, batches)
assert.NoError(t, historyDB.AddAccounts(accs))
exitTree := test.GenExitTree(nBatches, batches, accs)
err = historyDB.AddExitTree(exitTree)
assert.NoError(t, err)
}

View File

@@ -7,6 +7,7 @@ import (
ethCommon "github.com/ethereum/go-ethereum/common"
"github.com/hermeznetwork/hermez-node/common"
"github.com/iden3/go-iden3-crypto/babyjub"
"github.com/iden3/go-merkletree"
)
// HistoryTx is a representation of a generic Tx with additional information
@@ -15,6 +16,7 @@ type HistoryTx struct {
// Generic
IsL1 bool `meddler:"is_l1"`
TxID common.TxID `meddler:"id"`
ItemID int `meddler:"item_id"`
Type common.TxType `meddler:"type"`
Position int `meddler:"position"`
FromIdx *common.Idx `meddler:"from_idx"`
@@ -38,6 +40,8 @@ type HistoryTx struct {
// API extras
Timestamp time.Time `meddler:"timestamp,utctime"`
TotalItems int `meddler:"total_items"`
FirstItem int `meddler:"first_item"`
LastItem int `meddler:"last_item"`
TokenID common.TokenID `meddler:"token_id"`
TokenEthBlockNum int64 `meddler:"token_block"`
TokenEthAddr ethCommon.Address `meddler:"eth_addr"`
@@ -86,3 +90,27 @@ type TokenRead struct {
USD *float64 `json:"USD" meddler:"usd"`
USDUpdate *time.Time `json:"fiatUpdate" meddler:"usd_update,utctime"`
}
// HistoryExit is a representation of a exit with additional information
// required by the API, and extracted by joining token table
type HistoryExit struct {
ItemID int `meddler:"item_id"`
BatchNum common.BatchNum `meddler:"batch_num"`
AccountIdx common.Idx `meddler:"account_idx"`
MerkleProof *merkletree.CircomVerifierProof `meddler:"merkle_proof,json"`
Balance *big.Int `meddler:"balance,bigint"`
InstantWithdrawn *int64 `meddler:"instant_withdrawn"`
DelayedWithdrawRequest *int64 `meddler:"delayed_withdraw_request"`
DelayedWithdrawn *int64 `meddler:"delayed_withdrawn"`
TotalItems int `meddler:"total_items"`
FirstItem int `meddler:"first_item"`
LastItem int `meddler:"last_item"`
TokenID common.TokenID `meddler:"token_id"`
TokenEthBlockNum int64 `meddler:"token_block"`
TokenEthAddr ethCommon.Address `meddler:"eth_addr"`
TokenName string `meddler:"name"`
TokenSymbol string `meddler:"symbol"`
TokenDecimals uint64 `meddler:"decimals"`
TokenUSD *float64 `meddler:"usd"`
TokenUSDUpdate *time.Time `meddler:"usd_update"`
}

View File

@@ -28,17 +28,6 @@ CREATE TABLE batch (
total_fees_usd NUMERIC
);
CREATE TABLE exit_tree (
batch_num BIGINT REFERENCES batch (batch_num) ON DELETE CASCADE,
account_idx BIGINT,
merkle_proof BYTEA NOT NULL,
balance BYTEA NOT NULL,
instant_withdrawn BIGINT REFERENCES batch (batch_num) ON DELETE SET NULL,
delayed_withdraw_request BIGINT REFERENCES batch (batch_num) ON DELETE SET NULL,
delayed_withdrawn BIGINT REFERENCES batch (batch_num) ON DELETE SET NULL,
PRIMARY KEY (batch_num, account_idx)
);
CREATE TABLE bid (
slot_num BIGINT NOT NULL,
bid_value BYTEA NOT NULL,
@@ -58,6 +47,25 @@ CREATE TABLE token (
usd_update TIMESTAMP WITHOUT TIME ZONE
);
CREATE TABLE account (
idx BIGINT PRIMARY KEY,
token_id INT NOT NULL REFERENCES token (token_id) ON DELETE CASCADE,
batch_num BIGINT NOT NULL REFERENCES batch (batch_num) ON DELETE CASCADE,
bjj BYTEA NOT NULL,
eth_addr BYTEA NOT NULL
);
CREATE TABLE exit_tree (
item_id SERIAL PRIMARY KEY,
batch_num BIGINT REFERENCES batch (batch_num) ON DELETE CASCADE,
account_idx BIGINT REFERENCES account (idx) ON DELETE CASCADE,
merkle_proof BYTEA NOT NULL,
balance BYTEA NOT NULL,
instant_withdrawn BIGINT REFERENCES batch (batch_num) ON DELETE SET NULL,
delayed_withdraw_request BIGINT REFERENCES batch (batch_num) ON DELETE SET NULL,
delayed_withdrawn BIGINT REFERENCES batch (batch_num) ON DELETE SET NULL
);
-- +migrate StatementBegin
CREATE FUNCTION set_token_usd_update()
RETURNS TRIGGER
@@ -75,10 +83,13 @@ LANGUAGE plpgsql;
CREATE TRIGGER trigger_token_usd_update BEFORE UPDATE OR INSERT ON token
FOR EACH ROW EXECUTE PROCEDURE set_token_usd_update();
CREATE SEQUENCE tx_item_id;
CREATE TABLE tx (
-- Generic TX
item_id INTEGER PRIMARY KEY DEFAULT nextval('tx_item_id'),
is_l1 BOOLEAN NOT NULL,
id BYTEA PRIMARY KEY,
id BYTEA,
type VARCHAR(40) NOT NULL,
position INT NOT NULL,
from_idx BIGINT,
@@ -103,8 +114,6 @@ CREATE TABLE tx (
nonce BIGINT
);
CREATE INDEX tx_order ON tx (batch_num, position);
-- +migrate StatementBegin
CREATE FUNCTION fee_percentage(NUMERIC)
RETURNS NUMERIC
@@ -412,9 +421,10 @@ BEGIN
usd / POWER(10, decimals), usd_update, timestamp FROM token INNER JOIN block on token.eth_block_num = block.eth_block_num WHERE token_id = NEW.token_id;
IF _tx_timestamp - interval '24 hours' < _usd_update AND _tx_timestamp + interval '24 hours' > _usd_update THEN
NEW."amount_usd" = (SELECT _value * NEW.amount_f);
NEW."load_amount_usd" = (SELECT _value * NEW.load_amount_f);
IF NOT NEW.is_l1 THEN
NEW."fee_usd" = (SELECT NEW."amount_usd" * fee_percentage(NEW.fee::NUMERIC));
ELSE
NEW."load_amount_usd" = (SELECT _value * NEW.load_amount_f);
END IF;
END IF;
RETURN NEW;
@@ -433,8 +443,13 @@ $BODY$
BEGIN
IF NEW.forge_l1_txs_num IS NOT NULL THEN
UPDATE tx
SET batch_num = NEW.batch_num
WHERE user_origin AND NEW.forge_l1_txs_num = to_forge_l1_txs_num;
SET item_id = nextval('tx_item_id'), batch_num = NEW.batch_num
WHERE id IN (
SELECT id FROM tx
WHERE user_origin AND NEW.forge_l1_txs_num = to_forge_l1_txs_num
ORDER BY position
FOR UPDATE
);
END IF;
RETURN NEW;
END;
@@ -444,14 +459,6 @@ LANGUAGE plpgsql;
CREATE TRIGGER trigger_forge_l1_txs AFTER INSERT ON batch
FOR EACH ROW EXECUTE PROCEDURE forge_l1_user_txs();
CREATE TABLE account (
idx BIGINT PRIMARY KEY,
token_id INT NOT NULL REFERENCES token (token_id) ON DELETE CASCADE,
batch_num BIGINT NOT NULL REFERENCES batch (batch_num) ON DELETE CASCADE,
bjj BYTEA NOT NULL,
eth_addr BYTEA NOT NULL
);
CREATE TABLE rollup_vars (
eth_block_num BIGINT PRIMARY KEY REFERENCES block (eth_block_num) ON DELETE CASCADE,
forge_l1_timeout BYTEA NOT NULL,

View File

@@ -177,3 +177,18 @@ func SlicePtrsToSlice(slice interface{}) interface{} {
}
return res.Interface()
}
// Pagination give information on the items of a query
type Pagination struct {
TotalItems int `json:"totalItems"`
FirstItem int `json:"firstItem"`
LastItem int `json:"lastItem"`
FirstReturnedItem int `json:"-"`
LastReturnedItem int `json:"-"`
}
// Paginationer is an interface that allows getting pagination info on any struct
type Paginationer interface {
GetPagination() *Pagination
Len() int
}