332 lines
13 KiB
Python
332 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
from typing import List, Optional, Type, TypeVar, Union
|
|
|
|
from aiosqlite import Row
|
|
|
|
from chia.data_layer.data_layer_wallet import Mirror, SingletonRecord
|
|
from chia.types.blockchain_format.coin import Coin
|
|
from chia.types.blockchain_format.sized_bytes import bytes32
|
|
from chia.util.db_wrapper import DBWrapper2
|
|
from chia.util.ints import uint16, uint32, uint64
|
|
from chia.wallet.lineage_proof import LineageProof
|
|
|
|
_T_DataLayerStore = TypeVar("_T_DataLayerStore", bound="DataLayerStore")
|
|
|
|
|
|
def _row_to_singleton_record(row: Row) -> SingletonRecord:
|
|
return SingletonRecord(
|
|
bytes32(row[0]),
|
|
bytes32(row[1]),
|
|
bytes32(row[2]),
|
|
bytes32(row[3]),
|
|
bool(row[4]),
|
|
uint32(row[5]),
|
|
LineageProof.from_bytes(row[6]),
|
|
uint32(row[7]),
|
|
uint64(row[8]),
|
|
)
|
|
|
|
|
|
def _row_to_mirror(row: Row) -> Mirror:
|
|
urls: List[bytes] = []
|
|
byte_list: bytes = row[3]
|
|
while byte_list != b"":
|
|
length = uint16.from_bytes(byte_list[0:2])
|
|
url = byte_list[2 : length + 2]
|
|
byte_list = byte_list[length + 2 :]
|
|
urls.append(url)
|
|
return Mirror(bytes32(row[0]), bytes32(row[1]), uint64.from_bytes(row[2]), urls, bool(row[4]))
|
|
|
|
|
|
class DataLayerStore:
|
|
"""
|
|
WalletUserStore keeps track of all user created wallets and necessary smart-contract data
|
|
"""
|
|
|
|
db_wrapper: DBWrapper2
|
|
|
|
@classmethod
|
|
async def create(cls: Type[_T_DataLayerStore], db_wrapper: DBWrapper2) -> _T_DataLayerStore:
|
|
self = cls()
|
|
|
|
self.db_wrapper = db_wrapper
|
|
|
|
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
|
await conn.execute(
|
|
(
|
|
"CREATE TABLE IF NOT EXISTS singleton_records("
|
|
"coin_id blob PRIMARY KEY,"
|
|
" launcher_id blob,"
|
|
" root blob,"
|
|
" inner_puzzle_hash blob,"
|
|
" confirmed tinyint,"
|
|
" confirmed_at_height int,"
|
|
" proof blob,"
|
|
" generation int," # This first singleton will be 0, then 1, and so on. This is handled by the DB.
|
|
" timestamp int)"
|
|
)
|
|
)
|
|
|
|
await conn.execute(
|
|
(
|
|
"CREATE TABLE IF NOT EXISTS mirrors("
|
|
"coin_id blob PRIMARY KEY,"
|
|
"launcher_id blob,"
|
|
"amount blob,"
|
|
"urls blob,"
|
|
"ours tinyint)"
|
|
)
|
|
)
|
|
|
|
await conn.execute("CREATE INDEX IF NOT EXISTS coin_id on singleton_records(coin_id)")
|
|
await conn.execute("CREATE INDEX IF NOT EXISTS launcher_id on singleton_records(launcher_id)")
|
|
await conn.execute("CREATE INDEX IF NOT EXISTS root on singleton_records(root)")
|
|
await conn.execute("CREATE INDEX IF NOT EXISTS inner_puzzle_hash on singleton_records(inner_puzzle_hash)")
|
|
await conn.execute("CREATE INDEX IF NOT EXISTS confirmed_at_height on singleton_records(root)")
|
|
await conn.execute("CREATE INDEX IF NOT EXISTS generation on singleton_records(generation)")
|
|
|
|
await conn.execute(("CREATE TABLE IF NOT EXISTS launchers(id blob PRIMARY KEY, coin blob)"))
|
|
|
|
await conn.execute("CREATE INDEX IF NOT EXISTS id on launchers(id)")
|
|
|
|
return self
|
|
|
|
async def _clear_database(self) -> None:
|
|
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
|
await (await conn.execute("DELETE FROM singleton_records")).close()
|
|
|
|
async def add_singleton_record(self, record: SingletonRecord) -> None:
|
|
"""
|
|
Store SingletonRecord in DB.
|
|
"""
|
|
|
|
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
|
await conn.execute_insert(
|
|
"INSERT OR REPLACE INTO singleton_records VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
(
|
|
record.coin_id,
|
|
record.launcher_id,
|
|
record.root,
|
|
record.inner_puzzle_hash,
|
|
int(record.confirmed),
|
|
record.confirmed_at_height,
|
|
bytes(record.lineage_proof),
|
|
record.generation,
|
|
record.timestamp,
|
|
),
|
|
)
|
|
|
|
async def get_all_singletons_for_launcher(
|
|
self,
|
|
launcher_id: bytes32,
|
|
min_generation: Optional[uint32] = None,
|
|
max_generation: Optional[uint32] = None,
|
|
num_results: Optional[uint32] = None,
|
|
) -> List[SingletonRecord]:
|
|
"""
|
|
Returns stored singletons with a specific launcher ID.
|
|
"""
|
|
query_params: List[Union[bytes32, uint32]] = [launcher_id]
|
|
for optional_param in (min_generation, max_generation, num_results):
|
|
if optional_param is not None:
|
|
query_params.append(optional_param)
|
|
|
|
async with self.db_wrapper.reader_no_transaction() as conn:
|
|
cursor = await conn.execute(
|
|
"SELECT * from singleton_records WHERE launcher_id=? "
|
|
f"{'AND generation >=? ' if min_generation is not None else ''}"
|
|
f"{'AND generation <=? ' if max_generation is not None else ''}"
|
|
"ORDER BY generation DESC"
|
|
f"{' LIMIT ?' if num_results is not None else ''}",
|
|
tuple(query_params),
|
|
)
|
|
rows = await cursor.fetchall()
|
|
await cursor.close()
|
|
records = []
|
|
|
|
for row in rows:
|
|
records.append(_row_to_singleton_record(row))
|
|
|
|
return records
|
|
|
|
async def get_singleton_record(self, coin_id: bytes32) -> Optional[SingletonRecord]:
|
|
"""
|
|
Checks DB for SingletonRecord with coin_id: coin_id and returns it.
|
|
"""
|
|
# if tx_id in self.tx_record_cache:
|
|
# return self.tx_record_cache[tx_id]
|
|
|
|
async with self.db_wrapper.reader_no_transaction() as conn:
|
|
cursor = await conn.execute("SELECT * from singleton_records WHERE coin_id=?", (coin_id,))
|
|
row = await cursor.fetchone()
|
|
await cursor.close()
|
|
if row is not None:
|
|
return _row_to_singleton_record(row)
|
|
return None
|
|
|
|
async def get_latest_singleton(
|
|
self, launcher_id: bytes32, only_confirmed: bool = False
|
|
) -> Optional[SingletonRecord]:
|
|
"""
|
|
Checks DB for SingletonRecords with launcher_id: launcher_id and returns the most recent.
|
|
"""
|
|
# if tx_id in self.tx_record_cache:
|
|
# return self.tx_record_cache[tx_id]
|
|
async with self.db_wrapper.reader_no_transaction() as conn:
|
|
if only_confirmed:
|
|
# get latest confirmed root
|
|
cursor = await conn.execute(
|
|
"SELECT * from singleton_records WHERE launcher_id=? and confirmed = 1 "
|
|
"ORDER BY generation DESC LIMIT 1",
|
|
(launcher_id,),
|
|
)
|
|
else:
|
|
cursor = await conn.execute(
|
|
"SELECT * from singleton_records WHERE launcher_id=? ORDER BY generation DESC LIMIT 1",
|
|
(launcher_id,),
|
|
)
|
|
row = await cursor.fetchone()
|
|
await cursor.close()
|
|
if row is not None:
|
|
return _row_to_singleton_record(row)
|
|
return None
|
|
|
|
async def get_unconfirmed_singletons(self, launcher_id: bytes32) -> List[SingletonRecord]:
|
|
"""
|
|
Returns all singletons with a specific launcher id that have not yet been marked confirmed
|
|
"""
|
|
async with self.db_wrapper.reader_no_transaction() as conn:
|
|
cursor = await conn.execute(
|
|
"SELECT * from singleton_records WHERE launcher_id=? AND confirmed=0", (launcher_id,)
|
|
)
|
|
rows = await cursor.fetchall()
|
|
await cursor.close()
|
|
records = [_row_to_singleton_record(row) for row in rows]
|
|
|
|
return records
|
|
|
|
async def get_singletons_by_root(self, launcher_id: bytes32, root: bytes32) -> List[SingletonRecord]:
|
|
async with self.db_wrapper.reader_no_transaction() as conn:
|
|
cursor = await conn.execute(
|
|
"SELECT * from singleton_records WHERE launcher_id=? AND root=? ORDER BY generation DESC",
|
|
(launcher_id, root),
|
|
)
|
|
rows = await cursor.fetchall()
|
|
await cursor.close()
|
|
records = []
|
|
|
|
for row in rows:
|
|
records.append(_row_to_singleton_record(row))
|
|
|
|
return records
|
|
|
|
async def set_confirmed(self, coin_id: bytes32, height: uint32, timestamp: uint64) -> None:
|
|
"""
|
|
Updates singleton record to be confirmed.
|
|
"""
|
|
current: Optional[SingletonRecord] = await self.get_singleton_record(coin_id)
|
|
if current is None or current.confirmed_at_height == height:
|
|
return
|
|
|
|
await self.add_singleton_record(
|
|
dataclasses.replace(current, confirmed=True, confirmed_at_height=height, timestamp=timestamp)
|
|
)
|
|
|
|
async def delete_singleton_record(self, coin_id: bytes32) -> None:
|
|
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
|
await (await conn.execute("DELETE FROM singleton_records WHERE coin_id=?", (coin_id,))).close()
|
|
|
|
async def delete_singleton_records_by_launcher_id(self, launcher_id: bytes32) -> None:
|
|
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
|
await (await conn.execute("DELETE FROM singleton_records WHERE launcher_id=?", (launcher_id,))).close()
|
|
|
|
async def add_launcher(self, launcher: Coin) -> None:
|
|
"""
|
|
Add a new launcher coin's information to the DB
|
|
"""
|
|
launcher_bytes: bytes = launcher.parent_coin_info + launcher.puzzle_hash + bytes(uint64(launcher.amount))
|
|
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
|
await conn.execute_insert(
|
|
"INSERT OR REPLACE INTO launchers VALUES (?, ?)",
|
|
(launcher.name(), launcher_bytes),
|
|
)
|
|
|
|
async def get_launcher(self, launcher_id: bytes32) -> Optional[Coin]:
|
|
"""
|
|
Checks DB for a launcher with the specified ID and returns it.
|
|
"""
|
|
|
|
async with self.db_wrapper.reader_no_transaction() as conn:
|
|
cursor = await conn.execute("SELECT * from launchers WHERE id=?", (launcher_id,))
|
|
row = await cursor.fetchone()
|
|
await cursor.close()
|
|
if row is not None:
|
|
return Coin(bytes32(row[1][0:32]), bytes32(row[1][32:64]), uint64(int.from_bytes(row[1][64:72], "big")))
|
|
return None
|
|
|
|
async def get_all_launchers(self) -> List[bytes32]:
|
|
"""
|
|
Checks DB for all launchers.
|
|
"""
|
|
|
|
async with self.db_wrapper.reader_no_transaction() as conn:
|
|
cursor = await conn.execute("SELECT id from launchers")
|
|
rows = await cursor.fetchall()
|
|
await cursor.close()
|
|
|
|
return [bytes32(row[0]) for row in rows]
|
|
|
|
async def delete_launcher(self, launcher_id: bytes32) -> None:
|
|
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
|
await (await conn.execute("DELETE FROM launchers WHERE id=?", (launcher_id,))).close()
|
|
|
|
async def add_mirror(self, mirror: Mirror) -> None:
|
|
"""
|
|
Add a mirror coin to the DB
|
|
"""
|
|
|
|
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
|
await conn.execute_insert(
|
|
"INSERT OR REPLACE INTO mirrors VALUES (?, ?, ?, ?, ?)",
|
|
(
|
|
mirror.coin_id,
|
|
mirror.launcher_id,
|
|
bytes(mirror.amount),
|
|
b"".join([bytes(uint16(len(url))) + url for url in mirror.urls]), # prefix each item with a length
|
|
1 if mirror.ours else 0,
|
|
),
|
|
)
|
|
|
|
async def get_mirrors(self, launcher_id: bytes32) -> List[Mirror]:
|
|
async with self.db_wrapper.reader_no_transaction() as conn:
|
|
cursor = await conn.execute(
|
|
"SELECT * from mirrors WHERE launcher_id=?",
|
|
(launcher_id,),
|
|
)
|
|
rows = await cursor.fetchall()
|
|
await cursor.close()
|
|
mirrors: List[Mirror] = []
|
|
|
|
for row in rows:
|
|
mirrors.append(_row_to_mirror(row))
|
|
|
|
return mirrors
|
|
|
|
async def get_mirror(self, coin_id: bytes32) -> Mirror:
|
|
async with self.db_wrapper.reader_no_transaction() as conn:
|
|
cursor = await conn.execute(
|
|
"SELECT * from mirrors WHERE coin_id=?",
|
|
(coin_id,),
|
|
)
|
|
row = await cursor.fetchone()
|
|
await cursor.close()
|
|
assert row is not None
|
|
|
|
return _row_to_mirror(row)
|
|
|
|
async def delete_mirror(self, coin_id: bytes32) -> None:
|
|
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
|
await (await conn.execute("DELETE FROM mirrors WHERE coin_id=?", (coin_id,))).close()
|