Files
chia-blockchain/chia/wallet/vc_wallet/cr_cat_wallet.py
T
Arvid NorbergandGitHub fe15e9ee6f [CHIA-4326] enable redundant expressions check in mypy (#21143)
* fix type annotation in configure.py and init.py

* enable mypy redundant-expr and remove redundant expressions
2026-07-29 09:37:24 -05:00

872 lines
38 KiB
Python

from __future__ import annotations
import logging
import time
import traceback
from typing import TYPE_CHECKING, Any, ClassVar
from chia_rs import CoinSpend, G1Element, G2Element
from chia_rs.sized_bytes import bytes32
from chia_rs.sized_ints import uint8, uint32, uint64, uint128
from typing_extensions import Self, Unpack
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
from chia.util.byte_types import hexstr_to_bytes
from chia.util.hash import std_hash
from chia.util.streamable import VersionedBlob
from chia.wallet.cat_wallet.cat_info import CRCATInfo
from chia.wallet.cat_wallet.cat_utils import CAT_MOD_HASH, CAT_MOD_HASH_HASH, 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,
ConditionValidTimes,
CreateCoin,
CreateCoinAnnouncement,
CreatePuzzleAnnouncement,
UnknownCondition,
parse_timelock_info,
)
from chia.wallet.lineage_proof import LineageProof
from chia.wallet.outer_puzzles import AssetType
from chia.wallet.puzzle_drivers import PuzzleInfo
from chia.wallet.trading.offer import Offer
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.uncurried_puzzle import uncurry_puzzle
from chia.wallet.util.compute_hints import compute_spend_hints_and_additions
from chia.wallet.util.compute_memos import compute_memos
from chia.wallet.util.query_filter import HashFilter
from chia.wallet.util.transaction_type import TransactionType
from chia.wallet.util.wallet_sync_utils import fetch_coin_spend_for_coin_state
from chia.wallet.util.wallet_types import CoinType, WalletType
from chia.wallet.vc_wallet.cr_cat_drivers import (
CRCAT,
CRCATMetadata,
CRCATVersion,
ProofsChecker,
construct_cr_layer_hash,
construct_pending_approval_state,
)
from chia.wallet.vc_wallet.vc_drivers import VerifiedCredential
from chia.wallet.vc_wallet.vc_wallet import VCWallet
from chia.wallet.wallet import Wallet
from chia.wallet.wallet_action_scope import WalletActionScope
from chia.wallet.wallet_coin_record import MetadataTypes, WalletCoinRecord
from chia.wallet.wallet_info import WalletInfo
from chia.wallet.wallet_protocol import GSTOptionalArgs, WalletProtocol
from chia.wallet.wallet_spend_bundle import WalletSpendBundle
if TYPE_CHECKING:
from chia.wallet.wallet_state_manager import WalletStateManager
class CRCATWallet(CATWallet):
wallet_state_manager: WalletStateManager
log: logging.Logger
wallet_info: WalletInfo
info: CRCATInfo
standard_wallet: Wallet
wallet_type: ClassVar[WalletType] = WalletType.CRCAT
wallet_info_type: ClassVar[type[CRCATInfo]] = CRCATInfo
@staticmethod
def default_wallet_name_for_unknown_cat(limitations_program_hash: bytes32) -> str:
return f"CAT {limitations_program_hash.hex()[:16]}..."
@property
def cost_of_single_tx(self) -> int:
return 78000000 # Estimate measured in testing
@staticmethod
async def create_new_cat_wallet(
wallet_state_manager: WalletStateManager,
wallet: Wallet,
cat_tail_info: dict[str, Any],
amount: uint64,
action_scope: WalletActionScope,
fee: uint64 = uint64(0),
name: str | None = None,
push: bool = False,
) -> CATWallet: # pragma: no cover
raise NotImplementedError("create_new_cat_wallet is a legacy method and is not available on CR-CAT wallets")
@classmethod
async def get_or_create_wallet_for_cat(
cls,
wallet_state_manager: WalletStateManager,
wallet: Wallet,
limitations_program_hash: bytes32,
name: str | None = None,
authorized_providers: list[bytes32] | None = None,
proofs_checker: ProofsChecker | None = None,
) -> Self:
if authorized_providers is None or proofs_checker is None: # pragma: no cover
raise ValueError("get_or_create_wallet_for_cat was call on CRCATWallet without proper arguments")
self = cls()
self.standard_wallet = wallet
if name is None:
name = self.default_wallet_name_for_unknown_cat(limitations_program_hash)
self.log = logging.getLogger(name)
for id, w in wallet_state_manager.wallets.items():
if w.type() == cls.type():
assert isinstance(w, cls)
if w.get_asset_id() == limitations_program_hash:
self.log.warning("Not creating wallet for already existing CR-CAT wallet")
return w
self.wallet_state_manager = wallet_state_manager
self.info = cls.wallet_info_type(limitations_program_hash, None, authorized_providers, proofs_checker)
info_as_string = bytes(self.info).hex()
self.wallet_info = await wallet_state_manager.user_store.create_wallet(name, WalletType.CRCAT, info_as_string)
await self.wallet_state_manager.add_new_wallet(self)
return self
@classmethod
async def create_from_puzzle_info(
cls,
wallet_state_manager: WalletStateManager,
wallet: Wallet,
puzzle_driver: PuzzleInfo,
name: str | None = None,
# We're hinting this as Any for mypy by should explore adding this to the wallet protocol and hinting properly
potential_subclasses: dict[AssetType, Any] | None = None,
) -> Any:
if potential_subclasses is None:
potential_subclasses = {}
cr_layer: PuzzleInfo | None = puzzle_driver.also()
if cr_layer is None: # pragma: no cover
raise ValueError("create_from_puzzle_info called on CRCATWallet with a non CR-CAT puzzle driver")
return await cls.get_or_create_wallet_for_cat(
wallet_state_manager,
wallet,
puzzle_driver["tail"],
name,
[bytes32(provider) for provider in cr_layer["authorized_providers"]],
ProofsChecker.from_program(uncurry_puzzle(cr_layer["proofs_checker"])),
)
@staticmethod
async def create(
wallet_state_manager: WalletStateManager,
wallet: Wallet,
wallet_info: WalletInfo,
) -> CRCATWallet:
self = CRCATWallet()
self.log = logging.getLogger(__name__)
self.wallet_state_manager = wallet_state_manager
self.wallet_info = wallet_info
self.standard_wallet = wallet
self.info = self.wallet_info_type.from_bytes(hexstr_to_bytes(self.wallet_info.data))
return self
@classmethod
async def convert_to_cr(
cls,
cat_wallet: CATWallet,
authorized_providers: list[bytes32],
proofs_checker: ProofsChecker,
) -> None:
replace_self = cls()
replace_self.standard_wallet = cat_wallet.standard_wallet
replace_self.log = logging.getLogger(cat_wallet.get_name())
replace_self.log.info(f"Converting CAT wallet {cat_wallet.id()} to CR-CAT wallet")
replace_self.wallet_state_manager = cat_wallet.wallet_state_manager
replace_self.info = cls.wallet_info_type(
cat_wallet.cat_info.limitations_program_hash, None, authorized_providers, proofs_checker
)
await cat_wallet.wallet_state_manager.user_store.update_wallet(
WalletInfo(
cat_wallet.id(), cat_wallet.get_name(), uint8(WalletType.CRCAT.value), bytes(replace_self.info).hex()
)
)
updated_wallet_info = await cat_wallet.wallet_state_manager.user_store.get_wallet_by_id(cat_wallet.id())
assert updated_wallet_info is not None
replace_self.wallet_info = updated_wallet_info
cat_wallet.wallet_state_manager.wallets[cat_wallet.id()] = replace_self
@classmethod
def type(cls) -> WalletType:
return WalletType.CRCAT
def id(self) -> uint32:
return self.wallet_info.id
def get_asset_id(self) -> bytes32:
return self.info.limitations_program_hash
async def set_tail_program(self, tail_program: str) -> None: # pragma: no cover
raise NotImplementedError("set_tail_program is a legacy method and is not available on CR-CAT wallets")
async def coin_added(self, coin: Coin, height: uint32, peer: WSChiaConnection, coin_data: object | None) -> None:
"""Notification from wallet state manager that wallet has been received."""
self.log.info(f"CR-CAT wallet has been notified that {coin.name().hex()} was added")
try:
coin_state = await self.wallet_state_manager.wallet_node.get_coin_state([coin.parent_coin_info], peer=peer)
coin_spend = await fetch_coin_spend_for_coin_state(coin_state[0], peer)
await self.add_crcat_coin(coin_spend, coin, height)
except Exception as e:
self.log.debug(f"Exception: {e}, traceback: {traceback.format_exc()}")
async def add_crcat_coin(self, coin_spend: CoinSpend, coin: Coin, height: uint32) -> None:
try:
new_cr_cats: list[CRCAT] = CRCAT.get_next_from_coin_spend(coin_spend)
hint_dict = {
id: hc.hint
for id, hc in compute_spend_hints_and_additions(coin_spend)[0].items()
if hc.hint is not None
}
cr_cat: CRCAT = next(filter(lambda c: c.coin.name() == coin.name(), new_cr_cats))
if (
await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(
cr_cat.inner_puzzle_hash
)
is not None
):
self.log.info(f"Found CRCAT coin {coin.name().hex()}")
is_pending = False
elif (
cr_cat.inner_puzzle_hash
== construct_pending_approval_state(
hint_dict[coin.name()],
uint64(coin.amount),
).get_tree_hash()
):
self.log.info(f"Found pending approval CRCAT coin {coin.name().hex()}")
is_pending = True
created_timestamp = await self.wallet_state_manager.wallet_node.get_timestamp_for_height(uint32(height))
spend_bundle = WalletSpendBundle([coin_spend], G2Element())
memos = compute_memos(spend_bundle)
# This will override the tx created in the wallet state manager
tx_record = TransactionRecord(
confirmed_at_height=height,
created_at_time=uint64(created_timestamp),
to_puzzle_hash=hint_dict[coin.name()],
to_address=self.wallet_state_manager.encode_puzzle_hash(hint_dict[coin.name()]),
amount=uint64(coin.amount),
fee_amount=uint64(0),
confirmed=True,
sent=uint32(0),
spend_bundle=None,
additions=[coin],
removals=[coin_spend.coin],
wallet_id=self.id(),
sent_to=[],
trade_id=None,
type=uint32(TransactionType.INCOMING_CRCAT_PENDING),
name=coin.name(),
memos=memos,
valid_times=ConditionValidTimes(),
)
await self.wallet_state_manager.tx_store.add_transaction_record(tx_record)
else: # pragma: no cover
self.log.error(f"Unknown CRCAT inner puzzle, coin ID: {coin.name().hex()}")
return None
coin_record = WalletCoinRecord(
coin,
uint32(height),
uint32(0),
False,
False,
WalletType.CRCAT,
self.id(),
CoinType.CRCAT_PENDING if is_pending else CoinType.CRCAT,
VersionedBlob(
CRCATVersion.V1.value,
bytes(
CRCATMetadata(
cr_cat.lineage_proof, hint_dict[coin.name()] if is_pending else cr_cat.inner_puzzle_hash
)
),
),
)
await self.wallet_state_manager.coin_store.add_coin_record(coin_record)
except Exception:
# The parent is not a CAT which means we need to scrub all of its children from our DB
self.log.error(f"Cannot add CRCAT coin: {traceback.format_exc()}")
child_coin_records = await self.wallet_state_manager.coin_store.get_coin_records_by_parent_id(
coin_spend.coin.name()
)
if len(child_coin_records) > 0:
for record in child_coin_records:
if record.wallet_id == self.id(): # pragma: no cover
await self.wallet_state_manager.coin_store.delete_coin_record(record.coin.name())
# We also need to make sure there's no record of the transaction
await self.wallet_state_manager.tx_store.delete_transaction_record(record.coin.name())
def require_derivation_paths(self) -> bool:
return False
def puzzle_for_pk(self, pubkey: G1Element) -> Program: # pragma: no cover
raise NotImplementedError("puzzle_for_pk is a legacy method and is not available on CR-CAT wallets")
def puzzle_hash_for_pk(self, pubkey: G1Element) -> bytes32: # pragma: no cover
raise NotImplementedError("puzzle_hash_for_pk is a legacy method and is not available on CR-CAT wallets")
async def get_new_cat_puzzle_hash(self) -> bytes32: # pragma: no cover
raise NotImplementedError("get_new_cat_puzzle_hash is a legacy method and is not available on CR-CAT wallets")
async def inner_puzzle_for_cat_puzhash(self, cat_hash: bytes32) -> Program: # pragma: no cover
raise NotImplementedError(
"inner_puzzle_for_cat_puzhash is a legacy method and is not available on CR-CAT wallets"
)
async def is_coin_spendable(self, record: WalletCoinRecord) -> bool:
crcat: CRCAT = self.coin_record_to_crcat(record)
if not crcat.lineage_proof.is_none():
return True
return False
async def get_confirmed_balance(self, record_list: set[WalletCoinRecord] | None = None) -> uint128:
if record_list is None:
record_list = await self.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(
self.id(), CoinType.CRCAT
)
amount: uint128 = uint128(0)
for record in record_list:
crcat: CRCAT = self.coin_record_to_crcat(record)
if not crcat.lineage_proof.is_none():
amount = uint128(amount + record.coin.amount)
self.log.info(f"Confirmed balance for cat wallet {self.id()} is {amount}")
return uint128(amount)
async def get_pending_approval_balance(self, record_list: set[WalletCoinRecord] | None = None) -> uint128:
if record_list is None:
record_list = await self.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(
self.id(), CoinType.CRCAT_PENDING
)
amount: uint128 = uint128(0)
for record in record_list:
crcat: CRCAT = self.coin_record_to_crcat(record)
if not crcat.lineage_proof.is_none():
amount = uint128(amount + record.coin.amount)
self.log.info(f"Pending approval balance for cat wallet {self.id()} is {amount}")
return uint128(amount)
async def convert_puzzle_hash(self, puzzle_hash: bytes32) -> bytes32:
return puzzle_hash
@staticmethod
def get_metadata_from_record(coin_record: WalletCoinRecord) -> CRCATMetadata:
metadata: MetadataTypes = coin_record.parsed_metadata()
assert isinstance(metadata, CRCATMetadata)
return metadata
def coin_record_to_crcat(self, coin_record: WalletCoinRecord) -> CRCAT:
if coin_record.coin_type not in {CoinType.CRCAT, CoinType.CRCAT_PENDING}: # pragma: no cover
raise ValueError(f"Attempting to spend a non-CRCAT coin: {coin_record.coin.name().hex()}")
if coin_record.metadata is None: # pragma: no cover
raise ValueError(f"Attempting to spend a CRCAT coin without metadata: {coin_record.coin.name().hex()}")
try:
metadata: CRCATMetadata = CRCATWallet.get_metadata_from_record(coin_record)
crcat: CRCAT = CRCAT(
coin_record.coin,
self.info.limitations_program_hash,
metadata.lineage_proof,
self.info.authorized_providers,
self.info.proofs_checker.as_program(),
(
construct_pending_approval_state(
metadata.inner_puzzle_hash, uint64(coin_record.coin.amount)
).get_tree_hash()
if coin_record.coin_type == CoinType.CRCAT_PENDING
else metadata.inner_puzzle_hash
),
)
return crcat
except Exception as e: # pragma: no cover
raise ValueError(f"Error parsing CRCAT metadata: {e}")
async def get_lineage_proof_for_coin(self, coin: Coin) -> LineageProof | None: # pragma: no cover
raise RuntimeError("get_lineage_proof_for_coin is a legacy method and is not available on CR-CAT wallets")
async def _generate_unsigned_spendbundle(
self,
payments: list[CreateCoin],
action_scope: WalletActionScope,
fee: uint64 = uint64(0),
cat_discrepancy: tuple[int, Program, Program] | None = None, # (extra_delta, tail_reveal, tail_solution)
coins: set[Coin] | None = None,
extra_conditions: tuple[Condition, ...] = tuple(),
add_authorizations_to_cr_cats: bool = True,
) -> WalletSpendBundle:
if cat_discrepancy is not None:
extra_delta, tail_reveal, tail_solution = cat_discrepancy
else:
extra_delta, tail_reveal, tail_solution = 0, Program.to([]), Program.to([])
payment_amount: int = sum(p.amount for p in payments)
starting_amount: int = payment_amount - extra_delta
if coins is None:
cat_coins = list(
await self.select_coins(
uint64(starting_amount),
action_scope,
)
)
else:
cat_coins = list(coins)
cat_coins = sorted(cat_coins, key=Coin.name) # need determinism because we need definitive origin coin
selected_cat_amount = sum(c.amount for c in cat_coins)
assert selected_cat_amount >= starting_amount
# Figure out if we need to absorb/melt some XCH as part of this
regular_chia_to_claim: int = 0
if payment_amount > starting_amount:
# TODO: The no coverage comment is because minting is broken for both this and the standard CAT wallet
fee = uint64(fee + payment_amount - starting_amount) # pragma: no cover
elif payment_amount < starting_amount:
regular_chia_to_claim = payment_amount
need_chia_transaction = (fee > 0 or regular_chia_to_claim > 0) and (fee - regular_chia_to_claim != 0)
# Calculate standard puzzle solutions
change = selected_cat_amount - starting_amount
primaries: list[CreateCoin] = []
for payment in payments:
primaries.append(payment)
if change > 0:
origin_crcat_record = await self.wallet_state_manager.coin_store.get_coin_record(
next(iter(cat_coins)).name()
)
if origin_crcat_record is None:
raise RuntimeError("A CR-CAT coin was selected that we don't have a record for") # pragma: no cover
origin_crcat = self.coin_record_to_crcat(origin_crcat_record)
if add_authorizations_to_cr_cats:
change_puzhash = await action_scope.get_puzzle_hash(self.wallet_state_manager)
else:
change_puzhash = origin_crcat.inner_puzzle_hash
for payment in payments:
if change_puzhash == payment.puzzle_hash and change == payment.amount:
# We cannot create two coins has same id, create a new puzhash for the change
change_puzhash = await action_scope.get_puzzle_hash(
self.wallet_state_manager, override_reuse_puzhash_with=False
)
break
primaries.append(CreateCoin(change_puzhash, uint64(change), [change_puzhash]))
# Find the VC Wallet
vc_wallet: VCWallet
for wallet in self.wallet_state_manager.wallets.values():
if WalletType(wallet.type()) == WalletType.VC:
assert isinstance(wallet, VCWallet)
vc_wallet = wallet
break
else:
raise RuntimeError("CR-CATs cannot be spent without an appropriate VC") # pragma: no cover
# Loop through the coins we've selected and gather the information we need to spend them
vc: VerifiedCredential | None = None
vc_announcements_to_make: list[bytes] = []
inner_spends: list[tuple[CRCAT, int, Program, Program]] = []
first = True
announcement: CreateCoinAnnouncement | None = None
coin_ids: list[bytes32] = [coin.name() for coin in cat_coins]
coin_records: list[WalletCoinRecord] = (
await self.wallet_state_manager.coin_store.get_coin_records(coin_id_filter=HashFilter.include(coin_ids))
).records
assert len(coin_records) == len(cat_coins)
# sort the coin records to ensure they are in the same order as the CAT coins
coin_records = [rec for rec in sorted(coin_records, key=lambda rec: coin_ids.index(rec.coin.name()))]
for coin in coin_records:
if vc is None:
vc = await vc_wallet.get_vc_with_provider_in_and_proofs(
self.info.authorized_providers, self.info.proofs_checker.flags
)
if cat_discrepancy is not None:
cat_condition = UnknownCondition(
opcode=Program.to(51),
args=[
Program.NIL,
Program.to(-113),
tail_reveal,
tail_solution,
],
)
if first:
extra_conditions = (*extra_conditions, cat_condition)
crcat: CRCAT = self.coin_record_to_crcat(coin)
vc_announcements_to_make.append(crcat.expected_announcement())
if first:
announcement = CreateCoinAnnouncement(std_hash(b"".join([c.name() for c in cat_coins])), coin.name())
if need_chia_transaction:
if fee > regular_chia_to_claim:
await self.create_tandem_xch_tx(
fee,
uint64(regular_chia_to_claim),
action_scope,
extra_conditions=(announcement.corresponding_assertion(),),
)
innersol = self.standard_wallet.make_solution(
primaries=primaries,
conditions=(*extra_conditions, announcement),
)
elif regular_chia_to_claim > fee:
xch_announcement = await self.create_tandem_xch_tx(
fee,
uint64(regular_chia_to_claim),
action_scope,
)
assert xch_announcement is not None
innersol = self.standard_wallet.make_solution(
primaries=primaries,
conditions=(*extra_conditions, xch_announcement, announcement),
)
else:
# TODO: what about when they are equal?
raise Exception("Equality not handled")
else:
innersol = self.standard_wallet.make_solution(
primaries=primaries,
conditions=(*extra_conditions, announcement),
)
else:
assert announcement is not None
innersol = self.standard_wallet.make_solution(
primaries=[],
conditions=(announcement.corresponding_assertion(),),
)
inner_derivation_record = (
await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(
crcat.inner_puzzle_hash
)
)
if inner_derivation_record is None:
raise RuntimeError( # pragma: no cover
f"CR-CAT {crcat} has an inner puzzle hash {crcat.inner_puzzle_hash} that we don't have the keys for"
)
inner_puzzle: Program = self.standard_wallet.puzzle_for_pk(inner_derivation_record.pubkey)
inner_spends.append(
(
crcat,
extra_delta if first else 0,
inner_puzzle,
innersol,
)
)
first = False
if vc is None: # pragma: no cover
raise RuntimeError("Spending no cat coins is not an appropriate use of _generate_unsigned_spendbundle")
if vc.proof_hash is None:
raise RuntimeError("CR-CATs found an appropriate VC but that VC contains no proofs") # pragma: no cover
proof_of_inclusions: Program = await vc_wallet.proof_of_inclusions_for_root_and_keys(
vc.proof_hash, self.info.proofs_checker.flags
)
expected_announcements, coin_spends, _ = CRCAT.spend_many(
inner_spends,
proof_of_inclusions,
Program.NIL, # TODO: With more proofs checkers, this may need to be flexible. For now, it's hardcoded.
vc.proof_provider,
vc.launcher_id,
vc.wrap_inner_with_backdoor().get_tree_hash() if add_authorizations_to_cr_cats else None,
)
if add_authorizations_to_cr_cats:
assert announcement is not None
await vc_wallet.generate_signed_transaction(
[uint64(1)],
[await action_scope.get_puzzle_hash(self.wallet_state_manager)],
action_scope,
vc_id=vc.launcher_id,
extra_conditions=(
*expected_announcements,
announcement,
*(CreatePuzzleAnnouncement(ann) for ann in vc_announcements_to_make),
),
)
return WalletSpendBundle(coin_spends, G2Element())
async def generate_signed_transaction(
self,
amounts: list[uint64],
puzzle_hashes: list[bytes32],
action_scope: WalletActionScope,
fee: uint64 = uint64(0),
coins: set[Coin] | None = None,
memos: list[list[bytes]] | None = None,
extra_conditions: tuple[Condition, ...] = tuple(),
**kwargs: Unpack[GSTOptionalArgs],
) -> None:
# (extra_delta, tail_reveal, tail_solution)
cat_discrepancy: tuple[int, Program, Program] | None = kwargs.get("cat_discrepancy", None)
add_authorizations_to_cr_cats: bool = kwargs.get("add_authorizations_to_cr_cats", True)
if memos is None:
memos = [[] for _ in range(len(puzzle_hashes))]
if not (len(memos) == len(puzzle_hashes) == len(amounts)):
raise ValueError("Memos, puzzle_hashes, and amounts must have the same length") # pragma: no cover
payments = []
for amount, puzhash, memo_list in zip(amounts, puzzle_hashes, memos):
memos_with_hint: list[bytes] = [puzhash]
memos_with_hint.extend(memo_list)
# Force wrap the outgoing coins in the pending state if not going to us
payments.append(
CreateCoin(
(
construct_pending_approval_state(puzhash, amount).get_tree_hash()
if puzhash != Offer.ph()
and not await self.wallet_state_manager.puzzle_store.puzzle_hash_exists(puzhash)
else puzhash
),
amount,
memos_with_hint,
)
)
spend_bundle = await self._generate_unsigned_spendbundle(
payments,
action_scope,
fee,
cat_discrepancy=cat_discrepancy, # (extra_delta, tail_reveal, tail_solution)
coins=coins,
extra_conditions=extra_conditions,
add_authorizations_to_cr_cats=add_authorizations_to_cr_cats,
)
async with action_scope.use() as interface:
other_tx_removals: set[Coin] = {
removal for tx in interface.side_effects.transactions for removal in tx.removals
}
other_tx_additions: set[Coin] = {
addition for tx in interface.side_effects.transactions for addition in tx.additions
}
tx_list = [
self.wallet_state_manager.new_outgoing_transaction(
wallet_id=self.id(),
puzzle_hash=payment.puzzle_hash,
amount=uint64(payment.amount),
fee=fee,
# semantics guarantee not None here
spend_bundle=spend_bundle if i == 0 else None, # type: ignore[arg-type]
additions=list(set(spend_bundle.additions()) - other_tx_additions) if i == 0 else [],
removals=list(set(spend_bundle.removals()) - other_tx_removals) if i == 0 else [],
name=spend_bundle.name() if i == 0 else payment.to_program().get_tree_hash(),
extra_conditions=extra_conditions,
)
for i, payment in enumerate(payments)
]
interface.side_effects.transactions.extend(tx_list)
async def claim_pending_approval_balance(
self,
min_amount_to_claim: uint64,
action_scope: WalletActionScope,
fee: uint64 = uint64(0),
coins: set[Coin] | None = None,
min_coin_amount: uint64 | None = None,
max_coin_amount: uint64 | None = None,
excluded_coin_amounts: list[uint64] | None = None,
reuse_puzhash: bool | None = None,
extra_conditions: tuple[Condition, ...] = tuple(),
) -> None:
# Select the relevant CR-CAT coins
crcat_records: set[WalletCoinRecord] = await self.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(
self.id(), CoinType.CRCAT_PENDING
)
if coins is None:
if max_coin_amount is None:
max_coin_amount = uint64(self.wallet_state_manager.constants.MAX_COIN_AMOUNT)
coins = await select_coins(
await self.get_pending_approval_balance(),
action_scope.config.tx_config.coin_selection_config,
list(crcat_records),
{},
self.log,
uint128(min_amount_to_claim),
)
# Select the relevant XCH coins
if fee > 0:
chia_coins = await self.standard_wallet.select_coins(
fee,
action_scope,
)
else:
chia_coins = set()
# Select the relevant VC coin
vc_wallet: VCWallet = await self.wallet_state_manager.get_or_create_vc_wallet()
vc: VerifiedCredential | None = await vc_wallet.get_vc_with_provider_in_and_proofs(
self.info.authorized_providers, self.info.proofs_checker.flags
)
if vc is None: # pragma: no cover
raise RuntimeError(f"No VC exists that can approve spends for CR-CAT wallet {self.id()}")
if vc.proof_hash is None:
raise RuntimeError(f"VC {vc.launcher_id} has no proofs to authorize transaction") # pragma: no cover
proof_of_inclusions: Program = await vc_wallet.proof_of_inclusions_for_root_and_keys(
vc.proof_hash, self.info.proofs_checker.flags
)
# Generate the bundle nonce
nonce: bytes32 = Program.to(
[coin_as_list(c) for c in sorted(coins.union(chia_coins).union({vc.coin}), key=Coin.name)]
).get_tree_hash()
# Make CR-CAT bundle
crcats_and_puzhashes: list[tuple[CRCAT, bytes32]] = [
(crcat, CRCATWallet.get_metadata_from_record(record).inner_puzzle_hash)
for record in [r for r in crcat_records if r.coin in coins]
for crcat in [self.coin_record_to_crcat(record)]
]
expected_announcements, coin_spends, _ = CRCAT.spend_many(
[
(
crcat,
0,
construct_pending_approval_state(inner_puzhash, uint64(crcat.coin.amount)),
Program.to([nonce]),
)
for crcat, inner_puzhash in crcats_and_puzhashes
],
proof_of_inclusions,
Program.NIL, # TODO: With more proofs checkers, this may need to be flexible. For now, it's hardcoded.
vc.proof_provider,
vc.launcher_id,
vc.wrap_inner_with_backdoor().get_tree_hash(),
)
claim_bundle = WalletSpendBundle(coin_spends, G2Element())
# Make the Fee TX
if fee > 0:
await self.create_tandem_xch_tx(
fee,
uint64(0),
action_scope,
extra_conditions=tuple(expected_announcements),
)
# Make the VC TX
await vc_wallet.generate_signed_transaction(
[uint64(1)],
[await action_scope.get_puzzle_hash(self.wallet_state_manager)],
action_scope,
vc_id=vc.launcher_id,
extra_conditions=(
*extra_conditions,
*expected_announcements,
CreateCoinAnnouncement(nonce),
*(CreatePuzzleAnnouncement(crcat.expected_announcement()) for crcat, _ in crcats_and_puzhashes),
),
)
to_puzzle_hash = await action_scope.get_puzzle_hash(self.wallet_state_manager, override_reuse_puzhash_with=True)
async with action_scope.use() as interface:
other_additions: set[Coin] = {rem for tx in interface.side_effects.transactions for rem in tx.additions}
other_removals: set[Coin] = {rem for tx in interface.side_effects.transactions for rem in tx.removals}
interface.side_effects.transactions.append(
TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(time.time()),
to_puzzle_hash=to_puzzle_hash,
to_address=self.wallet_state_manager.encode_puzzle_hash(to_puzzle_hash),
amount=uint64(sum(c.amount for c in coins)),
fee_amount=fee,
confirmed=False,
sent=uint32(0),
spend_bundle=claim_bundle,
additions=list(set(claim_bundle.additions()) - other_additions),
removals=list(set(claim_bundle.removals()) - other_removals),
wallet_id=self.id(),
sent_to=[],
trade_id=None,
type=uint32(TransactionType.INCOMING_TX.value),
name=claim_bundle.name(),
memos=compute_memos(claim_bundle),
valid_times=parse_timelock_info(extra_conditions),
)
)
async def match_puzzle_info(self, puzzle_driver: PuzzleInfo) -> bool:
if (
AssetType(puzzle_driver.type()) == AssetType.CAT
and puzzle_driver["tail"] == self.info.limitations_program_hash
):
inner_puzzle_driver: PuzzleInfo | None = puzzle_driver.also()
if inner_puzzle_driver is None:
raise ValueError("Malformed puzzle driver passed to CRCATWallet.match_puzzle_info") # pragma: no cover
return (
AssetType(inner_puzzle_driver.type()) == AssetType.CR
and [bytes32(provider) for provider in inner_puzzle_driver["authorized_providers"]]
== self.info.authorized_providers
and ProofsChecker.from_program(uncurry_puzzle(inner_puzzle_driver["proofs_checker"]))
== self.info.proofs_checker
)
return False
async def get_puzzle_info(self, asset_id: bytes32) -> PuzzleInfo:
return PuzzleInfo(
{
"type": AssetType.CAT.value,
"tail": "0x" + self.info.limitations_program_hash.hex(),
"also": {
"type": AssetType.CR.value,
"authorized_providers": ["0x" + provider.hex() for provider in self.info.authorized_providers],
"proofs_checker": self.info.proofs_checker.as_program(),
},
}
)
async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool:
"""
This matches coins that are either CRCATs with the hint as the inner puzzle, or CRCATs in the pending approval
state that will come to us once claimed.
"""
authorized_providers_hash: bytes32 = Program.to(self.info.authorized_providers).get_tree_hash()
proofs_checker_hash: bytes32 = self.info.proofs_checker.as_program().get_tree_hash()
hint_inner_hash: bytes32 = construct_cr_layer_hash(
authorized_providers_hash,
proofs_checker_hash,
hint,
)
if (
construct_cat_puzzle(
Program.to(CAT_MOD_HASH),
self.info.limitations_program_hash,
hint_inner_hash,
mod_code_hash=CAT_MOD_HASH_HASH,
).get_tree_hash_precalc(hint, CAT_MOD_HASH, CAT_MOD_HASH_HASH, hint_inner_hash)
== coin.puzzle_hash
):
return True
pending_approval_inner_hash: bytes32 = construct_cr_layer_hash(
authorized_providers_hash,
proofs_checker_hash,
construct_pending_approval_state(hint, uint64(coin.amount)).get_tree_hash(),
)
if (
construct_cat_puzzle(
Program.to(CAT_MOD_HASH),
self.info.limitations_program_hash,
pending_approval_inner_hash,
mod_code_hash=CAT_MOD_HASH_HASH,
).get_tree_hash_precalc(CAT_MOD_HASH, CAT_MOD_HASH_HASH, pending_approval_inner_hash)
== coin.puzzle_hash
):
return True
else:
return False
if TYPE_CHECKING:
_dummy: WalletProtocol = CRCATWallet()