diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 327767d41d..e94cdcf765 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -18,7 +18,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.bech32m import bech32_decode, decode_puzzle_hash, encode_puzzle_hash from chia.util.config import load_config from chia.util.default_root import DEFAULT_ROOT_PATH -from chia.util.ints import uint16, uint32, uint64, uint128 +from chia.util.ints import uint16, uint32, uint64 from chia.wallet.did_wallet.did_info import DID_HRP from chia.wallet.nft_wallet.nft_info import NFT_HRP, NFTInfo from chia.wallet.trade_record import TradeRecord @@ -213,17 +213,17 @@ async def send(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> final_fee = uint64(int(fee * units["chia"])) final_amount: uint64 - final_min_coin_amount: uint128 + final_min_coin_amount: uint64 if typ == WalletType.STANDARD_WALLET: final_amount = uint64(int(amount * units["chia"])) - final_min_coin_amount = uint128(int(min_coin_amount * units["chia"])) + final_min_coin_amount = uint64(int(min_coin_amount * units["chia"])) print("Submitting transaction...") res = await wallet_client.send_transaction( str(wallet_id), final_amount, address, final_fee, memos, final_min_coin_amount ) elif typ == WalletType.CAT: final_amount = uint64(int(amount * units["cat"])) - final_min_coin_amount = uint128(int(min_coin_amount * units["cat"])) + final_min_coin_amount = uint64(int(min_coin_amount * units["cat"])) print("Submitting transaction...") res = await wallet_client.cat_spend( str(wallet_id), final_amount, address, final_fee, memos, final_min_coin_amount diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index d9869ac923..3d5c25e61b 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -23,7 +23,7 @@ from chia.types.spend_bundle import SpendBundle from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash from chia.util.byte_types import hexstr_to_bytes from chia.util.config import load_config -from chia.util.ints import uint8, uint32, uint64, uint16, uint128 +from chia.util.ints import uint8, uint32, uint64, uint16 from chia.util.keychain import KeyringIsLocked, bytes_to_mnemonic, generate_mnemonic from chia.util.path import path_from_root from chia.util.ws_message import WsRpcMessage, create_payload_dict @@ -840,7 +840,7 @@ class WalletRpcApi: memos = [mem.encode("utf-8") for mem in request["memos"]] fee: uint64 = uint64(request.get("fee", 0)) - min_coin_amount: uint128 = uint128(request.get("min_coin_amount", 0)) + min_coin_amount: uint64 = uint64(request.get("min_coin_amount", 0)) async with self.service.wallet_state_manager.lock: tx: TransactionRecord = await wallet.generate_signed_transaction( amount, puzzle_hash, fee, memos=memos, min_coin_amount=min_coin_amount @@ -896,10 +896,16 @@ class WalletRpcApi: amount = uint64(request["amount"]) wallet_id = uint32(request["wallet_id"]) + min_coin_amount = uint64(request.get("min_coin_amount", 0)) + excluded_coins: Optional[List] = request.get("excluded_coins") + if excluded_coins is not None: + excluded_coins = [Coin.from_json_dict(json_coin) for json_coin in excluded_coins] wallet = self.service.wallet_state_manager.wallets[wallet_id] async with self.service.wallet_state_manager.lock: - selected_coins = await wallet.select_coins(amount=amount) + selected_coins = await wallet.select_coins( + amount=amount, min_coin_amount=min_coin_amount, exclude=excluded_coins + ) return {"coins": [coin.to_json_dict() for coin in selected_coins]} @@ -999,7 +1005,7 @@ class WalletRpcApi: raise ValueError("An integer amount or fee is required (too many decimals)") amount: uint64 = uint64(request["amount"]) fee: uint64 = uint64(request.get("fee", 0)) - min_coin_amount: uint128 = uint128(request.get("min_coin_amount", 0)) + min_coin_amount: uint64 = uint64(request.get("min_coin_amount", 0)) async with self.service.wallet_state_manager.lock: txs: List[TransactionRecord] = await wallet.generate_signed_transaction( [amount], [puzzle_hash], fee, memos=[memos], min_coin_amount=min_coin_amount @@ -1037,7 +1043,7 @@ class WalletRpcApi: fee: uint64 = uint64(request.get("fee", 0)) validate_only: bool = request.get("validate_only", False) driver_dict_str: Optional[Dict[str, Any]] = request.get("driver_dict", None) - min_coin_amount: uint128 = uint128(request.get("min_coin_amount", 0)) + min_coin_amount: uint64 = uint64(request.get("min_coin_amount", 0)) # This driver_dict construction is to maintain backward compatibility where everything is assumed to be a CAT driver_dict: Dict[bytes32, PuzzleInfo] = {} @@ -1096,7 +1102,7 @@ class WalletRpcApi: offer_hex: str = request["offer"] offer = Offer.from_bech32(offer_hex) fee: uint64 = uint64(request.get("fee", 0)) - min_coin_amount: uint128 = uint128(request.get("min_coin_amount", 0)) + min_coin_amount: uint64 = uint64(request.get("min_coin_amount", 0)) async with self.service.wallet_state_manager.lock: (success, trade_record, error,) = await self.service.wallet_state_manager.trade_manager.respond_to_offer( @@ -1805,7 +1811,7 @@ class WalletRpcApi: additional_outputs.append({"puzzlehash": receiver_ph, "amount": amount, "memos": memos}) fee: uint64 = uint64(request.get("fee", 0)) - min_coin_amount: uint128 = uint128(request.get("min_coin_amount", 0)) + min_coin_amount: uint64 = uint64(request.get("min_coin_amount", 0)) coins = None if "coins" in request and len(request["coins"]) > 0: diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 9f164b3185..3727e30243 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -5,7 +5,7 @@ from chia.rpc.rpc_client import RpcClient from chia.types.announcement import Announcement from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.util.ints import uint32, uint64, uint128 +from chia.util.ints import uint32, uint64 from chia.wallet.trade_record import TradeRecord from chia.wallet.trading.offer import Offer from chia.wallet.transaction_record import TransactionRecord @@ -146,7 +146,7 @@ class WalletRpcClient(RpcClient): address: str, fee: uint64 = uint64(0), memos: Optional[List[str]] = None, - min_coin_amount: uint128 = uint128(0), + min_coin_amount: uint64 = uint64(0), ) -> TransactionRecord: if memos is None: send_dict: Dict = { @@ -213,7 +213,7 @@ class WalletRpcClient(RpcClient): fee: uint64 = uint64(0), coin_announcements: Optional[List[Announcement]] = None, puzzle_announcements: Optional[List[Announcement]] = None, - min_coin_amount: uint128 = uint128(0), + min_coin_amount: uint64 = uint64(0), ) -> TransactionRecord: # Converts bytes to hex for puzzle hashes additions_hex = [] @@ -255,8 +255,22 @@ class WalletRpcClient(RpcClient): response: Dict = await self.fetch("create_signed_transaction", request) return TransactionRecord.from_json_dict_convenience(response["signed_tx"]) - async def select_coins(self, *, amount: int, wallet_id: int) -> List[Coin]: - request = {"amount": amount, "wallet_id": wallet_id} + async def select_coins( + self, + *, + amount: int, + wallet_id: int, + excluded_coins: Optional[List[Coin]] = None, + min_coin_amount: uint64 = uint64(0), + ) -> List[Coin]: + if excluded_coins is None: + excluded_coins = [] + request = { + "amount": amount, + "wallet_id": wallet_id, + "min_coin_amount": min_coin_amount, + "excluded_coins": [excluded_coin.to_json_dict() for excluded_coin in excluded_coins], + } response: Dict[str, List[Dict]] = await self.fetch("select_coins", request) return [Coin.from_json_dict(coin) for coin in response["coins"]] @@ -505,7 +519,7 @@ class WalletRpcClient(RpcClient): inner_address: str, fee: uint64 = uint64(0), memos: Optional[List[str]] = None, - min_coin_amount: uint128 = uint128(0), + min_coin_amount: uint64 = uint64(0), ) -> TransactionRecord: send_dict = { "wallet_id": wallet_id, @@ -525,7 +539,7 @@ class WalletRpcClient(RpcClient): driver_dict: Dict[str, Any] = None, fee=uint64(0), validate_only: bool = False, - min_coin_amount: uint128 = uint128(0), + min_coin_amount: uint64 = uint64(0), ) -> Tuple[Optional[Offer], TradeRecord]: send_dict: Dict[str, int] = {} for key in offer_dict: @@ -552,7 +566,7 @@ class WalletRpcClient(RpcClient): res = await self.fetch("check_offer_validity", {"offer": offer.to_bech32()}) return res["valid"] - async def take_offer(self, offer: Offer, fee=uint64(0), min_coin_amount: uint128 = uint128(0)) -> TradeRecord: + async def take_offer(self, offer: Offer, fee=uint64(0), min_coin_amount: uint64 = uint64(0)) -> TradeRecord: res = await self.fetch( "take_offer", {"offer": offer.to_bech32(), "fee": fee, "min_coin_amount": min_coin_amount} ) diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index f5f5f8c59f..e40baeb11e 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -445,7 +445,7 @@ class CATWallet: return result async def select_coins( - self, amount: uint64, exclude: Optional[List[Coin]] = None, min_coin_amount: Optional[uint128] = None + self, amount: uint64, exclude: Optional[List[Coin]] = None, min_coin_amount: Optional[uint64] = None ) -> Set[Coin]: """ Returns a set of coins that can be used for generating a new transaction. @@ -532,7 +532,7 @@ class CATWallet: fee: uint64, amount_to_claim: uint64, announcement_to_assert: Optional[Announcement] = None, - min_coin_amount: Optional[uint128] = None, + min_coin_amount: Optional[uint64] = None, ) -> Tuple[TransactionRecord, Optional[Announcement]]: """ This function creates a non-CAT transaction to pay fees, contribute funds for issuance, and absorb melt value. @@ -586,7 +586,7 @@ class CATWallet: coins: Set[Coin] = None, coin_announcements_to_consume: Optional[Set[Announcement]] = None, puzzle_announcements_to_consume: Optional[Set[Announcement]] = None, - min_coin_amount: Optional[uint128] = None, + min_coin_amount: Optional[uint64] = None, ) -> Tuple[SpendBundle, Optional[TransactionRecord]]: if coin_announcements_to_consume is not None: coin_announcements_bytes: Optional[Set[bytes32]] = {a.name() for a in coin_announcements_to_consume} @@ -722,7 +722,7 @@ class CATWallet: memos: Optional[List[List[bytes]]] = None, coin_announcements_to_consume: Optional[Set[Announcement]] = None, puzzle_announcements_to_consume: Optional[Set[Announcement]] = None, - min_coin_amount: Optional[uint128] = None, + min_coin_amount: Optional[uint64] = None, ) -> List[TransactionRecord]: if memos is None: memos = [[] for _ in range(len(puzzle_hashes))] @@ -828,7 +828,7 @@ class CATWallet: return PuzzleInfo({"type": AssetType.CAT.value, "tail": "0x" + self.get_asset_id()}) async def get_coins_to_offer( - self, asset_id: Optional[bytes32], amount: uint64, min_coin_amount: Optional[uint128] = None + self, asset_id: Optional[bytes32], amount: uint64, min_coin_amount: Optional[uint64] = None ) -> Set[Coin]: balance = await self.get_confirmed_balance() if balance < amount: diff --git a/chia/wallet/coin_selection.py b/chia/wallet/coin_selection.py index 2ce747de7d..c9c764412b 100644 --- a/chia/wallet/coin_selection.py +++ b/chia/wallet/coin_selection.py @@ -16,7 +16,7 @@ async def select_coins( log: logging.Logger, amount: uint128, exclude: Optional[List[Coin]] = None, - min_coin_amount: Optional[uint128] = None, + min_coin_amount: Optional[uint64] = None, ) -> Set[Coin]: """ Returns a set of coins that can be used for generating a new transaction. @@ -24,7 +24,7 @@ async def select_coins( if exclude is None: exclude = [] if min_coin_amount is None: - min_coin_amount = uint128(0) + min_coin_amount = uint64(0) if amount > spendable_amount: error_msg = ( diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index 54ce636a3f..70c756df1b 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -299,7 +299,7 @@ class DIDWallet: return await self.wallet_state_manager.get_unconfirmed_balance(self.id(), record_list) async def select_coins( - self, amount: uint64, exclude: Optional[List[Coin]] = None, min_coin_amount: Optional[uint128] = None + self, amount: uint64, exclude: Optional[List[Coin]] = None, min_coin_amount: Optional[uint64] = None ) -> Optional[Set[Coin]]: """ Returns a set of coins that can be used for generating a new transaction. diff --git a/chia/wallet/nft_wallet/nft_wallet.py b/chia/wallet/nft_wallet/nft_wallet.py index f86573482d..e99c5ac22e 100644 --- a/chia/wallet/nft_wallet/nft_wallet.py +++ b/chia/wallet/nft_wallet/nft_wallet.py @@ -547,7 +547,7 @@ class NFTWallet: return puzzle_info async def get_coins_to_offer( - self, nft_id: bytes32, amount: uint64, min_coin_amount: Optional[uint128] = None + self, nft_id: bytes32, amount: uint64, min_coin_amount: Optional[uint64] = None ) -> Set[Coin]: nft_coin: Optional[NFTCoinInfo] = self.get_nft(nft_id) if nft_coin is None: diff --git a/chia/wallet/trade_manager.py b/chia/wallet/trade_manager.py index 530f5309bb..d426c2f158 100644 --- a/chia/wallet/trade_manager.py +++ b/chia/wallet/trade_manager.py @@ -11,7 +11,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.spend_bundle import SpendBundle from chia.util.db_wrapper import DBWrapper from chia.util.hash import std_hash -from chia.util.ints import uint32, uint64, uint128 +from chia.util.ints import uint32, uint64 from chia.wallet.nft_wallet.nft_wallet import NFTWallet from chia.wallet.outer_puzzles import AssetType from chia.wallet.payment import Payment @@ -297,7 +297,7 @@ class TradeManager: driver_dict: Optional[Dict[bytes32, PuzzleInfo]] = None, fee: uint64 = uint64(0), validate_only: bool = False, - min_coin_amount: Optional[uint128] = None, + min_coin_amount: Optional[uint64] = None, ) -> Tuple[bool, Optional[TradeRecord], Optional[str]]: if driver_dict is None: driver_dict = {} @@ -331,7 +331,7 @@ class TradeManager: offer_dict: Dict[Union[int, bytes32], int], driver_dict: Optional[Dict[bytes32, PuzzleInfo]] = None, fee: uint64 = uint64(0), - min_coin_amount: Optional[uint128] = None, + min_coin_amount: Optional[uint64] = None, ) -> Tuple[bool, Optional[Offer], Optional[str]]: """ Offer is dictionary of wallet ids and amount @@ -586,7 +586,7 @@ class TradeManager: return txs async def respond_to_offer( - self, offer: Offer, fee=uint64(0), min_coin_amount: Optional[uint128] = None + self, offer: Offer, fee=uint64(0), min_coin_amount: Optional[uint64] = None ) -> Tuple[bool, Optional[TradeRecord], Optional[str]]: take_offer_dict: Dict[Union[bytes32, int], int] = {} arbitrage: Dict[Optional[bytes32], int] = offer.arbitrage() diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 2b76b8573f..b3eb9d6111 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -247,7 +247,7 @@ class Wallet: return Program.to(python_program) async def select_coins( - self, amount: uint64, exclude: Optional[List[Coin]] = None, min_coin_amount: Optional[uint128] = None + self, amount: uint64, exclude: Optional[List[Coin]] = None, min_coin_amount: Optional[uint64] = None ) -> Set[Coin]: """ Returns a set of coins that can be used for generating a new transaction. @@ -291,7 +291,7 @@ class Wallet: puzzle_announcements_to_consume: Set[Announcement] = None, memos: Optional[List[bytes]] = None, negative_change_allowed: bool = False, - min_coin_amount: Optional[uint128] = None, + min_coin_amount: Optional[uint64] = None, ) -> List[CoinSpend]: """ Generates a unsigned transaction in form of List(Puzzle, Solutions) @@ -418,7 +418,7 @@ class Wallet: puzzle_announcements_to_consume: Set[Announcement] = None, memos: Optional[List[bytes]] = None, negative_change_allowed: bool = False, - min_coin_amount: Optional[uint128] = None, + min_coin_amount: Optional[uint64] = None, ) -> TransactionRecord: """ Use this to generate transaction. @@ -531,7 +531,7 @@ class Wallet: return spend_bundle async def get_coins_to_offer( - self, asset_id: Optional[bytes32], amount: uint64, min_coin_amount: Optional[uint128] = None + self, asset_id: Optional[bytes32], amount: uint64, min_coin_amount: Optional[uint64] = None ) -> Set[Coin]: if asset_id is not None: raise ValueError(f"The standard wallet cannot offer coins with asset id {asset_id}") diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 17e73b7c49..29fbd6a75d 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -19,6 +19,7 @@ from chia.server.server import ChiaServer 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 from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_record import CoinRecord @@ -1014,3 +1015,45 @@ async def test_key_and_address_endpoints(wallet_rpc_environment: WalletRpcTestEn # Delete all keys await client.delete_all_keys() assert len(await client.get_public_keys()) == 0 + + +@pytest.mark.asyncio +async def test_select_coins_rpc(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 + client_2: WalletRpcClient = env.wallet_2.rpc_client + + funds = await generate_funds(full_node_api, env.wallet_1) + + addr = encode_puzzle_hash(await wallet_2.get_new_puzzlehash(), "txch") + coin_300: List[Coin] + for tx_amount in [uint64(1000), uint64(300), uint64(1000), uint64(1000), uint64(10000)]: + funds -= tx_amount + # create coins for tests + tx = await client.send_transaction("1", tx_amount, addr) + spend_bundle = tx.spend_bundle + assert spend_bundle is not None + for coin in spend_bundle.additions(): + if coin.amount == uint64(300): + coin_300 = [coin] + + await time_out_assert(5, tx_in_mempool, True, client, tx.name) + await farm_transaction(full_node_api, wallet_node, spend_bundle) + await time_out_assert(5, get_confirmed_balance, funds, client, 1) + + # test min coin amount + min_coins: List[Coin] = await client_2.select_coins(amount=1000, wallet_id=1, min_coin_amount=uint64(1001)) + assert min_coins is not None + assert len(min_coins) == 1 and min_coins[0].amount == uint64(10000) + + # test excluded coins + with pytest.raises(ValueError): + await client_2.select_coins(amount=5000, wallet_id=1, excluded_coins=min_coins) + excluded_test = await client_2.select_coins(amount=1300, wallet_id=1, excluded_coins=coin_300) + assert len(excluded_test) == 2 + for coin in excluded_test: + assert coin != coin_300[0] diff --git a/tests/wallet/test_coin_selection.py b/tests/wallet/test_coin_selection.py index db7fa5edc6..7e28f0adf3 100644 --- a/tests/wallet/test_coin_selection.py +++ b/tests/wallet/test_coin_selection.py @@ -449,7 +449,7 @@ class TestCoinSelection: {}, logging.getLogger("test"), uint128(target_amount), - min_coin_amount=uint128(min_coin_amount), + min_coin_amount=uint64(min_coin_amount), ) assert result is not None # this should never happen assert sum(coin.amount for coin in result) >= target_amount