From efff83e7be543538bb9528a57e4fbd7f5529b77f Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Tue, 12 Sep 2023 09:28:01 -0700 Subject: [PATCH] Add timelock information to Trades and Transactions (#15915) --- chia/data_layer/data_layer_wallet.py | 5 +- chia/pools/pool_wallet.py | 5 +- chia/rpc/wallet_rpc_api.py | 1 + chia/wallet/cat_wallet/cat_wallet.py | 5 +- chia/wallet/conditions.py | 2 +- chia/wallet/did_wallet/did_wallet.py | 8 +- chia/wallet/nft_wallet/nft_wallet.py | 3 +- chia/wallet/trade_manager.py | 16 ++- chia/wallet/trade_record.py | 17 +++- chia/wallet/trading/trade_status.py | 1 + chia/wallet/trading/trade_store.py | 126 ++++++++++-------------- chia/wallet/transaction_record.py | 14 ++- chia/wallet/vc_wallet/cr_cat_wallet.py | 7 +- chia/wallet/vc_wallet/vc_wallet.py | 5 +- chia/wallet/wallet.py | 3 +- chia/wallet/wallet_node.py | 4 +- chia/wallet/wallet_state_manager.py | 8 +- chia/wallet/wallet_transaction_store.py | 73 +++++++++++--- tests/cmds/cmd_test_utils.py | 3 + tests/cmds/wallet/test_consts.py | 2 + tests/cmds/wallet/test_wallet.py | 7 ++ tests/wallet/test_transaction_store.py | 70 ++++++++++++- tests/wallet/test_wallet.py | 2 + tests/wallet/test_wallet_trade_store.py | 65 +++++++++++- 24 files changed, 348 insertions(+), 104 deletions(-) diff --git a/chia/data_layer/data_layer_wallet.py b/chia/data_layer/data_layer_wallet.py index bb8b8cada1..ed463b128c 100644 --- a/chia/data_layer/data_layer_wallet.py +++ b/chia/data_layer/data_layer_wallet.py @@ -25,7 +25,7 @@ from chia.types.condition_opcodes import ConditionOpcode from chia.types.spend_bundle import SpendBundle from chia.util.ints import uint8, uint32, uint64, uint128 from chia.util.streamable import Streamable, streamable -from chia.wallet.conditions import Condition, UnknownCondition +from chia.wallet.conditions import Condition, UnknownCondition, parse_timelock_info from chia.wallet.db_wallet.db_wallet_puzzles import ( ACS_MU, ACS_MU_PH, @@ -355,6 +355,7 @@ class DataLayerWallet: trade_id=None, type=uint32(TransactionType.INCOMING_TX.value), name=full_spend.name(), + valid_times=parse_timelock_info(extra_conditions), ) singleton_record = SingletonRecord( coin_id=Coin(launcher_coin.name(), full_puzzle.get_tree_hash(), uint64(1)).name(), @@ -608,6 +609,7 @@ class DataLayerWallet: trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), name=singleton_record.coin_id, + valid_times=parse_timelock_info(extra_conditions), ) assert dl_tx.spend_bundle is not None if fee > 0: @@ -822,6 +824,7 @@ class DataLayerWallet: trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), name=mirror_bundle.name(), + valid_times=parse_timelock_info(extra_conditions), ) ] diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py index 202fda35ed..d3bbe2cd2f 100644 --- a/chia/pools/pool_wallet.py +++ b/chia/pools/pool_wallet.py @@ -46,7 +46,7 @@ from chia.types.coin_record import CoinRecord from chia.types.coin_spend import CoinSpend, compute_additions from chia.types.spend_bundle import SpendBundle from chia.util.ints import uint32, uint64, uint128 -from chia.wallet.conditions import Condition +from chia.wallet.conditions import Condition, ConditionValidTimes, parse_timelock_info from chia.wallet.derive_keys import find_owner_sk from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_hash_for_synthetic_public_key from chia.wallet.sign_coin_spends import sign_coin_spends @@ -460,6 +460,7 @@ class PoolWallet: trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), name=spend_bundle.name(), + valid_times=parse_timelock_info(extra_conditions), ) await standard_wallet.push_transaction(standard_wallet_record) p2_singleton_puzzle_hash: bytes32 = launcher_id_to_p2_puzzle_hash( @@ -632,6 +633,7 @@ class PoolWallet: memos=[], type=uint32(TransactionType.OUTGOING_TX.value), name=signed_spend_bundle.name(), + valid_times=ConditionValidTimes(), ) await self.publish_transactions(tx_record, fee_tx) @@ -913,6 +915,7 @@ class PoolWallet: trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), name=full_spend.name(), + valid_times=ConditionValidTimes(), ) await self.publish_transactions(absorb_transaction, fee_tx) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index d8999f0053..7aa033b0af 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -3929,6 +3929,7 @@ class WalletRpcApi: parsed_request.min_amount_to_claim, tx_config, fee=parsed_request.fee, + extra_conditions=extra_conditions, ) for tx in txs: await self.service.wallet_state_manager.add_pending_transaction(tx) diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index 396ef2c98e..08987c8d5b 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -32,7 +32,7 @@ from chia.wallet.cat_wallet.cat_utils import ( ) from chia.wallet.cat_wallet.lineage_store import CATLineageStore from chia.wallet.coin_selection import select_coins -from chia.wallet.conditions import Condition, UnknownCondition +from chia.wallet.conditions import Condition, ConditionValidTimes, UnknownCondition, parse_timelock_info from chia.wallet.derivation_record import DerivationRecord from chia.wallet.lineage_proof import LineageProof from chia.wallet.outer_puzzles import AssetType @@ -171,6 +171,7 @@ class CATWallet: type=uint32(TransactionType.INCOMING_TX.value), name=bytes32(token_bytes()), memos=[], + valid_times=ConditionValidTimes(), ) chia_tx = dataclasses.replace(chia_tx, spend_bundle=spend_bundle) await self.standard_wallet.push_transaction(chia_tx) @@ -844,6 +845,7 @@ class CATWallet: type=uint32(TransactionType.OUTGOING_TX.value), name=spend_bundle.name(), memos=list(compute_memos(spend_bundle).items()), + valid_times=parse_timelock_info(extra_conditions), ) ] @@ -866,6 +868,7 @@ class CATWallet: type=chia_tx.type, name=chia_tx.name, memos=[], + valid_times=parse_timelock_info(extra_conditions), ) ) diff --git a/chia/wallet/conditions.py b/chia/wallet/conditions.py index 60246e9fa4..272363a18b 100644 --- a/chia/wallet/conditions.py +++ b/chia/wallet/conditions.py @@ -1210,7 +1210,7 @@ ABSOLUTE_PROPERTIES: Set[str] = {"min_time", "max_time", "min_height", "max_heig ALL_PROPERTIES: Set[str] = SECONDS_PROPERTIES | HEIGHT_PROPERTIES -def parse_timelock_info(conditions: List[Condition]) -> ConditionValidTimes: +def parse_timelock_info(conditions: Iterable[Condition]) -> ConditionValidTimes: valid_times: ConditionValidTimes = ConditionValidTimes() properties: Set[str] = ALL_PROPERTIES.copy() for condition in conditions: diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index c4e2076fcc..5ae6a39ea0 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -20,7 +20,7 @@ from chia.types.coin_spend import CoinSpend from chia.types.spend_bundle import SpendBundle from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict from chia.util.ints import uint32, uint64, uint128 -from chia.wallet.conditions import Condition +from chia.wallet.conditions import Condition, ConditionValidTimes, parse_timelock_info from chia.wallet.derivation_record import DerivationRecord from chia.wallet.derive_keys import master_sk_to_wallet_sk_unhardened from chia.wallet.did_wallet import did_wallet_puzzles @@ -618,6 +618,7 @@ class DIDWallet: type=uint32(TransactionType.OUTGOING_TX.value), name=bytes32(token_bytes()), memos=list(compute_memos(spend_bundle).items()), + valid_times=parse_timelock_info(extra_conditions), ) await self.wallet_state_manager.add_pending_transaction(did_record) @@ -714,6 +715,7 @@ class DIDWallet: type=uint32(TransactionType.OUTGOING_TX.value), name=bytes32(token_bytes()), memos=list(compute_memos(spend_bundle).items()), + valid_times=parse_timelock_info(extra_conditions), ) await self.wallet_state_manager.add_pending_transaction(did_record) return did_record @@ -834,6 +836,7 @@ class DIDWallet: type=uint32(TransactionType.OUTGOING_TX.value), name=bytes32(token_bytes()), memos=list(compute_memos(spend_bundle).items()), + valid_times=ConditionValidTimes(), ) await self.wallet_state_manager.add_pending_transaction(did_record) return spend_bundle @@ -915,6 +918,7 @@ class DIDWallet: type=uint32(TransactionType.INCOMING_TX.value), name=bytes32(token_bytes()), memos=list(compute_memos(spend_bundle).items()), + valid_times=parse_timelock_info(extra_conditions), ) attest_str: str = f"{self.get_my_DID()}:{bytes(message_spend_bundle).hex()}:{coin.parent_coin_info.hex()}:" attest_str += f"{self.did_info.current_inner.get_tree_hash().hex()}:{coin.amount}" @@ -1043,6 +1047,7 @@ class DIDWallet: type=uint32(TransactionType.OUTGOING_TX.value), name=bytes32(token_bytes()), memos=list(compute_memos(spend_bundle).items()), + valid_times=ConditionValidTimes(), ) await self.wallet_state_manager.add_pending_transaction(did_record) new_did_info = DIDInfo( @@ -1284,6 +1289,7 @@ class DIDWallet: type=uint32(TransactionType.INCOMING_TX.value), name=bytes32(token_bytes()), memos=[], + valid_times=ConditionValidTimes(), ) regular_record = dataclasses.replace(tx_record, spend_bundle=None) await self.wallet_state_manager.add_pending_transaction(regular_record) diff --git a/chia/wallet/nft_wallet/nft_wallet.py b/chia/wallet/nft_wallet/nft_wallet.py index 03ceb94dce..54d13de633 100644 --- a/chia/wallet/nft_wallet/nft_wallet.py +++ b/chia/wallet/nft_wallet/nft_wallet.py @@ -23,7 +23,7 @@ from chia.types.spend_bundle import SpendBundle from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict from chia.util.hash import std_hash from chia.util.ints import uint16, uint32, uint64, uint128 -from chia.wallet.conditions import Condition, UnknownCondition +from chia.wallet.conditions import Condition, UnknownCondition, parse_timelock_info from chia.wallet.derivation_record import DerivationRecord from chia.wallet.did_wallet import did_wallet_puzzles from chia.wallet.did_wallet.did_info import DIDInfo @@ -672,6 +672,7 @@ class NFTWallet: type=uint32(TransactionType.OUTGOING_TX.value), name=spend_bundle.name(), memos=list(compute_memos(spend_bundle).items()), + valid_times=parse_timelock_info(extra_conditions), ), ] diff --git a/chia/wallet/trade_manager.py b/chia/wallet/trade_manager.py index 6127506c45..762668a5e0 100644 --- a/chia/wallet/trade_manager.py +++ b/chia/wallet/trade_manager.py @@ -17,7 +17,7 @@ from chia.types.spend_bundle import SpendBundle from chia.util.db_wrapper import DBWrapper2 from chia.util.hash import std_hash from chia.util.ints import uint32, uint64 -from chia.wallet.conditions import Condition +from chia.wallet.conditions import Condition, ConditionValidTimes, parse_conditions_non_consensus, parse_timelock_info from chia.wallet.db_wallet.db_wallet_puzzles import ACS_MU_PH from chia.wallet.nft_wallet.nft_wallet import NFTWallet from chia.wallet.outer_puzzles import AssetType @@ -255,6 +255,7 @@ class TradeManager: continue cancellation_additions: List[Coin] = [] + valid_times: ConditionValidTimes = parse_timelock_info(extra_conditions) for coin in Offer.from_bytes(trade.offer).get_cancellation_coins(): wallet = await self.wallet_state_manager.get_wallet_for_coin(coin.name()) @@ -329,6 +330,7 @@ class TradeManager: type=uint32(TransactionType.INCOMING_TX.value), name=cancellation_additions[0].name(), memos=[], + valid_times=valid_times, ) ) @@ -397,6 +399,7 @@ class TradeManager: trade_id=created_offer.name(), status=uint32(TradeStatus.PENDING_ACCEPT.value), sent_to=[], + valid_times=parse_timelock_info(extra_conditions), ) if success is True and trade_offer is not None and not validate_only: @@ -617,6 +620,13 @@ class TradeManager: additions_dict.update({id: hc.coin for id, hc in hinted_coins.items()}) removals: List[Coin] = final_spend_bundle.removals() additions: List[Coin] = list(a for a in additions_dict.values() if a not in removals) + valid_times: ConditionValidTimes = parse_timelock_info( + parse_conditions_non_consensus( + condition + for spend in final_spend_bundle.coin_spends + for condition in spend.puzzle_reveal.to_program().run(spend.solution.to_program()).as_iter() + ) + ) all_fees = uint64(final_spend_bundle.fees()) txs = [] @@ -649,6 +659,7 @@ class TradeManager: type=uint32(TransactionType.INCOMING_TRADE.value), name=std_hash(final_spend_bundle.name() + addition.name()), memos=[(coin_id, [hint]) for coin_id, hint in hint_dict.items()], + valid_times=valid_times, ) ) else: # This is change @@ -696,6 +707,7 @@ class TradeManager: type=uint32(TransactionType.OUTGOING_TRADE.value), name=std_hash(final_spend_bundle.name() + removal_tree_hash), memos=[(coin_id, [hint]) for coin_id, hint in hint_dict.items()], + valid_times=valid_times, ) ) @@ -772,6 +784,7 @@ class TradeManager: trade_id=complete_offer.name(), status=uint32(TradeStatus.PENDING_CONFIRM.value), sent_to=[], + valid_times=parse_timelock_info(extra_conditions), ) await self.save_trade(trade_record, offer) @@ -794,6 +807,7 @@ class TradeManager: type=uint32(TransactionType.OUTGOING_TRADE.value), name=final_spend_bundle.name(), memos=[], + valid_times=ConditionValidTimes(), ) await self.wallet_state_manager.add_pending_transaction(push_tx) for tx in tx_records: diff --git a/chia/wallet/trade_record.py b/chia/wallet/trade_record.py index 57a49036f6..845eab3510 100644 --- a/chia/wallet/trade_record.py +++ b/chia/wallet/trade_record.py @@ -1,19 +1,22 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint8, uint32, uint64 from chia.util.streamable import Streamable, streamable +from chia.wallet.conditions import ConditionValidTimes from chia.wallet.trading.offer import Offer from chia.wallet.trading.trade_status import TradeStatus +_T_TradeRecord = TypeVar("_T_TradeRecord", bound="TradeRecordOld") + @streamable @dataclass(frozen=True) -class TradeRecord(Streamable): +class TradeRecordOld(Streamable): """ Used for storing transaction data and status in wallets. """ @@ -47,10 +50,18 @@ class TradeRecord(Streamable): return formatted @classmethod - def from_json_dict_convenience(cls, record: Dict[str, Any], offer: str = "") -> "TradeRecord": + def from_json_dict_convenience( + cls: Type[_T_TradeRecord], record: Dict[str, Any], offer: str = "" + ) -> _T_TradeRecord: new_record = record.copy() new_record["status"] = TradeStatus[record["status"]].value del new_record["summary"] del new_record["pending"] new_record["offer"] = offer return cls.from_json_dict(new_record) + + +@streamable +@dataclass(frozen=True) +class TradeRecord(TradeRecordOld): + valid_times: ConditionValidTimes diff --git a/chia/wallet/trading/trade_status.py b/chia/wallet/trading/trade_status.py index 9e4d1b9796..2ee463f50d 100644 --- a/chia/wallet/trading/trade_status.py +++ b/chia/wallet/trading/trade_status.py @@ -10,3 +10,4 @@ class TradeStatus(Enum): CANCELLED = 3 CONFIRMED = 4 FAILED = 5 + EXPIRED = 6 diff --git a/chia/wallet/trading/trade_store.py b/chia/wallet/trading/trade_store.py index 855041c5b1..6cd55aac44 100644 --- a/chia/wallet/trading/trade_store.py +++ b/chia/wallet/trading/trade_store.py @@ -1,8 +1,9 @@ from __future__ import annotations +import dataclasses import logging from time import perf_counter -from typing import List, Optional, Set, Tuple +from typing import Dict, List, Optional, Set, Tuple import aiosqlite @@ -11,7 +12,8 @@ from chia.types.mempool_inclusion_status import MempoolInclusionStatus from chia.util.db_wrapper import DBWrapper2 from chia.util.errors import Err from chia.util.ints import uint8, uint32 -from chia.wallet.trade_record import TradeRecord +from chia.wallet.conditions import ConditionValidTimes +from chia.wallet.trade_record import TradeRecord, TradeRecordOld from chia.wallet.trading.offer import Offer from chia.wallet.trading.trade_status import TradeStatus @@ -24,7 +26,7 @@ async def migrate_coin_of_interest(log: logging.Logger, db: aiosqlite.Connection inserts: List[Tuple[bytes32, bytes32]] = [] for row in rows: - record: TradeRecord = TradeRecord.from_bytes(row[0]) + record: TradeRecordOld = TradeRecordOld.from_bytes(row[0]) for coin in record.coins_of_interest: inserts.append((coin.name(), record.trade_id)) @@ -44,7 +46,7 @@ async def migrate_coin_of_interest(log: logging.Logger, db: aiosqlite.Connection ) -async def migrate_is_my_offer(log: logging.Logger, db_connection: aiosqlite.Connection) -> None: +async def migrate_is_my_offer(log: logging.Logger, db_connection: aiosqlite.Connection) -> None: # pragma: no cover """ Migrate the is_my_offer property contained in the serialized TradeRecord (trade_record column) to the is_my_offer column in the trade_records table. @@ -58,7 +60,7 @@ async def migrate_is_my_offer(log: logging.Logger, db_connection: aiosqlite.Conn updates: List[Tuple[int, str]] = [] for row in rows: - record = TradeRecord.from_bytes(row[0]) + record = TradeRecordOld.from_bytes(row[0]) is_my_offer = 1 if record.is_my_offer else 0 updates.append((is_my_offer, row[1])) @@ -140,6 +142,17 @@ class TradeStore: except aiosqlite.OperationalError: pass # ignore what is likely Duplicate column error + try: + await conn.execute("CREATE TABLE trade_record_times(trade_id blob PRIMARY KEY, valid_times blob)") + async with await conn.execute("SELECT trade_id from trade_records") as cursor: + trade_ids: List[bytes32] = [bytes32.from_hexstr(row[0]) for row in await cursor.fetchall()] + await conn.executemany( + "INSERT INTO trade_record_times (trade_id, valid_times) VALUES(?, ?)", + [(id, bytes(ConditionValidTimes())) for id in trade_ids], + ) + except aiosqlite.OperationalError: + pass # ignore what is likely Duplicate table error + await conn.execute("CREATE INDEX IF NOT EXISTS trade_confirmed_index on trade_records(confirmed_at_index)") await conn.execute("CREATE INDEX IF NOT EXISTS trade_status on trade_records(status)") await conn.execute("CREATE INDEX IF NOT EXISTS trade_id on trade_records(trade_id)") @@ -167,7 +180,9 @@ class TradeStore: "(trade_record, trade_id, status, confirmed_at_index, created_at_time, sent, offer_name, is_my_offer) " "VALUES(?, ?, ?, ?, ?, ?, ?, ?)", ( - bytes(record), + bytes( + TradeRecordOld(**{k: v for k, v in dataclasses.asdict(record).items() if k != "valid_times"}) + ), record.trade_id.hex(), record.status, record.confirmed_at_index, @@ -178,6 +193,14 @@ class TradeStore: ), ) await cursor.close() + cursor = await conn.execute( + "INSERT OR REPLACE INTO trade_record_times " "(trade_id, valid_times) " "VALUES(?, ?)", + ( + record.trade_id, + bytes(record.valid_times), + ), + ) + await cursor.close() # remove all current coin ids await conn.execute("DELETE FROM coin_of_interest_to_trade_record WHERE trade_id=?", (record.trade_id,)) # now recreate them all @@ -217,6 +240,7 @@ class TradeStore: trade_id=current.trade_id, status=uint32(status.value), sent_to=current.sent_to, + valid_times=current.valid_times, ) await self.add_trade_record(tx, offer_name) @@ -254,6 +278,7 @@ class TradeStore: trade_id=current.trade_id, status=current.status, sent_to=sent_to, + valid_times=current.valid_times, ) offer = Offer.from_bytes(current.offer) await self.add_trade_record(tx, offer.name()) @@ -296,8 +321,7 @@ class TradeStore: row = await cursor.fetchone() await cursor.close() if row is not None: - record: TradeRecord = TradeRecord.from_bytes(row[0]) - return record + return (await self._get_new_trade_records_from_old([TradeRecordOld.from_bytes(row[0])]))[0] return None async def get_trade_record_with_status(self, status: TradeStatus) -> List[TradeRecord]: @@ -308,12 +332,8 @@ class TradeStore: cursor = await conn.execute("SELECT trade_record from trade_records WHERE status=?", (status.value,)) rows = await cursor.fetchall() await cursor.close() - records = [] - for row in rows: - record = TradeRecord.from_bytes(row[0]) - records.append(record) - return records + return await self._get_new_trade_records_from_old([TradeRecordOld.from_bytes(row[0]) for row in rows]) async def get_coin_ids_of_interest_with_trade_statuses(self, trade_statuses: List[TradeStatus]) -> Set[bytes32]: """ @@ -330,39 +350,6 @@ class TradeStore: ) return {bytes32(row[0]) for row in rows} - async def get_not_sent(self) -> List[TradeRecord]: - """ - Returns the list of trades that have not been received by full node yet. - """ - - async with self.db_wrapper.reader_no_transaction() as conn: - cursor = await conn.execute("SELECT trade_record from trade_records WHERE sent List[TradeRecord]: - """ - Returns the list of all trades that have not yet been confirmed. - """ - - async with self.db_wrapper.reader_no_transaction() as conn: - cursor = await conn.execute("SELECT trade_record from trade_records WHERE confirmed=?", (0,)) - rows = await cursor.fetchall() - await cursor.close() - records = [] - - for row in rows: - record = TradeRecord.from_bytes(row[0]) - records.append(record) - - return records - async def get_all_trades(self) -> List[TradeRecord]: """ Returns all stored trades. @@ -372,13 +359,8 @@ class TradeStore: cursor = await conn.execute("SELECT trade_record from trade_records") rows = await cursor.fetchall() await cursor.close() - records = [] - for row in rows: - record = TradeRecord.from_bytes(row[0]) - records.append(record) - - return records + return await self._get_new_trade_records_from_old([TradeRecordOld.from_bytes(row[0]) for row in rows]) async def get_trades_between( self, @@ -481,29 +463,29 @@ class TradeStore: rows = await cursor.fetchall() await cursor.close() - records = [] - - for row in rows: - record = TradeRecord.from_bytes(row[0]) - records.append(record) - - return records - - async def get_trades_above(self, height: uint32) -> List[TradeRecord]: - async with self.db_wrapper.reader_no_transaction() as conn: - cursor = await conn.execute("SELECT trade_record from trade_records WHERE confirmed_at_index>?", (height,)) - rows = await cursor.fetchall() - await cursor.close() - records = [] - - for row in rows: - record = TradeRecord.from_bytes(row[0]) - records.append(record) - - return records + return await self._get_new_trade_records_from_old([TradeRecordOld.from_bytes(row[0]) for row in rows]) async def rollback_to_block(self, block_index: int) -> None: async with self.db_wrapper.writer_maybe_transaction() as conn: # Delete from storage cursor = await conn.execute("DELETE FROM trade_records WHERE confirmed_at_index>?", (block_index,)) await cursor.close() + + async def _get_new_trade_records_from_old(self, old_records: List[TradeRecordOld]) -> List[TradeRecord]: + async with self.db_wrapper.reader_no_transaction() as conn: + cursor = await conn.execute( + "SELECT trade_id, valid_times from trade_record_times WHERE " + f"trade_id IN ({','.join('?' * len(old_records))})", + tuple(trade.trade_id for trade in old_records), + ) + valid_times: Dict[bytes32, ConditionValidTimes] = { + bytes32(res[0]): ConditionValidTimes.from_bytes(res[1]) for res in await cursor.fetchall() + } + await cursor.close() + return [ + TradeRecord( + valid_times=valid_times[record.trade_id] if record.trade_id in valid_times else ConditionValidTimes(), + **dataclasses.asdict(record), + ) + for record in old_records + ] diff --git a/chia/wallet/transaction_record.py b/chia/wallet/transaction_record.py index 45e5712309..47d76da16a 100644 --- a/chia/wallet/transaction_record.py +++ b/chia/wallet/transaction_record.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Dict, Generic, List, Optional, Tuple, TypeVar +from typing import Dict, Generic, List, Optional, Tuple, Type, TypeVar from chia.consensus.coinbase import farmer_parent_id, pool_parent_id from chia.types.blockchain_format.coin import Coin @@ -12,9 +12,11 @@ from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash from chia.util.errors import Err from chia.util.ints import uint8, uint32, uint64 from chia.util.streamable import Streamable, streamable +from chia.wallet.conditions import ConditionValidTimes from chia.wallet.util.transaction_type import TransactionType T = TypeVar("T") +_T_TransactionRecord = TypeVar("_T_TransactionRecord", bound="TransactionRecordOld") minimum_send_attempts = 6 @@ -27,7 +29,7 @@ class ItemAndTransactionRecords(Generic[T]): @streamable @dataclass(frozen=True) -class TransactionRecord(Streamable): +class TransactionRecordOld(Streamable): """ Used for storing transaction data and status in wallets. """ @@ -81,7 +83,7 @@ class TransactionRecord(Streamable): return {coin_id: ms for coin_id, ms in self.memos} @classmethod - def from_json_dict_convenience(cls, modified_tx_input: Dict): + def from_json_dict_convenience(cls: Type[_T_TransactionRecord], modified_tx_input: Dict) -> _T_TransactionRecord: modified_tx = modified_tx_input.copy() if "to_address" in modified_tx: modified_tx["to_puzzle_hash"] = decode_puzzle_hash(modified_tx["to_address"]).hex() @@ -127,3 +129,9 @@ class TransactionRecord(Streamable): def hint_dict(self) -> Dict[bytes32, bytes32]: return {coin_id: bytes32(memos[0]) for coin_id, memos in self.memos if len(memos) > 0 and len(memos[0]) == 32} + + +@streamable +@dataclass(frozen=True) +class TransactionRecord(TransactionRecordOld): + valid_times: ConditionValidTimes diff --git a/chia/wallet/vc_wallet/cr_cat_wallet.py b/chia/wallet/vc_wallet/cr_cat_wallet.py index f5bc9cd3ed..8c4b38a5a1 100644 --- a/chia/wallet/vc_wallet/cr_cat_wallet.py +++ b/chia/wallet/vc_wallet/cr_cat_wallet.py @@ -24,7 +24,7 @@ from chia.wallet.cat_wallet.cat_info import CATCoinData, CRCATInfo from chia.wallet.cat_wallet.cat_utils import CAT_MOD, construct_cat_puzzle from chia.wallet.cat_wallet.cat_wallet import CATWallet from chia.wallet.coin_selection import select_coins -from chia.wallet.conditions import Condition, UnknownCondition +from chia.wallet.conditions import Condition, ConditionValidTimes, UnknownCondition, parse_timelock_info from chia.wallet.lineage_proof import LineageProof from chia.wallet.outer_puzzles import AssetType from chia.wallet.payment import Payment @@ -249,6 +249,7 @@ class CRCATWallet(CATWallet): type=uint32(TransactionType.INCOMING_CRCAT_PENDING), name=coin.name(), memos=list(memos.items()), + valid_times=ConditionValidTimes(), ) await self.wallet_state_manager.tx_store.add_transaction_record(tx_record) else: # pragma: no cover @@ -694,6 +695,7 @@ class CRCATWallet(CATWallet): type=uint32(TransactionType.OUTGOING_TX.value), name=signed_spend_bundle.name(), memos=list(compute_memos(signed_spend_bundle).items()), + valid_times=parse_timelock_info(extra_conditions), ) for i, payment in enumerate(payments) ] @@ -710,6 +712,7 @@ class CRCATWallet(CATWallet): max_coin_amount: Optional[uint64] = None, excluded_coin_amounts: Optional[List[uint64]] = None, reuse_puzhash: Optional[bool] = None, + extra_conditions: Tuple[Condition, ...] = tuple(), ) -> List[TransactionRecord]: # Select the relevant CR-CAT coins crcat_records: Set[WalletCoinRecord] = await self.wallet_state_manager.coin_store.get_unspent_coins_for_wallet( @@ -799,6 +802,7 @@ class CRCATWallet(CATWallet): puzzle_announcements=set(crcat.expected_announcement() for crcat, _ in crcats_and_puzhashes), coin_announcements={nonce}, coin_announcements_to_consume=set(expected_announcements), + extra_conditions=extra_conditions, ) claim_bundle = SpendBundle.aggregate( [claim_bundle, *(tx.spend_bundle for tx in vc_txs if tx.spend_bundle is not None)] @@ -822,6 +826,7 @@ class CRCATWallet(CATWallet): type=uint32(TransactionType.INCOMING_TX.value), name=claim_bundle.name(), memos=list(compute_memos(claim_bundle).items()), + valid_times=parse_timelock_info(extra_conditions), ), *(dataclasses.replace(tx, spend_bundle=None) for tx in vc_txs), *((dataclasses.replace(chia_tx, spend_bundle=None),) if chia_tx is not None else []), diff --git a/chia/wallet/vc_wallet/vc_wallet.py b/chia/wallet/vc_wallet/vc_wallet.py index 65aa87aa23..798b1559c2 100644 --- a/chia/wallet/vc_wallet/vc_wallet.py +++ b/chia/wallet/vc_wallet/vc_wallet.py @@ -21,7 +21,7 @@ from chia.types.spend_bundle import SpendBundle from chia.util.hash import std_hash from chia.util.ints import uint32, uint64, uint128 from chia.util.streamable import Streamable -from chia.wallet.conditions import Condition, UnknownCondition +from chia.wallet.conditions import Condition, UnknownCondition, parse_timelock_info from chia.wallet.did_wallet.did_wallet import DIDWallet from chia.wallet.payment import Payment from chia.wallet.puzzle_drivers import Solver @@ -216,6 +216,7 @@ class VCWallet: type=uint32(TransactionType.OUTGOING_TX.value), name=spend_bundle.name(), memos=list(compute_memos(spend_bundle).items()), + valid_times=parse_timelock_info(extra_conditions), ) return vc_record, [tx] @@ -356,6 +357,7 @@ class VCWallet: type=uint32(TransactionType.OUTGOING_TX.value), name=spend_bundle.name(), memos=list(compute_memos(spend_bundle).items()), + valid_times=parse_timelock_info(extra_conditions), ) ) return tx_list @@ -434,6 +436,7 @@ class VCWallet: type=uint32(TransactionType.OUTGOING_TX.value), name=final_bundle.name(), memos=list(compute_memos(final_bundle).items()), + valid_times=parse_timelock_info(extra_conditions), ) if fee > 0: chia_tx: TransactionRecord = await self.wallet_state_manager.main_wallet.create_tandem_xch_tx( diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 506d42ac69..82f37ca55a 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -18,7 +18,7 @@ from chia.util.hash import std_hash from chia.util.ints import uint32, uint64, uint128 from chia.util.streamable import Streamable from chia.wallet.coin_selection import select_coins -from chia.wallet.conditions import Condition +from chia.wallet.conditions import Condition, parse_timelock_info from chia.wallet.derivation_record import DerivationRecord from chia.wallet.payment import Payment from chia.wallet.puzzles.clawback.metadata import ClawbackMetadata @@ -495,6 +495,7 @@ class Wallet: type=uint32(TransactionType.OUTGOING_TX.value), name=spend_bundle.name(), memos=list(compute_memos(spend_bundle).items()), + valid_times=parse_timelock_info(extra_conditions), ) async def create_tandem_xch_tx( diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index de915362b4..d2c187d06c 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -269,7 +269,7 @@ class WalletNode: async def reset_sync_db(self, db_path: Union[Path, str], fingerprint: int) -> bool: conn: aiosqlite.Connection # are not part of core wallet tables, but might appear later - ignore_tables = {"lineage_proofs_", "sqlite_"} + ignore_tables = {"lineage_proofs_", "sqlite_", "MIGRATED_VALID_TIMES_TXS", "MIGRATED_VALID_TIMES_TRADES"} required_tables = [ "coin_record", "transaction_record", @@ -280,6 +280,8 @@ class WalletNode: "all_notification_ids", "key_val_store", "trade_records", + "trade_record_times", + "tx_times", "pool_state_transitions", "singleton_records", "mirrors", diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index a1f3dc479e..e621001385 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -51,7 +51,7 @@ from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS from chia.wallet.cat_wallet.cat_info import CATCoinData, CATInfo, CRCATInfo from chia.wallet.cat_wallet.cat_utils import CAT_MOD, CAT_MOD_HASH, construct_cat_puzzle, match_cat_puzzle from chia.wallet.cat_wallet.cat_wallet import CATWallet -from chia.wallet.conditions import Condition +from chia.wallet.conditions import Condition, ConditionValidTimes, parse_timelock_info from chia.wallet.db_wallet.db_wallet_puzzles import MIRROR_PUZZLE_HASH from chia.wallet.derivation_record import DerivationRecord from chia.wallet.derive_keys import ( @@ -896,6 +896,7 @@ class WalletStateManager: type=uint32(TransactionType.OUTGOING_CLAWBACK), name=spend_bundle.name(), memos=list(compute_memos(spend_bundle).items()), + valid_times=parse_timelock_info(extra_conditions), ) await self.add_pending_transaction(tx_record) # Update incoming tx to prevent double spend and mark it is pending @@ -1335,6 +1336,7 @@ class WalletStateManager: type=uint32(TransactionType.OUTGOING_CLAWBACK), name=clawback_spend_bundle.name(), memos=list(compute_memos(clawback_spend_bundle).items()), + valid_times=ConditionValidTimes(), ) await self.tx_store.add_transaction_record(tx_record) coin_record = WalletCoinRecord( @@ -1376,6 +1378,7 @@ class WalletStateManager: # Use coin ID as the TX ID to mapping with the coin table name=coin_record.coin.name(), memos=list(memos.items()), + valid_times=ConditionValidTimes(), ) await self.tx_store.add_transaction_record(tx_record) return None @@ -1561,6 +1564,7 @@ class WalletStateManager: type=uint32(tx_type), name=bytes32(token_bytes()), memos=[], + valid_times=ConditionValidTimes(), ) await self.tx_store.add_transaction_record(tx_record) @@ -1640,6 +1644,7 @@ class WalletStateManager: type=uint32(TransactionType.OUTGOING_TX.value), name=tx_name, memos=[], + valid_times=ConditionValidTimes(), ) await self.tx_store.add_transaction_record(tx_record) @@ -1958,6 +1963,7 @@ class WalletStateManager: type=uint32(tx_type), name=coin_name, memos=[], + valid_times=ConditionValidTimes(), ) if tx_record.amount > 0: await self.tx_store.add_transaction_record(tx_record) diff --git a/chia/wallet/wallet_transaction_store.py b/chia/wallet/wallet_transaction_store.py index ea216b4941..db37abee50 100644 --- a/chia/wallet/wallet_transaction_store.py +++ b/chia/wallet/wallet_transaction_store.py @@ -5,12 +5,15 @@ import logging import time from typing import Dict, List, Optional, Tuple +import aiosqlite + from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.mempool_inclusion_status import MempoolInclusionStatus from chia.util.db_wrapper import DBWrapper2 from chia.util.errors import Err from chia.util.ints import uint8, uint32 -from chia.wallet.transaction_record import TransactionRecord, minimum_send_attempts +from chia.wallet.conditions import ConditionValidTimes +from chia.wallet.transaction_record import TransactionRecord, TransactionRecordOld, minimum_send_attempts from chia.wallet.transaction_sorting import SortKey from chia.wallet.util.query_filter import FilterMode, TransactionTypeFilter from chia.wallet.util.transaction_type import TransactionType @@ -81,6 +84,17 @@ class WalletTransactionStore: "CREATE INDEX IF NOT EXISTS transaction_record_wallet_id on transaction_record(wallet_id)" ) + try: + await conn.execute("CREATE TABLE tx_times(txid blob PRIMARY KEY, valid_times blob)") + async with await conn.execute("SELECT bundle_id from transaction_record") as cursor: + txids: List[bytes32] = [bytes32(row[0]) for row in await cursor.fetchall()] + await conn.executemany( + "INSERT INTO tx_times (txid, valid_times) VALUES(?, ?)", + [(id, bytes(ConditionValidTimes())) for id in txids], + ) + except aiosqlite.OperationalError: + pass # ignore what is likely Duplicate table error + self.tx_submitted = {} self.last_wallet_tx_resend_time = int(time.time()) return self @@ -93,7 +107,16 @@ class WalletTransactionStore: await conn.execute_insert( "INSERT OR REPLACE INTO transaction_record VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ( - bytes(record), + bytes( + TransactionRecordOld( + spend_bundle=record.spend_bundle, + **{ + k: v + for k, v in dataclasses.asdict(record).items() + if k not in ("valid_times", "spend_bundle") + }, + ) + ), record.name, record.confirmed_at_height, record.created_at_time, @@ -107,6 +130,13 @@ class WalletTransactionStore: record.type, ), ) + await conn.execute_insert( + "INSERT OR REPLACE INTO tx_times " "(txid, valid_times) " "VALUES(?, ?)", + ( + record.name, + bytes(record.valid_times), + ), + ) async def delete_transaction_record(self, tx_id: bytes32) -> None: async with self.db_wrapper.writer_maybe_transaction() as conn: @@ -184,7 +214,7 @@ class WalletTransactionStore: ) ) if len(rows) > 0: - return TransactionRecord.from_bytes(rows[0][0]) + return (await self._get_new_tx_records_from_old([TransactionRecordOld.from_bytes(rows[0][0])]))[0] return None # TODO: This should probably be split into separate function, one that @@ -203,7 +233,7 @@ class WalletTransactionStore: records = [] for row in rows: - record = TransactionRecord.from_bytes(row[0]) + record = (await self._get_new_tx_records_from_old([TransactionRecordOld.from_bytes(row[0])]))[0] if include_accepted_txs: # Reset the "sent" state for peers that have replied about this transaction. Retain errors. record = dataclasses.replace(record, sent=1, sent_to=filter_ok_mempool_status(record.sent_to)) @@ -236,7 +266,7 @@ class WalletTransactionStore: "SELECT transaction_record from transaction_record WHERE confirmed=1 and (type=? or type=?)", (fee_int, pool_int), ) - return [TransactionRecord.from_bytes(row[0]) for row in rows] + return await self._get_new_tx_records_from_old([TransactionRecordOld.from_bytes(row[0]) for row in rows]) async def get_all_unconfirmed(self) -> List[TransactionRecord]: """ @@ -244,7 +274,7 @@ class WalletTransactionStore: """ async with self.db_wrapper.reader_no_transaction() as conn: rows = await conn.execute_fetchall("SELECT transaction_record from transaction_record WHERE confirmed=0") - return [TransactionRecord.from_bytes(row[0]) for row in rows] + return await self._get_new_tx_records_from_old([TransactionRecordOld.from_bytes(row[0]) for row in rows]) async def get_unconfirmed_for_wallet(self, wallet_id: int) -> List[TransactionRecord]: """ @@ -254,7 +284,7 @@ class WalletTransactionStore: rows = await conn.execute_fetchall( "SELECT transaction_record from transaction_record WHERE confirmed=0 AND wallet_id=?", (wallet_id,) ) - return [TransactionRecord.from_bytes(row[0]) for row in rows] + return await self._get_new_tx_records_from_old([TransactionRecordOld.from_bytes(row[0]) for row in rows]) async def get_transactions_between( self, @@ -307,7 +337,7 @@ class WalletTransactionStore: (wallet_id,), ) - return [TransactionRecord.from_bytes(row[0]) for row in rows] + return await self._get_new_tx_records_from_old([TransactionRecordOld.from_bytes(row[0]) for row in rows]) async def get_transaction_count_for_wallet( self, @@ -352,7 +382,7 @@ class WalletTransactionStore: type, ), ) - return [TransactionRecord.from_bytes(row[0]) for row in rows] + return await self._get_new_tx_records_from_old([TransactionRecordOld.from_bytes(row[0]) for row in rows]) async def get_all_transactions(self) -> List[TransactionRecord]: """ @@ -360,7 +390,7 @@ class WalletTransactionStore: """ async with self.db_wrapper.reader_no_transaction() as conn: rows = await conn.execute_fetchall("SELECT transaction_record from transaction_record") - return [TransactionRecord.from_bytes(row[0]) for row in rows] + return await self._get_new_tx_records_from_old([TransactionRecordOld.from_bytes(row[0]) for row in rows]) async def get_transaction_above(self, height: int) -> List[TransactionRecord]: # Can be -1 (get all tx) @@ -369,14 +399,14 @@ class WalletTransactionStore: rows = await conn.execute_fetchall( "SELECT transaction_record from transaction_record WHERE confirmed_at_height>?", (height,) ) - return [TransactionRecord.from_bytes(row[0]) for row in rows] + return await self._get_new_tx_records_from_old([TransactionRecordOld.from_bytes(row[0]) for row in rows]) async def get_transactions_by_trade_id(self, trade_id: bytes32) -> List[TransactionRecord]: async with self.db_wrapper.reader_no_transaction() as conn: rows = await conn.execute_fetchall( "SELECT transaction_record from transaction_record WHERE trade_id=?", (trade_id,) ) - return [TransactionRecord.from_bytes(row[0]) for row in rows] + return await self._get_new_tx_records_from_old([TransactionRecordOld.from_bytes(row[0]) for row in rows]) async def rollback_to_block(self, height: int): # Delete from storage @@ -396,3 +426,22 @@ class WalletTransactionStore: ), ) ).close() + + async def _get_new_tx_records_from_old(self, old_records: List[TransactionRecordOld]) -> List[TransactionRecord]: + async with self.db_wrapper.reader_no_transaction() as conn: + cursor = await conn.execute( + f"SELECT txid, valid_times from tx_times WHERE txid IN ({','.join('?' * len(old_records))})", + tuple(tx.name for tx in old_records), + ) + valid_times: Dict[bytes32, ConditionValidTimes] = { + bytes32(res[0]): ConditionValidTimes.from_bytes(res[1]) for res in await cursor.fetchall() + } + await cursor.close() + return [ + TransactionRecord( + valid_times=valid_times[record.name] if record.name in valid_times else ConditionValidTimes(), + spend_bundle=record.spend_bundle, + **{k: v for k, v in dataclasses.asdict(record).items() if k != "spend_bundle"}, + ) + for record in old_records + ] diff --git a/tests/cmds/cmd_test_utils.py b/tests/cmds/cmd_test_utils.py index ce6e977c5a..df2085e236 100644 --- a/tests/cmds/cmd_test_utils.py +++ b/tests/cmds/cmd_test_utils.py @@ -27,6 +27,7 @@ from chia.types.spend_bundle import SpendBundle from chia.util.bech32m import encode_puzzle_hash from chia.util.config import load_config from chia.util.ints import uint8, uint16, uint32, uint64 +from chia.wallet.conditions import ConditionValidTimes from chia.wallet.nft_wallet.nft_info import NFTInfo from chia.wallet.nft_wallet.nft_wallet import NFTWallet from chia.wallet.transaction_record import TransactionRecord @@ -126,6 +127,7 @@ class TestWalletRpcClient(TestRpcClient): type=uint32(TransactionType.OUTGOING_TX.value), name=bytes32([2] * 32), memos=[(bytes32([3] * 32), [bytes([4] * 32)])], + valid_times=ConditionValidTimes(), ) async def get_cat_name(self, wallet_id: int) -> str: @@ -270,6 +272,7 @@ class TestWalletRpcClient(TestRpcClient): type=uint32(TransactionType.OUTGOING_TX.value), name=bytes32([2] * 32), memos=[(bytes32([3] * 32), [bytes([4] * 32)])], + valid_times=ConditionValidTimes(), ) diff --git a/tests/cmds/wallet/test_consts.py b/tests/cmds/wallet/test_consts.py index dd3f5ed014..b3cfffdc36 100644 --- a/tests/cmds/wallet/test_consts.py +++ b/tests/cmds/wallet/test_consts.py @@ -6,6 +6,7 @@ from chia_rs import Coin from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.spend_bundle import SpendBundle from chia.util.ints import uint8, uint32, uint64 +from chia.wallet.conditions import ConditionValidTimes from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.transaction_type import TransactionType @@ -39,4 +40,5 @@ STD_TX = TransactionRecord( type=uint32(TransactionType.OUTGOING_TX.value), name=get_bytes32(2), memos=[(get_bytes32(3), [bytes([4] * 32)])], + valid_times=ConditionValidTimes(), ) diff --git a/tests/cmds/wallet/test_wallet.py b/tests/cmds/wallet/test_wallet.py index 45c9700249..4617f2aa81 100644 --- a/tests/cmds/wallet/test_wallet.py +++ b/tests/cmds/wallet/test_wallet.py @@ -13,6 +13,7 @@ from chia.types.signing_mode import SigningMode from chia.types.spend_bundle import SpendBundle from chia.util.bech32m import encode_puzzle_hash from chia.util.ints import uint8, uint32, uint64 +from chia.wallet.conditions import ConditionValidTimes from chia.wallet.trade_record import TradeRecord from chia.wallet.trading.offer import Offer from chia.wallet.trading.trade_status import TradeStatus @@ -121,6 +122,7 @@ def test_get_transactions(capsys: object, get_test_cli_clients: Tuple[TestRpcCli type=uint32(t_type.value), name=bytes32([2 + i] * 32), memos=[(bytes32([3 + i] * 32), [bytes([4 + i] * 32)])], + valid_times=ConditionValidTimes(), ) l_tx_rec.append(tx_rec) @@ -330,6 +332,7 @@ def test_send(capsys: object, get_test_cli_clients: Tuple[TestRpcClients, Path]) type=uint32(TransactionType.OUTGOING_CLAWBACK.value), name=get_bytes32(2), memos=[(get_bytes32(3), [bytes([4] * 32)])], + valid_times=ConditionValidTimes(), ) return tx_rec @@ -660,6 +663,7 @@ def test_make_offer(capsys: object, get_test_cli_clients: Tuple[TestRpcClients, coins_of_interest=[], trade_id=get_bytes32(2), status=uint32(TradeStatus.PENDING_ACCEPT.value), + valid_times=ConditionValidTimes(), ) return created_offer, trade_offer @@ -796,6 +800,7 @@ def test_get_offers(capsys: object, get_test_cli_clients: Tuple[TestRpcClients, ], trade_id=bytes32([1 + i] * 32), status=uint32(TradeStatus.PENDING_ACCEPT.value), + valid_times=ConditionValidTimes(), ) records.append(trade_offer) return records @@ -856,6 +861,7 @@ def test_take_offer(capsys: object, get_test_cli_clients: Tuple[TestRpcClients, coins_of_interest=offer.get_involved_coins(), trade_id=offer.name(), status=uint32(TradeStatus.PENDING_ACCEPT.value), + valid_times=ConditionValidTimes(), ) inst_rpc_client = TakeOfferRpcClient() # pylint: disable=no-value-for-parameter @@ -905,6 +911,7 @@ def test_cancel_offer(capsys: object, get_test_cli_clients: Tuple[TestRpcClients coins_of_interest=offer.get_involved_coins(), trade_id=offer.name(), status=uint32(TradeStatus.PENDING_ACCEPT.value), + valid_times=ConditionValidTimes(), ) async def cancel_offer( diff --git a/tests/wallet/test_transaction_store.py b/tests/wallet/test_transaction_store.py index d0092adf19..978c110d4b 100644 --- a/tests/wallet/test_transaction_store.py +++ b/tests/wallet/test_transaction_store.py @@ -11,7 +11,8 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.mempool_inclusion_status import MempoolInclusionStatus from chia.util.errors import Err from chia.util.ints import uint8, uint32, uint64 -from chia.wallet.transaction_record import TransactionRecord, minimum_send_attempts +from chia.wallet.conditions import ConditionValidTimes +from chia.wallet.transaction_record import TransactionRecord, TransactionRecordOld, minimum_send_attempts from chia.wallet.util.query_filter import TransactionTypeFilter from chia.wallet.util.transaction_type import TransactionType from chia.wallet.wallet_transaction_store import WalletTransactionStore, filter_ok_mempool_status @@ -38,6 +39,7 @@ tr1 = TransactionRecord( uint32(TransactionType.OUTGOING_TX), # type bytes32(token_bytes(32)), # name [], # List[Tuple[bytes32, List[bytes]]] memos + ConditionValidTimes(), ) @@ -738,3 +740,69 @@ async def test_transaction_record_is_valid() -> None: assert dataclasses.replace(tr1, sent_to=invalid_attempts + [mempool_success]).is_valid() assert dataclasses.replace(tr1, sent_to=invalid_attempts + [low_fee]).is_valid() assert dataclasses.replace(tr1, sent_to=invalid_attempts + [close_to_zero]).is_valid() + + +@pytest.mark.asyncio +async def test_valid_times_migration() -> None: + async with DBConnection(1) as db_wrapper: + async with db_wrapper.writer_maybe_transaction() as conn: + await conn.execute( + ( + "CREATE TABLE IF NOT EXISTS transaction_record(" + " transaction_record blob," + " bundle_id text PRIMARY KEY," + " confirmed_at_height bigint," + " created_at_time bigint," + " to_puzzle_hash text," + " amount blob," + " fee_amount blob," + " confirmed int," + " sent int," + " wallet_id bigint," + " trade_id text," + " type int)" + ) + ) + + old_record = TransactionRecordOld( + confirmed_at_height=uint32(0), + created_at_time=uint64(1000000000), + to_puzzle_hash=bytes32([0] * 32), + amount=uint64(0), + fee_amount=uint64(0), + confirmed=False, + sent=uint32(10), + spend_bundle=None, + additions=[], + removals=[], + wallet_id=uint32(1), + sent_to=[], + trade_id=None, + type=uint32(TransactionType.INCOMING_TX.value), + name=bytes32([0] * 32), + memos=[], + ) + + async with db_wrapper.writer_maybe_transaction() as conn: + await conn.execute_insert( + "INSERT OR REPLACE INTO transaction_record VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ( + bytes(old_record), + old_record.name, + old_record.confirmed_at_height, + old_record.created_at_time, + old_record.to_puzzle_hash.hex(), + bytes(old_record.amount), + bytes(old_record.fee_amount), + int(old_record.confirmed), + old_record.sent, + old_record.wallet_id, + old_record.trade_id, + old_record.type, + ), + ) + + store = await WalletTransactionStore.create(db_wrapper) + rec = await store.get_transaction_record(old_record.name) + assert rec is not None + assert rec.valid_times == ConditionValidTimes() diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index afcc1a2e99..49dbd87085 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -22,6 +22,7 @@ from chia.types.coin_spend import compute_additions from chia.types.peer_info import PeerInfo from chia.util.bech32m import encode_puzzle_hash from chia.util.ints import uint16, uint32, uint64 +from chia.wallet.conditions import ConditionValidTimes from chia.wallet.derive_keys import master_sk_to_wallet_sk from chia.wallet.payment import Payment from chia.wallet.transaction_record import TransactionRecord @@ -1423,6 +1424,7 @@ class TestWalletSimulator: type=uint32(TransactionType.OUTGOING_TX.value), name=name, memos=list(compute_memos(stolen_sb).items()), + valid_times=ConditionValidTimes(), ) await wallet.push_transaction(stolen_tx) diff --git a/tests/wallet/test_wallet_trade_store.py b/tests/wallet/test_wallet_trade_store.py index 3c33e72209..6ea4d9af7a 100644 --- a/tests/wallet/test_wallet_trade_store.py +++ b/tests/wallet/test_wallet_trade_store.py @@ -4,11 +4,15 @@ import time from secrets import token_bytes import pytest +from blspy import G2Element from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.spend_bundle import SpendBundle from chia.util.ints import uint32, uint64 -from chia.wallet.trade_record import TradeRecord +from chia.wallet.conditions import ConditionValidTimes +from chia.wallet.trade_record import TradeRecord, TradeRecordOld +from chia.wallet.trading.offer import Offer from chia.wallet.trading.trade_status import TradeStatus from chia.wallet.trading.trade_store import TradeStore, migrate_coin_of_interest from chia.wallet.util.wallet_types import WalletType @@ -46,6 +50,7 @@ async def test_get_coins_of_interest_with_trade_statuses() -> None: trade_id=tr1_name, status=uint32(TradeStatus.PENDING_ACCEPT.value), sent_to=[], + valid_times=ConditionValidTimes(), ) await trade_store.add_trade_record(tr1, offer_name=bytes32(token_bytes(32))) @@ -62,6 +67,7 @@ async def test_get_coins_of_interest_with_trade_statuses() -> None: trade_id=tr2_name, status=uint32(TradeStatus.PENDING_CONFIRM.value), sent_to=[], + valid_times=ConditionValidTimes(), ) await trade_store.add_trade_record(tr2, offer_name=bytes32(token_bytes(32))) @@ -86,6 +92,7 @@ async def test_get_coins_of_interest_with_trade_statuses() -> None: trade_id=tr2_name, status=uint32(TradeStatus.PENDING_CONFIRM.value), sent_to=[], + valid_times=ConditionValidTimes(), ) await trade_store.add_trade_record(tr2_1, offer_name=bytes32(token_bytes(32))) @@ -105,3 +112,59 @@ async def test_get_coins_of_interest_with_trade_statuses() -> None: assert await trade_store.get_coin_ids_of_interest_with_trade_statuses([TradeStatus.PENDING_ACCEPT]) == { coin_2.name() } + + +@pytest.mark.asyncio +async def test_valid_times_migration() -> None: + async with DBConnection(1) as db_wrapper: + async with db_wrapper.writer_maybe_transaction() as conn: + await conn.execute( + ( + "CREATE TABLE IF NOT EXISTS trade_records(" + " trade_record blob," + " trade_id text PRIMARY KEY," + " status int," + " confirmed_at_index int," + " created_at_time bigint," + " sent int," + " is_my_offer tinyint)" + ) + ) + + fake_offer = Offer({}, SpendBundle([], G2Element()), {}) + fake_coin = Coin(bytes32([0] * 32), bytes32([0] * 32), uint64(0)) + old_record = TradeRecordOld( + confirmed_at_index=uint32(0), + accepted_at_time=None, + created_at_time=uint64(1000000), + is_my_offer=True, + sent=uint32(0), + offer=bytes(fake_offer), + taken_offer=None, + coins_of_interest=[fake_coin], + trade_id=bytes32([0] * 32), + status=uint32(TradeStatus.PENDING_ACCEPT.value), + sent_to=[], + ) + + async with db_wrapper.writer_maybe_transaction() as conn: + cursor = await conn.execute( + "INSERT INTO trade_records " + "(trade_record, trade_id, status, confirmed_at_index, created_at_time, sent, is_my_offer) " + "VALUES(?, ?, ?, ?, ?, ?, ?)", + ( + bytes(old_record), + old_record.trade_id.hex(), + old_record.status, + old_record.confirmed_at_index, + old_record.created_at_time, + old_record.sent, + old_record.is_my_offer, + ), + ) + await cursor.close() + + trade_store = await TradeStore.create(db_wrapper) + rec = await trade_store.get_trade_record(old_record.trade_id) + assert rec is not None + assert rec.valid_times == ConditionValidTimes()