Merge pull request #11641 from AmineKhaldi/offer_generalization_bringup_into_release

Quexington's Offer generalization bringup into release/1.4.0
This commit is contained in:
William Allen
2022-05-26 22:36:39 -05:00
committed by GitHub
18 changed files with 700 additions and 139 deletions
+1 -1
View File
@@ -81,7 +81,7 @@ jobs:
- name: Test clvm code with pytest
run: |
. ./activate
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 4 -m "not benchmark" tests/clvm/test_chialisp_deserialization.py tests/clvm/test_clvm_compilation.py tests/clvm/test_clvm_step.py tests/clvm/test_program.py tests/clvm/test_puzzle_compression.py tests/clvm/test_puzzles.py tests/clvm/test_serialized_program.py tests/clvm/test_singletons.py tests/clvm/test_spend_sim.py
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 4 -m "not benchmark" tests/clvm/test_chialisp_deserialization.py tests/clvm/test_clvm_compilation.py tests/clvm/test_clvm_step.py tests/clvm/test_program.py tests/clvm/test_puzzle_compression.py tests/clvm/test_puzzle_drivers.py tests/clvm/test_puzzles.py tests/clvm/test_serialized_program.py tests/clvm/test_singletons.py tests/clvm/test_spend_sim.py
- name: Process coverage data
run: |
+1 -1
View File
@@ -80,7 +80,7 @@ jobs:
- name: Test clvm code with pytest
run: |
. ./activate
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 4 -m "not benchmark" tests/clvm/test_chialisp_deserialization.py tests/clvm/test_clvm_compilation.py tests/clvm/test_clvm_step.py tests/clvm/test_program.py tests/clvm/test_puzzle_compression.py tests/clvm/test_puzzles.py tests/clvm/test_serialized_program.py tests/clvm/test_singletons.py tests/clvm/test_spend_sim.py
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 4 -m "not benchmark" tests/clvm/test_chialisp_deserialization.py tests/clvm/test_clvm_compilation.py tests/clvm/test_clvm_step.py tests/clvm/test_program.py tests/clvm/test_puzzle_compression.py tests/clvm/test_puzzle_drivers.py tests/clvm/test_puzzles.py tests/clvm/test_serialized_program.py tests/clvm/test_singletons.py tests/clvm/test_spend_sim.py
- name: Process coverage data
run: |
+4 -4
View File
@@ -4,7 +4,7 @@ import sys
import time
from datetime import datetime
from decimal import Decimal
from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple
from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Union
import aiohttp
@@ -281,7 +281,7 @@ async def make_offer(args: dict, wallet_client: WalletRpcClient, fingerprint: in
if [] in [offers, requests]:
print("Not creating offer: Must be offering and requesting at least one asset")
else:
offer_dict: Dict[uint32, int] = {}
offer_dict: Dict[Union[uint32, str], int] = {}
printable_dict: Dict[str, Tuple[str, int, int]] = {} # Dict[asset_name, Tuple[amount, unit, multiplier]]
for item in [*offers, *requests]:
wallet_id, amount = tuple(item.split(":")[0:2])
@@ -375,7 +375,7 @@ async def print_trade_record(record, wallet_client: WalletRpcClient, summaries:
if summaries:
print("Summary:")
offer = Offer.from_bytes(record.offer)
offered, requested = offer.summary()
offered, requested, _ = offer.summary()
outbound_balances: Dict[str, int] = offer.get_pending_amounts()
fees: Decimal = Decimal(offer.bundle.fees())
cat_name_resolver = wallet_client.cat_asset_id_to_name
@@ -452,7 +452,7 @@ async def take_offer(args: dict, wallet_client: WalletRpcClient, fingerprint: in
print("Please enter a valid offer file or hex blob")
return
offered, requested = offer.summary()
offered, requested, _ = offer.summary()
cat_name_resolver = wallet_client.cat_asset_id_to_name
print("Summary:")
print(" OFFERED:")
+24 -4
View File
@@ -32,6 +32,8 @@ from chia.wallet.derive_keys import (
match_address_to_sk,
)
from chia.wallet.did_wallet.did_wallet import DIDWallet
from chia.wallet.outer_puzzles import AssetType
from chia.wallet.puzzle_drivers import PuzzleInfo
from chia.wallet.rl_wallet.rl_wallet import RLWallet
from chia.wallet.trade_record import TradeRecord
from chia.wallet.trading.offer import Offer
@@ -918,10 +920,28 @@ class WalletRpcApi:
offer: Dict[str, int] = request["offer"]
fee: uint64 = uint64(request.get("fee", 0))
validate_only: bool = request.get("validate_only", False)
driver_dict_str: Optional[Dict[str, Any]] = request.get("driver_dict", None)
# 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
else:
for key, value in driver_dict_str.items():
driver_dict[bytes32.from_hexstr(key)] = PuzzleInfo(value)
modified_offer = {}
for key in offer:
modified_offer[int(key)] = offer[key]
try:
modified_offer[bytes32.from_hexstr(key)] = offer[key]
except ValueError:
modified_offer[int(key)] = offer[key]
async with self.service.wallet_state_manager.lock:
(
@@ -929,7 +949,7 @@ class WalletRpcApi:
trade_record,
error,
) = await self.service.wallet_state_manager.trade_manager.create_offer_for_ids(
modified_offer, fee=fee, validate_only=validate_only
modified_offer, driver_dict, fee=fee, validate_only=validate_only
)
if success:
return {
@@ -942,9 +962,9 @@ class WalletRpcApi:
assert self.service.wallet_state_manager is not None
offer_hex: str = request["offer"]
offer = Offer.from_bech32(offer_hex)
offered, requested = offer.summary()
offered, requested, infos = offer.summary()
return {"summary": {"offered": offered, "requested": requested, "fees": offer.bundle.fees()}}
return {"summary": {"offered": offered, "requested": requested, "fees": offer.bundle.fees(), "infos": infos}}
async def check_offer_validity(self, request):
assert self.service.wallet_state_manager is not None
+14 -3
View File
@@ -1,4 +1,4 @@
from typing import Dict, List, Optional, Any, Tuple
from typing import Dict, List, Optional, Any, Tuple, Union
from chia.pools.pool_wallet_info import PoolWalletInfo
from chia.rpc.rpc_client import RpcClient
@@ -420,13 +420,24 @@ class WalletRpcClient(RpcClient):
# Offers
async def create_offer_for_ids(
self, offer_dict: Dict[uint32, int], fee=uint64(0), validate_only: bool = False
self,
offer_dict: Dict[Union[uint32, str], int],
driver_dict: Dict[str, Any] = None,
fee=uint64(0),
validate_only: bool = False,
) -> Tuple[Optional[Offer], TradeRecord]:
send_dict: Dict[str, int] = {}
for key in offer_dict:
send_dict[str(key)] = offer_dict[key]
res = await self.fetch("create_offer_for_ids", {"offer": send_dict, "validate_only": validate_only, "fee": fee})
req = {
"offer": send_dict,
"validate_only": validate_only,
"fee": fee,
}
if driver_dict is not None:
req["driver_dict"] = driver_dict
res = await self.fetch("create_offer_for_ids", req)
offer: Optional[Offer] = None if validate_only else Offer.from_bech32(res["offer"])
offer_str: str = "" if offer is None else bytes(offer).hex()
return offer, TradeRecord.from_json_dict_convenience(res["trade_record"], offer_str)
@@ -0,0 +1,92 @@
from dataclasses import dataclass
from typing import Any, List, Optional
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,
SpendableCAT,
construct_cat_puzzle,
match_cat_puzzle,
unsigned_spend_bundle_for_spendable_cats,
)
from chia.wallet.lineage_proof import LineageProof
from chia.wallet.puzzle_drivers import PuzzleInfo, Solver
@dataclass(frozen=True)
class CATOuterPuzzle:
_match: Any
_asset_id: Any
_construct: Any
_solve: Any
def match(self, puzzle: Program) -> Optional[PuzzleInfo]:
matched, curried_args = match_cat_puzzle(puzzle)
if matched:
_, tail_hash, inner_puzzle = curried_args
constructor_dict = {
"type": "CAT",
"tail": "0x" + tail_hash.as_python().hex(),
}
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 bytes32(constructor["tail"])
def construct(self, constructor: PuzzleInfo, inner_puzzle: Program) -> Program:
if constructor.also() is not None:
inner_puzzle = self._construct(constructor.also(), inner_puzzle)
return construct_cat_puzzle(CAT_MOD, constructor["tail"], inner_puzzle)
def solve(self, constructor: PuzzleInfo, solver: Solver, inner_puzzle: Program, inner_solution: Program) -> Program:
tail_hash: bytes32 = constructor["tail"]
spendable_cats: List[SpendableCAT] = []
target_coin: Coin
for coin_prog, spend_prog, puzzle, solution in [
*zip(
solver["siblings"].as_iter(),
solver["sibling_spends"].as_iter(),
solver["sibling_puzzles"].as_iter(),
solver["sibling_solutions"].as_iter(),
),
(
Program.to(solver["coin"]),
Program.to(solver["parent_spend"]),
inner_puzzle,
inner_solution,
),
]:
coin_bytes: bytes = coin_prog.as_python()
coin = Coin(bytes32(coin_bytes[0:32]), bytes32(coin_bytes[32:64]), uint64.from_bytes(coin_bytes[64:72]))
if coin_bytes == solver["coin"]:
target_coin = coin
parent_spend: CoinSpend = CoinSpend.from_bytes(spend_prog.as_python())
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)
matched, curried_args = match_cat_puzzle(parent_spend.puzzle_reveal.to_program())
assert matched
_, _, parent_inner_puzzle = curried_args
spendable_cats.append(
SpendableCAT(
coin,
tail_hash,
puzzle,
solution,
lineage_proof=LineageProof(
parent_coin.parent_coin_info, parent_inner_puzzle.get_tree_hash(), parent_coin.amount
),
)
)
bundle = unsigned_spend_bundle_for_spendable_cats(CAT_MOD, spendable_cats)
return next(cs.solution.to_program() for cs in bundle.coin_spends if cs.coin == target_coin)
+35
View File
@@ -37,6 +37,8 @@ from chia.wallet.cat_wallet.lineage_store import CATLineageStore
from chia.wallet.coin_selection import select_coins
from chia.wallet.derivation_record import DerivationRecord
from chia.wallet.lineage_proof import LineageProof
from chia.wallet.outer_puzzles import AssetType
from chia.wallet.puzzle_drivers import PuzzleInfo
from chia.wallet.payment import Payment
from chia.wallet.puzzles.tails import ALL_LIMITATIONS_PROGRAMS
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
@@ -205,6 +207,23 @@ class CATWallet:
await self.wallet_state_manager.add_new_wallet(self, self.id(), in_transaction=in_transaction)
return self
@classmethod
async def create_from_puzzle_info(
cls,
wallet_state_manager: Any,
wallet: Wallet,
puzzle_driver: PuzzleInfo,
name=None,
in_transaction=False,
) -> CATWallet:
return await cls.create_wallet_for_cat(
wallet_state_manager,
wallet,
puzzle_driver["tail"].hex(),
name,
in_transaction,
)
@staticmethod
async def create(
wallet_state_manager: Any,
@@ -790,3 +809,19 @@ class CATWallet:
wallet_info = WalletInfo(current_info.id, current_info.name, current_info.type, data_str)
self.wallet_info = wallet_info
await self.wallet_state_manager.user_store.update_wallet(wallet_info, in_transaction)
def match_puzzle_info(self, puzzle_driver: PuzzleInfo) -> bool:
return (
AssetType(puzzle_driver.type()) == AssetType.CAT
and puzzle_driver["tail"] == bytes.fromhex(self.get_asset_id())
and puzzle_driver.also() is None
)
def get_puzzle_info(self, asset_id: bytes32) -> PuzzleInfo:
return PuzzleInfo({"type": AssetType.CAT.value, "tail": "0x" + self.get_asset_id()})
async def get_coins_to_offer(self, asset_id: Optional[bytes32], amount: uint64) -> Set[Coin]:
balance = await self.get_confirmed_balance()
if balance < amount:
raise Exception(f"insufficient funds in wallet {self.id()}")
return await self.select_coins(amount)
+57
View File
@@ -0,0 +1,57 @@
from enum import Enum
from typing import Any, Dict, Optional
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.puzzle_drivers import PuzzleInfo, Solver
"""
This file provides a central location for acquiring drivers for outer puzzles like CATs, NFTs, etc.
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
- 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
- Given a PuzzleInfo object and an innermost puzzle, construct a puzzle reveal for a coin spend
- solve(self, constructor: PuzzleInfo, solver: Solver, inner_puzzle: Program, inner_solution: Program) -> Program
- Given a PuzzleInfo object, a Solver object, and an innermost puzzle and its solution return a solution for a spend
- The "Solver" object can contain any dictionary, it's up to the driver to enforce the needed elements of the API
- Some classes that wish to integrate with a driver may not have access to all of the info it needs so the driver
needs to raise errors appropriately
"""
class AssetType(Enum):
CAT = "CAT"
def match_puzzle(puzzle: Program) -> Optional[PuzzleInfo]:
for driver in driver_lookup.values():
potential_info: Optional[PuzzleInfo] = driver.match(puzzle)
if potential_info is not None:
return potential_info
return None
def construct_puzzle(constructor: PuzzleInfo, inner_puzzle: Program) -> Program:
return driver_lookup[AssetType(constructor.type())].construct(constructor, inner_puzzle) # type: ignore
def solve_puzzle(constructor: PuzzleInfo, solver: Solver, inner_puzzle: Program, inner_solution: Program) -> Program:
return driver_lookup[AssetType(constructor.type())].solve( # type: ignore
constructor, solver, inner_puzzle, inner_solution
)
def create_asset_id(constructor: PuzzleInfo) -> bytes32:
return driver_lookup[AssetType(constructor.type())].asset_id(constructor) # type: ignore
function_args = [match_puzzle, construct_puzzle, solve_puzzle, create_asset_id]
driver_lookup: Dict[AssetType, Any] = {
AssetType.CAT: CATOuterPuzzle(*function_args),
}
+92
View File
@@ -0,0 +1,92 @@
from dataclasses import dataclass
from typing import Any, Dict, Optional
from clvm.casts import int_from_bytes
from clvm.SExp import SExp
from clvm_tools.binutils import assemble, type_for_atom
from ir.Type import Type
from chia.types.blockchain_format.program import Program
"""
The following two classes act as wrapper classes around dictionaries of strings.
Values in the dictionary are assumed to be strings in CLVM format (0x for bytes, etc.)
When you access a value in the dictionary, it will be deserialized to a str, int, bytes, or Program appropriately.
"""
@dataclass(frozen=True)
class PuzzleInfo:
"""
There are two 'magic' keys in a PuzzleInfo object:
- 'type' must be an included key (for easy lookup of drivers)
- 'also' gets its own method as it's the supported way to do recursion of PuzzleInfos
"""
info: Dict[str, Any]
def __post_init__(self) -> None:
if "type" not in self.info:
raise ValueError("A type is required to initialize a puzzle driver")
def __getitem__(self, item: str) -> Any:
value = self.info[item]
return decode_info_value(PuzzleInfo, value)
def __eq__(self, other: object) -> bool:
for key, value in self.info.items():
try:
if self[key] != other[key]: # type: ignore
return False
except Exception:
return False
return True
def type(self) -> str:
return str(self.info["type"])
def also(self) -> Optional["PuzzleInfo"]:
if "also" in self.info:
return PuzzleInfo(self.info["also"])
else:
return None
@dataclass(frozen=True)
class Solver:
info: Dict[str, Any]
def __getitem__(self, item: str) -> Any:
value = self.info[item]
return decode_info_value(Solver, value)
def __eq__(self, other: object) -> bool:
for key, value in self.info.items():
try:
if self[key] != other[key]: # type: ignore
return False
except Exception:
return False
return True
def decode_info_value(cls: Any, value: Any) -> Any:
if isinstance(value, dict):
return cls(value)
elif isinstance(value, list):
return [decode_info_value(cls, v) for v in value]
else:
if value == "()": # special case
return Program.to([])
expression: SExp = assemble(value) # type: ignore
if expression.atom is None:
return Program(expression)
else:
atom: bytes = expression.atom
typ = type_for_atom(atom)
if typ == Type.QUOTES:
return bytes(atom).decode("utf8")
elif typ == Type.INT:
return int_from_bytes(atom)
else:
return atom
+110 -34
View File
@@ -2,7 +2,7 @@ import dataclasses
import logging
import time
import traceback
from typing import Any, Dict, List, Optional, Tuple, Union, Set
from typing import Any, Dict, List, Optional, Set, Tuple, Union
from chia.protocols.wallet_protocol import CoinState
from chia.types.blockchain_format.coin import Coin, coin_as_list
@@ -12,10 +12,10 @@ 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.cat_wallet.cat_wallet import CATWallet
from chia.wallet.payment import Payment
from chia.wallet.puzzle_drivers import PuzzleInfo
from chia.wallet.trade_record import TradeRecord
from chia.wallet.trading.offer import Offer, NotarizedPayment
from chia.wallet.trading.offer import NotarizedPayment, Offer
from chia.wallet.trading.trade_status import TradeStatus
from chia.wallet.trading.trade_store import TradeStore
from chia.wallet.transaction_record import TransactionRecord
@@ -26,6 +26,42 @@ from chia.wallet.wallet_coin_record import WalletCoinRecord
class TradeManager:
"""
This class is a driver for creating and accepting settlement_payments.clvm style offers.
By default, standard XCH is supported but to support other types of assets you must implement certain functions on
the asset's wallet as well as create a driver for its puzzle(s). Here is a guide to integrating a new types of
assets with this trade manager:
Puzzle Drivers:
- See chia/wallet/outer_puzzles.py for a full description of how to build these
- The `solve` method must be able to be solved by a Solver that looks like this:
Solver(
{
"coin": bytes
"parent_spend": bytes
"siblings": List[bytes] # other coins of the same type being offered
"sibling_spends": List[bytes] # The parent spends for the siblings
"sibling_puzzles": List[Program] # The inner puzzles of the siblings (always OFFER_MOD)
"sibling_solutions": List[Program] # The inner solution of the siblings
}
)
Wallet:
- Segments in this code that call general wallet methods are highlighted by comments: # ATTENTION: new wallets
- To be able to be traded, a wallet must implement these methods on itself:
- generate_signed_transaction(...) -> List[TransactionRecord] (See cat_wallet.py for full API)
- convert_puzzle_hash(puzzle_hash: bytes32) -> bytes32 # Converts a puzzlehash from outer to inner puzzle
- get_puzzle_info(asset_id: bytes32) -> PuzzleInfo
- get_coins_to_offer(asset_id: bytes32, amount: uint64) -> Set[Coin]
- If you would like assets from your wallet to be referenced with just a wallet ID, you must also implement:
- get_asset_id() -> bytes32
- Finally, you must make sure that your wallet will respond appropriately when these WSM methods are called:
- get_wallet_for_puzzle_info(puzzle_info: PuzzleInfo) -> <Your wallet>
- create_wallet_for_puzzle_info(puzzle_info: PuzzleInfo) -> <Your wallet>
- get_wallet_for_asset_id(asset_id: bytes32) -> <Your wallet>
"""
wallet_state_manager: Any
log: logging.Logger
trade_store: TradeStore
@@ -188,6 +224,7 @@ class TradeManager:
continue
new_ph = await wallet.get_new_puzzlehash()
# This should probably not switch on whether or not we're spending a CAT but it has to for now
# ATTENTION: new_wallets
if wallet.type() == WalletType.CAT:
txs = await wallet.generate_signed_transaction(
[coin.amount], [new_ph], fee=fee_to_pay, coins={coin}, ignore_max_send_amount=True
@@ -246,9 +283,15 @@ class TradeManager:
self.wallet_state_manager.state_changed("offer_added")
async def create_offer_for_ids(
self, offer: Dict[Union[int, bytes32], int], fee: uint64 = uint64(0), validate_only: bool = False
self,
offer: Dict[Union[int, bytes32], int],
driver_dict: Optional[Dict[bytes32, PuzzleInfo]] = None,
fee: uint64 = uint64(0),
validate_only: bool = False,
) -> Tuple[bool, Optional[TradeRecord], Optional[str]]:
success, created_offer, error = await self._create_offer_for_ids(offer, fee=fee)
if driver_dict is None:
driver_dict = {}
success, created_offer, error = await self._create_offer_for_ids(offer, driver_dict, fee=fee)
if not success or created_offer is None:
raise Exception(f"Error creating offer: {error}")
@@ -273,13 +316,18 @@ class TradeManager:
return success, trade_offer, error
async def _create_offer_for_ids(
self, offer_dict: Dict[Union[int, bytes32], int], fee: uint64 = uint64(0)
self,
offer_dict: Dict[Union[int, bytes32], int],
driver_dict: Optional[Dict[bytes32, PuzzleInfo]] = None,
fee: uint64 = uint64(0),
) -> Tuple[bool, Optional[Offer], Optional[str]]:
"""
Offer is dictionary of wallet ids and amount
"""
if driver_dict is None:
driver_dict = {}
try:
coins_to_offer: Dict[uint32, List[Coin]] = {}
coins_to_offer: Dict[Union[int, bytes32], List[Coin]] = {}
requested_payments: Dict[Optional[bytes32], List[Payment]] = {}
for id, amount in offer_dict.items():
if amount > 0:
@@ -288,44 +336,72 @@ class TradeManager:
wallet = self.wallet_state_manager.wallets[wallet_id]
p2_ph: bytes32 = await wallet.get_new_puzzlehash()
if wallet.type() == WalletType.STANDARD_WALLET:
key: Optional[bytes32] = None
asset_id: Optional[bytes32] = None
memos: List[bytes] = []
elif wallet.type() == WalletType.CAT:
key = bytes32(bytes.fromhex(wallet.get_asset_id()))
elif callable(getattr(wallet, "get_asset_id", None)): # ATTENTION: new wallets
asset_id = bytes32(bytes.fromhex(wallet.get_asset_id()))
memos = [p2_ph]
else:
raise ValueError(f"Offers are not implemented for {wallet.type()}")
raise ValueError(
f"Cannot request assets from wallet id {wallet.id()} without more information"
)
else:
p2_ph = await self.wallet_state_manager.main_wallet.get_new_puzzlehash()
key = id
asset_id = id
wallet = await self.wallet_state_manager.get_wallet_for_asset_id(asset_id.hex())
memos = [p2_ph]
requested_payments[key] = [Payment(p2_ph, uint64(amount), memos)]
requested_payments[asset_id] = [Payment(p2_ph, uint64(amount), memos)]
elif amount < 0:
assert isinstance(id, int)
wallet_id = uint32(id)
wallet = self.wallet_state_manager.wallets[wallet_id]
balance = await wallet.get_confirmed_balance()
if balance < abs(amount):
raise Exception(f"insufficient funds in wallet {wallet_id}")
coins_to_offer[wallet_id] = await wallet.select_coins(uint64(abs(amount)))
if isinstance(id, int):
wallet_id = uint32(id)
wallet = self.wallet_state_manager.wallets[wallet_id]
if wallet.type() == WalletType.STANDARD_WALLET:
asset_id = None
elif callable(getattr(wallet, "get_asset_id", None)): # ATTENTION: new wallets
asset_id = bytes32(bytes.fromhex(wallet.get_asset_id()))
else:
raise ValueError(
f"Cannot offer assets from wallet id {wallet.id()} without more information"
)
else:
asset_id = id
wallet = await self.wallet_state_manager.get_wallet_for_asset_id(asset_id.hex())
if not callable(getattr(wallet, "get_coins_to_offer", None)): # ATTENTION: new wallets
raise ValueError(f"Cannot offer coins from wallet id {wallet.id()}")
coins_to_offer[id] = await wallet.get_coins_to_offer(asset_id, uint64(abs(amount)))
elif amount == 0:
raise ValueError("You cannot offer nor request 0 amount of something")
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}"
)
else:
driver_dict[asset_id] = puzzle_driver
else:
raise ValueError(f"Wallet for asset id {asset_id} is not properly integrated with TradeManager")
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
)
announcements_to_assert = Offer.calculate_announcements(notarized_payments)
announcements_to_assert = Offer.calculate_announcements(notarized_payments, driver_dict)
all_transactions: List[TransactionRecord] = []
fee_left_to_pay: uint64 = fee
for wallet_id, selected_coins in coins_to_offer.items():
wallet = self.wallet_state_manager.wallets[wallet_id]
for id, selected_coins in coins_to_offer.items():
if isinstance(id, int):
wallet = self.wallet_state_manager.wallets[id]
else:
wallet = await self.wallet_state_manager.get_wallet_for_asset_id(id.hex())
# This should probably not switch on whether or not we're spending a CAT but it has to for now
# ATTENTION: new_wallets
if wallet.type() == WalletType.CAT:
txs = await wallet.generate_signed_transaction(
[abs(offer_dict[int(wallet_id)])],
[abs(offer_dict[id])],
[Offer.ph()],
fee=fee_left_to_pay,
coins=set(selected_coins),
@@ -334,7 +410,7 @@ class TradeManager:
all_transactions.extend(txs)
else:
tx = await wallet.generate_signed_transaction(
abs(offer_dict[int(wallet_id)]),
abs(offer_dict[id]),
Offer.ph(),
fee=fee_left_to_pay,
coins=set(selected_coins),
@@ -346,7 +422,7 @@ class TradeManager:
transaction_bundles: List[Optional[SpendBundle]] = [tx.spend_bundle for tx in all_transactions]
total_spend_bundle = SpendBundle.aggregate(list(filter(lambda b: b is not None, transaction_bundles)))
offer = Offer(notarized_payments, total_spend_bundle)
offer = Offer(notarized_payments, total_spend_bundle, driver_dict)
return True, offer, None
except Exception as e:
@@ -358,13 +434,12 @@ class TradeManager:
for key in offer.arbitrage():
wsm = self.wallet_state_manager
wallet: Wallet = wsm.main_wallet
if key is None:
continue
exists: Optional[Wallet] = await wsm.get_wallet_for_asset_id(key.hex())
# ATTENTION: new_wallets
exists: Optional[Wallet] = await wsm.get_wallet_for_puzzle_info(offer.driver_dict[key])
if exists is None:
self.log.info(f"Creating wallet for asset ID: {key}")
await CATWallet.create_wallet_for_cat(wsm, wallet, key.hex())
await wsm.create_wallet_for_puzzle_info(offer.driver_dict[key])
async def check_offer_validity(self, offer: Offer) -> bool:
all_removals: List[Coin] = offer.bundle.removals()
@@ -398,7 +473,7 @@ class TradeManager:
wallet_id, _ = wallet_info
if addition.parent_coin_info in settlement_coin_ids:
wallet = self.wallet_state_manager.wallets[wallet_id]
to_puzzle_hash = await wallet.convert_puzzle_hash(addition.puzzle_hash)
to_puzzle_hash = await wallet.convert_puzzle_hash(addition.puzzle_hash) # ATTENTION: new wallets
txs.append(
TransactionRecord(
confirmed_at_height=uint32(0),
@@ -472,9 +547,10 @@ class TradeManager:
wallet = self.wallet_state_manager.main_wallet
key: Union[bytes32, int] = int(wallet.id())
else:
# ATTENTION: new wallets
wallet = await self.wallet_state_manager.get_wallet_for_asset_id(asset_id.hex())
if wallet is None and amount < 0:
return False, None, f"Do not have a CAT of asset ID: {asset_id} to fulfill offer"
return False, None, f"Do not have a wallet for asset ID: {asset_id} to fulfill offer"
elif wallet is None:
key = asset_id
else:
@@ -486,7 +562,7 @@ class TradeManager:
if not valid:
return False, None, "This offer is no longer valid"
success, take_offer, error = await self._create_offer_for_ids(take_offer_dict, fee=fee)
success, take_offer, error = await self._create_offer_for_ids(take_offer_dict, offer.driver_dict, fee=fee)
if not success or take_offer is None:
return False, None, error
+2 -1
View File
@@ -33,10 +33,11 @@ class TradeRecord(Streamable):
formatted["status"] = TradeStatus(self.status).name
offer_to_summarize: bytes = self.offer if self.taken_offer is None else self.taken_offer
offer = Offer.from_bytes(offer_to_summarize)
offered, requested = offer.summary()
offered, requested, infos = offer.summary()
formatted["summary"] = {
"offered": offered,
"requested": requested,
"infos": infos,
"fees": offer.bundle.fees(),
}
formatted["pending"] = offer.get_pending_amounts()
+113 -73
View File
@@ -1,6 +1,8 @@
from dataclasses import dataclass
from typing import List, Optional, Dict, Set, Tuple
from typing import Any, Dict, List, Optional, Set, Tuple
from blspy import G2Element
from clvm_tools.binutils import disassemble
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.blockchain_format.coin import Coin, coin_as_list
@@ -8,23 +10,17 @@ from chia.types.blockchain_format.program import Program
from chia.types.announcement import Announcement
from chia.types.coin_spend import CoinSpend
from chia.types.spend_bundle import SpendBundle
from chia.util.bech32m import bech32_encode, bech32_decode, convertbits
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.payment import Payment
from chia.wallet.puzzle_drivers import PuzzleInfo, Solver
from chia.wallet.puzzles.load_clvm import load_clvm
from chia.wallet.util.puzzle_compression import (
compress_object_with_puzzles,
decompress_object_with_puzzles,
lowest_best_version,
)
from chia.wallet.cat_wallet.cat_utils import (
CAT_MOD,
SpendableCAT,
construct_cat_puzzle,
match_cat_puzzle,
unsigned_spend_bundle_for_spendable_cats,
)
from chia.wallet.lineage_proof import LineageProof
from chia.wallet.puzzles.load_clvm import load_clvm
from chia.wallet.payment import Payment
OFFER_MOD = load_clvm("settlement_payments.clvm")
ZERO_32 = bytes32([0] * 32)
@@ -48,6 +44,7 @@ class Offer:
Optional[bytes32], List[NotarizedPayment]
] # The key is the asset id of the asset being requested
bundle: SpendBundle
driver_dict: Dict[bytes32, PuzzleInfo] # asset_id -> asset driver
@staticmethod
def ph():
@@ -64,23 +61,25 @@ class Offer:
nonce: bytes32 = Program.to(sorted_coin_list).get_tree_hash()
notarized_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = {}
for tail_hash, payments in requested_payments.items():
notarized_payments[tail_hash] = []
for asset_id, payments in requested_payments.items():
notarized_payments[asset_id] = []
for p in payments:
puzzle_hash, amount, memos = tuple(p.as_condition_args())
notarized_payments[tail_hash].append(NotarizedPayment(puzzle_hash, amount, memos, nonce))
notarized_payments[asset_id].append(NotarizedPayment(puzzle_hash, amount, memos, nonce))
return notarized_payments
# The announcements returned from this function must be asserted in whatever spend bundle is created by the wallet
@staticmethod
def calculate_announcements(
notarized_payments: Dict[Optional[bytes32], List[NotarizedPayment]],
notarized_payments: Dict[Optional[bytes32], List[NotarizedPayment]], driver_dict: Dict[bytes32, PuzzleInfo]
) -> List[Announcement]:
announcements: List[Announcement] = []
for tail, payments in notarized_payments.items():
if tail is not None:
settlement_ph: bytes32 = construct_cat_puzzle(CAT_MOD, tail, OFFER_MOD).get_tree_hash()
for asset_id, payments in notarized_payments.items():
if asset_id is not None:
if asset_id not in driver_dict:
raise ValueError("Cannot calculate announcements without driver of requested item")
settlement_ph: bytes32 = construct_puzzle(driver_dict[asset_id], OFFER_MOD).get_tree_hash()
else:
settlement_ph = OFFER_MOD.get_tree_hash()
@@ -103,6 +102,11 @@ class Offer:
if len(set(payment_programs)) != len(payment_programs):
raise ValueError("Bundle has duplicate requested payments")
# Verify we have a type for every kind of asset
for asset_id in self.requested_payments:
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")
# This method does not get every coin that is being offered, only the `settlement_payment` children
def get_offered_coins(self) -> Dict[Optional[bytes32], List[Coin]]:
offered_coins: Dict[Optional[bytes32], List[Coin]] = {}
@@ -114,22 +118,20 @@ class Offer:
)[0].puzzle_reveal.to_program()
# Determine it's TAIL (or lack of)
matched, curried_args = match_cat_puzzle(parent_puzzle)
tail_hash: Optional[bytes32] = None
if matched:
_, tail_hash_program, _ = curried_args
tail_hash = bytes32(tail_hash_program.as_python())
offer_ph: bytes32 = construct_cat_puzzle(CAT_MOD, tail_hash, OFFER_MOD).get_tree_hash()
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()
else:
tail_hash = None
asset_id = None
offer_ph = OFFER_MOD.get_tree_hash()
# Check if the puzzle_hash matches the hypothetical `settlement_payments` puzzle hash
if addition.puzzle_hash == offer_ph:
if tail_hash in offered_coins:
offered_coins[tail_hash].append(addition)
if asset_id in offered_coins:
offered_coins[asset_id].append(addition)
else:
offered_coins[tail_hash] = [addition]
offered_coins[asset_id] = [addition]
return offered_coins
@@ -160,7 +162,7 @@ class Offer:
return arbitrage_dict
# This is a method mostly for the UI that creates a JSON summary of the offer
def summary(self) -> Tuple[Dict[str, int], Dict[str, int]]:
def summary(self) -> Tuple[Dict[str, int], Dict[str, int], Dict[str, Dict[str, Any]]]:
offered_amounts: Dict[Optional[bytes32], int] = self.get_offered_amounts()
requested_amounts: Dict[Optional[bytes32], int] = self.get_requested_amounts()
@@ -173,7 +175,11 @@ class Offer:
new_dic[key.hex()] = dic[key]
return new_dic
return keys_to_strings(offered_amounts), keys_to_strings(requested_amounts)
driver_dict: Dict[str, Any] = {}
for key, value in self.driver_dict.items():
driver_dict[key.hex()] = value.info
return keys_to_strings(offered_amounts), keys_to_strings(requested_amounts), driver_dict
# 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
@@ -234,6 +240,7 @@ class Offer:
def aggregate(cls, offers: List["Offer"]) -> "Offer":
total_requested_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = {}
total_bundle = SpendBundle([], G2Element())
total_driver_dict: Dict[bytes32, PuzzleInfo] = {}
for offer in offers:
# First check for any overlap in inputs
total_inputs: Set[Coin] = {cs.coin for cs in total_bundle.coin_spends}
@@ -242,15 +249,20 @@ class Offer:
raise ValueError("The aggregated offers overlap inputs")
# Next, do the aggregation
for tail, payments in offer.requested_payments.items():
if tail in total_requested_payments:
total_requested_payments[tail].extend(payments)
for asset_id, payments in offer.requested_payments.items():
if asset_id in total_requested_payments:
total_requested_payments[asset_id].extend(payments)
else:
total_requested_payments[tail] = payments
total_requested_payments[asset_id] = payments
for key, value in offer.driver_dict.items():
if key in total_driver_dict and total_driver_dict[key] != value:
raise ValueError(f"The offers to aggregate disagree on the drivers for {key.hex()}")
total_bundle = SpendBundle.aggregate([total_bundle, offer.bundle])
total_driver_dict.update(offer.driver_dict)
return cls(total_requested_payments, total_bundle)
return cls(total_requested_payments, total_bundle, total_driver_dict)
# Validity is defined by having enough funds within the offer to satisfy both sides
def is_valid(self) -> bool:
@@ -263,55 +275,82 @@ class Offer:
raise ValueError("Offer is currently incomplete")
completion_spends: List[CoinSpend] = []
for tail_hash, payments in self.requested_payments.items():
offered_coins: List[Coin] = self.get_offered_coins()[tail_hash]
for asset_id, payments in self.requested_payments.items():
offered_coins: List[Coin] = self.get_offered_coins()[asset_id]
# Because of CAT supply laws, we must specify a place for the leftovers to go
arbitrage_amount: int = self.arbitrage()[tail_hash]
arbitrage_amount: int = self.arbitrage()[asset_id]
all_payments: List[NotarizedPayment] = payments.copy()
if arbitrage_amount > 0:
assert arbitrage_amount is not None
assert arbitrage_ph is not None
all_payments.append(NotarizedPayment(arbitrage_ph, uint64(arbitrage_amount), []))
# Some assets need to know about siblings so we need to collect all spends first to be able to use them
coin_to_spend_dict: Dict[Coin, CoinSpend] = {}
coin_to_solution_dict: Dict[Coin, Program] = {}
for coin in offered_coins:
parent_spend: CoinSpend = list(
filter(lambda cs: cs.coin.name() == coin.parent_coin_info, self.bundle.coin_spends)
)[0]
coin_to_spend_dict[coin] = parent_spend
inner_solutions = []
if coin == offered_coins[0]:
nonces: List[bytes32] = [p.nonce for p in all_payments]
for nonce in list(dict.fromkeys(nonces)): # dedup without messing with order
nonce_payments: List[NotarizedPayment] = list(filter(lambda p: p.nonce == nonce, all_payments))
inner_solutions.append((nonce, [np.as_condition_args() for np in nonce_payments]))
coin_to_solution_dict[coin] = Program.to(inner_solutions)
if tail_hash:
# CATs have a special way to be solved so we have to do some calculation before getting the solution
parent_spend: CoinSpend = list(
filter(lambda cs: cs.coin.name() == coin.parent_coin_info, self.bundle.coin_spends)
)[0]
parent_coin: Coin = parent_spend.coin
matched, curried_args = match_cat_puzzle(parent_spend.puzzle_reveal.to_program())
assert matched
_, _, inner_puzzle = curried_args
spendable_cat = SpendableCAT(
coin,
tail_hash,
OFFER_MOD,
Program.to(inner_solutions),
lineage_proof=LineageProof(
parent_coin.parent_coin_info, inner_puzzle.get_tree_hash(), parent_coin.amount
for coin in offered_coins:
if asset_id:
siblings: str = "("
sibling_spends: str = "("
sibling_puzzles: str = "("
sibling_solutions: str = "("
disassembled_offer_mod: str = disassemble(OFFER_MOD)
for sibling_coin in offered_coins:
if sibling_coin != coin:
siblings += (
"0x"
+ sibling_coin.parent_coin_info.hex()
+ sibling_coin.puzzle_hash.hex()
+ bytes(sibling_coin.amount).hex()
)
sibling_spends += "0x" + bytes(coin_to_spend_dict[sibling_coin]).hex() + ")"
sibling_puzzles += disassembled_offer_mod
sibling_solutions += disassemble(coin_to_solution_dict[sibling_coin])
siblings += ")"
sibling_spends += ")"
sibling_puzzles += ")"
sibling_solutions += ")"
solution: Program = solve_puzzle(
self.driver_dict[asset_id],
Solver(
{
"coin": "0x"
+ coin.parent_coin_info.hex()
+ coin.puzzle_hash.hex()
+ bytes(coin.amount).hex(),
"parent_spend": "0x" + bytes(coin_to_spend_dict[coin]).hex(),
"siblings": siblings,
"sibling_spends": sibling_spends,
"sibling_puzzles": sibling_puzzles,
"sibling_solutions": sibling_solutions,
}
),
)
solution: Program = (
unsigned_spend_bundle_for_spendable_cats(CAT_MOD, [spendable_cat])
.coin_spends[0]
.solution.to_program()
OFFER_MOD,
Program.to(coin_to_solution_dict[coin]),
)
else:
solution = Program.to(inner_solutions)
solution = Program.to(coin_to_solution_dict[coin])
completion_spends.append(
CoinSpend(
coin,
construct_cat_puzzle(CAT_MOD, tail_hash, OFFER_MOD) if tail_hash else OFFER_MOD,
construct_puzzle(self.driver_dict[asset_id], OFFER_MOD) if asset_id else OFFER_MOD,
solution,
)
)
@@ -321,8 +360,8 @@ class Offer:
def to_spend_bundle(self) -> SpendBundle:
# Before we serialze this as a SpendBundle, we need to serialze the `requested_payments` as dummy CoinSpends
additional_coin_spends: List[CoinSpend] = []
for tail_hash, payments in self.requested_payments.items():
puzzle_reveal: Program = construct_cat_puzzle(CAT_MOD, tail_hash, OFFER_MOD) if tail_hash else OFFER_MOD
for asset_id, payments in self.requested_payments.items():
puzzle_reveal: Program = construct_puzzle(self.driver_dict[asset_id], OFFER_MOD) if asset_id else OFFER_MOD
inner_solutions = []
nonces: List[bytes32] = [p.nonce for p in payments]
for nonce in list(dict.fromkeys(nonces)): # dedup without messing with order
@@ -352,16 +391,17 @@ class Offer:
def from_spend_bundle(cls, bundle: SpendBundle) -> "Offer":
# Because of the `to_spend_bundle` method, we need to parse the dummy CoinSpends as `requested_payments`
requested_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = {}
driver_dict: Dict[bytes32, PuzzleInfo] = {}
leftover_coin_spends: List[CoinSpend] = []
for coin_spend in bundle.coin_spends:
driver = match_puzzle(coin_spend.puzzle_reveal.to_program())
if driver is not None:
asset_id = create_asset_id(driver)
assert asset_id is not None
driver_dict[asset_id] = driver
else:
asset_id = None
if coin_spend.coin.parent_coin_info == ZERO_32:
matched, curried_args = match_cat_puzzle(coin_spend.puzzle_reveal.to_program())
if matched:
_, tail_hash_program, _ = curried_args
tail_hash: Optional[bytes32] = bytes32(tail_hash_program.as_python())
else:
tail_hash = None
notarized_payments: List[NotarizedPayment] = []
for payment_group in coin_spend.solution.to_program().as_iter():
nonce = bytes32(payment_group.first().as_python())
@@ -369,12 +409,12 @@ class Offer:
notarized_payments.extend(
[NotarizedPayment.from_condition_and_nonce(condition, nonce) for condition in payment_args_list]
)
requested_payments[tail_hash] = notarized_payments
requested_payments[asset_id] = notarized_payments
else:
leftover_coin_spends.append(coin_spend)
return cls(requested_payments, SpendBundle(leftover_coin_spends, bundle.aggregated_signature))
return cls(requested_payments, SpendBundle(leftover_coin_spends, bundle.aggregated_signature), driver_dict)
def name(self) -> bytes32:
return self.to_spend_bundle().name()
+8
View File
@@ -526,3 +526,11 @@ class Wallet:
self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM,
)
return spend_bundle
async def get_coins_to_offer(self, asset_id: Optional[bytes32], amount: uint64) -> Set[Coin]:
if asset_id is not None:
raise ValueError(f"The standard wallet cannot offer coins with asset id {asset_id}")
balance = await self.get_confirmed_balance()
if balance < amount:
raise Exception(f"insufficient funds in wallet {self.id()}")
return await self.select_coins(amount)
+26
View File
@@ -37,6 +37,8 @@ from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS
from chia.wallet.derivation_record import DerivationRecord
from chia.wallet.derive_keys import master_sk_to_wallet_sk, master_sk_to_wallet_sk_unhardened
from chia.wallet.key_val_store import KeyValStore
from chia.wallet.outer_puzzles import AssetType
from chia.wallet.puzzle_drivers import PuzzleInfo
from chia.wallet.puzzles.cat_loader import CAT_MOD
from chia.wallet.rl_wallet.rl_wallet import RLWallet
from chia.wallet.settings.user_settings import UserSettings
@@ -111,6 +113,7 @@ class WalletStateManager:
wallet_node: Any
pool_store: WalletPoolStore
default_cats: Dict[str, Any]
asset_to_wallet_map: Dict[AssetType, Any]
@staticmethod
async def create(
@@ -176,6 +179,10 @@ class WalletStateManager:
self.wallets = {main_wallet_info.id: self.main_wallet}
self.asset_to_wallet_map = {
AssetType.CAT: CATWallet,
}
wallet = None
for wallet_info in await self.get_all_wallet_info_entries():
if wallet_info.type == WalletType.STANDARD_WALLET:
@@ -1134,6 +1141,25 @@ class WalletStateManager:
return wallet
return None
async def get_wallet_for_puzzle_info(self, puzzle_driver: PuzzleInfo):
for wallet in self.wallets.values():
match_function = getattr(wallet, "match_puzzle_info", None)
if match_function is not None and callable(match_function):
if match_function(puzzle_driver):
return wallet
return None
async def create_wallet_for_puzzle_info(self, puzzle_driver: PuzzleInfo, name=None, in_transaction=False):
if AssetType(puzzle_driver.type()) in self.asset_to_wallet_map:
async with self.lock:
await self.asset_to_wallet_map[AssetType(puzzle_driver.type())].create_from_puzzle_info(
self,
self.main_wallet,
puzzle_driver,
name,
in_transaction,
)
async def add_new_wallet(self, wallet: Any, wallet_id: int, create_puzzle_hashes=True, in_transaction=False):
self.wallets[uint32(wallet_id)] = wallet
if create_puzzle_hashes:
+43
View File
@@ -0,0 +1,43 @@
from typing import Any, Dict, Union
import pytest
from chia.types.blockchain_format.program import Program
from chia.wallet.puzzle_drivers import PuzzleInfo, Solver
def test_puzzle_info() -> None:
test_driver: Dict[str, Any] = {
"string": "hello",
"bytes": "0xcafef00d",
"int": "123",
"program": "(q . 'hello')",
"zero": "0",
"nil": "()",
}
test_also: Dict[str, Any] = {"type": "TEST", "string": "hello"}
test_driver["also"] = test_also
with pytest.raises(ValueError, match="A type is required"):
PuzzleInfo(test_driver)
solver = Solver(test_driver)
test_driver["type"] = "TEST"
puzzle_info = PuzzleInfo(test_driver)
assert puzzle_info.type() == "TEST"
assert puzzle_info.also() == PuzzleInfo(test_also)
capitalize_bytes = test_driver.copy()
capitalize_bytes["bytes"] = "0xCAFEF00D"
assert solver == Solver(capitalize_bytes)
assert puzzle_info == PuzzleInfo(capitalize_bytes)
obj: Union[PuzzleInfo, Solver]
for obj in (puzzle_info, solver): # type: ignore
assert obj["string"] == "hello"
assert obj["bytes"] == bytes.fromhex("cafef00d")
assert obj["int"] == 123
assert obj["program"] == Program.to((1, "hello"))
assert obj["zero"] == 0
assert obj["nil"] == Program.to([])
+49 -11
View File
@@ -1,4 +1,4 @@
from typing import Dict, Optional, List
from typing import Any, Dict, Optional, List
import pytest
from blspy import G2Element
@@ -17,6 +17,8 @@ from chia.wallet.cat_wallet.cat_utils import (
SpendableCAT,
unsigned_spend_bundle_for_spendable_cats,
)
from chia.wallet.outer_puzzles import AssetType
from chia.wallet.puzzle_drivers import PuzzleInfo
from chia.wallet.payment import Payment
from chia.wallet.trading.offer import Offer, NotarizedPayment
from tests.clvm.benchmark_costs import cost_of_spend_bundle
@@ -178,6 +180,19 @@ class TestOfferLifecycle:
red_coins: List[Coin] = all_coins["red"]
blue_coins: List[Coin] = all_coins["blue"]
driver_dict: Dict[bytes32, PuzzleInfo] = {
str_to_tail_hash("red"): PuzzleInfo(
{"type": AssetType.CAT.value, "tail": "0x" + str_to_tail_hash("red").hex()}
),
str_to_tail_hash("blue"): PuzzleInfo(
{"type": AssetType.CAT.value, "tail": "0x" + str_to_tail_hash("blue").hex()}
),
}
driver_dict_as_infos: Dict[str, Any] = {}
for key, value in driver_dict.items():
driver_dict_as_infos[key.hex()] = value.info
# Create an XCH Offer for RED
chia_requested_payments: Dict[Optional[bytes32], List[Payment]] = {
str_to_tail_hash("red"): [
@@ -189,29 +204,51 @@ class TestOfferLifecycle:
chia_requested_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = Offer.notarize_payments(
chia_requested_payments, chia_coins
)
chia_announcements: List[Announcement] = Offer.calculate_announcements(chia_requested_payments)
chia_announcements: List[Announcement] = Offer.calculate_announcements(chia_requested_payments, driver_dict)
chia_secured_bundle: SpendBundle = generate_secure_bundle(chia_coins, chia_announcements, 1000)
chia_offer = Offer(chia_requested_payments, chia_secured_bundle)
chia_offer = Offer(chia_requested_payments, chia_secured_bundle, driver_dict)
assert not chia_offer.is_valid()
# Create a RED Offer for XCH
red_coins_1 = red_coins[0:1]
red_coins_2 = red_coins[1:]
red_requested_payments: Dict[Optional[bytes32], List[Payment]] = {
None: [
Payment(acs_ph, 300, [b"red memo"]),
Payment(acs_ph, 400, [b"red memo"]),
Payment(acs_ph, 350, [b"red memo"]),
]
}
red_requested_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = Offer.notarize_payments(
red_requested_payments, red_coins
red_requested_payments, red_coins_1
)
red_announcements: List[Announcement] = Offer.calculate_announcements(red_requested_payments)
red_secured_bundle: SpendBundle = generate_secure_bundle(red_coins, red_announcements, 350, tail_str="red")
red_offer = Offer(red_requested_payments, red_secured_bundle)
red_announcements: List[Announcement] = Offer.calculate_announcements(red_requested_payments, driver_dict)
red_secured_bundle: SpendBundle = generate_secure_bundle(
red_coins_1, red_announcements, sum([c.amount for c in red_coins_1]), tail_str="red"
)
red_offer = Offer(red_requested_payments, red_secured_bundle, driver_dict)
assert not red_offer.is_valid()
red_requested_payments_2: Dict[Optional[bytes32], List[Payment]] = {
None: [
Payment(acs_ph, 50, [b"red memo"]),
]
}
red_requested_payments_2: Dict[Optional[bytes32], List[NotarizedPayment]] = Offer.notarize_payments(
red_requested_payments_2, red_coins_2
)
red_announcements_2: List[Announcement] = Offer.calculate_announcements(
red_requested_payments_2, driver_dict
)
red_secured_bundle_2: SpendBundle = generate_secure_bundle(
red_coins_2, red_announcements_2, sum([c.amount for c in red_coins_2]), tail_str="red"
)
red_offer_2 = Offer(red_requested_payments_2, red_secured_bundle_2, driver_dict)
assert not red_offer_2.is_valid()
# Test aggregation of offers
new_offer = Offer.aggregate([chia_offer, red_offer])
new_offer = Offer.aggregate([chia_offer, red_offer, red_offer_2])
assert new_offer.get_offered_amounts() == {None: 1000, str_to_tail_hash("red"): 350}
assert new_offer.get_requested_amounts() == {None: 700, str_to_tail_hash("red"): 300}
assert new_offer.is_valid()
@@ -229,11 +266,11 @@ class TestOfferLifecycle:
blue_requested_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = Offer.notarize_payments(
blue_requested_payments, blue_coins
)
blue_announcements: List[Announcement] = Offer.calculate_announcements(blue_requested_payments)
blue_announcements: List[Announcement] = Offer.calculate_announcements(blue_requested_payments, driver_dict)
blue_secured_bundle: SpendBundle = generate_secure_bundle(
blue_coins, blue_announcements, 2000, tail_str="blue"
)
blue_offer = Offer(blue_requested_payments, blue_secured_bundle)
blue_offer = Offer(blue_requested_payments, blue_secured_bundle, driver_dict)
assert not blue_offer.is_valid()
# Test a re-aggregation
@@ -251,6 +288,7 @@ class TestOfferLifecycle:
str_to_tail_hash("blue").hex(): 2000,
},
{"xch": 900, str_to_tail_hash("red").hex(): 350},
driver_dict_as_infos,
)
assert new_offer.get_pending_amounts() == {
"xch": 1200,
+18 -4
View File
@@ -1,6 +1,6 @@
import asyncio
from secrets import token_bytes
from typing import List
from typing import Any, Dict, List
import pytest
@@ -8,6 +8,8 @@ from chia.full_node.mempool_manager import MempoolManager
from chia.simulator.simulator_protocol import FarmNewBlockProtocol
from chia.util.ints import uint64
from chia.wallet.cat_wallet.cat_wallet import CATWallet
from chia.wallet.outer_puzzles import AssetType
from chia.wallet.puzzle_drivers import PuzzleInfo
from chia.wallet.trading.offer import Offer
from chia.wallet.trading.trade_status import TradeStatus
from chia.wallet.transaction_record import TransactionRecord
@@ -75,14 +77,14 @@ class TestCATTrades:
chia_for_cat = {
wallet_maker.id(): -1,
new_cat_wallet_maker.id(): 2, # This is the CAT that the taker made
bytes.fromhex(new_cat_wallet_maker.get_asset_id()): 2, # This is the CAT that the taker made
}
cat_for_chia = {
wallet_maker.id(): 3,
cat_wallet_maker.id(): -4, # The taker has no knowledge of this CAT yet
}
cat_for_cat = {
cat_wallet_maker.id(): -5,
bytes.fromhex(cat_wallet_maker.get_asset_id()): -5,
new_cat_wallet_maker.id(): 6,
}
chia_for_multiple_cat = {
@@ -101,6 +103,16 @@ class TestCATTrades:
new_cat_wallet_maker.id(): 15,
}
driver_dict: Dict[str, Dict[str, Any]] = {}
for wallet in (cat_wallet_maker, new_cat_wallet_maker):
asset_id: str = wallet.get_asset_id()
driver_dict[bytes.fromhex(asset_id)] = PuzzleInfo(
{
"type": AssetType.CAT.name,
"tail": "0x" + asset_id,
}
)
trade_manager_maker = wallet_node_maker.wallet_state_manager.trade_manager
trade_manager_taker = wallet_node_taker.wallet_state_manager.trade_manager
@@ -230,7 +242,9 @@ class TestCATTrades:
await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_taker, trade_take)
# chia_for_multiple_cat
success, trade_make, error = await trade_manager_maker.create_offer_for_ids(chia_for_multiple_cat)
success, trade_make, error = await trade_manager_maker.create_offer_for_ids(
chia_for_multiple_cat, driver_dict=driver_dict
)
await asyncio.sleep(1)
assert error is None
assert success is True
+11 -3
View File
@@ -651,16 +651,24 @@ async def test_offer_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment)
await time_out_assert(10, get_confirmed_balance, 4, wallet_2_rpc, cat_wallet_id)
# Create an offer of 5 chia for one CAT
offer, trade_record = await wallet_1_rpc.create_offer_for_ids({uint32(1): -5, cat_wallet_id: 1}, validate_only=True)
offer, trade_record = await wallet_1_rpc.create_offer_for_ids(
{uint32(1): -5, cat_asset_id.hex(): 1}, validate_only=True
)
all_offers = await wallet_1_rpc.get_all_offers()
assert len(all_offers) == 0
assert offer is None
offer, trade_record = await wallet_1_rpc.create_offer_for_ids({uint32(1): -5, cat_wallet_id: 1}, fee=uint64(1))
driver_dict: Dict[str, Any] = {cat_asset_id.hex(): {"type": "CAT", "tail": "0x" + cat_asset_id.hex()}}
offer, trade_record = await wallet_1_rpc.create_offer_for_ids(
{uint32(1): -5, cat_asset_id.hex(): 1},
driver_dict=driver_dict,
fee=uint64(1),
)
assert offer is not None
summary = await wallet_1_rpc.get_offer_summary(offer)
assert summary == {"offered": {"xch": 5}, "requested": {cat_asset_id.hex(): 1}, "fees": 1}
assert summary == {"offered": {"xch": 5}, "requested": {cat_asset_id.hex(): 1}, "infos": driver_dict, "fees": 1}
assert await wallet_1_rpc.check_offer_validity(offer)