mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2026-09-05 02:24:21 -05:00
Merge pull request #11832 from Chia-Network/rewind_nft1_offers
NFT1 offers again
This commit is contained in:
@@ -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: |
|
||||
|
||||
@@ -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,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]))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
+1
-1
@@ -83,4 +83,4 @@
|
||||
)
|
||||
|
||||
|
||||
)
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user