Skip to content

Commit 08a9a4d

Browse files
authored
[CHIA-1569] Fix fee behavior with chia wallet coins combine (#18679)
* Fix coin fees wrt coin combining * Add override behavior
1 parent b598762 commit 08a9a4d

File tree

5 files changed

+123
-16
lines changed

5 files changed

+123
-16
lines changed

chia/_tests/cmds/wallet/test_coins.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,15 @@ async def combine_coins(
6767

6868
inst_rpc_client = CoinsCombineRpcClient() # pylint: disable=no-value-for-parameter
6969
test_rpc_clients.wallet_rpc_client = inst_rpc_client
70+
assert sum(coin.amount for coin in STD_TX.removals) < 500_000_000_000
7071
command_args = [
7172
"wallet",
7273
"coins",
7374
"combine",
7475
FINGERPRINT_ARG,
7576
"-i1",
7677
"--largest-first",
77-
"-m0.001",
78+
"-m0.5",
7879
"--min-amount",
7980
"0.1",
8081
"--max-amount",
@@ -91,11 +92,13 @@ async def combine_coins(
9192
"150",
9293
]
9394
# these are various things that should be in the output
95+
assert_list = ["Fee is >= the amount of coins selected. To continue, please use --override flag."]
96+
run_cli_command_and_assert(capsys, root_dir, command_args, assert_list)
9497
assert_list = [
9598
"Transactions would combine up to 500 coins",
9699
f"To get status, use command: chia wallet get_transaction -f {FINGERPRINT} -tx 0x{STD_TX.name.hex()}",
97100
]
98-
run_cli_command_and_assert(capsys, root_dir, command_args, assert_list)
101+
run_cli_command_and_assert(capsys, root_dir, command_args + ["--override"], assert_list)
99102
expected_tx_config = TXConfig(
100103
min_coin_amount=uint64(100_000_000_000),
101104
max_coin_amount=uint64(200_000_000_000),
@@ -109,13 +112,18 @@ async def combine_coins(
109112
largest_first=True,
110113
target_coin_ids=[bytes32([0] * 32)],
111114
target_coin_amount=uint64(1_000_000_000_000),
112-
fee=uint64(1_000_000_000),
115+
fee=uint64(500_000_000_000),
113116
push=False,
114117
)
115118
expected_calls: logType = {
116-
"get_wallets": [(None,)],
117-
"get_synced": [()],
119+
"get_wallets": [(None,)] * 2,
120+
"get_synced": [()] * 2,
118121
"combine_coins": [
122+
(
123+
expected_request,
124+
expected_tx_config,
125+
test_condition_valid_times,
126+
),
119127
(
120128
expected_request,
121129
expected_tx_config,

chia/_tests/wallet/rpc/test_wallet_rpc.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3022,3 +3022,85 @@ async def test_combine_coins(wallet_environments: WalletTestFramework) -> None:
30223022
)
30233023
]
30243024
)
3025+
3026+
3027+
@pytest.mark.parametrize(
3028+
"wallet_environments",
3029+
[
3030+
{
3031+
"num_environments": 1,
3032+
"blocks_needed": [2],
3033+
"trusted": True, # irrelevant
3034+
"reuse_puzhash": True, # irrelevant
3035+
}
3036+
],
3037+
indirect=True,
3038+
)
3039+
@pytest.mark.limit_consensus_modes(reason="irrelevant")
3040+
@pytest.mark.anyio
3041+
async def test_fee_bigger_than_selection_coin_combining(wallet_environments: WalletTestFramework) -> None:
3042+
"""
3043+
This tests the case where the coins we would otherwise select are not enough to pay the fee.
3044+
"""
3045+
3046+
env = wallet_environments.environments[0]
3047+
env.wallet_aliases = {
3048+
"xch": 1,
3049+
"cat": 2,
3050+
}
3051+
3052+
# Should have 4 coins, two 1.75 XCH, two 0.25 XCH
3053+
3054+
# Grab one of the 0.25 ones to specify
3055+
async with env.wallet_state_manager.new_action_scope(wallet_environments.tx_config) as action_scope:
3056+
target_coin = list(await env.xch_wallet.select_coins(uint64(250_000_000_000), action_scope))[0]
3057+
assert target_coin.amount == 250_000_000_000
3058+
3059+
fee = uint64(1_750_000_000_000)
3060+
# Under standard circumstances we would select the small coins, but this is not enough to pay the fee
3061+
# Instead, we will grab the big coin first and combine it with one of the smaller coins
3062+
xch_combine_request = CombineCoins(
3063+
wallet_id=uint32(1),
3064+
number_of_coins=uint16(2),
3065+
fee=fee,
3066+
largest_first=False,
3067+
push=True,
3068+
)
3069+
3070+
# First test an error where fee selection causes too many coins to be selected
3071+
with pytest.raises(ResponseFailureError, match="without selecting more coins than specified: 3"):
3072+
await env.rpc_client.combine_coins(
3073+
dataclasses.replace(xch_combine_request, fee=uint64(2_250_000_000_000)),
3074+
wallet_environments.tx_config,
3075+
)
3076+
3077+
await env.rpc_client.combine_coins(
3078+
xch_combine_request,
3079+
wallet_environments.tx_config,
3080+
)
3081+
3082+
await wallet_environments.process_pending_states(
3083+
[
3084+
WalletStateTransition(
3085+
pre_block_balance_updates={
3086+
"xch": {
3087+
"unconfirmed_wallet_balance": -fee,
3088+
"spendable_balance": -2_000_000_000_000,
3089+
"pending_change": 250_000_000_000,
3090+
"max_send_amount": -2_000_000_000_000,
3091+
"pending_coin_removal_count": 2,
3092+
}
3093+
},
3094+
post_block_balance_updates={
3095+
"xch": {
3096+
"confirmed_wallet_balance": -fee,
3097+
"spendable_balance": 250_000_000_000,
3098+
"pending_change": -250_000_000_000,
3099+
"max_send_amount": 250_000_000_000,
3100+
"pending_coin_removal_count": -2,
3101+
"unspent_coin_count": -1, # combine 2 into 1
3102+
}
3103+
},
3104+
)
3105+
]
3106+
)

chia/cmds/coin_funcs.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ async def async_combine(
128128
largest_first: bool,
129129
push: bool,
130130
condition_valid_times: ConditionValidTimes,
131+
override: bool,
131132
) -> List[TransactionRecord]:
132133
async with get_wallet_client(wallet_rpc_port, fingerprint) as (wallet_client, fingerprint, config):
133134
try:
@@ -167,6 +168,14 @@ async def async_combine(
167168
timelock_info=condition_valid_times,
168169
)
169170

171+
if (
172+
not override
173+
and wallet_id == 1
174+
and fee >= sum(coin.amount for tx in resp.transactions for coin in tx.removals)
175+
):
176+
print("Fee is >= the amount of coins selected. To continue, please use --override flag.")
177+
return []
178+
170179
print(f"Transactions would combine up to {number_of_coins} coins.")
171180
if push:
172181
cli_confirm("Would you like to Continue? (y/n): ")

chia/cmds/coins.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ def list_cmd(
108108
default=False,
109109
help="Sort coins from largest to smallest or smallest to largest.",
110110
)
111+
@click.option("--override", help="Submits transaction without checking for unusual values", is_flag=True, default=False)
111112
@tx_out_cmd()
112113
def combine_cmd(
113114
wallet_rpc_port: Optional[int],
@@ -125,6 +126,7 @@ def combine_cmd(
125126
reuse: bool,
126127
push: bool,
127128
condition_valid_times: ConditionValidTimes,
129+
override: bool,
128130
) -> List[TransactionRecord]:
129131
from .coin_funcs import async_combine
130132

@@ -145,6 +147,7 @@ def combine_cmd(
145147
largest_first=largest_first,
146148
push=push,
147149
condition_valid_times=condition_valid_times,
150+
override=override,
148151
)
149152
)
150153

chia/rpc/wallet_rpc_api.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,18 +1204,22 @@ async def combine_coins(
12041204
async with action_scope.use() as interface:
12051205
interface.side_effects.selected_coins.extend(coins)
12061206

1207-
# Next let's select enough coins to meet the target if there is one
1208-
if request.target_coin_amount is not None:
1209-
fungible_amount_needed = request.target_coin_amount
1210-
if isinstance(wallet, Wallet):
1211-
fungible_amount_needed = uint64(request.target_coin_amount + request.fee)
1212-
amount_selected = sum(c.amount for c in coins)
1213-
if amount_selected < fungible_amount_needed:
1214-
coins.extend(
1215-
await wallet.select_coins(
1216-
amount=uint64(fungible_amount_needed - amount_selected), action_scope=action_scope
1217-
)
1207+
# Next let's select enough coins to meet the target + fee if there is one
1208+
fungible_amount_needed = uint64(0) if request.target_coin_amount is None else request.target_coin_amount
1209+
if isinstance(wallet, Wallet):
1210+
fungible_amount_needed = uint64(fungible_amount_needed + request.fee)
1211+
amount_selected = sum(c.amount for c in coins)
1212+
if amount_selected < fungible_amount_needed: # implicit fungible_amount_needed > 0 here
1213+
coins.extend(
1214+
await wallet.select_coins(
1215+
amount=uint64(fungible_amount_needed - amount_selected), action_scope=action_scope
12181216
)
1217+
)
1218+
1219+
if len(coins) > request.number_of_coins:
1220+
raise ValueError(
1221+
f"Options specified cannot be met without selecting more coins than specified: {len(coins)}"
1222+
)
12191223

12201224
# Now let's select enough coins to get to the target number to combine
12211225
if len(coins) < request.number_of_coins:
@@ -1243,6 +1247,7 @@ async def combine_coins(
12431247
uint64(sum(c.amount for c in coins)) if request.target_coin_amount is None else request.target_coin_amount
12441248
)
12451249
if isinstance(wallet, Wallet):
1250+
primary_output_amount = uint64(primary_output_amount - request.fee)
12461251
await wallet.generate_signed_transaction(
12471252
primary_output_amount,
12481253
await wallet.get_puzzle_hash(new=action_scope.config.tx_config.reuse_puzhash),

0 commit comments

Comments
 (0)