From 134b284ce36dc0359e9317efa443bab744a314a3 Mon Sep 17 00:00:00 2001 From: AnthonyLaw Date: Sat, 1 Jul 2023 04:19:28 +0800 Subject: [PATCH] [explorer/puller] task: add nem database for blocks table --- explorer/puller/db/NemDatabase.py | 60 +++++++ explorer/puller/tests/db/test_NemDatabase.py | 157 +++++++++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 explorer/puller/db/NemDatabase.py create mode 100644 explorer/puller/tests/db/test_NemDatabase.py diff --git a/explorer/puller/db/NemDatabase.py b/explorer/puller/db/NemDatabase.py new file mode 100644 index 0000000000..e352b7e622 --- /dev/null +++ b/explorer/puller/db/NemDatabase.py @@ -0,0 +1,60 @@ +from binascii import unhexlify + +from db.DatabaseConnection import DatabaseConnection + + +class NemDatabase(DatabaseConnection): + """Database containing Nem blockchain data.""" + + def create_tables(self): + """Creates blocks database tables.""" + + cursor = self.connection.cursor() + cursor.execute( + ''' + CREATE TABLE IF NOT EXISTS blocks ( + height bigint NOT NULL PRIMARY KEY, + timestamp timestamp NOT NULL, + totalFees bigint DEFAULT 0, + totalTransactions int DEFAULT 0, + difficulty bigInt NOT NULL, + hash bytea NOT NULL, + signer bytea NOT NULL, + signature bytea NOT NULL + ) + ''' + ) + + self.connection.commit() + + def insert_block(self, cursor, block): # pylint: disable=no-self-use + """Adds block height into table.""" + + cursor.execute( + ''' + INSERT INTO blocks + VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + ''', ( + block.height, + block.timestamp, + block.total_fees, + block.total_transactions, + block.difficulty, + unhexlify(block.block_hash), + unhexlify(block.signer), + unhexlify(block.signature) + ) + ) + + def get_current_height(self): + """Gets current height from database""" + + cursor = self.connection.cursor() + cursor.execute( + ''' + SELECT MAX(height) + FROM blocks + ''' + ) + results = cursor.fetchone() + return 0 if results[0] is None else results[0] diff --git a/explorer/puller/tests/db/test_NemDatabase.py b/explorer/puller/tests/db/test_NemDatabase.py new file mode 100644 index 0000000000..2e43060e0b --- /dev/null +++ b/explorer/puller/tests/db/test_NemDatabase.py @@ -0,0 +1,157 @@ +import datetime +import unittest +from binascii import hexlify +from collections import namedtuple + +import psycopg2 +import testing.postgresql + +from db.NemDatabase import NemDatabase + +Block = namedtuple('Block', ['height', 'timestamp', 'total_fees', 'total_transactions', 'difficulty', 'block_hash', 'signer', 'signature']) + + +# region test data + +BLOCKS = [ + Block( + 1, + '2015-03-29 00:06:25', + 102000000, + 5, + 100000000000000, + '438cf6375dab5a0d32f9b7bf151d4539e00a590f7c022d5572c7d41815a24be4', + '8d07f90fb4bbe7715fa327c926770166a11be2e494a970605f2e12557f66c9b9', + '2abdd19ad3efab0413b42772a586faa19dedb16d35f665f90d598046a2132c4a' + 'd1e71001545ceaa44e63c04345591e7aadbfd330af82a0d8a1da5643e791ff0f'), + Block( + 2, + '2015-03-29 20:34:19', + 201000000, + 3, + 80000000000000, + '1dd9d4d7b6af603d29c082f9aa4e123f07d18154ddbcd7ddc6702491b854c5e4', + 'f9bd190dd0c364261f5c8a74870cc7f7374e631352293c62ecc437657e5de2cd', + '1b81379847241e45da86b27911e5c9a9192ec04f644d98019657d32838b49c14' + '3eaa4815a3028b80f9affdbf0b94cd620f7a925e02783dda67b8627b69ddf70e') +] + +# endregion + + +class NemDatabaseTest(unittest.TestCase): + + def setUp(self): + self.postgresql = testing.postgresql.Postgresql() + self.db_config = { + 'database': self.postgresql.url().split('/')[-1], + 'user': 'postgres', + 'password': '', + 'host': self.postgresql.url().split('/')[2].split('@')[1].split(':')[0], + 'port': self.postgresql.url().split('/')[-2].split(':')[-1] + } + + def tearDown(self): + # Destroy the temporary PostgreSQL database + self.postgresql.stop() + + def test_can_create_tables(self): + # Arrange: + with NemDatabase(self.db_config) as nem_database: + # Act: + nem_database.create_tables() + + cursor = nem_database.connection.cursor() + cursor.execute( + ''' + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'blocks' + ''' + ) + result = cursor.fetchone() + + # Assert: + self.assertIsNotNone(result) + self.assertEqual(result[0], 'blocks') + + def test_can_insert_block(self): + # Arrange: + with NemDatabase(self.db_config) as nem_database: + nem_database.create_tables() + + cursor = nem_database.connection.cursor() + + # Act: + nem_database.insert_block(cursor, BLOCKS[0]) + + nem_database.connection.commit() + cursor.execute( + ''' + SELECT * + FROM blocks + WHERE height = %s + ''', + (BLOCKS[0].height, ) + ) + result = cursor.fetchone() + + # Convert memoryview objects to hex strings + result = tuple(hexlify(item).decode('utf-8') if isinstance(item, memoryview) else item for item in result) + + # Assert: + self.assertIsNotNone(result) + expected_result = ( + 1, + datetime.datetime(2015, 3, 29, 0, 6, 25), + 102000000, + 5, + 100000000000000, + BLOCKS[0].block_hash, + BLOCKS[0].signer, + BLOCKS[0].signature + ) + self.assertEqual(result, expected_result) + + def test_cannot_insert_same_block_multiple_times(self): + # Arrange: + with NemDatabase(self.db_config) as nem_database: + nem_database.create_tables() + + cursor = nem_database.connection.cursor() + + nem_database.insert_block(cursor, BLOCKS[0]) + + # Act + Assert: + with self.assertRaises(psycopg2.IntegrityError): + nem_database.insert_block(cursor, BLOCKS[0]) + + def test_can_get_current_height(self): + # Arrange: + with NemDatabase(self.db_config) as nem_database: + nem_database.create_tables() + + cursor = nem_database.connection.cursor() + + for block in BLOCKS: + nem_database.insert_block(cursor, block) + + nem_database.connection.commit() + + # Act: + result = nem_database.get_current_height() + + # Assert: + self.assertEqual(result, 2) + + def test_can_get_current_height_is_zero_database_empty(self): + # Arrange: + with NemDatabase(self.db_config) as nem_database: + nem_database.create_tables() + + # Act: + result = nem_database.get_current_height() + + # Assert: + self.assertEqual(result, 0)