async def get_coin_records_by_spent( self, spent: bool, spend_before_height: Optional[uint32] = None ) -> Set[WalletCoinRecord]: """ Returns set of CoinRecords that have not been spent yet. """ coins = set() if spend_before_height: cursor = await self.db_connection.execute( "SELECT * from coin_record WHERE spent=? OR spent_index>=?", (int(spent), spend_before_height), ) else: cursor = await self.db_connection.execute( "SELECT * from coin_record WHERE spent=?", (int(spent),) ) rows = await cursor.fetchall() await cursor.close() for row in rows: coin = Coin( bytes32(bytes.fromhex(row[6])), bytes32(bytes.fromhex(row[5])), row[7] ) coins.add( WalletCoinRecord( coin, row[1], row[2], row[3], row[4], WalletType(row[8]), row[9] ) ) return coins
async def get_unspent_coins_for_wallet( self, wallet_id: int) -> Set[WalletCoinRecord]: """ Returns set of CoinRecords that have not been spent yet for a wallet. """ async with self.wallet_cache_lock: if wallet_id in self.coin_wallet_record_cache: wallet_coins: Dict[ bytes32, WalletCoinRecord] = self.coin_wallet_record_cache[ wallet_id] return set(wallet_coins.values()) coin_set = set() cursor = await self.db_connection.execute( "SELECT * from coin_record WHERE spent=0 and wallet_id=?", (wallet_id, ), ) rows = await cursor.fetchall() await cursor.close() cache_dict = {} for row in rows: coin = Coin(bytes32(bytes.fromhex(row[6])), bytes32(bytes.fromhex(row[5])), uint64.from_bytes(row[7])) coin_record = WalletCoinRecord(coin, row[1], row[2], row[3], row[4], WalletType(row[8]), row[9]) coin_set.add(coin_record) cache_dict[coin.name()] = coin_record self.coin_wallet_record_cache[wallet_id] = cache_dict return coin_set
async def rollback_lca_to_block(self, block_index): # Update memory cache delete_queue: bytes32 = [] for coin_name, coin_record in self.coin_record_cache.items(): if coin_record.spent_block_index > block_index: new_record = WalletCoinRecord( coin_record.coin, coin_record.confirmed_block_index, coin_record.spent_block_index, False, coin_record.coinbase, coin_record.wallet_type, coin_record.wallet_id, ) self.coin_record_cache[coin_record.coin.name().hex()] = new_record if coin_record.confirmed_block_index > block_index: delete_queue.append(coin_name) for coin_name in delete_queue: del self.coin_record_cache[coin_name] # Delete from storage c1 = await self.db_connection.execute( "DELETE FROM coin_record WHERE confirmed_index>?", (block_index,) ) await c1.close() c2 = await self.db_connection.execute( "UPDATE coin_record SET spent_index = 0, spent = 0 WHERE spent_index>?", (block_index,), ) await c2.close() await self.remove_blocks_from_path(block_index) await self.db_connection.commit()
async def get_spendable_for_index( self, index: uint32, wallet_id: int ) -> Set[WalletCoinRecord]: """ Returns set of unspent coin records that are not coinbases, or if they are coinbases, must have been confirmed at or before index. """ coins = set() cursor_coinbase_coins = await self.db_connection.execute( "SELECT * from coin_record WHERE spent=? and confirmed_index<=? and wallet_id=? and coinbase=?", (0, int(index), wallet_id, 1), ) coinbase_rows = await cursor_coinbase_coins.fetchall() await cursor_coinbase_coins.close() cursor_regular_coins = await self.db_connection.execute( "SELECT * from coin_record WHERE spent=? and wallet_id=? and coinbase=?", (0, wallet_id, 0,), ) regular_rows = await cursor_regular_coins.fetchall() await cursor_regular_coins.close() for row in coinbase_rows + regular_rows: coin = Coin( bytes32(bytes.fromhex(row[6])), bytes32(bytes.fromhex(row[5])), row[7] ) coins.add( WalletCoinRecord( coin, row[1], row[2], row[3], row[4], WalletType(row[8]), row[9] ) ) return coins
async def get_unspent_coins_at_height( self, height: Optional[uint32] = None ) -> Set[WalletCoinRecord]: """ Returns set of CoinRecords that have not been spent yet. If a height is specified, We can also return coins that were unspent at this height (but maybe spent later). Finally, the coins must be confirmed at the height or less. """ coins = set() if height is not None: cursor = await self.db_connection.execute( "SELECT * from coin_record WHERE (spent=? OR spent_index>?) AND confirmed_index<=?", (0, height, height), ) else: cursor = await self.db_connection.execute( "SELECT * from coin_record WHERE spent=?", (0,) ) rows = await cursor.fetchall() await cursor.close() for row in rows: coin = Coin( bytes32(bytes.fromhex(row[6])), bytes32(bytes.fromhex(row[5])), row[7] ) coins.add( WalletCoinRecord( coin, row[1], row[2], row[3], row[4], WalletType(row[8]), row[9] ) ) return coins
async def coin_added(self, coin: Coin, index: uint32, coinbase: bool): """ Adding coin to the db """ info = await self.puzzle_store.wallet_info_for_puzzle_hash( coin.puzzle_hash) assert info is not None wallet_id, wallet_type = info if coinbase: now = uint64(int(time.time())) tx_record = TransactionRecord( confirmed_at_index=uint32(index), created_at_time=now, to_puzzle_hash=coin.puzzle_hash, amount=coin.amount, fee_amount=uint64(0), incoming=True, confirmed=True, sent=uint32(0), spend_bundle=None, additions=[coin], removals=[], wallet_id=wallet_id, sent_to=[], ) await self.tx_store.add_transaction_record(tx_record) else: unconfirmed_record = await self.tx_store.unconfirmed_with_addition_coin( coin.name()) if unconfirmed_record: # This is the change from this transaction await self.tx_store.set_confirmed(unconfirmed_record.name(), index) else: now = uint64(int(time.time())) tx_record = TransactionRecord( confirmed_at_index=uint32(index), created_at_time=now, to_puzzle_hash=coin.puzzle_hash, amount=coin.amount, fee_amount=uint64(0), incoming=True, confirmed=True, sent=uint32(0), spend_bundle=None, additions=[coin], removals=[], wallet_id=wallet_id, sent_to=[], ) await self.tx_store.add_transaction_record(tx_record) coin_record: WalletCoinRecord = WalletCoinRecord( coin, index, uint32(0), False, coinbase, wallet_type, wallet_id) await self.wallet_store.add_coin_record(coin_record) self.state_changed("coin_added")
async def get_coin_records_by_puzzle_hash(self, puzzle_hash: bytes32) -> List[WalletCoinRecord]: """Returns a list of all coin records with the given puzzle hash""" coins = set() cursor = await self.db_connection.execute("SELECT * from coin_record WHERE puzzle_hash=?", (puzzle_hash.hex(),)) rows = await cursor.fetchall() await cursor.close() for row in rows: coin = Coin(bytes32(bytes.fromhex(row[6])), bytes32(bytes.fromhex(row[5])), uint64.from_bytes(row[7])) coins.add(WalletCoinRecord(coin, row[1], row[2], row[3], row[4], WalletType(row[8]), row[9])) return list(coins)
async def get_coin_record_by_coin_id(self, coin_id: bytes32) -> Optional[WalletCoinRecord]: """Returns a coin records with the given name, if it exists""" cursor = await self.db_connection.execute("SELECT * from coin_record WHERE coin_name=?", (coin_id.hex(),)) row = await cursor.fetchone() await cursor.close() if row is None: return None coin = Coin(bytes32(bytes.fromhex(row[6])), bytes32(bytes.fromhex(row[5])), uint64.from_bytes(row[7])) coin_record = WalletCoinRecord(coin, row[1], row[2], row[3], row[4], WalletType(row[8]), row[9]) return coin_record
async def get_all_coins(self) -> Set[WalletCoinRecord]: """ Returns set of all CoinRecords.""" coins = set() cursor = await self.db_connection.execute("SELECT * from coin_record") rows = await cursor.fetchall() await cursor.close() for row in rows: coin = Coin(bytes32(bytes.fromhex(row[6])), bytes32(bytes.fromhex(row[5])), uint64.from_bytes(row[7])) coins.add(WalletCoinRecord(coin, row[1], row[2], row[3], row[4], WalletType(row[8]), row[9])) return coins
async def get_coin_record(self, coin_name: bytes32) -> Optional[WalletCoinRecord]: """ Returns CoinRecord with specified coin id. """ if coin_name in self.coin_record_cache: return self.coin_record_cache[coin_name] cursor = await self.db_connection.execute("SELECT * from coin_record WHERE coin_name=?", (coin_name.hex(),)) row = await cursor.fetchone() await cursor.close() if row is not None: coin = Coin(bytes32(bytes.fromhex(row[6])), bytes32(bytes.fromhex(row[5])), uint64.from_bytes(row[7])) return WalletCoinRecord(coin, row[1], row[2], row[3], row[4], WalletType(row[8]), row[9]) return None
async def set_spent(self, coin_name: bytes32, index: uint32): current: Optional[WalletCoinRecord] = await self.get_coin_record(coin_name) if current is None: return spent: WalletCoinRecord = WalletCoinRecord( current.coin, current.confirmed_block_index, index, True, current.coinbase, current.wallet_type, current.wallet_id, ) await self.add_coin_record(spent)
async def rollback_to_block(self, height: int): """ Rolls back the blockchain to block_index. All blocks confirmed after this point are removed from the LCA. All coins confirmed after this point are removed. All coins spent after this point are set to unspent. Can be -1 (rollback all) """ # Update memory cache delete_queue: List[WalletCoinRecord] = [] for coin_name, coin_record in self.coin_record_cache.items(): if coin_record.spent_block_height > height: new_record = WalletCoinRecord( coin_record.coin, coin_record.confirmed_block_height, coin_record.spent_block_height, False, coin_record.coinbase, coin_record.wallet_type, coin_record.wallet_id, ) self.coin_record_cache[coin_record.coin.name()] = new_record if coin_record.confirmed_block_height > height: delete_queue.append(coin_record) for coin_record in delete_queue: self.coin_record_cache.pop(coin_record.coin.name()) if coin_record.wallet_id in self.coin_wallet_record_cache: coin_cache = self.coin_wallet_record_cache[ coin_record.wallet_id] if coin_record.coin.name() in coin_cache: coin_cache.pop(coin_record.coin.name()) # Delete from storage c1 = await self.db_connection.execute( "DELETE FROM coin_record WHERE confirmed_height>?", (height, )) await c1.close() c2 = await self.db_connection.execute( "UPDATE coin_record SET spent_height = 0, spent = 0 WHERE spent_height>?", (height, ), ) c3 = await self.db_connection.execute( "UPDATE coin_record SET spent_height = 0, spent = 0 WHERE spent_height>?", (height, ), ) await c3.close() await c2.close() await self.db_connection.commit()
async def get_unspent_coins_for_wallet( self, wallet_id: int) -> Set[WalletCoinRecord]: """ Returns set of CoinRecords that have not been spent yet for a wallet. """ coins = set() cursor = await self.db_connection.execute( "SELECT * from coin_record WHERE spent=0 and wallet_id=?", (wallet_id, ), ) rows = await cursor.fetchall() await cursor.close() for row in rows: coin = Coin(bytes32(bytes.fromhex(row[6])), bytes32(bytes.fromhex(row[5])), row[7]) coins.add( WalletCoinRecord(coin, row[1], row[2], row[3], row[4], WalletType(row[8]), row[9])) return coins
async def coin_added( self, coin: Coin, coinbase: bool, fee_reward: bool, wallet_id: uint32, wallet_type: WalletType, height: uint32, ): """ Adding coin to DB """ self.log.info(f"Adding coin: {coin} at {height}") farm_reward = False if coinbase or fee_reward: farm_reward = True now = uint64(int(time.time())) if coinbase: tx_type: int = TransactionType.COINBASE_REWARD.value else: tx_type = TransactionType.FEE_REWARD.value tx_record = TransactionRecord( confirmed_at_height=uint32(height), created_at_time=now, to_puzzle_hash=coin.puzzle_hash, amount=coin.amount, fee_amount=uint64(0), confirmed=True, sent=uint32(0), spend_bundle=None, additions=[coin], removals=[], wallet_id=wallet_id, sent_to=[], trade_id=None, type=uint32(tx_type), name=coin.name(), ) await self.tx_store.add_transaction_record(tx_record) else: records = await self.tx_store.tx_with_addition_coin(coin.name(), wallet_id) if len(records) > 0: # This is the change from this transaction for record in records: if record.confirmed is False: await self.tx_store.set_confirmed(record.name, height) else: now = uint64(int(time.time())) tx_record = TransactionRecord( confirmed_at_height=uint32(height), created_at_time=now, to_puzzle_hash=coin.puzzle_hash, amount=coin.amount, fee_amount=uint64(0), confirmed=True, sent=uint32(0), spend_bundle=None, additions=[coin], removals=[], wallet_id=wallet_id, sent_to=[], trade_id=None, type=uint32(TransactionType.INCOMING_TX.value), name=coin.name(), ) if coin.amount > 0: await self.tx_store.add_transaction_record(tx_record) coin_record: WalletCoinRecord = WalletCoinRecord( coin, height, uint32(0), False, farm_reward, wallet_type, wallet_id ) await self.coin_store.add_coin_record(coin_record) if wallet_type == WalletType.COLOURED_COIN: wallet: CCWallet = self.wallets[wallet_id] header_hash: bytes32 = self.blockchain.height_to_hash(height) block: Optional[HeaderBlockRecord] = await self.block_store.get_header_block_record(header_hash) assert block is not None assert block.removals is not None await wallet.coin_added(coin, header_hash, block.removals, height) self.state_changed("coin_added", wallet_id)
async def test_store(self): db_filename = Path("blockchain_wallet_store_test.db") if db_filename.exists(): db_filename.unlink() db_connection = await aiosqlite.connect(db_filename) store = await WalletStore.create(db_connection) try: coin_1 = Coin(token_bytes(32), token_bytes(32), uint64(12312)) coin_2 = Coin(token_bytes(32), token_bytes(32), uint64(12312)) coin_3 = Coin(token_bytes(32), token_bytes(32), uint64(12312)) coin_4 = Coin(token_bytes(32), token_bytes(32), uint64(12312)) record_replaced = WalletCoinRecord(coin_1, uint32(8), uint32(0), False, True, WalletType.STANDARD_WALLET, 0) record_1 = WalletCoinRecord(coin_1, uint32(4), uint32(0), False, True, WalletType.STANDARD_WALLET, 0) record_2 = WalletCoinRecord(coin_2, uint32(5), uint32(0), False, True, WalletType.STANDARD_WALLET, 0) record_3 = WalletCoinRecord( coin_3, uint32(5), uint32(10), True, False, WalletType.STANDARD_WALLET, 0, ) record_4 = WalletCoinRecord( coin_4, uint32(5), uint32(15), True, False, WalletType.STANDARD_WALLET, 0, ) # Test add (replace) and get assert await store.get_coin_record(coin_1.name()) is None await store.add_coin_record(record_replaced) await store.add_coin_record(record_1) await store.add_coin_record(record_2) await store.add_coin_record(record_3) await store.add_coin_record(record_4) assert await store.get_coin_record(coin_1.name()) == record_1 # Test persistance await db_connection.close() db_connection = await aiosqlite.connect(db_filename) store = await WalletStore.create(db_connection) assert await store.get_coin_record(coin_1.name()) == record_1 # Test set spent await store.set_spent(coin_1.name(), uint32(12)) assert (await store.get_coin_record(coin_1.name())).spent assert (await store.get_coin_record(coin_1.name() )).spent_block_index == 12 # No coins at height 3 assert len(await store.get_unspent_coins_at_height(3)) == 0 assert len(await store.get_unspent_coins_at_height(4)) == 1 assert len(await store.get_unspent_coins_at_height(5)) == 4 assert len(await store.get_unspent_coins_at_height(11)) == 3 assert len(await store.get_unspent_coins_at_height(12)) == 2 assert len(await store.get_unspent_coins_at_height(15)) == 1 assert len(await store.get_unspent_coins_at_height(16)) == 1 assert len(await store.get_unspent_coins_at_height()) == 1 assert len(await store.get_unspent_coins_for_wallet(0)) == 1 assert len(await store.get_unspent_coins_for_wallet(1)) == 0 coin_5 = Coin(token_bytes(32), token_bytes(32), uint64(12312)) record_5 = WalletCoinRecord( coin_5, uint32(5), uint32(15), False, False, WalletType.STANDARD_WALLET, 1, ) await store.add_coin_record(record_5) assert len(await store.get_unspent_coins_for_wallet(1)) == 1 assert len(await store.get_spendable_for_index(100, 1)) == 1 assert len(await store.get_spendable_for_index(100, 0)) == 1 assert len(await store.get_spendable_for_index(0, 0)) == 0 coin_6 = Coin(token_bytes(32), coin_4.puzzle_hash, uint64(12312)) await store.add_coin_record(record_5) record_6 = WalletCoinRecord( coin_6, uint32(5), uint32(15), True, False, WalletType.STANDARD_WALLET, 2, ) await store.add_coin_record(record_6) assert (len(await store.get_coin_records_by_puzzle_hash( record_6.coin.puzzle_hash)) == 2) # 4 and 6 assert (len(await store.get_coin_records_by_puzzle_hash(token_bytes(32) )) == 0) assert await store.get_coin_record_by_coin_id(coin_6.name() ) == record_6 assert await store.get_coin_record_by_coin_id(token_bytes(32) ) is None # BLOCKS assert len(await store.get_lca_path()) == 0 # NOT lca block br_1 = BlockRecord( token_bytes(32), token_bytes(32), uint32(0), uint128(100), None, None, None, None, uint64(0), ) assert await store.get_block_record(br_1.header_hash) is None await store.add_block_record(br_1, False) assert len(await store.get_lca_path()) == 0 assert await store.get_block_record(br_1.header_hash) == br_1 # LCA genesis await store.add_block_record(br_1, True) assert await store.get_block_record(br_1.header_hash) == br_1 assert len(await store.get_lca_path()) == 1 assert (await store.get_lca_path())[br_1.header_hash] == br_1 br_2 = BlockRecord( token_bytes(32), token_bytes(32), uint32(1), uint128(100), None, None, None, None, uint64(0), ) await store.add_block_record(br_2, False) assert len(await store.get_lca_path()) == 1 await store.add_block_to_path(br_2.header_hash) assert len(await store.get_lca_path()) == 2 assert (await store.get_lca_path())[br_2.header_hash] == br_2 br_3 = BlockRecord( token_bytes(32), token_bytes(32), uint32(2), uint128(100), None, None, None, None, uint64(0), ) await store.add_block_record(br_3, True) assert len(await store.get_lca_path()) == 3 await store.remove_blocks_from_path(1) assert len(await store.get_lca_path()) == 2 await store.rollback_lca_to_block(0) assert len(await store.get_unspent_coins_at_height()) == 0 coin_7 = Coin(token_bytes(32), token_bytes(32), uint64(12312)) coin_8 = Coin(token_bytes(32), token_bytes(32), uint64(12312)) coin_9 = Coin(token_bytes(32), token_bytes(32), uint64(12312)) coin_10 = Coin(token_bytes(32), token_bytes(32), uint64(12312)) record_7 = WalletCoinRecord(coin_7, uint32(0), uint32(1), True, False, WalletType.STANDARD_WALLET, 1) record_8 = WalletCoinRecord(coin_8, uint32(1), uint32(2), True, False, WalletType.STANDARD_WALLET, 1) record_9 = WalletCoinRecord(coin_9, uint32(2), uint32(3), True, False, WalletType.STANDARD_WALLET, 1) record_10 = WalletCoinRecord( coin_10, uint32(3), uint32(4), True, False, WalletType.STANDARD_WALLET, 1, ) await store.add_coin_record(record_7) await store.add_coin_record(record_8) await store.add_coin_record(record_9) await store.add_coin_record(record_10) assert len(await store.get_unspent_coins_at_height(0)) == 1 assert len(await store.get_unspent_coins_at_height(1)) == 1 assert len(await store.get_unspent_coins_at_height(2)) == 1 assert len(await store.get_unspent_coins_at_height(3)) == 1 assert len(await store.get_unspent_coins_at_height(4)) == 0 await store.add_block_record(br_2, True) await store.add_block_record(br_3, True) await store.rollback_lca_to_block(1) assert len(await store.get_unspent_coins_at_height(0)) == 1 assert len(await store.get_unspent_coins_at_height(1)) == 1 assert len(await store.get_unspent_coins_at_height(2)) == 1 assert len(await store.get_unspent_coins_at_height(3)) == 1 assert len(await store.get_unspent_coins_at_height(4)) == 1 except AssertionError: await db_connection.close() raise await db_connection.close()