Compare commits

...

16 Commits

Author SHA1 Message Date
Matthew Howard 225b5b90dd
fix API bug and remove unused clsphex 2023-06-30 18:03:28 +01:00
Matthew Howard fb91d31b99
add concurrency test to check wallet isn't locking up coins when spend fails 2023-06-30 17:19:19 +01:00
Matthew Howard b2f13b8d01
delete unused tests 2023-06-29 15:23:23 +01:00
Matthew Howard ade81b18e3
re-add amount check assertion to proposal voting 2023-06-29 15:00:31 +01:00
Matthew Howard 2535a8b2d9
fix dao_wallet bug and commit hex files 2023-06-29 14:36:01 +01:00
matt-o-how d3f61ba42b
add singleton aggregator back into branch (#15662) 2023-06-29 14:28:56 +01:00
Geoff Walmsley 17727a4455
add singleton aggregator back into branch 2023-06-29 09:58:23 +01:00
Adam Kelly ceb62d6a96
linting 2023-06-28 12:08:21 -07:00
Adam Kelly ac6ff8b73a
Typechecking and function input validation 2023-06-28 11:26:39 -07:00
Adam Kelly 381260b3d1
Merge branch 'main' into dao-wallet 2023-06-28 11:14:56 -07:00
Adam Kelly 41b4cd30f7
Bring chia/wallet/wallet_state_manager.py up to date with main 2023-06-28 11:06:14 -07:00
Adam Kelly d106a35c65
Bring in chia/wallet/wallet_state_manager.py changes from dao_unlock_cat_fix 2023-06-28 10:50:41 -07:00
Adam Kelly 4ccaa22075
Take changes from dao_unlock_cat_fix 2023-06-28 09:40:52 -07:00
Matthew Howard e012ea7ecd
add assertion to prevent voting with 0 value 2023-06-27 15:11:57 +01:00
Adam Kelly 413263cdad
Newly created DAO Files 2023-06-20 09:25:22 -07:00
Adam Kelly e10960f132
DAO Wallet changes 2023-06-20 08:59:01 -07:00
85 changed files with 12266 additions and 71 deletions

View File

@ -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

View File

@ -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)

940
chia/cmds/dao.py Normal file
View File

@ -0,0 +1,940 @@
from __future__ import annotations
from typing import Optional
import click
from chia.cmds.cmds_util import execute_with_wallet
from chia.cmds.plotnft import validate_fee
@click.group("dao", short_help="Create, manage or show state of DAOs", no_args_is_help=True)
@click.pass_context
def dao_cmd(ctx: click.Context) -> None:
pass
# ----------------------------------------------------------------------------------------
# ADD
@dao_cmd.command("add", short_help="Create a wallet for an existing DAO", no_args_is_help=True)
@click.option(
"-wp",
"--wallet-rpc-port",
help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml",
type=int,
default=None,
)
@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
@click.option("-n", "--name", help="Set the DAO wallet name", type=str)
@click.option(
"-t",
"--treasury-id",
help="The Treasury ID of the DAO you want to track",
type=str,
required=True,
)
@click.option(
"-fa",
"--filter-amount",
help="The minimum number of votes a proposal needs before the wallet will recognise it",
type=int,
default=1,
show_default=True,
)
def dao_add_cmd(
wallet_rpc_port: Optional[int],
fingerprint: int,
treasury_id: str,
filter_amount: int,
name: Optional[str],
) -> None:
import asyncio
from .dao_funcs import add_dao_wallet
extra_params = {
"name": name,
"treasury_id": treasury_id,
"filter_amount": filter_amount,
}
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, add_dao_wallet))
# ----------------------------------------------------------------------------------------
# CREATE
@dao_cmd.command("create", short_help="Create a new DAO wallet and treasury", no_args_is_help=True)
@click.option(
"-wp",
"--wallet-rpc-port",
help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml",
type=int,
default=None,
)
@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
@click.option("-n", "--name", help="Set the DAO wallet name", type=str)
@click.option(
"-pt",
"--proposal-timelock",
help="The minimum number of blocks before a proposal can close",
type=int,
default=1000,
show_default=True,
)
@click.option(
"-sc",
"--soft-close",
help="The number of blocks a proposal must remain unspent before closing",
type=int,
default=20,
show_default=True,
)
@click.option(
"-ar",
"--attendance-required",
help="The minimum number of votes a proposal must receive to be accepted",
type=int,
required=True,
)
@click.option(
"-pp",
"--pass-percentage",
help="The percentage of 'yes' votes in basis points a proposal must receive to be accepted. 100% = 10000",
type=int,
default=5000,
show_default=True,
)
@click.option(
"-sd",
"--self-destruct",
help="The number of blocks required before a proposal can be automatically removed",
type=int,
default=10000,
show_default=True,
)
@click.option(
"-od",
"--oracle-delay",
help="The number of blocks required between oracle spends of the treasury",
type=int,
default=50,
show_default=True,
)
@click.option(
"-fa",
"--filter-amount",
help="The minimum number of votes a proposal needs before the wallet will recognise it",
type=int,
default=1,
show_default=True,
)
@click.option(
"-ca",
"--cat-amount",
help="The number of DAO CATs (in mojos) to create when initializing the DAO",
type=int,
required=True,
)
@click.option(
"-m",
"--fee",
help="Set the fees per transaction, in XCH.",
type=str,
default="0",
show_default=True,
callback=validate_fee,
)
@click.option(
"--reuse",
help="Reuse existing address for the change.",
is_flag=True,
default=False,
)
def dao_create_cmd(
wallet_rpc_port: Optional[int],
fingerprint: int,
proposal_timelock: int,
soft_close: int,
attendance_required: int,
pass_percentage: int,
self_destruct: int,
oracle_delay: int,
filter_amount: int,
cat_amount: int,
name: Optional[str],
fee: str,
reuse: bool,
) -> None:
import asyncio
from .dao_funcs import create_dao_wallet
print("Creating new DAO")
extra_params = {
"fee": fee,
"name": name,
"proposal_timelock": proposal_timelock,
"soft_close_length": soft_close,
"attendance_required": attendance_required,
"pass_percentage": pass_percentage,
"self_destruct_length": self_destruct,
"oracle_spend_delay": oracle_delay,
"filter_amount": filter_amount,
"amount_of_cats": cat_amount,
"reuse_puzhash": True if reuse else None,
}
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, create_dao_wallet))
# ----------------------------------------------------------------------------------------
# TREASURY FUNDS
@dao_cmd.command("add-funds", short_help="Send funds to a DAO treasury", no_args_is_help=True)
@click.option(
"-wp",
"--wallet-rpc-port",
help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml",
type=int,
default=None,
)
@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
@click.option("-i", "--wallet-id", help="Id of the wallet to use", type=int, required=True)
@click.option(
"-f",
"--funding-wallet-id",
help="The ID of the wallet to send funds from",
type=int,
required=True,
)
@click.option(
"-a",
"--amount",
help="The amount of funds to send",
type=str,
required=True,
)
@click.option(
"-m",
"--fee",
help="Set the fees per transaction, in XCH.",
type=str,
default="0",
show_default=True,
callback=validate_fee,
)
@click.option(
"--reuse",
help="Reuse existing address for the change.",
is_flag=True,
default=False,
)
def dao_add_funds_cmd(
wallet_rpc_port: Optional[int],
fingerprint: int,
wallet_id: int,
funding_wallet_id: int,
amount: str,
fee: str,
reuse: bool,
) -> None:
import asyncio
from .dao_funcs import add_funds_to_treasury
extra_params = {
"wallet_id": wallet_id,
"fee": fee,
"funding_wallet_id": funding_wallet_id,
"amount": amount,
"reuse_puzhash": True if reuse else None,
}
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, add_funds_to_treasury))
@dao_cmd.command("get-balance", short_help="Get the asset balances for a DAO treasury", no_args_is_help=True)
@click.option(
"-wp",
"--wallet-rpc-port",
help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml",
type=int,
default=None,
)
@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
@click.option("-i", "--wallet-id", help="Id of the wallet to use", type=int, required=True)
def dao_get_balance_cmd(
wallet_rpc_port: Optional[int],
fingerprint: int,
wallet_id: int,
) -> None:
import asyncio
from .dao_funcs import get_treasury_balance
extra_params = {
"wallet_id": wallet_id,
}
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, get_treasury_balance))
# ----------------------------------------------------------------------------------------
# LIST/SHOW PROPOSALS
@dao_cmd.command("list-proposals", short_help="List proposals for the DAO", no_args_is_help=True)
@click.option(
"-wp",
"--wallet-rpc-port",
help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml",
type=int,
default=None,
)
@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
@click.option("-i", "--wallet-id", help="Id of the wallet to use", type=int, required=True)
@click.option(
"-c",
"--include-closed",
help="Include previously closed proposals",
is_flag=True,
)
def dao_list_proposals_cmd(
wallet_rpc_port: Optional[int],
fingerprint: int,
wallet_id: int,
include_closed: Optional[bool],
) -> None:
import asyncio
from .dao_funcs import list_proposals
if not include_closed:
include_closed = False
extra_params = {
"wallet_id": wallet_id,
"include_closed": include_closed,
}
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, list_proposals))
@dao_cmd.command("show-proposal", short_help="Show the details of a specific proposal", no_args_is_help=True)
@click.option(
"-wp",
"--wallet-rpc-port",
help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml",
type=int,
default=None,
)
@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
@click.option("-i", "--wallet-id", help="Id of the wallet to use", type=int, required=True)
@click.option(
"-p",
"--proposal_id",
help="The ID of the proposal to fetch",
type=str,
required=True,
)
def dao_show_proposal_cmd(
wallet_rpc_port: Optional[int],
fingerprint: int,
wallet_id: int,
proposal_id: str,
) -> None:
import asyncio
from .dao_funcs import show_proposal
extra_params = {
"wallet_id": wallet_id,
"proposal_id": proposal_id,
}
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, show_proposal))
# ----------------------------------------------------------------------------------------
# VOTE
@dao_cmd.command("vote", short_help="Vote on a DAO proposal", no_args_is_help=True)
@click.option(
"-wp",
"--wallet-rpc-port",
help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml",
type=int,
default=None,
)
@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
@click.option("-i", "--wallet-id", help="Id of the wallet to use", type=int, required=True)
@click.option(
"-p",
"--proposal-id",
help="The ID of the proposal you are voting on",
type=str,
required=True,
)
@click.option(
"-a",
"--vote-amount",
help="The number of votes you want to cast",
type=int,
required=True,
)
@click.option(
"-n",
"--vote-no",
help="Use this option to vote against a proposal. If not present then the vote is for the proposal",
is_flag=True,
)
@click.option(
"-m",
"--fee",
help="Set the fees per transaction, in XCH.",
type=str,
default="0",
show_default=True,
callback=validate_fee,
)
@click.option(
"--reuse",
help="Reuse existing address for the change.",
is_flag=True,
default=False,
)
def dao_vote_cmd(
wallet_rpc_port: Optional[int],
fingerprint: int,
wallet_id: int,
proposal_id: str,
vote_amount: int,
vote_no: Optional[bool],
fee: str,
reuse: bool,
) -> None:
import asyncio
from .dao_funcs import vote_on_proposal
is_yes_vote = False if vote_no else True
extra_params = {
"wallet_id": wallet_id,
"fee": fee,
"proposal_id": proposal_id,
"vote_amount": vote_amount,
"is_yes_vote": is_yes_vote,
"reuse_puzhash": True if reuse else None,
}
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, vote_on_proposal))
# ----------------------------------------------------------------------------------------
# CLOSE PROPOSALS
@dao_cmd.command("close-proposal", short_help="Close a DAO proposal", no_args_is_help=True)
@click.option(
"-wp",
"--wallet-rpc-port",
help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml",
type=int,
default=None,
)
@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
@click.option("-i", "--wallet-id", help="Id of the wallet to use", type=int, required=True)
@click.option(
"-p",
"--proposal-id",
help="The ID of the proposal you are voting on",
type=str,
required=True,
)
@click.option(
"-m",
"--fee",
help="Set the fees per transaction, in XCH.",
type=str,
default="0",
show_default=True,
callback=validate_fee,
)
@click.option(
"--reuse",
help="Reuse existing address for the change.",
is_flag=True,
default=False,
)
def dao_close_proposal_cmd(
wallet_rpc_port: Optional[int],
fingerprint: int,
wallet_id: int,
proposal_id: str,
fee: str,
reuse: bool,
) -> None:
import asyncio
from .dao_funcs import close_proposal
extra_params = {
"wallet_id": wallet_id,
"fee": fee,
"proposal_id": proposal_id,
"reuse_puzhash": True if reuse else None,
}
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, close_proposal))
# ----------------------------------------------------------------------------------------
# LOCKUP COINS
@dao_cmd.command("lockup-coins", short_help="Lock DAO CATs for voting", no_args_is_help=True)
@click.option(
"-wp",
"--wallet-rpc-port",
help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml",
type=int,
default=None,
)
@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
@click.option("-i", "--wallet-id", help="Id of the wallet to use", type=int, required=True)
@click.option(
"-a",
"--amount",
help="The amount of new cats the proposal will mint",
type=str,
required=True,
)
@click.option(
"-m",
"--fee",
help="Set the fees per transaction, in XCH.",
type=str,
default="0",
show_default=True,
callback=validate_fee,
)
@click.option(
"--reuse",
help="Reuse existing address for the change.",
is_flag=True,
default=False,
)
def dao_lockup_coins_cmd(
wallet_rpc_port: Optional[int],
fingerprint: int,
wallet_id: int,
amount: str,
fee: str,
reuse: bool,
) -> None:
import asyncio
from .dao_funcs import lockup_coins
extra_params = {
"wallet_id": wallet_id,
"fee": fee,
"amount": amount,
"reuse_puzhash": True if reuse else None,
}
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, lockup_coins))
@dao_cmd.command("release-coins", short_help="Release closed proposals from DAO CATs", no_args_is_help=True)
@click.option(
"-wp",
"--wallet-rpc-port",
help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml",
type=int,
default=None,
)
@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
@click.option("-i", "--wallet-id", help="Id of the wallet to use", type=int, required=True)
@click.option(
"-m",
"--fee",
help="Set the fees per transaction, in XCH.",
type=str,
default="0",
show_default=True,
callback=validate_fee,
)
@click.option(
"--reuse",
help="Reuse existing address for the change.",
is_flag=True,
default=False,
)
def dao_release_coins_cmd(
wallet_rpc_port: Optional[int],
fingerprint: int,
wallet_id: int,
fee: str,
reuse: bool,
) -> None:
import asyncio
from .dao_funcs import release_coins
extra_params = {
"wallet_id": wallet_id,
"fee": fee,
"reuse_puzhash": True if reuse else None,
}
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, release_coins))
@dao_cmd.command("exit-lockup", short_help="Release DAO CATs from voting mode", no_args_is_help=True)
@click.option(
"-wp",
"--wallet-rpc-port",
help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml",
type=int,
default=None,
)
@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
@click.option("-i", "--wallet-id", help="Id of the wallet to use", type=int, required=True)
@click.option(
"-m",
"--fee",
help="Set the fees per transaction, in XCH.",
type=str,
default="0",
show_default=True,
callback=validate_fee,
)
@click.option(
"--reuse",
help="Reuse existing address for the change.",
is_flag=True,
default=False,
)
def dao_exit_lockup_cmd(
wallet_rpc_port: Optional[int],
fingerprint: int,
wallet_id: int,
fee: str,
reuse: bool,
) -> None:
import asyncio
from .dao_funcs import exit_lockup
extra_params = {
"wallet_id": wallet_id,
"fee": fee,
"reuse_puzhash": True if reuse else None,
}
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, exit_lockup))
# ----------------------------------------------------------------------------------------
# CREATE PROPOSALS
@dao_cmd.group("create-proposal", short_help="Create and add a proposal to a DAO", no_args_is_help=True)
@click.pass_context
def dao_proposal(ctx: click.Context) -> None:
pass
@dao_proposal.command("spend", short_help="Create a proposal to spend DAO funds", no_args_is_help=True)
@click.option(
"-wp",
"--wallet-rpc-port",
help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml",
type=int,
default=None,
)
@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
@click.option("-i", "--wallet-id", help="Id of the wallet to use", type=int, required=True)
@click.option(
"-t",
"--to-address",
help="The address the proposal will send funds to",
type=str,
required=False,
default=None,
)
@click.option(
"-a",
"--amount",
help="The amount of funds the proposal will send (in mojos)",
type=float,
required=False,
default=None,
)
@click.option(
"-v",
"--vote-amount",
help="The number of votes to add",
type=int,
required=True,
)
@click.option(
"-id",
"--asset-id",
help="The asset id of the funds the proposal will send. Leave blank for xch",
type=str,
required=False,
default=None,
)
@click.option(
"-j",
"--from-json",
help="Path to a json file containing a list of additions, for use in proposals with multiple spends",
type=str,
required=False,
default=None,
)
@click.option(
"-m",
"--fee",
help="Set the fees per transaction, in XCH.",
type=str,
default="0",
show_default=True,
callback=validate_fee,
)
@click.option(
"--reuse",
help="Reuse existing address for the change.",
is_flag=True,
default=False,
)
def dao_create_spend_proposal_cmd(
wallet_rpc_port: Optional[int],
fingerprint: int,
wallet_id: int,
vote_amount: int,
to_address: Optional[str],
amount: Optional[float],
asset_id: Optional[str],
from_json: Optional[str],
fee: str,
reuse: bool,
) -> None:
import asyncio
from .dao_funcs import create_spend_proposal
extra_params = {
"wallet_id": wallet_id,
"fee": fee,
"vote_amount": vote_amount,
"to_address": to_address,
"amount": amount,
"asset_id": asset_id,
"from_json": from_json,
"reuse_puzhash": True if reuse else None,
}
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, create_spend_proposal))
@dao_proposal.command("update", short_help="Create a proposal to change the DAO rules", no_args_is_help=True)
@click.option(
"-wp",
"--wallet-rpc-port",
help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml",
type=int,
default=None,
)
@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
@click.option("-i", "--wallet-id", help="Id of the wallet to use", type=int, required=True)
@click.option(
"-v",
"--vote-amount",
help="The number of votes to add",
type=int,
required=True,
)
@click.option(
"-pt",
"--proposal-timelock",
help="The new minimum number of blocks before a proposal can close",
type=int,
default=None,
required=False,
)
@click.option(
"-sc",
"--soft-close",
help="The number of blocks a proposal must remain unspent before closing",
type=int,
default=None,
required=False,
)
@click.option(
"-ar",
"--attendance-required",
help="The minimum number of votes a proposal must receive to be accepted",
type=int,
default=None,
required=False,
)
@click.option(
"-pp",
"--pass-percentage",
help="The percentage of 'yes' votes in basis points a proposal must receive to be accepted. 100% = 10000",
type=int,
default=None,
required=False,
)
@click.option(
"-sd",
"--self-destruct",
help="The number of blocks required before a proposal can be automatically removed",
type=int,
default=None,
required=False,
)
@click.option(
"-od",
"--oracle-delay",
help="The number of blocks required between oracle spends of the treasury",
type=int,
default=None,
required=False,
)
@click.option(
"-m",
"--fee",
help="Set the fees per transaction, in XCH.",
type=str,
default="0",
show_default=True,
callback=validate_fee,
)
@click.option(
"--reuse",
help="Reuse existing address for the change.",
is_flag=True,
default=False,
)
def dao_create_update_proposal_cmd(
wallet_rpc_port: Optional[int],
fingerprint: int,
wallet_id: int,
vote_amount: int,
proposal_timelock: Optional[int],
soft_close: Optional[int],
attendance_required: Optional[int],
pass_percentage: Optional[int],
self_destruct: Optional[int],
oracle_delay: Optional[int],
fee: str,
reuse: bool,
) -> None:
import asyncio
from .dao_funcs import create_update_proposal
extra_params = {
"wallet_id": wallet_id,
"fee": fee,
"vote_amount": vote_amount,
"proposal_timelock": proposal_timelock,
"soft_close_length": soft_close,
"attendance_required": attendance_required,
"pass_percentage": pass_percentage,
"self_destruct_length": self_destruct,
"oracle_spend_delay": oracle_delay,
"reuse_puzhash": True if reuse else None,
}
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, create_update_proposal))
@dao_proposal.command("mint", short_help="Create a proposal to mint new DAO CATs", no_args_is_help=True)
@click.option(
"-wp",
"--wallet-rpc-port",
help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml",
type=int,
default=None,
)
@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which key to use", type=int)
@click.option("-i", "--wallet-id", help="Id of the wallet to use", type=int, required=True)
@click.option(
"-a",
"--amount",
help="The amount of new cats the proposal will mint (in mojos)",
type=int,
required=True,
)
@click.option(
"-t",
"--to-address",
help="The address new cats will be minted to",
type=str,
required=True,
default=None,
)
@click.option(
"-v",
"--vote-amount",
help="The number of votes to add",
type=int,
required=True,
)
@click.option(
"-m",
"--fee",
help="Set the fees per transaction, in XCH.",
type=str,
default="0",
show_default=True,
callback=validate_fee,
)
@click.option(
"--reuse",
help="Reuse existing address for the change.",
is_flag=True,
default=False,
)
def dao_create_mint_proposal_cmd(
wallet_rpc_port: Optional[int],
fingerprint: int,
wallet_id: int,
amount: int,
to_address: int,
vote_amount: int,
fee: str,
reuse: bool,
) -> None:
import asyncio
from .dao_funcs import create_mint_proposal
extra_params = {
"wallet_id": wallet_id,
"fee": fee,
"amount": amount,
"cat_target_address": to_address,
"vote_amount": vote_amount,
"reuse_puzhash": True if reuse else None,
}
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, create_mint_proposal))
# ----------------------------------------------------------------------------------------
dao_cmd.add_command(dao_add_cmd)
dao_cmd.add_command(dao_create_cmd)
dao_cmd.add_command(dao_add_funds_cmd)
dao_cmd.add_command(dao_get_balance_cmd)
dao_cmd.add_command(dao_list_proposals_cmd)
dao_cmd.add_command(dao_show_proposal_cmd)
dao_cmd.add_command(dao_vote_cmd)
dao_cmd.add_command(dao_close_proposal_cmd)
dao_cmd.add_command(dao_lockup_coins_cmd)
dao_cmd.add_command(dao_exit_lockup_cmd)
dao_cmd.add_command(dao_release_coins_cmd)
dao_cmd.add_command(dao_proposal)
# TODO: status: how many of your voting coins are locked away vs. spendable, etc.

442
chia/cmds/dao_funcs.py Normal file
View File

@ -0,0 +1,442 @@
from __future__ import annotations
import asyncio
import time
from decimal import Decimal
from typing import Any, Dict
from chia.cmds.cmds_util import 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.rpc.wallet_rpc_client import WalletRpcClient
from chia.server.start_wallet import SERVICE_NAME
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.util.bech32m import encode_puzzle_hash
from chia.util.config import load_config, selected_network_address_prefix
from chia.util.default_root import DEFAULT_ROOT_PATH
from chia.util.ints import uint64
async def add_dao_wallet(args: Dict[str, Any], wallet_client: WalletRpcClient, fingerprint: 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.")
res = await wallet_client.create_new_dao_wallet(
mode="existing",
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_client: WalletRpcClient, fingerprint: int) -> None:
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"],
}
amount_of_cats = args["amount_of_cats"]
filter_amount = args["filter_amount"]
name = args["name"]
reuse_puzhash = args["reuse_puzhash"]
fee = Decimal(args["fee"])
final_fee: uint64 = uint64(int(fee * units["chia"]))
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,
reuse_puzhash=reuse_puzhash,
)
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 add_funds_to_treasury(args: Dict[str, Any], wallet_client: WalletRpcClient, fingerprint: int) -> None:
wallet_id = args["wallet_id"]
funding_wallet_id = args["funding_wallet_id"]
amount = Decimal(args["amount"])
reuse_puzhash = args["reuse_puzhash"]
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:
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,
reuse_puzhash=reuse_puzhash,
)
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))
return None
print("Transaction not yet submitted to nodes")
print(f"To get status, use command: chia wallet get_transaction -f {fingerprint} -tx 0x{tx_id}")
async def get_treasury_balance(args: Dict[str, Any], wallet_client: WalletRpcClient, fingerprint: int) -> None:
wallet_id = args["wallet_id"]
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
for asset_id, balance in balances.items():
if asset_id == "null":
print(f"XCH: {balance}")
else:
print(f"{asset_id}: {balance}")
async def list_proposals(args: Dict[str, Any], wallet_client: WalletRpcClient, fingerprint: int) -> None:
wallet_id = args["wallet_id"]
res = await wallet_client.dao_get_proposals(wallet_id=wallet_id)
proposals = res["proposals"]
lockup_time = res["lockup_time"]
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}")
close_height = prop["singleton_block_height"] - lockup_time
print(f"Closable at block height: {close_height}")
print("------------------------")
print(f"Proposals have {soft_close_length} blocks of soft close time.")
print("############################")
async def show_proposal(args: Dict[str, Any], wallet_client: WalletRpcClient, fingerprint: int) -> None:
wallet_id = args["wallet_id"]
proposal_id = args["proposal_id"]
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 = pd["proposal_type"]
if (ptype == "spend") and ("mint_amount" in pd):
ptype = "mint"
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}")
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:
print("{puzzle_hash} {amount}".format(**pmt))
if asset_conds:
print("Proposal asset Conditions")
for asset_id, conds in asset_conds:
print(f"Asset ID: {asset_id}")
for pmt in conds:
print("{puzzle_hash} {amount}".format(**pmt))
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"]
config = load_config(DEFAULT_ROOT_PATH, "config.yaml", SERVICE_NAME)
prefix = selected_network_address_prefix(config)
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_client: WalletRpcClient, fingerprint: 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"]
reuse_puzhash = args["reuse_puzhash"]
# wallet_id: int, proposal_id: str, vote_amount: uint64, is_yes_vote: bool = True, fee: uint64 = uint64(0)
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,
reuse_puzhash=reuse_puzhash,
)
spend_bundle = res["spend_bundle"]
if res["success"]:
print(f"Submitted spend bundle with name: {spend_bundle.name()}")
else:
print("Unable to generate vote transaction.")
async def close_proposal(args: Dict[str, Any], wallet_client: WalletRpcClient, fingerprint: int) -> None:
wallet_id = args["wallet_id"]
fee = args["fee"]
final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"]))
proposal_id = args["proposal_id"]
reuse_puzhash = args["reuse_puzhash"]
res = await wallet_client.dao_close_proposal(
wallet_id=wallet_id,
proposal_id=proposal_id,
fee=final_fee,
reuse_puzhash=reuse_puzhash,
)
# dao_close_proposal(self, wallet_id: int, proposal_id: str, fee: uint64 = uint64(0))
if res["success"]:
name = res["tx_id"]
print(f"Submitted proposal close transaction with name: {name}")
else:
print("Unable to generate close transaction.")
async def lockup_coins(args: Dict[str, Any], wallet_client: WalletRpcClient, fingerprint: 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"]))
reuse_puzhash = args["reuse_puzhash"]
# typ = await get_wallet_type(wallet_id=wallet_id, wallet_client=wallet_client)
res = await wallet_client.dao_send_to_lockup(
wallet_id=wallet_id,
amount=final_amount,
fee=final_fee,
reuse_puzhash=reuse_puzhash,
)
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))
return None
print("Transaction not yet submitted to nodes")
async def release_coins(args: Dict[str, Any], wallet_client: WalletRpcClient, fingerprint: int) -> None:
wallet_id = args["wallet_id"]
fee = args["fee"]
final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"]))
reuse_puzhash = args["reuse_puzhash"]
res = await wallet_client.dao_free_coins_from_finished_proposals(
wallet_id=wallet_id,
fee=final_fee,
reuse_puzhash=reuse_puzhash,
)
if res["success"]:
print("Transaction submitted.")
else:
print("Transaction failed.")
async def exit_lockup(args: Dict[str, Any], wallet_client: WalletRpcClient, fingerprint: int) -> None:
wallet_id = args["wallet_id"]
fee = args["fee"]
final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"]))
reuse_puzhash = args["reuse_puzhash"]
res = await wallet_client.dao_exit_lockup(
wallet_id=wallet_id,
coins=[],
fee=final_fee,
reuse_puzhash=reuse_puzhash,
)
if res["success"]:
print("Transaction submitted.")
else:
print("Transaction failed.")
async def create_spend_proposal(args: Dict[str, Any], wallet_client: WalletRpcClient, fingerprint: int) -> None:
wallet_id = args["wallet_id"]
fee = args["fee"]
final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"]))
reuse_puzhash = args["reuse_puzhash"]
if "to_address" in args:
address = args["to_address"]
else:
address = None
if "amount" in args:
amount = args["amount"]
else:
amount = None
if "from_json" in args:
additions = args["from_json"]
else:
additions = None
if additions is None and (address is None or amount is None):
print("ERROR: Must include a json specification or an address / amount pair.")
if "vote_amount" in args:
vote_amount = args["vote_amount"]
else:
vote_amount = None
res = await wallet_client.dao_create_proposal(
wallet_id=wallet_id,
proposal_type="spend",
additions=additions,
amount=amount,
inner_address=address,
vote_amount=vote_amount,
fee=final_fee,
reuse_puzhash=reuse_puzhash,
)
if res["success"]:
print("Successfully created proposal.")
else:
print("Failed to create proposal.")
async def create_update_proposal(args: Dict[str, Any], wallet_client: WalletRpcClient, fingerprint: int) -> None:
wallet_id = args["wallet_id"]
fee = Decimal(args["fee"])
final_fee: uint64 = uint64(int(fee * units["chia"]))
reuse_puzhash = args["reuse_puzhash"]
if "proposal_timelock" in args:
proposal_timelock = args["proposal_timelock"]
else:
proposal_timelock = None
if "soft_close_length" in args:
soft_close_length = args["soft_close_length"]
else:
soft_close_length = None
if "attendance_required" in args:
attendance_required = args["attendance_required"]
else:
attendance_required = None
if "pass_percentage" in args:
pass_percentage = args["pass_percentage"]
else:
pass_percentage = None
if "self_destruct_length" in args:
self_destruct_length = args["self_destruct_length"]
else:
self_destruct_length = None
if "oracle_spend_delay" in args:
oracle_spend_delay = args["oracle_spend_delay"]
else:
oracle_spend_delay = None
if "vote_amount" in args:
vote_amount = args["vote_amount"]
else:
vote_amount = None
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,
}
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,
reuse_puzhash=reuse_puzhash,
)
if res["success"]:
print("Successfully created proposal.")
else:
print("Failed to create proposal.")
async def create_mint_proposal(args: Dict[str, Any], wallet_client: WalletRpcClient, fingerprint: int) -> None:
wallet_id = args["wallet_id"]
fee = args["fee"]
final_fee: uint64 = uint64(int(Decimal(fee) * units["chia"]))
reuse_puzhash = args["reuse_puzhash"]
cat_target_address = args["cat_target_address"]
amount = args["amount"]
vote_amount = None
if "vote_amount" in args:
vote_amount = args["vote_amount"]
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,
reuse_puzhash=reuse_puzhash,
)
if res["success"]:
print("Successfully created proposal.")
else:
print("Failed to create proposal.")

View File

@ -14,10 +14,11 @@ 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.types.coin_spend import CoinSpend
from chia.util.ints import uint32, uint64
from chia.wallet.puzzles.load_clvm import load_clvm_maybe_recompile
from chia.wallet.puzzles.singleton_top_layer import puzzle_for_singleton
from chia.wallet.singleton import get_most_recent_singleton_coin_from_coin_spend
log = logging.getLogger(__name__)
# "Full" is the outer singleton, with the inner puzzle filled in
@ -282,14 +283,6 @@ def create_absorb_spend(
return coin_spends
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
def get_pubkey_from_member_inner_puzzle(inner_puzzle: Program) -> G1Element:
args = uncurry_pool_member_inner_puzzle(inner_puzzle)
if args is not None:

View File

@ -17,7 +17,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,
is_pool_member_inner_puzzle,
is_pool_waitingroom_inner_puzzle,
launcher_id_to_p2_puzzle_hash,
@ -48,6 +47,7 @@ from chia.types.spend_bundle import SpendBundle
from chia.util.ints import uint32, uint64, uint128
from chia.wallet.derive_keys import find_owner_sk
from chia.wallet.sign_coin_spends import sign_coin_spends
from chia.wallet.singleton import get_most_recent_singleton_coin_from_coin_spend
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.util.transaction_type import TransactionType
from chia.wallet.util.wallet_types import WalletType
@ -263,8 +263,8 @@ class PoolWallet:
async def apply_state_transition(self, new_state: CoinSpend, block_height: uint32) -> bool:
"""
Updates the Pool state (including DB) with new singleton spends.
The DB must be committed after calling this method. All validation should be done here. Returns True iff
the spend is a valid transition spend for the singleton, False otherwise.
The DB must be committed after calling this method. All validation should be done here.
Returns True iff the spend is a valid transition spend for the singleton, False otherwise.
"""
tip: Tuple[uint32, CoinSpend] = await self.get_tip()
tip_spend = tip[1]

View File

@ -39,6 +39,11 @@ from chia.util.streamable import Streamable, streamable
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_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.dao_wallet.dao_info import DAORules
from chia.wallet.dao_wallet.dao_utils import 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,
@ -51,9 +56,9 @@ from chia.wallet.did_wallet.did_info import 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
@ -194,6 +199,19 @@ 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_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,
@ -713,6 +731,42 @@ class WalletRpcApi:
}
else: # undefined did_type
pass
elif request["wallet_type"] == "dao_wallet":
name = None
if request["name"]:
name = request["name"]
if request["mode"] == "new":
if request["dao_rules"]:
dao_rules = DAORules.from_json_dict(request["dao_rules"])
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["amount_of_cats"]),
dao_rules,
uint64(request["filter_amount"]),
name,
uint64(request.get("fee", 0)),
)
elif request["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["treasury_id"]),
uint64(request["filter_amount"]),
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
@ -1565,7 +1619,7 @@ class WalletRpcApi:
)
if hold_lock:
async with self.service.wallet_state_manager.lock:
txs: List[TransactionRecord] = await wallet.generate_signed_transaction(
txs: List[TransactionRecord] = await wallet.generate_signed_transactions(
amounts,
puzzle_hashes,
fee,
@ -1581,7 +1635,7 @@ class WalletRpcApi:
for tx in txs:
await wallet.standard_wallet.push_transaction(tx)
else:
txs = await wallet.generate_signed_transaction(
txs = await wallet.generate_signed_transactions(
amounts,
puzzle_hashes,
fee,
@ -1949,7 +2003,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(),
@ -2120,7 +2174,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)
@ -2326,6 +2380,242 @@ class WalletRpcApi:
"transaction_id": txs.name,
}
##########################################################################################
# DAO Wallet
##########################################################################################
async def dao_adjust_filter_level(self, request) -> 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,
}
async def dao_add_funds_to_treasury(self, request) -> 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()
if wallet_type not in [WalletType.STANDARD_WALLET, WalletType.CAT]:
raise ValueError(f"Cannot fund a treasury with assets from a {wallet_type.name} wallet")
funding_tx = await dao_wallet.create_add_money_to_treasury_spend(
amount=uint64(request.get("amount")),
fee=uint64(request.get("fee", 0)),
funding_wallet_id=funding_wallet_id,
reuse_puzhash=request.get("reuse_puzhash", None),
)
return {"success": True, "tx_id": funding_tx.name}
async def dao_get_treasury_balance(self, request) -> 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)
balances[asset_id] = balance
return {"success": True, "balances": balances}
async def dao_send_to_lockup(self, request) -> 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.create_new_dao_cats(
amount,
push=True,
fee=fee,
reuse_puzhash=request.get("reuse_puzhash", None),
)
return {
"success": True,
"tx_id": txs[0].name,
}
async def dao_get_proposals(self, request) -> 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_list = dao_wallet.dao_info.proposals_list
dao_rules = get_treasury_rules_from_puzzle(dao_wallet.dao_info.current_treasury_innerpuz)
return {
"success": True,
"proposals": proposal_list,
"lockup_time": dao_rules.proposal_timelock,
"soft_close_length": dao_rules.soft_close_length,
}
async def dao_get_proposal_state(self, request) -> 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}
async def dao_exit_lockup(self, request) -> 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"]:
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))
tx = await dao_cat_wallet.exit_vote_state(
coins,
fee=fee,
reuse_puzhash=request.get("reuse_puzhash", None),
)
return {"success": True, "tx_id": tx.name()}
async def dao_create_proposal(self, request) -> 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]] = 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:
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 = dao_wallet.generate_simple_proposal_innerpuz(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,
)
proposed_puzzle = await dao_wallet.generate_update_proposal_innerpuz(new_rules)
elif request["proposal_type"] == "mint":
amount_of_cats = uint64(request["amount"])
mint_address = decode_puzzle_hash(request["cat_target_address"])
# amount_of_cats_to_create: uint64,
# cats_new_innerpuzhash: bytes32,
proposed_puzzle = await dao_wallet.generate_mint_proposal_innerpuz(amount_of_cats, mint_address)
else:
return {"success": False, "error": "Unknown proposal type."}
if "vote_amount" in request:
vote_amount = uint64(request["vote_amount"])
else:
vote_amount = None
fee = uint64(request.get("fee", 0))
tx = await dao_wallet.generate_new_proposal(
proposed_puzzle,
vote_amount,
fee,
reuse_puzhash=request.get("reuse_puzhash", None),
)
assert tx is not None
return {
"success": True,
"tx_id": tx.name().hex(),
}
async def dao_vote_on_proposal(self, request) -> 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))
sb = await dao_wallet.generate_proposal_vote_spend(
bytes32.from_hexstr(request["proposal_id"]),
vote_amount,
request["is_yes_vote"], # bool
fee,
push=True,
reuse_puzhash=request.get("reuse_puzhash", None),
)
assert sb is not None
return {"success": True, "spend_bundle": sb}
async def dao_parse_proposal(self, request) -> 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}
async def dao_close_proposal(self, request) -> 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:
genesis_id = bytes32.from_hexstr(request["genesis_id"])
else:
genesis_id = None
tx = await dao_wallet.create_proposal_close_spend(
bytes32.from_hexstr(request["proposal_id"]),
genesis_id,
fee,
# genesis_id,
push=True,
reuse_puzhash=request.get("reuse_puzhash", None),
)
assert tx is not None
return {"success": True, "tx_id": tx.name()}
async def dao_free_coins_from_finished_proposals(self, request) -> 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(
fee=fee,
reuse_puzhash=request.get("reuse_puzhash", None),
)
assert tx is not None
return {"success": True, "spend_name": tx.name()}
##########################################################################################
# NFT Wallet
##########################################################################################
@ -3118,7 +3408,7 @@ class WalletRpcApi:
else:
assert isinstance(wallet, CATWallet)
txs = await wallet.generate_signed_transaction(
txs = await wallet.generate_signed_transactions(
[amount_0] + [output.amount for output in additional_outputs],
[bytes32(puzzle_hash_0)] + [output.puzzle_hash for output in additional_outputs],
fee,

View File

@ -1199,6 +1199,191 @@ 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,
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),
reuse_puzhash: Optional[bool] = None,
) -> Dict:
"""
TODO: Rearrange argument list order
"""
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,
"reuse_puzhash": reuse_puzhash,
}
response = await self.fetch("create_new_wallet", request)
return response
async def dao_create_proposal(
self,
wallet_id: int,
proposal_type: str,
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, uint64]] = None,
fee: uint64 = uint64(0),
reuse_puzhash: Optional[bool] = None,
) -> 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,
"reuse_puzhash": reuse_puzhash,
}
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,
is_yes_vote: bool = True,
fee: uint64 = uint64(0),
reuse_puzhash: Optional[bool] = None,
):
request: Dict[str, Any] = {
"wallet_id": wallet_id,
"proposal_id": proposal_id,
"vote_amount": vote_amount,
"is_yes_vote": is_yes_vote,
"fee": fee,
"reuse_puzhash": reuse_puzhash,
}
response = await self.fetch("dao_vote_on_proposal", request)
return response
async def dao_get_proposals(self, wallet_id: int):
request: Dict[str, Any] = {"wallet_id": wallet_id}
response = await self.fetch("dao_get_proposals", request)
return response
async def dao_close_proposal(
self,
wallet_id: int,
proposal_id: str,
fee: uint64 = uint64(0),
reuse_puzhash: Optional[bool] = None,
):
request: Dict[str, Any] = {
"wallet_id": wallet_id,
"proposal_id": proposal_id,
"fee": fee,
"reuse_puzhash": reuse_puzhash,
}
response = await self.fetch("dao_close_proposal", request)
return response
async def dao_free_coins_from_finished_proposals(
self,
wallet_id: int,
fee: uint64 = uint64(0),
reuse_puzhash: Optional[bool] = None,
):
request: Dict[str, Any] = {
"wallet_id": wallet_id,
"fee": fee,
"reuse_puzhash": reuse_puzhash,
}
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,
fee: uint64 = uint64(0),
reuse_puzhash: Optional[bool] = None,
):
request: Dict[str, Any] = {
"wallet_id": wallet_id,
"funding_wallet_id": funding_wallet_id,
"amount": amount,
"fee": fee,
"reuse_puzhash": reuse_puzhash,
}
response = await self.fetch("dao_add_funds_to_treasury", request)
return response
async def dao_send_to_lockup(
self,
wallet_id: int,
amount: uint64,
fee: uint64 = uint64(0),
reuse_puzhash: Optional[bool] = None,
):
request: Dict[str, Any] = {
"wallet_id": wallet_id,
"amount": amount,
"fee": fee,
"reuse_puzhash": reuse_puzhash,
}
response = await self.fetch("dao_send_to_lockup", request)
return response
async def dao_exit_lockup(
self,
wallet_id: int,
coins: Optional[List[Dict]] = None,
fee: uint64 = uint64(0),
reuse_puzhash: Optional[bool] = None,
):
request: Dict[str, Any] = {
"wallet_id": wallet_id,
"coins": coins,
"fee": fee,
"reuse_puzhash": reuse_puzhash,
}
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, target_address: Optional[bytes32] = None, fee: uint64 = uint64(0)
) -> Tuple[VCRecord, List[TransactionRecord]]:

View File

@ -74,6 +74,10 @@ def construct_cat_puzzle(
return mod_code.curry(mod_code_hash, limitations_program_hash, inner_puzzle)
def construct_cat_puzzlehash_for_inner_puzzlehash(tail_hash: bytes32, inner_puzzle_hash: bytes32) -> bytes32:
return CAT_MOD.curry(CAT_MOD_HASH, tail_hash, inner_puzzle_hash).get_tree_hash_precalc(inner_puzzle_hash)
def subtotals_for_deltas(deltas: List[int]) -> List[int]:
"""
Given a list of deltas corresponding to input coins, create the "subtotals" list

View File

@ -176,6 +176,7 @@ class CATWallet:
chia_tx = dataclasses.replace(chia_tx, spend_bundle=spend_bundle)
await self.standard_wallet.push_transaction(chia_tx)
await self.standard_wallet.push_transaction(cat_record)
# breakpoint()
return self
@staticmethod
@ -304,7 +305,7 @@ class CATWallet:
spendable.sort(reverse=True, key=lambda record: record.coin.amount)
if self.cost_of_single_tx is None:
coin = spendable[0].coin
txs = await self.generate_signed_transaction(
txs = await self.generate_signed_transactions(
[uint64(coin.amount)], [coin.puzzle_hash], coins={coin}, ignore_max_send_amount=True
)
assert txs[0].spend_bundle
@ -806,7 +807,7 @@ class CATWallet:
chia_tx,
)
async def generate_signed_transaction(
async def generate_signed_transactions(
self,
amounts: List[uint64],
puzzle_hashes: List[bytes32],
@ -820,6 +821,7 @@ class CATWallet:
max_coin_amount: Optional[uint64] = None,
excluded_coin_amounts: Optional[List[uint64]] = None,
reuse_puzhash: Optional[bool] = None,
override_memos: Optional[bool] = None,
**kwargs: Unpack[GSTOptionalArgs],
) -> List[TransactionRecord]:
excluded_cat_coins: Optional[Set[Coin]] = kwargs.get("excluded_cat_coins", None)
@ -833,15 +835,18 @@ class CATWallet:
payments = []
for amount, puzhash, memo_list in zip(amounts, puzzle_hashes, memos):
memos_with_hint: List[bytes] = [puzhash]
memos_with_hint.extend(memo_list)
if override_memos:
memos_with_hint: List[bytes] = memo_list
else:
memos_with_hint = [puzhash]
memos_with_hint.extend(memo_list)
payments.append(Payment(puzhash, amount, memos_with_hint))
payment_sum = sum([p.amount for p in payments])
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,
fee,
@ -899,7 +904,6 @@ class CATWallet:
memos=[],
)
)
return tx_list
async def add_lineage(self, name: bytes32, lineage: Optional[LineageProof]) -> None:

View 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]

View File

@ -0,0 +1,846 @@
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 AugSchemeMPL, G1Element, G2Element
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.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict
from chia.util.ints import uint32, uint64, uint128
from chia.wallet.cat_wallet.cat_utils import (
CAT_MOD,
SpendableCAT,
construct_cat_puzzle,
match_cat_puzzle,
unsigned_spend_bundle_for_spendable_cats,
)
from chia.wallet.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.dao_wallet.dao_utils import (
DAO_FINISHED_STATE_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
DAO_TREASURY_MOD_HASH,
add_proposal_to_active_list,
get_active_votes_from_lockup_puzzle,
get_innerpuz_from_lockup_puzzle,
get_lockup_puzzle,
)
from chia.wallet.derivation_record import DerivationRecord
from chia.wallet.lineage_proof import LineageProof
from chia.wallet.payment import Payment
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
DEFAULT_HIDDEN_PUZZLE_HASH,
calculate_synthetic_secret_key,
)
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.uncurried_puzzle import uncurry_puzzle
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.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] = 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:
self.log.error(f"Error creating DAO CAT wallet: {e}")
# Do a migration of the lineage proofs
# cat_info = LegacyCATInfo.from_bytes(hexstr_to_bytes(self.wallet_info.data))
# self.cat_info = DAOCATInfo(cat_info.limitations_program_hash, cat_info.my_tail)
# self.lineage_store = await CATLineageStore.create(self.wallet_state_manager.db_wrapper, self.get_asset_id())
# for coin_id, lineage in cat_info.lineage_proofs:
# await self.add_lineage(coin_id, lineage)
# await self.save_info(self.cat_info)
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 inner_puzzle_for_cat_puzhash(self, cat_hash: bytes32) -> Program:
record: Optional[
DerivationRecord
] = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(cat_hash)
if record is None:
raise RuntimeError(f"Missing Derivation Record for CAT puzzle_hash {cat_hash}")
inner_puzzle: Program = self.standard_wallet.puzzle_for_pk(record.pubkey)
return inner_puzzle
async def coin_added(self, coin: Coin, height: uint32, peer: WSChiaConnection) -> 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")
# We can't get the inner puzzle for this coin's puzhash because it has the lockup layer.
# So look for it's parent coin, and get the inner puzzle for it, which should be the same as
# the one contained in the lockup.
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")
lockup_puz, lockup_args = cat_inner.uncurry()
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: Program = self.standard_wallet.puzzle_for_pk(record.pubkey)
else:
inner_puzzle = cat_inner.uncurry()[1].at("rrrrrrrrf")
active_votes_list = [bytes32(prop.as_atom()) for prop in lockup_args.at("rrrrrrrf").as_iter()]
if parent_spend.coin.puzzle_hash == coin.puzzle_hash:
# shortcut, works for change
lockup_puz = cat_inner
else:
# TODO: Move this section to dao_utils once we've got the close spend sorted
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:
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:
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:
compatible = False
break
if compatible:
coins.append(coin)
s += coin.coin.amount
if s >= amount:
break
if s < amount:
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,
curry_vals: Optional[Tuple[Program, Program, 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]
treasury_id = dao_wallet.dao_info.treasury_id
if curry_vals is None:
YES_VOTES, TOTAL_VOTES, INNERPUZHASH = dao_wallet.get_proposal_curry_values(proposal_id)
else:
YES_VOTES, TOTAL_VOTES, INNERPUZHASH = curry_vals
# proposal_curry_vals = [YES_VOTES, TOTAL_VOTES, INNERPUZ]
for lci in coins:
# 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
# new_innerpuzhash ; only include this if we're changing owners
coin = lci.coin
vote_info = 0
new_innerpuzzle = add_proposal_to_active_list(lci.inner_puzzle, proposal_id)
standard_inner_puz = get_innerpuz_from_lockup_puzzle(new_innerpuzzle)
# 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()
# We also collect 4 pieces of data for the DAOWallet in order to spend the Proposal properly
# vote_amount_or_solution ; The qty of "votes" to add or subtract. ALWAYS POSITIVE.
# vote_info_or_p2_singleton_mod_hash ; vote_info is whether we are voting YES or NO. XXX rename vote_type?
# vote_coin_id_or_current_cat_issuance ; this is either the coin ID we're taking a vote from OR...
# ; the total number of CATs in circulation according to the treasury
# 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_innerpuzhash_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
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
# CREATE_COIN new_puzzle coin.amount
# CREATE_PUZZLE_ANNOUNCEMENT (sha256tree (list new_proposal_vote_id_or_removal_id my_amount vote_info my_id))
# Payment(change_puzhash, uint64(change), [change_puzhash])
primaries = [
Payment(
new_innerpuzzle.get_tree_hash(),
uint64(vote_amount),
[standard_inner_puz.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
# CREATE_COIN new_puzzle vote_amount
# CREATE_COIN old_puzzle change
# CREATE_PUZZLE_ANNOUNCEMENT (sha256tree (list new_proposal_vote_id_or_removal_id my_amount vote_info my_id))
primaries = [
Payment(
new_innerpuzzle.get_tree_hash(),
uint64(vote_amount),
[new_innerpuzzle.get_tree_hash()],
),
]
if change > 0:
primaries.append(
Payment(
lci.inner_puzzle.get_tree_hash(),
uint64(change),
[lci.inner_puzzle.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))
# 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_inner_puzhash
# new_innerpuzhash ; only include this if we're changing owners
# proposal_curry_vals is:
# (
# TREASURY_MOD_HASH
# PROPOSAL_TIMER_MOD_HASH
# TREASURY_ID
# YES_VOTES
# TOTAL_VOTES
# INNERPUZHASH
# )
solution = Program.to(
[
coin.name(),
inner_solution,
coin.amount,
proposal_id,
[
DAO_TREASURY_MOD_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
treasury_id,
YES_VOTES,
TOTAL_VOTES,
INNERPUZHASH,
],
vote_info,
vote_amount,
lci.inner_puzzle.get_tree_hash(),
0,
]
)
# breakpoint()
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)
running_sum += coin.amount
cat_spend_bundle = unsigned_spend_bundle_for_spendable_cats(CAT_MOD, spendable_cat_list)
spend_bundle = await self.sign(cat_spend_bundle)
# breakpoint()
return spend_bundle
async def get_new_vote_state_puzzle(self, coins: Optional[List[Coin]] = None) -> Program:
innerpuz = await self.get_new_inner_puzzle()
puzzle = get_lockup_puzzle(
self.dao_cat_info.limitations_program_hash,
[],
innerpuz,
)
cat_puzzle: Program = construct_cat_puzzle(CAT_MOD, self.dao_cat_info.limitations_program_hash, puzzle)
# breakpoint()
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 create_new_dao_cats(
self,
amount: uint64,
push: bool = False,
fee: uint64 = uint64(0),
reuse_puzhash: Optional[bool] = None,
) -> Tuple[List[TransactionRecord], Optional[List[Coin]]]:
# 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:
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 = await cat_wallet.generate_signed_transactions(
[amount], [lockup_puzzle.get_tree_hash()], fee=fee, reuse_puzhash=reuse_puzhash
)
new_cats = []
cat_puzzle_hash: bytes32 = construct_cat_puzzle(
CAT_MOD, self.dao_cat_info.limitations_program_hash, lockup_puzzle
).get_tree_hash()
if push:
for tx in txs:
await self.wallet_state_manager.add_pending_transaction(tx)
for coin in tx.spend_bundle.additions():
if coin.puzzle_hash == cat_puzzle_hash:
new_cats.append(coin)
await self.wallet_state_manager.add_interested_puzzle_hashes([cat_puzzle_hash], [self.id()])
return txs, new_cats
async def exit_vote_state(
self,
coins: List[LockedCoinInfo],
fee: uint64 = uint64(0),
push: bool = True,
reuse_puzhash: Optional[bool] = None,
) -> SpendBundle:
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 reuse_puzhash:
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,
)
# 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,
inner_solution,
coin.amount,
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)
spent_coins.append(coin)
cat_spend_bundle = unsigned_spend_bundle_for_spendable_cats(CAT_MOD, spendable_cat_list)
spend_bundle = await self.sign(cat_spend_bundle)
if fee > 0:
dao_wallet = self.wallet_state_manager.wallets[self.dao_cat_info.dao_wallet_id]
chia_tx = await dao_wallet.create_tandem_xch_tx(fee, reuse_puzhash=reuse_puzhash)
assert chia_tx.spend_bundle is not None
full_spend = SpendBundle.aggregate([spend_bundle, chia_tx.spend_bundle])
else:
full_spend = spend_bundle
if push:
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=[],
)
await self.wallet_state_manager.add_pending_transaction(record)
# 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 spend_bundle
async def remove_active_proposal(
self, proposal_id_list: List[bytes32], fee: uint64 = uint64(0), push: bool = True
) -> 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)
locked_coins.append((lci, my_finished_proposals))
extra_delta, limitations_solution = 0, Program.to([])
limitations_program_reveal = Program.to([])
spendable_cat_list = []
# cat_wallet = await self.wallet_state_manager.user_store.get_wallet_by_id(self.dao_cat_info.free_cat_wallet_id)
dao_wallet = self.wallet_state_manager.wallets[self.dao_cat_info.dao_wallet_id]
for lci_proposals_tuple in locked_coins:
proposal_curry_vals = []
coin = lci_proposals_tuple[0].coin
proposals = lci_proposals_tuple[1]
for proposal_id in proposals:
YES_VOTES, TOTAL_VOTES, INNERPUZ = dao_wallet.get_proposal_curry_values(proposal_id)
proposal_curry_vals.append([YES_VOTES, TOTAL_VOTES, 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,
proposal_curry_vals,
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.sign(cat_spend_bundle)
if fee > 0:
dao_wallet = self.wallet_state_manager.wallets[self.dao_cat_info.dao_wallet_id]
chia_tx = await dao_wallet.create_tandem_xch_tx(fee)
assert chia_tx.spend_bundle is not None
full_spend = SpendBundle.aggregate([spend_bundle, chia_tx.spend_bundle])
else:
full_spend = spend_bundle
if push:
record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
to_puzzle_hash=DAO_FINISHED_STATE_HASH,
amount=uint64(1),
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=[],
)
await self.wallet_state_manager.add_pending_transaction(record)
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) -> bytes32:
puzzle = await self.get_new_inner_puzzle()
return puzzle.get_tree_hash()
async def get_new_inner_puzzle(self) -> Program:
return await self.standard_wallet.get_new_puzzle()
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 get_cat_spendable_coins(self, records: Optional[Set[WalletCoinRecord]] = None) -> List[WalletCoinRecord]:
result: List[WalletCoinRecord] = []
record_list: Set[WalletCoinRecord] = await self.wallet_state_manager.get_spendable_coins_for_wallet(
self.id(), records
)
for record in record_list:
lineage = await self.get_lineage_proof_for_coin(record.coin)
if lineage is not None and not lineage.is_none():
result.append(record)
return result
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,
exclude: Optional[List[Coin]] = None,
min_coin_amount: Optional[uint64] = None,
max_coin_amount: Optional[uint64] = None,
excluded_coin_amounts: Optional[List[uint64]] = None,
) -> 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 sign(self, spend_bundle: SpendBundle) -> SpendBundle:
sigs: List[G2Element] = []
for spend in spend_bundle.coin_spends:
args = match_cat_puzzle(uncurry_puzzle(spend.puzzle_reveal.to_program()))
if args is not None:
_, _, inner_puzzle = args
inner_puzzle = get_innerpuz_from_lockup_puzzle(inner_puzzle)
puzzle_hash = inner_puzzle.get_tree_hash()
private = await self.wallet_state_manager.get_private_key(puzzle_hash)
synthetic_secret_key = calculate_synthetic_secret_key(private, DEFAULT_HIDDEN_PUZZLE_HASH)
conditions = conditions_dict_for_solution(
spend.puzzle_reveal.to_program(),
spend.solution.to_program(),
self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM,
)
if conditions is not None:
synthetic_pk = synthetic_secret_key.get_g1()
for pk, msg in pkm_pairs_for_conditions_dict(
conditions, spend.coin.name(), self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA
):
try:
assert bytes(synthetic_pk) == pk
sigs.append(AugSchemeMPL.sign(synthetic_secret_key, msg))
except AssertionError:
raise ValueError("This spend bundle cannot be signed by this DAO CAT wallet")
agg_sig = AugSchemeMPL.aggregate(sigs)
return SpendBundle.aggregate([spend_bundle, SpendBundle([], agg_sig)])
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

View File

@ -0,0 +1,54 @@
from __future__ import annotations
from dataclasses import dataclass
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

View File

@ -0,0 +1,798 @@
from __future__ import annotations
import logging
from typing import Iterator, List, Optional, Tuple
from clvm.casts import int_from_bytes
from clvm.EvalError import EvalError
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.coin_spend import CoinSpend
from chia.util.ints import uint64
from chia.wallet.cat_wallet.cat_utils import CAT_MOD, CAT_MOD_HASH, match_cat_puzzle
from chia.wallet.dao_wallet.dao_info import DAORules
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 create_singleton_puzzle, get_inner_puzzle_from_singleton
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_RESALE_PREVENTION: Program = load_clvm("dao_resale_prevention_layer.clsp")
DAO_RESALE_PREVENTION_HASH: bytes32 = DAO_RESALE_PREVENTION.get_tree_hash()
DAO_CAT_TAIL: Program = load_clvm("genesis_by_coin_id_or_singleton.clsp")
DAO_CAT_TAIL_HASH: bytes32 = DAO_CAT_TAIL.get_tree_hash()
DAO_CAT_LAUNCHER: Program = load_clvm("dao_cat_launcher.clsp")
P2_CONDITIONS_MOD: Program = load_clvm("p2_conditions_curryable.clsp")
P2_CONDITIONS_MOD_HASH: bytes32 = P2_CONDITIONS_MOD.get_tree_hash()
DAO_SAFE_PAYMENT_MOD: Program = load_clvm("dao_safe_payment.clsp")
DAO_SAFE_PAYMENT_MOD_HASH: bytes32 = DAO_SAFE_PAYMENT_MOD.get_tree_hash()
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 singleton_struct_for_id(id: bytes32) -> Program:
singleton_struct: Program = Program.to((SINGLETON_MOD_HASH, (id, SINGLETON_LAUNCHER_HASH)))
return singleton_struct
def create_cat_launcher_for_singleton_id(id: bytes32) -> Program:
singleton_struct = 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 create_new_proposal_puzzle(
proposal_id: bytes32,
cat_tail_hash: bytes32,
treasury_id: bytes32,
proposed_puzzle_hash: bytes32,
) -> Program:
singleton_struct: Program = Program.to((SINGLETON_MOD_HASH, (proposal_id, SINGLETON_LAUNCHER_HASH)))
puzzle: Program = DAO_PROPOSAL_MOD.curry(
singleton_struct,
DAO_PROPOSAL_MOD_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
DAO_LOCKUP_MOD_HASH,
cat_tail_hash,
treasury_id,
0,
0,
"s",
proposed_puzzle_hash,
)
return puzzle
def get_treasury_puzzle(dao_rules: DAORules, treasury_id: bytes32, cat_tail_hash: bytes32) -> Program:
# SINGLETON_STRUCT ; (SINGLETON_MOD_HASH (SINGLETON_ID . LAUNCHER_PUZZLE_HASH))
# PROPOSAL_MOD_HASH
# PROPOSAL_TIMER_MOD_HASH
# CAT_MOD_HASH
# LOCKUP_MOD_HASH
# TREASURY_MOD_HASH
# CAT_TAIL_HASH
singleton_struct: Program = Program.to((SINGLETON_MOD_HASH, (treasury_id, SINGLETON_LAUNCHER_HASH)))
proposal_validator = DAO_PROPOSAL_VALIDATOR_MOD.curry(
singleton_struct,
DAO_PROPOSAL_MOD_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_LOCKUP_MOD_HASH,
DAO_TREASURY_MOD_HASH,
cat_tail_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
)
# 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
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) -> Program:
_, uncurried_args = treasury_puz.uncurry()
validator: Program = uncurried_args.rest().first()
return validator
def create_announcement_condition_for_nft_spend(
# treasury_id: bytes32, TODO: is treasury_id needed here?
nft_id: bytes32,
target_address: bytes32,
) -> Tuple[Program, Program]:
# TODO: this delegated puzzle does not actually work with NFTs - need to copy more of the code later
delegated_puzzle = Program.to([(1, [[51, target_address, 1]])])
announcement_condition = Program.to([62, Program.to([nft_id, delegated_puzzle.get_tree_hash()]).get_tree_hash()])
return announcement_condition, delegated_puzzle
def get_update_proposal_puzzle(dao_rules: DAORules, proposal_validator: Program) -> Program:
update_proposal = DAO_UPDATE_PROPOSAL_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 update_proposal
def get_dao_rules_from_update_proposal(puzzle: Program) -> DAORules:
mod, curried_args = puzzle.uncurry()
if mod != DAO_UPDATE_PROPOSAL_MOD:
raise ValueError("Not an update proposal.")
(
_,
proposal_validator,
proposal_timelock,
soft_close_length,
attendance_required,
pass_percentage,
self_destruct_length,
oracle_spend_delay,
) = curried_args.as_iter()
# proposal_timelock: uint64
# soft_close_length: uint64
# attendance_required: uint64
# pass_percentage: uint64
# self_destruct_length: uint64
# oracle_spend_delay: uint64
curried_args = uncurry_proposal_validator(proposal_validator)
(
SINGLETON_STRUCT,
PROPOSAL_MOD_HASH,
PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
LOCKUP_MOD_HASH,
TREASURY_MOD_HASH,
CAT_TAIL_HASH,
PROPOSAL_MINIMUM_AMOUNT,
PAYOUT_PUZHASH,
) = 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: Program
) -> Program:
puzzle: Program = DAO_LOCKUP_MOD.curry(
DAO_PROPOSAL_MOD_HASH,
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
DAO_LOCKUP_MOD_HASH,
DAO_FINISHED_STATE_HASH,
CAT_MOD_HASH,
cat_tail_hash,
previous_votes_list, # TODO: maybe format check this in this function
innerpuz,
)
return puzzle
def get_latest_lockup_puzzle_for_coin_spend(parent_spend: CoinSpend, inner_puzzle: Optional[Program] = None) -> Program:
puzzle = get_inner_puzzle_from_singleton(parent_spend.puzzle_reveal)
assert isinstance(puzzle, Program)
solution = parent_spend.solution.to_program().rest().rest().first()
if solution.first() == Program.to(0):
return puzzle
new_proposal_id = solution.rest().rest().rest().first().as_atom()
return add_proposal_to_active_list(puzzle, new_proposal_id, inner_puzzle)
def add_proposal_to_active_list(
lockup_puzzle: Program, proposal_id: bytes32, inner_puzzle: Optional[Program] = None
) -> Program:
curried_args = uncurry_lockup(lockup_puzzle).as_iter()
(
PROPOSAL_MOD_HASH,
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_PUZHASH,
LOCKUP_MOD_HASH,
DAO_FINISHED_STATE_HASH,
CAT_MOD_HASH,
CAT_TAIL_HASH,
ACTIVE_VOTES,
INNERPUZ,
) = curried_args
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 = uncurry_lockup(lockup_puzzle)
(
_PROPOSAL_MOD_HASH,
_SINGLETON_MOD_HASH,
_SINGLETON_LAUNCHER_HASH,
_LOCKUP_MOD_HASH,
_DAO_FINISHED_STATE_HASH,
_CAT_MOD_HASH,
_CAT_TAIL_HASH,
ACTIVE_VOTES,
_INNERPUZ,
) = list(curried_args.as_iter())
return Program(ACTIVE_VOTES)
def get_innerpuz_from_lockup_puzzle(lockup_puzzle: Program) -> Program:
curried_args = uncurry_lockup(lockup_puzzle)
(
_PROPOSAL_MOD_HASH,
_SINGLETON_MOD_HASH,
_SINGLETON_LAUNCHER_HASH,
_LOCKUP_MOD_HASH,
_DAO_FINISHED_STATE_HASH,
_CAT_MOD_HASH,
_CAT_TAIL_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)
"""
singleton_struct: Program = Program.to((SINGLETON_MOD_HASH, (proposal_id, SINGLETON_LAUNCHER_HASH)))
puzzle = DAO_PROPOSAL_MOD.curry(
singleton_struct,
DAO_PROPOSAL_MOD_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
DAO_LOCKUP_MOD_HASH,
cat_tail_hash,
treasury_id,
votes_sum,
total_votes,
proposed_puzzle_hash,
)
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)))
puzzle: Program = DAO_PROPOSAL_TIMER_MOD.curry(
DAO_PROPOSAL_MOD_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
DAO_LOCKUP_MOD_HASH,
DAO_FINISHED_STATE_HASH,
CAT_MOD_HASH,
cat_tail_hash,
parent_singleton_struct,
treasury_id,
)
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_MOD_HASH,
PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
LOCKUP_MOD_HASH,
TREASURY_MOD_HASH,
CAT_TAIL_HASH,
PROPOSAL_MINIMUM_AMOUNT,
PAYOUT_PUZHASH,
) = curried_args.as_iter()
return DAORules(
uint64(int_from_bytes(proposal_timelock.as_atom())),
uint64(int_from_bytes(soft_close_length.as_atom())),
uint64(int_from_bytes(attendance_required.as_atom())),
uint64(int_from_bytes(pass_percentage.as_atom())),
uint64(int_from_bytes(self_destruct_length.as_atom())),
uint64(int_from_bytes(oracle_spend_delay.as_atom())),
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]:
# curried_args = uncurry_treasury(puzzle_reveal)
# (
# DAO_TREASURY_MOD_HASH,
# DAO_PROPOSAL_VALIDATOR_MOD,
# proposal_timelock,
# soft_close_length,
# attendance_required_percentage,
# proposal_pass_percentage,
# proposal_self_destruct_length,
# oracle_spend_delay,
# ) = curried_args
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_PROPOSAL_VALIDATOR,
proposal_timelock,
soft_close_length,
attendance_required,
pass_percentage,
self_destruct_length,
oracle_spend_delay,
) = curried_args.as_iter()
return DAO_TREASURY_MOD.curry(
DAO_TREASURY_MOD_HASH,
DAO_PROPOSAL_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):
curried_args = uncurry_proposal(puzzle_reveal)
(
SINGLETON_STRUCT, # (SINGLETON_MOD_HASH, (SINGLETON_ID, LAUNCHER_PUZZLE_HASH))
PROPOSAL_MOD_HASH,
PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
TREASURY_MOD_HASH,
LOCKUP_MOD_HASH,
CAT_TAIL_HASH,
TREASURY_ID,
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
INNERPUZ_HASH,
) = curried_args.as_iter()
added_votes = solution.at("ff").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
else:
# Vote Type: YES
new_yes_votes = YES_VOTES.as_int() + added_votes
return DAO_PROPOSAL_MOD.curry(
SINGLETON_STRUCT,
DAO_PROPOSAL_MOD_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
DAO_LOCKUP_MOD_HASH,
CAT_TAIL_HASH,
TREASURY_ID,
new_yes_votes,
new_total_votes,
INNERPUZ_HASH,
)
else:
# we are in the finished state, puzzle is the same as ever
mod, currieds = puzzle_reveal.uncurry() # singleton
if mod == DAO_PROPOSAL_MOD:
(
SINGLETON_STRUCT, # (SINGLETON_MOD_HASH, (SINGLETON_ID, LAUNCHER_PUZZLE_HASH))
PROPOSAL_MOD_HASH,
PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
TREASURY_MOD_HASH,
LOCKUP_MOD_HASH,
CAT_TAIL_HASH,
TREASURY_ID,
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
INNERPUZ_HASH,
) = currieds.as_iter()
else:
SINGLETON_STRUCT, dao_finished_hash = currieds.as_iter()
assert SINGLETON_STRUCT is not None
return get_finished_state_puzzle(SINGLETON_STRUCT.rest().first().as_atom())
def get_finished_state_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 create_singleton_puzzle(finished_inner_puz, proposal_id)
def get_cat_tail_hash_from_treasury_puzzle(treasury_puzzle: Program) -> bytes32:
curried_args = uncurry_treasury(treasury_puzzle)
(
_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_MOD_HASH,
PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
LOCKUP_MOD_HASH,
TREASURY_MOD_HASH,
CAT_TAIL_HASH,
PROPOSAL_MINIMUM_AMOUNT,
PAYOUT_PUZHASH,
) = curried_args.as_iter()
return bytes32(CAT_TAIL_HASH.as_atom())
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:
return None
elif mod == CAT_MOD:
return bytes32(curried_args.at("rf").as_atom())
elif mod == SINGLETON_MOD:
return bytes32(curried_args.at("frf").as_atom())
else:
raise ValueError("DAO received coin with unknown puzzle")
def uncurry_proposal_validator(proposal_validator_program: Program) -> Program:
try:
mod, curried_args = proposal_validator_program.uncurry()
except ValueError as e:
log.debug("Cannot uncurry treasury puzzle: error: %s", e)
raise e
if mod != DAO_PROPOSAL_VALIDATOR_MOD:
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:
log.debug("Cannot uncurry treasury puzzle: error: %s", e)
raise e
if mod != DAO_TREASURY_MOD:
raise ValueError("Not a Treasury mod.")
return list(curried_args.as_iter())
def uncurry_proposal(proposal_puzzle: Program) -> Program:
try:
mod, curried_args = proposal_puzzle.uncurry()
except ValueError as e:
log.debug("Cannot uncurry proposal puzzle: error: %s", e)
raise e
if mod != DAO_PROPOSAL_MOD:
raise ValueError("Not a dao proposal mod.")
return curried_args
def uncurry_lockup(lockup_puzzle: Program) -> Program:
try:
mod, curried_args = lockup_puzzle.uncurry()
except ValueError as e:
log.debug("Cannot uncurry lockup puzzle: error: %s", e)
raise e
if mod != DAO_LOCKUP_MOD:
raise ValueError("Not a dao cat lockup mod.")
return curried_args
def get_proposal_args(puzzle: Program) -> Tuple[str, Program]:
try:
mod, curried_args = puzzle.uncurry()
except ValueError as e:
log.debug("Cannot uncurry spend puzzle: error: %s", e)
raise e
if mod == SPEND_P2_SINGLETON_MOD:
return "spend", curried_args
elif mod == DAO_UPDATE_PROPOSAL_MOD:
return "update", curried_args
else:
raise ValueError("Unrecognised proposal type")
def uncurry_spend_p2_singleton(spend_puzzle: Program) -> Program:
try:
mod, curried_args = spend_puzzle.uncurry()
except ValueError as e:
log.debug("Cannot uncurry spend puzzle: error: %s", e)
raise e
if mod != SPEND_P2_SINGLETON_MOD:
raise ValueError("Not a spend p2_singleton mod.")
return curried_args
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
# TODO: move curry_singleton to chia.wallet.singleton
# TODO: Is this correct? See create_fullpuz
# TODO: innerpuz type: is innerpuz a full reveal, or a hash?
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)
def get_curry_vals_from_proposal_puzzle(proposal_puzzle: Program) -> Tuple[Program, Program, Program]:
curried_args = uncurry_proposal(proposal_puzzle)
(
SINGLETON_STRUCT,
PROPOSAL_MOD_HASH,
PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
TREASURY_MOD_HASH,
LOCKUP_MOD_HASH,
CAT_TAIL_HASH,
TREASURY_ID,
YES_VOTES,
TOTAL_VOTES,
PROPOSED_PUZ_HASH,
) = curried_args.as_iter()
return YES_VOTES, TOTAL_VOTES, PROPOSED_PUZ_HASH
# 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:
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:
mod, curried_args = curried_args.rest().first().uncurry()
if mod == DAO_PROPOSAL_MOD:
return curried_args.as_iter() # type: ignore[no-any-return]
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:
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) -> Optional[bool]:
# TODO: handle case where solution is for existing p2_singleton
try:
if match_cat_puzzle(uncurried):
conditions = solution.at("frfr").as_iter()
elif uncurried.mod == MOD:
conditions = solution.at("rfr").as_iter()
else:
return None
for cond in conditions:
if (cond.list_len() == 4) and (cond.first().as_int() == 51):
maybe_treasury_id = cond.at("rrrff")
if cond.at("rf") == get_p2_singleton_puzhash(maybe_treasury_id):
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())
maybe_dao_lockup = uncurried.args.at("rrf").uncurry()
if maybe_dao_lockup[0] == DAO_LOCKUP_MOD:
innerpuz = maybe_dao_lockup[1].at("rrrrrrrf").uncurry()[0]
arg_list[2] = innerpuz
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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,33 @@
from __future__ import annotations
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
def get_dao_inner_puzhash_by_p2(
p2_puzzle_hash: bytes32,
launcher_id: bytes32,
metadata: Program = Program.to([]),
) -> bytes32:
"""
Calculate DAO inner puzzle hash from a P2 puzzle hash
:param p2_puzzle_hash: P2 puzzle hash
:param launcher_id: ID of the launch coin
:param metadata: DAO metadata
:return: DAO inner puzzle hash
"""
# backup_ids_hash = Program(Program.to(recovery_list)).get_tree_hash()
# singleton_struct = Program.to((SINGLETON_TOP_LAYER_MOD_HASH, (launcher_id, LAUNCHER_PUZZLE_HASH)))
#
# quoted_mod_hash = calculate_hash_of_quoted_mod_hash(TREASURY_INNERPUZ_MOD_HASH)
#
# return curry_and_treehash(
# quoted_mod_hash,
# p2_puzzle_hash,
# Program.to(backup_ids_hash).get_tree_hash(),
# Program.to(num_of_backup_ids_needed).get_tree_hash(),
# Program.to(singleton_struct).get_tree_hash(),
# metadata.get_tree_hash(),
# )
return bytes32(b"0" * 32)

View File

@ -233,7 +233,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())

View File

@ -187,7 +187,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

View File

@ -146,7 +146,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
@ -181,7 +181,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)

View 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)))
)
()
)
)
)

View File

@ -0,0 +1,71 @@
; This is an innerpuz inside a DAO CAT, or any CAT, or even any coin, which locks it up until a payment gets made
(mod (
INPUT_RATIO_INT ; the default here is 1 - meaning for one chia input you get exactly the OUTPUT_RATIO_INT output
OUTPUT_RATIO_INT ; the default here is 1 - meaning for one output you must pay exactly the INPUT_RATIO_INT as input
; IMPORTANT NOTE: this does not implicitly take into account the 1:1000 Chia/CAT conversation that the UI uses
SETTLEMENT_PAYMENTS_PUZHASH ; a notarized coin payment is `(nonce . ((puzzle_hash amount ...) (puzzle_hash amount ...) ...))`
; here we will use this coin's ID as the nonce to prevent someone from claiming multiple buy ins at once
BUY_IN_PAYMENT_ADDRESS ; this is the address we check that a payment has been made to before paying out
; for the DAO we curry the p2_singleton with the DAO Treasury ID in the wallet and then curry that here
new_puzhash ; this is secured by creating an announcement here which must be asserted elsewhere
; ^ probably by the puzzle that creates the settlement. Where that, the settlement, and this are spent together.
payment_amount ; secured in the same announcement as above
my_coin_id
my_puzhash
my_amount
)
(include condition_codes.clib)
(include curry-and-treehash.clib)
(include *standard-cl-21*)
(defconstant TEN_THOUSAND 10000)
; TODO: optimise out the repeated divmod - apparently it costs 1116 (third most expensive op)
(defun calculate_output (INPUT_RATIO_INT OUTPUT_RATIO_INT payment_amount)
(+
(* (f (divmod payment_amount INPUT_RATIO_INT)) OUTPUT_RATIO_INT)
(calculate_percentage output (f (divmod (* (r (divmod payment_amount INPUT_RATIO_INT)) TEN_THOUSAND) INPUT_RATIO_INT)))
)
)
(defun-inline calculate_percentage (amount percentage)
(f (divmod (* amount percentage) TEN_THOUSAND))
)
(defun create_outputs (my_puzhash new_puzhash my_amount calculated_output)
(list
(list CREATE_COIN my_puzhash (- my_amount calculate_output))
(list CREATE_COIN new_puzhash calculated_output)
)
)
(c
(list
ASSERT_PUZZLE_ANNOUNCEMENT
(sha256
SETTLEMENT_PAYMENTS_PUZHASH
(sha256tree
(c
my_coin_id
(list (list BUY_IN_PAYMENT_ADDRESS payment_amount))
)
)
)
)
(c
(list CREATE_COIN_ANNOUNCEMENT (sha256tree (c new_puzhash payment_amount)))
(c
(list ASSERT_MY_ID my_coin_id)
(c
(list ASSERT_MY_PUZZLEHASH my_puzhash)
(c
(list ASSERT_MY_AMOUNT my_amount)
(create_outputs my_puzhash new_puzhash my_amount (calculate_output INPUT_RATIO_INT OUTPUT_RATIO_INT payment_amount))
)
)
)
)
)
)

View File

@ -0,0 +1 @@
ff02ffff01ff04ffff04ffff013fffff04ffff0bff17ffff02ff04ffff04ff02ffff04ffff04ff82017fffff04ffff04ff2fffff04ff8200bfffff01808080ffff01808080ff8080808080ffff01808080ffff04ffff04ffff013cffff04ffff02ff04ffff04ff02ffff04ffff04ff5fff8200bf80ff80808080ffff01808080ffff04ffff04ffff018c4153534552545f4d595f4944ffff04ff82017fffff01808080ffff04ffff04ffff0148ffff04ff8202ffffff01808080ffff04ffff04ffff0149ffff04ff8205ffffff01808080ffff02ff0effff04ff02ffff04ff8202ffffff04ff5fffff04ff8205ffffff04ffff02ff0affff04ff02ffff04ff05ffff04ff0bffff04ff8200bfff808080808080ff808080808080808080808080ffff04ffff01ffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff04ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff04ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ffff10ffff12ffff05ffff14ff17ff058080ff0b80ffff05ffff14ffff12ffff01866f7574707574ffff05ffff14ffff12ffff06ffff14ff17ff058080ffff0182271080ff05808080ffff01822710808080ff04ffff04ffff0133ffff04ff05ffff04ffff11ff17ffff04ffff0102ffff04ffff04ffff0101ff0a80ffff04ffff04ffff0104ffff04ffff04ffff0101ff0280ffff01ff01808080ff8080808080ff80808080ffff04ffff04ffff0133ffff04ff0bffff04ff2fff80808080ff808080ff018080

View 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
)
)

View File

@ -0,0 +1 @@
ff02ffff01ff04ffff04ff06ffff04ff05ffff04ff0bffff04ffff04ff05ff8080ff8080808080ffff04ffff04ff04ffff04ff0bff808080ffff04ffff04ff06ffff04ff80ffff04ffff01818fffff04ff17ffff04ff2fff808080808080ff80808080ffff04ffff01ff4933ff018080

View 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
)
)
)
)

View File

@ -0,0 +1 @@
ff02ffff01ff04ffff04ff34ffff04ff2fffff04ff5fffff04ffff04ff2fff8080ff8080808080ffff04ffff04ff28ffff04ff5fff808080ffff02ff36ffff04ff02ffff04ffff0bff17ffff02ff26ffff04ff02ffff04ff05ffff04ff0bff8080808080ff3c80ffff04ff2fffff04ff5fff8080808080808080ffff04ffff01ffffff3dff4947ffff0233ff0401ffff01ff02ff02ffff03ff05ffff01ff02ff3affff04ff02ffff04ff0dffff04ffff0bff2affff0bff3cff2c80ffff0bff2affff0bff2affff0bff3cff1280ff0980ffff0bff2aff0bffff0bff3cff8080808080ff8080808080ffff010b80ff0180ffffff02ff2effff04ff02ffff04ff09ffff04ff0bffff04ffff02ff3effff04ff02ffff04ff05ff80808080ff808080808080ff04ffff04ff10ffff04ffff0bff05ffff02ff3effff04ff02ffff04ffff04ffff016dffff04ff0bff808080ff8080808080ff808080ffff04ffff04ff38ffff04ff05ff808080ff808080ffff0bff2affff0bff3cff2480ffff0bff2affff0bff2affff0bff3cff1280ff0580ffff0bff2affff02ff3affff04ff02ffff04ff07ffff04ffff0bff3cff3c80ff8080808080ffff0bff3cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080

View File

@ -0,0 +1,57 @@
(mod
(
SINGLETON_STRUCT ; ((SINGLETON_MOD_HASH, (SINGLETON_ID, LAUNCHER_PUZZLE_HASH)))
DAO_DIVIDEND_MOD
DAO_DIVIDEND_TIMER_MOD
CAT_MOD_HASH
TREASURY_MOD_HASH
LOCKUP_MOD_HASH
STARTING_CHIA_AMOUNT
CURRENT_CAT_ISSUANCE
TIMELOCK
spend_type
my_amount
)
(include condition_codes.clib)
(include curry-and-treehash.clib)
(defconstant dao_finished_state 0xfb015415f2e6a09c1141f880fc2135beec6adf2a19e4d02a191432846db16559)
; TODO: THIS FILE IS NOT FINISHED
(defun-inline calculate_percentage (amount percentage)
(f (divmod (* amount percentage) TEN_THOUSAND))
)
(defun calculate_dividend_timer_puzzlehash (DAO_DIVIDEND_TIMER_MOD)
()
)
(if spend_type
; claim
(c
(list ASSERT_MY_AMOUNT my_amount)
(if (> my_amount STARTING_CHIA_AMOUNT)
(if (= my_amount STARTING_CHIA_AMOUNT)
(list CREATE_COIN (calculate_dividend_timer_puzzlehash ) 0) ; create proposal timer - this prevents the dividend from starting with inaccurate amounts
()
)
(x)
)
)
; move to finished state
(list
(list CREATE_COIN dao_finished_state ONE)
(list ASSERT_MY_AMOUNT my_amount)
(list ASSERT_PUZZLE_ANNOUNCEMENT ; assert that the treasury has actually taken the payment
(sha256
;; Commented out to pass clvm compile checks
;; (calculate_treasury_puzzlehash)
(sha256tree (list (- my_amount ONE) (f (r SINGLETON_STRUCT))))
)
)
)
)
)

View File

@ -0,0 +1 @@
ff02ffff01ff02ffff03ff820bffffff01ff04ffff04ff08ffff04ff8217ffff808080ffff02ffff03ffff15ff8217ffff82017f80ffff01ff02ffff03ffff09ff8217ffff82017f80ffff01ff04ff1cffff04ffff02ff1affff04ff02ff808080ffff01ff80808080ff8080ff0180ffff01ff088080ff018080ffff01ff04ffff04ff1cffff04ff16ffff04ff12ff80808080ffff04ffff04ff08ffff04ff8217ffff808080ffff04ffff04ff14ffff04ffff0bffff02ff1effff04ff02ffff04ffff04ffff11ff8217ffff1280ffff04ff15ff808080ff8080808080ff808080ff8080808080ff0180ffff04ffff01ffff49ff3f33ffff0180ffa0fb015415f2e6a09c1141f880fc2135beec6adf2a19e4d02a191432846db16559ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080

View File

@ -0,0 +1,96 @@
; This is a persistent timer for a proposal which allows it to have a relative time that survives despite it being recreated.
; It has a curried TIMELOCK, which is applied as a ASSERT_HEIGHT_RELATIVE
; It creates/asserts announcements to pair it with the finishing spend of a proposal
(mod (
DIVIDEND_MOD_HASH
DIVIDEND_TIMER_MOD_HASH
CAT_MOD_HASH
CAT_TAIL
CURRENT_CAT_ISSUANCE
TIMELOCK
MY_PARENT_SINGLETON_STRUCT
TREASURY_ID
)
(include condition_codes.clib)
(include curry-and-treehash.clib)
(include *standard-cl-21*)
; TODO: THIS FILE IS NOT FINISHED
(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_dividend_puzzlehash (
PROPOSAL_SINGLETON_STRUCT
PROPOSAL_MOD_HASH
PROPOSAL_TIMER_MOD_HASH
CAT_MOD_HASH
CAT_TAIL
CURRENT_CAT_ISSUANCE
PROPOSAL_PASS_PERCENTAGE
TREASURY_ID
PROPOSAL_TIMELOCK
proposal_current_votes
proposal_total_votes
proposal_innerpuzhash
)
(puzzle-hash-of-curried-function PROPOSAL_MOD_HASH
proposal_innerpuzhash
(sha256 ONE proposal_total_votes)
(sha256 ONE proposal_current_votes)
(sha256 ONE PROPOSAL_TIMELOCK)
(sha256 ONE TREASURY_ID)
(sha256 ONE PROPOSAL_PASS_PERCENTAGE)
(sha256 ONE CURRENT_CAT_ISSUANCE)
(sha256 ONE CAT_TAIL)
(sha256 ONE CAT_MOD_HASH)
(sha256 ONE PROPOSAL_TIMER_MOD_HASH)
(sha256 ONE PROPOSAL_MOD_HASH)
(sha256tree PROPOSAL_SINGLETON_STRUCT)
)
)
; saves the calculated proposal puzzlehash to use in two conditions rather than calculating it twice
(defun generate_proposal_conditions (TREASURY_ID dividend_puzzlehash proposal_parent_id proposal_amount)
(list
(list ASSERT_PUZZLE_ANNOUNCEMENT (sha256 dividend_puzzlehash TREASURY_ID))
(list ASSERT_MY_PARENT_ID (calculate_coin_id proposal_parent_id dividend_puzzlehash proposal_amount))
)
)
; main
(c
(list ASSERT_HEIGHT_RELATIVE PROPOSAL_TIMELOCK)
(c
(list CREATE_PUZZLE_ANNOUNCEMENT (f (r MY_PARENT_SINGLETON_STRUCT)))
(generate_proposal_conditions
TREASURY_ID
(calculate_singleton_puzzle_hash MY_PARENT_SINGLETON_STRUCT
(calculate_dividend_puzzlehash
MY_PARENT_SINGLETON_STRUCT
PROPOSAL_MOD_HASH
PROPOSAL_TIMER_MOD_HASH
CAT_MOD_HASH
CAT_TAIL
CURRENT_CAT_ISSUANCE
PROPOSAL_PASS_PERCENTAGE
TREASURY_ID
PROPOSAL_TIMELOCK
proposal_current_votes
proposal_total_votes
proposal_innerpuzhash
)
)
proposal_parent_id
proposal_amount
)
)
)
)

View File

@ -0,0 +1 @@
ff02ffff01ff04ffff04ffff0152ffff04ffff019150524f504f53414c5f54494d454c4f434bffff01808080ffff04ffff04ffff013effff04ffff05ffff06ff82017f8080ffff01808080ffff02ff1effff04ff02ffff04ff8202ffffff04ffff02ff1affff04ff02ffff04ff82017fffff04ffff02ff16ffff04ff02ffff04ff82017fffff04ffff019150524f504f53414c5f4d4f445f48415348ffff04ffff019750524f504f53414c5f54494d45525f4d4f445f48415348ffff04ff17ffff04ff2fffff04ff5fffff04ffff019850524f504f53414c5f504153535f50455243454e54414745ffff04ff8202ffffff04ffff019150524f504f53414c5f54494d454c4f434bffff04ffff019670726f706f73616c5f63757272656e745f766f746573ffff04ffff019470726f706f73616c5f746f74616c5f766f746573ffff04ffff019570726f706f73616c5f696e6e657270757a68617368ff808080808080808080808080808080ff8080808080ffff04ffff019270726f706f73616c5f706172656e745f6964ffff04ffff018f70726f706f73616c5f616d6f756e74ff808080808080808080ffff04ffff01ffffff02ffff03ff05ffff01ff02ffff01ff02ff08ffff04ff02ffff04ffff06ff0580ffff04ffff0bffff0102ffff0bffff0101ffff010480ffff0bffff0102ffff0bffff0102ffff0bffff0101ffff010180ffff05ff058080ffff0bffff0102ff0bffff0bffff0101ffff018080808080ff8080808080ff0180ffff01ff02ffff010bff018080ff0180ffff0bffff0102ffff01a0a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ffff02ff08ffff04ff02ffff04ff07ffff01ffa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b280808080ffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a808080ff02ffff03ffff22ffff09ffff0dff0580ffff012080ffff09ffff0dff0b80ffff012080ffff15ff17ffff0181ff8080ffff01ff02ffff01ff0bff05ff0bff1780ff0180ffff01ff02ffff01ff0880ff018080ff0180ffffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff12ffff04ff02ffff04ffff05ff0580ff80808080ffff02ff12ffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff02ff14ffff04ff02ffff04ff09ffff04ff0bffff04ffff02ff12ffff04ff02ffff04ff05ff80808080ff808080808080ffff02ff14ffff04ff02ffff04ff0bffff04ff822fffffff04ffff0bffff0101ff8217ff80ffff04ffff0bffff0101ff820bff80ffff04ffff0bffff0101ff8205ff80ffff04ffff0bffff0101ff8202ff80ffff04ffff0bffff0101ff82017f80ffff04ffff0bffff0101ff8200bf80ffff04ffff0bffff0101ff5f80ffff04ffff0bffff0101ff2f80ffff04ffff0bffff0101ff1780ffff04ffff0bffff0101ff0b80ffff04ffff02ff12ffff04ff02ffff04ff05ff80808080ff80808080808080808080808080808080ff04ffff04ffff013fffff04ffff0bff0bff0580ff808080ffff04ffff04ffff0147ffff04ffff02ff1cffff04ff02ffff04ff17ffff04ff0bffff04ff2fff808080808080ff808080ff808080ff018080

View 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)
)
)
)

View File

@ -0,0 +1 @@
ff02ffff01ff04ffff04ffff0148ffff04ffff02ff16ffff04ff02ffff04ffff05ffff06ff018080ffff04ffff02ff1effff04ff02ffff04ff05ffff04ff0bff8080808080ff8080808080ffff01808080ffff04ffff04ffff0149ffff04ffff05ffff06ffff06ffff06ff0180808080ffff01808080ffff04ffff04ffff0133ffff04ffff02ff1effff04ff02ffff04ff05ffff04ff0bff8080808080ffff04ffff05ffff06ffff06ffff06ff0180808080ffff0180808080ffff04ffff04ffff013effff04ffff0180ffff01808080ffff018080808080ffff04ffff01ffffff02ffff03ff05ffff01ff02ffff01ff02ff08ffff04ff02ffff04ffff06ff0580ffff04ffff0bffff0102ffff0bffff0101ffff010480ffff0bffff0102ffff0bffff0102ffff0bffff0101ffff010180ffff05ff058080ffff0bffff0102ff0bffff0bffff0101ffff018080808080ff8080808080ff0180ffff01ff02ffff010bff018080ff0180ff0bffff0102ffff01a0a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ffff02ff08ffff04ff02ffff04ff07ffff01ffa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b280808080ffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a808080ffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff0affff04ff02ffff04ffff05ff0580ff80808080ffff02ff0affff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ffff02ff0cffff04ff02ffff04ff09ffff04ff0bffff04ffff02ff0affff04ff02ffff04ff05ff80808080ff808080808080ff02ff0cffff04ff02ffff04ff0bffff04ffff0bffff0101ff0b80ffff04ffff02ff0affff04ff02ffff04ff05ff80808080ff808080808080ff018080

View File

@ -0,0 +1 @@
7f3cc356732907933a8f9b1ccf16f71735d07340eb38c847aa402e97d75eb40b

View File

@ -0,0 +1,379 @@
; 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 (
PROPOSAL_MOD_HASH
SINGLETON_MOD_HASH
SINGLETON_LAUNCHER_PUZHASH
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
CAT_MOD_HASH
CAT_TAIL_HASH
ACTIVE_VOTES ; "active votes" list
INNERPUZ
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_curry_vals ; list of curry_vals which should match the order of removal_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 "wut")
(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 "butt")
(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 "cut")
)
)
)
(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 "hut" (f (r (f conditions))) my_inner_puzhash vote_added_puzhash)
)
)
(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 "jut")
)
(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 "jart")
)
)
)
; 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
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
CAT_MOD_HASH
CAT_TAIL_HASH
PROPOSAL_MOD_HASH
INNERPUZ
my_id
new_vote_id
active_votes
proposal_curry_vals
)
(if active_votes
(if (= new_vote_id (f active_votes)) ; check new vote id is not equal to an existent vote id
(x "fart")
(check_not_previously_voted
SINGLETON_MOD_HASH
SINGLETON_LAUNCHER_PUZHASH
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
CAT_MOD_HASH
CAT_TAIL_HASH
PROPOSAL_MOD_HASH
INNERPUZ
my_id
new_vote_id
(r active_votes)
proposal_curry_vals
)
)
(list ASSERT_PUZZLE_ANNOUNCEMENT (sha256 ; the problem might be here
(calculate_proposal_puzzlehash
PROPOSAL_MOD_HASH
CAT_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
LOCKUP_MOD_HASH
CAT_TAIL_HASH
(c SINGLETON_MOD_HASH (c new_vote_id SINGLETON_LAUNCHER_PUZHASH))
proposal_curry_vals
)
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_proposal_puzzlehash (
PROPOSAL_MOD_HASH
CAT_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
LOCKUP_MOD_HASH
CAT_TAIL_HASH
proposal_singleton_struct
(
TREASURY_MOD_HASH
PROPOSAL_TIMER_MOD_HASH
TREASURY_ID
YES_VOTES
TOTAL_VOTES
INNERPUZHASH
)
)
(calculate_singleton_puzzle_hash
proposal_singleton_struct
(puzzle-hash-of-curried-function PROPOSAL_MOD_HASH
(sha256 ONE INNERPUZHASH)
(sha256 ONE TOTAL_VOTES)
(sha256 ONE YES_VOTES)
(sha256 ONE TREASURY_ID)
(sha256 ONE CAT_TAIL_HASH)
(sha256 ONE LOCKUP_MOD_HASH)
(sha256 ONE TREASURY_MOD_HASH)
(sha256 ONE DAO_FINISHED_STATE_MOD_HASH)
(sha256 ONE CAT_MOD_HASH)
(sha256 ONE PROPOSAL_TIMER_MOD_HASH)
(sha256 ONE PROPOSAL_MOD_HASH)
(sha256tree proposal_singleton_struct)
)
)
)
; PROPOSAL_MOD_HASH
; SINGLETON_MOD_HASH
; SINGLETON_LAUNCHER_PUZHASH
; LOCKUP_MOD_HASH
; DAO_FINISHED_STATE_MOD_HASH
; CAT_MOD_HASH
; CAT_TAIL_HASH
; ACTIVE_VOTES ; "active votes" list
; INNERPUZ
(defun calculate_lockup_puzzlehash (
PROPOSAL_MOD_HASH
SINGLETON_MOD_HASH
SINGLETON_LAUNCHER_PUZHASH
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
CAT_MOD_HASH
CAT_TAIL_HASH
active_votes
innerpuzhash
)
(puzzle-hash-of-curried-function LOCKUP_MOD_HASH
innerpuzhash
(sha256tree active_votes)
(sha256 ONE CAT_TAIL_HASH)
(sha256 ONE CAT_MOD_HASH)
(sha256 ONE DAO_FINISHED_STATE_MOD_HASH)
(sha256 ONE LOCKUP_MOD_HASH)
(sha256 ONE SINGLETON_LAUNCHER_PUZHASH)
(sha256 ONE SINGLETON_MOD_HASH)
(sha256 ONE PROPOSAL_MOD_HASH)
)
)
(defun for_every_removal_id (
PROPOSAL_MOD_HASH
SINGLETON_MOD_HASH
SINGLETON_LAUNCHER_PUZHASH
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
CAT_MOD_HASH
CAT_TAIL_HASH
ACTIVE_VOTES
INNERPUZ
removal_ids
my_amount
proposal_curry_vals
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
PROPOSAL_MOD_HASH
SINGLETON_MOD_HASH
SINGLETON_LAUNCHER_PUZHASH
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
CAT_MOD_HASH
CAT_TAIL_HASH
ACTIVE_VOTES
INNERPUZ
(r removal_ids)
my_amount
(r proposal_curry_vals)
(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
PROPOSAL_MOD_HASH
SINGLETON_MOD_HASH
SINGLETON_LAUNCHER_PUZHASH
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
CAT_MOD_HASH
CAT_TAIL_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
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
CAT_MOD_HASH
CAT_TAIL_HASH
PROPOSAL_MOD_HASH
INNERPUZ
my_id
new_proposal_vote_id_or_removal_id
ACTIVE_VOTES
proposal_curry_vals
)
(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
PROPOSAL_MOD_HASH
SINGLETON_MOD_HASH
SINGLETON_LAUNCHER_PUZHASH
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
CAT_MOD_HASH
CAT_TAIL_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)) ; (sha256tree (list (f (r SINGLETON_STRUCT)) (f vote_amount_list) vote_info (f coin_id_list)))
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
PROPOSAL_MOD_HASH
SINGLETON_MOD_HASH
SINGLETON_LAUNCHER_PUZHASH
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
CAT_MOD_HASH
CAT_TAIL_HASH
ACTIVE_VOTES
INNERPUZ
new_proposal_vote_id_or_removal_id
my_amount
proposal_curry_vals
()
)
(a INNERPUZ inner_solution)
)
)
)

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
9fa63e652e131f89a9f8bb6f7abb5ffc6ac485a78dcfb8710cd9df5c368774d9

View File

@ -0,0 +1,422 @@
(mod (
SINGLETON_STRUCT ; (SINGLETON_MOD_HASH (SINGLETON_ID . LAUNCHER_PUZZLE_HASH))
PROPOSAL_MOD_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_MOD_HASH
CAT_TAIL_HASH
TREASURY_ID
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
PROPOSED_PUZ_HASH ; this is what runs if this proposal is successful - the inner puzzle of this proposal
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
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-inline calculate_win_percentage (TOTAL PERCENTAGE)
(f (divmod (* TOTAL PERCENTAGE) TEN_THOUSAND))
)
(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)
)
)
(defun calculate_timer_puzhash (
PROPOSAL_MOD_HASH
PROPOSAL_TIMER_MOD_HASH
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
CAT_MOD_HASH
CAT_TAIL_HASH
MY_SINGLETON_STRUCT
TREASURY_ID
)
(puzzle-hash-of-curried-function PROPOSAL_TIMER_MOD_HASH
(sha256 ONE TREASURY_ID)
(sha256tree MY_SINGLETON_STRUCT)
(sha256 ONE CAT_TAIL_HASH)
(sha256 ONE CAT_MOD_HASH)
(sha256 ONE DAO_FINISHED_STATE_MOD_HASH)
(sha256 ONE LOCKUP_MOD_HASH)
(sha256 ONE PROPOSAL_TIMER_MOD_HASH)
(sha256 ONE PROPOSAL_MOD_HASH)
)
)
(defun calculate_lockup_puzzlehash (
PROPOSAL_MOD_HASH
SINGLETON_MOD_HASH
SINGLETON_LAUNCHER_PUZHASH
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
CAT_MOD_HASH
CAT_TAIL_HASH
previous_votes
lockup_innerpuzhash
)
(puzzle-hash-of-curried-function LOCKUP_MOD_HASH
lockup_innerpuzhash
(sha256tree previous_votes)
(sha256 ONE CAT_TAIL_HASH)
(sha256 ONE CAT_MOD_HASH)
(sha256 ONE DAO_FINISHED_STATE_MOD_HASH)
(sha256 ONE LOCKUP_MOD_HASH)
(sha256 ONE SINGLETON_LAUNCHER_PUZHASH)
(sha256 ONE SINGLETON_MOD_HASH)
(sha256 ONE PROPOSAL_MOD_HASH)
)
)
(defun recreate_self (
SINGLETON_STRUCT
PROPOSAL_MOD_HASH
PROPOSAL_TIMER_MOD_HASH
CAT_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
TREASURY_MOD_HASH
LOCKUP_MOD_HASH
CAT_TAIL_HASH
TREASURY_ID
YES_VOTES
TOTAL_VOTES
PROPOSED_PUZ_HASH
)
(puzzle-hash-of-curried-function PROPOSAL_MOD_HASH
(sha256 ONE PROPOSED_PUZ_HASH)
(sha256 ONE TOTAL_VOTES)
(sha256 ONE YES_VOTES)
(sha256 ONE TREASURY_ID)
(sha256 ONE CAT_TAIL_HASH)
(sha256 ONE LOCKUP_MOD_HASH)
(sha256 ONE TREASURY_MOD_HASH)
(sha256 ONE DAO_FINISHED_STATE_MOD_HASH)
(sha256 ONE CAT_MOD_HASH)
(sha256 ONE PROPOSAL_TIMER_MOD_HASH)
(sha256 ONE PROPOSAL_MOD_HASH)
(sha256tree SINGLETON_STRUCT)
)
)
(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_STRUCT
CAT_MOD_HASH
CAT_TAIL_HASH
PROPOSAL_MOD_HASH
PROPOSAL_TIMER_MOD_HASH
TREASURY_MOD_HASH
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
TREASURY_ID
YES_VOTES
TOTAL_VOTES
PROPOSED_PUZ_HASH
coin_id_list
vote_amount_list
previous_votes
lockup_innerpuzhashes
vote_info
sum
output
my_amount
)
(if coin_id_list
(assert (> (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
PROPOSAL_MOD_HASH
(f SINGLETON_STRUCT)
(r (r SINGLETON_STRUCT))
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
CAT_MOD_HASH
CAT_TAIL_HASH
(f previous_votes)
(f lockup_innerpuzhashes)
)
)
(sha256tree (list (f (r SINGLETON_STRUCT)) (f vote_amount_list) vote_info (f coin_id_list)))
)
)
(loop_over_vote_coins
SINGLETON_STRUCT
CAT_MOD_HASH
CAT_TAIL_HASH
PROPOSAL_MOD_HASH
PROPOSAL_TIMER_MOD_HASH
TREASURY_MOD_HASH
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
TREASURY_ID
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
)
)
)
)
(c
(list
CREATE_COIN ; recreate self with vote information added
(recreate_self
SINGLETON_STRUCT
PROPOSAL_MOD_HASH
PROPOSAL_TIMER_MOD_HASH
CAT_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
TREASURY_MOD_HASH
LOCKUP_MOD_HASH
CAT_TAIL_HASH
TREASURY_ID
(if vote_info (+ YES_VOTES sum) YES_VOTES)
(+ TOTAL_VOTES sum)
PROPOSED_PUZ_HASH
)
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
(assert (> self_destruct_time vote_coin_ids_or_proposal_timelock_length)
(list
(list CREATE_COIN (calculate_finished_state SINGLETON_STRUCT 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 (f SINGLETON_STRUCT) (c TREASURY_ID (r (r SINGLETON_STRUCT))))
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
)
)
)
)
; 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 SINGLETON_STRUCT DAO_FINISHED_STATE_MOD_HASH) ONE (list TREASURY_ID))
(c
(list
ASSERT_PUZZLE_ANNOUNCEMENT
(sha256 ; external timer
(calculate_timer_puzhash
PROPOSAL_MOD_HASH
PROPOSAL_TIMER_MOD_HASH
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
CAT_MOD_HASH
CAT_TAIL_HASH
SINGLETON_STRUCT
TREASURY_ID
)
(f (r SINGLETON_STRUCT))
)
)
(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 (f SINGLETON_STRUCT) (c TREASURY_ID (r (r SINGLETON_STRUCT))))
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
)
(f (r SINGLETON_STRUCT)) ; 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 (f SINGLETON_STRUCT) (c TREASURY_ID (r (r SINGLETON_STRUCT))))
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_STRUCT
CAT_MOD_HASH
CAT_TAIL_HASH
PROPOSAL_MOD_HASH
PROPOSAL_TIMER_MOD_HASH
TREASURY_MOD_HASH
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
TREASURY_ID
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_MOD_HASH
PROPOSAL_TIMER_MOD_HASH
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
CAT_MOD_HASH
CAT_TAIL_HASH
SINGLETON_STRUCT
TREASURY_ID
)
0
)
(if (= (sha256tree innerpuz_reveal) PROPOSED_PUZ_HASH) ; reveal the proposed code on chain with the first vote
()
(x)
)
)
)
my_amount
)
)
)
)

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
a27440cdee44f910e80225592e51dc03721a9d819cc358165587fa2b34eef4cd

View File

@ -0,0 +1,124 @@
; 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_MOD_HASH
PROPOSAL_TIMER_MOD_HASH
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_HASH
CAT_MOD_HASH
CAT_TAIL_HASH
(@ MY_PARENT_SINGLETON_STRUCT (SINGLETON_MOD_HASH SINGLETON_ID . LAUNCHER_PUZZLE_HASH))
TREASURY_ID
treasury_mod_hash
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)
)
)
; PROPOSAL_MOD_HASH
; CAT_MOD_HASH
; LOCKUP_MOD_HASH
; CAT_TAIL_HASH
; MY_PARENT_SINGLETON_STRUCT
; treasury_mod_hash
; PROPOSAL_TIMER_MOD_HASH
; TREASURY_ID
; proposal_yes_votes
; proposal_total_votes
; proposal_innerpuzhash
(defun calculate_proposal_puzzlehash (
PROPOSAL_MOD_HASH
CAT_MOD_HASH
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_HASH
CAT_TAIL_HASH
proposal_singleton_struct
TREASURY_MOD_HASH
PROPOSAL_TIMER_MOD_HASH
TREASURY_ID
YES_VOTES
TOTAL_VOTES
INNERPUZHASH
)
(calculate_singleton_puzzle_hash
proposal_singleton_struct
(puzzle-hash-of-curried-function PROPOSAL_MOD_HASH
(sha256 ONE INNERPUZHASH)
(sha256 ONE TOTAL_VOTES)
(sha256 ONE YES_VOTES)
(sha256 ONE TREASURY_ID)
(sha256 ONE CAT_TAIL_HASH)
(sha256 ONE LOCKUP_MOD_HASH)
(sha256 ONE TREASURY_MOD_HASH)
(sha256 ONE DAO_FINISHED_STATE_HASH)
(sha256 ONE CAT_MOD_HASH)
(sha256 ONE PROPOSAL_TIMER_MOD_HASH)
(sha256 ONE PROPOSAL_MOD_HASH)
(sha256tree proposal_singleton_struct)
)
)
)
; 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
PROPOSAL_MOD_HASH
CAT_MOD_HASH
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_HASH
CAT_TAIL_HASH
MY_PARENT_SINGLETON_STRUCT
treasury_mod_hash
PROPOSAL_TIMER_MOD_HASH
TREASURY_ID
proposal_yes_votes
proposal_total_votes
proposal_innerpuzhash
)
proposal_timelock
)
)
(list
ASSERT_MY_PARENT_ID
(sha256
parent_parent
(calculate_proposal_puzzlehash
PROPOSAL_MOD_HASH
CAT_MOD_HASH
LOCKUP_MOD_HASH
DAO_FINISHED_STATE_HASH
CAT_TAIL_HASH
MY_PARENT_SINGLETON_STRUCT
treasury_mod_hash
PROPOSAL_TIMER_MOD_HASH
TREASURY_ID
0
0
proposal_innerpuzhash
)
parent_amount
)
)
)
)

View File

@ -0,0 +1 @@
ff02ffff01ff04ffff04ffff0152ffff04ff825fffffff01808080ffff04ffff04ffff013effff04ffff05ffff06ff82017f8080ffff01808080ffff04ffff04ffff013fffff04ffff0bffff02ff1effff04ff02ffff04ff05ffff04ff5fffff04ff17ffff04ff2fffff04ff8200bfffff04ff82017fffff04ff8205ffffff04ff0bffff04ff8202ffffff04ff820bffffff04ff8217ffffff04ff822fffff808080808080808080808080808080ff825fff80ffff01808080ffff04ffff04ffff0147ffff04ffff0bff8300bfffffff02ff1effff04ff02ffff04ff05ffff04ff5fffff04ff17ffff04ff2fffff04ff8200bfffff04ff82017fffff04ff8205ffffff04ff0bffff04ff8202ffffff04ffff0180ffff04ffff0180ffff04ff822fffff808080808080808080808080808080ff83017fff80ffff01808080ffff018080808080ffff04ffff01ffffff02ffff03ff05ffff01ff02ffff01ff02ff08ffff04ff02ffff04ffff06ff0580ffff04ffff0bffff0102ffff0bffff0101ffff010480ffff0bffff0102ffff0bffff0102ffff0bffff0101ffff010180ffff05ff058080ffff0bffff0102ff0bffff0bffff0101ffff018080808080ff8080808080ff0180ffff01ff02ffff010bff018080ff0180ff0bffff0102ffff01a0a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ffff02ff08ffff04ff02ffff04ff07ffff01ffa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b280808080ffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a808080ffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff0affff04ff02ffff04ffff05ff0580ff80808080ffff02ff0affff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ffff02ff0cffff04ff02ffff04ff09ffff04ff0bffff04ffff02ff0affff04ff02ffff04ff05ff80808080ff808080808080ff02ff16ffff04ff02ffff04ff8200bfffff04ffff02ff0cffff04ff02ffff04ff05ffff04ffff0bffff0101ff822fff80ffff04ffff0bffff0101ff8217ff80ffff04ffff0bffff0101ff820bff80ffff04ffff0bffff0101ff8205ff80ffff04ffff0bffff0101ff5f80ffff04ffff0bffff0101ff1780ffff04ffff0bffff0101ff82017f80ffff04ffff0bffff0101ff2f80ffff04ffff0bffff0101ff0b80ffff04ffff0bffff0101ff8202ff80ffff04ffff0bffff0101ff0580ffff04ffff02ff0affff04ff02ffff04ff8200bfff80808080ff80808080808080808080808080808080ff8080808080ff018080

View File

@ -0,0 +1 @@
5526d8dc33b60a23c86ac7184e8f7051515af16dbb7489555f389b84a5313c84

View File

@ -0,0 +1,111 @@
(mod
(
SINGLETON_STRUCT ; (SINGLETON_MOD_HASH (SINGLETON_ID . LAUNCHER_PUZZLE_HASH))
PROPOSAL_MOD_HASH
PROPOSAL_TIMER_MOD_HASH
CAT_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
LOCKUP_MOD_HASH
TREASURY_MOD_HASH
CAT_TAIL_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_MOD_HASH
PROPOSAL_SINGLETON_STRUCT
PROPOSAL_TIMER_MOD_HASH
CAT_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
TREASURY_MOD_HASH
LOCKUP_MOD_HASH
CAT_TAIL_HASH
TREASURY_ID
proposal_yes_votes
proposal_total_votes
proposal_innerpuz_hash
)
(puzzle-hash-of-curried-function PROPOSAL_MOD_HASH
(sha256 ONE proposal_innerpuz_hash)
(sha256 ONE proposal_total_votes)
(sha256 ONE proposal_yes_votes)
(sha256 ONE TREASURY_ID)
(sha256 ONE CAT_TAIL_HASH)
(sha256 ONE LOCKUP_MOD_HASH)
(sha256 ONE TREASURY_MOD_HASH)
(sha256 ONE DAO_FINISHED_STATE_MOD_HASH)
(sha256 ONE CAT_MOD_HASH)
(sha256 ONE PROPOSAL_TIMER_MOD_HASH)
(sha256 ONE PROPOSAL_MOD_HASH)
(sha256tree PROPOSAL_SINGLETON_STRUCT)
)
)
(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_MOD_HASH
(c (f SINGLETON_STRUCT) (c proposal_id (r (r SINGLETON_STRUCT))))
PROPOSAL_TIMER_MOD_HASH
CAT_MOD_HASH
DAO_FINISHED_STATE_MOD_HASH
TREASURY_MOD_HASH
LOCKUP_MOD_HASH
CAT_TAIL_HASH
(f (r SINGLETON_STRUCT))
yes_votes ; this is where we validate the yes votes and total votes
total_votes
delegated_puzzle_hash
)
)
coin_amount
)
)
(c
(list CREATE_COIN PROPOSAL_EXCESS_PAYOUT_PUZ_HASH (- coin_amount 1))
(c
(list CREATE_PUZZLE_ANNOUNCEMENT proposal_id) ; specify the proposal we're talking about
conditions
)
)
)
)

View File

@ -0,0 +1 @@
ff02ffff01ff02ffff03ffff15ffff10ff8317bfffffff010180ff8205ff80ffff01ff02ffff01ff02ffff03ffff15ff8302bfffff8217ff80ffff01ff02ffff01ff02ffff03ffff15ff8305bfffffff05ffff14ffff12ff8302bfffff822fff80ffff01822710808080ffff01ff02ffff01ff02ffff03ffff09ff83009fffffff02ff0affff04ff02ffff04ff830bbfffffff04ffff02ff0cffff04ff02ffff04ffff05ffff04ffff05ff0580ffff04ff83013fffffff06ffff06ff058080808080ffff04ffff02ff0cffff04ff02ffff04ff0bffff04ffff0bffff0101ff83015fff80ffff04ffff0bffff0101ff8302bfff80ffff04ffff0bffff0101ff8305bfff80ffff04ffff0bffff0101ffff05ffff06ff05808080ffff04ffff0bffff0101ff8202ff80ffff04ffff0bffff0101ff8200bf80ffff04ffff0bffff0101ff82017f80ffff04ffff0bffff0101ff5f80ffff04ffff0bffff0101ff2f80ffff04ffff0bffff0101ff1780ffff04ffff0bffff0101ff0b80ffff04ffff02ff0effff04ff02ffff04ffff04ffff05ff0580ffff04ff83013fffffff06ffff06ff0580808080ff80808080ff80808080808080808080808080808080ffff04ffff02ff0effff04ff02ffff04ffff04ffff05ff0580ffff04ff83013fffffff06ffff06ff0580808080ff80808080ff808080808080ffff04ff8317bfffff80808080808080ffff01ff02ffff01ff04ffff04ffff0133ffff04ff820bffffff04ffff11ff8317bfffffff010180ffff0180808080ffff04ffff04ffff013effff04ff83013fffffff01808080ff83017fff8080ff0180ffff01ff02ffff01ff0880ff018080ff0180ff0180ffff01ff02ffff01ff0880ff018080ff0180ff0180ffff01ff02ffff01ff0880ff018080ff0180ff0180ffff01ff02ffff01ff0880ff018080ff0180ffff04ffff01ffffff02ffff03ff05ffff01ff02ffff01ff02ff08ffff04ff02ffff04ffff06ff0580ffff04ffff0bffff0102ffff0bffff0101ffff010480ffff0bffff0102ffff0bffff0102ffff0bffff0101ffff010180ffff05ff058080ffff0bffff0102ff0bffff0bffff0101ffff018080808080ff8080808080ff0180ffff01ff02ffff010bff018080ff0180ff0bffff0102ffff01a0a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ffff02ff08ffff04ff02ffff04ff07ffff01ffa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b280808080ffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a808080ffff02ffff03ffff22ffff09ffff0dff0580ffff012080ffff09ffff0dff0b80ffff012080ffff15ff17ffff0181ff8080ffff01ff02ffff01ff0bff05ff0bff1780ff0180ffff01ff02ffff01ff0880ff018080ff0180ff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff0effff04ff02ffff04ffff05ff0580ff80808080ffff02ff0effff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff018080

View File

@ -0,0 +1 @@
edff0f36ca097ea55c867f8700cc4d48d267b91fd00ccee2db0fab6fe0645c67

View File

@ -0,0 +1,27 @@
; The function of this file is to force the DAO CAT underneath it to stay in locked voting mode for TIMELOCK period of time
(mod (
TIMELOCK ; absolute block height for when this coin should unlock
INNERPUZ
(@ inner_sol
(
my_id
inner_solution
my_amount
new_proposal_vote_id_or_removal_id ; this is enforced as being empty if we're trying to leave locked state
proposal_curry_vals
vote_info
)
)
)
(include condition_codes.clib)
(include curry-and-treehash.clib)
(include *standard-cl-21*)
; uncurry innerpuz and add the new vote procedure
(if new_proposal_vote_id_or_removal_id
(a INNERPUZ inner_sol) ; not trying to exit
(c (list ASSERT_HEIGHT_ABSOLUTE TIMELOCK) (a INNERPUZ inner_sol)) ; trying to exit
)
)

View File

@ -0,0 +1 @@
ff02ffff01ff02ffff03ff820177ffff01ff02ffff01ff02ff0bff1780ff0180ffff01ff02ffff01ff04ffff04ffff0153ffff04ff05ffff01808080ffff02ff0bff178080ff018080ff0180ffff04ff80ff018080

View File

@ -0,0 +1,24 @@
(mod (SINGLETON_STRUCT CONDITIONS SPEND_AMOUNT treasury_puzzle_hash treasury_amount)
(include condition_codes.clib)
(include curry-and-treehash.clib)
(include *standard-cl-21*)
(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)
)
)
(c
(list ASSERT_MY_AMOUNT treasury_amount)
(c
(list CREATE_COIN treasury_puzzle_hash (- treasury_amount SPEND_AMOUNT))
(c
(list ASSERT_MY_PUZZLEHASH (calculate_full_puzzle_hash SINGLETON_STRUCT treasury_puzzle_hash))
CONDITIONS
)
)
)
)

View File

@ -0,0 +1 @@
ff02ffff01ff04ffff04ffff0149ffff04ff5fffff01808080ffff04ffff04ffff0133ffff04ff2fffff04ffff11ff5fff1780ffff0180808080ffff04ffff04ffff0148ffff04ffff02ff0affff04ff02ffff04ffff05ff0580ffff04ff2fffff04ffff02ff0effff04ff02ffff04ff05ff80808080ff808080808080ffff01808080ff0b808080ffff04ffff01ffff02ffff03ff05ffff01ff02ffff01ff02ff04ffff04ff02ffff04ffff06ff0580ffff04ffff0bffff0102ffff0bffff0101ffff010480ffff0bffff0102ffff0bffff0102ffff0bffff0101ffff010180ffff05ff058080ffff0bffff0102ff0bffff0bffff0101ffff018080808080ff8080808080ff0180ffff01ff02ffff010bff018080ff0180ffff0bffff0102ffff01a0a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ffff02ff04ffff04ff02ffff04ff07ffff01ffa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b280808080ffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a808080ff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff0effff04ff02ffff04ffff05ff0580ff80808080ffff02ff0effff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff018080

View File

@ -0,0 +1,53 @@
(mod (
CONDITIONS
P2_SINGLETON_PUZHASH
p2_singleton_parent_amount_list
)
; NOTE: this code will NOT work for assets which have special layers like CATs and NFTs
; For those you'll need to make a new proposal type
; 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)
(defun loop_through_list (SPEND_AMOUNT P2_SINGLETON_PUZHASH p2_calculated p2_singleton_list total output)
(c
(list CREATE_PUZZLE_ANNOUNCEMENT p2_calculated)
(c
(list ASSERT_COIN_ANNOUNCEMENT (sha256 p2_calculated '$'))
(if p2_singleton_list
(loop_through_list
SPEND_AMOUNT
P2_SINGLETON_PUZHASH
(sha256 (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))))
)
(if (> total SPEND_AMOUNT)
(c (list CREATE_COIN P2_SINGLETON_PUZHASH (- total SPEND_AMOUNT)) output)
(x)
)
)
)
)
)
(defun sum_create_coins (conditions)
(if conditions
(+ (if (= (f (f conditions)) CREATE_COIN) (f (r (r (f conditions)))) 0) (sum_create_coins (r conditions)))
0
)
)
; main
(loop_through_list
(sum_create_coins CONDITIONS)
P2_SINGLETON_PUZHASH
(sha256 (f (f p2_singleton_parent_amount_list)) P2_SINGLETON_PUZHASH (f (r (f p2_singleton_parent_amount_list))))
(r p2_singleton_parent_amount_list)
(f (r (f p2_singleton_parent_amount_list)))
CONDITIONS
)
)

View File

@ -0,0 +1 @@
ff02ffff01ff02ff16ffff04ff02ffff04ffff02ff1effff04ff02ffff04ff05ff80808080ffff04ff0bffff04ffff0bff47ff0bff81a780ffff04ff37ffff04ff81a7ffff04ff05ff808080808080808080ffff04ffff01ffff3d33ff3effff04ffff04ff0affff04ff17ff808080ffff04ffff04ff08ffff04ffff0bff17ffff012480ff808080ffff02ffff03ff2fffff01ff02ff16ffff04ff02ffff04ff05ffff04ff0bffff04ffff0bff818fff0bff82014f80ffff04ff6fffff04ffff10ff5fff82014f80ff8080808080808080ffff01ff02ffff03ffff15ff5fff0580ffff01ff04ffff04ff0cffff04ff0bffff04ffff11ff5fff0580ff80808080ff81bf80ffff01ff088080ff018080ff01808080ff02ffff03ff05ffff01ff10ffff02ffff03ffff09ff11ff0c80ffff0159ff8080ff0180ffff02ff1effff04ff02ffff04ff0dff8080808080ff8080ff0180ff018080

View File

@ -0,0 +1 @@
54964584da94f665a970e4d8b6e2091c4f2abe447886db82f19583f1a7e43ceb

View File

@ -0,0 +1,198 @@
(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)
(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 (
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
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))))
)
(c
(list CREATE_COIN P2_SINGLETON_PUZHASH (- total SPEND_AMOUNT))
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 (
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
(sha256 (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
(c
(list CREATE_COIN P2_SINGLETON_PUZHASH (- (+ total (f (r (f parent_amount_list)))) create_coin_sum))
CONDITIONS_FOR_THIS_ASSET_TYPE
)
)
)
(for_each_asset
CAT_MOD_HASH
CONDITIONS_FOR_THIS_ASSET_TYPE
P2_SINGLETON_PUZHASH
p2_singleton_puzzle_hash
parent_amount_list
(+ total (f (r (f parent_amount_list))))
create_coin_sum
output
)
)
()
)
)
; loops through the list of ((tailhash conditions))
(defun for_each_asset_type (
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 p2_singleton_tailhash_parent_amount_list
(for_each_asset_type
CAT_MOD_HASH
P2_SINGLETON_PUZHASH
(r LIST_OF_TAILHASH_CONDITIONS)
(r p2_singleton_tailhash_parent_amount_list)
(for_each_asset
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
(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))))
(loop_through_list
(sum_create_coins CONDITIONS)
P2_SINGLETON_VIA_DELEGATED_PUZZLE_PUZHASH
(sha256 (f (f p2_singleton_parent_amount_list)) P2_SINGLETON_VIA_DELEGATED_PUZZLE_PUZHASH (f (r (f p2_singleton_parent_amount_list))))
(r p2_singleton_parent_amount_list)
(f (r (f p2_singleton_parent_amount_list)))
(for_each_asset_type
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
)
)
)
)
)

View File

@ -0,0 +1 @@
ff02ffff01ff04ffff04ff30ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff8202ffffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff808080ffff04ffff04ff58ffff04ff8202ffffff04ff34ffff04ffff04ff15ff8080ff8080808080ffff02ff26ffff04ff02ffff04ffff02ff5effff04ff02ffff04ff17ff80808080ffff04ff5fffff04ffff0bff82023fff5fff82053f80ffff04ff8201bfffff04ff82053fffff04ffff02ff7affff04ff02ffff04ff0bffff04ff5fffff04ff2fffff04ff82017fffff04ff17ff8080808080808080ff8080808080808080808080ffff04ffff01ffffffff3d48ff02ff333effff0401ff01ff02ff04ffff04ff78ffff04ffff02ff2effff04ff02ffff04ffff04ff05ffff04ff0bff808080ff80808080ff808080ffff04ffff04ff20ffff04ffff0bff05ffff012480ff808080ff178080ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff5cffff0bff34ff2480ffff0bff5cffff0bff5cffff0bff34ff2c80ff0980ffff0bff5cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ffff02ffff03ff5fffff01ff02ff7cffff04ff02ffff04ffff0bff82011fff2fff82029f80ffff04ffff02ffff03ff81dfffff01ff02ff2effff04ff02ffff01ff80808080ffff01ff02ff2effff04ff02ffff04ffff04ff34ffff04ffff04ff58ffff04ff17ffff04ffff11ffff10ff81bfff82029f80ff82017f80ff80808080ff0b8080ff8080808080ff0180ffff04ffff02ff5affff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ffff10ff81bfff82029f80ffff04ff82017fffff04ff8202ffff8080808080808080808080ff808080808080ff8080ff0180ff02ffff03ff2fffff01ff02ff7affff04ff02ffff04ff05ffff04ff0bffff04ff37ffff04ff6fffff04ffff02ff5affff04ff02ffff04ff05ffff04ffff02ffff03ffff09ff47ff818f80ffff0181a7ffff01ff088080ff0180ffff04ff0bffff04ffff02ff7effff04ff02ffff04ff05ffff04ff818fffff04ff0bff808080808080ffff04ff81cfffff04ff80ffff04ffff02ff5effff04ff02ffff04ff81a7ff80808080ffff04ff5fff8080808080808080808080ff8080808080808080ffff015f80ff0180ffffff04ffff04ff78ffff04ffff02ff2effff04ff02ffff04ffff04ff17ffff04ffff02ff2effff04ff02ffff01ff80808080ff808080ff80808080ff808080ffff04ffff04ff20ffff04ffff0bff17ffff012480ff808080ffff02ffff03ff2fffff01ff02ff26ffff04ff02ffff04ff05ffff04ff0bffff04ffff02ff2affff04ff02ffff04ff818fffff04ff0bffff04ff82014fff808080808080ffff04ff6fffff04ffff10ff5fff82014f80ff8080808080808080ffff01ff04ffff04ff58ffff04ff0bffff04ffff11ff5fff0580ff80808080ff81bf8080ff01808080ff0bff5cffff0bff34ff2880ffff0bff5cffff0bff5cffff0bff34ff2c80ff0580ffff0bff5cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ffff03ff05ffff01ff10ffff02ffff03ffff09ff11ff5880ffff01ff02ffff03ffff15ff59ff8080ffff0159ff8080ff0180ff8080ff0180ffff02ff5effff04ff02ffff04ff0dff8080808080ff8080ff0180ff02ff36ffff04ff02ffff04ff05ffff04ff17ffff04ffff0bff34ff0b80ffff04ffff0bff34ff0580ff80808080808080ff018080

View File

@ -0,0 +1 @@
fb3890f672c9df3cc69699e21446b89b55b65071d35bf5c80a49d11c9b79a68f

View File

@ -0,0 +1,109 @@
(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
(sha256
(f (r singleton_struct))
(calculate_singleton_puzzle_hash singleton_struct my_inner_puzhash)
ONE
)
)
(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
)
)
)
)

View File

@ -0,0 +1 @@
ff02ffff01ff04ffff04ffff013effff04ffff0180ffff01808080ffff02ffff03ff8217ffffff01ff02ffff01ff02ffff03ffff09ffff02ff0affff04ff02ffff04ff8217ffff80808080ff8215ff80ffff01ff02ffff01ff04ffff04ffff013dffff04ffff0bff8209ffffff02ff0affff04ff02ffff04ffff04ff8215ffffff04ff822dffffff01808080ff8080808080ffff01808080ffff02ff0bffff04ff5fffff04ff8200bfffff04ff8205ffffff04ff820bffffff04ffff02ff8217ffff822fff80ffff018080808080808080ff0180ffff01ff02ffff01ff0880ff018080ff0180ff0180ffff01ff02ffff01ff02ff1effff04ff02ffff04ff8202ffffff04ffff02ff0cffff04ff02ffff04ff05ffff04ffff0bffff0101ff8202ff80ffff04ffff0bffff0101ff82017f80ffff04ffff0bffff0101ff8200bf80ffff04ffff0bffff0101ff5f80ffff04ffff0bffff0101ff2f80ffff04ffff0bffff0101ff1780ffff04ffff02ff0affff04ff02ffff04ff0bff80808080ffff04ffff0bffff0101ff0580ff808080808080808080808080ffff04ff825fffff808080808080ff018080ff018080ffff04ffff01ffffff02ffff03ff05ffff01ff02ffff01ff02ff08ffff04ff02ffff04ffff06ff0580ffff04ffff0bffff0102ffff0bffff0101ffff010480ffff0bffff0102ffff0bffff0102ffff0bffff0101ffff010180ffff05ff058080ffff0bffff0102ff0bffff0bffff0101ffff018080808080ff8080808080ff0180ffff01ff02ffff010bff018080ff0180ff0bffff0102ffff01a0a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ffff02ff08ffff04ff02ffff04ff07ffff01ffa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b280808080ffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a808080ffff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff0affff04ff02ffff04ffff05ff0580ff80808080ffff02ff0affff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ffff02ff0cffff04ff02ffff04ff09ffff04ff0bffff04ffff02ff0affff04ff02ffff04ff05ff80808080ff808080808080ff04ffff02ffff03ff17ffff01ff02ffff01ff04ffff0146ffff04ffff0bffff05ffff06ff178080ffff02ff16ffff04ff02ffff04ff17ffff04ff0bff8080808080ffff010180ffff01808080ff0180ffff01ff02ffff01ff04ffff0152ffff04ff05ffff01808080ff018080ff0180ffff04ffff04ffff0133ffff04ff0bffff01ff01808080ff808080ff018080

View File

@ -0,0 +1,36 @@
(mod
(
TREASURY_MOD_HASH
PROPOSAL_VALIDATOR
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)
(sha256tree PROPOSAL_VALIDATOR)
(sha256 ONE TREASURY_MOD_HASH)
)
ONE
)
)
)

View File

@ -0,0 +1 @@
ff02ffff01ff04ffff04ffff0133ffff04ffff02ff0affff04ff02ffff04ff05ffff04ffff0bffff0101ff8202ff80ffff04ffff0bffff0101ff82017f80ffff04ffff0bffff0101ff8200bf80ffff04ffff0bffff0101ff5f80ffff04ffff0bffff0101ff2f80ffff04ffff0bffff0101ff1780ffff04ffff02ff0effff04ff02ffff04ff0bff80808080ffff04ffff0bffff0101ff0580ff808080808080808080808080ffff04ffff0101ffff0180808080ffff018080ffff04ffff01ffff02ffff03ff05ffff01ff02ffff01ff02ff04ffff04ff02ffff04ffff06ff0580ffff04ffff0bffff0102ffff0bffff0101ffff010480ffff0bffff0102ffff0bffff0102ffff0bffff0101ffff010180ffff05ff058080ffff0bffff0102ff0bffff0bffff0101ffff018080808080ff8080808080ff0180ffff01ff02ffff010bff018080ff0180ffff0bffff0102ffff01a0a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222ffff0bffff0102ffff0bffff0102ffff01a09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b2ff0580ffff0bffff0102ffff02ff04ffff04ff02ffff04ff07ffff01ffa09dcf97a184f32623d11a73124ceb99a5709b083721e878a16d78f596718ba7b280808080ffff01a04bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a808080ff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff0effff04ff02ffff04ffff05ff0580ff80808080ffff02ff0effff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ff018080

View File

@ -7,6 +7,24 @@
"covenant_layer": "b982796850336aabf9ab17c3f21e299f0c633444117ab5e9ebeafadf1860d9fc",
"create_nft_launcher_from_did": "7a32d2d9571d3436791c0ad3d7fcfdb9c43ace2b0f0ff13f98d29f0cc093f445",
"credential_restriction": "2fdfc1f058cfd65e7ec4e253bfeb394da163ecd0036f508df8629b0a2b8fde96",
"dao_cat_buy_in": "2421f1375866bd9e27ed35179003018e1e44cbdfee96592f5c6eb62c7ababd8a",
"dao_cat_eve": "488f55bedaca5a599544dfd5ab341e2e5c7e6fca67d9b98a3d856f876c52f53e",
"dao_cat_launcher": "15eb3382528a5d25af7f2af3942cd31e71dacf41320910698a753179684d970d",
"dao_dividend": "e3e4e15e7dd1694d776ba716c21dfff181530852682498220002ae72a8a261b2",
"dao_dividend_timer": "f85df70f39c65b836cccfa5c93939452319ab42136f653ffee3bbd795eb022dc",
"dao_finished_state": "7f3cc356732907933a8f9b1ccf16f71735d07340eb38c847aa402e97d75eb40b",
"dao_lockup": "9fa63e652e131f89a9f8bb6f7abb5ffc6ac485a78dcfb8710cd9df5c368774d9",
"dao_proposal": "a27440cdee44f910e80225592e51dc03721a9d819cc358165587fa2b34eef4cd",
"dao_proposal_timer": "87f2808340dab6a5fb8194c501b3133f1f850c5a8bf422e7b049762486819813",
"dao_proposal_validator": "0737418c68d175fcd5bb7584a6c736f8daca184f69f50bf8ad021592b5e8b538",
"dao_resale_prevention_layer": "5940dea172de20b34149f709931057eb27cd0d1e769830102345bb7b1e4751c5",
"dao_safe_payment": "3a86a13d78980b4bed1c709834630b47e4d3204125f1e1341be4ca40978649dd",
"dao_spend_p2_singleton": "54964584da94f665a970e4d8b6e2091c4f2abe447886db82f19583f1a7e43ceb",
"dao_spend_p2_singleton_v2": "5163024ea180c9ee3949da9db3105a0e3b706a1248ae4138ecd7078d77d5b7c9",
"dao_treasury": "8db946dbea7db3c6e99f45225b4159bd5f2ed0b53576a5c80aca2fe8ffdd99b6",
"dao_treasury_with_before_cond": "f1b1ae5386e9668a4ac2cd3bb34d9e610f0d324189619f5eb0c3c50437bb4d0f",
"dao_treasury_with_hack": "32865fb271c24f6092085fecaa3a4fa6d32d7436773be06700a0aa47539ebdcc",
"dao_update_proposal": "70567fde4ec4b6ed83a62bc0e92c3a391203b0d6f7a4d4bf3516c95662ba316c",
"decompress_coin_spend_entry": "9d98ed08770d31be4bd1bde4705dab388db5e7e9c349f5a76fc3c347aa3a0b79",
"decompress_coin_spend_entry_with_prefix": "92aa4bc8060a8836355a1884075141b4791ce1b67ae6092bb166b2845954bc89",
"decompress_puzzle": "fe94c58f1117afe315e0450daca1c62460ec1a1c439cd4018d79967a5d7d1370",
@ -19,6 +37,7 @@
"exigent_metadata_layer": "d5fd32e069fda83e230ccd8f6a7c4f652231aed5c755514b3d996cbeff4182b8",
"flag_proofs_checker": "fe2e3c631562fbb9be095297f762bf573705a0197164e9361ad5d50e045ba241",
"genesis_by_coin_id": "493afb89eed93ab86741b2aa61b8f5de495d33ff9b781dfc8919e602b2afa150",
"genesis_by_coin_id_or_singleton": "3bb7d336e4aaee5cd18bb3d8a01e15acfd56c80ff6801261783596c88a16603b",
"genesis_by_puzzle_hash": "de5a6e06d41518be97ff6365694f4f89475dda773dede267caa33da63b434e36",
"graftroot_dl_offers": "0893e36a88c064fddfa6f8abdb42c044584a98cb4273b80cccc83b4867b701a1",
"nft_intermediate_launcher": "7a32d2d9571d3436791c0ad3d7fcfdb9c43ace2b0f0ff13f98d29f0cc093f445",
@ -31,6 +50,7 @@
"p2_1_of_n": "46b29fd87fbeb6737600c4543931222a6c1ed3db6fa5601a3ca284a9f4efe780",
"p2_announced_delegated_puzzle": "c4d24c3c5349376f3e8f3aba202972091713b4ec4915f0f26192ae4ace0bd04d",
"p2_conditions": "1c77d7d5efde60a7a1d2d27db6d746bc8e568aea1ef8586ca967a0d60b83cc36",
"p2_conditions_curryable": "a12871fee210fb8619291eaea194581cbd2531e4b23759d225f6806923f63222",
"p2_delegated_conditions": "0ff94726f1a8dea5c3f70d3121945190778d3b2b3fcda3735a1f290977e98341",
"p2_delegated_puzzle": "542cde70d1102cd1b763220990873efc8ab15625ded7eae22cc11e21ef2e2f7c",
"p2_delegated_puzzle_or_hidden_puzzle": "e9aaa49f45bad5c889b86ee3341550c155cfdd10c3a6757de618d20612fffd52",
@ -39,6 +59,7 @@
"p2_puzzle_hash": "13e29a62b42cd2ef72a79e4bacdc59733ca6310d65af83d349360d36ec622363",
"p2_singleton": "40f828d8dd55603f4ff9fbf6b73271e904e69406982f4fbefae2c8dcceaf9834",
"p2_singleton_or_delayed_puzhash": "adb656e0211e2ab4f42069a4c5efc80dc907e7062be08bf1628c8e5b6d94d25b",
"p2_singleton_via_delegated_puzzle": "231334e31ed4e60867abb640eb904931a4ce248e68091442e3b0b47a2af77d06",
"pool_member_innerpuz": "a8490702e333ddd831a3ac9c22d0fa26d2bfeaf2d33608deb22f0e0123eb0494",
"pool_waitingroom_innerpuz": "a317541a765bf8375e1c6e7c13503d0d2cbf56cacad5182befe947e78e2c0307",
"rom_bootstrap_generator": "161bade1f822dcd62ab712ebaf30f3922a301e48a639e4295c5685f8bece7bd9",

View File

@ -0,0 +1,43 @@
; 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
singleton_inner_puzhash
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)
)
)
)
)

View File

@ -0,0 +1 @@
ff02ffff03ff5fffff01ff0880ffff01ff02ffff03ffff09ff5bff0280ff80ffff01ff02ffff03ffff09ff5bffff0bff82057fff05ff820b7f8080ff80ffff01ff088080ff018080ff018080ff0180

View File

@ -0,0 +1 @@
140c74d3e8c2b66cae5dca30d03cd532df12f71e9fc17f565e5a973930d11b1f

View File

@ -0,0 +1,3 @@
(mod (CONDITIONS_LIST)
CONDITIONS_LIST
)

View File

@ -0,0 +1 @@
02

View File

@ -0,0 +1,60 @@
;; 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 ; (parent_id puzhash amount)
)
(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 (sha256tree (list 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)
)
)
)
(defun-inline give_self_to_merge ()
(list
(list CREATE_COIN_ANNOUNCEMENT 0)
)
)
(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)
(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)
)
)
)

View File

@ -0,0 +1 @@
ff02ffff01ff04ffff04ffff0148ffff04ff0bffff01808080ffff02ffff03ff2fffff01ff02ffff01ff04ffff04ffff0146ffff04ff05ffff01808080ffff02ff0effff04ff02ffff04ff0bffff04ff2fffff04ff17ff80808080808080ff0180ffff01ff02ffff01ff04ffff04ffff013cffff04ffff0180ffff01808080ffff018080ff018080ff018080ffff04ffff01ffffff02ffff03ffff22ffff09ffff0dff0580ffff012080ffff09ffff0dff0b80ffff012080ffff15ff17ffff0181ff8080ffff01ff02ffff01ff0bff05ff0bff1780ff0180ffff01ff02ffff01ff0880ff018080ff0180ff02ffff03ffff07ff0580ffff01ff02ffff01ff0bffff0102ffff02ff0cffff04ff02ffff04ffff05ff0580ff80808080ffff02ff0cffff04ff02ffff04ffff06ff0580ff8080808080ff0180ffff01ff02ffff01ff0bffff0101ff0580ff018080ff0180ffff04ffff04ffff013dffff04ffff02ff0cffff04ff02ffff04ffff04ff05ffff01ff808080ff80808080ff808080ff0b80ff02ffff03ff0bffff01ff02ffff01ff02ff0affff04ff02ffff04ffff02ff08ffff04ff02ffff04ff23ffff04ff53ffff04ff8200b3ff808080808080ffff04ffff02ff0effff04ff02ffff04ff05ffff04ff1bffff04ffff10ff17ff8200b380ff808080808080ff8080808080ff0180ffff01ff02ffff01ff04ffff04ffff0152ffff04ffff0105ffff01808080ffff04ffff04ffff0133ffff04ff05ffff04ff17ffff0180808080ffff01808080ff018080ff0180ff018080

View 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)
)
)
)
)
)

View File

@ -0,0 +1 @@
ff02ffff01ff02ffff03ff17ffff01ff02ff0bff1780ffff01ff04ffff04ff18ffff04ffff0bffff02ff2effff04ff02ffff04ff09ffff04ff2fffff04ffff02ff3effff04ff02ffff04ff05ff80808080ff808080808080ffff02ff3effff04ff02ffff04ffff04ff82017fffff04ffff02ff3effff04ff02ffff04ff5fff80808080ff808080ff8080808080ff808080ffff04ffff04ff2cffff01ff248080ffff04ffff04ff10ffff04ff82017fff808080ffff02ff5fff81bf8080808080ff0180ffff04ffff01ffffff463fff02ff3c04ffff01ff0102ffff02ffff03ff05ffff01ff02ff16ffff04ff02ffff04ff0dffff04ffff0bff3affff0bff12ff3c80ffff0bff3affff0bff3affff0bff12ff2a80ff0980ffff0bff3aff0bffff0bff12ff8080808080ff8080808080ffff010b80ff0180ffff0bff3affff0bff12ff1480ffff0bff3affff0bff3affff0bff12ff2a80ff0580ffff0bff3affff02ff16ffff04ff02ffff04ff07ffff04ffff0bff12ff1280ff8080808080ffff0bff12ff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080

View File

@ -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
@ -24,6 +27,7 @@ GENESIS_BY_ID_MOD = load_clvm_maybe_recompile("genesis_by_coin_id.clsp")
GENESIS_BY_PUZHASH_MOD = load_clvm_maybe_recompile("genesis_by_puzzle_hash.clsp")
EVERYTHING_WITH_SIG_MOD = load_clvm_maybe_recompile("everything_with_signature.clsp")
DELEGATED_LIMITATIONS_MOD = load_clvm_maybe_recompile("delegated_tail.clsp")
GENESIS_BY_ID_OR_SINGLETON_MOD = load_clvm_maybe_recompile("genesis_by_coin_id_or_singleton.clsp")
class LimitationsProgram:
@ -191,6 +195,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]]:
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:
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
) -> Tuple[TransactionRecord, SpendBundle]:
if "coins" in tail_info:
coins: List[Coin] = tail_info["coins"]
origin_id = coins.copy().pop().name()
else:
coins = await wallet.standard_wallet.select_coins(amount)
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_record: TransactionRecord = await wallet.standard_wallet.generate_signed_transaction(
amount, minted_cat_puzzle_hash, uint64(0), origin_id=origin_id, coins=set(coins)
)
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] = {
@ -198,6 +289,7 @@ ALL_LIMITATIONS_PROGRAMS: Dict[str, Any] = {
"genesis_by_puzhash": GenesisByPuzhash,
"everything_with_signature": EverythingWithSig,
"delegated_limitations": DelegatedLimitations,
"genesis_by_id_or_singleton": GenesisByIdOrSingleton,
}

View File

@ -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
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,11 @@ 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

View 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:
return self.coin.name()

View File

@ -259,6 +259,12 @@ class TradeManager:
ignore_max_send_amount=True,
)
all_txs.append(tx)
elif wallet.type() == WalletType.CAT:
# ATTENTION: new_wallets
txs = await wallet.generate_signed_transactions(
[coin.amount], [new_ph], fee=fee_to_pay, coins={coin}, ignore_max_send_amount=True
)
all_txs.extend(txs)
else:
# ATTENTION: new_wallets
txs = await wallet.generate_signed_transaction(
@ -340,6 +346,15 @@ class TradeManager:
if tx is not None and tx.spend_bundle is not None:
bundles.append(tx.spend_bundle)
all_txs.append(dataclasses.replace(tx, spend_bundle=None))
elif wallet.type() == WalletType.CAT:
# ATTENTION: new_wallets
txs = await wallet.generate_signed_transactions(
[coin.amount], [new_ph], fee=fee_to_pay, coins={coin}, ignore_max_send_amount=True
)
for tx in txs:
if tx is not None and tx.spend_bundle is not None:
bundles.append(tx.spend_bundle)
all_txs.append(dataclasses.replace(tx, spend_bundle=None))
else:
# ATTENTION: new_wallets
txs = await wallet.generate_signed_transaction(
@ -601,6 +616,17 @@ class TradeManager:
reuse_puzhash=reuse_puzhash,
)
all_transactions.extend(txs)
elif wallet.type() == WalletType.CAT:
# ATTENTION: new_wallets
txs = await wallet.generate_signed_transactions(
[abs(offer_dict[id])],
[OFFER_MOD_OLD_HASH if old else Offer.ph()],
fee=fee_left_to_pay,
coins=set(selected_coins),
puzzle_announcements_to_consume=announcements_to_assert,
reuse_puzhash=reuse_puzhash,
)
all_transactions.extend(txs)
else:
# ATTENTION: new_wallets
txs = await wallet.generate_signed_transaction(

View File

@ -70,7 +70,8 @@ def debug_spend_bundle(spend_bundle, agg_sig_additional_data=DEFAULT_CONSTANTS.A
continue
print(f"consuming coin {dump_coin(coin)}")
print(f" with id {coin_name.hex()}")
print(f" with id {coin_name}")
print(f" ID in hex is {coin_name.hex()}")
print()
print(f"\nbrun -y main.sym '{bu_disassemble(puzzle_reveal)}' '{bu_disassemble(solution)}'")
conditions = conditions_dict_for_solution(puzzle_reveal, solution, INFINITE_COST)

View File

@ -26,6 +26,8 @@ class WalletType(IntEnum):
DATA_LAYER = 11
DATA_LAYER_OFFER = 12
VC = 13
DAO = 14
DAO_CAT = 15
class CoinType(IntEnum):

View File

@ -0,0 +1,230 @@
from __future__ import annotations
import json
import logging
from sqlite3 import Row
from typing import List, Type, TypeVar
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
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:
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,
)
if conditions is None:
raise RuntimeError("Failed to add spend for coin: %s ", coin_state.coin.name())
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:
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:
# Remove NFT in the users_nfts table
cursor = await conn.execute(
"UPDATE singletons SET removed_height=? WHERE singleton_id=?", (int(height), singleton_id.hex())
)
return cursor.rowcount > 0
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:
# Remove NFT in the users_nfts table
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)
return False
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()

View File

@ -8,7 +8,20 @@ import traceback
from contextlib import asynccontextmanager
from pathlib import Path
from secrets import token_bytes
from typing import TYPE_CHECKING, Any, AsyncIterator, Callable, Dict, Iterator, List, Optional, Set, Type, TypeVar
from typing import (
TYPE_CHECKING,
Any,
AsyncIterator,
Callable,
Dict,
Iterator,
List,
Optional,
Set,
Type,
TypeVar,
Union,
)
import aiosqlite
from blspy import G1Element, G2Element, PrivateKey
@ -49,6 +62,16 @@ from chia.util.path import path_from_root
from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS
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.dao_wallet.dao_utils import (
get_new_puzzle_from_treasury_solution,
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 (
@ -71,7 +94,7 @@ from chia.wallet.payment import Payment
from chia.wallet.puzzle_drivers import PuzzleInfo
from chia.wallet.puzzles.clawback.drivers import generate_clawback_spend_bundle, match_clawback_puzzle
from chia.wallet.puzzles.clawback.metadata import ClawbackMetadata, ClawbackVersion
from chia.wallet.singleton import create_singleton_puzzle
from chia.wallet.singleton import create_singleton_puzzle, 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
@ -102,6 +125,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
@ -115,6 +139,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
@ -153,6 +179,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
@ -169,6 +196,7 @@ class WalletStateManager:
wallet_node: WalletNode,
) -> WalletStateManager:
self = WalletStateManager()
self.config = config
self.constants = constants
self.server = server
@ -207,6 +235,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
@ -262,7 +291,22 @@ class WalletStateManager:
wallet_info,
)
elif wallet_type == WalletType.DATA_LAYER:
wallet = await DataLayerWallet.create(self, wallet_info)
wallet = await DataLayerWallet.create(
self,
wallet_info,
)
elif wallet_type == WalletType.DAO:
wallet = await DAOWallet.create(
self,
self.main_wallet,
wallet_info,
)
elif wallet_type == WalletType.DAO_CAT:
wallet = await DAOCATWallet.create(
self,
self.main_wallet,
wallet_info,
)
elif wallet_type == WalletType.VC: # pragma: no cover
wallet = await VCWallet.create(
self,
@ -647,7 +691,6 @@ class WalletStateManager:
or self.is_farmer_reward(uint32(coin_state.created_height), coin_state.coin)
):
return None
response: List[CoinState] = await self.wallet_node.get_coin_state(
[coin_state.coin.parent_coin_info], peer=peer, fork_height=fork_height
)
@ -662,9 +705,33 @@ class WalletStateManager:
return None
puzzle = Program.from_bytes(bytes(coin_spend.puzzle_reveal))
solution = Program.from_bytes(bytes(coin_spend.solution))
uncurried = uncurry_puzzle(puzzle)
# 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)
# 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)
# 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)
# 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)
funding_puzzle_check = match_funding_puzzle(uncurried, solution)
if funding_puzzle_check:
return await self.get_dao_wallet_from_coinspend_hint(coin_spend)
# Check if the coin is a CAT
cat_curried_args = match_cat_puzzle(uncurried)
if cat_curried_args is not None:
@ -776,6 +843,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:
@ -807,9 +876,6 @@ class WalletStateManager:
memos=list(compute_memos(spend_bundle).items()),
)
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]:
@ -846,6 +912,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
async def handle_cat(
self,
curried_args: Iterator[Program],
@ -871,7 +956,6 @@ class WalletStateManager:
derivation_record = await self.puzzle_store.get_derivation_record_for_puzzle_hash(bytes32(hint))
if derivation_record is not None:
break
if derivation_record is None:
self.log.info(f"Received state for the coin that doesn't belong to us {coin_state}")
return None
@ -936,6 +1020,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):
@ -1033,6 +1118,98 @@ 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)
inner_puzzle = get_new_puzzle_from_treasury_solution(
coin_spend.puzzle_reveal.to_program(), coin_spend.solution.to_program()
)
# If we can't find the wallet for this DAO but we've got here because we're subscribed, then create the wallet
assert isinstance(inner_puzzle, Program)
assert isinstance(singleton_id, bytes32)
dao_wallet = await DAOWallet.create_new_dao_wallet_for_existing_dao(
self,
self.main_wallet,
singleton_id,
)
assert dao_wallet is not None
return None
async def handle_dao_proposal(
self,
uncurried_args: Iterator[Program],
parent_coin_state: CoinState,
coin_state: CoinState,
coin_spend: CoinSpend,
) -> Optional[WalletIdentifier]:
(
SINGLETON_STRUCT, # (SINGLETON_MOD_HASH, (SINGLETON_ID, LAUNCHER_PUZZLE_HASH))
PROPOSAL_MOD_HASH,
PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_MOD_HASH,
TREASURY_MOD_HASH,
LOCKUP_MOD_HASH,
CAT_TAIL_HASH,
TREASURY_ID,
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
INNERPUZHASH,
) = 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
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:
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) -> Optional[WalletIdentifier]:
hint_list = compute_coin_hints(coin_spend)
if hint_list:
for wallet in self.wallets.values():
if wallet.type() == WalletType.DAO.value:
assert isinstance(wallet, DAOWallet)
if wallet.dao_info.treasury_id in hint_list:
return WalletIdentifier.create(wallet)
return None
async def handle_nft(
self, coin_spend: CoinSpend, uncurried_nft: UncurriedNFT, parent_coin_state: CoinState, coin_state: CoinState
) -> Optional[WalletIdentifier]:
@ -1455,6 +1632,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_TRANSACTION_TYPES:
for add_coin in tx_record.additions:
@ -1475,14 +1653,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:
@ -1744,10 +1925,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)
if wallet_type == WalletType.DAO:
return
await self.create_more_puzzle_hashes()
async def add_pending_transaction(self, tx_record: TransactionRecord) -> None:
@ -1971,22 +2156,43 @@ class WalletStateManager:
return filtered
async def new_peak(self, peak: NewPeakWallet) -> None:
valid_list = [WalletType.POOLING_WALLET, WalletType.DAO]
for wallet_id, wallet in self.wallets.items():
if wallet.type() == WalletType.POOLING_WALLET:
assert isinstance(wallet, PoolWallet)
await wallet.new_peak(uint64(peak.height))
if wallet.type() in valid_list:
valid = isinstance(wallet, PoolWallet) or isinstance(wallet, DAOWallet)
assert valid
await wallet.new_peak(uint64(peak.height)) # type: ignore[attr-defined]
current_time = int(time.time())
if self.wallet_node.last_wallet_tx_resend_time < current_time - self.wallet_node.wallet_tx_resend_timeout_secs:
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:

View File

@ -2,7 +2,7 @@
; logging options
log_cli = False
addopts = --verbose --tb=short -n auto -p no:monitor
log_level = WARNING
log_level = INFO
console_output_style = count
log_format = %(asctime)s %(name)s: %(levelname)s %(message)s
asyncio_mode = strict

View File

@ -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

View File

@ -208,7 +208,7 @@ class TestCATWallet:
assert cat_wallet.cat_info.limitations_program_hash == cat_wallet_2.cat_info.limitations_program_hash
cat_2_hash = await cat_wallet_2.get_new_inner_hash()
tx_records = await cat_wallet.generate_signed_transaction([uint64(60)], [cat_2_hash], fee=uint64(1))
tx_records = await cat_wallet.generate_signed_transactions([uint64(60)], [cat_2_hash], fee=uint64(1))
tx_id = None
for tx_record in tx_records:
await wallet.wallet_state_manager.add_pending_transaction(tx_record)
@ -241,7 +241,7 @@ class TestCATWallet:
assert len(memos[tx_id]) == 2
assert list(memos[tx_id].values())[0][0] == cat_2_hash.hex()
cat_hash = await cat_wallet.get_new_inner_hash()
tx_records = await cat_wallet_2.generate_signed_transaction([uint64(15)], [cat_hash])
tx_records = await cat_wallet_2.generate_signed_transactions([uint64(15)], [cat_hash])
for tx_record in tx_records:
await wallet.wallet_state_manager.add_pending_transaction(tx_record)
@ -315,7 +315,7 @@ class TestCATWallet:
assert cat_wallet.cat_info.limitations_program_hash == cat_wallet_2.cat_info.limitations_program_hash
cat_2_hash = await cat_wallet_2.get_new_inner_hash()
tx_records = await cat_wallet.generate_signed_transaction(
tx_records = await cat_wallet.generate_signed_transactions(
[uint64(60)], [cat_2_hash], fee=uint64(1), reuse_puzhash=True
)
for tx_record in tx_records:
@ -345,7 +345,7 @@ class TestCATWallet:
await time_out_assert(30, cat_wallet_2.get_unconfirmed_balance, 60)
cat_hash = await cat_wallet.get_new_inner_hash()
tx_records = await cat_wallet_2.generate_signed_transaction([uint64(15)], [cat_hash])
tx_records = await cat_wallet_2.generate_signed_transactions([uint64(15)], [cat_hash])
for tx_record in tx_records:
await wallet.wallet_state_manager.add_pending_transaction(tx_record)
@ -471,7 +471,7 @@ class TestCATWallet:
assert cat_wallet.cat_info.limitations_program_hash == cat_wallet_2.cat_info.limitations_program_hash
cat_2_hash = await cat_wallet_2.get_new_inner_hash()
tx_records = await cat_wallet.generate_signed_transaction([uint64(60)], [cat_2_hash], fee=uint64(1))
tx_records = await cat_wallet.generate_signed_transactions([uint64(60)], [cat_2_hash], fee=uint64(1))
for tx_record in tx_records:
await wallet.wallet_state_manager.add_pending_transaction(tx_record)
await full_node_api.process_transaction_records(records=tx_records)
@ -568,7 +568,7 @@ class TestCATWallet:
cat_1_hash = await cat_wallet_1.get_new_inner_hash()
cat_2_hash = await cat_wallet_2.get_new_inner_hash()
tx_records = await cat_wallet_0.generate_signed_transaction([uint64(60), uint64(20)], [cat_1_hash, cat_2_hash])
tx_records = await cat_wallet_0.generate_signed_transactions([uint64(60), uint64(20)], [cat_1_hash, cat_2_hash])
for tx_record in tx_records:
await wallet_0.wallet_state_manager.add_pending_transaction(tx_record)
await full_node_api.process_transaction_records(records=tx_records)
@ -584,11 +584,11 @@ class TestCATWallet:
cat_hash = await cat_wallet_0.get_new_inner_hash()
tx_records = await cat_wallet_1.generate_signed_transaction([uint64(15)], [cat_hash])
tx_records = await cat_wallet_1.generate_signed_transactions([uint64(15)], [cat_hash])
for tx_record in tx_records:
await wallet_1.wallet_state_manager.add_pending_transaction(tx_record)
tx_records_2 = await cat_wallet_2.generate_signed_transaction([uint64(20)], [cat_hash])
tx_records_2 = await cat_wallet_2.generate_signed_transactions([uint64(20)], [cat_hash])
for tx_record in tx_records_2:
await wallet_2.wallet_state_manager.add_pending_transaction(tx_record)
@ -606,11 +606,11 @@ class TestCATWallet:
txs = await wallet_1.wallet_state_manager.tx_store.get_transactions_between(cat_wallet_1.id(), 0, 100000)
print(len(txs))
# Test with Memo
tx_records_3: TransactionRecord = await cat_wallet_1.generate_signed_transaction(
tx_records_3: TransactionRecord = await cat_wallet_1.generate_signed_transactions(
[uint64(30)], [cat_hash], memos=[[b"Markus Walburg"]]
)
with pytest.raises(ValueError):
await cat_wallet_1.generate_signed_transaction(
await cat_wallet_1.generate_signed_transactions(
[uint64(30)], [cat_hash], memos=[[b"too"], [b"many"], [b"memos"]]
)
@ -682,7 +682,7 @@ class TestCATWallet:
amounts.append(uint64(i))
puzzle_hashes.append(cat_2_hash)
spent_coint = (await cat_wallet.get_cat_spendable_coins())[0].coin
tx_records = await cat_wallet.generate_signed_transaction(amounts, puzzle_hashes, coins={spent_coint})
tx_records = await cat_wallet.generate_signed_transactions(amounts, puzzle_hashes, coins={spent_coint})
for tx_record in tx_records:
await wallet.wallet_state_manager.add_pending_transaction(tx_record)
await full_node_api.process_transaction_records(records=tx_records)
@ -708,7 +708,7 @@ class TestCATWallet:
max_sent_amount = await cat_wallet.get_max_send_amount()
# 1) Generate transaction that is under the limit
[transaction_record] = await cat_wallet.generate_signed_transaction(
[transaction_record] = await cat_wallet.generate_signed_transactions(
[max_sent_amount - 1],
[ph],
)
@ -716,7 +716,7 @@ class TestCATWallet:
assert transaction_record.amount == uint64(max_sent_amount - 1)
# 2) Generate transaction that is equal to limit
[transaction_record] = await cat_wallet.generate_signed_transaction(
[transaction_record] = await cat_wallet.generate_signed_transactions(
[max_sent_amount],
[ph],
)
@ -725,7 +725,7 @@ class TestCATWallet:
# 3) Generate transaction that is greater than limit
with pytest.raises(ValueError):
await cat_wallet.generate_signed_transaction(
await cat_wallet.generate_signed_transactions(
[max_sent_amount + 1],
[ph],
)
@ -786,7 +786,7 @@ class TestCATWallet:
assert cat_wallet.cat_info.limitations_program_hash is not None
cat_2_hash = await wallet2.get_new_puzzlehash()
tx_records = await cat_wallet.generate_signed_transaction([uint64(60)], [cat_2_hash], memos=[[cat_2_hash]])
tx_records = await cat_wallet.generate_signed_transactions([uint64(60)], [cat_2_hash], memos=[[cat_2_hash]])
for tx_record in tx_records:
await wallet.wallet_state_manager.add_pending_transaction(tx_record)
@ -816,7 +816,7 @@ class TestCATWallet:
}
# Then we send another transaction
tx_records = await cat_wallet.generate_signed_transaction([uint64(10)], [cat_2_hash], memos=[[cat_2_hash]])
tx_records = await cat_wallet.generate_signed_transactions([uint64(10)], [cat_2_hash], memos=[[cat_2_hash]])
for tx_record in tx_records:
await wallet.wallet_state_manager.add_pending_transaction(tx_record)
@ -835,7 +835,7 @@ class TestCATWallet:
await time_out_assert(30, cat_wallet_2.get_unconfirmed_balance, 70)
cat_hash = await cat_wallet.get_new_inner_hash()
tx_records = await cat_wallet_2.generate_signed_transaction([uint64(5)], [cat_hash])
tx_records = await cat_wallet_2.generate_signed_transactions([uint64(5)], [cat_hash])
for tx_record in tx_records:
await wallet.wallet_state_manager.add_pending_transaction(tx_record)

View File

@ -0,0 +1,991 @@
from __future__ import annotations
import pytest
# mypy: ignore-errors
from clvm.casts import int_to_bytes
from chia.types.blockchain_format.coin import Coin
# from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import INFINITE_COST, Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.condition_opcodes import ConditionOpcode
from chia.util.condition_tools import conditions_dict_for_solution
from chia.util.hash import std_hash
from chia.util.ints import uint64
from chia.wallet.cat_wallet.cat_utils import CAT_MOD
from chia.wallet.puzzles.load_clvm import load_clvm
CAT_MOD_HASH: bytes32 = CAT_MOD.get_tree_hash()
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_RESALE_PREVENTION: Program = load_clvm("dao_resale_prevention_layer.clsp")
DAO_RESALE_PREVENTION_HASH: bytes32 = DAO_RESALE_PREVENTION.get_tree_hash()
DAO_CAT_TAIL: Program = load_clvm("genesis_by_coin_id_or_singleton.clsp")
DAO_CAT_TAIL_HASH: bytes32 = DAO_CAT_TAIL.get_tree_hash()
P2_CONDITIONS_MOD: Program = load_clvm("p2_conditions_curryable.clsp")
P2_CONDITIONS_MOD_HASH: bytes32 = P2_CONDITIONS_MOD.get_tree_hash()
DAO_SAFE_PAYMENT_MOD: Program = load_clvm("dao_safe_payment.clsp")
DAO_SAFE_PAYMENT_MOD_HASH: bytes32 = DAO_SAFE_PAYMENT_MOD.get_tree_hash()
P2_SINGLETON_MOD: Program = load_clvm("p2_singleton_via_delegated_puzzle.clsp")
P2_SINGLETON_MOD_HASH: bytes32 = P2_SINGLETON_MOD.get_tree_hash()
P2_SINGLETON_AGGREGATOR_MOD: Program = load_clvm("p2_singleton_aggregator.clsp")
P2_SINGLETON_AGGREGATOR_MOD_HASH: bytes32 = P2_SINGLETON_AGGREGATOR_MOD.get_tree_hash()
DAO_UPDATE_MOD: Program = load_clvm("dao_update_proposal.clsp")
DAO_UPDATE_MOD_HASH: bytes32 = DAO_UPDATE_MOD.get_tree_hash()
def test_finished_state() -> None:
proposal_id: Program = Program.to("proposal_id").get_tree_hash()
singleton_struct: Program = Program.to(
(SINGLETON_MOD.get_tree_hash(), (proposal_id, SINGLETON_LAUNCHER.get_tree_hash()))
)
finished_inner_puz = DAO_FINISHED_STATE.curry(singleton_struct, DAO_FINISHED_STATE_HASH)
finished_full_puz = SINGLETON_MOD.curry(singleton_struct, finished_inner_puz)
inner_sol = Program.to([1])
conds = finished_inner_puz.run(inner_sol).as_python()
assert conds[0][1] == finished_full_puz.get_tree_hash()
assert conds[2][1] == finished_inner_puz.get_tree_hash()
lineage = Program.to([proposal_id, finished_inner_puz.get_tree_hash(), 1])
full_sol = Program.to([lineage, 1, inner_sol])
conds = conditions_dict_for_solution(finished_full_puz, full_sol, INFINITE_COST)
assert conds[ConditionOpcode.ASSERT_MY_PUZZLEHASH][0].vars[0] == finished_full_puz.get_tree_hash()
assert conds[ConditionOpcode.CREATE_COIN][0].vars[0] == finished_full_puz.get_tree_hash()
def test_proposal() -> None:
proposal_pass_percentage: uint64 = uint64(5100)
CAT_TAIL_HASH: Program = Program.to("tail").get_tree_hash()
treasury_id: Program = Program.to("treasury").get_tree_hash()
singleton_id: Program = Program.to("singleton_id").get_tree_hash()
singleton_struct: Program = Program.to(
(SINGLETON_MOD.get_tree_hash(), (singleton_id, SINGLETON_LAUNCHER.get_tree_hash()))
)
self_destruct_time = 1000 # number of blocks
oracle_spend_delay = 10
active_votes_list = [0xFADEDDAB] # are the the ids of previously voted on proposals?
acs: Program = Program.to(1)
acs_ph: bytes32 = acs.get_tree_hash()
# make a lockup puz for the dao cat
lockup_puz = DAO_LOCKUP_MOD.curry(
DAO_PROPOSAL_MOD_HASH,
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
DAO_LOCKUP_MOD_HASH,
DAO_FINISHED_STATE_HASH,
CAT_MOD_HASH,
CAT_TAIL_HASH,
active_votes_list,
acs, # innerpuz
)
dao_cat_puz: Program = CAT_MOD.curry(CAT_MOD_HASH, CAT_TAIL_HASH, lockup_puz)
dao_cat_puzhash: bytes32 = dao_cat_puz.get_tree_hash()
# Test Voting
current_yes_votes = 20
current_total_votes = 100
full_proposal: Program = DAO_PROPOSAL_MOD.curry(
singleton_struct,
DAO_PROPOSAL_MOD_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
DAO_LOCKUP_MOD_HASH,
CAT_TAIL_HASH,
treasury_id,
current_yes_votes,
current_total_votes,
acs_ph,
)
vote_amount = 10
vote_type = 1 # yes vote
vote_coin_id = Program.to("vote_coin").get_tree_hash()
solution: Program = Program.to(
[
[vote_amount], # vote amounts
vote_type, # vote type (yes)
[vote_coin_id], # vote coin ids
[active_votes_list], # previous votes
[acs_ph], # lockup inner puz hash
0, # inner puz reveal
0, # soft close len
self_destruct_time,
oracle_spend_delay,
0,
1,
]
)
# Run the proposal and check its conditions
conditions = conditions_dict_for_solution(full_proposal, solution, INFINITE_COST)
# Puzzle Announcement of vote_coin_ids
assert conditions[ConditionOpcode.CREATE_PUZZLE_ANNOUNCEMENT][0].vars[0] == vote_coin_id
# Assert puzzle announcement from dao_cat of proposal_id and all vote details
apa_msg = Program.to([singleton_id, vote_amount, vote_type, vote_coin_id]).get_tree_hash()
assert conditions[ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT][0].vars[0] == std_hash(dao_cat_puzhash + apa_msg)
# Check that the proposal recreates itself with updated vote amounts
next_proposal: Program = DAO_PROPOSAL_MOD.curry(
singleton_struct,
DAO_PROPOSAL_MOD_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
DAO_LOCKUP_MOD_HASH,
CAT_TAIL_HASH,
treasury_id,
current_yes_votes + vote_amount,
current_total_votes + vote_amount,
acs_ph,
)
assert conditions[ConditionOpcode.CREATE_COIN][0].vars[0] == next_proposal.get_tree_hash()
assert conditions[ConditionOpcode.CREATE_COIN][0].vars[1] == int_to_bytes(1)
# Test Launch
current_yes_votes = 0
current_total_votes = 0
launch_proposal: Program = DAO_PROPOSAL_MOD.curry(
singleton_struct,
DAO_PROPOSAL_MOD_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
DAO_LOCKUP_MOD_HASH,
CAT_TAIL_HASH,
treasury_id,
current_yes_votes,
current_total_votes,
acs_ph,
)
vote_amount = 10
vote_type = 1 # yes vote
vote_coin_id = Program.to("vote_coin").get_tree_hash()
solution: Program = Program.to(
[
[vote_amount], # vote amounts
vote_type, # vote type (yes)
[vote_coin_id], # vote coin ids
# TODO: Check whether previous votes should be 0 in the first spend since
# proposal looks at (f previous_votes) during loop_over_vote_coins
[0], # previous votes
[acs_ph], # lockup inner puz hash
acs, # inner puz reveal
0, # soft close len
self_destruct_time,
oracle_spend_delay,
0,
1,
]
)
# Run the proposal and check its conditions
conditions = conditions_dict_for_solution(launch_proposal, solution, INFINITE_COST)
# check that the timer is created
timer_puz = DAO_PROPOSAL_TIMER_MOD.curry(
DAO_PROPOSAL_MOD_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
DAO_LOCKUP_MOD_HASH,
DAO_FINISHED_STATE_HASH,
CAT_MOD_HASH,
CAT_TAIL_HASH,
singleton_struct,
treasury_id,
)
timer_puzhash = timer_puz.get_tree_hash()
assert conditions[ConditionOpcode.CREATE_COIN][1].vars[0] == timer_puzhash
# Test exits
# Test attempt to close a passing proposal
current_yes_votes = 200
current_total_votes = 350
full_proposal: Program = DAO_PROPOSAL_MOD.curry(
singleton_struct,
DAO_PROPOSAL_MOD_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
DAO_LOCKUP_MOD_HASH,
CAT_TAIL_HASH,
treasury_id,
current_yes_votes,
current_total_votes,
acs_ph,
)
attendance_required = 200
proposal_timelock = 20
soft_close_length = 5
solution = Program.to(
[
Program.to("validator_hash").get_tree_hash(),
0,
# Program.to("receiver_hash").get_tree_hash(), # not needed anymore?
proposal_timelock,
proposal_pass_percentage,
attendance_required,
0,
soft_close_length,
self_destruct_time,
oracle_spend_delay,
0,
1,
]
)
conds = conditions_dict_for_solution(full_proposal, solution, INFINITE_COST)
# make a matching treasury puzzle for the APA
treasury_inner: Program = DAO_TREASURY_MOD.curry(
DAO_TREASURY_MOD_HASH,
Program.to("validator_hash"),
proposal_timelock,
soft_close_length,
attendance_required,
proposal_pass_percentage,
self_destruct_time,
oracle_spend_delay,
)
treasury: Program = SINGLETON_MOD.curry(
Program.to((SINGLETON_MOD_HASH, (treasury_id, SINGLETON_LAUNCHER_HASH))),
treasury_inner,
)
treasury_puzhash = treasury.get_tree_hash()
apa_msg = singleton_id
timer_apa = std_hash(timer_puzhash + singleton_id)
assert conds[ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT][0].vars[0] == timer_apa
assert conds[ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT][1].vars[0] == std_hash(treasury_puzhash + apa_msg)
# close a failed proposal
full_proposal: Program = DAO_PROPOSAL_MOD.curry(
singleton_struct,
DAO_PROPOSAL_MOD_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
DAO_LOCKUP_MOD_HASH,
CAT_TAIL_HASH,
treasury_id,
20, # failing number of yes votes
current_total_votes,
acs_ph,
)
solution = Program.to(
[
Program.to("validator_hash").get_tree_hash(),
0,
# Program.to("receiver_hash").get_tree_hash(), # not needed anymore?
proposal_timelock,
proposal_pass_percentage,
attendance_required,
0,
soft_close_length,
self_destruct_time,
oracle_spend_delay,
0,
1,
]
)
conds = conditions_dict_for_solution(full_proposal, solution, INFINITE_COST)
apa_msg = int_to_bytes(0)
assert conds[ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT][1].vars[0] == std_hash(treasury_puzhash + apa_msg)
assert conds[ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT][0].vars[0] == timer_apa
finished_puz = DAO_FINISHED_STATE.curry(singleton_struct, DAO_FINISHED_STATE_HASH)
assert conds[ConditionOpcode.CREATE_COIN][0].vars[0] == finished_puz.get_tree_hash()
# self destruct a proposal
attendance_required = 200
solution = Program.to(
[
Program.to("validator_hash").get_tree_hash(),
0,
# Program.to("receiver_hash").get_tree_hash(), # not needed anymore?
proposal_timelock,
proposal_pass_percentage,
attendance_required,
0,
soft_close_length,
self_destruct_time,
oracle_spend_delay,
1,
1,
]
)
conds = conditions_dict_for_solution(full_proposal, solution, INFINITE_COST)
assert conds[ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT][0].vars[0] == std_hash(treasury_puzhash + apa_msg)
assert conds[ConditionOpcode.CREATE_COIN][0].vars[0] == finished_puz.get_tree_hash()
def test_proposal_timer() -> None:
CAT_TAIL: Program = Program.to("tail").get_tree_hash()
treasury_id: Program = Program.to("treasury").get_tree_hash()
singleton_id: Program = Program.to("singleton_id").get_tree_hash()
singleton_struct: Program = Program.to(
(SINGLETON_MOD.get_tree_hash(), (singleton_id, SINGLETON_LAUNCHER.get_tree_hash()))
)
proposal_timer_full: Program = DAO_PROPOSAL_TIMER_MOD.curry(
DAO_PROPOSAL_MOD_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
DAO_LOCKUP_MOD_HASH,
DAO_FINISHED_STATE_HASH,
CAT_MOD_HASH,
CAT_TAIL,
singleton_struct,
treasury_id,
)
solution: Program = Program.to(
[
140,
180,
Program.to(1).get_tree_hash(),
Program.to("parent").get_tree_hash(),
23,
200,
Program.to("parent_parent").get_tree_hash(),
]
)
conds: Program = proposal_timer_full.run(solution)
assert len(conds.as_python()) == 4
def test_validator() -> None:
# This test covers proposal_validator and spend_p2_singleton
# Setup the treasury
treasury_id: Program = Program.to("treasury_id").get_tree_hash()
treasury_struct: Program = Program.to((SINGLETON_MOD_HASH, (treasury_id, SINGLETON_LAUNCHER_HASH)))
# Setup the proposal
proposal_id: Program = Program.to("proposal_id").get_tree_hash()
proposal_struct: Program = Program.to((SINGLETON_MOD.get_tree_hash(), (proposal_id, SINGLETON_LAUNCHER_HASH)))
CAT_TAIL_HASH: Program = Program.to("tail").get_tree_hash()
acs: Program = Program.to(1)
acs_ph: bytes32 = acs.get_tree_hash()
p2_singleton = P2_SINGLETON_MOD.curry(treasury_struct, P2_SINGLETON_AGGREGATOR_MOD)
p2_singleton_puzhash = p2_singleton.get_tree_hash()
parent_id = Program.to("parent").get_tree_hash()
locked_amount = 100000
spend_amount = 1100
conditions = [[51, 0xDABBAD00, 1000], [51, 0xCAFEF00D, 100]]
# Setup the validator
minimum_amt = 1
excess_puzhash = bytes32(b"1" * 32)
proposal_validator = DAO_PROPOSAL_VALIDATOR_MOD.curry(
treasury_struct,
DAO_PROPOSAL_MOD_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_LOCKUP_MOD_HASH,
DAO_TREASURY_MOD_HASH,
CAT_TAIL_HASH,
minimum_amt,
excess_puzhash,
)
# Can now create the treasury inner puz
treasury_inner = DAO_TREASURY_MOD.curry(
DAO_TREASURY_MOD_HASH,
proposal_validator,
10, # proposal len
5, # soft close
1000, # attendance
5100, # pass margin
20, # self_destruct len
3, # oracle delay
)
# Setup the spend_p2_singleton (proposal inner puz)
spend_p2_singleton = SPEND_P2_SINGLETON_MOD.curry(
treasury_struct, CAT_MOD_HASH, conditions, [], p2_singleton_puzhash # tailhash conds
)
spend_p2_singleton_puzhash = spend_p2_singleton.get_tree_hash()
parent_amt_list = [[parent_id, locked_amount]]
cat_parent_amt_list = []
spend_p2_singleton_solution = Program.to([parent_amt_list, cat_parent_amt_list, treasury_inner.get_tree_hash()])
output_conds = spend_p2_singleton.run(spend_p2_singleton_solution)
proposal: Program = DAO_PROPOSAL_MOD.curry(
proposal_struct,
DAO_PROPOSAL_MOD_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
DAO_LOCKUP_MOD_HASH,
CAT_TAIL_HASH,
treasury_id,
950,
1200,
spend_p2_singleton_puzhash,
)
full_proposal = SINGLETON_MOD.curry(proposal_struct, proposal)
proposal_amt = 10
proposal_coin_id = Coin(parent_id, full_proposal.get_tree_hash(), proposal_amt).name()
solution = Program.to(
[
1000,
5100,
[proposal_coin_id, spend_p2_singleton_puzhash, 0],
[proposal_id, 1200, 950, parent_id, proposal_amt],
output_conds,
]
)
conds: Program = proposal_validator.run(solution)
assert len(conds.as_python()) == 7 + len(conditions)
# test update
proposal: Program = DAO_PROPOSAL_MOD.curry(
proposal_struct,
DAO_PROPOSAL_MOD_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
DAO_LOCKUP_MOD_HASH,
CAT_TAIL_HASH,
treasury_id,
950,
1200,
acs_ph,
)
full_proposal = SINGLETON_MOD.curry(proposal_struct, proposal)
proposal_coin_id = Coin(parent_id, full_proposal.get_tree_hash(), proposal_amt).name()
solution = Program.to(
[
1000,
5100,
[proposal_coin_id, acs_ph, 0],
[proposal_id, 1200, 950, parent_id, proposal_amt],
[[51, 0xCAFE00D, spend_amount]],
]
)
conds: Program = proposal_validator.run(solution)
assert len(conds.as_python()) == 3
return
def test_merge_p2_singleton() -> None:
# Setup a singleton struct
singleton_inner: Program = Program.to(1)
singleton_id: Program = Program.to("singleton_id").get_tree_hash()
singleton_struct: Program = Program.to((SINGLETON_MOD_HASH, (singleton_id, SINGLETON_LAUNCHER_HASH)))
# Setup p2_singleton_via_delegated puz
my_id = Program.to("my_id").get_tree_hash()
p2_singleton = P2_SINGLETON_MOD.curry(singleton_struct, P2_SINGLETON_AGGREGATOR_MOD)
my_puzhash = p2_singleton.get_tree_hash()
# Spend to delegated puz
delegated_puz = Program.to(1)
delegated_sol = Program.to([[51, 0xCAFEF00D, 300]])
solution = Program.to([0, singleton_inner.get_tree_hash(), delegated_puz, delegated_sol, my_id])
conds = conditions_dict_for_solution(p2_singleton, solution, INFINITE_COST)
apa = std_hash(
SINGLETON_MOD.curry(singleton_struct, singleton_inner).get_tree_hash()
+ Program.to([my_id, delegated_puz.get_tree_hash()]).get_tree_hash()
)
assert len(conds) == 4
assert conds[ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT][0].vars[0] == apa
assert conds[ConditionOpcode.CREATE_COIN][0].vars[1] == int_to_bytes(300)
# Merge Spend (not output creator)
merge_sol = Program.to([[my_id, my_puzhash, 300, 0]])
conds = conditions_dict_for_solution(p2_singleton, merge_sol, INFINITE_COST)
assert len(conds) == 2
assert conds[ConditionOpcode.ASSERT_MY_PUZZLEHASH][0].vars[0] == my_puzhash
assert conds[ConditionOpcode.CREATE_COIN_ANNOUNCEMENT][0].vars[0] == int_to_bytes(0)
# Merge Spend (output creator)
fake_parent_id = Program.to("fake_parent").get_tree_hash()
merged_coin_id = Coin(fake_parent_id, my_puzhash, 200).name()
merge_sol = Program.to([[my_id, my_puzhash, 100, [[fake_parent_id, my_puzhash, 200]]]])
conds = conditions_dict_for_solution(p2_singleton, merge_sol, INFINITE_COST)
assert len(conds) == 5
assert conds[ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT][0].vars[0] == Program.to([merged_coin_id, 0]).get_tree_hash()
assert conds[ConditionOpcode.CREATE_COIN][0].vars[1] == int_to_bytes(300)
return
def test_treasury() -> None:
# Setup the treasury
treasury_id: Program = Program.to("treasury_id").get_tree_hash()
treasury_struct: Program = Program.to((SINGLETON_MOD_HASH, (treasury_id, SINGLETON_LAUNCHER_HASH)))
CAT_TAIL_HASH: Program = Program.to("tail").get_tree_hash()
proposal_id: Program = Program.to("singleton_id").get_tree_hash()
proposal_struct: Program = Program.to((SINGLETON_MOD_HASH, (proposal_id, SINGLETON_LAUNCHER_HASH)))
p2_singleton = P2_SINGLETON_MOD.curry(treasury_struct, P2_SINGLETON_AGGREGATOR_MOD)
p2_singleton_puzhash = p2_singleton.get_tree_hash()
parent_id = Program.to("parent").get_tree_hash()
locked_amount = 100000
oracle_spend_delay = 10
self_destruct_time = 1000
proposal_length = 40
soft_close_length = 5
attendance = 1000
pass_margin = 5100
conditions = [[51, 0xDABBAD00, 1000], [51, 0xCAFEF00D, 100]]
# Setup the validator
minimum_amt = 1
excess_puzhash = bytes32(b"1" * 32)
proposal_validator = DAO_PROPOSAL_VALIDATOR_MOD.curry(
treasury_struct,
DAO_PROPOSAL_MOD_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_LOCKUP_MOD_HASH,
DAO_TREASURY_MOD_HASH,
CAT_TAIL_HASH,
minimum_amt,
excess_puzhash,
)
# Can now create the treasury inner puz
treasury_inner = DAO_TREASURY_MOD.curry(
DAO_TREASURY_MOD_HASH,
proposal_validator,
proposal_length,
soft_close_length,
attendance,
pass_margin,
self_destruct_time,
oracle_spend_delay,
)
# Setup the spend_p2_singleton (proposal inner puz)
spend_p2_singleton = SPEND_P2_SINGLETON_MOD.curry(
treasury_struct, CAT_MOD_HASH, conditions, [], p2_singleton_puzhash # tailhash conds
)
spend_p2_singleton_puzhash = spend_p2_singleton.get_tree_hash()
parent_amt_list = [[parent_id, locked_amount]]
cat_parent_amt_list = []
spend_p2_singleton_solution = Program.to([parent_amt_list, cat_parent_amt_list, treasury_inner.get_tree_hash()])
proposal: Program = DAO_PROPOSAL_MOD.curry(
proposal_struct,
DAO_PROPOSAL_MOD_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
DAO_LOCKUP_MOD_HASH,
CAT_TAIL_HASH,
treasury_id,
950,
1200,
spend_p2_singleton_puzhash,
)
full_proposal = SINGLETON_MOD.curry(proposal_struct, proposal)
# Oracle spend
solution: Program = Program.to([0, 0, 0, 0, 0, treasury_struct])
conds: Program = treasury_inner.run(solution)
assert len(conds.as_python()) == 3
# Proposal Spend
proposal_amt = 10
proposal_coin_id = Coin(parent_id, full_proposal.get_tree_hash(), proposal_amt).name()
solution: Program = Program.to(
[
[proposal_coin_id, spend_p2_singleton_puzhash, 0, "s"],
[proposal_id, 1200, 950, parent_id, proposal_amt],
spend_p2_singleton,
spend_p2_singleton_solution,
]
)
conds = treasury_inner.run(solution)
assert len(conds.as_python()) == 9 + len(conditions)
def test_lockup() -> None:
CAT_TAIL: Program = Program.to("tail").get_tree_hash()
INNERPUZ = Program.to(1)
previous_votes = [0xFADEDDAB]
full_lockup_puz: Program = DAO_LOCKUP_MOD.curry(
DAO_PROPOSAL_MOD_HASH,
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
DAO_LOCKUP_MOD_HASH,
DAO_FINISHED_STATE_HASH,
CAT_MOD_HASH,
CAT_TAIL,
previous_votes,
INNERPUZ,
)
my_id = Program.to("my_id").get_tree_hash()
lockup_coin_amount = 20
# Test adding vote
new_proposal = 0xBADDADAB
new_vote_list = [new_proposal, 0xFADEDDAB]
child_puzhash = DAO_LOCKUP_MOD.curry(
DAO_PROPOSAL_MOD_HASH,
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
DAO_LOCKUP_MOD_HASH,
DAO_FINISHED_STATE_HASH,
CAT_MOD_HASH,
CAT_TAIL,
new_vote_list,
INNERPUZ,
).get_tree_hash()
message = Program.to([new_proposal, lockup_coin_amount, 1, my_id]).get_tree_hash()
generated_conditions = [[51, child_puzhash, lockup_coin_amount], [62, message]]
solution: Program = Program.to(
[
my_id,
generated_conditions,
20,
new_proposal,
[1, 2, 3, 4, 5, 6, 7, 8, 9, 0], # fake proposal curry vals
1,
20,
child_puzhash,
0,
]
)
conds: Program = full_lockup_puz.run(solution)
assert len(conds.as_python()) == 6
# Test Re-voting on same proposal fails
new_proposal = 0xBADDADAB
new_vote_list = [new_proposal, 0xBADDADAB]
child_puzhash = DAO_LOCKUP_MOD.curry(
DAO_PROPOSAL_MOD_HASH,
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
DAO_LOCKUP_MOD_HASH,
DAO_FINISHED_STATE_HASH,
CAT_MOD_HASH,
CAT_TAIL,
new_vote_list,
INNERPUZ,
).get_tree_hash()
message = Program.to([new_proposal, lockup_coin_amount, 1, my_id]).get_tree_hash()
generated_conditions = [[51, child_puzhash, lockup_coin_amount], [62, message]]
revote_solution: Program = Program.to(
[
my_id,
generated_conditions,
20,
new_proposal,
[1, 2, 3, 4, 5, 6, 7, 8, 9, 0], # fake proposal curry vals
1,
20,
child_puzhash,
0,
]
)
with pytest.raises(ValueError) as e_info:
conds: Program = full_lockup_puz.run(revote_solution)
assert e_info.value.args[0] == "clvm raise"
# Test vote removal
solution = Program.to(
[
0,
generated_conditions,
20,
[0xFADEDDAB],
[1, 2, 3, 4, 5, 6, 7, 8, 9, 0],
0,
0,
0,
0,
]
)
conds = full_lockup_puz.run(solution)
assert len(conds.as_python()) == 3
new_innerpuz = Program.to("new_inner")
new_innerpuzhash = new_innerpuz.get_tree_hash()
child_lockup = DAO_LOCKUP_MOD.curry(
DAO_PROPOSAL_MOD_HASH,
SINGLETON_MOD_HASH,
SINGLETON_LAUNCHER_HASH,
DAO_LOCKUP_MOD_HASH,
DAO_FINISHED_STATE_HASH,
CAT_MOD_HASH,
CAT_TAIL,
previous_votes,
new_innerpuz,
).get_tree_hash()
message = Program.to([0, 0, 0, my_id]).get_tree_hash()
spend_conds = [[51, child_lockup, lockup_coin_amount], [62, message]]
transfer_sol = Program.to(
[
my_id,
spend_conds,
lockup_coin_amount,
0,
[1, 2, 3, 4, 5, 6, 7, 8, 9, 0], # fake proposal curry vals
0,
0,
INNERPUZ.get_tree_hash(),
new_innerpuzhash,
]
)
conds = full_lockup_puz.run(transfer_sol)
assert conds.at("rrrrfrf").as_atom() == child_lockup
def test_proposal_innerpuz() -> None:
proposal_pass_percentage: uint64 = uint64(5100)
attendance_required: uint64 = uint64(1000)
proposal_timelock = 40
soft_close_length = 5
self_destruct_time = 1000
CAT_TAIL_HASH: Program = Program.to("tail").get_tree_hash()
oracle_spend_delay = 10
# Setup the treasury
treasury_id: Program = Program.to("treasury_id").get_tree_hash()
treasury_singleton_struct: Program = Program.to((SINGLETON_MOD_HASH, (treasury_id, SINGLETON_LAUNCHER_HASH)))
treasury_amount = 1
# setup the p2_singleton
p2_singleton = P2_SINGLETON_MOD.curry(treasury_singleton_struct, P2_SINGLETON_AGGREGATOR_MOD)
p2_singleton_puzhash = p2_singleton.get_tree_hash()
parent_id = Program.to("parent").get_tree_hash()
locked_amount = 100000
conditions = [[51, 0xDABBAD00, 1000], [51, 0xCAFEF00D, 100]]
min_amt = 1
excess_puzhash = bytes32(b"1" * 32)
proposal_validator = DAO_PROPOSAL_VALIDATOR_MOD.curry(
treasury_singleton_struct,
DAO_PROPOSAL_MOD_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_LOCKUP_MOD_HASH,
DAO_TREASURY_MOD_HASH,
CAT_TAIL_HASH,
min_amt,
excess_puzhash,
)
treasury_inner_puz: Program = DAO_TREASURY_MOD.curry(
DAO_TREASURY_MOD_HASH,
proposal_validator,
proposal_timelock,
soft_close_length,
attendance_required,
proposal_pass_percentage,
self_destruct_time,
oracle_spend_delay,
)
treasury_inner_puzhash = treasury_inner_puz.get_tree_hash()
full_treasury_puz = SINGLETON_MOD.curry(treasury_singleton_struct, treasury_inner_puz)
full_treasury_puzhash = full_treasury_puz.get_tree_hash()
# Setup the spend_p2_singleton (proposal inner puz)
spend_p2_singleton = SPEND_P2_SINGLETON_MOD.curry(
treasury_singleton_struct, CAT_MOD_HASH, conditions, [], p2_singleton_puzhash # tailhash conds
)
spend_p2_singleton_puzhash = spend_p2_singleton.get_tree_hash()
parent_amt_list = [[parent_id, locked_amount]]
cat_parent_amt_list = []
spend_p2_singleton_solution = Program.to([parent_amt_list, cat_parent_amt_list, treasury_inner_puzhash])
# Setup Proposal
proposal_id: Program = Program.to("proposal_id").get_tree_hash()
proposal_singleton_struct: Program = Program.to((SINGLETON_MOD_HASH, (proposal_id, SINGLETON_LAUNCHER_HASH)))
current_votes = 1200
yes_votes = 950
proposal: Program = DAO_PROPOSAL_MOD.curry(
proposal_singleton_struct,
DAO_PROPOSAL_MOD_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
DAO_LOCKUP_MOD_HASH,
CAT_TAIL_HASH,
treasury_id,
yes_votes,
current_votes,
spend_p2_singleton_puzhash,
)
full_proposal: Program = SINGLETON_MOD.curry(proposal_singleton_struct, proposal)
full_proposal_puzhash: bytes32 = full_proposal.get_tree_hash()
proposal_amt = 11
proposal_coin_id = Coin(parent_id, full_proposal_puzhash, proposal_amt).name()
treasury_solution: Program = Program.to(
[
[proposal_coin_id, spend_p2_singleton_puzhash, 0, "s"],
[proposal_id, current_votes, yes_votes, parent_id, proposal_amt],
spend_p2_singleton,
spend_p2_singleton_solution,
]
)
proposal_solution = Program.to(
[
proposal_validator.get_tree_hash(),
0,
proposal_timelock,
proposal_pass_percentage,
attendance_required,
0,
soft_close_length,
self_destruct_time,
oracle_spend_delay,
0,
proposal_amt,
]
)
# lineage_proof my_amount inner_solution
lineage_proof = [treasury_id, treasury_inner_puzhash, treasury_amount]
full_treasury_solution = Program.to([lineage_proof, treasury_amount, treasury_solution])
full_proposal_solution = Program.to([lineage_proof, proposal_amt, proposal_solution])
# Run the puzzles
# from clvm.casts import int_from_bytes
# cds = conditions_dict_for_solution(full_treasury_puz, full_treasury_solution, INFINITE_COST)[ConditionOpcode.CREATE_COIN]
# amts = [int_from_bytes(x.vars[1]) for x in cds]
# breakpoint()
treasury_conds: Program = conditions_dict_for_solution(full_treasury_puz, full_treasury_solution, INFINITE_COST)
proposal_conds: Program = conditions_dict_for_solution(full_proposal, full_proposal_solution, INFINITE_COST)
# Announcements
treasury_aca = treasury_conds[ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT][0].vars[0]
proposal_cca = proposal_conds[ConditionOpcode.CREATE_COIN_ANNOUNCEMENT][0].vars[0]
assert std_hash(proposal_coin_id + proposal_cca) == treasury_aca
treasury_cpas = [
std_hash(full_treasury_puzhash + cond.vars[0])
for cond in treasury_conds[ConditionOpcode.CREATE_PUZZLE_ANNOUNCEMENT]
]
proposal_apas = [cond.vars[0] for cond in proposal_conds[ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT]]
assert treasury_cpas[1] == proposal_apas[1]
# Test Proposal to update treasury
# Set up new treasury params
new_proposal_pass_percentage: uint64 = uint64(2500)
new_attendance_required: uint64 = uint64(500)
new_proposal_timelock = 900
new_soft_close_length = 10
new_self_destruct_time = 1000
new_oracle_spend_delay = 20
update_proposal = DAO_UPDATE_MOD.curry(
DAO_TREASURY_MOD_HASH,
proposal_validator,
new_proposal_timelock,
new_soft_close_length,
new_attendance_required,
new_proposal_pass_percentage,
new_self_destruct_time,
new_oracle_spend_delay,
)
update_proposal_puzhash = update_proposal.get_tree_hash()
update_proposal_sol = Program.to([])
proposal: Program = DAO_PROPOSAL_MOD.curry(
proposal_singleton_struct,
DAO_PROPOSAL_MOD_HASH,
DAO_PROPOSAL_TIMER_MOD_HASH,
CAT_MOD_HASH,
DAO_FINISHED_STATE_HASH,
DAO_TREASURY_MOD_HASH,
DAO_LOCKUP_MOD_HASH,
CAT_TAIL_HASH,
treasury_id,
yes_votes,
current_votes,
update_proposal_puzhash,
)
full_proposal: Program = SINGLETON_MOD.curry(proposal_singleton_struct, proposal)
full_proposal_puzhash: bytes32 = full_proposal.get_tree_hash()
proposal_coin_id = Coin(parent_id, full_proposal_puzhash, proposal_amt).name()
treasury_solution: Program = Program.to(
[
[proposal_coin_id, update_proposal_puzhash, 0, "u"],
[proposal_id, current_votes, yes_votes, parent_id, proposal_amt],
update_proposal,
update_proposal_sol,
]
)
proposal_solution = Program.to(
[
proposal_validator.get_tree_hash(),
0,
proposal_timelock,
proposal_pass_percentage,
attendance_required,
0,
soft_close_length,
self_destruct_time,
oracle_spend_delay,
0,
proposal_amt,
]
)
lineage_proof = [treasury_id, treasury_inner_puzhash, treasury_amount]
full_treasury_solution = Program.to([lineage_proof, treasury_amount, treasury_solution])
full_proposal_solution = Program.to([lineage_proof, proposal_amt, proposal_solution])
treasury_conds: Program = conditions_dict_for_solution(full_treasury_puz, full_treasury_solution, INFINITE_COST)
proposal_conds: Program = conditions_dict_for_solution(full_proposal, full_proposal_solution, INFINITE_COST)
treasury_aca = treasury_conds[ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT][0].vars[0]
proposal_cca = proposal_conds[ConditionOpcode.CREATE_COIN_ANNOUNCEMENT][0].vars[0]
assert std_hash(proposal_coin_id + proposal_cca) == treasury_aca
treasury_cpas = [
std_hash(full_treasury_puzhash + cond.vars[0])
for cond in treasury_conds[ConditionOpcode.CREATE_PUZZLE_ANNOUNCEMENT]
]
proposal_apas = [cond.vars[0] for cond in proposal_conds[ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT]]
assert treasury_cpas[1] == proposal_apas[1]

File diff suppressed because it is too large Load Diff

View File

@ -727,7 +727,7 @@ async def test_nft_offer_sell_nft_for_cat(
ph_taker_cat_1 = await wallet_taker.get_new_puzzlehash()
ph_taker_cat_2 = await wallet_taker.get_new_puzzlehash()
cat_tx_records = await cat_wallet_maker.generate_signed_transaction(
cat_tx_records = await cat_wallet_maker.generate_signed_transactions(
[cats_to_trade, cats_to_trade], [ph_taker_cat_1, ph_taker_cat_2], memos=[[ph_taker_cat_1], [ph_taker_cat_2]]
)
for tx_record in cat_tx_records:
@ -943,7 +943,7 @@ async def test_nft_offer_request_nft_for_cat(
extra_change = cats_to_mint - (2 * cats_to_trade)
amounts.append(uint64(extra_change))
puzzle_hashes.append(ph_taker_cat_1)
cat_tx_records = await cat_wallet_maker.generate_signed_transaction(amounts, puzzle_hashes)
cat_tx_records = await cat_wallet_maker.generate_signed_transactions(amounts, puzzle_hashes)
for tx_record in cat_tx_records:
await wallet_maker.wallet_state_manager.add_pending_transaction(tx_record)
await full_node_api.process_transaction_records(records=cat_tx_records)

View File

@ -0,0 +1,85 @@
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() -> 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, parent_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)
# child_coin = Coin(parent_coin.name(), parent_puz_hash, 1)
wallet_id = uint32(2)
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
# modify pending
await db.update_pending_transaction(record.coin.name(), False)
record_to_check = await db.get_records_by_coin_id(record.coin.name())
assert record_to_check[0].pending is False
@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

View File

@ -153,6 +153,9 @@ class ClvmBytes:
hash=generate_hash_bytes(hex_bytes=hex_bytes),
)
def strip(self) -> ClvmBytes:
return ClvmBytes(hex=self.hex.strip(b"\n"), hash=self.hash.strip(b"\n"))
# These files have the wrong extension for now so we'll just manually exclude them
excludes: typing.Set[str] = set()
@ -297,7 +300,7 @@ def check(use_cache: bool) -> int:
generated_bytes = ClvmBytes.from_hex_bytes(hex_bytes=generated_paths.hex.read_bytes())
if generated_bytes != reference_bytes:
if generated_bytes.strip() != reference_bytes.strip():
file_fail = True
error = f" reference: {reference_bytes!r}\n"
error += f" generated: {generated_bytes!r}"