From d0de8038cd95b71fa050f79e3685c51dcf05e13e Mon Sep 17 00:00:00 2001 From: Amine Khaldi Date: Fri, 27 May 2022 19:47:08 +0100 Subject: [PATCH] Bring DID/NFT effort into release/1.4.0 (#11662) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * initial commit * assert coin added in test * add support functions * add RPC calls (untestested and surely unworking) also change innerpuz * missing comma * update puzzle clvm hex sha256 * fixes * add load_wallet for nft_wallet * add singleton 1.1 and update innerpuz for it * update wallet for new innerpuz * use singleton 1.1 * swap DID to singleton 1.1 * Fix DID Wallet and DID Innerpuz * list hint * don't push automatically * change to determine_coin_type * stash debug * fix get_hints() * small fix en route to bigger fix later * push for straya * checkpoint * coin discovered in wallet * re-enable push of attests because message spends are different * change a few node calls for rebase and rebase * black and flake8 * flake8 test formatting * Add transfer test and some transfer functions * fix bug where innerpuzzle would not recreate itself properly * fix get_info for received NFT and reovme breakpoint * fix messages * debug commit * receive and send it back! * send it back and remove NFTs after they're sent * fix adding duplicates bug * mint NFT with RPC calls test * save newer coin info over older info * finish RPC tests * update to new version of singleton black * change puzzle to remove need for receive spend when trade_price = 0 * Begin conversion to trade price list * fix test to new trade_price_list system * Move towards goals in chialisp * fix clvm testss * remove unused code * more chialisp changes working towards CAT royalties * much progress on CAT royalties * round down to even on royalties * add delay to fix inconsistent tests * remove unused file * add assertions and fine tune sleep * add richer support for URIs / metadata and add periphery functions * switch to metadata, switch to puzzle announcements * update innerpuz commit hash * add clvm files to recomp list * fix rpc test * flake8 fixes for tests * flake 8 stupid fix * Big change to NFT innerpuz (#11141) * new innerpuz with passing clvm tests * more work on url updating * fix broken add_url tests * fix nft_wallet generate_nft code * more progress * fix wallet and change to auto-send on tradeprice == 0 * flake8 and black * NFTs are always amount 1 removed some unnecessary new_did info detritus from wallet * move trade_price_list into tp_solution * Update chia/wallet/nft_wallet/nft_puzzles.py * Update chia/wallet/nft_wallet/nft_puzzles.py * Update chia/wallet/nft_wallet/nft_puzzles.py * Update chia/wallet/nft_wallet/nft_puzzles.py * Update chia/wallet/nft_wallet/nft_puzzles.py * Update chia/wallet/nft_wallet/nft_puzzles.py * Update chia/wallet/nft_wallet/nft_puzzles.py * Update chia/wallet/nft_wallet/nft_puzzles.py * Update chia/wallet/nft_wallet/nft_puzzles.py * Update chia/wallet/nft_wallet/nft_puzzles.py * typing for create_full_puzzle_with_curry_params * Typing for update_metadata * Typing for get_trade_prices_list_from_inner_solution Co-authored-by: Jeff * Merge commit with main (9ff3fc993f69b5c59ecf14470284a5b9e799e6cf) (#11137) * Merged with main (9ff3fc993f69b5c59ecf14470284a5b9e799e6cf) * Linter and some test fixes * isort, linting, test fix * Fixes and bumped the GUI to point to nftdev * Couple of fixes * Bumping GUI to 7aa199a4fb49361e6005c1c9cbdc54f269f9220b * Bumping GUI to 155866f6d060944b29b40c147f7f02eaab2e50b7 * DID changes all in one (#10559) * Chialisp draft * Modify python code * Reformat & Fix tests * Chialisp draft * Modify python code * Bug fix & flake8 fix for NFT tests * Handle recovery * Chialisp draft * Modify python code * Reformat & Fix tests * Chialisp draft * Modify python code * Bug fix & flake8 fix for NFT tests * add clvm files to recomp list * fix rpc test * flake8 fixes for tests * flake 8 stupid fix * Bug fix & flake8 fix for NFT tests * Modify python code * Chialisp draft * Modify python code * Bug fix & flake8 fix for NFT tests * Generalize the message puzzle * Add Pubkey as hint * Receive DID * Add DID APIs & Tests * Fix tests * Test fixes. in_transaction is now passed as a param to the various callers that eventually call save_info. * Workflow Fix * Add test config for DID/NFT * Update workflow test yaml * Fix install test script * Fix typo * Resolve comments * Fix tests * Change did_innerpuz and fix wallets for new design (#11196) * correcting the design of did_innerpuz and related wallet changes * remove breakpoint comments * change decimal point accuracy of percentage system * secure new_amount by fixing it to our current amount * rename and update comments for new_amount - now my_amount * rename P2_PUZZLE to simply INNER_PUZZLE * fix variable re-declaration for flake/merge * black and flake8 - inclduing wallet_state_manager bug fix * update RPCs related to add_url added commented out tests too, but DID needs fixing first * Fix bugs in transfer case * Fix pre-commit * Fix install.sh test for bookworm Co-authored-by: matt Co-authored-by: Jeff Cruikshank Co-authored-by: matt-o-how <48453825+matt-o-how@users.noreply.github.com> Co-authored-by: ytx1991 * Merge commit with main 1.3.4 (d154105a6b35f94649f15bca4e3fb8a11a39e70e) (#11276) * slight simplification of interpreting the bytes received by the timelord. avoid redundant round-trips to strings (#10316) * move build and twine to be dev deps rather than workflow installs (#10291) * add db validate function to check consistency of blockchain database (#10398) * streamable: Cache stream functions (#10419) Apply the same pattern as we have for deserialization to serialization. This avoids all those recursive runtime lookups for "how to stream this object" which brings a nice speedup: ``` compare: benchmark mode | µs/iteration old | µs/iteration new | diff % to_bytes | 447.57 | 193.56 | -56.75 compare: full_block mode | µs/iteration old | µs/iteration new | diff % to_bytes | 110.32 | 61.09 | -44.62 ``` * Stop assuming `True == 1` (#10396) * Stop assuming `True == 1` * Removed unnecessary lines which confuses code readers * simplify self_hostname. It doesn't need to depend on initial-config.yaml -> create_block_tools() -> global variable bt (#10371) * Fix balance calculation so it works with future unconfirmed tx (#10447) * Only update height if we have processed everything before that height. (#10415) * Only update height if we have processed everything before that height. * Remove debug return * Forgot this file * Better sorting of coin states, and disconnect peer if we are connected to trusted * Fix disconnect, and don't mutate arguments * Fix comment * Update chia/wallet/wallet_node.py Co-authored-by: Kyle Altendorf * Update chia/wallet/wallet_state_manager.py Co-authored-by: Kyle Altendorf * Address comments Co-authored-by: Kyle Altendorf * GC connections in wallet (#10450) * Fix the year from 2021 to 2022 (#10462) * Ms.unused code (#10453) * Remove old backup stuff * Remove references to CC (couloured coins) which is deprecated terminology * removed no longer used import Co-authored-by: Kyle Altendorf * Don't load initial data older than 5 days (#10481) * When loading initial data, don't include IPs older than 5 days in the best timestamp dict * Don't load version data for hosts older than 5 days * Update the DMG background image (#10289) * Update the DMG background image * Updated with latest design. * Updated background.tiff with latest design * Fancier DMG customization support via build_dmg.js * npm_macos -> npm_macos_m1 * Pass in the app path to build_dmg.js * Peppering with __init__.py files to satisfy the precommit hook * Return the fees of an offer via RPC (#10480) * farmer: Cleanup request retry and some logs (#10484) * farmer: Bump next update times regardless of the request results * farmer: Drop additional "success/failure" log logic We already print the PUT response in `_pool_put_farmer` the other parts just lead to confusion if the pool didn't implement the PUT correct. * farmer: Print error responses from the pool with `WARNING` log level * Fix method name (#10500) * updated gui to e2202874e1cb922a57370a47b2aec7bcf152b57d * reverted to gui 800a0f6556b89e928b1cf027c996b5ed010a7799 * updated gui to 672cf2a74ade67a868df232772bd6358bce8dedf * Only rewrite config when there is a difference (#10522) * Only rewrite config when there is a difference * Use variable * Preserve existing pool payout_instructions when creating a PoolWallet (#10507) * Preserve existing pool payout_instructions when creating a PoolWallet * Updated the logged string when payout_instructions needs to be generated. * Tests for update_pool_config * isort * Logging change, linter fixes, and more comments. * Abort trusted sync if a state update fails (#10523) * Abort trusted sync if a state update fails * Fix lint * fix updating of sub-epoch-summary map (part of BlockHeightMap) when the entirey change, including genesis changes (#10486) * Fix propagation of errors when adding a key with an invalid mnemonic (#10274) * Fix propagation of error messages originating from the keychain server * Test that adding key with an invalid mnemonic returns the expected error * Added daemon tests for the add_private_key RPC. Reverted wallet_rpc_client test as the daemon test is better suited for GUI testing. * Reformatting updates * Formatting change as requested by the pre-commit gods. * catch up test_add_private_key() with dynamic ports (#10530) * Updated background and icon positions. (#10531) * improve the picking of free ports for tests (#10491) * Hide balances until we are synced (#10501) * make multiprocessing start method configurable (#10528) * make multiprocessing start method configurable * forkserver * corrections * fixup * optional * more optional * stop attempting anchors in the yaml * rework config handling * comment * Update URL for direct download of windows whl for upnp (#10540) * Add incoming tx records when cancelling an offer (#10538) * Add incoming tx records when cancelling an offer * show fee on all txs * wallet: Reduce log level for `Pulled from queue` message (#10529) * pools: Drop redundant `PoolWalletInfo.from_json_dict` (#10524) It exists the same way in its base class `Streamable`. * correct multiprocessing start method logging, add python_default (#10547) * correct multiprocessing start method logging, add python_default * todo -> regular comment * correct some comments to refer to CATs (#10548) * correct some comments to refer to CATs * one more * and in a test * Detect hints correctly in the TX (#10543) * Detect hints correctly in the TX * Only support list hints * Allow CAT autodiscovery, refactor CAT default naming (#10308) * Allow CAT autodiscovery * Add wallet autodiscovery test * Use dict get for automatically_add_unknown_cats with default false * Refactor name generation for new CATs into one place * Remove hardcoded default cat wallet name from wallet rpc test * initial-config.yaml comment nit * Wallet consistancy (#10532) * use db transaction, -1 in synced up to height, delete unused funcitons * use transaction info in key-val-store/pool-store * cat stores * db lock * remove unused lock, set synced not always in transaction * fix store tests Co-authored-by: wjblanke * benchmarks: Implement streamable data comparison (#10433) * Resubmit peak to timelord for failure. (#10551) * Initial commit resubmit peak to timelord. * Change how toandle exception. * Add some logging. (#10556) * Fix `install-gui.sh` (#10460) * Fixed an issue where running with gui git branch specified failed * Fixed an issue where install-gui.sh failed if npm>=7 and NodeJS < 16 were installed * Fixed inconsistent npm path issue * Fixed typo * Add fee to changing pool, and more PlotNFT syncing fixes (#10545) * Add fee to changing pool, and more aggressive disconnect of untrusted * Don't publish fee TX * Lint * Small plotnft related fixes * More plotnft fixes * Apply quexington suggestion * correct param for in_transaction * Support get_transaction and get_transactions in plotnft * Re-add publish_transaction and add comment * Don't rerun additions * adding 1.3.0 release notes to changelog (#10578) * adding 1.3.0 release notes to changelog * Typos. thx @paninaro * Adding requested adjustments to changelog * Require pytest-asyncio>=0.17.0 for @fixture() (#10560) * Convert tests/core/util/test_streamable.py to use pytest. Remove unneeded class. (#10577) * make sync fixtures not use async def (#10504) This is particularly relevant in cases where the scope is not function as that forces use of a wider scoped event loop fixture as well. * Fixup some hidden test errors (#10442) * Touching up changelog (#10584) * removing known issue that was only applicable to a beta release * Adding additional fixes to the changelog * Adding Unreleased section to track upcoming changes * context manager for socket in find_available_listen_port(), catch OSError (#10567) * Bump CAT wallet test timeout to 40 minutes (#10605) * Remove block tools and keychain globals (#10358) * Use bt fixture * rebase * Use local_hostname * Fix test_json (inheritance from unittest.TestCase) * Use correct BlockTools fixture for test_simulation * Pass bt fixture into cost calculation tests * flake8 * Add missing parameters to test functions * Fix from rebase issues * Remove set_shared_instance * Update comment * Remove unneeded comments * Remove unused code * Remove unused code, run `multiprocessing.set_start_method("spawn")` at correct time. * Revert unrelated change * Set daemon_port. Teardown services in correct order. BIG thanks to Mariano Sorgente for debugging help. * Add back type signature - rebase issue * Apply review fixes from Jeff * Document why we need a later pytest-asyncio version * Correct type for _configure_legacy_backend * See what's going on during CI mypy run * github workflows * mypy typing * Remove legacy Keyring create method * Start daemon first * Shutdown daemon coroutine properly * Remove un-needed daemon_port argument * Set chia-blockchain-gui to hash in main * Remove connect_to_daemon_port * Remove code that set "daemon_port" before calling `setup_daemon` * Remove self_hostname fixture and extra self_hostname global * Fix two test files that were not importing self_hostname * self_hostname fixture * Remove more unused test code * Simplify fixture * a problem with using random.randint() to pick a port is that all processes (running in parallel) are seeded the same, and so pick the same ports at the same time, causing conflicts. This uses proper entropy instead. (#10621) * `chia keys show` will default to displaying the first observer-derived wallet address. With the addition of the `-d` option, the non-observer derived wallet address can be displayed. (#10615) * pools: Fix `plotnft claim` command's output (#10609) If you currently claim rewards `claim_cmd` fails to print the txhash with the lookup hint in `submit_tx_with_confirmation` ``` Error performing operation on Plot NFT -f 172057028 wallet id: 12: 'dict' object has no attribute 'name' ``` Because `submit_tx_with_confirmation` expects a `TransactionRecord` as result from its callable parameter `func` but `pw_absorb_rewards` returns a dict which includes the `TransactionRecord` as value for the key `transaction`. This PR makes sure all other methods used as `func` callable have the same return behaviour as `pw_absorb_rewards`. We could have adjusted it the other way around (only return `TransactionRecord` in `pw_absorb_rewards`) but then we would drop information in the RPC client. With this PR you get: ``` Do chia wallet get_transaction -f 172057028 -tx 0x34f74a1ffd9da9a493b78463e635996fd03d4f805ade583acb9764df73355f9c to get status ``` * fix initial-config typo - log_maxbytesrotation (#10598) * Fix invalid DB commit (#10594) * Fix invalid DB commit * More fixes * Add raise * Fix test * correct spelling of genrated (#10653) * fix [Bug] #10569 (#10571) * Version control (#10479) * Added version control enforcement to macOS m1 * Added enforced version control * Added enforce version compliance * Added enforced version compliance * Added enforced versioning * Updating this to include DRY internal action. * Removed some unintended whitespace. * Removed some unintended whitespace. * CI re-run * Trying to figure out why it's failing one test. * Trying to figure out why it's failing one test. * Trying to figure out why it's failing one test. * plotting: Only lock while actually accessing `PlotManager.plots` (#10675) * Fix exception when `chia keys migrate` is run without needing migration (#10655) * Fix exception when `chia keys migrate` is run without needing migration * Linter fixes * Slightly different query for V2 DBs for getting compact/uncompact block counts, to ensure the available index is used / avoid a table scan (#10661) * Check prefix on send_transaction before sending (#10566) * Check prefix on send_transaction * Fix failing RPC tests - xch -> txch since tests are a testnet and we enforce valid prefixes with this PR * Ak.convert fixtures (#10612) * Use bt fixture * rebase * Use local_hostname * flake8 * Remove set_shared_instance * Remove unneeded comments * Revert unrelated change * Add back type signature - rebase issue * Correct type for _configure_legacy_backend * See what's going on during CI mypy run * github workflows * mypy typing * Remove legacy Keyring create method * Start daemon first * Set chia-blockchain-gui to hash in main * Fix two test files that were not importing self_hostname * self_hostname fixture * Convert all class fixtures to top level functions * updated gui to cdfa2b98217fa8755c0da4f7409e6f90032c4c4c * Better management of KeyringWrapper's keys_root_path when using TempKeyring for tests (#10636) * Better management of KeyringWrapper's keys_root_path when using TempKeyring for tests. * Move keys_root_path restoration code into `cleanup()` Added an assert to detect if an unexpected shared KeyringWrapper is injected during a test. * Conditionally restore keys_root_path for testing * better TLS1.3 check (#10552) * better TLS1.3 check * catch ValueError instead of Exception * Code simplification and cleanup * a few nits in comments * Add configuration locking (#10680) * Add configuration locking Extracted from https://github.com/Chia-Network/chia-blockchain/pull/10631 * note that fasteners will likely be replaced by filelock * Fix test_multiple_writers on macOS * create_all_ssl() doesn't need to be inside the config access lock * add warnings about not using async within get_config_lock() get lock contexts * no need to pre-touch the lock file * .yaml.lock instead of just .lock * test_multiple_writers() is sync * Revert "add warnings about not using async within get_config_lock() get lock contexts" This reverts commit 681af3835bbc8ab0ff6e1cca286d8b23fcbd0983. * reduce lock context size in chia_init() * use an exit stack in load_config() * avoid config existence precheck * only lock around the read in load_config() * do not raise e, just raise * tidy new imports * fix queue empty check in test_config.py * remove commented out code in test_config.py * remove unused import Co-authored-by: Jeff Cruikshank * fix usage of the bt fixture in conftest fixtures (#10688) * bump pre-commit mypy to 0.940 (#10672) * remove some event_loop() fixtures (#10420) * remove event_loop() fixtures * flake8 * flake8 * remove sys.exit() from daemon shutdown * bump full node test timeout. a lot... to see. * fixup some tests * back to module scope event loop fixture for test_full_node.py * Update test_full_node.py * Iterator... * for the whole directory * some fixtures back to module scope for reduced runtime * back to 40 minute workflow timeouts * these are being addressed separately * updated gui to c992d07c956501f92e84ead80127c6b1e882fc21 * tests: Add `_PYTEST_RAISE` to fix exception breakpoints with pytest (#10487) It's currently not possible to have the debuger stop on an uncaucht exception when debugging tests. With this patch, adding `_PYTEST_RAISE=1` to the environment variables in the pytest configuration template fixes this. * Fixed test failures on Windows. (#10740) * Convert helper method do_spend from a class method to a function (#10613) * Remove unused test code (#10614) * Ak.setup nodes (#10619) * Remove unused test code * Centralize fixture uses of setup_n_nodes * Centralize fixure uses of setup_two_nodes * Break up setup_nodes into setup_services, for individial services, and setup_nodes, for initializing different simulator configurations * Sort imports * more entropy in random listen ports (#10743) * update chia-blockchain-gui one commit for npm build fix (#10776) * Updating Changelog for point release (#10781) * Updating Changelog for point release * Adding missing changelog items * run benchmarks separately (#10754) * run benchmarks separately * only run benchmarks once, with the most recent python version we support * Change name to order of returned values. Enforce mandatory naming and inclusion of start_services parameter (#10769) * cmds: Implement `chia rpc` command (#10763) * cmds: Implement `chia rpc` command * Enable "timelord" client + some refactoring to enable "crawler" client * wallet: Fix `STANDARD_WALLET` creation for `wallet_info.id != 1` (#10757) * wallet: Optional wallet type parameter for `get_wallets` and `wallet show` (#10739) * wallet: Add optional `type` parameter to `get_wallets` and `wallet show` * tests: Use the `type` parameter for `get_wallets` in pool rpc tests * cmds: Ask for the name of the wallet type in CLI * harvester: Reuse legacy refresh interval if new params aren't available (#10729) * mypy 0.941 for pre-commit (#10728) * Add maker fee to remaining offer RPCs (#10726) * Add healthcheck endpoint to rpc services (#10718) * Add healthcheck endpoint to rpc services * Trailing whitespace ding * Fix typos lastest > latest (#10720) * fix typo in command line argument parsing for chia db validate (#10716) * New RPC get_coin_records_by_hint - Get coins for a given hint (#10715) * RPC endpoint to retrieve coins by hint * RPC client update for get_coin_records_by_hint * start writing tests for get_coin_records_by_hint * Address linting concerns. * Address flake8. Fix the get_coin_ids() call. * convert hint to bytes32 * tests for get_coin_records_by_hint Co-authored-by: Amine Khaldi * require test-cache repo is found in CI (#10711) * Issues found in RPC API review (#10702) * Removed unnecessary substitution * Recovered property which was accidentally removed in full node RPC API * Added backward compatibility to `get_additions_and_removals` full_node RPC API * Fixed full_node rpc client * minor followup to config locking (#10696) * minor lock scope reduction * use the lock in tests * Use the passed root_path in configure CLI command (#10694) * Improve the CI runs w.r.t. timelord installation (#10673) * Superficial analysis showed that only two test groups require (for now) installing the timelord. This change aims to save us hours of CI running time by simply running the install timelord script only for those test groups, with everything else having it omitted. Dedicated to @hoffmang9 * We don't need these anymore. * less optional around ssl (#10558) * less optional * clean up cruft * more * more * just a little less optional * cmds: Fix trusted peer hint in `chia wallet show` (#10695) `config` is the root config here. * Enable clvm_tools_rs by default (#10554) * Enable clvm_tools_rs by default * Re-add clvm_tools dep for now as it provides python idioms for interacting with clvm data * Take lint formatting * Adam: Try making this non-parallel * Try fix for threading issue in tests * Test whether turning off parallel runs causes things to work (temp) * Test use of temp files in clvm_tools_rs as a candidate solution for atomic replacement of hex output * Use proper git+https url scheme (oops) * Update to candidate 0.1.6 so we can test * Revert version bump to re-test * Test whether we can re-enable parallelism * Attempt to mitigate concurrent test running: return own conception of the compiled output. This will work if the failing path is downstream of recompilation * fix path to hex file * Probe for source of 0-length data * Further exploration: more assertions, hopefully to trigger in the test on ci * Do an even more paranoid check to verify that we observe a file whose filesystem reported size is much smaller than expected * Try a heavier handed approach, using heavyweight lockfiles on the filesystem * Import a simple lockfile implementation and use it to enforce mutual exclusion. Simplify it and remove the unwanted os traffic * Take lint, precommit advice, bump to clvm_tools_rs 0.1.6 now that it's released * Fix lint * While i was working on this, -n auto was on the command line so i think this didn't actually do anything, but reverting my change just in case * Lint * label the hashes re: pr * Add a lock.py for spot exclusivity using the filesystem (re: adam in the pr) and a convenience wrapper that hides the details * Formatting warning * Ensure type info is present and do the obvious return of the inner function's result * Use double quotes (lint) * Properly balance blank lines * Lint: alphabetize imports * One line is required here (lint) * Remove unnecessary assignment * when creating a new blockchain database implicitly, make it v2 (#10498) * when creating a new blockchain database implicitly, make it v2 * fix config deadlock * add select_coins RPC method (#10495) * add select_coins RPC method * typing fix * fix typing, casts * add RPC coin selection tests * black formatting * fix select_coins tests * improve error messages from chia db upgrade, specifically to help users if the disk is full (#10494) * more set -o errexit (#10468) * more set -o errexit -o pipefail * no pipefail, too fancy for dash at least... * Bump clvm_tools_rs to fix a problem running as daemon caused by old log message that is now eliminated (#10788) * when running multiple services in the same process (in tests), don't initialize logging for all of them, and don't set the proctitle of the test (#10686) * stop helping mkdir() do what it already does (#10802) * stop helping mkdir() do what it already does * flake8 * Capitalize display of Rpc -> RPC in `chia show -s` (#10797) * Remove accidental parameters from calls to setup_simulators_and_wallets and prevent future mistakes (#10770) * stop using deadsnakes, unless we need it (#10752) * stop using deadsnakes. and see... * only install dead snakes stuff if building the timelord on linux * small change to fix branch in contributing (#10805) * small change to fix branch in contributing * Update CONTRIBUTING.md * Rename confusing fixtures, especially ones with the same name but dif… (#10772) * Rename confusing fixtures, especially ones with the same name but different implementation * rename test_environment to test_plot_environment * Make it so setup_two_nodes is no longer the name of a fixture and a utility function * revert premature fixture rename: two_wallet_nodes_start_height_1 * atomic rollback for wallet (#10799) * atomic rollback for wallet * Handle cases where one node doesn't have the coin we are looking for (#10829) * Continue if one node doesn't have the coin * Pass in coin_state list * Pass in the single coinstate instead of list * more simplifications * run tests in parallel in CI (#10499) * Fix timelord installation for Debian. (#10841) * add optional persistence to SpendSim (#10780) * add optional persistence to SpendSim * Accidental rename * remove duplicate event_loop (#10768) * Adding check for python3.9 alongside python3.10 on Arch (#10363) * Adding check for python3.9 alongside python3.10 on Arch * Adjusting install.sh instructions for Arch * Disabling prescribed python install for Arch * Setting Arch install script to exit 0 to pass tests * Adding workflow step for functional Arch install testing * Adding noconfirm to pacman install command * Relocating Arch support message for install.sh * use DEFAULT_ROOT_PATH in tests (#10801) * Disable the pytest-monitor plugin in CI if not checking results (#10837) * disable the pytest-monitor plugin if not reporting results pytest-monitor uses multiprocessing and has caused multiple confusing issues. Perhaps it can be adjusted to not use multiprocessing, but for now lets just isolate the oddities to where we actually use it. * use a template for resource usage check, similar to timelord install * hint testconfig.custom_vars * Check for requesting items when creating an offer (#10864) * updated gui to 054d7b342e7c8284c9b58a775f87d393a1008bfe * Added `-n`/`--new-address` option to `chia wallet get_address` (#10861) * Added `-n`/`--new-address` option to `chia wallet get_address` * Formatting fix * Complemented --new-address with --latest-address per feedback * Minor output formatting/enhancements for `chia wallet show` (#10863) * Minor output formatting/enhancements for `chia wallet show` * Updated format based on internal poll results * Linter fix and row rearrangement. * Hardcoded SSL test certs/keys (#10828) * Hardcoded SSL test certs/keys * Added a second set of certs/keys. Cert/key sets are infinitely cycled-through using get_next_private_ca_cert_and_key() and get_next_nodes_certs_and_keys() * More cert/key sets and a tool to generate them * Updated SSL generator to sign with the appropriate root CA. Fixed linter issues. * Linter fixes * Updated generate_ssl_for_nodes() based on feedback * reduce indentation in a few functions in blockchain.py by negating early-exit checks and loop continues (#10872) * fix typo and index issues in wallet database (#10273) * fix typo in wallet_puzzle_store * check some SQL statements * deduplicate name SQL index * deduplicate wallet_type index * deduplicate wallet_id index * Update appdmg to 0.6.4 to work with macos 12.3 (#10886) * fixup and enable condition checking tests (#10888) * fixup and enable tests for the edge cases of absolute timestamp and absolute height conditions in mempool_manager * Update chia/full_node/full_node_api.py Co-authored-by: Adam Kelly <338792+aqk@users.noreply.github.com> Co-authored-by: Adam Kelly <338792+aqk@users.noreply.github.com> * Bump colorlog from 5.0.1 to 6.6.0 (#9207) Bumps [colorlog](https://github.com/borntyping/python-colorlog) from 5.0.1 to 6.6.0. - [Release notes](https://github.com/borntyping/python-colorlog/releases) - [Commits](https://github.com/borntyping/python-colorlog/compare/v5.0.1...v6.6.0) --- updated-dependencies: - dependency-name: colorlog dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump actions/checkout from 2 to 3 (#10505) * Bump actions/checkout from 2 to 3 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Update actions in templates too Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gene Hoffman * Bump github/super-linter from 4.8.1 to 4.9.1 (#10894) * Bump github/super-linter from 4.8.1 to 4.9.1 Bumps [github/super-linter](https://github.com/github/super-linter) from 4.8.1 to 4.9.1. - [Release notes](https://github.com/github/super-linter/releases) - [Changelog](https://github.com/github/super-linter/blob/main/docs/release-process.md) - [Commits](https://github.com/github/super-linter/compare/v4.8.1...v4.9.1) --- updated-dependencies: - dependency-name: github/super-linter dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Ignore too-many-function-args in test_type_checking.py * black Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gene Hoffman * fix type annotations for get_block_generator() (#10907) * fix type annotations for FullBlock.header_hash and FullBlock.prev_header_hash (#10909) * new DBWrapper supporting concurrent readers (#10166) * new DBWrapper supporting concurrent readers * adress review comments * fixup default database version, when file doesn't exist * remove unused argument * use rust clvm in Program.run() (#10878) * remove Program.from_serialized_program * run the rust clvm implementation (instead of python) even for wallet programs * Fix flaky trade test (#10921) * single thread executor (#10919) * add inline executor and an option to run single-threaded * add option to run test_full_sync in single-thread mode, to include block validation in profiles. Also attempt to speed it up by disabling db_sync * await the db commit in the async version of set_db_version (#10906) * bump pre-commit mypy to v0.942 (#10902) * bump clvm_tools dependency to make every chia-blockchain installation get the new brun that reports cost accurately (#10880) * wallet: Drop unused `WalletStateManager.load_wallets` (#10756) * Switch to integrated lock_and_load_config() context manager (#10698) * minor lock scope reduction * use the lock in tests * Use the passed root_path in configure CLI command * switch to lock_and_load_config() * oops * cleanup * make _load_config_maybe_locked() private * black * Remove future improvement opportunity TODO comment * move pytest.ini to the root directory (#10892) * move pytest.ini to the root directory * pytest.ini: testpaths = tests https://docs.pytest.org/en/7.1.x/reference/reference.html?highlight=testpaths#confval-testpaths * set CHIA_ROOT in tests instead of symlinking (#10682) * attempt to checkout test-cache directly to desired location * rebuild workflows * maybe we can use CHIA_ROOT * use CHIA_ROOT to find blocks and plots for tests * oops * more informational printing * oops * --capture no for debugging * flake8 * import os * undo some unrelated changes now covered elsewhere * undo some debug changes * rebuild workflows * Remove sys.exit() from chia daemon /exit endpoint (#10454) * asyncio.get_event_loop() is deprecated in 3.10, stop using it (mostly) (#10418) * asyncio.get_event_loop() is deprecated in 3.10, stop using it https://docs.python.org/3.10/library/asyncio-eventloop.html#asyncio.get_event_loop > Deprecated since version 3.10: Deprecation warning is emitted if there is no running event loop. In future Python releases, this function will be an alias of get_running_loop(). * black * run tests in CI via coverage (#9704) * Add coverage (without collection) * Separate test_block_compression() to avoid coverage-related hangs * Revert "Separate test_block_compression() to avoid coverage-related hangs" This reverts commit ebad3d001778b2344f0d2a651be0206d7f6dc847. * multiprocessing.set_start_method("spawn") * multiprocessing.set_start_method() in conftest.py * hand hold cc wallet tests * lint * spawn for running chia as well * handle already set start method case * a bit more timeout for test_multiple_writers * more timeout for test_writer_lock_blocked_by_readers * 45 minute tieout for tests/pools/ * 45 minute tieout for tests/pools/ * some more hand holding sleeps * report coverage in each workflow only really useful to make sure it is capturing something * oops * complete the job name and the JOB_NAME * better coverage result file names * reset worker process titles * rebuild workflows * rebuild workflows * black * black * rebuild workflows * push timeouts * actually include the updated workflows... * push more workflow timeouts * parallel=True * rebuild workflows * restrict click to < 8.1 for black (#10923) https://github.com/pallets/click/issues/2225 Doing this instead of updating since updating black will change several files due to some formatting change. I would like to take that on separately from unbreaking CI. * fixup workflow template merge env duplication (#10925) * ignore lack of hinting on clvm_tools.binutils.assemble() (#10926) * Contextualize some store test db names. (#10942) * Type check values in RL Wallet (#10935) * Use uint128 for wallet balances (#10936) * Add more type checks to CAT Wallet (#10934) * Bump actions/github-script from 4 to 6 (#10246) * Bump actions/github-script from 4 to 6 Bumps [actions/github-script](https://github.com/actions/github-script) from 4 to 6. - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/github-script dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Update to github.rest.* for calls to API for compat w/ github-script@v5+ Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Chris Marslender * Bump actions/setup-node from 2.4.1 to 3 (#10506) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2.4.1 to 3. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v2.4.1...v3) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump actions/cache from 2.1.6 to 3 (#10846) * Bump actions/cache from 2.1.6 to 3 Bumps [actions/cache](https://github.com/actions/cache) from 2.1.6 to 3. - [Release notes](https://github.com/actions/cache/releases) - [Commits](https://github.com/actions/cache/compare/v2.1.6...v3) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Update actions in templates also Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gene Hoffman * Fix trailing bytes shown in CAT asset ID row when using `chia wallet show` (#10924) * Truncate CAT asset_id output to 32 bytes. A wallet RPC change is needed to properly separate out the asset ID from the TAIL program returned by `get_all_wallet_info_entries()` * Move the truncation to the assignment location * Consolidate test fixtures (#10778) * Rename confusing fixtures, especially ones with the same name but different implementation * revert premature fixture rename: two_wallet_nodes_start_height_1 * Consolidate test fixtures * Quick fix for improper v2 DB initialization when targeting testnet (#10952) * bump timing threashold for mempool performance test (#10953) * Ms.parallel pool t (#10966) * Try parallel pool tests * Also change workflow files * Run less combinations * Todo for bad test * Try lower n * run more tests in parallel on CI (#10960) * run more tests in parallel on CI * fix test_farmer_get_harvesters to wait for plots to be loaded before asking about them * improve error message when a block is missing from the blockchain database (#10958) * improve error message when a block is missing from the blockchain database * Update chia/full_node/block_height_map.py Co-authored-by: Kyle Altendorf Co-authored-by: Kyle Altendorf * Also throw DB error on double spending a coin (#10947) * Throw error on double spending a coin * Throw error on double spending a coin * Improve test * reorg fixes (#10943) * when going through a reorg, maintain all chain state until the very end, when the new fork has been fully validated and added * when rolling back the chain, also rollback the height-to-hash map * add tests * Fix the issues in main (failing tests) (#10977) * Fix one of the issues in test_blockchain * Only rollback after all async operations are finished * back to a single option for workflow parallel config (#10979) * limit test output on CI by dropping -s and -v. Also, only print the 10 slowest tests, instead of all (#10959) * Ms.flaky gen speed (#10965) * Flaky test sometimes goes slower than 1 second * Add sleep to reduce flakiness * Increase timeout instead of sleeping to hopefully reduce flakiness * fix test_full_sync.py to only feed the blocks in the main chain to the node (#10974) * Bump peter-evans/create-pull-request from 3 to 4 (#10950) Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 3 to 4. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/v3...v4) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * normalized_to_identity_cc_ip from get_consecutive_blocks was being passed in as overflow_cc_challenge in get_full_block_and_block_record (#10941) * fix performance tests (#10983) * Check for vulnerable openssl (#10988) * Check for vulnerable openssl * Update OpenSSL on MacOS * First attempt - openssl Ubuntu 18.04 and 20.04 * place local/bin ahead in PATH * specify install openssl * correct path * run ldconfig * stop building and check for patched openssl * spell sudo right by removing it * Remove openssl building - 1st attempt RHs * Test Windows OpenSSL version HT @AmineKhaldi * Non Hobo patch the winstaller for CVE-2022-0778 (#10995) * apt show not needed (#10997) * install/upgrade openssl on Arch Linux also (#10999) * Compile python 3.9.11 which is aware of the openssl issue (#11001) * install.sh is not upgrading OpenSSL on MacOS (#11003) * MacOS isn't updating OpenSSL in install.sh * Exit if no brew on MacOS * Code the if tree like a pro instead. Co-authored-by: Kyle Altendorf Co-authored-by: Kyle Altendorf * force index in get_coin_records_by_names (#10987) * force index in get_coin_records_by_names * fix lint * Fix remaining linting issues (#10962) * FIx remaining linting issues * Revert type:ignore * Revert token_bytes change * streamable|pools: Fix `Optional` parsing in `dataclass_from_dict` (#10573) * Test more `Optional` parsing in `dataclass_from_dict` * Fix optional parsing in `dataclass_from_dict` * Fix pool wallet / tests * run_generator2 rust call and compact conditions data structure (#8862) * use run_generator2 rust call and compact spend bundle conditions data structure pervasively. * address review comments * Faster full node tests (#10986) * Start fast full node tests * Perf improvement on send_transaction * Major performance improvement for mempool test * Speed up another test * Speed up mempool tests startup * Lint * Debug tests * Try function scope for wallet_nodes * Update comment * Force apt to install the things we asked it to (#11047) * Force apt to install the things we asked it to * Update .github/workflows/benchmarks.yml Co-authored-by: dustinface <35775977+xdustinface@users.noreply.github.com> Co-authored-by: dustinface <35775977+xdustinface@users.noreply.github.com> * github: Drop unused `BUILD_VDF_CLIENT` variables (#11050) From my understanding this is only used by `chiavdf` source builds which happen only if `install-timelord.sh` gets called but it doesn't in the addressed cases. * bump up to 2.1.7 to fix inotify issue resolved by 848 (#11042) * fix memory leak in test_full_sync (#11004) * full_node: Drop unused `MempoolManager.constants_json` (#11046) * simplify some header hash getting and assertions (#11007) * Remove websockets dependency & do some refactoring (#10611) * remove old ws * Prepare test blocks and plots only for tests that need them. This saves us a couple more hours of CI running time. (#10975) * Adding clean-workspace step to benchmarks (#11063) * Checkout test blocks and plots for benchmarks workflow (#11068) * Improve handling of unknown pending balances (likely change from addi… (#10984) * Improve handling of unknown pending balances (likely change from adding a maker fee). Minor improvement for fingerprint selection -- enter/return selects the logged-in fingerprint. * Minor output formatting improvements when showing offer summaries. Minor wallet key selection improvements. Added tests for print_offer_summary * Linter fixes * isort fix * Coroutine -> Awaitable * Removed problematic fee calculation from get_pending_amounts per feedback. * print average block rate at different block height windows (#11064) * add -d for Install.ps1 (#11062) * Set keychain_proxy to None in await_closed() to support reinitialization (#11075) * Set keychain_proxy to None in await_closed() to support reinitialization. * Added `shutting_down` param to _await_closed() to control whether the keychain_proxy is closed. * Significantly speedup preparing test blocks and plots by opting for a release download instead of a shallow git clone, and also by putting a caching layer on top of that. (#11065) * Bump github/super-linter from 4.9.1 to 4.9.2 (#11067) Bumps [github/super-linter](https://github.com/github/super-linter) from 4.9.1 to 4.9.2. - [Release notes](https://github.com/github/super-linter/releases) - [Changelog](https://github.com/github/super-linter/blob/main/docs/release-process.md) - [Commits](https://github.com/github/super-linter/compare/v4.9.1...v4.9.2) --- updated-dependencies: - dependency-name: github/super-linter dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Remove dead snakes usage from benchmark tests (#11053) * Handle INSTALL_PYTHON_VERSION in Install.ps1, otherwise search 3.9/3.8/3.7 (#11034) * Handle INSTALL_PYTHON_VERSION in Install.ps1, otherwise search 3.9/3.8/3.7 * fix python availability check in Install.ps1 * when Install.ps1 does not find an acceptable python, list supported versions in order * Update Install.ps1 Co-authored-by: Matt Hauff Co-authored-by: Matt Hauff * wallet: Drop `puzzles/genesis_checkers.py` and related puzzles (#10790) Its all duplicated code and puzzles as far as i can tell, see `chia/wallet/puzzles/tails.py`. * Bump cryptography from 3.4.7 to 36.0.2 (#10787) Bumps [cryptography](https://github.com/pyca/cryptography) from 3.4.7 to 36.0.2. - [Release notes](https://github.com/pyca/cryptography/releases) - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/3.4.7...36.0.2) --- updated-dependencies: - dependency-name: cryptography dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * wallet: Improve logging in `create_more_puzzle_hashes` (#10761) * wallet: Improve logging in `create_more_puzzle_hashes` It's pretty spammy currently when scanning puzzle hashes. * scan -> create, Scanning -> Creating * `scanning_msg` -> `creating_msg` * Add /cat_get_unacknowledged API for accessing unknown CATs (#10382) * Add /cat_get_unacknowledged API for accessing unknown CATs * Reformat & fix cast issue * Integration tested & add unit test * Handle optional uint32 * Reformat * Reformat * Reformat * Merge PR 10308 * Reformat * Fix concurrent issue * Add state change notification * rename API * Fix failing tests * Updated state_change name Co-authored-by: Jeff Cruikshank * Increases the probability of connecting to local trusted node (#10633) * extend tests in test_blockchain to include more conditions, as well as ensuring consensus rules allow unknown condition parameters (#11079) * chia|tests|github: Implement, integrate and test plot sync protocol (#9695) * protocols|server: Define new harvester plot refreshing protocol messages * protocols: Bump `protocol_version` to `0.0.34` * tests: Introduce `setup_farmer_multi_harvester` Allows to run a test setup with 1 farmer and mutiple harvesters. * plotting: Add an initial plot loading indication to `PlotManager` * plotting|tests: Don't add removed duplicates to `total_result.removed` `PlotRefreshResult.removed` should only contain plots that were loaded properly before they were removed. It shouldn't contain e.g. removed duplicates or invalid plots since those are synced in an extra sync step and not as diff but as whole list every time. * harvester: Reset `PlotManager` on shutdown * plot_sync: Implement plot sync protocol * farmer|harvester: Integrate and enable plot sync * tests: Implement tests for the plot sync protocol * farmer|tests: Drop obsolete harvester caching code * setup: Add `chia.plot_sync` to packages * plot_sync: Type hints in `DeltaType` * plot_sync: Drop parameters in `super()` calls * plot_sync: Introduce `send_response` helper in `Receiver._process` * plot_sync: Add some parentheses Co-authored-by: Kyle Altendorf * plot_sync: Additional hint for a `Receiver.process_path_list` parameter * plot_sync: Force named parameters in `Receiver.process_path_list` * test: Fix fixtures after rebase * tests: Fix sorting after rebase * tests: Return type hint for `plot_sync_setup` * tests: Rename `WSChiaConnection` and move it in the outer scope * tests|plot_sync: More type hints * tests: Rework some delta tests * tests: Drop a `range` and iterate over the list directly * tests: Use the proper flags to overwrite * test: More missing duplicates tests * tests: Drop `ExpectedResult.reset` * tests: Reduce some asserts * tests: Add messages to some `assert False` statements * tests: Introduce `ErrorSimulation` enum in `test_sync_simulated.py` * tests: Use `secrects` instead of `Crypto.Random` * Fixes after rebase * Import from `typing_extensions` to support python 3.7 * Drop task name to support python 3.7 * Introduce `Sender.syncing`, `Sender.connected` and a log about the task * Add `tests/plot_sync/config.py` * Align the multi harvester fixture with what we do in other places * Update the workflows Co-authored-by: Kyle Altendorf * Add wallentx as additional assignee on mozilla CA update PRs (#11089) * rebuild workflows (#11092) * transition to using chia_rs module (#11094) * Fix the case of claiming a large number of coins (#11038) * Fix the case of claiming a large number of coins with a fee from a pool wallet * Revert change to unrelated test * Set PoolWallet.DEFAULT_MAX_CLAIM_SPENDS to 300 * A few review improvements * Ms.fast test blockchain (#11051) * more work on test blockchain * Optimize test_blockchain.py * Fix weight proof bug * Rename variable * first rc_sub_slot hash bug * New plots * try with a new ID * Run without cache * Address test blocks and plots preparation. * Update constant in test_compact_protocol(). * Update this constant too. * Revert accidental altering of the gui submodule in ae7e3295f280a591e76c4dffdea75fb74ea5de6f. * Fix benchmark test * Revert mozilla-ca change * Rebase on main Co-authored-by: almog Co-authored-by: Amine Khaldi * can we get by without dead snakes? (#11070) * can we get by without dead snakes? * Update install-timelord.sh * Revert "Update install-timelord.sh" This reverts commit cba3250b095efbac5459c430102a7914243bfa96. * do not install python dev package for timelords build in ci it is already there... * more quotes for sh * Changelog from 1.3.3 (#11081) * Updating changelog * Update appdmg to 0.6.4 to work with macos 12.3 (#10886) * restrict click to < 8.1 for black https://github.com/pallets/click/issues/2225 Doing this instead of updating since updating black will change several files due to some formatting change. I would like to take that on separately from unbreaking CI. * Check for vulnerable openssl (#10988) * Check for vulnerable openssl * Update OpenSSL on MacOS * First attempt - openssl Ubuntu 18.04 and 20.04 * place local/bin ahead in PATH * specify install openssl * correct path * run ldconfig * stop building and check for patched openssl * spell sudo right by removing it * Remove openssl building - 1st attempt RHs * Test Windows OpenSSL version HT @AmineKhaldi * Get updated openssl version (#10991) * Get updated openssl version * Update pyinstaller * Fix typo * lets try this * Let's try this * Try this Co-authored-by: Earle Lowe * Gh 1.3.3v2 (#11011) * Non Hobo patch the winstaller for CVE-2022-0778 (#10995) * install.sh is not upgrading OpenSSL on MacOS (#11003) * MacOS isn't updating OpenSSL in install.sh * Exit if no brew on MacOS * Code the if tree like a pro instead. Co-authored-by: Kyle Altendorf Co-authored-by: Kyle Altendorf * Remove hobo patch * apt show not needed (#10997) * install/upgrade openssl on Arch Linux also * Update CHANGELOG * revert Arch change backport Co-authored-by: Kyle Altendorf Co-authored-by: wallentx Co-authored-by: Chris Marslender Co-authored-by: Gene Hoffman <30377676+hoffmang9@users.noreply.github.com> Co-authored-by: William Allen Co-authored-by: Earle Lowe * consistently name installer github actions artifact zips (#11096) * git -C and consistent activation in installer builds (#11098) * updated gui to d714c21b4ee3ebbc7d18b5f819772cd9868d0bf5 * only check the version once in installer build workflows (#11099) * updated gui to 5f8b23fc7deb0b07f665c075ed491059f4d8b95c * updated gui to fccbd3e10d27673e39c01f0f89e47b5455b8331a * streamable: Simplify and force correct usage (#10509) * streamable: Merge `strictdataclass` into `Streamable` class * tests: Test not supported streamable types * streamable: Reorder decorators * streamable: Simplify streamable decorator and force correct usage/syntax * streamable: Just move some stuff around in the file * streamable: Improve syntax error messages * mypy: Drop `type_checking.py` and `test_type_checking.py` from exclusion * streamable: Use cached fields instead of `__annotations__` This is now possible after merging `__post_init__` into `Streamable` * Introduce `DefinitionError` as `StreamableError` * `/t` -> ` ` * Expose farm_block RPC for simulator (#10830) * expose farm block api to RPC for simulator * lint * pre-commit lint * Ms.plot load perf2 (#10978) * 2.7 seconds -> 0.45 seconds * Merge * Work on create_plots refactor * Try to fix tests * Try to fix tests * Use new functions * Fix block_tools by adding dir * Extra argument * Try to fix cyclic import * isort * Drop warning * Some cleanups around `exclude_final_dir` and directory adding * Cleanup `min_mainnet_k_size` checks * Drop unrelated changes * Fixes after rebase * Fix cyclic import * Update tests/block_tools.py Co-authored-by: dustinface <35775977+xdustinface@users.noreply.github.com> * Update tests/block_tools.py Co-authored-by: dustinface <35775977+xdustinface@users.noreply.github.com> Co-authored-by: xdustinface Co-authored-by: dustinface <35775977+xdustinface@users.noreply.github.com> * rebase and more fixes (#10885) * derivation from just a master public key (#11140) * Add dependencies macos rhel chiavdf (#11142) * Added steps, when building the chiavdf wheel for macos and rhel-based systems, to install cmake and/or gmp. * updated gui to d5b75bcf7a0fe1ef76775e7f1d5e12d169069676 * Revert "derivation from just a master public key (#11140)" (#11143) This reverts commit db536c615a0c7226ea8319fb98ec545eded23670. * Mark the github workspace as safe (#11159) * Mark the github workspace as safe * Move the git config step after git is installed in the test containers * updated gui to 81303fb962f4a627a2e1c55098e187a9057745da * optimize wallet tool by not caching the puzzle_hash -> derivation index, but caching puzzle_hash -> secret key (which is the lookup we're actually interested in). This avoids duplicating the actual derivation (#11154) * make listen port colissions in CI less likely (#11164) * Use get latest release endpoint for plotters, so that we ignore any pre-releases that could be returned by listReleases (#11165) * Build cli only version of debs (#11166) * Build cli only version of debs * Export the vars needed by j2 * Fix paths * Add symlink to chia in /usr/local/bin/ * Upload the cli only debs to s3 * Add init.py * Ensure SHA is on the dev build for amd64 * add tool to generate a blockchain with full blocks, as a benchmark (#11146) * Simplify how the chia symlink is created in the CLI .deb (#11188) * fix block_tools feature when specifying a list of block references. Also add feature keep_going_until_tx_block. (#11185) * Fix filename of latest intel dev installer (#11203) * Add start_crawler and start_seeder to pyinstaller config (#11205) * Pin mac intel installer to 10.15 (#11209) * Revert "Pin mac intel installer to 10.15 (#11209)" (#11210) This reverts commit 93a61eece1ab537f40cf3daf76be69dfce405762. * Adding changelog (#11223) * Adding changelog (#11223) Co-authored-by: Arvid Norberg Co-authored-by: Kyle Altendorf Co-authored-by: dustinface <35775977+xdustinface@users.noreply.github.com> Co-authored-by: ChiaMineJP Co-authored-by: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Co-authored-by: David Barratt Co-authored-by: Chris Marslender Co-authored-by: Matt Hauff Co-authored-by: William Blanke Co-authored-by: Juraj Oršulić Co-authored-by: Yostra Co-authored-by: Florin Chirica Co-authored-by: Richard Kiss Co-authored-by: William Allen Co-authored-by: Adam Kelly <338792+aqk@users.noreply.github.com> Co-authored-by: Johannes Tysiak Co-authored-by: Don Kackman Co-authored-by: austinsirkin Co-authored-by: Earle Lowe <30607889+emlowe@users.noreply.github.com> Co-authored-by: Brandon Butler Co-authored-by: Dave <72020697+daverof@users.noreply.github.com> Co-authored-by: Freddie Coleman Co-authored-by: Amine Khaldi Co-authored-by: arty Co-authored-by: Francesco Truzzi Co-authored-by: hugepants Co-authored-by: Jack Nelson Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gene Hoffman Co-authored-by: Gene Hoffman <30377676+hoffmang9@users.noreply.github.com> Co-authored-by: roseiliend <90035993+roseiliend@users.noreply.github.com> Co-authored-by: Kronus91 Co-authored-by: almog Co-authored-by: wallentx Co-authored-by: Earle Lowe Co-authored-by: xdustinface Co-authored-by: Patrick Maslana <79757486+pmaslana@users.noreply.github.com> * WIP commit for the new NFT spec * Update to optimized singleton * flesh out ownership layer * nft_transfer_nft: (#11181) * Introduce some error handling. * The new_did_inner_hash and trade_price params should be optional for now. * Add get_confirmed_balance(), get_unconfirmed_balance(), get_spendable_balance(), get_pending_change_balance() and get_max_send_amount() to NFTWallet. This fixes chia wallet show when NFTs are involved. (#11256) * Needed for DID CLI: (#11268) * RPC: Add /did_set_wallet_name and /did_get_wallet_name. * DIDWallet: Add set_name() and get_name(). * Generalize the Offer class to more than CATs * Remove CAT dependencies from trade_manager * Fix offer RPC * isort * Further generalize the Offer drivers * Update trade manager with new generalizations * Fix offer RPC again * Move outer_puzzles.py * pivot from string to clvm for dict entries * add test coverage for driver dict in RPC * Remove some CAT specific stuff from tm * Add comments explaining the changes * Minor fixes * isort and flake8 * More linting * Include drivers in offer summary * Better autodetection of drivers on offer creation * Forgot to update rpc test * checkpoint * fix test and optimise state_layer puz * rebuild workflows * Pass Recovery Info in the Transfer case (#11249) * Chialisp draft * Modify python code * Reformat & Fix tests * Chialisp draft * Modify python code * Bug fix & flake8 fix for NFT tests * Handle recovery * Chialisp draft * Modify python code * Reformat & Fix tests * Chialisp draft * Modify python code * Bug fix & flake8 fix for NFT tests * add clvm files to recomp list * fix rpc test * flake8 fixes for tests * flake 8 stupid fix * Bug fix & flake8 fix for NFT tests * Modify python code * Chialisp draft * Modify python code * Bug fix & flake8 fix for NFT tests * Generalize the message puzzle * Add Pubkey as hint * Receive DID * Add DID APIs & Tests * Fix tests * Test fixes. in_transaction is now passed as a param to the various callers that eventually call save_info. * Workflow Fix * Add test config for DID/NFT * Update workflow test yaml * Fix install test script * Fix typo * Resolve comments * Fix tests * Change did_innerpuz and fix wallets for new design (#11196) * correcting the design of did_innerpuz and related wallet changes * remove breakpoint comments * change decimal point accuracy of percentage system * secure new_amount by fixing it to our current amount * rename and update comments for new_amount - now my_amount * rename P2_PUZZLE to simply INNER_PUZZLE * fix variable re-declaration for flake/merge * black and flake8 - inclduing wallet_state_manager bug fix * update RPCs related to add_url added commented out tests too, but DID needs fixing first * Fix bugs in transfer case * Fix pre-commit * Fix install.sh test for bookworm * Pass recovery info in the transfer case * add clvm files to recomp list * flake8 fixes for tests * flake 8 stupid fix * Modify python code * Bug fix & flake8 fix for NFT tests * Pass recovery info in the transfer case * Fix tests * Fix security issue of the recovery empty list * Revert test code * Allow assign fee when creating the wallet * reformat * Add option for pass recovery list * DID wallet name generation & deduplication * Fix test * Remove file reading code * Fix tests Co-authored-by: matt Co-authored-by: Jeff Cruikshank Co-authored-by: matt-o-how <48453825+matt-o-how@users.noreply.github.com> Co-authored-by: ytx1991 * Bumping GUI to 447ac34942facbd1db64755144363268539ecf25 * metadata checkpoint * fix state layer for metadata updater * add test for metadata updating * removed second param for create_ann condition * forgot to compile clvm * compiled updated clvm sha256 * * Add fees support to DID wallet creation RPC client. (#11363) * Initial iteration of the chia wallet did create command. (#11237) * Add nft_get_nft_info API (#11351) * Add nft_get_nft_info API * Refine NFT uncurry code * Resolve comments * Resolve comments Co-authored-by: ytx1991 * almost generates a new nft * generating nfts * rpc fix * DID set wallet name RPC API: Ensure that the wallet type is a DID wallet before proceeding. (#11391) * Initial iteration of the chia wallet did set_name command. (#11334) * make NFTWallet a dataclass (#11139) * make NFTWallet a dataclass * isort * flake8 * mypy * Optional * Implement nft transfer program * Resolve comment * transfer wip * checkpoint * basic rpc api working * Fix data hash issue * pre commit hooks passing, tests pass * removed old NFT tests * workflow files? * Attempt to fix workflows. * Workflows? * Prepare test blocks and plots for NFT wallet tests. * Reflect the previous commit into workflows. * clvm compilation fix * future proof create_coin in nft state layer * Add RPC client support for the create_new_wallet API. (#11430) * nft_get_nfts returns a nicer response * get nfts rpc should now conform to old format * checkpoint * Fix tests * Remove impossible case * metadata updater working with latest puzzles * removed unused method * mypy fix * Bumping GUI to df86eca99aee33a484570f06588e9fdd6506c661 * added state change events in nft wallet (#11469) * added state change events in nft wallet * Attempt to fix workflows. Co-authored-by: Amine Khaldi * Bumping GUI to b4d3eebfe33345728a2314031ffad160865019da * fix for clvm compilation fail * made nft state layer more future proof * Add `chia.wallet.nft_wallet` to packages in setup.py * Initial iteration of the NFT0 chia wallet nft create command. (#11476) * Add wallet RPC client support for the NFT0 nft_mint_nft API. (#11477) * Automatically create NFT wallet (#11482) * Create NFT Wallet if there is not a NFT wallet * Fix pre-commit * Remove unnecessary log * fix to merge into main_dids * Initial iteration of the NFT0 chia nft mint command. * Initial iteration of the NFT0 chia wallet nft add_uri command. * Initial iteration of the NFT0 chia wallet nft transfer command. * Initial iteration of the NFT0 chia wallet nft list command. * NFT0: Make the wallet ID required for adding a URI. * Bumping GUI to 725d77abbae77463fea725396e169f4148b810b6 * NFT0: Make the wallet ID required for transferring an NFT. * Add fee option for NFT & limit NFT wallet creation (#11492) * Add fee option for NFT & limit NFT wallet creation * Fix pre-commit * Add fee for update metadata * Add the ability to set fees for minting NFTs, transferring NFTs and adding URIs to NFTs. * Fix handling fees on the RPC API side. * fix for missing minted coins * updating metadata with multiple uris fix * Consolidate test_did_rpc.py into test_wallet_rpc.py and make the tests use the wallet RPC client. (#4) * Update workflows and minor formatting. * faster tests, other fixes * potentially fixes the did test failure Co-authored-by: matt Co-authored-by: Yostra Co-authored-by: matt-o-how <48453825+matt-o-how@users.noreply.github.com> Co-authored-by: Jeff Co-authored-by: Kronus91 Co-authored-by: ytx1991 Co-authored-by: Arvid Norberg Co-authored-by: Kyle Altendorf Co-authored-by: dustinface <35775977+xdustinface@users.noreply.github.com> Co-authored-by: ChiaMineJP Co-authored-by: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Co-authored-by: David Barratt Co-authored-by: Chris Marslender Co-authored-by: Matt Hauff Co-authored-by: William Blanke Co-authored-by: Juraj Oršulić Co-authored-by: Florin Chirica Co-authored-by: Richard Kiss Co-authored-by: William Allen Co-authored-by: Adam Kelly <338792+aqk@users.noreply.github.com> Co-authored-by: Johannes Tysiak Co-authored-by: Don Kackman Co-authored-by: austinsirkin Co-authored-by: Earle Lowe <30607889+emlowe@users.noreply.github.com> Co-authored-by: Brandon Butler Co-authored-by: Dave <72020697+daverof@users.noreply.github.com> Co-authored-by: Freddie Coleman Co-authored-by: arty Co-authored-by: Francesco Truzzi Co-authored-by: hugepants Co-authored-by: Jack Nelson Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gene Hoffman Co-authored-by: Gene Hoffman <30377676+hoffmang9@users.noreply.github.com> Co-authored-by: roseiliend <90035993+roseiliend@users.noreply.github.com> Co-authored-by: almog Co-authored-by: wallentx Co-authored-by: Earle Lowe Co-authored-by: xdustinface Co-authored-by: Patrick Maslana <79757486+pmaslana@users.noreply.github.com> Co-authored-by: Sebastjan Co-authored-by: Andreas Greimel --- .../build-test-macos-wallet-did_wallet.yml | 20 +- .../build-test-macos-wallet-nft_wallet.yml | 117 +++ .../build-test-ubuntu-wallet-did_wallet.yml | 20 +- .../build-test-ubuntu-wallet-nft_wallet.yml | 119 +++ .github/workflows/test-install-scripts.yml | 10 + CHANGELOG.md | 1 + chia/cmds/wallet.py | 228 +++++ chia/cmds/wallet_funcs.py | 100 +++ chia/full_node/mempool_manager.py | 1 + chia/rpc/wallet_rpc_api.py | 233 ++++- chia/rpc/wallet_rpc_client.py | 129 ++- chia/types/coin_spend.py | 1 - chia/wallet/cat_wallet/cat_utils.py | 8 + chia/wallet/cat_wallet/cat_wallet.py | 2 - chia/wallet/did_wallet/did_info.py | 3 +- chia/wallet/did_wallet/did_wallet.py | 823 ++++++++++++------ chia/wallet/did_wallet/did_wallet_puzzles.py | 197 ++++- chia/wallet/nft_wallet/__init__.py | 0 chia/wallet/nft_wallet/nft_info.py | 50 ++ chia/wallet/nft_wallet/nft_puzzles.py | 101 +++ chia/wallet/nft_wallet/nft_wallet.py | 564 ++++++++++++ chia/wallet/nft_wallet/uncurry_nft.py | 122 +++ chia/wallet/puzzles/cat.clvm.hex | 2 +- chia/wallet/puzzles/did_innerpuz.clvm | 127 ++- chia/wallet/puzzles/did_innerpuz.clvm.hex | 2 +- .../puzzles/did_innerpuz.clvm.hex.sha256tree | 2 +- chia/wallet/puzzles/json.clib | 25 + chia/wallet/puzzles/nft_innerpuz.clvm | 85 ++ chia/wallet/puzzles/nft_innerpuz.clvm.hex | 1 + .../puzzles/nft_innerpuz.clvm.hex.sha256tree | 1 + chia/wallet/puzzles/nft_metadata_updater.clvm | 20 + .../puzzles/nft_metadata_updater.clvm.hex | 1 + .../nft_metadata_updater.clvm.hex.sha256tree | 1 + .../puzzles/nft_metadata_updater_default.clvm | 21 + .../nft_metadata_updater_default.clvm.hex | 1 + ...tadata_updater_default.clvm.hex.sha256tree | 1 + .../nft_ownership_transfer_program.clvm | 83 ++ .../nft_ownership_transfer_program.clvm.hex | 1 + ...rship_transfer_program.clvm.hex.sha256tree | 1 + chia/wallet/puzzles/nft_state_layer.clvm | 99 +++ chia/wallet/puzzles/nft_state_layer.clvm.hex | 1 + .../nft_state_layer.clvm.hex.sha256tree | 1 + chia/wallet/puzzles/nft_transfer_program.clvm | 139 +++ .../puzzles/nft_transfer_program.clvm.hex | 1 + .../nft_transfer_program.clvm.hex.sha256tree | 1 + .../p2_delegated_puzzle_or_hidden_puzzle.clvm | 20 +- .../puzzles/settlement_payments.clvm.hex | 2 +- chia/wallet/puzzles/sha256tree.clib | 11 + .../puzzles/singleton_top_layer_v1_1.clvm | 124 +++ .../puzzles/singleton_top_layer_v1_1.clvm.hex | 1 + ...ngleton_top_layer_v1_1.clvm.hex.sha256tree | 1 + chia/wallet/util/json_clvm_utils.py | 17 + chia/wallet/util/wallet_types.py | 1 + chia/wallet/wallet.py | 10 +- chia/wallet/wallet_node.py | 2 + chia/wallet/wallet_puzzle_store.py | 1 + chia/wallet/wallet_state_manager.py | 273 ++++-- mypy.ini | 2 +- setup.py | 1 + tests/clvm/test_clvm_compilation.py | 7 + tests/wallet/did_wallet/config.py | 3 + tests/wallet/did_wallet/test_did.py | 421 +++++++-- tests/wallet/did_wallet/test_did_rpc.py | 121 --- tests/wallet/nft_wallet/__init__.py | 0 tests/wallet/nft_wallet/config.py | 1 + tests/wallet/nft_wallet/test_nft_clvm.py | 93 ++ tests/wallet/nft_wallet/test_nft_wallet.py | 468 ++++++++++ tests/wallet/rpc/test_wallet_rpc.py | 92 ++ 68 files changed, 4450 insertions(+), 688 deletions(-) create mode 100644 .github/workflows/build-test-macos-wallet-nft_wallet.yml create mode 100644 .github/workflows/build-test-ubuntu-wallet-nft_wallet.yml create mode 100644 chia/wallet/nft_wallet/__init__.py create mode 100644 chia/wallet/nft_wallet/nft_info.py create mode 100644 chia/wallet/nft_wallet/nft_puzzles.py create mode 100644 chia/wallet/nft_wallet/nft_wallet.py create mode 100644 chia/wallet/nft_wallet/uncurry_nft.py create mode 100644 chia/wallet/puzzles/json.clib create mode 100644 chia/wallet/puzzles/nft_innerpuz.clvm create mode 100644 chia/wallet/puzzles/nft_innerpuz.clvm.hex create mode 100644 chia/wallet/puzzles/nft_innerpuz.clvm.hex.sha256tree create mode 100644 chia/wallet/puzzles/nft_metadata_updater.clvm create mode 100644 chia/wallet/puzzles/nft_metadata_updater.clvm.hex create mode 100644 chia/wallet/puzzles/nft_metadata_updater.clvm.hex.sha256tree create mode 100644 chia/wallet/puzzles/nft_metadata_updater_default.clvm create mode 100644 chia/wallet/puzzles/nft_metadata_updater_default.clvm.hex create mode 100644 chia/wallet/puzzles/nft_metadata_updater_default.clvm.hex.sha256tree create mode 100644 chia/wallet/puzzles/nft_ownership_transfer_program.clvm create mode 100644 chia/wallet/puzzles/nft_ownership_transfer_program.clvm.hex create mode 100644 chia/wallet/puzzles/nft_ownership_transfer_program.clvm.hex.sha256tree create mode 100644 chia/wallet/puzzles/nft_state_layer.clvm create mode 100644 chia/wallet/puzzles/nft_state_layer.clvm.hex create mode 100644 chia/wallet/puzzles/nft_state_layer.clvm.hex.sha256tree create mode 100644 chia/wallet/puzzles/nft_transfer_program.clvm create mode 100644 chia/wallet/puzzles/nft_transfer_program.clvm.hex create mode 100644 chia/wallet/puzzles/nft_transfer_program.clvm.hex.sha256tree create mode 100644 chia/wallet/puzzles/sha256tree.clib create mode 100644 chia/wallet/puzzles/singleton_top_layer_v1_1.clvm create mode 100644 chia/wallet/puzzles/singleton_top_layer_v1_1.clvm.hex create mode 100644 chia/wallet/puzzles/singleton_top_layer_v1_1.clvm.hex.sha256tree create mode 100644 chia/wallet/util/json_clvm_utils.py create mode 100644 tests/wallet/did_wallet/config.py delete mode 100644 tests/wallet/did_wallet/test_did_rpc.py create mode 100644 tests/wallet/nft_wallet/__init__.py create mode 100644 tests/wallet/nft_wallet/config.py create mode 100644 tests/wallet/nft_wallet/test_nft_clvm.py create mode 100644 tests/wallet/nft_wallet/test_nft_wallet.py diff --git a/.github/workflows/build-test-macos-wallet-did_wallet.yml b/.github/workflows/build-test-macos-wallet-did_wallet.yml index 1848904023fa..145cb33742ca 100644 --- a/.github/workflows/build-test-macos-wallet-did_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-did_wallet.yml @@ -24,7 +24,7 @@ jobs: build: name: MacOS wallet-did_wallet Tests runs-on: ${{ matrix.os }} - timeout-minutes: 30 + timeout-minutes: 50 strategy: fail-fast: false max-parallel: 4 @@ -67,7 +67,21 @@ jobs: restore-keys: | ${{ runner.os }}-pip- -# Omitted checking out blocks and plots repo Chia-Network/test-cache + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots + with: + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: 0.29.0 + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: @@ -81,7 +95,7 @@ jobs: - name: Test wallet-did_wallet code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 0 -m "not benchmark" tests/wallet/did_wallet/test_did.py tests/wallet/did_wallet/test_did_rpc.py + venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 0 -m "not benchmark" tests/wallet/did_wallet/test_did.py - name: Process coverage data run: | diff --git a/.github/workflows/build-test-macos-wallet-nft_wallet.yml b/.github/workflows/build-test-macos-wallet-nft_wallet.yml new file mode 100644 index 000000000000..85b57e8eecf4 --- /dev/null +++ b/.github/workflows/build-test-macos-wallet-nft_wallet.yml @@ -0,0 +1,117 @@ +# +# THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme +# +name: MacOS wallet-nft_wallet Test + +on: + push: + branches: + - 'long_lived/**' + - main + - 'release/**' + tags: + - '**' + pull_request: + branches: + - '**' + +concurrency: + # SHA is added to the end if on `main` to let all main workflows run + group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/') || startsWith(github.ref, 'refs/heads/long_lived/')) && github.sha || '' }} + cancel-in-progress: true + +jobs: + build: + name: MacOS wallet-nft_wallet Tests + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + max-parallel: 4 + matrix: + python-version: ['3.9', '3.10'] + os: [macOS-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-nft_wallet + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup Python environment + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: Create keychain for CI use + run: | + security create-keychain -p foo chiachain + security default-keychain -s chiachain + security unlock-keychain -p foo chiachain + security set-keychain-settings -t 7200 -u chiachain + + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + + - name: Cache pip + uses: actions/cache@v3 + with: + # Note that new runners may break this https://github.com/actions/cache/issues/292 + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots + with: + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: 0.29.0 + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia + + - name: Run install script + env: + INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} + run: | + brew install boost + sh install.sh -d + +# Omitted installing Timelord + + - name: Test wallet-nft_wallet code with pytest + run: | + . ./activate + venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 0 -m "not benchmark" tests/wallet/nft_wallet/test_nft_clvm.py tests/wallet/nft_wallet/test_nft_wallet.py + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v3 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error +# +# THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme +# diff --git a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml index cd5904a3cdf9..9df193ef667b 100644 --- a/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-did_wallet.yml @@ -24,7 +24,7 @@ jobs: build: name: Ubuntu wallet-did_wallet Test runs-on: ${{ matrix.os }} - timeout-minutes: 30 + timeout-minutes: 50 strategy: fail-fast: false max-parallel: 4 @@ -67,7 +67,21 @@ jobs: restore-keys: | ${{ runner.os }}-pip- -# Omitted checking out blocks and plots repo Chia-Network/test-cache + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots + with: + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: 0.29.0 + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia - name: Run install script env: @@ -80,7 +94,7 @@ jobs: - name: Test wallet-did_wallet code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 0 -m "not benchmark" tests/wallet/did_wallet/test_did.py tests/wallet/did_wallet/test_did_rpc.py + venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 0 -m "not benchmark" tests/wallet/did_wallet/test_did.py - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-wallet-nft_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-nft_wallet.yml new file mode 100644 index 000000000000..ac6138078d75 --- /dev/null +++ b/.github/workflows/build-test-ubuntu-wallet-nft_wallet.yml @@ -0,0 +1,119 @@ +# +# THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme +# +name: Ubuntu wallet-nft_wallet Test + +on: + push: + branches: + - 'long_lived/**' + - main + - 'release/**' + tags: + - '**' + pull_request: + branches: + - '**' + +concurrency: + # SHA is added to the end if on `main` to let all main workflows run + group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/') || startsWith(github.ref, 'refs/heads/long_lived/')) && github.sha || '' }} + cancel-in-progress: true + +jobs: + build: + name: Ubuntu wallet-nft_wallet Test + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + max-parallel: 4 + matrix: + python-version: ['3.7', '3.8', '3.9', '3.10'] + os: [ubuntu-latest] + env: + CHIA_ROOT: ${{ github.workspace }}/.chia/mainnet + JOB_FILE_NAME: tests_${{ matrix.os }}_python-${{ matrix.python-version }}_wallet-nft_wallet + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup Python environment + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache npm + uses: actions/cache@v3 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + + - name: Cache pip + uses: actions/cache@v3 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Cache test blocks and plots + uses: actions/cache@v2 + id: test-blocks-plots + with: + path: | + ${{ github.workspace }}/.chia/blocks + ${{ github.workspace }}/.chia/test-plots + key: 0.29.0 + + - name: Checkout test blocks and plots + if: steps.test-blocks-plots.outputs.cache-hit != 'true' + run: | + wget -qO- https://github.com/Chia-Network/test-cache/archive/refs/tags/0.29.0.tar.gz | tar xzf - + mkdir ${{ github.workspace }}/.chia + mv ${{ github.workspace }}/test-cache-0.29.0/* ${{ github.workspace }}/.chia + + - name: Run install script + env: + INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} + run: | + sh install.sh -d + +# Omitted installing Timelord + + - name: Test wallet-nft_wallet code with pytest + run: | + . ./activate + venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 0 -m "not benchmark" tests/wallet/nft_wallet/test_nft_clvm.py tests/wallet/nft_wallet/test_nft_wallet.py + + - name: Process coverage data + run: | + venv/bin/coverage combine --rcfile=.coveragerc .coverage.* + venv/bin/coverage xml --rcfile=.coveragerc -o coverage.xml + mkdir coverage_reports + cp .coverage "coverage_reports/.coverage.${{ env.JOB_FILE_NAME }}" + cp coverage.xml "coverage_reports/coverage.${{ env.JOB_FILE_NAME }}.xml" + venv/bin/coverage report --rcfile=.coveragerc --show-missing + + - name: Publish coverage + uses: actions/upload-artifact@v3 + with: + name: coverage + path: coverage_reports/* + if-no-files-found: error + +# Omitted resource usage check + +# +# THIS FILE IS GENERATED. SEE https://github.com/Chia-Network/chia-blockchain/tree/main/tests#readme +# diff --git a/.github/workflows/test-install-scripts.yml b/.github/workflows/test-install-scripts.yml index 58a86d14c7fe..dce814839df3 100644 --- a/.github/workflows/test-install-scripts.yml +++ b/.github/workflows/test-install-scripts.yml @@ -197,6 +197,16 @@ jobs: apt-get --yes update apt-get install --yes git lsb-release sudo + # @TODO this step can be removed once Python 3.10 is supported + # Python 3.10 is now the default in bookworm, so install 3.9 specifically so install does not fail + - name: Prepare debian:bookworm + if: ${{ matrix.distribution.name == 'debian:bookworm' }} + env: + DEBIAN_FRONTEND: noninteractive + run: | + apt-get update -y + apt-get install -y python3.9-venv + - name: Prepare Fedora if: ${{ matrix.distribution.type == 'fedora' }} run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index aaf9ee5f9e01..ff1431c56ccc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -263,6 +263,7 @@ 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. diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py index 9486e3947059..b277f0f5be9e 100644 --- a/chia/cmds/wallet.py +++ b/chia/cmds/wallet.py @@ -3,6 +3,7 @@ import click +from chia.cmds.plotnft import validate_fee from chia.wallet.util.wallet_types import WalletType from chia.wallet.transaction_sorting import SortKey @@ -391,3 +392,230 @@ def cancel_offer_cmd(wallet_rpc_port: Optional[int], fingerprint: int, id: str, from .wallet_funcs import execute_with_wallet, cancel_offer asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, cancel_offer)) + + +@wallet_cmd.group("did", short_help="DID related actions") +def did_cmd(): + pass + + +@did_cmd.command("create", short_help="Create DID wallet") +@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("-n", "--name", help="Set the DID wallet name", type=str) +@click.option( + "-a", + "--amount", + help="Set the DID amount in mojos. Value must be an odd number.", + type=int, + default=1, + show_default=True, +) +@click.option( + "-m", + "--fee", + help="Set the fees per transaction, in XCH.", + type=str, + default="0", + show_default=True, + callback=validate_fee, +) +def did_create_wallet_cmd( + wallet_rpc_port: Optional[int], fingerprint: int, name: Optional[str], amount: Optional[int], fee: Optional[int] +) -> None: + import asyncio + from .wallet_funcs import execute_with_wallet, create_did_wallet + + extra_params = {"amount": amount, "fee": fee, "name": name} + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, create_did_wallet)) + + +@did_cmd.command("set_name", short_help="Set DID wallet name") +@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", "--id", help="Id of the wallet to use", type=int, required=True) +@click.option("-n", "--name", help="Set the DID wallet name", type=str, required=True) +def did_wallet_name_cmd(wallet_rpc_port: Optional[int], fingerprint: int, id: int, name: str) -> None: + import asyncio + from .wallet_funcs import execute_with_wallet, did_set_wallet_name + + extra_params = {"wallet_id": id, "name": name} + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, did_set_wallet_name)) + + +@wallet_cmd.group("nft", short_help="NFT related actions") +def nft_cmd(): + pass + + +@nft_cmd.command("create", short_help="Create an NFT wallet") +@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 nft_wallet_create_cmd(wallet_rpc_port: Optional[int], fingerprint: int) -> None: + import asyncio + from .wallet_funcs import execute_with_wallet, create_nft_wallet + + extra_params: Dict[str, Any] = {} + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, create_nft_wallet)) + + +@nft_cmd.command("mint", short_help="Mint an NFT") +@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", "--id", help="Id of the NFT wallet to use", type=int, required=True) +@click.option("-aa", "--artist-address", help="Artist's backpayment address", type=str, required=True) +@click.option("-nh", "--hash", help="NFT content hash", type=str, required=True) +@click.option("-u", "--uris", help="Comma separated list of URIs", type=str, required=True) +@click.option( + "-m", + "--fee", + help="Set the fees per transaction, in XCH.", + type=str, + default="0", + show_default=True, + callback=validate_fee, +) +def nft_mint_cmd( + wallet_rpc_port: Optional[int], + fingerprint: int, + id: int, + artist_address: str, + hash: str, + uris: str, + fee: str, +) -> None: + import asyncio + from .wallet_funcs import execute_with_wallet, mint_nft + + extra_params = { + "wallet_id": id, + "artist_address": artist_address, + "hash": hash, + "uris": [u.strip() for u in uris.split(",")], + "fee": fee, + } + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, mint_nft)) + + +@nft_cmd.command("add_uri", short_help="Add an URI to an NFT") +@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", "--id", help="Id of the NFT wallet to use", type=int, required=True) +@click.option("-ni", "--nft-coin-id", help="Id of the NFT coin to add the URI to", type=str, required=True) +@click.option("-u", "--uri", help="URI to add to the NFT", type=str, required=True) +@click.option( + "-m", + "--fee", + help="Set the fees per transaction, in XCH.", + type=str, + default="0", + show_default=True, + callback=validate_fee, +) +def nft_add_uri_cmd( + wallet_rpc_port: Optional[int], + fingerprint: int, + id: int, + nft_coin_id: str, + uri: str, + fee: str, +) -> None: + import asyncio + from .wallet_funcs import execute_with_wallet, add_uri_to_nft + + extra_params = { + "wallet_id": id, + "nft_coin_id": nft_coin_id, + "uri": uri, + "fee": fee, + } + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, add_uri_to_nft)) + + +@nft_cmd.command("transfer", short_help="Transfer an NFT") +@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", "--id", help="Id of the NFT wallet to use", type=int, required=True) +@click.option("-ni", "--nft-coin-id", help="Id of the NFT coin to transfer", type=str, required=True) +@click.option("-aa", "--artist-address", help="Target artist's wallet address", type=str, required=True) +@click.option( + "-m", + "--fee", + help="Set the fees per transaction, in XCH.", + type=str, + default="0", + show_default=True, + callback=validate_fee, +) +def nft_transfer_cmd( + wallet_rpc_port: Optional[int], + fingerprint: int, + id: int, + nft_coin_id: str, + artist_address: str, + fee: str, +) -> None: + import asyncio + from .wallet_funcs import execute_with_wallet, transfer_nft + + extra_params = { + "wallet_id": id, + "nft_coin_id": nft_coin_id, + "artist_address": artist_address, + "fee": fee, + } + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, transfer_nft)) + + +@nft_cmd.command("list", short_help="List the current NFTs") +@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", "--id", help="Id of the NFT wallet to use", type=int, required=True) +def nft_list_cmd(wallet_rpc_port: Optional[int], fingerprint: int, id: int) -> None: + import asyncio + from .wallet_funcs import execute_with_wallet, list_nfts + + extra_params = {"wallet_id": id} + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, list_nfts)) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 20488eaa409c..210f1d6d2a77 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -643,3 +643,103 @@ async def execute_with_wallet( print(f"Exception from 'wallet' {e}") wallet_client.close() await wallet_client.await_closed() + + +async def create_did_wallet(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: + amount = args["amount"] + fee = args["fee"] + name = args["name"] + try: + response = await wallet_client.create_new_did_wallet(amount, fee, name) + wallet_id = response["wallet_id"] + my_did = response["my_did"] + print(f"Successfully created a DID wallet with name {name} and id {wallet_id} on key {fingerprint}") + print(f"Successfully created a DID {my_did} in the newly created DID wallet") + except Exception as e: + print(f"Failed to create DID wallet: {e}") + + +async def did_set_wallet_name(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: + wallet_id = args["wallet_id"] + name = args["name"] + try: + await wallet_client.did_set_wallet_name(wallet_id, name) + print(f"Successfully set a new name for DID wallet with id {wallet_id}: {name}") + except Exception as e: + print(f"Failed to set DID wallet name: {e}") + + +async def create_nft_wallet(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: + try: + response = await wallet_client.create_new_nft_wallet(None) + wallet_id = response["wallet_id"] + print(f"Successfully created an NFT wallet with id {wallet_id} on key {fingerprint}") + except Exception as e: + print(f"Failed to create NFT wallet: {e}") + + +async def mint_nft(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: + try: + wallet_id = args["wallet_id"] + artist_address = args["artist_address"] + hash = args["hash"] + uris = args["uris"] + fee = args["fee"] + response = await wallet_client.mint_nft(wallet_id, artist_address, hash, uris, fee) + spend_bundle = response["spend_bundle"] + print(f"NFT minted Successfully with spend bundle: {spend_bundle}") + except Exception as e: + print(f"Failed to mint NFT: {e}") + + +async def add_uri_to_nft(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: + try: + wallet_id = args["wallet_id"] + nft_coin_id = args["nft_coin_id"] + uri = args["uri"] + fee = args["fee"] + response = await wallet_client.add_uri_to_nft(wallet_id, nft_coin_id, uri, fee) + spend_bundle = response["spend_bundle"] + print(f"URI added successfully with spend bundle: {spend_bundle}") + except Exception as e: + print(f"Failed to add URI to NFT: {e}") + + +async def transfer_nft(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: + try: + wallet_id = args["wallet_id"] + nft_coin_id = args["nft_coin_id"] + artist_address = args["artist_address"] + fee = args["fee"] + response = await wallet_client.transfer_nft(wallet_id, nft_coin_id, artist_address, fee) + spend_bundle = response["spend_bundle"] + print(f"NFT transferred successfully with spend bundle: {spend_bundle}") + except Exception as e: + print(f"Failed to transfer NFT: {e}") + + +async def list_nfts(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: + wallet_id = args["wallet_id"] + try: + response = await wallet_client.list_nfts(wallet_id) + nft_list = response["nft_list"] + if len(nft_list) > 0: + from chia.wallet.nft_wallet.nft_info import NFTInfo + + indent: str = " " + + for n in nft_list: + nft = NFTInfo.from_json_dict(n) + print() + print(f"{'Launcher coin ID:'.ljust(23)} {nft.launcher_id}") + print(f"{'Current NFT coin ID:'.ljust(23)} {nft.nft_coin_id}") + print(f"{'NFT content hash:'.ljust(23)} {nft.data_hash}") + print(f"{'Current NFT version:'.ljust(23)} {nft.version}") + print() + print("URIs:") + for uri in nft.data_uris: + print(f"{indent}{uri}") + else: + print(f"No NFTs found for wallet with id {wallet_id} on key {fingerprint}") + except Exception as e: + print(f"Failed to list NFTs for wallet with id {wallet_id} on key {fingerprint}: {e}") diff --git a/chia/full_node/mempool_manager.py b/chia/full_node/mempool_manager.py index 6bbddb549d71..eb11ba13442e 100644 --- a/chia/full_node/mempool_manager.py +++ b/chia/full_node/mempool_manager.py @@ -316,6 +316,7 @@ async def add_spendbundle( # build removal list removal_names: List[bytes32] = [spend.coin_id for spend in npc_result.conds.spends] if set(removal_names) != set([s.name() for s in new_spend.removals()]): + # If you reach here it's probably because your program reveal doesn't match the coin's puzzle hash return None, MempoolInclusionStatus.FAILED, Err.INVALID_SPEND_BUNDLE additions = additions_for_npc(npc_result) diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 7c7c487969d4..62006e742ba4 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -1,24 +1,27 @@ import asyncio import dataclasses +import json import logging from pathlib import Path -from typing import Callable, Dict, List, Optional, Tuple, Set, Any +from typing import Any, Callable, Dict, List, Optional, Set, Tuple -from blspy import PrivateKey, G1Element +from blspy import G1Element, PrivateKey from chia.consensus.block_rewards import calculate_base_farmer_reward from chia.pools.pool_wallet import PoolWallet -from chia.pools.pool_wallet_info import create_pool_state, FARMING_TO_POOL, PoolWalletInfo, PoolState +from chia.pools.pool_wallet_info import FARMING_TO_POOL, PoolState, PoolWalletInfo, create_pool_state from chia.protocols.protocol_message_types import ProtocolMessageTypes from chia.server.outbound_message import NodeType, make_msg from chia.simulator.simulator_protocol import FarmNewBlockProtocol from chia.types.announcement import Announcement from chia.types.blockchain_format.coin import Coin, coin_as_list +from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.spend_bundle import SpendBundle from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash from chia.util.byte_types import hexstr_to_bytes -from chia.util.ints import uint32, uint64, uint8 +from chia.util.config import load_config +from chia.util.ints import uint8, uint32, uint64 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 @@ -32,6 +35,8 @@ match_address_to_sk, ) from chia.wallet.did_wallet.did_wallet import DIDWallet +from chia.wallet.nft_wallet.nft_puzzles import get_nft_info_from_puzzle +from chia.wallet.nft_wallet.nft_wallet import NFTWallet from chia.wallet.outer_puzzles import AssetType from chia.wallet.puzzle_drivers import PuzzleInfo from chia.wallet.rl_wallet.rl_wallet import RLWallet @@ -42,7 +47,6 @@ from chia.wallet.util.wallet_types import AmountWithPuzzlehash, WalletType from chia.wallet.wallet_info import WalletInfo from chia.wallet.wallet_node import WalletNode -from chia.util.config import load_config # Timeout for response from wallet/full node for sending a transaction TIMEOUT = 30 @@ -110,14 +114,25 @@ def get_routes(self) -> Dict[str, Callable]: "/cancel_offer": self.cancel_offer, "/get_cat_list": self.get_cat_list, # DID Wallet + "/did_set_wallet_name": self.did_set_wallet_name, + "/did_get_wallet_name": self.did_get_wallet_name, "/did_update_recovery_ids": self.did_update_recovery_ids, + "/did_update_metadata": self.did_update_metadata, "/did_get_pubkey": self.did_get_pubkey, "/did_get_did": self.did_get_did, "/did_recovery_spend": self.did_recovery_spend, "/did_get_recovery_list": self.did_get_recovery_list, + "/did_get_metadata": self.did_get_metadata, "/did_create_attest": self.did_create_attest, "/did_get_information_needed_for_recovery": self.did_get_information_needed_for_recovery, + "/did_get_current_coin_info": self.did_get_current_coin_info, "/did_create_backup_file": self.did_create_backup_file, + "/did_transfer_did": self.did_transfer_did, + # NFT Wallet + "/nft_mint_nft": self.nft_mint_nft, + "/nft_get_nfts": self.nft_get_nfts, + "/nft_transfer_nft": self.nft_transfer_nft, + "/nft_add_uri": self.nft_add_uri, # RL wallet "/rl_set_user_info": self.rl_set_user_info, "/send_clawback_transaction:": self.send_clawback_transaction, @@ -500,6 +515,11 @@ async def create_new_wallet(self, request: Dict): backup_dids.append(hexstr_to_bytes(d)) if len(backup_dids) > 0: num_needed = uint64(request["num_of_backup_ids_needed"]) + metadata: Dict[str, str] = {} + if "metadata" in request: + if type(request["metadata"]) is dict: + metadata = request["metadata"] + async with self.service.wallet_state_manager.lock: did_wallet: DIDWallet = await DIDWallet.create_new_did_wallet( wallet_state_manager, @@ -507,7 +527,11 @@ async def create_new_wallet(self, request: Dict): uint64(request["amount"]), backup_dids, uint64(num_needed), + metadata, + request.get("wallet_name", None), + uint64(request.get("fee", 0)), ) + my_did = did_wallet.get_my_DID() return { "success": True, @@ -519,7 +543,7 @@ async def create_new_wallet(self, request: Dict): elif request["did_type"] == "recovery": async with self.service.wallet_state_manager.lock: did_wallet = await DIDWallet.create_new_did_wallet_from_recovery( - wallet_state_manager, main_wallet, request["filename"] + wallet_state_manager, main_wallet, request["backup_data"] ) assert did_wallet.did_info.temp_coin is not None assert did_wallet.did_info.temp_puzhash is not None @@ -543,7 +567,26 @@ async def create_new_wallet(self, request: Dict): } else: # undefined did_type pass - + elif request["wallet_type"] == "nft_wallet": + for wallet in self.service.wallet_state_manager.wallets.values(): + if wallet.type() == WalletType.NFT: + # TODO Modify this for NFT1 + log.info("NFT wallet already existed, skipping.") + return { + "success": True, + "type": wallet.type(), + "wallet_id": wallet.id(), + } + async with self.service.wallet_state_manager.lock: + nft_wallet: NFTWallet = await NFTWallet.create_new_nft_wallet( + wallet_state_manager, + main_wallet, + ) + return { + "success": True, + "type": nft_wallet.type(), + "wallet_id": nft_wallet.id(), + } elif request["wallet_type"] == "pool_wallet": if request["mode"] == "new": owner_puzzle_hash: bytes32 = await self.service.wallet_state_manager.main_wallet.get_puzzle_hash(True) @@ -1065,10 +1108,28 @@ async def cancel_offer(self, request: Dict): # Distributed Identities ########################################################################################## + async def did_set_wallet_name(self, request): + assert self.service.wallet_state_manager is not None + wallet_id = int(request["wallet_id"]) + wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id] + if wallet.type() == WalletType.DISTRIBUTED_ID: + await wallet.set_name(str(request["name"])) + return {"success": True, "wallet_id": wallet_id} + else: + return {"success": False, "error": f"Wallet id {wallet_id} is not a DID wallet"} + + async def did_get_wallet_name(self, request): + assert self.service.wallet_state_manager is not None + wallet_id = int(request["wallet_id"]) + wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id] + name: str = await wallet.get_name() + return {"success": True, "wallet_id": wallet_id, "name": name} + async def did_update_recovery_ids(self, request): wallet_id = int(request["wallet_id"]) wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id] recovery_list = [] + success: bool = False for _ in request["new_list"]: recovery_list.append(hexstr_to_bytes(_)) if "num_verifications_required" in request: @@ -1078,9 +1139,27 @@ async def did_update_recovery_ids(self, request): async with self.service.wallet_state_manager.lock: update_success = await wallet.update_recovery_list(recovery_list, new_amount_verifications_required) # Update coin with new ID info - spend_bundle = await wallet.create_update_spend() + if update_success: + spend_bundle = await wallet.create_update_spend() + if spend_bundle is not None: + success = True + return {"success": success} - success = spend_bundle is not None and update_success + async def did_update_metadata(self, request): + wallet_id = int(request["wallet_id"]) + wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id] + metadata: Dict[str, str] = {} + success: bool = False + if "metadata" in request: + if type(request["metadata"]) is dict: + metadata = request["metadata"] + async with self.service.wallet_state_manager.lock: + update_success = await wallet.update_metadata(metadata) + # Update coin with new ID info + if update_success: + spend_bundle = await wallet.create_update_spend() + if spend_bundle is not None: + success = True return {"success": success} async def did_get_did(self, request): @@ -1105,21 +1184,31 @@ async def did_get_recovery_list(self, request): return { "success": True, "wallet_id": wallet_id, - "recover_list": recover_hex_list, + "recovery_list": recover_hex_list, "num_required": wallet.did_info.num_of_backup_ids_needed, } + async def did_get_metadata(self, request): + wallet_id = int(request["wallet_id"]) + wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id] + metadata = json.loads(wallet.did_info.metadata) + return { + "success": True, + "wallet_id": wallet_id, + "metadata": metadata, + } + async def did_recovery_spend(self, request): wallet_id = int(request["wallet_id"]) wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id] - if len(request["attest_filenames"]) < wallet.did_info.num_of_backup_ids_needed: + if len(request["attest_data"]) < wallet.did_info.num_of_backup_ids_needed: return {"success": False, "reason": "insufficient messages"} async with self.service.wallet_state_manager.lock: ( info_list, message_spend_bundle, - ) = await wallet.load_attest_files_for_recovery_spend(request["attest_filenames"]) + ) = await wallet.load_attest_files_for_recovery_spend(request["attest_data"]) if "pubkey" in request: pubkey = G1Element.from_bytes(hexstr_to_bytes(request["pubkey"])) @@ -1155,14 +1244,17 @@ async def did_create_attest(self, request): info = await wallet.get_info_for_recovery() coin = hexstr_to_bytes(request["coin_name"]) pubkey = G1Element.from_bytes(hexstr_to_bytes(request["pubkey"])) - spend_bundle = await wallet.create_attestment( - coin, hexstr_to_bytes(request["puzhash"]), pubkey, request["filename"] + spend_bundle, attest_data = await wallet.create_attestment( + coin, + hexstr_to_bytes(request["puzhash"]), + pubkey, ) - if spend_bundle is not None: + if info is not None and spend_bundle is not None: return { "success": True, "message_spend_bundle": bytes(spend_bundle).hex(), "info": [info[0].hex(), info[1].hex(), info[2]], + "attest_data": attest_data, } else: return {"success": False} @@ -1182,14 +1274,113 @@ async def did_get_information_needed_for_recovery(self, request): "backup_dids": did_wallet.did_info.backup_ids, } + async def did_get_current_coin_info(self, request): + wallet_id = int(request["wallet_id"]) + did_wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id] + my_did = did_wallet.get_my_DID() + did_coin_threeple = await did_wallet.get_info_for_recovery() + assert my_did is not None + assert did_coin_threeple is not None + return { + "success": True, + "wallet_id": wallet_id, + "my_did": my_did, + "did_parent": did_coin_threeple[0], + "did_innerpuz": did_coin_threeple[1], + "did_amount": did_coin_threeple[2], + } + async def did_create_backup_file(self, request): + wallet_id = int(request["wallet_id"]) + did_wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id] + return {"wallet_id": wallet_id, "success": True, "backup_data": did_wallet.create_backup()} + + async def did_transfer_did(self, request): + assert self.service.wallet_state_manager is not None + if await self.service.wallet_state_manager.synced() is False: + raise ValueError("Wallet needs to be fully synced.") + wallet_id = int(request["wallet_id"]) + did_wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id] + puzzle_hash: bytes32 = decode_puzzle_hash(request["inner_address"]) + async with self.service.wallet_state_manager.lock: + txs: TransactionRecord = await did_wallet.transfer_did( + puzzle_hash, uint64(request.get("fee", 0)), request.get("with_recovery_info", True) + ) + + return { + "success": True, + "transaction": txs.to_json_dict_convenience(self.service.config), + "transaction_id": txs.name, + } + + ########################################################################################## + # NFT Wallet + ########################################################################################## + + async def nft_mint_nft(self, request) -> Dict: + log.debug("Got minting RPC request: %s", request) + wallet_id = uint32(request["wallet_id"]) + assert self.service.wallet_state_manager + nft_wallet: NFTWallet = self.service.wallet_state_manager.wallets[wallet_id] + assert nft_wallet.type() == WalletType.NFT.value, nft_wallet.type() + address = request.get("artist_address") + if isinstance(address, str): + puzzle_hash = decode_puzzle_hash(address) + elif address is None: + puzzle_hash = await nft_wallet.standard_wallet.get_new_puzzlehash() + else: + puzzle_hash = address + metadata = Program.to( + [ + ("u", request["uris"]), + ("h", hexstr_to_bytes(request["hash"])), + ] + ) + fee = uint64(request.get("fee", 0)) + spend_bundle = await nft_wallet.generate_new_nft(metadata, puzzle_hash, fee=fee) + return {"wallet_id": wallet_id, "success": True, "spend_bundle": spend_bundle} + + async def nft_get_nfts(self, request) -> Dict: + wallet_id = uint32(request["wallet_id"]) + assert self.service.wallet_state_manager is not None + nft_wallet: NFTWallet = self.service.wallet_state_manager.wallets[wallet_id] + nfts = nft_wallet.get_current_nfts() + nft_info_list = [] + for nft in nfts: + nft_info_list.append(get_nft_info_from_puzzle(nft.full_puzzle, nft.coin)) + return {"wallet_id": wallet_id, "success": True, "nft_list": nft_info_list} + + async def nft_transfer_nft(self, request): + assert self.service.wallet_state_manager is not None + wallet_id = uint32(request["wallet_id"]) + address = request["target_address"] + if isinstance(address, str): + puzzle_hash = decode_puzzle_hash(address) + else: + return dict(success=False, error="target_address parameter missing") + nft_wallet: NFTWallet = self.service.wallet_state_manager.wallets[wallet_id] try: - wallet_id = int(request["wallet_id"]) - did_wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id] - did_wallet.create_backup(request["filename"]) - return {"wallet_id": wallet_id, "success": True} - except Exception: - return {"wallet_id": wallet_id, "success": False} + nft_coin_info = nft_wallet.get_nft_coin_by_id(bytes32.from_hexstr(request["nft_coin_id"])) + fee = uint64(request.get("fee", 0)) + spend_bundle = await nft_wallet.transfer_nft(nft_coin_info, puzzle_hash, fee=fee) + return {"wallet_id": wallet_id, "success": True, "spend_bundle": spend_bundle} + except Exception as e: + log.exception(f"Failed to transfer NFT: {e}") + return {"success": False, "error": str(e)} + + async def nft_add_uri(self, request) -> Dict: + assert self.service.wallet_state_manager is not None + wallet_id = uint32(request["wallet_id"]) + uri = request["uri"] + nft_wallet: NFTWallet = self.service.wallet_state_manager.wallets[wallet_id] + try: + nft_coin_info = nft_wallet.get_nft_coin_by_id(bytes32.from_hexstr(request["nft_coin_id"])) + fee = uint64(request.get("fee", 0)) + spend_bundle = await nft_wallet.update_metadata(nft_coin_info, uri, fee=fee) + return {"wallet_id": wallet_id, "success": True, "spend_bundle": spend_bundle} + except Exception as e: + log.exception(f"Failed to update NFT metadata: {e}") + return {"success": False, "error": str(e)} ########################################################################################## # Rate Limited Wallet diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 28dcf1872537..e0c98fbe72c6 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -240,18 +240,73 @@ async def select_coins(self, *, amount: int, wallet_id: int) -> List[Coin]: return [Coin.from_json_dict(coin) for coin in response["coins"]] # DID wallet - async def create_new_did_wallet(self, amount): + async def create_new_did_wallet( + self, + amount: int, + fee: int = 0, + name: Optional[str] = "DID Wallet", + backup_ids: List[str] = [], + required_num: int = 0, + ) -> Dict: request: Dict[str, Any] = { "wallet_type": "did_wallet", "did_type": "new", - "backup_dids": [], - "num_of_backup_ids_needed": 0, + "backup_dids": backup_ids, + "num_of_backup_ids_needed": required_num, "amount": amount, + "fee": fee, + "wallet_name": name, } response = await self.fetch("create_new_wallet", request) return response - async def create_new_did_wallet_from_recovery(self, filename): + async def get_did_id(self, wallet_id: int) -> Dict: + request: Dict[str, Any] = { + "wallet_id": wallet_id, + } + response = await self.fetch("did_get_did", request) + return response + + async def create_did_backup_file(self, wallet_id: int, filename: str) -> Dict: + request: Dict[str, Any] = { + "wallet_id": wallet_id, + "filename": filename, + } + response = await self.fetch("did_create_backup_file", request) + return response + + async def update_did_recovery_list(self, wallet_id: int, recovery_list: List[str], num_verification: int) -> Dict: + request: Dict[str, Any] = { + "wallet_id": wallet_id, + "new_list": recovery_list, + "num_verifications_required": num_verification, + } + response = await self.fetch("did_update_recovery_ids", request) + return response + + async def get_did_recovery_list(self, wallet_id: int) -> Dict: + request: Dict[str, Any] = { + "wallet_id": wallet_id, + } + response = await self.fetch("did_get_recovery_list", request) + return response + + async def update_did_metadata(self, wallet_id: int, metadata: Dict) -> Dict: + request: Dict[str, Any] = { + "wallet_id": wallet_id, + "metadata": metadata, + } + response = await self.fetch("did_update_metadata", request) + return response + + async def get_did_metadata(self, wallet_id: int) -> Dict: + request: Dict[str, Any] = { + "wallet_id": wallet_id, + } + response = await self.fetch("did_get_metadata", request) + return response + + async def create_new_did_wallet_from_recovery(self, filename: str) -> Dict: request: Dict[str, Any] = { "wallet_type": "did_wallet", "did_type": "recovery", @@ -260,7 +315,9 @@ async def create_new_did_wallet_from_recovery(self, filename): response = await self.fetch("create_new_wallet", request) return response - async def did_create_attest(self, wallet_id, coin_name, pubkey, puzhash, file_name): + async def did_create_attest( + self, wallet_id: int, coin_name: str, pubkey: str, puzhash: str, file_name: str + ) -> Dict: request: Dict[str, Any] = { "wallet_id": wallet_id, "coin_name": coin_name, @@ -271,7 +328,7 @@ async def did_create_attest(self, wallet_id, coin_name, pubkey, puzhash, file_na response = await self.fetch("did_create_attest", request) return response - async def did_recovery_spend(self, wallet_id, attest_filenames): + async def did_recovery_spend(self, wallet_id: int, attest_filenames: str) -> Dict: request: Dict[str, Any] = { "wallet_id": wallet_id, "attest_filenames": attest_filenames, @@ -279,6 +336,26 @@ async def did_recovery_spend(self, wallet_id, attest_filenames): response = await self.fetch("did_recovery_spend", request) return response + async def did_transfer_did(self, wallet_id: int, address: str, fee: int, with_recovery: bool) -> Dict: + request: Dict[str, Any] = { + "wallet_id": wallet_id, + "inner_address": address, + "fee": fee, + "with_recovery_info": with_recovery, + } + response = await self.fetch("did_transfer_did", request) + return response + + async def did_set_wallet_name(self, wallet_id: int, name: str) -> Dict: + request = {"wallet_id": wallet_id, "name": name} + response = await self.fetch("did_set_wallet_name", request) + return response + + async def did_get_wallet_name(self, wallet_id: int) -> Dict: + request = {"wallet_id": wallet_id} + response = await self.fetch("did_get_wallet_name", request) + return response + # TODO: test all invocations of create_new_pool_wallet with new fee arg. async def create_new_pool_wallet( self, @@ -496,3 +573,43 @@ async def get_all_offers( async def cancel_offer(self, trade_id: bytes32, fee=uint64(0), secure: bool = True): await self.fetch("cancel_offer", {"trade_id": trade_id.hex(), "secure": secure, "fee": fee}) + + # NFT wallet + async def create_new_nft_wallet(self, did_wallet_id): + request: Dict[str, Any] = { + "wallet_type": "nft_wallet", + "did_wallet_id": did_wallet_id, + } + response = await self.fetch("create_new_wallet", request) + return response + + async def mint_nft(self, wallet_id, artist_address, hash, uris, fee): + request: Dict[str, Any] = { + "wallet_id": wallet_id, + "artist_address": artist_address, + "hash": hash, + "uris": uris, + "fee": fee, + } + response = await self.fetch("nft_mint_nft", request) + return response + + async def add_uri_to_nft(self, wallet_id, nft_coin_id, uri, fee): + request: Dict[str, Any] = {"wallet_id": wallet_id, "nft_coin_id": nft_coin_id, "uri": uri, "fee": fee} + response = await self.fetch("nft_add_uri", request) + return response + + async def transfer_nft(self, wallet_id, nft_coin_id, artist_address, fee): + request: Dict[str, Any] = { + "wallet_id": wallet_id, + "nft_coin_id": nft_coin_id, + "target_address": artist_address, + "fee": fee, + } + response = await self.fetch("nft_transfer_nft", request) + return response + + async def list_nfts(self, wallet_id): + request: Dict[str, Any] = {"wallet_id": wallet_id} + response = await self.fetch("nft_get_nfts", request) + return response diff --git a/chia/types/coin_spend.py b/chia/types/coin_spend.py index 895fd761c3dc..9efc6aece8fd 100644 --- a/chia/types/coin_spend.py +++ b/chia/types/coin_spend.py @@ -1,6 +1,5 @@ from dataclasses import dataclass from typing import List - from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import SerializedProgram, INFINITE_COST from chia.util.chain_utils import additions_for_solution, fee_for_solution diff --git a/chia/wallet/cat_wallet/cat_utils.py b/chia/wallet/cat_wallet/cat_utils.py index 264d985fd88a..3e3004a530fc 100644 --- a/chia/wallet/cat_wallet/cat_utils.py +++ b/chia/wallet/cat_wallet/cat_utils.py @@ -41,6 +41,14 @@ def match_cat_puzzle(puzzle: Program) -> Tuple[bool, Iterator[Program]]: return False, iter(()) +def get_innerpuzzle_from_puzzle(puzzle: Program) -> Program: + mod, curried_args = puzzle.uncurry() + if mod == CAT_MOD: + return curried_args.rest().rest().first() + else: + raise ValueError("Not a CAT puzzle") + + def construct_cat_puzzle(mod_code: Program, limitations_program_hash: bytes32, inner_puzzle: Program) -> Program: """ Given an inner puzzle hash and tail hash calculate a puzzle program for a specific cc. diff --git a/chia/wallet/cat_wallet/cat_wallet.py b/chia/wallet/cat_wallet/cat_wallet.py index 5abe1e45b6ae..fa88b3913e51 100644 --- a/chia/wallet/cat_wallet/cat_wallet.py +++ b/chia/wallet/cat_wallet/cat_wallet.py @@ -733,7 +733,6 @@ async def generate_signed_transaction( max_send = await self.get_max_send_amount() if payment_sum > max_send: raise ValueError(f"Can't send more than {max_send} in a single transaction") - unsigned_spend_bundle, chia_tx = await self.generate_unsigned_spendbundle( payments, fee, @@ -742,7 +741,6 @@ async def generate_signed_transaction( puzzle_announcements_to_consume=puzzle_announcements_to_consume, ) spend_bundle = await self.sign(unsigned_spend_bundle) - # TODO add support for array in stored records tx_list = [ TransactionRecord( diff --git a/chia/wallet/did_wallet/did_info.py b/chia/wallet/did_wallet/did_info.py index d4a83a77e42c..cd84153016fc 100644 --- a/chia/wallet/did_wallet/did_info.py +++ b/chia/wallet/did_wallet/did_info.py @@ -13,7 +13,7 @@ @dataclass(frozen=True) class DIDInfo(Streamable): origin_coin: Optional[Coin] # Coin ID of this coin is our DID - backup_ids: List[bytes] + backup_ids: List[bytes32] num_of_backup_ids_needed: uint64 parent_info: List[Tuple[bytes32, Optional[LineageProof]]] # {coin.name(): LineageProof} current_inner: Optional[Program] # represents a Program as bytes @@ -21,3 +21,4 @@ class DIDInfo(Streamable): temp_puzhash: Optional[bytes32] temp_pubkey: Optional[bytes] sent_recovery_transaction: bool + metadata: str # JSON of the user defined metadata diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index c957157804c6..32f41050e167 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -1,9 +1,10 @@ import logging import time import json +import re from typing import Dict, Optional, List, Any, Set, Tuple -from blspy import AugSchemeMPL, G1Element +from blspy import AugSchemeMPL, G1Element, G2Element from secrets import token_bytes from chia.protocols import wallet_protocol from chia.protocols.wallet_protocol import CoinState @@ -15,7 +16,7 @@ from chia.types.spend_bundle import SpendBundle from chia.util.ints import uint64, uint32, uint8, uint128 from chia.wallet.util.transaction_type import TransactionType - +from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict from chia.wallet.did_wallet.did_info import DIDInfo from chia.wallet.lineage_proof import LineageProof from chia.wallet.transaction_record import TransactionRecord @@ -27,6 +28,11 @@ 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.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( + puzzle_for_pk, + DEFAULT_HIDDEN_PUZZLE_HASH, + calculate_synthetic_secret_key, +) class DIDWallet: @@ -46,12 +52,28 @@ async def create_new_did_wallet( amount: uint64, backups_ids: List = [], num_of_backup_ids_needed: uint64 = None, - name: str = None, + metadata: Dict[str, str] = {}, + name: Optional[str] = None, + fee: uint64 = uint64(0), ): """ + Create a brand new DID wallet This must be called under the wallet state manager lock + :param wallet_state_manager: Wallet state manager + :param wallet: Standard wallet + :param amount: Amount of the DID coin + :param backups_ids: A list of DIDs used for recovery this DID + :param num_of_backup_ids_needed: Needs how many recovery DIDs at least + :param metadata: Metadata saved in the DID + :param name: Wallet name + :param fee: transaction fee + :return: DID wallet """ + self = DIDWallet() + self.wallet_state_manager = wallet_state_manager + if name is None: + name = self.generate_wallet_name() self.base_puzzle_program = None self.base_inner_puzzle_hash = None self.standard_wallet = wallet @@ -62,15 +84,17 @@ async def create_new_did_wallet( raise ValueError("Not enough balance") if amount & 1 == 0: raise ValueError("DID amount must be odd number") - self.wallet_state_manager = wallet_state_manager + if num_of_backup_ids_needed is None: num_of_backup_ids_needed = uint64(len(backups_ids)) if num_of_backup_ids_needed > len(backups_ids): raise ValueError("Cannot require more IDs than are known.") - self.did_info = DIDInfo(None, backups_ids, num_of_backup_ids_needed, [], None, None, None, None, False) + self.did_info = DIDInfo( + None, backups_ids, num_of_backup_ids_needed, [], None, None, None, None, False, json.dumps(metadata) + ) info_as_string = json.dumps(self.did_info.to_json_dict()) self.wallet_info = await wallet_state_manager.user_store.create_wallet( - "DID Wallet", WalletType.DISTRIBUTED_ID.value, info_as_string + name, WalletType.DISTRIBUTED_ID.value, info_as_string ) if self.wallet_info is None: raise ValueError("Internal Error") @@ -101,7 +125,7 @@ async def create_new_did_wallet( created_at_time=uint64(int(time.time())), to_puzzle_hash=did_puzzle_hash, amount=uint64(amount), - fee_amount=uint64(0), + fee_amount=fee, confirmed=False, sent=uint32(10), spend_bundle=None, @@ -119,7 +143,7 @@ async def create_new_did_wallet( created_at_time=uint64(int(time.time())), to_puzzle_hash=did_puzzle_hash, amount=uint64(amount), - fee_amount=uint64(0), + fee_amount=fee, confirmed=False, sent=uint32(0), spend_bundle=spend_bundle, @@ -140,24 +164,107 @@ async def create_new_did_wallet( async def create_new_did_wallet_from_recovery( wallet_state_manager: Any, wallet: Wallet, - filename: str, - name: str = None, + backup_data: str, + name: Optional[str] = None, ): + """ + Create a DID wallet from a backup file + :param wallet_state_manager: Wallet state manager + :param wallet: Standard wallet + :param backup_data: A serialized backup data + :param name: Wallet name + :return: DID wallet + """ self = DIDWallet() + self.wallet_state_manager = wallet_state_manager + if name is None: + name = self.generate_wallet_name() self.base_puzzle_program = None self.base_inner_puzzle_hash = None self.standard_wallet = wallet self.log = logging.getLogger(name if name else __name__) - self.wallet_state_manager = wallet_state_manager - self.did_info = DIDInfo(None, [], uint64(0), [], None, None, None, None, False) + self.log.info("Creating DID wallet from recovery file ...") + # load backup will also set our DIDInfo + self.did_info = DIDWallet.deserialize_backup_data(backup_data) + self.check_existed_did() info_as_string = json.dumps(self.did_info.to_json_dict()) self.wallet_info = await wallet_state_manager.user_store.create_wallet( - "DID Wallet", WalletType.DISTRIBUTED_ID.value, info_as_string + name, WalletType.DISTRIBUTED_ID.value, info_as_string ) await self.wallet_state_manager.add_new_wallet(self, self.wallet_info.id) - # load backup will also set our DIDInfo - await self.load_backup(filename) + await self.save_info(self.did_info, False) + await self.wallet_state_manager.update_wallet_puzzle_hashes(self.wallet_info.id) + await self.load_parent(self.did_info) + if self.wallet_info is None: + raise ValueError("Internal Error") + self.wallet_id = self.wallet_info.id + return self + + @staticmethod + async def create_new_did_wallet_from_coin_spend( + wallet_state_manager: Any, + wallet: Wallet, + launch_coin: Coin, + inner_puzzle: Program, + coin_spend: CoinSpend, + name: Optional[str] = None, + ): + """ + Create a DID wallet from a transfer + :param wallet_state_manager: Wallet state manager + :param wallet: Main wallet + :param launch_coin: The launch coin of the DID + :param inner_puzzle: DID inner puzzle + :param coin_spend: DID transfer spend + :param name: Wallet name + :return: DID wallet + """ + self = DIDWallet() + self.wallet_state_manager = wallet_state_manager + if name is None: + name = self.generate_wallet_name() + self.base_puzzle_program = None + self.base_inner_puzzle_hash = None + self.standard_wallet = wallet + self.log = logging.getLogger(name if name else __name__) + + self.log.info(f"Creating DID wallet from a coin spend {launch_coin} ...") + # Create did info from the coin spend + args = did_wallet_puzzles.uncurry_innerpuz(inner_puzzle) + if args is None: + raise ValueError("Cannot uncurry the DID puzzle.") + _, recovery_list_hash, num_verification, _, metadata = args + full_solution: Program = Program.from_bytes(bytes(coin_spend.solution)) + inner_solution: Program = full_solution.rest().rest().first() + recovery_list: List[bytes32] = [] + backup_required: int = num_verification.as_int() + if recovery_list_hash != Program.to([]).get_tree_hash(): + for did in inner_solution.rest().rest().rest().rest().rest().as_python(): + recovery_list.append(did[0]) + self.did_info = DIDInfo( + launch_coin, + recovery_list, + uint64(backup_required), + [], + inner_puzzle, + None, + None, + None, + False, + json.dumps(did_wallet_puzzles.program_to_metadata(metadata)), + ) + self.check_existed_did() + info_as_string = json.dumps(self.did_info.to_json_dict()) + + self.wallet_info = await wallet_state_manager.user_store.create_wallet( + name, WalletType.DISTRIBUTED_ID.value, info_as_string, in_transaction=True + ) + + await self.wallet_state_manager.add_new_wallet(self, self.wallet_info.id) + await self.wallet_state_manager.update_wallet_puzzle_hashes(self.wallet_info.id) + await self.load_parent(self.did_info) + self.log.info(f"New DID wallet created {info_as_string}.") if self.wallet_info is None: raise ValueError("Internal Error") self.wallet_id = self.wallet_info.id @@ -170,6 +277,14 @@ async def create( wallet_info: WalletInfo, name: str = None, ): + """ + Create a DID wallet based on the local database + :param wallet_state_manager: Wallet state manager + :param wallet: Standard wallet + :param wallet_info: Serialized WalletInfo + :param name: Wallet name + :return: + """ self = DIDWallet() self.log = logging.getLogger(name if name else __name__) self.wallet_state_manager = wallet_state_manager @@ -223,8 +338,7 @@ async def get_pending_change_balance(self) -> uint64: return uint64(addition_amount) async def get_unconfirmed_balance(self, record_list=None) -> uint128: - confirmed = await self.get_confirmed_balance(record_list) - return await self.wallet_state_manager._get_unconfirmed_balance(self.id(), confirmed) + 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.""" @@ -288,6 +402,7 @@ async def coin_added(self, coin: Coin, _: uint32): None, None, False, + self.did_info.metadata, ) await self.save_info(new_info, True) @@ -320,131 +435,105 @@ async def coin_added(self, coin: Coin, _: uint32): ) await self.add_parent(coin.parent_coin_info, parent_info, False) - def create_backup(self, filename: str): + def create_backup(self) -> str: + """ + Create a serialized backup data for DIDInfo + :return: Serialized backup data + """ assert self.did_info.current_inner is not None assert self.did_info.origin_coin is not None - try: - f = open(filename, "w") - output_str = f"{self.did_info.origin_coin.parent_coin_info}:" - output_str += f"{self.did_info.origin_coin.puzzle_hash}:" - output_str += f"{self.did_info.origin_coin.amount}:" + output_str = f"{self.did_info.origin_coin.parent_coin_info}:" + output_str += f"{self.did_info.origin_coin.puzzle_hash}:" + output_str += f"{self.did_info.origin_coin.amount}:" + if len(self.did_info.backup_ids) > 0: for did in self.did_info.backup_ids: output_str = output_str + did.hex() + "," output_str = output_str[:-1] - output_str = ( - output_str + f":{bytes(self.did_info.current_inner).hex()}:{self.did_info.num_of_backup_ids_needed}" - ) - f.write(output_str) - f.close() - except Exception as e: - raise e - return None + output_str += f":{bytes(self.did_info.current_inner).hex()}:{self.did_info.num_of_backup_ids_needed}" + output_str += f":{self.did_info.metadata}" + return output_str - async def load_backup(self, filename: str): - try: - f = open(filename, "r") - details = f.readline().split(":") - f.close() - origin = Coin( - bytes32.fromhex(details[0]), - bytes32.fromhex(details[1]), - uint64(int(details[2])), - ) - backup_ids = [] - for d in details[3].split(","): - backup_ids.append(bytes.fromhex(d)) - num_of_backup_ids_needed = uint64(int(details[5])) - if num_of_backup_ids_needed > len(backup_ids): - raise Exception - innerpuz: Program = Program.from_bytes(bytes.fromhex(details[4])) - did_info: DIDInfo = DIDInfo( - origin, - backup_ids, - num_of_backup_ids_needed, - self.did_info.parent_info, - innerpuz, - None, - None, - None, - False, - ) - await self.save_info(did_info, False) - await self.wallet_state_manager.update_wallet_puzzle_hashes(self.wallet_info.id) - - # full_puz = did_wallet_puzzles.create_fullpuz(innerpuz, origin.name()) - # All additions in this block here: - new_puzhash = await self.get_new_inner_hash() - new_pubkey = bytes( - (await self.wallet_state_manager.get_unused_derivation_record(self.wallet_info.id)).pubkey - ) - parent_info = None + async def load_parent(self, did_info: DIDInfo): + """ + Load the parent info when importing a DID + :param did_info: DID info + :return: + """ + # full_puz = did_wallet_puzzles.create_fullpuz(innerpuz, origin.name()) + # All additions in this block here: + new_puzhash = await self.get_new_did_inner_hash() + new_pubkey = bytes((await self.wallet_state_manager.get_unused_derivation_record(self.wallet_info.id)).pubkey) + parent_info = None + assert did_info.origin_coin is not None + assert did_info.current_inner is not None + node = self.wallet_state_manager.wallet_node.get_full_node_peer() + children = await self.wallet_state_manager.wallet_node.fetch_children(node, did_info.origin_coin.name()) + while True: + if len(children) == 0: + break - node = self.wallet_state_manager.wallet_node.get_full_node_peer() - children = await self.wallet_state_manager.wallet_node.fetch_children(node, origin.name()) - while True: - if len(children) == 0: - break + children_state: CoinState = children[0] + coin = children_state.coin + name = coin.name() + children = await self.wallet_state_manager.wallet_node.fetch_children(node, name) + future_parent = LineageProof( + coin.parent_coin_info, + did_info.current_inner.get_tree_hash(), + coin.amount, + ) - children_state: CoinState = children[0] - coin = children_state.coin - name = coin.name() - children = await self.wallet_state_manager.wallet_node.fetch_children(node, name) - future_parent = LineageProof( - coin.parent_coin_info, - innerpuz.get_tree_hash(), - coin.amount, + await self.add_parent(coin.name(), future_parent, False) + if children_state.spent_height != children_state.created_height: + did_info = DIDInfo( + did_info.origin_coin, + did_info.backup_ids, + did_info.num_of_backup_ids_needed, + self.did_info.parent_info, + did_info.current_inner, + coin, + new_puzhash, + new_pubkey, + False, + did_info.metadata, ) - await self.add_parent(coin.name(), future_parent, False) - if children_state.spent_height != children_state.created_height: - did_info = DIDInfo( - origin, - backup_ids, - num_of_backup_ids_needed, - self.did_info.parent_info, - innerpuz, - coin, - new_puzhash, - new_pubkey, - False, - ) - await self.save_info(did_info, False) - assert children_state.created_height - puzzle_solution_request = wallet_protocol.RequestPuzzleSolution( - coin.parent_coin_info, children_state.created_height - ) - parent_state: CoinState = ( - await self.wallet_state_manager.wallet_node.get_coin_state([coin.parent_coin_info]) - )[0] - response = await node.request_puzzle_solution(puzzle_solution_request) - req_puz_sol = response.response - assert req_puz_sol.puzzle is not None - parent_innerpuz = did_wallet_puzzles.get_innerpuzzle_from_puzzle(req_puz_sol.puzzle) - assert parent_innerpuz is not None - parent_info = LineageProof( - parent_state.coin.parent_coin_info, - parent_innerpuz.get_tree_hash(), - parent_state.coin.amount, - ) - await self.add_parent(coin.parent_coin_info, parent_info, False) - assert parent_info is not None - return None - except Exception as e: - raise e + await self.save_info(did_info, False) + assert children_state.created_height + puzzle_solution_request = wallet_protocol.RequestPuzzleSolution( + coin.parent_coin_info, children_state.created_height + ) + parent_state: CoinState = ( + await self.wallet_state_manager.wallet_node.get_coin_state([coin.parent_coin_info]) + )[0] + response = await node.request_puzzle_solution(puzzle_solution_request) + req_puz_sol = response.response + assert req_puz_sol.puzzle is not None + parent_innerpuz = did_wallet_puzzles.get_innerpuzzle_from_puzzle(req_puz_sol.puzzle) + assert parent_innerpuz is not None + parent_info = LineageProof( + parent_state.coin.parent_coin_info, + parent_innerpuz.get_tree_hash(), + parent_state.coin.amount, + ) + await self.add_parent(coin.parent_coin_info, parent_info, False) + assert parent_info is not None - def puzzle_for_pk(self, pubkey: bytes) -> Program: - innerpuz = did_wallet_puzzles.create_innerpuz( - pubkey, self.did_info.backup_ids, self.did_info.num_of_backup_ids_needed - ) + def puzzle_for_pk(self, pubkey: G1Element) -> Program: if self.did_info.origin_coin is not None: + innerpuz = did_wallet_puzzles.create_innerpuz( + puzzle_for_pk(pubkey), + self.did_info.backup_ids, + self.did_info.num_of_backup_ids_needed, + self.did_info.origin_coin.name(), + did_wallet_puzzles.metadata_to_program(json.loads(self.did_info.metadata)), + ) return did_wallet_puzzles.create_fullpuz(innerpuz, self.did_info.origin_coin.name()) else: - # TODO: address hint error and remove ignore - # error: Argument 2 to "create_fullpuz" has incompatible type "int"; expected "bytes32" [arg-type] - return did_wallet_puzzles.create_fullpuz(innerpuz, 0x00) # type: ignore[arg-type] + innerpuz = Program.to((8, 0)) + return did_wallet_puzzles.create_fullpuz(innerpuz, bytes32([0] * 32)) async def get_new_puzzle(self) -> Program: return self.puzzle_for_pk( - bytes((await self.wallet_state_manager.get_unused_derivation_record(self.wallet_info.id)).pubkey) + (await self.wallet_state_manager.get_unused_derivation_record(self.wallet_info.id)).pubkey ) def get_my_DID(self) -> str: @@ -453,15 +542,28 @@ def get_my_DID(self) -> str: assert core is not None return core.hex() + async def set_name(self, new_name: str): + import dataclasses + + new_info = dataclasses.replace(self.wallet_info, name=new_name) + self.wallet_info = new_info + await self.wallet_state_manager.user_store.update_wallet(self.wallet_info, False) + + async def get_name(self): + return self.wallet_info.name + async def create_update_spend(self): assert self.did_info.current_inner is not None assert self.did_info.origin_coin is not None coins = await self.select_coins(1) assert coins is not None coin = coins.pop() - new_puzhash = await self.get_new_inner_hash() - # innerpuz solution is (mode amount messages new_puz) - innersol: Program = Program.to([1, coin.amount, [], new_puzhash]) + new_puzhash = await self.get_new_did_inner_hash() + # innerpuz solution is (mode, p2_solution) + p2_solution = self.standard_wallet.make_solution( + primaries=[{"puzzlehash": new_puzhash, "amount": uint64(coin.amount), "memos": [new_puzhash]}] + ) + innersol: Program = Program.to([1, p2_solution]) # full solution is (corehash parent_info my_amount innerpuz_reveal solution) innerpuz: Program = self.did_info.current_inner @@ -482,21 +584,9 @@ async def create_update_spend(self): innersol, ] ) - list_of_solutions = [CoinSpend(coin, full_puzzle, fullsol)] - # sign for AGG_SIG_ME - message = ( - Program.to([new_puzhash, coin.amount, []]).get_tree_hash() - + coin.name() - + self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA - ) - pubkey = did_wallet_puzzles.get_pubkey_from_innerpuz(innerpuz) - index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey) - private = master_sk_to_wallet_sk_unhardened(self.wallet_state_manager.private_key, index) - signature = AugSchemeMPL.sign(private, message) - # assert signature.validate([signature.PkMessagePair(pubkey, message)]) - sigs = [signature] - aggsig = AugSchemeMPL.aggregate(sigs) - spend_bundle = SpendBundle(list_of_solutions, aggsig) + list_of_coinspends = [CoinSpend(coin, full_puzzle, fullsol)] + unsigned_spend_bundle = SpendBundle(list_of_coinspends, G2Element()) + spend_bundle = await self.sign(unsigned_spend_bundle) did_record = TransactionRecord( confirmed_at_height=uint32(0), @@ -513,28 +603,56 @@ async def create_update_spend(self): sent_to=[], trade_id=None, type=uint32(TransactionType.OUTGOING_TX.value), - name=token_bytes(), + name=bytes32(token_bytes()), memos=list(compute_memos(spend_bundle).items()), ) await self.standard_wallet.push_transaction(did_record) return spend_bundle - # The message spend can send messages and also change your innerpuz - async def create_message_spend(self, messages: List[Tuple[int, bytes]], new_innerpuzhash: Optional[bytes32] = None): + async def transfer_did(self, new_puzhash: bytes32, fee: uint64, with_recovery: bool) -> TransactionRecord: + """ + Transfer the current DID to another owner + :param new_puzhash: New owner's p2_puzzle + :param fee: Transaction fee + :param with_recovery: A boolean indicates if the recovery info will be sent through the blockchain + :return: Spend bundle + """ assert self.did_info.current_inner is not None assert self.did_info.origin_coin is not None coins = await self.select_coins(1) assert coins is not None coin = coins.pop() - innerpuz: Program = self.did_info.current_inner - if new_innerpuzhash is None: - new_innerpuzhash = innerpuz.get_tree_hash() - # innerpuz solution is (mode amount messages new_puz) - innersol: Program = Program.to([1, coin.amount, messages, new_innerpuzhash]) + backup_ids = [] + backup_required = uint64(0) + if with_recovery: + backup_ids = self.did_info.backup_ids + backup_required = self.did_info.num_of_backup_ids_needed + new_did_puzhash = did_wallet_puzzles.get_inner_puzhash_by_p2( + new_puzhash, + backup_ids, + backup_required, + self.did_info.origin_coin.name(), + did_wallet_puzzles.metadata_to_program(json.loads(self.did_info.metadata)), + ) + p2_solution = self.standard_wallet.make_solution( + primaries=[ + { + "puzzlehash": new_did_puzhash, + "amount": uint64(coin.amount), + "memos": [new_puzhash], + } + ] + ) + # Need to include backup list reveal here, even we are don't recover + # innerpuz solution is + # (mode, p2_solution) + innersol: Program = Program.to([2, p2_solution]) + if with_recovery: + innersol = Program.to([2, p2_solution, [], [], [], self.did_info.backup_ids]) # full solution is (corehash parent_info my_amount innerpuz_reveal solution) full_puzzle: Program = did_wallet_puzzles.create_fullpuz( - innerpuz, + self.did_info.current_inner, self.did_info.origin_coin.name(), ) parent_info = self.get_parent_for_coin(coin) @@ -550,29 +668,16 @@ async def create_message_spend(self, messages: List[Tuple[int, bytes]], new_inne innersol, ] ) - list_of_solutions = [CoinSpend(coin, full_puzzle, fullsol)] - # sign for AGG_SIG_ME - # new_inner_puzhash amount message - message = ( - Program.to([new_innerpuzhash, coin.amount, messages]).get_tree_hash() - + coin.name() - + self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA - ) - pubkey = did_wallet_puzzles.get_pubkey_from_innerpuz(innerpuz) - index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey) - private = master_sk_to_wallet_sk_unhardened(self.wallet_state_manager.private_key, index) - signature = AugSchemeMPL.sign(private, message) - # assert signature.validate([signature.PkMessagePair(pubkey, message)]) - sigs = [signature] - aggsig = AugSchemeMPL.aggregate(sigs) - spend_bundle = SpendBundle(list_of_solutions, aggsig) + list_of_coinspends = [CoinSpend(coin, full_puzzle, fullsol)] + unsigned_spend_bundle = SpendBundle(list_of_coinspends, G2Element()) + spend_bundle = await self.sign(unsigned_spend_bundle) did_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), - to_puzzle_hash=new_innerpuzhash, + to_puzzle_hash=new_puzhash, amount=uint64(coin.amount), - fee_amount=uint64(0), + fee_amount=fee, confirmed=False, sent=uint32(0), spend_bundle=spend_bundle, @@ -586,7 +691,54 @@ async def create_message_spend(self, messages: List[Tuple[int, bytes]], new_inne memos=list(compute_memos(spend_bundle).items()), ) await self.standard_wallet.push_transaction(did_record) - return spend_bundle + return did_record + + # The message spend can tests\wallet\rpc\test_wallet_rpc.py send messages and also change your innerpuz + async def create_message_spend( + self, + coin_announcements: Optional[Set[bytes]] = None, + puzzle_announcements: Optional[Set[bytes]] = None, + new_innerpuzhash: Optional[bytes32] = None, + ): + assert self.did_info.current_inner is not None + assert self.did_info.origin_coin is not None + coins = await self.select_coins(1) + assert coins is not None + coin = coins.pop() + innerpuz: Program = self.did_info.current_inner + # Quote message puzzle & solution + if new_innerpuzhash is None: + new_innerpuzhash = innerpuz.get_tree_hash() + + p2_solution = self.standard_wallet.make_solution( + primaries=[{"puzzlehash": new_innerpuzhash, "amount": uint64(coin.amount), "memos": [new_innerpuzhash]}], + puzzle_announcements=puzzle_announcements, + coin_announcements=coin_announcements, + ) + # innerpuz solution is (mode p2_solution) + innersol: Program = Program.to([1, p2_solution]) + + # full solution is (corehash parent_info my_amount innerpuz_reveal solution) + full_puzzle: Program = did_wallet_puzzles.create_fullpuz( + innerpuz, + self.did_info.origin_coin.name(), + ) + parent_info = self.get_parent_for_coin(coin) + assert parent_info is not None + fullsol = Program.to( + [ + [ + parent_info.parent_name, + parent_info.inner_puzzle_hash, + parent_info.amount, + ], + coin.amount, + innersol, + ] + ) + list_of_coinspends = [CoinSpend(coin, full_puzzle, fullsol)] + unsigned_spend_bundle = SpendBundle(list_of_coinspends, G2Element()) + return await self.sign(unsigned_spend_bundle) # This is used to cash out, or update the id_list async def create_exit_spend(self, puzhash: bytes32): @@ -595,9 +747,10 @@ async def create_exit_spend(self, puzhash: bytes32): coins = await self.select_coins(1) assert coins is not None coin = coins.pop() - amount = coin.amount - 1 - # innerpuz solution is (mode amount new_puzhash) - innersol: Program = Program.to([0, amount, puzhash]) + message_puz = Program.to((1, [[51, puzhash, coin.amount - 1, [puzhash]], [51, 0x00, -113]])) + + # innerpuz solution is (mode p2_solution) + innersol: Program = Program.to([1, [[], message_puz, []]]) # full solution is (corehash parent_info my_amount innerpuz_reveal solution) innerpuz: Program = self.did_info.current_inner @@ -618,21 +771,9 @@ async def create_exit_spend(self, puzhash: bytes32): innersol, ] ) - list_of_solutions = [CoinSpend(coin, full_puzzle, fullsol)] - # sign for AGG_SIG_ME - message = ( - Program.to([amount, puzhash]).get_tree_hash() - + coin.name() - + self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA - ) - pubkey = did_wallet_puzzles.get_pubkey_from_innerpuz(innerpuz) - index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey) - private = master_sk_to_wallet_sk_unhardened(self.wallet_state_manager.private_key, index) - signature = AugSchemeMPL.sign(private, message) - # assert signature.validate([signature.PkMessagePair(pubkey, message)]) - sigs = [signature] - aggsig = AugSchemeMPL.aggregate(sigs) - spend_bundle = SpendBundle(list_of_solutions, aggsig) + list_of_coinspends = [CoinSpend(coin, full_puzzle, fullsol)] + unsigned_spend_bundle = SpendBundle(list_of_coinspends, G2Element()) + spend_bundle = await self.sign(unsigned_spend_bundle) did_record = TransactionRecord( confirmed_at_height=uint32(0), @@ -655,11 +796,18 @@ async def create_exit_spend(self, puzhash: bytes32): await self.standard_wallet.push_transaction(did_record) return spend_bundle - # Pushes the a SpendBundle to create a message coin on the blockchain + # Pushes a SpendBundle to create a message coin on the blockchain # Returns a SpendBundle for the recoverer to spend the message coin async def create_attestment( - self, recovering_coin_name: bytes32, newpuz: bytes32, pubkey: G1Element, filename=None - ) -> SpendBundle: + self, recovering_coin_name: bytes32, newpuz: bytes32, pubkey: G1Element + ) -> Tuple[SpendBundle, str]: + """ + Create an attestment + :param recovering_coin_name: Coin ID of the DID + :param newpuz: New puzzle hash + :param pubkey: New wallet pubkey + :return: (SpendBundle, attest string) + """ assert self.did_info.current_inner is not None assert self.did_info.origin_coin is not None coins = await self.select_coins(1) @@ -668,9 +816,14 @@ async def create_attestment( message = did_wallet_puzzles.create_recovery_message_puzzle(recovering_coin_name, newpuz, pubkey) innermessage = message.get_tree_hash() innerpuz: Program = self.did_info.current_inner - # innerpuz solution is (mode, amount, message, new_inner_puzhash) - messages = [(0, innermessage)] - innersol = Program.to([1, coin.amount, messages, innerpuz.get_tree_hash()]) + # innerpuz solution is (mode, p2_solution) + p2_solution = self.standard_wallet.make_solution( + primaries=[ + {"puzzlehash": innerpuz.get_tree_hash(), "amount": uint64(coin.amount), "memos": []}, + {"puzzlehash": innermessage, "amount": uint64(0), "memos": []}, + ], + ) + innersol = Program.to([1, p2_solution]) # full solution is (corehash parent_info my_amount innerpuz_reveal solution) full_puzzle: Program = did_wallet_puzzles.create_fullpuz( @@ -691,18 +844,11 @@ async def create_attestment( innersol, ] ) - list_of_solutions = [CoinSpend(coin, full_puzzle, fullsol)] + list_of_coinspends = [CoinSpend(coin, full_puzzle, fullsol)] message_spend = did_wallet_puzzles.create_spend_for_message(coin.name(), recovering_coin_name, newpuz, pubkey) message_spend_bundle = SpendBundle([message_spend], AugSchemeMPL.aggregate([])) - # sign for AGG_SIG_ME - to_sign = Program.to([innerpuz.get_tree_hash(), coin.amount, messages]).get_tree_hash() - message = to_sign + coin.name() + self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA - pubkey = did_wallet_puzzles.get_pubkey_from_innerpuz(innerpuz) - index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey) - private = master_sk_to_wallet_sk_unhardened(self.wallet_state_manager.private_key, index) - signature = AugSchemeMPL.sign(private, message) - # assert signature.validate([signature.PkMessagePair(pubkey, message)]) - spend_bundle = SpendBundle(list_of_solutions, signature) + unsigned_spend_bundle = SpendBundle(list_of_coinspends, G2Element()) + spend_bundle = await self.sign(unsigned_spend_bundle) did_record = TransactionRecord( confirmed_at_height=uint32(0), created_at_time=uint64(int(time.time())), @@ -721,51 +867,38 @@ async def create_attestment( name=bytes32(token_bytes()), memos=list(compute_memos(spend_bundle).items()), ) + attest_str: str = f"{self.get_my_DID()}:{bytes(message_spend_bundle).hex()}:{coin.parent_coin_info.hex()}:" + attest_str += f"{self.did_info.current_inner.get_tree_hash().hex()}:{coin.amount}" await self.standard_wallet.push_transaction(did_record) - if filename is not None: - f = open(filename, "w") - f.write(self.get_my_DID()) - f.write(":") - f.write(bytes(message_spend_bundle).hex()) - f.write(":") - parent = coin.parent_coin_info.hex() - innerpuzhash = self.did_info.current_inner.get_tree_hash().hex() - amount = coin.amount - f.write(parent) - f.write(":") - f.write(innerpuzhash) - f.write(":") - f.write(str(amount)) - f.close() - return message_spend_bundle - - # this is just for testing purposes, API should use create_attestment_now - async def get_info_for_recovery(self): + return message_spend_bundle, attest_str + + 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) - coin = coins.pop() - parent = coin.parent_coin_info - innerpuzhash = self.did_info.current_inner.get_tree_hash() - amount = coin.amount - return [parent, innerpuzhash, amount] + if coins is not None: + coin = coins.pop() + parent = coin.parent_coin_info + innerpuzhash = self.did_info.current_inner.get_tree_hash() + amount = coin.amount + return (parent, innerpuzhash, amount) + return None - async def load_attest_files_for_recovery_spend(self, filenames): + async def load_attest_files_for_recovery_spend(self, attest_data: List[str]) -> Tuple[List, SpendBundle]: spend_bundle_list = [] info_dict = {} try: - for i in filenames: - f = open(i) - info = f.read().split(":") + for attest in attest_data: + info = attest.split(":") info_dict[info[0]] = [ bytes.fromhex(info[2]), bytes.fromhex(info[3]), uint64(info[4]), ] - new_sb = SpendBundle.from_bytes(bytes.fromhex(info[1])) spend_bundle_list.append(new_sb) - f.close() # info_dict {0xidentity: "(0xparent_info 0xinnerpuz amount)"} - my_recovery_list: List[bytes] = self.did_info.backup_ids + my_recovery_list: List[bytes32] = self.did_info.backup_ids # convert info dict into recovery list - same order as wallet info_list = [] @@ -795,16 +928,16 @@ async def recovery_spend( ) -> SpendBundle: assert self.did_info.origin_coin is not None - # innersol is mode new_amount message new_inner_puzhash parent_innerpuzhash_amounts_for_recovery_ids pubkey recovery_list_reveal) # noqa + # innersol is mode new_amount_or_p2_solution new_inner_puzhash parent_innerpuzhash_amounts_for_recovery_ids pubkey recovery_list_reveal my_id) # noqa innersol: Program = Program.to( [ - 2, + 0, coin.amount, puzhash, - puzhash, parent_innerpuzhash_amounts_for_recovery_ids, bytes(pubkey), self.did_info.backup_ids, + coin.name(), ] ) # full solution is (parent_info my_amount solution) @@ -827,7 +960,7 @@ async def recovery_spend( innersol, ] ) - list_of_solutions = [CoinSpend(coin, full_puzzle, fullsol)] + list_of_coinspends = [CoinSpend(coin, full_puzzle, fullsol)] index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey) if index is None: @@ -840,9 +973,9 @@ async def recovery_spend( aggsig = AugSchemeMPL.aggregate(sigs) # assert AugSchemeMPL.verify(pubkey, message, aggsig) if spend_bundle is None: - spend_bundle = SpendBundle(list_of_solutions, aggsig) + spend_bundle = SpendBundle(list_of_coinspends, aggsig) else: - spend_bundle = spend_bundle.aggregate([spend_bundle, SpendBundle(list_of_solutions, aggsig)]) + spend_bundle = spend_bundle.aggregate([spend_bundle, SpendBundle(list_of_coinspends, aggsig)]) did_record = TransactionRecord( confirmed_at_height=uint32(0), @@ -873,41 +1006,72 @@ async def recovery_spend( self.did_info.temp_puzhash, self.did_info.temp_pubkey, True, + self.did_info.metadata, ) await self.save_info(new_did_info, True) return spend_bundle - async def get_new_innerpuz(self) -> Program: - devrec = await self.wallet_state_manager.get_unused_derivation_record(self.standard_wallet.id()) - pubkey = bytes(devrec.pubkey) - innerpuz = did_wallet_puzzles.create_innerpuz( - pubkey, - self.did_info.backup_ids, - uint64(self.did_info.num_of_backup_ids_needed), - ) + async def get_new_p2_inner_hash(self) -> bytes32: + puzzle = await self.get_new_p2_inner_puzzle() + return puzzle.get_tree_hash() + + async def get_new_p2_inner_puzzle(self) -> Program: + return await self.standard_wallet.get_new_puzzle() + + async def get_new_did_innerpuz(self, origin_id=None) -> Program: + if self.did_info.origin_coin is not None: + innerpuz = did_wallet_puzzles.create_innerpuz( + await self.get_new_p2_inner_puzzle(), + self.did_info.backup_ids, + uint64(self.did_info.num_of_backup_ids_needed), + self.did_info.origin_coin.name(), + did_wallet_puzzles.metadata_to_program(json.loads(self.did_info.metadata)), + ) + elif origin_id is not None: + innerpuz = did_wallet_puzzles.create_innerpuz( + await self.get_new_p2_inner_puzzle(), + self.did_info.backup_ids, + uint64(self.did_info.num_of_backup_ids_needed), + origin_id, + did_wallet_puzzles.metadata_to_program(json.loads(self.did_info.metadata)), + ) + else: + raise ValueError("must have origin coin") return innerpuz - async def get_new_inner_hash(self) -> bytes32: - innerpuz = await self.get_new_innerpuz() + async def get_new_did_inner_hash(self) -> bytes32: + innerpuz = await self.get_new_did_innerpuz() return innerpuz.get_tree_hash() - async def get_innerhash_for_pubkey(self, pubkey: bytes): - innerpuz = did_wallet_puzzles.create_innerpuz( - pubkey, + async def get_innerpuz_for_new_innerhash(self, pubkey: G1Element): + """ + Get the inner puzzle for a new owner + :param pubkey: Pubkey + :return: Inner puzzle + """ + # Note: the recovery list will be kept. + # In a selling case, the seller should clean the recovery list then transfer to the new owner. + assert self.did_info.origin_coin is not None + return did_wallet_puzzles.create_innerpuz( + puzzle_for_pk(pubkey), self.did_info.backup_ids, uint64(self.did_info.num_of_backup_ids_needed), + self.did_info.origin_coin.name(), + did_wallet_puzzles.metadata_to_program(json.loads(self.did_info.metadata)), ) - return innerpuz.get_tree_hash() async def inner_puzzle_for_did_puzzle(self, did_hash: bytes32) -> Program: record: DerivationRecord = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash( did_hash ) + assert self.did_info.origin_coin is not None inner_puzzle: Program = did_wallet_puzzles.create_innerpuz( - bytes(record.pubkey), + puzzle_for_pk(record.pubkey), self.did_info.backup_ids, self.did_info.num_of_backup_ids_needed, + self.did_info.origin_coin.name(), + did_wallet_puzzles.metadata_to_program(json.loads(self.did_info.metadata)), ) return inner_puzzle @@ -919,6 +1083,35 @@ def get_parent_for_coin(self, coin) -> Optional[LineageProof]: return parent_info + async def sign(self, spend_bundle: SpendBundle) -> SpendBundle: + sigs: List[G2Element] = [] + for spend in spend_bundle.coin_spends: + matched, puzzle_args = did_wallet_puzzles.match_did_puzzle(spend.puzzle_reveal.to_program()) + if matched: + p2_puzzle, _, _, _, _ = puzzle_args + puzzle_hash = p2_puzzle.get_tree_hash() + pubkey, private = await self.wallet_state_manager.get_keys(puzzle_hash) + synthetic_secret_key = calculate_synthetic_secret_key(private, DEFAULT_HIDDEN_PUZZLE_HASH) + error, conditions, cost = conditions_dict_for_solution( + spend.puzzle_reveal.to_program(), + spend.solution.to_program(), + self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, + ) + + if conditions is not None: + synthetic_pk = synthetic_secret_key.get_g1() + for pk, msg in pkm_pairs_for_conditions_dict( + conditions, spend.coin.name(), self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA + ): + try: + assert bytes(synthetic_pk) == pk + sigs.append(AugSchemeMPL.sign(synthetic_secret_key, msg)) + except AssertionError: + raise ValueError("This spend bundle cannot be signed by the DID wallet") + + agg_sig = AugSchemeMPL.aggregate(sigs) + return SpendBundle.aggregate([spend_bundle, SpendBundle([], agg_sig)]) + async def generate_new_decentralised_id(self, amount: uint64) -> Optional[SpendBundle]: """ This must be called under the wallet state manager lock @@ -932,7 +1125,7 @@ async def generate_new_decentralised_id(self, amount: uint64) -> Optional[SpendB genesis_launcher_puz = did_wallet_puzzles.SINGLETON_LAUNCHER launcher_coin = Coin(origin.name(), genesis_launcher_puz.get_tree_hash(), amount) - did_inner: Program = await self.get_new_innerpuz() + did_inner: Program = await self.get_new_did_innerpuz(launcher_coin.name()) did_inner_hash = did_inner.get_tree_hash() did_full_puz = did_wallet_puzzles.create_fullpuz(did_inner, launcher_coin.name()) did_puzzle_hash = did_full_puz.get_tree_hash() @@ -977,6 +1170,7 @@ async def generate_new_decentralised_id(self, amount: uint64) -> Optional[SpendB None, None, False, + self.did_info.metadata, ) await self.save_info(did_info, False) eve_spend = await self.generate_eve_spend(eve_coin, did_full_puz, did_inner) @@ -985,8 +1179,17 @@ async def generate_new_decentralised_id(self, amount: uint64) -> Optional[SpendB async def generate_eve_spend(self, coin: Coin, full_puzzle: Program, innerpuz: Program): assert self.did_info.origin_coin is not None - # innerpuz solution is (mode amount message new_puzhash) - innersol = Program.to([1, coin.amount, [], innerpuz.get_tree_hash()]) + # innerpuz solution is (mode p2_solution) + p2_solution = self.standard_wallet.make_solution( + primaries=[ + { + "puzzlehash": innerpuz.get_tree_hash(), + "amount": uint64(coin.amount), + "memos": [innerpuz.get_tree_hash()], + } + ] + ) + innersol = Program.to([1, p2_solution]) # full solution is (lineage_proof my_amount inner_solution) fullsol = Program.to( [ @@ -995,22 +1198,9 @@ async def generate_eve_spend(self, coin: Coin, full_puzzle: Program, innerpuz: P innersol, ] ) - list_of_solutions = [CoinSpend(coin, full_puzzle, fullsol)] - # sign for AGG_SIG_ME - message = ( - Program.to([innerpuz.get_tree_hash(), coin.amount, []]).get_tree_hash() - + coin.name() - + self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA - ) - pubkey = did_wallet_puzzles.get_pubkey_from_innerpuz(innerpuz) - record: Optional[DerivationRecord] = await self.wallet_state_manager.puzzle_store.record_for_pubkey(pubkey) - assert record is not None - private = master_sk_to_wallet_sk_unhardened(self.wallet_state_manager.private_key, record.index) - signature = AugSchemeMPL.sign(private, message) - sigs = [signature] - aggsig = AugSchemeMPL.aggregate(sigs) - spend_bundle = SpendBundle(list_of_solutions, aggsig) - return spend_bundle + list_of_coinspends = [CoinSpend(coin, full_puzzle, fullsol)] + unsigned_spend_bundle = SpendBundle(list_of_coinspends, G2Element()) + return await self.sign(unsigned_spend_bundle) async def get_frozen_amount(self) -> uint64: return await self.wallet_state_manager.get_frozen_balance(self.wallet_info.id) @@ -1040,10 +1230,11 @@ async def add_parent(self, name: bytes32, parent: Optional[LineageProof], in_tra self.did_info.temp_puzhash, self.did_info.temp_pubkey, self.did_info.sent_recovery_transaction, + self.did_info.metadata, ) await self.save_info(did_info, in_transaction) - async def update_recovery_list(self, recover_list: List[bytes], num_of_backup_ids_needed: uint64): + async def update_recovery_list(self, recover_list: List[bytes32], num_of_backup_ids_needed: uint64) -> bool: if num_of_backup_ids_needed > len(recover_list): return False did_info: DIDInfo = DIDInfo( @@ -1056,6 +1247,24 @@ async def update_recovery_list(self, recover_list: List[bytes], num_of_backup_id self.did_info.temp_puzhash, self.did_info.temp_pubkey, self.did_info.sent_recovery_transaction, + self.did_info.metadata, + ) + await self.save_info(did_info, False) + await self.wallet_state_manager.update_wallet_puzzle_hashes(self.wallet_info.id) + return True + + async def update_metadata(self, metadata: Dict[str, str]) -> bool: + did_info: DIDInfo = DIDInfo( + self.did_info.origin_coin, + self.did_info.backup_ids, + self.did_info.num_of_backup_ids_needed, + self.did_info.parent_info, + self.did_info.current_inner, + self.did_info.temp_coin, + self.did_info.temp_puzhash, + self.did_info.temp_pubkey, + self.did_info.sent_recovery_transaction, + json.dumps(metadata), ) await self.save_info(did_info, False) await self.wallet_state_manager.update_wallet_puzzle_hashes(self.wallet_info.id) @@ -1068,3 +1277,61 @@ async def save_info(self, did_info: DIDInfo, in_transaction: bool): wallet_info = WalletInfo(current_info.id, current_info.name, current_info.type, data_str) self.wallet_info = wallet_info await self.wallet_state_manager.user_store.update_wallet(wallet_info, in_transaction) + + def generate_wallet_name(self) -> str: + """ + Generate a new DID wallet name + :return: wallet name + """ + max_num = 0 + for wallet in self.wallet_state_manager.wallets.values(): + if wallet.type() == WalletType.DISTRIBUTED_ID: + matched = re.search(r"^Profile (\d+)$", wallet.wallet_info.name) + if matched and int(matched.group(1)) > max_num: + max_num = int(matched.group(1)) + return f"Profile {max_num + 1}" + + def check_existed_did(self): + """ + Check if the current DID is existed + :return: None + """ + for wallet in self.wallet_state_manager.wallets.values(): + if ( + wallet.type() == WalletType.DISTRIBUTED_ID + and self.did_info.origin_coin.name() == wallet.did_info.origin_coin.name() + ): + self.log.warning(f"DID {self.did_info.origin_coin} already existed, ignore the wallet creation.") + raise ValueError("Wallet already exists") + + @staticmethod + def deserialize_backup_data(backup_data: str) -> DIDInfo: + """ + Get a DIDInfo from a serialized string + :param backup_data: serialized + :return: DIDInfo + """ + details = backup_data.split(":") + origin = Coin(bytes32(bytes.fromhex(details[0])), bytes32(bytes.fromhex(details[1])), uint64(int(details[2]))) + backup_ids = [] + if len(details[3]) > 0: + for d in details[3].split(","): + backup_ids.append(bytes32.from_hexstr(d)) + num_of_backup_ids_needed = uint64(int(details[5])) + if num_of_backup_ids_needed > len(backup_ids): + raise Exception + innerpuz: Program = Program.from_bytes(bytes.fromhex(details[4])) + metadata: str = details[6] + did_info: DIDInfo = DIDInfo( + origin, + backup_ids, + num_of_backup_ids_needed, + [], + innerpuz, + None, + None, + None, + False, + metadata, + ) + return did_info diff --git a/chia/wallet/did_wallet/did_wallet_puzzles.py b/chia/wallet/did_wallet/did_wallet_puzzles.py index f5c0fa04bf7c..d0f3d6ff48c1 100644 --- a/chia/wallet/did_wallet/did_wallet_puzzles.py +++ b/chia/wallet/did_wallet/did_wallet_puzzles.py @@ -1,7 +1,7 @@ -from clvm_tools import binutils +from clvm_tools.binutils import assemble from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.blockchain_format.program import Program -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Iterator, Dict from blspy import G1Element from chia.types.blockchain_format.coin import Coin from chia.types.coin_spend import CoinSpend @@ -10,50 +10,95 @@ from chia.types.condition_opcodes import ConditionOpcode -SINGLETON_TOP_LAYER_MOD = load_clvm("singleton_top_layer.clvm") +SINGLETON_TOP_LAYER_MOD = load_clvm("singleton_top_layer_v1_1.clvm") LAUNCHER_PUZZLE = load_clvm("singleton_launcher.clvm") DID_INNERPUZ_MOD = load_clvm("did_innerpuz.clvm") SINGLETON_LAUNCHER = load_clvm("singleton_launcher.clvm") +SINGLETON_MOD_HASH = SINGLETON_TOP_LAYER_MOD.get_tree_hash() +LAUNCHER_PUZZLE_HASH = SINGLETON_LAUNCHER.get_tree_hash() +DID_INNERPUZ_MOD_HASH = DID_INNERPUZ_MOD.get_tree_hash() + + +def create_innerpuz( + p2_puzzle: Program, + recovery_list: List[bytes32], + num_of_backup_ids_needed: uint64, + launcher_id: bytes32, + metadata: Program = Program.to([]), +) -> Program: + """ + Create DID inner puzzle + :param p2_puzzle: Standard P2 puzzle + :param recovery_list: A list of DIDs used for the recovery + :param num_of_backup_ids_needed: Need how many DIDs for the recovery + :param launcher_id: ID of the launch coin + :param metadata: DID customized metadata + :return: DID inner puzzle + """ + backup_ids_hash = Program(Program.to(recovery_list)).get_tree_hash() + singleton_struct = Program.to((SINGLETON_MOD_HASH, (launcher_id, LAUNCHER_PUZZLE_HASH))) + return DID_INNERPUZ_MOD.curry(p2_puzzle, backup_ids_hash, num_of_backup_ids_needed, singleton_struct, metadata) + + +def get_inner_puzhash_by_p2( + p2_puzhash: bytes32, + recovery_list: List[bytes32], + num_of_backup_ids_needed: uint64, + launcher_id: bytes32, + metadata: Program = Program.to([]), +) -> bytes32: + """ + Calculate DID inner puzzle hash based on a P2 puzzle hash + :param p2_puzhash: P2 puzzle hash + :param recovery_list: A list of DIDs used for the recovery + :param num_of_backup_ids_needed: Need how many DIDs for the recovery + :param launcher_id: ID of the launch coin + :param metadata: DID customized metadata + :return: DID inner puzzle hash + """ + backup_ids_hash = Program(Program.to(recovery_list)).get_tree_hash() + singleton_struct = Program.to((SINGLETON_MOD_HASH, (launcher_id, LAUNCHER_PUZZLE_HASH))) + return DID_INNERPUZ_MOD.curry( + p2_puzhash, backup_ids_hash, num_of_backup_ids_needed, singleton_struct, metadata + ).get_tree_hash(p2_puzhash) -def create_innerpuz(pubkey: bytes, identities: List[bytes], num_of_backup_ids_needed: uint64) -> Program: - backup_ids_hash = Program(Program.to(identities)).get_tree_hash() - # MOD_HASH MY_PUBKEY RECOVERY_DID_LIST_HASH NUM_VERIFICATIONS_REQUIRED - return DID_INNERPUZ_MOD.curry(pubkey, backup_ids_hash, num_of_backup_ids_needed) - - -def create_fullpuz(innerpuz: Program, genesis_id: bytes32) -> Program: +def create_fullpuz(innerpuz: Program, launcher_id: bytes32) -> Program: + """ + Create a full puzzle of DID + :param innerpuz: DID inner puzzle + :param launcher_id: + :return: DID full puzzle + """ mod_hash = SINGLETON_TOP_LAYER_MOD.get_tree_hash() # singleton_struct = (MOD_HASH . (LAUNCHER_ID . LAUNCHER_PUZZLE_HASH)) - singleton_struct = Program.to((mod_hash, (genesis_id, LAUNCHER_PUZZLE.get_tree_hash()))) + singleton_struct = Program.to((mod_hash, (launcher_id, LAUNCHER_PUZZLE.get_tree_hash()))) return SINGLETON_TOP_LAYER_MOD.curry(singleton_struct, innerpuz) -def get_pubkey_from_innerpuz(innerpuz: Program) -> G1Element: - ret = uncurry_innerpuz(innerpuz) - if ret is not None: - pubkey_program = ret[0] - else: - raise ValueError("Unable to extract pubkey") - pubkey = G1Element.from_bytes(pubkey_program.as_atom()) - return pubkey - - -def is_did_innerpuz(inner_f: Program): +def is_did_innerpuz(inner_f: Program) -> bool: """ - You may want to generalize this if different `CAT_MOD` templates are supported. + Check if a puzzle is a DID inner mode + :param inner_f: puzzle + :return: Boolean """ return inner_f == DID_INNERPUZ_MOD -def is_did_core(inner_f: Program): +def is_did_core(inner_f: Program) -> bool: + """ + Check if a puzzle is a singleton mod + :param inner_f: puzzle + :return: Boolean + """ return inner_f == SINGLETON_TOP_LAYER_MOD -def uncurry_innerpuz(puzzle: Program) -> Optional[Tuple[Program, Program]]: +def uncurry_innerpuz(puzzle: Program) -> Optional[Tuple[Program, Program, Program, Program, Program]]: """ - Take a puzzle and return `None` if it's not a `CAT_MOD` cc, or - a triple of `mod_hash, genesis_coin_checker, inner_puzzle` if it is. + Uncurry a DID inner puzzle + :param puzzle: DID puzzle + :return: Curried parameters """ r = puzzle.uncurry() if r is None: @@ -62,11 +107,16 @@ def uncurry_innerpuz(puzzle: Program) -> Optional[Tuple[Program, Program]]: if not is_did_innerpuz(inner_f): return None - pubkey, id_list, num_of_backup_ids_needed = list(args.as_iter()) - return pubkey, id_list + p2_puzzle, id_list, num_of_backup_ids_needed, singleton_struct, metadata = list(args.as_iter()) + return p2_puzzle, id_list, num_of_backup_ids_needed, singleton_struct, metadata def get_innerpuzzle_from_puzzle(puzzle: Program) -> Optional[Program]: + """ + Extract the inner puzzle of a singleton + :param puzzle: Singleton puzzle + :return: Inner puzzle + """ r = puzzle.uncurry() if r is None: return None @@ -77,13 +127,36 @@ def get_innerpuzzle_from_puzzle(puzzle: Program) -> Optional[Program]: return INNER_PUZZLE -def create_recovery_message_puzzle(recovering_coin_id: bytes32, newpuz: bytes32, pubkey: G1Element): - puzstring = f"(q . ((0x{ConditionOpcode.CREATE_COIN_ANNOUNCEMENT.hex()} 0x{recovering_coin_id.hex()}) (0x{ConditionOpcode.AGG_SIG_UNSAFE.hex()} 0x{bytes(pubkey).hex()} 0x{newpuz.hex()})))" # noqa - puz = binutils.assemble(puzstring) - return Program.to(puz) - - -def create_spend_for_message(parent_of_message, recovering_coin, newpuz, pubkey): +def create_recovery_message_puzzle(recovering_coin_id: bytes32, newpuz: bytes32, pubkey: G1Element) -> Program: + """ + Create attestment message puzzle + :param recovering_coin_id: ID of the DID coin needs to recover + :param newpuz: New wallet puzzle hash + :param pubkey: New wallet pubkey + :return: Message puzzle + """ + return Program.to( + ( + 1, + [ + [ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, recovering_coin_id], + [ConditionOpcode.AGG_SIG_UNSAFE, bytes(pubkey), newpuz], + ], + ) + ) + + +def create_spend_for_message( + parent_of_message: bytes32, recovering_coin: bytes32, newpuz: bytes32, pubkey: G1Element +) -> CoinSpend: + """ + Create a CoinSpend for a atestment + :param parent_of_message: Parent coin ID + :param recovering_coin: ID of the DID coin needs to recover + :param newpuz: New wallet puzzle hash + :param pubkey: New wallet pubkey + :return: CoinSpend + """ puzzle = create_recovery_message_puzzle(recovering_coin, newpuz, pubkey) coin = Coin(parent_of_message, puzzle.get_tree_hash(), uint64(0)) solution = Program.to([]) @@ -91,10 +164,58 @@ def create_spend_for_message(parent_of_message, recovering_coin, newpuz, pubkey) return coinsol -# inspect puzzle and check it is a DID puzzle -def check_is_did_puzzle(puzzle: Program): +def match_did_puzzle(puzzle: Program) -> Tuple[bool, Iterator[Program]]: + """ + Given a puzzle test if it's a DID, if it is, return the curried arguments + :param puzzle: Puzzle + :return: Curried parameters + """ + try: + mod, curried_args = puzzle.uncurry() + if mod == SINGLETON_TOP_LAYER_MOD: + mod, curried_args = curried_args.rest().first().uncurry() + if mod == DID_INNERPUZ_MOD: + return True, curried_args.as_iter() + except Exception: + import traceback + + print(f"exception: {traceback.format_exc()}") + return False, iter(()) + return False, iter(()) + + +def check_is_did_puzzle(puzzle: Program) -> bool: + """ + Check if a puzzle is a DID puzzle + :param puzzle: Puzzle + :return: Boolean + """ r = puzzle.uncurry() if r is None: return r inner_f, args = r return is_did_core(inner_f) + + +def metadata_to_program(metadata: Dict) -> Program: + """ + Convert the metadata dict to a Chialisp program + :param metadata: User defined metadata + :return: Chialisp program + """ + kv_list = [] + for key, value in metadata.items(): + kv_list.append((assemble(key), assemble(value))) + return Program.to(kv_list) + + +def program_to_metadata(program: Program) -> Dict: + """ + Convert a program to a metadata dict + :param program: Chialisp program contains the metadata + :return: Metadata dict + """ + metadata = {} + for key, val in program.as_python(): + metadata[str(key, "utf-8")] = str(val, "utf-8") + return metadata diff --git a/chia/wallet/nft_wallet/__init__.py b/chia/wallet/nft_wallet/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/chia/wallet/nft_wallet/nft_info.py b/chia/wallet/nft_wallet/nft_info.py new file mode 100644 index 000000000000..b9c0f8c44de9 --- /dev/null +++ b/chia/wallet/nft_wallet/nft_info.py @@ -0,0 +1,50 @@ +from dataclasses import dataclass +from typing import List + +from chia.util.ints import uint64 +from chia.util.streamable import Streamable, streamable + + +@streamable +@dataclass(frozen=True) +class NFTInfo(Streamable): + """NFT Info for displaying NFT on the UI""" + + launcher_id: str + """Launcher coin ID""" + + nft_coin_id: str + """Current NFT coin ID""" + + did_owner: str + """Owner DID""" + + royalty: uint64 + """Percentage of the transaction fee paid to the author, e.g. 1000 = 1%""" + + data_uris: List[str] + """ A list of content URIs""" + + data_hash: str + """Hash of the content""" + + metadata_uris: List[str] + """A list of metadata URIs""" + + metadata_hash: str + """Hash of the metadata""" + + license_uris: List[str] + """A list of license URIs""" + + license_hash: str + """Hash of the license""" + + version: str + """Current NFT version""" + + edition_count: uint64 + """How many NFTs in the current series""" + + edition_number: uint64 + """Number of the current NFT in the series""" diff --git a/chia/wallet/nft_wallet/nft_puzzles.py b/chia/wallet/nft_wallet/nft_puzzles.py new file mode 100644 index 000000000000..30a01be3eae3 --- /dev/null +++ b/chia/wallet/nft_wallet/nft_puzzles.py @@ -0,0 +1,101 @@ +import logging + +from chia.types.blockchain_format.coin import Coin +from chia.types.blockchain_format.program import Program +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.ints import uint64 +from chia.wallet.nft_wallet.nft_info import NFTInfo +from chia.wallet.nft_wallet.uncurry_nft import UncurriedNFT +from chia.wallet.puzzles.load_clvm import load_clvm + +log = logging.getLogger(__name__) +SINGLETON_TOP_LAYER_MOD = load_clvm("singleton_top_layer_v1_1.clvm") +LAUNCHER_PUZZLE = load_clvm("singleton_launcher.clvm") +NFT_STATE_LAYER_MOD = load_clvm("nft_state_layer.clvm") +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_TRANSFER_PROGRAM = load_clvm("nft_transfer_program.clvm") +OFFER_MOD = load_clvm("settlement_payments.clvm") +NFT_METADATA_UPDATER = load_clvm("nft_metadata_updater_default.clvm") + + +def create_nft_layer_puzzle_with_curry_params( + metadata: Program, metadata_updater_hash: bytes32, inner_puzzle: Program +) -> Program: + """Curries params into nft_state_layer.clvm + + Args to curry: + NFT_STATE_LAYER_MOD_HASH + METADATA + METADATA_UPDATER_PUZZLE_HASH + INNER_PUZZLE""" + log.debug( + "Creating nft layer puzzle curry: mod_hash: %s, metadata: %r, metadata_hash: %s", + NFT_STATE_LAYER_MOD_HASH, + metadata, + metadata_updater_hash, + ) + return NFT_STATE_LAYER_MOD.curry(NFT_STATE_LAYER_MOD_HASH, metadata, metadata_updater_hash, inner_puzzle) + + +def create_full_puzzle_with_nft_puzzle(singleton_id: bytes32, inner_puzzle: Program) -> Program: + log.debug( + "Creating full NFT puzzle with inner puzzle: \n%r\n%r", + singleton_id, + inner_puzzle.get_tree_hash(), + ) + singleton_struct = Program.to((SINGLETON_MOD_HASH, (singleton_id, LAUNCHER_PUZZLE_HASH))) + + full_puzzle = SINGLETON_TOP_LAYER_MOD.curry(singleton_struct, inner_puzzle) + log.debug("Created NFT full puzzle with inner: %s", full_puzzle.get_tree_hash()) + return full_puzzle + + +def create_full_puzzle( + singleton_id: bytes32, metadata: Program, metadata_updater_puzhash: bytes32, inner_puzzle: Program +) -> Program: + log.debug( + "Creating full NFT puzzle with: \n%r\n%r\n%r\n%r", + singleton_id, + metadata.get_tree_hash(), + metadata_updater_puzhash, + inner_puzzle.get_tree_hash(), + ) + singleton_struct = Program.to((SINGLETON_MOD_HASH, (singleton_id, LAUNCHER_PUZZLE_HASH))) + singleton_inner_puzzle = create_nft_layer_puzzle_with_curry_params(metadata, metadata_updater_puzhash, inner_puzzle) + + full_puzzle = SINGLETON_TOP_LAYER_MOD.curry(singleton_struct, singleton_inner_puzzle) + log.debug("Created NFT full puzzle: %s", full_puzzle.get_tree_hash()) + return full_puzzle + + +def get_nft_info_from_puzzle(puzzle: Program, nft_coin: Coin) -> NFTInfo: + """ + Extract NFT info from a full puzzle + :param puzzle: NFT full puzzle + :param nft_coin: NFT coin + :return: NFTInfo + """ + # TODO Update this method after the NFT code finalized + uncurried_nft: UncurriedNFT = UncurriedNFT.uncurry(puzzle) + data_uris = [] + for uri in uncurried_nft.data_uris.as_python(): + data_uris.append(str(uri, "utf-8")) + + nft_info = NFTInfo( + uncurried_nft.singleton_launcher_id.as_python().hex().upper(), + nft_coin.name().hex().upper(), + uncurried_nft.owner_did.as_python().hex().upper(), + uint64(uncurried_nft.trade_price_percentage.as_int()), + data_uris, + uncurried_nft.data_hash.as_python().hex().upper(), + [], + "", + [], + "", + "NFT0", + uint64(1), + uint64(1), + ) + return nft_info diff --git a/chia/wallet/nft_wallet/nft_wallet.py b/chia/wallet/nft_wallet/nft_wallet.py new file mode 100644 index 000000000000..f0d70394e6e9 --- /dev/null +++ b/chia/wallet/nft_wallet/nft_wallet.py @@ -0,0 +1,564 @@ +import json +import logging +import time +from dataclasses import dataclass +from secrets import token_bytes +from typing import Any, Dict, List, Optional, Set, Type, TypeVar + +from blspy import AugSchemeMPL, G1Element, G2Element +from clvm.casts import int_from_bytes, int_to_bytes + +from chia.clvm.singleton import SINGLETON_TOP_LAYER_MOD +from chia.protocols.wallet_protocol import CoinState +from chia.server.outbound_message import NodeType +from chia.server.ws_connection import WSChiaConnection +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_spend import CoinSpend +from chia.types.spend_bundle import SpendBundle +from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict +from chia.util.ints import uint8, uint32, uint64, uint128 +from chia.util.streamable import Streamable, streamable +from chia.wallet.derivation_record import DerivationRecord +from chia.wallet.lineage_proof import LineageProof +from chia.wallet.nft_wallet import nft_puzzles +from chia.wallet.nft_wallet.nft_puzzles import LAUNCHER_PUZZLE, NFT_METADATA_UPDATER, NFT_STATE_LAYER_MOD_HASH +from chia.wallet.nft_wallet.uncurry_nft import UncurriedNFT +from chia.wallet.puzzles.load_clvm import load_clvm +from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( + DEFAULT_HIDDEN_PUZZLE_HASH, + calculate_synthetic_secret_key, + puzzle_for_pk, + solution_for_conditions, +) +from chia.wallet.puzzles.puzzle_utils import make_create_coin_condition +from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.util.compute_memos import compute_memos +from chia.wallet.util.debug_spend_bundle import disassemble +from chia.wallet.util.transaction_type import TransactionType +from chia.wallet.util.wallet_types import WalletType +from chia.wallet.wallet import Wallet +from chia.wallet.wallet_info import WalletInfo + +_T_NFTWallet = TypeVar("_T_NFTWallet", bound="NFTWallet") + + +OFFER_MOD = load_clvm("settlement_payments.clvm") + + +@streamable +@dataclass(frozen=True) +class NFTCoinInfo(Streamable): + coin: Coin + lineage_proof: LineageProof + full_puzzle: Program + + +@streamable +@dataclass(frozen=True) +class NFTWalletInfo(Streamable): + my_nft_coins: List[NFTCoinInfo] + did_wallet_id: Optional[uint32] = None + + +def create_fullpuz(innerpuz: Program, genesis_id: bytes32) -> Program: + mod_hash = SINGLETON_TOP_LAYER_MOD.get_tree_hash() + # singleton_struct = (MOD_HASH . (LAUNCHER_ID . LAUNCHER_PUZZLE_HASH)) + singleton_struct = Program.to((mod_hash, (genesis_id, LAUNCHER_PUZZLE.get_tree_hash()))) + return SINGLETON_TOP_LAYER_MOD.curry(singleton_struct, innerpuz) + + +class NFTWallet: + wallet_state_manager: Any + log: logging.Logger + wallet_info: WalletInfo + nft_wallet_info: NFTWalletInfo + standard_wallet: Wallet + wallet_id: int + + @classmethod + async def create_new_nft_wallet( + cls: Type[_T_NFTWallet], + wallet_state_manager: Any, + wallet: Wallet, + did_wallet_id: uint32 = None, + name: str = "", + in_transaction: bool = False, + ) -> _T_NFTWallet: + """ + This must be called under the wallet state manager lock + """ + self = cls() + self.standard_wallet = wallet + self.log = logging.getLogger(name if name else __name__) + self.wallet_state_manager = wallet_state_manager + self.nft_wallet_info = NFTWalletInfo([], did_wallet_id) + info_as_string = json.dumps(self.nft_wallet_info.to_json_dict()) + wallet_info = await wallet_state_manager.user_store.create_wallet( + "NFT Wallet" if not name else name, + uint32(WalletType.NFT.value), + info_as_string, + in_transaction=in_transaction, + ) + + if wallet_info is None: + raise ValueError("Internal Error") + self.wallet_info = wallet_info + self.wallet_id = self.wallet_info.id + self.log.debug("NFT wallet id: %r and standard wallet id: %r", self.wallet_id, self.standard_wallet.wallet_id) + + await self.wallet_state_manager.add_new_wallet(self, self.wallet_info.id, in_transaction=in_transaction) + self.log.debug("Generated a new NFT wallet: %s", self.__dict__) + if not did_wallet_id: + # default profile wallet + self.log.debug("Standard NFT wallet created") + else: + # TODO: handle DID wallet puzhash + raise NotImplementedError() + return self + + @classmethod + async def create( + cls: Type[_T_NFTWallet], + wallet_state_manager: Any, + wallet: Wallet, + wallet_info: WalletInfo, + name: Optional[str] = None, + ) -> _T_NFTWallet: + self = cls() + self.log = logging.getLogger(name if name else __name__) + self.wallet_state_manager = wallet_state_manager + self.wallet_info = wallet_info + self.wallet_id = wallet_info.id + self.standard_wallet = wallet + self.wallet_info = wallet_info + self.nft_wallet_info = NFTWalletInfo.from_json_dict(json.loads(wallet_info.data)) + return self + + @classmethod + def type(cls) -> uint8: + return uint8(WalletType.NFT) + + async def get_new_puzzle(self) -> Program: + self.log.debug("Getting new puzzle for NFT wallet: %s", self.id()) + return self.puzzle_for_pk((await self.wallet_state_manager.get_unused_derivation_record(self.id())).pubkey) + + def id(self) -> uint32: + return self.wallet_info.id + + async def get_confirmed_balance(self, record_list=None) -> uint128: + """The NFT wallet doesn't really have a balance.""" + return uint128(0) + + async def get_unconfirmed_balance(self, record_list=None) -> uint128: + """The NFT wallet doesn't really have a balance.""" + return uint128(0) + + async def get_spendable_balance(self, unspent_records=None) -> uint128: + """The NFT wallet doesn't really have a balance.""" + return uint128(0) + + async def get_pending_change_balance(self) -> uint64: + return uint64(0) + + async def get_max_send_amount(self, records=None): + """This is the confirmed balance, which we set to 0 as the NFT wallet doesn't have one.""" + return uint128(0) + + def get_nft_coin_by_id(self, nft_coin_id: bytes32) -> NFTCoinInfo: + for nft_coin in self.nft_wallet_info.my_nft_coins: + if nft_coin.coin.name() == nft_coin_id: + return nft_coin + raise KeyError(f"Couldn't find coin with id: {nft_coin_id}") + + async def add_nft_coin(self, coin: Coin, spent_height: uint32, in_transaction: bool) -> None: + await self.coin_added(coin, spent_height, in_transaction=in_transaction) + + async def coin_added(self, coin: Coin, height: uint32, in_transaction: bool) -> None: + """Notification from wallet state manager that wallet has been received.""" + self.log.info(f"NFT wallet %s has been notified that {coin} was added", self.wallet_info.name) + for coin_info in self.nft_wallet_info.my_nft_coins: + if coin_info.coin == coin: + return + wallet_node = self.wallet_state_manager.wallet_node + server = wallet_node.server + full_nodes: Dict[bytes32, WSChiaConnection] = server.connection_by_type.get(NodeType.FULL_NODE, {}) + cs: Optional[CoinSpend] = None + coin_states: Optional[List[CoinState]] = await self.wallet_state_manager.wallet_node.get_coin_state( + [coin.parent_coin_info] + ) + if not coin_states: + # farm coin + return + assert coin_states + parent_coin = coin_states[0].coin + for node_id in full_nodes: + node = server.all_connections[node_id] + cs = await wallet_node.fetch_puzzle_solution(node, height, parent_coin) + if cs is not None: + break + assert cs is not None + await self.puzzle_solution_received(cs, in_transaction=in_transaction) + + async def puzzle_solution_received(self, coin_spend: CoinSpend, in_transaction: bool) -> None: + self.log.debug("Puzzle solution received to wallet: %s", self.wallet_info) + coin_name = coin_spend.coin.name() + puzzle: Program = Program.from_bytes(bytes(coin_spend.puzzle_reveal)) + solution: Program = Program.from_bytes(bytes(coin_spend.solution)).rest().rest().first().first() + # At this point, the puzzle must be a NFT puzzle. + # This method will be called only when the walle state manager uncurried this coin as a NFT puzzle. + + uncurried_nft = UncurriedNFT.uncurry(puzzle) + self.log.info( + f"found the info for NFT coin {coin_name} {uncurried_nft.inner_puzzle} {uncurried_nft.singleton_struct}" + ) + singleton_id = bytes32(uncurried_nft.singleton_launcher_id.atom) + metadata = uncurried_nft.metadata + new_inner_puzzle = None + parent_inner_puzhash = uncurried_nft.nft_state_layer.get_tree_hash() + self.log.debug("Before spend metadata: %s %s \n%s", metadata, singleton_id, disassemble(solution)) + for condition in solution.rest().first().rest().as_iter(): + self.log.debug("Checking solution condition: %s", disassemble(condition)) + if condition.list_len() < 2: + # invalid condition + continue + condition_code = int_from_bytes(condition.first().atom) + self.log.debug("Checking condition code: %r", condition_code) + if condition_code == -24: + # metadata update + # (-24 (meta updater puzzle) url) + metadata_list = list(metadata.as_python()) + new_metadata = [] + for metadata_entry in metadata_list: + key = metadata_entry[0] + if key == b"u": + new_metadata.append((b"u", [condition.rest().rest().first().atom] + list(metadata_entry[1:]))) + else: + new_metadata.append((b"h", metadata_entry[1])) + metadata = Program.to(new_metadata) + elif condition_code == 51 and int_from_bytes(condition.rest().rest().first().atom) == 1: + puzhash = bytes32(condition.rest().first().atom) + self.log.debug("Got back puzhash from solution: %s", puzhash) + derivation_record: Optional[ + DerivationRecord + ] = await self.wallet_state_manager.puzzle_store.get_derivation_record_for_puzzle_hash(puzhash) + if derivation_record is None: + # we potentially sent it somewhere + await self.remove_coin(coin_spend.coin, in_transaction=in_transaction) + return + new_inner_puzzle = puzzle_for_pk(derivation_record.pubkey) + + else: + raise ValueError("Invalid condition") + if new_inner_puzzle is None: + raise ValueError("Invalid puzzle") + parent_coin = None + coin_record = await self.wallet_state_manager.coin_store.get_coin_record(coin_name) + if coin_record is None: + coin_states: Optional[List[CoinState]] = await self.wallet_state_manager.wallet_node.get_coin_state( + [coin_name] + ) + if coin_states is not None: + parent_coin = coin_states[0].coin + if coin_record is not None: + parent_coin = coin_record.coin + if parent_coin is None: + raise ValueError("Error in finding parent") + self.log.debug("Got back updated metadata: %s", metadata) + child_puzzle: Program = nft_puzzles.create_full_puzzle( + singleton_id, + metadata, + bytes32(uncurried_nft.metdata_updater_hash.atom), + new_inner_puzzle, + ) + self.log.debug( + "Created NFT full puzzle with inner: %s", + nft_puzzles.create_full_puzzle_with_nft_puzzle(singleton_id, uncurried_nft.inner_puzzle), + ) + child_coin: Optional[Coin] = None + for new_coin in coin_spend.additions(): + self.log.debug( + "Comparing addition: %s with %s, amount: %s ", + new_coin.puzzle_hash, + child_puzzle.get_tree_hash(), + new_coin.amount, + ) + if new_coin.puzzle_hash == child_puzzle.get_tree_hash(): + child_coin = new_coin + break + else: + raise ValueError("Invalid NFT spend on %r" % coin_name) + self.log.info("Adding a new NFT to wallet: %s", child_coin) + await self.add_coin( + child_coin, + child_puzzle, + LineageProof(parent_coin.parent_coin_info, parent_inner_puzhash, parent_coin.amount), + in_transaction=in_transaction, + ) + + async def add_coin(self, coin: Coin, puzzle: Program, lineage_proof: LineageProof, in_transaction: bool) -> None: + my_nft_coins = self.nft_wallet_info.my_nft_coins + for coin_info in my_nft_coins: + if coin_info.coin == coin: + my_nft_coins.remove(coin_info) + + my_nft_coins.append(NFTCoinInfo(coin, lineage_proof, puzzle)) + new_nft_wallet_info = NFTWalletInfo( + my_nft_coins, + self.nft_wallet_info.did_wallet_id, + ) + await self.save_info(new_nft_wallet_info, in_transaction=in_transaction) + await self.wallet_state_manager.add_interested_coin_ids([coin.name()], in_transaction=in_transaction) + self.wallet_state_manager.state_changed("nft_coin_added", self.wallet_info.id) + return + + async def remove_coin(self, coin: Coin, in_transaction: bool) -> None: + my_nft_coins = self.nft_wallet_info.my_nft_coins + for coin_info in my_nft_coins: + if coin_info.coin == coin: + my_nft_coins.remove(coin_info) + new_nft_wallet_info = NFTWalletInfo( + my_nft_coins, + self.nft_wallet_info.did_wallet_id, + ) + await self.save_info(new_nft_wallet_info, in_transaction=in_transaction) + self.wallet_state_manager.state_changed("nft_coin_removed", self.wallet_info.id) + return + + def puzzle_for_pk(self, pk: G1Element) -> Program: + if not self.nft_wallet_info.did_wallet_id: + inner_puzzle = self.standard_wallet.puzzle_for_pk(bytes(pk)) + else: + raise NotImplementedError + provenance_puzzle = Program.to([NFT_STATE_LAYER_MOD_HASH, inner_puzzle]) + self.log.debug( + "Wallet name %s generated a puzzle: %s", self.wallet_info.name, provenance_puzzle.get_tree_hash() + ) + return provenance_puzzle + + async def generate_new_nft( + self, metadata: Program, target_puzzle_hash: bytes32 = None, fee: uint64 = uint64(0) + ) -> Optional[SpendBundle]: + """ + This must be called under the wallet state manager lock + """ + amount = uint64(1) + coins = await self.standard_wallet.select_coins(amount) + if coins is None: + return None + self.log.debug("Attempt to generate a new NFT") + origin = coins.copy().pop() + genesis_launcher_puz = nft_puzzles.LAUNCHER_PUZZLE + launcher_coin = Coin(origin.name(), genesis_launcher_puz.get_tree_hash(), uint64(amount)) + self.log.debug("Generating NFT with launcher coin %s and metadata: %s", launcher_coin, metadata) + + inner_puzzle = await self.standard_wallet.get_new_puzzle() + # singleton eve + eve_fullpuz = nft_puzzles.create_full_puzzle( + launcher_coin.name(), metadata, NFT_METADATA_UPDATER.get_tree_hash(), inner_puzzle + ) + # launcher announcement + announcement_set: Set[Announcement] = set() + announcement_message = Program.to([eve_fullpuz.get_tree_hash(), amount, []]).get_tree_hash() + announcement_set.add(Announcement(launcher_coin.name(), announcement_message)) + + self.log.debug( + "Creating transaction for launcher: %s and other coins: %s (%s)", origin, coins, announcement_set + ) + # store the launcher transaction in the wallet state + tx_record: Optional[TransactionRecord] = await self.standard_wallet.generate_signed_transaction( + uint64(amount), + genesis_launcher_puz.get_tree_hash(), + fee, + origin.name(), + coins, + None, + False, + announcement_set, + ) + + genesis_launcher_solution = Program.to([eve_fullpuz.get_tree_hash(), amount, []]) + + # launcher spend to generate the singleton + launcher_cs = CoinSpend(launcher_coin, genesis_launcher_puz, genesis_launcher_solution) + launcher_sb = SpendBundle([launcher_cs], AugSchemeMPL.aggregate([])) + + eve_coin = Coin(launcher_coin.name(), eve_fullpuz.get_tree_hash(), uint64(amount)) + + if tx_record is None or tx_record.spend_bundle is None: + return None + + if not target_puzzle_hash: + target_puzzle_hash = inner_puzzle.get_tree_hash() + condition_list = [make_create_coin_condition(target_puzzle_hash, amount, [target_puzzle_hash])] + innersol = solution_for_conditions(condition_list) + # EVE SPEND BELOW + fullsol = Program.to( + [ + [launcher_coin.parent_coin_info, launcher_coin.amount], + eve_coin.amount, + Program.to( + [ + innersol, + amount, + 0, + ] + ), + ] + ) + list_of_coinspends = [CoinSpend(eve_coin, eve_fullpuz, fullsol)] + eve_spend_bundle = SpendBundle(list_of_coinspends, AugSchemeMPL.aggregate([])) + eve_spend_bundle = await self.sign(eve_spend_bundle) + full_spend = SpendBundle.aggregate([tx_record.spend_bundle, eve_spend_bundle, launcher_sb]) + + nft_record = TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + to_puzzle_hash=eve_fullpuz.get_tree_hash(), + amount=uint64(amount), + fee_amount=fee, + confirmed=False, + sent=uint32(0), + spend_bundle=full_spend, + additions=full_spend.additions(), + removals=full_spend.removals(), + wallet_id=self.wallet_info.id, + sent_to=[], + trade_id=None, + type=uint32(TransactionType.OUTGOING_TX.value), + name=bytes32(token_bytes()), + memos=list(compute_memos(full_spend).items()), + ) + await self.standard_wallet.push_transaction(nft_record) + return nft_record.spend_bundle + + async def sign(self, spend_bundle: SpendBundle) -> SpendBundle: + sigs: List[G2Element] = [] + for spend in spend_bundle.coin_spends: + try: + uncurried_nft = UncurriedNFT.uncurry(spend.puzzle_reveal.to_program()) + except Exception as e: + # This only happens while you changed the NFT chialisp but didn't update the UncurriedNFT class. + self.log.error(e) + else: + self.log.debug("Found a NFT state layer to sign") + puzzle_hash = uncurried_nft.inner_puzzle.get_tree_hash() + keys = await self.wallet_state_manager.get_keys(puzzle_hash) + assert keys + _, private = keys + synthetic_secret_key = calculate_synthetic_secret_key(private, DEFAULT_HIDDEN_PUZZLE_HASH) + error, conditions, cost = conditions_dict_for_solution( + spend.puzzle_reveal.to_program(), + spend.solution.to_program(), + self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, + ) + if conditions is not None: + synthetic_pk = synthetic_secret_key.get_g1() + for pk, msg in pkm_pairs_for_conditions_dict( + conditions, spend.coin.name(), self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA + ): + try: + assert bytes(synthetic_pk) == pk + sigs.append(AugSchemeMPL.sign(synthetic_secret_key, msg)) + except AssertionError: + raise ValueError("This spend bundle cannot be signed by the NFT wallet") + + agg_sig = AugSchemeMPL.aggregate(sigs) + return SpendBundle.aggregate([spend_bundle, SpendBundle([], agg_sig)]) + + async def _make_nft_transaction( + self, nft_coin_info: NFTCoinInfo, inner_solution: Program, fee: uint64 = uint64(0) + ) -> TransactionRecord: + + coin = nft_coin_info.coin + amount = coin.amount + full_puzzle = nft_coin_info.full_puzzle + lineage_proof = nft_coin_info.lineage_proof + self.log.debug("Inner solution: %r", disassemble(inner_solution)) + full_solution = Program.to( + [ + [lineage_proof.parent_name, lineage_proof.inner_puzzle_hash, lineage_proof.amount], + coin.amount, + Program.to( + [ + inner_solution, + amount, + 0, + ] + ), + ] + ) + list_of_coinspends = [CoinSpend(coin, full_puzzle.to_serialized_program(), full_solution)] + spend_bundle = SpendBundle(list_of_coinspends, AugSchemeMPL.aggregate([])) + spend_bundle = await self.sign(spend_bundle) + full_spend = SpendBundle.aggregate([spend_bundle]) + self.log.debug("Memos are: %r", list(compute_memos(full_spend).items())) + nft_record = TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + to_puzzle_hash=full_puzzle.get_tree_hash(), + amount=uint64(amount), + fee_amount=fee, + confirmed=False, + sent=uint32(0), + spend_bundle=full_spend, + additions=full_spend.additions(), + removals=full_spend.removals(), + wallet_id=self.wallet_info.id, + sent_to=[], + trade_id=None, + type=uint32(TransactionType.OUTGOING_TX.value), + name=bytes32(token_bytes()), + memos=list(compute_memos(full_spend).items()), + ) + return nft_record + + async def update_metadata( + self, nft_coin_info: NFTCoinInfo, uri: str, fee: uint64 = uint64(0) + ) -> Optional[SpendBundle]: + coin = nft_coin_info.coin + # we're not changing it + + uncurried_nft = UncurriedNFT.uncurry(nft_coin_info.full_puzzle) + + puzzle_hash = uncurried_nft.inner_puzzle.get_tree_hash() + condition_list = [make_create_coin_condition(puzzle_hash, coin.amount, [puzzle_hash])] + condition_list.append([int_to_bytes(-24), NFT_METADATA_UPDATER, uri.encode("utf-8")]) + + self.log.info("Attempting to add a url to NFT coin %s in the metadata: %s", nft_coin_info, uri) + inner_solution = solution_for_conditions(condition_list) + nft_tx_record = await self._make_nft_transaction(nft_coin_info, inner_solution, fee) + await self.standard_wallet.push_transaction(nft_tx_record) + return nft_tx_record.spend_bundle + + async def transfer_nft( + self, + nft_coin_info: NFTCoinInfo, + puzzle_hash: bytes32, + did_hash=None, + fee: uint64 = uint64(0), + ) -> Optional[SpendBundle]: + self.log.debug("Attempt to transfer a new NFT") + coin = nft_coin_info.coin + self.log.debug("Transfering NFT coin %r to puzhash: %s", nft_coin_info, puzzle_hash) + + amount = coin.amount + condition_list = [make_create_coin_condition(puzzle_hash, amount, [bytes32(puzzle_hash)])] + self.log.debug("Condition for new coin: %r", condition_list) + inner_solution = solution_for_conditions(condition_list) + nft_tx_record = await self._make_nft_transaction(nft_coin_info, inner_solution, fee) + await self.standard_wallet.push_transaction(nft_tx_record) + return nft_tx_record.spend_bundle + + def get_current_nfts(self) -> List[NFTCoinInfo]: + return self.nft_wallet_info.my_nft_coins + + async def save_info(self, nft_info: NFTWalletInfo, in_transaction: bool) -> None: + self.nft_wallet_info = nft_info + current_info = self.wallet_info + data_str = json.dumps(nft_info.to_json_dict()) + wallet_info = WalletInfo(current_info.id, current_info.name, current_info.type, data_str) + self.wallet_info = wallet_info + await self.wallet_state_manager.user_store.update_wallet(wallet_info, in_transaction) diff --git a/chia/wallet/nft_wallet/uncurry_nft.py b/chia/wallet/nft_wallet/uncurry_nft.py new file mode 100644 index 000000000000..5e43bc232bdb --- /dev/null +++ b/chia/wallet/nft_wallet/uncurry_nft.py @@ -0,0 +1,122 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Type, TypeVar + +from chia.types.blockchain_format.program import Program +from chia.wallet.puzzles.load_clvm import load_clvm + +SINGLETON_TOP_LAYER_MOD = load_clvm("singleton_top_layer_v1_1.clvm") +NFT_MOD = load_clvm("nft_state_layer.clvm") + +_T_UncurriedNFT = TypeVar("_T_UncurriedNFT", bound="UncurriedNFT") + + +@dataclass(frozen=True) +class UncurriedNFT: + """ + A simple solution for uncurry NFT puzzle. + Initial the class with a full NFT puzzle, it will do a deep uncurry. + This is the only place you need to change after modified the Chialisp curried parameters. + """ + + nft_mod_hash: Program + """NFT module hash""" + + nft_state_layer: Program + """NFT state layer puzzle""" + + singleton_struct: Program + """ + Singleton struct + [singleton_mod_hash, singleton_launcher_id, launcher_puzhash] + """ + singleton_mod_hash: Program + singleton_launcher_id: Program + launcher_puzhash: Program + + owner_did: Program + """Owner's DID""" + + metdata_updater_hash: Program + """Metadata updater puzzle hash""" + + transfer_program_hash: Program + """Puzzle hash of the transfer program""" + + transfer_program_curry_params: Program + """ + Curried parameters of the transfer program + [royalty_address, trade_price_percentage, settlement_mod_hash, cat_mod_hash] + """ + royalty_address: Program + trade_price_percentage: Program + settlement_mod_hash: Program + cat_mod_hash: Program + + metadata: Program + """ + NFT metadata + [("u", data_uris), ("h", data_hash)] + """ + data_uris: Program + data_hash: Program + + inner_puzzle: Program + """NFT state layer inner puzzle""" + + @classmethod + def uncurry(cls: Type[_T_UncurriedNFT], puzzle: Program) -> UncurriedNFT: + """ + Try to uncurry a NFT puzzle + :param cls UncurriedNFT class + :param puzzle: Puzzle program + :return Uncurried NFT + """ + mod, curried_args = puzzle.uncurry() + if mod != SINGLETON_TOP_LAYER_MOD: + raise ValueError(f"Cannot uncurry NFT puzzle, failed on singleton top layer: Mod {mod}") + try: + (singleton_struct, nft_state_layer) = curried_args.as_iter() + singleton_mod_hash = singleton_struct.first() + singleton_launcher_id = singleton_struct.rest().first() + launcher_puzhash = singleton_struct.rest().rest() + except ValueError as e: + raise ValueError(f"Cannot uncurry singleton top layer: Args {curried_args}") from e + + mod, curried_args = curried_args.rest().first().uncurry() + if mod != NFT_MOD: + raise ValueError(f"Cannot uncurry NFT puzzle, failed on NFT state layer: Mod {mod}") + try: + # Set nft parameters + (nft_mod_hash, metadata, metdata_updater_hash, inner_puzzle) = curried_args.as_iter() + + # Set metadata + for kv_pair in metadata.as_iter(): + if kv_pair.first().as_atom() == b"u": + data_uris = kv_pair.rest() + if kv_pair.first().as_atom() == b"h": + data_hash = kv_pair.rest() + except Exception as e: + raise ValueError(f"Cannot uncurry NFT state layer: Args {curried_args}") from e + return cls( + nft_mod_hash=nft_mod_hash, + nft_state_layer=nft_state_layer, + singleton_struct=singleton_struct, + singleton_mod_hash=singleton_mod_hash, + singleton_launcher_id=singleton_launcher_id, + launcher_puzhash=launcher_puzhash, + metadata=metadata, + data_uris=data_uris, + data_hash=data_hash, + metdata_updater_hash=metdata_updater_hash, + inner_puzzle=inner_puzzle, + # TODO Set/Remove following fields after NFT1 implemented + owner_did=Program.to([]), + transfer_program_hash=Program.to([]), + transfer_program_curry_params=Program.to([]), + royalty_address=Program.to([]), + trade_price_percentage=Program.to([]), + settlement_mod_hash=Program.to([]), + cat_mod_hash=Program.to([]), + ) diff --git a/chia/wallet/puzzles/cat.clvm.hex b/chia/wallet/puzzles/cat.clvm.hex index 613d2b96bb7a..89cddbc1e50d 100644 --- a/chia/wallet/puzzles/cat.clvm.hex +++ b/chia/wallet/puzzles/cat.clvm.hex @@ -1 +1 @@ -ff02ffff01ff02ff5effff04ff02ffff04ffff04ff05ffff04ffff0bff2cff0580ffff04ff0bff80808080ffff04ffff02ff17ff2f80ffff04ff5fffff04ffff02ff2effff04ff02ffff04ff17ff80808080ffff04ffff0bff82027fff82057fff820b7f80ffff04ff81bfffff04ff82017fffff04ff8202ffffff04ff8205ffffff04ff820bffff80808080808080808080808080ffff04ffff01ffffffff81ca3dff46ff0233ffff3c04ff01ff0181cbffffff02ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff22ffff0bff2cff3480ffff0bff22ffff0bff22ffff0bff2cff5c80ff0980ffff0bff22ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ff0bffff01ff02ffff03ffff09ffff02ff2effff04ff02ffff04ff13ff80808080ff820b9f80ffff01ff02ff26ffff04ff02ffff04ffff02ff13ffff04ff5fffff04ff17ffff04ff2fffff04ff81bfffff04ff82017fffff04ff1bff8080808080808080ffff04ff82017fff8080808080ffff01ff088080ff0180ffff01ff02ffff03ff17ffff01ff02ffff03ffff20ff81bf80ffff0182017fffff01ff088080ff0180ffff01ff088080ff018080ff0180ffff04ffff04ff05ff2780ffff04ffff10ff0bff5780ff778080ff02ffff03ff05ffff01ff02ffff03ffff09ffff02ffff03ffff09ff11ff7880ffff0159ff8080ff0180ffff01818f80ffff01ff02ff7affff04ff02ffff04ff0dffff04ff0bffff04ffff04ff81b9ff82017980ff808080808080ffff01ff02ff5affff04ff02ffff04ffff02ffff03ffff09ff11ff7880ffff01ff04ff78ffff04ffff02ff36ffff04ff02ffff04ff13ffff04ff29ffff04ffff0bff2cff5b80ffff04ff2bff80808080808080ff398080ffff01ff02ffff03ffff09ff11ff2480ffff01ff04ff24ffff04ffff0bff20ff2980ff398080ffff010980ff018080ff0180ffff04ffff02ffff03ffff09ff11ff7880ffff0159ff8080ff0180ffff04ffff02ff7affff04ff02ffff04ff0dffff04ff0bffff04ff17ff808080808080ff80808080808080ff0180ffff01ff04ff80ffff04ff80ff17808080ff0180ffffff02ffff03ff05ffff01ff04ff09ffff02ff26ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff0bff22ffff0bff2cff5880ffff0bff22ffff0bff22ffff0bff2cff5c80ff0580ffff0bff22ffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bff2cff058080ff0180ffff04ffff04ff28ffff04ff5fff808080ffff02ff7effff04ff02ffff04ffff04ffff04ff2fff0580ffff04ff5fff82017f8080ffff04ffff02ff7affff04ff02ffff04ff0bffff04ff05ffff01ff808080808080ffff04ff17ffff04ff81bfffff04ff82017fffff04ffff0bff8204ffffff02ff36ffff04ff02ffff04ff09ffff04ff820affffff04ffff0bff2cff2d80ffff04ff15ff80808080808080ff8216ff80ffff04ff8205ffffff04ff820bffff808080808080808080808080ff02ff2affff04ff02ffff04ff5fffff04ff3bffff04ffff02ffff03ff17ffff01ff09ff2dffff0bff27ffff02ff36ffff04ff02ffff04ff29ffff04ff57ffff04ffff0bff2cff81b980ffff04ff59ff80808080808080ff81b78080ff8080ff0180ffff04ff17ffff04ff05ffff04ff8202ffffff04ffff04ffff04ff24ffff04ffff0bff7cff2fff82017f80ff808080ffff04ffff04ff30ffff04ffff0bff81bfffff0bff7cff15ffff10ff82017fffff11ff8202dfff2b80ff8202ff808080ff808080ff138080ff80808080808080808080ff018080 +ff02ffff01ff02ff5effff04ff02ffff04ffff04ff05ffff04ffff0bff2cff0580ffff04ff0bff80808080ffff04ffff02ff17ff2f80ffff04ff5fffff04ffff02ff2effff04ff02ffff04ff17ff80808080ffff04ffff0bff82027fff82057fff820b7f80ffff04ff81bfffff04ff82017fffff04ff8202ffffff04ff8205ffffff04ff820bffff80808080808080808080808080ffff04ffff01ffffffff81ca3dff46ff0233ffff3c04ff01ff0181cbffffff02ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff22ffff0bff2cff3480ffff0bff22ffff0bff22ffff0bff2cff5c80ff0980ffff0bff22ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ff0bffff01ff02ffff03ffff09ffff02ff2effff04ff02ffff04ff13ff80808080ff820b9f80ffff01ff02ff26ffff04ff02ffff04ffff02ff13ffff04ff5fffff04ff17ffff04ff2fffff04ff81bfffff04ff82017fffff04ff1bff8080808080808080ffff04ff82017fff8080808080ffff01ff088080ff0180ffff01ff02ffff03ff17ffff01ff02ffff03ffff20ff81bf80ffff0182017fffff01ff088080ff0180ffff01ff088080ff018080ff0180ffff04ffff04ff05ff2780ffff04ffff10ff0bff5780ff778080ff02ffff03ff05ffff01ff02ffff03ffff09ffff02ffff03ffff09ff11ff7880ffff0159ff8080ff0180ffff01818f80ffff01ff02ff7affff04ff02ffff04ff0dffff04ff0bffff04ffff04ff81b9ff82017980ff808080808080ffff01ff02ff5affff04ff02ffff04ffff02ffff03ffff09ff11ff7880ffff01ff04ff78ffff04ffff02ff36ffff04ff02ffff04ff13ffff04ff29ffff04ffff0bff2cff5b80ffff04ff2bff80808080808080ff398080ffff01ff02ffff03ffff09ff11ff2480ffff01ff04ff24ffff04ffff0bff20ff2980ff398080ffff010980ff018080ff0180ffff04ffff02ffff03ffff09ff11ff7880ffff0159ff8080ff0180ffff04ffff02ff7affff04ff02ffff04ff0dffff04ff0bffff04ff17ff808080808080ff80808080808080ff0180ffff01ff04ff80ffff04ff80ff17808080ff0180ffffff02ffff03ff05ffff01ff04ff09ffff02ff26ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff0bff22ffff0bff2cff5880ffff0bff22ffff0bff22ffff0bff2cff5c80ff0580ffff0bff22ffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bff2cff058080ff0180ffff04ffff04ff28ffff04ff5fff808080ffff02ff7effff04ff02ffff04ffff04ffff04ff2fff0580ffff04ff5fff82017f8080ffff04ffff02ff7affff04ff02ffff04ff0bffff04ff05ffff01ff808080808080ffff04ff17ffff04ff81bfffff04ff82017fffff04ffff0bff8204ffffff02ff36ffff04ff02ffff04ff09ffff04ff820affffff04ffff0bff2cff2d80ffff04ff15ff80808080808080ff8216ff80ffff04ff8205ffffff04ff820bffff808080808080808080808080ff02ff2affff04ff02ffff04ff5fffff04ff3bffff04ffff02ffff03ff17ffff01ff09ff2dffff0bff27ffff02ff36ffff04ff02ffff04ff29ffff04ff57ffff04ffff0bff2cff81b980ffff04ff59ff80808080808080ff81b78080ff8080ff0180ffff04ff17ffff04ff05ffff04ff8202ffffff04ffff04ffff04ff24ffff04ffff0bff7cff2fff82017f80ff808080ffff04ffff04ff30ffff04ffff0bff81bfffff0bff7cff15ffff10ff82017fffff11ff8202dfff2b80ff8202ff808080ff808080ff138080ff80808080808080808080ff018080 \ No newline at end of file diff --git a/chia/wallet/puzzles/did_innerpuz.clvm b/chia/wallet/puzzles/did_innerpuz.clvm index 4671de0c8f72..9788f16f9fa1 100644 --- a/chia/wallet/puzzles/did_innerpuz.clvm +++ b/chia/wallet/puzzles/did_innerpuz.clvm @@ -5,17 +5,19 @@ (mod ( - MY_PUBKEY ; the public key of the owner used for signing transactions + INNER_PUZZLE ; Standard P2 inner puzzle, used to record the ownership of the DID. RECOVERY_DID_LIST_HASH ; the list of DIDs that can send messages to you for recovery we store only the hash so that we don't have to reveal every time we make a message spend NUM_VERIFICATIONS_REQUIRED ; how many of the above list are required for a recovery - Truths ; Truths are sent from the singleton layer - mode ; this indicates which spend mode we want. Create message, recover, or self-destruct - new_amount ; DIDs can receive payments so when we recreate ourselves sometimes we want to change our amount - message ; this is a list of messages when creating a message spend, or a new puzhash when recovering or self destructing - new_inner_puzhash ; this is used during the message creation spend to optionally give ourselves a new inner puzzle - this is useful for updating our recovery list + SINGLETON_STRUCT ; my singleton_struct, formerly a Truth - ((SINGLETON_MOD_HASH, (LAUNCHER_ID, LAUNCHER_PUZZLE_HASH))) + METADATA ; Customized metadata, e.g KYC info + mode ; this indicates which spend mode we want. 0. Recovery mode 1. Run INNER_PUZZLE with p2_solution + my_amount_or_inner_solution ; In mode 0, we use this to recover our coin and assert it is our actual amount + ; In mode 1 this is the solution of the inner P2 puzzle, only required in the create message mode and transfer mode. + new_inner_puzhash ; In recovery mode, this will be the new wallet DID puzzle hash parent_innerpuzhash_amounts_for_recovery_ids ; during a recovery we need extra information about our recovery list coins pubkey ; this is the new pubkey used for a recovery recovery_list_reveal ; this is the reveal of the stored list of DIDs approved for recovery + my_id ; my coin ID ) ;message is the new puzzle in the recovery and standard spend cases @@ -24,7 +26,6 @@ (include condition_codes.clvm) (include curry-and-treehash.clinc) - (include singleton_truths.clib) ; takes a lisp tree and returns the hash of it (defun sha256tree1 (TREE) @@ -50,7 +51,7 @@ ) - ;; return the full puzzlehash for a singleton with the innerpuzzle curried in + ; return the full puzzlehash for a singleton with the innerpuzzle curried in ; puzzle-hash-of-curried-function is imported from curry-and-treehash.clinc (defun-inline calculate_full_puzzle_hash (SINGLETON_STRUCT inner_puzzle_hash) (puzzle-hash-of-curried-function (f SINGLETON_STRUCT) @@ -59,104 +60,76 @@ ) ) - (defmacro create_new_coin (amount new_puz) - (qq (c CREATE_COIN (c (unquote new_puz) (c (unquote amount) ())))) - ) - ; this loops over our identities to check list, and checks if we have been given parent information for this identity ; the reason for this is because we might only require 3/5 of the IDs give approval messages for a recovery ; if we have the information for an identity then we create a consume message using that information - (defun check_messages_from_identities (SINGLETON_STRUCT num_verifications_required identities my_id output new_puz parent_innerpuzhash_amounts_for_recovery_ids pubkey num_verifications) + (defun check_messages_from_identities (SINGLETON_STRUCT num_verifications_required identities my_id new_puz parent_innerpuzhash_amounts_for_recovery_ids pubkey num_verifications) (if identities (if (f parent_innerpuzhash_amounts_for_recovery_ids) ; if we have parent information then we should create a consume coin condition - (check_messages_from_identities - SINGLETON_STRUCT - num_verifications_required - (r identities) - my_id - (c - (create_consume_message - ; create coin_id from DID - (create_coin_ID_for_recovery - SINGLETON_STRUCT - (f identities) - (f (f parent_innerpuzhash_amounts_for_recovery_ids)) - (f (r (f parent_innerpuzhash_amounts_for_recovery_ids))) - (f (r (r (f parent_innerpuzhash_amounts_for_recovery_ids))))) - my_id - new_puz - pubkey) - output) - new_puz - (r parent_innerpuzhash_amounts_for_recovery_ids) - pubkey - (+ num_verifications 1) + (c + (create_consume_message + ; create coin_id from DID + (create_coin_ID_for_recovery + SINGLETON_STRUCT + (f identities) + (f (f parent_innerpuzhash_amounts_for_recovery_ids)) + (f (r (f parent_innerpuzhash_amounts_for_recovery_ids))) + (f (r (r (f parent_innerpuzhash_amounts_for_recovery_ids))))) + my_id + new_puz + pubkey + ) + (check_messages_from_identities + SINGLETON_STRUCT + num_verifications_required + (r identities) + my_id + new_puz + (r parent_innerpuzhash_amounts_for_recovery_ids) + pubkey + (+ num_verifications 1) + ) ) ; if no parent information found for this identity, move on to next in list (check_messages_from_identities SINGLETON_STRUCT (r identities) my_id - output new_puz (r parent_innerpuzhash_amounts_for_recovery_ids) pubkey num_verifications ) ) - ;if we're out of identites to check for, return our output - (if (> num_verifications num_verifications_required) - (c (list AGG_SIG_UNSAFE pubkey new_puz) output) - (if (= num_verifications num_verifications_required) - (c (list AGG_SIG_UNSAFE pubkey new_puz) output) - (x) - ) - ) - ) - ) - - ; for a list of messages in the format (type . message) create a message - ; type 0 is 0 value coin - ; type 1 is coin announcement - ; type 2 is puzzle announcement - (defun create_messages (messages) - (if messages - (c - (if (f (f messages)) - (list (if (= (f (f messages)) 1) CREATE_COIN_ANNOUNCEMENT CREATE_PUZZLE_ANNOUNCEMENT) (r (f messages))) - (list CREATE_COIN (r (f messages)) 0) - ) - (create_messages (r messages)) + ;if we're out of identites to check for, check we have enough + (if (> num_verifications (- num_verifications_required 1)) + (list (list AGG_SIG_UNSAFE pubkey new_puz) ) + (x) ) - () ) ) ;Spend modes: - ;0 = exit spend - ;1 = create messages and recreate singleton - ;2 = recovery + ;0 = recovery + ;1 = run the INNER_PUZZLE ;MAIN - (if mode - (if (= mode 1) - ; mode one - create messages and recreate singleton - (c (list CREATE_COIN new_inner_puzhash new_amount) (c (list AGG_SIG_ME MY_PUBKEY (sha256tree1 (list new_inner_puzhash new_amount message))) (create_messages message))) - ; mode two - recovery - ; check that recovery list is not empty - (if recovery_list_reveal - (if (= (sha256tree1 recovery_list_reveal) RECOVERY_DID_LIST_HASH) - (check_messages_from_identities (singleton_struct_truth Truths) NUM_VERIFICATIONS_REQUIRED recovery_list_reveal (my_id_truth Truths) (list (create_new_coin new_amount message)) message parent_innerpuzhash_amounts_for_recovery_ids pubkey 0) - (x) + ; mode 1 - run INNER_PUZZLE + (a INNER_PUZZLE my_amount_or_inner_solution) + + ; mode 0 - recovery + (if (all (= (sha256tree1 recovery_list_reveal) RECOVERY_DID_LIST_HASH) (> NUM_VERIFICATIONS_REQUIRED 0)) + (c (list ASSERT_MY_AMOUNT my_amount_or_inner_solution) + (c (list CREATE_COIN new_inner_puzhash my_amount_or_inner_solution (list new_inner_puzhash)) + (c (list ASSERT_MY_COIN_ID my_id) + (check_messages_from_identities SINGLETON_STRUCT NUM_VERIFICATIONS_REQUIRED recovery_list_reveal my_id new_inner_puzhash parent_innerpuzhash_amounts_for_recovery_ids pubkey 0) + ) ) - (x) ) + (x) ) - ; mode zero - exit spend - (list (list CREATE_COIN 0x00 -113) (list CREATE_COIN message new_amount) (list AGG_SIG_ME MY_PUBKEY (sha256tree1 (list new_amount message)))) ) - ) diff --git a/chia/wallet/puzzles/did_innerpuz.clvm.hex b/chia/wallet/puzzles/did_innerpuz.clvm.hex index 57e1b6de5434..1b28a9a87d13 100644 --- a/chia/wallet/puzzles/did_innerpuz.clvm.hex +++ b/chia/wallet/puzzles/did_innerpuz.clvm.hex @@ -1 +1 @@ -ff02ffff01ff02ffff03ff5fffff01ff02ffff03ffff09ff5fffff010180ffff01ff04ffff04ff24ffff04ff8202ffffff04ff81bfff80808080ffff04ffff04ff20ffff04ff05ffff04ffff02ff7effff04ff02ffff04ffff04ff8202ffffff04ff81bfffff04ff82017fff80808080ff80808080ff80808080ffff02ff36ffff04ff02ffff04ff82017fff808080808080ffff01ff02ffff03ff8217ffffff01ff02ffff03ffff09ffff02ff7effff04ff02ffff04ff8217ffff80808080ff0b80ffff01ff02ff3affff04ff02ffff04ff8201efffff04ff17ffff04ff8217ffffff04ff818fffff04ffff04ffff04ff24ffff04ff82017fffff04ff81bfff80808080ff8080ffff04ff82017fffff04ff8205ffffff04ff820bffffff01ff808080808080808080808080ffff01ff088080ff0180ffff01ff088080ff018080ff0180ffff01ff04ffff04ff24ffff01ff00ff818f8080ffff04ffff04ff24ffff04ff82017fffff04ff81bfff80808080ffff04ffff04ff20ffff04ff05ffff04ffff02ff7effff04ff02ffff04ffff04ff81bfffff04ff82017fff808080ff80808080ff80808080ff8080808080ff0180ffff04ffff01ffffffff3231ff3d02ffff333cff3eff0401ffffff0102ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff32ffff0bff7cff5c80ffff0bff32ffff0bff32ffff0bff7cff2280ff0980ffff0bff32ff0bffff0bff7cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff17ffff01ff02ffff03ff82027fffff01ff02ff3affff04ff02ffff04ff05ffff04ff0bffff04ff37ffff04ff2fffff04ffff04ffff04ff28ffff04ffff0bffff0bffff02ff26ffff04ff02ffff04ff05ffff04ff27ffff04ff82047fffff04ff820a7fffff04ff82167fff8080808080808080ffff02ff7effff04ff02ffff04ffff02ff2effff04ff02ffff04ff2fffff04ff81bfffff04ff8202ffff808080808080ff8080808080ff2f80ff808080ff5f80ffff04ff81bfffff04ff82037fffff04ff8202ffffff04ffff10ff8205ffffff010180ff808080808080808080808080ffff01ff02ff3affff04ff02ffff04ff05ffff04ff37ffff04ff2fffff04ff5fffff04ff81bfffff04ff82037fffff04ff8202ffffff04ff8205ffff808080808080808080808080ff0180ffff01ff02ffff03ffff15ff8205ffff0b80ffff01ff04ffff04ff30ffff04ff8202ffffff04ff81bfff80808080ff5f80ffff01ff02ffff03ffff09ff8205ffff0b80ffff01ff04ffff04ff30ffff04ff8202ffffff04ff81bfff80808080ff5f80ffff01ff088080ff018080ff018080ff0180ffffff0bff17ffff02ff5effff04ff02ffff04ff09ffff04ff2fffff04ffff02ff7effff04ff02ffff04ffff04ff09ffff04ff0bff1d8080ff80808080ff808080808080ff5f80ff02ffff03ff05ffff01ff04ffff02ffff03ff11ffff01ff04ffff02ffff03ffff09ff11ffff010180ffff0134ffff012c80ff0180ffff04ff19ff808080ffff01ff04ff24ffff04ff19ffff01ff8080808080ff0180ffff02ff36ffff04ff02ffff04ff0dff8080808080ff8080ff0180ffff04ffff0101ffff04ffff04ff34ffff04ff05ff808080ffff04ffff04ff30ffff04ff17ffff04ff0bff80808080ff80808080ffff0bff32ffff0bff7cff3880ffff0bff32ffff0bff32ffff0bff7cff2280ff0580ffff0bff32ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff7cff7c80ff8080808080ffff0bff7cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff7effff04ff02ffff04ff09ff80808080ffff02ff7effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 \ No newline at end of file +ff02ffff01ff02ffff03ff81bfffff01ff02ff05ff82017f80ffff01ff02ffff03ffff22ffff09ffff02ff7effff04ff02ffff04ff8217ffff80808080ff0b80ffff15ff17ff808080ffff01ff04ffff04ff28ffff04ff82017fff808080ffff04ffff04ff34ffff04ff8202ffffff04ff82017fffff04ffff04ff8202ffff8080ff8080808080ffff04ffff04ff38ffff04ff822fffff808080ffff02ff26ffff04ff02ffff04ff2fffff04ff17ffff04ff8217ffffff04ff822fffffff04ff8202ffffff04ff8205ffffff04ff820bffffff01ff8080808080808080808080808080ffff01ff088080ff018080ff0180ffff04ffff01ffffffff313dff4946ffff0233ff3c04ffffff0101ff02ff02ffff03ff05ffff01ff02ff3affff04ff02ffff04ff0dffff04ffff0bff2affff0bff22ff3c80ffff0bff2affff0bff2affff0bff22ff3280ff0980ffff0bff2aff0bffff0bff22ff8080808080ff8080808080ffff010b80ff0180ffffff02ffff03ff17ffff01ff02ffff03ff82013fffff01ff04ffff04ff30ffff04ffff0bffff0bffff02ff36ffff04ff02ffff04ff05ffff04ff27ffff04ff82023fffff04ff82053fffff04ff820b3fff8080808080808080ffff02ff7effff04ff02ffff04ffff02ff2effff04ff02ffff04ff2fffff04ff5fffff04ff82017fff808080808080ff8080808080ff2f80ff808080ffff02ff26ffff04ff02ffff04ff05ffff04ff0bffff04ff37ffff04ff2fffff04ff5fffff04ff8201bfffff04ff82017fffff04ffff10ff8202ffffff010180ff808080808080808080808080ffff01ff02ff26ffff04ff02ffff04ff05ffff04ff37ffff04ff2fffff04ff5fffff04ff8201bfffff04ff82017fffff04ff8202ffff8080808080808080808080ff0180ffff01ff02ffff03ffff15ff8202ffffff11ff0bffff01018080ffff01ff04ffff04ff20ffff04ff82017fffff04ff5fff80808080ff8080ffff01ff088080ff018080ff0180ff0bff17ffff02ff5effff04ff02ffff04ff09ffff04ff2fffff04ffff02ff7effff04ff02ffff04ffff04ff09ffff04ff0bff1d8080ff80808080ff808080808080ff5f80ffff04ffff0101ffff04ffff04ff2cffff04ff05ff808080ffff04ffff04ff20ffff04ff17ffff04ff0bff80808080ff80808080ffff0bff2affff0bff22ff2480ffff0bff2affff0bff2affff0bff22ff3280ff0580ffff0bff2affff02ff3affff04ff02ffff04ff07ffff04ffff0bff22ff2280ff8080808080ffff0bff22ff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff7effff04ff02ffff04ff09ff80808080ffff02ff7effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 \ No newline at end of file diff --git a/chia/wallet/puzzles/did_innerpuz.clvm.hex.sha256tree b/chia/wallet/puzzles/did_innerpuz.clvm.hex.sha256tree index 552facb8aac8..8b72ac7b5ff0 100644 --- a/chia/wallet/puzzles/did_innerpuz.clvm.hex.sha256tree +++ b/chia/wallet/puzzles/did_innerpuz.clvm.hex.sha256tree @@ -1 +1 @@ -ef41902d9964f6050f87de98b5c4e34512b7d2abded3fe700f7850ff20323bf2 +33143d2bef64f14036742673afd158126b94284b4530a28c354fac202b0c910e diff --git a/chia/wallet/puzzles/json.clib b/chia/wallet/puzzles/json.clib new file mode 100644 index 000000000000..d9e3c50bebf1 --- /dev/null +++ b/chia/wallet/puzzles/json.clib @@ -0,0 +1,25 @@ +( + + (defun search_list_then_resume (key json_object result) + (if result + result + (search_for_value_given_key key (r json_object)) + ) + ) + + (defun search_for_value_given_key (key json_object) + (if json_object + (if (l (f from_json_dict)) ; if we are hitting an unlabelled item (atom), just move on + (if (= (f (f json_object)) key) ; if we have found our key + (r (f json_object)) ; return the value + (if (l (r (f json_object))) ; otherwise check if we have hit another dictionary + (search_list_then_resume key json_object (search_for_value_given_key key (r (f json_object)))) ; check new dictionary + (search_for_value_given_key key (r json_object)) + ) + ) + (search_for_value_given_key key (r json_object)) + ) + () + ) + ) +) diff --git a/chia/wallet/puzzles/nft_innerpuz.clvm b/chia/wallet/puzzles/nft_innerpuz.clvm new file mode 100644 index 000000000000..8644ef99b783 --- /dev/null +++ b/chia/wallet/puzzles/nft_innerpuz.clvm @@ -0,0 +1,85 @@ +(mod ( + NFT_MOD_HASH + SINGLETON_STRUCT ; ((SINGLETON_MOD_HASH, (SINGLETON_LAUNCHER_ID, LAUNCHER_PUZZLE_HASH))) + CURRENT_OWNER_DID + TRANSFER_PROGRAM_MOD_HASH + TRANSFER_PROGRAM_CURRY_PARAMS + METADATA + my_did_inner_hash + new_did + new_did_inner_hash + transfer_program_reveal + transfer_program_solution +) + + (include condition_codes.clvm) + (include curry-and-treehash.clinc) + (include singleton_truths.clib) + + ; 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) + ) + ) + + (defun-inline nft_puzzle_hash (NFT_MOD_HASH SINGLETON_STRUCT TRANSFER_PROGRAM_MOD_HASH next_owner_did transfer_program_curry_params metadata) + (puzzle-hash-of-curried-function NFT_MOD_HASH + (sha256tree1 metadata) + (sha256tree1 transfer_program_curry_params) + (sha256 ONE TRANSFER_PROGRAM_MOD_HASH) + (sha256 ONE next_owner_did) + (sha256tree1 SINGLETON_STRUCT) + (sha256 ONE NFT_MOD_HASH) + ) + ) + + ;; return the full puzzlehash for a singleton with the innerpuzzle curried in + ; puzzle-hash-of-curried-function is imported from curry-and-treehash.clinc + (defun-inline calculate_full_puzzle_hash (SINGLETON_STRUCT inner_puzzle_hash) + (puzzle-hash-of-curried-function (f SINGLETON_STRUCT) + inner_puzzle_hash + (sha256tree1 SINGLETON_STRUCT) + ) + ) + + ; if we find an update, recreate ourselves with the update + ; otherwise recreate ourselves as we are + (defun parse_transfer_prog_output (NFT_MOD_HASH SINGLETON_STRUCT TRANSFER_PROGRAM_MOD_HASH TRANSFER_PROGRAM_CURRY_PARAMS METADATA new_did output found_update) + (if output + (if (= (f (f output)) -22) + (c + (list CREATE_COIN (nft_puzzle_hash NFT_MOD_HASH SINGLETON_STRUCT TRANSFER_PROGRAM_MOD_HASH new_did (f (r (r (f output)))) (f (r (f output)))) ONE (list new_did)) + (parse_transfer_prog_output NFT_MOD_HASH SINGLETON_STRUCT TRANSFER_PROGRAM_MOD_HASH TRANSFER_PROGRAM_CURRY_PARAMS METADATA new_did (r output) 1) + ) + (c (f output) (parse_transfer_prog_output NFT_MOD_HASH SINGLETON_STRUCT TRANSFER_PROGRAM_MOD_HASH TRANSFER_PROGRAM_CURRY_PARAMS METADATA new_did (r output) found_update)) + ) + (if found_update ; if we're at the end of the loop, check if we've already recreated ourselves or not + () + (list (list CREATE_COIN (nft_puzzle_hash NFT_MOD_HASH SINGLETON_STRUCT TRANSFER_PROGRAM_MOD_HASH new_did TRANSFER_PROGRAM_CURRY_PARAMS METADATA) ONE (list new_did))) + ) + ) + ) + + ; main + (c + (list ASSERT_MY_AMOUNT ONE) + (if new_did + (if (= (sha256tree1 transfer_program_reveal) TRANSFER_PROGRAM_MOD_HASH) + (c + (list ASSERT_PUZZLE_ANNOUNCEMENT (sha256 (calculate_full_puzzle_hash (c (f SINGLETON_STRUCT) (c CURRENT_OWNER_DID (r (r SINGLETON_STRUCT)))) my_did_inner_hash) (sha256tree1 (list transfer_program_solution new_did)))) + ; (METADATA CURRY_PARAMS SINGLETON_STRUCT current_owner trade_price_list my_did_inner_hash new_did new_did_inner_hash my_nft_id solution) + (parse_transfer_prog_output NFT_MOD_HASH SINGLETON_STRUCT TRANSFER_PROGRAM_MOD_HASH TRANSFER_PROGRAM_CURRY_PARAMS METADATA new_did (a transfer_program_reveal (list METADATA TRANSFER_PROGRAM_CURRY_PARAMS SINGLETON_STRUCT CURRENT_OWNER_DID my_did_inner_hash new_did new_did_inner_hash transfer_program_solution)) 0) + ) + (x) + ) + (list + (list CREATE_COIN (nft_puzzle_hash NFT_MOD_HASH SINGLETON_STRUCT TRANSFER_PROGRAM_MOD_HASH CURRENT_OWNER_DID TRANSFER_PROGRAM_CURRY_PARAMS METADATA) ONE (list CURRENT_OWNER_DID)) + (list ASSERT_PUZZLE_ANNOUNCEMENT (sha256 (calculate_full_puzzle_hash (c (f SINGLETON_STRUCT) (c CURRENT_OWNER_DID (r (r SINGLETON_STRUCT)))) my_did_inner_hash) 'a')) + (list CREATE_PUZZLE_ANNOUNCEMENT CURRENT_OWNER_DID) + ) + ) + ) + +) diff --git a/chia/wallet/puzzles/nft_innerpuz.clvm.hex b/chia/wallet/puzzles/nft_innerpuz.clvm.hex new file mode 100644 index 000000000000..5b131e692658 --- /dev/null +++ b/chia/wallet/puzzles/nft_innerpuz.clvm.hex @@ -0,0 +1 @@ +ff02ffff01ff04ffff04ff10ffff04ff12ff808080ffff02ffff03ff8202ffffff01ff02ffff03ffff09ffff02ff3effff04ff02ffff04ff820bffff80808080ff2f80ffff01ff04ffff04ff28ffff04ffff0bffff02ff2effff04ff02ffff04ff13ffff04ff82017fffff04ffff02ff3effff04ff02ffff04ffff04ff13ffff04ff17ff3b8080ff80808080ff808080808080ffff02ff3effff04ff02ffff04ffff04ff8217ffffff04ff8202ffff808080ff8080808080ff808080ffff02ff36ffff04ff02ffff04ff05ffff04ff0bffff04ff2fffff04ff5fffff04ff81bfffff04ff8202ffffff04ffff02ff820bffffff04ff81bfffff04ff5fffff04ff0bffff04ff17ffff04ff82017fffff04ff8202ffffff04ff8205ffffff04ff8217ffff80808080808080808080ffff01ff808080808080808080808080ffff01ff088080ff0180ffff01ff04ffff04ff14ffff04ffff02ff2effff04ff02ffff04ff05ffff04ffff02ff3effff04ff02ffff04ff81bfff80808080ffff04ffff02ff3effff04ff02ffff04ff5fff80808080ffff04ffff0bff12ff2f80ffff04ffff0bff12ff1780ffff04ffff02ff3effff04ff02ffff04ff0bff80808080ffff04ffff0bff12ff0580ff80808080808080808080ffff04ff12ffff04ffff04ff17ff8080ff8080808080ffff04ffff04ff28ffff04ffff0bffff02ff2effff04ff02ffff04ff13ffff04ff82017fffff04ffff02ff3effff04ff02ffff04ffff04ff13ffff04ff17ff3b8080ff80808080ff808080808080ffff016180ff808080ffff04ffff04ff2cffff04ff17ff808080ff8080808080ff018080ffff04ffff01ffffff49ff3f02ff33ff3e04ffff01ff0102ffffff02ffff03ff05ffff01ff02ff26ffff04ff02ffff04ff0dffff04ffff0bff3affff0bff12ff3c80ffff0bff3affff0bff3affff0bff12ff2a80ff0980ffff0bff3aff0bffff0bff12ff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff82017fffff01ff02ffff03ffff09ff82047fffff0181ea80ffff01ff04ffff04ff14ffff04ffff02ff2effff04ff02ffff04ff05ffff04ffff02ff3effff04ff02ffff04ff820a7fff80808080ffff04ffff02ff3effff04ff02ffff04ff82167fff80808080ffff04ffff0bff12ff1780ffff04ffff0bff12ff81bf80ffff04ffff02ff3effff04ff02ffff04ff0bff80808080ffff04ffff0bff12ff0580ff80808080808080808080ffff04ff12ffff04ffff04ff81bfff8080ff8080808080ffff02ff36ffff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82037fffff01ff018080808080808080808080ffff01ff04ff82027fffff02ff36ffff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82037fffff04ff8202ffff80808080808080808080808080ff0180ffff01ff02ffff03ff8202ffff80ffff01ff04ffff04ff14ffff04ffff02ff2effff04ff02ffff04ff05ffff04ffff02ff3effff04ff02ffff04ff5fff80808080ffff04ffff02ff3effff04ff02ffff04ff2fff80808080ffff04ffff0bff12ff1780ffff04ffff0bff12ff81bf80ffff04ffff02ff3effff04ff02ffff04ff0bff80808080ffff04ffff0bff12ff0580ff80808080808080808080ffff04ff12ffff04ffff04ff81bfff8080ff8080808080ff808080ff018080ff0180ffff0bff3affff0bff12ff3880ffff0bff3affff0bff3affff0bff12ff2a80ff0580ffff0bff3affff02ff26ffff04ff02ffff04ff07ffff04ffff0bff12ff1280ff8080808080ffff0bff12ff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bff12ff058080ff0180ff018080 diff --git a/chia/wallet/puzzles/nft_innerpuz.clvm.hex.sha256tree b/chia/wallet/puzzles/nft_innerpuz.clvm.hex.sha256tree new file mode 100644 index 000000000000..3a7220622577 --- /dev/null +++ b/chia/wallet/puzzles/nft_innerpuz.clvm.hex.sha256tree @@ -0,0 +1 @@ +ae8ee2bf9e0bcc189d4d8efa3c8281235981abd75ade592ba71930aa6881f820 diff --git a/chia/wallet/puzzles/nft_metadata_updater.clvm b/chia/wallet/puzzles/nft_metadata_updater.clvm new file mode 100644 index 000000000000..91d5f56eae10 --- /dev/null +++ b/chia/wallet/puzzles/nft_metadata_updater.clvm @@ -0,0 +1,20 @@ +(mod (CURRENT_METADATA solution) + + ; once we find 'u' we don't need to continue looping + (defun add_url (METADATA new_url) + (if METADATA + (if (= (f (f METADATA)) 'u') + (c (c 'u' (c new_url (r (f METADATA)))) (r METADATA)) + (c (f METADATA) (add_url (r METADATA) new_url)) + ) + () + ) + ) + + ; main + ; returns (new_metadata conditions) + (if solution + (list (add_url CURRENT_METADATA solution) 0) + (list CURRENT_METADATA 0) + ) +) diff --git a/chia/wallet/puzzles/nft_metadata_updater.clvm.hex b/chia/wallet/puzzles/nft_metadata_updater.clvm.hex new file mode 100644 index 000000000000..4354bceba900 --- /dev/null +++ b/chia/wallet/puzzles/nft_metadata_updater.clvm.hex @@ -0,0 +1 @@ +ff02ffff01ff02ffff03ff0bffff01ff04ffff02ff02ffff04ff02ffff04ff05ffff04ff0bff8080808080ffff01ff808080ffff01ff04ff05ffff01ff80808080ff0180ffff04ffff01ff02ffff03ff05ffff01ff02ffff03ffff09ff11ffff017580ffff01ff04ffff04ffff0175ffff04ff0bff198080ff0d80ffff01ff04ff09ffff02ff02ffff04ff02ffff04ff0dffff04ff0bff80808080808080ff0180ff8080ff0180ff018080 diff --git a/chia/wallet/puzzles/nft_metadata_updater.clvm.hex.sha256tree b/chia/wallet/puzzles/nft_metadata_updater.clvm.hex.sha256tree new file mode 100644 index 000000000000..33850df9e7ae --- /dev/null +++ b/chia/wallet/puzzles/nft_metadata_updater.clvm.hex.sha256tree @@ -0,0 +1 @@ +3df9de54667a96f32eba322635f14d3474edadf23f396ba5a3e2e077a89a682a diff --git a/chia/wallet/puzzles/nft_metadata_updater_default.clvm b/chia/wallet/puzzles/nft_metadata_updater_default.clvm new file mode 100644 index 000000000000..028c0f032f58 --- /dev/null +++ b/chia/wallet/puzzles/nft_metadata_updater_default.clvm @@ -0,0 +1,21 @@ +(mod (CURRENT_METADATA METADATA_UPDATER_PUZZLE_HASH solution) + + ; METADATA and METADATA_UPDATER_PUZZLE_HASH are passed in as truths from the layer above + + ; This program returns ((new_metadata new_metadata_updater_puzhash) conditions) + + ; once we find 'u' we don't need to continue looping + (defun add_url (METADATA new_url) + (if METADATA + (if (= (f (f METADATA)) 'u') + (c (c 'u' (c new_url (r (f METADATA)))) (r METADATA)) + (c (f METADATA) (add_url (r METADATA) new_url)) + ) + () + ) + ) + + ; main + ; returns ((new_metadata new_metadata_updater_puzhash) conditions) + (list (list (if solution (add_url CURRENT_METADATA solution) CURRENT_METADATA) METADATA_UPDATER_PUZZLE_HASH) 0) +) diff --git a/chia/wallet/puzzles/nft_metadata_updater_default.clvm.hex b/chia/wallet/puzzles/nft_metadata_updater_default.clvm.hex new file mode 100644 index 000000000000..ea4ea319ed71 --- /dev/null +++ b/chia/wallet/puzzles/nft_metadata_updater_default.clvm.hex @@ -0,0 +1 @@ +ff02ffff01ff04ffff04ffff02ffff03ff17ffff01ff02ff02ffff04ff02ffff04ff05ffff04ff17ff8080808080ffff010580ff0180ffff04ff0bff808080ffff01ff808080ffff04ffff01ff02ffff03ff05ffff01ff02ffff03ffff09ff11ffff017580ffff01ff04ffff04ffff0175ffff04ff0bff198080ff0d80ffff01ff04ff09ffff02ff02ffff04ff02ffff04ff0dffff04ff0bff80808080808080ff0180ff8080ff0180ff018080 diff --git a/chia/wallet/puzzles/nft_metadata_updater_default.clvm.hex.sha256tree b/chia/wallet/puzzles/nft_metadata_updater_default.clvm.hex.sha256tree new file mode 100644 index 000000000000..e2554de5be82 --- /dev/null +++ b/chia/wallet/puzzles/nft_metadata_updater_default.clvm.hex.sha256tree @@ -0,0 +1 @@ +81970d352e6a39a241eaf8ca510a0e669e40d778ba612621c60a50ef6cf29c7b diff --git a/chia/wallet/puzzles/nft_ownership_transfer_program.clvm b/chia/wallet/puzzles/nft_ownership_transfer_program.clvm new file mode 100644 index 000000000000..ba676025be89 --- /dev/null +++ b/chia/wallet/puzzles/nft_ownership_transfer_program.clvm @@ -0,0 +1,83 @@ +(mod + ( + ; SINGLETON_STRUCT is ((SINGLETON_MOD_HASH, (SINGLETON_LAUNCHER_ID, LAUNCHER_PUZZLE_HASH))) + SINGLETON_STRUCT + ; CURRY_PARAMS are: (ROYALTY_ADDRESS TRADE_PRICE_PERCENTAGE SETTLEMENT_MOD_HASH CAT_MOD_HASH) + CURRY_PARAMS + ; DID of the current owner + CURRENT_OWNER + ; Royalty program puzzle hash + ROYALTY_PROG_PUZHASH + ; DID inner puzzle hash of the current owner + current_did_puzhash + ; New NFT owner's DID + new_owner + ; DID inner puzzle hash of the new owner + new_did_puzhash + ; trade_prices_list is a list of (amount type) pairs + ; type 0 is standard chia + ; any other type is a bytes32 mod hash of a launcher + trade_prices_list + ) + + (include condition_codes.clvm) + (include curry-and-treehash.clinc) + (include sha256tree.clib) + (defconstant CHANGE_OWNERSHIP -22) + (defconstant TEN_THOUSAND 10000) + + ;; return the full puzzlehash for a singleton with the innerpuzzle curried in + ; puzzle-hash-of-curried-function is imported from curry-and-treehash.clinc + (defun-inline calculate_full_puzzle_hash (SINGLETON_STRUCT inner_puzzle_hash) + (puzzle-hash-of-curried-function (f SINGLETON_STRUCT) + inner_puzzle_hash + (sha256tree SINGLETON_STRUCT) + ) + ) + + ; Given a singleton ID, generate the singleton struct + (defun-inline get_singleton_struct (SINGLETON_STRUCT singleton_id) + (c (f SINGLETON_STRUCT) (c singleton_id (r (r SINGLETON_STRUCT)))) + ) + + (defun-inline cat_settlement_puzzle_hash (CAT_MOD_HASH tail_hash SETTLEMENT_MOD_HASH) + (puzzle-hash-of-curried-function CAT_MOD_HASH + SETTLEMENT_MOD_HASH + (sha256 1 tail_hash) + (sha256 1 CAT_MOD_HASH) + ) + ) + + (defun round_down_to_even (value) + (if (logand value 1) (- value 1) value) + ) + + (defun-inline calculate_percentage (amount percentage) + (f (divmod (* amount percentage) TEN_THOUSAND)) + ) + + ; Loop of the trade prices list and either assert a puzzle announcement or generate xch + (defun parse_trade_prices_list ((ROYALTY_ADDRESS TRADE_PRICE_PERCENTAGE SETTLEMENT_MOD_HASH CAT_MOD_HASH) trade_prices_list my_nft_id) + (if trade_prices_list + (c + (if (r (f trade_prices_list)) + (list ASSERT_PUZZLE_ANNOUNCEMENT (sha256 (cat_settlement_puzzle_hash CAT_MOD_HASH (f (r (f trade_prices_list))) SETTLEMENT_MOD_HASH) (sha256tree (c my_nft_id (list (list ROYALTY_ADDRESS (calculate_percentage (f (f trade_prices_list)) TRADE_PRICE_PERCENTAGE))))))) + (list CREATE_COIN ROYALTY_ADDRESS (round_down_to_even (calculate_percentage (f (f trade_prices_list)) TRADE_PRICE_PERCENTAGE))) + ) + (parse_trade_prices_list (list ROYALTY_ADDRESS TRADE_PRICE_PERCENTAGE SETTLEMENT_MOD_HASH CAT_MOD_HASH) (r trade_prices_list) my_nft_id) + ) + () + ) + ) + + ; main + (list + (c (list CHANGE_OWNERSHIP new_owner) + (c (list ASSERT_PUZZLE_ANNOUNCEMENT (sha256 (calculate_full_puzzle_hash (get_singleton_struct SINGLETON_STRUCT CURRENT_OWNER) current_did_puzhash) (f (r SINGLETON_STRUCT)) (sha256tree royalty_prog_solution) new_owner)) + (c (list ASSERT_PUZZLE_ANNOUNCEMENT (sha256 (calculate_full_puzzle_hash (get_singleton_struct SINGLETON_STRUCT new_owner) new_did_puzhash) (f (r SINGLETON_STRUCT)) (sha256tree royalty_prog_solution))) + (parse_trade_prices_list CURRY_PARAMS trade_prices_list (f (r SINGLETON_STRUCT))) + ) + ) + ) + ) +) diff --git a/chia/wallet/puzzles/nft_ownership_transfer_program.clvm.hex b/chia/wallet/puzzles/nft_ownership_transfer_program.clvm.hex new file mode 100644 index 000000000000..956a1e674d5a --- /dev/null +++ b/chia/wallet/puzzles/nft_ownership_transfer_program.clvm.hex @@ -0,0 +1 @@ +ff02ffff01ff04ffff04ffff04ff38ffff04ff81bfff808080ffff04ffff04ff10ffff04ffff0bffff02ff36ffff04ff02ffff04ff09ffff04ff5fffff04ffff02ff3effff04ff02ffff04ffff04ff09ffff04ff17ff1d8080ff80808080ff808080808080ff15ffff02ff3effff04ff02ffff01ff95726f79616c74795f70726f675f736f6c7574696f6e808080ff81bf80ff808080ffff04ffff04ff10ffff04ffff0bffff02ff36ffff04ff02ffff04ff09ffff04ff82017fffff04ffff02ff3effff04ff02ffff04ffff04ff09ffff04ff81bfff1d8080ff80808080ff808080808080ff15ffff02ff3effff04ff02ffff01ff95726f79616c74795f70726f675f736f6c7574696f6e80808080ff808080ffff02ff26ffff04ff02ffff04ff0bffff04ff8202ffffff04ff15ff808080808080808080ff8080ffff04ffff01ffffff3fff0281eaffff3304ff0101ffff822710ff02ff02ffff03ff05ffff01ff02ff3affff04ff02ffff04ff0dffff04ffff0bff2affff0bff2cff3480ffff0bff2affff0bff2affff0bff2cff3c80ff0980ffff0bff2aff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffffff02ffff03ff0bffff01ff04ffff02ffff03ff33ffff01ff04ff10ffff04ffff0bffff02ff36ffff04ff02ffff04ff5dffff04ff2dffff04ffff0bffff0101ff5380ffff04ffff0bffff0101ff5d80ff80808080808080ffff02ff3effff04ff02ffff04ffff04ff17ffff04ffff04ff09ffff04ffff05ffff14ffff12ff23ff1580ff128080ff808080ff808080ff8080808080ff808080ffff01ff04ff24ffff04ff09ffff04ffff02ff2effff04ff02ffff04ffff05ffff14ffff12ff23ff1580ff128080ff80808080ff8080808080ff0180ffff02ff26ffff04ff02ffff04ffff04ff09ffff04ff15ffff04ff2dffff04ff5dff8080808080ffff04ff1bffff04ff17ff80808080808080ff8080ff0180ff0bff2affff0bff2cff2880ffff0bff2affff0bff2affff0bff2cff3c80ff0580ffff0bff2affff02ff3affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff18ff05ffff010180ffff01ff11ff05ffff010180ffff010580ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 diff --git a/chia/wallet/puzzles/nft_ownership_transfer_program.clvm.hex.sha256tree b/chia/wallet/puzzles/nft_ownership_transfer_program.clvm.hex.sha256tree new file mode 100644 index 000000000000..8c39f0f85ae9 --- /dev/null +++ b/chia/wallet/puzzles/nft_ownership_transfer_program.clvm.hex.sha256tree @@ -0,0 +1 @@ +03557552e0bf8b200d8be8089f54786195ab8c07e3e07c39641f2f4f1fc2e48e diff --git a/chia/wallet/puzzles/nft_state_layer.clvm b/chia/wallet/puzzles/nft_state_layer.clvm new file mode 100644 index 000000000000..2daeb0990264 --- /dev/null +++ b/chia/wallet/puzzles/nft_state_layer.clvm @@ -0,0 +1,99 @@ +(mod ( + NFT_STATE_LAYER_MOD_HASH + METADATA + METADATA_UPDATER_PUZZLE_HASH + INNER_PUZZLE + solution ; either to inner puzzle or metadata updater + my_amount + ) + + (include condition_codes.clvm) + (include curry-and-treehash.clinc) + + (defun sha256tree1 + (TREE) + (if (l TREE) + (sha256 2 (sha256tree1 (f TREE)) (sha256tree1 (r TREE))) + (sha256 1 TREE) + ) + ) + + (defun-inline nft_state_layer_puzzle_hash (NFT_STATE_LAYER_MOD_HASH METADATA METADATA_UPDATER_PUZZLE_HASH inner_puzzle_hash) + (puzzle-hash-of-curried-function NFT_STATE_LAYER_MOD_HASH + inner_puzzle_hash + (sha256 ONE METADATA_UPDATER_PUZZLE_HASH) + (sha256tree1 METADATA) + (sha256 ONE NFT_STATE_LAYER_MOD_HASH) + ) + ) + + (defun wrap_odd_create_coins (NFT_STATE_LAYER_MOD_HASH ((METADATA METADATA_UPDATER_PUZZLE_HASH) conditions) my_amount new_create_coin_condition) + (if conditions + (if (= (f (f conditions)) CREATE_COIN) + (if (logand (f (r (r (f conditions)))) ONE) + (wrap_odd_create_coins NFT_STATE_LAYER_MOD_HASH (list (list METADATA METADATA_UPDATER_PUZZLE_HASH) (r conditions)) my_amount (f conditions)) + (c (f conditions) (wrap_odd_create_coins NFT_STATE_LAYER_MOD_HASH (list (list METADATA METADATA_UPDATER_PUZZLE_HASH) (r conditions)) my_amount new_create_coin_condition)) + ) + (if (> (f (f conditions)) 0) + (c (f conditions) (wrap_odd_create_coins NFT_STATE_LAYER_MOD_HASH (list (list METADATA METADATA_UPDATER_PUZZLE_HASH) (r conditions)) my_amount new_create_coin_condition)) + (wrap_odd_create_coins NFT_STATE_LAYER_MOD_HASH (list (list METADATA METADATA_UPDATER_PUZZLE_HASH) (r conditions)) my_amount new_create_coin_condition) + ) + ) + (if new_create_coin_condition + (list (c CREATE_COIN + (c (nft_state_layer_puzzle_hash + NFT_STATE_LAYER_MOD_HASH METADATA + METADATA_UPDATER_PUZZLE_HASH + (f (r new_create_coin_condition))) + (c my_amount + (r (r (r new_create_coin_condition))) + ) + ) + )) + (x) + ) + ) + ) + + ; take two lists and merge them into one + (defun merge_list (list_a list_b) + (if list_a + (c (f list_a) (merge_list (r list_a) list_b)) + list_b + ) + ) + + (defun check_for_metadata_prog_reveal (METADATA_UPDATER_PUZZLE_HASH CURRENT_METADATA conditions_loop) + (if conditions_loop + (if (= (f (f conditions_loop)) -24) + (if (= (sha256tree1 (f (r (f conditions_loop)))) METADATA_UPDATER_PUZZLE_HASH) + (a (f (r (f conditions_loop))) (list CURRENT_METADATA METADATA_UPDATER_PUZZLE_HASH (f (r (r (f conditions_loop)))))) + (x) + ) + (check_for_metadata_prog_reveal METADATA_UPDATER_PUZZLE_HASH CURRENT_METADATA (r conditions_loop)) + ) + (list (list CURRENT_METADATA METADATA_UPDATER_PUZZLE_HASH) 0) + ) + ) + + (defun process_metadata_updater (metadata_result conditions) + (c (f metadata_result) (list (merge_list (f (r metadata_result)) conditions))) + ) + + (defun metadata_updater_loader (METADATA_UPDATER_PUZZLE_HASH CURRENT_METADATA conditions) + (process_metadata_updater (check_for_metadata_prog_reveal METADATA_UPDATER_PUZZLE_HASH CURRENT_METADATA conditions) conditions) + ) + + ; main + (c + (list ASSERT_MY_AMOUNT my_amount) + (wrap_odd_create_coins + NFT_STATE_LAYER_MOD_HASH + (metadata_updater_loader + METADATA_UPDATER_PUZZLE_HASH METADATA (a INNER_PUZZLE solution) + ) + my_amount + 0 + ) + ) +) diff --git a/chia/wallet/puzzles/nft_state_layer.clvm.hex b/chia/wallet/puzzles/nft_state_layer.clvm.hex new file mode 100644 index 000000000000..489e442eb3bb --- /dev/null +++ b/chia/wallet/puzzles/nft_state_layer.clvm.hex @@ -0,0 +1 @@ +ff02ffff01ff04ffff04ff10ffff04ff81bfff808080ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff3affff04ff02ffff04ff17ffff04ff0bffff04ffff02ff2fff5f80ff808080808080ffff04ff81bfffff01ff8080808080808080ffff04ffff01ffffff49ff0233ffff0401ff0102ffffffff02ffff03ff05ffff01ff02ff22ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff17ffff01ff02ffff03ffff09ff47ffff0181e880ffff01ff02ffff03ffff09ffff02ff2effff04ff02ffff04ff81a7ff80808080ff0580ffff01ff02ff81a7ffff04ff0bffff04ff05ffff04ff820167ff8080808080ffff01ff088080ff0180ffff01ff02ff32ffff04ff02ffff04ff05ffff04ff0bffff04ff37ff80808080808080ff0180ffff01ff04ffff04ff0bffff04ff05ff808080ffff01ff80808080ff0180ffff02ffff03ff05ffff01ff04ff09ffff02ff2affff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff02ff26ffff04ff02ffff04ffff02ff32ffff04ff02ffff04ff05ffff04ff0bffff04ff17ff808080808080ffff04ff17ff8080808080ffffff04ff09ffff04ffff02ff2affff04ff02ffff04ff15ffff04ff0bff8080808080ff808080ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff22ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff2bffff01ff02ffff03ffff09ff818bff3880ffff01ff02ffff03ffff18ff8202cbff3480ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff04ffff04ff23ffff04ff53ff808080ffff04ff6bff808080ffff04ff17ffff04ff4bff80808080808080ffff01ff04ff4bffff02ff3effff04ff02ffff04ff05ffff04ffff04ffff04ff23ffff04ff53ff808080ffff04ff6bff808080ffff04ff17ffff04ff2fff808080808080808080ff0180ffff01ff02ffff03ffff15ff818bff8080ffff01ff04ff4bffff02ff3effff04ff02ffff04ff05ffff04ffff04ffff04ff23ffff04ff53ff808080ffff04ff6bff808080ffff04ff17ffff04ff2fff8080808080808080ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff04ffff04ff23ffff04ff53ff808080ffff04ff6bff808080ffff04ff17ffff04ff2fff8080808080808080ff018080ff0180ffff01ff02ffff03ff2fffff01ff04ffff04ff38ffff04ffff02ff36ffff04ff02ffff04ff05ffff04ff81afffff04ffff0bff34ff5380ffff04ffff02ff2effff04ff02ffff04ff23ff80808080ffff04ffff0bff34ff0580ff8080808080808080ffff04ff17ff8201ef808080ff8080ffff01ff088080ff018080ff0180ff018080 diff --git a/chia/wallet/puzzles/nft_state_layer.clvm.hex.sha256tree b/chia/wallet/puzzles/nft_state_layer.clvm.hex.sha256tree new file mode 100644 index 000000000000..2b34d8759a94 --- /dev/null +++ b/chia/wallet/puzzles/nft_state_layer.clvm.hex.sha256tree @@ -0,0 +1 @@ +dd8135d546e291df295b376aa89fc409c8c50d7f655d1ff4e845637901bc2f8f diff --git a/chia/wallet/puzzles/nft_transfer_program.clvm b/chia/wallet/puzzles/nft_transfer_program.clvm new file mode 100644 index 000000000000..1332e900f71c --- /dev/null +++ b/chia/wallet/puzzles/nft_transfer_program.clvm @@ -0,0 +1,139 @@ +(mod (METADATA CURRY_PARAMS SINGLETON_STRUCT current_owner my_did_inner_hash new_did new_did_inner_hash solution) + ; METADATA and CURRY_PARAMS are not actually curried into the mod hash but are curried into the NFT layer and passed in + ; This effectively achieves the same result - fixing the values - but means that the transfer program mod never changes + ; CURRY_PARAMS are: (ROYALTY_ADDRESS TRADE_PRICE_PERCENTAGE SETTLEMENT_MOD_HASH CAT_MOD_HASH) + + ; SINGLETON_STRUCT is ((SINGLETON_MOD_HASH, (SINGLETON_LAUNCHER_ID, LAUNCHER_PUZZLE_HASH))) + + ; For this transfer program solution is (trade_prices_list new_url) + + (include condition_codes.clvm) + (include curry-and-treehash.clinc) + (defconstant TEN_THOUSAND 10000) + + ; trade_prices_list is a list of (amount type) pairs + ; type 0 is standard chia + ; any other type is a bytes32 mod hash of a launcher + + (defun-inline cat_settlement_puzzle_hash (CAT_MOD_HASH tail_hash SETTLEMENT_MOD_HASH) + (puzzle-hash-of-curried-function CAT_MOD_HASH + SETTLEMENT_MOD_HASH + (sha256 ONE tail_hash) + (sha256 ONE CAT_MOD_HASH) + ) + ) + + ;; return the full puzzlehash for a singleton with the innerpuzzle curried in + ; puzzle-hash-of-curried-function is imported from curry-and-treehash.clinc + (defun-inline calculate_full_puzzle_hash (SINGLETON_STRUCT inner_puzzle_hash) + (puzzle-hash-of-curried-function (f SINGLETON_STRUCT) + inner_puzzle_hash + (sha256tree1 SINGLETON_STRUCT) + ) + ) + + (defun round_down_to_even (value) + (if (logand value ONE) (- value ONE) value) + ) + + (defun-inline calculate_percentage (amount percentage) + (f (divmod (* amount percentage) TEN_THOUSAND)) + ) + + (defun sha256tree1 (TREE) + (if (l TREE) + (sha256 2 (sha256tree1 (f TREE)) (sha256tree1 (r TREE))) + (sha256 ONE TREE) + ) + ) + + ; Loop of the trade prices list and either assert a puzzle announcement or generate xch + (defun parse_trade_prices_list ((ROYALTY_ADDRESS TRADE_PRICE_PERCENTAGE SETTLEMENT_MOD_HASH CAT_MOD_HASH) trade_prices_list my_nft_id) + (if trade_prices_list + (c + (if (r (f trade_prices_list)) + (list ASSERT_PUZZLE_ANNOUNCEMENT (sha256 (cat_settlement_puzzle_hash CAT_MOD_HASH (f (r (f trade_prices_list))) SETTLEMENT_MOD_HASH) (sha256tree1 (c my_nft_id (list (list ROYALTY_ADDRESS (calculate_percentage (f (f trade_prices_list)) TRADE_PRICE_PERCENTAGE))))))) + (list CREATE_COIN ROYALTY_ADDRESS (round_down_to_even (calculate_percentage (f (f trade_prices_list)) TRADE_PRICE_PERCENTAGE))) + ) + (parse_trade_prices_list (list ROYALTY_ADDRESS TRADE_PRICE_PERCENTAGE SETTLEMENT_MOD_HASH CAT_MOD_HASH) (r trade_prices_list) my_nft_id) + ) + () + ) + ) + + ; if we are part of a complex trade then we assert an announcement from the recipient of the trade_price and then parse the trade price list + (defun generate_conditions_for_trade ( + SINGLETON_STRUCT + CURRENT_OWNER_DID + CURRY_PARAMS + my_did_inner_hash + new_did + new_did_inner_hash + trade_prices_list + trade_prices_list_hash + ) + (c (list CREATE_PUZZLE_ANNOUNCEMENT trade_prices_list_hash) + (c (list ASSERT_PUZZLE_ANNOUNCEMENT (sha256 (calculate_full_puzzle_hash (c (f SINGLETON_STRUCT) (c new_did (r (r SINGLETON_STRUCT)))) new_did_inner_hash) trade_prices_list_hash (f (r SINGLETON_STRUCT)))) + (parse_trade_prices_list CURRY_PARAMS trade_prices_list (f (r SINGLETON_STRUCT))) + ) + ) + ) + + ; once we find 'u' we don't need to continue looping + (defun add_url (METADATA new_url) + (if METADATA + (if (= (f (f METADATA)) 'u') + (c (c 'u' (c new_url (r (f METADATA)))) (r METADATA)) + (c (f METADATA) (add_url (r METADATA) new_url)) + ) + () + ) + ) + + (defun check_if_add_url (METADATA CURRY_PARAMS solution output) + (if solution + (c (list -22 (add_url METADATA solution) CURRY_PARAMS) output) + output + ) + ) + + (defun check_if_gift ( + SINGLETON_STRUCT + CURRENT_OWNER_DID + CURRY_PARAMS + my_did_inner_hash + new_did + new_did_inner_hash + trade_prices_list + ) + (if trade_prices_list + (generate_conditions_for_trade + SINGLETON_STRUCT + CURRENT_OWNER_DID + CURRY_PARAMS + my_did_inner_hash + new_did + new_did_inner_hash + trade_prices_list + (sha256tree1 trade_prices_list) + ) + (list (list CREATE_PUZZLE_ANNOUNCEMENT 0)) + ) + ) + + ; main + (check_if_add_url + METADATA + CURRY_PARAMS + (f (r solution)) + (check_if_gift + SINGLETON_STRUCT + current_owner + CURRY_PARAMS + my_did_inner_hash + new_did + new_did_inner_hash + (f solution) + ) + ) +) diff --git a/chia/wallet/puzzles/nft_transfer_program.clvm.hex b/chia/wallet/puzzles/nft_transfer_program.clvm.hex new file mode 100644 index 000000000000..30ef4f5e5811 --- /dev/null +++ b/chia/wallet/puzzles/nft_transfer_program.clvm.hex @@ -0,0 +1 @@ +ff02ffff01ff02ff2affff04ff02ffff04ff05ffff04ff0bffff04ff820affffff04ffff02ff3affff04ff02ffff04ff17ffff04ff2fffff04ff0bffff04ff5fffff04ff81bfffff04ff82017fffff04ff8204ffff80808080808080808080ff80808080808080ffff04ffff01ffffffff3f02ff333effff0401ff01ff82271002ffffffff02ffff03ff05ffff01ff02ffff03ffff09ff11ffff017580ffff01ff04ffff04ffff0175ffff04ff0bff198080ff0d80ffff01ff04ff09ffff02ff22ffff04ff02ffff04ff0dffff04ff0bff80808080808080ff0180ff8080ff0180ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff7cffff0bff34ff2480ffff0bff7cffff0bff7cffff0bff34ff2c80ff0980ffff0bff7cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ff17ffff01ff04ffff04ffff0181eaffff04ffff02ff22ffff04ff02ffff04ff05ffff04ff17ff8080808080ffff04ff0bff80808080ff2f80ffff012f80ff0180ff02ffff03ff82017fffff01ff02ff26ffff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fffff04ffff02ff7effff04ff02ffff04ff82017fff80808080ff8080808080808080808080ffff01ff04ffff04ff38ffff01ff808080ff808080ff0180ffffff04ffff04ff38ffff04ff8202ffff808080ffff04ffff04ff20ffff04ffff0bffff02ff2effff04ff02ffff04ff09ffff04ff81bfffff04ffff02ff7effff04ff02ffff04ffff04ff09ffff04ff5fff1d8080ff80808080ff808080808080ff8202ffff1580ff808080ffff02ff36ffff04ff02ffff04ff17ffff04ff82017fffff04ff15ff8080808080808080ff02ffff03ff0bffff01ff04ffff02ffff03ff33ffff01ff04ff20ffff04ffff0bffff02ff2effff04ff02ffff04ff5dffff04ff2dffff04ffff0bff34ff5380ffff04ffff0bff34ff5d80ff80808080808080ffff02ff7effff04ff02ffff04ffff04ff17ffff04ffff04ff09ffff04ffff05ffff14ffff12ff23ff1580ff5c8080ff808080ff808080ff8080808080ff808080ffff01ff04ff28ffff04ff09ffff04ffff02ff5effff04ff02ffff04ffff05ffff14ffff12ff23ff1580ff5c8080ff80808080ff8080808080ff0180ffff02ff36ffff04ff02ffff04ffff04ff09ffff04ff15ffff04ff2dffff04ff5dff8080808080ffff04ff1bffff04ff17ff80808080808080ff8080ff0180ffff0bff7cffff0bff34ff3080ffff0bff7cffff0bff7cffff0bff34ff2c80ff0580ffff0bff7cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff18ff05ff3480ffff01ff11ff05ff3480ffff010580ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff7effff04ff02ffff04ff09ff80808080ffff02ff7effff04ff02ffff04ff0dff8080808080ffff01ff0bff34ff058080ff0180ff018080 \ No newline at end of file diff --git a/chia/wallet/puzzles/nft_transfer_program.clvm.hex.sha256tree b/chia/wallet/puzzles/nft_transfer_program.clvm.hex.sha256tree new file mode 100644 index 000000000000..e62c27dba870 --- /dev/null +++ b/chia/wallet/puzzles/nft_transfer_program.clvm.hex.sha256tree @@ -0,0 +1 @@ +519b806c31cce5280fa37695149521a301f13f5696d1fd1eeee1856c2d14965c diff --git a/chia/wallet/puzzles/p2_delegated_puzzle_or_hidden_puzzle.clvm b/chia/wallet/puzzles/p2_delegated_puzzle_or_hidden_puzzle.clvm index aa85f3763e34..ba1bf691fca9 100644 --- a/chia/wallet/puzzles/p2_delegated_puzzle_or_hidden_puzzle.clvm +++ b/chia/wallet/puzzles/p2_delegated_puzzle_or_hidden_puzzle.clvm @@ -10,7 +10,7 @@ ; synthetic_key_offset: a private key cryptographically generated using the hidden ; puzzle and as inputs `original_public_key` ; -; synthetic_public_key: the public key that is the sum of `original_public_key` and the +; SYNTHETIC_PUBLIC_KEY: the public key that is the sum of `original_public_key` and the ; public key corresponding to `synthetic_key_offset` ; ; original_public_key: a public key, where knowledge of the corresponding private key @@ -23,17 +23,17 @@ (mod - ; A puzzle should commit to `synthetic_public_key` + ; A puzzle should commit to `SYNTHETIC_PUBLIC_KEY` ; ; The solution should pass in 0 for `original_public_key` if it wants to use ; an arbitrary `delegated_puzzle` (and `solution`) signed by the - ; `synthetic_public_key` (whose corresponding private key can be calculated + ; `SYNTHETIC_PUBLIC_KEY` (whose corresponding private key can be calculated ; if you know the private key for `original_public_key`) ; ; Or you can solve the hidden puzzle by revealing the `original_public_key`, ; the hidden puzzle in `delegated_puzzle`, and a solution to the hidden puzzle. - (synthetic_public_key original_public_key delegated_puzzle solution) + (SYNTHETIC_PUBLIC_KEY original_public_key delegated_puzzle solution) ; "assert" is a macro that wraps repeated instances of "if" ; usage: (assert A0 A1 ... An R) @@ -61,9 +61,9 @@ ; "is_hidden_puzzle_correct" returns true iff the hidden puzzle is correctly encoded - (defun-inline is_hidden_puzzle_correct (synthetic_public_key original_public_key delegated_puzzle) + (defun-inline is_hidden_puzzle_correct (SYNTHETIC_PUBLIC_KEY original_public_key delegated_puzzle) (= - synthetic_public_key + SYNTHETIC_PUBLIC_KEY (point_add original_public_key (pubkey_for_exp (sha256 original_public_key (sha256tree1 delegated_puzzle))) @@ -73,19 +73,19 @@ ; "possibly_prepend_aggsig" is the main entry point - (defun-inline possibly_prepend_aggsig (synthetic_public_key original_public_key delegated_puzzle conditions) + (defun-inline possibly_prepend_aggsig (SYNTHETIC_PUBLIC_KEY original_public_key delegated_puzzle conditions) (if original_public_key (assert - (is_hidden_puzzle_correct synthetic_public_key original_public_key delegated_puzzle) + (is_hidden_puzzle_correct SYNTHETIC_PUBLIC_KEY original_public_key delegated_puzzle) conditions ) - (c (list AGG_SIG_ME synthetic_public_key (sha256tree1 delegated_puzzle)) conditions) + (c (list AGG_SIG_ME SYNTHETIC_PUBLIC_KEY (sha256tree1 delegated_puzzle)) conditions) ) ) ; main entry point (possibly_prepend_aggsig - synthetic_public_key original_public_key delegated_puzzle + SYNTHETIC_PUBLIC_KEY original_public_key delegated_puzzle (a delegated_puzzle solution)) ) diff --git a/chia/wallet/puzzles/settlement_payments.clvm.hex b/chia/wallet/puzzles/settlement_payments.clvm.hex index c3319eaf4c60..66116e114535 100644 --- a/chia/wallet/puzzles/settlement_payments.clvm.hex +++ b/chia/wallet/puzzles/settlement_payments.clvm.hex @@ -1 +1 @@ -ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 +ff02ffff01ff02ff0affff04ff02ffff04ff03ff80808080ffff04ffff01ffff333effff02ffff03ff05ffff01ff04ffff04ff0cffff04ffff02ff1effff04ff02ffff04ff09ff80808080ff808080ffff02ff16ffff04ff02ffff04ff19ffff04ffff02ff0affff04ff02ffff04ff0dff80808080ff808080808080ff8080ff0180ffff02ffff03ff05ffff01ff04ffff04ff08ff0980ffff02ff16ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 \ No newline at end of file diff --git a/chia/wallet/puzzles/sha256tree.clib b/chia/wallet/puzzles/sha256tree.clib new file mode 100644 index 000000000000..46e43b5cf28b --- /dev/null +++ b/chia/wallet/puzzles/sha256tree.clib @@ -0,0 +1,11 @@ +( + ;; hash a tree + ;; This is used to calculate a puzzle hash given a puzzle program. + (defun sha256tree + (TREE) + (if (l TREE) + (sha256 2 (sha256tree (f TREE)) (sha256tree (r TREE))) + (sha256 1 TREE) + ) + ) +) \ No newline at end of file diff --git a/chia/wallet/puzzles/singleton_top_layer_v1_1.clvm b/chia/wallet/puzzles/singleton_top_layer_v1_1.clvm new file mode 100644 index 000000000000..5b59cfa38886 --- /dev/null +++ b/chia/wallet/puzzles/singleton_top_layer_v1_1.clvm @@ -0,0 +1,124 @@ +(mod (SINGLETON_STRUCT INNER_PUZZLE lineage_proof my_amount inner_solution) + +;; SINGLETON_STRUCT = (MOD_HASH . (LAUNCHER_ID . LAUNCHER_PUZZLE_HASH)) + +; SINGLETON_STRUCT, INNER_PUZZLE are curried in by the wallet + +; EXAMPLE SOLUTION '(0xfadeddab 0xdeadbeef 1 (0xdeadbeef 200) 50 ((51 0xfadeddab 100) (60 "trash") (51 deadbeef 0)))' + + +; This puzzle is a wrapper around an inner smart puzzle which guarantees uniqueness. +; It takes its singleton identity from a coin with a launcher puzzle which guarantees that it is unique. + + (include condition_codes.clvm) + (include curry-and-treehash.clinc) ; also imports the constant ONE == 1 + (include singleton_truths.clib) + + (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 sha256tree (TREE) + (if (l TREE) + (sha256 2 (sha256tree (f TREE)) (sha256tree (r TREE))) + (sha256 ONE TREE))) + + (defun-inline mod_hash_for_singleton_struct (SINGLETON_STRUCT) (f SINGLETON_STRUCT)) + (defun-inline launcher_id_for_singleton_struct (SINGLETON_STRUCT) (f (r SINGLETON_STRUCT))) + (defun-inline launcher_puzzle_hash_for_singleton_struct (SINGLETON_STRUCT) (r (r SINGLETON_STRUCT))) + + ;; return the full puzzlehash for a singleton with the innerpuzzle curried in + ; puzzle-hash-of-curried-function is imported from curry-and-treehash.clinc + (defun-inline calculate_full_puzzle_hash (SINGLETON_STRUCT inner_puzzle_hash) + (puzzle-hash-of-curried-function (mod_hash_for_singleton_struct SINGLETON_STRUCT) + inner_puzzle_hash + (sha256tree SINGLETON_STRUCT) + ) + ) + + (defun-inline morph_condition (condition SINGLETON_STRUCT) + (c (f condition) (c (calculate_full_puzzle_hash SINGLETON_STRUCT (f (r condition))) (r (r condition)))) + ) + + ; Assert exactly one output with odd value exists - ignore it if value is -113 + + ;; this function iterates over the output conditions from the inner puzzle & solution + ;; and both checks that exactly one unique singleton child is created (with odd valued output), + ;; and wraps the inner puzzle with this same singleton wrapper puzzle + ;; + ;; The special case where the output value is -113 means a child singleton is intentionally + ;; *NOT* being created, thus forever ending this singleton's existence + + (defun check_and_morph_conditions_for_singleton (SINGLETON_STRUCT conditions has_odd_output_been_found) + (if conditions + ; check if it's an odd create coin + (if (and (= (f (f conditions)) CREATE_COIN) (logand (f (r (r (f conditions)))) ONE)) + ; check that we haven't already found one + (assert (not has_odd_output_been_found) + ; then + (if (= (f (r (r (f conditions)))) -113) + ; If it's the melt condition we don't bother prepending this condition + (check_and_morph_conditions_for_singleton SINGLETON_STRUCT (r conditions) ONE) + ; If it isn't the melt condition, we morph it and prepend it + (c (morph_condition (f conditions) SINGLETON_STRUCT) (check_and_morph_conditions_for_singleton SINGLETON_STRUCT (r conditions) ONE)) + ) + ) + (c (f conditions) (check_and_morph_conditions_for_singleton SINGLETON_STRUCT (r conditions) has_odd_output_been_found)) + ) + (assert has_odd_output_been_found ()) + ) + ) + + ; assert that either the lineage proof is for a parent singleton, or, if it's for the launcher, verify it matched our launcher ID + ; then return a condition asserting it actually is our parent ID + (defun verify_lineage_proof (SINGLETON_STRUCT parent_id is_not_launcher) + (assert (any is_not_launcher (= parent_id (launcher_id_for_singleton_struct SINGLETON_STRUCT))) + ; then + (list ASSERT_MY_PARENT_ID parent_id) + ) + ) + + ; main + + ; if our value is not an odd amount then we are invalid + (assert (logand my_amount ONE) + ; then + (c + (list ASSERT_MY_AMOUNT my_amount) + (c + ; Verify the lineage proof by asserting our parent's ID + (verify_lineage_proof + SINGLETON_STRUCT + ; calculate our parent's ID + (sha256 + (parent_info_for_lineage_proof lineage_proof) + (if (is_not_eve_proof lineage_proof) ; The PH calculation changes based on the lineage proof + (calculate_full_puzzle_hash SINGLETON_STRUCT (puzzle_hash_for_lineage_proof lineage_proof)) ; wrap the innerpuz in a singleton + (launcher_puzzle_hash_for_singleton_struct SINGLETON_STRUCT) ; Use the static launcher puzzle hash + ) + (if (is_not_eve_proof lineage_proof) ; The position of "amount" changes based on the type on lineage proof + (amount_for_lineage_proof lineage_proof) + (amount_for_eve_proof lineage_proof) + ) + ) + (is_not_eve_proof lineage_proof) + ) + ; finally check all of the conditions for a single odd output to wrap + (check_and_morph_conditions_for_singleton SINGLETON_STRUCT (a INNER_PUZZLE inner_solution) 0) + ) + ) + ) +) diff --git a/chia/wallet/puzzles/singleton_top_layer_v1_1.clvm.hex b/chia/wallet/puzzles/singleton_top_layer_v1_1.clvm.hex new file mode 100644 index 000000000000..245a282670cd --- /dev/null +++ b/chia/wallet/puzzles/singleton_top_layer_v1_1.clvm.hex @@ -0,0 +1 @@ +ff02ffff01ff02ffff03ffff18ff2fff3c80ffff01ff04ffff04ff10ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff0bff27ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff02ffff03ff77ffff0181b7ffff015780ff018080ffff04ff77ff808080808080ffff02ff26ffff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffff49ff4702ff33ff0401ffff01ff02ff02ffff03ff05ffff01ff02ff3affff04ff02ffff04ff0dffff04ffff0bff2affff0bff3cff2c80ffff0bff2affff0bff2affff0bff3cff1280ff0980ffff0bff2aff0bffff0bff3cff8080808080ff8080808080ffff010b80ff0180ffffff02ffff03ff0bffff01ff02ffff03ffff02ffff03ffff09ff23ff1480ffff01ff02ffff03ffff18ff81b3ff3c80ffff01ff0101ff8080ff0180ff8080ff0180ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff26ffff04ff02ffff04ff05ffff04ff1bffff04ff3cff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff26ffff04ff02ffff04ff05ffff04ff1bffff04ff3cff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff26ffff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ff0bff2affff0bff3cff3880ffff0bff2affff0bff2affff0bff3cff1280ff0580ffff0bff2affff02ff3affff04ff02ffff04ff07ffff04ffff0bff3cff3c80ff8080808080ffff0bff3cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bff3cff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff28ffff04ff0bff808080ffff01ff088080ff0180ff018080 \ No newline at end of file diff --git a/chia/wallet/puzzles/singleton_top_layer_v1_1.clvm.hex.sha256tree b/chia/wallet/puzzles/singleton_top_layer_v1_1.clvm.hex.sha256tree new file mode 100644 index 000000000000..9084d8194e04 --- /dev/null +++ b/chia/wallet/puzzles/singleton_top_layer_v1_1.clvm.hex.sha256tree @@ -0,0 +1 @@ +f1e8350cec62f8204aaf867cc3c12cae369f619258206616108c6cfd7be760b3 diff --git a/chia/wallet/util/json_clvm_utils.py b/chia/wallet/util/json_clvm_utils.py new file mode 100644 index 000000000000..b3eff0c8e0aa --- /dev/null +++ b/chia/wallet/util/json_clvm_utils.py @@ -0,0 +1,17 @@ +from typing import Any + +from chia.types.blockchain_format.program import Program + + +def json_to_chialisp(json_data: Any) -> Any: + list_for_chialisp = [] + if isinstance(json_data, list): + for value in json_data: + list_for_chialisp.append(json_to_chialisp(value)) + else: + if isinstance(json_data, dict): + for key, value in json_data: + list_for_chialisp.append((key, json_to_chialisp(value))) + else: + list_for_chialisp = json_data + return Program.to(list_for_chialisp) diff --git a/chia/wallet/util/wallet_types.py b/chia/wallet/util/wallet_types.py index f5330c674ec7..2028abc32d6a 100644 --- a/chia/wallet/util/wallet_types.py +++ b/chia/wallet/util/wallet_types.py @@ -19,6 +19,7 @@ class WalletType(IntEnum): RECOVERABLE = 7 DISTRIBUTED_ID = 8 POOLING_WALLET = 9 + NFT = 10 class AmountWithPuzzlehash(TypedDict): diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 7ede14134a69..1825e963743c 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -206,7 +206,7 @@ def make_solution( me=None, coin_announcements: Optional[Set[bytes]] = None, coin_announcements_to_assert: Optional[Set[bytes32]] = None, - puzzle_announcements: Optional[Set[bytes32]] = None, + puzzle_announcements: Optional[Set[bytes]] = None, puzzle_announcements_to_assert: Optional[Set[bytes32]] = None, fee=0, ) -> Program: @@ -310,7 +310,7 @@ async def _generate_unsigned_transaction( max_send = await self.get_max_send_amount() if total_amount > max_send: 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)) assert len(coins) > 0 @@ -428,6 +428,7 @@ async def generate_signed_transaction( else: non_change_amount = uint64(amount + sum(p["amount"] for p in primaries)) + self.log.debug("Generating transaction for: %s %s %s", puzzle_hash, amount, repr(coins)) transaction = await self._generate_unsigned_transaction( amount, puzzle_hash, @@ -442,8 +443,7 @@ async def generate_signed_transaction( negative_change_allowed, ) assert len(transaction) > 0 - - self.log.info("About to sign a transaction") + self.log.info("About to sign a transaction: %s", transaction) await self.hack_populate_secret_keys_for_coin_spends(transaction) spend_bundle: SpendBundle = await sign_coin_spends( transaction, @@ -488,7 +488,7 @@ async def push_transaction(self, tx: TransactionRecord) -> None: await self.wallet_state_manager.wallet_node.update_ui() # This is to be aggregated together with a CAT offer to ensure that the trade happens - async def create_spend_bundle_relative_chia(self, chia_amount: int, exclude: List[Coin]) -> SpendBundle: + async def create_spend_bundle_relative_chia(self, chia_amount: int, exclude: List[Coin] = []) -> SpendBundle: list_of_solutions = [] utxos = None diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 2246eca3326a..8484cf3f333b 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -812,6 +812,8 @@ async def state_update_received(self, request: wallet_protocol.CoinStateUpdate, # untrusted peers. For trusted, we always process the state, and we process reorgs as well. assert self.wallet_state_manager is not None assert self.server is not None + for coin in request.items: + self.log.info(f"request coin: {coin.coin.name()}{coin}") async with self.wallet_state_manager.lock: await self.receive_state_from_peer( diff --git a/chia/wallet/wallet_puzzle_store.py b/chia/wallet/wallet_puzzle_store.py index f4113c3a7b59..f022a6211be0 100644 --- a/chia/wallet/wallet_puzzle_store.py +++ b/chia/wallet/wallet_puzzle_store.py @@ -90,6 +90,7 @@ async def add_derivation_paths(self, records: List[DerivationRecord], in_transac try: sql_records = [] for record in records: + log.debug("Adding derivation record: %s", record) self.all_puzzle_hashes.add(record.puzzle_hash) if record.hardened: hardened = 1 diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index af9171e448b1..263137401350 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -7,17 +7,18 @@ from collections import defaultdict from pathlib import Path from secrets import token_bytes -from typing import Any, Callable, Dict, List, Optional, Set, Tuple +from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Tuple import aiosqlite from blspy import G1Element, PrivateKey -from chia.consensus.coinbase import pool_parent_id, farmer_parent_id +from chia.consensus.coinbase import farmer_parent_id, pool_parent_id from chia.consensus.constants import ConsensusConstants from chia.pools.pool_puzzles import SINGLETON_LAUNCHER_HASH, solution_to_pool_state from chia.pools.pool_wallet import PoolWallet from chia.protocols import wallet_protocol -from chia.protocols.wallet_protocol import PuzzleSolutionResponse, RespondPuzzleSolution, CoinState +from chia.protocols.wallet_protocol import CoinState, PuzzleSolutionResponse, RespondPuzzleSolution +from chia.server.server import ChiaServer from chia.server.ws_connection import WSChiaConnection from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import Program @@ -27,16 +28,20 @@ from chia.types.mempool_inclusion_status import MempoolInclusionStatus from chia.util.byte_types import hexstr_to_bytes from chia.util.config import process_config_start_method +from chia.util.db_synchronous import db_synchronous_on from chia.util.db_wrapper import DBWrapper from chia.util.errors import Err -from chia.util.ints import uint32, uint64, uint128, uint8 -from chia.util.db_synchronous import db_synchronous_on -from chia.wallet.cat_wallet.cat_utils import match_cat_puzzle, construct_cat_puzzle -from chia.wallet.cat_wallet.cat_wallet import CATWallet +from chia.util.ints import uint8, uint32, uint64, uint128 from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS +from chia.wallet.cat_wallet.cat_utils import construct_cat_puzzle, match_cat_puzzle +from chia.wallet.cat_wallet.cat_wallet import CATWallet from chia.wallet.derivation_record import DerivationRecord from chia.wallet.derive_keys import master_sk_to_wallet_sk, master_sk_to_wallet_sk_unhardened +from chia.wallet.did_wallet.did_wallet import DIDWallet +from chia.wallet.did_wallet.did_wallet_puzzles import DID_INNERPUZ_MOD, create_fullpuz, match_did_puzzle from chia.wallet.key_val_store import KeyValStore +from chia.wallet.nft_wallet.nft_wallet import NFTWallet, NFTWalletInfo +from chia.wallet.nft_wallet.uncurry_nft import UncurriedNFT from chia.wallet.outer_puzzles import AssetType from chia.wallet.puzzle_drivers import PuzzleInfo from chia.wallet.puzzles.cat_loader import CAT_MOD @@ -61,8 +66,6 @@ from chia.wallet.wallet_sync_store import WalletSyncStore from chia.wallet.wallet_transaction_store import WalletTransactionStore from chia.wallet.wallet_user_store import WalletUserStore -from chia.server.server import ChiaServer -from chia.wallet.did_wallet.did_wallet import DIDWallet from chia.wallet.wallet_weight_proof_handler import WalletWeightProofHandler @@ -203,6 +206,12 @@ async def create( self.main_wallet, wallet_info, ) + elif wallet_info.type == WalletType.NFT: + wallet = await NFTWallet.create( + self, + self.main_wallet, + wallet_info, + ) elif wallet_info.type == WalletType.POOLING_WALLET: wallet = await PoolWallet.create_from_db( self, @@ -238,11 +247,12 @@ async def create_more_puzzle_hashes(self, from_zero: bool = False, in_transactio 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() 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() + self.log.debug("Tried finding unused: %s", unused) if unused is None: # This handles the case where the database is empty unused = uint32(0) @@ -357,11 +367,13 @@ async def get_unused_derivation_record( # If we have no unused public keys, we will create new ones unused: Optional[uint32] = await self.puzzle_store.get_unused_derivation_path() if unused is None: + self.log.debug("No unused paths, generate more ") await self.create_more_puzzle_hashes() + # Now we must have unused public keys + unused = await self.puzzle_store.get_unused_derivation_path() + assert unused is not None - # Now we must have unused public keys - unused = await self.puzzle_store.get_unused_derivation_path() - assert unused is not None + self.log.debug("Fetching derivation record for: %s %s %s", unused, wallet_id, hardened) record: Optional[DerivationRecord] = await self.puzzle_store.get_derivation_record( unused, wallet_id, hardened ) @@ -526,7 +538,7 @@ async def unconfirmed_removals_for_wallet(self, wallet_id: int) -> Dict[bytes32, removals[coin.name()] = coin return removals - async def fetch_parent_and_check_for_cat( + async def determine_coin_type( self, peer: WSChiaConnection, coin_state: CoinState, fork_height: Optional[uint32] ) -> Tuple[Optional[uint32], Optional[WalletType]]: if self.is_pool_reward(coin_state.created_height, coin_state.coin.parent_coin_info) or self.is_farmer_reward( @@ -542,55 +554,190 @@ async def fetch_parent_and_check_for_cat( return None, None parent_coin_state = response[0] assert parent_coin_state.spent_height == coin_state.created_height - wallet_id = None - wallet_type = None - cs: Optional[CoinSpend] = await self.wallet_node.fetch_puzzle_solution( + + coin_spend: Optional[CoinSpend] = await self.wallet_node.fetch_puzzle_solution( peer, parent_coin_state.spent_height, parent_coin_state.coin ) - if cs is None: + if coin_spend is None: return None, None - matched, curried_args = match_cat_puzzle(Program.from_bytes(bytes(cs.puzzle_reveal))) - if matched: - mod_hash, tail_hash, inner_puzzle = curried_args - inner_puzzle_hash = inner_puzzle.get_tree_hash() - self.log.info( - f"parent: {parent_coin_state.coin.name()} inner_puzzle_hash for parent is {inner_puzzle_hash}" - ) + # Check if the coin is a CAT + cat_matched, cat_curried_args = match_cat_puzzle(Program.from_bytes(bytes(coin_spend.puzzle_reveal))) + if cat_matched: + return await self.handle_cat(cat_curried_args, parent_coin_state, coin_state, coin_spend) + + # Check if the coin is a NFT + # hint + # First spend where 1 mojo coin -> Singleton launcher -> NFT -> NFT + try: + uncurried_nft = UncurriedNFT.uncurry(Program.from_bytes(bytes(coin_spend.puzzle_reveal))) + except Exception: + # This is not a NFT coin, skip NFT handling + pass + else: + return await self.handle_nft(coin_spend, uncurried_nft) - hint_list = compute_coin_hints(cs) - derivation_record = None - for hint in hint_list: - derivation_record = await self.puzzle_store.get_derivation_record_for_puzzle_hash(bytes32(hint)) - if derivation_record is not None: - break + # Check if the coin is a DID + did_matched, did_curried_args = match_did_puzzle(Program.from_bytes(bytes(coin_spend.puzzle_reveal))) + if did_matched: + return await self.handle_did(did_curried_args, parent_coin_state, coin_state, coin_spend) + + return None, None - if derivation_record is None: - self.log.info(f"Received state for the coin that doesn't belong to us {coin_state}") + async def handle_cat( + self, + curried_args: Iterator[Program], + parent_coin_state: CoinState, + coin_state: CoinState, + coin_spend: CoinSpend, + ) -> Tuple[Optional[uint32], Optional[WalletType]]: + """ + Handle the new coin when it is a CAT + :param curried_args: Curried arg of the CAT mod + :param parent_coin_state: Parent coin state + :param coin_state: Current coin state + :param coin_spend: New coin spend + :return: Wallet ID & Wallet Type + """ + wallet_id = None + wallet_type = None + mod_hash, tail_hash, inner_puzzle = curried_args + inner_puzzle_hash = inner_puzzle.get_tree_hash() + self.log.info(f"parent: {parent_coin_state.coin.name()} inner_puzzle_hash for parent is {inner_puzzle_hash}") + + hint_list = compute_coin_hints(coin_spend) + derivation_record = None + for hint in hint_list: + derivation_record = await self.puzzle_store.get_derivation_record_for_puzzle_hash(bytes32(hint)) + if derivation_record is not None: + break + + if derivation_record is None: + self.log.info(f"Received state for the coin that doesn't belong to us {coin_state}") + else: + our_inner_puzzle: Program = self.main_wallet.puzzle_for_pk(bytes(derivation_record.pubkey)) + asset_id: bytes32 = bytes32(bytes(tail_hash)[1:]) + cat_puzzle = construct_cat_puzzle(CAT_MOD, asset_id, our_inner_puzzle) + if cat_puzzle.get_tree_hash() != coin_state.coin.puzzle_hash: + return None, None + if bytes(tail_hash).hex()[2:] in self.default_cats or self.config.get( + "automatically_add_unknown_cats", False + ): + cat_wallet = await CATWallet.create_wallet_for_cat( + self, self.main_wallet, bytes(tail_hash).hex()[2:], in_transaction=True + ) + wallet_id = cat_wallet.id() + wallet_type = WalletType(cat_wallet.type()) + self.state_changed("wallet_created") else: - our_inner_puzzle: Program = self.main_wallet.puzzle_for_pk(bytes(derivation_record.pubkey)) - asset_id: bytes32 = bytes32(bytes(tail_hash)[1:]) - cat_puzzle = construct_cat_puzzle(CAT_MOD, asset_id, our_inner_puzzle) - if cat_puzzle.get_tree_hash() != coin_state.coin.puzzle_hash: - return None, None - if bytes(tail_hash).hex()[2:] in self.default_cats or self.config.get( - "automatically_add_unknown_cats", False - ): - cat_wallet = await CATWallet.create_wallet_for_cat( - self, self.main_wallet, bytes(tail_hash).hex()[2:], in_transaction=True - ) - wallet_id = cat_wallet.id() - wallet_type = WalletType(cat_wallet.type()) - self.state_changed("wallet_created") + # Found unacknowledged CAT, save it in the database. + await self.interested_store.add_unacknowledged_token( + asset_id, + CATWallet.default_wallet_name_for_unknown_cat(asset_id.hex()), + parent_coin_state.spent_height, + parent_coin_state.coin.puzzle_hash, + ) + self.state_changed("added_stray_cat") + return wallet_id, wallet_type + + async def handle_did( + self, + curried_args: Iterator[Program], + parent_coin_state: CoinState, + coin_state: CoinState, + coin_spend: CoinSpend, + ) -> Tuple[Optional[uint32], Optional[WalletType]]: + """ + Handle the new coin when it is a DID + :param curried_args: Curried arg of the DID mod + :param parent_coin_state: Parent coin state + :param coin_state: Current coin state + :param coin_spend: New coin spend + :return: Wallet ID & Wallet Type + """ + wallet_id = None + wallet_type = None + p2_puzzle, recovery_list_hash, num_verification, singleton_struct, metadata = curried_args + inner_puzzle_hash = p2_puzzle.get_tree_hash() + self.log.info(f"parent: {parent_coin_state.coin.name()} inner_puzzle_hash for parent is {inner_puzzle_hash}") + + hint_list = compute_coin_hints(coin_spend) + derivation_record = None + for hint in hint_list: + derivation_record = await self.puzzle_store.get_derivation_record_for_puzzle_hash(bytes32(hint)) + if derivation_record is not None: + break + + if derivation_record is None: + self.log.info(f"Received state for the coin that doesn't belong to us {coin_state}") + else: + our_inner_puzzle: Program = self.main_wallet.puzzle_for_pk(bytes(derivation_record.pubkey)) + + launch_id: bytes32 = bytes32(bytes(singleton_struct.rest().first())[1:]) + self.log.info(f"Found DID, launch_id {launch_id}.") + did_puzzle = DID_INNERPUZ_MOD.curry( + our_inner_puzzle, recovery_list_hash, num_verification, singleton_struct, metadata + ) + full_puzzle = create_fullpuz(did_puzzle, launch_id) + did_puzzle_empty_recovery = DID_INNERPUZ_MOD.curry( + our_inner_puzzle, Program.to([]).get_tree_hash(), uint64(0), singleton_struct, metadata + ) + full_puzzle_empty_recovery = create_fullpuz(did_puzzle_empty_recovery, launch_id) + if full_puzzle.get_tree_hash() != coin_state.coin.puzzle_hash: + if full_puzzle_empty_recovery.get_tree_hash() == coin_state.coin.puzzle_hash: + did_puzzle = did_puzzle_empty_recovery + self.log.info("DID recovery list was reset by the previous owner.") else: - # Found unacknowledged CAT, save it in the database. - await self.interested_store.add_unacknowledged_token( - asset_id, - CATWallet.default_wallet_name_for_unknown_cat(asset_id.hex()), - parent_coin_state.spent_height, - parent_coin_state.coin.puzzle_hash, - ) - self.state_changed("added_stray_cat") + self.log.error("DID puzzle hash doesn't match, please check curried parameters.") + return None, None + # Create DID wallet + response: List[CoinState] = await self.wallet_node.get_coin_state([launch_id]) + if len(response) == 0: + self.log.warning(f"Could not find the launch coin with ID: {launch_id}") + return None, None + launch_coin: CoinState = response[0] + did_wallet = await DIDWallet.create_new_did_wallet_from_coin_spend( + self, self.main_wallet, launch_coin.coin, did_puzzle, coin_spend, f"DID {launch_id.hex()}" + ) + wallet_id = did_wallet.id() + wallet_type = WalletType(did_wallet.type()) + self.state_changed("wallet_created") + return wallet_id, wallet_type + + async def handle_nft( + self, coin_spend: CoinSpend, uncurried_nft: UncurriedNFT + ) -> Tuple[Optional[uint32], Optional[WalletType]]: + """ + Handle the new coin when it is a NFT + :param coin_spend: New coin spend + :param uncurried_nft: Uncurried NFT + :return: Wallet ID & Wallet Type + """ + wallet_id = None + wallet_type = None + + self.log.debug("Handling NFT: %s", coin_spend) + for wallet_info in await self.get_all_wallet_info_entries(): + if wallet_info.type == WalletType.NFT: + nft_wallet_info = NFTWalletInfo.from_json_dict(json.loads(wallet_info.data)) + self.log.debug( + "Checking NFT wallet %r and inner puzzle %s", + wallet_info.name, + uncurried_nft.inner_puzzle.get_tree_hash(), + ) + if not nft_wallet_info.did_wallet_id: + # standard NFT wallet + wallet_id = wallet_info.id + wallet_type = WalletType.NFT + break + if wallet_id is None: + # TODO Modify this for NFT1 + self.log.info("Cannot find a NFT wallet, creating a new one.") + nft_wallet: NFTWallet = await NFTWallet.create_new_nft_wallet( + self, self.main_wallet, name="NFT Wallet", in_transaction=True + ) + wallet_id = uint32(nft_wallet.wallet_id) + wallet_type = WalletType.NFT return wallet_id, wallet_type @@ -638,7 +785,7 @@ async def new_coin_state( wallet_id = uint32(local_record.wallet_id) wallet_type = local_record.wallet_type elif coin_state.created_height is not None: - wallet_id, wallet_type = await self.fetch_parent_and_check_for_cat(peer, coin_state, fork_height) + wallet_id, wallet_type = await self.determine_coin_type(peer, coin_state, fork_height) if wallet_id is None or wallet_type is None: self.log.info(f"No wallet for coin state: {coin_state}") @@ -660,8 +807,10 @@ async def new_coin_state( # TODO implements this coin got reorged # TODO: we need to potentially roll back the pool wallet here pass + # if the new coin has not been spent (i.e not ephemeral) elif coin_state.created_height is not None and coin_state.spent_height is None: await self.coin_added(coin_state.coin, coin_state.created_height, all_txs, wallet_id, wallet_type) + # if the coin has been spent elif coin_state.created_height is not None and coin_state.spent_height is not None: self.log.info(f"Coin Removed: {coin_state}") record = await self.coin_store.get_coin_record(coin_state.coin.name()) @@ -822,6 +971,11 @@ async def new_coin_state( assert len(new_coin_state) == 1 curr_coin_state = new_coin_state[0] + elif record.wallet_type == WalletType.NFT: + if coin_state.spent_height is not None: + nft_wallet = self.wallets[uint32(record.wallet_id)] + await nft_wallet.remove_coin(coin_state.coin, in_transaction=True) + # Check if a child is a singleton launcher if children is None: children = await self.wallet_node.fetch_children(peer, coin_state.coin.name(), fork_height) @@ -934,7 +1088,9 @@ async def coin_added( if existing is not None: return None - self.log.info(f"Adding coin: {coin} at {height} wallet_id:{wallet_id}") + self.log.info( + f"Adding record to state manager coin: {coin} at {height} wallet_id:{wallet_id} and type: {wallet_type}" + ) farmer_reward = False pool_reward = False if self.is_farmer_reward(height, coin.parent_coin_info): @@ -1019,6 +1175,9 @@ async def coin_added( wallet = self.wallets[wallet_id] await wallet.coin_added(coin, height) + if wallet_type == WalletType.NFT: + await self.wallets[wallet_id].add_nft_coin(coin, height, in_transaction=True) + await self.create_more_puzzle_hashes(in_transaction=True) return coin_record_1 diff --git a/mypy.ini b/mypy.ini index 391484c956b2..ba8b5a82fca7 100644 --- a/mypy.ini +++ b/mypy.ini @@ -17,7 +17,7 @@ no_implicit_reexport = True strict_equality = True # list created by: venv/bin/mypy | sed -n 's/.py:.*//p' | sort | uniq | tr '/' '.' | tr '\n' ',' -[mypy-benchmarks.block_ref,benchmarks.block_store,benchmarks.coin_store,benchmarks.utils,build_scripts.installer-version,chia.clvm.spend_sim,chia.cmds.configure,chia.cmds.db,chia.cmds.db_upgrade_func,chia.cmds.farm_funcs,chia.cmds.init,chia.cmds.init_funcs,chia.cmds.keys,chia.cmds.keys_funcs,chia.cmds.passphrase,chia.cmds.passphrase_funcs,chia.cmds.plotnft,chia.cmds.plotnft_funcs,chia.cmds.plots,chia.cmds.plotters,chia.cmds.show,chia.cmds.start_funcs,chia.cmds.wallet,chia.cmds.wallet_funcs,chia.daemon.client,chia.daemon.keychain_proxy,chia.daemon.keychain_server,chia.daemon.server,chia.farmer.farmer,chia.farmer.farmer_api,chia.full_node.block_height_map,chia.full_node.block_store,chia.full_node.bundle_tools,chia.full_node.coin_store,chia.full_node.full_node,chia.full_node.full_node_api,chia.full_node.full_node_store,chia.full_node.generator,chia.full_node.hint_store,chia.full_node.lock_queue,chia.full_node.mempool,chia.full_node.mempool_check_conditions,chia.full_node.mempool_manager,chia.full_node.pending_tx_cache,chia.full_node.sync_store,chia.full_node.weight_proof,chia.harvester.harvester,chia.harvester.harvester_api,chia.introducer.introducer,chia.introducer.introducer_api,chia.plotters.bladebit,chia.plotters.chiapos,chia.plotters.install_plotter,chia.plotters.madmax,chia.plotters.plotters,chia.plotters.plotters_util,chia.plotting.check_plots,chia.plotting.create_plots,chia.plotting.manager,chia.plotting.util,chia.pools.pool_config,chia.pools.pool_puzzles,chia.pools.pool_wallet,chia.pools.pool_wallet_info,chia.protocols.pool_protocol,chia.rpc.crawler_rpc_api,chia.rpc.farmer_rpc_api,chia.rpc.farmer_rpc_client,chia.rpc.full_node_rpc_api,chia.rpc.full_node_rpc_client,chia.rpc.harvester_rpc_api,chia.rpc.harvester_rpc_client,chia.rpc.rpc_client,chia.rpc.rpc_server,chia.rpc.timelord_rpc_api,chia.rpc.util,chia.rpc.wallet_rpc_api,chia.rpc.wallet_rpc_client,chia.seeder.crawler,chia.seeder.crawler_api,chia.seeder.crawl_store,chia.seeder.dns_server,chia.seeder.peer_record,chia.seeder.start_crawler,chia.server.address_manager,chia.server.address_manager_store,chia.server.connection_utils,chia.server.introducer_peers,chia.server.node_discovery,chia.server.peer_store_resolver,chia.server.rate_limits,chia.server.reconnect_task,chia.server.server,chia.server.ssl_context,chia.server.start_farmer,chia.server.start_full_node,chia.server.start_harvester,chia.server.start_introducer,chia.server.start_service,chia.server.start_timelord,chia.server.start_wallet,chia.server.upnp,chia.server.ws_connection,chia.simulator.full_node_simulator,chia.simulator.start_simulator,chia.ssl.create_ssl,chia.timelord.iters_from_block,chia.timelord.timelord,chia.timelord.timelord_api,chia.timelord.timelord_launcher,chia.timelord.timelord_state,chia.types.announcement,chia.types.blockchain_format.classgroup,chia.types.blockchain_format.coin,chia.types.blockchain_format.program,chia.types.blockchain_format.proof_of_space,chia.types.blockchain_format.tree_hash,chia.types.blockchain_format.vdf,chia.types.full_block,chia.types.header_block,chia.types.mempool_item,chia.types.name_puzzle_condition,chia.types.peer_info,chia.types.spend_bundle,chia.types.transaction_queue_entry,chia.types.unfinished_block,chia.types.unfinished_header_block,chia.util.api_decorators,chia.util.block_cache,chia.util.cached_bls,chia.util.check_fork_next_block,chia.util.chia_logging,chia.util.config,chia.util.db_wrapper,chia.util.dump_keyring,chia.util.file_keyring,chia.util.files,chia.util.hash,chia.util.json_util,chia.util.keychain,chia.util.keyring_wrapper,chia.util.log_exceptions,chia.util.lru_cache,chia.util.make_test_constants,chia.util.merkle_set,chia.util.network,chia.util.partial_func,chia.util.pip_import,chia.util.profiler,chia.util.safe_cancel_task,chia.util.service_groups,chia.util.ssl_check,chia.util.validate_alert,chia.wallet.block_record,chia.wallet.cat_wallet.cat_utils,chia.wallet.cat_wallet.cat_wallet,chia.wallet.cat_wallet.lineage_store,chia.wallet.chialisp,chia.wallet.did_wallet.did_wallet,chia.wallet.did_wallet.did_wallet_puzzles,chia.wallet.key_val_store,chia.wallet.lineage_proof,chia.wallet.payment,chia.wallet.puzzles.load_clvm,chia.wallet.puzzles.p2_conditions,chia.wallet.puzzles.p2_delegated_conditions,chia.wallet.puzzles.p2_delegated_puzzle,chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle,chia.wallet.puzzles.p2_m_of_n_delegate_direct,chia.wallet.puzzles.p2_puzzle_hash,chia.wallet.puzzles.prefarm.spend_prefarm,chia.wallet.puzzles.puzzle_utils,chia.wallet.puzzles.rom_bootstrap_generator,chia.wallet.puzzles.singleton_top_layer,chia.wallet.puzzles.tails,chia.wallet.rl_wallet.rl_wallet,chia.wallet.rl_wallet.rl_wallet_puzzles,chia.wallet.secret_key_store,chia.wallet.settings.user_settings,chia.wallet.trade_manager,chia.wallet.trade_record,chia.wallet.trading.offer,chia.wallet.trading.trade_store,chia.wallet.transaction_record,chia.wallet.util.debug_spend_bundle,chia.wallet.util.new_peak_queue,chia.wallet.util.peer_request_cache,chia.wallet.util.wallet_sync_utils,chia.wallet.wallet,chia.wallet.wallet_action_store,chia.wallet.wallet_blockchain,chia.wallet.wallet_coin_store,chia.wallet.wallet_interested_store,chia.wallet.wallet_node,chia.wallet.wallet_node_api,chia.wallet.wallet_pool_store,chia.wallet.wallet_puzzle_store,chia.wallet.wallet_state_manager,chia.wallet.wallet_sync_store,chia.wallet.wallet_transaction_store,chia.wallet.wallet_user_store,chia.wallet.wallet_weight_proof_handler,installhelper,tests.blockchain.blockchain_test_utils,tests.blockchain.test_blockchain,tests.blockchain.test_blockchain_transactions,tests.block_tools,tests.build-init-files,tests.build-workflows,tests.clvm.coin_store,tests.clvm.test_chialisp_deserialization,tests.clvm.test_clvm_compilation,tests.clvm.test_program,tests.clvm.test_puzzle_compression,tests.clvm.test_puzzles,tests.clvm.test_serialized_program,tests.clvm.test_singletons,tests.clvm.test_spend_sim,tests.conftest,tests.connection_utils,tests.core.cmds.test_keys,tests.core.consensus.test_pot_iterations,tests.core.custom_types.test_coin,tests.core.custom_types.test_proof_of_space,tests.core.custom_types.test_spend_bundle,tests.core.daemon.test_daemon,tests.core.full_node.full_sync.test_full_sync,tests.core.full_node.stores.test_block_store,tests.core.full_node.stores.test_coin_store,tests.core.full_node.stores.test_full_node_store,tests.core.full_node.stores.test_hint_store,tests.core.full_node.stores.test_sync_store,tests.core.full_node.test_address_manager,tests.core.full_node.test_block_height_map,tests.core.full_node.test_conditions,tests.core.full_node.test_full_node,tests.core.full_node.test_mempool,tests.core.full_node.test_mempool_performance,tests.core.full_node.test_node_load,tests.core.full_node.test_peer_store_resolver,tests.core.full_node.test_performance,tests.core.full_node.test_transactions,tests.core.make_block_generator,tests.core.node_height,tests.core.server.test_dos,tests.core.server.test_rate_limits,tests.core.ssl.test_ssl,tests.core.test_cost_calculation,tests.core.test_crawler_rpc,tests.core.test_daemon_rpc,tests.core.test_db_conversion,tests.core.test_db_validation,tests.core.test_farmer_harvester_rpc,tests.core.test_filter,tests.core.test_full_node_rpc,tests.core.test_merkle_set,tests.core.test_setproctitle,tests.core.util.test_cached_bls,tests.core.util.test_config,tests.core.util.test_file_keyring_synchronization,tests.core.util.test_files,tests.core.util.test_keychain,tests.core.util.test_keyring_wrapper,tests.core.util.test_lru_cache,tests.core.util.test_significant_bits,tests.farmer_harvester.test_farmer_harvester,tests.generator.test_compression,tests.generator.test_generator_types,tests.generator.test_list_to_batches,tests.generator.test_rom,tests.generator.test_scan,tests.plotting.test_plot_manager,tests.pools.test_pool_cmdline,tests.pools.test_pool_config,tests.pools.test_pool_puzzles_lifecycle,tests.pools.test_pool_rpc,tests.pools.test_wallet_pool_store,tests.setup_nodes,tests.setup_services,tests.simulation.test_simulation,tests.time_out_assert,tests.tools.test_full_sync,tests.tools.test_run_block,tests.util.alert_server,tests.util.benchmark_cost,tests.util.blockchain,tests.util.build_network_protocol_files,tests.util.db_connection,tests.util.generator_tools_testing,tests.util.keyring,tests.util.key_tool,tests.util.rpc,tests.util.test_full_block_utils,tests.util.test_lock_queue,tests.util.test_misc,tests.util.test_network,tests.util.test_network_protocol_files,tests.wallet.cat_wallet.test_cat_lifecycle,tests.wallet.cat_wallet.test_cat_wallet,tests.wallet.cat_wallet.test_offer_lifecycle,tests.wallet.cat_wallet.test_trades,tests.wallet.did_wallet.test_did,tests.wallet.did_wallet.test_did_rpc,tests.wallet.rl_wallet.test_rl_rpc,tests.wallet.rl_wallet.test_rl_wallet,tests.wallet.rpc.test_wallet_rpc,tests.wallet.simple_sync.test_simple_sync_protocol,tests.wallet.sync.test_wallet_sync,tests.wallet.test_bech32m,tests.wallet.test_chialisp,tests.wallet.test_puzzle_store,tests.wallet.test_singleton,tests.wallet.test_singleton_lifecycle,tests.wallet.test_singleton_lifecycle_fast,tests.wallet.test_taproot,tests.wallet.test_wallet_blockchain,tests.wallet.test_wallet_interested_store,tests.wallet.test_wallet_key_val_store,tests.wallet.test_wallet_user_store,tests.wallet_tools,tests.weight_proof.test_weight_proof,tools.analyze-chain,tools.run_block,tools.test_full_sync] +[mypy-benchmarks.block_ref,benchmarks.block_store,benchmarks.coin_store,benchmarks.utils,build_scripts.installer-version,chia.clvm.spend_sim,chia.cmds.configure,chia.cmds.db,chia.cmds.db_upgrade_func,chia.cmds.farm_funcs,chia.cmds.init,chia.cmds.init_funcs,chia.cmds.keys,chia.cmds.keys_funcs,chia.cmds.passphrase,chia.cmds.passphrase_funcs,chia.cmds.plotnft,chia.cmds.plotnft_funcs,chia.cmds.plots,chia.cmds.plotters,chia.cmds.show,chia.cmds.start_funcs,chia.cmds.wallet,chia.cmds.wallet_funcs,chia.daemon.client,chia.daemon.keychain_proxy,chia.daemon.keychain_server,chia.daemon.server,chia.farmer.farmer,chia.farmer.farmer_api,chia.full_node.block_height_map,chia.full_node.block_store,chia.full_node.bundle_tools,chia.full_node.coin_store,chia.full_node.full_node,chia.full_node.full_node_api,chia.full_node.full_node_store,chia.full_node.generator,chia.full_node.hint_store,chia.full_node.lock_queue,chia.full_node.mempool,chia.full_node.mempool_check_conditions,chia.full_node.mempool_manager,chia.full_node.pending_tx_cache,chia.full_node.sync_store,chia.full_node.weight_proof,chia.harvester.harvester,chia.harvester.harvester_api,chia.introducer.introducer,chia.introducer.introducer_api,chia.plotters.bladebit,chia.plotters.chiapos,chia.plotters.install_plotter,chia.plotters.madmax,chia.plotters.plotters,chia.plotters.plotters_util,chia.plotting.check_plots,chia.plotting.create_plots,chia.plotting.manager,chia.plotting.util,chia.pools.pool_config,chia.pools.pool_puzzles,chia.pools.pool_wallet,chia.pools.pool_wallet_info,chia.protocols.pool_protocol,chia.rpc.crawler_rpc_api,chia.rpc.farmer_rpc_api,chia.rpc.farmer_rpc_client,chia.rpc.full_node_rpc_api,chia.rpc.full_node_rpc_client,chia.rpc.harvester_rpc_api,chia.rpc.harvester_rpc_client,chia.rpc.rpc_client,chia.rpc.rpc_server,chia.rpc.timelord_rpc_api,chia.rpc.util,chia.rpc.wallet_rpc_api,chia.rpc.wallet_rpc_client,chia.seeder.crawler,chia.seeder.crawler_api,chia.seeder.crawl_store,chia.seeder.dns_server,chia.seeder.peer_record,chia.seeder.start_crawler,chia.server.address_manager,chia.server.address_manager_store,chia.server.connection_utils,chia.server.introducer_peers,chia.server.node_discovery,chia.server.peer_store_resolver,chia.server.rate_limits,chia.server.reconnect_task,chia.server.server,chia.server.ssl_context,chia.server.start_farmer,chia.server.start_full_node,chia.server.start_harvester,chia.server.start_introducer,chia.server.start_service,chia.server.start_timelord,chia.server.start_wallet,chia.server.upnp,chia.server.ws_connection,chia.simulator.full_node_simulator,chia.simulator.start_simulator,chia.ssl.create_ssl,chia.timelord.iters_from_block,chia.timelord.timelord,chia.timelord.timelord_api,chia.timelord.timelord_launcher,chia.timelord.timelord_state,chia.types.announcement,chia.types.blockchain_format.classgroup,chia.types.blockchain_format.coin,chia.types.blockchain_format.program,chia.types.blockchain_format.proof_of_space,chia.types.blockchain_format.tree_hash,chia.types.blockchain_format.vdf,chia.types.full_block,chia.types.header_block,chia.types.mempool_item,chia.types.name_puzzle_condition,chia.types.peer_info,chia.types.spend_bundle,chia.types.transaction_queue_entry,chia.types.unfinished_block,chia.types.unfinished_header_block,chia.util.api_decorators,chia.util.block_cache,chia.util.cached_bls,chia.util.check_fork_next_block,chia.util.chia_logging,chia.util.config,chia.util.db_wrapper,chia.util.dump_keyring,chia.util.file_keyring,chia.util.files,chia.util.hash,chia.util.json_util,chia.util.keychain,chia.util.keyring_wrapper,chia.util.log_exceptions,chia.util.lru_cache,chia.util.make_test_constants,chia.util.merkle_set,chia.util.network,chia.util.partial_func,chia.util.pip_import,chia.util.profiler,chia.util.safe_cancel_task,chia.util.service_groups,chia.util.ssl_check,chia.util.validate_alert,chia.wallet.block_record,chia.wallet.cat_wallet.cat_utils,chia.wallet.cat_wallet.cat_wallet,chia.wallet.cat_wallet.lineage_store,chia.wallet.chialisp,chia.wallet.did_wallet.did_wallet,chia.wallet.did_wallet.did_wallet_puzzles,chia.wallet.key_val_store,chia.wallet.lineage_proof,chia.wallet.nft_wallet.nft_wallet,chia.wallet.payment,chia.wallet.puzzles.load_clvm,chia.wallet.puzzles.p2_conditions,chia.wallet.puzzles.p2_delegated_conditions,chia.wallet.puzzles.p2_delegated_puzzle,chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle,chia.wallet.puzzles.p2_m_of_n_delegate_direct,chia.wallet.puzzles.p2_puzzle_hash,chia.wallet.puzzles.prefarm.spend_prefarm,chia.wallet.puzzles.puzzle_utils,chia.wallet.puzzles.rom_bootstrap_generator,chia.wallet.puzzles.singleton_top_layer,chia.wallet.puzzles.tails,chia.wallet.rl_wallet.rl_wallet,chia.wallet.rl_wallet.rl_wallet_puzzles,chia.wallet.secret_key_store,chia.wallet.settings.user_settings,chia.wallet.trade_manager,chia.wallet.trade_record,chia.wallet.trading.offer,chia.wallet.trading.trade_store,chia.wallet.transaction_record,chia.wallet.util.debug_spend_bundle,chia.wallet.util.new_peak_queue,chia.wallet.util.peer_request_cache,chia.wallet.util.wallet_sync_utils,chia.wallet.wallet,chia.wallet.wallet_action_store,chia.wallet.wallet_blockchain,chia.wallet.wallet_coin_store,chia.wallet.wallet_interested_store,chia.wallet.wallet_node,chia.wallet.wallet_node_api,chia.wallet.wallet_pool_store,chia.wallet.wallet_puzzle_store,chia.wallet.wallet_state_manager,chia.wallet.wallet_sync_store,chia.wallet.wallet_transaction_store,chia.wallet.wallet_user_store,chia.wallet.wallet_weight_proof_handler,installhelper,tests.blockchain.blockchain_test_utils,tests.blockchain.test_blockchain,tests.blockchain.test_blockchain_transactions,tests.block_tools,tests.build-init-files,tests.build-workflows,tests.clvm.coin_store,tests.clvm.test_chialisp_deserialization,tests.clvm.test_clvm_compilation,tests.clvm.test_program,tests.clvm.test_puzzle_compression,tests.clvm.test_puzzles,tests.clvm.test_serialized_program,tests.clvm.test_singletons,tests.clvm.test_spend_sim,tests.conftest,tests.connection_utils,tests.core.cmds.test_keys,tests.core.consensus.test_pot_iterations,tests.core.custom_types.test_coin,tests.core.custom_types.test_proof_of_space,tests.core.custom_types.test_spend_bundle,tests.core.daemon.test_daemon,tests.core.full_node.full_sync.test_full_sync,tests.core.full_node.stores.test_block_store,tests.core.full_node.stores.test_coin_store,tests.core.full_node.stores.test_full_node_store,tests.core.full_node.stores.test_hint_store,tests.core.full_node.stores.test_sync_store,tests.core.full_node.test_address_manager,tests.core.full_node.test_block_height_map,tests.core.full_node.test_conditions,tests.core.full_node.test_full_node,tests.core.full_node.test_mempool,tests.core.full_node.test_mempool_performance,tests.core.full_node.test_node_load,tests.core.full_node.test_peer_store_resolver,tests.core.full_node.test_performance,tests.core.full_node.test_transactions,tests.core.make_block_generator,tests.core.node_height,tests.core.server.test_dos,tests.core.server.test_rate_limits,tests.core.ssl.test_ssl,tests.core.test_cost_calculation,tests.core.test_crawler_rpc,tests.core.test_daemon_rpc,tests.core.test_db_conversion,tests.core.test_db_validation,tests.core.test_farmer_harvester_rpc,tests.core.test_filter,tests.core.test_full_node_rpc,tests.core.test_merkle_set,tests.core.test_setproctitle,tests.core.util.test_cached_bls,tests.core.util.test_config,tests.core.util.test_file_keyring_synchronization,tests.core.util.test_files,tests.core.util.test_keychain,tests.core.util.test_keyring_wrapper,tests.core.util.test_lru_cache,tests.core.util.test_significant_bits,tests.farmer_harvester.test_farmer_harvester,tests.generator.test_compression,tests.generator.test_generator_types,tests.generator.test_list_to_batches,tests.generator.test_rom,tests.generator.test_scan,tests.plotting.test_plot_manager,tests.pools.test_pool_cmdline,tests.pools.test_pool_config,tests.pools.test_pool_puzzles_lifecycle,tests.pools.test_pool_rpc,tests.pools.test_wallet_pool_store,tests.setup_nodes,tests.setup_services,tests.simulation.test_simulation,tests.time_out_assert,tests.tools.test_full_sync,tests.tools.test_run_block,tests.util.alert_server,tests.util.benchmark_cost,tests.util.blockchain,tests.util.build_network_protocol_files,tests.util.db_connection,tests.util.generator_tools_testing,tests.util.keyring,tests.util.key_tool,tests.util.rpc,tests.util.test_full_block_utils,tests.util.test_lock_queue,tests.util.test_misc,tests.util.test_network,tests.util.test_network_protocol_files,tests.wallet.cat_wallet.test_cat_lifecycle,tests.wallet.cat_wallet.test_cat_wallet,tests.wallet.cat_wallet.test_offer_lifecycle,tests.wallet.cat_wallet.test_trades,tests.wallet.did_wallet.test_did,tests.wallet.did_wallet.test_did_rpc,tests.wallet.did_wallet.test_nft_rpc,tests.wallet.did_wallet.test_nft_wallet,tests.wallet.rl_wallet.test_rl_rpc,tests.wallet.rl_wallet.test_rl_wallet,tests.wallet.rpc.test_wallet_rpc,tests.wallet.simple_sync.test_simple_sync_protocol,tests.wallet.sync.test_wallet_sync,tests.wallet.test_bech32m,tests.wallet.test_chialisp,tests.wallet.test_puzzle_store,tests.wallet.test_singleton,tests.wallet.test_singleton_lifecycle,tests.wallet.test_singleton_lifecycle_fast,tests.wallet.test_taproot,tests.wallet.test_wallet_blockchain,tests.wallet.test_wallet_interested_store,tests.wallet.test_wallet_key_val_store,tests.wallet.test_wallet_user_store,tests.wallet_tools,tests.weight_proof.test_weight_proof,tools.analyze-chain,tools.run_block,tools.test_full_sync] disallow_any_generics = False disallow_subclassing_any = False disallow_untyped_calls = False diff --git a/setup.py b/setup.py index 6538e5ff470e..cdf00cdfb1df 100644 --- a/setup.py +++ b/setup.py @@ -109,6 +109,7 @@ "chia.wallet.rl_wallet", "chia.wallet.cat_wallet", "chia.wallet.did_wallet", + "chia.wallet.nft_wallet", "chia.wallet.settings", "chia.wallet.trading", "chia.wallet.util", diff --git a/tests/clvm/test_clvm_compilation.py b/tests/clvm/test_clvm_compilation.py index b3d052176724..f758c5092aa7 100644 --- a/tests/clvm/test_clvm_compilation.py +++ b/tests/clvm/test_clvm_compilation.py @@ -40,6 +40,13 @@ "chia/wallet/puzzles/delegated_tail.clvm", "chia/wallet/puzzles/settlement_payments.clvm", "chia/wallet/puzzles/genesis_by_coin_id.clvm", + "chia/wallet/puzzles/singleton_top_layer_v1_1.clvm", + "chia/wallet/puzzles/nft_innerpuz.clvm", + "chia/wallet/puzzles/nft_transfer_program.clvm", + "chia/wallet/puzzles/nft_ownership_transfer_program.clvm", + "chia/wallet/puzzles/nft_metadata_updater.clvm", + "chia/wallet/puzzles/nft_metadata_updater_default.clvm", + "chia/wallet/puzzles/nft_state_layer.clvm", ] ) diff --git a/tests/wallet/did_wallet/config.py b/tests/wallet/did_wallet/config.py new file mode 100644 index 000000000000..671807f010ec --- /dev/null +++ b/tests/wallet/did_wallet/config.py @@ -0,0 +1,3 @@ +# flake8: noqa: E501 +job_timeout = 50 +checkout_blocks_and_plots = True diff --git a/tests/wallet/did_wallet/test_did.py b/tests/wallet/did_wallet/test_did.py index 221a194583c0..03d4b0ba99db 100644 --- a/tests/wallet/did_wallet/test_did.py +++ b/tests/wallet/did_wallet/test_did.py @@ -1,3 +1,6 @@ +import json +from typing import Optional + import pytest from blspy import AugSchemeMPL @@ -7,10 +10,16 @@ from chia.types.peer_info import PeerInfo from chia.types.spend_bundle import SpendBundle from chia.util.ints import uint16, uint32, uint64 + +from chia.wallet.util.wallet_types import WalletType from chia.wallet.did_wallet.did_wallet import DIDWallet from tests.time_out_assert import time_out_assert, time_out_assert_not_none -pytestmark = pytest.mark.skip("TODO: Fix tests") +# pytestmark = pytest.mark.skip("TODO: Fix tests") + + +async def get_wallet_num(wallet_manager): + return len(await wallet_manager.get_all_wallet_info_entries()) class TestDIDWallet: @@ -58,6 +67,11 @@ async def test_creation_from_backup_file(self, self_hostname, three_wallet_nodes wallet_node_0.wallet_state_manager, wallet_0, uint64(101) ) + spend_bundle_list = await wallet_node_0.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(wallet_0.id()) + + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) + for i in range(1, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) @@ -72,6 +86,11 @@ async def test_creation_from_backup_file(self, self_hostname, three_wallet_nodes wallet_node_1.wallet_state_manager, wallet_1, uint64(201), backup_ids ) + spend_bundle_list = await wallet_node_1.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(wallet_1.id()) + + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) + for i in range(1, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) @@ -79,24 +98,29 @@ async def test_creation_from_backup_file(self, self_hostname, three_wallet_nodes await time_out_assert(15, did_wallet_1.get_unconfirmed_balance, 201) await time_out_assert(15, did_wallet_1.get_pending_change_balance, 0) - filename = "test.backup" - did_wallet_1.create_backup(filename) + backup_data = did_wallet_1.create_backup() # Wallet2 recovers DIDWallet2 to a new set of keys async with wallet_node_2.wallet_state_manager.lock: did_wallet_2 = await DIDWallet.create_new_did_wallet_from_recovery( - wallet_node_2.wallet_state_manager, wallet_2, filename + wallet_node_2.wallet_state_manager, wallet_2, backup_data ) coins = await did_wallet_1.select_coins(1) coin = coins.copy().pop() assert did_wallet_2.did_info.temp_coin == coin - newpuzhash = await did_wallet_2.get_new_inner_hash() + newpuzhash = await did_wallet_2.get_new_did_inner_hash() pubkey = bytes( (await did_wallet_2.wallet_state_manager.get_unused_derivation_record(did_wallet_2.wallet_info.id)).pubkey ) - message_spend_bundle = await did_wallet_0.create_attestment( - did_wallet_2.did_info.temp_coin.name(), newpuzhash, pubkey, "test.attest" + message_spend_bundle, attest_data = await did_wallet_0.create_attestment( + did_wallet_2.did_info.temp_coin.name(), newpuzhash, pubkey + ) + spend_bundle_list = await wallet_node_0.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( + did_wallet_0.id() ) + + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) print(f"pubkey: {pubkey}") for i in range(1, num_blocks): @@ -105,10 +129,10 @@ async def test_creation_from_backup_file(self, self_hostname, three_wallet_nodes ( test_info_list, test_message_spend_bundle, - ) = await did_wallet_2.load_attest_files_for_recovery_spend(["test.attest"]) + ) = await did_wallet_2.load_attest_files_for_recovery_spend([attest_data]) assert message_spend_bundle == test_message_spend_bundle - await did_wallet_2.recovery_spend( + spend_bundle = await did_wallet_2.recovery_spend( did_wallet_2.did_info.temp_coin, newpuzhash, test_info_list, @@ -117,6 +141,8 @@ async def test_creation_from_backup_file(self, self_hostname, three_wallet_nodes ) print(f"pubkey: {did_wallet_2}") + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) + for i in range(1, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) @@ -126,6 +152,13 @@ async def test_creation_from_backup_file(self, self_hostname, three_wallet_nodes some_ph = 32 * b"\2" await did_wallet_2.create_exit_spend(some_ph) + spend_bundle_list = await wallet_node_2.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( + did_wallet_2.id() + ) + + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) + for i in range(1, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) @@ -143,8 +176,8 @@ async def get_coins_with_ph(): async def test_did_recovery_with_multiple_backup_dids(self, self_hostname, two_wallet_nodes): num_blocks = 5 full_nodes, wallets = two_wallet_nodes - full_node_1 = full_nodes[0] - server_1 = full_node_1.server + full_node_api = full_nodes[0] + server_1 = full_node_api.server wallet_node, server_2 = wallets[0] wallet_node_2, server_3 = wallets[1] wallet = wallet_node.wallet_state_manager.main_wallet @@ -156,7 +189,7 @@ async def test_did_recovery_with_multiple_backup_dids(self, self_hostname, two_w await server_3.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) for i in range(1, num_blocks): - await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) funds = sum( [ @@ -171,10 +204,15 @@ async def test_did_recovery_with_multiple_backup_dids(self, self_hostname, two_w did_wallet: DIDWallet = await DIDWallet.create_new_did_wallet( wallet_node.wallet_state_manager, wallet, uint64(101) ) + assert did_wallet.wallet_info.name == "Profile 1" + spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(wallet.id()) + + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) ph = await wallet2.get_new_puzzlehash() for i in range(1, num_blocks): - await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) await time_out_assert(15, did_wallet.get_confirmed_balance, 101) await time_out_assert(15, did_wallet.get_unconfirmed_balance, 101) @@ -186,8 +224,13 @@ async def test_did_recovery_with_multiple_backup_dids(self, self_hostname, two_w wallet_node_2.wallet_state_manager, wallet2, uint64(101), recovery_list ) + spend_bundle_list = await wallet_node_2.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(wallet2.id()) + + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) + for i in range(1, num_blocks): - await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) await time_out_assert(15, did_wallet_2.get_confirmed_balance, 101) await time_out_assert(15, did_wallet_2.get_unconfirmed_balance, 101) @@ -201,9 +244,14 @@ async def test_did_recovery_with_multiple_backup_dids(self, self_hostname, two_w wallet_node_2.wallet_state_manager, wallet2, uint64(201), recovery_list ) + spend_bundle_list = await wallet_node_2.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(wallet2.id()) + + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) + ph2 = await wallet.get_new_puzzlehash() for i in range(1, num_blocks): - await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) assert did_wallet_3.did_info.backup_ids == recovery_list await time_out_assert(15, did_wallet_3.get_confirmed_balance, 201) @@ -211,36 +259,52 @@ async def test_did_recovery_with_multiple_backup_dids(self, self_hostname, two_w coins = await did_wallet_3.select_coins(1) coin = coins.pop() - filename = "test.backup" - did_wallet_3.create_backup(filename) + backup_data = did_wallet_3.create_backup() async with wallet_node.wallet_state_manager.lock: did_wallet_4 = await DIDWallet.create_new_did_wallet_from_recovery( wallet_node.wallet_state_manager, wallet, - filename, + backup_data, ) + assert did_wallet_4.wallet_info.name == "Profile 2" pubkey = ( await did_wallet_4.wallet_state_manager.get_unused_derivation_record(did_wallet_2.wallet_info.id) ).pubkey - new_ph = await did_wallet_4.get_new_inner_hash() - message_spend_bundle = await did_wallet.create_attestment(coin.name(), new_ph, pubkey, "test1.attest") - message_spend_bundle2 = await did_wallet_2.create_attestment(coin.name(), new_ph, pubkey, "test2.attest") + new_ph = await did_wallet_4.get_new_did_inner_hash() + message_spend_bundle, attest1 = await did_wallet.create_attestment(coin.name(), new_ph, pubkey) + spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(did_wallet.id()) + + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) + message_spend_bundle2, attest2 = await did_wallet_2.create_attestment(coin.name(), new_ph, pubkey) + spend_bundle_list = await wallet_node_2.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( + did_wallet_2.id() + ) + + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) message_spend_bundle = message_spend_bundle.aggregate([message_spend_bundle, message_spend_bundle2]) ( test_info_list, test_message_spend_bundle, - ) = await did_wallet_4.load_attest_files_for_recovery_spend(["test1.attest", "test2.attest"]) + ) = await did_wallet_4.load_attest_files_for_recovery_spend([attest1, attest2]) assert message_spend_bundle == test_message_spend_bundle for i in range(1, num_blocks): - await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) await did_wallet_4.recovery_spend(coin, new_ph, test_info_list, pubkey, message_spend_bundle) + spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( + did_wallet_4.id() + ) + + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) for i in range(1, num_blocks): - await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) await time_out_assert(15, did_wallet_4.get_confirmed_balance, 201) await time_out_assert(15, did_wallet_4.get_unconfirmed_balance, 201) @@ -251,8 +315,8 @@ async def test_did_recovery_with_multiple_backup_dids(self, self_hostname, two_w async def test_did_recovery_with_empty_set(self, self_hostname, two_wallet_nodes): num_blocks = 5 full_nodes, wallets = two_wallet_nodes - full_node_1 = full_nodes[0] - server_1 = full_node_1.server + full_node_api = full_nodes[0] + server_1 = full_node_api.server wallet_node, server_2 = wallets[0] wallet_node_2, server_3 = wallets[1] wallet = wallet_node.wallet_state_manager.main_wallet @@ -263,7 +327,7 @@ async def test_did_recovery_with_empty_set(self, self_hostname, two_wallet_nodes await server_3.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) for i in range(1, num_blocks): - await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) funds = sum( [ @@ -279,27 +343,37 @@ async def test_did_recovery_with_empty_set(self, self_hostname, two_wallet_nodes wallet_node.wallet_state_manager, wallet, uint64(101) ) + spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(wallet.id()) + + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) + for i in range(1, num_blocks): - await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) await time_out_assert(15, did_wallet.get_confirmed_balance, 101) await time_out_assert(15, did_wallet.get_unconfirmed_balance, 101) coins = await did_wallet.select_coins(1) coin = coins.pop() + print(f"Old coin id {coin.name()}") info = Program.to([]) pubkey = (await did_wallet.wallet_state_manager.get_unused_derivation_record(did_wallet.wallet_info.id)).pubkey - spend_bundle = await did_wallet.recovery_spend( - coin, ph, info, pubkey, SpendBundle([], AugSchemeMPL.aggregate([])) - ) - additions = spend_bundle.additions() - assert additions == [] + try: + spend_bundle = await did_wallet.recovery_spend( + coin, ph, info, pubkey, SpendBundle([], AugSchemeMPL.aggregate([])) + ) + except Exception: + # We expect a CLVM 80 error for this test + pass + else: + assert False @pytest.mark.asyncio async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes): num_blocks = 5 full_nodes, wallets = two_wallet_nodes - full_node_1 = full_nodes[0] - server_1 = full_node_1.server + full_node_api = full_nodes[0] + server_1 = full_node_api.server wallet_node, server_2 = wallets[0] wallet_node_2, server_3 = wallets[1] wallet = wallet_node.wallet_state_manager.main_wallet @@ -309,7 +383,7 @@ async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes): await server_2.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) await server_3.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) for i in range(1, num_blocks): - await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) funds = sum( [ @@ -324,10 +398,13 @@ async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes): did_wallet: DIDWallet = await DIDWallet.create_new_did_wallet( wallet_node.wallet_state_manager, wallet, uint64(101) ) + spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(wallet.id()) + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) ph2 = await wallet2.get_new_puzzlehash() for i in range(1, num_blocks): - await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) await time_out_assert(15, did_wallet.get_confirmed_balance, 101) await time_out_assert(15, did_wallet.get_unconfirmed_balance, 101) @@ -337,11 +414,15 @@ async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes): did_wallet_2: DIDWallet = await DIDWallet.create_new_did_wallet( wallet_node_2.wallet_state_manager, wallet2, uint64(101), recovery_list ) + spend_bundle_list = await wallet_node_2.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(wallet2.id()) + + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) ph = await wallet.get_new_puzzlehash() for i in range(1, num_blocks): - await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph)) - await time_out_assert(15, did_wallet_2.get_confirmed_balance, 101) - await time_out_assert(15, did_wallet_2.get_unconfirmed_balance, 101) + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + await time_out_assert(25, did_wallet_2.get_confirmed_balance, 101) + await time_out_assert(25, did_wallet_2.get_unconfirmed_balance, 101) assert did_wallet_2.did_info.backup_ids == recovery_list # Update coin with new ID info @@ -350,77 +431,297 @@ async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes): assert did_wallet.did_info.backup_ids == recovery_list await did_wallet.create_update_spend() + spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(did_wallet.id()) + + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) + for i in range(1, num_blocks): - await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) await time_out_assert(15, did_wallet.get_confirmed_balance, 101) await time_out_assert(15, did_wallet.get_unconfirmed_balance, 101) # DID Wallet 2 recovers into DID Wallet 3 with new innerpuz - filename = "test.backup" - did_wallet_2.create_backup(filename) + backup_data = did_wallet_2.create_backup() async with wallet_node.wallet_state_manager.lock: did_wallet_3 = await DIDWallet.create_new_did_wallet_from_recovery( wallet_node.wallet_state_manager, wallet, - filename, + backup_data, ) - new_ph = await did_wallet_3.get_new_inner_hash() + new_ph = await did_wallet_3.get_new_did_inner_hash() coins = await did_wallet_2.select_coins(1) coin = coins.pop() pubkey = ( await did_wallet_3.wallet_state_manager.get_unused_derivation_record(did_wallet_3.wallet_info.id) ).pubkey - message_spend_bundle = await did_wallet.create_attestment(coin.name(), new_ph, pubkey, "test.attest") + attest_data = (await did_wallet.create_attestment(coin.name(), new_ph, pubkey))[1] + spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(did_wallet.id()) + + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) for i in range(1, num_blocks): - await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) ( info, message_spend_bundle, - ) = await did_wallet_3.load_attest_files_for_recovery_spend(["test.attest"]) + ) = await did_wallet_3.load_attest_files_for_recovery_spend([attest_data]) await did_wallet_3.recovery_spend(coin, new_ph, info, pubkey, message_spend_bundle) + spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( + did_wallet_3.id() + ) + + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) for i in range(1, num_blocks): - await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) await time_out_assert(15, did_wallet_3.get_confirmed_balance, 101) await time_out_assert(15, did_wallet_3.get_unconfirmed_balance, 101) # DID Wallet 1 recovery spends into DID Wallet 4 - filename = "test.backup" - did_wallet.create_backup(filename) + backup_data = did_wallet.create_backup() async with wallet_node_2.wallet_state_manager.lock: did_wallet_4 = await DIDWallet.create_new_did_wallet_from_recovery( wallet_node_2.wallet_state_manager, wallet2, - filename, + backup_data, ) coins = await did_wallet.select_coins(1) coin = coins.pop() - new_ph = await did_wallet_4.get_new_inner_hash() + new_ph = await did_wallet_4.get_new_did_inner_hash() pubkey = ( await did_wallet_4.wallet_state_manager.get_unused_derivation_record(did_wallet_4.wallet_info.id) ).pubkey - await did_wallet_3.create_attestment(coin.name(), new_ph, pubkey, "test.attest") + attest1 = (await did_wallet_3.create_attestment(coin.name(), new_ph, pubkey))[1] + spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( + did_wallet_3.id() + ) + + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) for i in range(1, num_blocks): - await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) ( test_info_list, test_message_spend_bundle, - ) = await did_wallet_4.load_attest_files_for_recovery_spend(["test.attest"]) - spend_bundle = await did_wallet_4.recovery_spend( - coin, new_ph, test_info_list, pubkey, test_message_spend_bundle + ) = await did_wallet_4.load_attest_files_for_recovery_spend([attest1]) + await did_wallet_4.recovery_spend(coin, new_ph, test_info_list, pubkey, test_message_spend_bundle) + + spend_bundle_list = await wallet_node_2.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( + did_wallet_4.id() ) - await time_out_assert_not_none(15, full_node_1.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) + + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) for i in range(1, num_blocks): - await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) await time_out_assert(15, did_wallet_4.get_confirmed_balance, 101) await time_out_assert(15, did_wallet_4.get_unconfirmed_balance, 101) await time_out_assert(15, did_wallet.get_confirmed_balance, 0) await time_out_assert(15, did_wallet.get_unconfirmed_balance, 0) + + @pytest.mark.parametrize( + "with_recovery", + [True, False], + ) + @pytest.mark.asyncio + async def test_did_transfer(self, two_wallet_nodes, with_recovery): + num_blocks = 5 + full_nodes, wallets = two_wallet_nodes + full_node_api = full_nodes[0] + server_1 = full_node_api.server + wallet_node, server_2 = wallets[0] + wallet_node_2, server_3 = wallets[1] + wallet = wallet_node.wallet_state_manager.main_wallet + wallet2 = wallet_node_2.wallet_state_manager.main_wallet + ph = await wallet.get_new_puzzlehash() + + wallet_node.config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + + wallet_node_2.config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + + await server_2.start_client(PeerInfo("localhost", uint16(server_1._port)), None) + await server_3.start_client(PeerInfo("localhost", uint16(server_1._port)), None) + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + funds = sum( + [ + calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) + for i in range(1, num_blocks - 1) + ] + ) + + await time_out_assert(15, wallet.get_confirmed_balance, funds) + + async with wallet_node.wallet_state_manager.lock: + did_wallet_1: DIDWallet = await DIDWallet.create_new_did_wallet( + wallet_node.wallet_state_manager, + wallet, + uint64(101), + [bytes(ph)], + uint64(1), + {"Twitter": "Test", "GitHub": "测试"}, + ) + assert did_wallet_1.wallet_info.name == "Profile 1" + spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(wallet.id()) + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) + ph2 = await wallet2.get_new_puzzlehash() + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) + await time_out_assert(15, did_wallet_1.get_confirmed_balance, 101) + await time_out_assert(15, did_wallet_1.get_unconfirmed_balance, 101) + # Transfer DID + new_puzhash = await wallet2.get_new_puzzlehash() + await did_wallet_1.transfer_did(new_puzhash, uint64(0), with_recovery) + print(f"Original launch_id {did_wallet_1.did_info.origin_coin.name()}") + spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( + did_wallet_1.id() + ) + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) + ph2 = await wallet2.get_new_puzzlehash() + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) + # Check if the DID wallet is created in the wallet2 + await time_out_assert(30, len, 2, wallet_node_2.wallet_state_manager.wallets) + # Get the new DID wallet + did_wallets = list( + filter( + lambda w: (w.type == WalletType.DISTRIBUTED_ID), + await wallet_node_2.wallet_state_manager.get_all_wallet_info_entries(), + ) + ) + did_wallet_2: Optional[DIDWallet] = wallet_node_2.wallet_state_manager.wallets[did_wallets[0].id] + assert did_wallet_1.did_info.origin_coin == did_wallet_2.did_info.origin_coin + if with_recovery: + assert did_wallet_1.did_info.backup_ids[0] == did_wallet_2.did_info.backup_ids[0] + assert did_wallet_1.did_info.num_of_backup_ids_needed == did_wallet_2.did_info.num_of_backup_ids_needed + metadata = json.loads(did_wallet_1.did_info.metadata) + assert metadata["Twitter"] == "Test" + assert metadata["GitHub"] == "测试" + + @pytest.mark.asyncio + async def test_update_recovery_list(self, two_wallet_nodes): + num_blocks = 5 + full_nodes, wallets = two_wallet_nodes + full_node_api = full_nodes[0] + server_1 = full_node_api.server + wallet_node, server_2 = wallets[0] + wallet_node_2, server_3 = wallets[1] + wallet = wallet_node.wallet_state_manager.main_wallet + ph = await wallet.get_new_puzzlehash() + + wallet_node.config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + + wallet_node_2.config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + + await server_2.start_client(PeerInfo("localhost", uint16(server_1._port)), None) + await server_3.start_client(PeerInfo("localhost", uint16(server_1._port)), None) + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + funds = sum( + [ + calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) + for i in range(1, num_blocks - 1) + ] + ) + + await time_out_assert(15, wallet.get_confirmed_balance, funds) + + async with wallet_node.wallet_state_manager.lock: + did_wallet_1: DIDWallet = await DIDWallet.create_new_did_wallet( + wallet_node.wallet_state_manager, wallet, uint64(101), [] + ) + spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(wallet.id()) + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) + ph2 = await wallet.get_new_puzzlehash() + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) + await time_out_assert(15, did_wallet_1.get_confirmed_balance, 101) + await time_out_assert(15, did_wallet_1.get_unconfirmed_balance, 101) + await did_wallet_1.update_recovery_list([bytes(ph)], 1) + await did_wallet_1.create_update_spend() + ph2 = await wallet.get_new_puzzlehash() + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) + await time_out_assert(15, did_wallet_1.get_confirmed_balance, 101) + await time_out_assert(15, did_wallet_1.get_unconfirmed_balance, 101) + assert did_wallet_1.did_info.backup_ids[0] == bytes(ph) + assert did_wallet_1.did_info.num_of_backup_ids_needed == 1 + + @pytest.mark.asyncio + async def test_update_metadata(self, two_wallet_nodes): + num_blocks = 5 + full_nodes, wallets = two_wallet_nodes + full_node_api = full_nodes[0] + server_1 = full_node_api.server + wallet_node, server_2 = wallets[0] + wallet_node_2, server_3 = wallets[1] + wallet = wallet_node.wallet_state_manager.main_wallet + ph = await wallet.get_new_puzzlehash() + + wallet_node.config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + + wallet_node_2.config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + + await server_2.start_client(PeerInfo("localhost", uint16(server_1._port)), None) + await server_3.start_client(PeerInfo("localhost", uint16(server_1._port)), None) + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + funds = sum( + [ + calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) + for i in range(1, num_blocks - 1) + ] + ) + + await time_out_assert(15, wallet.get_confirmed_balance, funds) + + async with wallet_node.wallet_state_manager.lock: + did_wallet_1: DIDWallet = await DIDWallet.create_new_did_wallet( + wallet_node.wallet_state_manager, wallet, uint64(101), [] + ) + spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(wallet.id()) + spend_bundle = spend_bundle_list[0].spend_bundle + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, spend_bundle.name()) + ph2 = await wallet.get_new_puzzlehash() + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) + await time_out_assert(15, did_wallet_1.get_confirmed_balance, 101) + await time_out_assert(15, did_wallet_1.get_unconfirmed_balance, 101) + metadata = {} + metadata["Twitter"] = "http://www.twitter.com" + await did_wallet_1.update_metadata(metadata) + await did_wallet_1.create_update_spend() + ph2 = await wallet.get_new_puzzlehash() + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) + await time_out_assert(15, did_wallet_1.get_confirmed_balance, 101) + await time_out_assert(15, did_wallet_1.get_unconfirmed_balance, 101) + assert did_wallet_1.did_info.metadata.find("Twitter") > 0 diff --git a/tests/wallet/did_wallet/test_did_rpc.py b/tests/wallet/did_wallet/test_did_rpc.py deleted file mode 100644 index 4d9b20b987db..000000000000 --- a/tests/wallet/did_wallet/test_did_rpc.py +++ /dev/null @@ -1,121 +0,0 @@ -import logging - -import pytest - -from chia.rpc.rpc_server import start_rpc_server -from chia.rpc.wallet_rpc_api import WalletRpcApi -from chia.rpc.wallet_rpc_client import WalletRpcClient -from chia.simulator.simulator_protocol import FarmNewBlockProtocol -from chia.types.peer_info import PeerInfo -from chia.util.ints import uint16, uint64 -from chia.wallet.did_wallet.did_wallet import DIDWallet -from chia.wallet.util.wallet_types import WalletType -from tests.time_out_assert import time_out_assert - -log = logging.getLogger(__name__) - -pytestmark = pytest.mark.skip("TODO: Fix tests") - - -class TestDIDWallet: - @pytest.mark.asyncio - async def test_create_did(self, bt, three_wallet_nodes, self_hostname): - num_blocks = 4 - full_nodes, wallets = three_wallet_nodes - full_node_api = full_nodes[0] - full_node_server = full_node_api.server - wallet_node_0, wallet_server_0 = wallets[0] - wallet_node_1, wallet_server_1 = wallets[1] - wallet_node_2, wallet_server_2 = wallets[2] - MAX_WAIT_SECS = 30 - - wallet = wallet_node_0.wallet_state_manager.main_wallet - ph = await wallet.get_new_puzzlehash() - await wallet_server_0.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) - await wallet_server_1.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) - await wallet_server_2.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) - for i in range(0, num_blocks + 1): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"\0")) - - log.info("Waiting for initial money in Wallet 0 ...") - - api_one = WalletRpcApi(wallet_node_0) - config = bt.config - daemon_port = config["daemon_port"] - await wallet_server_0.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) - rpc_server_cleanup, test_rpc_port = await start_rpc_server( - api_one, - self_hostname, - daemon_port, - uint16(0), - lambda x: None, - bt.root_path, - config, - connect_to_daemon=False, - ) - client = await WalletRpcClient.create(self_hostname, test_rpc_port, bt.root_path, bt.config) - - async def got_initial_money(): - balances = await client.get_wallet_balance("1") - return balances["confirmed_wallet_balance"] > 0 - - await time_out_assert(timeout=MAX_WAIT_SECS, function=got_initial_money) - - val = await client.create_new_did_wallet(201) - - assert isinstance(val, dict) - if "success" in val: - assert val["success"] - assert val["type"] == WalletType.DISTRIBUTED_ID.value - assert val["wallet_id"] > 1 - assert len(val["my_did"]) == 64 - assert bytes.fromhex(val["my_did"]) - - main_wallet_2 = wallet_node_2.wallet_state_manager.main_wallet - ph2 = await main_wallet_2.get_new_puzzlehash() - for i in range(0, num_blocks + 1): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) - - recovery_list = [bytes.fromhex(val["my_did"])] - async with wallet_node_2.wallet_state_manager.lock: - did_wallet_2: DIDWallet = await DIDWallet.create_new_did_wallet( - wallet_node_2.wallet_state_manager, main_wallet_2, uint64(101), recovery_list - ) - for i in range(0, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"\0")) - - filename = "test.backup" - did_wallet_2.create_backup(filename) - - val = await client.create_new_did_wallet_from_recovery(filename) - if "success" in val: - assert val["success"] - assert val["type"] == WalletType.DISTRIBUTED_ID.value - assert val["wallet_id"] > 1 - did_wallet_id_3 = val["wallet_id"] - assert len(val["my_did"]) == 64 - assert bytes.fromhex(val["my_did"]) == did_wallet_2.did_info.origin_coin.name() - assert bytes.fromhex(val["coin_name"]) - assert bytes.fromhex(val["newpuzhash"]) - assert bytes.fromhex(val["pubkey"]) - - filename = "test.attest" - val = await client.did_create_attest( - did_wallet_2.wallet_id, val["coin_name"], val["pubkey"], val["newpuzhash"], filename - ) - if "success" in val: - assert val["success"] - for i in range(0, num_blocks): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"\0")) - - val = await client.did_recovery_spend(did_wallet_id_3, [filename]) - if "success" in val: - assert val["success"] - for i in range(0, num_blocks * 2): - await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"\0")) - - val = await client.get_wallet_balance(did_wallet_id_3) - - assert val["confirmed_wallet_balance"] == 101 - await rpc_server_cleanup() diff --git a/tests/wallet/nft_wallet/__init__.py b/tests/wallet/nft_wallet/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/wallet/nft_wallet/config.py b/tests/wallet/nft_wallet/config.py new file mode 100644 index 000000000000..0257db4372d0 --- /dev/null +++ b/tests/wallet/nft_wallet/config.py @@ -0,0 +1 @@ +checkout_blocks_and_plots = True diff --git a/tests/wallet/nft_wallet/test_nft_clvm.py b/tests/wallet/nft_wallet/test_nft_clvm.py new file mode 100644 index 000000000000..2f3b80951bca --- /dev/null +++ b/tests/wallet/nft_wallet/test_nft_clvm.py @@ -0,0 +1,93 @@ +from chia.types.blockchain_format.program import INFINITE_COST, Program +from chia.wallet.puzzles.load_clvm import load_clvm +from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_for_pk, solution_for_conditions +from chia.wallet.puzzles.puzzle_utils import make_create_coin_condition +from tests.core.make_block_generator import int_to_public_key + +SINGLETON_MOD = load_clvm("singleton_top_layer.clvm") +LAUNCHER_PUZZLE = load_clvm("singleton_launcher.clvm") +DID_MOD = load_clvm("did_innerpuz.clvm") +NFT_STATE_LAYER_MOD = load_clvm("nft_state_layer.clvm") +STANDARD_PUZZLE_MOD = load_clvm("p2_delegated_puzzle_or_hidden_puzzle.clvm") +LAUNCHER_PUZZLE_HASH = LAUNCHER_PUZZLE.get_tree_hash() +NFT_STATE_LAYER_MOD_HASH = NFT_STATE_LAYER_MOD.get_tree_hash() +SINGLETON_MOD_HASH = SINGLETON_MOD.get_tree_hash() + +LAUNCHER_ID = Program.to("launcher-id").get_tree_hash() +NFT_METADATA_UPDATER = load_clvm("nft_metadata_updater_default.clvm") + + +def test_new_nft_ownership_layer() -> None: + pubkey = int_to_public_key(1) + innerpuz = puzzle_for_pk(pubkey) + my_amount = 1 + destination: Program = puzzle_for_pk(int_to_public_key(2)) + condition_list = [make_create_coin_condition(destination.get_tree_hash(), my_amount, [])] + metadata = [ + ("u", ["https://www.chia.net/img/branding/chia-logo.svg"]), + ("h", 0xD4584AD463139FA8C0D9F68F4B59F185), + ] + solution = Program.to( + [ + NFT_STATE_LAYER_MOD_HASH, + metadata, + NFT_METADATA_UPDATER.get_tree_hash(), + innerpuz, + # below here is the solution + solution_for_conditions(condition_list), + my_amount, + 0, + ] + ) + + cost, res = NFT_STATE_LAYER_MOD.run_with_cost(INFINITE_COST, solution) + assert res.first().first().as_int() == 73 + assert res.first().rest().first().as_int() == 1 + assert res.rest().rest().first().first().as_int() == 51 + assert ( + res.rest().rest().first().rest().first().as_atom() + == NFT_STATE_LAYER_MOD.curry( + NFT_STATE_LAYER_MOD_HASH, metadata, NFT_METADATA_UPDATER.get_tree_hash(), destination + ).get_tree_hash() + ) + + +def test_update_metadata() -> None: + pubkey = int_to_public_key(1) + innerpuz = puzzle_for_pk(pubkey) + my_amount = 1 + destination: Program = puzzle_for_pk(int_to_public_key(2)) + condition_list = [make_create_coin_condition(destination.get_tree_hash(), my_amount, [])] + condition_list.append([-24, NFT_METADATA_UPDATER, "https://www.chia.net/img/branding/chia-logo-2.svg"]) + metadata = [ + ("u", ["https://www.chia.net/img/branding/chia-logo.svg"]), + ("h", 0xD4584AD463139FA8C0D9F68F4B59F185), + ] + solution = Program.to( + [ + NFT_STATE_LAYER_MOD_HASH, + metadata, + NFT_METADATA_UPDATER.get_tree_hash(), + innerpuz, + # below here is the solution + solution_for_conditions(condition_list), + my_amount, + 0, + ] + ) + + metadata = [ + ("u", ["https://www.chia.net/img/branding/chia-logo-2.svg", "https://www.chia.net/img/branding/chia-logo.svg"]), + ("h", 0xD4584AD463139FA8C0D9F68F4B59F185), + ] + + cost, res = NFT_STATE_LAYER_MOD.run_with_cost(INFINITE_COST, solution) + assert res.first().first().as_int() == 73 + assert res.first().rest().first().as_int() == 1 + assert res.rest().rest().first().first().as_int() == 51 + assert ( + res.rest().rest().first().rest().first().as_atom() + == NFT_STATE_LAYER_MOD.curry( + NFT_STATE_LAYER_MOD_HASH, metadata, NFT_METADATA_UPDATER.get_tree_hash(), destination + ).get_tree_hash() + ) diff --git a/tests/wallet/nft_wallet/test_nft_wallet.py b/tests/wallet/nft_wallet/test_nft_wallet.py new file mode 100644 index 000000000000..984f4d819e8e --- /dev/null +++ b/tests/wallet/nft_wallet/test_nft_wallet.py @@ -0,0 +1,468 @@ +import asyncio + +# pytestmark = pytest.mark.skip("TODO: Fix tests") +from typing import Any + +import pytest + +from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward +from chia.full_node.mempool_manager import MempoolManager +from chia.rpc.wallet_rpc_api import WalletRpcApi +from chia.simulator.full_node_simulator import FullNodeSimulator +from chia.simulator.simulator_protocol import FarmNewBlockProtocol +from chia.types.blockchain_format.program import Program +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.peer_info import PeerInfo +from chia.util.ints import uint16, uint32 +from chia.wallet.nft_wallet.nft_wallet import NFTWallet +from chia.wallet.util.compute_memos import compute_memos +from chia.wallet.util.wallet_types import WalletType +from tests.time_out_assert import time_out_assert, time_out_assert_not_none + + +async def tx_in_pool(mempool: MempoolManager, tx_id: bytes32) -> bool: + tx = mempool.get_spendbundle(tx_id) + if tx is None: + return False + return True + + +@pytest.mark.parametrize( + "trusted", + [True], +) +@pytest.mark.asyncio +async def test_nft_wallet_creation_automatically(two_wallet_nodes: Any, trusted: Any) -> None: + num_blocks = 5 + full_nodes, wallets = two_wallet_nodes + full_node_api = full_nodes[0] + full_node_server = full_node_api.server + wallet_node_0, server_0 = wallets[0] + wallet_node_1, server_1 = wallets[1] + wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + wallet_1 = wallet_node_1.wallet_state_manager.main_wallet + + ph = await wallet_0.get_new_puzzlehash() + ph1 = await wallet_1.get_new_puzzlehash() + + if trusted: + wallet_node_0.config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + wallet_node_1.config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + else: + wallet_node_0.config["trusted_peers"] = {} + wallet_node_1.config["trusted_peers"] = {} + + await server_0.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await server_1.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + funds = sum( + [calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) for i in range(1, num_blocks - 1)] + ) + + await time_out_assert(10, wallet_0.get_unconfirmed_balance, funds) + await time_out_assert(10, wallet_0.get_confirmed_balance, funds) + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1)) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + await time_out_assert(15, wallet_0.get_pending_change_balance, 0) + nft_wallet_0 = await NFTWallet.create_new_nft_wallet( + wallet_node_0.wallet_state_manager, wallet_0, name="NFT WALLET 1" + ) + metadata = Program.to( + [ + ("u", ["https://www.chia.net/img/branding/chia-logo.svg"]), + ("h", "0xD4584AD463139FA8C0D9F68F4B59F185"), + ] + ) + + sb = await nft_wallet_0.generate_new_nft(metadata) + assert sb + await time_out_assert_not_none(15, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1)) + + await time_out_assert(5, len, 1, nft_wallet_0.nft_wallet_info.my_nft_coins) + coins = nft_wallet_0.nft_wallet_info.my_nft_coins + assert len(coins) == 1, "nft not generated" + + sb = await nft_wallet_0.transfer_nft(coins[0], ph1) + assert sb is not None + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1)) + assert len(wallet_node_1.wallet_state_manager.wallets) == 2 + # Get the new NFT wallet + nft_wallets = await wallet_node_1.wallet_state_manager.get_all_wallet_info_entries(WalletType.NFT) + assert len(nft_wallets) == 1 + nft_wallet_1: NFTWallet = wallet_node_1.wallet_state_manager.wallets[nft_wallets[0].id] + await time_out_assert(5, len, 1, nft_wallet_1.nft_wallet_info.my_nft_coins) + coins = nft_wallet_0.nft_wallet_info.my_nft_coins + assert len(coins) == 0 + coins = nft_wallet_1.nft_wallet_info.my_nft_coins + assert len(coins) == 1 + + +@pytest.mark.parametrize( + "trusted", + [True], +) +@pytest.mark.asyncio +async def test_nft_wallet_creation_and_transfer(two_wallet_nodes: Any, trusted: Any) -> None: + num_blocks = 5 + full_nodes, wallets = two_wallet_nodes + full_node_api: FullNodeSimulator = full_nodes[0] + full_node_server = full_node_api.server + wallet_node_0, server_0 = wallets[0] + wallet_node_1, server_1 = wallets[1] + wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + wallet_1 = wallet_node_1.wallet_state_manager.main_wallet + + ph = await wallet_0.get_new_puzzlehash() + ph1 = await wallet_1.get_new_puzzlehash() + + if trusted: + wallet_node_0.config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + wallet_node_1.config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + else: + wallet_node_0.config["trusted_peers"] = {} + wallet_node_1.config["trusted_peers"] = {} + + await server_0.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await server_1.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + funds = sum( + [calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) for i in range(1, num_blocks - 1)] + ) + + await time_out_assert(10, wallet_0.get_unconfirmed_balance, funds) + await time_out_assert(10, wallet_0.get_confirmed_balance, funds) + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1)) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + await time_out_assert(15, wallet_0.get_pending_change_balance, 0) + nft_wallet_0 = await NFTWallet.create_new_nft_wallet( + wallet_node_0.wallet_state_manager, wallet_0, name="NFT WALLET 1" + ) + metadata = Program.to( + [ + ("u", ["https://www.chia.net/img/branding/chia-logo.svg"]), + ("h", "0xD4584AD463139FA8C0D9F68F4B59F185"), + ] + ) + + sb = await nft_wallet_0.generate_new_nft(metadata) + assert sb + # ensure hints are generated + assert compute_memos(sb) + await time_out_assert_not_none(15, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1)) + + coins = nft_wallet_0.nft_wallet_info.my_nft_coins + assert len(coins) == 1, "nft not generated" + + metadata = Program.to( + [ + ("u", ["https://www.test.net/logo.svg"]), + ("h", "0xD4584AD463139FA8C0D9F68F4B59F181"), + ] + ) + + sb = await nft_wallet_0.generate_new_nft(metadata) + assert sb + # ensure hints are generated + assert compute_memos(sb) + await time_out_assert_not_none(15, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1)) + await time_out_assert(15, len, 2, nft_wallet_0.nft_wallet_info.my_nft_coins) + coins = nft_wallet_0.nft_wallet_info.my_nft_coins + assert len(coins) == 2, "nft not generated" + + nft_wallet_1 = await NFTWallet.create_new_nft_wallet( + wallet_node_1.wallet_state_manager, wallet_1, name="NFT WALLET 2" + ) + sb = await nft_wallet_0.transfer_nft(coins[1], ph1) + + assert sb is not None + # ensure hints are generated + assert compute_memos(sb) + + await time_out_assert_not_none(15, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1)) + + coins = nft_wallet_0.nft_wallet_info.my_nft_coins + assert len(coins) == 1 + coins = nft_wallet_1.nft_wallet_info.my_nft_coins + assert len(coins) == 1 + + # Send it back to original owner + nsb = await nft_wallet_1.transfer_nft(coins[0], ph) + assert nsb is not None + + await time_out_assert_not_none(15, full_node_api.full_node.mempool_manager.get_spendbundle, nsb.name()) + # ensure hints are generated + assert compute_memos(nsb) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1)) + await time_out_assert(5, len, 2, nft_wallet_0.nft_wallet_info.my_nft_coins) + await time_out_assert(5, len, 0, nft_wallet_1.nft_wallet_info.my_nft_coins) + + +@pytest.mark.parametrize( + "trusted", + [True], +) +@pytest.mark.asyncio +async def test_nft_wallet_rpc_creation_and_list(two_wallet_nodes: Any, trusted: Any) -> None: + num_blocks = 5 + full_nodes, wallets = two_wallet_nodes + full_node_api = full_nodes[0] + full_node_server = full_node_api.server + wallet_node_0, server_0 = wallets[0] + wallet_node_1, server_1 = wallets[1] + wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + wallet_1 = wallet_node_1.wallet_state_manager.main_wallet + + ph = await wallet_0.get_new_puzzlehash() + _ = await wallet_1.get_new_puzzlehash() + + if trusted: + wallet_node_0.config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + wallet_node_1.config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + else: + wallet_node_0.config["trusted_peers"] = {} + wallet_node_1.config["trusted_peers"] = {} + + await server_0.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await server_1.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + api_0 = WalletRpcApi(wallet_node_0) + nft_wallet_0 = await api_0.create_new_wallet(dict(wallet_type="nft_wallet", name="NFT WALLET 1")) + assert isinstance(nft_wallet_0, dict) + assert nft_wallet_0.get("success") + nft_wallet_0_id = nft_wallet_0["wallet_id"] + + tr1 = await api_0.nft_mint_nft( + { + "wallet_id": nft_wallet_0_id, + "artist_address": ph, + "hash": "0xD4584AD463139FA8C0D9F68F4B59F185", + "uris": ["https://www.chia.net/img/branding/chia-logo.svg"], + } + ) + + assert isinstance(tr1, dict) + assert tr1.get("success") + sb = tr1["spend_bundle"] + + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + tr2 = await api_0.nft_mint_nft( + { + "wallet_id": nft_wallet_0_id, + "artist_address": ph, + "hash": "0xD4584AD463139FA8C0D9F68F4B59F184", + "uris": ["https://chialisp.com/img/logo.svg"], + } + ) + assert isinstance(tr2, dict) + assert tr2.get("success") + sb = tr2["spend_bundle"] + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + time_left = 5.0 + while time_left > 0: + coins_response = await api_0.nft_get_nfts(dict(wallet_id=nft_wallet_0_id)) + try: + assert isinstance(coins_response, dict) + assert coins_response.get("success") + coins = coins_response["nft_list"] + assert len(coins) == 2 + uris = [] + for coin in coins: + uris.append(coin.to_json_dict()["data_uris"][0]) + assert len(uris) == 2 + assert "https://chialisp.com/img/logo.svg" in uris + assert bytes32.fromhex(coins[1].to_json_dict()["nft_coin_id"]) in [x.name() for x in sb.additions()] + except AssertionError: + if time_left < 0: + raise + await asyncio.sleep(0.5) + time_left -= 0.5 + + +@pytest.mark.parametrize( + "trusted", + [True], +) +@pytest.mark.asyncio +async def test_nft_wallet_rpc_update_metadata(two_wallet_nodes: Any, trusted: Any) -> None: + num_blocks = 3 + full_nodes, wallets = two_wallet_nodes + full_node_api = full_nodes[0] + full_node_server = full_node_api.server + wallet_node_0, server_0 = wallets[0] + wallet_node_1, server_1 = wallets[1] + wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + wallet_1 = wallet_node_1.wallet_state_manager.main_wallet + + ph = await wallet_0.get_new_puzzlehash() + _ = await wallet_1.get_new_puzzlehash() + + if trusted: + wallet_node_0.config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + wallet_node_1.config["trusted_peers"] = { + full_node_api.full_node.server.node_id.hex(): full_node_api.full_node.server.node_id.hex() + } + else: + wallet_node_0.config["trusted_peers"] = {} + wallet_node_1.config["trusted_peers"] = {} + + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + await server_0.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + await server_1.start_client(PeerInfo("localhost", uint16(full_node_server._port)), None) + api_0 = WalletRpcApi(wallet_node_0) + await time_out_assert(10, wallet_node_0.wallet_state_manager.synced, True) + await time_out_assert(10, wallet_node_1.wallet_state_manager.synced, True) + nft_wallet_0 = await api_0.create_new_wallet(dict(wallet_type="nft_wallet", name="NFT WALLET 1")) + assert isinstance(nft_wallet_0, dict) + assert nft_wallet_0.get("success") + nft_wallet_0_id = nft_wallet_0["wallet_id"] + + # mint NFT + resp = await api_0.nft_mint_nft( + { + "wallet_id": nft_wallet_0_id, + "artist_address": ph, + "hash": "0xD4584AD463139FA8C0D9F68F4B59F185", + "uris": ["https://www.chia.net/img/branding/chia-logo.svg"], + } + ) + + assert resp.get("success") + sb = resp["spend_bundle"] + + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + time_left = 5.0 + coins_response = {} + while time_left > 0: + coins_response = await api_0.nft_get_nfts(dict(wallet_id=nft_wallet_0_id)) + if coins_response.get("nft_list"): + break + await asyncio.sleep(0.5) + time_left -= 0.5 + assert coins_response["nft_list"], isinstance(coins_response, dict) + assert coins_response.get("success") + coins = coins_response["nft_list"] + coin = coins[0].to_json_dict() + nft_coin_id = coin["nft_coin_id"] + # add another URI + tr1 = await api_0.nft_add_uri( + { + "wallet_id": nft_wallet_0_id, + "nft_coin_id": nft_coin_id, + "hash": "0xD4584AD463139FA8C0D9F68F4B59F185", + "uri": "https://www.chia.net/img/branding/chia-logo-white.svg", + } + ) + + assert isinstance(tr1, dict) + assert tr1.get("success") + sb = tr1["spend_bundle"] + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + # check that new URI was added + time_left = 5.0 + while time_left > 0: + coins_response = await api_0.nft_get_nfts(dict(wallet_id=nft_wallet_0_id)) + try: + assert isinstance(coins_response, dict) + assert coins_response.get("success") + coins = coins_response["nft_list"] + assert len(coins) == 1 + coin = coins[0].to_json_dict() + uris = coin["data_uris"] + assert len(uris) == 2 + assert "https://www.chia.net/img/branding/chia-logo-white.svg" in uris + except AssertionError: + if time_left < 0: + raise + await asyncio.sleep(0.5) + time_left -= 0.5 + # add yet another URI + nft_coin_id = coin["nft_coin_id"] + tr1 = await api_0.nft_add_uri( + { + "wallet_id": nft_wallet_0_id, + "nft_coin_id": nft_coin_id, + "hash": "0xD4584AD463139FA8C0D9F68F4B59F185", + "uri": "https://www.chia.net/img/branding/chia-logo-more-white.svg", + } + ) + + assert isinstance(tr1, dict) + assert tr1.get("success") + sb = tr1["spend_bundle"] + await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + for i in range(1, num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + time_left = 5.0 + while time_left > 0: + coins_response = await api_0.nft_get_nfts(dict(wallet_id=nft_wallet_0_id)) + try: + assert isinstance(coins_response, dict) + assert coins_response.get("success") + coins = coins_response["nft_list"] + assert len(coins) == 1 + coin = coins[0].to_json_dict() + uris = coin["data_uris"] + assert len(uris) == 3 + assert "https://www.chia.net/img/branding/chia-logo-more-white.svg" in uris + except AssertionError: + if time_left < 0: + raise + await asyncio.sleep(0.5) + time_left -= 0.5 diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index b0155fabf100..00a3e51b0a27 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -1,4 +1,5 @@ import dataclasses +import json import logging from operator import attrgetter from typing import Any, Dict, List, Optional, Tuple @@ -31,6 +32,7 @@ from chia.wallet.cat_wallet.cat_constants import DEFAULT_CATS from chia.wallet.cat_wallet.cat_wallet import CATWallet from chia.wallet.derive_keys import master_sk_to_wallet_sk, master_sk_to_wallet_sk_unhardened +from chia.wallet.did_wallet.did_wallet import DIDWallet from chia.wallet.trading.trade_status import TradeStatus from chia.wallet.transaction_record import TransactionRecord from chia.wallet.transaction_sorting import SortKey @@ -730,6 +732,96 @@ def only_ids(trades): assert len(all_offers) == 2 +@pytest.mark.asyncio +async def test_did_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment): + env: WalletRpcTestEnvironment = wallet_rpc_environment + + wallet_1: Wallet = env.wallet_1.wallet + wallet_2: Wallet = env.wallet_2.wallet + wallet_1_node: WalletNode = env.wallet_1.node + wallet_2_node: WalletNode = env.wallet_2.node + wallet_1_rpc: WalletRpcClient = env.wallet_1.rpc_client + full_node_api: FullNodeSimulator = env.full_node.api + + await generate_funds(env.full_node.api, env.wallet_1, 5) + + # Create a DID wallet + res = await wallet_1_rpc.create_new_did_wallet(amount=1, name=None) + assert res["success"] + did_wallet_id_0 = res["wallet_id"] + did_id_0 = res["my_did"] + + # Get wallet name + res = await wallet_1_rpc.did_get_wallet_name(did_wallet_id_0) + assert res["success"] + assert res["name"] == "Profile 1" + + # Set wallet name + new_wallet_name = "test name" + res = await wallet_1_rpc.did_set_wallet_name(did_wallet_id_0, new_wallet_name) + assert res["success"] + res = await wallet_1_rpc.did_get_wallet_name(did_wallet_id_0) + assert res["success"] + assert res["name"] == new_wallet_name + with pytest.raises(ValueError, match="Wallet id 1 is not a DID wallet"): + await wallet_1_rpc.did_set_wallet_name(wallet_1.id(), new_wallet_name) + + # Check DID ID + res = await wallet_1_rpc.get_did_id(did_wallet_id_0) + assert res["success"] + assert did_id_0 == res["my_did"] + # Create backup file + res = await wallet_1_rpc.create_did_backup_file(did_wallet_id_0, "backup.did") + assert res["success"] + + for _ in range(3): + await farm_transaction_block(full_node_api, wallet_1_node) + + # Update recovery list + res = await wallet_1_rpc.update_did_recovery_list(did_wallet_id_0, [did_id_0], 1) + assert res["success"] + res = await wallet_1_rpc.get_did_recovery_list(did_wallet_id_0) + assert res["num_required"] == 1 + assert res["recovery_list"][0] == did_id_0 + + for _ in range(3): + await farm_transaction_block(full_node_api, wallet_1_node) + + # Update metadata + res = await wallet_1_rpc.update_did_metadata(did_wallet_id_0, {"Twitter": "Https://test"}) + assert res["success"] + + for _ in range(3): + await farm_transaction_block(full_node_api, wallet_1_node) + + res = await wallet_1_rpc.get_did_metadata(did_wallet_id_0) + assert res["metadata"]["Twitter"] == "Https://test" + + for _ in range(3): + await farm_transaction_block(full_node_api, wallet_1_node) + + # Transfer DID + addr = encode_puzzle_hash(await wallet_2.get_new_puzzlehash(), "txch") + res = await wallet_1_rpc.did_transfer_did(did_wallet_id_0, addr, 0, True) + assert res["success"] + + for _ in range(3): + await farm_transaction_block(full_node_api, wallet_1_node) + + assert wallet_2_node.wallet_state_manager is not None + + did_wallets = list( + filter( + lambda w: (w.type == WalletType.DISTRIBUTED_ID), + await wallet_2_node.wallet_state_manager.get_all_wallet_info_entries(), + ) + ) + did_wallet_2: DIDWallet = wallet_2_node.wallet_state_manager.wallets[did_wallets[0].id] + assert did_wallet_2.get_my_DID() == did_id_0 + metadata = json.loads(did_wallet_2.did_info.metadata) + assert metadata["Twitter"] == "Https://test" + + @pytest.mark.asyncio async def test_key_and_address_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment): env: WalletRpcTestEnvironment = wallet_rpc_environment