mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2026-09-05 02:24:21 -05:00
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:
@@ -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: |
|
||||
|
||||
@@ -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,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:")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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([])
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user