Skip to content

Commit

Permalink
Add valid_times to Offer object (#16255)
Browse files Browse the repository at this point in the history
  • Loading branch information
Quexington authored Sep 14, 2023
1 parent ab07de7 commit b6dae18
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 11 deletions.
4 changes: 2 additions & 2 deletions chia/cmds/wallet_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:")
Expand Down
12 changes: 11 additions & 1 deletion chia/rpc/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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
20 changes: 18 additions & 2 deletions chia/rpc/wallet_rpc_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
20 changes: 18 additions & 2 deletions chia/wallet/trade_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion chia/wallet/trade_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
45 changes: 43 additions & 2 deletions chia/wallet/trading/offer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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()

Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions tests/wallet/cat_wallet/test_offer_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
22 changes: 21 additions & 1 deletion tests/wallet/rpc/test_wallet_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit b6dae18

Please sign in to comment.