Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Fixed an issue where cancelling NFT offer did not cancel other offers… #19129

Merged
merged 1 commit into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion chia/rpc/wallet_rpc_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2545,7 +2545,7 @@ async def cancel_offer(
fee: uint64 = uint64(request.get("fee", 0))
async with self.service.wallet_state_manager.lock:
await wsm.trade_manager.cancel_pending_offers(
[bytes32(trade_id)], action_scope, fee=fee, secure=secure, extra_conditions=extra_conditions
[trade_id], action_scope, fee=fee, secure=secure, extra_conditions=extra_conditions
)

return {"transactions": None} # tx_endpoint wrapper will take care of this
Expand Down
130 changes: 70 additions & 60 deletions chia/wallet/trade_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,15 @@ async def get_coins_of_interest(
)
return coin_ids

async def get_trade_by_coin(self, coin: Coin) -> Optional[TradeRecord]:
async def get_trades_by_coin(self, coin: Coin) -> list[TradeRecord]:
all_trades = await self.get_all_trades()
trades_by_coin = []
for trade in all_trades:
if trade.status == TradeStatus.CANCELLED.value:
continue
if coin in trade.coins_of_interest:
return trade
return None
trades_by_coin.append(trade)
return trades_by_coin

async def coins_of_interest_farmed(
self, coin_state: CoinState, fork_height: Optional[uint32], peer: WSChiaConnection
Expand All @@ -151,62 +152,63 @@ async def coins_of_interest_farmed(
If our coins got farmed but coins from other side didn't, we successfully canceled trade by spending inputs.
"""
self.log.info(f"coins_of_interest_farmed: {coin_state}")
trade = await self.get_trade_by_coin(coin_state.coin)
if trade is None:
self.log.error(f"Coin: {coin_state.coin}, not in any trade")
return
if coin_state.spent_height is None:
self.log.error(f"Coin: {coin_state.coin}, has not been spent so trade can remain valid")
# Then let's filter the offer into coins that WE offered
if (
self.most_recently_deserialized_trade is not None
and trade.trade_id == self.most_recently_deserialized_trade[0]
):
offer = self.most_recently_deserialized_trade[1]
else:
offer = Offer.from_bytes(trade.offer)
self.most_recently_deserialized_trade = (trade.trade_id, offer)
primary_coin_ids = [c.name() for c in offer.removals()]
# TODO: Add `WalletCoinStore.get_coins`.
result = await self.wallet_state_manager.coin_store.get_coin_records(
coin_id_filter=HashFilter.include(primary_coin_ids)
)
our_primary_coins: list[Coin] = [cr.coin for cr in result.records]
our_additions: list[Coin] = list(
filter(lambda c: offer.get_root_removal(c) in our_primary_coins, offer.additions())
)
our_addition_ids: list[bytes32] = [c.name() for c in our_additions]
trades = await self.get_trades_by_coin(coin_state.coin)
for trade in trades:
if trade is None:
self.log.error(f"Coin: {coin_state.coin}, not in any trade")
continue
if coin_state.spent_height is None:
self.log.error(f"Coin: {coin_state.coin}, has not been spent so trade can remain valid")
# Then let's filter the offer into coins that WE offered
if (
self.most_recently_deserialized_trade is not None
and trade.trade_id == self.most_recently_deserialized_trade[0]
):
offer = self.most_recently_deserialized_trade[1]
else:
offer = Offer.from_bytes(trade.offer)
self.most_recently_deserialized_trade = (trade.trade_id, offer)
primary_coin_ids = [c.name() for c in offer.removals()]
# TODO: Add `WalletCoinStore.get_coins`.
result = await self.wallet_state_manager.coin_store.get_coin_records(
coin_id_filter=HashFilter.include(primary_coin_ids)
)
our_primary_coins: list[Coin] = [cr.coin for cr in result.records]
our_additions: list[Coin] = list(
filter(lambda c: offer.get_root_removal(c) in our_primary_coins, offer.additions())
)
our_addition_ids: list[bytes32] = [c.name() for c in our_additions]

# And get all relevant coin states
coin_states = await self.wallet_state_manager.wallet_node.get_coin_state(
our_addition_ids,
peer=peer,
fork_height=fork_height,
)
assert coin_states is not None
coin_state_names: list[bytes32] = [cs.coin.name() for cs in coin_states]
# If any of our settlement_payments were spent, this offer was a success!
if set(our_addition_ids) == set(coin_state_names):
height = coin_state.spent_height
assert height is not None
await self.trade_store.set_status(trade.trade_id, TradeStatus.CONFIRMED, index=height)
tx_records: list[TransactionRecord] = await self.calculate_tx_records_for_offer(offer, False)
for tx in tx_records:
if TradeStatus(trade.status) == TradeStatus.PENDING_ACCEPT:
await self.wallet_state_manager.add_transaction(
dataclasses.replace(tx, confirmed_at_height=height, confirmed=True)
)
# And get all relevant coin states
coin_states = await self.wallet_state_manager.wallet_node.get_coin_state(
our_addition_ids,
peer=peer,
fork_height=fork_height,
)
assert coin_states is not None
coin_state_names: list[bytes32] = [cs.coin.name() for cs in coin_states]
# If any of our settlement_payments were spent, this offer was a success!
if set(our_addition_ids) == set(coin_state_names):
height = coin_state.spent_height
assert height is not None
await self.trade_store.set_status(trade.trade_id, TradeStatus.CONFIRMED, index=height)
tx_records: list[TransactionRecord] = await self.calculate_tx_records_for_offer(offer, False)
for tx in tx_records:
if TradeStatus(trade.status) == TradeStatus.PENDING_ACCEPT:
await self.wallet_state_manager.add_transaction(
dataclasses.replace(tx, confirmed_at_height=height, confirmed=True)
)

self.log.info(f"Trade with id: {trade.trade_id} confirmed at height: {height}")
else:
# In any other scenario this trade failed
await self.wallet_state_manager.delete_trade_transactions(trade.trade_id)
if trade.status == TradeStatus.PENDING_CANCEL.value:
await self.trade_store.set_status(trade.trade_id, TradeStatus.CANCELLED)
self.log.info(f"Trade with id: {trade.trade_id} canceled")
elif trade.status == TradeStatus.PENDING_CONFIRM.value:
await self.trade_store.set_status(trade.trade_id, TradeStatus.FAILED)
self.log.warning(f"Trade with id: {trade.trade_id} failed")
self.log.info(f"Trade with id: {trade.trade_id} confirmed at height: {height}")
else:
# In any other scenario this trade failed
await self.wallet_state_manager.delete_trade_transactions(trade.trade_id)
if trade.status == TradeStatus.PENDING_CANCEL.value:
await self.trade_store.set_status(trade.trade_id, TradeStatus.CANCELLED)
self.log.info(f"Trade with id: {trade.trade_id} canceled")
elif trade.status == TradeStatus.PENDING_CONFIRM.value:
await self.trade_store.set_status(trade.trade_id, TradeStatus.FAILED)
self.log.warning(f"Trade with id: {trade.trade_id} failed")

async def get_locked_coins(self) -> dict[bytes32, WalletCoinRecord]:
"""Returns a dictionary of confirmed coins that are locked by a trade."""
Expand Down Expand Up @@ -244,7 +246,7 @@ async def fail_pending_offer(self, trade_id: bytes32) -> None:

async def cancel_pending_offers(
self,
trades: list[bytes32],
trade_ids: list[bytes32],
action_scope: WalletActionScope,
fee: uint64 = uint64(0),
secure: bool = True, # Cancel with a transaction on chain
Expand All @@ -254,12 +256,12 @@ async def cancel_pending_offers(
"""This will create a transaction that includes coins that were offered"""

# Need to do some pre-figuring of announcements that will be need to be made
announcement_nonce: bytes32 = std_hash(b"".join(trades))
announcement_nonce: bytes32 = std_hash(b"".join(trade_ids))
trade_records: list[TradeRecord] = []
all_cancellation_coins: list[list[Coin]] = []
announcement_creations: deque[CreateCoinAnnouncement] = deque()
announcement_assertions: deque[AssertCoinAnnouncement] = deque()
for trade_id in trades:
for trade_id in trade_ids:
if trade_id in trade_cache:
trade = trade_cache[trade_id]
else:
Expand Down Expand Up @@ -294,6 +296,7 @@ async def cancel_pending_offers(

cancellation_additions: list[Coin] = []
valid_times: ConditionValidTimes = parse_timelock_info(extra_conditions)
trades_to_cancel: list[TradeRecord] = []
for coin in cancellation_coins:
wallet = await self.wallet_state_manager.get_wallet_for_coin(coin.name())

Expand Down Expand Up @@ -391,7 +394,14 @@ async def cancel_pending_offers(
)
all_txs.append(incoming_tx)

# The statuses of trades which offer cancellation coin needs to be set to `PENDING_CANCEL`
trades_to_cancel.extend(await self.get_trades_by_coin(coin))

await self.trade_store.set_status(trade.trade_id, TradeStatus.PENDING_CANCEL)
self.log.info(f"Cancelling trade: {trade.trade_id}")
for t in trades_to_cancel:
await self.trade_store.set_status(t.trade_id, TradeStatus.PENDING_CANCEL)
self.log.info(f"Cancelling trade: {t.trade_id} along with {trade.trade_id}")

if secure:
async with action_scope.use() as interface:
Expand Down
29 changes: 15 additions & 14 deletions chia/wallet/wallet_state_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2387,20 +2387,21 @@ async def remove_from_queue(
trade_coins_removed = set()
trades = []
for removed_coin in coins_removed:
trade = await self.trade_manager.get_trade_by_coin(removed_coin)
if trade is not None and trade.status in {
TradeStatus.PENDING_CONFIRM.value,
TradeStatus.PENDING_ACCEPT.value,
TradeStatus.PENDING_CANCEL.value,
}:
if trade not in trades:
trades.append(trade)
# offer was tied to these coins, lets subscribe to them to get a confirmation to
# cancel it if it's confirmed
# we send transactions to multiple peers, and in cases when mempool gets
# fragmented, it's safest to wait for confirmation from blockchain before setting
# offer to failed
trade_coins_removed.add(removed_coin.name())
trades_by_coin = await self.trade_manager.get_trades_by_coin(removed_coin)
for trade in trades_by_coin:
if trade is not None and trade.status in {
TradeStatus.PENDING_CONFIRM.value,
TradeStatus.PENDING_ACCEPT.value,
TradeStatus.PENDING_CANCEL.value,
}:
if trade not in trades:
trades.append(trade)
# offer was tied to these coins, lets subscribe to them to get a confirmation to
# cancel it if it's confirmed
# we send transactions to multiple peers, and in cases when mempool gets
# fragmented, it's safest to wait for confirmation from blockchain before setting
# offer to failed
trade_coins_removed.add(removed_coin.name())
if trades != [] and trade_coins_removed != set():
if not tx.is_valid():
# we've tried to send this transaction to a full node multiple times
Expand Down
Loading