mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2026-08-24 10:05:29 -05:00
[LABS-355] PlotNFT2 Wallet (#20410)
* Use a separate config for pooling information * New PlotNFT drivers * PlotNFT2 Wallet * Comment by @cursor * whoops * Comments by @cursor * Comments by @cursor * Comments by @cursor * Some tidying * fix test for memo adjustment * tweak additional memos again * Comments by cursor * Fix test for coverage * Upstream wallet fixes * Add `REMARK` option to `launch` * pre-commit * chmod * test coverage * Add wallet name * moar test coverage * fix custody architecture namespace * Comments by @matt-o-how * diff minimization * Contemporize(?) action scope test * diff minimization * Remove potentially unnecessary change * Comment by @cursor * whoops
This commit is contained in:
@@ -211,6 +211,9 @@ async def wallet_environments(
|
||||
if trusted_full_node
|
||||
else {}
|
||||
),
|
||||
"reuse_public_key_for_change": {
|
||||
str(service._node.logged_in_fingerprint): tx_config.reuse_puzhash
|
||||
},
|
||||
**config_overrides,
|
||||
}
|
||||
service._node.wallet_state_manager.config = service._node.config
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from __future__ import annotations
|
||||
|
||||
checkout_blocks_and_plots = True
|
||||
@@ -0,0 +1,972 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import re
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from chia_rs import G1Element
|
||||
from chia_rs.chia_rs import G2Element
|
||||
from chia_rs.sized_bytes import bytes32
|
||||
from chia_rs.sized_ints import uint32, uint64
|
||||
|
||||
from chia._tests.environments.wallet import WalletStateTransition, WalletTestFramework
|
||||
from chia.pools.plotnft_drivers import PlotNFT, PoolConfig, UserConfig
|
||||
from chia.simulator.simulator_protocol import ReorgProtocol
|
||||
from chia.types.blockchain_format.program import Program
|
||||
from chia.types.peer_info import PeerInfo
|
||||
from chia.wallet.plotnft_wallet.plotnft_wallet import PlotNFT2Wallet
|
||||
from chia.wallet.wallet_action_scope import PlotNFTTargetStateInfo
|
||||
from chia.wallet.wallet_request_types import PushTX
|
||||
from chia.wallet.wallet_spend_bundle import WalletSpendBundle
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"wallet_environments",
|
||||
[
|
||||
{
|
||||
"num_environments": 1,
|
||||
"blocks_needed": [1],
|
||||
}
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.limit_consensus_modes(reason="irrelevant")
|
||||
@pytest.mark.anyio
|
||||
async def test_plotnft_lifecycle(wallet_environments: WalletTestFramework, self_hostname: str) -> None:
|
||||
env = wallet_environments.environments[0]
|
||||
env.wallet_aliases = {
|
||||
"xch": 1,
|
||||
"plotnft": 2,
|
||||
}
|
||||
|
||||
POOL_REWARD_AMOUNT = uint64(1_750_000_000_000)
|
||||
|
||||
# CREATION
|
||||
creation_fee = POOL_REWARD_AMOUNT + 1
|
||||
async with env.wallet_state_manager.new_action_scope(wallet_environments.tx_config, push=True) as action_scope:
|
||||
await PlotNFT2Wallet.create_new(
|
||||
wallet_state_manager=env.wallet_state_manager,
|
||||
xch_wallet=env.xch_wallet,
|
||||
action_scope=action_scope,
|
||||
fee=uint64(creation_fee),
|
||||
)
|
||||
|
||||
await wallet_environments.process_pending_states(
|
||||
[
|
||||
WalletStateTransition(
|
||||
pre_block_balance_updates={
|
||||
"xch": {
|
||||
"unconfirmed_wallet_balance": -(creation_fee + 1),
|
||||
"<=#spendable_balance": -(creation_fee + 1),
|
||||
"<=#max_send_amount": -(creation_fee + 1),
|
||||
">=#pending_change": 0,
|
||||
">=#pending_coin_removal_count": 1,
|
||||
}
|
||||
},
|
||||
post_block_balance_updates={
|
||||
"xch": {
|
||||
"confirmed_wallet_balance": -(creation_fee + 1),
|
||||
">=#spendable_balance": 1,
|
||||
">=#max_send_amount": 1,
|
||||
"<=#pending_change": 0,
|
||||
"<=#pending_coin_removal_count": -1,
|
||||
"<=#unspent_coin_count": 0,
|
||||
},
|
||||
"plotnft": {"init": True, "unspent_coin_count": 1},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
plotnft_wallet = env.wallet_state_manager.get_wallet(
|
||||
uint32(env.wallet_aliases["plotnft"]), required_type=PlotNFT2Wallet
|
||||
)
|
||||
|
||||
# Reorg (creation)
|
||||
height = wallet_environments.full_node.full_node.blockchain.get_peak_height()
|
||||
assert height is not None
|
||||
await wallet_environments.full_node.reorg_from_index_to_new_index(
|
||||
ReorgProtocol(uint32(height - 1), uint32(height + 1), bytes32.zeros, None)
|
||||
)
|
||||
await wallet_environments.full_node.wait_for_wallet_synced(env.node)
|
||||
|
||||
await wallet_environments.process_pending_states(
|
||||
[
|
||||
WalletStateTransition(
|
||||
pre_block_balance_updates={
|
||||
"xch": {
|
||||
"confirmed_wallet_balance": creation_fee + 1,
|
||||
"<=#spendable_balance": creation_fee + 1,
|
||||
"<=#max_send_amount": creation_fee + 1,
|
||||
">=#pending_change": 0,
|
||||
">=#pending_coin_removal_count": 1,
|
||||
">=#unspent_coin_count": 0,
|
||||
},
|
||||
"plotnft": {"unspent_coin_count": -1},
|
||||
},
|
||||
post_block_balance_updates={
|
||||
"xch": {
|
||||
"confirmed_wallet_balance": -(creation_fee + 1),
|
||||
">=#spendable_balance": 1,
|
||||
">=#max_send_amount": 1,
|
||||
"<=#pending_change": 0,
|
||||
"<=#pending_coin_removal_count": -1,
|
||||
"<=#unspent_coin_count": 0,
|
||||
},
|
||||
"plotnft": {"unspent_coin_count": 1},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# (check an error)
|
||||
with pytest.raises(ValueError, match=re.escape("`leave_pool` called on a non-pooling or exiting PlotNFT")):
|
||||
async with env.wallet_state_manager.new_action_scope(wallet_environments.tx_config, push=True) as action_scope:
|
||||
await plotnft_wallet.leave_pool(action_scope=action_scope)
|
||||
|
||||
# REWARDS GENERATED
|
||||
NUM_REWARDS_FARMED = 2
|
||||
REWARDS_GAINED = POOL_REWARD_AMOUNT * NUM_REWARDS_FARMED
|
||||
await wallet_environments.full_node.farm_blocks_to_puzzlehash(
|
||||
count=NUM_REWARDS_FARMED, farm_to=plotnft_wallet.p2_singleton_puzzle_hash, guarantee_transaction_blocks=True
|
||||
)
|
||||
await wallet_environments.full_node.farm_blocks_to_puzzlehash(count=1)
|
||||
await wallet_environments.full_node.wait_for_wallet_synced(env.node)
|
||||
|
||||
await env.change_balances(
|
||||
{
|
||||
"plotnft": {
|
||||
"confirmed_wallet_balance": REWARDS_GAINED,
|
||||
"unconfirmed_wallet_balance": REWARDS_GAINED,
|
||||
"max_send_amount": REWARDS_GAINED,
|
||||
"spendable_balance": REWARDS_GAINED,
|
||||
"unspent_coin_count": NUM_REWARDS_FARMED,
|
||||
}
|
||||
}
|
||||
)
|
||||
await env.check_balances()
|
||||
|
||||
# Reorg (rewards generated)
|
||||
height = wallet_environments.full_node.full_node.blockchain.get_peak_height()
|
||||
assert height is not None
|
||||
await wallet_environments.full_node.reorg_from_index_to_new_index(
|
||||
ReorgProtocol(uint32(height - 3), uint32(height + 1), bytes32.zeros, None)
|
||||
)
|
||||
await wallet_environments.full_node.wait_for_wallet_synced(env.node)
|
||||
|
||||
await env.change_balances(
|
||||
{
|
||||
"plotnft": {
|
||||
"confirmed_wallet_balance": -REWARDS_GAINED,
|
||||
"unconfirmed_wallet_balance": -REWARDS_GAINED,
|
||||
"max_send_amount": -REWARDS_GAINED,
|
||||
"spendable_balance": -REWARDS_GAINED,
|
||||
"unspent_coin_count": -NUM_REWARDS_FARMED,
|
||||
}
|
||||
}
|
||||
)
|
||||
await env.check_balances()
|
||||
await wallet_environments.full_node.farm_blocks_to_puzzlehash(
|
||||
count=NUM_REWARDS_FARMED, farm_to=plotnft_wallet.p2_singleton_puzzle_hash, guarantee_transaction_blocks=True
|
||||
)
|
||||
await wallet_environments.full_node.farm_blocks_to_puzzlehash(count=1)
|
||||
await wallet_environments.full_node.wait_for_wallet_synced(env.node)
|
||||
|
||||
await env.change_balances(
|
||||
{
|
||||
"plotnft": {
|
||||
"confirmed_wallet_balance": REWARDS_GAINED,
|
||||
"unconfirmed_wallet_balance": REWARDS_GAINED,
|
||||
"max_send_amount": REWARDS_GAINED,
|
||||
"spendable_balance": REWARDS_GAINED,
|
||||
"unspent_coin_count": NUM_REWARDS_FARMED,
|
||||
}
|
||||
}
|
||||
)
|
||||
await env.check_balances()
|
||||
# check a branch of `get_unconfirmed_balance` (no records specified)
|
||||
assert await plotnft_wallet.get_unconfirmed_balance() == REWARDS_GAINED
|
||||
|
||||
# CLAIM REWARDS
|
||||
amount_to_succeed_in_claiming = 100
|
||||
async with env.wallet_state_manager.new_action_scope(wallet_environments.tx_config, push=True) as action_scope:
|
||||
with pytest.raises(ValueError, match="Fee is greater than the total amount of rewards"):
|
||||
await plotnft_wallet.claim_rewards(
|
||||
action_scope=action_scope,
|
||||
fee=uint64(REWARDS_GAINED + 1),
|
||||
)
|
||||
await plotnft_wallet.claim_rewards(
|
||||
action_scope=action_scope,
|
||||
fee=uint64(REWARDS_GAINED - amount_to_succeed_in_claiming),
|
||||
)
|
||||
|
||||
await wallet_environments.process_pending_states(
|
||||
[
|
||||
WalletStateTransition(
|
||||
pre_block_balance_updates={
|
||||
"plotnft": {
|
||||
"unconfirmed_wallet_balance": -REWARDS_GAINED,
|
||||
"spendable_balance": -REWARDS_GAINED,
|
||||
"max_send_amount": -REWARDS_GAINED,
|
||||
"pending_coin_removal_count": NUM_REWARDS_FARMED + 1,
|
||||
}
|
||||
},
|
||||
post_block_balance_updates={
|
||||
"xch": {
|
||||
"confirmed_wallet_balance": amount_to_succeed_in_claiming,
|
||||
"unconfirmed_wallet_balance": amount_to_succeed_in_claiming,
|
||||
"spendable_balance": amount_to_succeed_in_claiming,
|
||||
"max_send_amount": amount_to_succeed_in_claiming,
|
||||
"unspent_coin_count": 1,
|
||||
},
|
||||
"plotnft": {
|
||||
"confirmed_wallet_balance": -REWARDS_GAINED,
|
||||
"pending_coin_removal_count": -NUM_REWARDS_FARMED - 1,
|
||||
"unspent_coin_count": -NUM_REWARDS_FARMED,
|
||||
},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
async with env.wallet_state_manager.new_action_scope(wallet_environments.tx_config, push=True) as action_scope:
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=re.escape("No rewards to claim"),
|
||||
):
|
||||
await plotnft_wallet.claim_rewards(action_scope=action_scope)
|
||||
|
||||
# Reorg (claim rewards)
|
||||
height = wallet_environments.full_node.full_node.blockchain.get_peak_height()
|
||||
assert height is not None
|
||||
await wallet_environments.full_node.reorg_from_index_to_new_index(
|
||||
ReorgProtocol(uint32(height - 1), uint32(height + 1), bytes32.zeros, None)
|
||||
)
|
||||
await wallet_environments.full_node.wait_for_wallet_synced(env.node)
|
||||
|
||||
await wallet_environments.process_pending_states(
|
||||
[
|
||||
WalletStateTransition(
|
||||
pre_block_balance_updates={
|
||||
"xch": {
|
||||
"confirmed_wallet_balance": -amount_to_succeed_in_claiming,
|
||||
"unconfirmed_wallet_balance": -amount_to_succeed_in_claiming,
|
||||
"spendable_balance": -amount_to_succeed_in_claiming,
|
||||
"max_send_amount": -amount_to_succeed_in_claiming,
|
||||
"unspent_coin_count": -1,
|
||||
},
|
||||
"plotnft": {
|
||||
"confirmed_wallet_balance": REWARDS_GAINED,
|
||||
"pending_coin_removal_count": NUM_REWARDS_FARMED + 1,
|
||||
"unspent_coin_count": NUM_REWARDS_FARMED,
|
||||
},
|
||||
},
|
||||
post_block_balance_updates={
|
||||
"xch": {
|
||||
"confirmed_wallet_balance": amount_to_succeed_in_claiming,
|
||||
"unconfirmed_wallet_balance": amount_to_succeed_in_claiming,
|
||||
"spendable_balance": amount_to_succeed_in_claiming,
|
||||
"max_send_amount": amount_to_succeed_in_claiming,
|
||||
"unspent_coin_count": 1,
|
||||
},
|
||||
"plotnft": {
|
||||
"confirmed_wallet_balance": -REWARDS_GAINED,
|
||||
"pending_coin_removal_count": -NUM_REWARDS_FARMED - 1,
|
||||
"unspent_coin_count": -NUM_REWARDS_FARMED,
|
||||
},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# JOIN POOL
|
||||
joining_fee = uint64(1_000)
|
||||
|
||||
async with env.wallet_state_manager.new_action_scope(wallet_environments.tx_config, push=True) as action_scope:
|
||||
await plotnft_wallet.join_pool(
|
||||
pool_config=PoolConfig(
|
||||
pool_puzzle_hash=bytes32.zeros, heightlock=uint32(5), pool_memoization=Program.to(None)
|
||||
),
|
||||
pool_url="https://daurl.com",
|
||||
action_scope=action_scope,
|
||||
fee=joining_fee,
|
||||
)
|
||||
|
||||
await wallet_environments.process_pending_states(
|
||||
[
|
||||
WalletStateTransition(
|
||||
pre_block_balance_updates={
|
||||
"xch": {
|
||||
"unconfirmed_wallet_balance": -joining_fee,
|
||||
"<=#spendable_balance": -joining_fee,
|
||||
"<=#max_send_amount": -joining_fee,
|
||||
">=#pending_change": 0,
|
||||
">=#pending_coin_removal_count": 1,
|
||||
},
|
||||
"plotnft": {"pending_coin_removal_count": 1},
|
||||
},
|
||||
post_block_balance_updates={
|
||||
"xch": {
|
||||
"confirmed_wallet_balance": -joining_fee,
|
||||
">=#spendable_balance": 1,
|
||||
">=#max_send_amount": 1,
|
||||
"<=#pending_change": 0,
|
||||
"<=#pending_coin_removal_count": -1,
|
||||
"<=#unspent_coin_count": 0,
|
||||
},
|
||||
"plotnft": {"pending_coin_removal_count": -1},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
assert (
|
||||
await env.wallet_state_manager.plotnft2_store.get_latest_remark(plotnft_wallet.plotnft_id)
|
||||
== "https://daurl.com"
|
||||
)
|
||||
|
||||
# Reorg (join pool)
|
||||
height = wallet_environments.full_node.full_node.blockchain.get_peak_height()
|
||||
assert height is not None
|
||||
await wallet_environments.full_node.reorg_from_index_to_new_index(
|
||||
ReorgProtocol(uint32(height - 1), uint32(height + 1), bytes32.zeros, None)
|
||||
)
|
||||
await wallet_environments.full_node.wait_for_wallet_synced(env.node)
|
||||
|
||||
await wallet_environments.process_pending_states(
|
||||
[
|
||||
WalletStateTransition(
|
||||
pre_block_balance_updates={
|
||||
"xch": {
|
||||
"confirmed_wallet_balance": joining_fee,
|
||||
"<=#spendable_balance": -1,
|
||||
"<=#max_send_amount": -1,
|
||||
">=#pending_change": 0,
|
||||
">=#pending_coin_removal_count": 1,
|
||||
">=#unspent_coin_count": 0,
|
||||
},
|
||||
"plotnft": {"pending_coin_removal_count": 1},
|
||||
},
|
||||
post_block_balance_updates={
|
||||
"xch": {
|
||||
"confirmed_wallet_balance": -joining_fee,
|
||||
">=#spendable_balance": 1,
|
||||
">=#max_send_amount": 1,
|
||||
"<=#pending_change": 0,
|
||||
"<=#pending_coin_removal_count": -1,
|
||||
"<=#unspent_coin_count": 0,
|
||||
},
|
||||
"plotnft": {"pending_coin_removal_count": -1},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# RECEIVE REWARDS (while pooling)
|
||||
EXTRA_POOLING_REWARDS = 2
|
||||
await wallet_environments.full_node.farm_blocks_to_puzzlehash(
|
||||
count=NUM_REWARDS_FARMED + EXTRA_POOLING_REWARDS,
|
||||
farm_to=plotnft_wallet.p2_singleton_puzzle_hash,
|
||||
guarantee_transaction_blocks=True,
|
||||
)
|
||||
await wallet_environments.full_node.farm_blocks_to_puzzlehash(count=1)
|
||||
|
||||
await wallet_environments.full_node.wait_for_wallet_synced(env.node)
|
||||
await env.change_balances(
|
||||
{
|
||||
"plotnft": {
|
||||
"confirmed_wallet_balance": REWARDS_GAINED + POOL_REWARD_AMOUNT * EXTRA_POOLING_REWARDS,
|
||||
"unconfirmed_wallet_balance": REWARDS_GAINED + POOL_REWARD_AMOUNT * EXTRA_POOLING_REWARDS,
|
||||
"max_send_amount": REWARDS_GAINED + POOL_REWARD_AMOUNT * EXTRA_POOLING_REWARDS,
|
||||
"spendable_balance": REWARDS_GAINED + POOL_REWARD_AMOUNT * EXTRA_POOLING_REWARDS,
|
||||
"unspent_coin_count": NUM_REWARDS_FARMED + EXTRA_POOLING_REWARDS,
|
||||
}
|
||||
}
|
||||
)
|
||||
await env.check_balances()
|
||||
|
||||
async with env.wallet_state_manager.new_action_scope(wallet_environments.tx_config, push=True) as action_scope:
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=re.escape("Cannot claim rewards while pooling. If you're a pool, try `forward_pool_rewards`"),
|
||||
):
|
||||
await plotnft_wallet.claim_rewards(action_scope=action_scope)
|
||||
|
||||
# LOSE REWARDS (while pooling)
|
||||
pool_rewards = await env.wallet_state_manager.plotnft2_store.get_pool_rewards(plotnft_id=plotnft_wallet.plotnft_id)
|
||||
plotnft = await plotnft_wallet.get_current_plotnft()
|
||||
coin_spends = []
|
||||
singleton_coin_spend = None
|
||||
for reward in pool_rewards[0:-1]:
|
||||
new_coin_spends = plotnft.forward_pool_reward(reward)
|
||||
coin_spends += new_coin_spends
|
||||
singleton_coin_spend = next(iter(spend for spend in new_coin_spends if spend.coin.amount == 1))
|
||||
plotnft = PlotNFT.get_next_from_coin_spend(
|
||||
coin_spend=singleton_coin_spend, genesis_challenge=None, pre_uncurry=None, previous_plotnft_puzzle=plotnft
|
||||
)
|
||||
|
||||
NUM_CLAIMED = len(pool_rewards) - 1
|
||||
await wallet_environments.full_node_rpc_client.push_tx(WalletSpendBundle(coin_spends, G2Element()))
|
||||
|
||||
await wallet_environments.process_pending_states(
|
||||
[
|
||||
WalletStateTransition(
|
||||
pre_block_balance_updates={},
|
||||
post_block_balance_updates={
|
||||
"plotnft": {
|
||||
"confirmed_wallet_balance": -POOL_REWARD_AMOUNT * NUM_CLAIMED,
|
||||
"unconfirmed_wallet_balance": -POOL_REWARD_AMOUNT * NUM_CLAIMED,
|
||||
"max_send_amount": -POOL_REWARD_AMOUNT * NUM_CLAIMED,
|
||||
"spendable_balance": -POOL_REWARD_AMOUNT * NUM_CLAIMED,
|
||||
"unspent_coin_count": -NUM_CLAIMED,
|
||||
}
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# LEAVE POOL (to another)
|
||||
async with env.wallet_state_manager.new_action_scope(wallet_environments.tx_config, push=True) as action_scope:
|
||||
finish_leaving_fee = uint64(1_000_000_000)
|
||||
await plotnft_wallet.join_pool(
|
||||
action_scope=action_scope,
|
||||
fee=uint64(0),
|
||||
finish_leaving_fee=finish_leaving_fee,
|
||||
pool_url="https://daurl2.com",
|
||||
pool_config=PoolConfig(
|
||||
pool_puzzle_hash=bytes32.zeros, heightlock=uint32(5), pool_memoization=Program.to(None)
|
||||
),
|
||||
)
|
||||
|
||||
await wallet_environments.process_pending_states(
|
||||
[
|
||||
WalletStateTransition(
|
||||
pre_block_balance_updates={
|
||||
"xch": {},
|
||||
"plotnft": {
|
||||
"pending_coin_removal_count": 1,
|
||||
},
|
||||
},
|
||||
post_block_balance_updates={
|
||||
"xch": {},
|
||||
"plotnft": {
|
||||
"pending_coin_removal_count": -1,
|
||||
},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# (check an error)
|
||||
with pytest.raises(ValueError, match=re.escape("`leave_pool` called on a non-pooling or exiting PlotNFT")):
|
||||
async with env.wallet_state_manager.new_action_scope(wallet_environments.tx_config, push=True) as action_scope:
|
||||
await plotnft_wallet.leave_pool(action_scope=action_scope)
|
||||
|
||||
# Reorg (leave pool to another)
|
||||
height = wallet_environments.full_node.full_node.blockchain.get_peak_height()
|
||||
assert height is not None
|
||||
await wallet_environments.full_node.reorg_from_index_to_new_index(
|
||||
ReorgProtocol(uint32(height - 1), uint32(height + 1), bytes32.zeros, None)
|
||||
)
|
||||
await wallet_environments.full_node.wait_for_wallet_synced(env.node)
|
||||
|
||||
await wallet_environments.process_pending_states(
|
||||
[
|
||||
WalletStateTransition(
|
||||
pre_block_balance_updates={
|
||||
"xch": {},
|
||||
"plotnft": {
|
||||
"pending_coin_removal_count": 1,
|
||||
},
|
||||
},
|
||||
post_block_balance_updates={
|
||||
"xch": {},
|
||||
"plotnft": {
|
||||
"pending_coin_removal_count": -1,
|
||||
},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# FINISH LEAVING (to new pool)
|
||||
plotnft = await plotnft_wallet.get_current_plotnft()
|
||||
await wallet_environments.full_node.farm_blocks_to_puzzlehash(
|
||||
count=plotnft.guaranteed_pool_config.heightlock + 2, guarantee_transaction_blocks=True
|
||||
)
|
||||
|
||||
await wallet_environments.full_node.wait_for_wallet_synced(env.node)
|
||||
|
||||
await wallet_environments.process_pending_states(
|
||||
[
|
||||
WalletStateTransition(
|
||||
pre_block_balance_updates={
|
||||
"xch": {
|
||||
"unconfirmed_wallet_balance": -finish_leaving_fee,
|
||||
"<=#spendable_balance": -finish_leaving_fee,
|
||||
"<=#max_send_amount": -finish_leaving_fee,
|
||||
">=#pending_change": 0,
|
||||
">=#pending_coin_removal_count": 1,
|
||||
},
|
||||
"plotnft": {
|
||||
"pending_coin_removal_count": 2, # one for the exit, one for the join
|
||||
},
|
||||
},
|
||||
post_block_balance_updates={
|
||||
"xch": {
|
||||
"confirmed_wallet_balance": -finish_leaving_fee,
|
||||
">=#spendable_balance": 0,
|
||||
">=#max_send_amount": 0,
|
||||
"<=#pending_change": 0,
|
||||
"<=#pending_coin_removal_count": -1,
|
||||
"<=#unspent_coin_count": 0,
|
||||
},
|
||||
"plotnft": {
|
||||
"pending_coin_removal_count": -2,
|
||||
},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# LEAVE POOL
|
||||
async with env.wallet_state_manager.new_action_scope(wallet_environments.tx_config, push=True) as action_scope:
|
||||
leave_fee = uint64(1_000_000)
|
||||
finish_leaving_fee = uint64(1_000_000_000)
|
||||
await plotnft_wallet.leave_pool(action_scope=action_scope, fee=leave_fee, finish_leaving_fee=finish_leaving_fee)
|
||||
|
||||
await wallet_environments.process_pending_states(
|
||||
[
|
||||
WalletStateTransition(
|
||||
pre_block_balance_updates={
|
||||
"xch": {
|
||||
"unconfirmed_wallet_balance": -leave_fee,
|
||||
"<=#spendable_balance": -leave_fee,
|
||||
"<=#max_send_amount": -leave_fee,
|
||||
">=#pending_change": 0,
|
||||
">=#pending_coin_removal_count": 1,
|
||||
},
|
||||
"plotnft": {
|
||||
"pending_coin_removal_count": 1,
|
||||
},
|
||||
},
|
||||
post_block_balance_updates={
|
||||
"xch": {
|
||||
"confirmed_wallet_balance": -leave_fee,
|
||||
">=#spendable_balance": 1,
|
||||
">=#max_send_amount": 1,
|
||||
"<=#pending_change": 0,
|
||||
"<=#pending_coin_removal_count": -1,
|
||||
"<=#unspent_coin_count": 0,
|
||||
},
|
||||
"plotnft": {
|
||||
"pending_coin_removal_count": -1,
|
||||
},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
async with env.wallet_state_manager.new_action_scope(wallet_environments.tx_config, push=True) as action_scope:
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=re.escape("Cannot claim rewards while pooling. If you're a pool, try `forward_pool_rewards`"),
|
||||
):
|
||||
await plotnft_wallet.claim_rewards(action_scope=action_scope)
|
||||
|
||||
# Reorg (leave pool)
|
||||
height = wallet_environments.full_node.full_node.blockchain.get_peak_height()
|
||||
assert height is not None
|
||||
await wallet_environments.full_node.reorg_from_index_to_new_index(
|
||||
ReorgProtocol(uint32(height - 1), uint32(height + 1), bytes32.zeros, None)
|
||||
)
|
||||
await wallet_environments.full_node.wait_for_wallet_synced(env.node)
|
||||
|
||||
await wallet_environments.process_pending_states(
|
||||
[
|
||||
WalletStateTransition(
|
||||
pre_block_balance_updates={
|
||||
"xch": {
|
||||
"confirmed_wallet_balance": leave_fee,
|
||||
"<=#spendable_balance": -1,
|
||||
"<=#max_send_amount": -1,
|
||||
">=#pending_change": 0,
|
||||
">=#pending_coin_removal_count": 1,
|
||||
">=#unspent_coin_count": 0,
|
||||
},
|
||||
"plotnft": {
|
||||
"pending_coin_removal_count": 1,
|
||||
},
|
||||
},
|
||||
post_block_balance_updates={
|
||||
"xch": {
|
||||
"confirmed_wallet_balance": -leave_fee,
|
||||
">=#spendable_balance": 1,
|
||||
">=#max_send_amount": 1,
|
||||
"<=#pending_change": 0,
|
||||
"<=#pending_coin_removal_count": -1,
|
||||
"<=#unspent_coin_count": 0,
|
||||
},
|
||||
"plotnft": {
|
||||
"pending_coin_removal_count": -1,
|
||||
},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# LOSE REWARDS (while leaving)
|
||||
plotnft = await plotnft_wallet.get_current_plotnft()
|
||||
[pool_reward] = await env.wallet_state_manager.plotnft2_store.get_pool_rewards(plotnft_id=plotnft_wallet.plotnft_id)
|
||||
coin_spends = plotnft.forward_pool_reward(pool_reward)
|
||||
await env.rpc_client.push_tx(PushTX(spend_bundle=WalletSpendBundle(coin_spends, G2Element())))
|
||||
|
||||
await wallet_environments.process_pending_states(
|
||||
[
|
||||
WalletStateTransition(
|
||||
pre_block_balance_updates={},
|
||||
post_block_balance_updates={
|
||||
"plotnft": {
|
||||
"confirmed_wallet_balance": -POOL_REWARD_AMOUNT,
|
||||
"unconfirmed_wallet_balance": -POOL_REWARD_AMOUNT,
|
||||
"max_send_amount": -POOL_REWARD_AMOUNT,
|
||||
"spendable_balance": -POOL_REWARD_AMOUNT,
|
||||
"unspent_coin_count": -1,
|
||||
}
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# FINISH LEAVING
|
||||
plotnft = await plotnft_wallet.get_current_plotnft()
|
||||
await wallet_environments.full_node.farm_blocks_to_puzzlehash(
|
||||
count=plotnft.guaranteed_pool_config.heightlock + 2, guarantee_transaction_blocks=True
|
||||
)
|
||||
await wallet_environments.full_node.wait_for_wallet_synced(env.node)
|
||||
|
||||
await wallet_environments.process_pending_states(
|
||||
[
|
||||
WalletStateTransition(
|
||||
pre_block_balance_updates={
|
||||
"xch": {
|
||||
"unconfirmed_wallet_balance": -finish_leaving_fee,
|
||||
"<=#spendable_balance": -finish_leaving_fee,
|
||||
"<=#max_send_amount": -finish_leaving_fee,
|
||||
">=#pending_change": 0,
|
||||
">=#pending_coin_removal_count": 1,
|
||||
},
|
||||
"plotnft": {
|
||||
"pending_coin_removal_count": 1,
|
||||
},
|
||||
},
|
||||
post_block_balance_updates={
|
||||
"xch": {
|
||||
"confirmed_wallet_balance": -finish_leaving_fee,
|
||||
">=#spendable_balance": 0,
|
||||
">=#max_send_amount": 0,
|
||||
"<=#pending_change": 0,
|
||||
"<=#pending_coin_removal_count": -1,
|
||||
"<=#unspent_coin_count": 0,
|
||||
},
|
||||
"plotnft": {
|
||||
"pending_coin_removal_count": -1,
|
||||
},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# Reorg (finish leaving)
|
||||
height = wallet_environments.full_node.full_node.blockchain.get_peak_height()
|
||||
assert height is not None
|
||||
await wallet_environments.full_node.reorg_from_index_to_new_index(
|
||||
ReorgProtocol(uint32(height - 1), uint32(height + 1), bytes32.zeros, None)
|
||||
)
|
||||
await wallet_environments.full_node.wait_for_wallet_synced(env.node)
|
||||
|
||||
await wallet_environments.process_pending_states(
|
||||
[
|
||||
WalletStateTransition(
|
||||
pre_block_balance_updates={
|
||||
"xch": {
|
||||
"confirmed_wallet_balance": finish_leaving_fee,
|
||||
"<=#spendable_balance": 0,
|
||||
"<=#max_send_amount": 0,
|
||||
">=#pending_change": 0,
|
||||
">=#pending_coin_removal_count": 1,
|
||||
">=#unspent_coin_count": 0,
|
||||
},
|
||||
"plotnft": {
|
||||
"pending_coin_removal_count": 1,
|
||||
},
|
||||
},
|
||||
post_block_balance_updates={
|
||||
"xch": {
|
||||
"confirmed_wallet_balance": -finish_leaving_fee,
|
||||
">=#spendable_balance": 0,
|
||||
">=#max_send_amount": 0,
|
||||
"<=#pending_change": 0,
|
||||
"<=#pending_coin_removal_count": -1,
|
||||
"<=#unspent_coin_count": 0,
|
||||
},
|
||||
"plotnft": {
|
||||
"pending_coin_removal_count": -1,
|
||||
},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# Resync start
|
||||
env.node._close()
|
||||
await env.node._await_closed()
|
||||
env.node.config["database_path"] = "wallet/db/blockchain_wallet_v2_test1_CHALLENGE_KEY.sqlite"
|
||||
|
||||
# use second node to start the same wallet, reusing config
|
||||
await env.node._start()
|
||||
await env.peer_server.start_client(
|
||||
PeerInfo(self_hostname, wallet_environments.full_node.full_node.server.get_port()), None
|
||||
)
|
||||
await wallet_environments.full_node.wait_for_wallet_synced(env.node)
|
||||
await wallet_environments.process_pending_states([WalletStateTransition(), WalletStateTransition()])
|
||||
|
||||
rediscovered_plotnft_wallet = env.node.wallet_state_manager.wallets[uint32(env.wallet_aliases["plotnft"])]
|
||||
assert isinstance(rediscovered_plotnft_wallet, PlotNFT2Wallet)
|
||||
rediscovered_plotnft = await rediscovered_plotnft_wallet.get_current_plotnft()
|
||||
|
||||
# and test just a normal restart
|
||||
env.node._close()
|
||||
await env.node._await_closed()
|
||||
await env.node._start()
|
||||
await env.peer_server.start_client(
|
||||
PeerInfo(self_hostname, wallet_environments.full_node.full_node.server.get_port()), None
|
||||
)
|
||||
env.node.config["selected_network"] = "simulator"
|
||||
await wallet_environments.full_node.wait_for_wallet_synced(env.node)
|
||||
rediscovered_plotnft_wallet = env.node.wallet_state_manager.wallets[uint32(env.wallet_aliases["plotnft"])]
|
||||
assert isinstance(rediscovered_plotnft_wallet, PlotNFT2Wallet)
|
||||
assert await rediscovered_plotnft_wallet.get_current_plotnft() == rediscovered_plotnft
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"wallet_environments",
|
||||
[
|
||||
{
|
||||
"num_environments": 1,
|
||||
"blocks_needed": [1],
|
||||
}
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.limit_consensus_modes(reason="irrelevant")
|
||||
@pytest.mark.anyio
|
||||
async def test_plotnft_errors(wallet_environments: WalletTestFramework, self_hostname: str) -> None:
|
||||
env = wallet_environments.environments[0]
|
||||
env.wallet_aliases = {
|
||||
"xch": 1,
|
||||
"plotnft": 2,
|
||||
}
|
||||
|
||||
# creation error
|
||||
with pytest.raises(ValueError, match="pool_url and pool_config must be both None or both not None"):
|
||||
async with env.wallet_state_manager.new_action_scope(wallet_environments.tx_config, push=True) as action_scope:
|
||||
await PlotNFT2Wallet.create_new(
|
||||
wallet_state_manager=env.wallet_state_manager,
|
||||
xch_wallet=env.xch_wallet,
|
||||
action_scope=action_scope,
|
||||
fee=uint64(0),
|
||||
pool_config=None,
|
||||
pool_url="https://daurl.com",
|
||||
)
|
||||
with pytest.raises(ValueError, match="pool_url and pool_config must be both None or both not None"):
|
||||
async with env.wallet_state_manager.new_action_scope(wallet_environments.tx_config, push=True) as action_scope:
|
||||
await PlotNFT2Wallet.create_new(
|
||||
wallet_state_manager=env.wallet_state_manager,
|
||||
xch_wallet=env.xch_wallet,
|
||||
action_scope=action_scope,
|
||||
fee=uint64(0),
|
||||
pool_config=PoolConfig(
|
||||
pool_puzzle_hash=bytes32.zeros, heightlock=uint32(5), pool_memoization=Program.to(None)
|
||||
),
|
||||
pool_url=None,
|
||||
)
|
||||
|
||||
# create to pool
|
||||
async with env.wallet_state_manager.new_action_scope(wallet_environments.tx_config, push=True) as action_scope:
|
||||
await PlotNFT2Wallet.create_new(
|
||||
wallet_state_manager=env.wallet_state_manager,
|
||||
xch_wallet=env.xch_wallet,
|
||||
action_scope=action_scope,
|
||||
fee=uint64(0),
|
||||
pool_config=PoolConfig(
|
||||
pool_puzzle_hash=bytes32.zeros, heightlock=uint32(5), pool_memoization=Program.to(None)
|
||||
),
|
||||
pool_url="https://daurl.com",
|
||||
)
|
||||
|
||||
await wallet_environments.process_pending_states(
|
||||
[
|
||||
WalletStateTransition(
|
||||
pre_block_balance_updates={"xch": {"set_remainder": True}},
|
||||
post_block_balance_updates={
|
||||
"xch": {"set_remainder": True},
|
||||
"plotnft": {"init": True, "set_remainder": True},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
plotnft_wallet = env.wallet_state_manager.wallets[uint32(env.wallet_aliases["plotnft"])]
|
||||
assert isinstance(plotnft_wallet, PlotNFT2Wallet)
|
||||
# check a quick DB error
|
||||
with pytest.raises(ValueError, match="coin_ids must not be empty"):
|
||||
await env.wallet_state_manager.plotnft2_store.get_plotnfts(coin_ids=[])
|
||||
|
||||
# some `leave_pool` argument checks
|
||||
with pytest.raises(ValueError, match="Both new_pool_url or new_pool_config must be provided together"):
|
||||
await plotnft_wallet.leave_pool(
|
||||
action_scope=action_scope,
|
||||
new_pool_url=None,
|
||||
new_pool_config=PoolConfig(
|
||||
pool_puzzle_hash=bytes32.zeros, heightlock=uint32(5), pool_memoization=Program.to(None)
|
||||
),
|
||||
)
|
||||
with pytest.raises(ValueError, match="Both new_pool_url or new_pool_config must be provided together"):
|
||||
await plotnft_wallet.leave_pool(
|
||||
action_scope=action_scope,
|
||||
new_pool_url="https://daurl2.com",
|
||||
new_pool_config=None,
|
||||
)
|
||||
|
||||
# try to leave and delete necessary info while leaving to make sure wallet still leaves (with no fee and no pool)
|
||||
async with env.wallet_state_manager.new_action_scope(wallet_environments.tx_config, push=True) as action_scope:
|
||||
finish_leaving_fee = uint64(1_000_000_000)
|
||||
await plotnft_wallet.leave_pool(
|
||||
action_scope=action_scope,
|
||||
fee=uint64(0),
|
||||
finish_leaving_fee=finish_leaving_fee,
|
||||
new_pool_url="https://daurl2.com",
|
||||
new_pool_config=PoolConfig(
|
||||
pool_puzzle_hash=bytes32.zeros, heightlock=uint32(5), pool_memoization=Program.to(None)
|
||||
),
|
||||
)
|
||||
await wallet_environments.full_node.wait_for_wallet_synced(env.node)
|
||||
|
||||
await wallet_environments.process_pending_states(
|
||||
[
|
||||
WalletStateTransition(
|
||||
pre_block_balance_updates={"xch": {"set_remainder": True}, "plotnft": {"set_remainder": True}},
|
||||
post_block_balance_updates={"xch": {"set_remainder": True}, "plotnft": {"set_remainder": True}},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# deleting the finish_exiting_info so the wallet has to adapt and try just leave with no fee
|
||||
async with env.wallet_state_manager.plotnft2_store.db_wrapper.writer_maybe_transaction() as conn:
|
||||
await conn.execute("DELETE FROM finish_exiting_info WHERE wallet_id = ?", (plotnft_wallet.id(),))
|
||||
|
||||
# also adding an unconfirmed transaction to test that completion is not attempted when state is uncertain
|
||||
await env.wallet_state_manager.add_transaction(
|
||||
env.wallet_state_manager.new_outgoing_transaction(
|
||||
wallet_id=plotnft_wallet.id(),
|
||||
puzzle_hash=bytes32.zeros,
|
||||
amount=uint64(0),
|
||||
fee=uint64(0),
|
||||
spend_bundle=WalletSpendBundle([], G2Element()),
|
||||
additions=[],
|
||||
removals=[],
|
||||
name=bytes32.zeros,
|
||||
)
|
||||
)
|
||||
|
||||
# farm to where completion should happen
|
||||
plotnft = await plotnft_wallet.get_current_plotnft()
|
||||
await wallet_environments.full_node.farm_blocks_to_puzzlehash(
|
||||
count=plotnft.guaranteed_pool_config.heightlock + 2, guarantee_transaction_blocks=True
|
||||
)
|
||||
|
||||
await wallet_environments.full_node.wait_for_wallet_synced(env.node)
|
||||
# make sure nothing happens
|
||||
await wallet_environments.process_pending_states(
|
||||
[
|
||||
WalletStateTransition(
|
||||
pre_block_balance_updates={"xch": {}, "plotnft": {}},
|
||||
post_block_balance_updates={"xch": {}, "plotnft": {}},
|
||||
)
|
||||
],
|
||||
invalid_transactions=[bytes32.zeros],
|
||||
)
|
||||
# delete the tx_record
|
||||
async with env.wallet_state_manager.tx_store.db_wrapper.writer_maybe_transaction() as conn:
|
||||
await conn.execute("DELETE FROM transaction_record WHERE bundle_id = ?", (bytes32.zeros,))
|
||||
env.wallet_state_manager.tx_store.unconfirmed_txs = [
|
||||
tx for tx in env.wallet_state_manager.tx_store.unconfirmed_txs if tx.name != bytes32.zeros
|
||||
]
|
||||
# farm a block to re-trigger new_peak
|
||||
await wallet_environments.full_node.farm_blocks_to_puzzlehash(count=1, guarantee_transaction_blocks=True)
|
||||
await wallet_environments.full_node.wait_for_wallet_synced(env.node)
|
||||
|
||||
await wallet_environments.process_pending_states(
|
||||
[
|
||||
WalletStateTransition(
|
||||
pre_block_balance_updates={
|
||||
"xch": {},
|
||||
"plotnft": {
|
||||
"pending_coin_removal_count": 1, # only one, because no join
|
||||
},
|
||||
},
|
||||
post_block_balance_updates={
|
||||
"xch": {},
|
||||
"plotnft": {
|
||||
"pending_coin_removal_count": -1,
|
||||
},
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
# check a `join_pool` argument check
|
||||
with pytest.raises(ValueError, match="A fee to finish leaving was specified but PlotNFT does not need to leave"):
|
||||
await plotnft_wallet.join_pool(
|
||||
action_scope=action_scope,
|
||||
pool_config=PoolConfig(
|
||||
pool_puzzle_hash=bytes32.zeros, heightlock=uint32(5), pool_memoization=Program.to(None)
|
||||
),
|
||||
pool_url="https://daurl.com",
|
||||
finish_leaving_fee=uint64(1),
|
||||
)
|
||||
|
||||
# check a `coin_added` type guard
|
||||
plotnft = await plotnft_wallet.get_current_plotnft()
|
||||
with pytest.raises(ValueError, match="No index found for synthetic pubkey"):
|
||||
await plotnft_wallet.coin_added(
|
||||
coin=Mock(),
|
||||
height=uint32(0),
|
||||
peer=Mock(),
|
||||
coin_data=PlotNFT(
|
||||
launcher_id=bytes32.zeros,
|
||||
genesis_challenge=bytes32.zeros,
|
||||
user_config=UserConfig(synthetic_pubkey=G1Element()), # the important bit
|
||||
exiting=False,
|
||||
coin=Mock(),
|
||||
singleton_lineage_proof=Mock(),
|
||||
),
|
||||
)
|
||||
plotnft_after_raise = await plotnft_wallet.get_current_plotnft()
|
||||
assert plotnft_after_raise == plotnft
|
||||
|
||||
# check a __post_init__
|
||||
target_state = PlotNFTTargetStateInfo(
|
||||
wallet_id=plotnft_wallet.id(),
|
||||
exiting_fee=uint64(0),
|
||||
next_pool_url="blah",
|
||||
next_pool_puzzle_hash=bytes32.zeros,
|
||||
next_heightlock=uint32(0),
|
||||
next_pool_memoization=Program.to(None),
|
||||
)
|
||||
for field_name in ("next_pool_url", "next_pool_puzzle_hash", "next_heightlock", "next_pool_memoization"):
|
||||
with pytest.raises(
|
||||
ValueError, match="Error initializing next PlotNFT target state, not all options for join were specified"
|
||||
):
|
||||
dataclasses.replace(target_state, **{field_name: None}) # type: ignore[arg-type]
|
||||
@@ -13,7 +13,7 @@ from chia.types.blockchain_format.coin import Coin
|
||||
from chia.wallet.signer_protocol import SigningResponse
|
||||
from chia.wallet.transaction_record import TransactionRecord
|
||||
from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG
|
||||
from chia.wallet.wallet_action_scope import WalletSideEffects
|
||||
from chia.wallet.wallet_action_scope import PlotNFTTargetStateInfo, WalletSideEffects
|
||||
from chia.wallet.wallet_spend_bundle import WalletSpendBundle
|
||||
from chia.wallet.wallet_state_manager import WalletStateManager
|
||||
|
||||
@@ -44,6 +44,7 @@ class MockWalletStateManager:
|
||||
list[SigningResponse],
|
||||
list[WalletSpendBundle],
|
||||
list[SingletonRecord],
|
||||
PlotNFTTargetStateInfo | None,
|
||||
]
|
||||
| None
|
||||
) = None
|
||||
@@ -57,6 +58,7 @@ class MockWalletStateManager:
|
||||
additional_signing_responses: list[SigningResponse],
|
||||
extra_spends: list[WalletSpendBundle],
|
||||
singleton_records: list[SingletonRecord],
|
||||
plotnft_exiting_info: PlotNFTTargetStateInfo | None,
|
||||
) -> list[TransactionRecord]:
|
||||
self.most_recent_call = (
|
||||
txs,
|
||||
@@ -66,6 +68,7 @@ class MockWalletStateManager:
|
||||
additional_signing_responses,
|
||||
extra_spends,
|
||||
singleton_records,
|
||||
plotnft_exiting_info,
|
||||
)
|
||||
return txs
|
||||
|
||||
@@ -92,7 +95,7 @@ async def test_wallet_action_scope() -> None:
|
||||
action_scope.side_effects
|
||||
|
||||
assert action_scope.side_effects.transactions == [STD_TX]
|
||||
assert wsm.most_recent_call == ([STD_TX], True, False, True, [], [], [])
|
||||
assert wsm.most_recent_call == ([STD_TX], True, False, True, [], [], [], None)
|
||||
|
||||
async with wsm.new_action_scope( # type: ignore[attr-defined]
|
||||
DEFAULT_TX_CONFIG,
|
||||
@@ -107,4 +110,5 @@ async def test_wallet_action_scope() -> None:
|
||||
interface.side_effects.transactions = []
|
||||
|
||||
assert action_scope.side_effects.transactions == []
|
||||
assert wsm.most_recent_call == ([], False, True, True, [], [], [])
|
||||
# same as above because the lack of transactions prevents the add_pending_transactions call
|
||||
assert wsm.most_recent_call == ([STD_TX], True, False, True, [], [], [], None)
|
||||
|
||||
@@ -10,7 +10,7 @@ import yaml
|
||||
from chia_rs import G1Element
|
||||
from chia_rs.sized_byte_class import hexstr_to_bytes
|
||||
from chia_rs.sized_bytes import bytes32
|
||||
from typing_extensions import Self
|
||||
from typing_extensions import NotRequired, Self
|
||||
|
||||
from chia.util.config import lock_and_load_config, save_config
|
||||
from chia.util.lock import Lockfile
|
||||
@@ -24,6 +24,7 @@ class _PoolConfig(TypedDict):
|
||||
p2_singleton_puzzle_hash: str
|
||||
owner_public_key: str
|
||||
key_derivation_index: int
|
||||
version: NotRequired[int]
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
@@ -35,6 +36,7 @@ class PoolingShareState:
|
||||
p2_singleton_puzzle_hash: bytes32
|
||||
owner_public_key: G1Element
|
||||
key_derivation_index: int
|
||||
version: int = 1
|
||||
|
||||
@staticmethod
|
||||
def state_path(root_path: Path) -> Path:
|
||||
@@ -102,6 +104,7 @@ class PoolingShareState:
|
||||
p2_singleton_puzzle_hash=p2_singleton_puzzle_hash,
|
||||
owner_public_key=G1Element.from_bytes(hexstr_to_bytes(config["owner_public_key"])),
|
||||
key_derivation_index=config["key_derivation_index"],
|
||||
version=config.get("version", 1),
|
||||
)
|
||||
yield self # noqa: RUF075 # update in-memory entry only on successful exit
|
||||
for i, conf in enumerate(loaded_list):
|
||||
@@ -118,6 +121,7 @@ class PoolingShareState:
|
||||
"owner_public_key": bytes(self.owner_public_key).hex(),
|
||||
"p2_singleton_puzzle_hash": self.p2_singleton_puzzle_hash.hex(),
|
||||
"key_derivation_index": self.key_derivation_index,
|
||||
"version": self.version,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlite3 import Row
|
||||
|
||||
from chia_rs import Coin, G1Element
|
||||
from chia_rs.sized_bytes import bytes32
|
||||
from chia_rs.sized_ints import uint32, uint64
|
||||
from typing_extensions import Self
|
||||
|
||||
from chia.pools.plotnft_drivers import PlotNFT, PoolConfig, PoolReward, UserConfig
|
||||
from chia.types.blockchain_format.program import Program
|
||||
from chia.util.db_wrapper import DBWrapper2
|
||||
from chia.wallet.conditions import Remark
|
||||
from chia.wallet.lineage_proof import LineageProof
|
||||
from chia.wallet.wallet_action_scope import PlotNFTTargetStateInfo
|
||||
|
||||
DEFAULT_POOL_REWARDS_PER_CLAIM = 20
|
||||
|
||||
|
||||
def _row_to_plotnft(row: Row, genesis_challenge: bytes32) -> PlotNFT:
|
||||
return PlotNFT(
|
||||
coin=Coin(parent_coin_info=bytes32(row[1]), puzzle_hash=bytes32(row[2]), amount=uint64.from_bytes(row[3])),
|
||||
singleton_lineage_proof=LineageProof.from_bytes(row[4]),
|
||||
launcher_id=bytes32(row[5]),
|
||||
user_config=UserConfig(synthetic_pubkey=G1Element.from_bytes(row[6])),
|
||||
pool_config=PoolConfig(
|
||||
pool_puzzle_hash=bytes32(row[7]),
|
||||
heightlock=uint32.from_bytes(row[8]),
|
||||
pool_memoization=Program.from_bytes(row[9]),
|
||||
)
|
||||
if row[7:10] != (b"", b"", b"")
|
||||
else None,
|
||||
genesis_challenge=genesis_challenge,
|
||||
exiting=False if row[10] == 0 else True,
|
||||
remarks=[Remark(Program.to(row[11]))] if row[11] is not None else [],
|
||||
)
|
||||
|
||||
|
||||
class PlotNFTStore:
|
||||
db_wrapper: DBWrapper2
|
||||
genesis_challenge: bytes32
|
||||
|
||||
@classmethod
|
||||
async def create(cls, wrapper: DBWrapper2, genesis_challenge: bytes32) -> Self:
|
||||
self = cls()
|
||||
self.db_wrapper = wrapper
|
||||
self.genesis_challenge = genesis_challenge
|
||||
|
||||
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
||||
await conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS plotnft2s( "
|
||||
" coin_id blob PRIMARY KEY,"
|
||||
" parent_coin_id blob,"
|
||||
" puzzle_hash blob,"
|
||||
" amount blob,"
|
||||
" lineage_proof blob,"
|
||||
" launcher_id blob,"
|
||||
" synthetic_pubkey blob,"
|
||||
" pool_puzzle_hash blob,"
|
||||
" timelock blob,"
|
||||
" pool_memoization blob,"
|
||||
" exiting boolean,"
|
||||
" remark string,"
|
||||
" created_height int)"
|
||||
)
|
||||
|
||||
await conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS pool_reward2s( "
|
||||
" coin_id blob PRIMARY KEY,"
|
||||
" parent_coin_id blob,"
|
||||
" puzzle_hash blob,"
|
||||
" amount blob,"
|
||||
" singleton_id blob,"
|
||||
" height int,"
|
||||
" spent_height int)"
|
||||
)
|
||||
|
||||
await conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS finish_exiting_info (wallet_id int PRIMARY KEY, exiting_info blob)"
|
||||
)
|
||||
await conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS finish_exiting_height (wallet_id int PRIMARY KEY, height int)"
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
async def add_plotnft(self, *, plotnft: PlotNFT, created_height: uint32) -> None:
|
||||
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
||||
await conn.execute_insert(
|
||||
"INSERT OR REPLACE INTO plotnft2s "
|
||||
"(coin_id, parent_coin_id, puzzle_hash, amount, lineage_proof, launcher_id, synthetic_pubkey, "
|
||||
"pool_puzzle_hash, timelock, pool_memoization, exiting, remark, created_height) "
|
||||
"VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
plotnft.coin.name(),
|
||||
plotnft.coin.parent_coin_info,
|
||||
plotnft.coin.puzzle_hash,
|
||||
bytes(plotnft.coin.amount),
|
||||
bytes(plotnft.singleton_lineage_proof),
|
||||
plotnft.launcher_id,
|
||||
bytes(plotnft.user_config.synthetic_pubkey),
|
||||
plotnft.pool_config.pool_puzzle_hash if plotnft.pool_config is not None else b"",
|
||||
bytes(plotnft.pool_config.heightlock) if plotnft.pool_config is not None else b"",
|
||||
bytes(plotnft.pool_config.pool_memoization) if plotnft.pool_config is not None else b"",
|
||||
plotnft.exiting,
|
||||
str(plotnft.remarks[0].rest.atom, "utf8")
|
||||
if len(plotnft.remarks) > 0 and plotnft.remarks[0].rest.atom is not None
|
||||
else None,
|
||||
created_height,
|
||||
),
|
||||
)
|
||||
|
||||
async def add_pool_reward(self, *, pool_reward: PoolReward) -> None:
|
||||
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
||||
await conn.execute_insert(
|
||||
"INSERT OR REPLACE INTO pool_reward2s ("
|
||||
"coin_id, parent_coin_id, puzzle_hash, amount, singleton_id, height, spent_height) "
|
||||
"VALUES(?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
pool_reward.coin.name(),
|
||||
pool_reward.coin.parent_coin_info,
|
||||
pool_reward.coin.puzzle_hash,
|
||||
bytes(pool_reward.coin.amount),
|
||||
pool_reward.singleton_id,
|
||||
pool_reward.height,
|
||||
None,
|
||||
),
|
||||
)
|
||||
|
||||
async def mark_pool_reward_as_spent(self, *, reward_id: bytes32, spent_height: uint32) -> None:
|
||||
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
||||
await conn.execute_insert(
|
||||
"UPDATE pool_reward2s SET spent_height = ? WHERE coin_id = ?",
|
||||
(spent_height, reward_id),
|
||||
)
|
||||
|
||||
async def get_latest_plotnft(self, launcher_id: bytes32) -> PlotNFT:
|
||||
async with self.db_wrapper.reader() as conn:
|
||||
rows = await conn.execute_fetchall(
|
||||
"""
|
||||
SELECT *
|
||||
FROM plotnft2s
|
||||
WHERE launcher_id=?
|
||||
ORDER BY created_height DESC
|
||||
LIMIT 1;
|
||||
""",
|
||||
(launcher_id,),
|
||||
)
|
||||
return _row_to_plotnft(next(iter(rows)), self.genesis_challenge)
|
||||
|
||||
async def get_latest_remark(self, launcher_id: bytes32) -> str:
|
||||
async with self.db_wrapper.reader() as conn:
|
||||
rows = await conn.execute_fetchall(
|
||||
"""
|
||||
SELECT remark
|
||||
FROM plotnft2s
|
||||
WHERE launcher_id=?
|
||||
AND remark IS NOT NULL
|
||||
ORDER BY created_height DESC
|
||||
LIMIT 1;
|
||||
""",
|
||||
(launcher_id,),
|
||||
)
|
||||
return str(next(iter(rows))[0])
|
||||
|
||||
async def get_plotnfts(self, *, coin_ids: list[bytes32]) -> list[PlotNFT]:
|
||||
if coin_ids == []:
|
||||
raise ValueError("coin_ids must not be empty")
|
||||
async with self.db_wrapper.reader() as conn:
|
||||
rows = await conn.execute_fetchall(
|
||||
f"SELECT * from plotnft2s where coin_id in ({', '.join(['?'] * len(coin_ids))})", coin_ids
|
||||
)
|
||||
plot_nfts_selected = [_row_to_plotnft(row, self.genesis_challenge) for row in rows]
|
||||
if len(plot_nfts_selected) != len(coin_ids):
|
||||
symmetric_difference = set(bytes32(row[0]) for row in rows) ^ set(coin_ids)
|
||||
raise ValueError(f"coin IDs {symmetric_difference} not found in PlotNFTStore")
|
||||
else:
|
||||
return plot_nfts_selected
|
||||
|
||||
async def get_pool_rewards(
|
||||
self,
|
||||
*,
|
||||
plotnft_id: bytes32,
|
||||
max: int = DEFAULT_POOL_REWARDS_PER_CLAIM,
|
||||
include_spent: bool = False,
|
||||
) -> list[PoolReward]:
|
||||
async with self.db_wrapper.reader() as conn:
|
||||
rows = await conn.execute_fetchall(
|
||||
(
|
||||
"SELECT * from pool_reward2s WHERE singleton_id = ?"
|
||||
+ (" AND spent_height IS NULL" if not include_spent else "")
|
||||
+ " ORDER BY spent_height ASC LIMIT ?"
|
||||
),
|
||||
(plotnft_id, max),
|
||||
)
|
||||
pool_rewards_selected = [
|
||||
PoolReward(
|
||||
coin=Coin(
|
||||
parent_coin_info=bytes32(row[1]), puzzle_hash=bytes32(row[2]), amount=uint64.from_bytes(row[3])
|
||||
),
|
||||
singleton_id=bytes32(row[4]),
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
return pool_rewards_selected
|
||||
|
||||
async def add_exiting_info(self, *, exiting_info: PlotNFTTargetStateInfo) -> None:
|
||||
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
||||
await conn.execute(
|
||||
("INSERT OR REPLACE INTO finish_exiting_info (wallet_id, exiting_info) VALUES (?, ?)"),
|
||||
(exiting_info.wallet_id, bytes(exiting_info)),
|
||||
)
|
||||
|
||||
async def get_exiting_info(self, *, wallet_id: uint32) -> PlotNFTTargetStateInfo:
|
||||
async with self.db_wrapper.reader() as conn:
|
||||
rows = list(
|
||||
await conn.execute_fetchall(
|
||||
("SELECT exiting_info FROM finish_exiting_info WHERE wallet_id = ?"),
|
||||
(wallet_id,),
|
||||
)
|
||||
)
|
||||
if len(rows) == 0:
|
||||
return PlotNFTTargetStateInfo(
|
||||
wallet_id=wallet_id,
|
||||
exiting_fee=uint64(0),
|
||||
next_pool_url=None,
|
||||
next_pool_puzzle_hash=None,
|
||||
next_heightlock=None,
|
||||
next_pool_memoization=None,
|
||||
)
|
||||
else:
|
||||
return PlotNFTTargetStateInfo.from_bytes(rows[0][0])
|
||||
|
||||
async def add_exiting_height(self, *, wallet_id: uint32, height: uint32) -> None:
|
||||
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
||||
await conn.execute(
|
||||
("INSERT OR REPLACE INTO finish_exiting_height (wallet_id, height) VALUES (?, ?)"),
|
||||
(wallet_id, height),
|
||||
)
|
||||
|
||||
async def get_exiting_height(self, wallet_id: uint32) -> uint32 | None:
|
||||
async with self.db_wrapper.reader() as conn:
|
||||
rows = list(
|
||||
await conn.execute_fetchall(
|
||||
("SELECT height FROM finish_exiting_height WHERE wallet_id = ?"), (wallet_id,)
|
||||
)
|
||||
)
|
||||
if len(rows) == 0:
|
||||
return None
|
||||
else:
|
||||
return uint32(rows[0][0])
|
||||
|
||||
async def clear_exiting_info(self, wallet_id: uint32) -> None:
|
||||
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
||||
await conn.execute("DELETE FROM finish_exiting_info WHERE wallet_id = ?", (wallet_id,))
|
||||
await conn.execute("DELETE FROM finish_exiting_height WHERE wallet_id = ?", (wallet_id,))
|
||||
|
||||
async def rollback_to_block(self, *, height: int) -> None:
|
||||
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
||||
await conn.execute("DELETE FROM plotnft2s WHERE created_height > ?", (height,))
|
||||
await conn.execute("DELETE FROM pool_reward2s WHERE height > ?", (height,))
|
||||
await conn.execute("UPDATE pool_reward2s SET spent_height = NULL WHERE spent_height > ?", (height,))
|
||||
@@ -0,0 +1,545 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, ClassVar, cast, final
|
||||
|
||||
from chia_rs import G2Element
|
||||
from chia_rs.chia_rs import Coin, G1Element
|
||||
from chia_rs.sized_bytes import bytes32
|
||||
from chia_rs.sized_ints import uint32, uint64, uint128
|
||||
from typing_extensions import Self, Unpack
|
||||
|
||||
from chia.pools.plotnft_drivers import PlotNFT, PoolConfig, PoolReward, RewardPuzzle, SingletonStruct, UserConfig
|
||||
from chia.pools.pool_config import PoolingShareState
|
||||
from chia.server.ws_connection import WSChiaConnection
|
||||
from chia.types.blockchain_format.program import Program
|
||||
from chia.wallet.conditions import AssertCoinAnnouncement, Condition, CreateCoin, CreateCoinAnnouncement, Remark
|
||||
from chia.wallet.puzzles.custody.custody_architecture import DelegatedPuzzleAndSolution
|
||||
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_hash_for_synthetic_public_key
|
||||
from chia.wallet.util.wallet_types import WalletType
|
||||
from chia.wallet.wallet import Wallet
|
||||
from chia.wallet.wallet_action_scope import PlotNFTTargetStateInfo, WalletActionScope
|
||||
from chia.wallet.wallet_coin_record import WalletCoinRecord
|
||||
from chia.wallet.wallet_info import WalletInfo
|
||||
from chia.wallet.wallet_protocol import GSTOptionalArgs
|
||||
from chia.wallet.wallet_spend_bundle import WalletSpendBundle
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from chia.wallet.wallet_state_manager import WalletStateManager
|
||||
|
||||
|
||||
@final
|
||||
@dataclasses.dataclass
|
||||
class PlotNFT2Wallet:
|
||||
if TYPE_CHECKING:
|
||||
from chia.wallet.wallet_protocol import WalletProtocol
|
||||
|
||||
_protocol_check: ClassVar[WalletProtocol[object]] = cast("PlotNFT2Wallet", None)
|
||||
|
||||
wallet_state_manager: WalletStateManager
|
||||
xch_wallet: Wallet
|
||||
log: logging.Logger
|
||||
wallet_info: WalletInfo
|
||||
|
||||
@classmethod
|
||||
async def create(
|
||||
cls, *, wallet_state_manager: WalletStateManager, xch_wallet: Wallet, wallet_info: WalletInfo
|
||||
) -> Self:
|
||||
self = cls(
|
||||
wallet_state_manager=wallet_state_manager,
|
||||
xch_wallet=xch_wallet,
|
||||
log=logging.getLogger(__name__),
|
||||
wallet_info=wallet_info,
|
||||
)
|
||||
await wallet_state_manager.add_interested_puzzle_hashes(
|
||||
puzzle_hashes=[self.p2_singleton_puzzle_hash, self.hint], wallet_ids=[self.id(), self.id()]
|
||||
)
|
||||
if await wallet_state_manager.user_store.get_wallet_by_id(wallet_info.id) is None:
|
||||
await wallet_state_manager.user_store.create_wallet(
|
||||
name=wallet_info.name,
|
||||
wallet_type=wallet_info.type,
|
||||
data=wallet_info.data,
|
||||
id=wallet_info.id,
|
||||
)
|
||||
return self
|
||||
|
||||
@property
|
||||
def hint(self) -> bytes32:
|
||||
return SingletonStruct(launcher_id=self.plotnft_id).struct_hash()
|
||||
|
||||
@property
|
||||
def plotnft_id(self) -> bytes32:
|
||||
return bytes32.from_hexstr(self.wallet_info.data)
|
||||
|
||||
@property
|
||||
def p2_singleton_puzzle_hash(self) -> bytes32:
|
||||
return RewardPuzzle(singleton_id=self.plotnft_id).puzzle_hash()
|
||||
|
||||
@property
|
||||
def rewards_claim_puzhash(self) -> bytes32:
|
||||
with PoolingShareState.acquire(
|
||||
root_path=self.wallet_state_manager.root_path, p2_singleton_puzzle_hash=self.p2_singleton_puzzle_hash
|
||||
) as pool_config:
|
||||
return bytes32.from_hexstr(pool_config.payout_instructions)
|
||||
|
||||
@classmethod
|
||||
def type(cls) -> WalletType:
|
||||
return WalletType.PLOTNFT_2
|
||||
|
||||
def id(self) -> uint32:
|
||||
return self.wallet_info.id
|
||||
|
||||
def get_name(self) -> str:
|
||||
# A WalletProtocol stub that we may as well implement but there's no real use for it
|
||||
# Probably a good case to get it out of the WalletProtocol
|
||||
return self.wallet_info.name # pragma: no cover
|
||||
|
||||
# Actions
|
||||
@classmethod
|
||||
async def create_new(
|
||||
cls,
|
||||
*,
|
||||
wallet_state_manager: WalletStateManager,
|
||||
xch_wallet: Wallet,
|
||||
action_scope: WalletActionScope,
|
||||
fee: uint64,
|
||||
pool_config: PoolConfig | None = None,
|
||||
pool_url: str | None = None,
|
||||
extra_conditions: tuple[Condition, ...] = tuple(),
|
||||
) -> PlotNFT:
|
||||
if (pool_url is None and pool_config is not None) or (pool_url is not None and pool_config is None):
|
||||
raise ValueError("pool_url and pool_config must be both None or both not None")
|
||||
target_puzzle_hash = await action_scope.get_puzzle_hash(wallet_state_manager)
|
||||
target_pubkey = G1Element.from_bytes(await wallet_state_manager.get_public_key(target_puzzle_hash))
|
||||
origin_coins = await xch_wallet.select_coins(amount=uint64(fee + 1), action_scope=action_scope)
|
||||
announcement_assertions, coin_spends, new_plotnft = PlotNFT.launch(
|
||||
origin_coins=list(origin_coins),
|
||||
user_config=UserConfig(synthetic_pubkey=xch_wallet.convert_public_key_to_synthetic(target_pubkey)),
|
||||
genesis_challenge=wallet_state_manager.constants.GENESIS_CHALLENGE,
|
||||
hint=target_puzzle_hash,
|
||||
pool_config=pool_config,
|
||||
remark=Remark(rest=Program.to(pool_url)) if pool_url is not None else None,
|
||||
)
|
||||
async with action_scope.use() as interface:
|
||||
interface.side_effects.extra_spends.append(WalletSpendBundle(coin_spends, G2Element()))
|
||||
await xch_wallet.generate_signed_transaction(
|
||||
amounts=[uint64(1)],
|
||||
puzzle_hashes=[new_plotnft.singleton_struct.singleton_puzzles.singleton_launcher_hash],
|
||||
action_scope=action_scope,
|
||||
fee=fee,
|
||||
coins=origin_coins,
|
||||
extra_conditions=(*announcement_assertions, *extra_conditions),
|
||||
origin_id=coin_spends[0].coin.parent_coin_info,
|
||||
)
|
||||
return new_plotnft
|
||||
|
||||
async def claim_rewards(
|
||||
self,
|
||||
*,
|
||||
action_scope: WalletActionScope,
|
||||
fee: uint64 = uint64(0),
|
||||
extra_conditions: tuple[Condition, ...] = tuple(),
|
||||
) -> None:
|
||||
rewards_to_claim = await self.wallet_state_manager.plotnft2_store.get_pool_rewards(plotnft_id=self.plotnft_id)
|
||||
if len(rewards_to_claim) == 0:
|
||||
raise ValueError("No rewards to claim")
|
||||
total_reward_amount = uint64(sum(reward.coin.amount for reward in rewards_to_claim))
|
||||
if fee > total_reward_amount:
|
||||
raise ValueError("Fee is greater than the total amount of rewards")
|
||||
|
||||
plotnft = await self.get_current_plotnft()
|
||||
coin_spends = plotnft.claim_pool_rewards(
|
||||
rewards_to_claim=rewards_to_claim,
|
||||
reward_delegated_puzzles_and_solutions=[
|
||||
DelegatedPuzzleAndSolution(
|
||||
puzzle=self.xch_wallet.make_solution(
|
||||
primaries=[
|
||||
CreateCoin(
|
||||
puzzle_hash=self.rewards_claim_puzhash,
|
||||
amount=uint64(total_reward_amount - fee),
|
||||
),
|
||||
],
|
||||
fee=fee,
|
||||
conditions=(*extra_conditions, CreateCoinAnnouncement(b""))
|
||||
if len(rewards_to_claim) > 1
|
||||
else extra_conditions,
|
||||
).at("rf"), # strips away to just the delegated puzzle (bit of a hack)
|
||||
solution=Program.to(None),
|
||||
)
|
||||
if i == 0
|
||||
else DelegatedPuzzleAndSolution(
|
||||
puzzle=Program.to(
|
||||
(
|
||||
1,
|
||||
[
|
||||
AssertCoinAnnouncement(
|
||||
asserted_id=rewards_to_claim[0].coin.name(), asserted_msg=b""
|
||||
).to_program()
|
||||
],
|
||||
)
|
||||
),
|
||||
solution=Program.to(None),
|
||||
)
|
||||
for i, reward in enumerate(rewards_to_claim)
|
||||
],
|
||||
)
|
||||
|
||||
spend_bundle = WalletSpendBundle(coin_spends, G2Element())
|
||||
|
||||
async with action_scope.use() as interface:
|
||||
interface.side_effects.transactions.append(
|
||||
self.wallet_state_manager.new_outgoing_transaction(
|
||||
wallet_id=self.id(),
|
||||
puzzle_hash=self.rewards_claim_puzhash,
|
||||
amount=total_reward_amount,
|
||||
fee=fee,
|
||||
spend_bundle=spend_bundle,
|
||||
additions=[
|
||||
Coin(
|
||||
parent_coin_info=rewards_to_claim[0].coin.name(),
|
||||
puzzle_hash=self.rewards_claim_puzhash,
|
||||
amount=uint64(total_reward_amount - fee),
|
||||
),
|
||||
Coin(
|
||||
parent_coin_info=plotnft.coin.name(),
|
||||
puzzle_hash=plotnft.puzzle_hash(nonce=uint64(0)),
|
||||
amount=uint64(1),
|
||||
),
|
||||
],
|
||||
removals=[reward.coin for reward in rewards_to_claim] + [plotnft.coin],
|
||||
name=spend_bundle.name(),
|
||||
extra_conditions=extra_conditions,
|
||||
)
|
||||
)
|
||||
|
||||
async def join_pool(
|
||||
self,
|
||||
*,
|
||||
pool_config: PoolConfig,
|
||||
action_scope: WalletActionScope,
|
||||
fee: uint64 = uint64(0),
|
||||
finish_leaving_fee: uint64 = uint64(0),
|
||||
pool_url: str,
|
||||
extra_conditions: tuple[Condition, ...] = tuple(),
|
||||
plotnft_override: PlotNFT | None = None,
|
||||
) -> None:
|
||||
if plotnft_override is None:
|
||||
plotnft = await self.get_current_plotnft()
|
||||
else:
|
||||
plotnft = plotnft_override
|
||||
if plotnft.pool_config is not None:
|
||||
await self.leave_pool(
|
||||
action_scope=action_scope,
|
||||
fee=fee,
|
||||
finish_leaving_fee=finish_leaving_fee,
|
||||
extra_conditions=extra_conditions,
|
||||
new_pool_url=pool_url,
|
||||
new_pool_config=pool_config,
|
||||
)
|
||||
return
|
||||
elif finish_leaving_fee != uint64(0):
|
||||
raise ValueError("A fee to finish leaving was specified but PlotNFT does not need to leave")
|
||||
fee_hook = CreateCoinAnnouncement(msg=b"", coin_id=plotnft.coin.name())
|
||||
url_remark = Remark(rest=Program.to(pool_url))
|
||||
coin_spends = plotnft.join_pool(
|
||||
user_config=plotnft.user_config,
|
||||
pool_config=pool_config,
|
||||
extra_conditions=(*extra_conditions, fee_hook, url_remark),
|
||||
)
|
||||
if fee > 0:
|
||||
await self.xch_wallet.create_tandem_xch_tx(
|
||||
fee=fee,
|
||||
action_scope=action_scope,
|
||||
extra_conditions=(fee_hook.corresponding_assertion(),),
|
||||
)
|
||||
|
||||
spend_bundle = WalletSpendBundle(coin_spends, G2Element())
|
||||
|
||||
async with action_scope.use() as interface:
|
||||
interface.side_effects.transactions.append(
|
||||
self.wallet_state_manager.new_outgoing_transaction(
|
||||
wallet_id=self.id(),
|
||||
puzzle_hash=pool_config.pool_puzzle_hash,
|
||||
amount=uint64(1),
|
||||
fee=fee,
|
||||
spend_bundle=spend_bundle,
|
||||
additions=[
|
||||
Coin(
|
||||
parent_coin_info=plotnft.coin.name(),
|
||||
puzzle_hash=dataclasses.replace(plotnft, pool_config=pool_config).puzzle_hash(nonce=0),
|
||||
amount=uint64(1),
|
||||
)
|
||||
],
|
||||
removals=[plotnft.coin],
|
||||
name=spend_bundle.name(),
|
||||
extra_conditions=extra_conditions,
|
||||
)
|
||||
)
|
||||
|
||||
async def leave_pool(
|
||||
self,
|
||||
*,
|
||||
action_scope: WalletActionScope,
|
||||
fee: uint64 = uint64(0),
|
||||
finish_leaving_fee: uint64 = uint64(0),
|
||||
extra_conditions: tuple[Condition, ...] = tuple(),
|
||||
new_pool_url: str | None = None,
|
||||
new_pool_config: PoolConfig | None = None,
|
||||
) -> None:
|
||||
if (new_pool_url is None and new_pool_config is not None) or (
|
||||
new_pool_url is not None and new_pool_config is None
|
||||
):
|
||||
raise ValueError("Both new_pool_url or new_pool_config must be provided together")
|
||||
plotnft = await self.get_current_plotnft()
|
||||
if not plotnft.pooling or plotnft.exiting:
|
||||
raise ValueError("`leave_pool` called on a non-pooling or exiting PlotNFT")
|
||||
next_plotnft = dataclasses.replace(plotnft, exiting=True)
|
||||
fee_hook = CreateCoinAnnouncement(msg=b"", coin_id=plotnft.coin.name())
|
||||
exit_create_coin = plotnft.exit_to_waiting_room_condition()
|
||||
exit_to_waiting_room_dpuz_and_sol = DelegatedPuzzleAndSolution(
|
||||
puzzle=self.xch_wallet.make_solution(
|
||||
primaries=[exit_create_coin],
|
||||
conditions=(*extra_conditions, fee_hook),
|
||||
).at("rf"), # strips away to just the delegated puzzle (bit of a hack)
|
||||
solution=Program.to(None),
|
||||
)
|
||||
coin_spends = plotnft.exit_to_waiting_room(exit_to_waiting_room_dpuz_and_sol)
|
||||
if fee > 0:
|
||||
await self.xch_wallet.create_tandem_xch_tx(
|
||||
fee=fee,
|
||||
action_scope=action_scope,
|
||||
extra_conditions=(fee_hook.corresponding_assertion(),),
|
||||
)
|
||||
|
||||
spend_bundle = WalletSpendBundle(coin_spends, G2Element())
|
||||
|
||||
async with action_scope.use() as interface:
|
||||
interface.side_effects.plotnft_exiting_info = PlotNFTTargetStateInfo(
|
||||
wallet_id=self.id(),
|
||||
exiting_fee=finish_leaving_fee,
|
||||
next_pool_url=new_pool_url,
|
||||
next_pool_puzzle_hash=new_pool_config.pool_puzzle_hash if new_pool_config is not None else None,
|
||||
next_heightlock=new_pool_config.heightlock if new_pool_config is not None else None,
|
||||
next_pool_memoization=new_pool_config.pool_memoization if new_pool_config is not None else None,
|
||||
)
|
||||
interface.side_effects.transactions.append(
|
||||
self.wallet_state_manager.new_outgoing_transaction(
|
||||
wallet_id=self.id(),
|
||||
puzzle_hash=exit_create_coin.puzzle_hash,
|
||||
amount=uint64(1),
|
||||
fee=fee,
|
||||
spend_bundle=spend_bundle,
|
||||
additions=[
|
||||
Coin(
|
||||
parent_coin_info=plotnft.coin.name(),
|
||||
puzzle_hash=next_plotnft.puzzle_hash(nonce=0),
|
||||
amount=uint64(1),
|
||||
)
|
||||
],
|
||||
removals=[plotnft.coin],
|
||||
name=spend_bundle.name(),
|
||||
extra_conditions=extra_conditions,
|
||||
)
|
||||
)
|
||||
|
||||
async def _finish_leaving_pool(
|
||||
self,
|
||||
*,
|
||||
action_scope: WalletActionScope,
|
||||
exiting_info: PlotNFTTargetStateInfo,
|
||||
extra_conditions: tuple[Condition, ...] = tuple(),
|
||||
) -> None:
|
||||
plotnft = await self.get_current_plotnft()
|
||||
fee_hook = CreateCoinAnnouncement(msg=b"", coin_id=plotnft.coin.name())
|
||||
heightlock, exit_create_coin = plotnft.exit_from_waiting_room_conditions()
|
||||
exit_to_waiting_room_dpuz_and_sol = DelegatedPuzzleAndSolution(
|
||||
puzzle=self.xch_wallet.make_solution(
|
||||
primaries=[exit_create_coin],
|
||||
conditions=(fee_hook, heightlock, *extra_conditions),
|
||||
).at("rf"), # strips away to just the delegated puzzle (bit of a hack)
|
||||
solution=Program.to(None),
|
||||
)
|
||||
coin_spends = plotnft.exit_waiting_room(exit_to_waiting_room_dpuz_and_sol)
|
||||
next_plotnft = PlotNFT.get_next_from_coin_spend(
|
||||
coin_spend=coin_spends[0],
|
||||
genesis_challenge=self.wallet_state_manager.constants.GENESIS_CHALLENGE,
|
||||
previous_plotnft_puzzle=plotnft,
|
||||
)
|
||||
if exiting_info.exiting_fee > 0:
|
||||
await self.xch_wallet.create_tandem_xch_tx(
|
||||
fee=exiting_info.exiting_fee,
|
||||
action_scope=action_scope,
|
||||
extra_conditions=(fee_hook.corresponding_assertion(),),
|
||||
)
|
||||
|
||||
spend_bundle = WalletSpendBundle(coin_spends, G2Element())
|
||||
|
||||
async with action_scope.use() as interface:
|
||||
interface.side_effects.transactions.append(
|
||||
self.wallet_state_manager.new_outgoing_transaction(
|
||||
wallet_id=self.id(),
|
||||
puzzle_hash=exit_create_coin.puzzle_hash,
|
||||
amount=uint64(1),
|
||||
fee=exiting_info.exiting_fee,
|
||||
spend_bundle=spend_bundle,
|
||||
additions=[
|
||||
Coin(
|
||||
parent_coin_info=plotnft.coin.name(),
|
||||
puzzle_hash=dataclasses.replace(plotnft, pool_config=None, exiting=False).puzzle_hash(
|
||||
nonce=0
|
||||
),
|
||||
amount=uint64(1),
|
||||
)
|
||||
],
|
||||
removals=[plotnft.coin],
|
||||
name=spend_bundle.name(),
|
||||
extra_conditions=(heightlock,),
|
||||
)
|
||||
)
|
||||
if exiting_info.pool_url_and_config is not None:
|
||||
pool_url, pool_config = exiting_info.pool_url_and_config
|
||||
await self.join_pool(
|
||||
action_scope=action_scope,
|
||||
pool_config=pool_config,
|
||||
pool_url=pool_url,
|
||||
plotnft_override=next_plotnft,
|
||||
)
|
||||
|
||||
# Syncing
|
||||
async def coin_added(self, coin: Coin, height: uint32, peer: WSChiaConnection, coin_data: object | None) -> None:
|
||||
if isinstance(coin_data, PlotNFT):
|
||||
index = await self.wallet_state_manager.puzzle_store.index_for_puzzle_hash(
|
||||
puzzle_hash_for_synthetic_public_key(coin_data.user_config.synthetic_pubkey)
|
||||
)
|
||||
if index is None:
|
||||
raise ValueError(f"No index found for synthetic pubkey for launcher_id: {coin_data.launcher_id}")
|
||||
await self.wallet_state_manager.plotnft2_store.add_plotnft(plotnft=coin_data, created_height=height)
|
||||
self.log.info(f"Added PlotNFT {coin_data} at height {height}")
|
||||
if self.p2_singleton_puzzle_hash in PoolingShareState.get_all_p2_singleton_puzzle_hashes(
|
||||
root_path=self.wallet_state_manager.root_path
|
||||
):
|
||||
with PoolingShareState.acquire(
|
||||
root_path=self.wallet_state_manager.root_path,
|
||||
p2_singleton_puzzle_hash=self.p2_singleton_puzzle_hash,
|
||||
) as pool_config:
|
||||
pool_config.owner_public_key = coin_data.user_config.synthetic_pubkey
|
||||
pool_config.key_derivation_index = int(index)
|
||||
if coin_data.pool_config is not None:
|
||||
if coin_data.pool_config.pool_puzzle_hash != pool_config.target_puzzle_hash:
|
||||
pool_config.pool_url = await self.wallet_state_manager.plotnft2_store.get_latest_remark(
|
||||
coin_data.launcher_id
|
||||
)
|
||||
pool_config.target_puzzle_hash = coin_data.pool_config.pool_puzzle_hash
|
||||
else:
|
||||
pool_config.target_puzzle_hash = bytes32.from_hexstr(pool_config.payout_instructions)
|
||||
else:
|
||||
async with self.wallet_state_manager.new_action_scope(
|
||||
self.wallet_state_manager.tx_config, push=True
|
||||
) as action_scope:
|
||||
payout_puzzle_hash = await action_scope.get_puzzle_hash(self.wallet_state_manager)
|
||||
PoolingShareState(
|
||||
launcher_id=coin_data.launcher_id,
|
||||
pool_url=await self.wallet_state_manager.plotnft2_store.get_latest_remark(coin_data.launcher_id)
|
||||
if coin_data.pool_config is not None
|
||||
else "",
|
||||
owner_public_key=coin_data.user_config.synthetic_pubkey,
|
||||
target_puzzle_hash=coin_data.pool_config.pool_puzzle_hash
|
||||
if coin_data.pool_config is not None
|
||||
else payout_puzzle_hash,
|
||||
p2_singleton_puzzle_hash=self.p2_singleton_puzzle_hash,
|
||||
payout_instructions=payout_puzzle_hash.hex(),
|
||||
key_derivation_index=int(index),
|
||||
version=2,
|
||||
).add(root_path=self.wallet_state_manager.root_path)
|
||||
|
||||
if coin_data.exiting:
|
||||
await self.wallet_state_manager.plotnft2_store.add_exiting_height(
|
||||
wallet_id=self.id(), height=uint32(height + coin_data.guaranteed_pool_config.heightlock)
|
||||
)
|
||||
else:
|
||||
finish_height = await self.wallet_state_manager.plotnft2_store.get_exiting_height(wallet_id=self.id())
|
||||
if finish_height is not None and finish_height < height:
|
||||
await self.wallet_state_manager.plotnft2_store.clear_exiting_info(wallet_id=self.id())
|
||||
elif coin_data is None and coin.puzzle_hash == self.p2_singleton_puzzle_hash:
|
||||
if coin.parent_coin_info[0:16] == self.wallet_state_manager.constants.GENESIS_CHALLENGE[0:16]:
|
||||
await self.wallet_state_manager.plotnft2_store.add_pool_reward(
|
||||
pool_reward=PoolReward(singleton_id=self.plotnft_id, coin=coin)
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"A non-pooling reward coin was paid to PlotNFT with id: {self.plotnft_id}")
|
||||
|
||||
async def new_peak(self, height: uint32) -> None:
|
||||
finish_height = await self.wallet_state_manager.plotnft2_store.get_exiting_height(wallet_id=self.id())
|
||||
if finish_height is not None and finish_height <= height - 2: # 2 blocks for a little reorg safety
|
||||
if await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(wallet_id=self.id()) != []:
|
||||
self.log.info(f"Not finishing plotnft from wallet {self.id()} due to unconfirmed transactions")
|
||||
return None
|
||||
finish_info = await self.wallet_state_manager.plotnft2_store.get_exiting_info(wallet_id=self.id())
|
||||
async with self.wallet_state_manager.new_action_scope(
|
||||
self.wallet_state_manager.tx_config, push=True, sign=True, merge_spends=True
|
||||
) as action_scope:
|
||||
await self._finish_leaving_pool(action_scope=action_scope, exiting_info=finish_info)
|
||||
|
||||
# State
|
||||
async def get_current_plotnft(self) -> PlotNFT:
|
||||
return await self.wallet_state_manager.plotnft2_store.get_latest_plotnft(self.plotnft_id)
|
||||
|
||||
async def get_confirmed_balance(self, record_list: set[WalletCoinRecord] | None = None) -> uint128:
|
||||
return uint128(
|
||||
sum(
|
||||
cr.coin.amount
|
||||
for cr in await self.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(self.id())
|
||||
if cr.coin.amount != 1 # bit of a hack, but should work well enough to filter out the plotnft
|
||||
)
|
||||
)
|
||||
|
||||
async def get_unconfirmed_balance(self, unspent_records: set[WalletCoinRecord] | None = None) -> uint128:
|
||||
# bit of a hack, but should work well enough to filter out the plotnft
|
||||
if unspent_records is None:
|
||||
unspent_records = await self.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(self.id())
|
||||
unspent_records = set(cr for cr in unspent_records if cr.coin.amount != 1)
|
||||
|
||||
return await self.wallet_state_manager.get_confirmed_spendable_balance_for_wallet(self.id(), unspent_records)
|
||||
|
||||
async def get_spendable_balance(self, unspent_records: set[WalletCoinRecord] | None = None) -> uint128:
|
||||
return await self.get_unconfirmed_balance(unspent_records=unspent_records)
|
||||
|
||||
async def get_pending_change_balance(self) -> uint64:
|
||||
return uint64(0)
|
||||
|
||||
async def get_max_send_amount(self, records: set[WalletCoinRecord] | None = None) -> uint128:
|
||||
return await self.get_spendable_balance(records)
|
||||
|
||||
async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool: # pragma: no cover
|
||||
# We're choosing not to implement this for now as it shouldn't be necessary
|
||||
return False
|
||||
|
||||
# Wallet Protocol Stubs
|
||||
def puzzle_hash_for_pk(self, pubkey: G1Element) -> bytes32: # pragma: no cover
|
||||
raise RuntimeError("puzzle_hash_for_pk is not implemented for PlotNFT2Wallet")
|
||||
|
||||
def require_derivation_paths(self) -> bool:
|
||||
return False
|
||||
|
||||
async def generate_signed_transaction(
|
||||
self,
|
||||
amounts: list[uint64],
|
||||
puzzle_hashes: list[bytes32],
|
||||
action_scope: WalletActionScope,
|
||||
fee: uint64 = uint64(0),
|
||||
coins: set[Coin] | None = None,
|
||||
memos: list[list[bytes]] | None = None,
|
||||
extra_conditions: tuple[Condition, ...] = tuple(),
|
||||
**kwargs: Unpack[GSTOptionalArgs],
|
||||
) -> None: # pragma: no cover
|
||||
raise RuntimeError(
|
||||
"generate_signed_transaction is not implemented for PlotNFT2Wallet. Try join/leave/exit_pool instead."
|
||||
)
|
||||
|
||||
async def select_coins(
|
||||
self,
|
||||
amount: uint64,
|
||||
action_scope: WalletActionScope,
|
||||
) -> set[Coin]: # pragma: no cover
|
||||
raise RuntimeError("PlotNFT2Wallet does not support select_coins()")
|
||||
@@ -21,7 +21,7 @@ def compute_memos_for_spend(coin_spend: CoinSpend) -> dict[bytes32, list[bytes]]
|
||||
if type(condition[3]) is not list:
|
||||
# If it's not a list, it's not the correct format
|
||||
continue
|
||||
memos[coin_added.name()] = condition[3]
|
||||
memos[coin_added.name()] = [mem for mem in condition[3] if isinstance(mem, bytes)]
|
||||
return memos
|
||||
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ class WalletType(IntEnum):
|
||||
VC = 13
|
||||
CRCAT = 57
|
||||
RCAT = 132
|
||||
PLOTNFT_2 = 209
|
||||
REMOTE = 205
|
||||
|
||||
def to_json_dict(self) -> str:
|
||||
|
||||
@@ -31,6 +31,7 @@ from chia.wallet.puzzles.clawback.metadata import ClawbackMetadata
|
||||
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
|
||||
DEFAULT_HIDDEN_PUZZLE_HASH,
|
||||
calculate_synthetic_offset,
|
||||
calculate_synthetic_public_key,
|
||||
calculate_synthetic_secret_key,
|
||||
puzzle_for_pk,
|
||||
puzzle_hash_for_pk,
|
||||
@@ -114,6 +115,9 @@ class Wallet:
|
||||
def convert_secret_key_to_synthetic(self, secret_key: PrivateKey) -> PrivateKey:
|
||||
return calculate_synthetic_secret_key(secret_key, DEFAULT_HIDDEN_PUZZLE_HASH)
|
||||
|
||||
def convert_public_key_to_synthetic(self, public_key: G1Element) -> G1Element:
|
||||
return calculate_synthetic_public_key(public_key, DEFAULT_HIDDEN_PUZZLE_HASH)
|
||||
|
||||
async def get_confirmed_balance(self, record_list: set[WalletCoinRecord] | None = None) -> uint128:
|
||||
return await self.wallet_state_manager.get_confirmed_balance_for_wallet(self.id(), record_list)
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@ from typing import TYPE_CHECKING, cast, final
|
||||
|
||||
from chia_rs.chia_rs import G1Element
|
||||
from chia_rs.sized_bytes import bytes32
|
||||
from chia_rs.sized_ints import uint32, uint64
|
||||
|
||||
from chia.data_layer.singleton_record import SingletonRecord
|
||||
from chia.pools.plotnft_drivers import PoolConfig
|
||||
from chia.types.blockchain_format.coin import Coin
|
||||
from chia.types.blockchain_format.program import Program
|
||||
from chia.util.action_scope import ActionScope
|
||||
@@ -25,6 +27,50 @@ if TYPE_CHECKING:
|
||||
from chia.wallet.wallet_state_manager import WalletStateManager
|
||||
|
||||
|
||||
@streamable
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class PlotNFTTargetStateInfo(Streamable):
|
||||
wallet_id: uint32
|
||||
exiting_fee: uint64
|
||||
next_pool_url: str | None
|
||||
next_pool_puzzle_hash: bytes32 | None
|
||||
next_heightlock: uint32 | None
|
||||
next_pool_memoization: Program | None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if (self.next_pool_url, self.next_pool_puzzle_hash, self.next_heightlock, self.next_pool_memoization) != (
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
) and (
|
||||
None
|
||||
in {
|
||||
self.next_pool_url,
|
||||
self.next_pool_puzzle_hash,
|
||||
self.next_heightlock,
|
||||
}
|
||||
or self.next_pool_memoization is None
|
||||
):
|
||||
raise ValueError("Error initializing next PlotNFT target state, not all options for join were specified")
|
||||
return super().__post_init__()
|
||||
|
||||
@property
|
||||
def pool_url_and_config(self) -> tuple[str, PoolConfig] | None:
|
||||
if (
|
||||
self.next_pool_url is None
|
||||
or self.next_pool_puzzle_hash is None
|
||||
or self.next_heightlock is None
|
||||
or self.next_pool_memoization is None
|
||||
):
|
||||
return None
|
||||
return self.next_pool_url, PoolConfig(
|
||||
pool_puzzle_hash=self.next_pool_puzzle_hash,
|
||||
heightlock=self.next_heightlock,
|
||||
pool_memoization=self.next_pool_memoization,
|
||||
)
|
||||
|
||||
|
||||
@streamable
|
||||
@dataclass(frozen=True)
|
||||
class _StreamableWalletSideEffects(Streamable):
|
||||
@@ -33,6 +79,7 @@ class _StreamableWalletSideEffects(Streamable):
|
||||
extra_spends: list[WalletSpendBundle]
|
||||
selected_coins: list[Coin]
|
||||
singleton_records: list[SingletonRecord]
|
||||
plotnft_exiting_info: PlotNFTTargetStateInfo | None
|
||||
get_unused_derivation_record_result: StreambleGetUnusedDerivationRecordResult | None
|
||||
|
||||
|
||||
@@ -43,6 +90,7 @@ class WalletSideEffects:
|
||||
extra_spends: list[WalletSpendBundle] = field(default_factory=list)
|
||||
selected_coins: list[Coin] = field(default_factory=list)
|
||||
singleton_records: list[SingletonRecord] = field(default_factory=list)
|
||||
plotnft_exiting_info: PlotNFTTargetStateInfo | None = None
|
||||
get_unused_derivation_record_result: StreambleGetUnusedDerivationRecordResult | None = None
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
@@ -156,14 +204,16 @@ async def new_wallet_action_scope(
|
||||
|
||||
yield self
|
||||
|
||||
self.side_effects.transactions = await wallet_state_manager.add_pending_transactions(
|
||||
self.side_effects.transactions,
|
||||
push=push,
|
||||
merge_spends=merge_spends,
|
||||
sign=sign,
|
||||
additional_signing_responses=self.side_effects.signing_responses,
|
||||
extra_spends=self.side_effects.extra_spends,
|
||||
singleton_records=self.side_effects.singleton_records,
|
||||
)
|
||||
if self.side_effects.transactions != []:
|
||||
self.side_effects.transactions = await wallet_state_manager.add_pending_transactions(
|
||||
self.side_effects.transactions,
|
||||
push=push,
|
||||
merge_spends=merge_spends,
|
||||
sign=sign,
|
||||
additional_signing_responses=self.side_effects.signing_responses,
|
||||
extra_spends=self.side_effects.extra_spends,
|
||||
singleton_records=self.side_effects.singleton_records,
|
||||
plotnft_exiting_info=self.side_effects.plotnft_exiting_info,
|
||||
)
|
||||
if push and self.side_effects.get_unused_derivation_record_result is not None:
|
||||
await self.side_effects.get_unused_derivation_record_result.to_standard().commit(wallet_state_manager)
|
||||
|
||||
@@ -27,10 +27,8 @@ from chia.data_layer.data_layer_wallet import DataLayerWallet
|
||||
from chia.data_layer.dl_wallet_store import DataLayerStore
|
||||
from chia.data_layer.singleton_record import SingletonRecord
|
||||
from chia.pools import pool_config
|
||||
from chia.pools.pool_puzzles import (
|
||||
get_most_recent_singleton_coin_from_coin_spend,
|
||||
solution_to_pool_state,
|
||||
)
|
||||
from chia.pools.plotnft_drivers import GetNextPlotNFTError, PlotNFT
|
||||
from chia.pools.pool_puzzles import get_most_recent_singleton_coin_from_coin_spend, solution_to_pool_state
|
||||
from chia.pools.pool_wallet import PoolWallet
|
||||
from chia.protocols.outbound_message import NodeType
|
||||
from chia.rpc.rpc_server import StateChangedProtocol
|
||||
@@ -89,6 +87,8 @@ from chia.wallet.nft_wallet.nft_wallet import NFTWallet
|
||||
from chia.wallet.nft_wallet.uncurry_nft import NFTCoinData, UncurriedNFT
|
||||
from chia.wallet.notification_manager import NotificationManager
|
||||
from chia.wallet.outer_puzzles import AssetType
|
||||
from chia.wallet.plotnft_wallet.plotnft_store import PlotNFTStore
|
||||
from chia.wallet.plotnft_wallet.plotnft_wallet import PlotNFT2Wallet
|
||||
from chia.wallet.puzzle_drivers import PuzzleInfo
|
||||
from chia.wallet.puzzles.clawback.drivers import generate_clawback_spend_bundle, match_clawback_puzzle
|
||||
from chia.wallet.puzzles.clawback.metadata import ClawbackMetadata, ClawbackVersion
|
||||
@@ -134,7 +134,7 @@ from chia.wallet.vc_wallet.vc_drivers import VerifiedCredential, match_revocatio
|
||||
from chia.wallet.vc_wallet.vc_store import VCStore
|
||||
from chia.wallet.vc_wallet.vc_wallet import VCWallet
|
||||
from chia.wallet.wallet import Wallet
|
||||
from chia.wallet.wallet_action_scope import WalletActionScope, new_wallet_action_scope
|
||||
from chia.wallet.wallet_action_scope import PlotNFTTargetStateInfo, WalletActionScope, new_wallet_action_scope
|
||||
from chia.wallet.wallet_blockchain import WalletBlockchain
|
||||
from chia.wallet.wallet_coin_record import MetadataTypes, WalletCoinRecord
|
||||
from chia.wallet.wallet_coin_store import CoinRecordOrder, WalletCoinStore
|
||||
@@ -206,6 +206,7 @@ class WalletStateManager:
|
||||
interested_store: WalletInterestedStore
|
||||
remote_coin_store: RemoteCoinStore
|
||||
retry_store: WalletRetryStore
|
||||
plotnft2_store: PlotNFTStore
|
||||
multiprocessing_context: multiprocessing.context.BaseContext
|
||||
server: ChiaServer
|
||||
root_path: Path
|
||||
@@ -268,6 +269,7 @@ class WalletStateManager:
|
||||
self.interested_store = await WalletInterestedStore.create(self.db_wrapper)
|
||||
self.remote_coin_store = await RemoteCoinStore.create(self.db_wrapper)
|
||||
self.retry_store = await WalletRetryStore.create(self.db_wrapper)
|
||||
self.plotnft2_store = await PlotNFTStore.create(self.db_wrapper, self.constants.GENESIS_CHALLENGE)
|
||||
self.default_cats = DEFAULT_CATS
|
||||
|
||||
self.wallet_node = wallet_node
|
||||
@@ -360,6 +362,12 @@ class WalletStateManager:
|
||||
self.main_wallet,
|
||||
wallet_info,
|
||||
)
|
||||
elif wallet_type == WalletType.PLOTNFT_2:
|
||||
wallet = await PlotNFT2Wallet.create(
|
||||
wallet_state_manager=self,
|
||||
xch_wallet=self.main_wallet,
|
||||
wallet_info=wallet_info,
|
||||
)
|
||||
elif wallet_type == WalletType.REMOTE:
|
||||
wallet = await RemoteWallet.create(
|
||||
self,
|
||||
@@ -967,6 +975,53 @@ class WalletStateManager:
|
||||
vc: VerifiedCredential = VerifiedCredential.get_next_from_coin_spend(coin_spend)
|
||||
return await self.handle_vc(vc), vc
|
||||
|
||||
# Check if the coin is a PlotNFT
|
||||
if uncurried.mod == PlotNFT.singleton_puzzles.singleton_mod:
|
||||
try:
|
||||
try:
|
||||
previous_plotnft = (await self.plotnft2_store.get_plotnfts(coin_ids=[coin_spend.coin.name()]))[0]
|
||||
except ValueError:
|
||||
try:
|
||||
previous_plotnft = await self.plotnft2_store.get_latest_plotnft(
|
||||
launcher_id=bytes32(uncurried.args.at("frf").as_atom())
|
||||
)
|
||||
except RuntimeError:
|
||||
previous_plotnft = None
|
||||
next_plot_nft = PlotNFT.get_next_from_coin_spend(
|
||||
coin_spend=coin_spend,
|
||||
genesis_challenge=self.constants.GENESIS_CHALLENGE,
|
||||
pre_uncurry=uncurried,
|
||||
previous_plotnft_puzzle=previous_plotnft,
|
||||
)
|
||||
for id, wallet in self.wallets.items():
|
||||
if isinstance(wallet, PlotNFT2Wallet) and wallet.plotnft_id == next_plot_nft.launcher_id:
|
||||
matched_plotnft_wallet_id = id
|
||||
break
|
||||
else:
|
||||
matched_plotnft_wallet_id = None
|
||||
if matched_plotnft_wallet_id is None and coin_spend.coin.parent_coin_info == next_plot_nft.launcher_id:
|
||||
matched_plotnft_wallet_id = uint32(len(self.wallets) + 1)
|
||||
self.wallets[matched_plotnft_wallet_id] = await PlotNFT2Wallet.create(
|
||||
wallet_state_manager=self,
|
||||
xch_wallet=self.main_wallet,
|
||||
wallet_info=WalletInfo(
|
||||
id=matched_plotnft_wallet_id,
|
||||
name=next_plot_nft.launcher_id.hex(),
|
||||
type=uint8(WalletType.PLOTNFT_2),
|
||||
data=next_plot_nft.launcher_id.hex(),
|
||||
),
|
||||
)
|
||||
if matched_plotnft_wallet_id is None: # pragma: no cover
|
||||
# TODO: add support for receiving plotnfts you don't know about
|
||||
raise ValueError(f"No wallet id for plotnft with id {next_plot_nft.launcher_id}")
|
||||
# the Streamable hint is in error so we need this type ignore
|
||||
return WalletIdentifier( # type: ignore[return-value]
|
||||
id=matched_plotnft_wallet_id,
|
||||
type=WalletType.PLOTNFT_2,
|
||||
), next_plot_nft
|
||||
except GetNextPlotNFTError:
|
||||
pass
|
||||
|
||||
await self.notification_manager.potentially_add_new_notification(coin_state, coin_spend)
|
||||
|
||||
return None, None
|
||||
@@ -2082,6 +2137,25 @@ class WalletStateManager:
|
||||
if coin_state.spent_height is not None:
|
||||
vc_wallet = self.get_wallet(id=uint32(record.wallet_id), required_type=VCWallet)
|
||||
await vc_wallet.remove_coin(coin_state.coin, uint32(coin_state.spent_height))
|
||||
elif record.wallet_type == WalletType.PLOTNFT_2:
|
||||
if isinstance(coin_data, PlotNFT):
|
||||
await self.coin_added(
|
||||
coin_state.coin,
|
||||
uint32(coin_state.created_height),
|
||||
all_unconfirmed,
|
||||
wallet_identifier.id,
|
||||
wallet_identifier.type,
|
||||
peer,
|
||||
coin_name,
|
||||
coin_data,
|
||||
)
|
||||
await self.coin_store.set_spent(coin_name, uint32(coin_state.spent_height))
|
||||
await self.add_interested_coin_ids([coin_name])
|
||||
else:
|
||||
await self.plotnft2_store.mark_pool_reward_as_spent(
|
||||
reward_id=coin_name,
|
||||
spent_height=coin_state.spent_height,
|
||||
)
|
||||
|
||||
# Check if a child is a singleton launcher
|
||||
for child in children:
|
||||
@@ -2342,6 +2416,7 @@ class WalletStateManager:
|
||||
additional_signing_responses: list[SigningResponse] | None = None,
|
||||
extra_spends: list[WalletSpendBundle] | None = None,
|
||||
singleton_records: list[SingletonRecord] = [],
|
||||
plotnft_exiting_info: PlotNFTTargetStateInfo | None = None,
|
||||
) -> list[TransactionRecord]:
|
||||
"""
|
||||
Add a list of transactions to be submitted to the full node.
|
||||
@@ -2391,6 +2466,9 @@ class WalletStateManager:
|
||||
for singleton_record in singleton_records:
|
||||
await self.dl_store.add_singleton_record(singleton_record)
|
||||
|
||||
if plotnft_exiting_info is not None:
|
||||
await self.plotnft2_store.add_exiting_info(exiting_info=plotnft_exiting_info)
|
||||
|
||||
await self.add_interested_coin_ids(all_coins_names)
|
||||
|
||||
if actual_spend_involved:
|
||||
@@ -2506,6 +2584,7 @@ class WalletStateManager:
|
||||
await self.coin_store.rollback_to_block(height)
|
||||
await self.interested_store.rollback_to_block(height)
|
||||
await self.dl_store.rollback_to_block(height)
|
||||
await self.plotnft2_store.rollback_to_block(height=height)
|
||||
reorged: list[TransactionRecord] = await self.tx_store.get_transaction_above(height)
|
||||
await self.tx_store.rollback_to_block(height)
|
||||
for record in reorged:
|
||||
@@ -2645,9 +2724,9 @@ class WalletStateManager:
|
||||
|
||||
async def new_peak(self, height: uint32) -> None:
|
||||
for wallet_id, wallet in self.wallets.items():
|
||||
if wallet.type() == WalletType.POOLING_WALLET:
|
||||
assert isinstance(wallet, PoolWallet)
|
||||
if isinstance(wallet, (PoolWallet, PlotNFT2Wallet)):
|
||||
await wallet.new_peak(height)
|
||||
|
||||
current_time = int(time.time())
|
||||
|
||||
if self.wallet_node.last_wallet_tx_resend_time < current_time - self.wallet_node.wallet_tx_resend_timeout_secs:
|
||||
|
||||
Reference in New Issue
Block a user