-
Notifications
You must be signed in to change notification settings - Fork 2k
/
test_wallet_rpc.py
2551 lines (2208 loc) · 127 KB
/
test_wallet_rpc.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from __future__ import annotations
import asyncio
import dataclasses
import json
import logging
import random
from operator import attrgetter
from typing import Any, Dict, List, Optional, Tuple, cast
import aiosqlite
import pytest
from chia_rs import G2Element
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_server import RpcServer
from chia.rpc.wallet_rpc_api import WalletRpcApi
from chia.rpc.wallet_rpc_client import WalletRpcClient
from chia.server.server import ChiaServer
from chia.server.start_service import Service
from chia.simulator.full_node_simulator import FullNodeSimulator
from chia.simulator.simulator_protocol import FarmNewBlockProtocol
from chia.types.announcement import Announcement
from chia.types.blockchain_format.coin import Coin, coin_as_list
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.coin_record import CoinRecord
from chia.types.coin_spend import CoinSpend
from chia.types.peer_info import PeerInfo
from chia.types.signing_mode import SigningMode
from chia.types.spend_bundle import SpendBundle
from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash
from chia.util.config import load_config, lock_and_load_config, save_config
from chia.util.db_wrapper import DBWrapper2
from chia.util.hash import std_hash
from chia.util.ints import uint16, uint32, uint64
from chia.util.streamable import ConversionError, InvalidTypeError
from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS
from chia.wallet.cat_wallet.cat_utils import CAT_MOD, construct_cat_puzzle
from chia.wallet.cat_wallet.cat_wallet import CATWallet
from chia.wallet.conditions import ConditionValidTimes, Remark
from chia.wallet.derive_keys import master_sk_to_wallet_sk, master_sk_to_wallet_sk_unhardened
from chia.wallet.did_wallet.did_wallet import DIDWallet
from chia.wallet.nft_wallet.nft_wallet import NFTWallet
from chia.wallet.trading.trade_status import TradeStatus
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.transaction_sorting import SortKey
from chia.wallet.uncurried_puzzle import uncurry_puzzle
from chia.wallet.util.address_type import AddressType
from chia.wallet.util.compute_memos import compute_memos
from chia.wallet.util.query_filter import AmountFilter, HashFilter, TransactionTypeFilter
from chia.wallet.util.transaction_type import TransactionType
from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG, DEFAULT_TX_CONFIG
from chia.wallet.util.wallet_types import CoinType, WalletType
from chia.wallet.wallet import Wallet
from chia.wallet.wallet_coin_record import WalletCoinRecord
from chia.wallet.wallet_coin_store import GetCoinRecords
from chia.wallet.wallet_node import WalletNode
from chia.wallet.wallet_protocol import WalletProtocol
from tests.conftest import ConsensusMode
from tests.util.time_out_assert import time_out_assert, time_out_assert_not_none
from tests.wallet.test_wallet_coin_store import (
get_coin_records_amount_filter_tests,
get_coin_records_amount_range_tests,
get_coin_records_coin_id_filter_tests,
get_coin_records_coin_type_tests,
get_coin_records_confirmed_range_tests,
get_coin_records_include_total_count_tests,
get_coin_records_mixed_tests,
get_coin_records_offset_limit_tests,
get_coin_records_order_tests,
get_coin_records_parent_coin_id_filter_tests,
get_coin_records_puzzle_hash_filter_tests,
get_coin_records_reverse_tests,
get_coin_records_spent_range_tests,
get_coin_records_wallet_id_tests,
get_coin_records_wallet_type_tests,
record_1,
record_2,
record_3,
record_4,
record_5,
record_6,
record_7,
record_8,
record_9,
)
log = logging.getLogger(__name__)
@dataclasses.dataclass
class WalletBundle:
service: Service
node: WalletNode
rpc_client: WalletRpcClient
wallet: Wallet
@dataclasses.dataclass
class FullNodeBundle:
server: ChiaServer
api: FullNodeSimulator
rpc_client: FullNodeRpcClient
@dataclasses.dataclass
class WalletRpcTestEnvironment:
wallet_1: WalletBundle
wallet_2: WalletBundle
full_node: FullNodeBundle
async def farm_transaction_block(full_node_api: FullNodeSimulator, wallet_node: WalletNode):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"\00" * 32)))
await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20)
def check_mempool_spend_count(full_node_api: FullNodeSimulator, num_of_spends):
return full_node_api.full_node.mempool_manager.mempool.size() == num_of_spends
async def farm_transaction(full_node_api: FullNodeSimulator, wallet_node: WalletNode, spend_bundle: SpendBundle):
await time_out_assert(
20, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle, spend_bundle.name()
)
await farm_transaction_block(full_node_api, wallet_node)
assert full_node_api.full_node.mempool_manager.get_spendbundle(spend_bundle.name()) is None
async def generate_funds(full_node_api: FullNodeSimulator, wallet_bundle: WalletBundle, num_blocks: int = 1):
wallet_id = 1
initial_balances = await wallet_bundle.rpc_client.get_wallet_balance(wallet_id)
ph: bytes32 = decode_puzzle_hash(await wallet_bundle.rpc_client.get_next_address(wallet_id, True))
generated_funds = 0
for i in range(0, num_blocks):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
peak_height = full_node_api.full_node.blockchain.get_peak_height()
assert peak_height is not None
generated_funds += calculate_pool_reward(peak_height) + calculate_base_farmer_reward(peak_height)
# Farm a dummy block to confirm the created funds
await farm_transaction_block(full_node_api, wallet_bundle.node)
expected_confirmed = initial_balances["confirmed_wallet_balance"] + generated_funds
expected_unconfirmed = initial_balances["unconfirmed_wallet_balance"] + generated_funds
await time_out_assert(20, get_confirmed_balance, expected_confirmed, wallet_bundle.rpc_client, wallet_id)
await time_out_assert(20, get_unconfirmed_balance, expected_unconfirmed, wallet_bundle.rpc_client, wallet_id)
await time_out_assert(20, wallet_bundle.rpc_client.get_synced)
return generated_funds
@pytest.fixture(scope="function", params=[True, False])
async def wallet_rpc_environment(two_wallet_nodes_services, request, self_hostname):
full_node, wallets, bt = two_wallet_nodes_services
full_node_service = full_node[0]
full_node_api = full_node_service._api
full_node_server = full_node_api.full_node.server
wallet_service = wallets[0]
wallet_service_2 = wallets[1]
wallet_node = wallet_service._node
wallet_node_2 = wallet_service_2._node
wallet = wallet_node.wallet_state_manager.main_wallet
wallet_2 = wallet_node_2.wallet_state_manager.main_wallet
config = bt.config
hostname = config["self_hostname"]
if request.param:
wallet_node.config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()}
wallet_node_2.config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()}
else:
wallet_node.config["trusted_peers"] = {}
wallet_node_2.config["trusted_peers"] = {}
await wallet_node.server.start_client(PeerInfo(self_hostname, full_node_server.get_port()), None)
await wallet_node_2.server.start_client(PeerInfo(self_hostname, full_node_server.get_port()), None)
async with WalletRpcClient.create_as_context(
hostname, wallet_service.rpc_server.listen_port, wallet_service.root_path, wallet_service.config
) as client:
async with WalletRpcClient.create_as_context(
hostname, wallet_service_2.rpc_server.listen_port, wallet_service_2.root_path, wallet_service_2.config
) as client_2:
async with FullNodeRpcClient.create_as_context(
hostname,
full_node_service.rpc_server.listen_port,
full_node_service.root_path,
full_node_service.config,
) as client_node:
wallet_bundle_1: WalletBundle = WalletBundle(wallet_service, wallet_node, client, wallet)
wallet_bundle_2: WalletBundle = WalletBundle(wallet_service_2, wallet_node_2, client_2, wallet_2)
node_bundle: FullNodeBundle = FullNodeBundle(full_node_server, full_node_api, client_node)
yield WalletRpcTestEnvironment(wallet_bundle_1, wallet_bundle_2, node_bundle)
async def create_tx_outputs(wallet: Wallet, output_args: List[Tuple[int, Optional[List[str]]]]) -> List[Dict[str, Any]]:
outputs = []
for args in output_args:
output = {"amount": uint64(args[0]), "puzzle_hash": await wallet.get_new_puzzlehash()}
if args[1] is not None:
assert len(args[1]) > 0
output["memos"] = args[1]
outputs.append(output)
return outputs
async def assert_wallet_types(client: WalletRpcClient, expected: Dict[WalletType, int]) -> None:
for wallet_type in WalletType:
wallets = await client.get_wallets(wallet_type)
wallet_count = len(wallets)
if wallet_type in expected:
assert wallet_count == expected.get(wallet_type, 0)
for wallet in wallets:
assert wallet["type"] == wallet_type.value
def assert_tx_amounts(
tx: TransactionRecord,
outputs: List[Dict[str, Any]],
*,
amount_fee: uint64,
change_expected: bool,
is_cat: bool = False,
) -> None:
assert tx.fee_amount == amount_fee
assert tx.amount == sum(output["amount"] for output in outputs)
expected_additions = len(outputs) + 1 if change_expected else len(outputs)
if is_cat and amount_fee:
expected_additions += 1
assert len(tx.additions) == expected_additions
addition_amounts = [addition.amount for addition in tx.additions]
removal_amounts = [removal.amount for removal in tx.removals]
for output in outputs:
assert output["amount"] in addition_amounts
assert (sum(removal_amounts) - sum(addition_amounts)) == amount_fee
async def assert_push_tx_error(node_rpc: FullNodeRpcClient, tx: TransactionRecord):
spend_bundle = tx.spend_bundle
assert spend_bundle is not None
# check error for a ASSERT_ANNOUNCE_CONSUMED_FAILED and if the error is not there throw a value error
try:
await node_rpc.push_tx(spend_bundle)
except ValueError as error:
error_string = error.args[0]["error"] # noqa: # pylint: disable=E1126
if error_string.find("ASSERT_ANNOUNCE_CONSUMED_FAILED") == -1:
raise ValueError from error
async def assert_get_balance(rpc_client: WalletRpcClient, wallet_node: WalletNode, wallet: WalletProtocol) -> None:
expected_balance = await wallet_node.get_balance(wallet.id())
expected_balance_dict = expected_balance.to_json_dict()
expected_balance_dict["wallet_id"] = wallet.id()
expected_balance_dict["wallet_type"] = wallet.type()
expected_balance_dict["fingerprint"] = wallet_node.logged_in_fingerprint
if wallet.type() in {WalletType.CAT, WalletType.CRCAT}:
assert isinstance(wallet, CATWallet)
expected_balance_dict["asset_id"] = wallet.get_asset_id()
assert await rpc_client.get_wallet_balance(wallet.id()) == expected_balance_dict
async def tx_in_mempool(client: WalletRpcClient, transaction_id: bytes32):
tx = await client.get_transaction(1, transaction_id)
return tx.is_in_mempool()
async def get_confirmed_balance(client: WalletRpcClient, wallet_id: int):
return (await client.get_wallet_balance(wallet_id))["confirmed_wallet_balance"]
async def get_unconfirmed_balance(client: WalletRpcClient, wallet_id: int):
return (await client.get_wallet_balance(wallet_id))["unconfirmed_wallet_balance"]
def update_verify_signature_request(request: Dict[str, Any], prefix_hex_values: bool):
updated_request = request.copy()
updated_request["pubkey"] = ("0x" if prefix_hex_values else "") + updated_request["pubkey"]
updated_request["signature"] = ("0x" if prefix_hex_values else "") + updated_request["signature"]
return updated_request
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN, ConsensusMode.HARD_FORK_2_0], reason="save time")
@pytest.mark.anyio
async def test_send_transaction(wallet_rpc_environment: WalletRpcTestEnvironment):
env: WalletRpcTestEnvironment = wallet_rpc_environment
wallet_2: Wallet = env.wallet_2.wallet
wallet_node: WalletNode = env.wallet_1.node
full_node_api: FullNodeSimulator = env.full_node.api
client: WalletRpcClient = env.wallet_1.rpc_client
generated_funds = await generate_funds(full_node_api, env.wallet_1)
addr = encode_puzzle_hash(await wallet_2.get_new_puzzlehash(), "txch")
tx_amount = uint64(15600000)
with pytest.raises(ValueError):
await client.send_transaction(1, uint64(100000000000000001), addr, DEFAULT_TX_CONFIG)
# Tests sending a basic transaction
tx = await client.send_transaction(
1,
tx_amount,
addr,
memos=["this is a basic tx"],
tx_config=DEFAULT_TX_CONFIG.override(
excluded_coin_amounts=[uint64(250000000000)],
excluded_coin_ids=[bytes32([0] * 32)],
),
extra_conditions=(Remark(Program.to(("test", None))),),
)
transaction_id = tx.name
spend_bundle = tx.spend_bundle
assert spend_bundle is not None
await time_out_assert(20, tx_in_mempool, True, client, transaction_id)
await time_out_assert(20, get_unconfirmed_balance, generated_funds - tx_amount, client, 1)
await farm_transaction(full_node_api, wallet_node, spend_bundle)
# Checks that the memo can be retrieved
tx_confirmed = await client.get_transaction(1, transaction_id)
assert tx_confirmed.confirmed
assert len(tx_confirmed.get_memos()) == 1
assert [b"this is a basic tx"] in tx_confirmed.get_memos().values()
assert list(tx_confirmed.get_memos().keys())[0] in [a.name() for a in spend_bundle.additions()]
await time_out_assert(20, get_confirmed_balance, generated_funds - tx_amount, client, 1)
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN, ConsensusMode.HARD_FORK_2_0], reason="save time")
@pytest.mark.anyio
async def test_push_transactions(wallet_rpc_environment: WalletRpcTestEnvironment):
env: WalletRpcTestEnvironment = wallet_rpc_environment
wallet: Wallet = env.wallet_1.wallet
wallet_node: WalletNode = env.wallet_1.node
full_node_api: FullNodeSimulator = env.full_node.api
client: WalletRpcClient = env.wallet_1.rpc_client
await generate_funds(full_node_api, env.wallet_1)
outputs = await create_tx_outputs(wallet, [(1234321, None)])
tx = await client.create_signed_transaction(
outputs,
tx_config=DEFAULT_TX_CONFIG,
fee=uint64(100),
)
await client.push_transactions([tx])
spend_bundle = tx.spend_bundle
assert spend_bundle is not None
await farm_transaction(full_node_api, wallet_node, spend_bundle)
tx = await client.get_transaction(1, transaction_id=tx.name)
assert tx.confirmed
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN, ConsensusMode.HARD_FORK_2_0], reason="save time")
@pytest.mark.anyio
async def test_get_balance(wallet_rpc_environment: WalletRpcTestEnvironment):
env = wallet_rpc_environment
wallet: Wallet = env.wallet_1.wallet
wallet_node: WalletNode = env.wallet_1.node
full_node_api: FullNodeSimulator = env.full_node.api
wallet_rpc_client = env.wallet_1.rpc_client
await full_node_api.farm_blocks_to_wallet(2, wallet)
async with wallet_node.wallet_state_manager.lock:
cat_wallet: CATWallet = await CATWallet.create_new_cat_wallet(
wallet_node.wallet_state_manager, wallet, {"identifier": "genesis_by_id"}, uint64(100), DEFAULT_TX_CONFIG
)
await assert_get_balance(wallet_rpc_client, wallet_node, wallet)
await assert_get_balance(wallet_rpc_client, wallet_node, cat_wallet)
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN, ConsensusMode.HARD_FORK_2_0], reason="save time")
@pytest.mark.anyio
async def test_get_farmed_amount(wallet_rpc_environment: WalletRpcTestEnvironment):
env = wallet_rpc_environment
wallet: Wallet = env.wallet_1.wallet
full_node_api: FullNodeSimulator = env.full_node.api
wallet_rpc_client = env.wallet_1.rpc_client
await full_node_api.farm_blocks_to_wallet(2, wallet)
get_farmed_amount_result = await wallet_rpc_client.get_farmed_amount()
get_timestamp_for_height_result = await wallet_rpc_client.get_timestamp_for_height(uint32(2))
expected_result = {
"blocks_won": 2,
"farmed_amount": 4_000_000_000_000,
"farmer_reward_amount": 500_000_000_000,
"fee_amount": 0,
"last_height_farmed": 2,
"last_time_farmed": get_timestamp_for_height_result,
"pool_reward_amount": 3_500_000_000_000,
"success": True,
}
assert get_farmed_amount_result == expected_result
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN, ConsensusMode.HARD_FORK_2_0], reason="save time")
@pytest.mark.anyio
async def test_get_farmed_amount_with_fee(wallet_rpc_environment: WalletRpcTestEnvironment):
env = wallet_rpc_environment
wallet: Wallet = env.wallet_1.wallet
full_node_api: FullNodeSimulator = env.full_node.api
wallet_rpc_client = env.wallet_1.rpc_client
wallet_node: WalletNode = env.wallet_1.node
await generate_funds(full_node_api, env.wallet_1)
fee_amount = 100
[tx] = await wallet.generate_signed_transaction(
amount=uint64(5),
puzzle_hash=bytes32([0] * 32),
tx_config=DEFAULT_TX_CONFIG,
fee=uint64(fee_amount),
)
await wallet.push_transaction(tx)
our_ph = await wallet.get_new_puzzlehash()
await full_node_api.wait_transaction_records_entered_mempool(records=[tx])
await full_node_api.farm_blocks_to_puzzlehash(count=2, farm_to=our_ph, guarantee_transaction_blocks=True)
await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20)
result = await wallet_rpc_client.get_farmed_amount()
assert result["fee_amount"] == fee_amount
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN, ConsensusMode.HARD_FORK_2_0], reason="save time")
@pytest.mark.anyio
async def test_get_timestamp_for_height(wallet_rpc_environment: WalletRpcTestEnvironment):
env: WalletRpcTestEnvironment = wallet_rpc_environment
full_node_api: FullNodeSimulator = env.full_node.api
client: WalletRpcClient = env.wallet_1.rpc_client
await generate_funds(full_node_api, env.wallet_1)
# This tests that the client returns a uint64, rather than raising or returning something unexpected
uint64(await client.get_timestamp_for_height(uint32(1)))
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN, ConsensusMode.HARD_FORK_2_0], reason="save time")
@pytest.mark.parametrize(
"output_args, fee, select_coin, is_cat",
[
([(348026, None)], 0, False, False),
([(1270495230, ["memo_1"]), (902347, ["memo_2"])], 1, True, False),
([(84920, ["memo_1_0", "memo_1_1"]), (1, ["memo_2_0"])], 0, False, False),
(
[(32058710, ["memo_1_0", "memo_1_1"]), (1, ["memo_2_0"]), (923, ["memo_3_0", "memo_3_1"])],
32804,
True,
False,
),
([(1337, ["LEET"]), (81000, ["pingwei"])], 817, False, True),
([(120000000000, None), (120000000000, None)], 10000000000, True, False),
],
)
@pytest.mark.anyio
async def test_create_signed_transaction(
wallet_rpc_environment: WalletRpcTestEnvironment,
output_args: List[Tuple[int, Optional[List[str]]]],
fee: int,
select_coin: bool,
is_cat: bool,
):
env: WalletRpcTestEnvironment = wallet_rpc_environment
wallet_2: Wallet = env.wallet_2.wallet
wallet_1_node: WalletNode = env.wallet_1.node
wallet_1_rpc: WalletRpcClient = env.wallet_1.rpc_client
full_node_api: FullNodeSimulator = env.full_node.api
full_node_rpc: FullNodeRpcClient = env.full_node.rpc_client
generated_funds = await generate_funds(full_node_api, env.wallet_1)
wallet_id = 1
if is_cat:
generated_funds = 10**9
res = await wallet_1_rpc.create_new_cat_and_wallet(uint64(generated_funds), test=True)
assert res["success"]
wallet_id = res["wallet_id"]
await time_out_assert(5, check_mempool_spend_count, True, full_node_api, 1)
for i in range(5):
if check_mempool_spend_count(full_node_api, 0):
break
await farm_transaction_block(full_node_api, wallet_1_node)
outputs = await create_tx_outputs(wallet_2, output_args)
amount_outputs = sum(output["amount"] for output in outputs)
amount_fee = uint64(fee)
if is_cat:
amount_total = amount_outputs
else:
amount_total = amount_outputs + amount_fee
selected_coin = None
if select_coin:
selected_coin = await wallet_1_rpc.select_coins(
amount=amount_total, wallet_id=wallet_id, coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG
)
assert len(selected_coin) == 1
tx = await wallet_1_rpc.create_signed_transaction(
outputs,
coins=selected_coin,
fee=amount_fee,
wallet_id=wallet_id,
# shouldn't actually block it
tx_config=DEFAULT_TX_CONFIG.override(
excluded_coin_amounts=[uint64(selected_coin[0].amount)] if selected_coin is not None else [],
),
)
change_expected = not selected_coin or selected_coin[0].amount - amount_total > 0
assert_tx_amounts(tx, outputs, amount_fee=amount_fee, change_expected=change_expected, is_cat=is_cat)
# Farm the transaction and make sure the wallet balance reflects it correct
spend_bundle = tx.spend_bundle
assert spend_bundle is not None
push_res = await wallet_1_rpc.push_transactions([tx])
assert push_res["success"]
await farm_transaction(full_node_api, wallet_1_node, spend_bundle)
await time_out_assert(20, get_confirmed_balance, generated_funds - amount_total, wallet_1_rpc, wallet_id)
# Validate the memos
for output in outputs:
if "memos" in outputs:
found: bool = False
for addition in spend_bundle.additions():
if addition.amount == output["amount"] and addition.puzzle_hash.hex() == output["puzzle_hash"]:
cr: Optional[CoinRecord] = await full_node_rpc.get_coin_record_by_name(addition.name())
assert cr is not None
spend: Optional[CoinSpend] = await full_node_rpc.get_puzzle_and_solution(
addition.parent_coin_info, cr.confirmed_block_index
)
assert spend is not None
sb: SpendBundle = SpendBundle([spend], G2Element())
assert compute_memos(sb) == {addition.name(): [memo.encode() for memo in output["memos"]]}
found = True
assert found
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN, ConsensusMode.HARD_FORK_2_0], reason="save time")
@pytest.mark.anyio
async def test_create_signed_transaction_with_coin_announcement(wallet_rpc_environment: WalletRpcTestEnvironment):
env: WalletRpcTestEnvironment = wallet_rpc_environment
wallet_2: Wallet = env.wallet_2.wallet
full_node_api: FullNodeSimulator = env.full_node.api
client: WalletRpcClient = env.wallet_1.rpc_client
client_node: FullNodeRpcClient = env.full_node.rpc_client
await generate_funds(full_node_api, env.wallet_1)
signed_tx_amount = uint64(888000)
tx_coin_announcements = [
Announcement(
std_hash(b"coin_id_1"),
std_hash(b"message"),
b"\xca",
),
Announcement(
std_hash(b"coin_id_2"),
bytes(Program.to("a string")),
),
]
outputs = await create_tx_outputs(wallet_2, [(signed_tx_amount, None)])
tx_res: TransactionRecord = await client.create_signed_transaction(
outputs, tx_config=DEFAULT_TX_CONFIG, coin_announcements=tx_coin_announcements
)
assert_tx_amounts(tx_res, outputs, amount_fee=uint64(0), change_expected=True)
await assert_push_tx_error(client_node, tx_res)
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN, ConsensusMode.HARD_FORK_2_0], reason="save time")
@pytest.mark.anyio
async def test_create_signed_transaction_with_puzzle_announcement(wallet_rpc_environment: WalletRpcTestEnvironment):
env: WalletRpcTestEnvironment = wallet_rpc_environment
wallet_2: Wallet = env.wallet_2.wallet
full_node_api: FullNodeSimulator = env.full_node.api
client: WalletRpcClient = env.wallet_1.rpc_client
client_node: FullNodeRpcClient = env.full_node.rpc_client
await generate_funds(full_node_api, env.wallet_1)
signed_tx_amount = uint64(888000)
tx_puzzle_announcements = [
Announcement(
std_hash(b"puzzle_hash_1"),
b"message",
b"\xca",
),
Announcement(
std_hash(b"puzzle_hash_2"),
bytes(Program.to("a string")),
),
]
outputs = await create_tx_outputs(wallet_2, [(signed_tx_amount, None)])
tx_res = await client.create_signed_transaction(
outputs, tx_config=DEFAULT_TX_CONFIG, puzzle_announcements=tx_puzzle_announcements
)
assert_tx_amounts(tx_res, outputs, amount_fee=uint64(0), change_expected=True)
await assert_push_tx_error(client_node, tx_res)
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN, ConsensusMode.HARD_FORK_2_0], reason="save time")
@pytest.mark.anyio
async def test_create_signed_transaction_with_excluded_coins(wallet_rpc_environment: WalletRpcTestEnvironment) -> None:
env: WalletRpcTestEnvironment = wallet_rpc_environment
wallet_1: Wallet = env.wallet_1.wallet
wallet_1_rpc: WalletRpcClient = env.wallet_1.rpc_client
full_node_api: FullNodeSimulator = env.full_node.api
full_node_rpc: FullNodeRpcClient = env.full_node.rpc_client
await generate_funds(full_node_api, env.wallet_1)
async def it_does_not_include_the_excluded_coins() -> None:
selected_coins = await wallet_1_rpc.select_coins(
amount=250000000000, wallet_id=1, coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG
)
assert len(selected_coins) == 1
outputs = await create_tx_outputs(wallet_1, [(uint64(250000000000), None)])
tx = await wallet_1_rpc.create_signed_transaction(
outputs,
DEFAULT_TX_CONFIG.override(
excluded_coin_ids=[c.name() for c in selected_coins],
),
)
assert len(tx.removals) == 1
assert tx.removals[0] != selected_coins[0]
assert tx.removals[0].amount == uint64(1750000000000)
await assert_push_tx_error(full_node_rpc, tx)
async def it_throws_an_error_when_all_spendable_coins_are_excluded() -> None:
selected_coins = await wallet_1_rpc.select_coins(
amount=1750000000000, wallet_id=1, coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG
)
assert len(selected_coins) == 1
outputs = await create_tx_outputs(wallet_1, [(uint64(1750000000000), None)])
with pytest.raises(ValueError):
await wallet_1_rpc.create_signed_transaction(
outputs,
DEFAULT_TX_CONFIG.override(
excluded_coin_ids=[c.name() for c in selected_coins],
),
)
await it_does_not_include_the_excluded_coins()
await it_throws_an_error_when_all_spendable_coins_are_excluded()
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN, ConsensusMode.HARD_FORK_2_0], reason="save time")
@pytest.mark.anyio
async def test_spend_clawback_coins(wallet_rpc_environment: WalletRpcTestEnvironment):
env: WalletRpcTestEnvironment = wallet_rpc_environment
wallet_1_node: WalletNode = env.wallet_1.node
wallet_2_node: WalletNode = env.wallet_2.node
wallet_1_rpc: WalletRpcClient = env.wallet_1.rpc_client
wallet_2_rpc: WalletRpcClient = env.wallet_2.rpc_client
wallet_1 = wallet_1_node.wallet_state_manager.main_wallet
wallet_2 = wallet_2_node.wallet_state_manager.main_wallet
full_node_api: FullNodeSimulator = env.full_node.api
wallet_2_api = WalletRpcApi(wallet_2_node)
generated_funds = await generate_funds(full_node_api, env.wallet_1, 1)
await generate_funds(full_node_api, env.wallet_2, 1)
wallet_1_puzhash = await wallet_1.get_new_puzzlehash()
await full_node_api.wait_for_wallet_synced(wallet_node=wallet_1_node, timeout=20)
wallet_2_puzhash = await wallet_2.get_new_puzzlehash()
tx = await wallet_1_rpc.send_transaction(
wallet_id=1,
amount=uint64(500),
address=encode_puzzle_hash(wallet_2_puzhash, "txch"),
tx_config=DEFAULT_TX_CONFIG,
fee=uint64(0),
puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}],
)
clawback_coin_id_1 = tx.additions[0].name()
assert tx.spend_bundle is not None
await farm_transaction(full_node_api, wallet_1_node, tx.spend_bundle)
await full_node_api.wait_for_wallet_synced(wallet_node=wallet_2_node, timeout=20)
tx = await wallet_2_rpc.send_transaction(
wallet_id=1,
amount=uint64(500),
address=encode_puzzle_hash(wallet_1_puzhash, "txch"),
tx_config=DEFAULT_TX_CONFIG,
fee=uint64(0),
puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}],
)
assert tx.spend_bundle is not None
clawback_coin_id_2 = tx.additions[0].name()
await farm_transaction(full_node_api, wallet_2_node, tx.spend_bundle)
await time_out_assert(20, get_confirmed_balance, generated_funds - 500, wallet_1_rpc, 1)
await time_out_assert(20, get_confirmed_balance, generated_funds - 500, wallet_2_rpc, 1)
await asyncio.sleep(10)
# Test missing coin_ids
has_exception = False
try:
await wallet_2_api.spend_clawback_coins({})
except ValueError:
has_exception = True
assert has_exception
# Test coin ID is not a Clawback coin
invalid_coin_id = tx.removals[0].name()
resp = await wallet_2_rpc.spend_clawback_coins([invalid_coin_id], 500)
assert resp["success"]
assert resp["transaction_ids"] == []
# Test unsupported wallet
coin_record = await wallet_1_node.wallet_state_manager.coin_store.get_coin_record(clawback_coin_id_1)
assert coin_record is not None
await wallet_1_node.wallet_state_manager.coin_store.add_coin_record(
dataclasses.replace(coin_record, wallet_type=WalletType.CAT)
)
resp = await wallet_1_rpc.spend_clawback_coins([clawback_coin_id_1], 100)
assert resp["success"]
assert len(resp["transaction_ids"]) == 0
# Test missing metadata
await wallet_1_node.wallet_state_manager.coin_store.add_coin_record(dataclasses.replace(coin_record, metadata=None))
resp = await wallet_1_rpc.spend_clawback_coins([clawback_coin_id_1], 100)
assert resp["success"]
assert len(resp["transaction_ids"]) == 0
# Test missing incoming tx
coin_record = await wallet_1_node.wallet_state_manager.coin_store.get_coin_record(clawback_coin_id_2)
assert coin_record is not None
fake_coin = Coin(coin_record.coin.parent_coin_info, wallet_2_puzhash, coin_record.coin.amount)
await wallet_1_node.wallet_state_manager.coin_store.add_coin_record(
dataclasses.replace(coin_record, coin=fake_coin)
)
resp = await wallet_1_rpc.spend_clawback_coins([fake_coin.name()], 100)
assert resp["transaction_ids"] == []
# Test coin puzzle hash doesn't match the puzzle
tx = (await wallet_1.wallet_state_manager.tx_store.get_farming_rewards())[0]
await wallet_1.wallet_state_manager.tx_store.add_transaction_record(dataclasses.replace(tx, name=fake_coin.name()))
await wallet_1_node.wallet_state_manager.coin_store.add_coin_record(
dataclasses.replace(coin_record, coin=fake_coin)
)
resp = await wallet_1_rpc.spend_clawback_coins([fake_coin.name()], 100)
assert resp["transaction_ids"] == []
# Test claim spend
await wallet_2_api.set_auto_claim({"enabled": False, "tx_fee": 100, "min_amount": 0, "batch_size": 1})
resp = await wallet_2_rpc.spend_clawback_coins([clawback_coin_id_1, clawback_coin_id_2], 100)
assert resp["success"]
assert len(resp["transaction_ids"]) == 2
await time_out_assert_not_none(
10, full_node_api.full_node.mempool_manager.get_spendbundle, bytes32.from_hexstr(resp["transaction_ids"][0])
)
await time_out_assert_not_none(
10, full_node_api.full_node.mempool_manager.get_spendbundle, bytes32.from_hexstr(resp["transaction_ids"][1])
)
await farm_transaction_block(full_node_api, wallet_2_node)
await time_out_assert(20, get_confirmed_balance, generated_funds + 300, wallet_2_rpc, 1)
# Test spent coin
resp = await wallet_2_rpc.spend_clawback_coins([clawback_coin_id_1], 500)
assert resp["success"]
assert resp["transaction_ids"] == []
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN, ConsensusMode.HARD_FORK_2_0], reason="save time")
@pytest.mark.anyio
async def test_send_transaction_multi(wallet_rpc_environment: WalletRpcTestEnvironment):
env: WalletRpcTestEnvironment = wallet_rpc_environment
wallet_2: Wallet = env.wallet_2.wallet
wallet_node: WalletNode = env.wallet_1.node
full_node_api: FullNodeSimulator = env.full_node.api
client: WalletRpcClient = env.wallet_1.rpc_client
generated_funds = await generate_funds(full_node_api, env.wallet_1)
removals = await client.select_coins(
1750000000000, wallet_id=1, coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG
) # we want a coin that won't be selected by default
outputs = await create_tx_outputs(wallet_2, [(uint64(1), ["memo_1"]), (uint64(2), ["memo_2"])])
amount_outputs = sum(output["amount"] for output in outputs)
amount_fee = uint64(amount_outputs + 1)
send_tx_res: TransactionRecord = await client.send_transaction_multi(
1,
outputs,
DEFAULT_TX_CONFIG,
coins=removals,
fee=amount_fee,
)
spend_bundle = send_tx_res.spend_bundle
assert spend_bundle is not None
assert send_tx_res is not None
assert_tx_amounts(send_tx_res, outputs, amount_fee=amount_fee, change_expected=True)
assert send_tx_res.removals == removals
await farm_transaction(full_node_api, wallet_node, spend_bundle)
await time_out_assert(20, get_confirmed_balance, generated_funds - amount_outputs - amount_fee, client, 1)
# Checks that the memo can be retrieved
tx_confirmed = await client.get_transaction(1, send_tx_res.name)
assert tx_confirmed.confirmed
memos = tx_confirmed.get_memos()
assert len(memos) == len(outputs)
for output in outputs:
assert [output["memos"][0].encode()] in memos.values()
spend_bundle = send_tx_res.spend_bundle
assert spend_bundle is not None
for key in memos.keys():
assert key in [a.name() for a in spend_bundle.additions()]
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN, ConsensusMode.HARD_FORK_2_0], reason="save time")
@pytest.mark.anyio
async def test_get_transactions(wallet_rpc_environment: WalletRpcTestEnvironment):
env: WalletRpcTestEnvironment = wallet_rpc_environment
wallet: Wallet = env.wallet_1.wallet
wallet_node: WalletNode = env.wallet_1.node
full_node_api: FullNodeSimulator = env.full_node.api
client: WalletRpcClient = env.wallet_1.rpc_client
await generate_funds(full_node_api, env.wallet_1, 5)
all_transactions = await client.get_transactions(1)
assert len(all_transactions) >= 10
# Test transaction pagination
some_transactions = await client.get_transactions(1, 0, 5)
some_transactions_2 = await client.get_transactions(1, 5, 10)
assert some_transactions == all_transactions[0:5]
assert some_transactions_2 == all_transactions[5:10]
# Testing sorts
# Test the default sort (CONFIRMED_AT_HEIGHT)
assert all_transactions == sorted(all_transactions, key=attrgetter("confirmed_at_height"))
all_transactions = await client.get_transactions(1, reverse=True)
assert all_transactions == sorted(all_transactions, key=attrgetter("confirmed_at_height"), reverse=True)
# Test RELEVANCE
puzhash = await wallet.get_new_puzzlehash()
await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20)
await client.send_transaction(
1, uint64(1), encode_puzzle_hash(puzhash, "txch"), DEFAULT_TX_CONFIG
) # Create a pending tx
all_transactions = await client.get_transactions(1, sort_key=SortKey.RELEVANCE)
sorted_transactions = sorted(all_transactions, key=attrgetter("created_at_time"), reverse=True)
sorted_transactions = sorted(sorted_transactions, key=attrgetter("confirmed_at_height"), reverse=True)
sorted_transactions = sorted(sorted_transactions, key=attrgetter("confirmed"))
assert all_transactions == sorted_transactions
all_transactions = await client.get_transactions(1, sort_key=SortKey.RELEVANCE, reverse=True)
sorted_transactions = sorted(all_transactions, key=attrgetter("created_at_time"))
sorted_transactions = sorted(sorted_transactions, key=attrgetter("confirmed_at_height"))
sorted_transactions = sorted(sorted_transactions, key=attrgetter("confirmed"), reverse=True)
assert all_transactions == sorted_transactions
# Test get_transactions to address
ph_by_addr = await wallet.get_new_puzzlehash()
await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20)
await client.send_transaction(1, uint64(1), encode_puzzle_hash(ph_by_addr, "txch"), DEFAULT_TX_CONFIG)
await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20)
tx_for_address = await client.get_transactions(1, to_address=encode_puzzle_hash(ph_by_addr, "txch"))
assert len(tx_for_address) == 1
assert tx_for_address[0].to_puzzle_hash == ph_by_addr
# Test type filter
all_transactions = await client.get_transactions(
1, type_filter=TransactionTypeFilter.include([TransactionType.COINBASE_REWARD])
)
assert len(all_transactions) == 5
assert all(transaction.type == TransactionType.COINBASE_REWARD for transaction in all_transactions)
# Test confirmed filter
all_transactions = await client.get_transactions(1, confirmed=True)
assert len(all_transactions) == 10
assert all(transaction.confirmed for transaction in all_transactions)
all_transactions = await client.get_transactions(1, confirmed=False)
assert len(all_transactions) == 2
assert all(not transaction.confirmed for transaction in all_transactions)
# Test bypass broken txs
await wallet.wallet_state_manager.tx_store.add_transaction_record(
dataclasses.replace(all_transactions[0], type=uint32(TransactionType.INCOMING_CLAWBACK_SEND))
)
all_transactions = await client.get_transactions(
1, type_filter=TransactionTypeFilter.include([TransactionType.INCOMING_CLAWBACK_SEND]), confirmed=False
)
assert len(all_transactions) == 1
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN, ConsensusMode.HARD_FORK_2_0], reason="save time")
@pytest.mark.anyio
async def test_get_transaction_count(wallet_rpc_environment: WalletRpcTestEnvironment):
env: WalletRpcTestEnvironment = wallet_rpc_environment
full_node_api: FullNodeSimulator = env.full_node.api
client: WalletRpcClient = env.wallet_1.rpc_client
await generate_funds(full_node_api, env.wallet_1)
all_transactions = await client.get_transactions(1)
assert len(all_transactions) > 0
transaction_count = await client.get_transaction_count(1)
assert transaction_count == len(all_transactions)
assert await client.get_transaction_count(1, confirmed=False) == 0
assert (
await client.get_transaction_count(
1, type_filter=TransactionTypeFilter.include([TransactionType.INCOMING_CLAWBACK_SEND])
)
== 0
)
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.PLAIN, ConsensusMode.HARD_FORK_2_0], reason="save time")
@pytest.mark.anyio
async def test_cat_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment):
env: WalletRpcTestEnvironment = wallet_rpc_environment
wallet_node: WalletNode = env.wallet_1.node
client: WalletRpcClient = env.wallet_1.rpc_client
client_2: WalletRpcClient = env.wallet_2.rpc_client
full_node_api: FullNodeSimulator = env.full_node.api
await generate_funds(full_node_api, env.wallet_1, 1)
await generate_funds(full_node_api, env.wallet_2, 1)
# Test a deprecated path
with pytest.raises(ValueError, match="dropped"):
await client.fetch(
"create_new_wallet",
{
"wallet_type": "cat_wallet",
"mode": "new",
},
)
# Creates a CAT wallet with 100 mojos and a CAT with 20 mojos and fee=10
await client.create_new_cat_and_wallet(uint64(100), fee=uint64(10), test=True)
await time_out_assert(20, client.get_synced)
res = await client.create_new_cat_and_wallet(uint64(20), test=True)
assert res["success"]
cat_0_id = res["wallet_id"]
asset_id = bytes32.fromhex(res["asset_id"])
assert len(asset_id) > 0
await assert_wallet_types(client, {WalletType.STANDARD_WALLET: 1, WalletType.CAT: 2})
await assert_wallet_types(client_2, {WalletType.STANDARD_WALLET: 1})
bal_0 = await client.get_wallet_balance(cat_0_id)
assert bal_0["confirmed_wallet_balance"] == 0
assert bal_0["pending_coin_removal_count"] == 1
col = await client.get_cat_asset_id(cat_0_id)
assert col == asset_id
assert (await client.get_cat_name(cat_0_id)) == CATWallet.default_wallet_name_for_unknown_cat(asset_id.hex())
await client.set_cat_name(cat_0_id, "My cat")
assert (await client.get_cat_name(cat_0_id)) == "My cat"
result = await client.cat_asset_id_to_name(col)
assert result is not None
wid, name = result
assert wid == cat_0_id
assert name == "My cat"
result = await client.cat_asset_id_to_name(bytes32([0] * 32))
assert result is None
verified_asset_id = next(iter(DEFAULT_CATS.items()))[1]["asset_id"]
result = await client.cat_asset_id_to_name(bytes32.from_hexstr(verified_asset_id))
assert result is not None
should_be_none, name = result
assert should_be_none is None
assert name == next(iter(DEFAULT_CATS.items()))[1]["name"]
# make sure spend is in mempool before farming tx block
await time_out_assert(5, check_mempool_spend_count, True, full_node_api, 2)
for i in range(5):
if check_mempool_spend_count(full_node_api, 0):
break
await farm_transaction_block(full_node_api, wallet_node)
# check that we farmed the transaction
assert check_mempool_spend_count(full_node_api, 0)
await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=5)
await time_out_assert(5, get_confirmed_balance, 20, client, cat_0_id)
bal_0 = await client.get_wallet_balance(cat_0_id)
assert bal_0["pending_coin_removal_count"] == 0
assert bal_0["unspent_coin_count"] == 1