mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2024-09-11 10:56:14 +03:00
Dao wallet (#16500)
Co-authored-by: Adam Kelly <338792+aqk@users.noreply.github.com> Co-authored-by: Matthew Howard <m.o.howard97@gmail.com> Co-authored-by: matt-o-how <48453825+matt-o-how@users.noreply.github.com> Co-authored-by: Karlkim Suwanmongkol <karlkim@gmail.com> Co-authored-by: Sebastjan Trepca <trepca@gmail.com> Co-authored-by: Amine Khaldi <amine.khaldi@reactos.org> Co-authored-by: Kyle Altendorf <sda@fstab.net>
This commit is contained in:
parent
52fff1451b
commit
75af6a98d8
4
.flake8
4
.flake8
@ -7,3 +7,7 @@ per-file-ignores =
|
||||
tests/util/test_network_protocol_files.py:F405
|
||||
tests/util/test_network_protocol_json.py:F405
|
||||
tests/util/protocol_messages_json.py:E501
|
||||
chia/wallet/dao_wallet/dao_utils.py:E501
|
||||
chia/wallet/dao_wallet/dao_wallet.py:E501
|
||||
chia/wallet/cat_wallet/dao_cat_wallet.py:E501
|
||||
tests/wallet/dao_wallet/test_dao_clvm.py:E501
|
||||
|
@ -9,6 +9,7 @@ from chia import __version__
|
||||
from chia.cmds.beta import beta_cmd
|
||||
from chia.cmds.completion import completion
|
||||
from chia.cmds.configure import configure_cmd
|
||||
from chia.cmds.dao import dao_cmd
|
||||
from chia.cmds.data import data_cmd
|
||||
from chia.cmds.db import db_cmd
|
||||
from chia.cmds.dev import dev_cmd
|
||||
@ -128,6 +129,7 @@ cli.add_command(data_cmd)
|
||||
cli.add_command(passphrase_cmd)
|
||||
cli.add_command(beta_cmd)
|
||||
cli.add_command(completion)
|
||||
cli.add_command(dao_cmd)
|
||||
cli.add_command(dev_cmd)
|
||||
|
||||
|
||||
|
1003
chia/cmds/dao.py
Normal file
1003
chia/cmds/dao.py
Normal file
File diff suppressed because it is too large
Load Diff
580
chia/cmds/dao_funcs.py
Normal file
580
chia/cmds/dao_funcs.py
Normal file
@ -0,0 +1,580 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from decimal import Decimal
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from chia.cmds.cmds_util import CMDTXConfigLoader, get_wallet_client, transaction_status_msg, transaction_submitted_msg
|
||||
from chia.cmds.units import units
|
||||
from chia.cmds.wallet_funcs import get_mojo_per_unit, get_wallet_type
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash
|
||||
from chia.util.config import selected_network_address_prefix
|
||||
from chia.util.ints import uint64
|
||||
from chia.wallet.util.tx_config import DEFAULT_COIN_SELECTION_CONFIG
|
||||
from chia.wallet.util.wallet_types import WalletType
|
||||
|
||||
|
||||
async def add_dao_wallet(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None:
|
||||
treasury_id = args["treasury_id"]
|
||||
filter_amount = args["filter_amount"]
|
||||
name = args["name"]
|
||||
|
||||
print(f"Adding wallet for DAO: {treasury_id}")
|
||||
print("This may take awhile.")
|
||||
|
||||
async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
|
||||
res = await wallet_client.create_new_dao_wallet(
|
||||
mode="existing",
|
||||
tx_config=CMDTXConfigLoader.from_json_dict({"reuse_puzhash": True}).to_tx_config(
|
||||
units["chia"], config, fingerprint
|
||||
),
|
||||
dao_rules=None,
|
||||
amount_of_cats=None,
|
||||
treasury_id=treasury_id,
|
||||
filter_amount=filter_amount,
|
||||
name=name,
|
||||
)
|
||||
|
||||
print("Successfully created DAO Wallet")
|
||||
print("DAO Treasury ID: {treasury_id}".format(**res))
|
||||
print("DAO Wallet ID: {wallet_id}".format(**res))
|
||||
print("CAT Wallet ID: {cat_wallet_id}".format(**res))
|
||||
print("DAOCAT Wallet ID: {dao_cat_wallet_id}".format(**res))
|
||||
|
||||
|
||||
async def create_dao_wallet(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None:
|
||||
proposal_minimum = uint64(int(Decimal(args["proposal_minimum_amount"]) * units["chia"]))
|
||||
|
||||
if proposal_minimum % 2 == 0:
|
||||
proposal_minimum = uint64(1 + proposal_minimum)
|
||||
print("Adding 1 mojo to proposal minimum amount")
|
||||
|
||||
dao_rules = {
|
||||
"proposal_timelock": args["proposal_timelock"],
|
||||
"soft_close_length": args["soft_close_length"],
|
||||
"attendance_required": args["attendance_required"],
|
||||
"pass_percentage": args["pass_percentage"],
|
||||
"self_destruct_length": args["self_destruct_length"],
|
||||
"oracle_spend_delay": args["oracle_spend_delay"],
|
||||
"proposal_minimum_amount": proposal_minimum,
|
||||
}
|
||||
amount_of_cats = args["amount_of_cats"]
|
||||
filter_amount = args["filter_amount"]
|
||||
name = args["name"]
|
||||
|
||||
fee = Decimal(args["fee"])
|
||||
final_fee: uint64 = uint64(int(fee * units["chia"]))
|
||||
|
||||
fee_for_cat = Decimal(args["fee_for_cat"])
|
||||
final_fee_for_cat: uint64 = uint64(int(fee_for_cat * units["chia"]))
|
||||
|
||||
async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
|
||||
conf_coins, _, _ = await wallet_client.get_spendable_coins(
|
||||
wallet_id=1, coin_selection_config=DEFAULT_COIN_SELECTION_CONFIG
|
||||
)
|
||||
if len(conf_coins) < 2: # pragma: no cover
|
||||
raise ValueError("DAO creation requires at least 2 xch coins in your wallet.")
|
||||
res = await wallet_client.create_new_dao_wallet(
|
||||
mode="new",
|
||||
dao_rules=dao_rules,
|
||||
amount_of_cats=amount_of_cats,
|
||||
treasury_id=None,
|
||||
filter_amount=filter_amount,
|
||||
name=name,
|
||||
fee=final_fee,
|
||||
fee_for_cat=final_fee_for_cat,
|
||||
tx_config=CMDTXConfigLoader.from_json_dict(
|
||||
{
|
||||
"min_coin_amount": args["min_coin_amount"],
|
||||
"max_coin_amount": args["max_coin_amount"],
|
||||
"coins_to_exclude": args["coins_to_exclude"],
|
||||
"amounts_to_exclude": args["amounts_to_exclude"],
|
||||
"reuse_puzhash": args["reuse_puzhash"],
|
||||
}
|
||||
).to_tx_config(units["chia"], config, fingerprint),
|
||||
)
|
||||
|
||||
print("Successfully created DAO Wallet")
|
||||
print("DAO Treasury ID: {treasury_id}".format(**res))
|
||||
print("DAO Wallet ID: {wallet_id}".format(**res))
|
||||
print("CAT Wallet ID: {cat_wallet_id}".format(**res))
|
||||
print("DAOCAT Wallet ID: {dao_cat_wallet_id}".format(**res))
|
||||
|
||||
|
||||
async def get_treasury_id(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None:
|
||||
wallet_id = args["wallet_id"]
|
||||
|
||||
async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, _, _):
|
||||
res = await wallet_client.dao_get_treasury_id(wallet_id=wallet_id)
|
||||
treasury_id = res["treasury_id"]
|
||||
print(f"Treasury ID: {treasury_id}")
|
||||
|
||||
|
||||
async def get_rules(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None:
|
||||
wallet_id = args["wallet_id"]
|
||||
|
||||
async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, _, _):
|
||||
res = await wallet_client.dao_get_rules(wallet_id=wallet_id)
|
||||
rules = res["rules"]
|
||||
for rule, val in rules.items():
|
||||
print(f"{rule}: {val}")
|
||||
|
||||
|
||||
async def add_funds_to_treasury(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None:
|
||||
wallet_id = args["wallet_id"]
|
||||
funding_wallet_id = args["funding_wallet_id"]
|
||||
amount = Decimal(args["amount"])
|
||||
|
||||
async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
|
||||
try:
|
||||
typ = await get_wallet_type(wallet_id=funding_wallet_id, wallet_client=wallet_client)
|
||||
mojo_per_unit = get_mojo_per_unit(typ)
|
||||
except LookupError: # pragma: no cover
|
||||
print(f"Wallet id: {wallet_id} not found.")
|
||||
return
|
||||
|
||||
fee = Decimal(args["fee"])
|
||||
final_fee: uint64 = uint64(int(fee * units["chia"]))
|
||||
final_amount: uint64 = uint64(int(amount * mojo_per_unit))
|
||||
|
||||
res = await wallet_client.dao_add_funds_to_treasury(
|
||||
wallet_id=wallet_id,
|
||||
funding_wallet_id=funding_wallet_id,
|
||||
amount=final_amount,
|
||||
fee=final_fee,
|
||||
tx_config=CMDTXConfigLoader.from_json_dict(
|
||||
{
|
||||
"min_coin_amount": args["min_coin_amount"],
|
||||
"max_coin_amount": args["max_coin_amount"],
|
||||
"coins_to_exclude": args["coins_to_exclude"],
|
||||
"amounts_to_exclude": args["amounts_to_exclude"],
|
||||
"reuse_puzhash": args["reuse_puzhash"],
|
||||
}
|
||||
).to_tx_config(units["chia"], config, fingerprint),
|
||||
)
|
||||
|
||||
tx_id = res["tx_id"]
|
||||
start = time.time()
|
||||
while time.time() - start < 10:
|
||||
await asyncio.sleep(0.1)
|
||||
tx = await wallet_client.get_transaction(wallet_id, bytes32.from_hexstr(tx_id))
|
||||
if len(tx.sent_to) > 0:
|
||||
print(transaction_submitted_msg(tx))
|
||||
print(transaction_status_msg(fingerprint, tx_id[2:]))
|
||||
return None
|
||||
|
||||
print(f"Transaction not yet submitted to nodes. TX ID: {tx_id}") # pragma: no cover
|
||||
|
||||
|
||||
async def get_treasury_balance(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None:
|
||||
wallet_id = args["wallet_id"]
|
||||
|
||||
async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, _, _):
|
||||
res = await wallet_client.dao_get_treasury_balance(wallet_id=wallet_id)
|
||||
balances = res["balances"]
|
||||
|
||||
if not balances:
|
||||
print("The DAO treasury currently has no funds")
|
||||
return None
|
||||
|
||||
xch_mojos = get_mojo_per_unit(WalletType.STANDARD_WALLET)
|
||||
cat_mojos = get_mojo_per_unit(WalletType.CAT)
|
||||
for asset_id, balance in balances.items():
|
||||
if asset_id == "xch":
|
||||
print(f"XCH: {balance / xch_mojos}")
|
||||
else:
|
||||
print(f"{asset_id}: {balance / cat_mojos}")
|
||||
|
||||
|
||||
async def list_proposals(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None:
|
||||
wallet_id = args["wallet_id"]
|
||||
include_closed = args["include_closed"]
|
||||
|
||||
async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, _, _):
|
||||
res = await wallet_client.dao_get_proposals(wallet_id=wallet_id, include_closed=include_closed)
|
||||
proposals = res["proposals"]
|
||||
soft_close_length = res["soft_close_length"]
|
||||
print("############################")
|
||||
for prop in proposals:
|
||||
print("Proposal ID: {proposal_id}".format(**prop))
|
||||
prop_status = "CLOSED" if prop["closed"] else "OPEN"
|
||||
print(f"Status: {prop_status}")
|
||||
print("Votes for: {yes_votes}".format(**prop))
|
||||
votes_against = prop["amount_voted"] - prop["yes_votes"]
|
||||
print(f"Votes against: {votes_against}")
|
||||
print("------------------------")
|
||||
print(f"Proposals have {soft_close_length} blocks of soft close time.")
|
||||
print("############################")
|
||||
|
||||
|
||||
async def show_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None:
|
||||
wallet_id = args["wallet_id"]
|
||||
proposal_id = args["proposal_id"]
|
||||
|
||||
async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, _, config):
|
||||
res = await wallet_client.dao_parse_proposal(wallet_id, proposal_id)
|
||||
pd = res["proposal_dictionary"]
|
||||
blocks_needed = pd["state"]["blocks_needed"]
|
||||
passed = pd["state"]["passed"]
|
||||
closable = pd["state"]["closable"]
|
||||
status = "CLOSED" if pd["state"]["closed"] else "OPEN"
|
||||
votes_needed = pd["state"]["total_votes_needed"]
|
||||
yes_needed = pd["state"]["yes_votes_needed"]
|
||||
|
||||
ptype_val = pd["proposal_type"]
|
||||
if (ptype_val == "s") and ("mint_amount" in pd):
|
||||
ptype = "mint"
|
||||
elif ptype_val == "s":
|
||||
ptype = "spend"
|
||||
elif ptype_val == "u":
|
||||
ptype = "update"
|
||||
|
||||
print("")
|
||||
print(f"Details of Proposal: {proposal_id}")
|
||||
print("---------------------------")
|
||||
print("")
|
||||
print(f"Type: {ptype.upper()}")
|
||||
print(f"Status: {status}")
|
||||
print(f"Passed: {passed}")
|
||||
if not passed:
|
||||
print(f"Yes votes needed: {yes_needed}")
|
||||
|
||||
if not pd["state"]["closed"]:
|
||||
print(f"Closable: {closable}")
|
||||
if not closable:
|
||||
print(f"Total votes needed: {votes_needed}")
|
||||
print(f"Blocks remaining: {blocks_needed}")
|
||||
|
||||
prefix = selected_network_address_prefix(config)
|
||||
if ptype == "spend":
|
||||
xch_conds = pd["xch_conditions"]
|
||||
asset_conds = pd["asset_conditions"]
|
||||
print("")
|
||||
if xch_conds:
|
||||
print("Proposal XCH Conditions")
|
||||
for pmt in xch_conds:
|
||||
puzzle_hash = encode_puzzle_hash(bytes32.from_hexstr(pmt["puzzle_hash"]), prefix)
|
||||
amount = pmt["amount"]
|
||||
print(f"Address: {puzzle_hash}\nAmount: {amount}\n")
|
||||
if asset_conds:
|
||||
print("Proposal asset Conditions")
|
||||
for cond in asset_conds:
|
||||
asset_id = cond["asset_id"]
|
||||
print(f"Asset ID: {asset_id}")
|
||||
conds = cond["conditions"]
|
||||
for pmt in conds:
|
||||
puzzle_hash = encode_puzzle_hash(bytes32.from_hexstr(pmt["puzzle_hash"]), prefix)
|
||||
amount = pmt["amount"]
|
||||
print(f"Address: {puzzle_hash}\nAmount: {amount}\n")
|
||||
|
||||
elif ptype == "update":
|
||||
print("")
|
||||
print("Proposed Rules:")
|
||||
for key, val in pd["dao_rules"].items():
|
||||
print(f"{key}: {val}")
|
||||
|
||||
elif ptype == "mint":
|
||||
mint_amount = pd["mint_amount"]
|
||||
address = encode_puzzle_hash(bytes32.from_hexstr(pd["new_cat_puzhash"]), prefix)
|
||||
print("")
|
||||
print(f"Amount of CAT to mint: {mint_amount}")
|
||||
print(f"Address: {address}")
|
||||
|
||||
|
||||
async def vote_on_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None:
|
||||
wallet_id = args["wallet_id"]
|
||||
vote_amount = args["vote_amount"]
|
||||
fee = args["fee"]
|
||||
final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"]))
|
||||
proposal_id = args["proposal_id"]
|
||||
is_yes_vote = args["is_yes_vote"]
|
||||
|
||||
async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
|
||||
res = await wallet_client.dao_vote_on_proposal(
|
||||
wallet_id=wallet_id,
|
||||
proposal_id=proposal_id,
|
||||
vote_amount=vote_amount,
|
||||
is_yes_vote=is_yes_vote,
|
||||
fee=final_fee,
|
||||
tx_config=CMDTXConfigLoader.from_json_dict(
|
||||
{
|
||||
"min_coin_amount": args["min_coin_amount"],
|
||||
"max_coin_amount": args["max_coin_amount"],
|
||||
"coins_to_exclude": args["coins_to_exclude"],
|
||||
"amounts_to_exclude": args["amounts_to_exclude"],
|
||||
"reuse_puzhash": args["reuse_puzhash"],
|
||||
}
|
||||
).to_tx_config(units["chia"], config, fingerprint),
|
||||
)
|
||||
tx_id = res["tx_id"]
|
||||
start = time.time()
|
||||
while time.time() - start < 10:
|
||||
await asyncio.sleep(0.1)
|
||||
tx = await wallet_client.get_transaction(wallet_id, bytes32.from_hexstr(tx_id))
|
||||
if len(tx.sent_to) > 0:
|
||||
print(transaction_submitted_msg(tx))
|
||||
print(transaction_status_msg(fingerprint, tx_id[2:]))
|
||||
return None
|
||||
|
||||
print(f"Transaction not yet submitted to nodes. TX ID: {tx_id}") # pragma: no cover
|
||||
|
||||
|
||||
async def close_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None:
|
||||
wallet_id = args["wallet_id"]
|
||||
fee = args["fee"]
|
||||
final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"]))
|
||||
proposal_id = args["proposal_id"]
|
||||
self_destruct = args["self_destruct"]
|
||||
async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
|
||||
res = await wallet_client.dao_close_proposal(
|
||||
wallet_id=wallet_id,
|
||||
proposal_id=proposal_id,
|
||||
fee=final_fee,
|
||||
self_destruct=self_destruct,
|
||||
tx_config=CMDTXConfigLoader.from_json_dict(
|
||||
{
|
||||
"min_coin_amount": args["min_coin_amount"],
|
||||
"max_coin_amount": args["max_coin_amount"],
|
||||
"coins_to_exclude": args["coins_to_exclude"],
|
||||
"amounts_to_exclude": args["amounts_to_exclude"],
|
||||
"reuse_puzhash": args["reuse_puzhash"],
|
||||
}
|
||||
).to_tx_config(units["chia"], config, fingerprint),
|
||||
)
|
||||
tx_id = res["tx_id"]
|
||||
start = time.time()
|
||||
while time.time() - start < 10:
|
||||
await asyncio.sleep(0.1)
|
||||
tx = await wallet_client.get_transaction(wallet_id, bytes32.from_hexstr(tx_id))
|
||||
if len(tx.sent_to) > 0:
|
||||
print(transaction_submitted_msg(tx))
|
||||
print(transaction_status_msg(fingerprint, tx_id[2:]))
|
||||
return None
|
||||
|
||||
print(f"Transaction not yet submitted to nodes. TX ID: {tx_id}") # pragma: no cover
|
||||
|
||||
|
||||
async def lockup_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None:
|
||||
wallet_id = args["wallet_id"]
|
||||
amount = args["amount"]
|
||||
final_amount: uint64 = uint64(int(Decimal(amount) * units["cat"]))
|
||||
fee = args["fee"]
|
||||
final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"]))
|
||||
async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
|
||||
res = await wallet_client.dao_send_to_lockup(
|
||||
wallet_id=wallet_id,
|
||||
amount=final_amount,
|
||||
fee=final_fee,
|
||||
tx_config=CMDTXConfigLoader.from_json_dict(
|
||||
{
|
||||
"min_coin_amount": args["min_coin_amount"],
|
||||
"max_coin_amount": args["max_coin_amount"],
|
||||
"coins_to_exclude": args["coins_to_exclude"],
|
||||
"amounts_to_exclude": args["amounts_to_exclude"],
|
||||
"reuse_puzhash": args["reuse_puzhash"],
|
||||
}
|
||||
).to_tx_config(units["chia"], config, fingerprint),
|
||||
)
|
||||
tx_id = res["tx_id"]
|
||||
start = time.time()
|
||||
while time.time() - start < 10:
|
||||
await asyncio.sleep(0.1)
|
||||
tx = await wallet_client.get_transaction(wallet_id, bytes32.from_hexstr(tx_id))
|
||||
if len(tx.sent_to) > 0:
|
||||
print(transaction_submitted_msg(tx))
|
||||
print(transaction_status_msg(fingerprint, tx_id[2:]))
|
||||
return None
|
||||
|
||||
print(f"Transaction not yet submitted to nodes. TX ID: {tx_id}") # pragma: no cover
|
||||
|
||||
|
||||
async def release_coins(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None:
|
||||
wallet_id = args["wallet_id"]
|
||||
fee = args["fee"]
|
||||
final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"]))
|
||||
async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
|
||||
res = await wallet_client.dao_free_coins_from_finished_proposals(
|
||||
wallet_id=wallet_id,
|
||||
fee=final_fee,
|
||||
tx_config=CMDTXConfigLoader.from_json_dict(
|
||||
{
|
||||
"min_coin_amount": args["min_coin_amount"],
|
||||
"max_coin_amount": args["max_coin_amount"],
|
||||
"coins_to_exclude": args["coins_to_exclude"],
|
||||
"amounts_to_exclude": args["amounts_to_exclude"],
|
||||
"reuse_puzhash": args["reuse_puzhash"],
|
||||
}
|
||||
).to_tx_config(units["chia"], config, fingerprint),
|
||||
)
|
||||
tx_id = res["tx_id"]
|
||||
start = time.time()
|
||||
while time.time() - start < 10:
|
||||
await asyncio.sleep(0.1)
|
||||
tx = await wallet_client.get_transaction(wallet_id, bytes32.from_hexstr(tx_id))
|
||||
if len(tx.sent_to) > 0:
|
||||
print(transaction_submitted_msg(tx))
|
||||
print(transaction_status_msg(fingerprint, tx_id[2:]))
|
||||
return None
|
||||
print(f"Transaction not yet submitted to nodes. TX ID: {tx_id}") # pragma: no cover
|
||||
|
||||
|
||||
async def exit_lockup(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None:
|
||||
wallet_id = args["wallet_id"]
|
||||
fee = args["fee"]
|
||||
final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"]))
|
||||
async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
|
||||
res = await wallet_client.dao_exit_lockup(
|
||||
wallet_id=wallet_id,
|
||||
coins=[],
|
||||
fee=final_fee,
|
||||
tx_config=CMDTXConfigLoader.from_json_dict(
|
||||
{
|
||||
"min_coin_amount": args["min_coin_amount"],
|
||||
"max_coin_amount": args["max_coin_amount"],
|
||||
"coins_to_exclude": args["coins_to_exclude"],
|
||||
"amounts_to_exclude": args["amounts_to_exclude"],
|
||||
"reuse_puzhash": args["reuse_puzhash"],
|
||||
}
|
||||
).to_tx_config(units["chia"], config, fingerprint),
|
||||
)
|
||||
tx_id = res["tx_id"]
|
||||
start = time.time()
|
||||
while time.time() - start < 10:
|
||||
await asyncio.sleep(0.1)
|
||||
tx = await wallet_client.get_transaction(wallet_id, bytes32.from_hexstr(tx_id))
|
||||
if len(tx.sent_to) > 0:
|
||||
print(transaction_submitted_msg(tx))
|
||||
print(transaction_status_msg(fingerprint, tx_id[2:]))
|
||||
return None
|
||||
print(f"Transaction not yet submitted to nodes. TX ID: {tx_id}") # pragma: no cover
|
||||
|
||||
|
||||
async def create_spend_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None:
|
||||
wallet_id = args["wallet_id"]
|
||||
fee = args["fee"]
|
||||
final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"]))
|
||||
asset_id = args.get("asset_id")
|
||||
address = args.get("to_address")
|
||||
amount = args.get("amount")
|
||||
additions_file = args.get("from_json")
|
||||
if additions_file is None and (address is None or amount is None):
|
||||
raise ValueError("Must include a json specification or an address / amount pair.")
|
||||
if additions_file: # pragma: no cover
|
||||
with open(additions_file, "r") as f:
|
||||
additions_dict = json.load(f)
|
||||
additions = []
|
||||
for addition in additions_dict:
|
||||
addition["puzzle_hash"] = decode_puzzle_hash(addition["address"]).hex()
|
||||
del addition["address"]
|
||||
additions.append(addition)
|
||||
else:
|
||||
additions = None
|
||||
vote_amount = args.get("vote_amount")
|
||||
async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
|
||||
wallet_type = await get_wallet_type(wallet_id=wallet_id, wallet_client=wallet_client)
|
||||
mojo_per_unit = get_mojo_per_unit(wallet_type=wallet_type)
|
||||
final_amount: Optional[uint64] = uint64(int(Decimal(amount) * mojo_per_unit)) if amount else None
|
||||
res = await wallet_client.dao_create_proposal(
|
||||
wallet_id=wallet_id,
|
||||
proposal_type="spend",
|
||||
additions=additions,
|
||||
amount=final_amount,
|
||||
inner_address=address,
|
||||
asset_id=asset_id,
|
||||
vote_amount=vote_amount,
|
||||
fee=final_fee,
|
||||
tx_config=CMDTXConfigLoader.from_json_dict(
|
||||
{
|
||||
"min_coin_amount": args["min_coin_amount"],
|
||||
"max_coin_amount": args["max_coin_amount"],
|
||||
"coins_to_exclude": args["coins_to_exclude"],
|
||||
"amounts_to_exclude": args["amounts_to_exclude"],
|
||||
"reuse_puzhash": args["reuse_puzhash"],
|
||||
}
|
||||
).to_tx_config(units["chia"], config, fingerprint),
|
||||
)
|
||||
if res["success"]:
|
||||
asset_id_name = asset_id if asset_id else "XCH"
|
||||
print(f"Created spend proposal for asset: {asset_id_name}")
|
||||
print("Successfully created proposal.")
|
||||
print("Proposal ID: {}".format(res["proposal_id"]))
|
||||
else: # pragma: no cover
|
||||
print("Failed to create proposal.")
|
||||
|
||||
|
||||
async def create_update_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None:
|
||||
wallet_id = args["wallet_id"]
|
||||
fee = Decimal(args["fee"])
|
||||
final_fee: uint64 = uint64(int(fee * units["chia"]))
|
||||
proposal_timelock = args.get("proposal_timelock")
|
||||
soft_close_length = args.get("soft_close_length")
|
||||
attendance_required = args.get("attendance_required")
|
||||
pass_percentage = args.get("pass_percentage")
|
||||
self_destruct_length = args.get("self_destruct_length")
|
||||
oracle_spend_delay = args.get("oracle_spend_delay")
|
||||
vote_amount = args.get("vote_amount")
|
||||
new_dao_rules = {
|
||||
"proposal_timelock": proposal_timelock,
|
||||
"soft_close_length": soft_close_length,
|
||||
"attendance_required": attendance_required,
|
||||
"pass_percentage": pass_percentage,
|
||||
"self_destruct_length": self_destruct_length,
|
||||
"oracle_spend_delay": oracle_spend_delay,
|
||||
}
|
||||
async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
|
||||
res = await wallet_client.dao_create_proposal(
|
||||
wallet_id=wallet_id,
|
||||
proposal_type="update",
|
||||
new_dao_rules=new_dao_rules,
|
||||
vote_amount=vote_amount,
|
||||
fee=final_fee,
|
||||
tx_config=CMDTXConfigLoader.from_json_dict(
|
||||
{
|
||||
"min_coin_amount": args["min_coin_amount"],
|
||||
"max_coin_amount": args["max_coin_amount"],
|
||||
"coins_to_exclude": args["coins_to_exclude"],
|
||||
"amounts_to_exclude": args["amounts_to_exclude"],
|
||||
"reuse_puzhash": args["reuse_puzhash"],
|
||||
}
|
||||
).to_tx_config(units["chia"], config, fingerprint),
|
||||
)
|
||||
if res["success"]:
|
||||
print("Successfully created proposal.")
|
||||
print("Proposal ID: {}".format(res["proposal_id"]))
|
||||
else: # pragma: no cover
|
||||
print("Failed to create proposal.")
|
||||
|
||||
|
||||
async def create_mint_proposal(args: Dict[str, Any], wallet_rpc_port: Optional[int], fp: int) -> None:
|
||||
wallet_id = args["wallet_id"]
|
||||
fee = args["fee"]
|
||||
final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"]))
|
||||
cat_target_address = args["cat_target_address"]
|
||||
amount = args["amount"]
|
||||
vote_amount = args.get("vote_amount")
|
||||
async with get_wallet_client(wallet_rpc_port, fp) as (wallet_client, fingerprint, config):
|
||||
res = await wallet_client.dao_create_proposal(
|
||||
wallet_id=wallet_id,
|
||||
proposal_type="mint",
|
||||
cat_target_address=cat_target_address,
|
||||
amount=amount,
|
||||
vote_amount=vote_amount,
|
||||
fee=final_fee,
|
||||
tx_config=CMDTXConfigLoader.from_json_dict(
|
||||
{
|
||||
"min_coin_amount": args["min_coin_amount"],
|
||||
"max_coin_amount": args["max_coin_amount"],
|
||||
"coins_to_exclude": args["coins_to_exclude"],
|
||||
"amounts_to_exclude": args["amounts_to_exclude"],
|
||||
"reuse_puzhash": args["reuse_puzhash"],
|
||||
}
|
||||
).to_tx_config(units["chia"], config, fingerprint),
|
||||
)
|
||||
if res["success"]:
|
||||
print("Successfully created proposal.")
|
||||
print("Proposal ID: {}".format(res["proposal_id"]))
|
||||
else: # pragma: no cover
|
||||
print("Failed to create proposal.")
|
@ -95,6 +95,7 @@ def get_mojo_per_unit(wallet_type: WalletType) -> int: # pragma: no cover
|
||||
WalletType.POOLING_WALLET,
|
||||
WalletType.DATA_LAYER,
|
||||
WalletType.VC,
|
||||
WalletType.DAO,
|
||||
}:
|
||||
mojo_per_unit = units["chia"]
|
||||
elif wallet_type in {WalletType.CAT, WalletType.CRCAT}:
|
||||
@ -877,6 +878,10 @@ async def print_balances(
|
||||
my_did = get_did_response["did_id"]
|
||||
if my_did is not None and len(my_did) > 0:
|
||||
print(f"{indent}{'-DID ID:'.ljust(ljust)} {my_did}")
|
||||
elif typ == WalletType.DAO:
|
||||
get_id_response = await wallet_client.dao_get_treasury_id(wallet_id)
|
||||
treasury_id = get_id_response["treasury_id"][2:]
|
||||
print(f"{indent}{'-Treasury ID:'.ljust(ljust)} {treasury_id}")
|
||||
elif len(asset_id) > 0:
|
||||
print(f"{indent}{'-Asset ID:'.ljust(ljust)} {asset_id}")
|
||||
print(f"{indent}{'-Wallet ID:'.ljust(ljust)} {wallet_id}")
|
||||
|
@ -43,7 +43,17 @@ from chia.util.ws_message import WsRpcMessage, create_payload_dict
|
||||
from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS
|
||||
from chia.wallet.cat_wallet.cat_info import CRCATInfo
|
||||
from chia.wallet.cat_wallet.cat_wallet import CATWallet
|
||||
from chia.wallet.cat_wallet.dao_cat_info import LockedCoinInfo
|
||||
from chia.wallet.cat_wallet.dao_cat_wallet import DAOCATWallet
|
||||
from chia.wallet.conditions import Condition
|
||||
from chia.wallet.dao_wallet.dao_info import DAORules
|
||||
from chia.wallet.dao_wallet.dao_utils import (
|
||||
generate_mint_proposal_innerpuz,
|
||||
generate_simple_proposal_innerpuz,
|
||||
generate_update_proposal_innerpuz,
|
||||
get_treasury_rules_from_puzzle,
|
||||
)
|
||||
from chia.wallet.dao_wallet.dao_wallet import DAOWallet
|
||||
from chia.wallet.derive_keys import (
|
||||
MAX_POOL_WALLETS,
|
||||
master_sk_to_farmer_sk,
|
||||
@ -56,9 +66,9 @@ from chia.wallet.did_wallet.did_info import DIDCoinData, DIDInfo
|
||||
from chia.wallet.did_wallet.did_wallet import DIDWallet
|
||||
from chia.wallet.did_wallet.did_wallet_puzzles import (
|
||||
DID_INNERPUZ_MOD,
|
||||
did_program_to_metadata,
|
||||
match_did_puzzle,
|
||||
metadata_to_program,
|
||||
program_to_metadata,
|
||||
)
|
||||
from chia.wallet.nft_wallet import nft_puzzles
|
||||
from chia.wallet.nft_wallet.nft_info import NFTCoinInfo, NFTInfo
|
||||
@ -71,7 +81,11 @@ from chia.wallet.payment import Payment
|
||||
from chia.wallet.puzzle_drivers import PuzzleInfo, Solver
|
||||
from chia.wallet.puzzles.clawback.metadata import AutoClaimSettings, ClawbackMetadata
|
||||
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_hash_for_synthetic_public_key
|
||||
from chia.wallet.singleton import create_singleton_puzzle, get_inner_puzzle_from_singleton
|
||||
from chia.wallet.singleton import (
|
||||
SINGLETON_LAUNCHER_PUZZLE_HASH,
|
||||
create_singleton_puzzle,
|
||||
get_inner_puzzle_from_singleton,
|
||||
)
|
||||
from chia.wallet.trade_record import TradeRecord
|
||||
from chia.wallet.trading.offer import Offer
|
||||
from chia.wallet.transaction_record import TransactionRecord
|
||||
@ -202,6 +216,21 @@ class WalletRpcApi:
|
||||
"/did_message_spend": self.did_message_spend,
|
||||
"/did_get_info": self.did_get_info,
|
||||
"/did_find_lost_did": self.did_find_lost_did,
|
||||
# DAO Wallets
|
||||
"/dao_get_proposals": self.dao_get_proposals,
|
||||
"/dao_create_proposal": self.dao_create_proposal,
|
||||
"/dao_parse_proposal": self.dao_parse_proposal,
|
||||
"/dao_vote_on_proposal": self.dao_vote_on_proposal,
|
||||
"/dao_get_treasury_balance": self.dao_get_treasury_balance,
|
||||
"/dao_get_treasury_id": self.dao_get_treasury_id,
|
||||
"/dao_get_rules": self.dao_get_rules,
|
||||
"/dao_close_proposal": self.dao_close_proposal,
|
||||
"/dao_exit_lockup": self.dao_exit_lockup,
|
||||
"/dao_adjust_filter_level": self.dao_adjust_filter_level,
|
||||
"/dao_add_funds_to_treasury": self.dao_add_funds_to_treasury,
|
||||
"/dao_send_to_lockup": self.dao_send_to_lockup,
|
||||
"/dao_get_proposal_state": self.dao_get_proposal_state,
|
||||
"/dao_free_coins_from_finished_proposals": self.dao_free_coins_from_finished_proposals,
|
||||
# NFT Wallet
|
||||
"/nft_mint_nft": self.nft_mint_nft,
|
||||
"/nft_count_nfts": self.nft_count_nfts,
|
||||
@ -751,6 +780,44 @@ class WalletRpcApi:
|
||||
}
|
||||
else: # undefined did_type
|
||||
pass
|
||||
elif request["wallet_type"] == "dao_wallet":
|
||||
name = request.get("name", None)
|
||||
mode = request.get("mode", None)
|
||||
if mode == "new":
|
||||
dao_rules_json = request.get("dao_rules", None)
|
||||
if dao_rules_json:
|
||||
dao_rules = DAORules.from_json_dict(dao_rules_json)
|
||||
else:
|
||||
raise ValueError("DAO rules must be specified for wallet creation")
|
||||
async with self.service.wallet_state_manager.lock:
|
||||
dao_wallet = await DAOWallet.create_new_dao_and_wallet(
|
||||
wallet_state_manager,
|
||||
main_wallet,
|
||||
uint64(request.get("amount_of_cats", None)),
|
||||
dao_rules,
|
||||
tx_config,
|
||||
uint64(request.get("filter_amount", 1)),
|
||||
name,
|
||||
uint64(request.get("fee", 0)),
|
||||
uint64(request.get("fee_for_cat", 0)),
|
||||
)
|
||||
elif mode == "existing":
|
||||
# async with self.service.wallet_state_manager.lock:
|
||||
dao_wallet = await DAOWallet.create_new_dao_wallet_for_existing_dao(
|
||||
wallet_state_manager,
|
||||
main_wallet,
|
||||
bytes32.from_hexstr(request.get("treasury_id", None)),
|
||||
uint64(request.get("filter_amount", 1)),
|
||||
name,
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"type": dao_wallet.type(),
|
||||
"wallet_id": dao_wallet.id(),
|
||||
"treasury_id": dao_wallet.dao_info.treasury_id,
|
||||
"cat_wallet_id": dao_wallet.dao_info.cat_wallet_id,
|
||||
"dao_cat_wallet_id": dao_wallet.dao_info.dao_cat_wallet_id,
|
||||
}
|
||||
elif request["wallet_type"] == "nft_wallet":
|
||||
for wallet in self.service.wallet_state_manager.wallets.values():
|
||||
did_id: Optional[bytes32] = None
|
||||
@ -2120,7 +2187,7 @@ class WalletRpcApi:
|
||||
"public_key": public_key.atom.hex(),
|
||||
"recovery_list_hash": recovery_list_hash.atom.hex(),
|
||||
"num_verification": num_verification.as_int(),
|
||||
"metadata": program_to_metadata(metadata),
|
||||
"metadata": did_program_to_metadata(metadata),
|
||||
"launcher_id": singleton_struct.rest().first().atom.hex(),
|
||||
"full_puzzle": full_puzzle,
|
||||
"solution": Program.from_bytes(bytes(coin_spend.solution)).as_python(),
|
||||
@ -2297,7 +2364,7 @@ class WalletRpcApi:
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
json.dumps(did_wallet_puzzles.program_to_metadata(metadata)),
|
||||
json.dumps(did_wallet_puzzles.did_program_to_metadata(metadata)),
|
||||
)
|
||||
await did_wallet.save_info(did_info)
|
||||
await self.service.wallet_state_manager.update_wallet_puzzle_hashes(did_wallet.wallet_info.id)
|
||||
@ -2528,6 +2595,347 @@ class WalletRpcApi:
|
||||
"transaction_id": txs.name,
|
||||
}
|
||||
|
||||
##########################################################################################
|
||||
# DAO Wallet
|
||||
##########################################################################################
|
||||
|
||||
async def dao_adjust_filter_level(self, request: Dict[str, Any]) -> EndpointResult:
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
dao_wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=DAOWallet)
|
||||
await dao_wallet.adjust_filter_level(uint64(request["filter_level"]))
|
||||
return {
|
||||
"success": True,
|
||||
"dao_info": dao_wallet.dao_info,
|
||||
}
|
||||
|
||||
@tx_endpoint
|
||||
async def dao_add_funds_to_treasury(
|
||||
self,
|
||||
request: Dict[str, Any],
|
||||
tx_config: TXConfig = DEFAULT_TX_CONFIG,
|
||||
push: bool = True,
|
||||
extra_conditions: Tuple[Condition, ...] = tuple(),
|
||||
) -> EndpointResult:
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
dao_wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=DAOWallet)
|
||||
funding_wallet_id = uint32(request["funding_wallet_id"])
|
||||
wallet_type = self.service.wallet_state_manager.wallets[funding_wallet_id].type()
|
||||
amount = request.get("amount")
|
||||
assert amount
|
||||
if wallet_type not in [WalletType.STANDARD_WALLET, WalletType.CAT]: # pragma: no cover
|
||||
raise ValueError(f"Cannot fund a treasury with assets from a {wallet_type.name} wallet")
|
||||
funding_tx = await dao_wallet.create_add_funds_to_treasury_spend(
|
||||
uint64(amount),
|
||||
tx_config,
|
||||
fee=uint64(request.get("fee", 0)),
|
||||
funding_wallet_id=funding_wallet_id,
|
||||
extra_conditions=extra_conditions,
|
||||
)
|
||||
if push:
|
||||
await self.service.wallet_state_manager.add_pending_transaction(funding_tx)
|
||||
return {"success": True, "tx_id": funding_tx.name, "tx": funding_tx}
|
||||
|
||||
async def dao_get_treasury_balance(self, request: Dict[str, Any]) -> EndpointResult:
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
dao_wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=DAOWallet)
|
||||
assert dao_wallet is not None
|
||||
asset_list = dao_wallet.dao_info.assets
|
||||
balances = {}
|
||||
for asset_id in asset_list:
|
||||
balance = await dao_wallet.get_balance_by_asset_type(asset_id=asset_id)
|
||||
if asset_id is None:
|
||||
balances["xch"] = balance
|
||||
else:
|
||||
balances[asset_id.hex()] = balance
|
||||
return {"success": True, "balances": balances}
|
||||
|
||||
async def dao_get_treasury_id(self, request: Dict[str, Any]) -> EndpointResult:
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
dao_wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=DAOWallet)
|
||||
assert dao_wallet is not None
|
||||
treasury_id = dao_wallet.dao_info.treasury_id
|
||||
return {"treasury_id": treasury_id}
|
||||
|
||||
async def dao_get_rules(self, request: Dict[str, Any]) -> EndpointResult:
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
dao_wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=DAOWallet)
|
||||
assert dao_wallet is not None
|
||||
rules = dao_wallet.dao_rules
|
||||
return {"rules": rules}
|
||||
|
||||
@tx_endpoint
|
||||
async def dao_send_to_lockup(
|
||||
self,
|
||||
request: Dict[str, Any],
|
||||
tx_config: TXConfig = DEFAULT_TX_CONFIG,
|
||||
push: bool = True,
|
||||
extra_conditions: Tuple[Condition, ...] = tuple(),
|
||||
) -> EndpointResult:
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
dao_wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=DAOWallet)
|
||||
dao_cat_wallet = self.service.wallet_state_manager.get_wallet(
|
||||
id=dao_wallet.dao_info.dao_cat_wallet_id, required_type=DAOCATWallet
|
||||
)
|
||||
amount = uint64(request["amount"])
|
||||
fee = uint64(request.get("fee", 0))
|
||||
txs = await dao_cat_wallet.enter_dao_cat_voting_mode(
|
||||
amount,
|
||||
tx_config,
|
||||
fee=fee,
|
||||
extra_conditions=extra_conditions,
|
||||
)
|
||||
if push:
|
||||
for tx in txs:
|
||||
await self.service.wallet_state_manager.add_pending_transaction(tx)
|
||||
return {
|
||||
"success": True,
|
||||
"tx_id": txs[0].name,
|
||||
"txs": txs,
|
||||
}
|
||||
|
||||
async def dao_get_proposals(self, request: Dict[str, Any]) -> EndpointResult:
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
include_closed = request.get("include_closed", True)
|
||||
dao_wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=DAOWallet)
|
||||
assert dao_wallet is not None
|
||||
proposal_list = dao_wallet.dao_info.proposals_list
|
||||
if not include_closed:
|
||||
proposal_list = [prop for prop in proposal_list if not prop.closed]
|
||||
dao_rules = get_treasury_rules_from_puzzle(dao_wallet.dao_info.current_treasury_innerpuz)
|
||||
return {
|
||||
"success": True,
|
||||
"proposals": proposal_list,
|
||||
"proposal_timelock": dao_rules.proposal_timelock,
|
||||
"soft_close_length": dao_rules.soft_close_length,
|
||||
}
|
||||
|
||||
async def dao_get_proposal_state(self, request: Dict[str, Any]) -> EndpointResult:
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
dao_wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=DAOWallet)
|
||||
assert dao_wallet is not None
|
||||
state = await dao_wallet.get_proposal_state(bytes32.from_hexstr(request["proposal_id"]))
|
||||
return {"success": True, "state": state}
|
||||
|
||||
@tx_endpoint
|
||||
async def dao_exit_lockup(
|
||||
self,
|
||||
request: Dict[str, Any],
|
||||
tx_config: TXConfig = DEFAULT_TX_CONFIG,
|
||||
push: bool = True,
|
||||
extra_conditions: Tuple[Condition, ...] = tuple(),
|
||||
) -> EndpointResult:
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
dao_wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=DAOWallet)
|
||||
assert dao_wallet is not None
|
||||
dao_cat_wallet = self.service.wallet_state_manager.get_wallet(
|
||||
id=dao_wallet.dao_info.dao_cat_wallet_id, required_type=DAOCATWallet
|
||||
)
|
||||
assert dao_cat_wallet is not None
|
||||
if request["coins"]: # pragma: no cover
|
||||
coin_list = [Coin.from_json_dict(coin) for coin in request["coins"]]
|
||||
coins: List[LockedCoinInfo] = []
|
||||
for lci in dao_cat_wallet.dao_cat_info.locked_coins:
|
||||
if lci.coin in coin_list:
|
||||
coins.append(lci)
|
||||
else:
|
||||
coins = []
|
||||
for lci in dao_cat_wallet.dao_cat_info.locked_coins:
|
||||
if lci.active_votes == []:
|
||||
coins.append(lci)
|
||||
fee = uint64(request.get("fee", 0))
|
||||
exit_tx = await dao_cat_wallet.exit_vote_state(
|
||||
coins,
|
||||
tx_config,
|
||||
fee=fee,
|
||||
extra_conditions=extra_conditions,
|
||||
)
|
||||
if push:
|
||||
await self.service.wallet_state_manager.add_pending_transaction(exit_tx)
|
||||
return {"success": True, "tx_id": exit_tx.name, "tx": exit_tx}
|
||||
|
||||
@tx_endpoint
|
||||
async def dao_create_proposal(
|
||||
self,
|
||||
request: Dict[str, Any],
|
||||
tx_config: TXConfig = DEFAULT_TX_CONFIG,
|
||||
push: bool = True,
|
||||
extra_conditions: Tuple[Condition, ...] = tuple(),
|
||||
) -> EndpointResult:
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
dao_wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=DAOWallet)
|
||||
assert dao_wallet is not None
|
||||
|
||||
if request["proposal_type"] == "spend":
|
||||
amounts: List[uint64] = []
|
||||
puzzle_hashes: List[bytes32] = []
|
||||
asset_types: List[Optional[bytes32]] = []
|
||||
additions: Optional[List[Dict[str, Any]]] = request.get("additions")
|
||||
if additions is not None:
|
||||
for addition in additions:
|
||||
if "asset_id" in addition:
|
||||
asset_id = bytes32.from_hexstr(addition["asset_id"])
|
||||
else:
|
||||
asset_id = None
|
||||
receiver_ph = bytes32.from_hexstr(addition["puzzle_hash"])
|
||||
amount = uint64(addition["amount"])
|
||||
amounts.append(amount)
|
||||
puzzle_hashes.append(receiver_ph)
|
||||
asset_types.append(asset_id)
|
||||
else: # pragma: no cover
|
||||
amounts.append(uint64(request["amount"]))
|
||||
puzzle_hashes.append(decode_puzzle_hash(request["inner_address"]))
|
||||
if request["asset_id"] is not None:
|
||||
asset_types.append(bytes32.from_hexstr(request["asset_id"]))
|
||||
else:
|
||||
asset_types.append(None)
|
||||
proposed_puzzle = generate_simple_proposal_innerpuz(
|
||||
dao_wallet.dao_info.treasury_id, puzzle_hashes, amounts, asset_types
|
||||
)
|
||||
|
||||
elif request["proposal_type"] == "update":
|
||||
rules = dao_wallet.dao_rules
|
||||
prop = request["new_dao_rules"]
|
||||
new_rules = DAORules(
|
||||
proposal_timelock=prop.get("proposal_timelock") or rules.proposal_timelock,
|
||||
soft_close_length=prop.get("soft_close_length") or rules.soft_close_length,
|
||||
attendance_required=prop.get("attendance_required") or rules.attendance_required,
|
||||
proposal_minimum_amount=prop.get("proposal_minimum_amount") or rules.proposal_minimum_amount,
|
||||
pass_percentage=prop.get("pass_percentage") or rules.pass_percentage,
|
||||
self_destruct_length=prop.get("self_destruct_length") or rules.self_destruct_length,
|
||||
oracle_spend_delay=prop.get("oracle_spend_delay") or rules.oracle_spend_delay,
|
||||
)
|
||||
|
||||
current_innerpuz = dao_wallet.dao_info.current_treasury_innerpuz
|
||||
assert current_innerpuz is not None
|
||||
proposed_puzzle = await generate_update_proposal_innerpuz(current_innerpuz, new_rules)
|
||||
elif request["proposal_type"] == "mint":
|
||||
amount_of_cats = uint64(request["amount"])
|
||||
mint_address = decode_puzzle_hash(request["cat_target_address"])
|
||||
cat_wallet = self.service.wallet_state_manager.get_wallet(
|
||||
id=dao_wallet.dao_info.cat_wallet_id, required_type=CATWallet
|
||||
)
|
||||
proposed_puzzle = await generate_mint_proposal_innerpuz(
|
||||
dao_wallet.dao_info.treasury_id,
|
||||
cat_wallet.cat_info.limitations_program_hash,
|
||||
amount_of_cats,
|
||||
mint_address,
|
||||
)
|
||||
else: # pragma: no cover
|
||||
return {"success": False, "error": "Unknown proposal type."}
|
||||
|
||||
vote_amount = request.get("vote_amount")
|
||||
fee = uint64(request.get("fee", 0))
|
||||
proposal_tx = await dao_wallet.generate_new_proposal(
|
||||
proposed_puzzle,
|
||||
tx_config,
|
||||
vote_amount=vote_amount,
|
||||
fee=fee,
|
||||
extra_conditions=extra_conditions,
|
||||
)
|
||||
assert proposal_tx is not None
|
||||
await self.service.wallet_state_manager.add_pending_transaction(proposal_tx)
|
||||
assert isinstance(proposal_tx.removals, List)
|
||||
for coin in proposal_tx.removals:
|
||||
if coin.puzzle_hash == SINGLETON_LAUNCHER_PUZZLE_HASH:
|
||||
proposal_id = coin.name()
|
||||
break
|
||||
else: # pragma: no cover
|
||||
raise ValueError("Could not find proposal ID in transaction")
|
||||
return {
|
||||
"success": True,
|
||||
"proposal_id": proposal_id,
|
||||
"tx_id": proposal_tx.name.hex(),
|
||||
"tx": proposal_tx,
|
||||
}
|
||||
|
||||
@tx_endpoint
|
||||
async def dao_vote_on_proposal(
|
||||
self,
|
||||
request: Dict[str, Any],
|
||||
tx_config: TXConfig = DEFAULT_TX_CONFIG,
|
||||
push: bool = True,
|
||||
extra_conditions: Tuple[Condition, ...] = tuple(),
|
||||
) -> EndpointResult:
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
dao_wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=DAOWallet)
|
||||
assert dao_wallet is not None
|
||||
vote_amount = None
|
||||
if "vote_amount" in request:
|
||||
vote_amount = uint64(request["vote_amount"])
|
||||
fee = uint64(request.get("fee", 0))
|
||||
vote_tx = await dao_wallet.generate_proposal_vote_spend(
|
||||
bytes32.from_hexstr(request["proposal_id"]),
|
||||
vote_amount,
|
||||
request["is_yes_vote"], # bool
|
||||
tx_config,
|
||||
fee,
|
||||
extra_conditions=extra_conditions,
|
||||
)
|
||||
assert vote_tx is not None
|
||||
if push:
|
||||
await self.service.wallet_state_manager.add_pending_transaction(vote_tx)
|
||||
return {"success": True, "tx_id": vote_tx.name, "tx": vote_tx}
|
||||
|
||||
async def dao_parse_proposal(self, request: Dict[str, Any]) -> EndpointResult:
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
dao_wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=DAOWallet)
|
||||
assert dao_wallet is not None
|
||||
proposal_id = bytes32.from_hexstr(request["proposal_id"])
|
||||
proposal_dictionary = await dao_wallet.parse_proposal(proposal_id)
|
||||
assert proposal_dictionary is not None
|
||||
return {"success": True, "proposal_dictionary": proposal_dictionary}
|
||||
|
||||
@tx_endpoint
|
||||
async def dao_close_proposal(
|
||||
self,
|
||||
request: Dict[str, Any],
|
||||
tx_config: TXConfig = DEFAULT_TX_CONFIG,
|
||||
push: bool = True,
|
||||
extra_conditions: Tuple[Condition, ...] = tuple(),
|
||||
) -> EndpointResult:
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
dao_wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=DAOWallet)
|
||||
assert dao_wallet is not None
|
||||
fee = uint64(request.get("fee", 0))
|
||||
if "genesis_id" in request: # pragma: no cover
|
||||
genesis_id = bytes32.from_hexstr(request["genesis_id"])
|
||||
else:
|
||||
genesis_id = None
|
||||
self_destruct = request.get("self_destruct", None)
|
||||
tx = await dao_wallet.create_proposal_close_spend(
|
||||
bytes32.from_hexstr(request["proposal_id"]),
|
||||
tx_config,
|
||||
genesis_id,
|
||||
fee=fee,
|
||||
self_destruct=self_destruct,
|
||||
extra_conditions=extra_conditions,
|
||||
)
|
||||
assert tx is not None
|
||||
await self.service.wallet_state_manager.add_pending_transaction(tx)
|
||||
return {"success": True, "tx_id": tx.name, "tx": tx}
|
||||
|
||||
@tx_endpoint
|
||||
async def dao_free_coins_from_finished_proposals(
|
||||
self,
|
||||
request: Dict[str, Any],
|
||||
tx_config: TXConfig = DEFAULT_TX_CONFIG,
|
||||
push: bool = True,
|
||||
extra_conditions: Tuple[Condition, ...] = tuple(),
|
||||
) -> EndpointResult:
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
fee = uint64(request.get("fee", 0))
|
||||
dao_wallet = self.service.wallet_state_manager.get_wallet(id=wallet_id, required_type=DAOWallet)
|
||||
assert dao_wallet is not None
|
||||
tx = await dao_wallet.free_coins_from_finished_proposals(
|
||||
tx_config,
|
||||
fee=fee,
|
||||
extra_conditions=extra_conditions,
|
||||
)
|
||||
assert tx is not None
|
||||
await self.service.wallet_state_manager.add_pending_transaction(tx)
|
||||
|
||||
return {"success": True, "tx_id": tx.name, "tx": tx}
|
||||
|
||||
##########################################################################################
|
||||
# NFT Wallet
|
||||
##########################################################################################
|
||||
|
@ -1343,6 +1343,224 @@ class WalletRpcClient(RpcClient):
|
||||
response = await self.fetch("sign_message_by_id", {"id": id, "message": message})
|
||||
return response["pubkey"], response["signature"], response["signing_mode"]
|
||||
|
||||
# DAOs
|
||||
async def create_new_dao_wallet(
|
||||
self,
|
||||
mode: str,
|
||||
tx_config: TXConfig,
|
||||
dao_rules: Optional[Dict[str, uint64]] = None,
|
||||
amount_of_cats: Optional[uint64] = None,
|
||||
treasury_id: Optional[bytes32] = None,
|
||||
filter_amount: uint64 = uint64(1),
|
||||
name: Optional[str] = None,
|
||||
fee: uint64 = uint64(0),
|
||||
fee_for_cat: uint64 = uint64(0),
|
||||
extra_conditions: Tuple[Condition, ...] = tuple(),
|
||||
) -> Dict:
|
||||
request: Dict[str, Any] = {
|
||||
"wallet_type": "dao_wallet",
|
||||
"mode": mode,
|
||||
"treasury_id": treasury_id,
|
||||
"dao_rules": dao_rules,
|
||||
"amount_of_cats": amount_of_cats,
|
||||
"filter_amount": filter_amount,
|
||||
"name": name,
|
||||
"fee": fee,
|
||||
"fee_for_cat": fee_for_cat,
|
||||
"extra_conditions": list(extra_conditions),
|
||||
**tx_config.to_json_dict(),
|
||||
}
|
||||
response = await self.fetch("create_new_wallet", request)
|
||||
return response
|
||||
|
||||
async def dao_get_treasury_id(
|
||||
self,
|
||||
wallet_id: int,
|
||||
) -> Dict:
|
||||
request: Dict[str, Any] = {"wallet_id": wallet_id}
|
||||
response = await self.fetch("dao_get_treasury_id", request)
|
||||
return response
|
||||
|
||||
async def dao_get_rules(
|
||||
self,
|
||||
wallet_id: int,
|
||||
) -> Dict:
|
||||
request: Dict[str, Any] = {"wallet_id": wallet_id}
|
||||
response = await self.fetch("dao_get_rules", request)
|
||||
return response
|
||||
|
||||
async def dao_create_proposal(
|
||||
self,
|
||||
wallet_id: int,
|
||||
proposal_type: str,
|
||||
tx_config: TXConfig,
|
||||
additions: Optional[List[Dict]] = None,
|
||||
amount: Optional[uint64] = None,
|
||||
inner_address: Optional[str] = None,
|
||||
asset_id: Optional[str] = None,
|
||||
cat_target_address: Optional[str] = None,
|
||||
vote_amount: Optional[int] = None,
|
||||
new_dao_rules: Optional[Dict[str, Optional[uint64]]] = None,
|
||||
fee: uint64 = uint64(0),
|
||||
extra_conditions: Tuple[Condition, ...] = tuple(),
|
||||
) -> Dict:
|
||||
request: Dict[str, Any] = {
|
||||
"wallet_id": wallet_id,
|
||||
"proposal_type": proposal_type,
|
||||
"additions": additions,
|
||||
"amount": amount,
|
||||
"inner_address": inner_address,
|
||||
"asset_id": asset_id,
|
||||
"cat_target_address": cat_target_address,
|
||||
"vote_amount": vote_amount,
|
||||
"new_dao_rules": new_dao_rules,
|
||||
"fee": fee,
|
||||
"extra_conditions": list(extra_conditions),
|
||||
**tx_config.to_json_dict(),
|
||||
}
|
||||
|
||||
response = await self.fetch("dao_create_proposal", request)
|
||||
return response
|
||||
|
||||
async def dao_get_proposal_state(self, wallet_id: int, proposal_id: str):
|
||||
request: Dict[str, Any] = {"wallet_id": wallet_id, "proposal_id": proposal_id}
|
||||
response = await self.fetch("dao_get_proposal_state", request)
|
||||
return response
|
||||
|
||||
async def dao_parse_proposal(self, wallet_id: int, proposal_id: str):
|
||||
request: Dict[str, Any] = {"wallet_id": wallet_id, "proposal_id": proposal_id}
|
||||
response = await self.fetch("dao_parse_proposal", request)
|
||||
return response
|
||||
|
||||
async def dao_vote_on_proposal(
|
||||
self,
|
||||
wallet_id: int,
|
||||
proposal_id: str,
|
||||
vote_amount: uint64,
|
||||
tx_config: TXConfig,
|
||||
is_yes_vote: bool = True,
|
||||
fee: uint64 = uint64(0),
|
||||
extra_conditions: Tuple[Condition, ...] = tuple(),
|
||||
):
|
||||
request: Dict[str, Any] = {
|
||||
"wallet_id": wallet_id,
|
||||
"proposal_id": proposal_id,
|
||||
"vote_amount": vote_amount,
|
||||
"is_yes_vote": is_yes_vote,
|
||||
"fee": fee,
|
||||
"extra_conditions": list(extra_conditions),
|
||||
**tx_config.to_json_dict(),
|
||||
}
|
||||
response = await self.fetch("dao_vote_on_proposal", request)
|
||||
return response
|
||||
|
||||
async def dao_get_proposals(self, wallet_id: int, include_closed: bool = True):
|
||||
request: Dict[str, Any] = {"wallet_id": wallet_id, "include_closed": include_closed}
|
||||
response = await self.fetch("dao_get_proposals", request)
|
||||
return response
|
||||
|
||||
async def dao_close_proposal(
|
||||
self,
|
||||
wallet_id: int,
|
||||
proposal_id: str,
|
||||
tx_config: TXConfig,
|
||||
self_destruct: bool = None,
|
||||
fee: uint64 = uint64(0),
|
||||
extra_conditions: Tuple[Condition, ...] = tuple(),
|
||||
):
|
||||
request: Dict[str, Any] = {
|
||||
"wallet_id": wallet_id,
|
||||
"proposal_id": proposal_id,
|
||||
"self_destruct": self_destruct,
|
||||
"fee": fee,
|
||||
"extra_conditions": list(extra_conditions),
|
||||
**tx_config.to_json_dict(),
|
||||
}
|
||||
response = await self.fetch("dao_close_proposal", request)
|
||||
return response
|
||||
|
||||
async def dao_free_coins_from_finished_proposals(
|
||||
self,
|
||||
wallet_id: int,
|
||||
tx_config: TXConfig,
|
||||
fee: uint64 = uint64(0),
|
||||
extra_conditions: Tuple[Condition, ...] = tuple(),
|
||||
):
|
||||
request: Dict[str, Any] = {
|
||||
"wallet_id": wallet_id,
|
||||
"fee": fee,
|
||||
"extra_conditions": list(extra_conditions),
|
||||
**tx_config.to_json_dict(),
|
||||
}
|
||||
response = await self.fetch("dao_free_coins_from_finished_proposals", request)
|
||||
return response
|
||||
|
||||
async def dao_get_treasury_balance(self, wallet_id: int):
|
||||
request: Dict[str, Any] = {"wallet_id": wallet_id}
|
||||
response = await self.fetch("dao_get_treasury_balance", request)
|
||||
return response
|
||||
|
||||
async def dao_add_funds_to_treasury(
|
||||
self,
|
||||
wallet_id: int,
|
||||
funding_wallet_id: int,
|
||||
amount: uint64,
|
||||
tx_config: TXConfig,
|
||||
fee: uint64 = uint64(0),
|
||||
extra_conditions: Tuple[Condition, ...] = tuple(),
|
||||
):
|
||||
request: Dict[str, Any] = {
|
||||
"wallet_id": wallet_id,
|
||||
"funding_wallet_id": funding_wallet_id,
|
||||
"amount": amount,
|
||||
"fee": fee,
|
||||
"extra_conditions": list(extra_conditions),
|
||||
**tx_config.to_json_dict(),
|
||||
}
|
||||
response = await self.fetch("dao_add_funds_to_treasury", request)
|
||||
return response
|
||||
|
||||
async def dao_send_to_lockup(
|
||||
self,
|
||||
wallet_id: int,
|
||||
amount: uint64,
|
||||
tx_config: TXConfig,
|
||||
fee: uint64 = uint64(0),
|
||||
extra_conditions: Tuple[Condition, ...] = tuple(),
|
||||
):
|
||||
request: Dict[str, Any] = {
|
||||
"wallet_id": wallet_id,
|
||||
"amount": amount,
|
||||
"fee": fee,
|
||||
"extra_conditions": list(extra_conditions),
|
||||
**tx_config.to_json_dict(),
|
||||
}
|
||||
response = await self.fetch("dao_send_to_lockup", request)
|
||||
return response
|
||||
|
||||
async def dao_exit_lockup(
|
||||
self,
|
||||
wallet_id: int,
|
||||
tx_config: TXConfig,
|
||||
coins: Optional[List[Dict]] = None,
|
||||
fee: uint64 = uint64(0),
|
||||
extra_conditions: Tuple[Condition, ...] = tuple(),
|
||||
):
|
||||
request: Dict[str, Any] = {
|
||||
"wallet_id": wallet_id,
|
||||
"coins": coins,
|
||||
"fee": fee,
|
||||
"extra_conditions": list(extra_conditions),
|
||||
**tx_config.to_json_dict(),
|
||||
}
|
||||
response = await self.fetch("dao_exit_lockup", request)
|
||||
return response
|
||||
|
||||
async def dao_adjust_filter_level(self, wallet_id: int, filter_level: int):
|
||||
request: Dict[str, Any] = {"wallet_id": wallet_id, "filter_level": filter_level}
|
||||
response = await self.fetch("dao_adjust_filter_level", request)
|
||||
return response
|
||||
|
||||
async def vc_mint(
|
||||
self,
|
||||
did_id: bytes32,
|
||||
|
@ -813,7 +813,7 @@ class CATWallet:
|
||||
if not ignore_max_send_amount:
|
||||
max_send = await self.get_max_send_amount()
|
||||
if payment_sum > max_send:
|
||||
raise ValueError(f"Can't send more than {max_send} mojos in a single transaction")
|
||||
raise ValueError(f" Insufficient funds. Your max amount is {max_send} mojos in a single transaction.")
|
||||
unsigned_spend_bundle, chia_tx = await self.generate_unsigned_spendbundle(
|
||||
payments,
|
||||
tx_config,
|
||||
@ -870,7 +870,6 @@ class CATWallet:
|
||||
valid_times=parse_timelock_info(extra_conditions),
|
||||
)
|
||||
)
|
||||
|
||||
return tx_list
|
||||
|
||||
async def add_lineage(self, name: bytes32, lineage: Optional[LineageProof]) -> None:
|
||||
|
28
chia/wallet/cat_wallet/dao_cat_info.py
Normal file
28
chia/wallet/cat_wallet/dao_cat_info.py
Normal file
@ -0,0 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import 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.util.ints import uint64
|
||||
from chia.util.streamable import Streamable, streamable
|
||||
|
||||
|
||||
@streamable
|
||||
@dataclass(frozen=True)
|
||||
class LockedCoinInfo(Streamable):
|
||||
coin: Coin
|
||||
inner_puzzle: Program # This is the lockup puzzle, not the lockup_puzzle's inner_puzzle
|
||||
active_votes: List[Optional[bytes32]]
|
||||
|
||||
|
||||
@streamable
|
||||
@dataclass(frozen=True)
|
||||
class DAOCATInfo(Streamable):
|
||||
dao_wallet_id: uint64
|
||||
free_cat_wallet_id: uint64
|
||||
limitations_program_hash: bytes32
|
||||
my_tail: Optional[Program] # this is the program
|
||||
locked_coins: List[LockedCoinInfo]
|
677
chia/wallet/cat_wallet/dao_cat_wallet.py
Normal file
677
chia/wallet/cat_wallet/dao_cat_wallet.py
Normal file
@ -0,0 +1,677 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from secrets import token_bytes
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, List, Optional, Set, Tuple, cast
|
||||
|
||||
from blspy import G1Element
|
||||
|
||||
from chia.server.ws_connection import WSChiaConnection
|
||||
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.spend_bundle import SpendBundle
|
||||
from chia.util.byte_types import hexstr_to_bytes
|
||||
from chia.util.ints import uint32, uint64, uint128
|
||||
from chia.wallet.cat_wallet.cat_utils import (
|
||||
CAT_MOD,
|
||||
SpendableCAT,
|
||||
construct_cat_puzzle,
|
||||
unsigned_spend_bundle_for_spendable_cats,
|
||||
)
|
||||
from chia.wallet.cat_wallet.cat_wallet import CATWallet
|
||||
from chia.wallet.cat_wallet.dao_cat_info import DAOCATInfo, LockedCoinInfo
|
||||
from chia.wallet.cat_wallet.lineage_store import CATLineageStore
|
||||
from chia.wallet.conditions import Condition, parse_timelock_info
|
||||
from chia.wallet.dao_wallet.dao_utils import (
|
||||
add_proposal_to_active_list,
|
||||
get_active_votes_from_lockup_puzzle,
|
||||
get_finished_state_inner_puzzle,
|
||||
get_innerpuz_from_lockup_puzzle,
|
||||
get_lockup_puzzle,
|
||||
)
|
||||
from chia.wallet.lineage_proof import LineageProof
|
||||
from chia.wallet.payment import Payment
|
||||
from chia.wallet.transaction_record import TransactionRecord
|
||||
from chia.wallet.util.curry_and_treehash import calculate_hash_of_quoted_mod_hash
|
||||
from chia.wallet.util.transaction_type import TransactionType
|
||||
from chia.wallet.util.tx_config import CoinSelectionConfig, TXConfig
|
||||
from chia.wallet.util.wallet_sync_utils import fetch_coin_spend
|
||||
from chia.wallet.util.wallet_types import WalletType
|
||||
from chia.wallet.wallet import Wallet
|
||||
from chia.wallet.wallet_coin_record import WalletCoinRecord
|
||||
from chia.wallet.wallet_info import WalletInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from chia.wallet.wallet_state_manager import WalletStateManager
|
||||
|
||||
CAT_MOD_HASH = CAT_MOD.get_tree_hash()
|
||||
CAT_MOD_HASH_HASH = Program.to(CAT_MOD_HASH).get_tree_hash()
|
||||
QUOTED_MOD_HASH = calculate_hash_of_quoted_mod_hash(CAT_MOD_HASH)
|
||||
|
||||
|
||||
class DAOCATWallet:
|
||||
if TYPE_CHECKING:
|
||||
from chia.wallet.wallet_protocol import WalletProtocol
|
||||
|
||||
_protocol_check: ClassVar[WalletProtocol[DAOCATInfo]] = cast("DAOCATWallet", None)
|
||||
|
||||
wallet_state_manager: Any
|
||||
log: logging.Logger
|
||||
wallet_info: WalletInfo
|
||||
dao_cat_info: DAOCATInfo
|
||||
standard_wallet: Wallet
|
||||
cost_of_single_tx: Optional[int]
|
||||
lineage_store: CATLineageStore
|
||||
|
||||
@classmethod
|
||||
def type(cls) -> WalletType:
|
||||
return WalletType.DAO_CAT
|
||||
|
||||
@staticmethod
|
||||
async def create(
|
||||
wallet_state_manager: WalletStateManager,
|
||||
wallet: Wallet,
|
||||
wallet_info: WalletInfo,
|
||||
) -> DAOCATWallet:
|
||||
self = DAOCATWallet()
|
||||
self.log = logging.getLogger(__name__)
|
||||
|
||||
self.cost_of_single_tx = None
|
||||
self.wallet_state_manager = wallet_state_manager
|
||||
self.wallet_info = wallet_info
|
||||
self.standard_wallet = wallet
|
||||
try:
|
||||
self.dao_cat_info = DAOCATInfo.from_bytes(hexstr_to_bytes(self.wallet_info.data))
|
||||
self.lineage_store = await CATLineageStore.create(self.wallet_state_manager.db_wrapper, self.get_asset_id())
|
||||
except AssertionError as e: # pragma: no cover
|
||||
self.log.error(f"Error creating DAO CAT wallet: {e}")
|
||||
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
async def get_or_create_wallet_for_cat(
|
||||
wallet_state_manager: Any,
|
||||
wallet: Wallet,
|
||||
limitations_program_hash_hex: str,
|
||||
name: Optional[str] = None,
|
||||
) -> DAOCATWallet:
|
||||
self = DAOCATWallet()
|
||||
self.cost_of_single_tx = None
|
||||
self.standard_wallet = wallet
|
||||
self.log = logging.getLogger(__name__)
|
||||
|
||||
limitations_program_hash_hex = bytes32.from_hexstr(limitations_program_hash_hex).hex() # Normalize the format
|
||||
|
||||
dao_wallet_id = None
|
||||
free_cat_wallet_id = None
|
||||
for id, w in wallet_state_manager.wallets.items():
|
||||
if w.type() == DAOCATWallet.type():
|
||||
assert isinstance(w, DAOCATWallet)
|
||||
if w.get_asset_id() == limitations_program_hash_hex:
|
||||
self.log.warning("Not creating wallet for already existing DAO CAT wallet")
|
||||
return w
|
||||
elif w.type() == CATWallet.type():
|
||||
assert isinstance(w, CATWallet)
|
||||
if w.get_asset_id() == limitations_program_hash_hex:
|
||||
free_cat_wallet_id = w.id()
|
||||
assert free_cat_wallet_id is not None
|
||||
for id, w in wallet_state_manager.wallets.items():
|
||||
if w.type() == WalletType.DAO:
|
||||
self.log.info(f"FOUND DAO WALLET: {w}")
|
||||
self.log.info(f"ALL WALLETS: {wallet_state_manager.wallets}")
|
||||
if w.get_cat_wallet_id() == free_cat_wallet_id:
|
||||
dao_wallet_id = w.id()
|
||||
assert dao_wallet_id is not None
|
||||
self.wallet_state_manager = wallet_state_manager
|
||||
if name is None:
|
||||
name = CATWallet.default_wallet_name_for_unknown_cat(limitations_program_hash_hex)
|
||||
|
||||
limitations_program_hash = bytes32(hexstr_to_bytes(limitations_program_hash_hex))
|
||||
|
||||
self.dao_cat_info = DAOCATInfo(
|
||||
dao_wallet_id,
|
||||
uint64(free_cat_wallet_id),
|
||||
limitations_program_hash,
|
||||
None,
|
||||
[],
|
||||
)
|
||||
info_as_string = bytes(self.dao_cat_info).hex()
|
||||
self.wallet_info = await wallet_state_manager.user_store.create_wallet(name, WalletType.DAO_CAT, info_as_string)
|
||||
|
||||
self.lineage_store = await CATLineageStore.create(self.wallet_state_manager.db_wrapper, self.get_asset_id())
|
||||
await self.wallet_state_manager.add_new_wallet(self)
|
||||
return self
|
||||
|
||||
async def coin_added(self, coin: Coin, height: uint32, peer: WSChiaConnection, coin_data: Optional[Any]) -> None:
|
||||
"""Notification from wallet state manager that wallet has been received."""
|
||||
self.log.info(f"DAO CAT wallet has been notified that {coin} was added")
|
||||
wallet_node: Any = self.wallet_state_manager.wallet_node
|
||||
parent_coin = (await wallet_node.get_coin_state([coin.parent_coin_info], peer, height))[0]
|
||||
parent_spend = await fetch_coin_spend(height, parent_coin.coin, peer)
|
||||
uncurried = parent_spend.puzzle_reveal.uncurry()
|
||||
cat_inner = uncurried[1].at("rrf")
|
||||
active_votes_list: List[Optional[bytes32]] = []
|
||||
|
||||
record = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(coin.puzzle_hash)
|
||||
if record:
|
||||
inner_puzzle: Optional[Program] = self.standard_wallet.puzzle_for_pk(record.pubkey)
|
||||
else:
|
||||
inner_puzzle = get_innerpuz_from_lockup_puzzle(cat_inner)
|
||||
assert isinstance(inner_puzzle, Program)
|
||||
active_votes_list = get_active_votes_from_lockup_puzzle(cat_inner)
|
||||
active_votes_list = [x.as_atom() for x in active_votes_list.as_iter()]
|
||||
|
||||
if parent_spend.coin.puzzle_hash == coin.puzzle_hash:
|
||||
# shortcut, works for change
|
||||
lockup_puz = cat_inner
|
||||
else:
|
||||
solution = parent_spend.solution.to_program().first()
|
||||
if solution.first() == Program.to(0):
|
||||
# No vote is being added so inner puz stays the same
|
||||
try:
|
||||
removals = solution.at("rrrf")
|
||||
if removals != Program.to(0):
|
||||
for removal in removals.as_iter():
|
||||
active_votes_list.remove(bytes32(removal.as_atom()))
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
new_vote = solution.at("rrrf")
|
||||
active_votes_list.insert(0, bytes32(new_vote.as_atom()))
|
||||
|
||||
lockup_puz = get_lockup_puzzle(
|
||||
self.dao_cat_info.limitations_program_hash,
|
||||
active_votes_list,
|
||||
inner_puzzle,
|
||||
)
|
||||
|
||||
new_cat_puzhash = construct_cat_puzzle(
|
||||
CAT_MOD, self.dao_cat_info.limitations_program_hash, lockup_puz
|
||||
).get_tree_hash()
|
||||
|
||||
if new_cat_puzhash != coin.puzzle_hash: # pragma: no cover
|
||||
raise ValueError(f"Cannot add coin - incorrect lockup puzzle: {coin}")
|
||||
|
||||
lineage_proof = LineageProof(coin.parent_coin_info, lockup_puz.get_tree_hash(), uint64(coin.amount))
|
||||
await self.add_lineage(coin.name(), lineage_proof)
|
||||
|
||||
# add the new coin to the list of locked coins and remove the spent coin
|
||||
locked_coins = [x for x in self.dao_cat_info.locked_coins if x.coin != parent_spend.coin]
|
||||
new_info = LockedCoinInfo(coin, lockup_puz, active_votes_list)
|
||||
if new_info not in locked_coins:
|
||||
locked_coins.append(LockedCoinInfo(coin, lockup_puz, active_votes_list))
|
||||
dao_cat_info: DAOCATInfo = DAOCATInfo(
|
||||
self.dao_cat_info.dao_wallet_id,
|
||||
self.dao_cat_info.free_cat_wallet_id,
|
||||
self.dao_cat_info.limitations_program_hash,
|
||||
self.dao_cat_info.my_tail,
|
||||
locked_coins,
|
||||
)
|
||||
await self.save_info(dao_cat_info)
|
||||
|
||||
async def add_lineage(self, name: bytes32, lineage: Optional[LineageProof]) -> None:
|
||||
"""
|
||||
Lineage proofs are stored as a list of parent coins and the lineage proof you will need if they are the
|
||||
parent of the coin you are trying to spend. 'If I'm your parent, here's the info you need to spend yourself'
|
||||
"""
|
||||
self.log.info(f"Adding parent {name.hex()}: {lineage}")
|
||||
if lineage is not None:
|
||||
await self.lineage_store.add_lineage_proof(name, lineage)
|
||||
|
||||
async def get_lineage_proof_for_coin(self, coin: Coin) -> Optional[LineageProof]:
|
||||
return await self.lineage_store.get_lineage_proof(coin.parent_coin_info)
|
||||
|
||||
async def remove_lineage(self, name: bytes32) -> None: # pragma: no cover
|
||||
self.log.info(f"Removing parent {name} (probably had a non-CAT parent)")
|
||||
await self.lineage_store.remove_lineage_proof(name)
|
||||
|
||||
async def advanced_select_coins(self, amount: uint64, proposal_id: bytes32) -> List[LockedCoinInfo]:
|
||||
coins = []
|
||||
s = 0
|
||||
for coin in self.dao_cat_info.locked_coins:
|
||||
compatible = True
|
||||
for active_vote in coin.active_votes:
|
||||
if active_vote == proposal_id: # pragma: no cover
|
||||
compatible = False
|
||||
break
|
||||
if compatible:
|
||||
coins.append(coin)
|
||||
s += coin.coin.amount
|
||||
if s >= amount:
|
||||
break
|
||||
if s < amount: # pragma: no cover
|
||||
raise ValueError(
|
||||
"We do not have enough CATs in Voting Mode right now. "
|
||||
"Please convert some more or try again with permission to convert."
|
||||
)
|
||||
return coins
|
||||
|
||||
def id(self) -> uint32:
|
||||
return self.wallet_info.id
|
||||
|
||||
async def create_vote_spend(
|
||||
self,
|
||||
amount: uint64,
|
||||
proposal_id: bytes32,
|
||||
is_yes_vote: bool,
|
||||
proposal_puzzle: Optional[Program] = None,
|
||||
) -> SpendBundle:
|
||||
coins: List[LockedCoinInfo] = await self.advanced_select_coins(amount, proposal_id)
|
||||
running_sum = 0 # this will be used for change calculation
|
||||
change = sum(c.coin.amount for c in coins) - amount
|
||||
extra_delta, limitations_solution = 0, Program.to([])
|
||||
limitations_program_reveal = Program.to([])
|
||||
spendable_cat_list = []
|
||||
dao_wallet = self.wallet_state_manager.wallets[self.dao_cat_info.dao_wallet_id]
|
||||
if proposal_puzzle is None: # pragma: no cover
|
||||
proposal_puzzle = dao_wallet.get_proposal_puzzle(proposal_id)
|
||||
assert proposal_puzzle is not None
|
||||
for lci in coins:
|
||||
coin = lci.coin
|
||||
vote_info = 0
|
||||
new_innerpuzzle = add_proposal_to_active_list(lci.inner_puzzle, proposal_id)
|
||||
assert new_innerpuzzle is not None
|
||||
standard_inner_puz = get_innerpuz_from_lockup_puzzle(new_innerpuzzle)
|
||||
assert isinstance(standard_inner_puz, Program)
|
||||
# add_proposal_to_active_list also verifies that the lci.inner_puzzle is accurate
|
||||
# We must create either: one coin with the new puzzle and all our value
|
||||
# OR
|
||||
# a coin with the new puzzle and part of our amount AND a coin with our current puzzle and the change
|
||||
# We must also create a puzzle announcement which announces the following:
|
||||
# message = (sha256tree (list new_proposal_vote_id_or_removal_id vote_amount vote_info my_id))
|
||||
message = Program.to([proposal_id, amount, is_yes_vote, coin.name()]).get_tree_hash()
|
||||
vote_amounts_list = []
|
||||
voting_coin_id_list = []
|
||||
previous_votes_list = []
|
||||
lockup_innerpuz_list = []
|
||||
if running_sum + coin.amount <= amount:
|
||||
vote_amount = coin.amount
|
||||
running_sum = running_sum + coin.amount
|
||||
primaries = [
|
||||
Payment(
|
||||
new_innerpuzzle.get_tree_hash(),
|
||||
uint64(vote_amount),
|
||||
[standard_inner_puz.get_tree_hash()],
|
||||
)
|
||||
]
|
||||
message = Program.to([proposal_id, vote_amount, is_yes_vote, coin.name()]).get_tree_hash()
|
||||
puzzle_announcements = set([message])
|
||||
inner_solution = self.standard_wallet.make_solution(
|
||||
primaries=primaries, puzzle_announcements=puzzle_announcements
|
||||
)
|
||||
else:
|
||||
vote_amount = amount - running_sum
|
||||
running_sum = running_sum + coin.amount
|
||||
primaries = [
|
||||
Payment(
|
||||
new_innerpuzzle.get_tree_hash(),
|
||||
uint64(vote_amount),
|
||||
[standard_inner_puz.get_tree_hash()],
|
||||
),
|
||||
]
|
||||
if change > 0:
|
||||
primaries.append(
|
||||
Payment(
|
||||
lci.inner_puzzle.get_tree_hash(),
|
||||
uint64(change),
|
||||
[lci.inner_puzzle.get_tree_hash()],
|
||||
)
|
||||
)
|
||||
message = Program.to([proposal_id, vote_amount, is_yes_vote, coin.name()]).get_tree_hash()
|
||||
puzzle_announcements = set([message])
|
||||
inner_solution = self.standard_wallet.make_solution(
|
||||
primaries=primaries, puzzle_announcements=puzzle_announcements
|
||||
)
|
||||
if is_yes_vote:
|
||||
vote_info = 1
|
||||
vote_amounts_list.append(vote_amount)
|
||||
voting_coin_id_list.append(coin.name())
|
||||
previous_votes_list.append(get_active_votes_from_lockup_puzzle(lci.inner_puzzle))
|
||||
lockup_innerpuz_list.append(get_innerpuz_from_lockup_puzzle(lci.inner_puzzle))
|
||||
solution = Program.to(
|
||||
[
|
||||
coin.name(),
|
||||
inner_solution,
|
||||
coin.amount,
|
||||
proposal_id,
|
||||
proposal_puzzle.get_tree_hash(),
|
||||
vote_info,
|
||||
vote_amount,
|
||||
lci.inner_puzzle.get_tree_hash(),
|
||||
0,
|
||||
]
|
||||
)
|
||||
lineage_proof = await self.get_lineage_proof_for_coin(coin)
|
||||
assert lineage_proof is not None
|
||||
new_spendable_cat = SpendableCAT(
|
||||
coin,
|
||||
self.dao_cat_info.limitations_program_hash,
|
||||
lci.inner_puzzle,
|
||||
solution,
|
||||
limitations_solution=limitations_solution,
|
||||
extra_delta=extra_delta,
|
||||
lineage_proof=lineage_proof,
|
||||
limitations_program_reveal=limitations_program_reveal,
|
||||
)
|
||||
spendable_cat_list.append(new_spendable_cat)
|
||||
|
||||
cat_spend_bundle = unsigned_spend_bundle_for_spendable_cats(CAT_MOD, spendable_cat_list)
|
||||
spend_bundle = await self.wallet_state_manager.sign_transaction(cat_spend_bundle.coin_spends)
|
||||
assert isinstance(spend_bundle, SpendBundle)
|
||||
return spend_bundle
|
||||
|
||||
async def enter_dao_cat_voting_mode(
|
||||
self,
|
||||
amount: uint64,
|
||||
tx_config: TXConfig,
|
||||
fee: uint64 = uint64(0),
|
||||
extra_conditions: Tuple[Condition, ...] = tuple(),
|
||||
) -> List[TransactionRecord]:
|
||||
"""
|
||||
Enter existing CATs for the DAO into voting mode
|
||||
"""
|
||||
# check there are enough cats to convert
|
||||
cat_wallet = self.wallet_state_manager.wallets[self.dao_cat_info.free_cat_wallet_id]
|
||||
cat_balance = await cat_wallet.get_spendable_balance()
|
||||
if cat_balance < amount: # pragma: no cover
|
||||
raise ValueError(f"Insufficient CAT balance. Requested: {amount} Available: {cat_balance}")
|
||||
# get the lockup puzzle hash
|
||||
lockup_puzzle = await self.get_new_puzzle()
|
||||
# create the cat spend
|
||||
txs: List[TransactionRecord] = await cat_wallet.generate_signed_transaction(
|
||||
[amount],
|
||||
[lockup_puzzle.get_tree_hash()],
|
||||
tx_config,
|
||||
fee=fee,
|
||||
extra_conditions=extra_conditions,
|
||||
)
|
||||
cat_puzzle_hash: bytes32 = construct_cat_puzzle(
|
||||
CAT_MOD, self.dao_cat_info.limitations_program_hash, lockup_puzzle
|
||||
).get_tree_hash()
|
||||
await self.wallet_state_manager.add_interested_puzzle_hashes([cat_puzzle_hash], [self.id()])
|
||||
return txs
|
||||
|
||||
async def exit_vote_state(
|
||||
self,
|
||||
coins: List[LockedCoinInfo],
|
||||
tx_config: TXConfig,
|
||||
fee: uint64 = uint64(0),
|
||||
extra_conditions: Tuple[Condition, ...] = tuple(),
|
||||
) -> TransactionRecord:
|
||||
extra_delta, limitations_solution = 0, Program.to([])
|
||||
limitations_program_reveal = Program.to([])
|
||||
spendable_cat_list = []
|
||||
total_amt = 0
|
||||
spent_coins = []
|
||||
for lci in coins:
|
||||
coin = lci.coin
|
||||
if tx_config.reuse_puzhash: # pragma: no cover
|
||||
new_inner_puzhash = await self.standard_wallet.get_puzzle_hash(new=False)
|
||||
else:
|
||||
new_inner_puzhash = await self.standard_wallet.get_puzzle_hash(new=True)
|
||||
|
||||
# CREATE_COIN new_puzzle coin.amount
|
||||
primaries = [
|
||||
Payment(
|
||||
new_inner_puzhash,
|
||||
uint64(coin.amount),
|
||||
[new_inner_puzhash],
|
||||
),
|
||||
]
|
||||
total_amt += coin.amount
|
||||
inner_solution = self.standard_wallet.make_solution(
|
||||
primaries=primaries,
|
||||
)
|
||||
# Create the solution using only the values needed for exiting the lockup mode (my_id = 0)
|
||||
solution = Program.to(
|
||||
[
|
||||
0, # my_id
|
||||
inner_solution,
|
||||
coin.amount,
|
||||
0, # new_proposal_vote_id_or_removal_id
|
||||
0, # proposal_innerpuzhash
|
||||
0, # vote_info
|
||||
0, # vote_amount
|
||||
0, # my_inner_puzhash
|
||||
]
|
||||
)
|
||||
lineage_proof = await self.get_lineage_proof_for_coin(coin)
|
||||
assert lineage_proof is not None
|
||||
new_spendable_cat = SpendableCAT(
|
||||
coin,
|
||||
self.dao_cat_info.limitations_program_hash,
|
||||
lci.inner_puzzle,
|
||||
solution,
|
||||
limitations_solution=limitations_solution,
|
||||
extra_delta=extra_delta,
|
||||
lineage_proof=lineage_proof,
|
||||
limitations_program_reveal=limitations_program_reveal,
|
||||
)
|
||||
spendable_cat_list.append(new_spendable_cat)
|
||||
spent_coins.append(coin)
|
||||
|
||||
cat_spend_bundle = unsigned_spend_bundle_for_spendable_cats(CAT_MOD, spendable_cat_list)
|
||||
spend_bundle: SpendBundle = await self.wallet_state_manager.sign_transaction(cat_spend_bundle.coin_spends)
|
||||
|
||||
if fee > 0: # pragma: no cover
|
||||
chia_tx = await self.standard_wallet.create_tandem_xch_tx(
|
||||
fee,
|
||||
tx_config,
|
||||
)
|
||||
assert chia_tx.spend_bundle is not None
|
||||
full_spend = SpendBundle.aggregate([spend_bundle, chia_tx.spend_bundle])
|
||||
else:
|
||||
full_spend = spend_bundle
|
||||
|
||||
record = TransactionRecord(
|
||||
confirmed_at_height=uint32(0),
|
||||
created_at_time=uint64(int(time.time())),
|
||||
to_puzzle_hash=new_inner_puzhash,
|
||||
amount=uint64(total_amt),
|
||||
fee_amount=fee,
|
||||
confirmed=False,
|
||||
sent=uint32(10),
|
||||
spend_bundle=full_spend,
|
||||
additions=full_spend.additions(),
|
||||
removals=full_spend.removals(),
|
||||
wallet_id=self.id(),
|
||||
sent_to=[],
|
||||
trade_id=None,
|
||||
type=uint32(TransactionType.INCOMING_TX.value),
|
||||
name=bytes32(token_bytes()),
|
||||
memos=[],
|
||||
valid_times=parse_timelock_info(extra_conditions),
|
||||
)
|
||||
|
||||
# TODO: Hack to just drop coins from locked list. Need to catch this event in WSM to
|
||||
# check if we're adding CATs from our DAO CAT wallet and update the locked coin list
|
||||
# accordingly
|
||||
new_locked_coins = [x for x in self.dao_cat_info.locked_coins if x.coin not in spent_coins]
|
||||
dao_cat_info: DAOCATInfo = DAOCATInfo(
|
||||
self.dao_cat_info.dao_wallet_id,
|
||||
self.dao_cat_info.free_cat_wallet_id,
|
||||
self.dao_cat_info.limitations_program_hash,
|
||||
self.dao_cat_info.my_tail,
|
||||
new_locked_coins,
|
||||
)
|
||||
await self.save_info(dao_cat_info)
|
||||
return record
|
||||
|
||||
async def remove_active_proposal(
|
||||
self, proposal_id_list: List[bytes32], tx_config: TXConfig, fee: uint64 = uint64(0)
|
||||
) -> SpendBundle:
|
||||
locked_coins: List[Tuple[LockedCoinInfo, List[bytes32]]] = []
|
||||
for lci in self.dao_cat_info.locked_coins:
|
||||
my_finished_proposals = []
|
||||
for active_vote in lci.active_votes:
|
||||
if active_vote in proposal_id_list:
|
||||
my_finished_proposals.append(active_vote)
|
||||
if my_finished_proposals:
|
||||
locked_coins.append((lci, my_finished_proposals))
|
||||
extra_delta, limitations_solution = 0, Program.to([])
|
||||
limitations_program_reveal = Program.to([])
|
||||
spendable_cat_list = []
|
||||
|
||||
for lci_proposals_tuple in locked_coins:
|
||||
proposal_innerpuzhashes = []
|
||||
coin = lci_proposals_tuple[0].coin
|
||||
lci = lci_proposals_tuple[0]
|
||||
proposals = lci_proposals_tuple[1]
|
||||
for proposal_id in proposals:
|
||||
INNERPUZ = get_finished_state_inner_puzzle(proposal_id)
|
||||
proposal_innerpuzhashes.append(INNERPUZ)
|
||||
# new_innerpuzzle = await cat_wallet.get_new_inner_puzzle()
|
||||
# my_id ; if my_id is 0 we do the return to return_address (exit voting mode) spend case
|
||||
# inner_solution
|
||||
# my_amount
|
||||
# new_proposal_vote_id_or_removal_id ; if we're exiting fully, set this to 0
|
||||
# proposal_curry_vals
|
||||
# vote_info
|
||||
# vote_amount
|
||||
# my_puzhash
|
||||
solution = Program.to(
|
||||
[
|
||||
0,
|
||||
0,
|
||||
coin.amount,
|
||||
proposals,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
]
|
||||
)
|
||||
lineage_proof = await self.get_lineage_proof_for_coin(coin)
|
||||
assert lineage_proof is not None
|
||||
new_spendable_cat = SpendableCAT(
|
||||
coin,
|
||||
self.dao_cat_info.limitations_program_hash,
|
||||
lci.inner_puzzle,
|
||||
solution,
|
||||
limitations_solution=limitations_solution,
|
||||
extra_delta=extra_delta,
|
||||
lineage_proof=lineage_proof,
|
||||
limitations_program_reveal=limitations_program_reveal,
|
||||
)
|
||||
spendable_cat_list.append(new_spendable_cat)
|
||||
|
||||
cat_spend_bundle = unsigned_spend_bundle_for_spendable_cats(CAT_MOD, spendable_cat_list)
|
||||
spend_bundle = await self.wallet_state_manager.sign_transaction(cat_spend_bundle.coin_spends)
|
||||
|
||||
if fee > 0: # pragma: no cover
|
||||
chia_tx = await self.standard_wallet.create_tandem_xch_tx(fee, tx_config=tx_config)
|
||||
assert chia_tx.spend_bundle is not None
|
||||
full_spend = SpendBundle.aggregate([spend_bundle, chia_tx.spend_bundle])
|
||||
else:
|
||||
full_spend = spend_bundle
|
||||
|
||||
return full_spend
|
||||
|
||||
def get_asset_id(self) -> str:
|
||||
return bytes(self.dao_cat_info.limitations_program_hash).hex()
|
||||
|
||||
async def get_new_inner_hash(self, tx_config: TXConfig) -> bytes32:
|
||||
puzzle = await self.get_new_inner_puzzle(tx_config)
|
||||
return puzzle.get_tree_hash()
|
||||
|
||||
async def get_new_inner_puzzle(self, tx_config: TXConfig) -> Program:
|
||||
return await self.standard_wallet.get_puzzle(new=not tx_config.reuse_puzhash)
|
||||
|
||||
async def get_new_puzzle(self) -> Program:
|
||||
record = await self.wallet_state_manager.get_unused_derivation_record(self.id())
|
||||
inner_puzzle = self.standard_wallet.puzzle_for_pk(record.pubkey)
|
||||
puzzle = get_lockup_puzzle(
|
||||
self.dao_cat_info.limitations_program_hash,
|
||||
[],
|
||||
inner_puzzle,
|
||||
)
|
||||
cat_puzzle: Program = construct_cat_puzzle(CAT_MOD, self.dao_cat_info.limitations_program_hash, puzzle)
|
||||
await self.wallet_state_manager.add_interested_puzzle_hashes([puzzle.get_tree_hash()], [self.id()])
|
||||
await self.wallet_state_manager.add_interested_puzzle_hashes([cat_puzzle.get_tree_hash()], [self.id()])
|
||||
return puzzle
|
||||
|
||||
async def get_new_puzzlehash(self) -> bytes32:
|
||||
puzzle = await self.get_new_puzzle()
|
||||
return puzzle.get_tree_hash()
|
||||
|
||||
def puzzle_for_pk(self, pubkey: G1Element) -> Program:
|
||||
inner_puzzle = self.standard_wallet.puzzle_for_pk(pubkey)
|
||||
puzzle = get_lockup_puzzle(
|
||||
self.dao_cat_info.limitations_program_hash,
|
||||
[],
|
||||
inner_puzzle,
|
||||
)
|
||||
cat_puzzle: Program = construct_cat_puzzle(CAT_MOD, self.dao_cat_info.limitations_program_hash, puzzle)
|
||||
return cat_puzzle
|
||||
|
||||
def puzzle_hash_for_pk(self, pubkey: G1Element) -> bytes32:
|
||||
puzzle = self.puzzle_for_pk(pubkey)
|
||||
return puzzle.get_tree_hash()
|
||||
|
||||
def require_derivation_paths(self) -> bool:
|
||||
return True
|
||||
|
||||
async def match_hinted_coin(self, coin: Coin, hint: bytes32) -> bool:
|
||||
raise NotImplementedError("Method not implemented for DAO CAT Wallet") # pragma: no cover
|
||||
|
||||
async def get_spendable_balance(self, records: Optional[Set[WalletCoinRecord]] = None) -> uint128:
|
||||
return uint128(0)
|
||||
|
||||
async def get_confirmed_balance(self, record_list: Optional[Set[WalletCoinRecord]] = None) -> uint128:
|
||||
amount = 0
|
||||
for coin in self.dao_cat_info.locked_coins:
|
||||
amount += coin.coin.amount
|
||||
return uint128(amount)
|
||||
|
||||
async def get_unconfirmed_balance(self, unspent_records: Optional[Set[WalletCoinRecord]] = None) -> uint128:
|
||||
return uint128(0)
|
||||
|
||||
async def get_pending_change_balance(self) -> uint64:
|
||||
return uint64(0)
|
||||
|
||||
async def select_coins(
|
||||
self,
|
||||
amount: uint64,
|
||||
coin_selection_config: CoinSelectionConfig,
|
||||
) -> Set[Coin]:
|
||||
return set()
|
||||
|
||||
async def get_max_send_amount(self, unspent_records: Optional[Set[WalletCoinRecord]] = None) -> uint128:
|
||||
return uint128(0)
|
||||
|
||||
async def get_votable_balance(
|
||||
self,
|
||||
proposal_id: Optional[bytes32] = None,
|
||||
include_free_cats: bool = True,
|
||||
) -> uint64:
|
||||
balance = 0
|
||||
for coin in self.dao_cat_info.locked_coins:
|
||||
if proposal_id is not None:
|
||||
compatible = True
|
||||
for active_vote in coin.active_votes:
|
||||
if active_vote == proposal_id:
|
||||
compatible = False
|
||||
break
|
||||
if compatible:
|
||||
balance += coin.coin.amount
|
||||
else:
|
||||
balance += coin.coin.amount
|
||||
if include_free_cats:
|
||||
cat_wallet = self.wallet_state_manager.wallets[self.dao_cat_info.free_cat_wallet_id]
|
||||
cat_balance = await cat_wallet.get_spendable_balance()
|
||||
balance += cat_balance
|
||||
return uint64(balance)
|
||||
|
||||
async def save_info(self, dao_cat_info: DAOCATInfo) -> None:
|
||||
self.dao_cat_info = dao_cat_info
|
||||
current_info = self.wallet_info
|
||||
data_str = bytes(dao_cat_info).hex()
|
||||
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)
|
||||
|
||||
def get_name(self) -> str:
|
||||
return self.wallet_info.name
|
@ -0,0 +1,42 @@
|
||||
; This is a TAIL for use with cat.clvm.
|
||||
;
|
||||
; This checker allows new CATs to be created if they have a particular coin id as parent
|
||||
;
|
||||
; The genesis_id is curried in, making this lineage_check program unique and giving the CAT it's uniqueness
|
||||
(mod (
|
||||
GENESIS_ID
|
||||
MINT_LAUNCHER_PUZZLE_HASH
|
||||
Truths
|
||||
parent_is_cat
|
||||
lineage_proof
|
||||
delta
|
||||
inner_conditions
|
||||
( ; solution
|
||||
parent_parent_id
|
||||
parent_amount
|
||||
)
|
||||
)
|
||||
|
||||
(include cat_truths.clib)
|
||||
(include curry-and-treehash.clib)
|
||||
|
||||
(if delta
|
||||
(x)
|
||||
(if (= (my_parent_cat_truth Truths) GENESIS_ID)
|
||||
()
|
||||
(if
|
||||
(=
|
||||
(my_parent_cat_truth Truths)
|
||||
(sha256
|
||||
parent_parent_id
|
||||
MINT_LAUNCHER_PUZZLE_HASH
|
||||
parent_amount
|
||||
)
|
||||
)
|
||||
()
|
||||
(x)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
)
|
@ -0,0 +1 @@
|
||||
ff02ffff03ff5fffff01ff0880ffff01ff02ffff03ffff09ff5bff0280ff80ffff01ff02ffff03ffff09ff5bffff0bff82027fff05ff82057f8080ff80ffff01ff088080ff018080ff018080ff0180
|
61
chia/wallet/dao_wallet/dao_info.py
Normal file
61
chia/wallet/dao_wallet/dao_info.py
Normal file
@ -0,0 +1,61 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
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 uint32, uint64
|
||||
from chia.util.streamable import Streamable, streamable
|
||||
from chia.wallet.lineage_proof import LineageProof
|
||||
|
||||
|
||||
@streamable
|
||||
@dataclass(frozen=True)
|
||||
class ProposalInfo(Streamable):
|
||||
proposal_id: bytes32 # this is launcher_id
|
||||
inner_puzzle: Program
|
||||
amount_voted: uint64
|
||||
yes_votes: uint64
|
||||
current_coin: Coin
|
||||
current_innerpuz: Optional[Program]
|
||||
timer_coin: Optional[Coin] # if this is None then the proposal has finished
|
||||
singleton_block_height: uint32 # Block height that current proposal singleton coin was created in
|
||||
passed: Optional[bool]
|
||||
closed: Optional[bool]
|
||||
|
||||
|
||||
@streamable
|
||||
@dataclass(frozen=True)
|
||||
class DAOInfo(Streamable):
|
||||
treasury_id: bytes32
|
||||
cat_wallet_id: uint32
|
||||
dao_cat_wallet_id: uint32
|
||||
proposals_list: List[ProposalInfo]
|
||||
parent_info: List[Tuple[bytes32, Optional[LineageProof]]] # {coin.name(): LineageProof}
|
||||
current_treasury_coin: Optional[Coin]
|
||||
current_treasury_innerpuz: Optional[Program]
|
||||
singleton_block_height: uint32 # the block height that the current treasury singleton was created in
|
||||
filter_below_vote_amount: uint64 # we ignore proposals with fewer votes than this - defaults to 1
|
||||
assets: List[Optional[bytes32]]
|
||||
current_height: uint64
|
||||
|
||||
|
||||
@streamable
|
||||
@dataclass(frozen=True)
|
||||
class DAORules(Streamable):
|
||||
proposal_timelock: uint64
|
||||
soft_close_length: uint64
|
||||
attendance_required: uint64
|
||||
pass_percentage: uint64
|
||||
self_destruct_length: uint64
|
||||
oracle_spend_delay: uint64
|
||||
proposal_minimum_amount: uint64
|
||||
|
||||
|
||||
class ProposalType(Enum):
|
||||
SPEND = "s"
|
||||
UPDATE = "u"
|
||||
MINT = "m"
|
809
chia/wallet/dao_wallet/dao_utils.py
Normal file
809
chia/wallet/dao_wallet/dao_utils.py
Normal file
@ -0,0 +1,809 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from itertools import chain
|
||||
from typing import Iterator, List, Optional, Tuple
|
||||
|
||||
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(
|
||||
proposal_timelock.as_int(),
|
||||
soft_close_length.as_int(),
|
||||
attendance_required.as_int(),
|
||||
pass_percentage.as_int(),
|
||||
self_destruct_length.as_int(),
|
||||
oracle_spend_delay.as_int(),
|
||||
proposal_minimum_amount.as_int(),
|
||||
)
|
||||
return dao_rules
|
||||
|
||||
|
||||
def get_spend_p2_singleton_puzzle(
|
||||
treasury_id: bytes32, xch_conditions: Optional[List], asset_conditions: Optional[List[Tuple]] # type: ignore
|
||||
) -> 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: bytes32, previous_votes_list: List[Optional[bytes32]], 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 = uncurry_proposal_validator(proposal_validator)
|
||||
(
|
||||
SINGLETON_STRUCT,
|
||||
PROPOSAL_SELF_HASH,
|
||||
PROPOSAL_MINIMUM_AMOUNT,
|
||||
PAYOUT_PUZHASH,
|
||||
) = curried_args.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=proposal_id.as_atom(),
|
||||
cat_tail_hash=cat_tail_hash.as_atom(),
|
||||
treasury_id=treasury_id.as_atom(),
|
||||
votes_sum=new_yes_votes,
|
||||
total_votes=new_total_votes,
|
||||
proposed_puzzle_hash=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
|
||||
import traceback
|
||||
|
||||
print(f"exception: {traceback.format_exc()}")
|
||||
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:
|
||||
import traceback
|
||||
|
||||
print(f"exception: {traceback.format_exc()}")
|
||||
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
|
||||
import traceback
|
||||
|
||||
print(f"exception: {traceback.format_exc()}")
|
||||
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):
|
||||
import traceback
|
||||
|
||||
print(f"exception: {traceback.format_exc()}")
|
||||
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:
|
||||
import traceback
|
||||
|
||||
print(f"exception: {traceback.format_exc()}")
|
||||
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 = []
|
||||
cat_conds = []
|
||||
for recipient_puzhash, amount, asset_type in zip(recipient_puzhashes, amounts, asset_types):
|
||||
if asset_type:
|
||||
cat_conds.append([asset_type, [[51, recipient_puzhash, amount]]])
|
||||
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), [])
|
||||
return puzzle
|
2155
chia/wallet/dao_wallet/dao_wallet.py
Normal file
2155
chia/wallet/dao_wallet/dao_wallet.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -235,7 +235,7 @@ class DIDWallet:
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
json.dumps(did_wallet_puzzles.program_to_metadata(metadata)),
|
||||
json.dumps(did_wallet_puzzles.did_program_to_metadata(metadata)),
|
||||
)
|
||||
self.check_existed_did()
|
||||
info_as_string = json.dumps(self.did_info.to_json_dict())
|
||||
@ -404,7 +404,7 @@ class DIDWallet:
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
json.dumps(did_wallet_puzzles.program_to_metadata(did_data.metadata)),
|
||||
json.dumps(did_wallet_puzzles.did_program_to_metadata(did_data.metadata)),
|
||||
)
|
||||
await self.save_info(new_info)
|
||||
|
||||
|
@ -191,7 +191,7 @@ def metadata_to_program(metadata: Dict) -> Program:
|
||||
return Program.to(kv_list)
|
||||
|
||||
|
||||
def program_to_metadata(program: Program) -> Dict:
|
||||
def did_program_to_metadata(program: Program) -> Dict:
|
||||
"""
|
||||
Convert a program to a metadata dict
|
||||
:param program: Chialisp program contains the metadata
|
||||
|
@ -155,7 +155,7 @@ def metadata_to_program(metadata: Dict[bytes, Any]) -> Program:
|
||||
return program
|
||||
|
||||
|
||||
def program_to_metadata(program: Program) -> Dict[bytes, Any]:
|
||||
def nft_program_to_metadata(program: Program) -> Dict[bytes, Any]:
|
||||
"""
|
||||
Convert a program to a metadata dict
|
||||
:param program: Chialisp program contains the metadata
|
||||
@ -190,7 +190,7 @@ def update_metadata(metadata: Program, update_condition: Program) -> Program:
|
||||
:param update_condition: Update metadata conditions
|
||||
:return: Updated metadata
|
||||
"""
|
||||
new_metadata: Dict[bytes, Any] = program_to_metadata(metadata)
|
||||
new_metadata: Dict[bytes, Any] = nft_program_to_metadata(metadata)
|
||||
uri: Program = update_condition.rest().rest().first()
|
||||
prepend_value(uri.first().as_python(), uri.rest(), new_metadata)
|
||||
return metadata_to_program(new_metadata)
|
||||
|
16
chia/wallet/puzzles/curry_by_index.clib
Normal file
16
chia/wallet/puzzles/curry_by_index.clib
Normal file
@ -0,0 +1,16 @@
|
||||
(
|
||||
(defun recurry_by_index_ordered (
|
||||
index_value_pairs_list ; MUST BE ORDERED
|
||||
current_position ; must be 0 on initial call
|
||||
CURRENT_PARAMS ; current list of curry params
|
||||
)
|
||||
(if index_value_pairs_list
|
||||
(if (= (f (f index_value_pairs_list)) current_position)
|
||||
(c (r (f index_value_pairs_list)) (recurry_by_index_ordered (r index_value_pairs_list) (+ current_position 1) (r CURRENT_PARAMS)))
|
||||
(c (f CURRENT_PARAMS) (recurry_by_index_ordered index_value_pairs_list (+ current_position 1) (r CURRENT_PARAMS)))
|
||||
)
|
||||
()
|
||||
)
|
||||
)
|
||||
|
||||
)
|
17
chia/wallet/puzzles/dao_cat_eve.clsp
Normal file
17
chia/wallet/puzzles/dao_cat_eve.clsp
Normal file
@ -0,0 +1,17 @@
|
||||
; This file is what the first form the CAT takes, and then it gets immediately eve spent out of here.
|
||||
; This allows the coin to move into its real state already having been eve spent and validated to not be a fake CAT.
|
||||
; The trick is that we won't know what the real state puzzlehash reveal is, but we will know what this is.
|
||||
; Mint into this, eve spend out of this
|
||||
(mod (
|
||||
NEW_PUZZLE_HASH ; this is the CAT inner_puzzle
|
||||
my_amount
|
||||
tail_reveal
|
||||
tail_solution
|
||||
)
|
||||
(include condition_codes.clib)
|
||||
(list
|
||||
(list CREATE_COIN NEW_PUZZLE_HASH my_amount (list NEW_PUZZLE_HASH))
|
||||
(list ASSERT_MY_AMOUNT my_amount)
|
||||
(list CREATE_COIN 0 -113 tail_reveal tail_solution) ; this is secure because anything but the real values won't work
|
||||
)
|
||||
)
|
1
chia/wallet/puzzles/dao_cat_eve.clsp.hex
Normal file
1
chia/wallet/puzzles/dao_cat_eve.clsp.hex
Normal file
@ -0,0 +1 @@
|
||||
ff02ffff01ff04ffff04ff06ffff04ff05ffff04ff0bffff04ffff04ff05ff8080ff8080808080ffff04ffff04ff04ffff04ff0bff808080ffff04ffff04ff06ffff04ff80ffff04ffff01818fffff04ff17ffff04ff2fff808080808080ff80808080ffff04ffff01ff4933ff018080
|
36
chia/wallet/puzzles/dao_cat_launcher.clsp
Normal file
36
chia/wallet/puzzles/dao_cat_launcher.clsp
Normal file
@ -0,0 +1,36 @@
|
||||
(mod (
|
||||
TREASURY_SINGLETON_STRUCT
|
||||
treasury_inner_puz_hash
|
||||
parent_parent
|
||||
new_puzzle_hash ; the full CAT puzzle
|
||||
amount
|
||||
)
|
||||
(include condition_codes.clib)
|
||||
(include curry-and-treehash.clib)
|
||||
|
||||
(defun calculate_singleton_puzzle_hash (PROPOSAL_SINGLETON_STRUCT inner_puzzle_hash)
|
||||
(puzzle-hash-of-curried-function (f PROPOSAL_SINGLETON_STRUCT)
|
||||
inner_puzzle_hash
|
||||
(sha256tree PROPOSAL_SINGLETON_STRUCT)
|
||||
)
|
||||
)
|
||||
|
||||
(defun create_parent_conditions (parent_id new_puzzle_hash amount)
|
||||
(list
|
||||
(list ASSERT_COIN_ANNOUNCEMENT (sha256 parent_id (sha256tree (list 'm' new_puzzle_hash))))
|
||||
(list ASSERT_MY_PARENT_ID parent_id)
|
||||
)
|
||||
)
|
||||
|
||||
(c
|
||||
(list CREATE_COIN new_puzzle_hash amount (list new_puzzle_hash))
|
||||
(c
|
||||
(list ASSERT_MY_AMOUNT amount)
|
||||
(create_parent_conditions
|
||||
(sha256 parent_parent (calculate_singleton_puzzle_hash TREASURY_SINGLETON_STRUCT treasury_inner_puz_hash) ONE)
|
||||
new_puzzle_hash
|
||||
amount
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
1
chia/wallet/puzzles/dao_cat_launcher.clsp.hex
Normal file
1
chia/wallet/puzzles/dao_cat_launcher.clsp.hex
Normal file
@ -0,0 +1 @@
|
||||
ff02ffff01ff04ffff04ff34ffff04ff2fffff04ff5fffff04ffff04ff2fff8080ff8080808080ffff04ffff04ff28ffff04ff5fff808080ffff02ff36ffff04ff02ffff04ffff0bff17ffff02ff26ffff04ff02ffff04ff05ffff04ff0bff8080808080ff3c80ffff04ff2fffff04ff5fff8080808080808080ffff04ffff01ffffff3dff4947ffff0233ff0401ffff01ff02ff02ffff03ff05ffff01ff02ff3affff04ff02ffff04ff0dffff04ffff0bff2affff0bff3cff2c80ffff0bff2affff0bff2affff0bff3cff1280ff0980ffff0bff2aff0bffff0bff3cff8080808080ff8080808080ffff010b80ff0180ffffff02ff2effff04ff02ffff04ff09ffff04ff0bffff04ffff02ff3effff04ff02ffff04ff05ff80808080ff808080808080ff04ffff04ff10ffff04ffff0bff05ffff02ff3effff04ff02ffff04ffff04ffff016dffff04ff0bff808080ff8080808080ff808080ffff04ffff04ff38ffff04ff05ff808080ff808080ffff0bff2affff0bff3cff2480ffff0bff2affff0bff2affff0bff3cff1280ff0580ffff0bff2affff02ff3affff04ff02ffff04ff07ffff04ffff0bff3cff3c80ff8080808080ffff0bff3cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080
|
35
chia/wallet/puzzles/dao_finished_state.clsp
Normal file
35
chia/wallet/puzzles/dao_finished_state.clsp
Normal file
@ -0,0 +1,35 @@
|
||||
; This code is the end state of a proposal or a dividend.
|
||||
; It is an oracle which simply recreates itself and emits an announcement that it has concluded operation
|
||||
|
||||
(mod (SINGLETON_STRUCT DAO_FINISHED_STATE_MOD_HASH my_amount)
|
||||
(include condition_codes.clib)
|
||||
(include curry-and-treehash.clib)
|
||||
(include *standard-cl-21*)
|
||||
|
||||
(defun wrap_in_singleton (SINGLETON_STRUCT my_inner_puzhash)
|
||||
(puzzle-hash-of-curried-function (f SINGLETON_STRUCT)
|
||||
my_inner_puzhash
|
||||
(sha256tree SINGLETON_STRUCT)
|
||||
)
|
||||
)
|
||||
|
||||
(defun recreate_self (SINGLETON_STRUCT DAO_FINISHED_STATE_MOD_HASH)
|
||||
(puzzle-hash-of-curried-function DAO_FINISHED_STATE_MOD_HASH
|
||||
(sha256 ONE DAO_FINISHED_STATE_MOD_HASH)
|
||||
(sha256tree SINGLETON_STRUCT)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
(let
|
||||
(
|
||||
(my_inner_puzhash (recreate_self SINGLETON_STRUCT DAO_FINISHED_STATE_MOD_HASH))
|
||||
)
|
||||
(list
|
||||
(list ASSERT_MY_PUZZLEHASH (wrap_in_singleton SINGLETON_STRUCT my_inner_puzhash))
|
||||
(list ASSERT_MY_AMOUNT my_amount)
|
||||
(list CREATE_COIN my_inner_puzhash my_amount)
|
||||
(list CREATE_PUZZLE_ANNOUNCEMENT 0)
|
||||
)
|
||||
)
|
||||
)
|
1
chia/wallet/puzzles/dao_finished_state.clsp.hex
Normal file
1
chia/wallet/puzzles/dao_finished_state.clsp.hex
Normal file
@ -0,0 +1 @@
|
||||
ff02ffff01ff04ffff04ffff0148ffff04ffff02ff16ffff04ff02ffff04ffff05ffff06ff018080ffff04ffff02ff1effff04ff02ffff04ff05ffff04ff0bff8080808080ff8080808080ffff01808080ffff04ffff04ffff0149ffff04ffff05ffff06ffff06ffff06ff0180808080ffff01808080ffff04ffff04ffff0133ffff04ffff02ff1effff04ff02ffff04ff05ffff04ff0bff8080808080ffff04ffff05ffff06ffff06ffff06ff0180808080ffff0180808080ffff04ffff04ffff013effff04ffff0180ffff01808080ffff018080808080ffff04ffff01ffffff02ffff03ff05ffff01ff02ffff01ff02ff08ffff04ff02ffff04ffff06ff0580ffff04ffff0bffff0102ffff0bffff0101ffff010480ffff0bffff0102ffff0bffff0102ffff0bffff0101ffff010180ffff05ff058080ffff0bffff0102ff0bffff0bffff0101ffff018080808080ff8080808080ff0180ffff01ff02ffff010bff018080ff0180ff0bffff0102ffff01a0a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ffff02ff08ffff04ff02ffff04ff07ffff01ffa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b280808080ffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a808080ffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff0affff04ff02ffff04ffff05ff0580ff80808080ffff02ff0affff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ffff02ff0cffff04ff02ffff04ff09ffff04ff0bffff04ffff02ff0affff04ff02ffff04ff05ff80808080ff808080808080ff02ff0cffff04ff02ffff04ff0bffff04ffff0bffff0101ff0b80ffff04ffff02ff0affff04ff02ffff04ff05ff80808080ff808080808080ff018080
|
@ -0,0 +1 @@
|
||||
7f3cc356732907933a8f9b1ccf16f71735d07340eb38c847aa402e97d75eb40b
|
288
chia/wallet/puzzles/dao_lockup.clsp
Normal file
288
chia/wallet/puzzles/dao_lockup.clsp
Normal file
@ -0,0 +1,288 @@
|
||||
; This code is the "voting mode" for a DAO CAT.
|
||||
; The coin can be spent from this state to vote on a proposal or claim a dividend.
|
||||
; It locks the CAT in while it has active votes/dividends going on.
|
||||
; Once a vote or dividend closes, then the coin can spend itself to remove that coin from the "active list"
|
||||
; If the "active list" is empty the coin can leave the voting mode
|
||||
|
||||
(mod (
|
||||
; this is the first curry
|
||||
SINGLETON_MOD_HASH
|
||||
SINGLETON_LAUNCHER_PUZHASH
|
||||
DAO_FINISHED_STATE_MOD_HASH
|
||||
CAT_MOD_HASH
|
||||
CAT_TAIL_HASH
|
||||
; this is the second curry
|
||||
SELF_HASH ; this is the self_hash Optimization
|
||||
ACTIVE_VOTES ; "active votes" list
|
||||
INNERPUZ
|
||||
; this is the solution
|
||||
my_id ; if my_id is 0 we do the return to return_address (exit voting mode) spend case
|
||||
inner_solution
|
||||
my_amount
|
||||
new_proposal_vote_id_or_removal_id ; removal_id is a list of removal_ids
|
||||
proposal_innerpuzhash ; list of singleton innerpuzhashes which should match the order of the new_proposal_vote_id list
|
||||
vote_info
|
||||
vote_amount
|
||||
my_inner_puzhash
|
||||
new_innerpuzhash ; only include this if we're changing owners - secured because coin is still made from inner_puz
|
||||
)
|
||||
(include condition_codes.clib)
|
||||
(include curry-and-treehash.clib)
|
||||
(include *standard-cl-21*)
|
||||
|
||||
(defun calculate_finished_state (singleton_struct dao_finished_state)
|
||||
(puzzle-hash-of-curried-function dao_finished_state
|
||||
(sha256 ONE dao_finished_state)
|
||||
(sha256tree singleton_struct)
|
||||
)
|
||||
)
|
||||
|
||||
; take two lists and merge them into one
|
||||
(defun merge_list (list_a list_b)
|
||||
(if list_a
|
||||
(c (f list_a) (merge_list (r list_a) list_b))
|
||||
list_b
|
||||
)
|
||||
)
|
||||
|
||||
(defun wrap_in_cat_layer (CAT_MOD_HASH CAT_TAIL_HASH INNERPUZHASH)
|
||||
(puzzle-hash-of-curried-function CAT_MOD_HASH
|
||||
INNERPUZHASH
|
||||
(sha256 ONE CAT_TAIL_HASH)
|
||||
(sha256 ONE CAT_MOD_HASH)
|
||||
)
|
||||
)
|
||||
|
||||
; loop through conditions and check that they aren't trying to create anything they shouldn't
|
||||
(defun check_conditions (conditions vote_added_puzhash my_amount message vote_amount my_inner_puzhash seen_vote seen_change)
|
||||
(if conditions
|
||||
(if (= (f (f conditions)) CREATE_COIN) ; this guarantees that the new coin is obeying the rules - other coins are banned to avoid re-voting
|
||||
(if (= (f (r (f conditions))) vote_added_puzhash)
|
||||
(if seen_vote ; assert we haven't already made a coin with the new vote included
|
||||
(x)
|
||||
(if (= (f (r (r (f conditions)))) my_amount) ; we vote with all our value
|
||||
(if seen_change ; assert that we haven't already recreated ourself in some fashion
|
||||
(x)
|
||||
(c (f conditions) (check_conditions (r conditions) vote_added_puzhash my_amount message vote_amount my_inner_puzhash 1 1))
|
||||
)
|
||||
(if (= (f (r (r (f conditions)))) vote_amount) ; we vote with part of our power
|
||||
(c (f conditions) (check_conditions (r conditions) vote_added_puzhash my_amount message vote_amount my_inner_puzhash 1 seen_change))
|
||||
(x)
|
||||
)
|
||||
)
|
||||
)
|
||||
(if (all
|
||||
(= (f (r (f conditions))) my_inner_puzhash)
|
||||
(not seen_change)
|
||||
(= (f (r (r (f conditions)))) (- my_amount vote_amount))
|
||||
) ; we recreate ourselves with unused voting power
|
||||
(c (f conditions) (check_conditions (r conditions) vote_added_puzhash my_amount message vote_amount my_inner_puzhash seen_vote 1))
|
||||
(x)
|
||||
)
|
||||
)
|
||||
(if (= (f (f conditions)) CREATE_PUZZLE_ANNOUNCEMENT) ; this secures the values used to generate message - other messages are banned in case of LIES
|
||||
(if (= (f (r (f conditions))) message)
|
||||
(c (f conditions) (check_conditions (r conditions) vote_added_puzhash my_amount message vote_amount my_inner_puzhash seen_vote seen_change))
|
||||
(x)
|
||||
)
|
||||
(c (f conditions) (check_conditions (r conditions) vote_added_puzhash my_amount message vote_amount my_inner_puzhash seen_vote seen_change))
|
||||
)
|
||||
)
|
||||
(if (all seen_vote seen_change) ; check all value is accounted for
|
||||
()
|
||||
(x)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
; go through our list of active votes and check that we aren't revoting
|
||||
(defun check_not_previously_voted (
|
||||
SINGLETON_MOD_HASH
|
||||
SINGLETON_LAUNCHER_PUZHASH
|
||||
INNERPUZ
|
||||
my_id
|
||||
new_vote_id
|
||||
active_votes
|
||||
proposal_innerpuzhash
|
||||
)
|
||||
(if active_votes
|
||||
(if (= new_vote_id (f active_votes)) ; check new vote id is not equal to an existent vote id
|
||||
(x)
|
||||
(check_not_previously_voted
|
||||
SINGLETON_MOD_HASH
|
||||
SINGLETON_LAUNCHER_PUZHASH
|
||||
INNERPUZ
|
||||
my_id
|
||||
new_vote_id
|
||||
(r active_votes)
|
||||
proposal_innerpuzhash
|
||||
)
|
||||
)
|
||||
(list ASSERT_PUZZLE_ANNOUNCEMENT
|
||||
(sha256
|
||||
(calculate_singleton_puzzle_hash
|
||||
(c SINGLETON_MOD_HASH (c new_vote_id SINGLETON_LAUNCHER_PUZHASH))
|
||||
proposal_innerpuzhash
|
||||
)
|
||||
my_id
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
(defun calculate_singleton_puzzle_hash (PROPOSAL_SINGLETON_STRUCT inner_puzzle_hash)
|
||||
(puzzle-hash-of-curried-function (f PROPOSAL_SINGLETON_STRUCT)
|
||||
inner_puzzle_hash
|
||||
(sha256tree PROPOSAL_SINGLETON_STRUCT)
|
||||
)
|
||||
)
|
||||
|
||||
(defun calculate_lockup_puzzlehash (
|
||||
SELF_HASH
|
||||
active_votes
|
||||
innerpuzhash
|
||||
)
|
||||
(puzzle-hash-of-curried-function SELF_HASH
|
||||
innerpuzhash
|
||||
(sha256tree active_votes)
|
||||
(sha256 ONE SELF_HASH)
|
||||
)
|
||||
)
|
||||
|
||||
(defun for_every_removal_id (
|
||||
SINGLETON_MOD_HASH
|
||||
SINGLETON_LAUNCHER_PUZHASH
|
||||
SELF_HASH
|
||||
DAO_FINISHED_STATE_MOD_HASH
|
||||
CAT_MOD_HASH
|
||||
CAT_TAIL_HASH
|
||||
ACTIVE_VOTES
|
||||
INNERPUZ
|
||||
removal_ids
|
||||
my_amount
|
||||
unused_votes
|
||||
)
|
||||
(if removal_ids
|
||||
(c
|
||||
(list
|
||||
ASSERT_PUZZLE_ANNOUNCEMENT ; check proposal is actually finished
|
||||
(sha256
|
||||
(calculate_singleton_puzzle_hash
|
||||
(c SINGLETON_MOD_HASH (c (f removal_ids) SINGLETON_LAUNCHER_PUZHASH))
|
||||
(calculate_finished_state
|
||||
(c SINGLETON_MOD_HASH (c (f removal_ids) SINGLETON_LAUNCHER_PUZHASH))
|
||||
DAO_FINISHED_STATE_MOD_HASH
|
||||
)
|
||||
)
|
||||
0
|
||||
)
|
||||
)
|
||||
(for_every_removal_id
|
||||
SINGLETON_MOD_HASH
|
||||
SINGLETON_LAUNCHER_PUZHASH
|
||||
SELF_HASH
|
||||
DAO_FINISHED_STATE_MOD_HASH
|
||||
CAT_MOD_HASH
|
||||
CAT_TAIL_HASH
|
||||
ACTIVE_VOTES
|
||||
INNERPUZ
|
||||
(r removal_ids)
|
||||
my_amount
|
||||
(c (f removal_ids) unused_votes)
|
||||
)
|
||||
)
|
||||
(list
|
||||
(list ASSERT_MY_AMOUNT my_amount) ; assert that we aren't lying about our amount to free up money and re-vote
|
||||
(list
|
||||
CREATE_COIN ; recreate self with the finished proposal ID removed
|
||||
(calculate_lockup_puzzlehash
|
||||
SELF_HASH
|
||||
(remove_list_one_entries_from_list_two unused_votes ACTIVE_VOTES)
|
||||
(sha256tree INNERPUZ)
|
||||
)
|
||||
my_amount
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
(defun remove_list_one_entries_from_list_two (list_one list_two)
|
||||
(if list_one
|
||||
(remove_item_from_list (f list_one) (remove_list_one_entries_from_list_two (r list_one) list_two))
|
||||
list_two
|
||||
)
|
||||
)
|
||||
|
||||
(defun remove_item_from_list (item list_one)
|
||||
(if list_one
|
||||
(if (= (f list_one) item)
|
||||
(r list_one) ; assuming there are no duplicates
|
||||
(c (f list_one) (remove_item_from_list item (r list_one)))
|
||||
)
|
||||
() ; item was never in list_one, return list_two entirely
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
; main
|
||||
(if my_id
|
||||
(c (list ASSERT_MY_PUZZLEHASH (wrap_in_cat_layer CAT_MOD_HASH CAT_TAIL_HASH my_inner_puzhash))
|
||||
(c
|
||||
(list ASSERT_MY_AMOUNT my_amount)
|
||||
(c
|
||||
(list ASSERT_MY_COIN_ID my_id)
|
||||
(c
|
||||
(if new_proposal_vote_id_or_removal_id
|
||||
(check_not_previously_voted ; this returns a single condition asserting announcement from vote singleton
|
||||
SINGLETON_MOD_HASH
|
||||
SINGLETON_LAUNCHER_PUZHASH
|
||||
INNERPUZ
|
||||
my_id
|
||||
new_proposal_vote_id_or_removal_id
|
||||
ACTIVE_VOTES
|
||||
proposal_innerpuzhash
|
||||
)
|
||||
(list REMARK)
|
||||
)
|
||||
|
||||
; loop over conditions and check that we aren't trying to leave voting state
|
||||
(check_conditions
|
||||
(a INNERPUZ inner_solution)
|
||||
(calculate_lockup_puzzlehash ; compare created coin to our own calculation on what the next puzzle should be
|
||||
SELF_HASH
|
||||
(if new_proposal_vote_id_or_removal_id (c new_proposal_vote_id_or_removal_id ACTIVE_VOTES) ACTIVE_VOTES)
|
||||
(if new_innerpuzhash new_innerpuzhash (sha256tree INNERPUZ))
|
||||
)
|
||||
my_amount
|
||||
; TODO: add namespace to this announcement to allow announcements from the innerpuz
|
||||
(sha256tree (list new_proposal_vote_id_or_removal_id vote_amount vote_info my_id))
|
||||
vote_amount
|
||||
my_inner_puzhash
|
||||
0
|
||||
0
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
; return to return_address or remove something from active list - check if our locked list is empty
|
||||
(if ACTIVE_VOTES
|
||||
(for_every_removal_id ; locked list is not empty, so we must be trying to remove something from it
|
||||
SINGLETON_MOD_HASH
|
||||
SINGLETON_LAUNCHER_PUZHASH
|
||||
SELF_HASH
|
||||
DAO_FINISHED_STATE_MOD_HASH
|
||||
CAT_MOD_HASH
|
||||
CAT_TAIL_HASH
|
||||
ACTIVE_VOTES
|
||||
INNERPUZ
|
||||
new_proposal_vote_id_or_removal_id
|
||||
my_amount
|
||||
()
|
||||
)
|
||||
(a INNERPUZ inner_solution)
|
||||
)
|
||||
)
|
||||
)
|
1
chia/wallet/puzzles/dao_lockup.clsp.hex
Normal file
1
chia/wallet/puzzles/dao_lockup.clsp.hex
Normal file
File diff suppressed because one or more lines are too long
1
chia/wallet/puzzles/dao_lockup.clsp.hex.sha256tree
Normal file
1
chia/wallet/puzzles/dao_lockup.clsp.hex.sha256tree
Normal file
@ -0,0 +1 @@
|
||||
9fa63e652e131f89a9f8bb6f7abb5ffc6ac485a78dcfb8710cd9df5c368774d9
|
374
chia/wallet/puzzles/dao_proposal.clsp
Normal file
374
chia/wallet/puzzles/dao_proposal.clsp
Normal file
@ -0,0 +1,374 @@
|
||||
(mod (
|
||||
; first hash
|
||||
PROPOSAL_TIMER_MOD_HASH ; proposal timer needs to know which proposal created it
|
||||
SINGLETON_MOD_HASH
|
||||
LAUNCHER_PUZZLE_HASH
|
||||
CAT_MOD_HASH
|
||||
DAO_FINISHED_STATE_MOD_HASH
|
||||
TREASURY_MOD_HASH
|
||||
LOCKUP_SELF_HASH
|
||||
CAT_TAIL_HASH
|
||||
TREASURY_ID
|
||||
; second hash
|
||||
SELF_HASH
|
||||
SINGLETON_ID
|
||||
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
|
||||
; solution
|
||||
vote_amounts_or_proposal_validator_hash ; The qty of "votes" to add or subtract. ALWAYS POSITIVE.
|
||||
vote_info ; vote_info is whether we are voting YES or NO. XXX rename vote_type?
|
||||
vote_coin_ids_or_proposal_timelock_length ; this is either the coin ID we're taking a vote from
|
||||
previous_votes_or_pass_margin ; this is the active votes of the lockup we're communicating with
|
||||
; OR this is what percentage of the total votes must be YES - represented as an integer from 0 to 10,000 - typically this is set at 5100 (51%)
|
||||
lockup_innerpuzhashes_or_attendance_required ; this is either the innerpuz of the locked up CAT we're taking a vote from OR
|
||||
; the attendance required - the percentage of the current issuance which must have voted represented as 0 to 10,000 - this is announced by the treasury
|
||||
innerpuz_reveal ; this is only added during the first vote
|
||||
soft_close_length ; revealed by the treasury - 0 in add vote case
|
||||
self_destruct_time ; revealed by the treasury
|
||||
oracle_spend_delay ; used to recreate the treasury
|
||||
self_destruct_flag ; if not 0, do the self-destruct spend
|
||||
my_amount
|
||||
)
|
||||
(include condition_codes.clib)
|
||||
(include utility_macros.clib)
|
||||
(include curry-and-treehash.clib)
|
||||
(include *standard-cl-21*)
|
||||
|
||||
(defconstant TEN_THOUSAND 10000)
|
||||
|
||||
(defun is_member (e L)
|
||||
(if L
|
||||
(if (= e (f L))
|
||||
1
|
||||
(is_member e (r L))
|
||||
)
|
||||
0
|
||||
)
|
||||
)
|
||||
|
||||
(defun-inline calculate_win_percentage (TOTAL PERCENTAGE)
|
||||
(f (divmod (* TOTAL PERCENTAGE) TEN_THOUSAND))
|
||||
)
|
||||
|
||||
(defun calculate_finished_state (singleton_struct DAO_FINISHED_STATE_MOD_HASH)
|
||||
(puzzle-hash-of-curried-function DAO_FINISHED_STATE_MOD_HASH
|
||||
(sha256 ONE DAO_FINISHED_STATE_MOD_HASH)
|
||||
(sha256tree singleton_struct)
|
||||
)
|
||||
)
|
||||
|
||||
(defun calculate_timer_puzhash (
|
||||
PROPOSAL_TIMER_MOD_HASH
|
||||
SELF_HASH
|
||||
MY_SINGLETON_STRUCT
|
||||
)
|
||||
(puzzle-hash-of-curried-function PROPOSAL_TIMER_MOD_HASH
|
||||
(sha256tree MY_SINGLETON_STRUCT)
|
||||
(sha256 ONE SELF_HASH)
|
||||
)
|
||||
)
|
||||
|
||||
(defun calculate_lockup_puzzlehash (
|
||||
LOCKUP_SELF_HASH
|
||||
previous_votes
|
||||
lockup_innerpuzhash
|
||||
)
|
||||
(puzzle-hash-of-curried-function LOCKUP_SELF_HASH
|
||||
lockup_innerpuzhash
|
||||
(sha256tree previous_votes)
|
||||
(sha256 ONE LOCKUP_SELF_HASH)
|
||||
)
|
||||
)
|
||||
|
||||
(defun recreate_self (
|
||||
SELF_HASH
|
||||
PROPOSAL_ID
|
||||
PROPOSED_PUZ_HASH
|
||||
YES_VOTES
|
||||
TOTAL_VOTES
|
||||
)
|
||||
(puzzle-hash-of-curried-function SELF_HASH
|
||||
(sha256 ONE TOTAL_VOTES)
|
||||
(sha256 ONE YES_VOTES)
|
||||
(sha256 ONE PROPOSED_PUZ_HASH)
|
||||
(sha256 ONE PROPOSAL_ID)
|
||||
(sha256 ONE SELF_HASH)
|
||||
)
|
||||
)
|
||||
|
||||
(defun wrap_in_cat_layer (CAT_MOD_HASH CAT_TAIL_HASH INNERPUZHASH)
|
||||
(puzzle-hash-of-curried-function CAT_MOD_HASH
|
||||
INNERPUZHASH
|
||||
(sha256 ONE CAT_TAIL_HASH)
|
||||
(sha256 ONE CAT_MOD_HASH)
|
||||
)
|
||||
)
|
||||
|
||||
(defun calculate_singleton_puzzle_hash (PROPOSAL_SINGLETON_STRUCT inner_puzzle_hash)
|
||||
(puzzle-hash-of-curried-function (f PROPOSAL_SINGLETON_STRUCT)
|
||||
inner_puzzle_hash
|
||||
(sha256tree PROPOSAL_SINGLETON_STRUCT)
|
||||
)
|
||||
)
|
||||
|
||||
(defun calculate_treasury_puzzlehash (
|
||||
treasury_singleton_struct
|
||||
TREASURY_MOD_HASH
|
||||
PROPOSAL_VALIDATOR_HASH
|
||||
PROPOSAL_LENGTH
|
||||
PROPOSAL_SOFTCLOSE_LENGTH
|
||||
attendance_required
|
||||
pass_percentage
|
||||
self_destruct_time
|
||||
oracle_spend_delay
|
||||
)
|
||||
|
||||
(calculate_singleton_puzzle_hash treasury_singleton_struct
|
||||
(puzzle-hash-of-curried-function TREASURY_MOD_HASH
|
||||
(sha256 ONE oracle_spend_delay)
|
||||
(sha256 ONE self_destruct_time)
|
||||
(sha256 ONE pass_percentage)
|
||||
(sha256 ONE attendance_required)
|
||||
(sha256 ONE PROPOSAL_SOFTCLOSE_LENGTH)
|
||||
(sha256 ONE PROPOSAL_LENGTH)
|
||||
PROPOSAL_VALIDATOR_HASH
|
||||
(sha256 ONE TREASURY_MOD_HASH)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
(defun loop_over_vote_coins (
|
||||
SINGLETON_ID
|
||||
LOCKUP_SELF_HASH
|
||||
CAT_MOD_HASH
|
||||
CAT_TAIL_HASH
|
||||
TREASURY_ID
|
||||
SELF_HASH
|
||||
YES_VOTES
|
||||
TOTAL_VOTES
|
||||
PROPOSED_PUZ_HASH
|
||||
coin_id_list
|
||||
vote_amount_list
|
||||
previous_votes
|
||||
lockup_innerpuzhashes
|
||||
vote_info
|
||||
sum
|
||||
output
|
||||
my_amount
|
||||
distinct_ids
|
||||
)
|
||||
(if coin_id_list
|
||||
(if (> (f vote_amount_list) 0)
|
||||
(c
|
||||
(list CREATE_PUZZLE_ANNOUNCEMENT (f coin_id_list))
|
||||
(c
|
||||
(list
|
||||
ASSERT_PUZZLE_ANNOUNCEMENT ; take the vote
|
||||
(sha256
|
||||
(wrap_in_cat_layer
|
||||
CAT_MOD_HASH
|
||||
CAT_TAIL_HASH
|
||||
(calculate_lockup_puzzlehash ; because the message comes from
|
||||
LOCKUP_SELF_HASH
|
||||
(f previous_votes)
|
||||
(f lockup_innerpuzhashes)
|
||||
)
|
||||
)
|
||||
(sha256tree (list SINGLETON_ID (f vote_amount_list) vote_info (f coin_id_list)))
|
||||
)
|
||||
)
|
||||
(loop_over_vote_coins
|
||||
SINGLETON_ID
|
||||
LOCKUP_SELF_HASH
|
||||
CAT_MOD_HASH
|
||||
CAT_TAIL_HASH
|
||||
TREASURY_ID
|
||||
SELF_HASH
|
||||
YES_VOTES
|
||||
TOTAL_VOTES
|
||||
PROPOSED_PUZ_HASH
|
||||
(r coin_id_list)
|
||||
(r vote_amount_list)
|
||||
(r previous_votes)
|
||||
(r lockup_innerpuzhashes)
|
||||
vote_info
|
||||
(+ (f vote_amount_list) sum)
|
||||
output
|
||||
my_amount
|
||||
(if (is_member (f coin_id_list) distinct_ids) (x) (c (f coin_id_list) distinct_ids))
|
||||
)
|
||||
)
|
||||
)
|
||||
(x)
|
||||
)
|
||||
(c
|
||||
(list
|
||||
CREATE_COIN ; recreate self with vote information added
|
||||
(recreate_self
|
||||
SELF_HASH
|
||||
SINGLETON_ID
|
||||
PROPOSED_PUZ_HASH
|
||||
(if vote_info (+ YES_VOTES sum) YES_VOTES)
|
||||
(+ TOTAL_VOTES sum)
|
||||
)
|
||||
my_amount
|
||||
(list TREASURY_ID) ; hint to Treasury ID so people can find it
|
||||
)
|
||||
(c
|
||||
(list ASSERT_MY_AMOUNT my_amount)
|
||||
output
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
|
||||
(if self_destruct_flag
|
||||
; assert self_destruct_time > proposal_timelock_length
|
||||
; this is the code path for if we've not been accepted by the treasury for a long time, and we're "bad" for some reason
|
||||
(if (> self_destruct_time vote_coin_ids_or_proposal_timelock_length)
|
||||
(list
|
||||
(list CREATE_COIN (calculate_finished_state (c SINGLETON_MOD_HASH (c SINGLETON_ID LAUNCHER_PUZZLE_HASH)) DAO_FINISHED_STATE_MOD_HASH) ONE (list TREASURY_ID))
|
||||
(list ASSERT_HEIGHT_RELATIVE self_destruct_time)
|
||||
(list ASSERT_PUZZLE_ANNOUNCEMENT ; make sure that we have a matching treasury oracle spend
|
||||
(sha256
|
||||
(calculate_treasury_puzzlehash
|
||||
(c SINGLETON_MOD_HASH (c TREASURY_ID LAUNCHER_PUZZLE_HASH))
|
||||
TREASURY_MOD_HASH
|
||||
vote_amounts_or_proposal_validator_hash
|
||||
vote_coin_ids_or_proposal_timelock_length ; check the veracity of these values by if the treasury uses them
|
||||
soft_close_length
|
||||
lockup_innerpuzhashes_or_attendance_required
|
||||
previous_votes_or_pass_margin
|
||||
self_destruct_time
|
||||
oracle_spend_delay
|
||||
)
|
||||
0 ; the arguments are secured implicitly in the puzzle of the treasury
|
||||
)
|
||||
)
|
||||
)
|
||||
(x)
|
||||
)
|
||||
; We're not trying to self destruct
|
||||
; Check whether we have a soft close to either try closing the proposal or adding votes
|
||||
; soft_close_length is used to prevent people from spamming the proposal and preventing others from being able to vote.
|
||||
; Someone could add 1 'no' vote to the proposal in every block until the proposal timelock has passed and then close the proposal as failed.
|
||||
; soft_close_length imposes some fixed number of blocks have passed without the proposal being spent before it can be closed.
|
||||
; This means there will always be some time for people to vote if they want before a proposal is closed.
|
||||
(if soft_close_length
|
||||
; Add the conditions which apply in both passed and failed cases
|
||||
(c
|
||||
(list ASSERT_HEIGHT_RELATIVE soft_close_length)
|
||||
(c
|
||||
(list CREATE_COIN (calculate_finished_state (c SINGLETON_MOD_HASH (c SINGLETON_ID LAUNCHER_PUZZLE_HASH)) DAO_FINISHED_STATE_MOD_HASH) ONE (list TREASURY_ID))
|
||||
(c
|
||||
(list
|
||||
ASSERT_PUZZLE_ANNOUNCEMENT
|
||||
(sha256 ; external timer
|
||||
(calculate_timer_puzhash
|
||||
PROPOSAL_TIMER_MOD_HASH
|
||||
SELF_HASH
|
||||
(c SINGLETON_MOD_HASH (c SINGLETON_ID LAUNCHER_PUZZLE_HASH))
|
||||
|
||||
)
|
||||
SINGLETON_ID
|
||||
)
|
||||
)
|
||||
(c
|
||||
(list CREATE_PUZZLE_ANNOUNCEMENT vote_coin_ids_or_proposal_timelock_length)
|
||||
; We are trying to close the proposal, so check whether it passed or failed
|
||||
(if
|
||||
(all
|
||||
(> TOTAL_VOTES lockup_innerpuzhashes_or_attendance_required)
|
||||
(> YES_VOTES (calculate_win_percentage TOTAL_VOTES previous_votes_or_pass_margin))
|
||||
)
|
||||
; Passed
|
||||
(list
|
||||
(list CREATE_COIN_ANNOUNCEMENT (sha256tree (list PROPOSED_PUZ_HASH 0))) ; the 0 at the end is announcement_args in proposal_validators
|
||||
; the above coin annnouncement lets us validate this coin in the proposal validator
|
||||
(list ASSERT_PUZZLE_ANNOUNCEMENT ; make sure that we actually have a matching treasury spend
|
||||
(sha256
|
||||
(calculate_treasury_puzzlehash
|
||||
(c SINGLETON_MOD_HASH (c TREASURY_ID LAUNCHER_PUZZLE_HASH))
|
||||
TREASURY_MOD_HASH
|
||||
vote_amounts_or_proposal_validator_hash
|
||||
vote_coin_ids_or_proposal_timelock_length ; check the veracity of these values by if the treasury uses them
|
||||
soft_close_length
|
||||
lockup_innerpuzhashes_or_attendance_required
|
||||
previous_votes_or_pass_margin
|
||||
self_destruct_time
|
||||
oracle_spend_delay
|
||||
)
|
||||
SINGLETON_ID ; directed at singleton, but most values are implicitly announced in the puzzle
|
||||
)
|
||||
)
|
||||
)
|
||||
; Failed
|
||||
(list
|
||||
(list ASSERT_PUZZLE_ANNOUNCEMENT ; make sure that we verify solution values against the treasury's oracle spend
|
||||
(sha256
|
||||
(calculate_treasury_puzzlehash
|
||||
(c SINGLETON_MOD_HASH (c TREASURY_ID LAUNCHER_PUZZLE_HASH))
|
||||
TREASURY_MOD_HASH
|
||||
vote_amounts_or_proposal_validator_hash
|
||||
vote_coin_ids_or_proposal_timelock_length ; check the veracity of these values by if the treasury uses them
|
||||
soft_close_length
|
||||
lockup_innerpuzhashes_or_attendance_required
|
||||
previous_votes_or_pass_margin
|
||||
self_destruct_time
|
||||
oracle_spend_delay
|
||||
)
|
||||
0 ; the arguments are secured implicitly in the puzzle of the treasury
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
; no soft_close_length so run the add votes path
|
||||
(loop_over_vote_coins
|
||||
SINGLETON_ID
|
||||
LOCKUP_SELF_HASH
|
||||
CAT_MOD_HASH
|
||||
CAT_TAIL_HASH
|
||||
TREASURY_ID
|
||||
SELF_HASH
|
||||
YES_VOTES
|
||||
TOTAL_VOTES
|
||||
PROPOSED_PUZ_HASH
|
||||
vote_coin_ids_or_proposal_timelock_length
|
||||
vote_amounts_or_proposal_validator_hash
|
||||
previous_votes_or_pass_margin
|
||||
lockup_innerpuzhashes_or_attendance_required
|
||||
vote_info
|
||||
0
|
||||
(if (any YES_VOTES TOTAL_VOTES) ; this prevents the timer from being created if the coin has been created with fake votes
|
||||
()
|
||||
(c
|
||||
(list
|
||||
CREATE_COIN
|
||||
(calculate_timer_puzhash
|
||||
PROPOSAL_TIMER_MOD_HASH
|
||||
SELF_HASH
|
||||
(c SINGLETON_MOD_HASH (c SINGLETON_ID LAUNCHER_PUZZLE_HASH)) ; SINGLETON_STRUCT
|
||||
)
|
||||
0
|
||||
)
|
||||
(if (= (sha256tree innerpuz_reveal) PROPOSED_PUZ_HASH) ; reveal the proposed code on chain with the first vote
|
||||
()
|
||||
(x)
|
||||
)
|
||||
)
|
||||
)
|
||||
my_amount
|
||||
()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
1
chia/wallet/puzzles/dao_proposal.clsp.hex
Normal file
1
chia/wallet/puzzles/dao_proposal.clsp.hex
Normal file
File diff suppressed because one or more lines are too long
1
chia/wallet/puzzles/dao_proposal.clsp.hex.sha256tree
Normal file
1
chia/wallet/puzzles/dao_proposal.clsp.hex.sha256tree
Normal file
@ -0,0 +1 @@
|
||||
a27440cdee44f910e80225592e51dc03721a9d819cc358165587fa2b34eef4cd
|
78
chia/wallet/puzzles/dao_proposal_timer.clsp
Normal file
78
chia/wallet/puzzles/dao_proposal_timer.clsp
Normal file
@ -0,0 +1,78 @@
|
||||
; This is a persistent timer for a proposal which allows it to have a relative time that survives despite it being recreated.
|
||||
; The closing time is contained in the timelock and passed in to the solution, and confirmed via an announcement from the Proposal
|
||||
; It creates/asserts announcements to pair it with the finishing spend of a proposal
|
||||
|
||||
(mod (
|
||||
PROPOSAL_SELF_HASH
|
||||
MY_PARENT_SINGLETON_STRUCT
|
||||
proposal_yes_votes
|
||||
proposal_total_votes
|
||||
proposal_innerpuzhash
|
||||
proposal_timelock
|
||||
parent_parent
|
||||
parent_amount
|
||||
)
|
||||
(include condition_codes.clib)
|
||||
(include curry-and-treehash.clib)
|
||||
(include *standard-cl-21*)
|
||||
|
||||
(defun calculate_singleton_puzzle_hash (PROPOSAL_SINGLETON_STRUCT inner_puzzle_hash)
|
||||
(puzzle-hash-of-curried-function (f PROPOSAL_SINGLETON_STRUCT)
|
||||
inner_puzzle_hash
|
||||
(sha256tree PROPOSAL_SINGLETON_STRUCT)
|
||||
)
|
||||
)
|
||||
|
||||
(defun calculate_proposal_puzzlehash (
|
||||
PROPOSAL_SINGLETON_STRUCT
|
||||
PROPOSAL_SELF_HASH
|
||||
proposal_yes_votes
|
||||
proposal_total_votes
|
||||
proposal_innerpuzhash
|
||||
)
|
||||
(calculate_singleton_puzzle_hash
|
||||
PROPOSAL_SINGLETON_STRUCT
|
||||
(puzzle-hash-of-curried-function PROPOSAL_SELF_HASH
|
||||
(sha256 ONE proposal_total_votes)
|
||||
(sha256 ONE proposal_yes_votes)
|
||||
(sha256 ONE proposal_innerpuzhash)
|
||||
(sha256 ONE (f (r PROPOSAL_SINGLETON_STRUCT)))
|
||||
(sha256 ONE PROPOSAL_SELF_HASH)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
; main
|
||||
(list
|
||||
(list ASSERT_HEIGHT_RELATIVE proposal_timelock)
|
||||
(list CREATE_PUZZLE_ANNOUNCEMENT (f (r MY_PARENT_SINGLETON_STRUCT)))
|
||||
(list
|
||||
ASSERT_PUZZLE_ANNOUNCEMENT
|
||||
(sha256
|
||||
(calculate_proposal_puzzlehash
|
||||
MY_PARENT_SINGLETON_STRUCT
|
||||
PROPOSAL_SELF_HASH
|
||||
proposal_yes_votes
|
||||
proposal_total_votes
|
||||
proposal_innerpuzhash
|
||||
)
|
||||
proposal_timelock
|
||||
)
|
||||
)
|
||||
(list
|
||||
ASSERT_MY_PARENT_ID
|
||||
(sha256
|
||||
parent_parent
|
||||
(calculate_proposal_puzzlehash
|
||||
MY_PARENT_SINGLETON_STRUCT
|
||||
PROPOSAL_SELF_HASH
|
||||
0
|
||||
0
|
||||
proposal_innerpuzhash
|
||||
)
|
||||
parent_amount
|
||||
)
|
||||
|
||||
)
|
||||
)
|
||||
)
|
1
chia/wallet/puzzles/dao_proposal_timer.clsp.hex
Normal file
1
chia/wallet/puzzles/dao_proposal_timer.clsp.hex
Normal file
@ -0,0 +1 @@
|
||||
ff02ffff01ff04ffff04ffff0152ffff04ff8200bfffff01808080ffff04ffff04ffff013effff04ffff05ffff06ff0b8080ffff01808080ffff04ffff04ffff013fffff04ffff0bffff02ff1effff04ff02ffff04ff0bffff04ff05ffff04ff17ffff04ff2fffff04ff5fff8080808080808080ff8200bf80ffff01808080ffff04ffff04ffff0147ffff04ffff0bff82017fffff02ff1effff04ff02ffff04ff0bffff04ff05ffff04ffff0180ffff04ffff0180ffff04ff5fff8080808080808080ff8202ff80ffff01808080ffff018080808080ffff04ffff01ffffff02ffff03ff05ffff01ff02ffff01ff02ff08ffff04ff02ffff04ffff06ff0580ffff04ffff0bffff0102ffff0bffff0101ffff010480ffff0bffff0102ffff0bffff0102ffff0bffff0101ffff010180ffff05ff058080ffff0bffff0102ff0bffff0bffff0101ffff018080808080ff8080808080ff0180ffff01ff02ffff010bff018080ff0180ff0bffff0102ffff01a0a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ffff02ff08ffff04ff02ffff04ff07ffff01ffa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b280808080ffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a808080ffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff0affff04ff02ffff04ffff05ff0580ff80808080ffff02ff0affff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ffff02ff0cffff04ff02ffff04ff09ffff04ff0bffff04ffff02ff0affff04ff02ffff04ff05ff80808080ff808080808080ff02ff16ffff04ff02ffff04ff05ffff04ffff02ff0cffff04ff02ffff04ff0bffff04ffff0bffff0101ff2f80ffff04ffff0bffff0101ff1780ffff04ffff0bffff0101ff5f80ffff04ffff0bffff0101ff1580ffff04ffff0bffff0101ff0b80ff808080808080808080ff8080808080ff018080
|
@ -0,0 +1 @@
|
||||
5526d8dc33b60a23c86ac7184e8f7051515af16dbb7489555f389b84a5313c84
|
87
chia/wallet/puzzles/dao_proposal_validator.clsp
Normal file
87
chia/wallet/puzzles/dao_proposal_validator.clsp
Normal file
@ -0,0 +1,87 @@
|
||||
(mod
|
||||
(
|
||||
SINGLETON_STRUCT ; (SINGLETON_MOD_HASH (SINGLETON_ID . LAUNCHER_PUZZLE_HASH))
|
||||
PROPOSAL_SELF_HASH
|
||||
PROPOSAL_MINIMUM_AMOUNT
|
||||
PROPOSAL_EXCESS_PAYOUT_PUZ_HASH ; this is where the excess money gets paid out to
|
||||
Attendance_Required ; this is passed in as a Truth from above
|
||||
Pass_Margin ; this is a pass in as a Truth from above
|
||||
(announcement_source delegated_puzzle_hash announcement_args)
|
||||
(
|
||||
proposal_id
|
||||
total_votes
|
||||
yes_votes
|
||||
coin_parent
|
||||
coin_amount
|
||||
)
|
||||
conditions
|
||||
)
|
||||
|
||||
(include condition_codes.clib)
|
||||
(include curry-and-treehash.clib)
|
||||
(include utility_macros.clib)
|
||||
(include *standard-cl-21*)
|
||||
|
||||
(defconstant TEN_THOUSAND 10000)
|
||||
|
||||
(defun-inline calculate_win_percentage (TOTAL PERCENTAGE)
|
||||
(f (divmod (* TOTAL PERCENTAGE) TEN_THOUSAND))
|
||||
)
|
||||
|
||||
(defun-inline calculate_full_puzzle_hash (SINGLETON_STRUCT inner_puzzle_hash)
|
||||
(puzzle-hash-of-curried-function (f SINGLETON_STRUCT)
|
||||
inner_puzzle_hash
|
||||
(sha256tree SINGLETON_STRUCT)
|
||||
)
|
||||
)
|
||||
|
||||
(defun-inline calculate_proposal_puzzle (
|
||||
PROPOSAL_SELF_HASH
|
||||
proposal_singleton_id
|
||||
proposal_yes_votes
|
||||
proposal_total_votes
|
||||
proposal_innerpuz_hash
|
||||
)
|
||||
(puzzle-hash-of-curried-function PROPOSAL_SELF_HASH
|
||||
(sha256 ONE proposal_total_votes)
|
||||
(sha256 ONE proposal_yes_votes)
|
||||
(sha256 ONE proposal_innerpuz_hash)
|
||||
(sha256 ONE proposal_singleton_id)
|
||||
(sha256 ONE PROPOSAL_SELF_HASH)
|
||||
)
|
||||
)
|
||||
|
||||
(assert
|
||||
; (= (sha256tree my_solution) announcement_args) - quex suggested this. We don't need to check it now. Can be used for future functionality.
|
||||
(> (+ coin_amount ONE) PROPOSAL_MINIMUM_AMOUNT) ; >=
|
||||
(> total_votes Attendance_Required) ; TODO: we might want to change this to storing total cats and calculating like with yes votes
|
||||
(> yes_votes (calculate_win_percentage total_votes Pass_Margin))
|
||||
(=
|
||||
announcement_source
|
||||
(calculate_coin_id
|
||||
coin_parent
|
||||
(calculate_full_puzzle_hash
|
||||
(c (f SINGLETON_STRUCT) (c proposal_id (r (r SINGLETON_STRUCT))))
|
||||
(calculate_proposal_puzzle
|
||||
PROPOSAL_SELF_HASH
|
||||
proposal_id
|
||||
yes_votes ; this is where we validate the yes votes and total votes
|
||||
total_votes
|
||||
delegated_puzzle_hash
|
||||
)
|
||||
)
|
||||
coin_amount
|
||||
)
|
||||
)
|
||||
(c
|
||||
(list CREATE_PUZZLE_ANNOUNCEMENT proposal_id) ; specify the proposal we're talking about
|
||||
(if (> (- coin_amount 1) 0)
|
||||
(c
|
||||
(list CREATE_COIN PROPOSAL_EXCESS_PAYOUT_PUZ_HASH (- coin_amount 1) (list (f (r SINGLETON_STRUCT))))
|
||||
conditions
|
||||
)
|
||||
conditions
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
1
chia/wallet/puzzles/dao_proposal_validator.clsp.hex
Normal file
1
chia/wallet/puzzles/dao_proposal_validator.clsp.hex
Normal file
@ -0,0 +1 @@
|
||||
ff02ffff01ff02ffff03ffff15ffff10ff825effffff010180ff1780ffff01ff02ffff01ff02ffff03ffff15ff820affff5f80ffff01ff02ffff01ff02ffff03ffff15ff8216ffffff05ffff14ffff12ff820affff8200bf80ffff01822710808080ffff01ff02ffff01ff02ffff03ffff09ff82027fffff02ff0affff04ff02ffff04ff822effffff04ffff02ff0cffff04ff02ffff04ffff05ffff04ffff05ff0580ffff04ff8204ffffff06ffff06ff058080808080ffff04ffff02ff0cffff04ff02ffff04ff0bffff04ffff0bffff0101ff820aff80ffff04ffff0bffff0101ff8216ff80ffff04ffff0bffff0101ff82057f80ffff04ffff0bffff0101ff8204ff80ffff04ffff0bffff0101ff0b80ff808080808080808080ffff04ffff02ff0effff04ff02ffff04ffff04ffff05ff0580ffff04ff8204ffffff06ffff06ff0580808080ff80808080ff808080808080ffff04ff825effff80808080808080ffff01ff02ffff01ff04ffff04ffff013effff04ff8204ffffff01808080ffff02ffff03ffff15ffff11ff825effffff010180ffff018080ffff01ff02ffff01ff04ffff04ffff0133ffff04ff2fffff04ffff11ff825effffff010180ffff04ffff04ffff05ffff06ff058080ffff018080ffff018080808080ff8205ff80ff0180ffff01ff02ffff018205ffff018080ff018080ff0180ffff01ff02ffff01ff0880ff018080ff0180ff0180ffff01ff02ffff01ff0880ff018080ff0180ff0180ffff01ff02ffff01ff0880ff018080ff0180ff0180ffff01ff02ffff01ff0880ff018080ff0180ffff04ffff01ffffff02ffff03ff05ffff01ff02ffff01ff02ff08ffff04ff02ffff04ffff06ff0580ffff04ffff0bffff0102ffff0bffff0101ffff010480ffff0bffff0102ffff0bffff0102ffff0bffff0101ffff010180ffff05ff058080ffff0bffff0102ff0bffff0bffff0101ffff018080808080ff8080808080ff0180ffff01ff02ffff010bff018080ff0180ff0bffff0102ffff01a0a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ffff02ff08ffff04ff02ffff04ff07ffff01ffa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b280808080ffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a808080ffff02ffff03ffff22ffff09ffff0dff0580ffff012080ffff09ffff0dff0b80ffff012080ffff15ff17ffff0181ff8080ffff01ff02ffff01ff0bff05ff0bff1780ff0180ffff01ff02ffff01ff0880ff018080ff0180ff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff0effff04ff02ffff04ffff05ff0580ff80808080ffff02ff0effff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff018080
|
@ -0,0 +1 @@
|
||||
edff0f36ca097ea55c867f8700cc4d48d267b91fd00ccee2db0fab6fe0645c67
|
226
chia/wallet/puzzles/dao_spend_p2_singleton_v2.clsp
Normal file
226
chia/wallet/puzzles/dao_spend_p2_singleton_v2.clsp
Normal file
@ -0,0 +1,226 @@
|
||||
(mod (
|
||||
TREASURY_SINGLETON_STRUCT
|
||||
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
|
||||
p2_singleton_parent_amount_list ; for xch this is just a list of (coin_parent coin_amount)
|
||||
p2_singleton_tailhash_parent_amount_list ; list of ((asset (parent amount) (parent amount)... ) (asset (parent amount)... )... ),
|
||||
; must match order of curryed asset list
|
||||
; the last (parent amount) gets given the puzzlehash, the rest get given 0
|
||||
treasury_inner_puzhash
|
||||
)
|
||||
; we need to track CAT_TYPE and DELEGATED_PUZZLE
|
||||
; list of (asset_type (parent amount))
|
||||
|
||||
; If you're writing a proposal you'll want to use this layer
|
||||
; if you don't, your proposal might be invalidated if the p2_singleton coins get spent
|
||||
|
||||
(include condition_codes.clib)
|
||||
(include curry-and-treehash.clib)
|
||||
(include *standard-cl-21*)
|
||||
|
||||
(defun-inline calculate_singleton_puzzle_hash (PROPOSAL_SINGLETON_STRUCT inner_puzzle_hash)
|
||||
(puzzle-hash-of-curried-function (f PROPOSAL_SINGLETON_STRUCT)
|
||||
inner_puzzle_hash
|
||||
(sha256tree PROPOSAL_SINGLETON_STRUCT)
|
||||
)
|
||||
)
|
||||
|
||||
(defun loop_through_list (
|
||||
TREASURY_SINGLETON_STRUCT
|
||||
SPEND_AMOUNT
|
||||
P2_SINGLETON_PUZHASH
|
||||
p2_calculated
|
||||
p2_singleton_list
|
||||
total
|
||||
output
|
||||
)
|
||||
(c
|
||||
(list CREATE_PUZZLE_ANNOUNCEMENT (sha256tree (list p2_calculated (sha256tree 0))))
|
||||
(c
|
||||
(list ASSERT_COIN_ANNOUNCEMENT (sha256 p2_calculated '$'))
|
||||
(if p2_singleton_list
|
||||
(loop_through_list
|
||||
TREASURY_SINGLETON_STRUCT
|
||||
SPEND_AMOUNT
|
||||
P2_SINGLETON_PUZHASH
|
||||
(calculate_coin_id (f (f p2_singleton_list)) P2_SINGLETON_PUZHASH (f (r (f p2_singleton_list))))
|
||||
(r p2_singleton_list)
|
||||
(+ total (f (r (f p2_singleton_list))))
|
||||
output
|
||||
)
|
||||
(if (> (- total SPEND_AMOUNT) 0)
|
||||
(c
|
||||
(list CREATE_COIN P2_SINGLETON_PUZHASH (- total SPEND_AMOUNT) (list P2_SINGLETON_PUZHASH))
|
||||
output
|
||||
)
|
||||
output
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
(defun add_announcements_to_result (p2_calculated delegated_puzhash output)
|
||||
(c
|
||||
(list CREATE_PUZZLE_ANNOUNCEMENT (sha256tree (list p2_calculated delegated_puzhash)))
|
||||
(c
|
||||
(list ASSERT_COIN_ANNOUNCEMENT (sha256 p2_calculated '$'))
|
||||
output
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
(defun sum_create_coins (conditions)
|
||||
(if conditions
|
||||
(+
|
||||
(if
|
||||
(= (f (f conditions)) CREATE_COIN)
|
||||
(if
|
||||
(> (f (r (r (f conditions)))) 0) ; make an exception for -113 and other magic conditions
|
||||
(f (r (r (f conditions))))
|
||||
0
|
||||
)
|
||||
0
|
||||
)
|
||||
(sum_create_coins (r conditions))
|
||||
)
|
||||
0
|
||||
)
|
||||
)
|
||||
|
||||
(defun-inline calculate_delegated_puzzlehash (CONDITIONS)
|
||||
(sha256tree (c ONE CONDITIONS)) ; this makes (q . CONDITIONS)
|
||||
)
|
||||
|
||||
(defun wrap_in_cat_layer (CAT_MOD_HASH CAT_TAIL_HASH INNERPUZHASH)
|
||||
(puzzle-hash-of-curried-function CAT_MOD_HASH
|
||||
INNERPUZHASH
|
||||
(sha256 ONE CAT_TAIL_HASH)
|
||||
(sha256 ONE CAT_MOD_HASH)
|
||||
)
|
||||
)
|
||||
|
||||
; for a given asset type, loop through the cat coins and generate the announcements required for each
|
||||
(defun for_each_asset (
|
||||
TREASURY_SINGLETON_STRUCT
|
||||
CAT_MOD_HASH
|
||||
CONDITIONS_FOR_THIS_ASSET_TYPE
|
||||
P2_SINGLETON_PUZHASH
|
||||
p2_singleton_puzzle_hash
|
||||
parent_amount_list
|
||||
total
|
||||
create_coin_sum
|
||||
output
|
||||
)
|
||||
(if parent_amount_list
|
||||
(add_announcements_to_result
|
||||
(calculate_coin_id (f (f parent_amount_list)) p2_singleton_puzzle_hash (f (r (f parent_amount_list))))
|
||||
(if
|
||||
(r parent_amount_list) ; this is the delegated_puzhash
|
||||
(sha256tree 0) ; most coins destroy themselves
|
||||
(calculate_delegated_puzzlehash ; the last coin creates the conditions
|
||||
(let ((coin_sum (- (+ total (f (r (f parent_amount_list)))) create_coin_sum)))
|
||||
(if (> coin_sum 0)
|
||||
(c
|
||||
(list CREATE_COIN P2_SINGLETON_PUZHASH coin_sum (list P2_SINGLETON_PUZHASH))
|
||||
CONDITIONS_FOR_THIS_ASSET_TYPE
|
||||
)
|
||||
CONDITIONS_FOR_THIS_ASSET_TYPE
|
||||
))
|
||||
)
|
||||
)
|
||||
(for_each_asset
|
||||
TREASURY_SINGLETON_STRUCT
|
||||
CAT_MOD_HASH
|
||||
CONDITIONS_FOR_THIS_ASSET_TYPE
|
||||
P2_SINGLETON_PUZHASH
|
||||
p2_singleton_puzzle_hash
|
||||
(r parent_amount_list)
|
||||
(+ total (f (r (f parent_amount_list))))
|
||||
create_coin_sum
|
||||
output
|
||||
)
|
||||
)
|
||||
output
|
||||
)
|
||||
)
|
||||
|
||||
; loops through the list of ((tailhash conditions))
|
||||
(defun for_each_asset_type (
|
||||
TREASURY_SINGLETON_STRUCT
|
||||
CAT_MOD_HASH
|
||||
P2_SINGLETON_PUZHASH
|
||||
LIST_OF_TAILHASH_CONDITIONS
|
||||
p2_singleton_tailhash_parent_amount_list ; ((tailhash ((parent amount) (parent_amount)... ) (tailhash (parent amount))..)
|
||||
output
|
||||
)
|
||||
(if LIST_OF_TAILHASH_CONDITIONS
|
||||
(for_each_asset_type
|
||||
TREASURY_SINGLETON_STRUCT
|
||||
CAT_MOD_HASH
|
||||
P2_SINGLETON_PUZHASH
|
||||
(r LIST_OF_TAILHASH_CONDITIONS)
|
||||
(r p2_singleton_tailhash_parent_amount_list)
|
||||
(for_each_asset
|
||||
TREASURY_SINGLETON_STRUCT
|
||||
CAT_MOD_HASH
|
||||
(if
|
||||
(=
|
||||
(f (f LIST_OF_TAILHASH_CONDITIONS))
|
||||
(f (f p2_singleton_tailhash_parent_amount_list))
|
||||
)
|
||||
(f (r (f LIST_OF_TAILHASH_CONDITIONS)))
|
||||
(x) ; bad solution format
|
||||
)
|
||||
P2_SINGLETON_PUZHASH
|
||||
(wrap_in_cat_layer CAT_MOD_HASH (f (f p2_singleton_tailhash_parent_amount_list)) P2_SINGLETON_PUZHASH) ; p2_singleton_puzzle_hash
|
||||
(f (r (f p2_singleton_tailhash_parent_amount_list))) ; list of ((parent amount) (parent amount)...)
|
||||
0 ; current total - initialise as 0
|
||||
(sum_create_coins (f (r (f LIST_OF_TAILHASH_CONDITIONS))))
|
||||
output ; add new conditions to previous calculated output conditions
|
||||
)
|
||||
)
|
||||
output ; at the end of the loop output our calculated conditions
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
; main
|
||||
(c
|
||||
(list ASSERT_MY_PUZZLEHASH (calculate_singleton_puzzle_hash TREASURY_SINGLETON_STRUCT treasury_inner_puzhash))
|
||||
(c
|
||||
(list CREATE_COIN treasury_inner_puzhash ONE (list (f (r TREASURY_SINGLETON_STRUCT))))
|
||||
(if CONDITIONS
|
||||
(loop_through_list
|
||||
TREASURY_SINGLETON_STRUCT
|
||||
(sum_create_coins CONDITIONS)
|
||||
P2_SINGLETON_VIA_DELEGATED_PUZZLE_PUZHASH
|
||||
(if p2_singleton_parent_amount_list (calculate_coin_id (f (f p2_singleton_parent_amount_list)) P2_SINGLETON_VIA_DELEGATED_PUZZLE_PUZHASH (f (r (f p2_singleton_parent_amount_list)))) ())
|
||||
(if p2_singleton_parent_amount_list (r p2_singleton_parent_amount_list) ())
|
||||
(if p2_singleton_parent_amount_list (f (r (f p2_singleton_parent_amount_list))) ())
|
||||
(for_each_asset_type
|
||||
TREASURY_SINGLETON_STRUCT
|
||||
CAT_MOD_HASH
|
||||
P2_SINGLETON_VIA_DELEGATED_PUZZLE_PUZHASH
|
||||
LIST_OF_TAILHASH_CONDITIONS
|
||||
p2_singleton_tailhash_parent_amount_list ; ((tailhash ((parent amount) (parent_amount)... ) (tailhash (parent amount))..)
|
||||
CONDITIONS
|
||||
)
|
||||
)
|
||||
(for_each_asset_type
|
||||
TREASURY_SINGLETON_STRUCT
|
||||
CAT_MOD_HASH
|
||||
P2_SINGLETON_VIA_DELEGATED_PUZZLE_PUZHASH
|
||||
LIST_OF_TAILHASH_CONDITIONS
|
||||
p2_singleton_tailhash_parent_amount_list ; ((tailhash ((parent amount) (parent_amount)... ) (tailhash (parent amount))..)
|
||||
CONDITIONS
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
)
|
1
chia/wallet/puzzles/dao_spend_p2_singleton_v2.clsp.hex
Normal file
1
chia/wallet/puzzles/dao_spend_p2_singleton_v2.clsp.hex
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
||||
fb3890f672c9df3cc69699e21446b89b55b65071d35bf5c80a49d11c9b79a68f
|
112
chia/wallet/puzzles/dao_treasury.clsp
Normal file
112
chia/wallet/puzzles/dao_treasury.clsp
Normal file
@ -0,0 +1,112 @@
|
||||
(mod
|
||||
(
|
||||
TREASURY_MOD_HASH
|
||||
PROPOSAL_VALIDATOR ; this is the curryed proposal validator
|
||||
PROPOSAL_LENGTH
|
||||
PROPOSAL_SOFTCLOSE_LENGTH
|
||||
ATTENDANCE_REQUIRED
|
||||
PASS_MARGIN ; this is a percentage 0 - 10,000 - 51% would be 5100
|
||||
PROPOSAL_SELF_DESTRUCT_TIME ; time in seconds after which proposals can be automatically closed
|
||||
ORACLE_SPEND_DELAY ; timelock delay for oracle spend
|
||||
(@ proposal_announcement (announcement_source delegated_puzzle_hash announcement_args))
|
||||
proposal_validator_solution
|
||||
delegated_puzzle_reveal ; this is the reveal of the puzzle announced by the proposal
|
||||
delegated_solution ; this is not secure unless the delegated puzzle secures it
|
||||
my_singleton_struct
|
||||
)
|
||||
(include utility_macros.clib)
|
||||
(include condition_codes.clib)
|
||||
(include curry-and-treehash.clib)
|
||||
(include *standard-cl-21*)
|
||||
|
||||
(defun-inline recreate_self (
|
||||
TREASURY_MOD_HASH
|
||||
PROPOSAL_VALIDATOR
|
||||
PROPOSAL_LENGTH
|
||||
PROPOSAL_SOFTCLOSE_LENGTH
|
||||
ATTENDANCE_REQUIRED
|
||||
PASS_MARGIN
|
||||
PROPOSAL_SELF_DESTRUCT_TIME
|
||||
ORACLE_SPEND_DELAY
|
||||
)
|
||||
(puzzle-hash-of-curried-function TREASURY_MOD_HASH
|
||||
(sha256 ONE ORACLE_SPEND_DELAY)
|
||||
(sha256 ONE PROPOSAL_SELF_DESTRUCT_TIME)
|
||||
(sha256 ONE PASS_MARGIN)
|
||||
(sha256 ONE ATTENDANCE_REQUIRED)
|
||||
(sha256 ONE PROPOSAL_SOFTCLOSE_LENGTH)
|
||||
(sha256 ONE PROPOSAL_LENGTH)
|
||||
(sha256tree PROPOSAL_VALIDATOR)
|
||||
(sha256 ONE TREASURY_MOD_HASH)
|
||||
)
|
||||
)
|
||||
|
||||
(defun calculate_singleton_puzzle_hash (SINGLETON_STRUCT inner_puzzle_hash)
|
||||
(puzzle-hash-of-curried-function (f SINGLETON_STRUCT)
|
||||
inner_puzzle_hash
|
||||
(sha256tree SINGLETON_STRUCT)
|
||||
)
|
||||
)
|
||||
|
||||
(defun stager (ORACLE_SPEND_DELAY my_inner_puzhash singleton_struct)
|
||||
(c
|
||||
(if singleton_struct
|
||||
(list ASSERT_MY_COIN_ID
|
||||
(calculate_coin_id
|
||||
(f (r singleton_struct))
|
||||
(calculate_singleton_puzzle_hash singleton_struct my_inner_puzhash)
|
||||
ONE
|
||||
)
|
||||
;; TODO: When the new condition codes are available, use ASSERT_EPHEMERAL to ensure this
|
||||
;; spend path is only used in the eve spend.
|
||||
;; (list ASSERT_EPHEMERAL)
|
||||
)
|
||||
(list ASSERT_HEIGHT_RELATIVE ORACLE_SPEND_DELAY)
|
||||
)
|
||||
(list (list CREATE_COIN my_inner_puzhash ONE))
|
||||
)
|
||||
)
|
||||
|
||||
(c
|
||||
(list CREATE_PUZZLE_ANNOUNCEMENT 0) ; the arguments are secured implicitly in the puzzle of the treasury
|
||||
(if delegated_puzzle_reveal
|
||||
; if we're checking a proposal (testing if it has passed)
|
||||
(if (= (sha256tree delegated_puzzle_reveal) delegated_puzzle_hash)
|
||||
; Merge the treasury conditions with the proposal validator conditions
|
||||
; If the update case then the validator returns the new treasury create coin
|
||||
; If the spend case then we need to recreate the treasury outselves
|
||||
; treasury specific conditions
|
||||
|
||||
(c
|
||||
(list ASSERT_COIN_ANNOUNCEMENT (sha256 announcement_source (sha256tree (list delegated_puzzle_hash announcement_args)))) ; announcement source is validated inside the ProposalValidator
|
||||
(a
|
||||
PROPOSAL_VALIDATOR
|
||||
(list
|
||||
ATTENDANCE_REQUIRED
|
||||
PASS_MARGIN
|
||||
proposal_announcement
|
||||
proposal_validator_solution
|
||||
(a delegated_puzzle_reveal delegated_solution)
|
||||
)
|
||||
)
|
||||
)
|
||||
(x)
|
||||
)
|
||||
; no proposal_flag so create the oracle announcement
|
||||
(stager
|
||||
ORACLE_SPEND_DELAY
|
||||
(recreate_self
|
||||
TREASURY_MOD_HASH
|
||||
PROPOSAL_VALIDATOR
|
||||
PROPOSAL_LENGTH
|
||||
PROPOSAL_SOFTCLOSE_LENGTH
|
||||
ATTENDANCE_REQUIRED
|
||||
PASS_MARGIN
|
||||
PROPOSAL_SELF_DESTRUCT_TIME
|
||||
ORACLE_SPEND_DELAY
|
||||
)
|
||||
my_singleton_struct
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
1
chia/wallet/puzzles/dao_treasury.clsp.hex
Normal file
1
chia/wallet/puzzles/dao_treasury.clsp.hex
Normal file
@ -0,0 +1 @@
|
||||
ff02ffff01ff04ffff04ffff013effff04ffff0180ffff01808080ffff02ffff03ff8217ffffff01ff02ffff01ff02ffff03ffff09ffff02ff0affff04ff02ffff04ff8217ffff80808080ff8215ff80ffff01ff02ffff01ff04ffff04ffff013dffff04ffff0bff8209ffffff02ff0affff04ff02ffff04ffff04ff8215ffffff04ff822dffffff01808080ff8080808080ffff01808080ffff02ff0bffff04ff5fffff04ff8200bfffff04ff8205ffffff04ff820bffffff04ffff02ff8217ffff822fff80ffff018080808080808080ff0180ffff01ff02ffff01ff0880ff018080ff0180ff0180ffff01ff02ffff01ff02ff1effff04ff02ffff04ff8202ffffff04ffff02ff14ffff04ff02ffff04ff05ffff04ffff0bffff0101ff8202ff80ffff04ffff0bffff0101ff82017f80ffff04ffff0bffff0101ff8200bf80ffff04ffff0bffff0101ff5f80ffff04ffff0bffff0101ff2f80ffff04ffff0bffff0101ff1780ffff04ffff02ff0affff04ff02ffff04ff0bff80808080ffff04ffff0bffff0101ff0580ff808080808080808080808080ffff04ff825fffff808080808080ff018080ff018080ffff04ffff01ffffff02ffff03ff05ffff01ff02ffff01ff02ff08ffff04ff02ffff04ffff06ff0580ffff04ffff0bffff0102ffff0bffff0101ffff010480ffff0bffff0102ffff0bffff0102ffff0bffff0101ffff010180ffff05ff058080ffff0bffff0102ff0bffff0bffff0101ffff018080808080ff8080808080ff0180ffff01ff02ffff010bff018080ff0180ffff0bffff0102ffff01a0a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ffff02ff08ffff04ff02ffff04ff07ffff01ffa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b280808080ffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a808080ff02ffff03ffff22ffff09ffff0dff0580ffff012080ffff09ffff0dff0b80ffff012080ffff15ff17ffff0181ff8080ffff01ff02ffff01ff0bff05ff0bff1780ff0180ffff01ff02ffff01ff0880ff018080ff0180ffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff0affff04ff02ffff04ffff05ff0580ff80808080ffff02ff0affff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ffff02ff14ffff04ff02ffff04ff09ffff04ff0bffff04ffff02ff0affff04ff02ffff04ff05ff80808080ff808080808080ff04ffff02ffff03ff17ffff01ff02ffff01ff04ffff0146ffff04ffff02ff1cffff04ff02ffff04ffff05ffff06ff178080ffff04ffff02ff16ffff04ff02ffff04ff17ffff04ff0bff8080808080ffff04ffff0101ff808080808080ffff01808080ff0180ffff01ff02ffff01ff04ffff0152ffff04ff05ffff01808080ff018080ff0180ffff04ffff04ffff0133ffff04ff0bffff01ff01808080ff808080ff018080
|
44
chia/wallet/puzzles/dao_update_proposal.clsp
Normal file
44
chia/wallet/puzzles/dao_update_proposal.clsp
Normal file
@ -0,0 +1,44 @@
|
||||
(mod
|
||||
(
|
||||
TREASURY_MOD_HASH
|
||||
VALIDATOR_MOD_HASH
|
||||
SINGLETON_STRUCT ; (SINGLETON_MOD_HASH (SINGLETON_ID . LAUNCHER_PUZZLE_HASH))
|
||||
PROPOSAL_SELF_HASH
|
||||
PROPOSAL_MINIMUM_AMOUNT
|
||||
PROPOSAL_EXCESS_PAYOUT_PUZHASH
|
||||
PROPOSAL_LENGTH
|
||||
PROPOSAL_SOFTCLOSE_LENGTH
|
||||
ATTENDANCE_REQUIRED
|
||||
PASS_MARGIN
|
||||
PROPOSAL_SELF_DESTRUCT_TIME
|
||||
ORACLE_SPEND_DELAY
|
||||
)
|
||||
;; This is a proposal to update treasury conditions for a DAO
|
||||
|
||||
|
||||
(include condition_codes.clib)
|
||||
(include curry-and-treehash.clib)
|
||||
(include *standard-cl-21*)
|
||||
(include utility_macros.clib)
|
||||
|
||||
(list
|
||||
(list CREATE_COIN
|
||||
(puzzle-hash-of-curried-function TREASURY_MOD_HASH
|
||||
(sha256 ONE ORACLE_SPEND_DELAY)
|
||||
(sha256 ONE PROPOSAL_SELF_DESTRUCT_TIME)
|
||||
(sha256 ONE PASS_MARGIN)
|
||||
(sha256 ONE ATTENDANCE_REQUIRED)
|
||||
(sha256 ONE PROPOSAL_SOFTCLOSE_LENGTH)
|
||||
(sha256 ONE PROPOSAL_LENGTH)
|
||||
(puzzle-hash-of-curried-function VALIDATOR_MOD_HASH
|
||||
(sha256 ONE PROPOSAL_EXCESS_PAYOUT_PUZHASH)
|
||||
(sha256 ONE PROPOSAL_MINIMUM_AMOUNT)
|
||||
(sha256 ONE PROPOSAL_SELF_HASH)
|
||||
(sha256tree SINGLETON_STRUCT)
|
||||
)
|
||||
(sha256 ONE TREASURY_MOD_HASH)
|
||||
)
|
||||
ONE
|
||||
)
|
||||
)
|
||||
)
|
1
chia/wallet/puzzles/dao_update_proposal.clsp.hex
Normal file
1
chia/wallet/puzzles/dao_update_proposal.clsp.hex
Normal file
@ -0,0 +1 @@
|
||||
ff02ffff01ff04ffff04ffff0133ffff04ffff02ff0affff04ff02ffff04ff05ffff04ffff0bffff0101ff822fff80ffff04ffff0bffff0101ff8217ff80ffff04ffff0bffff0101ff820bff80ffff04ffff0bffff0101ff8205ff80ffff04ffff0bffff0101ff8202ff80ffff04ffff0bffff0101ff82017f80ffff04ffff02ff0affff04ff02ffff04ff0bffff04ffff0bffff0101ff8200bf80ffff04ffff0bffff0101ff5f80ffff04ffff0bffff0101ff2f80ffff04ffff02ff0effff04ff02ffff04ff17ff80808080ff8080808080808080ffff04ffff0bffff0101ff0580ff808080808080808080808080ffff04ffff0101ffff0180808080ffff018080ffff04ffff01ffff02ffff03ff05ffff01ff02ffff01ff02ff04ffff04ff02ffff04ffff06ff0580ffff04ffff0bffff0102ffff0bffff0101ffff010480ffff0bffff0102ffff0bffff0102ffff0bffff0101ffff010180ffff05ff058080ffff0bffff0102ff0bffff0bffff0101ffff018080808080ff8080808080ff0180ffff01ff02ffff010bff018080ff0180ffff0bffff0102ffff01a0a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ffff02ff04ffff04ff02ffff04ff07ffff01ffa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b280808080ffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a808080ff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff0effff04ff02ffff04ffff05ff0580ff80808080ffff02ff0effff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff018080
|
@ -7,6 +7,16 @@
|
||||
"covenant_layer": "b982796850336aabf9ab17c3f21e299f0c633444117ab5e9ebeafadf1860d9fc",
|
||||
"create_nft_launcher_from_did": "7a32d2d9571d3436791c0ad3d7fcfdb9c43ace2b0f0ff13f98d29f0cc093f445",
|
||||
"credential_restriction": "2fdfc1f058cfd65e7ec4e253bfeb394da163ecd0036f508df8629b0a2b8fde96",
|
||||
"dao_cat_eve": "488f55bedaca5a599544dfd5ab341e2e5c7e6fca67d9b98a3d856f876c52f53e",
|
||||
"dao_cat_launcher": "a01a838d18d4e031e937c79fa3f80f213fa00a3e64af6c16a1f137770cd3a567",
|
||||
"dao_finished_state": "694c99e1fb07671771bbca3d110880693a9ecc37a6529891ec979d0f3e760eba",
|
||||
"dao_lockup": "d6215f0916715a69fbbf2d1a679f437fde81787adeb90c666642fb9c2deff7ce",
|
||||
"dao_proposal": "b25dd85e418791aebb7d7486914b1998e117da989a66a40f00259f925b3ddb16",
|
||||
"dao_proposal_timer": "1acd912fca662d1474f7a6c762280fc1430875bef518883387086c1125027526",
|
||||
"dao_proposal_validator": "507197e6645e3741efc200de19edc4556be1352c09ce1c9edaad7f1a4fc9d6a1",
|
||||
"dao_spend_p2_singleton_v2": "e76c813d409e11ab58989a234eff1907b17d295496e6fc125781750d6a530a58",
|
||||
"dao_treasury": "a80ab006a05f8fa0156c4bec25b747075c61338c6d0c0ffe95fd04ea96c636d7",
|
||||
"dao_update_proposal": "fc032384cfece9b542c3e1ea77ba119fb1013a3d74b622302c0b670447e4343d",
|
||||
"decompress_coin_spend_entry": "9d98ed08770d31be4bd1bde4705dab388db5e7e9c349f5a76fc3c347aa3a0b79",
|
||||
"decompress_coin_spend_entry_with_prefix": "92aa4bc8060a8836355a1884075141b4791ce1b67ae6092bb166b2845954bc89",
|
||||
"decompress_puzzle": "fe94c58f1117afe315e0450daca1c62460ec1a1c439cd4018d79967a5d7d1370",
|
||||
@ -19,6 +29,7 @@
|
||||
"exigent_metadata_layer": "d5fd32e069fda83e230ccd8f6a7c4f652231aed5c755514b3d996cbeff4182b8",
|
||||
"flag_proofs_checker": "fe2e3c631562fbb9be095297f762bf573705a0197164e9361ad5d50e045ba241",
|
||||
"genesis_by_coin_id": "493afb89eed93ab86741b2aa61b8f5de495d33ff9b781dfc8919e602b2afa150",
|
||||
"genesis_by_coin_id_or_singleton": "40170305e3a71c3e7523f37fbcfc3188f9f949da0818a6331f28251e76e8c56f",
|
||||
"genesis_by_puzzle_hash": "de5a6e06d41518be97ff6365694f4f89475dda773dede267caa33da63b434e36",
|
||||
"graftroot_dl_offers": "0893e36a88c064fddfa6f8abdb42c044584a98cb4273b80cccc83b4867b701a1",
|
||||
"nft_intermediate_launcher": "7a32d2d9571d3436791c0ad3d7fcfdb9c43ace2b0f0ff13f98d29f0cc093f445",
|
||||
@ -38,7 +49,9 @@
|
||||
"p2_parent": "b10ce2d0b18dcf8c21ddfaf55d9b9f0adcbf1e0beb55b1a8b9cad9bbff4e5f22",
|
||||
"p2_puzzle_hash": "13e29a62b42cd2ef72a79e4bacdc59733ca6310d65af83d349360d36ec622363",
|
||||
"p2_singleton": "40f828d8dd55603f4ff9fbf6b73271e904e69406982f4fbefae2c8dcceaf9834",
|
||||
"p2_singleton_aggregator": "f537d6ec44aa42f45da8180d9c172663d215828f5dbcc6e2f82a96e6536a30c3",
|
||||
"p2_singleton_or_delayed_puzhash": "adb656e0211e2ab4f42069a4c5efc80dc907e7062be08bf1628c8e5b6d94d25b",
|
||||
"p2_singleton_via_delegated_puzzle": "9590eaa169e45b655a31d3c06bbd355a3e2b2e3e410d3829748ce08ab249c39e",
|
||||
"pool_member_innerpuz": "a8490702e333ddd831a3ac9c22d0fa26d2bfeaf2d33608deb22f0e0123eb0494",
|
||||
"pool_waitingroom_innerpuz": "a317541a765bf8375e1c6e7c13503d0d2cbf56cacad5182befe947e78e2c0307",
|
||||
"rom_bootstrap_generator": "161bade1f822dcd62ab712ebaf30f3922a301e48a639e4295c5685f8bece7bd9",
|
||||
|
71
chia/wallet/puzzles/p2_singleton_aggregator.clsp
Normal file
71
chia/wallet/puzzles/p2_singleton_aggregator.clsp
Normal file
@ -0,0 +1,71 @@
|
||||
;; Works with p2_singleton_via_delegated_puzzle
|
||||
;; When we have many p2_singleton coins and want to aggregate them together
|
||||
|
||||
|
||||
(mod
|
||||
(
|
||||
my_id
|
||||
my_puzhash
|
||||
my_amount
|
||||
list_of_parent_puzhash_amounts ; list of (parent_id puzhash amount) for the merge
|
||||
output_parent_amount ; (parent_id amount) of the coin creating the output
|
||||
)
|
||||
|
||||
(include condition_codes.clib)
|
||||
(include curry-and-treehash.clib)
|
||||
(include *standard-cl-21*)
|
||||
|
||||
(defun cons_announcements_to_output (coin_id output)
|
||||
(c
|
||||
(list ASSERT_COIN_ANNOUNCEMENT (sha256 coin_id 0))
|
||||
output
|
||||
)
|
||||
)
|
||||
|
||||
(defun for_parent_puzhash_amounts
|
||||
(
|
||||
my_puzhash
|
||||
(@ coin_info_list ((@ first (parent puzhash amount)) . rest))
|
||||
total
|
||||
)
|
||||
(if coin_info_list
|
||||
(cons_announcements_to_output
|
||||
(calculate_coin_id parent puzhash amount)
|
||||
(for_parent_puzhash_amounts my_puzhash rest (+ total amount))
|
||||
)
|
||||
(list
|
||||
(list ASSERT_HEIGHT_RELATIVE 5) ; TODO: should this be higher or lower?
|
||||
(list CREATE_COIN my_puzhash total (list my_puzhash))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
(defun-inline give_self_to_merge (my_puzhash (@ output_info (parent amount)))
|
||||
;; Coins being merged are asserting the output coin id, and insisting it has the same puzhash as them
|
||||
;; This ensures that the puzzle which issues the CREATE_COIN condition is the same as this puzzle.
|
||||
(list
|
||||
(list CREATE_COIN_ANNOUNCEMENT 0)
|
||||
(list ASSERT_COIN_ANNOUNCEMENT (sha256 (calculate_coin_id parent my_puzhash amount) 0))
|
||||
)
|
||||
)
|
||||
|
||||
(c
|
||||
(list ASSERT_MY_AMOUNT my_amount)
|
||||
(c
|
||||
(list ASSERT_MY_PUZZLEHASH my_puzhash)
|
||||
(if list_of_parent_puzhash_amounts
|
||||
; we are making the output
|
||||
(c
|
||||
(list ASSERT_MY_COIN_ID my_id)
|
||||
(c
|
||||
(list CREATE_COIN_ANNOUNCEMENT 0)
|
||||
(for_parent_puzhash_amounts my_puzhash list_of_parent_puzhash_amounts my_amount)
|
||||
)
|
||||
)
|
||||
; we are letting another coin make the output
|
||||
(give_self_to_merge my_puzhash output_parent_amount)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
)
|
1
chia/wallet/puzzles/p2_singleton_aggregator.clsp.hex
Normal file
1
chia/wallet/puzzles/p2_singleton_aggregator.clsp.hex
Normal file
@ -0,0 +1 @@
|
||||
ff02ffff01ff04ffff04ffff0149ffff04ff17ffff01808080ffff04ffff04ffff0148ffff04ff0bffff01808080ffff02ffff03ff2fffff01ff02ffff01ff04ffff04ffff0146ffff04ff05ffff01808080ffff04ffff04ffff013cffff04ffff0180ffff01808080ffff02ff0effff04ff02ffff04ff0bffff04ff2fffff04ff17ff8080808080808080ff0180ffff01ff02ffff01ff04ffff04ffff013cffff04ffff0180ffff01808080ffff04ffff04ffff013dffff04ffff0bffff02ff04ffff04ff02ffff04ffff05ff5f80ffff04ff0bffff04ffff05ffff06ff5f8080ff808080808080ffff018080ffff01808080ffff01808080ff018080ff01808080ffff04ffff01ffff02ffff03ffff22ffff09ffff0dff0580ffff012080ffff09ffff0dff0b80ffff012080ffff15ff17ffff0181ff8080ffff01ff02ffff01ff0bff05ff0bff1780ff0180ffff01ff02ffff01ff0880ff018080ff0180ffff04ffff04ffff013dffff04ffff0bff05ff8080ff808080ff0b80ff02ffff03ff0bffff01ff02ffff01ff02ff0affff04ff02ffff04ffff02ff04ffff04ff02ffff04ff23ffff04ff53ffff04ff8200b3ff808080808080ffff04ffff02ff0effff04ff02ffff04ff05ffff04ff1bffff04ffff10ff17ff8200b380ff808080808080ff8080808080ff0180ffff01ff02ffff01ff04ffff04ffff0152ffff04ffff0105ffff01808080ffff04ffff04ffff0133ffff04ff05ffff04ff17ffff04ffff04ff05ffff018080ffff018080808080ffff01808080ff018080ff0180ff018080
|
47
chia/wallet/puzzles/p2_singleton_via_delegated_puzzle.clsp
Normal file
47
chia/wallet/puzzles/p2_singleton_via_delegated_puzzle.clsp
Normal file
@ -0,0 +1,47 @@
|
||||
;; This puzzle holds an amount which can be spent via two spend paths:
|
||||
;; 1. to a delegated puzzle provided our owner singleton creates a puzzle announcement of this coin's id and the delegated puzzle.
|
||||
;; 2. coins of this puzzle type can be merged together without the owner singleton's permission. This spend type is useful for DAOs which use this puzzle to custody funds and want to keep a reasonable limit on the number of coins tracked by DAO wallets.
|
||||
;; The AGGREGATOR_PUZZLE is curried in to preserve generality and so its logic can be updated without requiring any change to the spend to delegated path. Optionally the Aggregator puzzle can be `(x)` to close off this spend path
|
||||
|
||||
(mod (
|
||||
SINGLETON_STRUCT
|
||||
AGGREGATOR_PUZZLE
|
||||
aggregator_solution ; (my_id my_puzhash list_of_parent_puzhash_amounts my_amount)
|
||||
singleton_inner_puzhash
|
||||
delegated_puzzle
|
||||
delegated_solution
|
||||
my_id
|
||||
)
|
||||
|
||||
(include condition_codes.clib)
|
||||
(include curry-and-treehash.clib)
|
||||
|
||||
(defun-inline calculate_full_puzzle_hash (SINGLETON_STRUCT singleton_inner_puzhash)
|
||||
(puzzle-hash-of-curried-function (f SINGLETON_STRUCT)
|
||||
singleton_inner_puzhash
|
||||
(sha256tree SINGLETON_STRUCT)
|
||||
)
|
||||
)
|
||||
|
||||
(if aggregator_solution
|
||||
; we are merging coins to make a larger coin
|
||||
(a AGGREGATOR_PUZZLE aggregator_solution)
|
||||
; we are being spent by our singleton
|
||||
(c
|
||||
(list
|
||||
ASSERT_PUZZLE_ANNOUNCEMENT
|
||||
(sha256
|
||||
(calculate_full_puzzle_hash SINGLETON_STRUCT singleton_inner_puzhash)
|
||||
(sha256tree (list my_id (sha256tree delegated_puzzle)))
|
||||
)
|
||||
)
|
||||
(c
|
||||
(list CREATE_COIN_ANNOUNCEMENT '$')
|
||||
(c
|
||||
(list ASSERT_MY_COIN_ID my_id)
|
||||
(a delegated_puzzle delegated_solution)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
@ -0,0 +1 @@
|
||||
ff02ffff01ff02ffff03ff17ffff01ff02ff0bff1780ffff01ff04ffff04ff18ffff04ffff0bffff02ff2effff04ff02ffff04ff09ffff04ff2fffff04ffff02ff3effff04ff02ffff04ff05ff80808080ff808080808080ffff02ff3effff04ff02ffff04ffff04ff82017fffff04ffff02ff3effff04ff02ffff04ff5fff80808080ff808080ff8080808080ff808080ffff04ffff04ff2cffff01ff248080ffff04ffff04ff10ffff04ff82017fff808080ffff02ff5fff81bf8080808080ff0180ffff04ffff01ffffff463fff02ff3c04ffff01ff0102ffff02ffff03ff05ffff01ff02ff16ffff04ff02ffff04ff0dffff04ffff0bff3affff0bff12ff3c80ffff0bff3affff0bff3affff0bff12ff2a80ff0980ffff0bff3aff0bffff0bff12ff8080808080ff8080808080ffff010b80ff0180ffff0bff3affff0bff12ff1480ffff0bff3affff0bff3affff0bff12ff2a80ff0580ffff0bff3affff02ff16ffff04ff02ffff04ff07ffff04ffff0bff12ff1280ff8080808080ffff0bff12ff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080
|
@ -2,6 +2,8 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from chia_rs import Coin
|
||||
|
||||
from chia.types.blockchain_format.program import Program
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.types.spend_bundle import SpendBundle
|
||||
@ -15,6 +17,7 @@ from chia.wallet.cat_wallet.cat_utils import (
|
||||
unsigned_spend_bundle_for_spendable_cats,
|
||||
)
|
||||
from chia.wallet.cat_wallet.lineage_store import CATLineageStore
|
||||
from chia.wallet.dao_wallet.dao_utils import create_cat_launcher_for_singleton_id
|
||||
from chia.wallet.lineage_proof import LineageProof
|
||||
from chia.wallet.payment import Payment
|
||||
from chia.wallet.puzzles.load_clvm import load_clvm_maybe_recompile
|
||||
@ -33,6 +36,9 @@ EVERYTHING_WITH_SIG_MOD = load_clvm_maybe_recompile(
|
||||
DELEGATED_LIMITATIONS_MOD = load_clvm_maybe_recompile(
|
||||
"delegated_tail.clsp", package_or_requirement="chia.wallet.cat_wallet.puzzles"
|
||||
)
|
||||
GENESIS_BY_ID_OR_SINGLETON_MOD = load_clvm_maybe_recompile(
|
||||
"genesis_by_coin_id_or_singleton.clsp", package_or_requirement="chia.wallet.cat_wallet.puzzles"
|
||||
)
|
||||
|
||||
|
||||
class LimitationsProgram:
|
||||
@ -202,6 +208,93 @@ class DelegatedLimitations(LimitationsProgram):
|
||||
)
|
||||
|
||||
|
||||
class GenesisByIdOrSingleton(LimitationsProgram):
|
||||
"""
|
||||
This TAIL allows for another TAIL to be used, as long as a signature of that TAIL's puzzlehash is included.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def match(uncurried_mod: Program, curried_args: Program) -> Tuple[bool, List[Program]]: # pragma: no cover
|
||||
if uncurried_mod == GENESIS_BY_ID_OR_SINGLETON_MOD:
|
||||
genesis_id = curried_args.first()
|
||||
return True, [genesis_id.as_atom()]
|
||||
else:
|
||||
return False, []
|
||||
|
||||
@staticmethod
|
||||
def construct(args: List[Program]) -> Program:
|
||||
return GENESIS_BY_ID_OR_SINGLETON_MOD.curry(
|
||||
args[0],
|
||||
args[1],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def solve(args: List[Program], solution_dict: Dict) -> Program: # pragma: no cover
|
||||
pid = hexstr_to_bytes(solution_dict["parent_coin_info"])
|
||||
return Program.to([pid, solution_dict["amount"]])
|
||||
|
||||
@classmethod
|
||||
async def generate_issuance_bundle(
|
||||
cls, wallet, tail_info: Dict, amount: uint64, tx_config: TXConfig, fee: uint64 = uint64(0)
|
||||
) -> Tuple[TransactionRecord, SpendBundle]:
|
||||
if "coins" in tail_info:
|
||||
coins: List[Coin] = tail_info["coins"]
|
||||
origin_id = coins.copy().pop().name()
|
||||
else: # pragma: no cover
|
||||
coins = await wallet.standard_wallet.select_coins(amount + fee, tx_config.coin_selection_config)
|
||||
origin = coins.copy().pop()
|
||||
origin_id = origin.name()
|
||||
|
||||
cat_inner: Program = await wallet.get_new_inner_puzzle()
|
||||
# GENESIS_ID
|
||||
# TREASURY_SINGLETON_STRUCT ; (SINGLETON_MOD_HASH, (LAUNCHER_ID, LAUNCHER_PUZZLE_HASH))
|
||||
launcher_puzhash = create_cat_launcher_for_singleton_id(tail_info["treasury_id"]).get_tree_hash()
|
||||
tail: Program = cls.construct(
|
||||
[
|
||||
Program.to(origin_id),
|
||||
Program.to(launcher_puzhash),
|
||||
]
|
||||
)
|
||||
|
||||
wallet.lineage_store = await CATLineageStore.create(
|
||||
wallet.wallet_state_manager.db_wrapper, tail.get_tree_hash().hex()
|
||||
)
|
||||
await wallet.add_lineage(origin_id, LineageProof())
|
||||
|
||||
minted_cat_puzzle_hash: bytes32 = construct_cat_puzzle(CAT_MOD, tail.get_tree_hash(), cat_inner).get_tree_hash()
|
||||
|
||||
tx_records: List[TransactionRecord] = await wallet.standard_wallet.generate_signed_transaction(
|
||||
amount, minted_cat_puzzle_hash, tx_config, fee, coins=set(coins), origin_id=origin_id
|
||||
)
|
||||
tx_record: TransactionRecord = tx_records[0]
|
||||
assert tx_record.spend_bundle is not None
|
||||
payment = Payment(cat_inner.get_tree_hash(), amount)
|
||||
inner_solution = wallet.standard_wallet.add_condition_to_solution(
|
||||
Program.to([51, 0, -113, tail, []]),
|
||||
wallet.standard_wallet.make_solution(
|
||||
primaries=[payment],
|
||||
),
|
||||
)
|
||||
eve_spend = unsigned_spend_bundle_for_spendable_cats(
|
||||
CAT_MOD,
|
||||
[
|
||||
SpendableCAT(
|
||||
list(filter(lambda a: a.amount == amount, tx_record.additions))[0],
|
||||
tail.get_tree_hash(),
|
||||
cat_inner,
|
||||
inner_solution,
|
||||
limitations_program_reveal=tail,
|
||||
)
|
||||
],
|
||||
)
|
||||
signed_eve_spend = await wallet.sign(eve_spend)
|
||||
|
||||
if wallet.cat_info.my_tail is None:
|
||||
await wallet.save_info(CATInfo(tail.get_tree_hash(), tail))
|
||||
|
||||
return tx_record, SpendBundle.aggregate([tx_record.spend_bundle, signed_eve_spend])
|
||||
|
||||
|
||||
# This should probably be much more elegant than just a dictionary with strings as identifiers
|
||||
# Right now this is small and experimental so it can stay like this
|
||||
ALL_LIMITATIONS_PROGRAMS: Dict[str, Any] = {
|
||||
@ -209,6 +302,7 @@ ALL_LIMITATIONS_PROGRAMS: Dict[str, Any] = {
|
||||
"genesis_by_puzhash": GenesisByPuzhash,
|
||||
"everything_with_signature": EverythingWithSig,
|
||||
"delegated_limitations": DelegatedLimitations,
|
||||
"genesis_by_id_or_singleton": GenesisByIdOrSingleton,
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,9 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from chia.types.blockchain_format.coin import Coin
|
||||
from chia.types.blockchain_format.program import Program
|
||||
from chia.types.blockchain_format.serialized_program import SerializedProgram
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.types.coin_spend import CoinSpend, compute_additions
|
||||
from chia.wallet.puzzles.load_clvm import load_clvm_maybe_recompile
|
||||
from chia.wallet.util.curry_and_treehash import calculate_hash_of_quoted_mod_hash, curry_and_treehash
|
||||
|
||||
@ -14,7 +17,7 @@ SINGLETON_LAUNCHER_PUZZLE = load_clvm_maybe_recompile("singleton_launcher.clsp")
|
||||
SINGLETON_LAUNCHER_PUZZLE_HASH = SINGLETON_LAUNCHER_PUZZLE.get_tree_hash()
|
||||
|
||||
|
||||
def get_inner_puzzle_from_singleton(puzzle: Program) -> Optional[Program]:
|
||||
def get_inner_puzzle_from_singleton(puzzle: Union[Program, SerializedProgram]) -> Optional[Program]:
|
||||
"""
|
||||
Extract the inner puzzle of a singleton
|
||||
:param puzzle: Singleton puzzle
|
||||
@ -30,7 +33,23 @@ def get_inner_puzzle_from_singleton(puzzle: Program) -> Optional[Program]:
|
||||
return Program(INNER_PUZZLE)
|
||||
|
||||
|
||||
def is_singleton(inner_f: Program) -> bool:
|
||||
def get_singleton_id_from_puzzle(puzzle: Union[Program, SerializedProgram]) -> Optional[bytes32]:
|
||||
"""
|
||||
Extract the singleton ID from a singleton puzzle
|
||||
:param puzzle: Singleton puzzle
|
||||
:return: Inner puzzle
|
||||
"""
|
||||
r = puzzle.uncurry()
|
||||
if r is None:
|
||||
return None # pragma: no cover
|
||||
inner_f, args = r
|
||||
if not is_singleton(inner_f):
|
||||
return None
|
||||
SINGLETON_STRUCT, INNER_PUZZLE = list(args.as_iter())
|
||||
return bytes32(Program(SINGLETON_STRUCT).rest().first().as_atom())
|
||||
|
||||
|
||||
def is_singleton(inner_f: Union[Program, SerializedProgram]) -> bool:
|
||||
"""
|
||||
Check if a puzzle is a singleton mod
|
||||
:param inner_f: puzzle
|
||||
@ -52,7 +71,7 @@ def create_singleton_puzzle_hash(innerpuz_hash: bytes32, launcher_id: bytes32) -
|
||||
return curry_and_treehash(SINGLETON_TOP_LAYER_MOD_HASH_QUOTED, singleton_struct.get_tree_hash(), innerpuz_hash)
|
||||
|
||||
|
||||
def create_singleton_puzzle(innerpuz: Program, launcher_id: bytes32) -> Program:
|
||||
def create_singleton_puzzle(innerpuz: Union[Program, SerializedProgram], launcher_id: bytes32) -> Program:
|
||||
"""
|
||||
Create a full Singleton puzzle
|
||||
:param innerpuz: Singleton inner puzzle
|
||||
@ -62,3 +81,16 @@ def create_singleton_puzzle(innerpuz: Program, launcher_id: bytes32) -> Program:
|
||||
# singleton_struct = (MOD_HASH . (LAUNCHER_ID . LAUNCHER_PUZZLE_HASH))
|
||||
singleton_struct = Program.to((SINGLETON_TOP_LAYER_MOD_HASH, (launcher_id, SINGLETON_LAUNCHER_PUZZLE_HASH)))
|
||||
return SINGLETON_TOP_LAYER_MOD.curry(singleton_struct, innerpuz)
|
||||
|
||||
|
||||
def get_most_recent_singleton_coin_from_coin_spend(coin_sol: CoinSpend) -> Optional[Coin]:
|
||||
additions: List[Coin] = compute_additions(coin_sol)
|
||||
for coin in additions:
|
||||
if coin.amount % 2 == 1:
|
||||
return coin
|
||||
return None # pragma: no cover
|
||||
|
||||
|
||||
def get_singleton_struct_for_id(id: bytes32) -> Program:
|
||||
singleton_struct: Program = Program.to((SINGLETON_TOP_LAYER_MOD_HASH, (id, SINGLETON_LAUNCHER_PUZZLE_HASH)))
|
||||
return singleton_struct
|
||||
|
30
chia/wallet/singleton_record.py
Normal file
30
chia/wallet/singleton_record.py
Normal file
@ -0,0 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
|
||||
from chia.types.blockchain_format.coin import Coin
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.types.coin_spend import CoinSpend
|
||||
from chia.util.ints import uint32
|
||||
from chia.wallet.lineage_proof import LineageProof
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SingletonRecord:
|
||||
"""
|
||||
These are values that correspond to a singleton in the WalletSingletonStore
|
||||
"""
|
||||
|
||||
coin: Coin
|
||||
singleton_id: bytes32
|
||||
wallet_id: uint32
|
||||
parent_coinspend: CoinSpend
|
||||
inner_puzzle_hash: Optional[bytes32]
|
||||
pending: bool
|
||||
removed_height: int
|
||||
lineage_proof: LineageProof
|
||||
custom_data: Optional[Any]
|
||||
|
||||
def name(self) -> bytes32: # pragma: no cover
|
||||
return self.coin.name()
|
@ -26,6 +26,8 @@ class WalletType(IntEnum):
|
||||
DATA_LAYER = 11
|
||||
DATA_LAYER_OFFER = 12
|
||||
VC = 13
|
||||
DAO = 14
|
||||
DAO_CAT = 15
|
||||
CRCAT = 57
|
||||
|
||||
|
||||
|
@ -283,6 +283,7 @@ class WalletNode:
|
||||
"trade_record_times",
|
||||
"tx_times",
|
||||
"pool_state_transitions",
|
||||
"singletons",
|
||||
"singleton_records",
|
||||
"mirrors",
|
||||
"launchers",
|
||||
|
260
chia/wallet/wallet_singleton_store.py
Normal file
260
chia/wallet/wallet_singleton_store.py
Normal file
@ -0,0 +1,260 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from sqlite3 import Row
|
||||
from typing import List, Optional, Type, TypeVar, Union
|
||||
|
||||
from clvm.casts import int_from_bytes
|
||||
|
||||
from chia.consensus.default_constants import DEFAULT_CONSTANTS
|
||||
from chia.types.blockchain_format.coin import Coin
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.types.coin_spend import CoinSpend
|
||||
from chia.types.condition_opcodes import ConditionOpcode
|
||||
from chia.util.condition_tools import conditions_dict_for_solution
|
||||
from chia.util.db_wrapper import DBWrapper2, execute_fetchone
|
||||
from chia.util.ints import uint32
|
||||
from chia.wallet import singleton
|
||||
from chia.wallet.lineage_proof import LineageProof
|
||||
from chia.wallet.singleton import get_inner_puzzle_from_singleton, get_singleton_id_from_puzzle
|
||||
from chia.wallet.singleton_record import SingletonRecord
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
_T_WalletSingletonStore = TypeVar("_T_WalletSingletonStore", bound="WalletSingletonStore")
|
||||
|
||||
|
||||
class WalletSingletonStore:
|
||||
db_wrapper: DBWrapper2
|
||||
|
||||
@classmethod
|
||||
async def create(cls: Type[_T_WalletSingletonStore], wrapper: DBWrapper2) -> _T_WalletSingletonStore:
|
||||
self = cls()
|
||||
self.db_wrapper = wrapper
|
||||
|
||||
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
||||
await conn.execute(
|
||||
(
|
||||
"CREATE TABLE IF NOT EXISTS singletons("
|
||||
"coin_id blob PRIMARY KEY,"
|
||||
" coin text,"
|
||||
" singleton_id blob,"
|
||||
" wallet_id int,"
|
||||
" parent_coin_spend blob,"
|
||||
" inner_puzzle_hash blob,"
|
||||
" pending tinyint,"
|
||||
" removed_height int,"
|
||||
" lineage_proof blob,"
|
||||
" custom_data blob)"
|
||||
)
|
||||
)
|
||||
|
||||
await conn.execute("CREATE INDEX IF NOT EXISTS removed_height_index on singletons(removed_height)")
|
||||
|
||||
return self
|
||||
|
||||
async def save_singleton(self, record: SingletonRecord) -> None:
|
||||
singleton_id = singleton.get_singleton_id_from_puzzle(record.parent_coinspend.puzzle_reveal)
|
||||
if singleton_id is None: # pragma: no cover
|
||||
raise RuntimeError(
|
||||
"Failed to derive Singleton ID from puzzle reveal in parent spend %s", record.parent_coinspend
|
||||
)
|
||||
pending_int = 0
|
||||
if record.pending:
|
||||
pending_int = 1
|
||||
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
||||
columns = (
|
||||
"coin_id, coin, singleton_id, wallet_id, parent_coin_spend, inner_puzzle_hash, "
|
||||
"pending, removed_height, lineage_proof, custom_data"
|
||||
)
|
||||
await conn.execute(
|
||||
f"INSERT or REPLACE INTO singletons ({columns}) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
record.coin.name().hex(),
|
||||
json.dumps(record.coin.to_json_dict()),
|
||||
singleton_id.hex(),
|
||||
record.wallet_id,
|
||||
bytes(record.parent_coinspend),
|
||||
record.inner_puzzle_hash,
|
||||
pending_int,
|
||||
record.removed_height,
|
||||
bytes(record.lineage_proof),
|
||||
record.custom_data,
|
||||
),
|
||||
)
|
||||
|
||||
async def add_spend(
|
||||
self,
|
||||
wallet_id: uint32,
|
||||
coin_state: CoinSpend,
|
||||
block_height: uint32 = uint32(0),
|
||||
pending: bool = True,
|
||||
) -> None:
|
||||
"""Given a coin spend of a singleton, attempt to calculate the child coin and details
|
||||
for the new singleton record. Add the new record to the store and remove the old record
|
||||
if it exists
|
||||
"""
|
||||
# get singleton_id from puzzle_reveal
|
||||
singleton_id = get_singleton_id_from_puzzle(coin_state.puzzle_reveal)
|
||||
if not singleton_id:
|
||||
raise RuntimeError("Coin to add is not a valid singleton")
|
||||
|
||||
# get details for singleton record
|
||||
conditions = conditions_dict_for_solution(
|
||||
coin_state.puzzle_reveal.to_program(),
|
||||
coin_state.solution.to_program(),
|
||||
DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM,
|
||||
)
|
||||
|
||||
cc_cond = [cond for cond in conditions[ConditionOpcode.CREATE_COIN] if int_from_bytes(cond.vars[1]) % 2 == 1][0]
|
||||
|
||||
coin = Coin(coin_state.coin.name(), cc_cond.vars[0], int_from_bytes(cc_cond.vars[1]))
|
||||
inner_puz = get_inner_puzzle_from_singleton(coin_state.puzzle_reveal)
|
||||
if inner_puz is None: # pragma: no cover
|
||||
raise RuntimeError("Could not get inner puzzle from puzzle reveal in coin spend %s", coin_state)
|
||||
|
||||
lineage_bytes = [x.as_atom() for x in coin_state.solution.to_program().first().as_iter()]
|
||||
if len(lineage_bytes) == 2:
|
||||
lineage_proof = LineageProof(lineage_bytes[0], None, int_from_bytes(lineage_bytes[1]))
|
||||
else:
|
||||
lineage_proof = LineageProof(lineage_bytes[0], lineage_bytes[1], int_from_bytes(lineage_bytes[2]))
|
||||
# Create and save the new singleton record
|
||||
new_record = SingletonRecord(
|
||||
coin, singleton_id, wallet_id, coin_state, inner_puz.get_tree_hash(), pending, 0, lineage_proof, None
|
||||
)
|
||||
await self.save_singleton(new_record)
|
||||
# check if coin is in DB and mark deleted if found
|
||||
current_records = await self.get_records_by_coin_id(coin_state.coin.name())
|
||||
if len(current_records) > 0:
|
||||
await self.delete_singleton_by_coin_id(coin_state.coin.name(), block_height)
|
||||
return
|
||||
|
||||
def _to_singleton_record(self, row: Row) -> SingletonRecord:
|
||||
return SingletonRecord(
|
||||
coin=Coin.from_json_dict(json.loads(row[1])),
|
||||
singleton_id=bytes32.from_hexstr(row[2]),
|
||||
wallet_id=uint32(row[3]),
|
||||
parent_coinspend=CoinSpend.from_bytes(row[4]),
|
||||
inner_puzzle_hash=bytes32.from_bytes(row[5]), # inner puz hash
|
||||
pending=True if row[6] == 1 else False,
|
||||
removed_height=uint32(row[7]),
|
||||
lineage_proof=LineageProof.from_bytes(row[8]),
|
||||
custom_data=row[9],
|
||||
)
|
||||
|
||||
async def delete_singleton_by_singleton_id(self, singleton_id: bytes32, height: uint32) -> bool:
|
||||
"""Tries to mark a given singleton as deleted at specific height
|
||||
|
||||
This is due to how re-org works
|
||||
Returns `True` if singleton was found and marked deleted or `False` if not."""
|
||||
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
||||
cursor = await conn.execute(
|
||||
"UPDATE singletons SET removed_height=? WHERE singleton_id=?", (int(height), singleton_id.hex())
|
||||
)
|
||||
if cursor.rowcount > 0:
|
||||
log.info("Deleted singleton with singleton id: %s", singleton_id.hex())
|
||||
return True
|
||||
log.warning("Couldn't find singleton with singleton id to delete: %s", singleton_id.hex())
|
||||
return False
|
||||
|
||||
async def delete_singleton_by_coin_id(self, coin_id: bytes32, height: uint32) -> bool:
|
||||
"""Tries to mark a given singleton as deleted at specific height
|
||||
|
||||
This is due to how re-org works
|
||||
Returns `True` if singleton was found and marked deleted or `False` if not."""
|
||||
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
||||
cursor = await conn.execute(
|
||||
"UPDATE singletons SET removed_height=? WHERE coin_id=?", (int(height), coin_id.hex())
|
||||
)
|
||||
if cursor.rowcount > 0:
|
||||
log.info("Deleted singleton with coin id: %s", coin_id.hex())
|
||||
return True
|
||||
log.warning("Couldn't find singleton with coin id to delete: %s", coin_id.hex())
|
||||
return False
|
||||
|
||||
async def delete_wallet(self, wallet_id: uint32) -> None:
|
||||
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
||||
cursor = await conn.execute("DELETE FROM singletons WHERE wallet_id=?", (wallet_id,))
|
||||
await cursor.close()
|
||||
|
||||
async def update_pending_transaction(self, coin_id: bytes32, pending: bool) -> bool:
|
||||
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
||||
c = await conn.execute(
|
||||
"UPDATE singletons SET pending=? WHERE coin_id = ?",
|
||||
(pending, coin_id.hex()),
|
||||
)
|
||||
return c.rowcount > 0
|
||||
|
||||
async def get_records_by_wallet_id(self, wallet_id: int) -> List[SingletonRecord]:
|
||||
"""
|
||||
Retrieves all entries for a wallet ID.
|
||||
"""
|
||||
|
||||
async with self.db_wrapper.reader_no_transaction() as conn:
|
||||
rows = await conn.execute_fetchall(
|
||||
"SELECT * FROM singletons WHERE wallet_id = ? ORDER BY removed_height",
|
||||
(wallet_id,),
|
||||
)
|
||||
return [self._to_singleton_record(row) for row in rows]
|
||||
|
||||
async def get_records_by_coin_id(self, coin_id: bytes32) -> List[SingletonRecord]:
|
||||
"""
|
||||
Retrieves all entries for a coin ID.
|
||||
"""
|
||||
|
||||
async with self.db_wrapper.reader_no_transaction() as conn:
|
||||
rows = await conn.execute_fetchall(
|
||||
"SELECT * FROM singletons WHERE coin_id = ?",
|
||||
(coin_id.hex(),),
|
||||
)
|
||||
return [self._to_singleton_record(row) for row in rows]
|
||||
|
||||
async def get_records_by_singleton_id(self, singleton_id: bytes32) -> List[SingletonRecord]:
|
||||
"""
|
||||
Retrieves all entries for a singleton ID.
|
||||
"""
|
||||
|
||||
async with self.db_wrapper.reader_no_transaction() as conn:
|
||||
rows = await conn.execute_fetchall(
|
||||
"SELECT * FROM singletons WHERE singleton_id = ? ORDER BY removed_height",
|
||||
(singleton_id.hex(),),
|
||||
)
|
||||
return [self._to_singleton_record(row) for row in rows]
|
||||
|
||||
async def rollback(self, height: int, wallet_id_arg: int) -> None:
|
||||
"""
|
||||
Rollback removes all entries which have entry_height > height passed in. Note that this is not committed to the
|
||||
DB until db_wrapper.commit() is called. However, it is written to the cache, so it can be fetched with
|
||||
get_all_state_transitions.
|
||||
"""
|
||||
|
||||
async with self.db_wrapper.writer_maybe_transaction() as conn:
|
||||
cursor = await conn.execute(
|
||||
"DELETE FROM singletons WHERE removed_height>? AND wallet_id=?", (height, wallet_id_arg)
|
||||
)
|
||||
await cursor.close()
|
||||
|
||||
async def count(self, wallet_id: Optional[uint32] = None) -> int:
|
||||
sql = "SELECT COUNT(singleton_id) FROM singletons WHERE removed_height=0"
|
||||
params: List[uint32] = []
|
||||
if wallet_id is not None:
|
||||
sql += " AND wallet_id=?"
|
||||
params.append(wallet_id)
|
||||
async with self.db_wrapper.reader_no_transaction() as conn:
|
||||
count_row = await execute_fetchone(conn, sql, params)
|
||||
if count_row:
|
||||
return int(count_row[0])
|
||||
return -1 # pragma: no cover
|
||||
|
||||
async def is_empty(self, wallet_id: Optional[uint32] = None) -> bool:
|
||||
sql = "SELECT 1 FROM singletons WHERE removed_height=0"
|
||||
params: List[Union[uint32, bytes32]] = []
|
||||
if wallet_id is not None:
|
||||
sql += " AND wallet_id=?"
|
||||
params.append(wallet_id)
|
||||
sql += " LIMIT 1"
|
||||
async with self.db_wrapper.reader_no_transaction() as conn:
|
||||
count_row = await execute_fetchone(conn, sql, params)
|
||||
if count_row:
|
||||
return False
|
||||
return True
|
@ -7,7 +7,21 @@ import time
|
||||
import traceback
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, AsyncIterator, Callable, Dict, List, Optional, Set, Tuple, Type, TypeVar, Union
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
AsyncIterator,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
import aiosqlite
|
||||
from blspy import G1Element, G2Element, PrivateKey
|
||||
@ -50,7 +64,17 @@ from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS
|
||||
from chia.wallet.cat_wallet.cat_info import CATCoinData, CATInfo, CRCATInfo
|
||||
from chia.wallet.cat_wallet.cat_utils import CAT_MOD, CAT_MOD_HASH, construct_cat_puzzle, match_cat_puzzle
|
||||
from chia.wallet.cat_wallet.cat_wallet import CATWallet
|
||||
from chia.wallet.cat_wallet.dao_cat_wallet import DAOCATWallet
|
||||
from chia.wallet.conditions import Condition, ConditionValidTimes, parse_timelock_info
|
||||
from chia.wallet.dao_wallet.dao_utils import (
|
||||
get_p2_singleton_puzhash,
|
||||
match_dao_cat_puzzle,
|
||||
match_finished_puzzle,
|
||||
match_funding_puzzle,
|
||||
match_proposal_puzzle,
|
||||
match_treasury_puzzle,
|
||||
)
|
||||
from chia.wallet.dao_wallet.dao_wallet import DAOWallet
|
||||
from chia.wallet.db_wallet.db_wallet_puzzles import MIRROR_PUZZLE_HASH
|
||||
from chia.wallet.derivation_record import DerivationRecord
|
||||
from chia.wallet.derive_keys import (
|
||||
@ -80,7 +104,7 @@ from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
|
||||
puzzle_hash_for_synthetic_public_key,
|
||||
)
|
||||
from chia.wallet.sign_coin_spends import sign_coin_spends
|
||||
from chia.wallet.singleton import create_singleton_puzzle, get_inner_puzzle_from_singleton
|
||||
from chia.wallet.singleton import create_singleton_puzzle, get_inner_puzzle_from_singleton, get_singleton_id_from_puzzle
|
||||
from chia.wallet.trade_manager import TradeManager
|
||||
from chia.wallet.trading.trade_status import TradeStatus
|
||||
from chia.wallet.transaction_record import TransactionRecord
|
||||
@ -114,6 +138,7 @@ from chia.wallet.wallet_pool_store import WalletPoolStore
|
||||
from chia.wallet.wallet_protocol import WalletProtocol
|
||||
from chia.wallet.wallet_puzzle_store import WalletPuzzleStore
|
||||
from chia.wallet.wallet_retry_store import WalletRetryStore
|
||||
from chia.wallet.wallet_singleton_store import WalletSingletonStore
|
||||
from chia.wallet.wallet_transaction_store import WalletTransactionStore
|
||||
from chia.wallet.wallet_user_store import WalletUserStore
|
||||
|
||||
@ -127,6 +152,8 @@ PendingTxCallback = Callable[[], None]
|
||||
|
||||
|
||||
class WalletStateManager:
|
||||
interested_ph_cache: Dict[bytes32, List[int]] = {}
|
||||
interested_coin_cache: Dict[bytes32, List[int]] = {}
|
||||
constants: ConsensusConstants
|
||||
config: Dict[str, Any]
|
||||
tx_store: WalletTransactionStore
|
||||
@ -165,6 +192,7 @@ class WalletStateManager:
|
||||
wallet_node: WalletNode
|
||||
pool_store: WalletPoolStore
|
||||
dl_store: DataLayerStore
|
||||
singleton_store: WalletSingletonStore
|
||||
default_cats: Dict[str, Any]
|
||||
asset_to_wallet_map: Dict[AssetType, Any]
|
||||
initial_num_public_keys: int
|
||||
@ -181,6 +209,7 @@ class WalletStateManager:
|
||||
wallet_node: WalletNode,
|
||||
) -> WalletStateManager:
|
||||
self = WalletStateManager()
|
||||
|
||||
self.config = config
|
||||
self.constants = constants
|
||||
self.server = server
|
||||
@ -219,6 +248,7 @@ class WalletStateManager:
|
||||
self.dl_store = await DataLayerStore.create(self.db_wrapper)
|
||||
self.interested_store = await WalletInterestedStore.create(self.db_wrapper)
|
||||
self.retry_store = await WalletRetryStore.create(self.db_wrapper)
|
||||
self.singleton_store = await WalletSingletonStore.create(self.db_wrapper)
|
||||
self.default_cats = DEFAULT_CATS
|
||||
|
||||
self.wallet_node = wallet_node
|
||||
@ -273,8 +303,23 @@ class WalletStateManager:
|
||||
self.main_wallet,
|
||||
wallet_info,
|
||||
)
|
||||
elif wallet_type == WalletType.DATA_LAYER:
|
||||
wallet = await DataLayerWallet.create(self, wallet_info)
|
||||
elif wallet_type == WalletType.DATA_LAYER: # pragma: no cover
|
||||
wallet = await DataLayerWallet.create(
|
||||
self,
|
||||
wallet_info,
|
||||
)
|
||||
elif wallet_type == WalletType.DAO: # pragma: no cover
|
||||
wallet = await DAOWallet.create(
|
||||
self,
|
||||
self.main_wallet,
|
||||
wallet_info,
|
||||
)
|
||||
elif wallet_type == WalletType.DAO_CAT: # pragma: no cover
|
||||
wallet = await DAOCATWallet.create(
|
||||
self,
|
||||
self.main_wallet,
|
||||
wallet_info,
|
||||
)
|
||||
elif wallet_type == WalletType.VC: # pragma: no cover
|
||||
wallet = await VCWallet.create(
|
||||
self,
|
||||
@ -712,8 +757,42 @@ class WalletStateManager:
|
||||
coin_spend = await fetch_coin_spend_for_coin_state(parent_coin_state, peer)
|
||||
|
||||
puzzle = Program.from_bytes(bytes(coin_spend.puzzle_reveal))
|
||||
solution = Program.from_bytes(bytes(coin_spend.solution))
|
||||
|
||||
uncurried = uncurry_puzzle(puzzle)
|
||||
|
||||
dao_ids = []
|
||||
wallets = self.wallets.values()
|
||||
for wallet in wallets:
|
||||
if wallet.type() == WalletType.DAO.value:
|
||||
assert isinstance(wallet, DAOWallet)
|
||||
dao_ids.append(wallet.dao_info.treasury_id)
|
||||
funding_puzzle_check = match_funding_puzzle(uncurried, solution, coin_state.coin, dao_ids)
|
||||
if funding_puzzle_check:
|
||||
return await self.get_dao_wallet_from_coinspend_hint(coin_spend, coin_state), None
|
||||
|
||||
# Check if the coin is a DAO Treasury
|
||||
dao_curried_args = match_treasury_puzzle(uncurried.mod, uncurried.args)
|
||||
if dao_curried_args is not None:
|
||||
return await self.handle_dao_treasury(dao_curried_args, parent_coin_state, coin_state, coin_spend), None
|
||||
# Check if the coin is a Proposal and that it isn't the timer coin (amount == 0)
|
||||
dao_curried_args = match_proposal_puzzle(uncurried.mod, uncurried.args)
|
||||
if (dao_curried_args is not None) and (coin_state.coin.amount != 0):
|
||||
return await self.handle_dao_proposal(dao_curried_args, parent_coin_state, coin_state, coin_spend), None
|
||||
|
||||
# Check if the coin is a finished proposal
|
||||
dao_curried_args = match_finished_puzzle(uncurried.mod, uncurried.args)
|
||||
if dao_curried_args is not None:
|
||||
return (
|
||||
await self.handle_dao_finished_proposals(dao_curried_args, parent_coin_state, coin_state, coin_spend),
|
||||
None,
|
||||
)
|
||||
|
||||
# Check if the coin is a DAO CAT
|
||||
dao_cat_args = match_dao_cat_puzzle(uncurried)
|
||||
if dao_cat_args:
|
||||
return await self.handle_dao_cat(dao_cat_args, parent_coin_state, coin_state, coin_spend), None
|
||||
|
||||
# Check if the coin is a CAT
|
||||
cat_curried_args = match_cat_puzzle(uncurried)
|
||||
if cat_curried_args is not None:
|
||||
@ -868,6 +947,8 @@ class WalletStateManager:
|
||||
)
|
||||
coin_spend: CoinSpend = generate_clawback_spend_bundle(coin, metadata, inner_puzzle, inner_solution)
|
||||
coin_spends.append(coin_spend)
|
||||
# Update incoming tx to prevent double spend and mark it is pending
|
||||
await self.tx_store.increment_sent(incoming_tx.name, "", MempoolInclusionStatus.PENDING, None)
|
||||
except Exception as e:
|
||||
self.log.error(f"Failed to create clawback spend bundle for {coin.name().hex()}: {e}")
|
||||
if len(coin_spends) == 0:
|
||||
@ -900,9 +981,6 @@ class WalletStateManager:
|
||||
valid_times=parse_timelock_info(extra_conditions),
|
||||
)
|
||||
await self.add_pending_transaction(tx_record)
|
||||
# Update incoming tx to prevent double spend and mark it is pending
|
||||
for coin_spend in coin_spends:
|
||||
await self.tx_store.increment_sent(coin_spend.coin.name(), "", MempoolInclusionStatus.PENDING, None)
|
||||
return [tx_record.name]
|
||||
|
||||
async def filter_spam(self, new_coin_state: List[CoinState]) -> List[CoinState]:
|
||||
@ -939,6 +1017,25 @@ class WalletStateManager:
|
||||
wallet_identifier = await self.get_wallet_identifier_for_puzzle_hash(coin_state.coin.puzzle_hash)
|
||||
return wallet_identifier is not None and wallet_identifier.type == WalletType.STANDARD_WALLET
|
||||
|
||||
async def handle_dao_cat(
|
||||
self,
|
||||
curried_args: Iterator[Program],
|
||||
parent_coin_state: CoinState,
|
||||
coin_state: CoinState,
|
||||
coin_spend: CoinSpend,
|
||||
) -> Optional[WalletIdentifier]:
|
||||
"""
|
||||
Handle the new coin when it is a DAO CAT
|
||||
"""
|
||||
mod_hash, tail_hash, inner_puzzle = curried_args
|
||||
asset_id: bytes32 = bytes32(bytes(tail_hash)[1:])
|
||||
for wallet in self.wallets.values():
|
||||
if wallet.type() == WalletType.DAO_CAT:
|
||||
assert isinstance(wallet, DAOCATWallet)
|
||||
if wallet.dao_cat_info.limitations_program_hash == asset_id:
|
||||
return WalletIdentifier.create(wallet)
|
||||
return None # pragma: no cover
|
||||
|
||||
async def handle_cat(
|
||||
self,
|
||||
parent_data: CATCoinData,
|
||||
@ -1072,6 +1169,7 @@ class WalletStateManager:
|
||||
if derivation_record is None:
|
||||
self.log.info(f"Received state for the coin that doesn't belong to us {coin_state}")
|
||||
# Check if it was owned by us
|
||||
# If the puzzle inside is no longer recognised then delete the wallet associated
|
||||
removed_wallet_ids = []
|
||||
for wallet in self.wallets.values():
|
||||
if not isinstance(wallet, DIDWallet):
|
||||
@ -1181,6 +1279,94 @@ class WalletStateManager:
|
||||
minter_did = bytes32(bytes(singleton_struct.rest().first())[1:])
|
||||
return minter_did
|
||||
|
||||
async def handle_dao_treasury(
|
||||
self,
|
||||
uncurried_args: Iterator[Program],
|
||||
parent_coin_state: CoinState,
|
||||
coin_state: CoinState,
|
||||
coin_spend: CoinSpend,
|
||||
) -> Optional[WalletIdentifier]:
|
||||
self.log.info("Entering dao_treasury handling in WalletStateManager")
|
||||
singleton_id = get_singleton_id_from_puzzle(coin_spend.puzzle_reveal)
|
||||
for wallet in self.wallets.values():
|
||||
if wallet.type() == WalletType.DAO:
|
||||
assert isinstance(wallet, DAOWallet)
|
||||
if wallet.dao_info.treasury_id == singleton_id:
|
||||
return WalletIdentifier.create(wallet)
|
||||
|
||||
# TODO: If we can't find the wallet for this DAO but we've got here because we're subscribed,
|
||||
# then create the wallet. (see early in dao-wallet commits for how to do this)
|
||||
return None # pragma: no cover
|
||||
|
||||
async def handle_dao_proposal(
|
||||
self,
|
||||
uncurried_args: Iterator[Program],
|
||||
parent_coin_state: CoinState,
|
||||
coin_state: CoinState,
|
||||
coin_spend: CoinSpend,
|
||||
) -> Optional[WalletIdentifier]:
|
||||
(
|
||||
# ; second hash
|
||||
SELF_HASH,
|
||||
PROPOSAL_ID,
|
||||
PROPOSED_PUZ_HASH,
|
||||
YES_VOTES,
|
||||
TOTAL_VOTES,
|
||||
# ; first hash
|
||||
PROPOSAL_TIMER_MOD_HASH,
|
||||
SINGLETON_MOD_HASH,
|
||||
SINGLETON_LAUNCHER_PUZHASH,
|
||||
CAT_MOD_HASH,
|
||||
DAO_FINISHED_STATE_MOD_HASH,
|
||||
TREASURY_MOD_HASH,
|
||||
LOCKUP_SELF_HASH,
|
||||
CAT_TAIL_HASH,
|
||||
TREASURY_ID,
|
||||
) = uncurried_args
|
||||
for wallet in self.wallets.values():
|
||||
if wallet.type() == WalletType.DAO:
|
||||
assert isinstance(wallet, DAOWallet)
|
||||
if wallet.dao_info.treasury_id == TREASURY_ID.as_atom():
|
||||
assert isinstance(coin_state.created_height, int)
|
||||
await wallet.add_or_update_proposal_info(coin_spend, uint32(coin_state.created_height))
|
||||
return WalletIdentifier.create(wallet)
|
||||
return None # pragma: no cover
|
||||
|
||||
async def handle_dao_finished_proposals(
|
||||
self,
|
||||
uncurried_args: Iterator[Program],
|
||||
parent_coin_state: CoinState,
|
||||
coin_state: CoinState,
|
||||
coin_spend: CoinSpend,
|
||||
) -> Optional[WalletIdentifier]:
|
||||
if coin_state.created_height is None: # pragma: no cover
|
||||
raise ValueError("coin_state argument to handle_dao_finished_proposals cannot have created_height of None")
|
||||
(
|
||||
SINGLETON_STRUCT, # (SINGLETON_MOD_HASH, (SINGLETON_ID, LAUNCHER_PUZZLE_HASH))
|
||||
FINISHED_STATE_MOD_HASH,
|
||||
) = uncurried_args
|
||||
proposal_id = SINGLETON_STRUCT.rest().first().as_atom()
|
||||
for wallet in self.wallets.values():
|
||||
if wallet.type() == WalletType.DAO:
|
||||
assert isinstance(wallet, DAOWallet)
|
||||
for proposal_info in wallet.dao_info.proposals_list:
|
||||
if proposal_info.proposal_id == proposal_id:
|
||||
await wallet.add_or_update_proposal_info(coin_spend, uint32(coin_state.created_height))
|
||||
return WalletIdentifier.create(wallet)
|
||||
return None
|
||||
|
||||
async def get_dao_wallet_from_coinspend_hint(
|
||||
self, coin_spend: CoinSpend, coin_state: CoinState
|
||||
) -> Optional[WalletIdentifier]:
|
||||
hinted_coin = compute_spend_hints_and_additions(coin_spend)[coin_state.coin.name()]
|
||||
if hinted_coin:
|
||||
for wallet in self.wallets.values():
|
||||
if wallet.type() == WalletType.DAO.value:
|
||||
assert isinstance(wallet, DAOWallet)
|
||||
if get_p2_singleton_puzhash(wallet.dao_info.treasury_id) == hinted_coin.hint:
|
||||
return WalletIdentifier.create(wallet)
|
||||
return None
|
||||
|
||||
async def handle_nft(
|
||||
self,
|
||||
nft_data: NFTCoinData,
|
||||
@ -1658,6 +1844,7 @@ class WalletStateManager:
|
||||
if record.coin_type == CoinType.CLAWBACK:
|
||||
await self.interested_store.remove_interested_coin_id(coin_state.coin.name())
|
||||
confirmed_tx_records: List[TransactionRecord] = []
|
||||
|
||||
for tx_record in all_unconfirmed:
|
||||
if tx_record.type in CLAWBACK_INCOMING_TRANSACTION_TYPES:
|
||||
for add_coin in tx_record.additions:
|
||||
@ -1678,14 +1865,17 @@ class WalletStateManager:
|
||||
unconfirmed_record.name, uint32(coin_state.spent_height)
|
||||
)
|
||||
|
||||
if record.wallet_type == WalletType.POOLING_WALLET:
|
||||
if record.wallet_type in [WalletType.POOLING_WALLET, WalletType.DAO]:
|
||||
wallet_type_to_class = {WalletType.POOLING_WALLET: PoolWallet, WalletType.DAO: DAOWallet}
|
||||
if coin_state.spent_height is not None and coin_state.coin.amount == uint64(1):
|
||||
pool_wallet = self.get_wallet(id=uint32(record.wallet_id), required_type=PoolWallet)
|
||||
singleton_wallet: Union[PoolWallet, DAOWallet] = self.get_wallet(
|
||||
id=uint32(record.wallet_id), required_type=wallet_type_to_class[record.wallet_type]
|
||||
)
|
||||
curr_coin_state: CoinState = coin_state
|
||||
|
||||
while curr_coin_state.spent_height is not None:
|
||||
cs = await fetch_coin_spend_for_coin_state(curr_coin_state, peer)
|
||||
success = await pool_wallet.apply_state_transition(
|
||||
cs: CoinSpend = await fetch_coin_spend_for_coin_state(curr_coin_state, peer)
|
||||
success = await singleton_wallet.apply_state_transition(
|
||||
cs, uint32(curr_coin_state.spent_height)
|
||||
)
|
||||
if not success:
|
||||
@ -1977,10 +2167,14 @@ class WalletStateManager:
|
||||
coin_record: WalletCoinRecord = WalletCoinRecord(
|
||||
coin, height, uint32(0), False, coinbase, wallet_type, wallet_id
|
||||
)
|
||||
|
||||
await self.coin_store.add_coin_record(coin_record, coin_name)
|
||||
|
||||
await self.wallets[wallet_id].coin_added(coin, height, peer, coin_data)
|
||||
|
||||
if wallet_type == WalletType.DAO:
|
||||
return
|
||||
|
||||
await self.create_more_puzzle_hashes()
|
||||
|
||||
async def add_pending_transaction(self, tx_record: TransactionRecord) -> None:
|
||||
@ -2222,12 +2416,31 @@ class WalletStateManager:
|
||||
self.tx_pending_changed()
|
||||
|
||||
async def add_interested_puzzle_hashes(self, puzzle_hashes: List[bytes32], wallet_ids: List[int]) -> None:
|
||||
# TODO: It's unclear if the intended use for this is that each puzzle hash should store all
|
||||
# the elements of wallet_ids. It only stores one wallet_id per puzzle hash in the interested_store
|
||||
# but the coin_cache keeps all wallet_ids for each puzzle hash
|
||||
for puzzle_hash in puzzle_hashes:
|
||||
if puzzle_hash in self.interested_coin_cache:
|
||||
wallet_ids_to_add = list(
|
||||
set([w for w in wallet_ids if w not in self.interested_coin_cache[puzzle_hash]])
|
||||
)
|
||||
self.interested_coin_cache[puzzle_hash].extend(wallet_ids_to_add)
|
||||
else:
|
||||
self.interested_coin_cache[puzzle_hash] = list(set(wallet_ids))
|
||||
for puzzle_hash, wallet_id in zip(puzzle_hashes, wallet_ids):
|
||||
await self.interested_store.add_interested_puzzle_hash(puzzle_hash, wallet_id)
|
||||
if len(puzzle_hashes) > 0:
|
||||
await self.wallet_node.new_peak_queue.subscribe_to_puzzle_hashes(puzzle_hashes)
|
||||
|
||||
async def add_interested_coin_ids(self, coin_ids: List[bytes32]) -> None:
|
||||
async def add_interested_coin_ids(self, coin_ids: List[bytes32], wallet_ids: List[int] = []) -> None:
|
||||
# TODO: FIX: wallet_ids is sometimes populated unexpectedly when called from add_pending_transaction
|
||||
for coin_id in coin_ids:
|
||||
if coin_id in self.interested_coin_cache:
|
||||
# prevent repeated wallet_ids from appearing in the coin cache
|
||||
wallet_ids_to_add = list(set([w for w in wallet_ids if w not in self.interested_coin_cache[coin_id]]))
|
||||
self.interested_coin_cache[coin_id].extend(wallet_ids_to_add)
|
||||
else:
|
||||
self.interested_coin_cache[coin_id] = list(set(wallet_ids))
|
||||
for coin_id in coin_ids:
|
||||
await self.interested_store.add_interested_coin_id(coin_id)
|
||||
if len(coin_ids) > 0:
|
||||
|
531
tests/cmds/wallet/test_dao.py
Normal file
531
tests/cmds/wallet/test_dao.py
Normal file
@ -0,0 +1,531 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
from secrets import token_bytes
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import pytest
|
||||
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.util.bech32m import encode_puzzle_hash
|
||||
from chia.util.ints import uint8, uint32, uint64
|
||||
from chia.wallet.conditions import parse_timelock_info
|
||||
from chia.wallet.transaction_record import TransactionRecord
|
||||
from chia.wallet.util.transaction_type import TransactionType
|
||||
from chia.wallet.util.tx_config import TXConfig
|
||||
from chia.wallet.util.wallet_types import WalletType
|
||||
from tests.cmds.cmd_test_utils import TestRpcClients, TestWalletRpcClient, run_cli_command_and_assert
|
||||
from tests.cmds.wallet.test_consts import FINGERPRINT_ARG
|
||||
|
||||
# DAO Commands
|
||||
|
||||
|
||||
def test_dao_create(capsys: object, get_test_cli_clients: Tuple[TestRpcClients, Path]) -> None:
|
||||
test_rpc_clients, root_dir = get_test_cli_clients
|
||||
|
||||
# set RPC Client
|
||||
class DAOCreateRpcClient(TestWalletRpcClient):
|
||||
async def create_new_dao_wallet(
|
||||
self,
|
||||
mode: str,
|
||||
tx_config: TXConfig,
|
||||
dao_rules: Optional[Dict[str, uint64]] = None,
|
||||
amount_of_cats: Optional[uint64] = None,
|
||||
treasury_id: Optional[bytes32] = None,
|
||||
filter_amount: uint64 = uint64(1),
|
||||
name: Optional[str] = None,
|
||||
fee: uint64 = uint64(0),
|
||||
fee_for_cat: uint64 = uint64(0),
|
||||
) -> Dict[str, Union[str, int, bytes32]]:
|
||||
if not treasury_id:
|
||||
treasury_id = bytes32(token_bytes(32))
|
||||
return {
|
||||
"success": True,
|
||||
"type": "DAO",
|
||||
"wallet_id": 2,
|
||||
"treasury_id": treasury_id,
|
||||
"cat_wallet_id": 3,
|
||||
"dao_cat_wallet_id": 4,
|
||||
}
|
||||
|
||||
inst_rpc_client = DAOCreateRpcClient() # pylint: disable=no-value-for-parameter
|
||||
test_rpc_clients.wallet_rpc_client = inst_rpc_client
|
||||
command_args = [
|
||||
"dao",
|
||||
"create",
|
||||
FINGERPRINT_ARG,
|
||||
"-n test",
|
||||
"--attendance-required",
|
||||
"1000",
|
||||
"--cat-amount",
|
||||
"100000",
|
||||
"-m0.1",
|
||||
"--reuse",
|
||||
]
|
||||
# these are various things that should be in the output
|
||||
assert_list = ["Successfully created DAO Wallet", "DAO Wallet ID: 2", "CAT Wallet ID: 3", "DAOCAT Wallet ID: 4"]
|
||||
run_cli_command_and_assert(capsys, root_dir, command_args, assert_list)
|
||||
|
||||
# Check command raises if proposal minimum is even
|
||||
odd_pm_command_args = [
|
||||
"dao",
|
||||
"create",
|
||||
FINGERPRINT_ARG,
|
||||
"-n test",
|
||||
"--attendance-required",
|
||||
"1000",
|
||||
"--cat-amount",
|
||||
"100000",
|
||||
"--proposal-minimum",
|
||||
"10",
|
||||
"-m0.1",
|
||||
"--reuse",
|
||||
]
|
||||
extra_assert_list = [
|
||||
"Adding 1 mojo to proposal minimum amount",
|
||||
]
|
||||
run_cli_command_and_assert(capsys, root_dir, odd_pm_command_args, extra_assert_list)
|
||||
|
||||
# Add wallet for existing DAO
|
||||
add_command_args = [
|
||||
"dao",
|
||||
"add",
|
||||
FINGERPRINT_ARG,
|
||||
"-n test",
|
||||
"-t",
|
||||
bytes32(token_bytes(32)).hex(),
|
||||
"--filter-amount",
|
||||
"1",
|
||||
]
|
||||
run_cli_command_and_assert(capsys, root_dir, add_command_args, assert_list)
|
||||
|
||||
|
||||
def test_dao_treasury(capsys: object, get_test_cli_clients: Tuple[TestRpcClients, Path]) -> None:
|
||||
test_rpc_clients, root_dir = get_test_cli_clients
|
||||
|
||||
class DAOCreateRpcClient(TestWalletRpcClient):
|
||||
async def dao_get_treasury_id(
|
||||
self,
|
||||
wallet_id: int,
|
||||
) -> Dict[str, str]:
|
||||
return {"treasury_id": "0xCAFEF00D"}
|
||||
|
||||
async def dao_get_treasury_balance(self, wallet_id: int) -> Dict[str, Union[str, bool, Dict[str, int]]]:
|
||||
if wallet_id == 2:
|
||||
return {"success": True, "balances": {"xch": 1000000000000, "0xCAFEF00D": 10000000}}
|
||||
else:
|
||||
return {"success": True, "balances": {}}
|
||||
|
||||
async def dao_add_funds_to_treasury(
|
||||
self,
|
||||
wallet_id: int,
|
||||
funding_wallet_id: int,
|
||||
amount: uint64,
|
||||
tx_config: TXConfig,
|
||||
fee: uint64 = uint64(0),
|
||||
reuse_puzhash: Optional[bool] = None,
|
||||
) -> Dict[str, Union[str, bool]]:
|
||||
return {"success": True, "tx_id": bytes32(b"1" * 32).hex()}
|
||||
|
||||
async def dao_get_rules(
|
||||
self,
|
||||
wallet_id: int,
|
||||
) -> Dict[str, Dict[str, int]]:
|
||||
return {"rules": {"proposal_minimum": 100}}
|
||||
|
||||
async def get_transaction(self, wallet_id: int, transaction_id: bytes32) -> TransactionRecord:
|
||||
return TransactionRecord(
|
||||
confirmed_at_height=uint32(0),
|
||||
created_at_time=uint64(int(time.time())),
|
||||
to_puzzle_hash=bytes32(b"2" * 32),
|
||||
amount=uint64(10),
|
||||
fee_amount=uint64(1),
|
||||
confirmed=True,
|
||||
sent=uint32(10),
|
||||
spend_bundle=None,
|
||||
additions=[],
|
||||
removals=[],
|
||||
wallet_id=uint32(1),
|
||||
sent_to=[("peer1", uint8(1), None)],
|
||||
trade_id=None,
|
||||
type=uint32(TransactionType.INCOMING_TX.value),
|
||||
name=bytes32(token_bytes()),
|
||||
memos=[],
|
||||
valid_times=parse_timelock_info(tuple()),
|
||||
)
|
||||
|
||||
inst_rpc_client = DAOCreateRpcClient() # pylint: disable=no-value-for-parameter
|
||||
test_rpc_clients.wallet_rpc_client = inst_rpc_client
|
||||
|
||||
get_id_args = ["dao", "get_id", FINGERPRINT_ARG, "-i 2"]
|
||||
get_id_asserts = ["Treasury ID: 0xCAFEF00D"]
|
||||
run_cli_command_and_assert(capsys, root_dir, get_id_args, get_id_asserts)
|
||||
|
||||
get_balance_args = ["dao", "balance", FINGERPRINT_ARG, "-i 2"]
|
||||
get_balance_asserts = ["XCH: 1.0", "0xCAFEF00D: 10000.0"]
|
||||
run_cli_command_and_assert(capsys, root_dir, get_balance_args, get_balance_asserts)
|
||||
|
||||
no_balance_args = ["dao", "balance", FINGERPRINT_ARG, "-i 3"]
|
||||
no_balance_asserts = ["The DAO treasury currently has no funds"]
|
||||
run_cli_command_and_assert(capsys, root_dir, no_balance_args, no_balance_asserts)
|
||||
|
||||
add_funds_args = ["dao", "add_funds", FINGERPRINT_ARG, "-i 2", "-w 1", "-a", "10", "-m 0.1", "--reuse"]
|
||||
add_funds_asserts = [
|
||||
"Transaction submitted to nodes",
|
||||
]
|
||||
run_cli_command_and_assert(capsys, root_dir, add_funds_args, add_funds_asserts)
|
||||
|
||||
rules_args = ["dao", "rules", FINGERPRINT_ARG, "-i 2"]
|
||||
rules_asserts = "proposal_minimum: 100"
|
||||
run_cli_command_and_assert(capsys, root_dir, rules_args, rules_asserts)
|
||||
|
||||
|
||||
def test_dao_proposals(capsys: object, get_test_cli_clients: Tuple[TestRpcClients, Path]) -> None:
|
||||
test_rpc_clients, root_dir = get_test_cli_clients
|
||||
|
||||
# set RPC Client
|
||||
class DAOCreateRpcClient(TestWalletRpcClient):
|
||||
async def dao_get_proposals(
|
||||
self,
|
||||
wallet_id: int,
|
||||
include_closed: bool = True,
|
||||
) -> Dict[str, Union[bool, int, List[Any]]]:
|
||||
proposal = {
|
||||
"proposal_id": "0xCAFEF00D",
|
||||
"amount_voted": uint64(10),
|
||||
"yes_votes": uint64(10),
|
||||
"passed": True,
|
||||
"closed": True,
|
||||
}
|
||||
proposal_2 = {
|
||||
"proposal_id": "0xFEEDBEEF",
|
||||
"amount_voted": uint64(120),
|
||||
"yes_votes": uint64(100),
|
||||
"passed": True,
|
||||
"closed": False,
|
||||
}
|
||||
return {
|
||||
"success": True,
|
||||
"proposals": [proposal, proposal_2],
|
||||
"proposal_timelock": 5,
|
||||
"soft_close_length": 10,
|
||||
}
|
||||
|
||||
async def dao_parse_proposal(
|
||||
self,
|
||||
wallet_id: int,
|
||||
proposal_id: str,
|
||||
) -> Dict[str, Union[bool, Dict[str, Any]]]:
|
||||
if proposal_id == "0xCAFEF00D":
|
||||
puzhash = bytes32(b"1" * 32).hex()
|
||||
asset_id = bytes32(b"2" * 32).hex()
|
||||
proposal_details: Dict[str, Any] = {
|
||||
"proposal_type": "s",
|
||||
"xch_conditions": [{"puzzle_hash": puzhash, "amount": 100}],
|
||||
"asset_conditions": [
|
||||
{"asset_id": asset_id, "conditions": [{"puzzle_hash": puzhash, "amount": 123}]}
|
||||
],
|
||||
}
|
||||
elif proposal_id == "0xFEEDBEEF":
|
||||
proposal_details = {
|
||||
"proposal_type": "u",
|
||||
"dao_rules": {
|
||||
"proposal_timelock": 10,
|
||||
"soft_close_length": 50,
|
||||
},
|
||||
}
|
||||
else:
|
||||
proposal_details = {
|
||||
"proposal_type": "s",
|
||||
"mint_amount": 1000,
|
||||
"new_cat_puzhash": bytes32(b"x" * 32).hex(),
|
||||
}
|
||||
proposal_state = {
|
||||
"state": {
|
||||
"passed": False,
|
||||
"closable": False,
|
||||
"closed": False,
|
||||
"total_votes_needed": 10,
|
||||
"yes_votes_needed": 20,
|
||||
"blocks_needed": 30,
|
||||
}
|
||||
}
|
||||
proposal_dict = {**proposal_state, **proposal_details}
|
||||
return {"success": True, "proposal_dictionary": proposal_dict}
|
||||
|
||||
async def dao_vote_on_proposal(
|
||||
self,
|
||||
wallet_id: int,
|
||||
proposal_id: str,
|
||||
vote_amount: int,
|
||||
tx_config: TXConfig,
|
||||
is_yes_vote: bool,
|
||||
fee: uint64 = uint64(0),
|
||||
) -> Dict[str, Union[str, bool]]:
|
||||
return {"success": True, "tx_id": bytes32(b"1" * 32).hex()}
|
||||
|
||||
async def dao_close_proposal(
|
||||
self,
|
||||
wallet_id: int,
|
||||
proposal_id: str,
|
||||
tx_config: TXConfig,
|
||||
fee: uint64 = uint64(0),
|
||||
self_destruct: bool = False,
|
||||
reuse_puzhash: Optional[bool] = None,
|
||||
) -> Dict[str, Union[str, bool]]:
|
||||
return {"success": True, "tx_id": bytes32(b"1" * 32).hex()}
|
||||
|
||||
async def dao_create_proposal(
|
||||
self,
|
||||
wallet_id: int,
|
||||
proposal_type: str,
|
||||
tx_config: TXConfig,
|
||||
additions: Optional[List[Dict[str, Any]]] = None,
|
||||
amount: Optional[uint64] = None,
|
||||
inner_address: Optional[str] = None,
|
||||
asset_id: Optional[str] = None,
|
||||
cat_target_address: Optional[str] = None,
|
||||
vote_amount: Optional[int] = None,
|
||||
new_dao_rules: Optional[Dict[str, uint64]] = None,
|
||||
fee: uint64 = uint64(0),
|
||||
reuse_puzhash: Optional[bool] = None,
|
||||
) -> Dict[str, Union[str, bool]]:
|
||||
return {"success": True, "proposal_id": "0xCAFEF00D"}
|
||||
|
||||
async def get_wallets(self, wallet_type: Optional[WalletType] = None) -> List[Dict[str, Union[str, int]]]:
|
||||
return [{"id": 1, "type": 0}, {"id": 2, "type": 14}]
|
||||
|
||||
async def get_transaction(self, wallet_id: int, transaction_id: bytes32) -> TransactionRecord:
|
||||
return TransactionRecord(
|
||||
confirmed_at_height=uint32(0),
|
||||
created_at_time=uint64(int(time.time())),
|
||||
to_puzzle_hash=bytes32(b"2" * 32),
|
||||
amount=uint64(10),
|
||||
fee_amount=uint64(1),
|
||||
confirmed=True,
|
||||
sent=uint32(10),
|
||||
spend_bundle=None,
|
||||
additions=[],
|
||||
removals=[],
|
||||
wallet_id=uint32(1),
|
||||
sent_to=[("peer1", uint8(1), None)],
|
||||
trade_id=None,
|
||||
type=uint32(TransactionType.INCOMING_TX.value),
|
||||
name=bytes32(b"x" * 32),
|
||||
memos=[],
|
||||
valid_times=parse_timelock_info(tuple()),
|
||||
)
|
||||
|
||||
# List all proposals
|
||||
inst_rpc_client = DAOCreateRpcClient() # pylint: disable=no-value-for-parameter
|
||||
test_rpc_clients.wallet_rpc_client = inst_rpc_client
|
||||
list_args = ["dao", "list_proposals", FINGERPRINT_ARG, "-i 2"]
|
||||
# these are various things that should be in the output
|
||||
list_asserts = [
|
||||
"Proposal ID: 0xCAFEF00D",
|
||||
"Status: OPEN",
|
||||
"Votes for: 10",
|
||||
"Votes against: 0",
|
||||
"Proposal ID: 0xFEEDBEEF",
|
||||
"Status: CLOSED",
|
||||
"Votes for: 100",
|
||||
"Votes against: 20",
|
||||
"Proposals have 10 blocks of soft close time.",
|
||||
]
|
||||
run_cli_command_and_assert(capsys, root_dir, list_args, list_asserts)
|
||||
|
||||
# Show details of specific proposal
|
||||
parse_spend_args = ["dao", "show_proposal", FINGERPRINT_ARG, "-i 2", "-p", "0xCAFEF00D"]
|
||||
address = encode_puzzle_hash(bytes32(b"1" * 32), "xch")
|
||||
asset_id = bytes32(b"2" * 32).hex()
|
||||
parse_spend_asserts = [
|
||||
"Type: SPEND",
|
||||
"Status: OPEN",
|
||||
"Passed: False",
|
||||
"Closable: False",
|
||||
"Total votes needed: 10",
|
||||
"Yes votes needed: 20",
|
||||
"Blocks remaining: 30",
|
||||
"Proposal XCH Conditions",
|
||||
f"Address: {address}",
|
||||
"Amount: 100",
|
||||
"Proposal asset Conditions",
|
||||
f"Asset ID: {asset_id}",
|
||||
f"Address: {address}",
|
||||
"Amount: 123",
|
||||
]
|
||||
run_cli_command_and_assert(capsys, root_dir, parse_spend_args, parse_spend_asserts)
|
||||
|
||||
parse_update_args = ["dao", "show_proposal", FINGERPRINT_ARG, "-i2", "-p", "0xFEEDBEEF"]
|
||||
parse_update_asserts = [
|
||||
"Type: UPDATE",
|
||||
"proposal_timelock: 10",
|
||||
"soft_close_length: 50",
|
||||
]
|
||||
run_cli_command_and_assert(capsys, root_dir, parse_update_args, parse_update_asserts)
|
||||
|
||||
parse_mint_args = ["dao", "show_proposal", FINGERPRINT_ARG, "-i2", "-p", "0xDABBAD00"]
|
||||
parse_mint_asserts = [
|
||||
"Type: MINT",
|
||||
"Amount of CAT to mint: 1000",
|
||||
"Address: {}".format(encode_puzzle_hash(bytes32(b"x" * 32), "xch")),
|
||||
]
|
||||
run_cli_command_and_assert(capsys, root_dir, parse_mint_args, parse_mint_asserts)
|
||||
|
||||
# Vote on a proposal
|
||||
vote_args = ["dao", "vote", FINGERPRINT_ARG, "-i 2", "-p", "0xFEEDBEEF", "-a", "1000", "-n", "-m 0.1", "--reuse"]
|
||||
vote_asserts = ["Transaction submitted to nodes"]
|
||||
run_cli_command_and_assert(capsys, root_dir, vote_args, vote_asserts)
|
||||
|
||||
# Close a proposal
|
||||
close_args = ["dao", "close_proposal", FINGERPRINT_ARG, "-i 2", "-p", "0xFEEDBEEF", "-d", "-m 0.1", "--reuse"]
|
||||
close_asserts = ["Transaction submitted to nodes"]
|
||||
run_cli_command_and_assert(capsys, root_dir, close_args, close_asserts)
|
||||
|
||||
# Create a spend proposal
|
||||
address = encode_puzzle_hash(bytes32(b"x" * 32), "xch")
|
||||
spend_args = [
|
||||
"dao",
|
||||
"create_proposal",
|
||||
"spend",
|
||||
FINGERPRINT_ARG,
|
||||
"-i 2",
|
||||
"-t",
|
||||
address,
|
||||
"-a",
|
||||
"10",
|
||||
"-v",
|
||||
"1000",
|
||||
"--asset-id",
|
||||
"0xFEEDBEEF",
|
||||
"-m 0.1",
|
||||
"--reuse",
|
||||
]
|
||||
proposal_asserts = ["Successfully created proposal", "Proposal ID: 0xCAFEF00D"]
|
||||
run_cli_command_and_assert(capsys, root_dir, spend_args, proposal_asserts)
|
||||
|
||||
bad_spend_args = [
|
||||
"dao",
|
||||
"create_proposal",
|
||||
"spend",
|
||||
FINGERPRINT_ARG,
|
||||
"-i 2",
|
||||
"-t",
|
||||
address,
|
||||
"-v",
|
||||
"1000",
|
||||
"--asset-id",
|
||||
"0xFEEDBEEF",
|
||||
"-m 0.1",
|
||||
"--reuse",
|
||||
]
|
||||
proposal_asserts = ["Successfully created proposal", "Proposal ID: 0xCAFEF00D"]
|
||||
with pytest.raises(ValueError) as e_info:
|
||||
run_cli_command_and_assert(capsys, root_dir, bad_spend_args, proposal_asserts)
|
||||
assert e_info.value.args[0] == "Must include a json specification or an address / amount pair."
|
||||
|
||||
# Create an update proposal
|
||||
update_args = [
|
||||
"dao",
|
||||
"create_proposal",
|
||||
"update",
|
||||
FINGERPRINT_ARG,
|
||||
"-i 2",
|
||||
"-v",
|
||||
"1000",
|
||||
"--proposal-timelock",
|
||||
"4",
|
||||
"-m 0.1",
|
||||
"--reuse",
|
||||
]
|
||||
run_cli_command_and_assert(capsys, root_dir, update_args, proposal_asserts)
|
||||
|
||||
# Create a mint proposal
|
||||
mint_args = [
|
||||
"dao",
|
||||
"create_proposal",
|
||||
"mint",
|
||||
FINGERPRINT_ARG,
|
||||
"-i 2",
|
||||
"-v",
|
||||
"1000",
|
||||
"-a",
|
||||
"100",
|
||||
"-t",
|
||||
address,
|
||||
"-m 0.1",
|
||||
"--reuse",
|
||||
]
|
||||
run_cli_command_and_assert(capsys, root_dir, mint_args, proposal_asserts)
|
||||
|
||||
|
||||
def test_dao_cats(capsys: object, get_test_cli_clients: Tuple[TestRpcClients, Path]) -> None:
|
||||
test_rpc_clients, root_dir = get_test_cli_clients
|
||||
|
||||
# set RPC Client
|
||||
class DAOCreateRpcClient(TestWalletRpcClient):
|
||||
async def dao_send_to_lockup(
|
||||
self,
|
||||
wallet_id: int,
|
||||
amount: uint64,
|
||||
tx_config: TXConfig,
|
||||
fee: uint64 = uint64(0),
|
||||
reuse_puzhash: Optional[bool] = None,
|
||||
) -> Dict[str, Union[str, int]]:
|
||||
return {"success": True, "tx_id": bytes32(b"x" * 32).hex()}
|
||||
|
||||
async def dao_free_coins_from_finished_proposals(
|
||||
self,
|
||||
wallet_id: int,
|
||||
tx_config: TXConfig,
|
||||
fee: uint64 = uint64(0),
|
||||
reuse_puzhash: Optional[bool] = None,
|
||||
) -> Dict[str, Union[str, int]]:
|
||||
return {"success": True, "tx_id": bytes32(b"x" * 32).hex()}
|
||||
|
||||
async def dao_exit_lockup(
|
||||
self,
|
||||
wallet_id: int,
|
||||
tx_config: TXConfig,
|
||||
coins: Optional[List[Dict[str, Union[str, int]]]] = None,
|
||||
fee: uint64 = uint64(0),
|
||||
reuse_puzhash: Optional[bool] = None,
|
||||
) -> Dict[str, Union[str, int]]:
|
||||
return {"success": True, "tx_id": bytes32(b"x" * 32).hex()}
|
||||
|
||||
async def get_transaction(self, wallet_id: int, transaction_id: bytes32) -> TransactionRecord:
|
||||
return TransactionRecord(
|
||||
confirmed_at_height=uint32(0),
|
||||
created_at_time=uint64(int(time.time())),
|
||||
to_puzzle_hash=bytes32(b"2" * 32),
|
||||
amount=uint64(10),
|
||||
fee_amount=uint64(1),
|
||||
confirmed=True,
|
||||
sent=uint32(10),
|
||||
spend_bundle=None,
|
||||
additions=[],
|
||||
removals=[],
|
||||
wallet_id=uint32(1),
|
||||
sent_to=[("peer1", uint8(1), None)],
|
||||
trade_id=None,
|
||||
type=uint32(TransactionType.INCOMING_TX.value),
|
||||
name=bytes32(b"x" * 32),
|
||||
memos=[],
|
||||
valid_times=parse_timelock_info(tuple()),
|
||||
)
|
||||
|
||||
inst_rpc_client = DAOCreateRpcClient() # pylint: disable=no-value-for-parameter
|
||||
test_rpc_clients.wallet_rpc_client = inst_rpc_client
|
||||
lockup_args = ["dao", "lockup_coins", FINGERPRINT_ARG, "-i 2", "-a", "1000", "-m 0.1", "--reuse"]
|
||||
lockup_asserts = ["Transaction submitted to nodes"]
|
||||
run_cli_command_and_assert(capsys, root_dir, lockup_args, lockup_asserts)
|
||||
|
||||
release_args = ["dao", "release_coins", FINGERPRINT_ARG, "-i 2", "-m 0.1", "--reuse"]
|
||||
# tx_id = bytes32(b"x" * 32).hex()
|
||||
release_asserts = ["Transaction submitted to nodes"]
|
||||
run_cli_command_and_assert(capsys, root_dir, release_args, release_asserts)
|
||||
|
||||
exit_args = ["dao", "exit_lockup", FINGERPRINT_ARG, "-i 2", "-m 0.1", "--reuse"]
|
||||
exit_asserts = ["Transaction submitted to nodes"]
|
||||
run_cli_command_and_assert(capsys, root_dir, exit_args, exit_asserts)
|
@ -16,7 +16,6 @@ from chia.pools.pool_puzzles import (
|
||||
create_travel_spend,
|
||||
create_waiting_room_inner_puzzle,
|
||||
get_delayed_puz_info_from_launcher_spend,
|
||||
get_most_recent_singleton_coin_from_coin_spend,
|
||||
get_pubkey_from_member_inner_puzzle,
|
||||
get_seconds_and_delayed_puzhash_from_p2_singleton_puzzle,
|
||||
is_pool_singleton_inner_puzzle,
|
||||
@ -39,6 +38,7 @@ from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
|
||||
puzzle_for_pk,
|
||||
solution_for_conditions,
|
||||
)
|
||||
from chia.wallet.singleton import get_most_recent_singleton_coin_from_coin_spend
|
||||
from tests.clvm.coin_store import BadSpendBundleError, CoinStore, CoinTimestamp
|
||||
from tests.clvm.test_puzzles import public_key_for_index, secret_exponent_for_index
|
||||
from tests.util.key_tool import KeyTool
|
||||
|
3
tests/wallet/dao_wallet/config.py
Normal file
3
tests/wallet/dao_wallet/config.py
Normal file
@ -0,0 +1,3 @@
|
||||
from __future__ import annotations
|
||||
|
||||
checkout_blocks_and_plots = True
|
1261
tests/wallet/dao_wallet/test_dao_clvm.py
Normal file
1261
tests/wallet/dao_wallet/test_dao_clvm.py
Normal file
File diff suppressed because it is too large
Load Diff
3649
tests/wallet/dao_wallet/test_dao_wallets.py
Normal file
3649
tests/wallet/dao_wallet/test_dao_wallets.py
Normal file
File diff suppressed because it is too large
Load Diff
152
tests/wallet/test_singleton_store.py
Normal file
152
tests/wallet/test_singleton_store.py
Normal file
@ -0,0 +1,152 @@
|
||||
from __future__ import annotations
|
||||
|
||||
# import dataclasses
|
||||
from secrets import token_bytes
|
||||
|
||||
import pytest
|
||||
|
||||
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 uint32, uint64
|
||||
|
||||
# from chia.wallet.dao_wallet.dao_wallet import DAOInfo, DAOWallet
|
||||
from chia.wallet.lineage_proof import LineageProof
|
||||
from chia.wallet.singleton import create_singleton_puzzle
|
||||
from chia.wallet.singleton_record import SingletonRecord
|
||||
from chia.wallet.wallet_singleton_store import WalletSingletonStore
|
||||
from tests.util.db_connection import DBConnection
|
||||
|
||||
|
||||
def get_record(wallet_id: uint32 = uint32(2)) -> SingletonRecord:
|
||||
launcher_id = bytes32(token_bytes(32))
|
||||
inner_puz = Program.to(1)
|
||||
inner_puz_hash = inner_puz.get_tree_hash()
|
||||
parent_puz = create_singleton_puzzle(inner_puz, launcher_id)
|
||||
parent_puz_hash = parent_puz.get_tree_hash()
|
||||
parent_coin = Coin(launcher_id, parent_puz_hash, 1)
|
||||
inner_sol = Program.to([[51, inner_puz_hash, 1]])
|
||||
lineage_proof = LineageProof(launcher_id, inner_puz.get_tree_hash(), uint64(1))
|
||||
parent_sol = Program.to([lineage_proof.to_program(), 1, inner_sol])
|
||||
parent_coinspend = CoinSpend(parent_coin, parent_puz, parent_sol)
|
||||
pending = True
|
||||
removed_height = 0
|
||||
custom_data = "{'key': 'value'}"
|
||||
record = SingletonRecord(
|
||||
coin=parent_coin,
|
||||
singleton_id=launcher_id,
|
||||
wallet_id=wallet_id,
|
||||
parent_coinspend=parent_coinspend,
|
||||
inner_puzzle_hash=inner_puz_hash,
|
||||
pending=pending,
|
||||
removed_height=removed_height,
|
||||
lineage_proof=lineage_proof,
|
||||
custom_data=custom_data,
|
||||
)
|
||||
return record
|
||||
|
||||
|
||||
class TestSingletonStore:
|
||||
@pytest.mark.asyncio
|
||||
async def test_singleton_insert(self) -> None:
|
||||
async with DBConnection(1) as wrapper:
|
||||
db = await WalletSingletonStore.create(wrapper)
|
||||
record = get_record()
|
||||
await db.save_singleton(record)
|
||||
records_by_wallet = await db.get_records_by_wallet_id(record.wallet_id)
|
||||
assert records_by_wallet[0] == record
|
||||
record_by_coin_id = await db.get_records_by_coin_id(record.coin.name())
|
||||
assert record_by_coin_id[0] == record
|
||||
records_by_singleton_id = await db.get_records_by_singleton_id(record.singleton_id)
|
||||
assert records_by_singleton_id[0] == record
|
||||
# update pending
|
||||
await db.update_pending_transaction(record.coin.name(), False)
|
||||
record_to_check = (await db.get_records_by_coin_id(record.coin.name()))[0]
|
||||
assert record_to_check.pending is False
|
||||
assert record_to_check.custom_data == "{'key': 'value'}"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_singleton_add_spend(self) -> None:
|
||||
async with DBConnection(1) as wrapper:
|
||||
db = await WalletSingletonStore.create(wrapper)
|
||||
record = get_record()
|
||||
child_coin = Coin(record.coin.name(), record.coin.puzzle_hash, 1)
|
||||
parent_coinspend = record.parent_coinspend
|
||||
|
||||
# test add spend
|
||||
await db.add_spend(uint32(2), parent_coinspend, uint32(10))
|
||||
record_by_id = (await db.get_records_by_coin_id(child_coin.name()))[0]
|
||||
assert record_by_id
|
||||
|
||||
# Test adding a non-singleton will fail
|
||||
inner_puz = Program.to(1)
|
||||
inner_puz_hash = inner_puz.get_tree_hash()
|
||||
bad_coin = Coin(record.singleton_id, inner_puz_hash, 1)
|
||||
inner_sol = Program.to([[51, inner_puz_hash, 1]])
|
||||
bad_coinspend = CoinSpend(bad_coin, inner_puz, inner_sol)
|
||||
with pytest.raises(RuntimeError) as e_info:
|
||||
await db.add_spend(uint32(2), bad_coinspend, uint32(10))
|
||||
assert e_info.value.args[0] == "Coin to add is not a valid singleton"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_singleton_remove(self) -> None:
|
||||
async with DBConnection(1) as wrapper:
|
||||
db = await WalletSingletonStore.create(wrapper)
|
||||
record_1 = get_record()
|
||||
record_2 = get_record()
|
||||
await db.save_singleton(record_1)
|
||||
await db.save_singleton(record_2)
|
||||
resp_1 = await db.delete_singleton_by_coin_id(record_1.coin.name(), uint32(1))
|
||||
assert resp_1
|
||||
resp_2 = await db.delete_singleton_by_singleton_id(record_2.singleton_id, uint32(1))
|
||||
assert resp_2
|
||||
record = (await db.get_records_by_coin_id(record_1.coin.name()))[0]
|
||||
assert record.removed_height == 1
|
||||
record = (await db.get_records_by_coin_id(record_2.coin.name()))[0]
|
||||
assert record.removed_height == 1
|
||||
# delete a non-existing coin id
|
||||
fake_id = bytes32(b"x" * 32)
|
||||
resp_3 = await db.delete_singleton_by_coin_id(fake_id, uint32(10))
|
||||
assert not resp_3
|
||||
# delete a non-existing singleton id
|
||||
resp_4 = await db.delete_singleton_by_singleton_id(fake_id, uint32(10))
|
||||
assert not resp_4
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_singleton_delete_wallet(self) -> None:
|
||||
async with DBConnection(1) as wrapper:
|
||||
db = await WalletSingletonStore.create(wrapper)
|
||||
for i in range(1, 5):
|
||||
wallet_id = uint32(i)
|
||||
for _ in range(5):
|
||||
record = get_record(wallet_id)
|
||||
await db.save_singleton(record)
|
||||
assert not (await db.is_empty(wallet_id))
|
||||
|
||||
for j in range(1, 5):
|
||||
wallet_id = uint32(j)
|
||||
start_count = await db.count()
|
||||
await db.delete_wallet(wallet_id)
|
||||
assert (await db.count(wallet_id)) == 0
|
||||
assert await db.is_empty(wallet_id)
|
||||
end_count = await db.count()
|
||||
assert end_count == start_count - 5
|
||||
|
||||
assert await db.is_empty()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_singleton_reorg(self) -> None:
|
||||
async with DBConnection(1) as wrapper:
|
||||
db = await WalletSingletonStore.create(wrapper)
|
||||
record = get_record()
|
||||
# save the singleton
|
||||
await db.save_singleton(record)
|
||||
# delete it at block 10
|
||||
await db.delete_singleton_by_coin_id(record.coin.name(), uint32(10))
|
||||
record_by_id = (await db.get_records_by_coin_id(record.coin.name()))[0]
|
||||
assert record_by_id.removed_height == 10
|
||||
# rollback
|
||||
await db.rollback(5, uint32(2))
|
||||
reorged_record_by_id = await db.get_records_by_coin_id(record.coin.name())
|
||||
assert not reorged_record_by_id
|
Loading…
Reference in New Issue
Block a user