chia-blockchain/chia/wallet/dao_wallet/dao_utils.py

811 lines
30 KiB
Python
Raw Normal View History

from __future__ import annotations
import logging
from itertools import chain
2023-12-15 18:58:47 +03:00
from typing import Any, Iterator, List, Optional, Tuple, Union
from clvm.EvalError import EvalError
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.util.ints import uint64
from chia.wallet.cat_wallet.cat_utils import CAT_MOD, CAT_MOD_HASH, construct_cat_puzzle
from chia.wallet.dao_wallet.dao_info import DAORules, ProposalType
from chia.wallet.puzzles.load_clvm import load_clvm
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import MOD
from chia.wallet.singleton import get_singleton_struct_for_id
from chia.wallet.uncurried_puzzle import UncurriedPuzzle
SINGLETON_MOD: Program = load_clvm("singleton_top_layer_v1_1.clsp")
SINGLETON_MOD_HASH: bytes32 = SINGLETON_MOD.get_tree_hash()
SINGLETON_LAUNCHER: Program = load_clvm("singleton_launcher.clsp")
SINGLETON_LAUNCHER_HASH: bytes32 = SINGLETON_LAUNCHER.get_tree_hash()
DAO_LOCKUP_MOD: Program = load_clvm("dao_lockup.clsp")
DAO_LOCKUP_MOD_HASH: bytes32 = DAO_LOCKUP_MOD.get_tree_hash()
DAO_PROPOSAL_TIMER_MOD: Program = load_clvm("dao_proposal_timer.clsp")
DAO_PROPOSAL_TIMER_MOD_HASH: bytes32 = DAO_PROPOSAL_TIMER_MOD.get_tree_hash()
DAO_PROPOSAL_MOD: Program = load_clvm("dao_proposal.clsp")
DAO_PROPOSAL_MOD_HASH: bytes32 = DAO_PROPOSAL_MOD.get_tree_hash()
DAO_PROPOSAL_VALIDATOR_MOD: Program = load_clvm("dao_proposal_validator.clsp")
DAO_PROPOSAL_VALIDATOR_MOD_HASH: bytes32 = DAO_PROPOSAL_VALIDATOR_MOD.get_tree_hash()
DAO_TREASURY_MOD: Program = load_clvm("dao_treasury.clsp")
DAO_TREASURY_MOD_HASH: bytes32 = DAO_TREASURY_MOD.get_tree_hash()
SPEND_P2_SINGLETON_MOD: Program = load_clvm("dao_spend_p2_singleton_v2.clsp")
SPEND_P2_SINGLETON_MOD_HASH: bytes32 = SPEND_P2_SINGLETON_MOD.get_tree_hash()
DAO_FINISHED_STATE: Program = load_clvm("dao_finished_state.clsp")
DAO_FINISHED_STATE_HASH: bytes32 = DAO_FINISHED_STATE.get_tree_hash()
DAO_CAT_TAIL: Program = load_clvm(
"genesis_by_coin_id_or_singleton.clsp", package_or_requirement="chia.wallet.cat_wallet.puzzles"
)
DAO_CAT_TAIL_HASH: bytes32 = DAO_CAT_TAIL.get_tree_hash()
DAO_CAT_LAUNCHER: Program = load_clvm("dao_cat_launcher.clsp")
P2_SINGLETON_MOD: Program = load_clvm("p2_singleton_via_delegated_puzzle.clsp")
P2_SINGLETON_MOD_HASH: bytes32 = P2_SINGLETON_MOD.get_tree_hash()
DAO_UPDATE_PROPOSAL_MOD: Program = load_clvm("dao_update_proposal.clsp")
DAO_UPDATE_PROPOSAL_MOD_HASH: bytes32 = DAO_UPDATE_PROPOSAL_MOD.get_tree_hash()
DAO_CAT_EVE: Program = load_clvm("dao_cat_eve.clsp")
P2_SINGLETON_AGGREGATOR_MOD: Program = load_clvm("p2_singleton_aggregator.clsp")
log = logging.Logger(__name__)
def create_cat_launcher_for_singleton_id(id: bytes32) -> Program:
singleton_struct = get_singleton_struct_for_id(id)
return DAO_CAT_LAUNCHER.curry(singleton_struct)
def curry_cat_eve(next_puzzle_hash: bytes32) -> Program:
return DAO_CAT_EVE.curry(next_puzzle_hash)
def get_treasury_puzzle(dao_rules: DAORules, treasury_id: bytes32, cat_tail_hash: bytes32) -> Program:
singleton_struct: Program = Program.to((SINGLETON_MOD_HASH, (treasury_id, SINGLETON_LAUNCHER_HASH)))
lockup_puzzle: Program = DAO_LOCKUP_MOD.curry(
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
DAO_FINISHED_STATE_HASH,
CAT_MOD_HASH,
cat_tail_hash,
)
proposal_self_hash = DAO_PROPOSAL_MOD.curry(
DAO_PROPOSAL_TIMER_MOD_HASH,
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
lockup_puzzle.get_tree_hash(),
cat_tail_hash,
treasury_id,
).get_tree_hash()
proposal_validator = DAO_PROPOSAL_VALIDATOR_MOD.curry(
singleton_struct,
proposal_self_hash,
dao_rules.proposal_minimum_amount,
get_p2_singleton_puzzle(
treasury_id
).get_tree_hash(), # TODO: let people set this later - for now a hidden feature
)
puzzle = DAO_TREASURY_MOD.curry(
DAO_TREASURY_MOD_HASH,
proposal_validator,
dao_rules.proposal_timelock,
dao_rules.soft_close_length,
dao_rules.attendance_required,
dao_rules.pass_percentage,
dao_rules.self_destruct_length,
dao_rules.oracle_spend_delay,
)
return puzzle
def get_proposal_validator(treasury_puz: Program, proposal_minimum_amount: uint64) -> Program:
_, uncurried_args = treasury_puz.uncurry()
validator: Program = uncurried_args.rest().first()
validator_args = validator.uncurry()[1]
(
singleton_struct,
proposal_self_hash,
_,
p2_puzhash,
) = validator_args.as_iter()
proposal_validator = DAO_PROPOSAL_VALIDATOR_MOD.curry(
singleton_struct,
proposal_self_hash,
proposal_minimum_amount,
p2_puzhash,
)
return proposal_validator
def get_update_proposal_puzzle(dao_rules: DAORules, proposal_validator: Program) -> Program:
validator_args = uncurry_proposal_validator(proposal_validator)
(
singleton_struct,
proposal_self_hash,
_,
proposal_excess_puzhash,
) = validator_args.as_iter()
update_proposal = DAO_UPDATE_PROPOSAL_MOD.curry(
DAO_TREASURY_MOD_HASH,
DAO_PROPOSAL_VALIDATOR_MOD_HASH,
singleton_struct,
proposal_self_hash,
dao_rules.proposal_minimum_amount,
proposal_excess_puzhash,
dao_rules.proposal_timelock,
dao_rules.soft_close_length,
dao_rules.attendance_required,
dao_rules.pass_percentage,
dao_rules.self_destruct_length,
dao_rules.oracle_spend_delay,
)
return update_proposal
def get_dao_rules_from_update_proposal(puzzle: Program) -> DAORules:
mod, curried_args = puzzle.uncurry()
if mod != DAO_UPDATE_PROPOSAL_MOD: # pragma: no cover
raise ValueError("Not an update proposal.")
(
_,
_,
_,
_,
proposal_minimum_amount,
_,
proposal_timelock,
soft_close_length,
attendance_required,
pass_percentage,
self_destruct_length,
oracle_spend_delay,
) = curried_args.as_iter()
dao_rules = DAORules(
2023-12-15 18:58:47 +03:00
uint64(proposal_timelock.as_int()),
uint64(soft_close_length.as_int()),
uint64(attendance_required.as_int()),
uint64(pass_percentage.as_int()),
uint64(self_destruct_length.as_int()),
uint64(oracle_spend_delay.as_int()),
uint64(proposal_minimum_amount.as_int()),
)
return dao_rules
2023-12-15 18:58:47 +03:00
def get_spend_p2_singleton_puzzle(treasury_id: bytes32, xch_conditions: Program, asset_conditions: Program) -> Program:
# TODO: typecheck get_spend_p2_singleton_puzzle arguments
# TODO: add tests for get_spend_p2_singleton_puzzle: pass xch_conditions as Puzzle, List and ConditionWithArgs
#
# CAT_MOD_HASH
# CONDITIONS ; XCH conditions, to be generated by the treasury
# LIST_OF_TAILHASH_CONDITIONS ; the delegated puzzlehash must be curried in to the proposal.
# ; Puzzlehash is only run in the last coin for that asset
# ; ((TAIL_HASH CONDITIONS) (TAIL_HASH CONDITIONS)... )
# P2_SINGLETON_VIA_DELEGATED_PUZZLE_PUZHASH
treasury_struct = Program.to((SINGLETON_MOD_HASH, (treasury_id, SINGLETON_LAUNCHER_HASH)))
puzzle: Program = SPEND_P2_SINGLETON_MOD.curry(
treasury_struct,
CAT_MOD_HASH,
xch_conditions,
asset_conditions,
P2_SINGLETON_MOD.curry(treasury_struct, P2_SINGLETON_AGGREGATOR_MOD).get_tree_hash(),
)
return puzzle
def get_p2_singleton_puzzle(treasury_id: bytes32, asset_id: Optional[bytes32] = None) -> Program:
singleton_struct: Program = Program.to((SINGLETON_MOD_HASH, (treasury_id, SINGLETON_LAUNCHER_HASH)))
inner_puzzle = P2_SINGLETON_MOD.curry(singleton_struct, P2_SINGLETON_AGGREGATOR_MOD)
if asset_id:
# CAT
puzzle = CAT_MOD.curry(CAT_MOD_HASH, asset_id, inner_puzzle)
return Program(puzzle)
else:
# XCH
return inner_puzzle
def get_p2_singleton_puzhash(treasury_id: bytes32, asset_id: Optional[bytes32] = None) -> bytes32:
puz = get_p2_singleton_puzzle(treasury_id, asset_id)
assert puz is not None
return puz.get_tree_hash()
def get_lockup_puzzle(
2023-12-15 18:58:47 +03:00
cat_tail_hash: Union[bytes32, Program],
previous_votes_list: Union[List[Optional[bytes32]], Program],
innerpuz: Optional[Program],
) -> Program:
self_hash: Program = DAO_LOCKUP_MOD.curry(
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
DAO_FINISHED_STATE_HASH,
CAT_MOD_HASH,
cat_tail_hash,
)
puzzle = self_hash.curry(
self_hash.get_tree_hash(),
previous_votes_list, # TODO: maybe format check this in this function
innerpuz,
)
return puzzle
def add_proposal_to_active_list(
lockup_puzzle: Program, proposal_id: bytes32, inner_puzzle: Optional[Program] = None
) -> Program:
curried_args, c_a = uncurry_lockup(lockup_puzzle)
(
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_PUZHASH,
DAO_FINISHED_STATE_HASH,
CAT_MOD_HASH,
CAT_TAIL_HASH,
) = c_a.as_iter()
(SELF_HASH, ACTIVE_VOTES, INNERPUZ) = curried_args.as_iter()
new_active_votes = Program.to(proposal_id).cons(ACTIVE_VOTES) # (c proposal_id ACTIVE_VOTES)
if inner_puzzle is None:
inner_puzzle = INNERPUZ
return get_lockup_puzzle(CAT_TAIL_HASH, new_active_votes, inner_puzzle)
def get_active_votes_from_lockup_puzzle(lockup_puzzle: Program) -> Program:
curried_args, c_a = uncurry_lockup(lockup_puzzle)
(
_SINGLETON_MOD_HASH,
_SINGLETON_LAUNCHER_HASH,
_DAO_FINISHED_STATE_HASH,
_CAT_MOD_HASH,
_CAT_TAIL_HASH,
) = list(c_a.as_iter())
(
self_hash,
ACTIVE_VOTES,
_INNERPUZ,
) = curried_args.as_iter()
return Program(ACTIVE_VOTES)
def get_innerpuz_from_lockup_puzzle(lockup_puzzle: Program) -> Optional[Program]:
try:
curried_args, c_a = uncurry_lockup(lockup_puzzle)
except Exception as e: # pragma: no cover
log.debug("Could not uncurry inner puzzle from lockup: %s", e)
return None
(
_SINGLETON_MOD_HASH,
_SINGLETON_LAUNCHER_HASH,
_DAO_FINISHED_STATE_HASH,
_CAT_MOD_HASH,
_CAT_TAIL_HASH,
) = list(c_a.as_iter())
(
self_hash,
_ACTIVE_VOTES,
INNERPUZ,
) = list(curried_args.as_iter())
return Program(INNERPUZ)
def get_proposal_puzzle(
*,
proposal_id: bytes32,
cat_tail_hash: bytes32,
treasury_id: bytes32,
votes_sum: uint64,
total_votes: uint64,
proposed_puzzle_hash: bytes32,
) -> Program:
"""
spend_or_update_flag can take on the following values, ranked from safest to most dangerous:
s for spend only
u for update only
d for dangerous (can do anything)
"""
lockup_puzzle: Program = DAO_LOCKUP_MOD.curry(
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
DAO_FINISHED_STATE_HASH,
CAT_MOD_HASH,
cat_tail_hash,
)
# SINGLETON_STRUCT ; (SINGLETON_MOD_HASH (SINGLETON_ID . LAUNCHER_PUZZLE_HASH))
# PROPOSAL_TIMER_MOD_HASH ; proposal timer needs to know which proposal created it, AND
# CAT_MOD_HASH
# DAO_FINISHED_STATE_MOD_HASH
# TREASURY_MOD_HASH
# LOCKUP_SELF_HASH
# CAT_TAIL_HASH
# TREASURY_ID
# ; second hash
# SELF_HASH
# PROPOSED_PUZ_HASH ; this is what runs if this proposal is successful - the inner puzzle of this proposal
# YES_VOTES ; yes votes are +1, no votes don't tally - we compare yes_votes/total_votes at the end
# TOTAL_VOTES ; how many people responded
curry_one = DAO_PROPOSAL_MOD.curry(
DAO_PROPOSAL_TIMER_MOD_HASH,
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
lockup_puzzle.get_tree_hash(),
cat_tail_hash,
treasury_id,
)
puzzle = curry_one.curry(
curry_one.get_tree_hash(),
proposal_id,
proposed_puzzle_hash,
votes_sum,
total_votes,
)
return puzzle
def get_proposal_timer_puzzle(
cat_tail_hash: bytes32,
proposal_id: bytes32,
treasury_id: bytes32,
) -> Program:
parent_singleton_struct: Program = Program.to((SINGLETON_MOD_HASH, (proposal_id, SINGLETON_LAUNCHER_HASH)))
lockup_puzzle: Program = DAO_LOCKUP_MOD.curry(
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
DAO_FINISHED_STATE_HASH,
CAT_MOD_HASH,
cat_tail_hash,
)
PROPOSAL_SELF_HASH = DAO_PROPOSAL_MOD.curry(
DAO_PROPOSAL_TIMER_MOD_HASH,
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
lockup_puzzle.get_tree_hash(),
cat_tail_hash,
treasury_id,
).get_tree_hash()
puzzle: Program = DAO_PROPOSAL_TIMER_MOD.curry(
PROPOSAL_SELF_HASH,
parent_singleton_struct,
)
return puzzle
def get_treasury_rules_from_puzzle(puzzle_reveal: Optional[Program]) -> DAORules:
assert isinstance(puzzle_reveal, Program)
curried_args = uncurry_treasury(puzzle_reveal)
(
_DAO_TREASURY_MOD_HASH,
proposal_validator,
proposal_timelock,
soft_close_length,
attendance_required,
pass_percentage,
self_destruct_length,
oracle_spend_delay,
) = curried_args
2023-12-15 18:58:47 +03:00
curried_args_prg = uncurry_proposal_validator(proposal_validator)
(
SINGLETON_STRUCT,
PROPOSAL_SELF_HASH,
PROPOSAL_MINIMUM_AMOUNT,
PAYOUT_PUZHASH,
2023-12-15 18:58:47 +03:00
) = curried_args_prg.as_iter()
return DAORules(
uint64(proposal_timelock.as_int()),
uint64(soft_close_length.as_int()),
uint64(attendance_required.as_int()),
uint64(pass_percentage.as_int()),
uint64(self_destruct_length.as_int()),
uint64(oracle_spend_delay.as_int()),
uint64(PROPOSAL_MINIMUM_AMOUNT.as_int()),
)
# This takes the treasury puzzle and treasury solution, not the full puzzle and full solution
# This also returns the treasury puzzle and not the full puzzle
def get_new_puzzle_from_treasury_solution(puzzle_reveal: Program, solution: Program) -> Optional[Program]:
if solution.rest().rest().first() != Program.to(0):
# Proposal Spend
mod, curried_args = solution.at("rrf").uncurry()
if mod == DAO_UPDATE_PROPOSAL_MOD:
(
DAO_TREASURY_MOD_HASH,
DAO_VALIDATOR_MOD_HASH,
TREASURY_SINGLETON_STRUCT,
PROPOSAL_SELF_HASH,
proposal_minimum_amount,
PROPOSAL_EXCESS_PAYOUT_PUZ_HASH,
proposal_timelock,
soft_close_length,
attendance_required,
pass_percentage,
self_destruct_length,
oracle_spend_delay,
) = curried_args.as_iter()
new_validator = DAO_PROPOSAL_VALIDATOR_MOD.curry(
TREASURY_SINGLETON_STRUCT, PROPOSAL_SELF_HASH, proposal_minimum_amount, PROPOSAL_EXCESS_PAYOUT_PUZ_HASH
)
return DAO_TREASURY_MOD.curry(
DAO_TREASURY_MOD_HASH,
new_validator,
proposal_timelock,
soft_close_length,
attendance_required,
pass_percentage,
self_destruct_length,
oracle_spend_delay,
)
else:
return puzzle_reveal
else:
# Oracle Spend - treasury is unchanged
return puzzle_reveal
# This takes the proposal puzzle and proposal solution, not the full puzzle and full solution
# This also returns the proposal puzzle and not the full puzzle
def get_new_puzzle_from_proposal_solution(puzzle_reveal: Program, solution: Program) -> Optional[Program]:
# Check if soft_close_length is in solution. If not, then add votes, otherwise close proposal
if len(solution.as_python()) == 1:
return puzzle_reveal # we're finished, shortcut this function
if solution.at("rrrrrrf") == Program.to(0):
c_a, curried_args = uncurry_proposal(puzzle_reveal)
assert isinstance(curried_args, Program)
(
DAO_PROPOSAL_TIMER_MOD_HASH,
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_PUZHASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
lockup_self_hash,
cat_tail_hash,
treasury_id,
) = curried_args.as_iter()
assert isinstance(c_a, Program)
(
curry_one,
proposal_id,
proposed_puzzle_hash,
yes_votes,
total_votes,
) = c_a.as_iter()
added_votes = 0
for vote_amount in solution.first().as_iter():
added_votes += vote_amount.as_int()
new_total_votes = total_votes.as_int() + added_votes
if solution.at("rf") == Program.to(0):
# Vote Type: NO
new_yes_votes = yes_votes.as_int()
else:
# Vote Type: YES
new_yes_votes = yes_votes.as_int() + added_votes
return get_proposal_puzzle(
2023-12-15 18:58:47 +03:00
proposal_id=bytes32(proposal_id.as_atom()),
cat_tail_hash=bytes32(cat_tail_hash.as_atom()),
treasury_id=bytes32(treasury_id.as_atom()),
votes_sum=uint64(new_yes_votes),
total_votes=uint64(new_total_votes),
proposed_puzzle_hash=bytes32(proposed_puzzle_hash.as_atom()),
)
else:
# we are in the finished state, puzzle is the same as ever
mod, currieds = puzzle_reveal.uncurry() # uncurry to self_hash
# check if our parent was the last non-finished state
if mod.uncurry()[0] == DAO_PROPOSAL_MOD:
c_a, curried_args = uncurry_proposal(puzzle_reveal)
(
DAO_PROPOSAL_TIMER_MOD_HASH,
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_PUZHASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
lockup_self_hash,
cat_tail_hash,
treasury_id,
) = curried_args.as_iter()
(
curry_one,
proposal_id,
proposed_puzzle_hash,
yes_votes,
total_votes,
) = c_a.as_iter()
else: # pragma: no cover
SINGLETON_STRUCT, dao_finished_hash = currieds.as_iter()
proposal_id = SINGLETON_STRUCT.rest().first()
return get_finished_state_inner_puzzle(bytes32(proposal_id.as_atom()))
def get_finished_state_inner_puzzle(proposal_id: bytes32) -> Program:
singleton_struct: Program = Program.to((SINGLETON_MOD_HASH, (proposal_id, SINGLETON_LAUNCHER_HASH)))
finished_inner_puz: Program = DAO_FINISHED_STATE.curry(singleton_struct, DAO_FINISHED_STATE_HASH)
return finished_inner_puz
def get_finished_state_puzzle(proposal_id: bytes32) -> Program:
return curry_singleton(proposal_id, get_finished_state_inner_puzzle(proposal_id))
def get_proposed_puzzle_reveal_from_solution(solution: Program) -> Program:
prog = Program.from_bytes(bytes(solution))
return prog.at("rrfrrrrrf")
def get_asset_id_from_puzzle(puzzle: Program) -> Optional[bytes32]:
mod, curried_args = puzzle.uncurry()
if mod == MOD: # pragma: no cover
return None
elif mod == CAT_MOD:
return bytes32(curried_args.at("rf").as_atom())
elif mod == SINGLETON_MOD: # pragma: no cover
return bytes32(curried_args.at("frf").as_atom())
else:
raise ValueError("DAO received coin with unknown puzzle") # pragma: no cover
def uncurry_proposal_validator(proposal_validator_program: Program) -> Program:
try:
mod, curried_args = proposal_validator_program.uncurry()
except ValueError as e: # pragma: no cover
log.debug("Cannot uncurry treasury puzzle: error: %s", e)
raise e
if mod != DAO_PROPOSAL_VALIDATOR_MOD: # pragma: no cover
raise ValueError("Not a Treasury mod.")
return curried_args
def uncurry_treasury(treasury_puzzle: Program) -> List[Program]:
try:
mod, curried_args = treasury_puzzle.uncurry()
except ValueError as e: # pragma: no cover
log.debug("Cannot uncurry treasury puzzle: error: %s", e)
raise e
if mod != DAO_TREASURY_MOD: # pragma: no cover
raise ValueError("Not a Treasury mod.")
return list(curried_args.as_iter())
def uncurry_proposal(proposal_puzzle: Program) -> Tuple[Program, Program]:
try:
mod, curried_args = proposal_puzzle.uncurry()
except ValueError as e: # pragma: no cover
log.debug("Cannot uncurry proposal puzzle: error: %s", e)
raise e
try:
mod, c_a = mod.uncurry()
except ValueError as e: # pragma: no cover
log.debug("Cannot uncurry lockup puzzle: error: %s", e)
raise e
if mod != DAO_PROPOSAL_MOD:
raise ValueError("Not a dao proposal mod.")
return curried_args, c_a
def uncurry_lockup(lockup_puzzle: Program) -> Tuple[Program, Program]:
try:
mod, curried_args = lockup_puzzle.uncurry()
except ValueError as e: # pragma: no cover
log.debug("Cannot uncurry lockup puzzle: error: %s", e)
raise e
try:
mod, c_a = mod.uncurry()
except ValueError as e: # pragma: no cover
log.debug("Cannot uncurry lockup puzzle: error: %s", e)
raise e
if mod != DAO_LOCKUP_MOD:
log.debug("Puzzle is not a dao cat lockup mod")
return curried_args, c_a
# This is the proposed puzzle
def get_proposal_args(puzzle: Program) -> Tuple[ProposalType, Program]:
try:
mod, curried_args = puzzle.uncurry()
except ValueError as e: # pragma: no cover
log.debug("Cannot uncurry spend puzzle: error: %s", e)
raise e
if mod == SPEND_P2_SINGLETON_MOD:
return ProposalType.SPEND, curried_args
elif mod == DAO_UPDATE_PROPOSAL_MOD:
return ProposalType.UPDATE, curried_args
else:
raise ValueError("Unrecognised proposal type")
def generate_cat_tail(genesis_coin_id: bytes32, treasury_id: bytes32) -> Program:
dao_cat_launcher = create_cat_launcher_for_singleton_id(treasury_id).get_tree_hash()
puzzle = DAO_CAT_TAIL.curry(genesis_coin_id, dao_cat_launcher)
return puzzle
def curry_singleton(singleton_id: bytes32, innerpuz: Program) -> Program:
singleton_struct = Program.to((SINGLETON_MOD_HASH, (singleton_id, SINGLETON_LAUNCHER_HASH)))
return SINGLETON_MOD.curry(singleton_struct, innerpuz)
# This is for use in the WalletStateManager to determine the type of coin received
def match_treasury_puzzle(mod: Program, curried_args: Program) -> Optional[Iterator[Program]]:
"""
Given a puzzle test if it's a Treasury, if it is, return the curried arguments
:param mod: Puzzle
:param curried_args: Puzzle
:return: Curried parameters
"""
try:
if mod == SINGLETON_MOD:
mod, curried_args = curried_args.rest().first().uncurry()
if mod == DAO_TREASURY_MOD:
return curried_args.first().as_iter() # type: ignore[no-any-return]
except ValueError: # pragma: no cover
2023-10-27 04:00:36 +03:00
# We just pass here to prevent spamming logs with error messages when WSM checks incoming coins
pass
return None
# This is for use in the WalletStateManager to determine the type of coin received
def match_proposal_puzzle(mod: Program, curried_args: Program) -> Optional[Iterator[Program]]:
"""
Given a puzzle test if it's a Proposal, if it is, return the curried arguments
:param curried_args: Puzzle
:return: Curried parameters
"""
try:
if mod == SINGLETON_MOD:
c_a, curried_args = uncurry_proposal(curried_args.rest().first())
assert c_a is not None and curried_args is not None
ret = chain(c_a.as_iter(), curried_args.as_iter())
return ret
except ValueError:
2023-10-27 04:00:36 +03:00
# We just pass here to prevent spamming logs with error messages when WSM checks incoming coins
pass
return None
def match_finished_puzzle(mod: Program, curried_args: Program) -> Optional[Iterator[Program]]:
"""
Given a puzzle test if it's a Proposal, if it is, return the curried arguments
:param curried_args: Puzzle
:return: Curried parameters
"""
try:
if mod == SINGLETON_MOD:
mod, curried_args = curried_args.rest().first().uncurry()
if mod == DAO_FINISHED_STATE:
return curried_args.as_iter() # type: ignore[no-any-return]
except ValueError: # pragma: no cover
2023-10-27 04:00:36 +03:00
# We just pass here to prevent spamming logs with error messages when WSM checks incoming coins
pass
return None
# This is used in WSM to determine whether we have a dao funding spend
def match_funding_puzzle(
uncurried: UncurriedPuzzle, solution: Program, coin: Coin, dao_ids: List[bytes32] = []
) -> Optional[bool]:
if not dao_ids:
return None
try:
if uncurried.mod == CAT_MOD:
conditions = solution.at("frfr").as_iter()
elif uncurried.mod == MOD:
conditions = solution.at("rfr").as_iter()
elif uncurried.mod == SINGLETON_MOD:
inner_puz, _ = uncurried.args.at("rf").uncurry()
if inner_puz == DAO_TREASURY_MOD:
delegated_puz = solution.at("rrfrrf")
delegated_mod, delegated_args = delegated_puz.uncurry()
if delegated_puz.uncurry()[0] == SPEND_P2_SINGLETON_MOD:
if coin.puzzle_hash == delegated_args.at("rrrrf").as_atom(): # pragma: no cover
return True
return None # pragma: no cover
else:
return None
fund_puzhashes = [get_p2_singleton_puzhash(dao_id) for dao_id in dao_ids]
for cond in conditions:
if (cond.list_len() == 4) and (cond.first().as_int() == 51):
if cond.at("rrrff") in fund_puzhashes:
return True
except (ValueError, EvalError):
2023-10-27 04:00:36 +03:00
# We just pass here to prevent spamming logs with error messages when WSM checks incoming coins
pass
return None
def match_dao_cat_puzzle(uncurried: UncurriedPuzzle) -> Optional[Iterator[Program]]:
try:
if uncurried.mod == CAT_MOD:
arg_list = list(uncurried.args.as_iter())
inner_puz = get_innerpuz_from_lockup_puzzle(uncurried.args.at("rrf"))
if inner_puz is not None:
dao_cat_args: Iterator[Program] = Program.to(arg_list).as_iter()
return dao_cat_args
except ValueError:
2023-10-27 04:00:36 +03:00
# We just pass here to prevent spamming logs with error messages when WSM checks incoming coins
pass
return None
def generate_simple_proposal_innerpuz(
treasury_id: bytes32,
recipient_puzhashes: List[bytes32],
amounts: List[uint64],
asset_types: List[Optional[bytes32]] = [None],
) -> Program:
if len(recipient_puzhashes) != len(amounts) != len(asset_types): # pragma: no cover
raise ValueError("Mismatch in the number of recipients, amounts, or asset types")
2023-11-03 16:30:41 +03:00
xch_conds: List[Any] = []
cat_conds: List[Any] = []
seen_assets = set()
for recipient_puzhash, amount, asset_type in zip(recipient_puzhashes, amounts, asset_types):
if asset_type:
2023-11-03 16:30:41 +03:00
if asset_type in seen_assets:
asset_conds = [x for x in cat_conds if x[0] == asset_type][0]
asset_conds[1].append([51, recipient_puzhash, amount, [recipient_puzhash]])
else:
cat_conds.append([asset_type, [[51, recipient_puzhash, amount, [recipient_puzhash]]]])
seen_assets.add(asset_type)
else:
xch_conds.append([51, recipient_puzhash, amount])
puzzle = get_spend_p2_singleton_puzzle(treasury_id, Program.to(xch_conds), Program.to(cat_conds))
return puzzle
async def generate_update_proposal_innerpuz(
current_treasury_innerpuz: Program,
new_dao_rules: DAORules,
new_proposal_validator: Optional[Program] = None,
) -> Program:
if not new_proposal_validator:
assert isinstance(current_treasury_innerpuz, Program)
new_proposal_validator = get_proposal_validator(
current_treasury_innerpuz, new_dao_rules.proposal_minimum_amount
)
return get_update_proposal_puzzle(new_dao_rules, new_proposal_validator)
async def generate_mint_proposal_innerpuz(
treasury_id: bytes32,
cat_tail_hash: bytes32,
amount_of_cats_to_create: uint64,
cats_new_innerpuzhash: bytes32,
) -> Program:
if amount_of_cats_to_create % 2 == 1: # pragma: no cover
raise ValueError("Minting proposals must mint an even number of CATs")
cat_launcher = create_cat_launcher_for_singleton_id(treasury_id)
# cat_wallet: CATWallet = self.wallet_state_manager.wallets[self.dao_info.cat_wallet_id]
# cat_tail_hash = cat_wallet.cat_info.limitations_program_hash
eve_puz_hash = curry_cat_eve(cats_new_innerpuzhash)
full_puz = construct_cat_puzzle(CAT_MOD, cat_tail_hash, eve_puz_hash)
xch_conditions = [
[
51,
cat_launcher.get_tree_hash(),
uint64(amount_of_cats_to_create),
[cats_new_innerpuzhash],
], # create cat_launcher coin
[
60,
Program.to([ProposalType.MINT.value, full_puz.get_tree_hash()]).get_tree_hash(),
], # make an announcement for the launcher to assert
]
2023-12-15 18:58:47 +03:00
puzzle = get_spend_p2_singleton_puzzle(treasury_id, Program.to(xch_conditions), Program.to([]))
return puzzle