Skip to content

Commit

Permalink
Clawback resync (#15496)
Browse files Browse the repository at this point in the history
* Double check coin state

* Fix coverage

* Resolve comments

* Fix unit test

* Fix redundant tx

* Add unit test

* Fix unit test

* refine unit test

* Remove duplicate test

* Fix test on Windows

* Fix clawback memo

* Fix revert

* Improve clawback test

* Remove pragma

* Resolve comments

* Revert clawback test fix

* Add comment for tx confirmed

* Resolve comments

* Remove unrelated change

* Resolve comments

* Fix coverage
  • Loading branch information
ytx1991 committed Jul 11, 2023
1 parent 1e534d0 commit 40a347f
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 19 deletions.
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 @@ -175,7 +175,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 paginate is None:
paginate = sys.stdout.isatty()
Expand Down Expand Up @@ -214,7 +214,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 = {
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:]
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())
# 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())
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,
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
Loading

0 comments on commit 40a347f

Please sign in to comment.