diff --git a/chia/_tests/core/cmds/test_wallet.py b/chia/_tests/core/cmds/test_wallet.py index 9a5fed2b5fe6..79834ed271b4 100644 --- a/chia/_tests/core/cmds/test_wallet.py +++ b/chia/_tests/core/cmds/test_wallet.py @@ -26,7 +26,7 @@ async def cat_name_resolver(request: CATAssetIDToName) -> CATAssetIDToNameRespon @pytest.mark.anyio async def test_print_offer_summary_xch(capsys: Any) -> None: - summary_dict = {"xch": 1_000_000_000_000} + summary_dict = {"xch": str(1_000_000_000_000)} await print_offer_summary(cat_name_resolver, summary_dict) @@ -38,7 +38,7 @@ async def test_print_offer_summary_xch(capsys: Any) -> None: @pytest.mark.anyio async def test_print_offer_summary_cat(capsys: Any) -> None: summary_dict = { - TEST_DUCKSAUCE_ASSET_ID: 1_000, + TEST_DUCKSAUCE_ASSET_ID: str(1_000), } await print_offer_summary(cat_name_resolver, summary_dict) @@ -51,8 +51,8 @@ async def test_print_offer_summary_cat(capsys: Any) -> None: @pytest.mark.anyio async def test_print_offer_summary_multiple_cats(capsys: Any) -> None: summary_dict = { - TEST_DUCKSAUCE_ASSET_ID: 1_000, - TEST_CRUNCHBERRIES_ASSET_ID: 2_000, + TEST_DUCKSAUCE_ASSET_ID: str(1_000), + TEST_CRUNCHBERRIES_ASSET_ID: str(2_000), } await print_offer_summary(cat_name_resolver, summary_dict) @@ -66,10 +66,10 @@ async def test_print_offer_summary_multiple_cats(capsys: Any) -> None: @pytest.mark.anyio async def test_print_offer_summary_xch_and_cats(capsys: Any) -> None: summary_dict = { - "xch": 2_500_000_000_000, - TEST_DUCKSAUCE_ASSET_ID: 1_111, - TEST_CRUNCHBERRIES_ASSET_ID: 2_222, - TEST_UNICORNTEARS_ASSET_ID: 3_333, + "xch": str(2_500_000_000_000), + TEST_DUCKSAUCE_ASSET_ID: str(1_111), + TEST_CRUNCHBERRIES_ASSET_ID: str(2_222), + TEST_UNICORNTEARS_ASSET_ID: str(3_333), } await print_offer_summary(cat_name_resolver, summary_dict) @@ -85,10 +85,10 @@ async def test_print_offer_summary_xch_and_cats(capsys: Any) -> None: @pytest.mark.anyio async def test_print_offer_summary_xch_and_cats_with_zero_values(capsys: Any) -> None: summary_dict = { - "xch": 0, - TEST_DUCKSAUCE_ASSET_ID: 0, - TEST_CRUNCHBERRIES_ASSET_ID: 0, - TEST_UNICORNTEARS_ASSET_ID: 0, + "xch": str(0), + TEST_DUCKSAUCE_ASSET_ID: str(0), + TEST_CRUNCHBERRIES_ASSET_ID: str(0), + TEST_UNICORNTEARS_ASSET_ID: str(0), } await print_offer_summary(cat_name_resolver, summary_dict) @@ -104,8 +104,8 @@ async def test_print_offer_summary_xch_and_cats_with_zero_values(capsys: Any) -> @pytest.mark.anyio async def test_print_offer_summary_cat_with_fee_and_change(capsys: Any) -> None: summary_dict = { - TEST_DUCKSAUCE_ASSET_ID: 1_000, - "unknown": 3_456, + TEST_DUCKSAUCE_ASSET_ID: str(1_000), + "unknown": str(3_456), } await print_offer_summary(cat_name_resolver, summary_dict, has_fee=True) @@ -118,7 +118,7 @@ async def test_print_offer_summary_cat_with_fee_and_change(capsys: Any) -> None: @pytest.mark.anyio async def test_print_offer_summary_xch_with_one_mojo(capsys: Any) -> None: - summary_dict = {"xch": 1} + summary_dict = {"xch": str(1)} await print_offer_summary(cat_name_resolver, summary_dict) diff --git a/chia/_tests/wallet/cat_wallet/test_offer_lifecycle.py b/chia/_tests/wallet/cat_wallet/test_offer_lifecycle.py index b350fec85b64..b98de6992563 100644 --- a/chia/_tests/wallet/cat_wallet/test_offer_lifecycle.py +++ b/chia/_tests/wallet/cat_wallet/test_offer_lifecycle.py @@ -173,7 +173,7 @@ async def test_complex_offer(cost_logger: CostLogger) -> None: {"type": AssetType.CAT.value, "tail": "0x" + str_to_tail_hash("blue").hex()} ), } - driver_dict_as_infos = {key.hex(): value.info for key, value in driver_dict.items()} + driver_dict_as_summary = {key.hex(): value for key, value in driver_dict.items()} # Create an XCH Offer for RED chia_requested_payments: dict[Optional[bytes32], list[CreateCoin]] = { @@ -239,9 +239,9 @@ async def test_complex_offer(cost_logger: CostLogger) -> None: } assert new_offer.get_requested_amounts() == {None: 900, str_to_tail_hash("red"): 350} assert new_offer.summary() == ( - {"xch": 1000, str_to_tail_hash("red").hex(): 350, str_to_tail_hash("blue").hex(): 2000}, - {"xch": 900, str_to_tail_hash("red").hex(): 350}, - driver_dict_as_infos, + {"xch": "1000", str_to_tail_hash("red").hex(): "350", str_to_tail_hash("blue").hex(): "2000"}, + {"xch": "900", str_to_tail_hash("red").hex(): "350"}, + driver_dict_as_summary, ConditionValidTimes(), ) assert new_offer.get_pending_amounts() == { diff --git a/chia/_tests/wallet/cat_wallet/test_trades.py b/chia/_tests/wallet/cat_wallet/test_trades.py index 9ffecf46d9f3..3fce269718c7 100644 --- a/chia/_tests/wallet/cat_wallet/test_trades.py +++ b/chia/_tests/wallet/cat_wallet/test_trades.py @@ -28,7 +28,7 @@ from chia.wallet.puzzle_drivers import PuzzleInfo from chia.wallet.trade_manager import TradeManager from chia.wallet.trade_record import TradeRecord -from chia.wallet.trading.offer import Offer, OfferSummary +from chia.wallet.trading.offer import Offer, OfferSpecification from chia.wallet.trading.trade_status import TradeStatus from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.transaction_type import TransactionType @@ -415,29 +415,29 @@ async def test_cat_trades( await env_maker.check_balances() # Create the trade parameters - chia_for_cat: OfferSummary = { + chia_for_cat: OfferSpecification = { wallet_maker.id(): -1, bytes32.from_hexstr(new_cat_wallet_maker.get_asset_id()): 2, # This is the CAT that the taker made } - cat_for_chia: OfferSummary = { + cat_for_chia: OfferSpecification = { wallet_maker.id(): 3, cat_wallet_maker.id(): -4, # The taker has no knowledge of this CAT yet } - cat_for_cat: OfferSummary = { + cat_for_cat: OfferSpecification = { bytes32.from_hexstr(cat_wallet_maker.get_asset_id()): -5, new_cat_wallet_maker.id(): 6, } - chia_for_multiple_cat: OfferSummary = { + chia_for_multiple_cat: OfferSpecification = { wallet_maker.id(): -7, cat_wallet_maker.id(): 8, new_cat_wallet_maker.id(): 9, } - multiple_cat_for_chia: OfferSummary = { + multiple_cat_for_chia: OfferSpecification = { wallet_maker.id(): 10, cat_wallet_maker.id(): -11, new_cat_wallet_maker.id(): -12, } - chia_and_cat_for_cat: OfferSummary = { + chia_and_cat_for_cat: OfferSpecification = { wallet_maker.id(): -13, cat_wallet_maker.id(): -14, new_cat_wallet_maker.id(): 15, @@ -1673,12 +1673,12 @@ async def test_trade_cancellation(wallet_environments: WalletTestFramework, wall ] ) - cat_for_chia: OfferSummary = { + cat_for_chia: OfferSpecification = { env_maker.wallet_aliases["xch"]: 1, env_maker.wallet_aliases["cat"]: -2, } - chia_for_cat: OfferSummary = { + chia_for_cat: OfferSpecification = { env_maker.wallet_aliases["xch"]: -3, env_maker.wallet_aliases["cat"]: 4, } @@ -1846,7 +1846,7 @@ async def test_trade_cancellation(wallet_environments: WalletTestFramework, wall await time_out_assert(15, get_trade_and_status, TradeStatus.CANCELLED, trade_manager_maker, trade_make) # Now let's test the case where two coins need to be spent in order to cancel - chia_and_cat_for_something: OfferSummary = { + chia_and_cat_for_something: OfferSpecification = { env_maker.wallet_aliases["xch"]: -5, env_maker.wallet_aliases["cat"]: -6, bytes32.zeros: 1, # Doesn't matter @@ -1991,7 +1991,7 @@ async def test_trade_conflict(wallet_environments: WalletTestFramework, wallet_t ] ) - cat_for_chia: OfferSummary = { + cat_for_chia: OfferSpecification = { env_maker.wallet_aliases["xch"]: 1000, env_maker.wallet_aliases["cat"]: -4, } @@ -2182,7 +2182,7 @@ async def test_trade_bad_spend(wallet_environments: WalletTestFramework, wallet_ ] ) - cat_for_chia: OfferSummary = { + cat_for_chia: OfferSpecification = { env_maker.wallet_aliases["xch"]: 1000, env_maker.wallet_aliases["cat"]: -4, } @@ -2304,7 +2304,7 @@ async def test_trade_high_fee(wallet_environments: WalletTestFramework, wallet_t ] ) - cat_for_chia: OfferSummary = { + cat_for_chia: OfferSpecification = { env_maker.wallet_aliases["xch"]: 1000, env_maker.wallet_aliases["cat"]: -4, } @@ -2449,15 +2449,15 @@ async def test_aggregated_trade_state(wallet_environments: WalletTestFramework, ] ) - cat_for_chia: OfferSummary = { + cat_for_chia: OfferSpecification = { env_maker.wallet_aliases["xch"]: 2, env_maker.wallet_aliases["cat"]: -2, } - chia_for_cat: OfferSummary = { + chia_for_cat: OfferSpecification = { env_maker.wallet_aliases["xch"]: -1, env_maker.wallet_aliases["cat"]: 1, } - combined_summary: OfferSummary = { + combined_summary: OfferSpecification = { env_maker.wallet_aliases["xch"]: cat_for_chia[env_maker.wallet_aliases["xch"]] + chia_for_cat[env_maker.wallet_aliases["xch"]], env_maker.wallet_aliases["cat"]: cat_for_chia[env_maker.wallet_aliases["cat"]] diff --git a/chia/_tests/wallet/db_wallet/test_dl_offers.py b/chia/_tests/wallet/db_wallet/test_dl_offers.py index 47be4d7a0a89..d4703a147de7 100644 --- a/chia/_tests/wallet/db_wallet/test_dl_offers.py +++ b/chia/_tests/wallet/db_wallet/test_dl_offers.py @@ -8,7 +8,7 @@ from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework from chia._tests.util.time_out_assert import time_out_assert -from chia.data_layer.data_layer_wallet import DataLayerWallet +from chia.data_layer.data_layer_wallet import DataLayerSummary, DataLayerWallet, SingletonDependencies, SingletonSummary from chia.wallet.puzzle_drivers import Solver from chia.wallet.trade_record import TradeRecord from chia.wallet.trading.offer import Offer @@ -179,20 +179,20 @@ async def test_dl_offers(wallet_environments: WalletTestFramework) -> None: assert success is True assert offer_maker is not None - assert await trade_manager_taker.get_offer_summary(Offer.from_bytes(offer_maker.offer)) == { - "offered": [ - { - "launcher_id": launcher_id_maker.hex(), - "new_root": maker_root.hex(), - "dependencies": [ - { - "launcher_id": launcher_id_taker.hex(), - "values_to_prove": [taker_branch.hex()], - } + assert await trade_manager_taker.get_dl_offer_summary(Offer.from_bytes(offer_maker.offer)) == DataLayerSummary( + offered=[ + SingletonSummary( + launcher_id=launcher_id_maker, + new_root=maker_root, + dependencies=[ + SingletonDependencies( + launcher_id=launcher_id_taker, + values_to_prove=[taker_branch], + ) ], - } + ) ] - } + ) [_maker_offer], signing_response = await wsm_maker.sign_offers([Offer.from_bytes(offer_maker.offer)]) async with trade_manager_taker.wallet_state_manager.new_action_scope( @@ -231,30 +231,30 @@ async def test_dl_offers(wallet_environments: WalletTestFramework) -> None: ) assert offer_taker is not None - assert await trade_manager_maker.get_offer_summary(Offer.from_bytes(offer_taker.offer)) == { - "offered": [ - { - "launcher_id": launcher_id_maker.hex(), - "new_root": maker_root.hex(), - "dependencies": [ - { - "launcher_id": launcher_id_taker.hex(), - "values_to_prove": [taker_branch.hex()], - } + assert await trade_manager_maker.get_dl_offer_summary(Offer.from_bytes(offer_taker.offer)) == DataLayerSummary( + offered=[ + SingletonSummary( + launcher_id=launcher_id_maker, + new_root=maker_root, + dependencies=[ + SingletonDependencies( + launcher_id=launcher_id_taker, + values_to_prove=[taker_branch], + ) ], - }, - { - "launcher_id": launcher_id_taker.hex(), - "new_root": taker_root.hex(), - "dependencies": [ - { - "launcher_id": launcher_id_maker.hex(), - "values_to_prove": [maker_branch.hex()], - } + ), + SingletonSummary( + launcher_id=launcher_id_taker, + new_root=taker_root, + dependencies=[ + SingletonDependencies( + launcher_id=launcher_id_maker, + values_to_prove=[maker_branch], + ), ], - }, + ), ] - } + ) await wallet_environments.process_pending_states( [ diff --git a/chia/_tests/wallet/nft_wallet/test_nft_1_offers.py b/chia/_tests/wallet/nft_wallet/test_nft_1_offers.py index 0b09bc0d2c5b..5a9530fe35af 100644 --- a/chia/_tests/wallet/nft_wallet/test_nft_1_offers.py +++ b/chia/_tests/wallet/nft_wallet/test_nft_1_offers.py @@ -17,7 +17,7 @@ from chia.wallet.nft_wallet.nft_wallet import NFTWallet from chia.wallet.outer_puzzles import create_asset_id, match_puzzle from chia.wallet.puzzle_drivers import PuzzleInfo -from chia.wallet.trading.offer import Offer, OfferSummary +from chia.wallet.trading.offer import Offer, OfferSpecification from chia.wallet.trading.trade_status import TradeStatus from chia.wallet.uncurried_puzzle import uncurry_puzzle @@ -163,7 +163,7 @@ async def test_nft_offer_sell_nft(wallet_environments: WalletTestFramework, zero xch_requested = 1000 maker_fee = uint64(433) - offer_did_nft_for_xch: OfferSummary = {nft_to_offer_asset_id: -1, wallet_maker.id(): xch_requested} + offer_did_nft_for_xch: OfferSpecification = {nft_to_offer_asset_id: -1, wallet_maker.id(): xch_requested} async with trade_manager_maker.wallet_state_manager.new_action_scope( wallet_environments.tx_config, push=False @@ -395,7 +395,7 @@ async def test_nft_offer_request_nft(wallet_environments: WalletTestFramework, z maker_fee = uint64(10) driver_dict = {nft_to_request_asset_id: nft_to_request_info} - offer_dict: OfferSummary = {nft_to_request_asset_id: 1, wallet_maker.id(): -xch_offered} + offer_dict: OfferSpecification = {nft_to_request_asset_id: 1, wallet_maker.id(): -xch_offered} async with trade_manager_maker.wallet_state_manager.new_action_scope( wallet_environments.tx_config, push=False @@ -685,7 +685,7 @@ async def test_nft_offer_sell_did_to_did(wallet_environments: WalletTestFramewor xch_requested = 1000 maker_fee = uint64(433) - offer_did_nft_for_xch: OfferSummary = {nft_to_offer_asset_id: -1, wallet_maker.id(): xch_requested} + offer_did_nft_for_xch: OfferSpecification = {nft_to_offer_asset_id: -1, wallet_maker.id(): xch_requested} async with trade_manager_maker.wallet_state_manager.new_action_scope( wallet_environments.tx_config, push=False @@ -990,7 +990,7 @@ async def test_nft_offer_sell_nft_for_cat( cats_requested = 1000 maker_fee = uint64(433) - offer_did_nft_for_xch: OfferSummary = {nft_to_offer_asset_id: -1, cat_wallet_maker.id(): cats_requested} + offer_did_nft_for_xch: OfferSpecification = {nft_to_offer_asset_id: -1, cat_wallet_maker.id(): cats_requested} async with trade_manager_maker.wallet_state_manager.new_action_scope( wallet_environments.tx_config, push=False @@ -1347,7 +1347,7 @@ async def test_nft_offer_request_nft_for_cat( maker_fee = uint64(433) driver_dict = {nft_to_request_asset_id: nft_to_request_info} - offer_dict: OfferSummary = {nft_to_request_asset_id: 1, cat_wallet_maker.id(): -cats_requested} + offer_dict: OfferSpecification = {nft_to_request_asset_id: 1, cat_wallet_maker.id(): -cats_requested} async with trade_manager_maker.wallet_state_manager.new_action_scope( wallet_environments.tx_config, push=False @@ -1583,7 +1583,7 @@ async def test_nft_offer_sell_cancel(wallet_environments: WalletTestFramework) - xch_requested = 1000 maker_fee = uint64(433) - offer_did_nft_for_xch: OfferSummary = {nft_to_offer_asset_id: -1, wallet_maker.id(): xch_requested} + offer_did_nft_for_xch: OfferSpecification = {nft_to_offer_asset_id: -1, wallet_maker.id(): xch_requested} async with trade_manager_maker.wallet_state_manager.new_action_scope( wallet_environments.tx_config, push=False @@ -1976,7 +1976,7 @@ async def test_complex_nft_offer( CAT_REQUESTED = 100000 FEE = uint64(2000000000000) - complex_nft_offer: OfferSummary = { + complex_nft_offer: OfferSpecification = { nft_to_offer_asset_id_maker: -1, cat_wallet_maker.id(): CAT_REQUESTED * -1, 1: XCH_REQUESTED, diff --git a/chia/_tests/wallet/rpc/test_wallet_rpc.py b/chia/_tests/wallet/rpc/test_wallet_rpc.py index 2e15fa7ede5b..25110c79ac79 100644 --- a/chia/_tests/wallet/rpc/test_wallet_rpc.py +++ b/chia/_tests/wallet/rpc/test_wallet_rpc.py @@ -72,6 +72,7 @@ from chia.wallet.cat_wallet.r_cat_wallet import RCATWallet from chia.wallet.conditions import ( ConditionValidTimes, + ConditionValidTimesAbsolute, CreateCoinAnnouncement, CreatePuzzleAnnouncement, Remark, @@ -85,7 +86,7 @@ from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_hash_for_pk from chia.wallet.signer_protocol import UnsignedTransaction from chia.wallet.trade_record import TradeRecord -from chia.wallet.trading.offer import Offer +from chia.wallet.trading.offer import Offer, OfferSummary from chia.wallet.trading.trade_status import TradeStatus from chia.wallet.transaction_record import TransactionRecord from chia.wallet.transaction_sorting import SortKey @@ -132,6 +133,7 @@ GetCoinRecordsByNames, GetNextAddress, GetNotifications, + GetOfferSummary, GetPrivateKey, GetSpendableCoins, GetSyncStatusResponse, @@ -1574,25 +1576,22 @@ async def test_offer_endpoints(wallet_environments: WalletTestFramework, wallet_ ) offer = create_res.offer - id, summary = await env_1.rpc_client.get_offer_summary(offer) - assert id == offer.name() - id, advanced_summary = await env_1.rpc_client.get_offer_summary(offer, advanced=True) - assert id == offer.name() - assert summary == { - "offered": {"xch": 5}, - "requested": {cat_asset_id.hex(): 1}, - "infos": {key.hex(): info.info for key, info in driver_dict.items()}, - "fees": 1, - "additions": [c.name().hex() for c in offer.additions()], - "removals": [c.name().hex() for c in offer.removals()], - "valid_times": { - "max_height": None, - "max_time": None, - "min_height": None, - "min_time": None, - }, - } - assert advanced_summary == summary + offer_summary_response = await env_1.rpc_client.get_offer_summary(GetOfferSummary(offer.to_bech32())) + assert offer_summary_response.id == offer.name() + offer_summary_response_advanced = await env_1.rpc_client.get_offer_summary( + GetOfferSummary(offer.to_bech32(), advanced=True) + ) + assert offer_summary_response_advanced.id == offer.name() + assert offer_summary_response_advanced.summary == OfferSummary( + offered={"xch": "5"}, + requested={cat_asset_id.hex(): "1"}, + infos={key.hex(): info for key, info in driver_dict.items()}, + fees=uint64(1), + additions=[c.name() for c in offer.additions()], + removals=[c.name() for c in offer.removals()], + valid_times=ConditionValidTimesAbsolute(), + ) + assert offer_summary_response_advanced.summary == offer_summary_response.summary offer_validity_response = await env_1.rpc_client.check_offer_validity(CheckOfferValidity(offer.to_bech32())) assert offer_validity_response.id == offer.name() diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 2f17ecd7fd91..3f9172704e72 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -670,7 +670,7 @@ def timestamp_to_time(timestamp: int) -> str: async def print_offer_summary( - cat_name_resolver: CATNameResolver, sum_dict: dict[str, int], has_fee: bool = False, network_xch: str = "XCH" + cat_name_resolver: CATNameResolver, sum_dict: dict[str, str], has_fee: bool = False, network_xch: str = "XCH" ) -> None: for asset_id, amount in sum_dict.items(): description: str = "" @@ -721,7 +721,7 @@ async def print_trade_record(record: TradeRecord, wallet_client: WalletRpcClient print("Summary:") offer = Offer.from_bytes(record.offer) offered, requested, _, _ = offer.summary() - outbound_balances: dict[str, int] = offer.get_pending_amounts() + outbound_balances: dict[str, str] = {k: str(v) for k, v in offer.get_pending_amounts().items()} fees: Decimal = Decimal(offer.fees()) cat_name_resolver = wallet_client.cat_asset_id_to_name print(" OFFERED:") diff --git a/chia/data_layer/data_layer.py b/chia/data_layer/data_layer.py index 293470f275d0..4a7e14e023f9 100644 --- a/chia/data_layer/data_layer.py +++ b/chia/data_layer/data_layer.py @@ -1274,7 +1274,7 @@ async def cancel_offer(self, trade_id: bytes32, secure: bool, fee: uint64) -> No trade_record = await self.wallet_rpc.get_offer(trade_id=trade_id, file_contents=True) trading_offer = TradingOffer.from_bytes(trade_record.offer) summary = await DataLayerWallet.get_offer_summary(offer=trading_offer) - store_ids = [bytes32.from_hexstr(offered["launcher_id"]) for offered in summary["offered"]] + store_ids = [offered.launcher_id for offered in summary.offered] await self.wallet_rpc.cancel_offer( trade_id=trade_id, diff --git a/chia/data_layer/data_layer_wallet.py b/chia/data_layer/data_layer_wallet.py index 847b6e8c0116..11d5b173b520 100644 --- a/chia/data_layer/data_layer_wallet.py +++ b/chia/data_layer/data_layer_wallet.py @@ -86,6 +86,27 @@ def decode_urls(urls: list[bytes]) -> list[str]: return [url.decode("utf8") for url in urls] +@streamable +@dataclasses.dataclass(frozen=True) +class SingletonDependencies(Streamable): + launcher_id: bytes32 + values_to_prove: list[bytes] + + +@streamable +@dataclasses.dataclass(frozen=True) +class SingletonSummary(Streamable): + launcher_id: bytes32 + new_root: bytes32 + dependencies: list[SingletonDependencies] + + +@streamable +@dataclasses.dataclass(frozen=True) +class DataLayerSummary(Streamable): + offered: list[SingletonSummary] + + @final class DataLayerWallet: if TYPE_CHECKING: @@ -1145,8 +1166,8 @@ async def finish_graftroot_solutions(offer: Offer, solver: Solver) -> Offer: return Offer({}, WalletSpendBundle(new_spends, offer.aggregated_signature()), offer.driver_dict) @staticmethod - async def get_offer_summary(offer: Offer) -> dict[str, Any]: - summary: dict[str, Any] = {"offered": []} + async def get_offer_summary(offer: Offer) -> DataLayerSummary: + singleton_summaries = [] for spend in offer.coin_spends(): solution = Program.from_serialized(spend.solution) matched, curried_args = match_dl_singleton(spend.puzzle_reveal) @@ -1161,21 +1182,22 @@ async def get_offer_summary(offer: Offer) -> dict[str, Any]: cs for cs in offer.coin_spends() if cs.coin.parent_coin_info == spend.coin.name() ) _, child_curried_args = match_dl_singleton(child_spend.puzzle_reveal) - singleton_summary = { - "launcher_id": list(curried_args)[2].as_python().hex(), - "new_root": list(child_curried_args)[1].as_python().hex(), - "dependencies": [], - } _, singleton_structs, _, values_to_prove = graftroot_curried_args.as_iter() + dependencies = [] for struct, values in zip(singleton_structs.as_iter(), values_to_prove.as_iter()): - singleton_summary["dependencies"].append( - { - "launcher_id": struct.at("rf").as_python().hex(), - "values_to_prove": [value.as_python().hex() for value in values.as_iter()], - } + dependencies.append( + SingletonDependencies( + launcher_id=bytes32(struct.at("rf").as_atom()), + values_to_prove=[value.as_atom() for value in values.as_iter()], + ) ) - summary["offered"].append(singleton_summary) - return summary + singleton_summary = SingletonSummary( + launcher_id=bytes32(list(curried_args)[2].as_atom()), + new_root=bytes32(list(child_curried_args)[1].as_atom()), + dependencies=dependencies, + ) + singleton_summaries.append(singleton_summary) + return DataLayerSummary(offered=singleton_summaries) async def select_coins( self, @@ -1191,7 +1213,7 @@ async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: def verify_offer( maker: tuple[StoreProofs, ...], taker: tuple[OfferStore, ...], - summary: dict[str, Any], + summary: DataLayerSummary, ) -> None: # TODO: consistency in error messages # TODO: custom exceptions @@ -1233,10 +1255,7 @@ def verify_offer( raise OfferIntegrityError("maker: no roots referenced for store id") # TODO: what about validating duplicate entries are consistent? - maker_from_offer = { - bytes32.from_hexstr(offered["launcher_id"]): bytes32.from_hexstr(offered["new_root"]) - for offered in summary["offered"] - } + maker_from_offer = {offered.launcher_id: offered.new_root for offered in summary.offered} maker_from_reference = { # verified above that there is at least one proof and all combined hashes match @@ -1248,11 +1267,9 @@ def verify_offer( raise OfferIntegrityError("maker: offered stores and their roots do not match the reference data") taker_from_offer = { - bytes32.from_hexstr(dependency["launcher_id"]): [ - bytes32.from_hexstr(value) for value in dependency["values_to_prove"] - ] - for offered in summary["offered"] - for dependency in offered["dependencies"] + dependency.launcher_id: [bytes32(value) for value in dependency.values_to_prove] + for offered in summary.offered + for dependency in offered.dependencies } taker_from_reference = { diff --git a/chia/wallet/conditions.py b/chia/wallet/conditions.py index 5b12b1c4ad21..7442a2ff765b 100644 --- a/chia/wallet/conditions.py +++ b/chia/wallet/conditions.py @@ -1408,37 +1408,54 @@ def conditions_to_json_dicts(conditions: Iterable[Condition]) -> list[dict[str, @streamable @dataclass(frozen=True) -class ConditionValidTimes(Streamable): - min_secs_since_created: Optional[uint64] = None # ASSERT_SECONDS_RELATIVE +class ConditionValidTimesAbsolute(Streamable): min_time: Optional[uint64] = None # ASSERT_SECONDS_ABSOLUTE - min_blocks_since_created: Optional[uint32] = None # ASSERT_HEIGHT_RELATIVE min_height: Optional[uint32] = None # ASSERT_HEIGHT_ABSOLUTE - max_secs_after_created: Optional[uint64] = None # ASSERT_BEFORE_SECONDS_RELATIVE max_time: Optional[uint64] = None # ASSERT_BEFORE_SECONDS_ABSOLUTE - max_blocks_after_created: Optional[uint32] = None # ASSERT_BEFORE_HEIGHT_RELATIVE max_height: Optional[uint32] = None # ASSERT_BEFORE_HEIGHT_ABSOLUTE def to_conditions(self) -> list[Condition]: final_condition_list: list[Condition] = [] - if self.min_secs_since_created is not None: - final_condition_list.append(AssertSecondsRelative(self.min_secs_since_created)) if self.min_time is not None: final_condition_list.append(AssertSecondsAbsolute(self.min_time)) - if self.min_blocks_since_created is not None: - final_condition_list.append(AssertHeightRelative(self.min_blocks_since_created)) if self.min_height is not None: final_condition_list.append(AssertHeightAbsolute(self.min_height)) - if self.max_secs_after_created is not None: - final_condition_list.append(AssertBeforeSecondsRelative(self.max_secs_after_created)) if self.max_time is not None: final_condition_list.append(AssertBeforeSecondsAbsolute(self.max_time)) - if self.max_blocks_after_created is not None: - final_condition_list.append(AssertBeforeHeightRelative(self.max_blocks_after_created)) if self.max_height is not None: final_condition_list.append(AssertBeforeHeightAbsolute(self.max_height)) + return final_condition_list + + +@streamable +@dataclass(frozen=True) +class ConditionValidTimes(ConditionValidTimesAbsolute): + min_secs_since_created: Optional[uint64] = None # ASSERT_SECONDS_RELATIVE + min_blocks_since_created: Optional[uint32] = None # ASSERT_HEIGHT_RELATIVE + max_secs_after_created: Optional[uint64] = None # ASSERT_BEFORE_SECONDS_RELATIVE + max_blocks_after_created: Optional[uint32] = None # ASSERT_BEFORE_HEIGHT_RELATIVE + + def to_conditions(self) -> list[Condition]: + final_condition_list = super().to_conditions() + if self.min_secs_since_created is not None: + final_condition_list.append(AssertSecondsRelative(self.min_secs_since_created)) + if self.min_blocks_since_created is not None: + final_condition_list.append(AssertHeightRelative(self.min_blocks_since_created)) + if self.max_secs_after_created is not None: + final_condition_list.append(AssertBeforeSecondsRelative(self.max_secs_after_created)) + if self.max_blocks_after_created is not None: + final_condition_list.append(AssertBeforeHeightRelative(self.max_blocks_after_created)) return final_condition_list + def only_absolutes(self) -> ConditionValidTimesAbsolute: + return ConditionValidTimesAbsolute( + min_time=self.min_time, + min_height=self.min_height, + max_time=self.max_time, + max_height=self.max_height, + ) + condition_valid_times_hints = get_type_hints(ConditionValidTimes) condition_valid_times_types: dict[str, type[int]] = {} diff --git a/chia/wallet/trade_manager.py b/chia/wallet/trade_manager.py index 72a8a95b426e..82d54eaa17b9 100644 --- a/chia/wallet/trade_manager.py +++ b/chia/wallet/trade_manager.py @@ -11,7 +11,7 @@ from chia_rs.sized_ints import uint32, uint64 from typing_extensions import Literal -from chia.data_layer.data_layer_wallet import DataLayerWallet +from chia.data_layer.data_layer_wallet import DataLayerSummary, DataLayerWallet from chia.server.ws_connection import WSChiaConnection from chia.types.blockchain_format.coin import Coin, coin_as_list from chia.types.blockchain_format.program import Program, run @@ -966,7 +966,7 @@ def check_for_owner_change_in_drivers(self, puzzle_info: PuzzleInfo, driver_info return True return False - async def get_offer_summary(self, offer: Offer) -> dict[str, Any]: + async def get_dl_offer_summary(self, offer: Offer) -> Optional[DataLayerSummary]: for puzzle_info in offer.driver_dict.values(): if ( puzzle_info.check_type( @@ -978,27 +978,7 @@ async def get_offer_summary(self, offer: Offer) -> dict[str, Any]: and puzzle_info.also()["updater_hash"] == ACS_MU_PH # type: ignore ): return await DataLayerWallet.get_offer_summary(offer) - # Otherwise just return the same thing as the RPC normally does - offered, requested, infos, valid_times = offer.summary() - return { - "offered": offered, - "requested": requested, - "fees": offer.fees(), - "additions": [c.name().hex() for c in offer.additions()], - "removals": [c.name().hex() for c in offer.removals()], - "infos": infos, - "valid_times": { - k: v - for k, v in valid_times.to_json_dict().items() - if k - not in { - "max_secs_after_created", - "min_secs_since_created", - "max_blocks_after_created", - "min_blocks_since_created", - } - }, - } + return None async def check_for_final_modifications( self, offer: Offer, solver: Solver, action_scope: WalletActionScope diff --git a/chia/wallet/trading/offer.py b/chia/wallet/trading/offer.py index ca9193216868..5279d8339f7e 100644 --- a/chia/wallet/trading/offer.py +++ b/chia/wallet/trading/offer.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Any, BinaryIO, Optional, Union +from typing import BinaryIO, Optional, Union from chia_puzzles_py.programs import SETTLEMENT_PAYMENT, SETTLEMENT_PAYMENT_HASH from chia_rs import CoinSpend, G2Element @@ -15,12 +15,13 @@ from chia.types.coin_spend import make_spend from chia.util.bech32m import bech32_decode, bech32_encode, convertbits from chia.util.errors import Err, ValidationError -from chia.util.streamable import parse_rust +from chia.util.streamable import Streamable, parse_rust, streamable from chia.wallet.conditions import ( AssertCoinAnnouncement, AssertPuzzleAnnouncement, Condition, ConditionValidTimes, + ConditionValidTimesAbsolute, CreateCoin, parse_conditions_non_consensus, parse_timelock_info, @@ -43,7 +44,7 @@ ) from chia.wallet.wallet_spend_bundle import WalletSpendBundle -OfferSummary = dict[Union[int, bytes32], int] +OfferSpecification = dict[Union[int, bytes32], int] OFFER_MOD = Program.from_bytes(SETTLEMENT_PAYMENT) OFFER_MOD_HASH = bytes32(SETTLEMENT_PAYMENT_HASH) @@ -76,6 +77,18 @@ def name(self) -> bytes32: return self.to_program().get_tree_hash() +@streamable +@dataclass(frozen=True) +class OfferSummary(Streamable): + offered: dict[str, str] # str for negative int support + requested: dict[str, str] # str for negative int support + fees: uint64 + infos: dict[str, PuzzleInfo] + additions: list[bytes32] + removals: list[bytes32] + valid_times: ConditionValidTimesAbsolute + + @dataclass(frozen=True, eq=False) class Offer: requested_payments: dict[ @@ -320,26 +333,26 @@ def arbitrage(self) -> dict[Optional[bytes32], int]: return arbitrage_dict # This is a method mostly for the UI that creates a JSON summary of the offer - def summary(self) -> tuple[dict[str, int], dict[str, int], dict[str, dict[str, Any]], ConditionValidTimes]: + def summary(self) -> tuple[dict[str, str], dict[str, str], dict[str, PuzzleInfo], ConditionValidTimes]: offered_amounts: dict[Optional[bytes32], int] = self.get_offered_amounts() requested_amounts: dict[Optional[bytes32], int] = self.get_requested_amounts() - def keys_to_strings(dic: dict[Optional[bytes32], Any]) -> dict[str, Any]: - new_dic: dict[str, Any] = {} + def keys_and_amounts_to_strings(dic: dict[Optional[bytes32], int]) -> dict[str, str]: + new_dic: dict[str, str] = {} for key, val in dic.items(): if key is None: - new_dic["xch"] = val + new_dic["xch"] = str(val) else: - new_dic[key.hex()] = val + new_dic[key.hex()] = str(val) return new_dic - driver_dict: dict[str, Any] = {} + driver_dict: dict[str, PuzzleInfo] = {} for key, value in self.driver_dict.items(): - driver_dict[key.hex()] = value.info + driver_dict[key.hex()] = value return ( - keys_to_strings(offered_amounts), - keys_to_strings(requested_amounts), + keys_and_amounts_to_strings(offered_amounts), + keys_and_amounts_to_strings(requested_amounts), driver_dict, self.absolute_valid_times_ban_relatives(), ) diff --git a/chia/wallet/wallet_request_types.py b/chia/wallet/wallet_request_types.py index e6f3eba3f47e..a8b7b6212fb0 100644 --- a/chia/wallet/wallet_request_types.py +++ b/chia/wallet/wallet_request_types.py @@ -2,6 +2,7 @@ import sys from dataclasses import dataclass, field +from functools import cached_property from typing import Any, BinaryIO, Optional, Union, final from chia_rs import Coin, G1Element, G2Element, PrivateKey @@ -9,7 +10,7 @@ from chia_rs.sized_ints import uint8, uint16, uint32, uint64 from typing_extensions import Self, dataclass_transform -from chia.data_layer.data_layer_wallet import Mirror +from chia.data_layer.data_layer_wallet import DataLayerSummary, Mirror from chia.data_layer.singleton_record import SingletonRecord from chia.pools.pool_wallet_info import PoolWalletInfo from chia.types.blockchain_format.program import Program @@ -28,7 +29,7 @@ UnsignedTransaction, ) from chia.wallet.trade_record import TradeRecord -from chia.wallet.trading.offer import Offer +from chia.wallet.trading.offer import Offer, OfferSummary from chia.wallet.transaction_record import TransactionRecord from chia.wallet.transaction_sorting import SortKey from chia.wallet.util.clvm_streamable import json_deserialize_with_clvm_streamable @@ -662,6 +663,51 @@ class CATAssetIDToNameResponse(Streamable): name: Optional[str] +@streamable +@dataclass(frozen=True) +class GetOfferSummary(Streamable): + offer: str + advanced: bool = False + + @cached_property + def parsed_offer(self) -> Offer: + return Offer.from_bech32(self.offer) + + +@streamable +@dataclass(frozen=True) +class GetOfferSummaryResponse(Streamable): + id: bytes32 + summary: Optional[OfferSummary] = None + data_layer_summary: Optional[DataLayerSummary] = None + + def __post_init__(self) -> None: + if self.summary is not None and self.data_layer_summary is not None: + raise ValueError("Cannot have both summary and data_layer_summary") + elif self.summary is None and self.data_layer_summary is None: + raise ValueError("Must have either summary or data_layer_summary") + super().__post_init__() + + def to_json_dict(self) -> dict[str, Any]: + serialized = super().to_json_dict() + if self.data_layer_summary is not None: + serialized["summary"] = serialized["data_layer_summary"] + del serialized["data_layer_summary"] + return serialized + + @classmethod + def from_json_dict(cls, json_dict: dict[str, Any]) -> Self: + if isinstance(json_dict["summary"]["offered"], dict): + summary: Union[OfferSummary, DataLayerSummary] = OfferSummary.from_json_dict(json_dict["summary"]) + else: + summary = DataLayerSummary.from_json_dict(json_dict["summary"]) + return cls( + id=bytes32.from_hexstr(json_dict["id"]), + summary=summary if isinstance(summary, OfferSummary) else None, + data_layer_summary=summary if isinstance(summary, DataLayerSummary) else None, + ) + + @streamable @dataclass(frozen=True) class CheckOfferValidity(Streamable): diff --git a/chia/wallet/wallet_rpc_api.py b/chia/wallet/wallet_rpc_api.py index 93617c462683..bb3b25647368 100644 --- a/chia/wallet/wallet_rpc_api.py +++ b/chia/wallet/wallet_rpc_api.py @@ -82,7 +82,7 @@ get_inner_puzzle_from_singleton, ) from chia.wallet.trade_record import TradeRecord -from chia.wallet.trading.offer import Offer +from chia.wallet.trading.offer import Offer, OfferSummary from chia.wallet.transaction_record import TransactionRecord from chia.wallet.uncurried_puzzle import uncurry_puzzle from chia.wallet.util.address_type import AddressType, is_valid_address @@ -198,6 +198,8 @@ GetNotifications, GetNotificationsResponse, GetOffersCountResponse, + GetOfferSummary, + GetOfferSummaryResponse, GetPrivateKey, GetPrivateKeyFormat, GetPrivateKeyResponse, @@ -2256,65 +2258,61 @@ async def create_offer_for_ids( trade_record=result[1], ) - async def get_offer_summary(self, request: dict[str, Any]) -> EndpointResult: - offer_hex: str = request["offer"] - - offer = Offer.from_bech32(offer_hex) - offered, requested, infos, valid_times = offer.summary() - - if request.get("advanced", False): - response = { - "summary": { - "offered": offered, - "requested": requested, - "fees": offer.fees(), - "infos": infos, - "additions": [c.name().hex() for c in offer.additions()], - "removals": [c.name().hex() for c in offer.removals()], - "valid_times": { - k: v - for k, v in valid_times.to_json_dict().items() - if k - not in { - "max_secs_after_created", - "min_secs_since_created", - "max_blocks_after_created", - "min_blocks_since_created", - } - }, - }, - "id": offer.name(), - } + @marshal + async def get_offer_summary(self, request: GetOfferSummary) -> GetOfferSummaryResponse: + dl_summary = None + if not request.advanced: + dl_summary = await self.service.wallet_state_manager.trade_manager.get_dl_offer_summary( + request.parsed_offer + ) + if dl_summary is not None: + response = GetOfferSummaryResponse( + data_layer_summary=dl_summary, + id=request.parsed_offer.name(), + ) else: - response = { - "summary": await self.service.wallet_state_manager.trade_manager.get_offer_summary(offer), - "id": offer.name(), - } + offered, requested, infos, valid_times = request.parsed_offer.summary() + response = GetOfferSummaryResponse( + summary=OfferSummary( + offered=offered, + requested=requested, + fees=uint64(request.parsed_offer.fees()), + infos=infos, + additions=[c.name() for c in request.parsed_offer.additions()], + removals=[c.name() for c in request.parsed_offer.removals()], + valid_times=valid_times.only_absolutes(), + ), + id=request.parsed_offer.name(), + ) # This is a bit of a hack in favor of returning some more manageable information about CR-CATs # A more general solution surely exists, but I'm not sure what it is right now - return { - **response, - "summary": { - **response["summary"], # type: ignore[dict-item] - "infos": { + return dataclasses.replace( + response, + summary=dataclasses.replace( + response.summary, + infos={ key: ( - { - **info, - "also": { - **info["also"], - "flags": ProofsChecker.from_program( - uncurry_puzzle(Program(assemble(info["also"]["proofs_checker"]))) - ).flags, - }, - } - if "also" in info and "proofs_checker" in info["also"] + PuzzleInfo( + { + **info.info, + "also": { + **info.info["also"], + "flags": ProofsChecker.from_program( + uncurry_puzzle(Program(assemble(info.info["also"]["proofs_checker"]))) + ).flags, + }, + } + ) + if "also" in info.info and "proofs_checker" in info.info["also"] else info ) - for key, info in response["summary"]["infos"].items() # type: ignore[index] + for key, info in response.summary.infos.items() }, - }, - } + ) + if response.summary is not None + else None, + ) @marshal async def check_offer_validity(self, request: CheckOfferValidity) -> CheckOfferValidityResponse: diff --git a/chia/wallet/wallet_rpc_client.py b/chia/wallet/wallet_rpc_client.py index c3d183d3bc85..f4a42150e12e 100644 --- a/chia/wallet/wallet_rpc_client.py +++ b/chia/wallet/wallet_rpc_client.py @@ -108,6 +108,8 @@ GetNotifications, GetNotificationsResponse, GetOffersCountResponse, + GetOfferSummary, + GetOfferSummaryResponse, GetPrivateKey, GetPrivateKeyResponse, GetPublicKeysResponse, @@ -678,11 +680,8 @@ async def create_offer_for_ids( ) ) - async def get_offer_summary( - self, offer: Offer, advanced: bool = False - ) -> tuple[bytes32, dict[str, dict[str, int]]]: - res = await self.fetch("get_offer_summary", {"offer": offer.to_bech32(), "advanced": advanced}) - return bytes32.from_hexstr(res["id"]), res["summary"] + async def get_offer_summary(self, request: GetOfferSummary) -> GetOfferSummaryResponse: + return GetOfferSummaryResponse.from_json_dict(await self.fetch("get_offer_summary", request.to_json_dict())) async def check_offer_validity(self, request: CheckOfferValidity) -> CheckOfferValidityResponse: return CheckOfferValidityResponse.from_json_dict(