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
31 changes: 12 additions & 19 deletions chia/_tests/cmds/wallet/test_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
)
from chia.cmds.cmds_util import TransactionBundle
from chia.protocols.outbound_message import NodeType
from chia.types.blockchain_format.program import Program
from chia.types.signing_mode import SigningMode
from chia.util.bech32m import encode_puzzle_hash
from chia.wallet.conditions import Condition, ConditionValidTimes
Expand All @@ -47,6 +46,7 @@
CATAssetIDToNameResponse,
CATSetName,
CATSetNameResponse,
CATSpend,
CATSpendResponse,
ClawbackPuzzleDecoratorOverride,
CreateOfferForIDsResponse,
Expand Down Expand Up @@ -385,31 +385,24 @@ async def send_transaction(

async def cat_spend(
self,
wallet_id: int,
request: CATSpend,
tx_config: TXConfig,
amount: Optional[uint64] = None,
inner_address: Optional[str] = None,
fee: uint64 = uint64(0),
memos: Optional[list[str]] = None,
additions: Optional[list[dict[str, Any]]] = None,
removals: Optional[list[Coin]] = None,
cat_discrepancy: Optional[tuple[int, Program, Program]] = None, # (extra_delta, tail_reveal, tail_solution)
push: bool = True,
extra_conditions: tuple[Condition, ...] = tuple(),
timelock_info: ConditionValidTimes = ConditionValidTimes(),
) -> CATSpendResponse:
self.add_to_log(
"cat_spend",
(
wallet_id,
request.wallet_id,
tx_config,
amount,
inner_address,
fee,
memos,
additions,
removals,
cat_discrepancy,
push,
request.amount,
request.inner_address,
request.fee,
request.memos,
request.additions,
request.coins,
request.cat_discrepancy,
request.push,
timelock_info,
),
)
Expand Down
105 changes: 87 additions & 18 deletions chia/_tests/wallet/rpc/test_wallet_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
import logging
import random
import re
from collections.abc import AsyncIterator
from operator import attrgetter
from typing import Any, Optional
Expand Down Expand Up @@ -108,6 +109,7 @@
CATGetAssetID,
CATGetName,
CATSetName,
CATSpend,
CheckDeleteKey,
CheckOfferValidity,
ClawbackPuzzleDecoratorOverride,
Expand Down Expand Up @@ -1255,18 +1257,69 @@ async def test_cat_endpoints(wallet_environments: WalletTestFramework, wallet_ty
# Test CAT spend without a fee
with pytest.raises(ValueError):
await env_0.rpc_client.cat_spend(
cat_0_id,
DEFAULT_TX_CONFIG.override(
CATSpend(
wallet_id=cat_0_id,
amount=uint64(4),
inner_address=addr_1,
fee=uint64(0),
memos=["the cat memo"],
push=False,
),
tx_config=wallet_environments.tx_config.override(
excluded_coin_amounts=[uint64(100)],
excluded_coin_ids=[bytes32.zeros],
),
uint64(4),
addr_1,
uint64(0),
["the cat memo"],
)

# Test some validation errors
with pytest.raises(
ValueError,
match=re.escape('Must specify "additions" or "amount"+"inner_address"+"memos", but not both.'),
):
await env_0.rpc_client.cat_spend(
CATSpend(
wallet_id=cat_0_id,
amount=uint64(4),
inner_address=addr_1,
memos=["the cat memo"],
additions=[],
),
tx_config=wallet_environments.tx_config,
)

with pytest.raises(ValueError, match=re.escape('Must specify "amount" and "inner_address" together.')):
await env_0.rpc_client.cat_spend(
CATSpend(
wallet_id=cat_0_id,
amount=uint64(4),
inner_address=None,
),
tx_config=wallet_environments.tx_config,
)

with pytest.raises(
ValueError,
match=re.escape('Must specify \\"extra_delta\\", \\"tail_reveal\\" and \\"tail_solution\\" together.'),
):
await env_0.rpc_client.cat_spend(
CATSpend(
wallet_id=cat_0_id,
additions=[],
extra_delta="1",
),
tx_config=wallet_environments.tx_config,
)

tx_res = await env_0.rpc_client.cat_spend(
cat_0_id, wallet_environments.tx_config, uint64(4), addr_1, uint64(0), ["the cat memo"]
CATSpend(
wallet_id=cat_0_id,
amount=uint64(4),
inner_address=addr_1,
fee=uint64(0),
memos=["the cat memo"],
push=True,
),
tx_config=wallet_environments.tx_config,
)

spend_bundle = tx_res.transaction.spend_bundle
Expand Down Expand Up @@ -1312,7 +1365,15 @@ async def test_cat_endpoints(wallet_environments: WalletTestFramework, wallet_ty

# Test CAT spend with a fee
tx_res = await env_0.rpc_client.cat_spend(
cat_0_id, wallet_environments.tx_config, uint64(1), addr_1, uint64(5_000_000), ["the cat memo"]
CATSpend(
wallet_id=cat_0_id,
amount=uint64(1),
inner_address=addr_1,
fee=uint64(5_000_000),
memos=["the cat memo"],
push=True,
),
wallet_environments.tx_config,
)

spend_bundle = tx_res.transaction.spend_bundle
Expand Down Expand Up @@ -1379,13 +1440,16 @@ async def test_cat_endpoints(wallet_environments: WalletTestFramework, wallet_ty
)
)
tx_res = await env_0.rpc_client.cat_spend(
cat_0_id,
CATSpend(
wallet_id=cat_0_id,
amount=uint64(1),
inner_address=addr_1,
fee=uint64(5_000_000),
memos=["the cat memo"],
coins=select_coins_response.coins,
push=True,
),
wallet_environments.tx_config,
uint64(1),
addr_1,
uint64(5_000_000),
["the cat memo"],
removals=select_coins_response.coins,
)

spend_bundle = tx_res.transaction.spend_bundle
Expand Down Expand Up @@ -3109,11 +3173,16 @@ async def test_cat_spend_run_tail(wallet_rpc_environment: WalletRpcTestEnvironme
# Attempt to melt it fully
tx = (
await client.cat_spend(
cat_wallet_id,
amount=uint64(0),
CATSpend(
wallet_id=cat_wallet_id,
amount=uint64(0),
inner_address=encode_puzzle_hash(our_ph, "txch"),
extra_delta=str(tx_amount * -1),
tail_reveal=b"\x80",
tail_solution=b"\x80",
push=True,
),
tx_config=DEFAULT_TX_CONFIG,
inner_address=encode_puzzle_hash(our_ph, "txch"),
cat_discrepancy=(tx_amount * -1, Program.to(None), Program.to(None)),
)
).transaction
transaction_id = tx.name
Expand Down
41 changes: 26 additions & 15 deletions chia/_tests/wallet/vc_wallet/test_vc_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from chia.wallet.wallet import Wallet
from chia.wallet.wallet_node import WalletNode
from chia.wallet.wallet_request_types import (
CATSpend,
GetTransactions,
GetWallets,
VCAddProofs,
Expand Down Expand Up @@ -383,12 +384,15 @@ async def test_vc_lifecycle(wallet_environments: WalletTestFramework) -> None:
wallet_1_addr = encode_puzzle_hash(wallet_1_ph, "txch")
txs = (
await client_0.cat_spend(
cr_cat_wallet_0.id(),
CATSpend(
wallet_id=cr_cat_wallet_0.id(),
amount=uint64(90),
inner_address=wallet_1_addr,
fee=uint64(2000000000),
memos=["hey"],
push=True,
),
wallet_environments.tx_config,
uint64(90),
wallet_1_addr,
uint64(2000000000),
memos=["hey"],
)
).transactions
await wallet_environments.process_pending_states(
Expand Down Expand Up @@ -557,23 +561,30 @@ async def test_vc_lifecycle(wallet_environments: WalletTestFramework) -> None:
# (Negative test) Try to spend a CR-CAT that we don't have a valid VC for
with pytest.raises(ValueError):
await client_0.cat_spend(
cr_cat_wallet_0.id(),
wallet_environments.tx_config,
uint64(10),
wallet_1_addr,
CATSpend(
wallet_id=cr_cat_wallet_0.id(),
amount=uint64(10),
inner_address=wallet_1_addr,
),
tx_config=wallet_environments.tx_config,
)

# Test melting a CRCAT
# This is intended to trigger an edge case where the output and change are the same forcing a new puzhash
with wallet_environments.new_puzzle_hashes_allowed():
tx = (
await client_1.cat_spend(
env_1.dealias_wallet_id("crcat"),
wallet_environments.tx_config,
uint64(20),
wallet_1_addr,
uint64(0),
cat_discrepancy=(-50, Program.to(None), Program.to(None)),
CATSpend(
wallet_id=env_1.dealias_wallet_id("crcat"),
amount=uint64(20),
inner_address=wallet_1_addr,
fee=uint64(0),
extra_delta=str(-50),
tail_reveal=b"\x80",
tail_solution=b"\x80",
push=True,
),
tx_config=wallet_environments.tx_config,
)
).transaction
[tx] = await wallet_node_1.wallet_state_manager.add_pending_transactions([tx])
Expand Down
17 changes: 10 additions & 7 deletions chia/cmds/wallet_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
CATAssetIDToNameResponse,
CATGetName,
CATSetName,
CATSpend,
CATSpendResponse,
ClawbackPuzzleDecoratorOverride,
DeleteNotifications,
Expand Down Expand Up @@ -401,18 +402,20 @@ async def send(
elif typ in {WalletType.CAT, WalletType.CRCAT, WalletType.RCAT}:
print("Submitting transaction...")
res = await wallet_client.cat_spend(
wallet_id,
CMDTXConfigLoader(
CATSpend(
wallet_id=uint32(wallet_id),
amount=final_amount,
inner_address=address.original_address,
fee=fee,
memos=memos,
push=push,
),
tx_config=CMDTXConfigLoader(
min_coin_amount=min_coin_amount,
max_coin_amount=max_coin_amount,
excluded_coin_ids=list(excluded_coin_ids),
reuse_puzhash=reuse_puzhash,
).to_tx_config(mojo_per_unit, config, fingerprint),
final_amount,
address.original_address,
fee,
memos,
push=push,
timelock_info=condition_valid_times,
)
else:
Expand Down
62 changes: 55 additions & 7 deletions chia/wallet/wallet_request_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1388,6 +1388,61 @@ class CombineCoinsResponse(TransactionEndpointResponse):
pass


# utility for CATSpend
# unfortunate that we can't use CreateCoin but the memos are taken as strings not bytes
@streamable
@dataclass(frozen=True)
class Addition(Streamable):
amount: uint64
puzzle_hash: bytes32
memos: Optional[list[str]] = None


@streamable
@kw_only_dataclass
class CATSpend(TransactionEndpointRequest):
wallet_id: uint32 = field(default_factory=default_raise)
additions: Optional[list[Addition]] = None
amount: Optional[uint64] = None
inner_address: Optional[str] = None
memos: Optional[list[str]] = None
coins: Optional[list[Coin]] = None
extra_delta: Optional[str] = None # str to support negative ints :(
tail_reveal: Optional[bytes] = None
tail_solution: Optional[bytes] = None

def __post_init__(self) -> None:
if (
self.additions is not None
and (self.amount is not None or self.inner_address is not None or self.memos is not None)
) or (self.additions is None and self.amount is None and self.inner_address is None and self.memos is None):
raise ValueError('Must specify "additions" or "amount"+"inner_address"+"memos", but not both.')
elif self.additions is None and None in {self.amount, self.inner_address}:
raise ValueError('Must specify "amount" and "inner_address" together.')
super().__post_init__()

@property
def cat_discrepancy(self) -> Optional[tuple[int, Program, Program]]:
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]
)


@streamable
@dataclass(frozen=True)
class CATSpendResponse(TransactionEndpointResponse):
transaction: TransactionRecord
transaction_id: bytes32


@streamable
@kw_only_dataclass
class DIDMessageSpend(TransactionEndpointRequest):
Expand Down Expand Up @@ -1780,13 +1835,6 @@ class CreateSignedTransactionsResponse(TransactionEndpointResponse):
signed_tx: TransactionRecord


@streamable
@dataclass(frozen=True)
class CATSpendResponse(TransactionEndpointResponse):
transaction: TransactionRecord
transaction_id: bytes32


@streamable
@dataclass(frozen=True)
class _OfferEndpointResponse(TransactionEndpointResponse):
Expand Down
Loading
Loading