Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CHIA-1129] Extract coin splitting logic from CLI -> RPC #18480

Merged
merged 6 commits into from
Aug 28, 2024
Merged
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
50 changes: 20 additions & 30 deletions chia/_tests/cmds/wallet/test_coins.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
from __future__ import annotations

from pathlib import Path
from typing import List, Optional, Tuple
from typing import List, Tuple

from chia_rs import Coin

from chia._tests.cmds.cmd_test_utils import TestRpcClients, TestWalletRpcClient, logType, run_cli_command_and_assert
from chia._tests.cmds.wallet.test_consts import FINGERPRINT, FINGERPRINT_ARG, get_bytes32
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.coin_record import CoinRecord
from chia.util.ints import uint32, uint64
from chia._tests.cmds.wallet.test_consts import FINGERPRINT, FINGERPRINT_ARG, STD_TX, STD_UTX, get_bytes32
from chia.rpc.wallet_request_types import SplitCoins, SplitCoinsResponse
from chia.util.ints import uint16, uint32, uint64
from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG, CoinSelectionConfig, TXConfig

# Coin Commands
Expand Down Expand Up @@ -168,23 +167,13 @@ def test_coins_split(capsys: object, get_test_cli_clients: Tuple[TestRpcClients,

# set RPC Client
class CoinsSplitRpcClient(TestWalletRpcClient):
async def get_coin_records_by_names(
async def split_coins(
self,
names: List[bytes32],
include_spent_coins: bool = True,
start_height: Optional[int] = None,
end_height: Optional[int] = None,
) -> List[CoinRecord]:
self.add_to_log("get_coin_records_by_names", (names, include_spent_coins, start_height, end_height))
return [
CoinRecord(
Coin(get_bytes32(1), get_bytes32(2), uint64(100000000000)),
uint32(123456),
uint32(0),
False,
uint64(0),
),
]
args: SplitCoins,
tx_config: TXConfig,
) -> SplitCoinsResponse:
self.add_to_log("split_coins", (args, tx_config))
return SplitCoinsResponse([STD_UTX], [STD_TX])

inst_rpc_client = CoinsSplitRpcClient() # pylint: disable=no-value-for-parameter
test_rpc_clients.wallet_rpc_client = inst_rpc_client
Expand All @@ -202,23 +191,24 @@ async def get_coin_records_by_names(
]
# these are various things that should be in the output
assert_list = [
f"To get status, use command: chia wallet get_transaction -f {FINGERPRINT} -tx 0x{get_bytes32(2).hex()}",
f"To get status, use command: chia wallet get_transaction -f {FINGERPRINT} -tx 0x{STD_TX.name.hex()}",
"WARNING: The amount per coin: 1E-7 is less than the dust threshold: 1e-06.",
]
run_cli_command_and_assert(capsys, root_dir, command_args, assert_list)
expected_calls: logType = {
"get_wallets": [(None,)],
"get_synced": [()],
"get_coin_records_by_names": [([target_coin_id], True, None, None)],
"get_next_address": [(1, True) for i in range(10)],
"send_transaction_multi": [
"split_coins": [
(
1,
[{"amount": 100000, "puzzle_hash": bytes32([i] * 32)} for i in range(10)],
SplitCoins(
wallet_id=uint32(1),
number_of_coins=uint16(10),
amount_per_coin=uint64(100_000),
target_coin_id=target_coin_id,
fee=uint64(1_000_000_000),
push=True,
),
DEFAULT_TX_CONFIG,
[Coin(get_bytes32(1), get_bytes32(2), uint64(100000000000))],
1000000000,
True,
)
],
}
Expand Down
177 changes: 176 additions & 1 deletion chia/_tests/wallet/rpc/test_wallet_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import pytest
from chia_rs import G2Element

from chia._tests.conftest import ConsensusMode
from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework
from chia._tests.util.time_out_assert import time_out_assert, time_out_assert_not_none
from chia._tests.wallet.test_wallet_coin_store import (
get_coin_records_amount_filter_tests,
Expand Down Expand Up @@ -42,8 +44,9 @@
from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward
from chia.consensus.coinbase import create_puzzlehash_for_pk
from chia.rpc.full_node_rpc_client import FullNodeRpcClient
from chia.rpc.rpc_client import ResponseFailureError
from chia.rpc.rpc_server import RpcServer
from chia.rpc.wallet_request_types import GetNotifications
from chia.rpc.wallet_request_types import GetNotifications, SplitCoins, SplitCoinsResponse
from chia.rpc.wallet_rpc_api import WalletRpcApi
from chia.rpc.wallet_rpc_client import WalletRpcClient
from chia.server.server import ChiaServer
Expand Down Expand Up @@ -2598,3 +2601,175 @@ async def test_get_balances(wallet_rpc_environment: WalletRpcTestEnvironment):
assert len(bal_ids) == 2
assert bal["2"]["confirmed_wallet_balance"] == 100
assert bal["3"]["confirmed_wallet_balance"] == 20


@pytest.mark.parametrize(
"wallet_environments",
[
{
"num_environments": 1,
"blocks_needed": [1],
}
],
indirect=True,
)
@pytest.mark.limit_consensus_modes([ConsensusMode.PLAIN], reason="irrelevant")
@pytest.mark.anyio
async def test_split_coins(wallet_environments: WalletTestFramework) -> None:
env = wallet_environments.environments[0]
env.wallet_aliases = {
"xch": 1,
"cat": 2,
}

# Test XCH first
async with env.wallet_state_manager.new_action_scope(wallet_environments.tx_config) as action_scope:
target_coin = list(await env.xch_wallet.select_coins(uint64(250_000_000_000), action_scope))[0]
assert target_coin.amount == 250_000_000_000

xch_request = SplitCoins(
wallet_id=uint32(1),
number_of_coins=uint16(100),
amount_per_coin=uint64(100),
target_coin_id=target_coin.name(),
fee=uint64(1_000_000_000_000), # 1 XCH
push=True,
)

with pytest.raises(ResponseFailureError, match="501 coins is greater then the maximum limit of 500 coins"):
await env.rpc_client.split_coins(
dataclasses.replace(xch_request, number_of_coins=uint16(501)),
wallet_environments.tx_config,
)

with pytest.raises(ResponseFailureError, match="Could not find coin with ID 00000000000000000"):
await env.rpc_client.split_coins(
dataclasses.replace(xch_request, target_coin_id=bytes32([0] * 32)),
wallet_environments.tx_config,
)

with pytest.raises(ResponseFailureError, match="is less than the total amount of the split"):
await env.rpc_client.split_coins(
dataclasses.replace(xch_request, amount_per_coin=uint64(1_000_000_000_000)),
wallet_environments.tx_config,
)

with pytest.raises(ResponseFailureError, match="Wallet with ID 42 does not exist"):
await env.rpc_client.split_coins(
dataclasses.replace(xch_request, wallet_id=uint32(42)),
wallet_environments.tx_config,
)

env.wallet_state_manager.wallets[uint32(42)] = object() # type: ignore[assignment]
with pytest.raises(ResponseFailureError, match="Cannot split coins from non-fungible wallet types"):
await env.rpc_client.split_coins(
dataclasses.replace(xch_request, wallet_id=uint32(42)),
wallet_environments.tx_config,
)
del env.wallet_state_manager.wallets[uint32(42)]

response = await env.rpc_client.split_coins(
dataclasses.replace(xch_request, number_of_coins=uint16(0)),
wallet_environments.tx_config,
)
assert response == SplitCoinsResponse([], [])

await env.rpc_client.split_coins(
xch_request,
wallet_environments.tx_config,
)

await wallet_environments.process_pending_states(
[
WalletStateTransition(
pre_block_balance_updates={
"xch": {
"unconfirmed_wallet_balance": -1_000_000_000_000, # just the fee
"spendable_balance": -2_000_000_000_000,
"pending_change": 1_000_000_000_000,
"max_send_amount": -2_000_000_000_000,
"pending_coin_removal_count": 2,
}
},
post_block_balance_updates={
"xch": {
"confirmed_wallet_balance": -1_000_000_000_000, # just the fee
"spendable_balance": 1_000_000_000_000,
"pending_change": -1_000_000_000_000,
"max_send_amount": 1_000_000_000_000,
"pending_coin_removal_count": -2,
"unspent_coin_count": 99, # split 1 into 100 i.e. +99
}
},
)
]
)

# Now do CATs
async with env.wallet_state_manager.new_action_scope(wallet_environments.tx_config, push=True) as action_scope:
cat_wallet = await CATWallet.create_new_cat_wallet(
env.wallet_state_manager,
env.xch_wallet,
{"identifier": "genesis_by_id"},
uint64(50),
action_scope,
)

await wallet_environments.process_pending_states(
[
WalletStateTransition(
# no need to test this, it is tested elsewhere
pre_block_balance_updates={
"xch": {"set_remainder": True},
"cat": {"init": True, "set_remainder": True},
},
post_block_balance_updates={
"xch": {"set_remainder": True},
"cat": {"set_remainder": True},
},
)
]
)

async with env.wallet_state_manager.new_action_scope(wallet_environments.tx_config) as action_scope:
target_coin = list(await cat_wallet.select_coins(uint64(50), action_scope))[0]
assert target_coin.amount == 50

cat_request = SplitCoins(
wallet_id=uint32(2),
number_of_coins=uint16(50),
amount_per_coin=uint64(1),
target_coin_id=target_coin.name(),
push=True,
)

await env.rpc_client.split_coins(
cat_request,
wallet_environments.tx_config,
)

await wallet_environments.process_pending_states(
[
WalletStateTransition(
pre_block_balance_updates={
"cat": {
"unconfirmed_wallet_balance": 0,
"spendable_balance": -50,
"pending_change": 50,
"max_send_amount": -50,
"pending_coin_removal_count": 1,
}
},
post_block_balance_updates={
"cat": {
"confirmed_wallet_balance": 0,
"spendable_balance": 50,
"pending_change": -50,
"max_send_amount": 50,
"pending_coin_removal_count": -1,
"unspent_coin_count": 49, # split 1 into 50 i.e. +49
}
},
)
]
)
56 changes: 22 additions & 34 deletions chia/cmds/coin_funcs.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
from __future__ import annotations

import sys
from typing import Dict, List, Optional, Sequence, Tuple, Union
from typing import List, Optional, Sequence, Tuple

from chia.cmds.cmds_util import CMDCoinSelectionConfigLoader, CMDTXConfigLoader, cli_confirm, get_wallet_client
from chia.cmds.param_types import CliAmount
from chia.cmds.wallet_funcs import get_mojo_per_unit, get_wallet_type, print_balance
from chia.rpc.wallet_request_types import SplitCoins
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.coin_record import CoinRecord
from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash
from chia.util.config import selected_network_address_prefix
from chia.util.ints import uint64, uint128
from chia.util.ints import uint16, uint32, uint64, uint128
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.util.wallet_types import WalletType

Expand Down Expand Up @@ -199,15 +199,11 @@ async def async_split(
fee: uint64,
number_of_coins: int,
amount_per_coin: CliAmount,
target_coin_id_str: str,
target_coin_id: bytes32,
# TODO: [add TXConfig args]
push: bool,
) -> List[TransactionRecord]:
async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, config):
target_coin_id: bytes32 = bytes32.from_hexstr(target_coin_id_str)
if number_of_coins > 500:
print(f"{number_of_coins} coins is greater then the maximum limit of 500 coins.")
return []
try:
wallet_type = await get_wallet_type(wallet_id=wallet_id, wallet_client=wallet_client)
mojo_per_unit = get_mojo_per_unit(wallet_type)
Expand All @@ -217,39 +213,31 @@ async def async_split(
if not await wallet_client.get_synced():
print("Wallet not synced. Please wait.")
return []
is_xch: bool = wallet_type == WalletType.STANDARD_WALLET # this lets us know if we are directly spitting Chia

final_amount_per_coin = amount_per_coin.convert_amount(mojo_per_unit)
total_amount = final_amount_per_coin * number_of_coins
if is_xch:
total_amount += fee
# get full coin record from name, and validate information about it.
removal_coin_record: CoinRecord = (await wallet_client.get_coin_records_by_names([target_coin_id]))[0]
if removal_coin_record.coin.amount < total_amount:
print(
f"Coin amount: {removal_coin_record.coin.amount / mojo_per_unit} "
f"is less than the total amount of the split: {total_amount / mojo_per_unit}, exiting."
)
print("Try using a smaller fee or amount.")
return []
additions: List[Dict[str, Union[uint64, bytes32]]] = []
for i in range(number_of_coins): # for readability.
# we always use new addresses
target_ph: bytes32 = decode_puzzle_hash(await wallet_client.get_next_address(wallet_id, new_address=True))
additions.append({"amount": final_amount_per_coin, "puzzle_hash": target_ph})

tx_config = CMDTXConfigLoader(
# TODO: [add TXConfig args]
).to_tx_config(mojo_per_unit, config, fingerprint)

transaction: TransactionRecord = (
await wallet_client.send_transaction_multi(
wallet_id, additions, tx_config, [removal_coin_record.coin], fee, push=push
transactions: List[TransactionRecord] = (
await wallet_client.split_coins(
SplitCoins(
wallet_id=uint32(wallet_id),
number_of_coins=uint16(number_of_coins),
amount_per_coin=uint64(final_amount_per_coin),
target_coin_id=target_coin_id,
fee=fee,
push=push,
),
tx_config=tx_config,
)
).transaction
tx_id = transaction.name.hex()
).transactions

if push:
print(f"Transaction sent: {tx_id}")
print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id}")
for tx in transactions:
print(f"Transaction sent: {tx.name}")
print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx.name}")
dust_threshold = config.get("xch_spam_amount", 1000000) # min amount per coin in mojo
spam_filter_after_n_txs = config.get("spam_filter_after_n_txs", 200) # how many txs to wait before filtering
if final_amount_per_coin < dust_threshold and wallet_type == WalletType.STANDARD_WALLET:
Expand All @@ -259,4 +247,4 @@ async def async_split(
f"{'will' if number_of_coins > spam_filter_after_n_txs else 'may'} not show up in your wallet unless "
f"you decrease the dust limit to below {final_amount_per_coin} mojos or disable it by setting it to 0."
)
return [transaction]
return transactions
Loading
Loading