mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2024-11-28 21:10:24 +03:00
811 lines
30 KiB
Python
811 lines
30 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from itertools import chain
|
|
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(
|
|
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
|
|
|
|
|
|
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(
|
|
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
|
|
curried_args_prg = uncurry_proposal_validator(proposal_validator)
|
|
(
|
|
SINGLETON_STRUCT,
|
|
PROPOSAL_SELF_HASH,
|
|
PROPOSAL_MINIMUM_AMOUNT,
|
|
PAYOUT_PUZHASH,
|
|
) = 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(
|
|
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
|
|
# 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:
|
|
# 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
|
|
# 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):
|
|
# 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:
|
|
# 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")
|
|
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:
|
|
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
|
|
]
|
|
puzzle = get_spend_p2_singleton_puzzle(treasury_id, Program.to(xch_conditions), Program.to([]))
|
|
return puzzle
|