Merge pull request #11832 from Chia-Network/rewind_nft1_offers

NFT1 offers again
This commit is contained in:
Amine Khaldi
2022-06-16 21:04:27 +01:00
committed by GitHub
22 changed files with 1151 additions and 63 deletions
@@ -95,7 +95,7 @@ jobs:
- name: Test wallet-cat_wallet code with pytest
run: |
. ./activate
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 0 -m "not benchmark" tests/wallet/cat_wallet/test_cat_lifecycle.py tests/wallet/cat_wallet/test_cat_wallet.py tests/wallet/cat_wallet/test_offer_lifecycle.py tests/wallet/cat_wallet/test_trades.py
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 0 -m "not benchmark" tests/wallet/cat_wallet/test_cat_lifecycle.py tests/wallet/cat_wallet/test_cat_outer_puzzle.py tests/wallet/cat_wallet/test_cat_wallet.py tests/wallet/cat_wallet/test_offer_lifecycle.py tests/wallet/cat_wallet/test_trades.py
- name: Process coverage data
run: |
@@ -95,7 +95,7 @@ jobs:
- name: Test wallet-nft_wallet code with pytest
run: |
. ./activate
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 0 -m "not benchmark" tests/wallet/nft_wallet/test_nft_lifecycle.py tests/wallet/nft_wallet/test_nft_offers.py tests/wallet/nft_wallet/test_nft_puzzles.py tests/wallet/nft_wallet/test_nft_wallet.py
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 0 -m "not benchmark" tests/wallet/nft_wallet/test_nft_1_offers.py tests/wallet/nft_wallet/test_nft_lifecycle.py tests/wallet/nft_wallet/test_nft_offers.py tests/wallet/nft_wallet/test_nft_puzzles.py tests/wallet/nft_wallet/test_nft_wallet.py tests/wallet/nft_wallet/test_ownership_outer_puzzle.py
- name: Process coverage data
run: |
@@ -94,7 +94,7 @@ jobs:
- name: Test wallet-cat_wallet code with pytest
run: |
. ./activate
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 0 -m "not benchmark" tests/wallet/cat_wallet/test_cat_lifecycle.py tests/wallet/cat_wallet/test_cat_wallet.py tests/wallet/cat_wallet/test_offer_lifecycle.py tests/wallet/cat_wallet/test_trades.py
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 0 -m "not benchmark" tests/wallet/cat_wallet/test_cat_lifecycle.py tests/wallet/cat_wallet/test_cat_outer_puzzle.py tests/wallet/cat_wallet/test_cat_wallet.py tests/wallet/cat_wallet/test_offer_lifecycle.py tests/wallet/cat_wallet/test_trades.py
- name: Process coverage data
run: |
@@ -94,7 +94,7 @@ jobs:
- name: Test wallet-nft_wallet code with pytest
run: |
. ./activate
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 0 -m "not benchmark" tests/wallet/nft_wallet/test_nft_lifecycle.py tests/wallet/nft_wallet/test_nft_offers.py tests/wallet/nft_wallet/test_nft_puzzles.py tests/wallet/nft_wallet/test_nft_wallet.py
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 0 -m "not benchmark" tests/wallet/nft_wallet/test_nft_1_offers.py tests/wallet/nft_wallet/test_nft_lifecycle.py tests/wallet/nft_wallet/test_nft_offers.py tests/wallet/nft_wallet/test_nft_puzzles.py tests/wallet/nft_wallet/test_nft_wallet.py tests/wallet/nft_wallet/test_ownership_outer_puzzle.py
- name: Process coverage data
run: |
+8 -7
View File
@@ -984,13 +984,14 @@ class WalletRpcApi:
# This driver_dict construction is to maintain backward compatibility where everything is assumed to be a CAT
driver_dict: Dict[bytes32, PuzzleInfo] = {}
if driver_dict_str is None:
for key in offer:
try:
driver_dict[bytes32.from_hexstr(key)] = PuzzleInfo(
{"type": AssetType.CAT.value, "tail": "0x" + key}
)
except ValueError:
pass
for key, amount in offer.items():
if amount > 0:
try:
driver_dict[bytes32.from_hexstr(key)] = PuzzleInfo(
{"type": AssetType.CAT.value, "tail": "0x" + key}
)
except ValueError:
pass
else:
for key, value in driver_dict_str.items():
driver_dict[bytes32.from_hexstr(key)] = PuzzleInfo(value)
+23 -1
View File
@@ -23,6 +23,8 @@ class CATOuterPuzzle:
_asset_id: Any
_construct: Any
_solve: Any
_get_inner_puzzle: Any
_get_inner_solution: Any
def match(self, puzzle: Program) -> Optional[PuzzleInfo]:
matched, curried_args = match_cat_puzzle(puzzle)
@@ -39,6 +41,26 @@ class CATOuterPuzzle:
else:
return None
def get_inner_puzzle(self, constructor: PuzzleInfo, puzzle_reveal: Program) -> Optional[Program]:
matched, curried_args = match_cat_puzzle(puzzle_reveal)
if matched:
_, _, inner_puzzle = curried_args
if constructor.also() is not None:
deep_inner_puzzle: Optional[Program] = self._get_inner_puzzle(constructor.also(), inner_puzzle)
return deep_inner_puzzle
else:
return inner_puzzle
else:
raise ValueError("This driver is not for the specified puzzle reveal")
def get_inner_solution(self, constructor: PuzzleInfo, solution: Program) -> Optional[Program]:
my_inner_solution: Program = solution.first()
if constructor.also():
deep_inner_solution: Optional[Program] = self._get_inner_solution(constructor.also(), my_inner_solution)
return deep_inner_solution
else:
return my_inner_solution
def asset_id(self, constructor: PuzzleInfo) -> Optional[bytes32]:
return bytes32(constructor["tail"])
@@ -73,7 +95,7 @@ class CATOuterPuzzle:
parent_coin: Coin = parent_spend.coin
if constructor.also() is not None:
puzzle = self._construct(constructor.also(), puzzle)
solution = self._solve(constructor.also(), solver, puzzle, solution)
solution = self._solve(constructor.also(), solver, inner_puzzle, inner_solution)
matched, curried_args = match_cat_puzzle(parent_spend.puzzle_reveal.to_program())
assert matched
_, _, parent_inner_puzzle = curried_args
@@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import Any, List, Optional, Tuple, Union
from typing import Any, List, Optional, Tuple
from clvm_tools.binutils import disassemble
@@ -14,11 +14,11 @@ NFT_STATE_LAYER_MOD = load_clvm("nft_state_layer.clvm")
NFT_STATE_LAYER_MOD_HASH = NFT_STATE_LAYER_MOD.get_tree_hash()
def match_metadata_layer_puzzle(puzzle: Program) -> Tuple[bool, Union[List[Any], Program]]:
def match_metadata_layer_puzzle(puzzle: Program) -> Tuple[bool, List[Program]]:
mod, meta_args = puzzle.uncurry()
if mod == NFT_STATE_LAYER_MOD:
return True, list(meta_args.as_iter())
return False, Program.to([])
return False, []
def puzzle_for_metadata_layer(metadata: Program, updater_hash: bytes32, inner_puzzle: Program) -> Program:
@@ -35,6 +35,8 @@ class MetadataOuterPuzzle:
_asset_id: Any
_construct: Any
_solve: Any
_get_inner_puzzle: Any
_get_inner_solution: Any
def match(self, puzzle: Program) -> Optional[PuzzleInfo]:
matched, curried_args = match_metadata_layer_puzzle(puzzle)
@@ -61,6 +63,26 @@ class MetadataOuterPuzzle:
inner_puzzle = self._construct(constructor.also(), inner_puzzle)
return puzzle_for_metadata_layer(constructor["metadata"], constructor["updater_hash"], inner_puzzle)
def get_inner_puzzle(self, constructor: PuzzleInfo, puzzle_reveal: Program) -> Optional[Program]:
matched, curried_args = match_metadata_layer_puzzle(puzzle_reveal)
if matched:
_, _, _, inner_puzzle = curried_args
if constructor.also() is not None:
deep_inner_puzzle: Optional[Program] = self._get_inner_puzzle(constructor.also(), inner_puzzle)
return deep_inner_puzzle
else:
return inner_puzzle
else:
raise ValueError("This driver is not for the specified puzzle reveal")
def get_inner_solution(self, constructor: PuzzleInfo, solution: Program) -> Optional[Program]:
my_inner_solution: Program = solution.first()
if constructor.also():
deep_inner_solution: Optional[Program] = self._get_inner_solution(constructor.also(), my_inner_solution)
return deep_inner_solution
else:
return my_inner_solution
def solve(self, constructor: PuzzleInfo, solver: Solver, inner_puzzle: Program, inner_solution: Program) -> Program:
coin_bytes: bytes = solver["coin"]
coin: Coin = Coin(bytes32(coin_bytes[0:32]), bytes32(coin_bytes[32:64]), uint64.from_bytes(coin_bytes[64:72]))
+9 -8
View File
@@ -278,12 +278,12 @@ def get_metadata_and_phs(unft: UncurriedNFT, solution: SerializedProgram) -> Tup
return metadata, puzhash_for_derivation
def recurry_nft_puzzle(unft: UncurriedNFT, solution: Program, sp2_puzzle: Program) -> Program:
def recurry_nft_puzzle(unft: UncurriedNFT, solution: Program, new_inner_puzzle: Program) -> Program:
log.debug("Generating NFT puzzle with ownership support: %s", disassemble(solution))
conditions = solution.at("frfr").as_iter()
conditions = unft.p2_puzzle.run(unft.get_innermost_solution(solution))
new_did_id = unft.owner_did
new_puzhash = None
for condition in conditions:
for condition in conditions.as_iter():
if condition.first().as_int() == -10:
# this is the change owner magic condition
new_did_id = condition.at("rf").atom
@@ -292,14 +292,15 @@ def recurry_nft_puzzle(unft: UncurriedNFT, solution: Program, sp2_puzzle: Progra
# assert new_puzhash and new_did_id
log.debug(f"Found NFT puzzle details: {new_did_id} {new_puzhash}")
assert unft.transfer_program
inner_puzzle = construct_ownership_layer(new_did_id, unft.transfer_program, sp2_puzzle)
return inner_puzzle
new_ownership_puzzle = construct_ownership_layer(new_did_id, unft.transfer_program, new_inner_puzzle)
return new_ownership_puzzle
def get_new_owner_did(solution: Program) -> Optional[bytes32]:
conditions = solution.at("rrfffrfr").as_iter()
def get_new_owner_did(unft: UncurriedNFT, solution: Program) -> Optional[bytes32]:
conditions = unft.p2_puzzle.run(unft.get_innermost_solution(solution))
new_did_id = None
for condition in conditions:
for condition in conditions.as_iter():
if condition.first().as_int() == -10:
# this is the change owner magic condition
new_did_id = condition.at("rf").atom
+158 -5
View File
@@ -29,7 +29,7 @@ from chia.wallet.nft_wallet.nft_puzzles import (
get_metadata_and_phs,
)
from chia.wallet.nft_wallet.uncurry_nft import UncurriedNFT
from chia.wallet.outer_puzzles import AssetType, match_puzzle
from chia.wallet.outer_puzzles import AssetType, construct_puzzle, match_puzzle
from chia.wallet.payment import Payment
from chia.wallet.puzzle_drivers import PuzzleInfo
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
@@ -39,11 +39,12 @@ from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
solution_for_conditions,
)
from chia.wallet.puzzles.puzzle_utils import make_create_coin_condition
from chia.wallet.trading.offer import OFFER_MOD, NotarizedPayment, Offer
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.util.compute_memos import compute_memos
from chia.wallet.util.debug_spend_bundle import disassemble
from chia.wallet.util.transaction_type import TransactionType
from chia.wallet.util.wallet_types import WalletType
from chia.wallet.util.wallet_types import AmountWithPuzzlehash, WalletType
from chia.wallet.wallet import Wallet
from chia.wallet.wallet_info import WalletInfo
@@ -192,7 +193,6 @@ class NFTWallet:
self.log.debug("Puzzle solution received to wallet: %s", self.wallet_info)
coin_name = coin_spend.coin.name()
puzzle: Program = Program.from_bytes(bytes(coin_spend.puzzle_reveal))
delegated_puz_solution: Program = coin_spend.solution.to_program().rest().rest().first().first()
# At this point, the puzzle must be a NFT puzzle.
# This method will be called only when the wallet state manager uncurried this coin as a NFT puzzle.
@@ -216,7 +216,8 @@ class NFTWallet:
return
p2_puzzle = puzzle_for_pk(derivation_record.pubkey)
if uncurried_nft.supports_did:
inner_puzzle = nft_puzzles.recurry_nft_puzzle(uncurried_nft, delegated_puz_solution, p2_puzzle)
inner_puzzle = nft_puzzles.recurry_nft_puzzle(uncurried_nft, coin_spend.solution.to_program(), p2_puzzle)
else:
inner_puzzle = p2_puzzle
child_puzzle: Program = nft_puzzles.create_full_puzzle(
@@ -848,7 +849,159 @@ class NFTWallet:
unsigned_spend_bundle = SpendBundle.aggregate([nft_spend_bundle, chia_spend_bundle])
return unsigned_spend_bundle, chia_tx
return (unsigned_spend_bundle, chia_tx)
@staticmethod
async def make_nft1_offer(
wallet_state_manager: Any,
offer_dict: Dict[Optional[bytes32], int],
driver_dict: Dict[bytes32, PuzzleInfo],
fee: uint64,
) -> Offer:
amounts = list(offer_dict.values())
if len(offer_dict) != 2 or (amounts[0] > 0 == amounts[1] > 0):
raise ValueError("Royalty enabled NFTs only support offering/requesting one NFT for one currency")
first_asset_id = list(offer_dict.items())[0][0]
if first_asset_id is None:
nft: bool = False
else:
nft = driver_dict[first_asset_id].check_type(
[
AssetType.SINGLETON.value,
AssetType.METADATA.value,
AssetType.OWNERSHIP.value,
]
)
offered: bool = list(offer_dict.items())[0][1] < 0
if offered:
offered_asset_id: Optional[bytes32] = first_asset_id
requested_asset_id: Optional[bytes32] = list(offer_dict.items())[1][0]
else:
offered_asset_id = list(offer_dict.items())[1][0]
requested_asset_id = first_asset_id
if nft == offered:
assert offered_asset_id is not None # hello mypy
driver_dict[offered_asset_id].info["also"]["also"]["owner"] = "()"
wallet = await wallet_state_manager.get_wallet_for_asset_id(offered_asset_id.hex())
p2_ph = await wallet_state_manager.main_wallet.get_new_puzzlehash()
offered_amount: uint64 = uint64(abs(offer_dict[offered_asset_id]))
offered_coin_info = wallet.get_nft(offered_asset_id)
offered_coin: Coin = offered_coin_info.coin
requested_amount = offer_dict[requested_asset_id]
if requested_asset_id is None:
trade_prices = Program.to([[uint64(requested_amount), OFFER_MOD.get_tree_hash()]])
else:
trade_prices = Program.to(
[
[
uint64(requested_amount),
construct_puzzle(driver_dict[requested_asset_id], OFFER_MOD).get_tree_hash(),
]
]
)
notarized_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = Offer.notarize_payments(
{requested_asset_id: [Payment(p2_ph, uint64(requested_amount), [p2_ph])]}, [offered_coin]
)
announcements = Offer.calculate_announcements(notarized_payments, driver_dict)
txs = await wallet.generate_signed_transaction(
[offered_amount],
[Offer.ph()],
fee=fee,
coins=set([offered_coin]),
puzzle_announcements_to_consume=set(announcements),
trade_prices_list=trade_prices,
)
transaction_bundles: List[SpendBundle] = [tx.spend_bundle for tx in txs if tx.spend_bundle is not None]
total_spend_bundle = SpendBundle.aggregate(transaction_bundles)
return Offer(notarized_payments, total_spend_bundle, driver_dict)
else:
assert isinstance(requested_asset_id, bytes32)
driver_dict[requested_asset_id].info["also"]["also"]["owner"] = "()"
requested_info = driver_dict[requested_asset_id]
transfer_info = requested_info.also().also() # type: ignore
assert isinstance(transfer_info, PuzzleInfo)
royalty_percentage = uint16(transfer_info["transfer_program"]["royalty_percentage"])
royalty_address = bytes32(transfer_info["transfer_program"]["royalty_address"])
p2_ph = await wallet_state_manager.main_wallet.get_new_puzzlehash()
requested_payments: Dict[Optional[bytes32], List[Payment]] = {
requested_asset_id: [Payment(p2_ph, uint64(offer_dict[requested_asset_id]), [p2_ph])]
}
offered_amount = uint64(abs(offer_dict[offered_asset_id]))
royalty_amount = uint64(offered_amount * royalty_percentage / 10000)
if offered_amount == royalty_amount:
raise ValueError("Amount offered and amount paid in royalties are equal")
if offered_asset_id is None:
# std xch offer
wallet = wallet_state_manager.main_wallet
else:
# cat offer
wallet = await wallet_state_manager.get_wallet_for_asset_id(offered_asset_id)
if wallet.type() == WalletType.STANDARD_WALLET:
coin_amount_needed: int = offered_amount + royalty_amount + fee
else:
coin_amount_needed = offered_amount + royalty_amount
pmt_coins = list(await wallet.get_coins_to_offer(offered_asset_id, coin_amount_needed))
notarized_payments = Offer.notarize_payments(requested_payments, pmt_coins)
announcements_to_assert = Offer.calculate_announcements(notarized_payments, driver_dict)
# Calculate the royalty announcement separately
announcements_to_assert.extend(
Offer.calculate_announcements(
{
offered_asset_id: [
NotarizedPayment(royalty_address, royalty_amount, [royalty_address], requested_asset_id)
]
},
driver_dict,
)
)
if wallet.type() == WalletType.STANDARD_WALLET:
tx = await wallet.generate_signed_transaction(
offered_amount,
Offer.ph(),
primaries=[AmountWithPuzzlehash({"amount": royalty_amount, "puzzlehash": Offer.ph(), "memos": []})],
fee=fee,
coins=set(pmt_coins),
puzzle_announcements_to_consume=announcements_to_assert,
)
all_transactions: List[TransactionRecord] = [tx]
else:
txs = await wallet.generate_signed_transaction(
[offered_amount, royalty_amount],
[Offer.ph(), Offer.ph()],
fee=fee,
coins=set(pmt_coins),
puzzle_announcements_to_consume=announcements_to_assert,
)
all_transactions = txs
txn_bundles: List[SpendBundle] = [tx.spend_bundle for tx in all_transactions if tx.spend_bundle is not None]
txn_spend_bundle = SpendBundle.aggregate(txn_bundles)
# Create a spend bundle for the royalty payout from OFFER MOD
for txn in txn_bundles:
for coin in txn.additions():
if coin.amount == royalty_amount:
royalty_coin = coin
break
assert royalty_coin
# make the royalty payment solution
# ((nft_launcher_id . ((ROYALTY_ADDRESS, royalty_amount, (ROYALTY_ADDRESS)))))
royalty_sol = Program.to([[requested_asset_id, [royalty_address, royalty_amount, [royalty_address]]]])
if offered_asset_id is None:
offer_puzzle: Program = OFFER_MOD
else:
offer_puzzle = construct_puzzle(driver_dict[offered_asset_id], OFFER_MOD)
royalty_spend = SpendBundle([CoinSpend(royalty_coin, offer_puzzle, royalty_sol)], G2Element())
total_spend_bundle = SpendBundle.aggregate([txn_spend_bundle, royalty_spend])
offer = Offer(notarized_payments, total_spend_bundle, driver_dict)
return offer
async def set_nft_did(
self, nft_coin_info: NFTCoinInfo, did_id: Optional[bytes32], fee: uint64 = uint64(0)
@@ -0,0 +1,96 @@
from dataclasses import dataclass
from typing import Any, List, Optional, Tuple, Union
from clvm_tools.binutils import disassemble
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.wallet.puzzle_drivers import PuzzleInfo, Solver
from chia.wallet.puzzles.load_clvm import load_clvm
OWNERSHIP_LAYER_MOD = load_clvm("nft_ownership_layer.clvm")
def match_ownership_layer_puzzle(puzzle: Program) -> Tuple[bool, List[Program]]:
mod, args = puzzle.uncurry()
if mod == OWNERSHIP_LAYER_MOD:
return True, list(args.as_iter())
return False, []
def puzzle_for_ownership_layer(
current_owner: Union[Program, bytes], transfer_program: Program, inner_puzzle: Program
) -> Program:
return OWNERSHIP_LAYER_MOD.curry(OWNERSHIP_LAYER_MOD.get_tree_hash(), current_owner, transfer_program, inner_puzzle)
def solution_for_ownership_layer(inner_solution: Program) -> Program:
return Program.to([inner_solution]) # type: ignore
@dataclass(frozen=True)
class OwnershipOuterPuzzle:
_match: Any
_asset_id: Any
_construct: Any
_solve: Any
_get_inner_puzzle: Any
_get_inner_solution: Any
def match(self, puzzle: Program) -> Optional[PuzzleInfo]:
matched, curried_args = match_ownership_layer_puzzle(puzzle)
if matched:
_, current_owner, transfer_program, inner_puzzle = curried_args
owner_bytes: bytes = current_owner.as_python()
tp_match: Optional[PuzzleInfo] = self._match(transfer_program)
constructor_dict = {
"type": "ownership",
"owner": "()" if owner_bytes == b"" else "0x" + owner_bytes.hex(),
"transfer_program": (
disassemble(transfer_program) if tp_match is None else tp_match.info # type: ignore
),
}
next_constructor = self._match(inner_puzzle)
if next_constructor is not None:
constructor_dict["also"] = next_constructor.info
return PuzzleInfo(constructor_dict)
else:
return None
def asset_id(self, constructor: PuzzleInfo) -> Optional[bytes32]:
return None
def construct(self, constructor: PuzzleInfo, inner_puzzle: Program) -> Program:
if constructor.also() is not None:
inner_puzzle = self._construct(constructor.also(), inner_puzzle)
transfer_program_info: Union[PuzzleInfo, Program] = constructor["transfer_program"]
if isinstance(transfer_program_info, Program):
transfer_program: Program = transfer_program_info
else:
transfer_program = self._construct(transfer_program_info, inner_puzzle)
return puzzle_for_ownership_layer(constructor["owner"], transfer_program, inner_puzzle)
def get_inner_puzzle(self, constructor: PuzzleInfo, puzzle_reveal: Program) -> Optional[Program]:
matched, curried_args = match_ownership_layer_puzzle(puzzle_reveal)
if matched:
_, _, _, inner_puzzle = curried_args
if constructor.also() is not None:
deep_inner_puzzle: Optional[Program] = self._get_inner_puzzle(constructor.also(), inner_puzzle)
return deep_inner_puzzle
else:
return inner_puzzle
else:
raise ValueError("This driver is not for the specified puzzle reveal")
def get_inner_solution(self, constructor: PuzzleInfo, solution: Program) -> Optional[Program]:
my_inner_solution: Program = solution.first()
if constructor.also():
deep_inner_solution: Optional[Program] = self._get_inner_solution(constructor.also(), my_inner_solution)
return deep_inner_solution
else:
return my_inner_solution
def solve(self, constructor: PuzzleInfo, solver: Solver, inner_puzzle: Program, inner_solution: Program) -> Program:
if constructor.also() is not None:
inner_solution = self._solve(constructor.also(), solver, inner_puzzle, inner_solution)
return solution_for_ownership_layer(inner_solution)
@@ -22,6 +22,8 @@ class SingletonOuterPuzzle:
_asset_id: Any
_construct: Any
_solve: Any
_get_inner_puzzle: Any
_get_inner_solution: Any
def match(self, puzzle: Program) -> Optional[PuzzleInfo]:
matched, curried_args = match_singleton_puzzle(puzzle)
@@ -48,6 +50,26 @@ class SingletonOuterPuzzle:
launcher_hash = constructor["launcher_ph"] if "launcher_ph" in constructor else SINGLETON_LAUNCHER_HASH
return puzzle_for_singleton(constructor["launcher_id"], inner_puzzle, launcher_hash)
def get_inner_puzzle(self, constructor: PuzzleInfo, puzzle_reveal: Program) -> Optional[Program]:
matched, curried_args = match_singleton_puzzle(puzzle_reveal)
if matched:
_, inner_puzzle = curried_args
if constructor.also() is not None:
deep_inner_puzzle: Optional[Program] = self._get_inner_puzzle(constructor.also(), inner_puzzle)
return deep_inner_puzzle
else:
return inner_puzzle
else:
raise ValueError("This driver is not for the specified puzzle reveal")
def get_inner_solution(self, constructor: PuzzleInfo, solution: Program) -> Optional[Program]:
my_inner_solution: Program = solution.at("rrf")
if constructor.also():
deep_inner_solution: Optional[Program] = self._get_inner_solution(constructor.also(), my_inner_solution)
return deep_inner_solution
else:
return my_inner_solution
def solve(self, constructor: PuzzleInfo, solver: Solver, inner_puzzle: Program, inner_solution: Program) -> Program:
coin_bytes: bytes = solver["coin"]
coin: Coin = Coin(bytes32(coin_bytes[0:32]), bytes32(coin_bytes[32:64]), uint64.from_bytes(coin_bytes[64:72]))
@@ -0,0 +1,78 @@
from dataclasses import dataclass
from typing import Any, List, Optional, Tuple
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.util.ints import uint16
from chia.wallet.puzzle_drivers import PuzzleInfo, Solver
from chia.wallet.puzzles.load_clvm import load_clvm
from chia.wallet.puzzles.singleton_top_layer_v1_1 import SINGLETON_LAUNCHER_HASH, SINGLETON_MOD_HASH
TRANSFER_PROGRAM_MOD = load_clvm("nft_ownership_transfer_program_one_way_claim_with_royalties.clvm")
def match_transfer_program_puzzle(puzzle: Program) -> Tuple[bool, List[Program]]:
mod, args = puzzle.uncurry()
if mod == TRANSFER_PROGRAM_MOD:
return True, list(args.as_iter())
return False, []
def puzzle_for_transfer_program(launcher_id: bytes32, royalty_puzzle_hash: bytes32, percentage: uint16) -> Program:
singleton_struct = Program.to((SINGLETON_MOD_HASH, (launcher_id, SINGLETON_LAUNCHER_HASH)))
return TRANSFER_PROGRAM_MOD.curry(
singleton_struct,
royalty_puzzle_hash,
percentage,
)
def solution_for_transfer_program(
conditions: Program,
current_owner: Optional[bytes32],
new_did: bytes32,
new_did_inner_hash: bytes32,
trade_prices_list: Program,
) -> Program:
return Program.to([conditions, current_owner, [new_did, trade_prices_list, new_did_inner_hash]]) # type: ignore
@dataclass(frozen=True)
class TransferProgramPuzzle:
_match: Any
_asset_id: Any
_construct: Any
_solve: Any
_get_inner_puzzle: Any
_get_inner_solution: Any
def match(self, puzzle: Program) -> Optional[PuzzleInfo]:
matched, curried_args = match_transfer_program_puzzle(puzzle)
if matched:
singleton_struct, royalty_puzzle_hash, percentage = curried_args
constructor_dict = {
"type": "royalty transfer program",
"launcher_id": "0x" + singleton_struct.rest().first().as_python().hex(),
"royalty_address": "0x" + royalty_puzzle_hash.as_python().hex(),
"royalty_percentage": str(percentage.as_int()),
}
return PuzzleInfo(constructor_dict)
else:
return None
def asset_id(self, constructor: PuzzleInfo) -> Optional[bytes32]:
return None
def construct(self, constructor: PuzzleInfo, inner_puzzle: Program) -> Program:
return puzzle_for_transfer_program(
constructor["launcher_id"], constructor["royalty_address"], constructor["royalty_percentage"]
)
def get_inner_puzzle(self, constructor: PuzzleInfo, puzzle_reveal: Program) -> Optional[Program]:
return None
def get_inner_solution(self, constructor: PuzzleInfo, solution: Program) -> Optional[Program]:
return None
def solve(self, constructor: PuzzleInfo, solver: Solver, inner_puzzle: Program, inner_solution: Program) -> Program:
return Program.to(None) # type: ignore
+17 -1
View File
@@ -5,7 +5,9 @@ from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.wallet.cat_wallet.cat_outer_puzzle import CATOuterPuzzle
from chia.wallet.nft_wallet.metadata_outer_puzzle import MetadataOuterPuzzle
from chia.wallet.nft_wallet.ownership_outer_puzzle import OwnershipOuterPuzzle
from chia.wallet.nft_wallet.singleton_outer_puzzle import SingletonOuterPuzzle
from chia.wallet.nft_wallet.transfer_program_puzzle import TransferProgramPuzzle
from chia.wallet.puzzle_drivers import PuzzleInfo, Solver
"""
@@ -14,6 +16,8 @@ This file provides a central location for acquiring drivers for outer puzzles li
A driver for a puzzle must include the following functions:
- match(self, puzzle: Program) -> Optional[PuzzleInfo]
- Given a puzzle reveal, return a PuzzleInfo object that can be used to reconstruct it later
- get_inner_puzzle(self, constructor: PuzzleInfo, puzzle_reveal: Program) -> Optional[Program]:
- Given a PuzzleInfo object and a puzzle reveal, pull out this outer puzzle's inner puzzle
- asset_id(self, constructor: PuzzleInfo) -> Optional[bytes32]
- Given a PuzzleInfo object, generate a 32 byte ID for use in dictionaries, etc.
- construct(self, constructor: PuzzleInfo, inner_puzzle: Program) -> Program
@@ -30,6 +34,8 @@ class AssetType(Enum):
CAT = "CAT"
SINGLETON = "singleton"
METADATA = "metadata"
OWNERSHIP = "ownership"
ROYALTY_TRANSFER_PROGRAM = "royalty transfer program"
def match_puzzle(puzzle: Program) -> Optional[PuzzleInfo]:
@@ -50,14 +56,24 @@ def solve_puzzle(constructor: PuzzleInfo, solver: Solver, inner_puzzle: Program,
)
def get_inner_puzzle(constructor: PuzzleInfo, puzzle_reveal: Program) -> Optional[Program]:
return driver_lookup[AssetType(constructor.type())].get_inner_puzzle(constructor, puzzle_reveal) # type: ignore
def get_inner_solution(constructor: PuzzleInfo, solution: Program) -> Optional[Program]:
return driver_lookup[AssetType(constructor.type())].get_inner_solution(constructor, solution) # type: ignore
def create_asset_id(constructor: PuzzleInfo) -> bytes32:
return driver_lookup[AssetType(constructor.type())].asset_id(constructor) # type: ignore
function_args = [match_puzzle, create_asset_id, construct_puzzle, solve_puzzle]
function_args = [match_puzzle, create_asset_id, construct_puzzle, solve_puzzle, get_inner_puzzle, get_inner_solution]
driver_lookup: Dict[AssetType, Any] = {
AssetType.CAT: CATOuterPuzzle(*function_args),
AssetType.SINGLETON: SingletonOuterPuzzle(*function_args),
AssetType.METADATA: MetadataOuterPuzzle(*function_args),
AssetType.OWNERSHIP: OwnershipOuterPuzzle(*function_args),
AssetType.ROYALTY_TRANSFER_PROGRAM: TransferProgramPuzzle(*function_args),
}
+17 -1
View File
@@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import Any, Dict, Optional
from typing import Any, Dict, List, Optional
from clvm.casts import int_from_bytes
from clvm.SExp import SExp
@@ -57,6 +57,22 @@ class PuzzleInfo:
else:
return None
def check_type(self, types: List[str]) -> bool:
if types == []:
if self.also() is None:
return True
else:
return False
else:
if self.type() == types[0]:
types.pop(0)
if self.also():
return self.also().check_type(types) # type: ignore
else:
return self.check_type(types)
else:
return False
@dataclass(frozen=True)
class Solver:
+71 -4
View File
@@ -12,6 +12,8 @@ from chia.types.spend_bundle import SpendBundle
from chia.util.db_wrapper import DBWrapper
from chia.util.hash import std_hash
from chia.util.ints import uint32, uint64
from chia.wallet.nft_wallet.nft_wallet import NFTWallet
from chia.wallet.outer_puzzles import AssetType
from chia.wallet.payment import Payment
from chia.wallet.puzzle_drivers import PuzzleInfo
from chia.wallet.trade_record import TradeRecord
@@ -23,6 +25,9 @@ from chia.wallet.util.transaction_type import TransactionType
from chia.wallet.util.wallet_types import WalletType
from chia.wallet.wallet import Wallet
from chia.wallet.wallet_coin_record import WalletCoinRecord
from chia.wallet.puzzles.load_clvm import load_clvm
OFFER_MOD = load_clvm("settlement_payments.clvm")
class TradeManager:
@@ -333,6 +338,7 @@ class TradeManager:
try:
coins_to_offer: Dict[Union[int, bytes32], List[Coin]] = {}
requested_payments: Dict[Optional[bytes32], List[Payment]] = {}
offer_dict_no_ints: Dict[Optional[bytes32], int] = {}
for id, amount in offer_dict.items():
if amount > 0:
if isinstance(id, int):
@@ -376,18 +382,32 @@ class TradeManager:
elif amount == 0:
raise ValueError("You cannot offer nor request 0 amount of something")
offer_dict_no_ints[asset_id] = amount
if asset_id is not None and wallet is not None:
if callable(getattr(wallet, "get_puzzle_info", None)):
puzzle_driver: PuzzleInfo = wallet.get_puzzle_info(asset_id)
if asset_id in driver_dict and driver_dict[asset_id] != puzzle_driver:
raise ValueError(
f"driver_dict specified {driver_dict[asset_id]}, was expecting {puzzle_driver}"
)
# ignore the case if we're an nft transfering the did owner
if self.check_for_owner_change_in_drivers(puzzle_driver, driver_dict[asset_id]):
driver_dict[asset_id] = puzzle_driver
else:
raise ValueError(
f"driver_dict specified {driver_dict[asset_id]}, was expecting {puzzle_driver}"
)
else:
driver_dict[asset_id] = puzzle_driver
else:
raise ValueError(f"Wallet for asset id {asset_id} is not properly integrated with TradeManager")
potential_special_offer: Optional[Offer] = await self.check_for_special_offer_making(
offer_dict_no_ints,
driver_dict,
fee,
)
if potential_special_offer is not None:
return True, potential_special_offer, None
all_coins: List[Coin] = [c for coins in coins_to_offer.values() for c in coins]
notarized_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = Offer.notarize_payments(
requested_payments, all_coins
@@ -524,13 +544,16 @@ class TradeManager:
removal_dict.setdefault(wallet_id, [])
removal_dict[wallet_id].append(removal)
all_removals: List[bytes32] = [r.name() for removals in removal_dict.values() for r in removals]
for wid, grouped_removals in removal_dict.items():
wallet = self.wallet_state_manager.wallets[wid]
to_puzzle_hash = bytes32([1] * 32) # We use all zeros to be clear not to send here
removal_tree_hash = Program.to([coin_as_list(rem) for rem in grouped_removals]).get_tree_hash()
# We also need to calculate the sent amount
removed: int = sum(c.amount for c in grouped_removals)
change_coins: List[Coin] = addition_dict[wid] if wid in addition_dict else []
potential_change_coins: List[Coin] = addition_dict[wid] if wid in addition_dict else []
change_coins: List[Coin] = [c for c in potential_change_coins if c.parent_coin_info in all_removals]
change_amount: int = sum(c.amount for c in change_coins)
sent_amount: int = removed - change_amount
txs.append(
@@ -585,6 +608,7 @@ class TradeManager:
complete_offer = Offer.aggregate([offer, take_offer])
assert complete_offer.is_valid()
final_spend_bundle: SpendBundle = complete_offer.to_valid_spend()
await self.maybe_create_wallets_for_offer(complete_offer)
@@ -631,3 +655,46 @@ class TradeManager:
await self.wallet_state_manager.add_transaction(tx)
return True, trade_record, None
async def check_for_special_offer_making(
self,
offer_dict: Dict[Optional[bytes32], int],
driver_dict: Dict[bytes32, PuzzleInfo],
fee: uint64 = uint64(0),
) -> Optional[Offer]:
for puzzle_info in driver_dict.values():
if (
puzzle_info.check_type(
[
AssetType.SINGLETON.value,
AssetType.METADATA.value,
AssetType.OWNERSHIP.value,
]
)
and isinstance(puzzle_info.also().also()["transfer_program"], PuzzleInfo) # type: ignore
and puzzle_info.also().also()["transfer_program"].type() # type: ignore
== AssetType.ROYALTY_TRANSFER_PROGRAM.value
):
return await NFTWallet.make_nft1_offer(self.wallet_state_manager, offer_dict, driver_dict, fee)
return None
async def check_for_owner_change_in_drivers(self, puzzle_info: PuzzleInfo, driver_info: PuzzleInfo) -> bool:
if puzzle_info.check_type(
[
AssetType.SINGLETON.value,
AssetType.METADATA.value,
AssetType.OWNERSHIP.value,
]
) and driver_info.check_type(
[
AssetType.SINGLETON.value,
AssetType.METADATA.value,
AssetType.OWNERSHIP.value,
]
):
old_owner = driver_info.also().also().info["owner"] # type: ignore
puzzle_info.also().also().info["owner"] = old_owner # type: ignore
if driver_info == puzzle_info:
return True
return False
+68 -22
View File
@@ -12,7 +12,14 @@ from chia.types.coin_spend import CoinSpend
from chia.types.spend_bundle import SpendBundle
from chia.util.bech32m import bech32_decode, bech32_encode, convertbits
from chia.util.ints import uint64
from chia.wallet.outer_puzzles import construct_puzzle, create_asset_id, match_puzzle, solve_puzzle
from chia.wallet.outer_puzzles import (
construct_puzzle,
create_asset_id,
match_puzzle,
solve_puzzle,
get_inner_puzzle,
get_inner_solution,
)
from chia.wallet.payment import Payment
from chia.wallet.puzzle_drivers import PuzzleInfo, Solver
from chia.wallet.puzzles.load_clvm import load_clvm
@@ -107,31 +114,70 @@ class Offer:
if asset_id is not None and asset_id not in self.driver_dict:
raise ValueError("Offer does not have enough driver information about the requested payments")
def additions(self) -> List[Coin]:
final_list: List[Coin] = []
for cs in self.bundle.coin_spends:
try:
final_list.extend(cs.additions())
except Exception:
pass
return final_list
def removals(self) -> List[Coin]:
return self.bundle.removals()
def incomplete_spends(self) -> List[CoinSpend]:
final_list: List[CoinSpend] = []
for cs in self.bundle.coin_spends:
try:
cs.additions()
except Exception:
final_list.append(cs)
return final_list
# This method does not get every coin that is being offered, only the `settlement_payment` children
# It's also a little heuristic, but it should get most things
def get_offered_coins(self) -> Dict[Optional[bytes32], List[Coin]]:
offered_coins: Dict[Optional[bytes32], List[Coin]] = {}
for addition in self.bundle.additions():
# Get the parent puzzle
parent_puzzle: Program = list(
filter(lambda cs: cs.coin.name() == addition.parent_coin_info, self.bundle.coin_spends)
)[0].puzzle_reveal.to_program()
OFFER_HASH: bytes32 = OFFER_MOD.get_tree_hash()
for parent_spend in self.bundle.coin_spends:
coins_for_this_spend: List[Coin] = []
parent_puzzle: Program = parent_spend.puzzle_reveal.to_program()
parent_solution: Program = parent_spend.solution.to_program()
additions: List[Coin] = [a for a in parent_spend.additions() if a not in self.bundle.removals()]
# Determine it's TAIL (or lack of)
puzzle_driver = match_puzzle(parent_puzzle)
if puzzle_driver is not None:
asset_id = create_asset_id(puzzle_driver)
offer_ph: bytes32 = construct_puzzle(self.driver_dict[asset_id], OFFER_MOD).get_tree_hash()
inner_puzzle: Optional[Program] = get_inner_puzzle(puzzle_driver, parent_puzzle)
inner_solution: Optional[Program] = get_inner_solution(puzzle_driver, parent_solution)
assert inner_puzzle is not None and inner_solution is not None
conditions: Program = inner_puzzle.run(inner_solution)
for condition in conditions.as_iter():
if condition.first() == 51 and condition.rest().first() == OFFER_HASH:
additions_w_amount: List[Coin] = [
a for a in additions if a.amount == condition.rest().rest().first().as_int()
]
if len(additions_w_amount) == 1:
coins_for_this_spend.append(additions_w_amount[0])
else:
additions_w_amount_and_puzhash: List[Coin] = [
a
for a in additions_w_amount
if a.puzzle_hash
== construct_puzzle(puzzle_driver, OFFER_HASH).get_tree_hash(OFFER_HASH) # type: ignore
]
if len(additions_w_amount_and_puzhash) == 1:
coins_for_this_spend.append(additions_w_amount_and_puzhash[0])
else:
asset_id = None
offer_ph = OFFER_MOD.get_tree_hash()
coins_for_this_spend.extend([a for a in additions if a.puzzle_hash == OFFER_HASH])
# Check if the puzzle_hash matches the hypothetical `settlement_payments` puzzle hash
if addition.puzzle_hash == offer_ph:
if asset_id in offered_coins:
offered_coins[asset_id].append(addition)
else:
offered_coins[asset_id] = [addition]
if coins_for_this_spend != []:
offered_coins.setdefault(asset_id, [])
offered_coins[asset_id].extend(coins_for_this_spend)
return offered_coins
@@ -166,8 +212,8 @@ class Offer:
offered_amounts: Dict[Optional[bytes32], int] = self.get_offered_amounts()
requested_amounts: Dict[Optional[bytes32], int] = self.get_requested_amounts()
def keys_to_strings(dic: Dict[Optional[bytes32], int]) -> Dict[str, int]:
new_dic: Dict[str, int] = {}
def keys_to_strings(dic: Dict[Optional[bytes32], Any]) -> Dict[str, Any]:
new_dic: Dict[str, Any] = {}
for key in dic:
if key is None:
new_dic["xch"] = dic[key]
@@ -184,8 +230,8 @@ class Offer:
# Also mostly for the UI, returns a dictionary of assets and how much of them is pended for this offer
# This method is also imperfect for sufficiently complex spends
def get_pending_amounts(self) -> Dict[str, int]:
all_additions: List[Coin] = self.bundle.additions()
all_removals: List[Coin] = self.bundle.removals()
all_additions: List[Coin] = self.additions()
all_removals: List[Coin] = self.removals()
non_ephemeral_removals: List[Coin] = list(filter(lambda c: c not in all_additions, all_removals))
pending_dict: Dict[str, int] = {}
@@ -209,13 +255,13 @@ class Offer:
# This method returns all of the coins that are being used in the offer (without which it would be invalid)
def get_involved_coins(self) -> List[Coin]:
additions = self.bundle.additions()
return list(filter(lambda c: c not in additions, self.bundle.removals()))
additions = self.additions()
return list(filter(lambda c: c not in additions, self.removals()))
# This returns the non-ephemeral removal that is an ancestor of the specified coin
# This should maybe move to the SpendBundle object at some point
def get_root_removal(self, coin: Coin) -> Coin:
all_removals: Set[Coin] = set(self.bundle.removals())
all_removals: Set[Coin] = set(self.removals())
all_removal_ids: Set[bytes32] = {c.name() for c in all_removals}
non_ephemeral_removals: Set[Coin] = {
c for c in all_removals if c.parent_coin_info not in {r.name() for r in all_removals}
+1 -1
View File
@@ -724,7 +724,7 @@ class WalletStateManager:
did_id = None
if uncurried_nft.supports_did:
# Try to get the latest owner DID
did_id = get_new_owner_did(coin_spend.solution.to_program())
did_id = get_new_owner_did(uncurried_nft, coin_spend.solution.to_program())
if did_id is None:
# No DID owner update, use the original DID
did_id = uncurried_nft.owner_did
@@ -0,0 +1,66 @@
from typing import Optional
from clvm_tools.binutils import disassemble
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.coin_spend import CoinSpend
from chia.util.ints import uint64
from chia.wallet.cat_wallet.cat_utils import CAT_MOD, construct_cat_puzzle
from chia.wallet.outer_puzzles import (
construct_puzzle,
create_asset_id,
get_inner_puzzle,
get_inner_solution,
match_puzzle,
solve_puzzle,
)
from chia.wallet.puzzle_drivers import PuzzleInfo, Solver
def test_cat_outer_puzzle() -> None:
ACS = Program.to(1)
tail = bytes32([0] * 32)
cat_puzzle: Program = construct_cat_puzzle(CAT_MOD, tail, ACS)
double_cat_puzzle: Program = construct_cat_puzzle(CAT_MOD, tail, cat_puzzle)
cat_driver: Optional[PuzzleInfo] = match_puzzle(double_cat_puzzle)
assert cat_driver is not None
assert cat_driver.type() == "CAT"
assert cat_driver["tail"] == tail
inside_cat_driver: Optional[PuzzleInfo] = cat_driver.also()
assert inside_cat_driver is not None
assert inside_cat_driver.type() == "CAT"
assert inside_cat_driver["tail"] == tail
assert construct_puzzle(cat_driver, ACS) == double_cat_puzzle
assert get_inner_puzzle(cat_driver, double_cat_puzzle) == ACS
assert create_asset_id(cat_driver) == tail
# Set up for solve
parent_coin = Coin(tail, double_cat_puzzle.get_tree_hash(), uint64(100))
child_coin = Coin(parent_coin.name(), double_cat_puzzle.get_tree_hash(), uint64(100))
parent_spend = CoinSpend(parent_coin, double_cat_puzzle.to_serialized_program(), Program.to([]))
child_coin_as_hex: str = (
"0x" + child_coin.parent_coin_info.hex() + child_coin.puzzle_hash.hex() + bytes(child_coin.amount).hex()
)
parent_spend_as_hex: str = "0x" + bytes(parent_spend).hex()
inner_solution = Program.to([[51, ACS.get_tree_hash(), 100]])
solution: Program = solve_puzzle(
cat_driver,
Solver(
{
"coin": child_coin_as_hex,
"parent_spend": parent_spend_as_hex,
"siblings": "(" + child_coin_as_hex + ")",
"sibling_spends": "(" + parent_spend_as_hex + ")",
"sibling_puzzles": "(" + disassemble(ACS) + ")", # type: ignore
"sibling_solutions": "(" + disassemble(inner_solution) + ")", # type: ignore
}
),
ACS,
inner_solution,
)
double_cat_puzzle.run(solution)
assert get_inner_solution(cat_driver, solution) == inner_solution
@@ -0,0 +1,331 @@
import asyncio
import logging
# from secrets import token_bytes
from typing import Any, Optional
import pytest
from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward
from chia.full_node.mempool_manager import MempoolManager
# from chia.rpc.wallet_rpc_api import WalletRpcApi
from chia.simulator.full_node_simulator import FullNodeSimulator
from chia.simulator.simulator_protocol import FarmNewBlockProtocol
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.peer_info import PeerInfo
# from chia.util.bech32m import encode_puzzle_hash
# from chia.util.byte_types import hexstr_to_bytes
from chia.util.ints import uint16, uint32, uint64
from chia.wallet.did_wallet.did_wallet import DIDWallet
from chia.wallet.nft_wallet.nft_wallet import NFTWallet
from chia.wallet.outer_puzzles import create_asset_id, match_puzzle
from chia.wallet.puzzle_drivers import PuzzleInfo
from chia.wallet.trading.offer import Offer
from chia.wallet.util.compute_memos import compute_memos
# from chia.wallet.util.wallet_types import WalletType
from tests.time_out_assert import time_out_assert, time_out_assert_not_none
# from clvm_tools.binutils import disassemble
logging.getLogger("aiosqlite").setLevel(logging.INFO) # Too much logging on debug level
async def tx_in_pool(mempool: MempoolManager, tx_id: bytes32) -> bool:
tx = mempool.get_spendbundle(tx_id)
if tx is None:
return False
return True
@pytest.mark.parametrize(
"trusted",
[True],
)
@pytest.mark.asyncio
# @pytest.mark.skip
async def test_nft_offer_sell_nft(two_wallet_nodes: Any, trusted: Any) -> None:
num_blocks = 2
full_nodes, wallets = two_wallet_nodes
full_node_api: FullNodeSimulator = full_nodes[0]
full_node_server = full_node_api.server
wallet_node_maker, server_0 = wallets[0]
wallet_node_taker, server_1 = wallets[1]
wallet_maker = wallet_node_maker.wallet_state_manager.main_wallet
wallet_taker = wallet_node_taker.wallet_state_manager.main_wallet
ph_maker = await wallet_maker.get_new_puzzlehash()
ph_taker = await wallet_taker.get_new_puzzlehash()
# token_ph = bytes32(token_bytes())
if trusted:
wallet_node_maker.config["trusted_peers"] = {
full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex()
}
wallet_node_taker.config["trusted_peers"] = {
full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex()
}
else:
wallet_node_maker.config["trusted_peers"] = {}
wallet_node_taker.config["trusted_peers"] = {}
await server_0.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None)
await server_1.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None)
for _ in range(1, num_blocks):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_maker))
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_taker))
funds = sum(
[calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) for i in range(1, num_blocks - 1)]
)
await time_out_assert(10, wallet_maker.get_unconfirmed_balance, funds)
await time_out_assert(10, wallet_maker.get_confirmed_balance, funds)
await time_out_assert(10, wallet_taker.get_unconfirmed_balance, funds)
await time_out_assert(10, wallet_taker.get_confirmed_balance, funds)
for _ in range(1, num_blocks):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_maker))
for _ in range(1, num_blocks):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_maker))
did_wallet_maker: DIDWallet = await DIDWallet.create_new_did_wallet(
wallet_node_maker.wallet_state_manager, wallet_maker, uint64(1)
)
spend_bundle_list = await wallet_node_maker.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(
wallet_maker.id()
)
spend_bundle = spend_bundle_list[0].spend_bundle
await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name())
for _ in range(1, num_blocks):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_maker))
await time_out_assert(15, wallet_maker.get_pending_change_balance, 0)
hex_did_id = did_wallet_maker.get_my_DID()
did_id = bytes32.fromhex(hex_did_id)
target_puzhash = ph_maker
royalty_puzhash = ph_maker
royalty_percentage = uint16(200)
nft_wallet_maker = await NFTWallet.create_new_nft_wallet(
wallet_node_maker.wallet_state_manager, wallet_maker, name="NFT WALLET DID 1", did_id=did_id
)
metadata = Program.to(
[
("u", ["https://www.chia.net/img/branding/chia-logo.svg"]),
("h", "0xD4584AD463139FA8C0D9F68F4B59F185"),
]
)
await time_out_assert(10, wallet_maker.get_unconfirmed_balance, 5999999999999)
await time_out_assert(10, wallet_maker.get_confirmed_balance, 5999999999999)
sb = await nft_wallet_maker.generate_new_nft(
metadata,
target_puzhash,
royalty_puzhash,
royalty_percentage,
did_id,
)
assert sb
# ensure hints are generated
assert compute_memos(sb)
await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name())
for i in range(1, num_blocks):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(bytes32([0] * 32)))
await time_out_assert(10, len, 1, nft_wallet_maker.my_nft_coins)
# TAKER SETUP - NO DID
nft_wallet_taker = await NFTWallet.create_new_nft_wallet(
wallet_node_taker.wallet_state_manager, wallet_taker, name="NFT WALLET TAKER"
)
# maker create offer: NFT for xch
trade_manager_maker = wallet_maker.wallet_state_manager.trade_manager
trade_manager_taker = wallet_taker.wallet_state_manager.trade_manager
coins_maker = nft_wallet_maker.my_nft_coins
assert len(coins_maker) == 1
coins_taker = nft_wallet_taker.my_nft_coins
assert len(coins_taker) == 0
nft_to_offer = coins_maker[0]
nft_to_offer_info: Optional[PuzzleInfo] = match_puzzle(nft_to_offer.full_puzzle)
nft_to_offer_asset_id: bytes32 = create_asset_id(nft_to_offer_info) # type: ignore
xch_requested = 1000
maker_fee = uint64(433)
offer_did_nft_for_xch = {nft_to_offer_asset_id: -1, wallet_maker.id(): xch_requested}
success, trade_make, error = await trade_manager_maker.create_offer_for_ids(
offer_did_nft_for_xch, {}, fee=maker_fee
)
await asyncio.sleep(1)
assert success is True
assert error is None
assert trade_make is not None
success, trade_take, error = await trade_manager_taker.respond_to_offer(
Offer.from_bytes(trade_make.offer), fee=uint64(1)
)
await asyncio.sleep(1)
assert error is None
assert success is True
assert trade_take is not None
for _ in range(1, num_blocks):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_maker))
await time_out_assert(5, len, 0, nft_wallet_maker.my_nft_coins)
await time_out_assert(5, len, 1, nft_wallet_taker.my_nft_coins)
@pytest.mark.parametrize(
"trusted",
[True],
)
@pytest.mark.asyncio
# @pytest.mark.skip
async def test_nft_offer_request_nft(two_wallet_nodes: Any, trusted: Any) -> None:
num_blocks = 2
full_nodes, wallets = two_wallet_nodes
full_node_api: FullNodeSimulator = full_nodes[0]
full_node_server = full_node_api.server
wallet_node_maker, server_0 = wallets[0]
wallet_node_taker, server_1 = wallets[1]
wallet_maker = wallet_node_maker.wallet_state_manager.main_wallet
wallet_taker = wallet_node_taker.wallet_state_manager.main_wallet
ph_maker = await wallet_maker.get_new_puzzlehash()
ph_taker = await wallet_taker.get_new_puzzlehash()
# token_ph = bytes32(token_bytes())
if trusted:
wallet_node_maker.config["trusted_peers"] = {
full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex()
}
wallet_node_taker.config["trusted_peers"] = {
full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex()
}
else:
wallet_node_maker.config["trusted_peers"] = {}
wallet_node_taker.config["trusted_peers"] = {}
await server_0.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None)
await server_1.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None)
for _ in range(1, num_blocks):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_maker))
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_taker))
funds = sum(
[calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) for i in range(1, num_blocks - 1)]
)
await time_out_assert(10, wallet_maker.get_unconfirmed_balance, funds)
await time_out_assert(10, wallet_maker.get_confirmed_balance, funds)
for _ in range(1, num_blocks):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_maker))
for _ in range(1, num_blocks):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_maker))
did_wallet_taker: DIDWallet = await DIDWallet.create_new_did_wallet(
wallet_node_taker.wallet_state_manager, wallet_taker, uint64(1)
)
spend_bundle_list = await wallet_node_taker.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(
wallet_maker.id()
)
spend_bundle = spend_bundle_list[0].spend_bundle
await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name())
for _ in range(1, num_blocks):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_taker))
await time_out_assert(15, wallet_taker.get_pending_change_balance, 0)
hex_did_id = did_wallet_taker.get_my_DID()
did_id = bytes32.fromhex(hex_did_id)
target_puzhash = ph_taker
royalty_puzhash = ph_taker
royalty_percentage = uint16(200)
nft_wallet_taker = await NFTWallet.create_new_nft_wallet(
wallet_node_taker.wallet_state_manager, wallet_taker, name="NFT WALLET DID TAKER", did_id=did_id
)
metadata = Program.to(
[
("u", ["https://www.chia.net/img/branding/chia-logo.svg"]),
("h", "0xD4584AD463139FA8C0D9F68F4B59F185"),
]
)
await time_out_assert(10, wallet_taker.get_unconfirmed_balance, 1999999999999)
await time_out_assert(10, wallet_taker.get_confirmed_balance, 1999999999999)
sb = await nft_wallet_taker.generate_new_nft(
metadata,
target_puzhash,
royalty_puzhash,
royalty_percentage,
did_id,
)
assert sb
# ensure hints are generated
assert compute_memos(sb)
await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name())
for i in range(1, num_blocks):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_maker))
await time_out_assert(10, len, 1, nft_wallet_taker.my_nft_coins)
# MAKER SETUP - NO DID
nft_wallet_maker = await NFTWallet.create_new_nft_wallet(
wallet_node_maker.wallet_state_manager, wallet_maker, name="NFT WALLET MAKER"
)
# maker create offer: NFT for xch
trade_manager_maker = wallet_maker.wallet_state_manager.trade_manager
trade_manager_taker = wallet_taker.wallet_state_manager.trade_manager
coins_maker = nft_wallet_maker.my_nft_coins
assert len(coins_maker) == 0
coins_taker = nft_wallet_taker.my_nft_coins
assert len(coins_taker) == 1
nft_to_request = coins_taker[0]
nft_to_request_info: Optional[PuzzleInfo] = match_puzzle(nft_to_request.full_puzzle)
assert isinstance(nft_to_request_info, PuzzleInfo)
nft_to_request_asset_id = create_asset_id(nft_to_request_info)
xch_offered = 1000
offer_fee = 10
driver_dict = {nft_to_request_asset_id: nft_to_request_info}
offer_dict = {nft_to_request_asset_id: 1, wallet_maker.id(): -xch_offered}
success, trade_make, error = await trade_manager_maker.create_offer_for_ids(offer_dict, driver_dict, fee=offer_fee)
await asyncio.sleep(1)
assert success is True
assert error is None
assert trade_make is not None
success, trade_take, error = await trade_manager_taker.respond_to_offer(
Offer.from_bytes(trade_make.offer), fee=uint64(1)
)
await asyncio.sleep(1)
assert error is None
assert success is True
assert trade_take is not None
for _ in range(1, num_blocks):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_maker))
await time_out_assert(5, len, 1, nft_wallet_maker.my_nft_coins)
await time_out_assert(5, len, 0, nft_wallet_taker.my_nft_coins)
+81 -5
View File
@@ -1,3 +1,4 @@
from secrets import token_bytes
from typing import Tuple
from clvm.casts import int_from_bytes
@@ -11,11 +12,12 @@ from chia.wallet.nft_wallet.nft_puzzles import (
create_nft_layer_puzzle_with_curry_params,
recurry_nft_puzzle,
)
from chia.wallet.outer_puzzles import match_puzzle
from chia.wallet.puzzles.load_clvm import load_clvm
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_for_pk, solution_for_conditions
from tests.core.make_block_generator import int_to_public_key
SINGLETON_MOD = load_clvm("singleton_top_layer.clvm")
SINGLETON_MOD = load_clvm("singleton_top_layer_v1_1.clvm")
LAUNCHER_PUZZLE = load_clvm("singleton_launcher.clvm")
DID_MOD = load_clvm("did_innerpuz.clvm")
NFT_STATE_LAYER_MOD = load_clvm("nft_state_layer.clvm")
@@ -30,6 +32,76 @@ LAUNCHER_ID = Program.to(b"launcher-id").get_tree_hash()
NFT_METADATA_UPDATER_DEFAULT = load_clvm("nft_metadata_updater_default.clvm")
def test_nft_transfer_puzzle_hashes():
maker_pk = int_to_public_key(111)
maker_p2_puz = puzzle_for_pk(maker_pk)
maker_p2_ph = maker_p2_puz.get_tree_hash()
maker_did = Program.to("maker did").get_tree_hash()
# maker_did_inner_hash = Program.to("maker did inner hash").get_tree_hash()
metadata = [
("u", ["https://www.chia.net/img/branding/chia-logo.svg"]),
("h", 0xD4584AD463139FA8C0D9F68F4B59F185),
]
metadata_updater_hash = NFT_METADATA_UPDATER_DEFAULT.get_tree_hash()
# royalty_addr = maker_p2_ph
royalty_pc = 2000 # basis pts
nft_id = Program.to("nft id").get_tree_hash()
SINGLETON_STRUCT = Program.to((SINGLETON_MOD_HASH, (nft_id, LAUNCHER_PUZZLE_HASH)))
transfer_puz = NFT_TRANSFER_PROGRAM_DEFAULT.curry(
SINGLETON_STRUCT,
maker_p2_ph,
royalty_pc,
)
ownership_puz = NFT_OWNERSHIP_LAYER.curry(
NFT_OWNERSHIP_LAYER.get_tree_hash(), maker_did, transfer_puz, maker_p2_puz
)
metadata_puz = NFT_STATE_LAYER_MOD.curry(
NFT_STATE_LAYER_MOD.get_tree_hash(), metadata, metadata_updater_hash, ownership_puz
)
nft_puz = SINGLETON_MOD.curry(SINGLETON_STRUCT, metadata_puz)
nft_info = match_puzzle(nft_puz)
assert nft_info.also().also() is not None
unft = uncurry_nft.UncurriedNFT.uncurry(nft_puz)
assert unft.supports_did
# setup transfer
taker_pk = int_to_public_key(222)
taker_p2_puz = puzzle_for_pk(taker_pk)
taker_p2_ph = taker_p2_puz.get_tree_hash()
# make nft solution
fake_lineage_proof = Program.to([token_bytes(32), maker_p2_ph, 1])
transfer_conditions = Program.to([[51, taker_p2_ph, 1, [taker_p2_ph]], [-10, [], [], []]])
ownership_sol = Program.to([solution_for_conditions(transfer_conditions)])
metadata_sol = Program.to([ownership_sol])
nft_sol = Program.to([fake_lineage_proof, 1, metadata_sol])
conds = nft_puz.run(nft_sol)
# get the new NFT puzhash
for cond in conds.as_iter():
if cond.first().as_int() == 51:
expected_ph = bytes32(cond.at("rf").atom)
# recreate the puzzle for new_puzhash
new_ownership_puz = NFT_OWNERSHIP_LAYER.curry(NFT_OWNERSHIP_LAYER.get_tree_hash(), None, transfer_puz, taker_p2_puz)
new_metadata_puz = NFT_STATE_LAYER_MOD.curry(
NFT_STATE_LAYER_MOD.get_tree_hash(), metadata, metadata_updater_hash, new_ownership_puz
)
new_nft_puz = SINGLETON_MOD.curry(SINGLETON_STRUCT, new_metadata_puz)
calculated_ph = new_nft_puz.get_tree_hash()
assert expected_ph == calculated_ph
def make_a_new_solution() -> Tuple[Program, Program]:
destination = int_to_public_key(2)
p2_puzzle = puzzle_for_pk(destination)
@@ -49,8 +121,12 @@ def make_a_new_solution() -> Tuple[Program, Program]:
]
solution = Program.to(
[
[solution_for_conditions(condition_list)],
]
[],
[],
[
[solution_for_conditions(condition_list)],
],
],
)
return p2_puzzle, solution
@@ -105,12 +181,12 @@ def test_transfer_puzzle_builder() -> None:
NFT_METADATA_UPDATER_DEFAULT.get_tree_hash(),
ownership_puzzle,
)
clvm_puzzle_hash = get_updated_nft_puzzle(clvm_nft_puzzle, solution)
clvm_puzzle_hash = get_updated_nft_puzzle(clvm_nft_puzzle, solution.at("rrf"))
unft = uncurry_nft.UncurriedNFT.uncurry(puzzle)
assert unft.nft_state_layer == clvm_nft_puzzle
assert unft.inner_puzzle == ownership_puzzle
assert unft.p2_puzzle == p2_puzzle
ol_puzzle = recurry_nft_puzzle(unft, solution.first(), sp2_puzzle)
ol_puzzle = recurry_nft_puzzle(unft, solution, sp2_puzzle)
nft_puzzle = create_nft_layer_puzzle_with_curry_params(
Program.to(metadata), NFT_METADATA_UPDATER_DEFAULT.get_tree_hash(), ol_puzzle
)
@@ -0,0 +1,75 @@
from typing import Optional
from clvm_tools.binutils import assemble
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.util.ints import uint16
from chia.wallet.nft_wallet.ownership_outer_puzzle import puzzle_for_ownership_layer
from chia.wallet.nft_wallet.transfer_program_puzzle import puzzle_for_transfer_program
from chia.wallet.outer_puzzles import (
construct_puzzle,
create_asset_id,
get_inner_puzzle,
get_inner_solution,
match_puzzle,
solve_puzzle,
)
from chia.wallet.puzzle_drivers import PuzzleInfo, Solver
def test_ownership_outer_puzzle() -> None:
ACS = Program.to(1)
NIL = Program.to([])
owner = bytes32([0] * 32)
# (mod (current_owner conditions solution)
# (list current_owner () conditions)
# )
transfer_program = assemble( # type: ignore
"""
(c 2 (c () (c 5 ())))
"""
)
transfer_program_default: Program = puzzle_for_transfer_program(bytes32([1] * 32), bytes32([2] * 32), uint16(5000))
ownership_puzzle: Program = puzzle_for_ownership_layer(owner, transfer_program, ACS)
ownership_puzzle_empty: Program = puzzle_for_ownership_layer(NIL, transfer_program, ACS)
ownership_puzzle_default: Program = puzzle_for_ownership_layer(owner, transfer_program_default, ACS)
ownership_driver: Optional[PuzzleInfo] = match_puzzle(ownership_puzzle)
ownership_driver_empty: Optional[PuzzleInfo] = match_puzzle(ownership_puzzle_empty)
ownership_driver_default: Optional[PuzzleInfo] = match_puzzle(ownership_puzzle_default)
transfer_program_driver: Optional[PuzzleInfo] = match_puzzle(transfer_program_default)
assert ownership_driver is not None
assert ownership_driver_empty is not None
assert ownership_driver_default is not None
assert transfer_program_driver is not None
assert ownership_driver.type() == "ownership"
assert ownership_driver["owner"] == owner
assert ownership_driver_empty["owner"] == NIL
assert ownership_driver["transfer_program"] == transfer_program
assert ownership_driver_default["transfer_program"] == transfer_program_driver
assert transfer_program_driver.type() == "royalty transfer program"
assert transfer_program_driver["launcher_id"] == bytes32([1] * 32)
assert transfer_program_driver["royalty_address"] == bytes32([2] * 32)
assert transfer_program_driver["royalty_percentage"] == 5000
assert construct_puzzle(ownership_driver, ACS) == ownership_puzzle
assert construct_puzzle(ownership_driver_empty, ACS) == ownership_puzzle_empty
assert construct_puzzle(ownership_driver_default, ACS) == ownership_puzzle_default
assert get_inner_puzzle(ownership_driver, ownership_puzzle) == ACS
assert create_asset_id(ownership_driver) is None
# Set up for solve
inner_solution = Program.to(
[
[51, ACS.get_tree_hash(), 1],
[-10],
]
)
solution: Program = solve_puzzle(
ownership_driver,
Solver({}),
ACS,
inner_solution,
)
ownership_puzzle.run(solution)
assert get_inner_solution(ownership_driver, solution) == inner_solution