2018-11-07 21:50:43 +00:00
|
|
|
// VulcanizeDB
|
2019-03-12 15:46:42 +00:00
|
|
|
// Copyright © 2019 Vulcanize
|
2018-11-07 21:50:43 +00:00
|
|
|
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
2018-02-13 16:31:57 +00:00
|
|
|
package repositories
|
2018-02-02 21:53:16 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"database/sql"
|
2018-05-02 16:17:02 +00:00
|
|
|
"errors"
|
2018-02-02 21:53:16 +00:00
|
|
|
"github.com/jmoiron/sqlx"
|
2019-08-06 21:57:08 +00:00
|
|
|
"github.com/sirupsen/logrus"
|
2018-02-02 21:53:16 +00:00
|
|
|
"github.com/vulcanize/vulcanizedb/pkg/core"
|
2018-02-13 16:31:57 +00:00
|
|
|
"github.com/vulcanize/vulcanizedb/pkg/datastore"
|
|
|
|
"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres"
|
2018-02-02 21:53:16 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
blocksFromHeadBeforeFinal = 20
|
|
|
|
)
|
|
|
|
|
2018-05-02 16:17:02 +00:00
|
|
|
var ErrBlockExists = errors.New("Won't add block that already exists.")
|
|
|
|
|
2018-02-12 16:54:05 +00:00
|
|
|
type BlockRepository struct {
|
2018-05-07 15:41:02 +00:00
|
|
|
database *postgres.DB
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewBlockRepository(database *postgres.DB) *BlockRepository {
|
|
|
|
return &BlockRepository{database: database}
|
2018-02-12 16:54:05 +00:00
|
|
|
}
|
|
|
|
|
2019-02-14 15:03:57 +00:00
|
|
|
func (blockRepository BlockRepository) SetBlocksStatus(chainHead int64) error {
|
2018-02-02 21:53:16 +00:00
|
|
|
cutoff := chainHead - blocksFromHeadBeforeFinal
|
2019-02-14 15:03:57 +00:00
|
|
|
_, err := blockRepository.database.Exec(`
|
2018-02-02 21:53:16 +00:00
|
|
|
UPDATE blocks SET is_final = TRUE
|
2018-02-02 22:12:14 +00:00
|
|
|
WHERE is_final = FALSE AND number < $1`,
|
2018-02-02 21:53:16 +00:00
|
|
|
cutoff)
|
2019-02-20 11:01:19 +00:00
|
|
|
|
|
|
|
return err
|
2018-02-02 21:53:16 +00:00
|
|
|
}
|
|
|
|
|
2018-05-02 16:17:02 +00:00
|
|
|
func (blockRepository BlockRepository) CreateOrUpdateBlock(block core.Block) (int64, error) {
|
2018-02-02 21:53:16 +00:00
|
|
|
var err error
|
2019-10-18 16:16:19 +00:00
|
|
|
var blockID int64
|
2018-02-12 16:54:05 +00:00
|
|
|
retrievedBlockHash, ok := blockRepository.getBlockHash(block)
|
2018-02-02 21:53:16 +00:00
|
|
|
if !ok {
|
2018-05-02 16:17:02 +00:00
|
|
|
return blockRepository.insertBlock(block)
|
2018-02-02 21:53:16 +00:00
|
|
|
}
|
|
|
|
if ok && retrievedBlockHash != block.Hash {
|
2018-02-12 16:54:05 +00:00
|
|
|
err = blockRepository.removeBlock(block.Number)
|
2018-02-02 21:53:16 +00:00
|
|
|
if err != nil {
|
2018-05-02 16:17:02 +00:00
|
|
|
return 0, err
|
2018-02-02 21:53:16 +00:00
|
|
|
}
|
2018-05-02 16:17:02 +00:00
|
|
|
return blockRepository.insertBlock(block)
|
2018-02-02 21:53:16 +00:00
|
|
|
}
|
2019-10-18 16:16:19 +00:00
|
|
|
return blockID, ErrBlockExists
|
2018-02-02 21:53:16 +00:00
|
|
|
}
|
|
|
|
|
2019-10-18 16:16:19 +00:00
|
|
|
func (blockRepository BlockRepository) MissingBlockNumbers(startingBlockNumber int64, highestBlockNumber int64, nodeID string) []int64 {
|
2018-02-02 21:53:16 +00:00
|
|
|
numbers := make([]int64, 0)
|
2019-02-14 15:03:57 +00:00
|
|
|
err := blockRepository.database.Select(&numbers,
|
2018-02-02 21:53:16 +00:00
|
|
|
`SELECT all_block_numbers
|
2018-06-08 16:26:25 +00:00
|
|
|
FROM (
|
|
|
|
SELECT generate_series($1::INT, $2::INT) AS all_block_numbers) series
|
|
|
|
WHERE all_block_numbers NOT IN (
|
|
|
|
SELECT number FROM blocks WHERE eth_node_fingerprint = $3
|
|
|
|
) `,
|
2018-02-02 21:53:16 +00:00
|
|
|
startingBlockNumber,
|
2019-10-18 16:16:19 +00:00
|
|
|
highestBlockNumber, nodeID)
|
2019-02-14 15:03:57 +00:00
|
|
|
if err != nil {
|
2019-08-06 21:57:08 +00:00
|
|
|
logrus.Error("MissingBlockNumbers: error getting blocks: ", err)
|
2019-02-14 15:03:57 +00:00
|
|
|
}
|
2018-02-02 21:53:16 +00:00
|
|
|
return numbers
|
|
|
|
}
|
|
|
|
|
2018-02-12 16:54:05 +00:00
|
|
|
func (blockRepository BlockRepository) GetBlock(blockNumber int64) (core.Block, error) {
|
2018-05-07 15:41:02 +00:00
|
|
|
blockRows := blockRepository.database.QueryRowx(
|
2018-02-02 21:53:16 +00:00
|
|
|
`SELECT id,
|
2018-02-02 22:12:14 +00:00
|
|
|
number,
|
2019-04-04 20:21:39 +00:00
|
|
|
gas_limit,
|
|
|
|
gas_used,
|
2018-02-02 22:12:14 +00:00
|
|
|
time,
|
|
|
|
difficulty,
|
|
|
|
hash,
|
|
|
|
nonce,
|
2019-04-04 20:21:39 +00:00
|
|
|
parent_hash,
|
2018-02-02 22:12:14 +00:00
|
|
|
size,
|
2018-02-02 21:53:16 +00:00
|
|
|
uncle_hash,
|
|
|
|
is_final,
|
2018-02-02 22:12:14 +00:00
|
|
|
miner,
|
|
|
|
extra_data,
|
|
|
|
reward,
|
|
|
|
uncles_reward
|
2018-02-02 21:53:16 +00:00
|
|
|
FROM blocks
|
2018-05-07 15:41:02 +00:00
|
|
|
WHERE eth_node_id = $1 AND number = $2`, blockRepository.database.NodeID, blockNumber)
|
2018-02-12 16:54:05 +00:00
|
|
|
savedBlock, err := blockRepository.loadBlock(blockRows)
|
2018-02-02 21:53:16 +00:00
|
|
|
if err != nil {
|
|
|
|
switch err {
|
|
|
|
case sql.ErrNoRows:
|
2018-02-13 16:31:57 +00:00
|
|
|
return core.Block{}, datastore.ErrBlockDoesNotExist(blockNumber)
|
2018-02-02 21:53:16 +00:00
|
|
|
default:
|
2019-08-06 21:57:08 +00:00
|
|
|
logrus.Error("GetBlock: error loading blocks: ", err)
|
2018-02-02 21:53:16 +00:00
|
|
|
return savedBlock, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return savedBlock, nil
|
|
|
|
}
|
|
|
|
|
2018-05-02 16:17:02 +00:00
|
|
|
func (blockRepository BlockRepository) insertBlock(block core.Block) (int64, error) {
|
2019-10-18 16:16:19 +00:00
|
|
|
var blockID int64
|
2019-03-19 19:11:26 +00:00
|
|
|
tx, beginErr := blockRepository.database.Beginx()
|
|
|
|
if beginErr != nil {
|
|
|
|
return 0, postgres.ErrBeginTransactionFailed(beginErr)
|
|
|
|
}
|
|
|
|
insertBlockErr := tx.QueryRow(
|
2018-02-02 21:53:16 +00:00
|
|
|
`INSERT INTO blocks
|
2019-04-04 20:21:39 +00:00
|
|
|
(eth_node_id, number, gas_limit, gas_used, time, difficulty, hash, nonce, parent_hash, size, uncle_hash, is_final, miner, extra_data, reward, uncles_reward, eth_node_fingerprint)
|
2018-05-07 15:41:02 +00:00
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
2018-02-02 21:53:16 +00:00
|
|
|
RETURNING id `,
|
2019-03-22 03:43:06 +00:00
|
|
|
blockRepository.database.NodeID,
|
|
|
|
block.Number,
|
|
|
|
block.GasLimit,
|
|
|
|
block.GasUsed,
|
|
|
|
block.Time,
|
|
|
|
block.Difficulty,
|
|
|
|
block.Hash,
|
|
|
|
block.Nonce,
|
|
|
|
block.ParentHash,
|
|
|
|
block.Size,
|
|
|
|
block.UncleHash,
|
|
|
|
block.IsFinal,
|
|
|
|
block.Miner,
|
|
|
|
block.ExtraData,
|
2019-03-21 19:52:24 +00:00
|
|
|
nullStringToZero(block.Reward),
|
|
|
|
nullStringToZero(block.UnclesReward),
|
2019-03-22 03:43:06 +00:00
|
|
|
blockRepository.database.Node.ID).
|
2019-10-18 16:16:19 +00:00
|
|
|
Scan(&blockID)
|
2019-03-19 19:11:26 +00:00
|
|
|
if insertBlockErr != nil {
|
|
|
|
rollbackErr := tx.Rollback()
|
|
|
|
if rollbackErr != nil {
|
2019-08-06 21:57:08 +00:00
|
|
|
logrus.Error("failed to rollback transaction: ", rollbackErr)
|
2019-03-19 19:11:26 +00:00
|
|
|
}
|
|
|
|
return 0, postgres.ErrDBInsertFailed(insertBlockErr)
|
2018-02-02 21:53:16 +00:00
|
|
|
}
|
2019-04-02 18:30:15 +00:00
|
|
|
if len(block.Uncles) > 0 {
|
2019-10-18 16:16:19 +00:00
|
|
|
insertUncleErr := blockRepository.createUncles(tx, blockID, block.Hash, block.Uncles)
|
2019-03-21 19:52:24 +00:00
|
|
|
if insertUncleErr != nil {
|
|
|
|
tx.Rollback()
|
|
|
|
return 0, postgres.ErrDBInsertFailed(insertUncleErr)
|
|
|
|
}
|
|
|
|
}
|
2018-05-02 16:17:02 +00:00
|
|
|
if len(block.Transactions) > 0 {
|
2019-10-18 16:16:19 +00:00
|
|
|
insertTxErr := blockRepository.createTransactions(tx, blockID, block.Transactions)
|
2019-03-19 19:11:26 +00:00
|
|
|
if insertTxErr != nil {
|
|
|
|
rollbackErr := tx.Rollback()
|
|
|
|
if rollbackErr != nil {
|
2019-08-06 21:57:08 +00:00
|
|
|
logrus.Warn("failed to rollback transaction: ", rollbackErr)
|
2019-03-19 19:11:26 +00:00
|
|
|
}
|
|
|
|
return 0, postgres.ErrDBInsertFailed(insertTxErr)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
commitErr := tx.Commit()
|
|
|
|
if commitErr != nil {
|
|
|
|
rollbackErr := tx.Rollback()
|
|
|
|
if rollbackErr != nil {
|
2019-08-06 21:57:08 +00:00
|
|
|
logrus.Warn("failed to rollback transaction: ", rollbackErr)
|
2018-05-02 16:17:02 +00:00
|
|
|
}
|
2019-03-19 19:11:26 +00:00
|
|
|
return 0, commitErr
|
2018-02-02 21:53:16 +00:00
|
|
|
}
|
2019-10-18 16:16:19 +00:00
|
|
|
return blockID, nil
|
2018-02-02 21:53:16 +00:00
|
|
|
}
|
|
|
|
|
2019-10-18 16:16:19 +00:00
|
|
|
func (blockRepository BlockRepository) createUncles(tx *sqlx.Tx, blockID int64, blockHash string, uncles []core.Uncle) error {
|
2019-04-02 18:30:15 +00:00
|
|
|
for _, uncle := range uncles {
|
2019-10-18 16:16:19 +00:00
|
|
|
err := blockRepository.createUncle(tx, blockID, uncle)
|
2019-04-02 18:30:15 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2019-03-21 19:52:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-10-18 16:16:19 +00:00
|
|
|
func (blockRepository BlockRepository) createUncle(tx *sqlx.Tx, blockID int64, uncle core.Uncle) error {
|
2019-03-21 19:52:24 +00:00
|
|
|
_, err := tx.Exec(
|
2019-04-02 18:30:15 +00:00
|
|
|
`INSERT INTO uncles
|
2019-04-04 20:21:39 +00:00
|
|
|
(hash, block_id, reward, miner, raw, block_timestamp, eth_node_id, eth_node_fingerprint)
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7::NUMERIC, $8)
|
2019-03-21 19:52:24 +00:00
|
|
|
RETURNING id`,
|
2019-10-18 16:16:19 +00:00
|
|
|
uncle.Hash, blockID, nullStringToZero(uncle.Reward), uncle.Miner, uncle.Raw, uncle.Timestamp, blockRepository.database.NodeID, blockRepository.database.Node.ID)
|
2019-03-21 19:52:24 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-10-18 16:16:19 +00:00
|
|
|
func (blockRepository BlockRepository) createTransactions(tx *sqlx.Tx, blockID int64, transactions []core.TransactionModel) error {
|
2018-02-02 21:53:16 +00:00
|
|
|
for _, transaction := range transactions {
|
2019-10-18 16:16:19 +00:00
|
|
|
err := blockRepository.createTransaction(tx, blockID, transaction)
|
2018-02-02 21:53:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
//Fields like value lose precision if converted to
|
|
|
|
//int64 so convert to string instead. But nil
|
|
|
|
//big.Int -> string = "" so convert to "0"
|
|
|
|
func nullStringToZero(s string) string {
|
|
|
|
if s == "" {
|
|
|
|
return "0"
|
|
|
|
}
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
2019-10-18 16:16:19 +00:00
|
|
|
func (blockRepository BlockRepository) createTransaction(tx *sqlx.Tx, blockID int64, transaction core.TransactionModel) error {
|
2018-05-01 16:35:01 +00:00
|
|
|
_, err := tx.Exec(
|
2019-03-19 19:11:26 +00:00
|
|
|
`INSERT INTO full_sync_transactions
|
2019-04-04 20:21:39 +00:00
|
|
|
(block_id, gas_limit, gas_price, hash, input_data, nonce, raw, tx_from, tx_index, tx_to, "value")
|
2019-03-19 19:11:26 +00:00
|
|
|
VALUES ($1, $2::NUMERIC, $3::NUMERIC, $4, $5, $6::NUMERIC, $7, $8, $9::NUMERIC, $10, $11::NUMERIC)
|
2019-10-18 16:16:19 +00:00
|
|
|
RETURNING id`, blockID, transaction.GasLimit, transaction.GasPrice, transaction.Hash, transaction.Data,
|
2019-03-22 03:43:06 +00:00
|
|
|
transaction.Nonce, transaction.Raw, transaction.From, transaction.TxIndex, transaction.To, nullStringToZero(transaction.Value))
|
2018-02-02 21:53:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if hasReceipt(transaction) {
|
2019-08-05 15:38:37 +00:00
|
|
|
receiptRepo := FullSyncReceiptRepository{}
|
2019-10-18 16:16:19 +00:00
|
|
|
receiptID, err := receiptRepo.CreateFullSyncReceiptInTx(blockID, transaction.Receipt, tx)
|
2018-02-02 21:53:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if hasLogs(transaction) {
|
2019-10-18 16:16:19 +00:00
|
|
|
err = blockRepository.createLogs(tx, transaction.Receipt.Logs, receiptID)
|
2018-02-02 21:53:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-03-27 04:05:30 +00:00
|
|
|
func hasLogs(transaction core.TransactionModel) bool {
|
2018-02-02 21:53:16 +00:00
|
|
|
return len(transaction.Receipt.Logs) > 0
|
|
|
|
}
|
|
|
|
|
2019-03-27 04:05:30 +00:00
|
|
|
func hasReceipt(transaction core.TransactionModel) bool {
|
2018-02-02 21:53:16 +00:00
|
|
|
return transaction.Receipt.TxHash != ""
|
|
|
|
}
|
|
|
|
|
2018-02-12 16:54:05 +00:00
|
|
|
func (blockRepository BlockRepository) getBlockHash(block core.Block) (string, bool) {
|
2018-02-02 21:53:16 +00:00
|
|
|
var retrievedBlockHash string
|
2019-03-19 19:11:26 +00:00
|
|
|
// TODO: handle possible error
|
2018-05-07 15:41:02 +00:00
|
|
|
blockRepository.database.Get(&retrievedBlockHash,
|
2018-02-02 22:12:14 +00:00
|
|
|
`SELECT hash
|
2018-02-02 21:53:16 +00:00
|
|
|
FROM blocks
|
2018-03-21 14:53:20 +00:00
|
|
|
WHERE number = $1 AND eth_node_id = $2`,
|
2018-05-07 15:41:02 +00:00
|
|
|
block.Number, blockRepository.database.NodeID)
|
2018-02-02 21:53:16 +00:00
|
|
|
return retrievedBlockHash, blockExists(retrievedBlockHash)
|
|
|
|
}
|
|
|
|
|
2019-10-18 16:16:19 +00:00
|
|
|
func (blockRepository BlockRepository) createLogs(tx *sqlx.Tx, logs []core.FullSyncLog, receiptID int64) error {
|
2018-02-02 21:53:16 +00:00
|
|
|
for _, tlog := range logs {
|
|
|
|
_, err := tx.Exec(
|
2019-07-18 21:24:25 +00:00
|
|
|
`INSERT INTO full_sync_logs (block_number, address, tx_hash, index, topic0, topic1, topic2, topic3, data, receipt_id)
|
2018-02-02 21:53:16 +00:00
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
|
|
`,
|
2019-10-18 16:16:19 +00:00
|
|
|
tlog.BlockNumber, tlog.Address, tlog.TxHash, tlog.Index, tlog.Topics[0], tlog.Topics[1], tlog.Topics[2], tlog.Topics[3], tlog.Data, receiptID,
|
2018-02-02 21:53:16 +00:00
|
|
|
)
|
|
|
|
if err != nil {
|
2019-03-19 19:11:26 +00:00
|
|
|
return postgres.ErrDBInsertFailed(err)
|
2018-02-02 21:53:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func blockExists(retrievedBlockHash string) bool {
|
|
|
|
return retrievedBlockHash != ""
|
|
|
|
}
|
|
|
|
|
2018-02-12 16:54:05 +00:00
|
|
|
func (blockRepository BlockRepository) removeBlock(blockNumber int64) error {
|
2018-05-07 15:41:02 +00:00
|
|
|
_, err := blockRepository.database.Exec(
|
2019-03-19 19:11:26 +00:00
|
|
|
`DELETE FROM blocks WHERE number=$1 AND eth_node_id=$2`,
|
2018-05-07 15:41:02 +00:00
|
|
|
blockNumber, blockRepository.database.NodeID)
|
2018-02-02 21:53:16 +00:00
|
|
|
if err != nil {
|
2019-03-19 19:11:26 +00:00
|
|
|
return postgres.ErrDBDeleteFailed(err)
|
2018-02-02 21:53:16 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-02-12 16:54:05 +00:00
|
|
|
func (blockRepository BlockRepository) loadBlock(blockRows *sqlx.Row) (core.Block, error) {
|
2018-02-02 21:53:16 +00:00
|
|
|
type b struct {
|
|
|
|
ID int
|
|
|
|
core.Block
|
|
|
|
}
|
|
|
|
var block b
|
|
|
|
err := blockRows.StructScan(&block)
|
|
|
|
if err != nil {
|
2019-08-06 21:57:08 +00:00
|
|
|
logrus.Error("loadBlock: error loading block: ", err)
|
2018-02-02 21:53:16 +00:00
|
|
|
return core.Block{}, err
|
|
|
|
}
|
2018-05-07 15:41:02 +00:00
|
|
|
transactionRows, err := blockRepository.database.Queryx(`
|
2019-03-19 19:11:26 +00:00
|
|
|
SELECT hash,
|
2019-04-04 20:21:39 +00:00
|
|
|
gas_limit,
|
|
|
|
gas_price,
|
2019-03-19 19:11:26 +00:00
|
|
|
input_data,
|
|
|
|
nonce,
|
|
|
|
raw,
|
|
|
|
tx_from,
|
|
|
|
tx_index,
|
|
|
|
tx_to,
|
|
|
|
value
|
|
|
|
FROM full_sync_transactions
|
|
|
|
WHERE block_id = $1
|
|
|
|
ORDER BY hash`, block.ID)
|
2018-02-02 21:53:16 +00:00
|
|
|
if err != nil {
|
2019-08-06 21:57:08 +00:00
|
|
|
logrus.Error("loadBlock: error fetting transactions: ", err)
|
2018-02-02 21:53:16 +00:00
|
|
|
return core.Block{}, err
|
|
|
|
}
|
2018-02-12 16:54:05 +00:00
|
|
|
block.Transactions = blockRepository.LoadTransactions(transactionRows)
|
2018-02-02 21:53:16 +00:00
|
|
|
return block.Block, nil
|
|
|
|
}
|
|
|
|
|
2019-03-27 04:05:30 +00:00
|
|
|
func (blockRepository BlockRepository) LoadTransactions(transactionRows *sqlx.Rows) []core.TransactionModel {
|
|
|
|
var transactions []core.TransactionModel
|
2018-02-02 21:53:16 +00:00
|
|
|
for transactionRows.Next() {
|
2019-03-27 04:05:30 +00:00
|
|
|
var transaction core.TransactionModel
|
2018-02-02 21:53:16 +00:00
|
|
|
err := transactionRows.StructScan(&transaction)
|
|
|
|
if err != nil {
|
2019-08-06 21:57:08 +00:00
|
|
|
logrus.Fatal(err)
|
2018-02-02 21:53:16 +00:00
|
|
|
}
|
|
|
|
transactions = append(transactions, transaction)
|
|
|
|
}
|
|
|
|
return transactions
|
|
|
|
}
|