Skip to content

Commit

Permalink
Merge pull request #11641 from AmineKhaldi/offer_generalization_bring…
Browse files Browse the repository at this point in the history
…up_into_release

Quexington's Offer generalization bringup into release/1.4.0
  • Loading branch information
wallentx authored May 27, 2022
2 parents 46ebed3 + 71731c8 commit 954642a
Show file tree
Hide file tree
Showing 18 changed files with 700 additions and 139 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-test-macos-clvm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-test-ubuntu-clvm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
8 changes: 4 additions & 4 deletions chia/cmds/wallet_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
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

Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:")
Expand Down
28 changes: 24 additions & 4 deletions chia/rpc/wallet_rpc_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
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
Expand Down Expand Up @@ -918,18 +920,36 @@ async def create_offer_for_ids(self, request):
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:
(
success,
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 {
Expand All @@ -942,9 +962,9 @@ async def get_offer_summary(self, request):
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
Expand Down
17 changes: 14 additions & 3 deletions chia/rpc/wallet_rpc_client.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -420,13 +420,24 @@ async def cat_spend(

# 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)
Expand Down
92 changes: 92 additions & 0 deletions chia/wallet/cat_wallet/cat_outer_puzzle.py
Original file line number Diff line number Diff line change
@@ -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 changes: 35 additions & 0 deletions chia/wallet/cat_wallet/cat_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
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 (
Expand Down Expand Up @@ -205,6 +207,23 @@ async def create_wallet_for_cat(
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,
Expand Down Expand Up @@ -790,3 +809,19 @@ async def save_info(self, cat_info: CATInfo, in_transaction):
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 changes: 57 additions & 0 deletions chia/wallet/outer_puzzles.py
Original file line number Diff line number Diff line change
@@ -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),
}
Loading

0 comments on commit 954642a

Please sign in to comment.