From 075a9177e45e83e6980517b166b871bd01f07c85 Mon Sep 17 00:00:00 2001 From: Sebastjan Date: Thu, 7 Jul 2022 14:42:06 +0200 Subject: [PATCH 01/38] updated wallet name --- chia/wallet/wallet_node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index c6296ecb11..4a2ef18287 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -201,10 +201,10 @@ class WalletNode: .replace("CHALLENGE", self.config["selected_network"]) .replace("KEY", db_path_key_suffix) ) - path = path_from_root(self.root_path, db_path_replaced.replace("v1", "v2")) + path = path_from_root(self.root_path, db_path_replaced.replace("v1", "v2_r1")) mkdir(path.parent) - standalone_path = path_from_root(STANDALONE_ROOT_PATH, f"{db_path_replaced.replace('v2', 'v1')}_new") + standalone_path = path_from_root(STANDALONE_ROOT_PATH, f"{db_path_replaced.replace('v2_r1', 'v1')}_new") if not path.exists(): if standalone_path.exists(): self.log.info(f"Copying wallet db from {standalone_path} to {path}") From 4cb54ccda4bf17fe006852bbde19cc1f9ede20c8 Mon Sep 17 00:00:00 2001 From: Sebastjan Date: Thu, 7 Jul 2022 16:47:07 +0200 Subject: [PATCH 02/38] deprecated series --- chia/cmds/wallet.py | 24 +++++++++++++++++++----- chia/cmds/wallet_funcs.py | 15 +++++++-------- chia/rpc/wallet_rpc_api.py | 4 ++-- chia/rpc/wallet_rpc_client.py | 8 ++++---- 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py index 8a31848460..83f7bc210f 100644 --- a/chia/cmds/wallet.py +++ b/chia/cmds/wallet.py @@ -4,8 +4,8 @@ from typing import Any, Dict, Optional, Tuple import click from chia.cmds.plotnft import validate_fee -from chia.wallet.util.wallet_types import WalletType from chia.wallet.transaction_sorting import SortKey +from chia.wallet.util.wallet_types import WalletType @click.group("wallet", short_help="Manage your wallet") @@ -518,8 +518,12 @@ def nft_wallet_create_cmd( @click.option("-mu", "--metadata-uris", help="Comma separated list of metadata URIs", type=str) @click.option("-lh", "--license-hash", help="NFT license hash", type=str, default="") @click.option("-lu", "--license-uris", help="Comma separated list of license URIs", type=str) -@click.option("-st", "--series-total", help="NFT series total number", type=int, default=1, show_default=True) -@click.option("-sn", "--series-number", help="NFT seriese number", type=int, default=1, show_default=True) +@click.option( + "-st", "--series-total", help="[DEPRECATED] NFT series total number", type=int, default=1, show_default=True +) +@click.option("-sn", "--series-number", help="[DEPRECATED] NFT series number", type=int, default=1, show_default=True) +@click.option("-ec", "--edition-count", help="NFT edition count, defaults to 1", type=int) +@click.option("-en", "--edition-number", help="NFT edition number, defaults to 1", type=int) @click.option( "-m", "--fee", @@ -552,6 +556,8 @@ def nft_mint_cmd( license_uris: Optional[str], series_total: Optional[int], series_number: Optional[int], + edition_count: Optional[int], + edition_number: Optional[int], fee: str, royalty_percentage_fraction: int, ) -> None: @@ -568,6 +574,14 @@ def nft_mint_cmd( else: license_uris_list = [lu.strip() for lu in license_uris.split(",")] + if not (edition_number and edition_count): + if series_number and series_total: + print("\nWARNING: Series total(-st) and number(-sn) options are *deprecated*, please use -en and -ec.\n") + edition_number = series_number + edition_count = series_total + else: + edition_number = 1 + edition_count = 1 extra_params = { "wallet_id": id, "royalty_address": royalty_address, @@ -579,8 +593,8 @@ def nft_mint_cmd( "metadata_uris": metadata_uris_list, "license_hash": license_hash, "license_uris": license_uris_list, - "series_total": series_total, - "series_number": series_number, + "edition_count": edition_count, + "edition_number": edition_number, "fee": fee, "royalty_percentage": royalty_percentage_fraction, } diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 6669bd4582..02e4367ef8 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -8,6 +8,7 @@ from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Union import aiohttp +from chia.cmds.cmds_util import transaction_submitted_msg, transaction_status_msg from chia.cmds.show import print_connections from chia.cmds.units import units from chia.rpc.wallet_rpc_client import WalletRpcClient @@ -18,7 +19,6 @@ from chia.util.bech32m import bech32_decode, decode_puzzle_hash, encode_puzzle_h from chia.util.config import load_config from chia.util.default_root import DEFAULT_ROOT_PATH from chia.util.ints import uint16, uint32, uint64 -from chia.cmds.cmds_util import transaction_submitted_msg, transaction_status_msg from chia.wallet.did_wallet.did_info import DID_HRP from chia.wallet.nft_wallet.nft_info import NFT_HRP, NFTInfo from chia.wallet.trade_record import TradeRecord @@ -30,7 +30,6 @@ from chia.wallet.util.wallet_types import WalletType CATNameResolver = Callable[[bytes32], Awaitable[Optional[Tuple[Optional[uint32], str]]]] - transaction_type_descriptions = { TransactionType.INCOMING_TX: "received", TransactionType.OUTGOING_TX: "sent", @@ -550,7 +549,7 @@ def wallet_coin_unit(typ: WalletType, address_prefix: str) -> Tuple[str, int]: def print_balance(amount: int, scale: int, address_prefix: str) -> str: - ret = f"{amount/scale} {address_prefix} " + ret = f"{amount / scale} {address_prefix} " if scale > 1: ret += f"({amount} mojo)" return ret @@ -645,7 +644,7 @@ async def get_wallet(wallet_client: WalletRpcClient, fingerprint: int = None) -> current_sync_status = "Not Synced" print("Wallet keys:") for i, fp in enumerate(fingerprints): - row: str = f"{i+1}) " + row: str = f"{i + 1}) " row += "* " if fp == logged_in_fingerprint else spacing row += f"{fp}" if fp == logged_in_fingerprint and len(current_sync_status) > 0: @@ -769,8 +768,8 @@ async def mint_nft(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) metadata_uris = args["metadata_uris"] license_hash = args["license_hash"] license_uris = args["license_uris"] - series_total = args["series_total"] - series_number = args["series_number"] + edition_count = args["edition_count"] + edition_number = args["edition_number"] fee: int = int(Decimal(args["fee"]) * units["chia"]) royalty_percentage = args["royalty_percentage"] try: @@ -798,8 +797,8 @@ async def mint_nft(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) metadata_uris, license_hash, license_uris, - series_total, - series_number, + edition_count, + edition_number, fee, royalty_percentage, did_id, diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 814dd962db..a4d8f9bd8d 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -1376,8 +1376,8 @@ class WalletRpcApi: ("h", hexstr_to_bytes(request["hash"])), ("mu", request.get("meta_uris", [])), ("lu", request.get("license_uris", [])), - ("sn", uint64(request.get("series_number", 1))), - ("st", uint64(request.get("series_total", 1))), + ("sn", uint64(request.get("edition_number", 1))), + ("st", uint64(request.get("edition_total", 1))), ] if "meta_hash" in request and len(request["meta_hash"]) > 0: metadata_list.append(("mh", hexstr_to_bytes(request["meta_hash"]))) diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 07e1161cb2..732e7ac4f6 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -595,8 +595,8 @@ class WalletRpcClient(RpcClient): meta_uris=[], license_hash="", license_uris=[], - series_total=1, - series_number=1, + edition_count=1, + edition_number=1, fee=0, royalty_percentage=0, did_id=None, @@ -611,8 +611,8 @@ class WalletRpcClient(RpcClient): "meta_uris": meta_uris, "license_hash": license_hash, "license_uris": license_uris, - "series_number": series_number, - "series_total": series_total, + "series_number": edition_number, + "series_total": edition_count, "royalty_percentage": royalty_percentage, "did_id": did_id, "fee": fee, From 16b0eb57d993f86da4407041a43ddcd68b45edd1 Mon Sep 17 00:00:00 2001 From: matt Date: Fri, 8 Jul 2022 16:53:37 +0100 Subject: [PATCH 03/38] swap to cat2 --- chia/wallet/cat_wallet/cat_wallet.py | 2 +- chia/wallet/puzzles/cat.clvm.hex | 1 - chia/wallet/puzzles/cat.clvm.hex.sha256tree | 1 - chia/wallet/puzzles/cat_loader.py | 2 +- chia/wallet/puzzles/{cat.clvm => cat_v2.clvm} | 814 +++++++++--------- chia/wallet/puzzles/cat_v2.clvm.hex | 1 + .../wallet/puzzles/cat_v2.clvm.hex.sha256tree | 1 + tests/clvm/test_clvm_compilation.py | 2 +- .../cat_wallet/test_cat_outer_puzzle.py | 7 +- 9 files changed, 408 insertions(+), 423 deletions(-) delete mode 100644 chia/wallet/puzzles/cat.clvm.hex delete mode 100644 chia/wallet/puzzles/cat.clvm.hex.sha256tree rename chia/wallet/puzzles/{cat.clvm => cat_v2.clvm} (91%) create mode 100644 chia/wallet/puzzles/cat_v2.clvm.hex create mode 100644 chia/wallet/puzzles/cat_v2.clvm.hex.sha256tree diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index fa88b3913e..0dce6230c9 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -644,7 +644,7 @@ class CATWallet: for coin in cat_coins: if first: first = False - announcement = Announcement(coin.name(), std_hash(b"".join([c.name() for c in cat_coins])), b"\xca") + announcement = Announcement(coin.name(), std_hash(b"".join([c.name() for c in cat_coins]))) if need_chia_transaction: if fee > regular_chia_to_claim: chia_tx, _ = await self.create_tandem_xch_tx( diff --git a/chia/wallet/puzzles/cat.clvm.hex b/chia/wallet/puzzles/cat.clvm.hex deleted file mode 100644 index 89cddbc1e5..0000000000 --- a/chia/wallet/puzzles/cat.clvm.hex +++ /dev/null @@ -1 +0,0 @@ -ff02ffff01ff02ff5effff04ff02ffff04ffff04ff05ffff04ffff0bff2cff0580ffff04ff0bff80808080ffff04ffff02ff17ff2f80ffff04ff5fffff04ffff02ff2effff04ff02ffff04ff17ff80808080ffff04ffff0bff82027fff82057fff820b7f80ffff04ff81bfffff04ff82017fffff04ff8202ffffff04ff8205ffffff04ff820bffff80808080808080808080808080ffff04ffff01ffffffff81ca3dff46ff0233ffff3c04ff01ff0181cbffffff02ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff22ffff0bff2cff3480ffff0bff22ffff0bff22ffff0bff2cff5c80ff0980ffff0bff22ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ff0bffff01ff02ffff03ffff09ffff02ff2effff04ff02ffff04ff13ff80808080ff820b9f80ffff01ff02ff26ffff04ff02ffff04ffff02ff13ffff04ff5fffff04ff17ffff04ff2fffff04ff81bfffff04ff82017fffff04ff1bff8080808080808080ffff04ff82017fff8080808080ffff01ff088080ff0180ffff01ff02ffff03ff17ffff01ff02ffff03ffff20ff81bf80ffff0182017fffff01ff088080ff0180ffff01ff088080ff018080ff0180ffff04ffff04ff05ff2780ffff04ffff10ff0bff5780ff778080ff02ffff03ff05ffff01ff02ffff03ffff09ffff02ffff03ffff09ff11ff7880ffff0159ff8080ff0180ffff01818f80ffff01ff02ff7affff04ff02ffff04ff0dffff04ff0bffff04ffff04ff81b9ff82017980ff808080808080ffff01ff02ff5affff04ff02ffff04ffff02ffff03ffff09ff11ff7880ffff01ff04ff78ffff04ffff02ff36ffff04ff02ffff04ff13ffff04ff29ffff04ffff0bff2cff5b80ffff04ff2bff80808080808080ff398080ffff01ff02ffff03ffff09ff11ff2480ffff01ff04ff24ffff04ffff0bff20ff2980ff398080ffff010980ff018080ff0180ffff04ffff02ffff03ffff09ff11ff7880ffff0159ff8080ff0180ffff04ffff02ff7affff04ff02ffff04ff0dffff04ff0bffff04ff17ff808080808080ff80808080808080ff0180ffff01ff04ff80ffff04ff80ff17808080ff0180ffffff02ffff03ff05ffff01ff04ff09ffff02ff26ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff0bff22ffff0bff2cff5880ffff0bff22ffff0bff22ffff0bff2cff5c80ff0580ffff0bff22ffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bff2cff058080ff0180ffff04ffff04ff28ffff04ff5fff808080ffff02ff7effff04ff02ffff04ffff04ffff04ff2fff0580ffff04ff5fff82017f8080ffff04ffff02ff7affff04ff02ffff04ff0bffff04ff05ffff01ff808080808080ffff04ff17ffff04ff81bfffff04ff82017fffff04ffff0bff8204ffffff02ff36ffff04ff02ffff04ff09ffff04ff820affffff04ffff0bff2cff2d80ffff04ff15ff80808080808080ff8216ff80ffff04ff8205ffffff04ff820bffff808080808080808080808080ff02ff2affff04ff02ffff04ff5fffff04ff3bffff04ffff02ffff03ff17ffff01ff09ff2dffff0bff27ffff02ff36ffff04ff02ffff04ff29ffff04ff57ffff04ffff0bff2cff81b980ffff04ff59ff80808080808080ff81b78080ff8080ff0180ffff04ff17ffff04ff05ffff04ff8202ffffff04ffff04ffff04ff24ffff04ffff0bff7cff2fff82017f80ff808080ffff04ffff04ff30ffff04ffff0bff81bfffff0bff7cff15ffff10ff82017fffff11ff8202dfff2b80ff8202ff808080ff808080ff138080ff80808080808080808080ff018080 \ No newline at end of file diff --git a/chia/wallet/puzzles/cat.clvm.hex.sha256tree b/chia/wallet/puzzles/cat.clvm.hex.sha256tree deleted file mode 100644 index abaa181659..0000000000 --- a/chia/wallet/puzzles/cat.clvm.hex.sha256tree +++ /dev/null @@ -1 +0,0 @@ -72dec062874cd4d3aab892a0906688a1ae412b0109982e1797a170add88bdcdc diff --git a/chia/wallet/puzzles/cat_loader.py b/chia/wallet/puzzles/cat_loader.py index 448fea212e..64632a64bb 100644 --- a/chia/wallet/puzzles/cat_loader.py +++ b/chia/wallet/puzzles/cat_loader.py @@ -1,4 +1,4 @@ from chia.wallet.puzzles.load_clvm import load_clvm -CAT_MOD = load_clvm("cat.clvm", package_or_requirement=__name__) +CAT_MOD = load_clvm("cat_v2.clvm", package_or_requirement=__name__) LOCK_INNER_PUZZLE = load_clvm("lock.inner.puzzle.clvm", package_or_requirement=__name__) diff --git a/chia/wallet/puzzles/cat.clvm b/chia/wallet/puzzles/cat_v2.clvm similarity index 91% rename from chia/wallet/puzzles/cat.clvm rename to chia/wallet/puzzles/cat_v2.clvm index e16f212389..3f6199bddf 100644 --- a/chia/wallet/puzzles/cat.clvm +++ b/chia/wallet/puzzles/cat_v2.clvm @@ -1,417 +1,397 @@ -; Coins locked with this puzzle are spendable cats. -; -; Choose a list of n inputs (n>=1), I_1, ... I_n with amounts A_1, ... A_n. -; -; We put them in a ring, so "previous" and "next" have intuitive k-1 and k+1 semantics, -; wrapping so {n} and 0 are the same, ie. all indices are mod n. -; -; Each coin creates 0 or more coins with total output value O_k. -; Let D_k = the "debt" O_k - A_k contribution of coin I_k, ie. how much debt this input accumulates. -; Some coins may spend more than they contribute and some may spend less, ie. D_k need -; not be zero. That's okay. It's enough for the total of all D_k in the ring to be 0. -; -; A coin can calculate its own D_k since it can verify A_k (it's hashed into the coin id) -; and it can sum up `CREATE_COIN` conditions for O_k. -; -; Defines a "subtotal of debts" S_k for each coin as follows: -; -; S_1 = 0 -; S_k = S_{k-1} + D_{k-1} -; -; Here's the main trick that shows the ring sums to 0. -; You can prove by induction that S_{k+1} = D_1 + D_2 + ... + D_k. -; But it's a ring, so S_{n+1} is also S_1, which is 0. So D_1 + D_2 + ... + D_k = 0. -; So the total debts must be 0, ie. no coins are created or destroyed. -; -; Each coin's solution includes I_{k-1}, I_k, and I_{k+1} along with proofs that I_{k}, and I_{k+1} are CATs of the same type. -; Each coin's solution includes S_{k-1}. It calculates D_k = O_k - A_k, and then S_k = S_{k-1} + D_{k-1} -; -; Announcements are used to ensure that each S_k follows the pattern is valid. -; Announcements automatically commit to their own coin id. -; Coin I_k creates an announcement that further commits to I_{k-1} and S_{k-1}. -; -; Coin I_k gets a proof that I_{k+1} is a cat, so it knows it must also create an announcement -; when spent. It checks that I_{k+1} creates an announcement committing to I_k and S_k. -; -; So S_{k+1} is correct iff S_k is correct. -; -; Coins also receive proofs that their neighbours are CATs, ensuring the announcements aren't forgeries. -; Inner puzzles and the CAT layer prepend `CREATE_COIN_ANNOUNCEMENT` with different prefixes to avoid forgeries. -; Ring announcements use 0xcb, and inner puzzles are given 0xca -; -; In summary, I_k generates a coin_announcement Y_k ("Y" for "yell") as follows: -; -; Y_k: hash of I_k (automatically), I_{k-1}, S_k -; -; Each coin creates an assert_coin_announcement to ensure that the next coin's announcement is as expected: -; Y_{k+1} : hash of I_{k+1}, I_k, S_{k+1} -; -; TLDR: -; I_k : coins -; A_k : amount coin k contributes -; O_k : amount coin k spend -; D_k : difference/delta that coin k incurs (A - O) -; S_k : subtotal of debts D_1 + D_2 ... + D_k -; Y_k : announcements created by coin k commiting to I_{k-1}, I_k, S_k -; -; All conditions go through a "transformer" that looks for CREATE_COIN conditions -; generated by the inner solution, and wraps the puzzle hash ensuring the output is a cat. -; -; Three output conditions are prepended to the list of conditions for each I_k: -; (ASSERT_MY_ID I_k) to ensure that the passed in value for I_k is correct -; (CREATE_COIN_ANNOUNCEMENT I_{k-1} S_k) to create this coin's announcement -; (ASSERT_COIN_ANNOUNCEMENT hashed_announcement(Y_{k+1})) to ensure the next coin really is next and -; the relative values of S_k and S_{k+1} are correct -; -; This is all we need to do to ensure cats exactly balance in the inputs and outputs. -; -; Proof: -; Consider n, k, I_k values, O_k values, S_k and A_k as above. -; For the (CREATE_COIN_ANNOUNCEMENT Y_{k+1}) (created by the next coin) -; and (ASSERT_COIN_ANNOUNCEMENT hashed(Y_{k+1})) to match, -; we see that I_k can ensure that is has the correct value for S_{k+1}. -; -; By induction, we see that S_{m+1} = sum(i, 1, m) [O_i - A_i] = sum(i, 1, m) O_i - sum(i, 1, m) A_i -; So S_{n+1} = sum(i, 1, n) O_i - sum(i, 1, n) A_i. But S_{n+1} is actually S_1 = 0, -; so thus sum(i, 1, n) O_i = sum (i, 1, n) A_i, ie. output total equals input total. - -;; GLOSSARY: -;; MOD_HASH: this code's sha256 tree hash -;; TAIL_PROGRAM_HASH: the program that determines if a coin can mint new cats, burn cats, and check if its lineage is valid if its parent is not a CAT -;; INNER_PUZZLE: an independent puzzle protecting the coins. Solutions to this puzzle are expected to generate `AGG_SIG` conditions and possibly `CREATE_COIN` conditions. -;; ---- items above are curried into the puzzle hash ---- -;; inner_puzzle_solution: the solution to the inner puzzle -;; prev_coin_id: the id for the previous coin -;; tail_program_reveal: reveal of TAIL_PROGRAM_HASH required to run the program if desired -;; tail_solution: optional solution passed into tail_program -;; lineage_proof: optional proof that our coin's parent is a CAT -;; this_coin_info: (parent_id puzzle_hash amount) -;; next_coin_proof: (parent_id inner_puzzle_hash amount) -;; prev_subtotal: the subtotal between prev-coin and this-coin -;; extra_delta: an amount that is added to our delta and checked by the TAIL program -;; - -(mod ( - MOD_HASH ;; curried into puzzle - TAIL_PROGRAM_HASH ;; curried into puzzle - INNER_PUZZLE ;; curried into puzzle - inner_puzzle_solution ;; if invalid, INNER_PUZZLE will fail - lineage_proof ;; This is the parent's coin info, used to check if the parent was a CAT. Optional if using tail_program. - prev_coin_id ;; used in this coin's announcement, prev_coin ASSERT_COIN_ANNOUNCEMENT will fail if wrong - this_coin_info ;; verified with ASSERT_MY_COIN_ID - next_coin_proof ;; used to generate ASSERT_COIN_ANNOUNCEMENT - prev_subtotal ;; included in announcement, prev_coin ASSERT_COIN_ANNOUNCEMENT will fail if wrong - extra_delta ;; this is the "legal discrepancy" between your real delta and what you're announcing your delta is - ) - - ;;;;; start library code - - (include condition_codes.clvm) - (include curry-and-treehash.clinc) - (include cat_truths.clib) - - (defconstant ANNOUNCEMENT_MORPH_BYTE 0xca) - (defconstant RING_MORPH_BYTE 0xcb) - - (defmacro assert items - (if (r items) - (list if (f items) (c assert (r items)) (q . (x))) - (f items) - ) - ) - - (defmacro and ARGS - (if ARGS - (qq (if (unquote (f ARGS)) - (unquote (c and (r ARGS))) - () - )) - 1) - ) - - ; takes a lisp tree and returns the hash of it - (defun sha256tree1 (TREE) - (if (l TREE) - (sha256 2 (sha256tree1 (f TREE)) (sha256tree1 (r TREE))) - (sha256 ONE TREE))) - - ; 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 - ) - ) - - ; cat_mod_struct = (MOD_HASH MOD_HASH_hash GENESIS_COIN_CHECKER GENESIS_COIN_CHECKER_hash) - - (defun-inline mod_hash_from_cat_mod_struct (cat_mod_struct) (f cat_mod_struct)) - (defun-inline mod_hash_hash_from_cat_mod_struct (cat_mod_struct) (f (r cat_mod_struct))) - (defun-inline tail_program_hash_from_cat_mod_struct (cat_mod_struct) (f (r (r cat_mod_struct)))) - - ;;;;; end library code - - ;; return the puzzle hash for a cat with the given `GENESIS_COIN_CHECKER_hash` & `INNER_PUZZLE` - (defun-inline cat_puzzle_hash (cat_mod_struct inner_puzzle_hash) - (puzzle-hash-of-curried-function (mod_hash_from_cat_mod_struct cat_mod_struct) - inner_puzzle_hash - (sha256 ONE (tail_program_hash_from_cat_mod_struct cat_mod_struct)) - (mod_hash_hash_from_cat_mod_struct cat_mod_struct) - ) - ) - - ;; tweak `CREATE_COIN` condition by wrapping the puzzle hash, forcing it to be a cat - ;; prepend `CREATE_COIN_ANNOUNCEMENT` with 0xca as bytes so it cannot be used to cheat the coin ring - - (defun-inline morph_condition (condition cat_mod_struct) - (if (= (f condition) CREATE_COIN) - (c CREATE_COIN - (c (cat_puzzle_hash cat_mod_struct (f (r condition))) - (r (r condition))) - ) - (if (= (f condition) CREATE_COIN_ANNOUNCEMENT) - (c CREATE_COIN_ANNOUNCEMENT - (c (sha256 ANNOUNCEMENT_MORPH_BYTE (f (r condition))) - (r (r condition)) - ) - ) - condition - ) - ) - ) - - ;; given a coin's parent, inner_puzzle and amount, and the cat_mod_struct, calculate the id of the coin - (defun-inline coin_id_for_proof (coin cat_mod_struct) - (sha256 (f coin) (cat_puzzle_hash cat_mod_struct (f (r coin))) (f (r (r coin)))) - ) - - ;; utility to fetch coin amount from coin - (defun-inline input_amount_for_coin (coin) - (f (r (r coin))) - ) - - ;; calculate the hash of an announcement - ;; we add 0xcb so ring announcements exist in a different namespace to announcements from inner_puzzles - (defun-inline calculate_annoucement_id (this_coin_id this_subtotal next_coin_id cat_mod_struct) - (sha256 next_coin_id (sha256 RING_MORPH_BYTE this_coin_id this_subtotal)) - ) - - ;; create the `ASSERT_COIN_ANNOUNCEMENT` condition that ensures the next coin's announcement is correct - (defun-inline create_assert_next_announcement_condition (this_coin_id this_subtotal next_coin_id cat_mod_struct) - (list ASSERT_COIN_ANNOUNCEMENT - (calculate_annoucement_id this_coin_id - this_subtotal - next_coin_id - cat_mod_struct - ) - ) - ) - - ;; here we commit to I_{k-1} and S_k - ;; we add 0xcb so ring announcements exist in a different namespace to announcements from inner_puzzles - (defun-inline create_announcement_condition (prev_coin_id prev_subtotal) - (list CREATE_COIN_ANNOUNCEMENT - (sha256 RING_MORPH_BYTE prev_coin_id prev_subtotal) - ) - ) - - ;;;;;;;;;;;;;;;;;;;;;;;;;;; - - ;; this function takes a condition and returns an integer indicating - ;; the value of all output coins created with CREATE_COIN. If it's not - ;; a CREATE_COIN condition, it returns 0. - - (defun-inline output_value_for_condition (condition) - (if (= (f condition) CREATE_COIN) - (f (r (r condition))) - 0 - ) - ) - - ;; add two conditions to the list of morphed conditions: - ;; CREATE_COIN_ANNOUNCEMENT for my announcement - ;; ASSERT_COIN_ANNOUNCEMENT for the next coin's announcement - (defun-inline generate_final_output_conditions - ( - prev_subtotal - this_subtotal - morphed_conditions - prev_coin_id - this_coin_id - next_coin_id - cat_mod_struct - ) - (c (create_announcement_condition prev_coin_id prev_subtotal) - (c (create_assert_next_announcement_condition this_coin_id this_subtotal next_coin_id cat_mod_struct) - morphed_conditions) - ) - ) - - - ;; This next section of code loops through all of the conditions to do three things: - ;; 1) Look for a "magic" value of -113 and, if one exists, filter it, and take note of the tail reveal and solution - ;; 2) Morph any CREATE_COIN or CREATE_COIN_ANNOUNCEMENT conditions - ;; 3) Sum the total output amount of all of the CREATE_COINs that are output by the inner puzzle - ;; - ;; After everything return a struct in the format (morphed_conditions . (output_sum . tail_reveal_and_solution)) - ;; If multiple magic conditions are specified, the later one will take precedence - - (defun-inline condition_tail_reveal (condition) (f (r (r (r condition))))) - (defun-inline condition_tail_solution (condition) (f (r (r (r (r condition)))))) - - (defun cons_onto_first_and_add_to_second (morphed_condition output_value struct) - (c (c morphed_condition (f struct)) (c (+ output_value (f (r struct))) (r (r struct)))) - ) - - (defun find_and_strip_tail_info (inner_conditions cat_mod_struct tail_reveal_and_solution) - (if inner_conditions - (if (= (output_value_for_condition (f inner_conditions)) -113) ; Checks this is a CREATE_COIN of value -113 - (find_and_strip_tail_info - (r inner_conditions) - cat_mod_struct - (c (condition_tail_reveal (f inner_conditions)) (condition_tail_solution (f inner_conditions))) - ) - (cons_onto_first_and_add_to_second - (morph_condition (f inner_conditions) cat_mod_struct) - (output_value_for_condition (f inner_conditions)) - (find_and_strip_tail_info - (r inner_conditions) - cat_mod_struct - tail_reveal_and_solution - ) - ) - ) - (c () (c 0 tail_reveal_and_solution)) - ) - ) - - ;;;;;;;;;;;;;;;;;;;;;;;;;;; lineage checking - - ;; return true iff parent of `this_coin_info` is provably a cat - ;; A 'lineage proof' consists of (parent_parent_id parent_INNER_puzzle_hash parent_amount) - ;; We use this information to construct a coin who's puzzle has been wrapped in this MOD and verify that, - ;; once wrapped, it matches our parent coin's ID. - (defun-inline is_parent_cat ( - cat_mod_struct - parent_id - lineage_proof - ) - (= parent_id - (sha256 (f lineage_proof) - (cat_puzzle_hash cat_mod_struct (f (r lineage_proof))) - (f (r (r lineage_proof))) - ) - ) - ) - - (defun check_lineage_or_run_tail_program - ( - this_coin_info - tail_reveal_and_solution - parent_is_cat ; flag which says whether or not the parent CAT check ran and passed - lineage_proof - Truths - extra_delta - inner_conditions - ) - (if tail_reveal_and_solution - (assert (= (sha256tree1 (f tail_reveal_and_solution)) (cat_tail_program_hash_truth Truths)) - (merge_list - (a (f tail_reveal_and_solution) - (list - Truths - parent_is_cat - lineage_proof ; Lineage proof is only guaranteed to be true if parent_is_cat - extra_delta - inner_conditions - (r tail_reveal_and_solution) - ) - ) - inner_conditions - ) - ) - (assert parent_is_cat (not extra_delta) - inner_conditions - ) - ) - ) - - ;;;;;;;;;;;;;;;;;;;;;;;;;;; - - (defun stager_two ( - Truths - (inner_conditions . (output_sum . tail_reveal_and_solution)) - lineage_proof - prev_coin_id - this_coin_info - next_coin_id - prev_subtotal - extra_delta - ) - (check_lineage_or_run_tail_program - this_coin_info - tail_reveal_and_solution - (if lineage_proof (is_parent_cat (cat_struct_truth Truths) (my_parent_cat_truth Truths) lineage_proof) ()) - lineage_proof - Truths - extra_delta - (generate_final_output_conditions - prev_subtotal - ; the expression on the next line calculates `this_subtotal` by adding the delta to `prev_subtotal` - (+ prev_subtotal (- (input_amount_for_coin this_coin_info) output_sum) extra_delta) - inner_conditions - prev_coin_id - (my_id_cat_truth Truths) - next_coin_id - (cat_struct_truth Truths) - ) - ) - ) - - ; CAT TRUTHS struct is: ; CAT Truths is: ((Inner puzzle hash . (MOD hash . (MOD hash hash . TAIL hash))) . (my_id . (my_parent_info my_puzhash my_amount))) - ; create truths - this_coin_info verified true because we calculated my ID from it! - ; lineage proof is verified later by cat parent check or tail_program - - (defun stager ( - cat_mod_struct - inner_conditions - lineage_proof - inner_puzzle_hash - my_id - prev_coin_id - this_coin_info - next_coin_proof - prev_subtotal - extra_delta - ) - (c (list ASSERT_MY_COIN_ID my_id) (stager_two - (cat_truth_data_to_truth_struct - inner_puzzle_hash - cat_mod_struct - my_id - this_coin_info - ) - (find_and_strip_tail_info inner_conditions cat_mod_struct ()) - lineage_proof - prev_coin_id - this_coin_info - (coin_id_for_proof next_coin_proof cat_mod_struct) - prev_subtotal - extra_delta - )) - ) - - (stager - ;; calculate cat_mod_struct, inner_puzzle_hash, coin_id - (list MOD_HASH (sha256 ONE MOD_HASH) TAIL_PROGRAM_HASH) - (a INNER_PUZZLE inner_puzzle_solution) - lineage_proof - (sha256tree1 INNER_PUZZLE) - (sha256 (f this_coin_info) (f (r this_coin_info)) (f (r (r this_coin_info)))) - prev_coin_id ; ID - this_coin_info ; (parent_id puzzle_hash amount) - next_coin_proof ; (parent_id innerpuzhash amount) - prev_subtotal - extra_delta - ) -) +; Coins locked with this puzzle are spendable cats. +; +; Choose a list of n inputs (n>=1), I_1, ... I_n with amounts A_1, ... A_n. +; +; We put them in a ring, so "previous" and "next" have intuitive k-1 and k+1 semantics, +; wrapping so {n} and 0 are the same, ie. all indices are mod n. +; +; Each coin creates 0 or more coins with total output value O_k. +; Let D_k = the "debt" O_k - A_k contribution of coin I_k, ie. how much debt this input accumulates. +; Some coins may spend more than they contribute and some may spend less, ie. D_k need +; not be zero. That's okay. It's enough for the total of all D_k in the ring to be 0. +; +; A coin can calculate its own D_k since it can verify A_k (it's hashed into the coin id) +; and it can sum up `CREATE_COIN` conditions for O_k. +; +; Defines a "subtotal of debts" S_k for each coin as follows: +; +; S_1 = 0 +; S_k = S_{k-1} + D_{k-1} +; +; Here's the main trick that shows the ring sums to 0. +; You can prove by induction that S_{k+1} = D_1 + D_2 + ... + D_k. +; But it's a ring, so S_{n+1} is also S_1, which is 0. So D_1 + D_2 + ... + D_k = 0. +; So the total debts must be 0, ie. no coins are created or destroyed. +; +; Each coin's solution includes I_{k-1}, I_k, and I_{k+1} along with proofs that I_{k}, and I_{k+1} are CATs of the same type. +; Each coin's solution includes S_{k-1}. It calculates D_k = O_k - A_k, and then S_k = S_{k-1} + D_{k-1} +; +; Announcements are used to ensure that each S_k follows the pattern is valid. +; Announcements automatically commit to their own coin id. +; Coin I_k creates an announcement that further commits to I_{k-1} and S_{k-1}. +; +; Coin I_k gets a proof that I_{k+1} is a cat, so it knows it must also create an announcement +; when spent. It checks that I_{k+1} creates an announcement committing to I_k and S_k. +; +; So S_{k+1} is correct iff S_k is correct. +; +; Coins also receive proofs that their neighbours are CATs, ensuring the announcements aren't forgeries. +; Inner puzzles and the CAT layer prepend `CREATE_COIN_ANNOUNCEMENT` with different prefixes to avoid forgeries. +; Ring announcements use 0xcb, and inner puzzles are given 0xca +; +; In summary, I_k generates a coin_announcement Y_k ("Y" for "yell") as follows: +; +; Y_k: hash of I_k (automatically), I_{k-1}, S_k +; +; Each coin creates an assert_coin_announcement to ensure that the next coin's announcement is as expected: +; Y_{k+1} : hash of I_{k+1}, I_k, S_{k+1} +; +; TLDR: +; I_k : coins +; A_k : amount coin k contributes +; O_k : amount coin k spend +; D_k : difference/delta that coin k incurs (A - O) +; S_k : subtotal of debts D_1 + D_2 ... + D_k +; Y_k : announcements created by coin k commiting to I_{k-1}, I_k, S_k +; +; All conditions go through a "transformer" that looks for CREATE_COIN conditions +; generated by the inner solution, and wraps the puzzle hash ensuring the output is a cat. +; +; Three output conditions are prepended to the list of conditions for each I_k: +; (ASSERT_MY_ID I_k) to ensure that the passed in value for I_k is correct +; (CREATE_COIN_ANNOUNCEMENT I_{k-1} S_k) to create this coin's announcement +; (ASSERT_COIN_ANNOUNCEMENT hashed_announcement(Y_{k+1})) to ensure the next coin really is next and +; the relative values of S_k and S_{k+1} are correct +; +; This is all we need to do to ensure cats exactly balance in the inputs and outputs. +; +; Proof: +; Consider n, k, I_k values, O_k values, S_k and A_k as above. +; For the (CREATE_COIN_ANNOUNCEMENT Y_{k+1}) (created by the next coin) +; and (ASSERT_COIN_ANNOUNCEMENT hashed(Y_{k+1})) to match, +; we see that I_k can ensure that is has the correct value for S_{k+1}. +; +; By induction, we see that S_{m+1} = sum(i, 1, m) [O_i - A_i] = sum(i, 1, m) O_i - sum(i, 1, m) A_i +; So S_{n+1} = sum(i, 1, n) O_i - sum(i, 1, n) A_i. But S_{n+1} is actually S_1 = 0, +; so thus sum(i, 1, n) O_i = sum (i, 1, n) A_i, ie. output total equals input total. + +;; GLOSSARY: +;; MOD_HASH: this code's sha256 tree hash +;; TAIL_PROGRAM_HASH: the program that determines if a coin can mint new cats, burn cats, and check if its lineage is valid if its parent is not a CAT +;; INNER_PUZZLE: an independent puzzle protecting the coins. Solutions to this puzzle are expected to generate `AGG_SIG` conditions and possibly `CREATE_COIN` conditions. +;; ---- items above are curried into the puzzle hash ---- +;; inner_puzzle_solution: the solution to the inner puzzle +;; prev_coin_id: the id for the previous coin +;; tail_program_reveal: reveal of TAIL_PROGRAM_HASH required to run the program if desired +;; tail_solution: optional solution passed into tail_program +;; lineage_proof: optional proof that our coin's parent is a CAT +;; this_coin_info: (parent_id puzzle_hash amount) +;; next_coin_proof: (parent_id inner_puzzle_hash amount) +;; prev_subtotal: the subtotal between prev-coin and this-coin +;; extra_delta: an amount that is added to our delta and checked by the TAIL program +;; + +(mod ( + MOD_HASH ;; curried into puzzle + TAIL_PROGRAM_HASH ;; curried into puzzle + INNER_PUZZLE ;; curried into puzzle + inner_puzzle_solution ;; if invalid, INNER_PUZZLE will fail + lineage_proof ;; This is the parent's coin info, used to check if the parent was a CAT. Optional if using tail_program. + prev_coin_id ;; used in this coin's announcement, prev_coin ASSERT_COIN_ANNOUNCEMENT will fail if wrong + this_coin_info ;; verified with ASSERT_MY_COIN_ID + next_coin_proof ;; used to generate ASSERT_COIN_ANNOUNCEMENT + prev_subtotal ;; included in announcement, prev_coin ASSERT_COIN_ANNOUNCEMENT will fail if wrong + extra_delta ;; this is the "legal discrepancy" between your real delta and what you're announcing your delta is + ) + + ;;;;; start library code + + (include condition_codes.clvm) + (include curry-and-treehash.clinc) + (include cat_truths.clib) + (include utility_macros.clib) + + (defconstant RING_MORPH_BYTE 0xcb) + + + ; 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 + ) + ) + + ; cat_mod_struct = (MOD_HASH MOD_HASH_hash GENESIS_COIN_CHECKER GENESIS_COIN_CHECKER_hash) + + (defun-inline mod_hash_from_cat_mod_struct (cat_mod_struct) (f cat_mod_struct)) + (defun-inline mod_hash_hash_from_cat_mod_struct (cat_mod_struct) (f (r cat_mod_struct))) + (defun-inline tail_program_hash_from_cat_mod_struct (cat_mod_struct) (f (r (r cat_mod_struct)))) + + ;;;;; end library code + + ;; return the puzzle hash for a cat with the given `GENESIS_COIN_CHECKER_hash` & `INNER_PUZZLE` + (defun-inline cat_puzzle_hash (cat_mod_struct inner_puzzle_hash) + (puzzle-hash-of-curried-function (mod_hash_from_cat_mod_struct cat_mod_struct) + inner_puzzle_hash + (sha256 ONE (tail_program_hash_from_cat_mod_struct cat_mod_struct)) + (mod_hash_hash_from_cat_mod_struct cat_mod_struct) + ) + ) + + ;; assert `CREATE_COIN_ANNOUNCEMENT` doesn't contain the RING_MORPH_BYTE bytes so it cannot be used to cheat the coin ring + + (defun-inline morph_condition (condition cat_mod_struct) + (if (= (f condition) CREATE_COIN) + (c CREATE_COIN + (c (cat_puzzle_hash cat_mod_struct (f (r condition))) + (r (r condition))) + ) + (if (= (f condition) CREATE_COIN_ANNOUNCEMENT) + (assert (not (and + (= 33 (strlen (f (r condition)))) + (= (substr (f (r condition)) 0 ONE) RING_MORPH_BYTE) ; lazy eval + )) + ; then + condition + ) + condition + ) + ) + ) + + ;; given a coin's parent, inner_puzzle and amount, and the cat_mod_struct, calculate the id of the coin + (defun-inline coin_id_for_proof (coin cat_mod_struct) + (calculate_coin_id (f coin) (cat_puzzle_hash cat_mod_struct (f (r coin))) (f (r (r coin)))) + ) + + ;; utility to fetch coin amount from coin + (defun-inline input_amount_for_coin (coin) + (f (r (r coin))) + ) + + ;; calculate the hash of an announcement + ;; we add 0xcb so ring announcements exist in a different namespace to announcements from inner_puzzles + (defun-inline calculate_annoucement_id (this_coin_id this_subtotal next_coin_id cat_mod_struct) + (sha256 next_coin_id RING_MORPH_BYTE (sha256tree (list this_coin_id this_subtotal))) + ) + + ;; create the `ASSERT_COIN_ANNOUNCEMENT` condition that ensures the next coin's announcement is correct + (defun-inline create_assert_next_announcement_condition (this_coin_id this_subtotal next_coin_id cat_mod_struct) + (list ASSERT_COIN_ANNOUNCEMENT + (calculate_annoucement_id this_coin_id + this_subtotal + next_coin_id + cat_mod_struct + ) + ) + ) + + ;; here we commit to I_{k-1} and S_k + ;; we add 0xcb so ring announcements exist in a different namespace to announcements from inner_puzzles + (defun-inline create_announcement_condition (prev_coin_id prev_subtotal) + (list CREATE_COIN_ANNOUNCEMENT + (concat RING_MORPH_BYTE (sha256tree (list prev_coin_id prev_subtotal))) + ) + ) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;; + + ;; this function takes a condition and returns an integer indicating + ;; the value of all output coins created with CREATE_COIN. If it's not + ;; a CREATE_COIN condition, it returns 0. + + (defun-inline output_value_for_condition (condition) + (if (= (f condition) CREATE_COIN) + (f (r (r condition))) + 0 + ) + ) + + ;; add two conditions to the list of morphed conditions: + ;; CREATE_COIN_ANNOUNCEMENT for my announcement + ;; ASSERT_COIN_ANNOUNCEMENT for the next coin's announcement + (defun-inline generate_final_output_conditions + ( + prev_subtotal + this_subtotal + morphed_conditions + prev_coin_id + this_coin_id + next_coin_id + cat_mod_struct + ) + (c (create_announcement_condition prev_coin_id prev_subtotal) + (c (create_assert_next_announcement_condition this_coin_id this_subtotal next_coin_id cat_mod_struct) + morphed_conditions) + ) + ) + + + ;; This next section of code loops through all of the conditions to do three things: + ;; 1) Look for a "magic" value of -113 and, if one exists, filter it, and take note of the tail reveal and solution + ;; 2) Morph any CREATE_COIN or CREATE_COIN_ANNOUNCEMENT conditions + ;; 3) Sum the total output amount of all of the CREATE_COINs that are output by the inner puzzle + ;; + ;; After everything return a struct in the format (morphed_conditions . (output_sum . tail_reveal_and_solution)) + ;; If multiple magic conditions are specified, the later one will take precedence + + (defun-inline condition_tail_reveal (condition) (f (r (r (r condition))))) + (defun-inline condition_tail_solution (condition) (f (r (r (r (r condition)))))) + + (defun cons_onto_first_and_add_to_second (morphed_condition output_value struct) + (c (c morphed_condition (f struct)) (c (+ output_value (f (r struct))) (r (r struct)))) + ) + + (defun find_and_strip_tail_info (inner_conditions cat_mod_struct tail_reveal_and_solution) + (if inner_conditions + (if (= (output_value_for_condition (f inner_conditions)) -113) ; Checks this is a CREATE_COIN of value -113 + (find_and_strip_tail_info + (r inner_conditions) + cat_mod_struct + (c (condition_tail_reveal (f inner_conditions)) (condition_tail_solution (f inner_conditions))) + ) + (cons_onto_first_and_add_to_second + (morph_condition (f inner_conditions) cat_mod_struct) + (output_value_for_condition (f inner_conditions)) + (find_and_strip_tail_info + (r inner_conditions) + cat_mod_struct + tail_reveal_and_solution + ) + ) + ) + (c () (c 0 tail_reveal_and_solution)) + ) + ) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;; lineage checking + + ;; return true iff parent of `this_coin_info` is provably a cat + ;; A 'lineage proof' consists of (parent_parent_id parent_INNER_puzzle_hash parent_amount) + ;; We use this information to construct a coin who's puzzle has been wrapped in this MOD and verify that, + ;; once wrapped, it matches our parent coin's ID. + (defun-inline is_parent_cat ( + cat_mod_struct + parent_id + lineage_proof + ) + (= parent_id + (calculate_coin_id (f lineage_proof) + (cat_puzzle_hash cat_mod_struct (f (r lineage_proof))) + (f (r (r lineage_proof))) + ) + ) + ) + + (defun check_lineage_or_run_tail_program + ( + this_coin_info + tail_reveal_and_solution + parent_is_cat ; flag which says whether or not the parent CAT check ran and passed + lineage_proof + Truths + extra_delta + inner_conditions + ) + (if tail_reveal_and_solution + (assert (= (sha256tree (f tail_reveal_and_solution)) (cat_tail_program_hash_truth Truths)) + (merge_list + (a (f tail_reveal_and_solution) + (list + Truths + parent_is_cat + lineage_proof ; Lineage proof is only guaranteed to be true if parent_is_cat + extra_delta + inner_conditions + (r tail_reveal_and_solution) + ) + ) + inner_conditions + ) + ) + (assert parent_is_cat (not extra_delta) + inner_conditions + ) + ) + ) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;; + + (defun stager_two ( + Truths + (inner_conditions . (output_sum . tail_reveal_and_solution)) + lineage_proof + prev_coin_id + this_coin_info + next_coin_id + prev_subtotal + extra_delta + ) + (check_lineage_or_run_tail_program + this_coin_info + tail_reveal_and_solution + (if lineage_proof (is_parent_cat (cat_struct_truth Truths) (my_parent_cat_truth Truths) lineage_proof) ()) + lineage_proof + Truths + extra_delta + (generate_final_output_conditions + prev_subtotal + ; the expression on the next line calculates `this_subtotal` by adding the delta to `prev_subtotal` + (+ prev_subtotal (- (input_amount_for_coin this_coin_info) output_sum) extra_delta) + inner_conditions + prev_coin_id + (my_id_cat_truth Truths) + next_coin_id + (cat_struct_truth Truths) + ) + ) + ) + + ; CAT TRUTHS struct is: ; CAT Truths is: ((Inner puzzle hash . (MOD hash . (MOD hash hash . TAIL hash))) . (my_id . (my_parent_info my_puzhash my_amount))) + ; create truths - this_coin_info verified true because we calculated my ID from it! + ; lineage proof is verified later by cat parent check or tail_program + + (defun stager ( + cat_mod_struct + inner_conditions + lineage_proof + inner_puzzle_hash + my_id + prev_coin_id + this_coin_info + next_coin_proof + prev_subtotal + extra_delta + ) + (c (list ASSERT_MY_COIN_ID my_id) (stager_two + (cat_truth_data_to_truth_struct + inner_puzzle_hash + cat_mod_struct + my_id + this_coin_info + ) + (find_and_strip_tail_info inner_conditions cat_mod_struct ()) + lineage_proof + prev_coin_id + this_coin_info + (coin_id_for_proof next_coin_proof cat_mod_struct) + prev_subtotal + extra_delta + )) + ) + + (stager + ;; calculate cat_mod_struct, inner_puzzle_hash, coin_id + (list MOD_HASH (sha256 ONE MOD_HASH) TAIL_PROGRAM_HASH) + (a INNER_PUZZLE inner_puzzle_solution) + lineage_proof + (sha256tree INNER_PUZZLE) + (calculate_coin_id (f this_coin_info) (f (r this_coin_info)) (f (r (r this_coin_info)))) + prev_coin_id ; ID + this_coin_info ; (parent_id puzzle_hash amount) + next_coin_proof ; (parent_id innerpuzhash amount) + prev_subtotal + extra_delta + ) +) diff --git a/chia/wallet/puzzles/cat_v2.clvm.hex b/chia/wallet/puzzles/cat_v2.clvm.hex new file mode 100644 index 0000000000..e4d7116163 --- /dev/null +++ b/chia/wallet/puzzles/cat_v2.clvm.hex @@ -0,0 +1 @@ +ff02ffff01ff02ff5effff04ff02ffff04ffff04ff05ffff04ffff0bff34ff0580ffff04ff0bff80808080ffff04ffff02ff17ff2f80ffff04ff5fffff04ffff02ff2effff04ff02ffff04ff17ff80808080ffff04ffff02ff2affff04ff02ffff04ff82027fffff04ff82057fffff04ff820b7fff808080808080ffff04ff81bfffff04ff82017fffff04ff8202ffffff04ff8205ffffff04ff820bffff80808080808080808080808080ffff04ffff01ffffffff3d46ff02ff333cffff0401ff01ff81cb02ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff7cffff0bff34ff2480ffff0bff7cffff0bff7cffff0bff34ff2c80ff0980ffff0bff7cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ffff02ffff03ff0bffff01ff02ffff03ffff09ffff02ff2effff04ff02ffff04ff13ff80808080ff820b9f80ffff01ff02ff56ffff04ff02ffff04ffff02ff13ffff04ff5fffff04ff17ffff04ff2fffff04ff81bfffff04ff82017fffff04ff1bff8080808080808080ffff04ff82017fff8080808080ffff01ff088080ff0180ffff01ff02ffff03ff17ffff01ff02ffff03ffff20ff81bf80ffff0182017fffff01ff088080ff0180ffff01ff088080ff018080ff0180ff04ffff04ff05ff2780ffff04ffff10ff0bff5780ff778080ffffff02ffff03ff05ffff01ff02ffff03ffff09ffff02ffff03ffff09ff11ff5880ffff0159ff8080ff0180ffff01818f80ffff01ff02ff26ffff04ff02ffff04ff0dffff04ff0bffff04ffff04ff81b9ff82017980ff808080808080ffff01ff02ff7affff04ff02ffff04ffff02ffff03ffff09ff11ff5880ffff01ff04ff58ffff04ffff02ff76ffff04ff02ffff04ff13ffff04ff29ffff04ffff0bff34ff5b80ffff04ff2bff80808080808080ff398080ffff01ff02ffff03ffff09ff11ff7880ffff01ff02ffff03ffff20ffff02ffff03ffff09ffff0121ffff0dff298080ffff01ff02ffff03ffff09ffff0cff29ff80ff3480ff5c80ffff01ff0101ff8080ff0180ff8080ff018080ffff0109ffff01ff088080ff0180ffff010980ff018080ff0180ffff04ffff02ffff03ffff09ff11ff5880ffff0159ff8080ff0180ffff04ffff02ff26ffff04ff02ffff04ff0dffff04ff0bffff04ff17ff808080808080ff80808080808080ff0180ffff01ff04ff80ffff04ff80ff17808080ff0180ffff02ffff03ff05ffff01ff04ff09ffff02ff56ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff0bff7cffff0bff34ff2880ffff0bff7cffff0bff7cffff0bff34ff2c80ff0580ffff0bff7cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff04ffff04ff30ffff04ff5fff808080ffff02ff7effff04ff02ffff04ffff04ffff04ff2fff0580ffff04ff5fff82017f8080ffff04ffff02ff26ffff04ff02ffff04ff0bffff04ff05ffff01ff808080808080ffff04ff17ffff04ff81bfffff04ff82017fffff04ffff02ff2affff04ff02ffff04ff8204ffffff04ffff02ff76ffff04ff02ffff04ff09ffff04ff820affffff04ffff0bff34ff2d80ffff04ff15ff80808080808080ffff04ff8216ffff808080808080ffff04ff8205ffffff04ff820bffff808080808080808080808080ff02ff5affff04ff02ffff04ff5fffff04ff3bffff04ffff02ffff03ff17ffff01ff09ff2dffff02ff2affff04ff02ffff04ff27ffff04ffff02ff76ffff04ff02ffff04ff29ffff04ff57ffff04ffff0bff34ff81b980ffff04ff59ff80808080808080ffff04ff81b7ff80808080808080ff8080ff0180ffff04ff17ffff04ff05ffff04ff8202ffffff04ffff04ffff04ff78ffff04ffff0eff5cffff02ff2effff04ff02ffff04ffff04ff2fffff04ff82017fff808080ff8080808080ff808080ffff04ffff04ff20ffff04ffff0bff81bfff5cffff02ff2effff04ff02ffff04ffff04ff15ffff04ffff10ff82017fffff11ff8202dfff2b80ff8202ff80ff808080ff8080808080ff808080ff138080ff80808080808080808080ff018080 \ No newline at end of file diff --git a/chia/wallet/puzzles/cat_v2.clvm.hex.sha256tree b/chia/wallet/puzzles/cat_v2.clvm.hex.sha256tree new file mode 100644 index 0000000000..d572827bdd --- /dev/null +++ b/chia/wallet/puzzles/cat_v2.clvm.hex.sha256tree @@ -0,0 +1 @@ +37bef360ee858133b69d595a906dc45d01af50379dad515eb9518abb7c1d2a7a diff --git a/tests/clvm/test_clvm_compilation.py b/tests/clvm/test_clvm_compilation.py index 98f89fcf0a..00b385ae3e 100644 --- a/tests/clvm/test_clvm_compilation.py +++ b/tests/clvm/test_clvm_compilation.py @@ -8,7 +8,7 @@ from chia.types.blockchain_format.program import Program, SerializedProgram wallet_program_files = set( [ "chia/wallet/puzzles/calculate_synthetic_public_key.clvm", - "chia/wallet/puzzles/cat.clvm", + "chia/wallet/puzzles/cat_v2.clvm", "chia/wallet/puzzles/chialisp_deserialisation.clvm", "chia/wallet/puzzles/rom_bootstrap_generator.clvm", "chia/wallet/puzzles/generator_for_single_coin.clvm", diff --git a/tests/wallet/cat_wallet/test_cat_outer_puzzle.py b/tests/wallet/cat_wallet/test_cat_outer_puzzle.py index 4e76572a6d..94363b2653 100644 --- a/tests/wallet/cat_wallet/test_cat_outer_puzzle.py +++ b/tests/wallet/cat_wallet/test_cat_outer_puzzle.py @@ -62,5 +62,10 @@ def test_cat_outer_puzzle() -> None: ACS, inner_solution, ) - double_cat_puzzle.run(solution) + try: + double_cat_puzzle.run(solution) + except Exception as e: + assert e is not None # this should be failing + else: + assert False assert get_inner_solution(cat_driver, solution) == inner_solution From 123ba6389ab17509c8fb8a6170bb0822bb2d184e Mon Sep 17 00:00:00 2001 From: wallentx Date: Fri, 8 Jul 2022 15:58:22 -0500 Subject: [PATCH 04/38] updating .gitmodule to point to defender-gui --- .gitmodules | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitmodules b/.gitmodules index 0f84cbc8b0..a68862c8b9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,8 +1,8 @@ -[submodule "chia-blockchain-gui"] - path = chia-blockchain-gui - url = https://github.com/Chia-Network/chia-blockchain-gui.git - branch = pools [submodule "mozilla-ca"] path = mozilla-ca url = https://github.com/Chia-Network/mozilla-ca.git branch = main +[submodule "chia-blockchain-gui"] + path = chia-blockchain-gui + url = https://github.com/Chia-Network/defender-gui + branch = release/1.5.0 From 38124507d049da3f3b3b9a69c13a37cbe23536c4 Mon Sep 17 00:00:00 2001 From: Chris Marslender Date: Tue, 5 Jul 2022 16:45:43 -0500 Subject: [PATCH 05/38] Remove break (its preventing other sockets from getting data when earlier ones have an error) (#12241) --- chia/daemon/server.py | 1 - 1 file changed, 1 deletion(-) diff --git a/chia/daemon/server.py b/chia/daemon/server.py index cf98d93694..401bfe8961 100644 --- a/chia/daemon/server.py +++ b/chia/daemon/server.py @@ -244,7 +244,6 @@ class WebSocketServer: self.log.error(f"Unexpected exception trying to send to websocket: {e} {tb}") self.remove_connection(socket) await socket.close() - break else: service_name = "Unknown" if ws in self.remote_address_map: From 6fff876a7793188146ce80fef646569de2a1733d Mon Sep 17 00:00:00 2001 From: Jack Nelson Date: Thu, 7 Jul 2022 16:35:49 -0400 Subject: [PATCH 06/38] Convert DID Wallet to use the new coin selection algorithm that the normal wallet and the CAT wallet already use (#12063) * small type change * use coin_selection.py with DID Wallet use more efficient coin selection methods. * Add special DID edgecase + fix int type --- chia/rpc/wallet_rpc_api.py | 2 +- chia/wallet/did_wallet/did_wallet.py | 66 +++++++++++++--------------- chia/wallet/wallet.py | 2 +- 3 files changed, 32 insertions(+), 38 deletions(-) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index a4d8f9bd8d..d343bf6a71 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -1189,7 +1189,7 @@ class WalletRpcApi: wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id] my_did: str = encode_puzzle_hash(bytes32.fromhex(wallet.get_my_DID()), DID_HRP) async with self.service.wallet_state_manager.lock: - coins = await wallet.select_coins(1) + coins = await wallet.select_coins(uint64(1)) if coins is None or coins == set(): return {"success": True, "wallet_id": wallet_id, "my_did": my_did} else: diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index fbb46e09a1..54ce636a3f 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -29,6 +29,7 @@ from chia.wallet.wallet_info import WalletInfo from chia.wallet.derivation_record import DerivationRecord from chia.wallet.did_wallet import did_wallet_puzzles from chia.wallet.derive_keys import master_sk_to_wallet_sk_unhardened +from chia.wallet.coin_selection import select_coins from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( puzzle_for_pk, DEFAULT_HIDDEN_PUZZLE_HASH, @@ -297,50 +298,43 @@ class DIDWallet: async def get_unconfirmed_balance(self, record_list=None) -> uint128: return await self.wallet_state_manager.get_unconfirmed_balance(self.id(), record_list) - async def select_coins(self, amount, exclude: List[Coin] = None) -> Optional[Set[Coin]]: - """Returns a set of coins that can be used for generating a new transaction.""" - if exclude is None: - exclude = [] + async def select_coins( + self, amount: uint64, exclude: Optional[List[Coin]] = None, min_coin_amount: Optional[uint128] = None + ) -> Optional[Set[Coin]]: + """ + Returns a set of coins that can be used for generating a new transaction. + Note: Must be called under wallet state manager lock + """ - spendable_amount = await self.get_spendable_balance() + spendable_amount: uint128 = await self.get_spendable_balance() + + # Only DID Wallet will return none when this happens, so we do it before select_coins would throw an error. if amount > spendable_amount: self.log.warning(f"Can't select {amount}, from spendable {spendable_amount} for wallet id {self.id()}") return None - self.log.info(f"About to select coins for amount {amount}") - unspent: List[WalletCoinRecord] = list( + spendable_coins: List[WalletCoinRecord] = list( await self.wallet_state_manager.get_spendable_coins_for_wallet(self.wallet_info.id) ) - sum_value = 0 - used_coins: Set = set() - - # Use older coins first - unspent.sort(key=lambda r: r.confirmed_block_height) # Try to use coins from the store, if there isn't enough of "unused" # coins use change coins that are not confirmed yet unconfirmed_removals: Dict[bytes32, Coin] = await self.wallet_state_manager.unconfirmed_removals_for_wallet( self.wallet_info.id ) - for coinrecord in unspent: - if sum_value >= amount and len(used_coins) > 0: - break - if coinrecord.coin.name() in unconfirmed_removals: - continue - if coinrecord.coin in exclude: - continue - sum_value += coinrecord.coin.amount - used_coins.add(coinrecord.coin) - # This happens when we couldn't use one of the coins because it's already used - # but unconfirmed, and we are waiting for the change. (unconfirmed_additions) - if sum_value < amount: - raise ValueError( - "Can't make this transaction at the moment. Waiting for the change from the previous transaction." - ) - - self.log.info(f"Successfully selected coins: {used_coins}") - return used_coins + coins = await select_coins( + spendable_amount, + self.wallet_state_manager.constants.MAX_COIN_AMOUNT, + spendable_coins, + unconfirmed_removals, + self.log, + uint128(amount), + exclude, + min_coin_amount, + ) + assert sum(c.amount for c in coins) >= amount + return coins # This will be used in the recovery case where we don't have the parent info already async def coin_added(self, coin: Coin, _: uint32): @@ -539,7 +533,7 @@ class DIDWallet: async def create_update_spend(self, fee: uint64 = uint64(0)): assert self.did_info.current_inner is not None assert self.did_info.origin_coin is not None - coins = await self.select_coins(1) + coins = await self.select_coins(uint64(1)) assert coins is not None coin = coins.pop() new_puzhash = await self.get_new_did_inner_hash() @@ -614,7 +608,7 @@ class DIDWallet: """ assert self.did_info.current_inner is not None assert self.did_info.origin_coin is not None - coins = await self.select_coins(1) + coins = await self.select_coins(uint64(1)) assert coins is not None coin = coins.pop() backup_ids = [] @@ -706,7 +700,7 @@ class DIDWallet: ): assert self.did_info.current_inner is not None assert self.did_info.origin_coin is not None - coins = await self.select_coins(1) + coins = await self.select_coins(uint64(1)) assert coins is not None coin = coins.pop() innerpuz: Program = self.did_info.current_inner @@ -748,7 +742,7 @@ class DIDWallet: async def create_exit_spend(self, puzhash: bytes32): assert self.did_info.current_inner is not None assert self.did_info.origin_coin is not None - coins = await self.select_coins(1) + coins = await self.select_coins(uint64(1)) assert coins is not None coin = coins.pop() message_puz = Program.to((1, [[51, puzhash, coin.amount - 1, [puzhash]], [51, 0x00, -113]])) @@ -814,7 +808,7 @@ class DIDWallet: """ assert self.did_info.current_inner is not None assert self.did_info.origin_coin is not None - coins = await self.select_coins(1) + coins = await self.select_coins(uint64(1)) assert coins is not None and coins != set() coin = coins.pop() message = did_wallet_puzzles.create_recovery_message_puzzle(recovering_coin_name, newpuz, pubkey) @@ -879,7 +873,7 @@ class DIDWallet: async def get_info_for_recovery(self) -> Optional[Tuple[bytes32, bytes32, uint64]]: assert self.did_info.current_inner is not None assert self.did_info.origin_coin is not None - coins = await self.select_coins(1) + coins = await self.select_coins(uint64(1)) if coins is not None: coin = coins.pop() parent = coin.parent_coin_info diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 5e9db29a84..d26190957c 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -247,7 +247,7 @@ class Wallet: return Program.to(python_program) async def select_coins( - self, amount: uint64, exclude: List[Coin] = None, min_coin_amount: Optional[uint128] = None + self, amount: uint64, exclude: Optional[List[Coin]] = None, min_coin_amount: Optional[uint128] = None ) -> Set[Coin]: """ Returns a set of coins that can be used for generating a new transaction. From e9915eb3278639a811ac355ec1a5c3c71cafc3db Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Fri, 8 Jul 2022 10:00:42 +0900 Subject: [PATCH 07/38] Ms.fix coin selection (#12261) * Fix coin selection bug * Fix properly * Fallback in cases of too many coins selected * Also check for num coins * Lint issues. * Add another test * No sorting, and faster knapsack * Lint fix * Remove comment and useless check * Lint line --- chia/wallet/coin_selection.py | 69 +++++++---- tests/wallet/test_coin_selection.py | 174 +++++++++++++++++++++++++++- 2 files changed, 215 insertions(+), 28 deletions(-) diff --git a/chia/wallet/coin_selection.py b/chia/wallet/coin_selection.py index 92d950acc7..2ce747de7d 100644 --- a/chia/wallet/coin_selection.py +++ b/chia/wallet/coin_selection.py @@ -78,28 +78,31 @@ async def select_coins( log.debug(f"Selected all smaller coins because they equate to an exact match of the target.: {smaller_coins}") return set(smaller_coins) elif smaller_coin_sum < amount: - smallest_coin = select_smallest_coin_over_target(len(smaller_coins), valid_spendable_coins) + smallest_coin: Optional[Coin] = select_smallest_coin_over_target(amount, valid_spendable_coins) + assert smallest_coin is not None # Since we know we have enough, there must be a larger coin log.debug(f"Selected closest greater coin: {smallest_coin.name()}") return {smallest_coin} elif smaller_coin_sum > amount: - coin_set = knapsack_coin_algorithm(smaller_coins, amount, max_coin_amount) + coin_set: Optional[Set[Coin]] = knapsack_coin_algorithm(smaller_coins, amount, max_coin_amount, max_num_coins) log.debug(f"Selected coins from knapsack algorithm: {coin_set}") if coin_set is None: - raise ValueError("Knapsack algorithm failed to find a solution.") - if len(coin_set) > max_num_coins: - coin = select_smallest_coin_over_target(len(smaller_coins), valid_spendable_coins) - if coin is None or coin.amount < amount: - raise ValueError( - f"Transaction of {amount} mojo would use more than " - f"{max_num_coins} coins. Try sending a smaller amount" - ) - coin_set = {coin} + coin_set = sum_largest_coins(amount, smaller_coins) + if coin_set is None or len(coin_set) > max_num_coins: + greater_coin = select_smallest_coin_over_target(amount, valid_spendable_coins) + if greater_coin is None: + raise ValueError( + f"Transaction of {amount} mojo would use more than " + f"{max_num_coins} coins. Try sending a smaller amount" + ) + coin_set = {greater_coin} return coin_set else: # if smaller_coin_sum == amount and len(smaller_coins) >= max_num_coins. - coin = select_smallest_coin_over_target(len(smaller_coins), valid_spendable_coins) - log.debug(f"Resorted to selecting smallest coin over target due to dust.: {coin}") - return {coin} + potential_large_coin: Optional[Coin] = select_smallest_coin_over_target(amount, valid_spendable_coins) + if potential_large_coin is None: + raise ValueError("Too many coins are required to make this transaction") + log.debug(f"Resorted to selecting smallest coin over target due to dust.: {potential_large_coin}") + return {potential_large_coin} # These algorithms were based off of the algorithms in: @@ -113,21 +116,22 @@ def check_for_exact_match(coin_list: List[Coin], target: uint64) -> Optional[Coi return None -# amount of coins smaller than target, followed by a list of all valid spendable coins sorted in descending order. -def select_smallest_coin_over_target(smaller_coin_amount: int, valid_spendable_coin_list: List[Coin]) -> Coin: - if smaller_coin_amount >= len(valid_spendable_coin_list): - raise ValueError("Unable to select coins for this transaction. Try sending a smaller amount") - if smaller_coin_amount > 0: # in case we only have bigger coins. - greater_coins = valid_spendable_coin_list[:-smaller_coin_amount] - else: - greater_coins = valid_spendable_coin_list - coin = greater_coins[len(greater_coins) - 1] # select the coin with the least value. - return coin +# amount of coins smaller than target, followed by a list of all valid spendable coins. +# Coins must be sorted in descending amount order. +def select_smallest_coin_over_target(target: uint128, sorted_coin_list: List[Coin]) -> Optional[Coin]: + if sorted_coin_list[0].amount < target: + return None + for coin in reversed(sorted_coin_list): + if coin.amount >= target: + return coin + assert False # Should never reach here # we use this to find the set of coins which have total value closest to the target, but at least the target. # IMPORTANT: The coins have to be sorted in descending order or else this function will not work. -def knapsack_coin_algorithm(smaller_coins: List[Coin], target: uint128, max_coin_amount: int) -> Optional[Set[Coin]]: +def knapsack_coin_algorithm( + smaller_coins: List[Coin], target: uint128, max_coin_amount: int, max_num_coins: int +) -> Optional[Set[Coin]]: best_set_sum = max_coin_amount best_set_of_coins: Optional[Set[Coin]] = None for i in range(1000): @@ -142,6 +146,8 @@ def knapsack_coin_algorithm(smaller_coins: List[Coin], target: uint128, max_coin # the second pass runs to finish the set if the first pass didn't finish the set. # this makes each trial random and increases the chance of getting a perfect set. if (n_pass == 0 and bool(random.getrandbits(1))) or (n_pass == 1 and coin not in selected_coins): + if len(selected_coins) > max_num_coins: + break selected_coins_sum += coin.amount selected_coins.add(coin) if selected_coins_sum == target: @@ -155,3 +161,16 @@ def knapsack_coin_algorithm(smaller_coins: List[Coin], target: uint128, max_coin selected_coins.remove(coin) n_pass += 1 return best_set_of_coins + + +# Adds up the largest coins in the list, resulting in the minimum number of selected coins. A solution +# is guaranteed if and only if the sum(coins) >= target. Coins must be sorted in descending amount order. +def sum_largest_coins(target: uint128, sorted_coins: List[Coin]) -> Optional[Set[Coin]]: + total_value = 0 + selected_coins: Set[Coin] = set() + for coin in sorted_coins: + total_value += coin.amount + selected_coins.add(coin) + if total_value >= target: + return selected_coins + return None diff --git a/tests/wallet/test_coin_selection.py b/tests/wallet/test_coin_selection.py index 71815ba252..db7fa5edc6 100644 --- a/tests/wallet/test_coin_selection.py +++ b/tests/wallet/test_coin_selection.py @@ -1,4 +1,5 @@ import logging +import time from random import randrange from typing import List, Set @@ -9,7 +10,13 @@ from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.hash import std_hash from chia.util.ints import uint32, uint64, uint128 -from chia.wallet.coin_selection import check_for_exact_match, knapsack_coin_algorithm, select_coins +from chia.wallet.coin_selection import ( + check_for_exact_match, + knapsack_coin_algorithm, + select_coins, + select_smallest_coin_over_target, + sum_largest_coins, +) from chia.wallet.util.wallet_types import WalletType from chia.wallet.wallet_coin_record import WalletCoinRecord @@ -37,7 +44,9 @@ class TestCoinSelection: amounts.sort(reverse=True) coin_list: List[Coin] = [Coin(a_hash, a_hash, uint64(100000000 * a)) for a in amounts] for i in range(tries): - knapsack = knapsack_coin_algorithm(coin_list, uint128(30000000000000), DEFAULT_CONSTANTS.MAX_COIN_AMOUNT) + knapsack = knapsack_coin_algorithm( + coin_list, uint128(30000000000000), DEFAULT_CONSTANTS.MAX_COIN_AMOUNT, 999999 + ) assert knapsack is not None assert sum([coin.amount for coin in knapsack]) >= 310000000 @@ -47,7 +56,7 @@ class TestCoinSelection: coin_list: List[Coin] = [Coin(a_hash, a_hash, uint64(a)) for a in coin_amounts] # coin_list = set([coin for a in coin_amounts]) for i in range(100): - knapsack = knapsack_coin_algorithm(coin_list, uint128(265), DEFAULT_CONSTANTS.MAX_COIN_AMOUNT) + knapsack = knapsack_coin_algorithm(coin_list, uint128(265), DEFAULT_CONSTANTS.MAX_COIN_AMOUNT, 99999) assert knapsack is not None selected_sum = sum(coin.amount for coin in list(knapsack)) assert 265 <= selected_sum <= 280 # Selects a set of coins which does exceed by too much @@ -98,6 +107,7 @@ class TestCoinSelection: ) # make sure coins are not identical. for target_amount in [10000, 9999]: + print("Target amount: ", target_amount) result: Set[Coin] = await select_coins( spendable_amount, DEFAULT_CONSTANTS.MAX_COIN_AMOUNT, @@ -132,6 +142,66 @@ class TestCoinSelection: assert coin.amount > 1 assert len(dusty_result) <= 500 + # test when we have multiple coins under target, and a lot of dust coins. + spendable_amount = uint128(25000 + 10000) + new_coin_list: List[WalletCoinRecord] = [] + for i in range(5): + new_coin_list.append( + WalletCoinRecord( + Coin(a_hash, std_hash(i), uint64(5000)), uint32(1), uint32(1), False, True, WalletType(0), 1 + ) + ) + + for i in range(10000): + new_coin_list.append( + WalletCoinRecord( + Coin(a_hash, std_hash(i), uint64(1)), uint32(1), uint32(1), False, True, WalletType(0), 1 + ) + ) + for target_amount in [20000, 15000, 10000, 5000]: # select the first 100 values + dusty_below_target: Set[Coin] = await select_coins( + spendable_amount, + DEFAULT_CONSTANTS.MAX_COIN_AMOUNT, + new_coin_list, + {}, + logging.getLogger("test"), + uint128(target_amount), + ) + assert dusty_below_target is not None + assert sum([coin.amount for coin in dusty_below_target]) >= target_amount + for coin in dusty_below_target: + assert coin.amount == 5000 + assert len(dusty_below_target) <= 500 + + @pytest.mark.asyncio + async def test_dust_and_one_large_coin(self, a_hash: bytes32) -> None: + # test when we have a lot of dust and 1 large coin + spendable_amount = uint128(50000 + 10000) + new_coin_list: List[WalletCoinRecord] = [ + WalletCoinRecord( + Coin(a_hash, std_hash(b"123"), uint64(50000)), uint32(1), uint32(1), False, True, WalletType(0), 1 + ) + ] + + for i in range(10000): + new_coin_list.append( + WalletCoinRecord( + Coin(a_hash, std_hash(i), uint64(1)), uint32(1), uint32(1), False, True, WalletType(0), 1 + ) + ) + for target_amount in [50000, 10001, 10000, 9999]: + dusty_below_target: Set[Coin] = await select_coins( + spendable_amount, + DEFAULT_CONSTANTS.MAX_COIN_AMOUNT, + new_coin_list, + {}, + logging.getLogger("test"), + uint128(target_amount), + ) + assert dusty_below_target is not None + assert sum([coin.amount for coin in dusty_below_target]) >= target_amount + assert len(dusty_below_target) <= 500 + @pytest.mark.asyncio async def test_coin_selection_failure(self, a_hash: bytes32) -> None: spendable_amount = uint128(10000) @@ -288,3 +358,101 @@ class TestCoinSelection: assert sum([coin.amount for coin in multiple_greater_result]) > target_amount assert sum([coin.amount for coin in multiple_greater_result]) == 90000 assert len(multiple_greater_result) == 1 + + @pytest.mark.asyncio + async def test_coin_selection_difficult(self, a_hash: bytes32) -> None: + num_coins = 40 + spendable_amount = uint128(num_coins * 1000) + coin_list: List[WalletCoinRecord] = [ + WalletCoinRecord( + Coin(a_hash, std_hash(i.to_bytes(4, "big")), uint64(1000)), + uint32(1), + uint32(1), + False, + True, + WalletType(0), + 1, + ) + for i in range(num_coins) + ] + target_amount = spendable_amount - 1 + result: Set[Coin] = await select_coins( + spendable_amount, + DEFAULT_CONSTANTS.MAX_COIN_AMOUNT, + coin_list, + {}, + logging.getLogger("test"), + uint128(target_amount), + ) + assert result is not None + print(result) + print(sum([c.amount for c in result])) + assert sum([coin.amount for coin in result]) >= target_amount + + @pytest.mark.asyncio + async def test_smallest_coin_over_amount(self, a_hash: bytes32) -> None: + coin_list: List[Coin] = [ + Coin(a_hash, std_hash(i.to_bytes(4, "big")), uint64((39 - i) * 1000)) for i in range(40) + ] + assert select_smallest_coin_over_target(uint128(100), coin_list) == coin_list[39 - 1] + assert select_smallest_coin_over_target(uint128(1000), coin_list) == coin_list[39 - 1] + assert select_smallest_coin_over_target(uint128(1001), coin_list) == coin_list[39 - 2] + assert select_smallest_coin_over_target(uint128(37000), coin_list) == coin_list[39 - 37] + assert select_smallest_coin_over_target(uint128(39000), coin_list) == coin_list[39 - 39] + assert select_smallest_coin_over_target(uint128(39001), coin_list) is None + + @pytest.mark.asyncio + async def test_sum_largest_coins(self, a_hash: bytes32) -> None: + coin_list: List[Coin] = list( + reversed([Coin(a_hash, std_hash(i.to_bytes(4, "big")), uint64(i)) for i in range(41)]) + ) + assert sum_largest_coins(uint128(40), coin_list) == {coin_list[0]} + assert sum_largest_coins(uint128(79), coin_list) == {coin_list[0], coin_list[1]} + assert sum_largest_coins(uint128(40000), coin_list) is None + + @pytest.mark.asyncio + async def test_knapsack_perf(self, a_hash: bytes32) -> None: + start = time.time() + coin_list: List[Coin] = [ + Coin(a_hash, std_hash(i.to_bytes(4, "big")), uint64((200000 - i) * 1000)) for i in range(200000) + ] + knapsack_coin_algorithm(coin_list, uint128(2000000), 9999999999999999, 500) + + # Just a sanity check, it's actually much faster than this time + assert time.time() - start < 10000 + + @pytest.mark.asyncio + async def test_coin_selection_min_coin(self, a_hash: bytes32) -> None: + spendable_amount = uint128(5000000 + 500 + 40050) + coin_list: List[WalletCoinRecord] = [ + WalletCoinRecord(Coin(a_hash, a_hash, uint64(5000000)), uint32(1), uint32(1), False, True, WalletType(0), 1) + ] + for i in range(500): + coin_list.append( + WalletCoinRecord( + Coin(a_hash, std_hash(i), uint64(1)), uint32(1), uint32(1), False, True, WalletType(0), 1 + ) + ) + for i in range(1, 90): + coin_list.append( + WalletCoinRecord( + Coin(a_hash, std_hash(i), uint64(i * 10)), uint32(1), uint32(1), False, True, WalletType(0), 1 + ) + ) + # make sure coins are not identical. + for target_amount in [500, 1000, 50000, 500000]: + for min_coin_amount in [10, 100, 200, 300, 1000]: + result: Set[Coin] = await select_coins( + spendable_amount, + DEFAULT_CONSTANTS.MAX_COIN_AMOUNT, + coin_list, + {}, + logging.getLogger("test"), + uint128(target_amount), + min_coin_amount=uint128(min_coin_amount), + ) + assert result is not None # this should never happen + assert sum(coin.amount for coin in result) >= target_amount + for coin in result: + assert not coin.amount < min_coin_amount + assert len(result) <= 500 From 3adae8d9b0b09a70dcfb2bf203ad2e822c65c1ed Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Fri, 8 Jul 2022 13:42:17 +0900 Subject: [PATCH 08/38] Tx submission idempotance, and prioritize wallet (#12282) * Tx submission idempotance, and prioritize wallet * TODO comment --- chia/full_node/full_node.py | 5 ++++- chia/full_node/full_node_api.py | 12 ++++++++---- tests/core/full_node/test_full_node.py | 11 ++++++++--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/chia/full_node/full_node.py b/chia/full_node/full_node.py index 489f7bd6ae..0db693d2d4 100644 --- a/chia/full_node/full_node.py +++ b/chia/full_node/full_node.py @@ -2060,6 +2060,9 @@ class FullNode: if not test and not (await self.synced()): return MempoolInclusionStatus.FAILED, Err.NO_TRANSACTIONS_WHILE_SYNCING + if self.mempool_manager.get_spendbundle(spend_name) is not None: + self.mempool_manager.remove_seen(spend_name) + return MempoolInclusionStatus.SUCCESS, None if self.mempool_manager.seen(spend_name): return MempoolInclusionStatus.FAILED, Err.ALREADY_INCLUDING_TRANSACTION self.mempool_manager.add_and_maybe_pop_seen(spend_name) @@ -2081,7 +2084,7 @@ class FullNode: async with self._blockchain_lock_low_priority: if self.mempool_manager.get_spendbundle(spend_name) is not None: self.mempool_manager.remove_seen(spend_name) - return MempoolInclusionStatus.FAILED, Err.ALREADY_INCLUDING_TRANSACTION + return MempoolInclusionStatus.SUCCESS, None cost, status, error = await self.mempool_manager.add_spendbundle(transaction, cost_result, spend_name) if status == MempoolInclusionStatus.SUCCESS: self.log.debug( diff --git a/chia/full_node/full_node_api.py b/chia/full_node/full_node_api.py index 8ec145c317..963f63c3a2 100644 --- a/chia/full_node/full_node_api.py +++ b/chia/full_node/full_node_api.py @@ -247,9 +247,9 @@ class FullNodeAPI: if self.full_node.transaction_queue.full(): self.full_node.dropped_tx.add(spend_name) return None - # Higher fee means priority is a smaller number, which means it will be handled earlier + # TODO: Use fee in priority calculation, to prioritize high fee TXs await self.full_node.transaction_queue.put( - (0, TransactionQueueEntry(tx.transaction, tx_bytes, spend_name, peer, test)) + (1, TransactionQueueEntry(tx.transaction, tx_bytes, spend_name, peer, test)) ) return None @@ -1242,6 +1242,11 @@ class FullNodeAPI: @api_request async def send_transaction(self, request: wallet_protocol.SendTransaction, *, test=False) -> Optional[Message]: spend_name = request.transaction.name() + if self.full_node.mempool_manager.get_spendbundle(spend_name) is not None: + self.full_node.mempool_manager.remove_seen(spend_name) + response = wallet_protocol.TransactionAck(spend_name, uint8(MempoolInclusionStatus.SUCCESS), None) + return make_msg(ProtocolMessageTypes.transaction_ack, response) + await self.full_node.transaction_queue.put( (0, TransactionQueueEntry(request.transaction, None, spend_name, None, test)) ) @@ -1271,8 +1276,7 @@ class FullNodeAPI: ) else: response = wallet_protocol.TransactionAck(spend_name, uint8(status.value), error_name) - msg = make_msg(ProtocolMessageTypes.transaction_ack, response) - return msg + return make_msg(ProtocolMessageTypes.transaction_ack, response) @api_request async def request_puzzle_solution(self, request: wallet_protocol.RequestPuzzleSolution) -> Optional[Message]: diff --git a/tests/core/full_node/test_full_node.py b/tests/core/full_node/test_full_node.py index 186fff3ce9..8b50f96638 100644 --- a/tests/core/full_node/test_full_node.py +++ b/tests/core/full_node/test_full_node.py @@ -17,6 +17,7 @@ from chia.protocols import full_node_protocol as fnp, full_node_protocol, wallet from chia.protocols import timelord_protocol from chia.protocols.full_node_protocol import RespondTransaction from chia.protocols.protocol_message_types import ProtocolMessageTypes +from chia.protocols.wallet_protocol import SendTransaction, TransactionAck from chia.server.address_manager import AddressManager from chia.server.outbound_message import Message from chia.simulator.simulator_protocol import FarmNewBlockProtocol @@ -908,12 +909,16 @@ class TestFullNodeProtocol: await time_out_assert(10, new_transaction_not_requested, True, incoming_queue, new_transaction) - # Cannot resubmit transaction + # Idempotence in resubmission status, err = await full_node_1.full_node.respond_transaction( successful_bundle, successful_bundle.name(), peer, test=True ) - assert status == MempoolInclusionStatus.FAILED - assert err == Err.ALREADY_INCLUDING_TRANSACTION + assert status == MempoolInclusionStatus.SUCCESS + assert err is None + + # Resubmission through wallet is also fine + response_msg = await full_node_1.send_transaction(SendTransaction(successful_bundle), test=True) + assert TransactionAck.from_bytes(response_msg.data).status == MempoolInclusionStatus.SUCCESS.value # Farm one block to clear mempool await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(receiver_puzzlehash)) From 3e4ee359a13e3937e4e8d5c1ec80d7140bfa54b8 Mon Sep 17 00:00:00 2001 From: wallentx Date: Sat, 9 Jul 2022 00:38:59 -0500 Subject: [PATCH 09/38] Updating gui modules --- .gitmodules | 2 +- chia-blockchain-gui | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index a68862c8b9..3f8ad5b5fd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,7 +2,7 @@ path = mozilla-ca url = https://github.com/Chia-Network/mozilla-ca.git branch = main -[submodule "chia-blockchain-gui"] +[submodule "defender-gui"] path = chia-blockchain-gui url = https://github.com/Chia-Network/defender-gui branch = release/1.5.0 diff --git a/chia-blockchain-gui b/chia-blockchain-gui index f62c216cce..9a46244aa3 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit f62c216cce6b89ef23a1b2b0a2dd6fc2a439649a +Subproject commit 9a46244aa3c7d438eb10ae70ad749fcd13dd807c From 48f53fcbea048f7366a16f1f57b0e2dd4a173495 Mon Sep 17 00:00:00 2001 From: Jack Nelson Date: Thu, 7 Jul 2022 01:17:26 -0400 Subject: [PATCH 10/38] extend min_coin to rpc calls & cli for coin selection (#12274) * add tests to test if min_coin is working they are passing, but no harm in being safe * expand min coin amount across wallet.py * extend to trade_manager * add new options to rpc's almost done lol. * add min_coin_amount to wallet send * make param non optional alleviate None errors * add cat wallet changes, rpc and all + fix a bug i accidentally made oops --- chia/cmds/wallet.py | 19 ++++++++++++++- chia/cmds/wallet_funcs.py | 14 ++++++++--- chia/rpc/wallet_rpc_api.py | 36 ++++++++++++++-------------- chia/rpc/wallet_rpc_client.py | 31 ++++++++++++++++++++---- chia/wallet/cat_wallet/cat_wallet.py | 25 +++++++++++++------ chia/wallet/nft_wallet/nft_wallet.py | 6 +++-- chia/wallet/trade_manager.py | 13 +++++----- chia/wallet/wallet.py | 11 ++++++--- 8 files changed, 110 insertions(+), 45 deletions(-) diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py index 83f7bc210f..0a85bc4061 100644 --- a/chia/cmds/wallet.py +++ b/chia/cmds/wallet.py @@ -146,6 +146,14 @@ def get_transactions_cmd( @click.option( "-o", "--override", help="Submits transaction without checking for unusual values", is_flag=True, default=False ) +@click.option( + "-ma", + "--min_coin_amount", + help="Ignore coins worth less then this much XCH or CAT units", + type=str, + required=False, + default="0", +) def send_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -155,8 +163,17 @@ def send_cmd( fee: str, address: str, override: bool, + min_coin_amount: str, ) -> None: - extra_params = {"id": id, "amount": amount, "memo": memo, "fee": fee, "address": address, "override": override} + extra_params = { + "id": id, + "amount": amount, + "memo": memo, + "fee": fee, + "address": address, + "override": override, + "min_coin_amount": min_coin_amount, + } import asyncio from .wallet_funcs import execute_with_wallet, send diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 02e4367ef8..88dac2bd8a 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -18,7 +18,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.bech32m import bech32_decode, decode_puzzle_hash, encode_puzzle_hash from chia.util.config import load_config from chia.util.default_root import DEFAULT_ROOT_PATH -from chia.util.ints import uint16, uint32, uint64 +from chia.util.ints import uint16, uint32, uint64, uint128 from chia.wallet.did_wallet.did_info import DID_HRP from chia.wallet.nft_wallet.nft_info import NFT_HRP, NFTInfo from chia.wallet.trade_record import TradeRecord @@ -191,6 +191,7 @@ async def send(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> fee = Decimal(args["fee"]) address = args["address"] override = args["override"] + min_coin_amount = Decimal(args["min_coin_amount"]) memo = args["memo"] if memo is None: memos = None @@ -212,14 +213,21 @@ async def send(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> final_fee = uint64(int(fee * units["chia"])) final_amount: uint64 + final_min_coin_amount: uint128 if typ == WalletType.STANDARD_WALLET: final_amount = uint64(int(amount * units["chia"])) + final_min_coin_amount = uint128(int(min_coin_amount * units["chia"])) print("Submitting transaction...") - res = await wallet_client.send_transaction(str(wallet_id), final_amount, address, final_fee, memos) + res = await wallet_client.send_transaction( + str(wallet_id), final_amount, address, final_fee, memos, final_min_coin_amount + ) elif typ == WalletType.CAT: final_amount = uint64(int(amount * units["cat"])) + final_min_coin_amount = uint128(int(min_coin_amount * units["cat"])) print("Submitting transaction...") - res = await wallet_client.cat_spend(str(wallet_id), final_amount, address, final_fee, memos) + res = await wallet_client.cat_spend( + str(wallet_id), final_amount, address, final_fee, memos, final_min_coin_amount + ) else: print("Only standard wallet and CAT wallets are supported") return diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index d343bf6a71..3572f0aaab 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -23,7 +23,7 @@ from chia.types.spend_bundle import SpendBundle from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash from chia.util.byte_types import hexstr_to_bytes from chia.util.config import load_config -from chia.util.ints import uint8, uint32, uint64, uint16 +from chia.util.ints import uint8, uint32, uint64, uint16, uint128 from chia.util.keychain import KeyringIsLocked, bytes_to_mnemonic, generate_mnemonic from chia.util.path import path_from_root from chia.util.ws_message import WsRpcMessage, create_payload_dict @@ -830,12 +830,12 @@ class WalletRpcApi: if "memos" in request: memos = [mem.encode("utf-8") for mem in request["memos"]] - if "fee" in request: - fee = uint64(request["fee"]) - else: - fee = uint64(0) + fee: uint64 = uint64(request.get("fee", 0)) + min_coin_amount: uint128 = uint128(request.get("min_coin_amount", 0)) async with self.service.wallet_state_manager.lock: - tx: TransactionRecord = await wallet.generate_signed_transaction(amount, puzzle_hash, fee, memos=memos) + tx: TransactionRecord = await wallet.generate_signed_transaction( + amount, puzzle_hash, fee, memos=memos, min_coin_amount=min_coin_amount + ) await wallet.push_transaction(tx) # Transaction may not have been included in the mempool yet. Use get_transaction to check. @@ -941,13 +941,11 @@ class WalletRpcApi: if not isinstance(request["amount"], int) or not isinstance(request["fee"], int): raise ValueError("An integer amount or fee is required (too many decimals)") amount: uint64 = uint64(request["amount"]) - if "fee" in request: - fee = uint64(request["fee"]) - else: - fee = uint64(0) + fee: uint64 = uint64(request.get("fee", 0)) + min_coin_amount: uint128 = uint128(request.get("min_coin_amount", 0)) async with self.service.wallet_state_manager.lock: - txs: TransactionRecord = await wallet.generate_signed_transaction( - [amount], [puzzle_hash], fee, memos=[memos] + txs: List[TransactionRecord] = await wallet.generate_signed_transaction( + [amount], [puzzle_hash], fee, memos=[memos], min_coin_amount=min_coin_amount ) for tx in txs: await wallet.standard_wallet.push_transaction(tx) @@ -982,6 +980,7 @@ class WalletRpcApi: fee: uint64 = uint64(request.get("fee", 0)) validate_only: bool = request.get("validate_only", False) driver_dict_str: Optional[Dict[str, Any]] = request.get("driver_dict", None) + min_coin_amount: uint128 = uint128(request.get("min_coin_amount", 0)) # This driver_dict construction is to maintain backward compatibility where everything is assumed to be a CAT driver_dict: Dict[bytes32, PuzzleInfo] = {} @@ -1011,8 +1010,7 @@ class WalletRpcApi: trade_record, error, ) = await self.service.wallet_state_manager.trade_manager.create_offer_for_ids( - modified_offer, driver_dict, fee=fee, validate_only=validate_only - ) + modified_offer, driver_dict, fee=fee, validate_only=validate_only, min_coin_amount=min_coin_amount) if success: return { "offer": Offer.from_bytes(trade_record.offer).to_bech32(), @@ -1040,13 +1038,14 @@ class WalletRpcApi: offer_hex: str = request["offer"] offer = Offer.from_bech32(offer_hex) fee: uint64 = uint64(request.get("fee", 0)) + min_coin_amount: uint128 = uint128(request.get("min_coin_amount", 0)) async with self.service.wallet_state_manager.lock: ( success, trade_record, error, - ) = await self.service.wallet_state_manager.trade_manager.respond_to_offer(offer, fee=fee) + ) = await self.service.wallet_state_manager.trade_manager.respond_to_offer(offer, fee=fee, min_coin_amount=min_coin_amount) if not success: raise ValueError(error) return {"trade_record": trade_record.to_json_dict_convenience()} @@ -1749,9 +1748,8 @@ class WalletRpcApi: memos = [] if "memos" not in addition else [mem.encode("utf-8") for mem in addition["memos"]] additional_outputs.append({"puzzlehash": receiver_ph, "amount": amount, "memos": memos}) - fee = uint64(0) - if "fee" in request: - fee = uint64(request["fee"]) + fee: uint64 = uint64(request.get("fee", 0)) + min_coin_amount: uint128 = uint128(request.get("min_coin_amount", 0)) coins = None if "coins" in request and len(request["coins"]) > 0: @@ -1803,6 +1801,7 @@ class WalletRpcApi: memos=memos_0, coin_announcements_to_consume=coin_announcements, puzzle_announcements_to_consume=puzzle_announcements, + min_coin_amount=min_coin_amount, ) else: signed_tx = await self.service.wallet_state_manager.main_wallet.generate_signed_transaction( @@ -1815,6 +1814,7 @@ class WalletRpcApi: memos=memos_0, coin_announcements_to_consume=coin_announcements, puzzle_announcements_to_consume=puzzle_announcements, + min_coin_amount=min_coin_amount, ) return {"signed_tx": signed_tx.to_json_dict_convenience(self.service.config)} diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 732e7ac4f6..12d09996dd 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -5,7 +5,7 @@ from chia.rpc.rpc_client import RpcClient from chia.types.announcement import Announcement from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.util.ints import uint32, uint64 +from chia.util.ints import uint32, uint64, uint128 from chia.wallet.trade_record import TradeRecord from chia.wallet.trading.offer import Offer from chia.wallet.transaction_record import TransactionRecord @@ -140,10 +140,22 @@ class WalletRpcClient(RpcClient): return (await self.fetch("get_next_address", {"wallet_id": wallet_id, "new_address": new_address}))["address"] async def send_transaction( - self, wallet_id: str, amount: uint64, address: str, fee: uint64 = uint64(0), memos: Optional[List[str]] = None + self, + wallet_id: str, + amount: uint64, + address: str, + fee: uint64 = uint64(0), + memos: Optional[List[str]] = None, + min_coin_amount: uint128 = uint128(0), ) -> TransactionRecord: if memos is None: - send_dict: Dict = {"wallet_id": wallet_id, "amount": amount, "address": address, "fee": fee} + send_dict: Dict = { + "wallet_id": wallet_id, + "amount": amount, + "address": address, + "fee": fee, + "min_coin_amount": min_coin_amount, + } else: send_dict = { "wallet_id": wallet_id, @@ -151,6 +163,7 @@ class WalletRpcClient(RpcClient): "address": address, "fee": fee, "memos": memos, + "min_coin_amount": min_coin_amount, } res = await self.fetch("send_transaction", send_dict) return TransactionRecord.from_json_dict_convenience(res["transaction"]) @@ -194,6 +207,7 @@ class WalletRpcClient(RpcClient): fee: uint64 = uint64(0), coin_announcements: Optional[List[Announcement]] = None, puzzle_announcements: Optional[List[Announcement]] = None, + min_coin_amount: uint128 = uint128(0), ) -> TransactionRecord: # Converts bytes to hex for puzzle hashes additions_hex = [] @@ -205,6 +219,7 @@ class WalletRpcClient(RpcClient): request: Dict[str, Any] = { "additions": additions_hex, "fee": fee, + "min_coin_amount": min_coin_amount, } if coin_announcements is not None and len(coin_announcements) > 0: @@ -484,6 +499,7 @@ class WalletRpcClient(RpcClient): inner_address: str, fee: uint64 = uint64(0), memos: Optional[List[str]] = None, + min_coin_amount: uint128 = uint128(0), ) -> TransactionRecord: send_dict = { "wallet_id": wallet_id, @@ -491,6 +507,7 @@ class WalletRpcClient(RpcClient): "inner_address": inner_address, "fee": fee, "memos": memos if memos else [], + "min_coin_amount": min_coin_amount, } res = await self.fetch("cat_spend", send_dict) return TransactionRecord.from_json_dict_convenience(res["transaction"]) @@ -502,6 +519,7 @@ class WalletRpcClient(RpcClient): driver_dict: Dict[str, Any] = None, fee=uint64(0), validate_only: bool = False, + min_coin_amount: uint128 = uint128(0), ) -> Tuple[Optional[Offer], TradeRecord]: send_dict: Dict[str, int] = {} for key in offer_dict: @@ -511,6 +529,7 @@ class WalletRpcClient(RpcClient): "offer": send_dict, "validate_only": validate_only, "fee": fee, + "min_coin_amount": min_coin_amount, } if driver_dict is not None: req["driver_dict"] = driver_dict @@ -527,8 +546,10 @@ class WalletRpcClient(RpcClient): res = await self.fetch("check_offer_validity", {"offer": offer.to_bech32()}) return res["valid"] - async def take_offer(self, offer: Offer, fee=uint64(0)) -> TradeRecord: - res = await self.fetch("take_offer", {"offer": offer.to_bech32(), "fee": fee}) + async def take_offer(self, offer: Offer, fee=uint64(0), min_coin_amount: uint128 = uint128(0)) -> TradeRecord: + res = await self.fetch( + "take_offer", {"offer": offer.to_bech32(), "fee": fee, "min_coin_amount": min_coin_amount} + ) return TradeRecord.from_json_dict_convenience(res["trade_record"]) async def get_offer(self, trade_id: bytes32, file_contents: bool = False) -> TradeRecord: diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index 0dce6230c9..f5f5f8c59f 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -532,6 +532,7 @@ class CATWallet: fee: uint64, amount_to_claim: uint64, announcement_to_assert: Optional[Announcement] = None, + min_coin_amount: Optional[uint128] = None, ) -> Tuple[TransactionRecord, Optional[Announcement]]: """ This function creates a non-CAT transaction to pay fees, contribute funds for issuance, and absorb melt value. @@ -540,7 +541,7 @@ class CATWallet: """ announcement = None if fee > amount_to_claim: - chia_coins = await self.standard_wallet.select_coins(fee) + chia_coins = await self.standard_wallet.select_coins(fee, min_coin_amount=min_coin_amount) origin_id = list(chia_coins)[0].name() chia_tx = await self.standard_wallet.generate_signed_transaction( uint64(0), @@ -564,7 +565,7 @@ class CATWallet: assert message is not None announcement = Announcement(origin_id, message) else: - chia_coins = await self.standard_wallet.select_coins(fee) + chia_coins = await self.standard_wallet.select_coins(fee, min_coin_amount=min_coin_amount) selected_amount = sum([c.amount for c in chia_coins]) chia_tx = await self.standard_wallet.generate_signed_transaction( uint64(selected_amount + amount_to_claim - fee), @@ -585,6 +586,7 @@ class CATWallet: coins: Set[Coin] = None, coin_announcements_to_consume: Optional[Set[Announcement]] = None, puzzle_announcements_to_consume: Optional[Set[Announcement]] = None, + min_coin_amount: Optional[uint128] = None, ) -> Tuple[SpendBundle, Optional[TransactionRecord]]: if coin_announcements_to_consume is not None: coin_announcements_bytes: Optional[Set[bytes32]] = {a.name() for a in coin_announcements_to_consume} @@ -604,7 +606,7 @@ class CATWallet: starting_amount: int = payment_amount - extra_delta if coins is None: - cat_coins = await self.select_coins(uint64(starting_amount)) + cat_coins = await self.select_coins(uint64(starting_amount), min_coin_amount=min_coin_amount) else: cat_coins = coins @@ -648,7 +650,10 @@ class CATWallet: if need_chia_transaction: if fee > regular_chia_to_claim: chia_tx, _ = await self.create_tandem_xch_tx( - fee, uint64(regular_chia_to_claim), announcement_to_assert=announcement + fee, + uint64(regular_chia_to_claim), + announcement_to_assert=announcement, + min_coin_amount=min_coin_amount, ) innersol = self.standard_wallet.make_solution( primaries=primaries, @@ -657,7 +662,9 @@ class CATWallet: puzzle_announcements_to_assert=puzzle_announcements_bytes, ) elif regular_chia_to_claim > fee: - chia_tx, _ = await self.create_tandem_xch_tx(fee, uint64(regular_chia_to_claim)) + chia_tx, _ = await self.create_tandem_xch_tx( + fee, uint64(regular_chia_to_claim), min_coin_amount=min_coin_amount + ) innersol = self.standard_wallet.make_solution( primaries=primaries, coin_announcements={announcement.message}, @@ -715,6 +722,7 @@ class CATWallet: memos: Optional[List[List[bytes]]] = None, coin_announcements_to_consume: Optional[Set[Announcement]] = None, puzzle_announcements_to_consume: Optional[Set[Announcement]] = None, + min_coin_amount: Optional[uint128] = None, ) -> List[TransactionRecord]: if memos is None: memos = [[] for _ in range(len(puzzle_hashes))] @@ -739,6 +747,7 @@ class CATWallet: coins=coins, coin_announcements_to_consume=coin_announcements_to_consume, puzzle_announcements_to_consume=puzzle_announcements_to_consume, + min_coin_amount=min_coin_amount, ) spend_bundle = await self.sign(unsigned_spend_bundle) # TODO add support for array in stored records @@ -818,8 +827,10 @@ class CATWallet: def get_puzzle_info(self, asset_id: bytes32) -> PuzzleInfo: return PuzzleInfo({"type": AssetType.CAT.value, "tail": "0x" + self.get_asset_id()}) - async def get_coins_to_offer(self, asset_id: Optional[bytes32], amount: uint64) -> Set[Coin]: + async def get_coins_to_offer( + self, asset_id: Optional[bytes32], amount: uint64, min_coin_amount: Optional[uint128] = None + ) -> Set[Coin]: balance = await self.get_confirmed_balance() if balance < amount: raise Exception(f"insufficient funds in wallet {self.id()}") - return await self.select_coins(amount) + return await self.select_coins(amount, min_coin_amount=min_coin_amount) diff --git a/chia/wallet/nft_wallet/nft_wallet.py b/chia/wallet/nft_wallet/nft_wallet.py index 0281f64b45..f86573482d 100644 --- a/chia/wallet/nft_wallet/nft_wallet.py +++ b/chia/wallet/nft_wallet/nft_wallet.py @@ -546,11 +546,13 @@ class NFTWallet: else: return puzzle_info - async def get_coins_to_offer(self, nft_id: bytes32, amount: uint64) -> Set[Coin]: + async def get_coins_to_offer( + self, nft_id: bytes32, amount: uint64, min_coin_amount: Optional[uint128] = None + ) -> Set[Coin]: nft_coin: Optional[NFTCoinInfo] = self.get_nft(nft_id) if nft_coin is None: raise ValueError("An asset ID was specified that this wallet doesn't track") - return set([nft_coin.coin]) + return {nft_coin.coin} def match_puzzle_info(self, puzzle_driver: PuzzleInfo) -> bool: return ( diff --git a/chia/wallet/trade_manager.py b/chia/wallet/trade_manager.py index e7a9913a7e..a9986150a8 100644 --- a/chia/wallet/trade_manager.py +++ b/chia/wallet/trade_manager.py @@ -11,7 +11,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.spend_bundle import SpendBundle from chia.util.db_wrapper import DBWrapper from chia.util.hash import std_hash -from chia.util.ints import uint32, uint64 +from chia.util.ints import uint32, uint64, uint128 from chia.wallet.nft_wallet.nft_wallet import NFTWallet from chia.wallet.outer_puzzles import AssetType from chia.wallet.payment import Payment @@ -297,13 +297,13 @@ class TradeManager: driver_dict: Optional[Dict[bytes32, PuzzleInfo]] = None, fee: uint64 = uint64(0), validate_only: bool = False, + min_coin_amount: Optional[uint128] = None, ) -> Tuple[bool, Optional[TradeRecord], Optional[str]]: if driver_dict is None: driver_dict = {} - success, created_offer, error = await self._create_offer_for_ids(offer, driver_dict, fee=fee) + success, created_offer, error = await self._create_offer_for_ids(offer, driver_dict, fee=fee, min_coin_amount=min_coin_amount) if not success or created_offer is None: raise Exception(f"Error creating offer: {error}") - now = uint64(int(time.time())) trade_offer: TradeRecord = TradeRecord( confirmed_at_index=uint32(0), @@ -329,6 +329,7 @@ class TradeManager: offer_dict: Dict[Union[int, bytes32], int], driver_dict: Optional[Dict[bytes32, PuzzleInfo]] = None, fee: uint64 = uint64(0), + min_coin_amount: Optional[uint128] = None, ) -> Tuple[bool, Optional[Offer], Optional[str]]: """ Offer is dictionary of wallet ids and amount @@ -378,7 +379,7 @@ class TradeManager: wallet = await self.wallet_state_manager.get_wallet_for_asset_id(asset_id.hex()) if not callable(getattr(wallet, "get_coins_to_offer", None)): # ATTENTION: new wallets raise ValueError(f"Cannot offer coins from wallet id {wallet.id()}") - coins_to_offer[id] = await wallet.get_coins_to_offer(asset_id, uint64(abs(amount))) + coins_to_offer[id] = await wallet.get_coins_to_offer(asset_id, uint64(abs(amount)), min_coin_amount) elif amount == 0: raise ValueError("You cannot offer nor request 0 amount of something") @@ -582,7 +583,7 @@ class TradeManager: return txs - async def respond_to_offer(self, offer: Offer, fee=uint64(0)) -> Tuple[bool, Optional[TradeRecord], Optional[str]]: + async def respond_to_offer(self, offer: Offer, fee=uint64(0), min_coin_amount: Optional[uint128] = None) -> Tuple[bool, Optional[TradeRecord], Optional[str]]: take_offer_dict: Dict[Union[bytes32, int], int] = {} arbitrage: Dict[Optional[bytes32], int] = offer.arbitrage() @@ -605,7 +606,7 @@ class TradeManager: valid: bool = await self.check_offer_validity(offer) if not valid: return False, None, "This offer is no longer valid" - success, take_offer, error = await self._create_offer_for_ids(take_offer_dict, offer.driver_dict, fee=fee) + success, take_offer, error = await self._create_offer_for_ids(take_offer_dict, offer.driver_dict, fee=fee, min_coin_amount=min_coin_amount) if not success or take_offer is None: return False, None, error diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index d26190957c..2b76b8573f 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -291,6 +291,7 @@ class Wallet: puzzle_announcements_to_consume: Set[Announcement] = None, memos: Optional[List[bytes]] = None, negative_change_allowed: bool = False, + min_coin_amount: Optional[uint128] = None, ) -> List[CoinSpend]: """ Generates a unsigned transaction in form of List(Puzzle, Solutions) @@ -312,7 +313,7 @@ class Wallet: raise ValueError(f"Can't send more than {max_send} in a single transaction") self.log.debug("Got back max send amount: %s", max_send) if coins is None: - coins = await self.select_coins(uint64(total_amount)) + coins = await self.select_coins(uint64(total_amount), min_coin_amount=min_coin_amount) assert len(coins) > 0 self.log.info(f"coins is not None {coins}") spend_value = sum([coin.amount for coin in coins]) @@ -417,6 +418,7 @@ class Wallet: puzzle_announcements_to_consume: Set[Announcement] = None, memos: Optional[List[bytes]] = None, negative_change_allowed: bool = False, + min_coin_amount: Optional[uint128] = None, ) -> TransactionRecord: """ Use this to generate transaction. @@ -441,6 +443,7 @@ class Wallet: puzzle_announcements_to_consume, memos, negative_change_allowed, + min_coin_amount, ) assert len(transaction) > 0 self.log.info("About to sign a transaction: %s", transaction) @@ -527,10 +530,12 @@ class Wallet: ) return spend_bundle - async def get_coins_to_offer(self, asset_id: Optional[bytes32], amount: uint64) -> Set[Coin]: + async def get_coins_to_offer( + self, asset_id: Optional[bytes32], amount: uint64, min_coin_amount: Optional[uint128] = None + ) -> Set[Coin]: if asset_id is not None: raise ValueError(f"The standard wallet cannot offer coins with asset id {asset_id}") balance = await self.get_confirmed_balance() if balance < amount: raise Exception(f"insufficient funds in wallet {self.id()}") - return await self.select_coins(amount) + return await self.select_coins(amount, min_coin_amount=min_coin_amount) From dd327744a01d5f94eeb491bf6445a1e738517396 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Tue, 12 Jul 2022 07:44:59 -0500 Subject: [PATCH 11/38] Fix offer compression backwards compatibility --- chia/wallet/util/puzzle_compression.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/chia/wallet/util/puzzle_compression.py b/chia/wallet/util/puzzle_compression.py index 7aca14e43b..f435e2d1fe 100644 --- a/chia/wallet/util/puzzle_compression.py +++ b/chia/wallet/util/puzzle_compression.py @@ -2,6 +2,7 @@ import zlib from typing import List +from chia.types.blockchain_format.program import Program from chia.wallet.puzzles.load_clvm import load_clvm from chia.wallet.puzzles import p2_delegated_puzzle_or_hidden_puzzle as standard_puzzle from chia.wallet.puzzles.cat_loader import CAT_MOD @@ -13,16 +14,24 @@ from chia.wallet.nft_wallet.nft_puzzles import ( NFT_TRANSFER_PROGRAM_DEFAULT, ) +# Need the legacy CAT mod for zlib backwards compatibility +LEGACY_CAT_MOD = Program.fromhex( + "ff02ffff01ff02ff5effff04ff02ffff04ffff04ff05ffff04ffff0bff2cff0580ffff04ff0bff80808080ffff04ffff02ff17ff2f80ffff04ff5fffff04ffff02ff2effff04ff02ffff04ff17ff80808080ffff04ffff0bff82027fff82057fff820b7f80ffff04ff81bfffff04ff82017fffff04ff8202ffffff04ff8205ffffff04ff820bffff80808080808080808080808080ffff04ffff01ffffffff81ca3dff46ff0233ffff3c04ff01ff0181cbffffff02ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff22ffff0bff2cff3480ffff0bff22ffff0bff22ffff0bff2cff5c80ff0980ffff0bff22ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ff0bffff01ff02ffff03ffff09ffff02ff2effff04ff02ffff04ff13ff80808080ff820b9f80ffff01ff02ff26ffff04ff02ffff04ffff02ff13ffff04ff5fffff04ff17ffff04ff2fffff04ff81bfffff04ff82017fffff04ff1bff8080808080808080ffff04ff82017fff8080808080ffff01ff088080ff0180ffff01ff02ffff03ff17ffff01ff02ffff03ffff20ff81bf80ffff0182017fffff01ff088080ff0180ffff01ff088080ff018080ff0180ffff04ffff04ff05ff2780ffff04ffff10ff0bff5780ff778080ff02ffff03ff05ffff01ff02ffff03ffff09ffff02ffff03ffff09ff11ff7880ffff0159ff8080ff0180ffff01818f80ffff01ff02ff7affff04ff02ffff04ff0dffff04ff0bffff04ffff04ff81b9ff82017980ff808080808080ffff01ff02ff5affff04ff02ffff04ffff02ffff03ffff09ff11ff7880ffff01ff04ff78ffff04ffff02ff36ffff04ff02ffff04ff13ffff04ff29ffff04ffff0bff2cff5b80ffff04ff2bff80808080808080ff398080ffff01ff02ffff03ffff09ff11ff2480ffff01ff04ff24ffff04ffff0bff20ff2980ff398080ffff010980ff018080ff0180ffff04ffff02ffff03ffff09ff11ff7880ffff0159ff8080ff0180ffff04ffff02ff7affff04ff02ffff04ff0dffff04ff0bffff04ff17ff808080808080ff80808080808080ff0180ffff01ff04ff80ffff04ff80ff17808080ff0180ffffff02ffff03ff05ffff01ff04ff09ffff02ff26ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff0bff22ffff0bff2cff5880ffff0bff22ffff0bff22ffff0bff2cff5c80ff0580ffff0bff22ffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bff2cff058080ff0180ffff04ffff04ff28ffff04ff5fff808080ffff02ff7effff04ff02ffff04ffff04ffff04ff2fff0580ffff04ff5fff82017f8080ffff04ffff02ff7affff04ff02ffff04ff0bffff04ff05ffff01ff808080808080ffff04ff17ffff04ff81bfffff04ff82017fffff04ffff0bff8204ffffff02ff36ffff04ff02ffff04ff09ffff04ff820affffff04ffff0bff2cff2d80ffff04ff15ff80808080808080ff8216ff80ffff04ff8205ffffff04ff820bffff808080808080808080808080ff02ff2affff04ff02ffff04ff5fffff04ff3bffff04ffff02ffff03ff17ffff01ff09ff2dffff0bff27ffff02ff36ffff04ff02ffff04ff29ffff04ff57ffff04ffff0bff2cff81b980ffff04ff59ff80808080808080ff81b78080ff8080ff0180ffff04ff17ffff04ff05ffff04ff8202ffffff04ffff04ffff04ff24ffff04ffff0bff7cff2fff82017f80ff808080ffff04ffff04ff30ffff04ffff0bff81bfffff0bff7cff15ffff10ff82017fffff11ff8202dfff2b80ff8202ff808080ff808080ff138080ff80808080808080808080ff018080" # noqa +) + OFFER_MOD = load_clvm("settlement_payments.clvm") +# For backwards compatibility to work, we must assume that these mods (already deployed) will not change +# In the case that they do change and we don't support the old asset then we need to keep around the legacy module ZDICT = [ - bytes(standard_puzzle.MOD) + bytes(CAT_MOD), + bytes(standard_puzzle.MOD) + bytes(LEGACY_CAT_MOD), bytes(OFFER_MOD), bytes(SINGLETON_TOP_LAYER_MOD) + bytes(NFT_STATE_LAYER_MOD) + bytes(NFT_OWNERSHIP_LAYER) + bytes(NFT_METADATA_UPDATER) + bytes(NFT_TRANSFER_PROGRAM_DEFAULT), + bytes(CAT_MOD), # more dictionaries go here ] From 214d5e07e5eff29753ddbf6aa7bd35e1d67863fb Mon Sep 17 00:00:00 2001 From: wallentx Date: Tue, 12 Jul 2022 19:19:35 -0500 Subject: [PATCH 12/38] bumping gui pin to head of release/1.5.0 --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index 9a46244aa3..b4c51675c7 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit 9a46244aa3c7d438eb10ae70ad749fcd13dd807c +Subproject commit b4c51675c7f7a9c315ee63c9a5ae2b328c4944ea From aedccaa1784cb1cf3596ebf4dcb15fac4ffc335b Mon Sep 17 00:00:00 2001 From: Jeff Cruikshank Date: Wed, 13 Jul 2022 11:55:28 -0700 Subject: [PATCH 13/38] Calculate NFT royalty amount --- chia/cmds/wallet_funcs.py | 63 ++++++++++++++++++++++++++- chia/wallet/nft_wallet/nft_puzzles.py | 1 + 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 88dac2bd8a..18daad4840 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -20,6 +20,7 @@ from chia.util.config import load_config from chia.util.default_root import DEFAULT_ROOT_PATH from chia.util.ints import uint16, uint32, uint64, uint128 from chia.wallet.did_wallet.did_info import DID_HRP +from chia.wallet.nft_wallet.nft_puzzles import NFT_METADATA_UPDATER_PUZZLE_HASH from chia.wallet.nft_wallet.nft_info import NFT_HRP, NFTInfo from chia.wallet.trade_record import TradeRecord from chia.wallet.trading.offer import Offer @@ -515,13 +516,33 @@ async def take_offer(args: dict, wallet_client: WalletRpcClient, fingerprint: in print("Please enter a valid offer file or hex blob") return - offered, requested, _ = offer.summary() + offered, requested, driver_dict = offer.summary() cat_name_resolver = wallet_client.cat_asset_id_to_name print("Summary:") print(" OFFERED:") await print_offer_summary(cat_name_resolver, offered) print(" REQUESTED:") await print_offer_summary(cat_name_resolver, requested) + + nft_coin_id: Optional[bytes32] = nft_coin_id_from_offer(driver_dict) + nft_royalty_percentage: int = ( + 0 if nft_coin_id is None else await get_nft_royalty_percentage(nft_coin_id, wallet_client) + ) + if nft_royalty_percentage > 0: + print("NFT Royalty Amount:") + nft_royalty_asset_id, nft_royalty_amount = calculate_nft_royalty_amount( + offered, requested, nft_coin_id, nft_royalty_percentage + ) + nft_royalty_currency = ( + "XCH" + if nft_royalty_asset_id == "xch" + else (await cat_name_resolver(bytes32.fromhex(nft_royalty_asset_id)))[1] + ) + nft_royalty_divisor = units["chia"] if nft_royalty_asset_id == "xch" else units["cat"] + print( + f" {Decimal(nft_royalty_amount) / nft_royalty_divisor} {nft_royalty_currency} ({nft_royalty_amount} mojos)" + ) + print(f"Included Fees: {Decimal(offer.bundle.fees()) / units['chia']}") if not examine_only: @@ -929,3 +950,43 @@ async def get_nft_info(args: Dict, wallet_client: WalletRpcClient, fingerprint: print_nft_info(nft_info) except Exception as e: print(f"Failed to get NFT info: {e}") + + +async def get_nft_royalty_percentage(nft_coin_id: bytes32, wallet_client: WalletRpcClient) -> int: + info = NFTInfo.from_json_dict((await wallet_client.get_nft_info(nft_coin_id.hex()))["nft_info"]) + return info.royalty_percentage + + +def calculate_nft_royalty_amount( + offered: Dict[str, Any], requested: Dict[str, Any], nft_coin_id: bytes32, nft_royalty_percentage: int +) -> Tuple[str, int]: + nft_asset_id = nft_coin_id.hex() + amount_dict: Dict[str, Any] = requested if nft_asset_id in offered else offered + amounts: List[Tuple[str, int]] = list(amount_dict.items()) + + if len(amounts) != 1 or not isinstance(amounts[0][1], int): + raise ValueError("Royalty enabled NFTs only support offering/requesting one NFT for one currency") + + royalty_amount: uint64 = uint64(amounts[0][1] * nft_royalty_percentage / 10000) + royalty_asset_id = amounts[0][0] + return royalty_asset_id, royalty_amount + + +def driver_dict_asset_is_nft(driver_dict: Dict[str, Any], asset_id: str) -> bool: + asset_dict: Dict[str, Any] = driver_dict[asset_id] + if asset_dict.get("type") == "singleton": + updater_hash_hexstr: Optional[str] = asset_dict.get("also", {}).get("updater_hash") + try: + updater_hash = bytes32.from_hexstr(updater_hash_hexstr) + return updater_hash == NFT_METADATA_UPDATER_PUZZLE_HASH + except ValueError: + # Failed to construct bytes32 from updater_hash_hexstr + pass + return False + + +def nft_coin_id_from_offer(driver_dict: Dict[str, Any]) -> Optional[bytes32]: + nft_asset_id: Optional[str] = next( + (key for key in driver_dict.keys() if driver_dict_asset_is_nft(driver_dict, key)), None + ) + return bytes32.fromhex(nft_asset_id) if nft_asset_id is not None else None diff --git a/chia/wallet/nft_wallet/nft_puzzles.py b/chia/wallet/nft_wallet/nft_puzzles.py index c25ac6e836..01b6b7985a 100644 --- a/chia/wallet/nft_wallet/nft_puzzles.py +++ b/chia/wallet/nft_wallet/nft_puzzles.py @@ -20,6 +20,7 @@ LAUNCHER_PUZZLE_HASH = LAUNCHER_PUZZLE.get_tree_hash() SINGLETON_MOD_HASH = SINGLETON_TOP_LAYER_MOD.get_tree_hash() NFT_STATE_LAYER_MOD_HASH = NFT_STATE_LAYER_MOD.get_tree_hash() NFT_METADATA_UPDATER = load_clvm("nft_metadata_updater_default.clvm") +NFT_METADATA_UPDATER_PUZZLE_HASH = NFT_METADATA_UPDATER.get_tree_hash() NFT_OWNERSHIP_LAYER = load_clvm("nft_ownership_layer.clvm") NFT_TRANSFER_PROGRAM_DEFAULT = load_clvm("nft_ownership_transfer_program_one_way_claim_with_royalties.clvm") STANDARD_PUZZLE_MOD = load_clvm("p2_delegated_puzzle_or_hidden_puzzle.clvm") From e456007f3924a0396bb804cbb195b7c6c13f42e3 Mon Sep 17 00:00:00 2001 From: Kronus91 Date: Fri, 8 Jul 2022 12:30:30 -0700 Subject: [PATCH 14/38] Create NFT wallet after the DID created (#12175) --- chia/rpc/wallet_rpc_api.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 3572f0aaab..690224598e 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -548,11 +548,17 @@ class WalletRpcApi: uint64(request.get("fee", 0)), ) - my_did = encode_puzzle_hash(bytes32.fromhex(did_wallet.get_my_DID()), DID_HRP) + my_did_id = encode_puzzle_hash(bytes32.fromhex(did_wallet.get_my_DID()), DID_HRP) + await NFTWallet.create_new_nft_wallet( + wallet_state_manager, + main_wallet, + bytes32.fromhex(did_wallet.get_my_DID()), + request.get("wallet_name", None), + ) return { "success": True, "type": did_wallet.type(), - "my_did": my_did, + "my_did": my_did_id, "wallet_id": did_wallet.id(), } From 640926dfe2b5f21efe03dc3141a865896a564cf8 Mon Sep 17 00:00:00 2001 From: wallentx Date: Wed, 13 Jul 2022 15:47:11 -0500 Subject: [PATCH 15/38] Bumping gui --- chia-blockchain-gui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia-blockchain-gui b/chia-blockchain-gui index b4c51675c7..ca6cf9bee3 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit b4c51675c7f7a9c315ee63c9a5ae2b328c4944ea +Subproject commit ca6cf9bee377741170ebbc2813f0cd4f0c3f2c64 From c891d5645fd20d7e5d30cafe06c0c84d7e5f496e Mon Sep 17 00:00:00 2001 From: Jeff Cruikshank Date: Wed, 13 Jul 2022 15:09:49 -0700 Subject: [PATCH 16/38] Show total amount to be paid for NFT offers --- chia/cmds/wallet_funcs.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 18daad4840..0cc8037b3f 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -524,28 +524,42 @@ async def take_offer(args: dict, wallet_client: WalletRpcClient, fingerprint: in print(" REQUESTED:") await print_offer_summary(cat_name_resolver, requested) + print() + nft_coin_id: Optional[bytes32] = nft_coin_id_from_offer(driver_dict) nft_royalty_percentage: int = ( 0 if nft_coin_id is None else await get_nft_royalty_percentage(nft_coin_id, wallet_client) ) + nft_total_amount_requested_str: Optional[str] = None if nft_royalty_percentage > 0: - print("NFT Royalty Amount:") - nft_royalty_asset_id, nft_royalty_amount = calculate_nft_royalty_amount( + print("NFT Royalty Fee:") + nft_royalty_asset_id, nft_royalty_amount, nft_total_amount_requested = calculate_nft_royalty_amount( offered, requested, nft_coin_id, nft_royalty_percentage ) - nft_royalty_currency = ( - "XCH" - if nft_royalty_asset_id == "xch" - else (await cat_name_resolver(bytes32.fromhex(nft_royalty_asset_id)))[1] - ) + nft_royalty_currency: str = "Unknown CAT" + if nft_royalty_asset_id == "xch": + nft_royalty_currency = "XCH" + else: + result = await cat_name_resolver(bytes32.fromhex(nft_royalty_asset_id)) + if result is not None: + nft_royalty_currency = result[1] + nft_royalty_divisor = units["chia"] if nft_royalty_asset_id == "xch" else units["cat"] + nft_total_amount_requested_str = ( + f"{Decimal(nft_total_amount_requested) / nft_royalty_divisor} {nft_royalty_currency}" + ) print( - f" {Decimal(nft_royalty_amount) / nft_royalty_divisor} {nft_royalty_currency} ({nft_royalty_amount} mojos)" + f" {Decimal(nft_royalty_amount) / nft_royalty_divisor} {nft_royalty_currency} " + f"({nft_royalty_amount} mojos)" ) print(f"Included Fees: {Decimal(offer.bundle.fees()) / units['chia']}") + if nft_total_amount_requested_str is not None: + print(f"Total Amount Requested: {nft_total_amount_requested_str}") + if not examine_only: + print() confirmation = input("Would you like to take this offer? (y/n): ") if confirmation in ["y", "yes"]: trade_record = await wallet_client.take_offer(offer, fee=fee) @@ -959,7 +973,7 @@ async def get_nft_royalty_percentage(nft_coin_id: bytes32, wallet_client: Wallet def calculate_nft_royalty_amount( offered: Dict[str, Any], requested: Dict[str, Any], nft_coin_id: bytes32, nft_royalty_percentage: int -) -> Tuple[str, int]: +) -> Tuple[str, int, int]: nft_asset_id = nft_coin_id.hex() amount_dict: Dict[str, Any] = requested if nft_asset_id in offered else offered amounts: List[Tuple[str, int]] = list(amount_dict.items()) @@ -969,7 +983,8 @@ def calculate_nft_royalty_amount( royalty_amount: uint64 = uint64(amounts[0][1] * nft_royalty_percentage / 10000) royalty_asset_id = amounts[0][0] - return royalty_asset_id, royalty_amount + total_amount_requested = (requested[royalty_asset_id] if amount_dict == requested else 0) + royalty_amount + return royalty_asset_id, royalty_amount, total_amount_requested def driver_dict_asset_is_nft(driver_dict: Dict[str, Any], asset_id: str) -> bool: From 369a190b456992c49f7bd51469bc7055c9a8a384 Mon Sep 17 00:00:00 2001 From: Jeff Cruikshank Date: Thu, 14 Jul 2022 10:31:52 -0700 Subject: [PATCH 17/38] Fix for NFT0 and NFT+Royalty detection suggested by quex --- chia/cmds/wallet_funcs.py | 26 ++++++++++---------------- chia/wallet/nft_wallet/nft_puzzles.py | 1 - 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 0cc8037b3f..be9081f242 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -20,7 +20,6 @@ from chia.util.config import load_config from chia.util.default_root import DEFAULT_ROOT_PATH from chia.util.ints import uint16, uint32, uint64, uint128 from chia.wallet.did_wallet.did_info import DID_HRP -from chia.wallet.nft_wallet.nft_puzzles import NFT_METADATA_UPDATER_PUZZLE_HASH from chia.wallet.nft_wallet.nft_info import NFT_HRP, NFTInfo from chia.wallet.trade_record import TradeRecord from chia.wallet.trading.offer import Offer @@ -526,7 +525,7 @@ async def take_offer(args: dict, wallet_client: WalletRpcClient, fingerprint: in print() - nft_coin_id: Optional[bytes32] = nft_coin_id_from_offer(driver_dict) + nft_coin_id: Optional[bytes32] = nft_coin_id_supporting_royalties_from_offer(driver_dict) nft_royalty_percentage: int = ( 0 if nft_coin_id is None else await get_nft_royalty_percentage(nft_coin_id, wallet_client) ) @@ -968,7 +967,7 @@ async def get_nft_info(args: Dict, wallet_client: WalletRpcClient, fingerprint: async def get_nft_royalty_percentage(nft_coin_id: bytes32, wallet_client: WalletRpcClient) -> int: info = NFTInfo.from_json_dict((await wallet_client.get_nft_info(nft_coin_id.hex()))["nft_info"]) - return info.royalty_percentage + return info.royalty_percentage if info.royalty_percentage is not None else 0 def calculate_nft_royalty_amount( @@ -987,21 +986,16 @@ def calculate_nft_royalty_amount( return royalty_asset_id, royalty_amount, total_amount_requested -def driver_dict_asset_is_nft(driver_dict: Dict[str, Any], asset_id: str) -> bool: +def driver_dict_asset_is_nft_supporting_royalties(driver_dict: Dict[str, Any], asset_id: str) -> bool: asset_dict: Dict[str, Any] = driver_dict[asset_id] - if asset_dict.get("type") == "singleton": - updater_hash_hexstr: Optional[str] = asset_dict.get("also", {}).get("updater_hash") - try: - updater_hash = bytes32.from_hexstr(updater_hash_hexstr) - return updater_hash == NFT_METADATA_UPDATER_PUZZLE_HASH - except ValueError: - # Failed to construct bytes32 from updater_hash_hexstr - pass - return False + return ( + asset_dict.get("type") == "singleton" + and asset_dict.get("also", {}).get("type") == "metadata" + and asset_dict.get("also", {}).get("also", {}).get("type") == "ownership" + ) - -def nft_coin_id_from_offer(driver_dict: Dict[str, Any]) -> Optional[bytes32]: +def nft_coin_id_supporting_royalties_from_offer(driver_dict: Dict[str, Any]) -> Optional[bytes32]: nft_asset_id: Optional[str] = next( - (key for key in driver_dict.keys() if driver_dict_asset_is_nft(driver_dict, key)), None + (key for key in driver_dict.keys() if driver_dict_asset_is_nft_supporting_royalties(driver_dict, key)), None ) return bytes32.fromhex(nft_asset_id) if nft_asset_id is not None else None diff --git a/chia/wallet/nft_wallet/nft_puzzles.py b/chia/wallet/nft_wallet/nft_puzzles.py index 01b6b7985a..c25ac6e836 100644 --- a/chia/wallet/nft_wallet/nft_puzzles.py +++ b/chia/wallet/nft_wallet/nft_puzzles.py @@ -20,7 +20,6 @@ LAUNCHER_PUZZLE_HASH = LAUNCHER_PUZZLE.get_tree_hash() SINGLETON_MOD_HASH = SINGLETON_TOP_LAYER_MOD.get_tree_hash() NFT_STATE_LAYER_MOD_HASH = NFT_STATE_LAYER_MOD.get_tree_hash() NFT_METADATA_UPDATER = load_clvm("nft_metadata_updater_default.clvm") -NFT_METADATA_UPDATER_PUZZLE_HASH = NFT_METADATA_UPDATER.get_tree_hash() NFT_OWNERSHIP_LAYER = load_clvm("nft_ownership_layer.clvm") NFT_TRANSFER_PROGRAM_DEFAULT = load_clvm("nft_ownership_transfer_program_one_way_claim_with_royalties.clvm") STANDARD_PUZZLE_MOD = load_clvm("p2_delegated_puzzle_or_hidden_puzzle.clvm") From 8cbbeb3bf2f1b1bdea29f95e30a185b0cf6a1fc9 Mon Sep 17 00:00:00 2001 From: Jeff Cruikshank Date: Thu, 14 Jul 2022 10:43:22 -0700 Subject: [PATCH 18/38] Linter fix and formatting change --- chia/cmds/wallet_funcs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index be9081f242..63c408c38a 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -530,7 +530,7 @@ async def take_offer(args: dict, wallet_client: WalletRpcClient, fingerprint: in 0 if nft_coin_id is None else await get_nft_royalty_percentage(nft_coin_id, wallet_client) ) nft_total_amount_requested_str: Optional[str] = None - if nft_royalty_percentage > 0: + if nft_coin_id is not None and nft_royalty_percentage > 0: print("NFT Royalty Fee:") nft_royalty_asset_id, nft_royalty_amount, nft_total_amount_requested = calculate_nft_royalty_amount( offered, requested, nft_coin_id, nft_royalty_percentage @@ -994,6 +994,7 @@ def driver_dict_asset_is_nft_supporting_royalties(driver_dict: Dict[str, Any], a and asset_dict.get("also", {}).get("also", {}).get("type") == "ownership" ) + def nft_coin_id_supporting_royalties_from_offer(driver_dict: Dict[str, Any]) -> Optional[bytes32]: nft_asset_id: Optional[str] = next( (key for key in driver_dict.keys() if driver_dict_asset_is_nft_supporting_royalties(driver_dict, key)), None From 47e93d1b58dd40c4e09c1c77baae30988ced7a81 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 18 Jul 2022 21:55:14 -0700 Subject: [PATCH 19/38] Add RPCs for getting/extending the current derivation path index (#12472) --- chia/cmds/wallet.py | 39 ++++++++++++++++++++++++ chia/cmds/wallet_funcs.py | 13 ++++++++ chia/rpc/wallet_rpc_api.py | 46 +++++++++++++++++++++++++++++ chia/rpc/wallet_rpc_client.py | 6 ++++ chia/wallet/wallet_state_manager.py | 11 +++++-- 5 files changed, 113 insertions(+), 2 deletions(-) diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py index 0a85bc4061..df6fae1b05 100644 --- a/chia/cmds/wallet.py +++ b/chia/cmds/wallet.py @@ -254,6 +254,45 @@ def delete_unconfirmed_transactions_cmd(wallet_rpc_port: Optional[int], id, fing asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, delete_unconfirmed_transactions)) +@wallet_cmd.command("get_derivation_index", short_help="Get the last puzzle hash derivation path index") +@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 wallet to use", type=int) +def get_derivation_index_cmd(wallet_rpc_port: Optional[int], fingerprint: int) -> None: + extra_params: Dict[str, Any] = {} + import asyncio + from .wallet_funcs import execute_with_wallet, get_derivation_index + + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, get_derivation_index)) + + +@wallet_cmd.command( + "update_derivation_index", short_help="Generate additional derived puzzle hashes starting at the provided index" +) +@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 wallet to use", type=int) +@click.option( + "-i", "--index", help="Index to set. Must be greater than the current derivation index", type=int, required=True +) +def update_derivation_index_cmd(wallet_rpc_port: Optional[int], fingerprint: int, index: int) -> None: + extra_params = {"index": index} + import asyncio + from .wallet_funcs import execute_with_wallet, update_derivation_index + + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, update_derivation_index)) + + @wallet_cmd.command("add_token", short_help="Add/Rename a CAT to the wallet by its asset ID") @click.option( "-wp", diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 63c408c38a..327767d41d 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -259,6 +259,19 @@ async def delete_unconfirmed_transactions(args: dict, wallet_client: WalletRpcCl print(f"Successfully deleted all unconfirmed transactions for wallet id {wallet_id} on key {fingerprint}") +async def get_derivation_index(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: + res = await wallet_client.get_current_derivation_index() + print(f"Last derivation index: {res}") + + +async def update_derivation_index(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: + index = args["index"] + print("Updating derivation index... This may take a while.") + res = await wallet_client.extend_derivation_index(index) + print(f"Updated derivation index: {res}") + print("Your balances may take a while to update.") + + async def add_token(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: asset_id = args["asset_id"] token_name = args["token_name"] diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 690224598e..27a9f92c54 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -56,6 +56,7 @@ from chia.wallet.wallet_node import WalletNode # Timeout for response from wallet/full node for sending a transaction TIMEOUT = 30 +MAX_DERIVATION_INDEX_DELTA = 1000 log = logging.getLogger(__name__) @@ -103,6 +104,8 @@ class WalletRpcApi: "/create_signed_transaction": self.create_signed_transaction, "/delete_unconfirmed_transactions": self.delete_unconfirmed_transactions, "/select_coins": self.select_coins, + "/get_current_derivation_index": self.get_current_derivation_index, + "/extend_derivation_index": self.extend_derivation_index, # CATs and trading "/cat_set_name": self.cat_set_name, "/cat_asset_id_to_name": self.cat_asset_id_to_name, @@ -900,6 +903,49 @@ class WalletRpcApi: return {"coins": [coin.to_json_dict() for coin in selected_coins]} + async def get_current_derivation_index(self, request) -> Dict[str, Any]: + assert self.service.wallet_state_manager is not None + + index: Optional[uint32] = await self.service.wallet_state_manager.puzzle_store.get_last_derivation_path() + + return {"success": True, "index": index} + + async def extend_derivation_index(self, request) -> Dict[str, Any]: + assert self.service.wallet_state_manager is not None + + # Require a new max derivation index + if "index" not in request: + raise ValueError("Derivation index is required") + + # Require that the wallet is fully synced + synced = await self.service.wallet_state_manager.synced() + if synced is False: + raise ValueError("Wallet needs to be fully synced before extending derivation index") + + index = uint32(request["index"]) + current: Optional[uint32] = await self.service.wallet_state_manager.puzzle_store.get_last_derivation_path() + + # Additional sanity check that the wallet is synced + if current is None: + raise ValueError("No current derivation record found, unable to extend index") + + # Require that the new index is greater than the current index + if index <= current: + raise ValueError(f"New derivation index must be greater than current index: {current}") + + if index - current > MAX_DERIVATION_INDEX_DELTA: + raise ValueError( + "Too many derivations requested. " + f"Use a derivation index less than {current + MAX_DERIVATION_INDEX_DELTA + 1}" + ) + + await self.service.wallet_state_manager.create_more_puzzle_hashes(from_zero=False, up_to_index=index) + + updated: Optional[uint32] = await self.service.wallet_state_manager.puzzle_store.get_last_derivation_path() + updated_index = updated if updated is not None else None + + return {"success": True, "index": updated_index} + ########################################################################################## # CATs and Trading ########################################################################################## diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 12d09996dd..9f164b3185 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -197,6 +197,12 @@ class WalletRpcClient(RpcClient): ) return None + async def get_current_derivation_index(self) -> str: + return (await self.fetch("get_current_derivation_index", {}))["index"] + + async def extend_derivation_index(self, index: int) -> str: + return (await self.fetch("extend_derivation_index", {"index": index}))["index"] + async def get_farmed_amount(self) -> Dict: return await self.fetch("get_farmed_amount", {}) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index f128b8a233..90f56eab7f 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -248,14 +248,18 @@ class WalletStateManager: pubkey = private.get_g1() return pubkey, private - async def create_more_puzzle_hashes(self, from_zero: bool = False, in_transaction=False): + async def create_more_puzzle_hashes( + self, from_zero: bool = False, in_transaction=False, up_to_index: Optional[uint32] = None + ): """ For all wallets in the user store, generates the first few puzzle hashes so that we can restore the wallet from only the private keys. """ targets = list(self.wallets.keys()) self.log.debug("Target wallets to generate puzzle hashes for: %s", repr(targets)) - unused: Optional[uint32] = await self.puzzle_store.get_unused_derivation_path() + unused: Optional[uint32] = ( + up_to_index if up_to_index is not None else await self.puzzle_store.get_unused_derivation_path() + ) if unused is None: # This handles the case where the database has entries but they have all been used unused = await self.puzzle_store.get_last_derivation_path() @@ -264,6 +268,7 @@ class WalletStateManager: # This handles the case where the database is empty unused = uint32(0) + self.log.debug(f"Requested to generate puzzle hashes to at least index {unused}") to_generate = self.config["initial_num_public_keys"] for wallet_id in targets: @@ -334,6 +339,8 @@ class WalletStateManager: [record.wallet_id for record in derivation_paths], in_transaction, ) + if len(derivation_paths) > 0: + self.state_changed("new_derivation_index", data_object={"index": derivation_paths[-1].index}) if unused > 0: await self.puzzle_store.set_used_up_to(uint32(unused - 1), in_transaction) From ccd5d993af62acc40f4bdc39017c4a0447a5240f Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Tue, 19 Jul 2022 14:48:20 +0900 Subject: [PATCH 20/38] Sleep to allow neworking layer to execute (#12463) * Sleep to allow neworking layer to execute * Add comment --- chia/wallet/wallet_state_manager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 90f56eab7f..e9dcf7dfa2 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -322,6 +322,9 @@ class WalletStateManager: self.log.debug( f"Puzzle at index {index} wallet ID {wallet_id} puzzle hash {puzzlehash_unhardened.hex()}" ) + # We await sleep here to allow an asyncio context switch (since the other parts of this loop do + # not have await and therefore block). This can prevent networking layer from responding to ping. + await asyncio.sleep(0) derivation_paths.append( DerivationRecord( uint32(index), From ae01212b39e507abaa0d84980ba5b6df1cdd535a Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 19 Jul 2022 14:15:06 -0700 Subject: [PATCH 21/38] Added param to indicate how many additional phs create_more_puzzle_hashes should create. (#12493) Account for range() not including last_index when `up_to_index` is provided. --- chia/rpc/wallet_rpc_api.py | 4 +++- chia/wallet/wallet_state_manager.py | 13 +++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 27a9f92c54..b147843422 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -939,7 +939,9 @@ class WalletRpcApi: f"Use a derivation index less than {current + MAX_DERIVATION_INDEX_DELTA + 1}" ) - await self.service.wallet_state_manager.create_more_puzzle_hashes(from_zero=False, up_to_index=index) + await self.service.wallet_state_manager.create_more_puzzle_hashes( + from_zero=False, up_to_index=index, num_additional_phs=0 + ) updated: Optional[uint32] = await self.service.wallet_state_manager.puzzle_store.get_last_derivation_path() updated_index = updated if updated is not None else None diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index e9dcf7dfa2..ddf5d1b265 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -249,7 +249,11 @@ class WalletStateManager: return pubkey, private async def create_more_puzzle_hashes( - self, from_zero: bool = False, in_transaction=False, up_to_index: Optional[uint32] = None + self, + from_zero: bool = False, + in_transaction=False, + up_to_index: Optional[uint32] = None, + num_additional_phs: Optional[int] = None, ): """ For all wallets in the user store, generates the first few puzzle hashes so @@ -258,7 +262,7 @@ class WalletStateManager: targets = list(self.wallets.keys()) self.log.debug("Target wallets to generate puzzle hashes for: %s", repr(targets)) unused: Optional[uint32] = ( - up_to_index if up_to_index is not None else await self.puzzle_store.get_unused_derivation_path() + uint32(up_to_index + 1) if up_to_index is not None else await self.puzzle_store.get_unused_derivation_path() ) if unused is None: # This handles the case where the database has entries but they have all been used @@ -269,7 +273,8 @@ class WalletStateManager: unused = uint32(0) self.log.debug(f"Requested to generate puzzle hashes to at least index {unused}") - to_generate = self.config["initial_num_public_keys"] + to_generate = num_additional_phs if num_additional_phs is not None else self.config["initial_num_public_keys"] + new_paths: bool = False for wallet_id in targets: target_wallet = self.wallets[wallet_id] @@ -293,7 +298,7 @@ class WalletStateManager: if start_index >= last_index: self.log.debug(f"Nothing to create for for wallet_id: {wallet_id}, index: {start_index}") else: - creating_msg = f"Creating puzzle hashes from {start_index} to {last_index} for wallet_id: {wallet_id}" + creating_msg = f"Creating puzzle hashes from {start_index} to {last_index-1} for wallet_id: {wallet_id}" self.log.info(f"Start: {creating_msg}") for index in range(start_index, last_index): if WalletType(target_wallet.type()) == WalletType.POOLING_WALLET: From 3dfc3707da9256b24406f7cf35a55c1b102b95df Mon Sep 17 00:00:00 2001 From: Jeff Cruikshank Date: Tue, 19 Jul 2022 15:12:01 -0700 Subject: [PATCH 22/38] Fixed the wallet db rename from v2/v1 to v2_r1. Removed vestigial code for dealing with the lite wallet db now that we're syncing v2_r1 from scratch. --- chia/util/default_root.py | 3 --- chia/wallet/wallet_node.py | 32 ++++++++++++---------- tests/wallet/test_wallet.py | 53 +++++++++++++++++++++++++++++++++++-- 3 files changed, 69 insertions(+), 19 deletions(-) diff --git a/chia/util/default_root.py b/chia/util/default_root.py index 6998044484..7e9727af07 100644 --- a/chia/util/default_root.py +++ b/chia/util/default_root.py @@ -2,8 +2,5 @@ import os from pathlib import Path DEFAULT_ROOT_PATH = Path(os.path.expanduser(os.getenv("CHIA_ROOT", "~/.chia/mainnet"))).resolve() -STANDALONE_ROOT_PATH = Path( - os.path.expanduser(os.getenv("CHIA_STANDALONE_WALLET_ROOT", "~/.chia/standalone_wallet")) -).resolve() DEFAULT_KEYS_ROOT_PATH = Path(os.path.expanduser(os.getenv("CHIA_KEYS_ROOT", "~/.chia_keys"))).resolve() diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 4a2ef18287..252b991961 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -48,7 +48,6 @@ from chia.types.weight_proof import SubEpochData, WeightProof from chia.util.byte_types import hexstr_to_bytes from chia.util.chunks import chunks from chia.util.config import WALLET_PEERS_PATH_KEY_DEPRECATED -from chia.util.default_root import STANDALONE_ROOT_PATH from chia.util.ints import uint32, uint64 from chia.util.keychain import Keychain, KeyringIsLocked from chia.util.path import mkdir, path_from_root @@ -70,6 +69,23 @@ from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_state_manager import WalletStateManager +def get_wallet_db_path(root_path: Path, config: Dict[str, Any], key_fingerprint: str) -> Path: + """ + Construct a path to the wallet db. Uses config values and the wallet key's fingerprint to + determine the wallet db filename. + """ + db_path_replaced: str = ( + config["database_path"].replace("CHALLENGE", config["selected_network"]).replace("KEY", key_fingerprint) + ) + + # "v2_r1" is the current wallet db version identifier + if "v2_r1" not in db_path_replaced: + db_path_replaced = db_path_replaced.replace("v2", "v2_r1").replace("v1", "v2_r1") + + path: Path = path_from_root(root_path, db_path_replaced) + return path + + class WalletNode: key_config: Dict config: Dict @@ -195,21 +211,9 @@ class WalletNode: if self.config.get("enable_profiler", False): asyncio.create_task(profile_task(self.root_path, "wallet", self.log)) - db_path_key_suffix = str(private_key.get_g1().get_fingerprint()) - db_path_replaced: str = ( - self.config["database_path"] - .replace("CHALLENGE", self.config["selected_network"]) - .replace("KEY", db_path_key_suffix) - ) - path = path_from_root(self.root_path, db_path_replaced.replace("v1", "v2_r1")) + path: Path = get_wallet_db_path(self.root_path, self.config, str(private_key.get_g1().get_fingerprint())) mkdir(path.parent) - standalone_path = path_from_root(STANDALONE_ROOT_PATH, f"{db_path_replaced.replace('v2_r1', 'v1')}_new") - if not path.exists(): - if standalone_path.exists(): - self.log.info(f"Copying wallet db from {standalone_path} to {path}") - path.write_bytes(standalone_path.read_bytes()) - assert self.server is not None self.wallet_state_manager = await WalletStateManager.create( private_key, diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index 7c37c35ab3..65c3168d90 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -1,6 +1,7 @@ import asyncio import time -from typing import List, Tuple +from pathlib import Path +from typing import Any, Dict, List, Tuple import pytest @@ -18,7 +19,7 @@ from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.compute_memos import compute_memos from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.wallet_types import AmountWithPuzzlehash -from chia.wallet.wallet_node import WalletNode +from chia.wallet.wallet_node import WalletNode, get_wallet_db_path from chia.wallet.wallet_state_manager import WalletStateManager from tests.pools.test_pool_rpc import wallet_is_synced from tests.time_out_assert import time_out_assert, time_out_assert_not_none @@ -910,3 +911,51 @@ class TestWalletSimulator: await time_out_assert(5, wallet.get_confirmed_balance, funds - AMOUNT_TO_SEND) await time_out_assert(5, wallet.get_unconfirmed_balance, funds - AMOUNT_TO_SEND) + + +def test_get_wallet_db_path_v2_r1() -> None: + root_path: Path = Path("/x/y/z/.chia/mainnet") + config: Dict[str, Any] = { + "database_path": "wallet/db/blockchain_wallet_v2_r1_CHALLENGE_KEY.sqlite", + "selected_network": "mainnet", + } + fingerprint: str = "1234567890" + wallet_db_path: Path = get_wallet_db_path(root_path, config, fingerprint) + + assert wallet_db_path == Path("/x/y/z/.chia/mainnet/wallet/db/blockchain_wallet_v2_r1_mainnet_1234567890.sqlite") + + +def test_get_wallet_db_path_v2() -> None: + root_path: Path = Path("/x/y/z/.chia/mainnet") + config: Dict[str, Any] = { + "database_path": "wallet/db/blockchain_wallet_v2_CHALLENGE_KEY.sqlite", + "selected_network": "mainnet", + } + fingerprint: str = "1234567890" + wallet_db_path: Path = get_wallet_db_path(root_path, config, fingerprint) + + assert wallet_db_path == Path("/x/y/z/.chia/mainnet/wallet/db/blockchain_wallet_v2_r1_mainnet_1234567890.sqlite") + + +def test_get_wallet_db_path_v1() -> None: + root_path: Path = Path("/x/y/z/.chia/mainnet") + config: Dict[str, Any] = { + "database_path": "wallet/db/blockchain_wallet_v1_CHALLENGE_KEY.sqlite", + "selected_network": "mainnet", + } + fingerprint: str = "1234567890" + wallet_db_path: Path = get_wallet_db_path(root_path, config, fingerprint) + + assert wallet_db_path == Path("/x/y/z/.chia/mainnet/wallet/db/blockchain_wallet_v2_r1_mainnet_1234567890.sqlite") + + +def test_get_wallet_db_path_testnet() -> None: + root_path: Path = Path("/x/y/z/.chia/testnet") + config: Dict[str, Any] = { + "database_path": "wallet/db/blockchain_wallet_v2_CHALLENGE_KEY.sqlite", + "selected_network": "testnet", + } + fingerprint: str = "1234567890" + wallet_db_path: Path = get_wallet_db_path(root_path, config, fingerprint) + + assert wallet_db_path == Path("/x/y/z/.chia/testnet/wallet/db/blockchain_wallet_v2_r1_testnet_1234567890.sqlite") From c7d60da0ccce7f49ab8c056a81435fb6783a9b9e Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 20 Jul 2022 13:07:17 -0700 Subject: [PATCH 23/38] When extending the derivation index, make sure we don't mark previously (#12513) unused indices as used. This helps minimize gaps in the address space. --- chia/rpc/wallet_rpc_api.py | 5 ++++- chia/wallet/wallet_state_manager.py | 8 +++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index b147843422..e071f89cce 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -939,8 +939,11 @@ class WalletRpcApi: f"Use a derivation index less than {current + MAX_DERIVATION_INDEX_DELTA + 1}" ) + # Since we've bumping the derivation index without having found any new puzzles, we want + # to preserve the current last used index, so we call create_more_puzzle_hashes with + # mark_existing_as_used=False await self.service.wallet_state_manager.create_more_puzzle_hashes( - from_zero=False, up_to_index=index, num_additional_phs=0 + from_zero=False, mark_existing_as_used=False, up_to_index=index, num_additional_phs=0 ) updated: Optional[uint32] = await self.service.wallet_state_manager.puzzle_store.get_last_derivation_path() diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index ddf5d1b265..5b374d8b9d 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -252,6 +252,7 @@ class WalletStateManager: self, from_zero: bool = False, in_transaction=False, + mark_existing_as_used=True, up_to_index: Optional[uint32] = None, num_additional_phs: Optional[int] = None, ): @@ -349,9 +350,10 @@ class WalletStateManager: ) if len(derivation_paths) > 0: self.state_changed("new_derivation_index", data_object={"index": derivation_paths[-1].index}) - - if unused > 0: - await self.puzzle_store.set_used_up_to(uint32(unused - 1), in_transaction) + # By default, we'll mark previously generated unused puzzle hashes as used if we have new paths + if mark_existing_as_used and unused > 0 and new_paths: + self.log.info(f"Updating last used derivation index: {unused - 1}") + await self.puzzle_store.set_used_up_to(uint32(unused - 1)) async def update_wallet_puzzle_hashes(self, wallet_id, in_transaction=False): derivation_paths: List[DerivationRecord] = [] From 0754a2d675eba656c631839b998b39367a5293c9 Mon Sep 17 00:00:00 2001 From: wallentx Date: Thu, 21 Jul 2022 15:35:25 -0500 Subject: [PATCH 24/38] Updating SBX asset ID --- chia/wallet/cat_wallet/cat_constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/wallet/cat_wallet/cat_constants.py b/chia/wallet/cat_wallet/cat_constants.py index 25a1c370ca..1d757402f0 100644 --- a/chia/wallet/cat_wallet/cat_constants.py +++ b/chia/wallet/cat_wallet/cat_constants.py @@ -1,5 +1,5 @@ SPACEBUCKS = { - "asset_id": "78ad32a8c9ea70f27d73e9306fc467bab2a6b15b30289791e37ab6e8612212b1", + "asset_id": "a628c1c2c6fcb74d53746157e438e108eab5c0bb3e5c80ff9b1910b3e4832913", "name": "Spacebucks", "symbol": "SBX", } From a2c78e04a6af79c95fb1e6517f6fe2bfee466fd8 Mon Sep 17 00:00:00 2001 From: William Allen Date: Mon, 25 Jul 2022 17:06:36 -0500 Subject: [PATCH 25/38] Adding 1.5.0 changelog (#56) * Adding 1.5.0 changelog * Adding CVE fix --- CHANGELOG.md | 58 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53efa8ebc2..14f4578e78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,15 +10,49 @@ for setuptools_scm/PEP 440 reasons. ### What's Changed +## 1.5.0 Chia blockchain 2022-7-26 + +### Added + +- Added derivation index information to the Wallet UI to show the current derivation index height +- Added section in Settings to allow the user to manually update the derivation index height in order to ensure the wallet finds all the coins +- Added a tooltip for users to understand why their CAT balance has changed as new CAT2 tokens get re-issued +- There is now a `blockchain_wallet_v2_r1_*.sqlite` DB that will be created, which will sync from 0 to look for CAT2 tokens. This preserves a copy of your previous wallet DB so that you are able to look up previous transactions by using an older wallet client +- Extended `min_coin` to RPC calls, and CLI for coin selection +- Show DID in the offer preview for NFTs +- Added wallet RPCs (`get_derivation_index`, `update_derivation_index`) to enable the GUI, and CLI to report what the current derivation index is for scanning wallet addresses, and also allows a user to move that index forward to broaden the set of addresses to scan for coins + +### Changed + +- Changed the DID Wallet to use the new coin selection algorithm that the Standard Wallet, and the CAT Wallet already use +- Changed returning the result of send_transaction to happen after the transaction has been added to the queue, rather than it just being added to the mempool. +- Increased the priority of wallet transactions vs full node broadcasted transactions, so we don't have to wait in line as a wallet user +- Deprecated the `-st, --series-total` and `-sn, --series-number` RPC and CLI NFT minting options in favor of `-ec, --edition-count` and `-en, --edition-number` to align with NFT industry terms +- When creating a DID profile, a DID-linked NFT wallet is automatically created +- Update `chia wallet take_offer` to show NFT royalties that will be paid out when an offer is taken +- Added a parameter to indicate how many additional puzzle hashes `create_more_puzzle_hashes` should create + +### Fixed + +- Fixed [CVE-2022-36447] where in tokens previously minted on the Chia blockchain using the `CAT1` standard can be inflated in arbitrary amounts by any holder of the token. Total amount of the token can be increased as high as the malicious actor pleases. This is true for every `CAT1` on the Chia blockchain, regardless of issuance rules. This attack is auditable on-chain, so maliciously altered coins can potentially be "marked" by off-chain observers as malicious. +- Fixed issue that prevented websockets from being attempted if an earlier websocket failed +- Fixed issue where `test_smallest_coin_over_amount` did not work properly when all coins were smaller than the amount +- Fixed a performance issue with knapsack that caused it to keep searching for more coins than could actually be selected. Performance with 200k coins: + - Old: 60 seconds + - New: 0.78 seconds +- Fixed offer compression backwards compatibility +- Fixed royalty percentage check for NFT0 NFTs, and made the check for an offer containing an NFT more generalized +- Fixed timing with asyncio context switching that could prevent networking layer from responding to ping + ## 1.4.0 Chia blockchain 2022-6-29 ### Added - Added support for NFTs!!! :party: -- Added `chia wallet nft` command (see https://docs.chia.net/docs/13cli/did_cli) -- Added `chia wallet did` command (see https://docs.chia.net/docs/12rpcs/nft_rpcs) -- Added RPCs for DID (see https://docs.chia.net/docs/12rpcs/did_rpcs) -- Added RPCs for NFT (see https://docs.chia.net/docs/12rpcs/nft_rpcs) +- Added `chia wallet nft` command (see ) +- Added `chia wallet did` command (see ) +- Added RPCs for DID (see ) +- Added RPCs for NFT (see ) - Enable stricter mempool rule when dealing with multiple extra arguments - Added a retry when loading pool info from a pool at 2 minute intervals - Added CLI options `--sort-by-height` and –sort-by-relevance` to `chia wallet get_transactions` @@ -228,7 +262,7 @@ There is a known issue where harvesters will not reconnect to the farmer automat ## 1.3.0 Chia blockchain 2022-3-07 -### Added: +### Added - CAT wallet support - add wallets for your favorite CATs. - Offers - make, take, and share your offers. @@ -248,7 +282,7 @@ There is a known issue where harvesters will not reconnect to the farmer automat - Added *multiprocessing_start_method:* entry in config.yaml that allows setting the python *start method* for multiprocessing (default is *spawn* on Windows & MacOS, *fork* on Unix). - Added option to "Cancel transaction" accepted offers that are stuck in "pending". -### Changed: +### Changed - Lite wallet client sync updated to only require 3 peers instead of 5. - Only CATs from the default CAT list will be automatically added, all other unknown CATs will need to be manually added (thanks to @ojura, this behavior can be toggled in config.yaml). @@ -277,7 +311,7 @@ There is a known issue where harvesters will not reconnect to the farmer automat - It should not be expected that wallet info, such as payout address, should not reflect what their desired values until everything has completed syncing. - The payout instructions may not be editable via the GUI until syncing has completed. -### Fixed: +### Fixed - Offer history limit has been fixed to show all offers now instead of limiting to just 49 offers. - Fixed issues with using madmax CLI options -w, -G, -2, -t and -d (Issue 9163) (thanks @randomisresistance and @lasers8oclockday1). @@ -302,7 +336,7 @@ There is a known issue where harvesters will not reconnect to the farmer automat - Memory leak in the full node sync store where peak hashes were stored without being pruned. - Fixed a timelord issue which could cause a few blocks to not be infused on chain if a certain proof of space signs conflicting blocks. -### Known Issues: +### Known Issues - When you are adding plots and you choose the option to “create a Plot NFT”, you will get an error message “Initial_target_state” and the plots will not get created. - Workaround: Create the Plot NFT first in the “Pool” tab, and then add your plots and choose the created plot NFT in the drop down. @@ -317,11 +351,10 @@ There is a known issue where harvesters will not reconnect to the farmer automat ## 1.2.11 Chia blockchain 2021-11-4 -Farmers rejoice: today's release integrates two plotters in broad use in the Chia community: Bladebit, created by @harold-b, and Madmax, created by @madMAx43v3r. Both of these plotters bring significant improvements in plotting time. More plotting info [here](https://github.com/Chia-Network/chia-blockchain/wiki/Alternative--Plotters). -This release also includes several important performance improvements as a result of last weekends "Dust Storm", with two goals in mind: make sure everyone can farm at all times, and improve how many transactions per second each node can accept, especially for low-end hardware. Please know that these optimizations are only the first wave in a series of many over the next few releases to help address this going forward. While the changes we have implemented in this update may not necessarily solve for _every_ possible congestion scenario, they should go a long way towards helping low-end systems perform closer to expectations if this happens again. - ### Added +- Farmers rejoice: today's release integrates two plotters in broad use in the Chia community: Bladebit, created by @harold-b, and Madmax, created by @madMAx43v3r. Both of these plotters bring significant improvements in plotting time. More plotting info [here](https://github.com/Chia-Network/chia-blockchain/wiki/Alternative--Plotters). +- This release also includes several important performance improvements as a result of last weekends "Dust Storm", with two goals in mind: make sure everyone can farm at all times, and improve how many transactions per second each node can accept, especially for low-end hardware. Please know that these optimizations are only the first wave in a series of many over the next few releases to help address this going forward. While the changes we have implemented in this update may not necessarily solve for *every* possible congestion scenario, they should go a long way towards helping low-end systems perform closer to expectations if this happens again. - Performance improvements for nodes to support higher transaction volumes, especially for low powered devices like RaspBerry Pi. Full details at [#9050](https://github.com/Chia-Network/chia-blockchain/pull/9050). - Improved multi-core usage through process pools. - Prioritized block validation. @@ -349,7 +382,6 @@ This release also includes several important performance improvements as a resul - PlotNFT transactions via CLI (e.g. `chia plotnft join`) now accept a fee parameter, but it is not yet operable. - ## 1.2.10 Chia blockchain 2021-10-25 We have some great improvements in this release: We launched our migration of keys to a common encrypted keyring.yaml file, and we secure this with an optional passphrase in both GUI and CLI. We've added a passphrase hint in case you forget your passphrase. More info on our [wiki](https://github.com/Chia-Network/chia-blockchain/wiki/Passphrase-Protected-Chia-Keys-and-Key-Storage-Migration). We also launched a new Chialisp compiler in clvm_tools_rs which substantially improves compile time for Chialisp developers. We also addressed a widely reported issue in which a system failure, such as a power outage, would require some farmers to sync their full node from zero. This release also includes several other improvements and fixes. @@ -1991,7 +2023,7 @@ relic. We will make a patch available for these systems shortly. ### Added - There is now full transaction support on the Chia blockchain. In this initial Beta 1.0 release, all transaction types are supported though the wallets and UIs currently only directly support basic transactions like coinbase rewards and sending coins while paying fees. UI support for our [smart transactions](https://github.com/Chia-Network/wallets/blob/main/README.md) will be available in the UIs shortly. -- Wallet and Node GUI’s are available on Windows, Mac, and desktop Linux platforms. We now use an Electron UI that is a full light client wallet that can also serve as a node UI. Our Windows Electron Wallet can run standalone by connecting to other nodes on the network or another node you run. WSL 2 on Windows can run everything except the Wallet but you can run the Wallet on the native Windows side of the same machine. Also the WSL 2 install process is 3 times faster and _much_ easier. Windows native node/farmer/plotting functionality are coming soon. +- Wallet and Node GUI’s are available on Windows, Mac, and desktop Linux platforms. We now use an Electron UI that is a full light client wallet that can also serve as a node UI. Our Windows Electron Wallet can run standalone by connecting to other nodes on the network or another node you run. WSL 2 on Windows can run everything except the Wallet but you can run the Wallet on the native Windows side of the same machine. Also the WSL 2 install process is 3 times faster and *much* easier. Windows native node/farmer/plotting functionality are coming soon. - Install is significantly easier with less dependencies on all supported platforms. - If you’re a farmer you can use the Wallet to keep track of your earnings. Either use the same keys.yaml on the same machine or copy the keys.yaml to another machine where you want to track of and spend your coins. - We have continued to make improvements to the speed of VDF squaring, creating a VDF proof, and verifying a VDF proof. From 787e96b8edc6ed95ca7a6d6ade115a62e6bff672 Mon Sep 17 00:00:00 2001 From: William Allen Date: Tue, 26 Jul 2022 11:09:07 -0500 Subject: [PATCH 26/38] Updating gitmodules (#57) * Updating gitmodules * Pinning gui --- .gitmodules | 4 ++-- chia-blockchain-gui | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index 3f8ad5b5fd..6392285dd1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,7 +2,7 @@ path = mozilla-ca url = https://github.com/Chia-Network/mozilla-ca.git branch = main -[submodule "defender-gui"] +[submodule "chia-blockchain-gui"] path = chia-blockchain-gui - url = https://github.com/Chia-Network/defender-gui + url = https://github.com/Chia-Network/chia-blockchain-gui.git branch = release/1.5.0 diff --git a/chia-blockchain-gui b/chia-blockchain-gui index ca6cf9bee3..baa47b29db 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit ca6cf9bee377741170ebbc2813f0cd4f0c3f2c64 +Subproject commit baa47b29db10b0408c051cbf7da8f6e1f31a7b66 From 024c24c10d1e8d7833e000dd3c189578474eec37 Mon Sep 17 00:00:00 2001 From: Earle Lowe Date: Tue, 26 Jul 2022 16:13:48 -0700 Subject: [PATCH 27/38] black fixes --- chia/rpc/wallet_rpc_api.py | 11 +++++------ chia/wallet/trade_manager.py | 12 +++++++++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index e071f89cce..d9869ac923 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -1067,7 +1067,8 @@ class WalletRpcApi: trade_record, error, ) = await self.service.wallet_state_manager.trade_manager.create_offer_for_ids( - modified_offer, driver_dict, fee=fee, validate_only=validate_only, min_coin_amount=min_coin_amount) + modified_offer, driver_dict, fee=fee, validate_only=validate_only, min_coin_amount=min_coin_amount + ) if success: return { "offer": Offer.from_bytes(trade_record.offer).to_bech32(), @@ -1098,11 +1099,9 @@ class WalletRpcApi: min_coin_amount: uint128 = uint128(request.get("min_coin_amount", 0)) async with self.service.wallet_state_manager.lock: - ( - success, - trade_record, - error, - ) = await self.service.wallet_state_manager.trade_manager.respond_to_offer(offer, fee=fee, min_coin_amount=min_coin_amount) + (success, trade_record, error,) = await self.service.wallet_state_manager.trade_manager.respond_to_offer( + offer, fee=fee, min_coin_amount=min_coin_amount + ) if not success: raise ValueError(error) return {"trade_record": trade_record.to_json_dict_convenience()} diff --git a/chia/wallet/trade_manager.py b/chia/wallet/trade_manager.py index a9986150a8..530f5309bb 100644 --- a/chia/wallet/trade_manager.py +++ b/chia/wallet/trade_manager.py @@ -301,7 +301,9 @@ class TradeManager: ) -> Tuple[bool, Optional[TradeRecord], Optional[str]]: if driver_dict is None: driver_dict = {} - success, created_offer, error = await self._create_offer_for_ids(offer, driver_dict, fee=fee, min_coin_amount=min_coin_amount) + success, created_offer, error = await self._create_offer_for_ids( + offer, driver_dict, fee=fee, min_coin_amount=min_coin_amount + ) if not success or created_offer is None: raise Exception(f"Error creating offer: {error}") now = uint64(int(time.time())) @@ -583,7 +585,9 @@ class TradeManager: return txs - async def respond_to_offer(self, offer: Offer, fee=uint64(0), min_coin_amount: Optional[uint128] = None) -> Tuple[bool, Optional[TradeRecord], Optional[str]]: + async def respond_to_offer( + self, offer: Offer, fee=uint64(0), min_coin_amount: Optional[uint128] = None + ) -> Tuple[bool, Optional[TradeRecord], Optional[str]]: take_offer_dict: Dict[Union[bytes32, int], int] = {} arbitrage: Dict[Optional[bytes32], int] = offer.arbitrage() @@ -606,7 +610,9 @@ class TradeManager: valid: bool = await self.check_offer_validity(offer) if not valid: return False, None, "This offer is no longer valid" - success, take_offer, error = await self._create_offer_for_ids(take_offer_dict, offer.driver_dict, fee=fee, min_coin_amount=min_coin_amount) + success, take_offer, error = await self._create_offer_for_ids( + take_offer_dict, offer.driver_dict, fee=fee, min_coin_amount=min_coin_amount + ) if not success or take_offer is None: return False, None, error From 7bdeee3f16f104f5b172b10ff7a012a591b286be Mon Sep 17 00:00:00 2001 From: Earle Lowe Date: Tue, 26 Jul 2022 16:34:18 -0700 Subject: [PATCH 28/38] mypy fixes --- chia/util/files.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/chia/util/files.py b/chia/util/files.py index b122132ca8..dde9d1fa45 100644 --- a/chia/util/files.py +++ b/chia/util/files.py @@ -3,10 +3,11 @@ import logging import os import shutil -from aiofiles import tempfile # type: ignore from pathlib import Path from typing import Union +from aiofiles import tempfile +from typing_extensions import Literal log = logging.getLogger(__name__) @@ -66,7 +67,7 @@ async def write_file_async(file_path: Path, data: Union[str, bytes], *, file_mod # Create the parent directory if necessary os.makedirs(file_path.parent, mode=dir_mode, exist_ok=True) - mode: str = "w+" if type(data) == str else "w+b" + mode: Literal["w+", "w+b"] = "w+" if type(data) == str else "w+b" temp_file_path: Path async with tempfile.NamedTemporaryFile(dir=file_path.parent, mode=mode, delete=False) as f: temp_file_path = f.name From 482c231976650c28df64a8ce82c6b69fdcb48372 Mon Sep 17 00:00:00 2001 From: Earle Lowe Date: Tue, 26 Jul 2022 17:13:50 -0700 Subject: [PATCH 29/38] xfail some run_block tests that need CAT2 update --- tests/tools/test_run_block.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/tools/test_run_block.py b/tests/tools/test_run_block.py index 140f9c9521..a6bd8afe54 100644 --- a/tests/tools/test_run_block.py +++ b/tests/tools/test_run_block.py @@ -2,6 +2,8 @@ import json from pathlib import Path from typing import List +import pytest + from chia.consensus.default_constants import DEFAULT_CONSTANTS from chia.types.condition_opcodes import ConditionOpcode from chia.types.condition_with_args import ConditionWithArgs @@ -53,6 +55,7 @@ def test_block_no_generator(): assert not cat_list +@pytest.mark.xfail(reason="Needs update to CAT2") def test_block_retired_cat_with_memo(): dirname = Path(__file__).parent with open(dirname / "396963.json") as f: @@ -73,6 +76,7 @@ def test_block_retired_cat_with_memo(): assert found +@pytest.mark.xfail(reason="Needs update to CAT2") def test_block_retired_cat_no_memo(): dirname = Path(__file__).parent with open(dirname / "392111.json") as f: @@ -94,6 +98,7 @@ def test_block_retired_cat_no_memo(): assert found +@pytest.mark.xfail(reason="Needs update to CAT2") def test_block_cat(): dirname = Path(__file__).parent with open(dirname / "149988.json") as f: From 17a3c7444904b9791b852900d55e0c4fab2e2877 Mon Sep 17 00:00:00 2001 From: Jack Nelson Date: Tue, 26 Jul 2022 20:25:28 -0400 Subject: [PATCH 30/38] fix test --- tests/clvm/test_puzzle_compression.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/clvm/test_puzzle_compression.py b/tests/clvm/test_puzzle_compression.py index 03b91e615d..028d19e565 100644 --- a/tests/clvm/test_puzzle_compression.py +++ b/tests/clvm/test_puzzle_compression.py @@ -83,7 +83,7 @@ class TestPuzzleCompression: self.compression_factors["unknown_and_standard"] = len(bytes(compressed)) / len(bytes(coin_spend)) def test_lowest_best_version(self): - assert lowest_best_version([bytes(CAT_MOD)]) == 1 + assert lowest_best_version([bytes(CAT_MOD)]) == 4 assert lowest_best_version([bytes(OFFER_MOD)]) == 2 def test_version_override(self): From 7b03900bb42bd86fc990a8f1093e67c147e5b704 Mon Sep 17 00:00:00 2001 From: Earle Lowe Date: Tue, 26 Jul 2022 17:53:28 -0700 Subject: [PATCH 31/38] Fix test based on series<->edition changes --- tests/wallet/nft_wallet/test_nft_wallet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/wallet/nft_wallet/test_nft_wallet.py b/tests/wallet/nft_wallet/test_nft_wallet.py index cb7f9f08ef..f8c5410a39 100644 --- a/tests/wallet/nft_wallet/test_nft_wallet.py +++ b/tests/wallet/nft_wallet/test_nft_wallet.py @@ -775,8 +775,8 @@ async def test_nft_rpc_mint(two_wallet_nodes: Any, trusted: Any) -> None: "license_uris": license_uris, "license_hash": license_hash, "meta_hash": meta_hash, - "series_number": sn, - "series_total": st, + "edition_number": sn, + "edition_total": st, "meta_uris": meta_uris, "royalty_address": royalty_address, "target_address": ph, From 06c1c7be9487ff53f0486480099e11f2bb28b507 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 23 Jul 2022 23:51:41 -0400 Subject: [PATCH 32/38] Drop EOL impish and hirsute (#12559) (cherry picked from commit 189790ced27c5dfc71df5f0035252c653f085152) --- .github/workflows/test-install-scripts.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/test-install-scripts.yml b/.github/workflows/test-install-scripts.yml index 58a86d14c7..63169dcc1e 100644 --- a/.github/workflows/test-install-scripts.yml +++ b/.github/workflows/test-install-scripts.yml @@ -107,14 +107,6 @@ jobs: type: ubuntu # https://packages.ubuntu.com/focal/python3 (20.04, 3.8) url: "docker://ubuntu:focal" - - name: ubuntu:hirsute (21.04) - type: ubuntu - # https://packages.ubuntu.com/hirsute/python3 (21.04, 3.9) - url: "docker://ubuntu:hirsute" - - name: ubuntu:impish (21.10) - type: ubuntu - # https://packages.ubuntu.com/impish/python3 (21.10, 3.9) - url: "docker://ubuntu:impish" - name: ubuntu:jammy (22.04) type: ubuntu # https://packages.ubuntu.com/jammy/python3 (22.04, 3.10) From 20ad937fd507994d6d8a8afa5f9079e35e48a896 Mon Sep 17 00:00:00 2001 From: Jack Nelson Date: Fri, 15 Jul 2022 21:50:41 -0400 Subject: [PATCH 33/38] Expand select_coins rpc (#12360) * change type to uint64, 128 is too big anyway * add new options to select_coins endpoint * oops * add tests and finalize * oops (cherry picked from commit d5bf4d8b59e3f0757f046ff6c9b7b2fd878920b4) --- chia/cmds/wallet_funcs.py | 8 +++--- chia/rpc/wallet_rpc_api.py | 20 ++++++++----- chia/rpc/wallet_rpc_client.py | 30 +++++++++++++------ chia/wallet/cat_wallet/cat_wallet.py | 10 +++---- chia/wallet/coin_selection.py | 4 +-- chia/wallet/did_wallet/did_wallet.py | 2 +- chia/wallet/nft_wallet/nft_wallet.py | 2 +- chia/wallet/trade_manager.py | 8 +++--- chia/wallet/wallet.py | 8 +++--- tests/wallet/rpc/test_wallet_rpc.py | 43 ++++++++++++++++++++++++++++ tests/wallet/test_coin_selection.py | 2 +- 11 files changed, 100 insertions(+), 37 deletions(-) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 327767d41d..e94cdcf765 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -18,7 +18,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.bech32m import bech32_decode, decode_puzzle_hash, encode_puzzle_hash from chia.util.config import load_config from chia.util.default_root import DEFAULT_ROOT_PATH -from chia.util.ints import uint16, uint32, uint64, uint128 +from chia.util.ints import uint16, uint32, uint64 from chia.wallet.did_wallet.did_info import DID_HRP from chia.wallet.nft_wallet.nft_info import NFT_HRP, NFTInfo from chia.wallet.trade_record import TradeRecord @@ -213,17 +213,17 @@ async def send(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> final_fee = uint64(int(fee * units["chia"])) final_amount: uint64 - final_min_coin_amount: uint128 + final_min_coin_amount: uint64 if typ == WalletType.STANDARD_WALLET: final_amount = uint64(int(amount * units["chia"])) - final_min_coin_amount = uint128(int(min_coin_amount * units["chia"])) + final_min_coin_amount = uint64(int(min_coin_amount * units["chia"])) print("Submitting transaction...") res = await wallet_client.send_transaction( str(wallet_id), final_amount, address, final_fee, memos, final_min_coin_amount ) elif typ == WalletType.CAT: final_amount = uint64(int(amount * units["cat"])) - final_min_coin_amount = uint128(int(min_coin_amount * units["cat"])) + final_min_coin_amount = uint64(int(min_coin_amount * units["cat"])) print("Submitting transaction...") res = await wallet_client.cat_spend( str(wallet_id), final_amount, address, final_fee, memos, final_min_coin_amount diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index d9869ac923..3d5c25e61b 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -23,7 +23,7 @@ from chia.types.spend_bundle import SpendBundle from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash from chia.util.byte_types import hexstr_to_bytes from chia.util.config import load_config -from chia.util.ints import uint8, uint32, uint64, uint16, uint128 +from chia.util.ints import uint8, uint32, uint64, uint16 from chia.util.keychain import KeyringIsLocked, bytes_to_mnemonic, generate_mnemonic from chia.util.path import path_from_root from chia.util.ws_message import WsRpcMessage, create_payload_dict @@ -840,7 +840,7 @@ class WalletRpcApi: memos = [mem.encode("utf-8") for mem in request["memos"]] fee: uint64 = uint64(request.get("fee", 0)) - min_coin_amount: uint128 = uint128(request.get("min_coin_amount", 0)) + min_coin_amount: uint64 = uint64(request.get("min_coin_amount", 0)) async with self.service.wallet_state_manager.lock: tx: TransactionRecord = await wallet.generate_signed_transaction( amount, puzzle_hash, fee, memos=memos, min_coin_amount=min_coin_amount @@ -896,10 +896,16 @@ class WalletRpcApi: amount = uint64(request["amount"]) wallet_id = uint32(request["wallet_id"]) + min_coin_amount = uint64(request.get("min_coin_amount", 0)) + excluded_coins: Optional[List] = request.get("excluded_coins") + if excluded_coins is not None: + excluded_coins = [Coin.from_json_dict(json_coin) for json_coin in excluded_coins] wallet = self.service.wallet_state_manager.wallets[wallet_id] async with self.service.wallet_state_manager.lock: - selected_coins = await wallet.select_coins(amount=amount) + selected_coins = await wallet.select_coins( + amount=amount, min_coin_amount=min_coin_amount, exclude=excluded_coins + ) return {"coins": [coin.to_json_dict() for coin in selected_coins]} @@ -999,7 +1005,7 @@ class WalletRpcApi: raise ValueError("An integer amount or fee is required (too many decimals)") amount: uint64 = uint64(request["amount"]) fee: uint64 = uint64(request.get("fee", 0)) - min_coin_amount: uint128 = uint128(request.get("min_coin_amount", 0)) + min_coin_amount: uint64 = uint64(request.get("min_coin_amount", 0)) async with self.service.wallet_state_manager.lock: txs: List[TransactionRecord] = await wallet.generate_signed_transaction( [amount], [puzzle_hash], fee, memos=[memos], min_coin_amount=min_coin_amount @@ -1037,7 +1043,7 @@ class WalletRpcApi: fee: uint64 = uint64(request.get("fee", 0)) validate_only: bool = request.get("validate_only", False) driver_dict_str: Optional[Dict[str, Any]] = request.get("driver_dict", None) - min_coin_amount: uint128 = uint128(request.get("min_coin_amount", 0)) + min_coin_amount: uint64 = uint64(request.get("min_coin_amount", 0)) # This driver_dict construction is to maintain backward compatibility where everything is assumed to be a CAT driver_dict: Dict[bytes32, PuzzleInfo] = {} @@ -1096,7 +1102,7 @@ class WalletRpcApi: offer_hex: str = request["offer"] offer = Offer.from_bech32(offer_hex) fee: uint64 = uint64(request.get("fee", 0)) - min_coin_amount: uint128 = uint128(request.get("min_coin_amount", 0)) + min_coin_amount: uint64 = uint64(request.get("min_coin_amount", 0)) async with self.service.wallet_state_manager.lock: (success, trade_record, error,) = await self.service.wallet_state_manager.trade_manager.respond_to_offer( @@ -1805,7 +1811,7 @@ class WalletRpcApi: additional_outputs.append({"puzzlehash": receiver_ph, "amount": amount, "memos": memos}) fee: uint64 = uint64(request.get("fee", 0)) - min_coin_amount: uint128 = uint128(request.get("min_coin_amount", 0)) + min_coin_amount: uint64 = uint64(request.get("min_coin_amount", 0)) coins = None if "coins" in request and len(request["coins"]) > 0: diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 9f164b3185..3727e30243 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -5,7 +5,7 @@ from chia.rpc.rpc_client import RpcClient from chia.types.announcement import Announcement from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.util.ints import uint32, uint64, uint128 +from chia.util.ints import uint32, uint64 from chia.wallet.trade_record import TradeRecord from chia.wallet.trading.offer import Offer from chia.wallet.transaction_record import TransactionRecord @@ -146,7 +146,7 @@ class WalletRpcClient(RpcClient): address: str, fee: uint64 = uint64(0), memos: Optional[List[str]] = None, - min_coin_amount: uint128 = uint128(0), + min_coin_amount: uint64 = uint64(0), ) -> TransactionRecord: if memos is None: send_dict: Dict = { @@ -213,7 +213,7 @@ class WalletRpcClient(RpcClient): fee: uint64 = uint64(0), coin_announcements: Optional[List[Announcement]] = None, puzzle_announcements: Optional[List[Announcement]] = None, - min_coin_amount: uint128 = uint128(0), + min_coin_amount: uint64 = uint64(0), ) -> TransactionRecord: # Converts bytes to hex for puzzle hashes additions_hex = [] @@ -255,8 +255,22 @@ class WalletRpcClient(RpcClient): response: Dict = await self.fetch("create_signed_transaction", request) return TransactionRecord.from_json_dict_convenience(response["signed_tx"]) - async def select_coins(self, *, amount: int, wallet_id: int) -> List[Coin]: - request = {"amount": amount, "wallet_id": wallet_id} + async def select_coins( + self, + *, + amount: int, + wallet_id: int, + excluded_coins: Optional[List[Coin]] = None, + min_coin_amount: uint64 = uint64(0), + ) -> List[Coin]: + if excluded_coins is None: + excluded_coins = [] + request = { + "amount": amount, + "wallet_id": wallet_id, + "min_coin_amount": min_coin_amount, + "excluded_coins": [excluded_coin.to_json_dict() for excluded_coin in excluded_coins], + } response: Dict[str, List[Dict]] = await self.fetch("select_coins", request) return [Coin.from_json_dict(coin) for coin in response["coins"]] @@ -505,7 +519,7 @@ class WalletRpcClient(RpcClient): inner_address: str, fee: uint64 = uint64(0), memos: Optional[List[str]] = None, - min_coin_amount: uint128 = uint128(0), + min_coin_amount: uint64 = uint64(0), ) -> TransactionRecord: send_dict = { "wallet_id": wallet_id, @@ -525,7 +539,7 @@ class WalletRpcClient(RpcClient): driver_dict: Dict[str, Any] = None, fee=uint64(0), validate_only: bool = False, - min_coin_amount: uint128 = uint128(0), + min_coin_amount: uint64 = uint64(0), ) -> Tuple[Optional[Offer], TradeRecord]: send_dict: Dict[str, int] = {} for key in offer_dict: @@ -552,7 +566,7 @@ class WalletRpcClient(RpcClient): res = await self.fetch("check_offer_validity", {"offer": offer.to_bech32()}) return res["valid"] - async def take_offer(self, offer: Offer, fee=uint64(0), min_coin_amount: uint128 = uint128(0)) -> TradeRecord: + async def take_offer(self, offer: Offer, fee=uint64(0), min_coin_amount: uint64 = uint64(0)) -> TradeRecord: res = await self.fetch( "take_offer", {"offer": offer.to_bech32(), "fee": fee, "min_coin_amount": min_coin_amount} ) diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index f5f5f8c59f..e40baeb11e 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -445,7 +445,7 @@ class CATWallet: return result async def select_coins( - self, amount: uint64, exclude: Optional[List[Coin]] = None, min_coin_amount: Optional[uint128] = None + self, amount: uint64, exclude: Optional[List[Coin]] = None, min_coin_amount: Optional[uint64] = None ) -> Set[Coin]: """ Returns a set of coins that can be used for generating a new transaction. @@ -532,7 +532,7 @@ class CATWallet: fee: uint64, amount_to_claim: uint64, announcement_to_assert: Optional[Announcement] = None, - min_coin_amount: Optional[uint128] = None, + min_coin_amount: Optional[uint64] = None, ) -> Tuple[TransactionRecord, Optional[Announcement]]: """ This function creates a non-CAT transaction to pay fees, contribute funds for issuance, and absorb melt value. @@ -586,7 +586,7 @@ class CATWallet: coins: Set[Coin] = None, coin_announcements_to_consume: Optional[Set[Announcement]] = None, puzzle_announcements_to_consume: Optional[Set[Announcement]] = None, - min_coin_amount: Optional[uint128] = None, + min_coin_amount: Optional[uint64] = None, ) -> Tuple[SpendBundle, Optional[TransactionRecord]]: if coin_announcements_to_consume is not None: coin_announcements_bytes: Optional[Set[bytes32]] = {a.name() for a in coin_announcements_to_consume} @@ -722,7 +722,7 @@ class CATWallet: memos: Optional[List[List[bytes]]] = None, coin_announcements_to_consume: Optional[Set[Announcement]] = None, puzzle_announcements_to_consume: Optional[Set[Announcement]] = None, - min_coin_amount: Optional[uint128] = None, + min_coin_amount: Optional[uint64] = None, ) -> List[TransactionRecord]: if memos is None: memos = [[] for _ in range(len(puzzle_hashes))] @@ -828,7 +828,7 @@ class CATWallet: return PuzzleInfo({"type": AssetType.CAT.value, "tail": "0x" + self.get_asset_id()}) async def get_coins_to_offer( - self, asset_id: Optional[bytes32], amount: uint64, min_coin_amount: Optional[uint128] = None + self, asset_id: Optional[bytes32], amount: uint64, min_coin_amount: Optional[uint64] = None ) -> Set[Coin]: balance = await self.get_confirmed_balance() if balance < amount: diff --git a/chia/wallet/coin_selection.py b/chia/wallet/coin_selection.py index 2ce747de7d..c9c764412b 100644 --- a/chia/wallet/coin_selection.py +++ b/chia/wallet/coin_selection.py @@ -16,7 +16,7 @@ async def select_coins( log: logging.Logger, amount: uint128, exclude: Optional[List[Coin]] = None, - min_coin_amount: Optional[uint128] = None, + min_coin_amount: Optional[uint64] = None, ) -> Set[Coin]: """ Returns a set of coins that can be used for generating a new transaction. @@ -24,7 +24,7 @@ async def select_coins( if exclude is None: exclude = [] if min_coin_amount is None: - min_coin_amount = uint128(0) + min_coin_amount = uint64(0) if amount > spendable_amount: error_msg = ( diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index 54ce636a3f..70c756df1b 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -299,7 +299,7 @@ class DIDWallet: return await self.wallet_state_manager.get_unconfirmed_balance(self.id(), record_list) async def select_coins( - self, amount: uint64, exclude: Optional[List[Coin]] = None, min_coin_amount: Optional[uint128] = None + self, amount: uint64, exclude: Optional[List[Coin]] = None, min_coin_amount: Optional[uint64] = None ) -> Optional[Set[Coin]]: """ Returns a set of coins that can be used for generating a new transaction. diff --git a/chia/wallet/nft_wallet/nft_wallet.py b/chia/wallet/nft_wallet/nft_wallet.py index f86573482d..e99c5ac22e 100644 --- a/chia/wallet/nft_wallet/nft_wallet.py +++ b/chia/wallet/nft_wallet/nft_wallet.py @@ -547,7 +547,7 @@ class NFTWallet: return puzzle_info async def get_coins_to_offer( - self, nft_id: bytes32, amount: uint64, min_coin_amount: Optional[uint128] = None + self, nft_id: bytes32, amount: uint64, min_coin_amount: Optional[uint64] = None ) -> Set[Coin]: nft_coin: Optional[NFTCoinInfo] = self.get_nft(nft_id) if nft_coin is None: diff --git a/chia/wallet/trade_manager.py b/chia/wallet/trade_manager.py index 530f5309bb..d426c2f158 100644 --- a/chia/wallet/trade_manager.py +++ b/chia/wallet/trade_manager.py @@ -11,7 +11,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.spend_bundle import SpendBundle from chia.util.db_wrapper import DBWrapper from chia.util.hash import std_hash -from chia.util.ints import uint32, uint64, uint128 +from chia.util.ints import uint32, uint64 from chia.wallet.nft_wallet.nft_wallet import NFTWallet from chia.wallet.outer_puzzles import AssetType from chia.wallet.payment import Payment @@ -297,7 +297,7 @@ class TradeManager: driver_dict: Optional[Dict[bytes32, PuzzleInfo]] = None, fee: uint64 = uint64(0), validate_only: bool = False, - min_coin_amount: Optional[uint128] = None, + min_coin_amount: Optional[uint64] = None, ) -> Tuple[bool, Optional[TradeRecord], Optional[str]]: if driver_dict is None: driver_dict = {} @@ -331,7 +331,7 @@ class TradeManager: offer_dict: Dict[Union[int, bytes32], int], driver_dict: Optional[Dict[bytes32, PuzzleInfo]] = None, fee: uint64 = uint64(0), - min_coin_amount: Optional[uint128] = None, + min_coin_amount: Optional[uint64] = None, ) -> Tuple[bool, Optional[Offer], Optional[str]]: """ Offer is dictionary of wallet ids and amount @@ -586,7 +586,7 @@ class TradeManager: return txs async def respond_to_offer( - self, offer: Offer, fee=uint64(0), min_coin_amount: Optional[uint128] = None + self, offer: Offer, fee=uint64(0), min_coin_amount: Optional[uint64] = None ) -> Tuple[bool, Optional[TradeRecord], Optional[str]]: take_offer_dict: Dict[Union[bytes32, int], int] = {} arbitrage: Dict[Optional[bytes32], int] = offer.arbitrage() diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 2b76b8573f..b3eb9d6111 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -247,7 +247,7 @@ class Wallet: return Program.to(python_program) async def select_coins( - self, amount: uint64, exclude: Optional[List[Coin]] = None, min_coin_amount: Optional[uint128] = None + self, amount: uint64, exclude: Optional[List[Coin]] = None, min_coin_amount: Optional[uint64] = None ) -> Set[Coin]: """ Returns a set of coins that can be used for generating a new transaction. @@ -291,7 +291,7 @@ class Wallet: puzzle_announcements_to_consume: Set[Announcement] = None, memos: Optional[List[bytes]] = None, negative_change_allowed: bool = False, - min_coin_amount: Optional[uint128] = None, + min_coin_amount: Optional[uint64] = None, ) -> List[CoinSpend]: """ Generates a unsigned transaction in form of List(Puzzle, Solutions) @@ -418,7 +418,7 @@ class Wallet: puzzle_announcements_to_consume: Set[Announcement] = None, memos: Optional[List[bytes]] = None, negative_change_allowed: bool = False, - min_coin_amount: Optional[uint128] = None, + min_coin_amount: Optional[uint64] = None, ) -> TransactionRecord: """ Use this to generate transaction. @@ -531,7 +531,7 @@ class Wallet: return spend_bundle async def get_coins_to_offer( - self, asset_id: Optional[bytes32], amount: uint64, min_coin_amount: Optional[uint128] = None + self, asset_id: Optional[bytes32], amount: uint64, min_coin_amount: Optional[uint64] = None ) -> Set[Coin]: if asset_id is not None: raise ValueError(f"The standard wallet cannot offer coins with asset id {asset_id}") diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 17e73b7c49..29fbd6a75d 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -19,6 +19,7 @@ from chia.server.server import ChiaServer from chia.simulator.full_node_simulator import FullNodeSimulator from chia.simulator.simulator_protocol import FarmNewBlockProtocol from chia.types.announcement import Announcement +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_record import CoinRecord @@ -1014,3 +1015,45 @@ async def test_key_and_address_endpoints(wallet_rpc_environment: WalletRpcTestEn # Delete all keys await client.delete_all_keys() assert len(await client.get_public_keys()) == 0 + + +@pytest.mark.asyncio +async def test_select_coins_rpc(wallet_rpc_environment: WalletRpcTestEnvironment): + env: WalletRpcTestEnvironment = wallet_rpc_environment + + wallet_2: Wallet = env.wallet_2.wallet + wallet_node: WalletNode = env.wallet_1.node + full_node_api: FullNodeSimulator = env.full_node.api + client: WalletRpcClient = env.wallet_1.rpc_client + client_2: WalletRpcClient = env.wallet_2.rpc_client + + funds = await generate_funds(full_node_api, env.wallet_1) + + addr = encode_puzzle_hash(await wallet_2.get_new_puzzlehash(), "txch") + coin_300: List[Coin] + for tx_amount in [uint64(1000), uint64(300), uint64(1000), uint64(1000), uint64(10000)]: + funds -= tx_amount + # create coins for tests + tx = await client.send_transaction("1", tx_amount, addr) + spend_bundle = tx.spend_bundle + assert spend_bundle is not None + for coin in spend_bundle.additions(): + if coin.amount == uint64(300): + coin_300 = [coin] + + await time_out_assert(5, tx_in_mempool, True, client, tx.name) + await farm_transaction(full_node_api, wallet_node, spend_bundle) + await time_out_assert(5, get_confirmed_balance, funds, client, 1) + + # test min coin amount + min_coins: List[Coin] = await client_2.select_coins(amount=1000, wallet_id=1, min_coin_amount=uint64(1001)) + assert min_coins is not None + assert len(min_coins) == 1 and min_coins[0].amount == uint64(10000) + + # test excluded coins + with pytest.raises(ValueError): + await client_2.select_coins(amount=5000, wallet_id=1, excluded_coins=min_coins) + excluded_test = await client_2.select_coins(amount=1300, wallet_id=1, excluded_coins=coin_300) + assert len(excluded_test) == 2 + for coin in excluded_test: + assert coin != coin_300[0] diff --git a/tests/wallet/test_coin_selection.py b/tests/wallet/test_coin_selection.py index db7fa5edc6..7e28f0adf3 100644 --- a/tests/wallet/test_coin_selection.py +++ b/tests/wallet/test_coin_selection.py @@ -449,7 +449,7 @@ class TestCoinSelection: {}, logging.getLogger("test"), uint128(target_amount), - min_coin_amount=uint128(min_coin_amount), + min_coin_amount=uint64(min_coin_amount), ) assert result is not None # this should never happen assert sum(coin.amount for coin in result) >= target_amount From adee268889fa8e7b8fe8c5ec0198ef58327b071f Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 27 Jul 2022 10:16:38 -0700 Subject: [PATCH 34/38] .resolve() for wallet db path tests in windows --- tests/wallet/test_wallet.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index 68e5caebe7..dc634ed6a4 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -897,7 +897,7 @@ class TestWalletSimulator: def test_get_wallet_db_path_v2_r1() -> None: - root_path: Path = Path("/x/y/z/.chia/mainnet") + root_path: Path = Path("/x/y/z/.chia/mainnet").resolve() config: Dict[str, Any] = { "database_path": "wallet/db/blockchain_wallet_v2_r1_CHALLENGE_KEY.sqlite", "selected_network": "mainnet", @@ -909,7 +909,7 @@ def test_get_wallet_db_path_v2_r1() -> None: def test_get_wallet_db_path_v2() -> None: - root_path: Path = Path("/x/y/z/.chia/mainnet") + root_path: Path = Path("/x/y/z/.chia/mainnet").resolve() config: Dict[str, Any] = { "database_path": "wallet/db/blockchain_wallet_v2_CHALLENGE_KEY.sqlite", "selected_network": "mainnet", @@ -921,7 +921,7 @@ def test_get_wallet_db_path_v2() -> None: def test_get_wallet_db_path_v1() -> None: - root_path: Path = Path("/x/y/z/.chia/mainnet") + root_path: Path = Path("/x/y/z/.chia/mainnet").resolve() config: Dict[str, Any] = { "database_path": "wallet/db/blockchain_wallet_v1_CHALLENGE_KEY.sqlite", "selected_network": "mainnet", @@ -933,7 +933,7 @@ def test_get_wallet_db_path_v1() -> None: def test_get_wallet_db_path_testnet() -> None: - root_path: Path = Path("/x/y/z/.chia/testnet") + root_path: Path = Path("/x/y/z/.chia/testnet").resolve() config: Dict[str, Any] = { "database_path": "wallet/db/blockchain_wallet_v2_CHALLENGE_KEY.sqlite", "selected_network": "testnet", From c9c0e7eb6acb95fe3a3ba9aa39ef69730417d368 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 27 Jul 2022 20:00:44 -0700 Subject: [PATCH 35/38] followup to actually fix the wallet db path tests --- tests/wallet/test_wallet.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/wallet/test_wallet.py b/tests/wallet/test_wallet.py index dc634ed6a4..9884c05fda 100644 --- a/tests/wallet/test_wallet.py +++ b/tests/wallet/test_wallet.py @@ -905,7 +905,7 @@ def test_get_wallet_db_path_v2_r1() -> None: fingerprint: str = "1234567890" wallet_db_path: Path = get_wallet_db_path(root_path, config, fingerprint) - assert wallet_db_path == Path("/x/y/z/.chia/mainnet/wallet/db/blockchain_wallet_v2_r1_mainnet_1234567890.sqlite") + assert wallet_db_path == root_path.joinpath("wallet/db/blockchain_wallet_v2_r1_mainnet_1234567890.sqlite") def test_get_wallet_db_path_v2() -> None: @@ -917,7 +917,7 @@ def test_get_wallet_db_path_v2() -> None: fingerprint: str = "1234567890" wallet_db_path: Path = get_wallet_db_path(root_path, config, fingerprint) - assert wallet_db_path == Path("/x/y/z/.chia/mainnet/wallet/db/blockchain_wallet_v2_r1_mainnet_1234567890.sqlite") + assert wallet_db_path == root_path.joinpath("wallet/db/blockchain_wallet_v2_r1_mainnet_1234567890.sqlite") def test_get_wallet_db_path_v1() -> None: @@ -929,7 +929,7 @@ def test_get_wallet_db_path_v1() -> None: fingerprint: str = "1234567890" wallet_db_path: Path = get_wallet_db_path(root_path, config, fingerprint) - assert wallet_db_path == Path("/x/y/z/.chia/mainnet/wallet/db/blockchain_wallet_v2_r1_mainnet_1234567890.sqlite") + assert wallet_db_path == root_path.joinpath("wallet/db/blockchain_wallet_v2_r1_mainnet_1234567890.sqlite") def test_get_wallet_db_path_testnet() -> None: @@ -941,4 +941,4 @@ def test_get_wallet_db_path_testnet() -> None: fingerprint: str = "1234567890" wallet_db_path: Path = get_wallet_db_path(root_path, config, fingerprint) - assert wallet_db_path == Path("/x/y/z/.chia/testnet/wallet/db/blockchain_wallet_v2_r1_testnet_1234567890.sqlite") + assert wallet_db_path == root_path.joinpath("wallet/db/blockchain_wallet_v2_r1_testnet_1234567890.sqlite") From 7fa3bf7c1f8237c5ae6c48baedd9866bd5d82a52 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 28 Jul 2022 01:20:30 -0400 Subject: [PATCH 36/38] Update .gitmodules --- .gitmodules | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitmodules b/.gitmodules index 6392285dd1..596e3d8820 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,8 +1,8 @@ +[submodule "chia-blockchain-gui"] + path = chia-blockchain-gui + url = https://github.com/Chia-Network/chia-blockchain-gui.git + branch = main [submodule "mozilla-ca"] path = mozilla-ca url = https://github.com/Chia-Network/mozilla-ca.git branch = main -[submodule "chia-blockchain-gui"] - path = chia-blockchain-gui - url = https://github.com/Chia-Network/chia-blockchain-gui.git - branch = release/1.5.0 From 085dec9bbe59e979213ae67b2744dcd4d04b76a2 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 28 Jul 2022 02:40:09 -0400 Subject: [PATCH 37/38] Update wallet_node.py --- chia/wallet/wallet_node.py | 1 - 1 file changed, 1 deletion(-) diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index a4318faace..d3ef93b180 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -228,7 +228,6 @@ class WalletNode: path: Path = get_wallet_db_path(self.root_path, self.config, str(private_key.get_g1().get_fingerprint())) path.parent.mkdir(parents=True, exist_ok=True) - assert self.server is not None self._wallet_state_manager = await WalletStateManager.create( private_key, self.config, From d6db255c0e9c908a50db020b1c4c43178a9d7d25 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 28 Jul 2022 02:46:43 -0400 Subject: [PATCH 38/38] Update trade_manager.py --- chia/wallet/trade_manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/chia/wallet/trade_manager.py b/chia/wallet/trade_manager.py index 8c891fe1f1..0dcac8609a 100644 --- a/chia/wallet/trade_manager.py +++ b/chia/wallet/trade_manager.py @@ -308,6 +308,7 @@ class TradeManager: raise Exception(f"Error creating offer: {result[2]}") success, created_offer, error = result + now = uint64(int(time.time())) trade_offer: TradeRecord = TradeRecord( confirmed_at_index=uint32(0),