diff --git a/.github/workflows/test-install-scripts.yml b/.github/workflows/test-install-scripts.yml index 94e263ecf2f2..fddd52d68f0e 100644 --- a/.github/workflows/test-install-scripts.yml +++ b/.github/workflows/test-install-scripts.yml @@ -20,6 +20,10 @@ concurrency: group: ${{ github.event_name == 'pull_request' && format('{0}-{1}', github.workflow_ref, github.event.pull_request.number) || github.run_id }} cancel-in-progress: true +defaults: + run: + shell: bash + jobs: test_scripts: name: Native ${{ matrix.os.emoji }} ${{ matrix.arch.emoji }} ${{ matrix.development.name }} - ${{ matrix.editable.name }} @@ -109,6 +113,7 @@ jobs: - name: Run install-gui script (Windows) if: matrix.os.matrix == 'windows' + shell: pwsh run: | ./Install-gui.ps1 diff --git a/chia/_tests/cmds/wallet/test_notifications.py b/chia/_tests/cmds/wallet/test_notifications.py index cde0ca62b0c1..65e6e773da59 100644 --- a/chia/_tests/cmds/wallet/test_notifications.py +++ b/chia/_tests/cmds/wallet/test_notifications.py @@ -1,18 +1,23 @@ from __future__ import annotations from pathlib import Path -from typing import cast from chia_rs.sized_bytes import bytes32 from chia_rs.sized_ints import uint32, uint64 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._tests.cmds.wallet.test_consts import FINGERPRINT, FINGERPRINT_ARG, STD_TX, STD_UTX, get_bytes32 from chia.util.bech32m import encode_puzzle_hash -from chia.wallet.conditions import ConditionValidTimes +from chia.wallet.conditions import Condition, ConditionValidTimes from chia.wallet.notification_store import Notification -from chia.wallet.transaction_record import TransactionRecord -from chia.wallet.wallet_request_types import DeleteNotifications, GetNotifications, GetNotificationsResponse +from chia.wallet.util.tx_config import TXConfig +from chia.wallet.wallet_request_types import ( + DeleteNotifications, + GetNotifications, + GetNotificationsResponse, + SendNotification, + SendNotificationResponse, +) test_condition_valid_times: ConditionValidTimes = ConditionValidTimes(min_time=uint64(100), max_time=uint64(150)) @@ -26,20 +31,17 @@ def test_notifications_send(capsys: object, get_test_cli_clients: tuple[TestRpcC class NotificationsSendRpcClient(TestWalletRpcClient): async def send_notification( self, - target: bytes32, - msg: bytes, - amount: uint64, - fee: uint64 = uint64(0), - push: bool = True, + request: SendNotification, + tx_config: TXConfig, + extra_conditions: tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), - ) -> TransactionRecord: - self.add_to_log("send_notification", (target, msg, amount, fee, push, timelock_info)) - - class FakeTransactionRecord: - def __init__(self, name: str) -> None: - self.name = name + ) -> SendNotificationResponse: + self.add_to_log( + "send_notification", + (request.target, request.message, request.amount, request.fee, request.push, timelock_info), + ) - return cast(TransactionRecord, FakeTransactionRecord(get_bytes32(2).hex())) + return SendNotificationResponse([STD_UTX], [STD_TX], tx=STD_TX) inst_rpc_client = NotificationsSendRpcClient() test_rpc_clients.wallet_rpc_client = inst_rpc_client diff --git a/chia/_tests/core/mempool/test_mempool_manager.py b/chia/_tests/core/mempool/test_mempool_manager.py index 24d92fe0efcb..61d224333ce5 100644 --- a/chia/_tests/core/mempool/test_mempool_manager.py +++ b/chia/_tests/core/mempool/test_mempool_manager.py @@ -579,7 +579,6 @@ def make_bundle_spends_map_and_fee( bundle_coin_spends[coin_id] = BundleCoinSpend( coin_spend=coin_spend, eligible_for_dedup=bool(spend_conds.flags & ELIGIBLE_FOR_DEDUP), - eligible_for_fast_forward=bool(spend_conds.flags & ELIGIBLE_FOR_FF), additions=additions, cost=uint64(spend_conds.condition_cost + spend_conds.execution_cost), latest_singleton_lineage=UnspentLineageInfo(coin_id, coin_spend.coin.parent_coin_info, bytes32([0] * 32)) @@ -890,9 +889,13 @@ def mk_bcs(coin_spend: CoinSpend, flags: int = 0) -> BundleCoinSpend: return BundleCoinSpend( coin_spend=coin_spend, eligible_for_dedup=bool(flags & ELIGIBLE_FOR_DEDUP), - eligible_for_fast_forward=bool(flags & ELIGIBLE_FOR_FF), additions=[], cost=uint64(0), + latest_singleton_lineage=UnspentLineageInfo( + coin_spend.coin.name(), coin_spend.coin.parent_coin_info, bytes32([0] * 32) + ) + if flags & ELIGIBLE_FOR_FF + else None, ) @@ -1119,8 +1122,7 @@ def make_test_coins() -> list[Coin]: ], ) def test_can_replace(existing_items: list[MempoolItem], new_item: MempoolItem, expected: bool) -> None: - removals = {c.name() for c in new_item.spend_bundle.removals()} - assert can_replace(existing_items, removals, new_item) == expected + assert can_replace(existing_items, new_item) == expected @pytest.mark.anyio @@ -1802,16 +1804,16 @@ async def test_bundle_coin_spends() -> None: assert mi123e.bundle_coin_spends[coins[i].name()] == BundleCoinSpend( coin_spend=sb123.coin_spends[i], eligible_for_dedup=False, - eligible_for_fast_forward=False, additions=[Coin(coins[i].name(), IDENTITY_PUZZLE_HASH, coins[i].amount)], cost=uint64(ConditionCost.CREATE_COIN.value + ConditionCost.AGG_SIG.value + execution_cost), + latest_singleton_lineage=None, ) assert mi123e.bundle_coin_spends[coins[3].name()] == BundleCoinSpend( coin_spend=eligible_sb.coin_spends[0], eligible_for_dedup=True, - eligible_for_fast_forward=False, additions=[Coin(coins[3].name(), IDENTITY_PUZZLE_HASH, coins[3].amount)], cost=uint64(ConditionCost.CREATE_COIN.value + execution_cost), + latest_singleton_lineage=None, ) @@ -2463,7 +2465,7 @@ async def test_new_peak_ff_eviction( item = mempool_manager.get_mempool_item(bundle.name()) assert item is not None singleton_name = singleton_spend.coin.name() - assert item.bundle_coin_spends[singleton_name].eligible_for_fast_forward + assert item.bundle_coin_spends[singleton_name].supports_fast_forward latest_singleton_lineage = item.bundle_coin_spends[singleton_name].latest_singleton_lineage assert latest_singleton_lineage is not None assert latest_singleton_lineage.coin_id == singleton_name @@ -2495,7 +2497,7 @@ async def test_new_peak_ff_eviction( else: item = mempool_manager.get_mempool_item(bundle.name()) assert item is not None - assert item.bundle_coin_spends[singleton_spend.coin.name()].eligible_for_fast_forward + assert item.bundle_coin_spends[singleton_spend.coin.name()].supports_fast_forward latest_singleton_lineage = item.bundle_coin_spends[singleton_spend.coin.name()].latest_singleton_lineage assert latest_singleton_lineage is not None assert latest_singleton_lineage.coin_id == singleton_spend.coin.name() @@ -2552,9 +2554,9 @@ async def test_multiple_ff(use_optimization: bool) -> None: item = mempool_manager.get_mempool_item(bundle.name()) assert item is not None - assert item.bundle_coin_spends[singleton_spend1.coin.name()].eligible_for_fast_forward - assert item.bundle_coin_spends[singleton_spend2.coin.name()].eligible_for_fast_forward - assert not item.bundle_coin_spends[coin_spend.coin.name()].eligible_for_fast_forward + assert item.bundle_coin_spends[singleton_spend1.coin.name()].supports_fast_forward + assert item.bundle_coin_spends[singleton_spend2.coin.name()].supports_fast_forward + assert not item.bundle_coin_spends[coin_spend.coin.name()].supports_fast_forward # spend the singleton coin2 and make coin3 the latest version coins.update_lineage(singleton_ph, singleton_spend3.coin) @@ -2616,7 +2618,7 @@ async def test_advancing_ff(use_optimization: bool) -> None: item = mempool_manager.get_mempool_item(bundle.name()) assert item is not None spend = item.bundle_coin_spends[spend_a.coin.name()] - assert spend.eligible_for_fast_forward + assert spend.supports_fast_forward assert spend.latest_singleton_lineage is not None assert spend.latest_singleton_lineage.coin_id == spend_a.coin.name() @@ -2628,7 +2630,7 @@ async def test_advancing_ff(use_optimization: bool) -> None: item = mempool_manager.get_mempool_item(bundle.name()) assert item is not None spend = item.bundle_coin_spends[spend_a.coin.name()] - assert spend.eligible_for_fast_forward + assert spend.supports_fast_forward assert spend.latest_singleton_lineage is not None assert spend.latest_singleton_lineage.coin_id == spend_b.coin.name() @@ -2640,7 +2642,7 @@ async def test_advancing_ff(use_optimization: bool) -> None: item = mempool_manager.get_mempool_item(bundle.name()) assert item is not None spend = item.bundle_coin_spends[spend_a.coin.name()] - assert spend.eligible_for_fast_forward + assert spend.supports_fast_forward assert spend.latest_singleton_lineage is not None assert spend.latest_singleton_lineage.coin_id == spend_c.coin.name() diff --git a/chia/_tests/core/mempool/test_singleton_fast_forward.py b/chia/_tests/core/mempool/test_singleton_fast_forward.py index 3d6a01a38b6a..183f1b214422 100644 --- a/chia/_tests/core/mempool/test_singleton_fast_forward.py +++ b/chia/_tests/core/mempool/test_singleton_fast_forward.py @@ -52,7 +52,7 @@ def test_process_fast_forward_spends_nothing_to_do() -> None: sb = spend_bundle_from_conditions(conditions, TEST_COIN, sig) item = mempool_item_from_spendbundle(sb) # This coin is not eligible for fast forward - assert item.bundle_coin_spends[TEST_COIN_ID].eligible_for_fast_forward is False + assert not item.bundle_coin_spends[TEST_COIN_ID].supports_fast_forward internal_mempool_item = InternalMempoolItem(sb, item.conds, item.height_added_to_mempool, item.bundle_coin_spends) original_version = dataclasses.replace(internal_mempool_item) singleton_ff = SingletonFastForward() @@ -63,28 +63,6 @@ def test_process_fast_forward_spends_nothing_to_do() -> None: assert bundle_coin_spends == original_version.bundle_coin_spends -def test_process_fast_forward_spends_unknown_ff() -> None: - """ - This tests the case when we process for the first time but we are unable - to lookup the latest version from the item's latest singleton lineage - """ - test_coin = Coin(TEST_COIN_ID, IDENTITY_PUZZLE_HASH, uint64(1)) - conditions = [[ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, 1]] - sb = spend_bundle_from_conditions(conditions, test_coin) - item = mempool_item_from_spendbundle(sb) - # The coin is eligible for fast forward - assert item.bundle_coin_spends[test_coin.name()].eligible_for_fast_forward is True - item.bundle_coin_spends[test_coin.name()].latest_singleton_lineage = None - internal_mempool_item = InternalMempoolItem(sb, item.conds, item.height_added_to_mempool, item.bundle_coin_spends) - singleton_ff = SingletonFastForward() - # We have no fast forward records yet, so we'll process this coin for the - # first time here, but the item's latest singleton lineage returns None - with pytest.raises(ValueError, match="Cannot proceed with singleton spend fast forward"): - singleton_ff.process_fast_forward_spends( - mempool_item=internal_mempool_item, height=TEST_HEIGHT, constants=DEFAULT_CONSTANTS - ) - - def test_process_fast_forward_spends_latest_unspent() -> None: """ This tests the case when we are the latest singleton version already, so @@ -103,7 +81,7 @@ def test_process_fast_forward_spends_latest_unspent() -> None: conditions = [[ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, test_amount]] sb = spend_bundle_from_conditions(conditions, test_coin) item = mempool_item_from_spendbundle(sb) - assert item.bundle_coin_spends[test_coin.name()].eligible_for_fast_forward is True + assert item.bundle_coin_spends[test_coin.name()].supports_fast_forward item.bundle_coin_spends[test_coin.name()].latest_singleton_lineage = test_unspent_lineage_info internal_mempool_item = InternalMempoolItem(sb, item.conds, item.height_added_to_mempool, item.bundle_coin_spends) original_version = dataclasses.replace(internal_mempool_item) @@ -168,12 +146,12 @@ def test_perform_the_fast_forward() -> None: "517b0dadb0c310ded24dd86dff8205398080ff808080" ) test_coin_spend = CoinSpend(test_coin, test_puzzle_reveal, test_solution) - test_spend_data = BundleCoinSpend(test_coin_spend, False, True, [test_child_coin], uint64(0)) test_unspent_lineage_info = UnspentLineageInfo( coin_id=latest_unspent_coin.name(), parent_id=latest_unspent_coin.parent_coin_info, parent_parent_id=test_child_coin.parent_coin_info, ) + test_spend_data = BundleCoinSpend(test_coin_spend, False, [test_child_coin], uint64(0), test_unspent_lineage_info) # Start from a fresh state of fast forward spends fast_forward_spends: dict[bytes32, UnspentLineageInfo] = {} # Perform the fast forward on the test coin (the grandparent) diff --git a/chia/_tests/core/util/test_lru_cache.py b/chia/_tests/core/util/test_lru_cache.py index 905529cc23b4..fb68f585ce6d 100644 --- a/chia/_tests/core/util/test_lru_cache.py +++ b/chia/_tests/core/util/test_lru_cache.py @@ -2,6 +2,8 @@ import unittest +import pytest + from chia.util.lru_cache import LRUCache @@ -54,3 +56,17 @@ def test_lru_cache(self): assert len(cache.cache) == 5 assert cache.get(b"0") is None assert cache.get(b"1") == 1 + + +@pytest.mark.parametrize(argnames="capacity", argvalues=[-10, -1, 0]) +def test_with_zero_capacity(capacity: int) -> None: + cache: LRUCache[bytes, int] = LRUCache(capacity=capacity) + cache.put(b"0", 1) + assert cache.get(b"0") is None + assert len(cache.cache) == 0 + + +@pytest.mark.parametrize(argnames="capacity", argvalues=[-10, -1, 0, 1, 5, 10]) +def test_get_capacity(capacity: int) -> None: + cache: LRUCache[object, object] = LRUCache(capacity=capacity) + assert cache.get_capacity() == capacity diff --git a/chia/_tests/wallet/rpc/test_wallet_rpc.py b/chia/_tests/wallet/rpc/test_wallet_rpc.py index 8c5082d3b5df..779ff8a3ed88 100644 --- a/chia/_tests/wallet/rpc/test_wallet_rpc.py +++ b/chia/_tests/wallet/rpc/test_wallet_rpc.py @@ -144,6 +144,7 @@ PushTX, RoyaltyAsset, SelectCoins, + SendNotification, SendTransaction, SetWalletResyncOnStartup, SpendClawbackCoins, @@ -2621,21 +2622,25 @@ async def test_notification_rpcs(wallet_rpc_environment: WalletRpcTestEnvironmen env.wallet_2.node.config["enable_notifications"] = True env.wallet_2.node.config["required_notification_amount"] = 100000000000 async with wallet_2.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope: - tx = await client.send_notification( - await action_scope.get_puzzle_hash(wallet_2.wallet_state_manager), - b"hello", - uint64(100000000000), - fee=uint64(100000000000), + response = await client.send_notification( + SendNotification( + target=(await action_scope.get_puzzle_hash(wallet_2.wallet_state_manager)), + message=b"hello", + amount=uint64(100000000000), + fee=uint64(100000000000), + push=True, + ), + tx_config=DEFAULT_TX_CONFIG, ) - assert tx.spend_bundle is not None + assert response.tx.spend_bundle is not None await time_out_assert( 5, full_node_api.full_node.mempool_manager.get_spendbundle, - tx.spend_bundle, - tx.spend_bundle.name(), + response.tx.spend_bundle, + response.tx.spend_bundle.name(), ) - await farm_transaction(full_node_api, wallet_node, tx.spend_bundle) + await farm_transaction(full_node_api, wallet_node, response.tx.spend_bundle) await time_out_assert(20, env.wallet_2.wallet.get_confirmed_balance, uint64(100000000000)) notification = (await client_2.get_notifications(GetNotifications())).notifications[0] @@ -2648,21 +2653,25 @@ async def test_notification_rpcs(wallet_rpc_environment: WalletRpcTestEnvironmen assert [] == (await client_2.get_notifications(GetNotifications([notification.id]))).notifications async with wallet_2.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope: - tx = await client.send_notification( - await action_scope.get_puzzle_hash(wallet_2.wallet_state_manager), - b"hello", - uint64(100000000000), - fee=uint64(100000000000), + response = await client.send_notification( + SendNotification( + target=(await action_scope.get_puzzle_hash(wallet_2.wallet_state_manager)), + message=b"hello", + amount=uint64(100000000000), + fee=uint64(100000000000), + push=True, + ), + tx_config=DEFAULT_TX_CONFIG, ) - assert tx.spend_bundle is not None + assert response.tx.spend_bundle is not None await time_out_assert( 5, full_node_api.full_node.mempool_manager.get_spendbundle, - tx.spend_bundle, - tx.spend_bundle.name(), + response.tx.spend_bundle, + response.tx.spend_bundle.name(), ) - await farm_transaction(full_node_api, wallet_node, tx.spend_bundle) + await farm_transaction(full_node_api, wallet_node, response.tx.spend_bundle) await time_out_assert(20, env.wallet_2.wallet.get_confirmed_balance, uint64(200000000000)) notification = (await client_2.get_notifications(GetNotifications())).notifications[0] diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index f111500df6e3..373f960c3961 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -74,6 +74,7 @@ NFTSetNFTDID, NFTTransferNFT, RoyaltyAsset, + SendNotification, SendTransaction, SendTransactionResponse, SignMessageByAddress, @@ -1576,19 +1577,25 @@ async def send_notification( async with get_wallet_client(root_path, wallet_rpc_port, fp) as (wallet_client, fingerprint, _): amount: uint64 = cli_amount.convert_amount(units["chia"]) - tx = await wallet_client.send_notification( - address.puzzle_hash, - message, - amount, - fee, - push=push, + response = await wallet_client.send_notification( + SendNotification( + address.puzzle_hash, + message, + amount, + fee=fee, + push=push, + ), + tx_config=DEFAULT_TX_CONFIG, timelock_info=condition_valid_times, ) if push: print("Notification sent successfully.") - print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx.name}") - return [tx] + print( + "To get status, use command: chia wallet get_transaction" + f" -f {fingerprint} -tx 0x{response.transactions[0].name}" + ) + return response.transactions async def get_notifications( diff --git a/chia/full_node/eligible_coin_spends.py b/chia/full_node/eligible_coin_spends.py index 559b682a2514..e67705dcc5f6 100644 --- a/chia/full_node/eligible_coin_spends.py +++ b/chia/full_node/eligible_coin_spends.py @@ -196,7 +196,7 @@ def process_fast_forward_spends( new_bundle_coin_spends = {} fast_forwarded_spends = 0 for coin_id, spend_data in mempool_item.bundle_coin_spends.items(): - if not spend_data.eligible_for_fast_forward: + if not spend_data.supports_fast_forward: # Nothing to do for this spend, moving on new_coin_spends.append(spend_data.coin_spend) new_bundle_coin_spends[coin_id] = spend_data @@ -210,8 +210,7 @@ def process_fast_forward_spends( unspent_lineage_info = self.fast_forward_spends.get(spend_data.coin_spend.coin.puzzle_hash) if unspent_lineage_info is None: # We didn't, so let's check the item's latest lineage info - if spend_data.latest_singleton_lineage is None: - raise ValueError("Cannot proceed with singleton spend fast forward.") + assert spend_data.latest_singleton_lineage is not None unspent_lineage_info = spend_data.latest_singleton_lineage # See if we're the most recent version if unspent_lineage_info.coin_id == coin_id: @@ -235,9 +234,9 @@ def process_fast_forward_spends( new_bundle_coin_spends[new_coin_spend.coin.name()] = BundleCoinSpend( coin_spend=new_coin_spend, eligible_for_dedup=spend_data.eligible_for_dedup, - eligible_for_fast_forward=spend_data.eligible_for_fast_forward, additions=patched_additions, cost=spend_data.cost, + latest_singleton_lineage=self.fast_forward_spends.get(spend_data.coin_spend.coin.puzzle_hash), ) # Update the list of coins spends that will make the new fast # forward spend bundle @@ -258,9 +257,9 @@ def process_fast_forward_spends( new_bundle_coin_spends[new_coin_spend.coin.name()] = BundleCoinSpend( coin_spend=new_coin_spend, eligible_for_dedup=spend_data.eligible_for_dedup, - eligible_for_fast_forward=spend_data.eligible_for_fast_forward, additions=patched_additions, cost=spend_data.cost, + latest_singleton_lineage=self.fast_forward_spends.get(spend_data.coin_spend.coin.puzzle_hash), ) # Update the list of coins spends that make the new fast forward bundle new_coin_spends.append(new_coin_spend) diff --git a/chia/full_node/mempool.py b/chia/full_node/mempool.py index ade1ee17ca49..f175c3c3c106 100644 --- a/chia/full_node/mempool.py +++ b/chia/full_node/mempool.py @@ -611,7 +611,7 @@ def create_bundle_from_mempool_items( # might fit, but we also want to avoid spending too much # time on potentially expensive ones, hence this shortcut. if any( - sd.eligible_for_dedup or sd.eligible_for_fast_forward for sd in item.bundle_coin_spends.values() + sd.eligible_for_dedup or sd.supports_fast_forward for sd in item.bundle_coin_spends.values() ): log.info(f"Skipping transaction with dedup or FF spends {name}") continue diff --git a/chia/full_node/mempool_manager.py b/chia/full_node/mempool_manager.py index 034afe4b0a13..6739062a18ff 100644 --- a/chia/full_node/mempool_manager.py +++ b/chia/full_node/mempool_manager.py @@ -243,7 +243,7 @@ def check_removals( conflicts = set() for coin_id, coin_bcs in bundle_coin_spends.items(): # 1. Checks if it's been spent already - if removals[coin_id].spent and not coin_bcs.eligible_for_fast_forward: + if removals[coin_id].spent and not coin_bcs.supports_fast_forward: return Err.DOUBLE_SPEND, [] # 2. Checks if there's a mempool conflict @@ -269,12 +269,12 @@ def check_removals( return Err.INVALID_SPEND_BUNDLE, [] # if the spend we're adding to the mempool is not DEDUP nor FF, it's # just a regular conflict - if not coin_bcs.eligible_for_fast_forward and not coin_bcs.eligible_for_dedup: + if not coin_bcs.supports_fast_forward and not coin_bcs.eligible_for_dedup: conflicts.add(item) # if the spend we're adding is FF, but there's a conflicting spend # that isn't FF, they can't be chained, so that's a conflict - elif coin_bcs.eligible_for_fast_forward and not conflict_bcs.eligible_for_fast_forward: + elif coin_bcs.supports_fast_forward and not conflict_bcs.supports_fast_forward: conflicts.add(item) # if the spend we're adding is DEDUP, but there's a conflicting spend @@ -611,8 +611,7 @@ async def validate_spend_bundle( return Err.INVALID_COIN_SOLUTION, None, [] lineage_info = None - eligible_for_ff = bool(spend_conds.flags & ELIGIBLE_FOR_FF) and supports_fast_forward(coin_spend) - if eligible_for_ff: + if bool(spend_conds.flags & ELIGIBLE_FOR_FF) and supports_fast_forward(coin_spend): # Make sure the fast forward spend still has a version that is # still unspent, because if the singleton has been spent in a # non-FF spend, this fast forward spend will never become valid. @@ -622,8 +621,6 @@ async def validate_spend_bundle( # spent_index will also fail this test, and such spends will # fall back to be treated as non-FF spends. lineage_info = await get_unspent_lineage_info_for_puzzle_hash(spend_conds.puzzle_hash) - if lineage_info is None: - eligible_for_ff = False spend_additions = [] for puzzle_hash, amount, _ in spend_conds.create_coin: @@ -635,7 +632,6 @@ async def validate_spend_bundle( bundle_coin_spends[coin_id] = BundleCoinSpend( coin_spend=coin_spend, eligible_for_dedup=bool(spend_conds.flags & ELIGIBLE_FOR_DEDUP), - eligible_for_fast_forward=eligible_for_ff, additions=spend_additions, cost=uint64(spend_conds.condition_cost + spend_conds.execution_cost), latest_singleton_lineage=lineage_info, @@ -644,7 +640,7 @@ async def validate_spend_bundle( # fast forward spends are only allowed when bundled with other, non-FF, spends # in order to evict an FF spend, it must be associated with a normal # spend that can be included in a block or invalidated some other way - if all([s.eligible_for_fast_forward for s in bundle_coin_spends.values()]): + if all([s.supports_fast_forward for s in bundle_coin_spends.values()]): return Err.INVALID_SPEND_BUNDLE, None, [] removal_record_dict: dict[bytes32, CoinRecord] = {} @@ -764,7 +760,7 @@ async def validate_spend_bundle( if fail_reason is Err.MEMPOOL_CONFLICT: log.debug(f"Replace attempted. number of MempoolItems: {len(conflicts)}") - if not can_replace(conflicts, removal_names, potential): + if not can_replace(conflicts, potential): return Err.MEMPOOL_CONFLICT, potential, [] duration = time.monotonic() - start_time @@ -1024,17 +1020,12 @@ def optional_max(a: Optional[T], b: Optional[T]) -> Optional[T]: return max((v for v in [a, b] if v is not None), default=None) -def can_replace( - conflicting_items: list[MempoolItem], - removal_names: set[bytes32], - new_item: MempoolItem, -) -> bool: +def can_replace(conflicting_items: list[MempoolItem], new_item: MempoolItem) -> bool: """ This function implements the mempool replacement rules. Given a Mempool item we're attempting to insert into the mempool (new_item) and the set of existing mempool items that conflict with it, this function answers the question whether - the existing items can be replaced by the new one. The removals parameter are - the coin IDs the new mempool item is spending. + the existing items can be replaced by the new one. """ conflicting_fees = 0 @@ -1059,10 +1050,10 @@ def can_replace( # fee than AB therefore kicking out A altogether. The better way to solve this would be to keep a cache # of booted transactions like A, and retry them after they get removed from mempool due to a conflict. for coin_id, bcs in item.bundle_coin_spends.items(): - if coin_id not in removal_names: + if coin_id not in new_item.bundle_coin_spends: log.debug("Rejecting conflicting tx as it does not spend conflicting coin %s", coin_id) return False - if bcs.eligible_for_fast_forward: + if bcs.supports_fast_forward: existing_ff_spends.add(bytes32(coin_id)) if bcs.eligible_for_dedup: existing_dedup_spends.add(bytes32(coin_id)) @@ -1113,7 +1104,7 @@ def can_replace( if len(existing_ff_spends) > 0 or len(existing_dedup_spends) > 0: for coin_id, bcs in new_item.bundle_coin_spends.items(): - if not bcs.eligible_for_fast_forward and coin_id in existing_ff_spends: + if not bcs.supports_fast_forward and coin_id in existing_ff_spends: log.debug("Rejecting conflicting tx due to changing ELIGIBLE_FOR_FF of coin spend %s", coin_id) return False diff --git a/chia/types/mempool_item.py b/chia/types/mempool_item.py index 76f233c592c5..1a796d9aaaea 100644 --- a/chia/types/mempool_item.py +++ b/chia/types/mempool_item.py @@ -22,7 +22,6 @@ class UnspentLineageInfo: class BundleCoinSpend: coin_spend: CoinSpend eligible_for_dedup: bool - eligible_for_fast_forward: bool additions: list[Coin] # cost on the specific solution in this item. The cost includes execution # cost and conditions cost, not byte-cost. @@ -32,7 +31,11 @@ class BundleCoinSpend: # current unspent lineage belonging to this singleton, that we would rebase # this spend on top of if we were to make a block now # When finding MempoolItems by coin ID, we use Coin ID from it if it's set - latest_singleton_lineage: Optional[UnspentLineageInfo] = None + latest_singleton_lineage: Optional[UnspentLineageInfo] + + @property + def supports_fast_forward(self) -> bool: + return self.latest_singleton_lineage is not None @dataclass(frozen=True) diff --git a/chia/wallet/wallet_request_types.py b/chia/wallet/wallet_request_types.py index d40fcfc8df75..326a9fa11d0d 100644 --- a/chia/wallet/wallet_request_types.py +++ b/chia/wallet/wallet_request_types.py @@ -1234,6 +1234,20 @@ class SpendClawbackCoinsResponse(TransactionEndpointResponse): transaction_ids: list[bytes32] +@streamable +@dataclass(frozen=True) +class SendNotification(TransactionEndpointRequest): + target: bytes32 = field(default_factory=default_raise) + message: bytes = field(default_factory=default_raise) + amount: uint64 = uint64(0) + + +@streamable +@dataclass(frozen=True) +class SendNotificationResponse(TransactionEndpointResponse): + tx: TransactionRecord + + @streamable @dataclass(frozen=True) class PushTransactions(TransactionEndpointRequest): diff --git a/chia/wallet/wallet_rpc_api.py b/chia/wallet/wallet_rpc_api.py index 0d4a97610ff2..aaacbaba29ae 100644 --- a/chia/wallet/wallet_rpc_api.py +++ b/chia/wallet/wallet_rpc_api.py @@ -248,6 +248,8 @@ PWStatusResponse, SelectCoins, SelectCoinsResponse, + SendNotification, + SendNotificationResponse, SendTransaction, SendTransactionResponse, SetWalletResyncOnStartup, @@ -1937,22 +1939,24 @@ async def delete_notifications(self, request: DeleteNotifications) -> Empty: return Empty() @tx_endpoint(push=True) + @marshal async def send_notification( self, - request: dict[str, Any], + request: SendNotification, action_scope: WalletActionScope, extra_conditions: tuple[Condition, ...] = tuple(), - ) -> EndpointResult: + ) -> SendNotificationResponse: await self.service.wallet_state_manager.notification_manager.send_new_notification( - bytes32.from_hexstr(request["target"]), - bytes.fromhex(request["message"]), - uint64(request["amount"]), + request.target, + request.message, + request.amount, action_scope, - request.get("fee", uint64(0)), + request.fee, extra_conditions=extra_conditions, ) - return {"tx": None, "transactions": None} # tx_endpoint wrapper will take care of this + # tx_endpoint will take care of these default values + return SendNotificationResponse([], [], tx=REPLACEABLE_TRANSACTION_RECORD) @marshal async def verify_signature(self, request: VerifySignature) -> VerifySignatureResponse: diff --git a/chia/wallet/wallet_rpc_client.py b/chia/wallet/wallet_rpc_client.py index 14d2a2fcd515..75a578fcae3b 100644 --- a/chia/wallet/wallet_rpc_client.py +++ b/chia/wallet/wallet_rpc_client.py @@ -162,6 +162,8 @@ PWStatusResponse, SelectCoins, SelectCoinsResponse, + SendNotification, + SendNotificationResponse, SendTransaction, SendTransactionMultiResponse, SendTransactionResponse, @@ -1088,27 +1090,16 @@ async def delete_notifications(self, request: DeleteNotifications) -> None: async def send_notification( self, - target: bytes32, - msg: bytes, - amount: uint64, - fee: uint64 = uint64(0), + request: SendNotification, + tx_config: TXConfig, extra_conditions: tuple[Condition, ...] = tuple(), timelock_info: ConditionValidTimes = ConditionValidTimes(), - push: bool = True, - ) -> TransactionRecord: - response = await self.fetch( - "send_notification", - { - "target": target.hex(), - "message": msg.hex(), - "amount": amount, - "fee": fee, - "extra_conditions": conditions_to_json_dicts(extra_conditions), - "push": push, - **timelock_info.to_json_dict(), - }, + ) -> SendNotificationResponse: + return SendNotificationResponse.from_json_dict( + await self.fetch( + "send_notification", request.json_serialize_for_transport(tx_config, extra_conditions, timelock_info) + ) ) - return TransactionRecord.from_json_dict(response["tx"]) async def sign_message_by_address(self, request: SignMessageByAddress) -> SignMessageByAddressResponse: return SignMessageByAddressResponse.from_json_dict( diff --git a/poetry.lock b/poetry.lock index d8106e536089..8d568bb23e73 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1011,19 +1011,19 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "clvm" -version = "0.9.14" +version = "0.9.15" description = "[Contract Language | Chialisp] Virtual Machine" optional = false python-versions = "<4,>=3.8.1" groups = ["main"] files = [ - {file = "clvm-0.9.14-py3-none-any.whl", hash = "sha256:a95bb9fa9c331ced3fab934c5d04825afd0e1872bb0676777cb536d5c2bef970"}, - {file = "clvm-0.9.14.tar.gz", hash = "sha256:4060beb0bd6f0f22cf75298503eb58aca45aa0d66f047531d382af5c22d59da7"}, + {file = "clvm-0.9.15-py3-none-any.whl", hash = "sha256:3f4b37ce496bf08beeb95f727352e536068c77310977f3b2f815985fcb232b3c"}, + {file = "clvm-0.9.15.tar.gz", hash = "sha256:e6551d5be734b46aeac86dc84b16c31290ae96126873dacfc3a05b939dea703d"}, ] [package.dependencies] chia_rs = ">=0.2.13" -importlib_metadata = ">=7.1,<8.0" +importlib_metadata = ">=8.7,<9.0" typing-extensions = ">=4.0,<5.0" [package.extras] @@ -1574,23 +1574,27 @@ files = [ [[package]] name = "importlib-metadata" -version = "7.2.1" +version = "8.7.0" description = "Read metadata from Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "importlib_metadata-7.2.1-py3-none-any.whl", hash = "sha256:ffef94b0b66046dd8ea2d619b701fe978d9264d38f3998bc4c27ec3b146a87c8"}, - {file = "importlib_metadata-7.2.1.tar.gz", hash = "sha256:509ecb2ab77071db5137c655e24ceb3eee66e7bbc6574165d0d114d9fc4bbe68"}, + {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, + {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, ] [package.dependencies] -zipp = ">=0.5" +zipp = ">=3.20" [package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] +test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] [[package]] name = "importlib-resources" @@ -3680,19 +3684,23 @@ propcache = ">=0.2.0" [[package]] name = "zipp" -version = "3.19.1" +version = "3.23.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "zipp-3.19.1-py3-none-any.whl", hash = "sha256:2828e64edb5386ea6a52e7ba7cdb17bb30a73a858f5eb6eb93d8d36f5ea26091"}, - {file = "zipp-3.19.1.tar.gz", hash = "sha256:35427f6d5594f4acf82d25541438348c26736fa9b3afa2754bcd63cdb99d8e8f"}, + {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, + {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, ] [package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] [[package]] name = "zstd"