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
42 changes: 1 addition & 41 deletions chia/_tests/cmds/cmd_test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

import chia.cmds.wallet_funcs
from chia._tests.cmds.testing_classes import create_test_block_record
from chia._tests.cmds.wallet.test_consts import STD_TX, STD_UTX, get_bytes32
from chia._tests.cmds.wallet.test_consts import get_bytes32
from chia.cmds.chia import cli as chia_cli
from chia.cmds.cmds_util import _T_RpcClient, node_config_section_names
from chia.consensus.default_constants import DEFAULT_CONSTANTS
Expand All @@ -30,7 +30,6 @@
from chia.wallet.nft_wallet.nft_wallet import NFTWallet
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.util.transaction_type import TransactionType
from chia.wallet.util.tx_config import TXConfig
from chia.wallet.util.wallet_types import WalletType
from chia.wallet.wallet_request_types import (
CATAssetIDToName,
Expand All @@ -46,7 +45,6 @@
NFTCalculateRoyaltiesResponse,
NFTGetInfo,
NFTGetInfoResponse,
SendTransactionMultiResponse,
SignMessageByAddress,
SignMessageByAddressResponse,
SignMessageByID,
Expand Down Expand Up @@ -235,44 +233,6 @@ async def nft_calculate_royalties(
)
)

async def send_transaction_multi(
self,
wallet_id: int,
additions: list[dict[str, object]],
tx_config: TXConfig,
coins: Optional[list[Coin]] = None,
fee: uint64 = uint64(0),
push: bool = True,
timelock_info: ConditionValidTimes = ConditionValidTimes(),
) -> SendTransactionMultiResponse:
self.add_to_log("send_transaction_multi", (wallet_id, additions, tx_config, coins, fee, push, timelock_info))
name = bytes32([2] * 32)
return SendTransactionMultiResponse(
[STD_UTX],
[STD_TX],
TransactionRecord(
confirmed_at_height=uint32(1),
created_at_time=uint64(1234),
to_puzzle_hash=bytes32([1] * 32),
to_address=encode_puzzle_hash(bytes32([1] * 32), "xch"),
amount=uint64(12345678),
fee_amount=uint64(1234567),
confirmed=False,
sent=uint32(0),
spend_bundle=WalletSpendBundle([], G2Element()),
additions=[Coin(bytes32([1] * 32), bytes32([2] * 32), uint64(12345678))],
removals=[Coin(bytes32([2] * 32), bytes32([4] * 32), uint64(12345678))],
wallet_id=uint32(1),
sent_to=[("aaaaa", uint8(1), None)],
trade_id=None,
type=uint32(TransactionType.OUTGOING_TX.value),
name=name,
memos={bytes32([3] * 32): [bytes([4] * 32)]},
valid_times=ConditionValidTimes(),
),
name,
)


@dataclass
class TestFullNodeRpcClient(TestRpcClient):
Expand Down
19 changes: 12 additions & 7 deletions chia/_tests/wallet/rpc/test_wallet_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@
SelectCoins,
SendNotification,
SendTransaction,
SendTransactionMulti,
SetWalletResyncOnStartup,
SpendClawbackCoins,
SplitCoins,
Expand Down Expand Up @@ -1003,11 +1004,14 @@ async def test_send_transaction_multi(wallet_rpc_environment: WalletRpcTestEnvir

send_tx_res: TransactionRecord = (
await client.send_transaction_multi(
1,
[{**output.to_json_dict(), "puzzle_hash": output.puzzle_hash} for output in outputs],
DEFAULT_TX_CONFIG,
coins=select_coins_response.coins,
fee=amount_fee,
SendTransactionMulti(
wallet_id=uint32(1),
additions=outputs,
coins=select_coins_response.coins,
fee=amount_fee,
push=True,
),
tx_config=DEFAULT_TX_CONFIG,
)
).transaction
spend_bundle = send_tx_res.spend_bundle
Expand Down Expand Up @@ -1503,10 +1507,11 @@ async def test_offer_endpoints(wallet_environments: WalletTestFramework, wallet_
# Creates a wallet for the same CAT on wallet_2 and send 4 CAT from wallet_1 to it
await env_2.rpc_client.create_wallet_for_existing_cat(cat_asset_id)
wallet_2_address = (await env_2.rpc_client.get_next_address(GetNextAddress(cat_wallet_id, False))).address
adds = [{"puzzle_hash": decode_puzzle_hash(wallet_2_address), "amount": uint64(4), "memos": ["the cat memo"]}]
adds = [Addition(puzzle_hash=decode_puzzle_hash(wallet_2_address), amount=uint64(4), memos=["the cat memo"])]
tx_res = (
await env_1.rpc_client.send_transaction_multi(
cat_wallet_id, additions=adds, tx_config=wallet_environments.tx_config, fee=uint64(0)
SendTransactionMulti(wallet_id=uint32(cat_wallet_id), additions=adds, fee=uint64(0), push=True),
tx_config=wallet_environments.tx_config,
)
).transaction
spend_bundle = tx_res.spend_bundle
Expand Down
101 changes: 89 additions & 12 deletions chia/wallet/wallet_request_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import sys
from dataclasses import dataclass, field
from functools import cached_property
from typing import Any, BinaryIO, Optional, Union, final
from typing import Any, BinaryIO, Optional, TypeVar, Union, final

from chia_rs import Coin, G1Element, G2Element, PrivateKey
from chia_rs.sized_bytes import bytes32
Expand Down Expand Up @@ -1870,17 +1870,6 @@ class VCRevokeResponse(TransactionEndpointResponse):
pass


# TODO: The section below needs corresponding request types
# TODO: The section below should be added to the API (currently only for client)


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


@streamable
@dataclass(frozen=True)
class CSTCoinAnnouncement(Streamable):
Expand Down Expand Up @@ -1945,6 +1934,94 @@ class CreateSignedTransactionsResponse(TransactionEndpointResponse):
signed_tx: TransactionRecord


_T_SendTransactionMultiProxy = TypeVar("_T_SendTransactionMultiProxy", CATSpend, CreateSignedTransaction)


@streamable
@dataclass(frozen=True)
class SendTransactionMulti(TransactionEndpointRequest):
# primarily for cat_spend
wallet_id: uint32 = field(default_factory=default_raise)
additions: Optional[list[Addition]] = None # for both
amount: Optional[uint64] = None
inner_address: Optional[str] = None
memos: Optional[list[str]] = None
coins: Optional[list[Coin]] = None # for both
extra_delta: Optional[str] = None # str to support negative ints :(
tail_reveal: Optional[bytes] = None
tail_solution: Optional[bytes] = None
# for create_signed_transaction
morph_bytes: Optional[bytes] = None
coin_announcements: Optional[list[CSTCoinAnnouncement]] = None
puzzle_announcements: Optional[list[CSTPuzzleAnnouncement]] = None

def convert_to_proxy(self, proxy_type: type[_T_SendTransactionMultiProxy]) -> _T_SendTransactionMultiProxy:
if proxy_type is CATSpend:
if self.morph_bytes is not None:
raise ValueError(
'Specified "morph_bytes" for a CAT-type wallet. Maybe you meant to specify an XCH wallet?'
)
elif self.coin_announcements or self.puzzle_announcements is not None:
raise ValueError(
'Specified "coin/puzzle_announcements" for a CAT-type wallet.'
"Maybe you meant to specify an XCH wallet?"
)

# not sure why mypy hasn't understood this is purely a CATSpend
return proxy_type(
wallet_id=self.wallet_id,
additions=self.additions, # type: ignore[arg-type]
amount=self.amount, # type: ignore[call-arg]
inner_address=self.inner_address,
memos=self.memos,
coins=self.coins,
extra_delta=self.extra_delta,
tail_reveal=self.tail_reveal,
tail_solution=self.tail_solution,
fee=self.fee,
push=self.push,
sign=self.sign,
)
elif proxy_type is CreateSignedTransaction:
if self.amount is not None:
raise ValueError('Specified "amount" for an XCH wallet. Maybe you meant to specify a CAT-type wallet?')
elif self.inner_address is not None:
raise ValueError(
'Specified "inner_address" for an XCH wallet. Maybe you meant to specify a CAT-type wallet?'
)
elif self.memos is not None:
raise ValueError('Specified "memos" for an XCH wallet. Maybe you meant to specify a CAT-type wallet?')
elif self.extra_delta is not None or self.tail_reveal is not None or self.tail_solution is not None:
raise ValueError(
'Specified "extra_delta", "tail_reveal", or "tail_solution" for an XCH wallet.'
"Maybe you meant to specify a CAT-type wallet?"
)
elif self.additions is None:
raise ValueError('"additions" are required for XCH wallets.')

# not sure why mypy hasn't understood this is purely a CreateSignedTransaction
return proxy_type(
additions=self.additions,
wallet_id=self.wallet_id,
coins=self.coins,
morph_bytes=self.morph_bytes, # type: ignore[call-arg]
coin_announcements=self.coin_announcements if self.coin_announcements is not None else [],
puzzle_announcements=self.puzzle_announcements if self.puzzle_announcements is not None else [],
fee=self.fee,
push=self.push,
sign=self.sign,
)
else:
raise ValueError("An unsupported wallet type was selected for `send_transaction_multi`")


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


@streamable
@dataclass(frozen=True)
class _OfferEndpointResponse(TransactionEndpointResponse):
Expand Down
83 changes: 53 additions & 30 deletions chia/wallet/wallet_rpc_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@
SendNotification,
SendNotificationResponse,
SendTransaction,
SendTransactionMulti,
SendTransactionMultiResponse,
SendTransactionResponse,
SetWalletResyncOnStartup,
SignMessageByAddress,
Expand Down Expand Up @@ -374,25 +376,40 @@ async def rpc_endpoint(
):
raise ValueError("Relative timelocks are not currently supported in the RPC")

async with self.service.wallet_state_manager.new_action_scope(
tx_config,
push=request.get("push", push),
merge_spends=request.get("merge_spends", merge_spends),
sign=request.get("sign", self.service.config.get("auto_sign_txs", True)),
) as action_scope:
if "action_scope_override" in kwargs:
response: EndpointResult = await func(
self,
request,
*args,
action_scope,
kwargs["action_scope_override"],
extra_conditions=extra_conditions,
**kwargs,
**{k: v for k, v in kwargs.items() if k != "action_scope_override"},
)
action_scope = cast(WalletActionScope, kwargs["action_scope_override"])
else:
async with self.service.wallet_state_manager.new_action_scope(
tx_config,
push=request.get("push", push),
merge_spends=request.get("merge_spends", merge_spends),
sign=request.get("sign", self.service.config.get("auto_sign_txs", True)),
) as action_scope:
response = await func(
self,
request,
*args,
action_scope,
extra_conditions=extra_conditions,
**kwargs,
)

if func.__name__ == "create_new_wallet" and "transactions" not in response:
# unfortunately, this API isn't solely a tx endpoint
return response

if "action_scope_override" in kwargs:
# deferring to parent action scope
return response

unsigned_txs = await self.service.wallet_state_manager.gather_signing_info_for_txs(
action_scope.side_effects.transactions
)
Expand Down Expand Up @@ -1665,35 +1682,41 @@ async def send_transaction(
# tx_endpoint will take care of the default values here
return SendTransactionResponse([], [], transaction=REPLACEABLE_TRANSACTION_RECORD, transaction_id=bytes32.zeros)

async def send_transaction_multi(self, request: dict[str, Any]) -> EndpointResult:
@tx_endpoint(push=True)
@marshal
async def send_transaction_multi(
self,
request: SendTransactionMulti,
action_scope: WalletActionScope,
extra_conditions: tuple[Condition, ...] = tuple(),
) -> SendTransactionMultiResponse:
if await self.service.wallet_state_manager.synced() is False:
raise ValueError("Wallet needs to be fully synced before sending transactions")

# This is required because this is a "@tx_endpoint" that calls other @tx_endpoints
request.setdefault("push", True)
request.setdefault("merge_spends", True)

wallet_id = uint32(request["wallet_id"])
wallet = self.service.wallet_state_manager.wallets[wallet_id]
wallet = self.service.wallet_state_manager.wallets[request.wallet_id]

async with self.service.wallet_state_manager.lock:
if wallet.type() in {WalletType.CAT, WalletType.CRCAT, WalletType.RCAT}:
assert isinstance(wallet, CATWallet)
response = await self.cat_spend(request, hold_lock=False)
transaction = response["transaction"]
transactions = response["transactions"]
if issubclass(type(wallet), CATWallet):
await self.cat_spend(
request.convert_to_proxy(CATSpend).json_serialize_for_transport(
action_scope.config.tx_config, extra_conditions, ConditionValidTimes()
),
hold_lock=False,
action_scope_override=action_scope,
)
else:
response = await self.create_signed_transaction(request, hold_lock=False)
transaction = response["signed_tx"]
transactions = response["transactions"]
await self.create_signed_transaction(
request.convert_to_proxy(CreateSignedTransaction).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.
return {
"transaction": transaction,
"transaction_id": TransactionRecord.from_json_dict(transaction).name,
"transactions": transactions,
"unsigned_transactions": response["unsigned_transactions"],
}
# tx_endpoint will take care of these values
return SendTransactionMultiResponse(
[], [], transaction=REPLACEABLE_TRANSACTION_RECORD, transaction_id=bytes32.zeros
)

@tx_endpoint(push=True, merge_spends=False)
@marshal
Expand Down
Loading
Loading