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

Clawback resync #15496

Merged
merged 24 commits into from
Jul 11, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions chia/cmds/wallet_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from chia.wallet.util.address_type import AddressType, ensure_valid_address
from chia.wallet.util.puzzle_decorator_type import PuzzleDecoratorType
from chia.wallet.util.query_filter import HashFilter, TransactionTypeFilter
from chia.wallet.util.transaction_type import CLAWBACK_TRANSACTION_TYPES, TransactionType
from chia.wallet.util.transaction_type import CLAWBACK_INCOMING_TRANSACTION_TYPES, TransactionType
from chia.wallet.util.wallet_types import WalletType
from chia.wallet.vc_wallet.vc_store import VCProofs
from chia.wallet.wallet_coin_store import GetCoinRecords
Expand Down Expand Up @@ -177,7 +177,7 @@ async def get_transactions(
sort_key: SortKey,
reverse: bool,
clawback: bool,
) -> None:
) -> None: # pragma: no cover
async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
if wallet_client is None:
return
Expand Down Expand Up @@ -218,7 +218,7 @@ async def get_transactions(
if i + j + skipped >= len(txs):
break
coin_record: Optional[Dict[str, Any]] = None
if txs[i + j + skipped].type in CLAWBACK_TRANSACTION_TYPES:
if txs[i + j + skipped].type in CLAWBACK_INCOMING_TRANSACTION_TYPES:
coin_records = await wallet_client.get_coin_records(
GetCoinRecords(coin_id_filter=HashFilter.include([txs[i + j + skipped].additions[0].name()]))
)
Expand Down
4 changes: 2 additions & 2 deletions chia/rpc/wallet_rpc_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
from chia.wallet.util.compute_hints import compute_spend_hints_and_additions
from chia.wallet.util.compute_memos import compute_memos
from chia.wallet.util.query_filter import HashFilter, TransactionTypeFilter
from chia.wallet.util.transaction_type import CLAWBACK_TRANSACTION_TYPES, TransactionType
from chia.wallet.util.transaction_type import CLAWBACK_INCOMING_TRANSACTION_TYPES, TransactionType
from chia.wallet.util.wallet_sync_utils import fetch_coin_spend_for_coin_state
from chia.wallet.util.wallet_types import CoinType, WalletType
from chia.wallet.vc_wallet.vc_store import VCProofs
Expand Down Expand Up @@ -903,7 +903,7 @@ async def get_transactions(self, request: Dict) -> EndpointResult:
try:
tx = (await self._convert_tx_puzzle_hash(tr)).to_json_dict_convenience(self.service.config)
tx_list.append(tx)
if tx["type"] not in CLAWBACK_TRANSACTION_TYPES:
if tx["type"] not in CLAWBACK_INCOMING_TRANSACTION_TYPES:
continue
coin: Coin = tr.additions[0]
record: Optional[WalletCoinRecord] = await self.service.wallet_state_manager.coin_store.get_coin_record(
Expand Down
2 changes: 1 addition & 1 deletion chia/wallet/util/transaction_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class TransactionType(IntEnum):
OUTGOING_CLAWBACK = 8


CLAWBACK_TRANSACTION_TYPES = {
CLAWBACK_INCOMING_TRANSACTION_TYPES = {
xdustinface marked this conversation as resolved.
Show resolved Hide resolved
TransactionType.INCOMING_CLAWBACK_SEND.value,
TransactionType.INCOMING_CLAWBACK_RECEIVE.value,
}
56 changes: 46 additions & 10 deletions chia/wallet/wallet_state_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
from chia.wallet.util.compute_memos import compute_memos
from chia.wallet.util.puzzle_decorator import PuzzleDecoratorManager
from chia.wallet.util.query_filter import HashFilter
from chia.wallet.util.transaction_type import CLAWBACK_TRANSACTION_TYPES, TransactionType
from chia.wallet.util.transaction_type import CLAWBACK_INCOMING_TRANSACTION_TYPES, TransactionType
from chia.wallet.util.wallet_sync_utils import (
PeerRequestException,
fetch_coin_spend_for_coin_state,
Expand Down Expand Up @@ -761,15 +761,15 @@ async def spend_clawback_coins(self, clawback_coins: Dict[Coin, ClawbackMetadata
if self.main_wallet.secret_key_store.secret_key_for_public_key(derivation_record.pubkey) is None:
await self.main_wallet.hack_populate_secret_key_for_puzzle_hash(derivation_record.puzzle_hash)
amount = uint64(amount + coin.amount)
# Remove the clawback hint since it is unnecessary for the XCH coin
memos: List[bytes] = [] if len(incoming_tx.memos) == 0 else incoming_tx.memos[0][1][1:]
ytx1991 marked this conversation as resolved.
Show resolved Hide resolved
inner_puzzle: Program = self.main_wallet.puzzle_for_pk(derivation_record.pubkey)
inner_solution: Program = self.main_wallet.make_solution(
primaries=[
Payment(
derivation_record.puzzle_hash,
uint64(coin.amount),
[]
if len(incoming_tx.memos) == 0
else incoming_tx.memos[0][1], # Forward memo of the first coin
memos, # Forward memo of the first coin
)
],
coin_announcements=None if len(coin_spends) > 0 or fee == 0 else {message},
Expand Down Expand Up @@ -1156,11 +1156,42 @@ async def handle_clawback(
if is_recipient is not None:
spend_bundle = SpendBundle([coin_spend], G2Element())
memos = compute_memos(spend_bundle)
spent_height: uint32 = uint32(0)
if coin_state.spent_height is not None:
self.log.debug("Resync clawback coin: %s", coin_state.coin.name().hex())
xdustinface marked this conversation as resolved.
Show resolved Hide resolved
# Resync case
spent_height = uint32(coin_state.spent_height)
# Create Clawback outgoing transaction
created_timestamp = await self.wallet_node.get_timestamp_for_height(uint32(coin_state.spent_height))
clawback_coin_spend: CoinSpend = await fetch_coin_spend_for_coin_state(coin_state, peer)
clawback_spend_bundle: SpendBundle = SpendBundle([clawback_coin_spend], G2Element())
if await self.puzzle_store.puzzle_hash_exists(clawback_spend_bundle.additions()[0].puzzle_hash):
tx_record = TransactionRecord(
confirmed_at_height=uint32(coin_state.spent_height),
created_at_time=created_timestamp,
to_puzzle_hash=metadata.sender_puzzle_hash
if clawback_spend_bundle.additions()[0].puzzle_hash == metadata.sender_puzzle_hash
else metadata.recipient_puzzle_hash,
amount=uint64(coin_state.coin.amount),
fee_amount=uint64(0),
confirmed=True,
sent=uint32(0),
spend_bundle=clawback_spend_bundle,
additions=clawback_spend_bundle.additions(),
removals=clawback_spend_bundle.removals(),
wallet_id=uint32(1),
sent_to=[],
trade_id=None,
type=uint32(TransactionType.OUTGOING_CLAWBACK),
name=clawback_spend_bundle.name(),
memos=list(compute_memos(clawback_spend_bundle).items()),
)
await self.tx_store.add_transaction_record(tx_record)
coin_record = WalletCoinRecord(
coin_state.coin,
uint32(coin_state.created_height),
uint32(0),
False,
spent_height,
spent_height != 0,
False,
WalletType.STANDARD_WALLET,
1,
Expand All @@ -1170,15 +1201,16 @@ async def handle_clawback(
# Add merkle coin
await self.coin_store.add_coin_record(coin_record)
# Add tx record
# We use TransactionRecord.confirmed to indicate if a Clawback transaction is claimable
# If the Clawback coin is unspent, confirmed should be false
created_timestamp = await self.wallet_node.get_timestamp_for_height(uint32(coin_state.created_height))
spend_bundle = SpendBundle([coin_spend], G2Element())
xdustinface marked this conversation as resolved.
Show resolved Hide resolved
tx_record = TransactionRecord(
confirmed_at_height=uint32(coin_state.created_height),
created_at_time=uint64(created_timestamp),
to_puzzle_hash=metadata.recipient_puzzle_hash,
amount=uint64(coin_state.coin.amount),
fee_amount=uint64(0),
confirmed=False,
confirmed=spent_height != 0,
sent=uint32(0),
spend_bundle=None,
additions=[coin_state.coin],
Expand Down Expand Up @@ -1421,6 +1453,10 @@ async def _add_coin_states(
tx_record.name, uint32(coin_state.spent_height)
)
else:
tx_name = bytes(coin_state.coin.name())
for added_coin in additions:
tx_name += bytes(added_coin.name())
tx_name = std_hash(tx_name)
tx_record = TransactionRecord(
confirmed_at_height=uint32(coin_state.spent_height),
created_at_time=uint64(spent_timestamp),
Expand All @@ -1438,7 +1474,7 @@ async def _add_coin_states(
sent_to=[],
trade_id=None,
type=uint32(TransactionType.OUTGOING_TX.value),
name=bytes32(token_bytes()),
name=tx_name,
ytx1991 marked this conversation as resolved.
Show resolved Hide resolved
memos=[],
)

Expand All @@ -1449,7 +1485,7 @@ async def _add_coin_states(
await self.interested_store.remove_interested_coin_id(coin_state.coin.name())
confirmed_tx_records: List[TransactionRecord] = []
for tx_record in all_unconfirmed:
if tx_record.type in CLAWBACK_TRANSACTION_TYPES:
if tx_record.type in CLAWBACK_INCOMING_TRANSACTION_TYPES:
for add_coin in tx_record.additions:
if add_coin == coin_state.coin:
confirmed_tx_records.append(tx_record)
Expand Down
29 changes: 27 additions & 2 deletions tests/wallet/rpc/test_wallet_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2013,14 +2013,35 @@ async def test_set_wallet_resync_on_startup(wallet_rpc_environment: WalletRpcTes

wallet_node: WalletNode = env.wallet_1.node
wallet_node_2: WalletNode = env.wallet_2.node
# Test Clawback resync
tx = await wc.send_transaction(
wallet_id=1,
amount=uint64(500),
address=address,
fee=uint64(0),
puzzle_decorator_override=[{"decorator": "CLAWBACK", "clawback_timelock": 5}],
)
clawback_coin_id = tx.additions[0].name()
assert tx.spend_bundle is not None
await farm_transaction(full_node_api, wallet_node, tx.spend_bundle)
await time_out_assert(20, wc.get_synced)
await asyncio.sleep(10)
resp = await wc.spend_clawback_coins([clawback_coin_id], 0)
assert resp["success"]
assert len(resp["transaction_ids"]) == 1
await time_out_assert_not_none(
10, full_node_api.full_node.mempool_manager.get_spendbundle, bytes32.from_hexstr(resp["transaction_ids"][0])
)
await farm_transaction_block(full_node_api, wallet_node)
await time_out_assert(20, wc.get_synced)
wallet_node_2._close()
await wallet_node_2._await_closed()
# set flag to reset wallet sync data on start
await client.set_wallet_resync_on_startup()
fingerprint = wallet_node.logged_in_fingerprint
assert wallet_node._wallet_state_manager
# 2 reward coins, 1 DID, 1 NFT
assert len(await wallet_node._wallet_state_manager.coin_store.get_all_unspent_coins()) == 4
# 2 reward coins, 1 DID, 1 NFT, 1 clawbacked coin
assert len(await wallet_node._wallet_state_manager.coin_store.get_all_unspent_coins()) == 5
assert await wallet_node._wallet_state_manager.nft_store.count() == 1
# standard wallet, did wallet, nft wallet, did nft wallet
assert len(await wallet_node.wallet_state_manager.user_store.get_all_wallet_info_entries()) == 4
Expand All @@ -2041,6 +2062,10 @@ async def test_set_wallet_resync_on_startup(wallet_rpc_environment: WalletRpcTes
after_txs = await wallet_node_2.wallet_state_manager.tx_store.get_all_transactions()
# transactions should be the same
assert after_txs == before_txs
# Check clawback
clawback_tx = await wallet_node_2.wallet_state_manager.tx_store.get_transaction_record(clawback_coin_id)
assert clawback_tx is not None
assert clawback_tx.confirmed
# only coin_store was populated in this case, but now should be empty
assert len(await wallet_node_2._wallet_state_manager.coin_store.get_all_unspent_coins()) == 0
assert await wallet_node_2._wallet_state_manager.nft_store.count() == 0
Expand Down