Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions chia/_tests/wallet/rpc/test_wallet_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1225,7 +1225,7 @@ async def test_cat_endpoints(wallet_environments: WalletTestFramework, wallet_ty
amount=uint64(4),
inner_address=addr_1,
memos=["the cat memo"],
additions=[],
additions=[Addition(amount=uint64(0), puzzle_hash=bytes32.zeros)],
),
tx_config=wallet_environments.tx_config,
)
Expand All @@ -1247,7 +1247,7 @@ async def test_cat_endpoints(wallet_environments: WalletTestFramework, wallet_ty
await env_0.rpc_client.cat_spend(
CATSpend(
wallet_id=cat_0_id,
additions=[],
additions=[Addition(amount=uint64(0), puzzle_hash=bytes32.zeros)],
extra_delta="1",
),
tx_config=wallet_environments.tx_config,
Expand Down
40 changes: 29 additions & 11 deletions chia/wallet/wallet_request_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1468,6 +1468,22 @@ class Addition(Streamable):
memos: list[str] | None = None


def cat_discrepancy_validation(
extra_delta: str | None, tail_reveal: bytes | None, tail_solution: bytes | None
) -> tuple[int, Program, Program] | None:
if extra_delta is None and tail_reveal is None and tail_solution is None:
return None
elif None in {extra_delta, tail_reveal, tail_solution}:
raise ValueError('Must specify "extra_delta", "tail_reveal" and "tail_solution" together.')
else:
# Curious that mypy doesn't see the elif and know that none of these are None
return (
int(extra_delta), # type: ignore[arg-type]
Program.from_bytes(tail_reveal), # type: ignore[arg-type]
Program.from_bytes(tail_solution), # type: ignore[arg-type]
)


@streamable
@dataclass(frozen=True, kw_only=True)
class CATSpend(TransactionEndpointRequest):
Expand All @@ -1493,17 +1509,7 @@ def __post_init__(self) -> None:

@property
def cat_discrepancy(self) -> tuple[int, Program, Program] | None:
if self.extra_delta is None and self.tail_reveal is None and self.tail_solution is None:
return None
elif None in {self.extra_delta, self.tail_reveal, self.tail_solution}:
raise ValueError('Must specify "extra_delta", "tail_reveal" and "tail_solution" together.')
else:
# Curious that mypy doesn't see the elif and know that none of these are None
return (
int(self.extra_delta), # type: ignore[arg-type]
Program.from_bytes(self.tail_reveal), # type: ignore[arg-type]
Program.from_bytes(self.tail_solution), # type: ignore[arg-type]
)
return cat_discrepancy_validation(self.extra_delta, self.tail_reveal, self.tail_solution)


@streamable
Expand Down Expand Up @@ -1910,6 +1916,14 @@ class CreateSignedTransaction(TransactionEndpointRequest):
morph_bytes: bytes | None = None
coin_announcements: list[CSTCoinAnnouncement] = field(default_factory=list)
puzzle_announcements: list[CSTPuzzleAnnouncement] = field(default_factory=list)
# cat specific
extra_delta: str | None = None # str to support negative ints :(
tail_reveal: bytes | None = None
tail_solution: bytes | None = None
# Technically this value was meant to support many types here
# However, only one is supported right now and there are no plans to extend
# So, as a slight hack, we'll specify that only Clawback is supported
puzzle_decorator: list[ClawbackPuzzleDecoratorOverride] | None = None

def __post_init__(self) -> None:
if len(self.additions) < 1:
Expand Down Expand Up @@ -1943,6 +1957,10 @@ def asserted_puzzle_announcements(self) -> tuple[AssertPuzzleAnnouncement, ...]:
for pa in self.puzzle_announcements
)

@property
def cat_discrepancy(self) -> tuple[int, Program, Program] | None:
return cat_discrepancy_validation(self.extra_delta, self.tail_reveal, self.tail_solution)


@streamable
@dataclass(frozen=True)
Expand Down
122 changes: 57 additions & 65 deletions chia/wallet/wallet_rpc_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
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
from chia.wallet.util.address_type import AddressType, ensure_valid_address, is_valid_address
from chia.wallet.util.clvm_streamable import json_serialize_with_clvm_streamable
from chia.wallet.util.compute_hints import compute_spend_hints_and_additions
from chia.wallet.util.compute_memos import compute_memos
Expand All @@ -104,6 +104,7 @@
from chia.wallet.wallet_node import WalletNode, get_wallet_db_path
from chia.wallet.wallet_protocol import WalletProtocol
from chia.wallet.wallet_request_types import (
Addition,
AddKey,
AddKeyResponse,
ApplySignatures,
Expand Down Expand Up @@ -1507,24 +1508,27 @@ async def send_transaction(
action_scope: WalletActionScope,
extra_conditions: tuple[Condition, ...] = tuple(),
) -> SendTransactionResponse:
wallet = self.service.wallet_state_manager.get_wallet(id=request.wallet_id, required_type=Wallet)

# TODO: Add support for multiple puzhash/amount/memo sets
selected_network = self.service.config["selected_network"]
expected_prefix = self.service.config["network_overrides"]["config"][selected_network]["address_prefix"]
if request.address[0 : len(expected_prefix)] != expected_prefix:
raise ValueError("Unexpected Address Prefix")

await wallet.generate_signed_transaction(
[request.amount],
[decode_puzzle_hash(request.address)],
action_scope,
request.fee,
memos=[[mem.encode("utf-8") for mem in request.memos]],
puzzle_decorator_override=[request.puzzle_decorator[0].to_json_dict()]
if request.puzzle_decorator is not None
else None,
extra_conditions=extra_conditions,
# opportunity to raise
self.service.wallet_state_manager.get_wallet(id=request.wallet_id, required_type=Wallet)
await self.create_signed_transaction(
CreateSignedTransaction(
additions=[
Addition(
request.amount,
decode_puzzle_hash(
ensure_valid_address(
request.address, allowed_types={AddressType.XCH}, config=self.service.config
)
),
request.memos,
)
],
wallet_id=request.wallet_id,
fee=request.fee,
puzzle_decorator=request.puzzle_decorator,
).json_serialize_for_transport(action_scope.config.tx_config, extra_conditions, ConditionValidTimes()),
hold_lock=False,
action_scope_override=action_scope,
)

# Transaction may not have been included in the mempool yet. Use get_transaction to check.
Expand Down Expand Up @@ -2005,52 +2009,36 @@ async def cat_spend(
extra_conditions: tuple[Condition, ...] = tuple(),
hold_lock: bool = True,
) -> CATSpendResponse:
wallet = self.service.wallet_state_manager.get_wallet(id=request.wallet_id, required_type=CATWallet)

amounts: list[uint64] = []
puzzle_hashes: list[bytes32] = []
memos: list[list[bytes]] = []
if request.additions is not None:
for addition in request.additions:
if addition.amount > self.service.constants.MAX_COIN_AMOUNT:
raise ValueError(f"Coin amount cannot exceed {self.service.constants.MAX_COIN_AMOUNT}")
amounts.append(addition.amount)
puzzle_hashes.append(addition.puzzle_hash)
if addition.memos is not None:
memos.append([mem.encode("utf-8") for mem in addition.memos])
else:
# Our __post_init__ guards against these not being None
amounts.append(request.amount) # type: ignore[arg-type]
puzzle_hashes.append(decode_puzzle_hash(request.inner_address)) # type: ignore[arg-type]
if request.memos is not None:
memos.append([mem.encode("utf-8") for mem in request.memos])
coins: set[Coin] | None = None
if request.coins is not None and len(request.coins) > 0:
coins = set(request.coins)

if hold_lock:
async with self.service.wallet_state_manager.lock:
await wallet.generate_signed_transaction(
amounts,
puzzle_hashes,
action_scope,
request.fee,
cat_discrepancy=request.cat_discrepancy,
coins=coins,
memos=memos if memos else None,
extra_conditions=extra_conditions,
)
else:
await wallet.generate_signed_transaction(
amounts,
puzzle_hashes,
action_scope,
request.fee,
cat_discrepancy=request.cat_discrepancy,
coins=coins,
memos=memos if memos else None,
extra_conditions=extra_conditions,
)
# opportunity to raise
self.service.wallet_state_manager.get_wallet(id=request.wallet_id, required_type=CATWallet)
await self.create_signed_transaction(
CreateSignedTransaction(
additions=request.additions
if request.additions is not None
else [
Addition(
# Our __post_init__ guards against these not being None
request.amount, # type: ignore[arg-type]
decode_puzzle_hash(
ensure_valid_address(
request.inner_address, # type: ignore[arg-type]
allowed_types={AddressType.XCH},
config=self.service.config,
)
),
request.memos,
)
],
wallet_id=request.wallet_id,
fee=request.fee,
coins=request.coins,
extra_delta=request.extra_delta,
tail_reveal=request.tail_reveal,
tail_solution=request.tail_solution,
).json_serialize_for_transport(action_scope.config.tx_config, extra_conditions, ConditionValidTimes()),
hold_lock=hold_lock,
action_scope_override=action_scope,
)

# tx_endpoint will fill in these default values
return CATSpendResponse([], [], transaction=REPLACEABLE_TRANSACTION_RECORD, transaction_id=bytes32.zeros)
Expand Down Expand Up @@ -3362,6 +3350,10 @@ async def _generate_signed_transaction() -> CreateSignedTransactionsResponse:
request.fee,
coins=request.coin_set,
memos=[memos_0] + [output.memos if output.memos is not None else [] for output in additional_outputs],
puzzle_decorator_override=[dec.to_json_dict() for dec in request.puzzle_decorator]
if request.puzzle_decorator is not None
else None,
cat_discrepancy=request.cat_discrepancy,
extra_conditions=(
*extra_conditions,
*request.asserted_coin_announcements,
Expand Down
Loading