diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 86c32aadde28..1fe3417d4ff5 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -613,7 +613,7 @@ async def print_trade_record(record: TradeRecord, wallet_client: WalletRpcClient if summaries: print("Summary:") offer = Offer.from_bytes(record.offer) - offered, requested, _ = offer.summary() + offered, requested, _, _ = offer.summary() outbound_balances: Dict[str, int] = offer.get_pending_amounts() fees: Decimal = Decimal(offer.fees()) cat_name_resolver = wallet_client.cat_asset_id_to_name @@ -701,7 +701,7 @@ async def take_offer( print("Please enter a valid offer file or hex blob") return - offered, requested, _ = offer.summary() + offered, requested, _, _ = offer.summary() cat_name_resolver = wallet_client.cat_asset_id_to_name network_xch = AddressType.XCH.hrp(config).upper() print("Summary:") diff --git a/chia/rpc/util.py b/chia/rpc/util.py index d7407b560b8d..edee869ba40e 100644 --- a/chia/rpc/util.py +++ b/chia/rpc/util.py @@ -8,7 +8,7 @@ from chia.types.blockchain_format.coin import Coin from chia.util.json_util import obj_to_response -from chia.wallet.conditions import Condition, ConditionValidTimes, conditions_from_json_dicts +from chia.wallet.conditions import Condition, ConditionValidTimes, conditions_from_json_dicts, parse_timelock_info from chia.wallet.util.tx_config import TXConfig, TXConfigLoader log = logging.getLogger(__name__) @@ -69,6 +69,16 @@ async def rpc_endpoint(self, request: Dict[str, Any], *args, **kwargs) -> Dict[s if "extra_conditions" in request: extra_conditions = tuple(conditions_from_json_dicts(request["extra_conditions"])) extra_conditions = (*extra_conditions, *ConditionValidTimes.from_json_dict(request).to_conditions()) + + valid_times: ConditionValidTimes = parse_timelock_info(extra_conditions) + if ( + valid_times.max_secs_after_created is not None + or valid_times.min_secs_since_created is not None + or valid_times.max_blocks_after_created is not None + or valid_times.min_blocks_since_created is not None + ): + raise ValueError("Relative timelocks are not currently supported in the RPC") + return await func(self, request, *args, tx_config=tx_config, extra_conditions=extra_conditions, **kwargs) return rpc_endpoint diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 7aa033b0af22..9ca666f09476 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -1730,11 +1730,27 @@ async def get_offer_summary(self, request: Dict[str, Any]) -> EndpointResult: ### offer = Offer.from_bech32(offer_hex) - offered, requested, infos = offer.summary() + offered, requested, infos, valid_times = offer.summary() if request.get("advanced", False): response = { - "summary": {"offered": offered, "requested": requested, "fees": offer.fees(), "infos": infos}, + "summary": { + "offered": offered, + "requested": requested, + "fees": offer.fees(), + "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", + ) + }, + }, "id": offer.name(), } else: diff --git a/chia/wallet/trade_manager.py b/chia/wallet/trade_manager.py index 762668a5e036..25c53c80465a 100644 --- a/chia/wallet/trade_manager.py +++ b/chia/wallet/trade_manager.py @@ -880,8 +880,24 @@ async def get_offer_summary(self, offer: Offer) -> Dict[str, Any]: ): return await DataLayerWallet.get_offer_summary(offer) # Otherwise just return the same thing as the RPC normally does - offered, requested, infos = offer.summary() - return {"offered": offered, "requested": requested, "fees": offer.fees(), "infos": infos} + offered, requested, infos, valid_times = offer.summary() + return { + "offered": offered, + "requested": requested, + "fees": offer.fees(), + "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", + ) + }, + } async def check_for_final_modifications( self, offer: Offer, solver: Solver, tx_config: TXConfig diff --git a/chia/wallet/trade_record.py b/chia/wallet/trade_record.py index 845eab3510f5..3b8a91d5d9c0 100644 --- a/chia/wallet/trade_record.py +++ b/chia/wallet/trade_record.py @@ -38,7 +38,7 @@ def to_json_dict_convenience(self) -> Dict[str, Any]: formatted["status"] = TradeStatus(self.status).name offer_to_summarize: bytes = self.offer if self.taken_offer is None else self.taken_offer offer = Offer.from_bytes(offer_to_summarize) - offered, requested, infos = offer.summary() + offered, requested, infos, _ = offer.summary() formatted["summary"] = { "offered": offered, "requested": requested, diff --git a/chia/wallet/trading/offer.py b/chia/wallet/trading/offer.py index ca57e4b9fde3..f3329dfd1f28 100644 --- a/chia/wallet/trading/offer.py +++ b/chia/wallet/trading/offer.py @@ -16,6 +16,7 @@ from chia.util.bech32m import bech32_decode, bech32_encode, convertbits from chia.util.errors import Err, ValidationError from chia.util.ints import uint64 +from chia.wallet.conditions import Condition, ConditionValidTimes, parse_conditions_non_consensus, parse_timelock_info from chia.wallet.outer_puzzles import ( construct_puzzle, create_asset_id, @@ -77,6 +78,7 @@ class Offer: _additions: Dict[Coin, List[Coin]] = field(init=False) _offered_coins: Dict[Optional[bytes32], List[Coin]] = field(init=False) _final_spend_bundle: Optional[SpendBundle] = field(init=False) + _conditions: Optional[Dict[Coin, List[Condition]]] = field(init=False) @staticmethod def ph() -> bytes32: @@ -148,6 +150,40 @@ def __post_init__(self) -> None: if max_cost < 0: raise ValidationError(Err.BLOCK_COST_EXCEEDS_MAX, "compute_additions for CoinSpend") object.__setattr__(self, "_additions", adds) + object.__setattr__(self, "_conditions", None) + + def conditions(self) -> Dict[Coin, List[Condition]]: + if self._conditions is None: + conditions: Dict[Coin, List[Condition]] = {} + max_cost = DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM + for cs in self._bundle.coin_spends: + try: + cost, conds = cs.puzzle_reveal.run_with_cost(max_cost, cs.solution) + max_cost -= cost + conditions[cs.coin] = parse_conditions_non_consensus(conds.as_iter()) + except Exception: # pragma: no cover + continue + if max_cost < 0: # pragma: no cover + raise ValidationError(Err.BLOCK_COST_EXCEEDS_MAX, "computing conditions for CoinSpend") + object.__setattr__(self, "_conditions", conditions) + assert self._conditions is not None, "self._conditions is None" + return self._conditions + + def valid_times(self) -> Dict[Coin, ConditionValidTimes]: + return {coin: parse_timelock_info(conditions) for coin, conditions in self.conditions().items()} + + def absolute_valid_times_ban_relatives(self) -> ConditionValidTimes: + valid_times: ConditionValidTimes = parse_timelock_info( + [c for conditions in self.conditions().values() for c in conditions] + ) + if ( + valid_times.max_secs_after_created is not None + or valid_times.min_secs_since_created is not None + or valid_times.max_blocks_after_created is not None + or valid_times.min_blocks_since_created is not None + ): + raise ValueError("Offers with relative timelocks are not currently supported") + return valid_times def additions(self) -> List[Coin]: return [c for additions in self._additions.values() for c in additions] @@ -270,7 +306,7 @@ 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]]]: + def summary(self) -> Tuple[Dict[str, int], Dict[str, int], Dict[str, Dict[str, Any]], ConditionValidTimes]: offered_amounts: Dict[Optional[bytes32], int] = self.get_offered_amounts() requested_amounts: Dict[Optional[bytes32], int] = self.get_requested_amounts() @@ -287,7 +323,12 @@ def keys_to_strings(dic: Dict[Optional[bytes32], Any]) -> Dict[str, Any]: for key, value in self.driver_dict.items(): driver_dict[key.hex()] = value.info - return keys_to_strings(offered_amounts), keys_to_strings(requested_amounts), driver_dict + return ( + keys_to_strings(offered_amounts), + keys_to_strings(requested_amounts), + driver_dict, + self.absolute_valid_times_ban_relatives(), + ) # Also mostly for the UI, returns a dictionary of assets and how much of them is pended for this offer # This method is also imperfect for sufficiently complex spends diff --git a/tests/wallet/cat_wallet/test_offer_lifecycle.py b/tests/wallet/cat_wallet/test_offer_lifecycle.py index 8dd0e1d46dd2..ad7423e6e6f3 100644 --- a/tests/wallet/cat_wallet/test_offer_lifecycle.py +++ b/tests/wallet/cat_wallet/test_offer_lifecycle.py @@ -21,6 +21,7 @@ construct_cat_puzzle, unsigned_spend_bundle_for_spendable_cats, ) +from chia.wallet.conditions import ConditionValidTimes from chia.wallet.outer_puzzles import AssetType from chia.wallet.payment import Payment from chia.wallet.puzzle_drivers import PuzzleInfo @@ -288,6 +289,7 @@ async def test_complex_offer(self, cost_logger): }, {"xch": 900, str_to_tail_hash("red").hex(): 350}, driver_dict_as_infos, + ConditionValidTimes(), ) assert new_offer.get_pending_amounts() == { "xch": 1200, diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index e890e97546d4..7f76b8794a9f 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -42,6 +42,7 @@ from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS 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.conditions import ConditionValidTimes from chia.wallet.derive_keys import master_sk_to_wallet_sk, master_sk_to_wallet_sk_unhardened from chia.wallet.did_wallet.did_wallet import DIDWallet from chia.wallet.nft_wallet.nft_wallet import NFTWallet @@ -1148,7 +1149,18 @@ async def test_offer_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment) assert id == offer.name() id, advanced_summary = await wallet_1_rpc.get_offer_summary(offer, advanced=True) assert id == offer.name() - assert summary == {"offered": {"xch": 5}, "requested": {cat_asset_id.hex(): 1}, "infos": driver_dict, "fees": 1} + assert summary == { + "offered": {"xch": 5}, + "requested": {cat_asset_id.hex(): 1}, + "infos": driver_dict, + "fees": 1, + "valid_times": { + "max_height": None, + "max_time": None, + "min_height": None, + "min_time": None, + }, + } assert advanced_summary == summary id, valid = await wallet_1_rpc.check_offer_validity(offer) @@ -1298,6 +1310,14 @@ def only_ids(trades): assert len([o for o in await wallet_1_rpc.get_all_offers() if o.status == TradeStatus.PENDING_ACCEPT.value]) == 0 await time_out_assert(5, check_mempool_spend_count, True, full_node_api, 1) + with pytest.raises(ValueError, match="not currently supported"): + await wallet_1_rpc.create_offer_for_ids( + {uint32(1): -5, cat_asset_id.hex(): 1}, + DEFAULT_TX_CONFIG, + driver_dict=driver_dict, + timelock_info=ConditionValidTimes(min_secs_since_created=uint64(1)), + ) + @pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN, ConsensusMode.HARD_FORK_2_0], reason="save time") @pytest.mark.asyncio