Skip to content

Commit b9710b1

Browse files
Quexingtonarvidn
andauthored
Add RCATs to wallet (#19795)
* Add RCATs to wallet * undo outer puzzle changes and fix tests * logging * update testnet overrides in wallet * some empirical debugging * comments by @altendky * fix an accident * a bit better * comment by @altendky * Add RCAT to all relevant enumerations * reorder --------- Co-authored-by: arvidn <[email protected]>
1 parent 1fba37e commit b9710b1

21 files changed

+657
-222
lines changed

chia/_tests/wallet/cat_wallet/test_cat_wallet.py

Lines changed: 161 additions & 92 deletions
Large diffs are not rendered by default.

chia/_tests/wallet/cat_wallet/test_trades.py

Lines changed: 15 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework
1313
from chia._tests.util.get_name_puzzle_conditions import get_name_puzzle_conditions
1414
from chia._tests.util.time_out_assert import time_out_assert
15+
from chia._tests.wallet.cat_wallet.test_cat_wallet import mint_cat
1516
from chia._tests.wallet.vc_wallet.test_vc_wallet import mint_cr_cat
1617
from chia.consensus.cost_calculator import NPCResult
1718
from chia.consensus.default_constants import DEFAULT_CONSTANTS
@@ -20,6 +21,7 @@
2021
from chia.util.bech32m import encode_puzzle_hash
2122
from chia.util.hash import std_hash
2223
from chia.wallet.cat_wallet.cat_wallet import CATWallet
24+
from chia.wallet.cat_wallet.r_cat_wallet import RCATWallet
2325
from chia.wallet.conditions import CreateCoinAnnouncement, parse_conditions_non_consensus
2426
from chia.wallet.did_wallet.did_wallet import DIDWallet
2527
from chia.wallet.outer_puzzles import AssetType
@@ -96,10 +98,12 @@ async def get_trade_and_status(trade_manager: TradeManager, trade: TradeRecord)
9698
],
9799
indirect=["wallet_environments"],
98100
)
101+
@pytest.mark.parametrize("wallet_type", [CATWallet, RCATWallet])
99102
@pytest.mark.limit_consensus_modes(reason="irrelevant")
100103
async def test_cat_trades(
101104
wallet_environments: WalletTestFramework,
102105
credential_restricted: bool,
106+
wallet_type: type[CATWallet],
103107
active_softfork_height: uint32,
104108
) -> None:
105109
# Setup
@@ -352,52 +356,11 @@ async def test_cat_trades(
352356
}
353357

354358
# Mint some standard CATs
355-
async with wallet_maker.wallet_state_manager.new_action_scope(
356-
wallet_environments.tx_config, push=True
357-
) as action_scope:
358-
cat_wallet_maker = await CATWallet.create_new_cat_wallet(
359-
wallet_node_maker.wallet_state_manager,
360-
wallet_maker,
361-
{"identifier": "genesis_by_id"},
362-
uint64(100),
363-
action_scope,
364-
)
365-
366-
async with wallet_taker.wallet_state_manager.new_action_scope(
367-
wallet_environments.tx_config, push=True
368-
) as action_scope:
369-
new_cat_wallet_taker = await CATWallet.create_new_cat_wallet(
370-
wallet_node_taker.wallet_state_manager,
371-
wallet_taker,
372-
{"identifier": "genesis_by_id"},
373-
uint64(100),
374-
action_scope,
375-
)
376-
377-
await wallet_environments.process_pending_states(
378-
[
379-
# Balance checking for this scenario is covered in test_cat_wallet
380-
WalletStateTransition(
381-
pre_block_balance_updates={
382-
"xch": {"set_remainder": True},
383-
"cat": {"init": True, "set_remainder": True},
384-
},
385-
post_block_balance_updates={
386-
"xch": {"set_remainder": True},
387-
"cat": {"set_remainder": True},
388-
},
389-
),
390-
WalletStateTransition(
391-
pre_block_balance_updates={
392-
"xch": {"set_remainder": True},
393-
"new cat": {"init": True, "set_remainder": True},
394-
},
395-
post_block_balance_updates={
396-
"xch": {"set_remainder": True},
397-
"new cat": {"set_remainder": True},
398-
},
399-
),
400-
]
359+
cat_wallet_maker = await mint_cat(
360+
wallet_environments, env_maker, "xch", "cat", uint64(100), wallet_type, "cat maker"
361+
)
362+
new_cat_wallet_taker = await mint_cat(
363+
wallet_environments, env_taker, "xch", "new cat", uint64(100), wallet_type, "cat taker"
401364
)
402365

403366
if credential_restricted:
@@ -428,8 +391,12 @@ async def test_cat_trades(
428391
proofs_checker_taker,
429392
)
430393
else:
431-
new_cat_wallet_maker = await CATWallet.get_or_create_wallet_for_cat(
432-
wallet_node_maker.wallet_state_manager, wallet_maker, new_cat_wallet_taker.get_asset_id()
394+
if wallet_type is RCATWallet:
395+
extra_args: Any = (bytes32.zeros,)
396+
else:
397+
extra_args = tuple()
398+
new_cat_wallet_maker = await wallet_type.get_or_create_wallet_for_cat(
399+
wallet_node_maker.wallet_state_manager, wallet_maker, new_cat_wallet_taker.get_asset_id(), *extra_args
433400
)
434401

435402
await env_maker.change_balances(

chia/cmds/wallet_funcs.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def get_mojo_per_unit(wallet_type: WalletType) -> int:
132132
WalletType.VC,
133133
}:
134134
mojo_per_unit = units["chia"]
135-
elif wallet_type in {WalletType.CAT, WalletType.CRCAT}:
135+
elif wallet_type in {WalletType.CAT, WalletType.CRCAT, WalletType.RCAT}:
136136
mojo_per_unit = units["cat"]
137137
elif wallet_type in {WalletType.NFT, WalletType.DECENTRALIZED_ID}:
138138
mojo_per_unit = units["mojo"]
@@ -166,7 +166,7 @@ async def get_unit_name_for_wallet_id(
166166
WalletType.VC,
167167
}:
168168
name: str = config["network_overrides"]["config"][config["selected_network"]]["address_prefix"].upper()
169-
elif wallet_type in {WalletType.CAT, WalletType.CRCAT}:
169+
elif wallet_type in {WalletType.CAT, WalletType.CRCAT, WalletType.RCAT}:
170170
name = await wallet_client.get_cat_name(wallet_id=wallet_id)
171171
else:
172172
raise LookupError(f"Operation is not supported for Wallet type {wallet_type.name}")
@@ -358,7 +358,7 @@ async def send(
358358
push=push,
359359
timelock_info=condition_valid_times,
360360
)
361-
elif typ in {WalletType.CAT, WalletType.CRCAT}:
361+
elif typ in {WalletType.CAT, WalletType.CRCAT, WalletType.RCAT}:
362362
print("Submitting transaction...")
363363
res = await wallet_client.cat_spend(
364364
wallet_id,
@@ -891,7 +891,7 @@ async def cancel_offer(
891891

892892

893893
def wallet_coin_unit(typ: WalletType, address_prefix: str) -> tuple[str, int]:
894-
if typ in {WalletType.CAT, WalletType.CRCAT}:
894+
if typ in {WalletType.CAT, WalletType.CRCAT, WalletType.RCAT}:
895895
return "", units["cat"]
896896
if typ in {WalletType.STANDARD_WALLET, WalletType.POOLING_WALLET, WalletType.MULTI_SIG}:
897897
return address_prefix, units["chia"]

chia/wallet/cat_wallet/cat_info.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ class CATInfo(Streamable):
1919
my_tail: Optional[Program] # this is the program
2020

2121

22+
@streamable
23+
@dataclass(frozen=True)
24+
class RCATInfo(CATInfo):
25+
hidden_puzzle_hash: bytes32
26+
27+
2228
@streamable
2329
@dataclass(frozen=True)
2430
class CATCoinData(Streamable):
@@ -41,8 +47,6 @@ class LegacyCATInfo(Streamable):
4147

4248
@streamable
4349
@dataclass(frozen=True)
44-
class CRCATInfo(Streamable):
45-
limitations_program_hash: bytes32
46-
my_tail: Optional[Program] # this is the program
50+
class CRCATInfo(CATInfo):
4751
authorized_providers: list[bytes32]
4852
proofs_checker: ProofsChecker

chia/wallet/cat_wallet/cat_outer_puzzle.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class CATOuterPuzzle:
2626
_match: Callable[[UncurriedPuzzle], Optional[PuzzleInfo]]
2727
_construct: Callable[[PuzzleInfo, Program], Program]
2828
_solve: Callable[[PuzzleInfo, Solver, Program, Program], Program]
29-
_get_inner_puzzle: Callable[[PuzzleInfo, UncurriedPuzzle], Optional[Program]]
29+
_get_inner_puzzle: Callable[[PuzzleInfo, UncurriedPuzzle, Optional[Program]], Optional[Program]]
3030
_get_inner_solution: Callable[[PuzzleInfo, Program], Optional[Program]]
3131

3232
def match(self, puzzle: UncurriedPuzzle) -> Optional[PuzzleInfo]:
@@ -43,14 +43,18 @@ def match(self, puzzle: UncurriedPuzzle) -> Optional[PuzzleInfo]:
4343
constructor_dict["also"] = next_constructor.info
4444
return PuzzleInfo(constructor_dict)
4545

46-
def get_inner_puzzle(self, constructor: PuzzleInfo, puzzle_reveal: UncurriedPuzzle) -> Optional[Program]:
46+
def get_inner_puzzle(
47+
self, constructor: PuzzleInfo, puzzle_reveal: UncurriedPuzzle, solution: Optional[Program] = None
48+
) -> Optional[Program]:
4749
args = match_cat_puzzle(puzzle_reveal)
4850
if args is None:
4951
raise ValueError("This driver is not for the specified puzzle reveal")
5052
_, _, inner_puzzle = args
5153
also = constructor.also()
5254
if also is not None:
53-
deep_inner_puzzle: Optional[Program] = self._get_inner_puzzle(also, uncurry_puzzle(inner_puzzle))
55+
deep_inner_puzzle: Optional[Program] = self._get_inner_puzzle(
56+
also, uncurry_puzzle(inner_puzzle), solution.first() if solution is not None else None
57+
)
5458
return deep_inner_puzzle
5559
else:
5660
return inner_puzzle

chia/wallet/cat_wallet/cat_wallet.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ class CATWallet:
9393
cat_info: CATInfo
9494
standard_wallet: Wallet
9595
lineage_store: CATLineageStore
96+
wallet_type: ClassVar[WalletType] = WalletType.CAT
97+
wallet_info_type: ClassVar[type[CATInfo]] = CATInfo
9698

9799
@staticmethod
98100
def default_wallet_name_for_unknown_cat(limitations_program_hash_hex: str) -> str:
@@ -120,15 +122,15 @@ async def create_new_cat_wallet(
120122

121123
# We use 00 bytes because it's not optional. We must check this is overridden during issuance.
122124
empty_bytes = bytes32(32 * b"\0")
123-
self.cat_info = CATInfo(empty_bytes, None)
125+
self.cat_info = self.wallet_info_type(empty_bytes, None)
124126
info_as_string = bytes(self.cat_info).hex()
125127
# If the name is not provided, it will be autogenerated based on the resulting tail hash.
126128
# For now, give the wallet a temporary name "CAT WALLET" until we get the tail hash
127129
original_name = name
128130
if name is None:
129131
name = "CAT WALLET"
130132

131-
self.wallet_info = await wallet_state_manager.user_store.create_wallet(name, WalletType.CAT, info_as_string)
133+
self.wallet_info = await wallet_state_manager.user_store.create_wallet(name, self.wallet_type, info_as_string)
132134

133135
try:
134136
spend_bundle = await ALL_LIMITATIONS_PROGRAMS[cat_tail_info["identifier"]].generate_issuance_bundle(
@@ -222,9 +224,9 @@ async def get_or_create_wallet_for_cat(
222224
name = self.default_wallet_name_for_unknown_cat(limitations_program_hash_hex)
223225

224226
limitations_program_hash = bytes32.from_hexstr(limitations_program_hash_hex)
225-
self.cat_info = CATInfo(limitations_program_hash, None)
227+
self.cat_info = cls.wallet_info_type(limitations_program_hash, None)
226228
info_as_string = bytes(self.cat_info).hex()
227-
self.wallet_info = await wallet_state_manager.user_store.create_wallet(name, WalletType.CAT, info_as_string)
229+
self.wallet_info = await wallet_state_manager.user_store.create_wallet(name, self.wallet_type, info_as_string)
228230

229231
self.lineage_store = await CATLineageStore.create(self.wallet_state_manager.db_wrapper, self.get_asset_id())
230232
await self.wallet_state_manager.add_new_wallet(self)
@@ -273,26 +275,27 @@ async def create_from_puzzle_info(
273275
name,
274276
)
275277

276-
@staticmethod
278+
@classmethod
277279
async def create(
280+
cls,
278281
wallet_state_manager: WalletStateManager,
279282
wallet: Wallet,
280283
wallet_info: WalletInfo,
281-
) -> CATWallet:
282-
self = CATWallet()
284+
) -> Self:
285+
self = cls()
283286

284287
self.log = logging.getLogger(__name__)
285288

286289
self.wallet_state_manager = wallet_state_manager
287290
self.wallet_info = wallet_info
288291
self.standard_wallet = wallet
289292
try:
290-
self.cat_info = CATInfo.from_bytes(hexstr_to_bytes(self.wallet_info.data))
293+
self.cat_info = cls.wallet_info_type.from_bytes(hexstr_to_bytes(self.wallet_info.data))
291294
self.lineage_store = await CATLineageStore.create(self.wallet_state_manager.db_wrapper, self.get_asset_id())
292295
except AssertionError:
293296
# Do a migration of the lineage proofs
294297
cat_info = LegacyCATInfo.from_bytes(hexstr_to_bytes(self.wallet_info.data))
295-
self.cat_info = CATInfo(cat_info.limitations_program_hash, cat_info.my_tail)
298+
self.cat_info = cls.wallet_info_type(cat_info.limitations_program_hash, cat_info.my_tail)
296299
self.lineage_store = await CATLineageStore.create(self.wallet_state_manager.db_wrapper, self.get_asset_id())
297300
for coin_id, lineage in cat_info.lineage_proofs:
298301
await self.add_lineage(coin_id, lineage)
@@ -302,7 +305,7 @@ async def create(
302305

303306
@classmethod
304307
def type(cls) -> WalletType:
305-
return WalletType.CAT
308+
return cls.wallet_type
306309

307310
def id(self) -> uint32:
308311
return self.wallet_info.id
@@ -601,6 +604,14 @@ async def create_tandem_xch_tx(
601604

602605
return announcement
603606

607+
async def make_inner_solution(
608+
self,
609+
coin: Coin,
610+
primaries: list[CreateCoin],
611+
conditions: tuple[Condition, ...] = tuple(),
612+
) -> Program:
613+
return self.standard_wallet.make_solution(primaries=primaries, conditions=conditions)
614+
604615
async def generate_unsigned_spendbundle(
605616
self,
606617
payments: list[CreateCoin],
@@ -686,7 +697,8 @@ async def generate_unsigned_spendbundle(
686697
action_scope,
687698
extra_conditions=(announcement.corresponding_assertion(),),
688699
)
689-
innersol = self.standard_wallet.make_solution(
700+
innersol = await self.make_inner_solution(
701+
coin=coin,
690702
primaries=primaries,
691703
conditions=(*extra_conditions, announcement),
692704
)
@@ -697,21 +709,23 @@ async def generate_unsigned_spendbundle(
697709
action_scope,
698710
)
699711
assert xch_announcement is not None
700-
innersol = self.standard_wallet.make_solution(
712+
innersol = await self.make_inner_solution(
713+
coin=coin,
701714
primaries=primaries,
702715
conditions=(*extra_conditions, xch_announcement, announcement),
703716
)
704717
else:
705718
# TODO: what about when they are equal?
706719
raise Exception("Equality not handled")
707720
else:
708-
innersol = self.standard_wallet.make_solution(
721+
innersol = await self.make_inner_solution(
722+
coin=coin,
709723
primaries=primaries,
710724
conditions=(*extra_conditions, announcement),
711725
)
712726
else:
713-
innersol = self.standard_wallet.make_solution(
714-
primaries=[], conditions=(announcement.corresponding_assertion(),)
727+
innersol = await self.make_inner_solution(
728+
coin=coin, primaries=[], conditions=(announcement.corresponding_assertion(),)
715729
)
716730
inner_puzzle = await self.inner_puzzle_for_cat_puzhash(coin.puzzle_hash)
717731
lineage_proof = await self.get_lineage_proof_for_coin(coin)

chia/wallet/cat_wallet/lineage_store.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,10 @@ async def get_all_lineage_proofs(self) -> dict[bytes32, LineageProof]:
7373
lineage_dict[bytes32.from_hexstr(row[0])] = LineageProof.from_bytes(row[1])
7474

7575
return lineage_dict
76+
77+
async def is_empty(self) -> bool:
78+
async with self.db_wrapper.reader_no_transaction() as conn:
79+
cursor = await conn.execute(f"SELECT COUNT(*) FROM {self.table_name}")
80+
row_count = await cursor.fetchone()
81+
assert row_count is not None
82+
return bool(row_count[0] == 0)

0 commit comments

Comments
 (0)