Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
118 changes: 53 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,25 @@ 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,
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 +2007,34 @@ 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,
)
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 +3346,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