diff --git a/observers/blockchain_db_observer.go b/observers/blockchain_db_observer.go index 0c1d0fdb..911930ef 100644 --- a/observers/blockchain_db_observer.go +++ b/observers/blockchain_db_observer.go @@ -2,8 +2,8 @@ package observers import ( "github.com/8thlight/vulcanizedb/core" + "github.com/8thlight/vulcanizedb/repositories" "github.com/jmoiron/sqlx" - _ "github.com/lib/pq" ) type BlockchainDBObserver struct { @@ -11,25 +11,5 @@ type BlockchainDBObserver struct { } func (observer BlockchainDBObserver) NotifyBlockAdded(block core.Block) { - insertedBlockId := saveBlock(observer, block) - saveTransactions(insertedBlockId, block.Transactions, observer) -} - -func saveBlock(observer BlockchainDBObserver, block core.Block) int64 { - insertedBlock := observer.Db.QueryRow( - "Insert INTO blocks "+ - "(block_number, block_gaslimit, block_gasused, block_time, block_difficulty, block_hash, block_nonce, block_parenthash, block_size, uncle_hash) "+ - "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id", - block.Number, block.GasLimit, block.GasUsed, block.Time, block.Difficulty, block.Hash, block.Nonce, block.ParentHash, block.Size, block.UncleHash) - var blockId int64 - insertedBlock.Scan(&blockId) - return blockId -} - -func saveTransactions(blockId int64, transactions []core.Transaction, observer BlockchainDBObserver) { - for _, transaction := range transactions { - observer.Db.MustExec("Insert INTO transactions "+ - "(block_id, tx_hash, tx_nonce, tx_to, tx_gaslimit, tx_gasprice, tx_value) VALUES ($1, $2, $3, $4, $5, $6, $7)", - blockId, transaction.Hash, transaction.Nonce, transaction.To, transaction.GasLimit, transaction.GasPrice, transaction.Value) - } + repositories.NewPostgres(observer.Db).CreateBlock(block) } diff --git a/observers/blockchain_db_observer_test.go b/observers/blockchain_db_observer_test.go index bc977b88..4bbcec73 100644 --- a/observers/blockchain_db_observer_test.go +++ b/observers/blockchain_db_observer_test.go @@ -6,6 +6,7 @@ import ( "github.com/8thlight/vulcanizedb/config" "github.com/8thlight/vulcanizedb/core" "github.com/8thlight/vulcanizedb/observers" + "github.com/8thlight/vulcanizedb/repositories" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" . "github.com/onsi/ginkgo" @@ -38,180 +39,19 @@ var _ = Describe("Saving blocks to the database", func() { Expect(observer).NotTo(BeNil()) }) - It("connects to the database", func() { - Expect(err).Should(BeNil()) - Expect(db).ShouldNot(BeNil()) - }) - - It("starts with no blocks", func() { - var count int - queryError := db.Get(&count, "SELECT COUNT(*) FROM blocks") - Expect(queryError).Should(BeNil()) - Expect(count).Should(Equal(0)) - }) - - It("inserts a block", func() { - // setup a block in memory - blockNumber := int64(123) - gasLimit := int64(1000000) - gasUsed := int64(10) - blockHash := "x123" - blockParentHash := "x456" - blockNonce := "0x881db2ca900682e9a9" - blockTime := int64(1508981640) - uncleHash := "x789" - blockSize := int64(1000) - difficulty := int64(10) + It("saves a block with one transaction", func() { block := core.Block{ - Difficulty: difficulty, - GasLimit: gasLimit, - GasUsed: gasUsed, - Hash: blockHash, - Nonce: blockNonce, - Number: blockNumber, - ParentHash: blockParentHash, - Size: blockSize, - Time: blockTime, - UncleHash: uncleHash, + Number: 123, + Transactions: []core.Transaction{{}}, } - // save the block to the database observer := observers.BlockchainDBObserver{Db: db} observer.NotifyBlockAdded(block) - // find the saved block - rows, err := db.Query( - "SELECT block_number, block_gaslimit, block_gasused, block_time, block_difficulty, block_hash, block_nonce, block_parenthash, block_size, uncle_hash FROM blocks") - Expect(err).To(BeNil()) - var savedBlocks []core.Block - for rows.Next() { - var blockHash string - var blockNonce string - var blockNumber int64 - var blockParentHash string - var blockSize int64 - var blockTime float64 - var difficulty int64 - var gasLimit float64 - var gasUsed float64 - var uncleHash string - rows.Scan(&blockNumber, &gasLimit, &gasUsed, &blockTime, &difficulty, &blockHash, &blockNonce, &blockParentHash, &blockSize, &uncleHash) - savedBlock := core.Block{ - Difficulty: difficulty, - GasLimit: int64(gasLimit), - GasUsed: int64(gasUsed), - Hash: blockHash, - Nonce: blockNonce, - Number: blockNumber, - ParentHash: blockParentHash, - Size: blockSize, - Time: int64(blockTime), - UncleHash: uncleHash, - } - savedBlocks = append(savedBlocks, savedBlock) - } - // assert against the attributes - Expect(len(savedBlocks)).To(Equal(1)) - Expect(savedBlocks[0].Difficulty).To(Equal(difficulty)) - Expect(savedBlocks[0].GasLimit).To(Equal(gasLimit)) - Expect(savedBlocks[0].GasUsed).To(Equal(gasUsed)) - Expect(savedBlocks[0].Hash).To(Equal(blockHash)) - Expect(savedBlocks[0].Nonce).To(Equal(blockNonce)) - Expect(savedBlocks[0].Number).To(Equal(blockNumber)) - Expect(savedBlocks[0].ParentHash).To(Equal(blockParentHash)) - Expect(savedBlocks[0].Size).To(Equal(blockSize)) - Expect(savedBlocks[0].Time).To(Equal(blockTime)) - Expect(savedBlocks[0].UncleHash).To(Equal(uncleHash)) - }) - - var _ = Describe("Saving transactions to the database", func() { - - It("inserts a transaction", func() { - gasLimit := int64(5000) - gasPrice := int64(3) - nonce := uint64(10000) - to := "1234567890" - value := int64(10) - - txRecord := core.Transaction{ - Hash: "x1234", - GasPrice: gasPrice, - GasLimit: gasLimit, - Nonce: nonce, - To: to, - Value: value, - } - block := core.Block{Transactions: []core.Transaction{txRecord}} - - observer := observers.BlockchainDBObserver{Db: db} - observer.NotifyBlockAdded(block) - - rows, err := db.Query("SELECT tx_hash, tx_nonce, tx_to, tx_gaslimit, tx_gasprice, tx_value FROM transactions") - Expect(err).To(BeNil()) - - var savedTransactions []core.Transaction - for rows.Next() { - var dbHash string - var dbNonce uint64 - var dbTo string - var dbGasLimit int64 - var dbGasPrice int64 - var dbValue int64 - rows.Scan(&dbHash, &dbNonce, &dbTo, &dbGasLimit, &dbGasPrice, &dbValue) - savedTransaction := core.Transaction{ - GasLimit: dbGasLimit, - GasPrice: dbGasPrice, - Hash: dbHash, - Nonce: dbNonce, - To: dbTo, - Value: dbValue, - } - savedTransactions = append(savedTransactions, savedTransaction) - } - - Expect(len(savedTransactions)).To(Equal(1)) - savedTransaction := savedTransactions[0] - Expect(savedTransaction.GasLimit).To(Equal(gasLimit)) - Expect(savedTransaction.GasPrice).To(Equal(gasPrice)) - Expect(savedTransaction.Hash).To(Equal(txRecord.Hash)) - Expect(savedTransaction.Nonce).To(Equal(nonce)) - Expect(savedTransaction.To).To(Equal(to)) - Expect(savedTransaction.Value).To(Equal(value)) - }) - - It("associates the transaction with the block", func() { - txRecord := core.Transaction{} - block := core.Block{ - Transactions: []core.Transaction{txRecord}, - } - - observer := observers.BlockchainDBObserver{Db: db} - observer.NotifyBlockAdded(block) - - blockRows, err := db.Query("SELECT id FROM blocks") - Expect(err).To(BeNil()) - - var actualBlockIds []int64 - for blockRows.Next() { - var actualBlockId int64 - blockRows.Scan(&actualBlockId) - actualBlockIds = append(actualBlockIds, actualBlockId) - } - - transactionRows, err := db.Query("SELECT block_id FROM transactions") - Expect(err).To(BeNil()) - - var transactionBlockIds []int64 - for transactionRows.Next() { - var transactionBlockId int64 - transactionRows.Scan(&transactionBlockId) - transactionBlockIds = append(transactionBlockIds, transactionBlockId) - } - - Expect(len(actualBlockIds)).To(Equal(1)) - Expect(len(transactionBlockIds)).To(Equal(1)) - Expect(transactionBlockIds[0]).To(Equal(actualBlockIds[0])) - }) + repository := repositories.NewPostgres(db) + savedBlock := repository.FindBlockByNumber(123) + Expect(savedBlock).NotTo(BeNil()) + Expect(len(savedBlock.Transactions)).To(Equal(1)) }) }) diff --git a/repositories/postgres.go b/repositories/postgres.go new file mode 100644 index 00000000..d976f452 --- /dev/null +++ b/repositories/postgres.go @@ -0,0 +1,108 @@ +package repositories + +import ( + "database/sql" + + "github.com/8thlight/vulcanizedb/core" + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" +) + +type Postgres struct { + Db *sqlx.DB +} + +func NewPostgres(db *sqlx.DB) Postgres { + return Postgres{Db: db} +} + +func (repository Postgres) FindBlockByNumber(blockNumber int64) *core.Block { + blockRows, _ := repository.Db.Query("SELECT id, block_number, block_gaslimit, block_gasused, block_time, block_difficulty, block_hash, block_nonce, block_parenthash, block_size, uncle_hash FROM blocks") + var savedBlocks []core.Block + for blockRows.Next() { + savedBlock := repository.loadBlock(blockRows) + savedBlocks = append(savedBlocks, savedBlock) + } + if len(savedBlocks) > 0 { + return &savedBlocks[0] + } else { + return nil + } +} + +func (repository Postgres) BlockCount() int { + var count int + repository.Db.Get(&count, "SELECT COUNT(*) FROM blocks") + return count +} + +func (repository Postgres) CreateBlock(block core.Block) { + insertedBlock := repository.Db.QueryRow( + "Insert INTO blocks "+ + "(block_number, block_gaslimit, block_gasused, block_time, block_difficulty, block_hash, block_nonce, block_parenthash, block_size, uncle_hash) "+ + "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id", + block.Number, block.GasLimit, block.GasUsed, block.Time, block.Difficulty, block.Hash, block.Nonce, block.ParentHash, block.Size, block.UncleHash) + var blockId int64 + insertedBlock.Scan(&blockId) + repository.createTransactions(blockId, block.Transactions) +} + +func (repository Postgres) createTransactions(blockId int64, transactions []core.Transaction) { + for _, transaction := range transactions { + repository.Db.MustExec("Insert INTO transactions "+ + "(block_id, tx_hash, tx_nonce, tx_to, tx_gaslimit, tx_gasprice, tx_value) VALUES ($1, $2, $3, $4, $5, $6, $7)", + blockId, transaction.Hash, transaction.Nonce, transaction.To, transaction.GasLimit, transaction.GasPrice, transaction.Value) + } +} + +func (repository Postgres) loadBlock(blockRows *sql.Rows) core.Block { + var blockId int64 + var blockHash string + var blockNonce string + var blockNumber int64 + var blockParentHash string + var blockSize int64 + var blockTime float64 + var difficulty int64 + var gasLimit float64 + var gasUsed float64 + var uncleHash string + blockRows.Scan(&blockId, &blockNumber, &gasLimit, &gasUsed, &blockTime, &difficulty, &blockHash, &blockNonce, &blockParentHash, &blockSize, &uncleHash) + transactions := repository.loadTransactions(blockId) + return core.Block{ + Difficulty: difficulty, + GasLimit: int64(gasLimit), + GasUsed: int64(gasUsed), + Hash: blockHash, + Nonce: blockNonce, + Number: blockNumber, + ParentHash: blockParentHash, + Size: blockSize, + Time: int64(blockTime), + Transactions: transactions, + UncleHash: uncleHash, + } +} +func (repository Postgres) loadTransactions(blockId int64) []core.Transaction { + transactionRows, _ := repository.Db.Query("SELECT tx_hash, tx_nonce, tx_to, tx_gaslimit, tx_gasprice, tx_value FROM transactions") + var transactions []core.Transaction + for transactionRows.Next() { + var hash string + var nonce uint64 + var to string + var gasLimit int64 + var gasPrice int64 + var value int64 + transactionRows.Scan(&hash, &nonce, &to, &gasLimit, &gasPrice, &value) + transaction := core.Transaction{ + Hash: hash, + Nonce: nonce, + To: to, + GasLimit: gasLimit, + GasPrice: gasPrice, + Value: value, + } + transactions = append(transactions, transaction) + } + return transactions +} diff --git a/repositories/postgres_test.go b/repositories/postgres_test.go new file mode 100644 index 00000000..7449e767 --- /dev/null +++ b/repositories/postgres_test.go @@ -0,0 +1,159 @@ +package repositories_test + +import ( + "github.com/8thlight/vulcanizedb/config" + "github.com/8thlight/vulcanizedb/core" + "github.com/8thlight/vulcanizedb/repositories" + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("The Postgres Repository", func() { + + var db *sqlx.DB + var err error + + BeforeEach(func() { + pgConfig := config.DbConnectionString(config.NewConfig("private").Database) + db, err = sqlx.Connect("postgres", pgConfig) + db.MustExec("DELETE FROM transactions") + db.MustExec("DELETE FROM blocks") + }) + + AfterEach(func() { + db.Close() + }) + + It("connects to the database", func() { + Expect(err).Should(BeNil()) + Expect(db).ShouldNot(BeNil()) + }) + + Describe("Saving blocks", func() { + It("starts with no blocks", func() { + count := repositories.NewPostgres(db).BlockCount() + Expect(count).Should(Equal(0)) + }) + + It("increments the block count", func() { + block := core.Block{Number: 123} + repository := repositories.NewPostgres(db) + + repository.CreateBlock(block) + + Expect(repository.BlockCount()).To(Equal(1)) + }) + + It("saves the attributes of the block", func() { + blockNumber := int64(123) + gasLimit := int64(1000000) + gasUsed := int64(10) + blockHash := "x123" + blockParentHash := "x456" + blockNonce := "0x881db2ca900682e9a9" + blockTime := int64(1508981640) + uncleHash := "x789" + blockSize := int64(1000) + difficulty := int64(10) + block := core.Block{ + Difficulty: difficulty, + GasLimit: gasLimit, + GasUsed: gasUsed, + Hash: blockHash, + Nonce: blockNonce, + Number: blockNumber, + ParentHash: blockParentHash, + Size: blockSize, + Time: blockTime, + UncleHash: uncleHash, + } + + repository := repositories.NewPostgres(db) + + repository.CreateBlock(block) + + savedBlock := repository.FindBlockByNumber(blockNumber) + Expect(savedBlock.Difficulty).To(Equal(difficulty)) + Expect(savedBlock.GasLimit).To(Equal(gasLimit)) + Expect(savedBlock.GasUsed).To(Equal(gasUsed)) + Expect(savedBlock.Hash).To(Equal(blockHash)) + Expect(savedBlock.Nonce).To(Equal(blockNonce)) + Expect(savedBlock.Number).To(Equal(blockNumber)) + Expect(savedBlock.ParentHash).To(Equal(blockParentHash)) + Expect(savedBlock.Size).To(Equal(blockSize)) + Expect(savedBlock.Time).To(Equal(blockTime)) + Expect(savedBlock.UncleHash).To(Equal(uncleHash)) + }) + + It("does not find a block when searching for a number that does not exist", func() { + repository := repositories.NewPostgres(db) + + savedBlock := repository.FindBlockByNumber(111) + + Expect(savedBlock).To(BeNil()) + }) + + It("saves one transaction associated to the block", func() { + block := core.Block{ + Number: 123, + Transactions: []core.Transaction{{}}, + } + repository := repositories.NewPostgres(db) + + repository.CreateBlock(block) + + savedBlock := repository.FindBlockByNumber(123) + Expect(len(savedBlock.Transactions)).To(Equal(1)) + }) + + It("saves two transactions associated to the block", func() { + block := core.Block{ + Number: 123, + Transactions: []core.Transaction{{}, {}}, + } + repository := repositories.NewPostgres(db) + + repository.CreateBlock(block) + + savedBlock := repository.FindBlockByNumber(123) + Expect(len(savedBlock.Transactions)).To(Equal(2)) + }) + + It("saves the attributes associated to a transaction", func() { + gasLimit := int64(5000) + gasPrice := int64(3) + nonce := uint64(10000) + to := "1234567890" + value := int64(10) + + transaction := core.Transaction{ + Hash: "x1234", + GasPrice: gasPrice, + GasLimit: gasLimit, + Nonce: nonce, + To: to, + Value: value, + } + block := core.Block{ + Number: 123, + Transactions: []core.Transaction{transaction}, + } + repository := repositories.NewPostgres(db) + + repository.CreateBlock(block) + + savedBlock := repository.FindBlockByNumber(123) + Expect(len(savedBlock.Transactions)).To(Equal(1)) + savedTransaction := savedBlock.Transactions[0] + Expect(savedTransaction.Hash).To(Equal(transaction.Hash)) + Expect(savedTransaction.To).To(Equal(to)) + Expect(savedTransaction.Nonce).To(Equal(nonce)) + Expect(savedTransaction.GasLimit).To(Equal(gasLimit)) + Expect(savedTransaction.GasPrice).To(Equal(gasPrice)) + Expect(savedTransaction.Value).To(Equal(value)) + }) + }) + +}) diff --git a/repositories/repositories_suite_test.go b/repositories/repositories_suite_test.go new file mode 100644 index 00000000..3d106454 --- /dev/null +++ b/repositories/repositories_suite_test.go @@ -0,0 +1,13 @@ +package repositories_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestRepositories(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Repositories Suite") +}