mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2026-08-24 10:05:29 -05:00
Merge pull request #12267 from Chia-Network/catchup/long_lived_atari_from_long_lived_datalayer_c438d08bc132cad349f03d3868d7cb386813d692
catchup: into long_lived/atari from long_lived/datalayer @ c438d08bc1
This commit is contained in:
@@ -95,7 +95,7 @@ jobs:
|
||||
- name: Test wallet-cat_wallet code with pytest
|
||||
run: |
|
||||
. ./activate
|
||||
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 0 -m "not benchmark" tests/wallet/cat_wallet/test_cat_lifecycle.py tests/wallet/cat_wallet/test_cat_wallet.py tests/wallet/cat_wallet/test_offer_lifecycle.py tests/wallet/cat_wallet/test_trades.py
|
||||
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 0 -m "not benchmark" tests/wallet/cat_wallet/test_cat_lifecycle.py tests/wallet/cat_wallet/test_cat_outer_puzzle.py tests/wallet/cat_wallet/test_cat_wallet.py tests/wallet/cat_wallet/test_offer_lifecycle.py tests/wallet/cat_wallet/test_trades.py
|
||||
|
||||
- name: Process coverage data
|
||||
run: |
|
||||
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
- 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
|
||||
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 0 -m "not benchmark" tests/wallet/nft_wallet/test_nft_1_offers.py tests/wallet/nft_wallet/test_nft_lifecycle.py tests/wallet/nft_wallet/test_nft_offers.py tests/wallet/nft_wallet/test_nft_puzzles.py tests/wallet/nft_wallet/test_nft_wallet.py tests/wallet/nft_wallet/test_ownership_outer_puzzle.py
|
||||
|
||||
- name: Process coverage data
|
||||
run: |
|
||||
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
- name: Test wallet code with pytest
|
||||
run: |
|
||||
. ./activate
|
||||
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 4 -m "not benchmark" tests/wallet/test_bech32m.py tests/wallet/test_chialisp.py tests/wallet/test_coin_selection.py tests/wallet/test_puzzle_store.py tests/wallet/test_singleton.py tests/wallet/test_singleton_lifecycle.py tests/wallet/test_singleton_lifecycle_fast.py tests/wallet/test_taproot.py tests/wallet/test_wallet.py tests/wallet/test_wallet_blockchain.py tests/wallet/test_wallet_interested_store.py tests/wallet/test_wallet_key_val_store.py tests/wallet/test_wallet_retry.py tests/wallet/test_wallet_store.py tests/wallet/test_wallet_user_store.py
|
||||
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 4 -m "not benchmark" tests/wallet/test_bech32m.py tests/wallet/test_chialisp.py tests/wallet/test_coin_selection.py tests/wallet/test_nft_store.py tests/wallet/test_puzzle_store.py tests/wallet/test_singleton.py tests/wallet/test_singleton_lifecycle.py tests/wallet/test_singleton_lifecycle_fast.py tests/wallet/test_taproot.py tests/wallet/test_wallet.py tests/wallet/test_wallet_blockchain.py tests/wallet/test_wallet_interested_store.py tests/wallet/test_wallet_key_val_store.py tests/wallet/test_wallet_retry.py tests/wallet/test_wallet_store.py tests/wallet/test_wallet_user_store.py
|
||||
|
||||
- name: Process coverage data
|
||||
run: |
|
||||
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
- name: Test wallet-cat_wallet code with pytest
|
||||
run: |
|
||||
. ./activate
|
||||
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 0 -m "not benchmark" tests/wallet/cat_wallet/test_cat_lifecycle.py tests/wallet/cat_wallet/test_cat_wallet.py tests/wallet/cat_wallet/test_offer_lifecycle.py tests/wallet/cat_wallet/test_trades.py
|
||||
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 0 -m "not benchmark" tests/wallet/cat_wallet/test_cat_lifecycle.py tests/wallet/cat_wallet/test_cat_outer_puzzle.py tests/wallet/cat_wallet/test_cat_wallet.py tests/wallet/cat_wallet/test_offer_lifecycle.py tests/wallet/cat_wallet/test_trades.py
|
||||
|
||||
- name: Process coverage data
|
||||
run: |
|
||||
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
- 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
|
||||
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 0 -m "not benchmark" tests/wallet/nft_wallet/test_nft_1_offers.py tests/wallet/nft_wallet/test_nft_lifecycle.py tests/wallet/nft_wallet/test_nft_offers.py tests/wallet/nft_wallet/test_nft_puzzles.py tests/wallet/nft_wallet/test_nft_wallet.py tests/wallet/nft_wallet/test_ownership_outer_puzzle.py
|
||||
|
||||
- name: Process coverage data
|
||||
run: |
|
||||
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
- name: Test wallet code with pytest
|
||||
run: |
|
||||
. ./activate
|
||||
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 4 -m "not benchmark" tests/wallet/test_bech32m.py tests/wallet/test_chialisp.py tests/wallet/test_coin_selection.py tests/wallet/test_puzzle_store.py tests/wallet/test_singleton.py tests/wallet/test_singleton_lifecycle.py tests/wallet/test_singleton_lifecycle_fast.py tests/wallet/test_taproot.py tests/wallet/test_wallet.py tests/wallet/test_wallet_blockchain.py tests/wallet/test_wallet_interested_store.py tests/wallet/test_wallet_key_val_store.py tests/wallet/test_wallet_retry.py tests/wallet/test_wallet_store.py tests/wallet/test_wallet_user_store.py
|
||||
venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 4 -m "not benchmark" tests/wallet/test_bech32m.py tests/wallet/test_chialisp.py tests/wallet/test_coin_selection.py tests/wallet/test_nft_store.py tests/wallet/test_puzzle_store.py tests/wallet/test_singleton.py tests/wallet/test_singleton_lifecycle.py tests/wallet/test_singleton_lifecycle_fast.py tests/wallet/test_taproot.py tests/wallet/test_wallet.py tests/wallet/test_wallet_blockchain.py tests/wallet/test_wallet_interested_store.py tests/wallet/test_wallet_key_val_store.py tests/wallet/test_wallet_retry.py tests/wallet/test_wallet_store.py tests/wallet/test_wallet_user_store.py
|
||||
|
||||
- name: Process coverage data
|
||||
run: |
|
||||
|
||||
@@ -19,4 +19,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v3
|
||||
- name: Run install script
|
||||
run: |
|
||||
sh install.sh -d
|
||||
- uses: pre-commit/action@v2.0.3
|
||||
|
||||
@@ -197,16 +197,6 @@ 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: |
|
||||
|
||||
@@ -59,12 +59,10 @@ repos:
|
||||
rev: 3.9.2
|
||||
hooks:
|
||||
- id: flake8
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.950
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies: [filelock, pytest, pytest-asyncio, types-aiofiles, types-click, types-setuptools, types-PyYAML]
|
||||
# This intentionally counters the settings in mypy.ini to allow a loose local
|
||||
# check and a strict CI check. This difference may or may not be retained long
|
||||
# term.
|
||||
args: [--no-warn-unused-ignores]
|
||||
- id: mypy
|
||||
name: mypy
|
||||
entry: ./activated.py mypy
|
||||
language: python
|
||||
pass_filenames: false
|
||||
|
||||
@@ -10,6 +10,92 @@ for setuptools_scm/PEP 440 reasons.
|
||||
|
||||
### What's Changed
|
||||
|
||||
## 1.4.0 Chia blockchain 2022-6-29
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for NFTs!!! :party:
|
||||
- Added `chia wallet nft` command (see https://docs.chia.net/docs/13cli/did_cli)
|
||||
- Added `chia wallet did` command (see https://docs.chia.net/docs/12rpcs/nft_rpcs)
|
||||
- Added RPCs for DID (see https://docs.chia.net/docs/12rpcs/did_rpcs)
|
||||
- Added RPCs for NFT (see https://docs.chia.net/docs/12rpcs/nft_rpcs)
|
||||
- Enable stricter mempool rule when dealing with multiple extra arguments
|
||||
- Added a retry when loading pool info from a pool at 2 minute intervals
|
||||
- Added CLI options `--sort-by-height` and –sort-by-relevance` to `chia wallet get_transactions`
|
||||
- Harvester: Introduce `recursive_plot_scan`
|
||||
- Add libgmp-dev to Bladebit installation - thanks to @TheLastCicada
|
||||
- Add support for multiple of the same CAT in aggregate offers - Thanks to @roseiliend
|
||||
|
||||
### Changed
|
||||
|
||||
- New coin selection algorithm based on bitcoin knapsack. Previously chia selected the largest coin
|
||||
- Updated chiapos to 1.0.10
|
||||
- Updated chiavdf to 1.0.6
|
||||
- Updated blspy to 1.0.13
|
||||
- Updated setproctitle to 1.2.3
|
||||
- Updated PyYAML to 6.0
|
||||
- Updated pyinstaller to 5.0
|
||||
- Bump clvm_tools_rs version to 0.1.9 for clvm stepper and add a test
|
||||
- Modest speedup of syncing by batching coin lookups
|
||||
- Cmds: Use the new `plot_count` of `get_pool_state` in `plotnft show`
|
||||
- Set mempool size back to the original size at launch
|
||||
- Plotting|tests|setup: Improve `PlotManager` cache
|
||||
- Wallet: Drop unused `WalletStateManager.get_derivation_index`
|
||||
- Harvester: Tweak `get_plots` RPC
|
||||
- Remove explicit multidict version from setup.py
|
||||
- Simplify install.sh ubuntu version tracking
|
||||
- Optimize BLS verification when public key is repeated
|
||||
- Use Install.ps1 in build_windows.ps1
|
||||
- Updated warning about `CHIA_ROOT` being set when running init
|
||||
- Cmds: Adjust stop daemon output
|
||||
- Remove unused functions on MerkleSet
|
||||
- Optimize `hash_coin_list()`
|
||||
- Update CONTRIBUTING.md
|
||||
- Remove outdated 3.8 upgrade comment
|
||||
- Hint refactor
|
||||
- Replace MerkleSet with the rust implementation
|
||||
- Simplify SizedBytes and StructStream
|
||||
- Allow services to set a non-default max request body size limit
|
||||
- Reduce the redundant computations of coin_ids in block_body_validation
|
||||
- Uses the new `from_bytes_unchecked` method in blspy, to improve perfo…
|
||||
- Remove the cache from CoinStore
|
||||
- Keep daemon websocket alive during keyring unlock
|
||||
- Support searching derived addresses on testnet.
|
||||
- Optimize code to not perform useless subgroup checks
|
||||
- Restore missing hints being stored as None (instead of 0-length bytes)
|
||||
- Coin simplification
|
||||
- Harvester: Use a set instead of a list to speed up availability checks
|
||||
- Improved performance of debug log output
|
||||
- Update plotters installation to include an `apt update` - thanks to @TheLastCicada
|
||||
- Early return from `_set_spent function` - Thanks @neurosis69
|
||||
- Remove redundant condition in `get_coin_records` - Thanks @neurosis69
|
||||
- Write python version error to stderr - thanks to @LuaKT
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed issues with harvesters not reconnecting properly - fixes #11466
|
||||
- Return not synced if there are no connections - fixes #12090
|
||||
- Fix issues with wallet resending transactions on various mempool and node errors - fixes #10873
|
||||
- Fix some issues with `plotnft show` (#11897)
|
||||
- Handle ephemeral ports and dual stack (ipv4 & ipv6)
|
||||
- Fix issues when wallet syncing and rolling back too far in the past
|
||||
- Fixes issues with the Farmer Reward dialog incorrectly reporting there is no private key (#11036)
|
||||
- Fix race condition, blockchain can change between two calls to get_peak
|
||||
- Wallet: Fix `CATLineageStore` creation in `create_new_cat_wallet`
|
||||
- Fix incorrect return in "rollback_to_block"
|
||||
- Wallet: Some rollback fixes
|
||||
- Fix issue with missing coins
|
||||
- Fix Newer block issue
|
||||
- Fix jsonify bool
|
||||
- Fix wallet introducers for testnet
|
||||
- Correct wallet CLI sent/received indication
|
||||
- Correct "Older block not found" error message
|
||||
- Print MempoolInclusionStatus as string
|
||||
- Optimize Program.curry()
|
||||
- Improve detection of disconnected websocket between services
|
||||
- Correct install.sh usage short options list
|
||||
- Make sure we set the sync to height correctly when we roll back
|
||||
|
||||
## 1.3.5 Chia blockchain 2022-5-11
|
||||
|
||||
### Added
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$script_directory = Split-Path $MyInvocation.MyCommand.Path -Parent
|
||||
|
||||
& "$script_directory/venv/Scripts/Activate.ps1"
|
||||
& "$args"
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
here = pathlib.Path(__file__).parent
|
||||
|
||||
|
||||
def main(*args: str) -> int:
|
||||
if len(args) == 0:
|
||||
print("Parameters required")
|
||||
return 1
|
||||
|
||||
if sys.platform == "win32":
|
||||
script = "activated.ps1"
|
||||
command = ["powershell", os.fspath(here.joinpath(script)), *args]
|
||||
else:
|
||||
script = "activated.sh"
|
||||
command = [os.fspath(here.joinpath(script)), *args]
|
||||
|
||||
completed_process = subprocess.run(command)
|
||||
|
||||
return completed_process.returncode
|
||||
|
||||
|
||||
sys.exit(main(*sys.argv[1:]))
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
|
||||
SCRIPT_DIRECTORY=$(cd -- "$(dirname -- "$0")"; pwd)
|
||||
# shellcheck disable=SC1091
|
||||
source "${SCRIPT_DIRECTORY}/venv/bin/activate"
|
||||
|
||||
"$@"
|
||||
+1
-1
Submodule chia-blockchain-gui updated: 58681e2cc0...7a75fada58
@@ -117,6 +117,7 @@ def configure(
|
||||
config["introducer"]["port"] = int(testnet_port)
|
||||
config["full_node"]["introducer_peer"]["host"] = testnet_introducer
|
||||
config["full_node"]["dns_servers"] = [testnet_dns_introducer]
|
||||
config["wallet"]["introducer_peer"]["host"] = testnet_introducer
|
||||
config["wallet"]["dns_servers"] = [testnet_dns_introducer]
|
||||
config["selected_network"] = testnet
|
||||
config["harvester"]["selected_network"] = testnet
|
||||
@@ -154,6 +155,8 @@ def configure(
|
||||
config["introducer"]["port"] = int(mainnet_port)
|
||||
config["full_node"]["introducer_peer"]["host"] = mainnet_introducer
|
||||
config["full_node"]["dns_servers"] = [mainnet_dns_introducer]
|
||||
config["wallet"]["introducer_peer"]["host"] = mainnet_introducer
|
||||
config["wallet"]["dns_servers"] = [mainnet_dns_introducer]
|
||||
config["selected_network"] = net
|
||||
config["harvester"]["selected_network"] = net
|
||||
config["pool"]["selected_network"] = net
|
||||
|
||||
+16
-17
@@ -8,7 +8,7 @@ import json
|
||||
import time
|
||||
|
||||
from pprint import pprint
|
||||
from typing import List, Dict, Optional, Callable
|
||||
from typing import Any, List, Dict, Optional, Callable
|
||||
|
||||
from chia.cmds.units import units
|
||||
from chia.cmds.wallet_funcs import print_balance, wallet_coin_unit
|
||||
@@ -114,7 +114,7 @@ async def pprint_pool_wallet_state(
|
||||
wallet_id: int,
|
||||
pool_wallet_info: PoolWalletInfo,
|
||||
address_prefix: str,
|
||||
pool_state_dict: Dict,
|
||||
pool_state_dict: Optional[Dict[str, Any]],
|
||||
):
|
||||
if pool_wallet_info.current.state == PoolSingletonState.LEAVING_POOL and pool_wallet_info.target is None:
|
||||
expected_leave_height = pool_wallet_info.singleton_block_height + pool_wallet_info.current.relative_lock_height
|
||||
@@ -127,7 +127,7 @@ async def pprint_pool_wallet_state(
|
||||
"Target address (not for plotting): "
|
||||
f"{encode_puzzle_hash(pool_wallet_info.current.target_puzzle_hash, address_prefix)}"
|
||||
)
|
||||
print(f"Number of plots: {pool_state_dict[pool_wallet_info.launcher_id]['plot_count']}")
|
||||
print(f"Number of plots: {0 if pool_state_dict is None else pool_state_dict['plot_count']}")
|
||||
print(f"Owner public key: {pool_wallet_info.current.owner_pubkey}")
|
||||
|
||||
print(
|
||||
@@ -145,12 +145,11 @@ async def pprint_pool_wallet_state(
|
||||
print(f"Claimable balance: {print_balance(balance, scale, address_prefix)}")
|
||||
if pool_wallet_info.current.state == PoolSingletonState.FARMING_TO_POOL:
|
||||
print(f"Current pool URL: {pool_wallet_info.current.pool_url}")
|
||||
if pool_wallet_info.launcher_id in pool_state_dict:
|
||||
pool_state = pool_state_dict[pool_wallet_info.launcher_id]
|
||||
print(f"Current difficulty: {pool_state_dict[pool_wallet_info.launcher_id]['current_difficulty']}")
|
||||
print(f"Points balance: {pool_state_dict[pool_wallet_info.launcher_id]['current_points']}")
|
||||
points_found_24h = [points for timestamp, points in pool_state["points_found_24h"]]
|
||||
points_acknowledged_24h = [points for timestamp, points in pool_state["points_acknowledged_24h"]]
|
||||
if pool_state_dict is not None:
|
||||
print(f"Current difficulty: {pool_state_dict['current_difficulty']}")
|
||||
print(f"Points balance: {pool_state_dict['current_points']}")
|
||||
points_found_24h = [points for timestamp, points in pool_state_dict["points_found_24h"]]
|
||||
points_acknowledged_24h = [points for timestamp, points in pool_state_dict["points_acknowledged_24h"]]
|
||||
summed_points_found_24h = sum(points_found_24h)
|
||||
summed_points_acknowledged_24h = sum(points_acknowledged_24h)
|
||||
if summed_points_found_24h == 0:
|
||||
@@ -159,13 +158,13 @@ async def pprint_pool_wallet_state(
|
||||
success_pct = summed_points_acknowledged_24h / summed_points_found_24h
|
||||
print(f"Points found (24h): {summed_points_found_24h}")
|
||||
print(f"Percent Successful Points (24h): {success_pct:.2%}")
|
||||
payout_instructions: str = pool_state_dict["pool_config"]["payout_instructions"]
|
||||
try:
|
||||
payout_address = encode_puzzle_hash(bytes32.fromhex(payout_instructions), address_prefix)
|
||||
print(f"Payout instructions (pool will pay to this address): {payout_address}")
|
||||
except Exception:
|
||||
print(f"Payout instructions (pool will pay you with this): {payout_instructions}")
|
||||
print(f"Relative lock height: {pool_wallet_info.current.relative_lock_height} blocks")
|
||||
payout_instructions: str = pool_state_dict[pool_wallet_info.launcher_id]["pool_config"]["payout_instructions"]
|
||||
try:
|
||||
payout_address = encode_puzzle_hash(bytes32.fromhex(payout_instructions), address_prefix)
|
||||
print(f"Payout instructions (pool will pay to this address): {payout_address}")
|
||||
except Exception:
|
||||
print(f"Payout instructions (pool will pay you with this): {payout_instructions}")
|
||||
if pool_wallet_info.current.state == PoolSingletonState.LEAVING_POOL:
|
||||
expected_leave_height = pool_wallet_info.singleton_block_height + pool_wallet_info.current.relative_lock_height
|
||||
if pool_wallet_info.target is not None:
|
||||
@@ -210,7 +209,7 @@ async def show(args: dict, wallet_client: WalletRpcClient, fingerprint: int) ->
|
||||
wallet_id_passed_in,
|
||||
pool_wallet_info,
|
||||
address_prefix,
|
||||
pool_state_dict,
|
||||
pool_state_dict.get(pool_wallet_info.launcher_id),
|
||||
)
|
||||
else:
|
||||
print(f"Wallet height: {await wallet_client.get_height_info()}")
|
||||
@@ -226,7 +225,7 @@ async def show(args: dict, wallet_client: WalletRpcClient, fingerprint: int) ->
|
||||
wallet_id,
|
||||
pool_wallet_info,
|
||||
address_prefix,
|
||||
pool_state_dict,
|
||||
pool_state_dict.get(pool_wallet_info.launcher_id),
|
||||
)
|
||||
print("")
|
||||
farmer_client.close()
|
||||
|
||||
+146
-9
@@ -455,6 +455,24 @@ def did_wallet_name_cmd(wallet_rpc_port: Optional[int], fingerprint: int, id: in
|
||||
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, did_set_wallet_name))
|
||||
|
||||
|
||||
@did_cmd.command("get_did", short_help="Get DID from 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("-i", "--id", help="Id of the wallet to use", type=int, required=True)
|
||||
def did_get_did_cmd(wallet_rpc_port: Optional[int], fingerprint: int, id: int) -> None:
|
||||
import asyncio
|
||||
from .wallet_funcs import execute_with_wallet, get_did
|
||||
|
||||
extra_params = {"did_wallet_id": id}
|
||||
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, get_did))
|
||||
|
||||
|
||||
@wallet_cmd.group("nft", short_help="NFT related actions")
|
||||
def nft_cmd():
|
||||
pass
|
||||
@@ -469,11 +487,15 @@ def nft_cmd():
|
||||
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:
|
||||
@click.option("-di", "--did-id", help="DID Id to use", type=str)
|
||||
@click.option("-n", "--name", help="Set the NFT wallet name", type=str)
|
||||
def nft_wallet_create_cmd(
|
||||
wallet_rpc_port: Optional[int], fingerprint: int, did_id: Optional[str], name: Optional[str]
|
||||
) -> None:
|
||||
import asyncio
|
||||
from .wallet_funcs import execute_with_wallet, create_nft_wallet
|
||||
|
||||
extra_params: Dict[str, Any] = {}
|
||||
extra_params: Dict[str, Any] = {"did_id": did_id, "name": name}
|
||||
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, create_nft_wallet))
|
||||
|
||||
|
||||
@@ -487,9 +509,17 @@ def nft_wallet_create_cmd(wallet_rpc_port: Optional[int], fingerprint: int) -> N
|
||||
)
|
||||
@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("-ra", "--royalty-address", help="Royalty address", type=str)
|
||||
@click.option("-ta", "--target-address", help="Target address", type=str)
|
||||
@click.option("--no-did-ownership", help="Disable DID ownership support", is_flag=True, default=False)
|
||||
@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("-mh", "--metadata-hash", help="NFT metadata hash", type=str, default="")
|
||||
@click.option("-mu", "--metadata-uris", help="Comma separated list of metadata URIs", type=str)
|
||||
@click.option("-lh", "--license-hash", help="NFT license hash", type=str, default="")
|
||||
@click.option("-lu", "--license-uris", help="Comma separated list of license URIs", type=str)
|
||||
@click.option("-st", "--series-total", help="NFT series total number", type=int, default=1, show_default=True)
|
||||
@click.option("-sn", "--series-number", help="NFT seriese number", type=int, default=1, show_default=True)
|
||||
@click.option(
|
||||
"-m",
|
||||
"--fee",
|
||||
@@ -499,24 +529,60 @@ def nft_wallet_create_cmd(wallet_rpc_port: Optional[int], fingerprint: int) -> N
|
||||
show_default=True,
|
||||
callback=validate_fee,
|
||||
)
|
||||
@click.option(
|
||||
"-rp",
|
||||
"--royalty-percentage-fraction",
|
||||
help="NFT royalty percentage fraction in basis points. Example: 175 would represent 1.75%",
|
||||
type=int,
|
||||
default=0,
|
||||
show_default=True,
|
||||
)
|
||||
def nft_mint_cmd(
|
||||
wallet_rpc_port: Optional[int],
|
||||
fingerprint: int,
|
||||
id: int,
|
||||
artist_address: str,
|
||||
royalty_address: Optional[str],
|
||||
target_address: Optional[str],
|
||||
no_did_ownership: bool,
|
||||
hash: str,
|
||||
uris: str,
|
||||
metadata_hash: Optional[str],
|
||||
metadata_uris: Optional[str],
|
||||
license_hash: Optional[str],
|
||||
license_uris: Optional[str],
|
||||
series_total: Optional[int],
|
||||
series_number: Optional[int],
|
||||
fee: str,
|
||||
royalty_percentage_fraction: int,
|
||||
) -> None:
|
||||
import asyncio
|
||||
from .wallet_funcs import execute_with_wallet, mint_nft
|
||||
|
||||
if metadata_uris is None:
|
||||
metadata_uris_list = []
|
||||
else:
|
||||
metadata_uris_list = [mu.strip() for mu in metadata_uris.split(",")]
|
||||
|
||||
if license_uris is None:
|
||||
license_uris_list = []
|
||||
else:
|
||||
license_uris_list = [lu.strip() for lu in license_uris.split(",")]
|
||||
|
||||
extra_params = {
|
||||
"wallet_id": id,
|
||||
"artist_address": artist_address,
|
||||
"royalty_address": royalty_address,
|
||||
"target_address": target_address,
|
||||
"no_did_ownership": no_did_ownership,
|
||||
"hash": hash,
|
||||
"uris": [u.strip() for u in uris.split(",")],
|
||||
"metadata_hash": metadata_hash,
|
||||
"metadata_uris": metadata_uris_list,
|
||||
"license_hash": license_hash,
|
||||
"license_uris": license_uris_list,
|
||||
"series_total": series_total,
|
||||
"series_number": series_number,
|
||||
"fee": fee,
|
||||
"royalty_percentage": royalty_percentage_fraction,
|
||||
}
|
||||
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, mint_nft))
|
||||
|
||||
@@ -532,7 +598,9 @@ def nft_mint_cmd(
|
||||
@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("-u", "--uri", help="URI to add to the NFT", type=str)
|
||||
@click.option("-mu", "--metadata-uri", help="Metadata URI to add to the NFT", type=str)
|
||||
@click.option("-lu", "--license-uri", help="License URI to add to the NFT", type=str)
|
||||
@click.option(
|
||||
"-m",
|
||||
"--fee",
|
||||
@@ -548,6 +616,8 @@ def nft_add_uri_cmd(
|
||||
id: int,
|
||||
nft_coin_id: str,
|
||||
uri: str,
|
||||
metadata_uri: str,
|
||||
license_uri: str,
|
||||
fee: str,
|
||||
) -> None:
|
||||
import asyncio
|
||||
@@ -557,6 +627,8 @@ def nft_add_uri_cmd(
|
||||
"wallet_id": id,
|
||||
"nft_coin_id": nft_coin_id,
|
||||
"uri": uri,
|
||||
"metadata_uri": metadata_uri,
|
||||
"license_uri": license_uri,
|
||||
"fee": fee,
|
||||
}
|
||||
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, add_uri_to_nft))
|
||||
@@ -573,7 +645,7 @@ def nft_add_uri_cmd(
|
||||
@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("-ta", "--target-address", help="Target recipient wallet address", type=str, required=True)
|
||||
@click.option(
|
||||
"-m",
|
||||
"--fee",
|
||||
@@ -588,7 +660,7 @@ def nft_transfer_cmd(
|
||||
fingerprint: int,
|
||||
id: int,
|
||||
nft_coin_id: str,
|
||||
artist_address: str,
|
||||
target_address: str,
|
||||
fee: str,
|
||||
) -> None:
|
||||
import asyncio
|
||||
@@ -597,7 +669,7 @@ def nft_transfer_cmd(
|
||||
extra_params = {
|
||||
"wallet_id": id,
|
||||
"nft_coin_id": nft_coin_id,
|
||||
"artist_address": artist_address,
|
||||
"target_address": target_address,
|
||||
"fee": fee,
|
||||
}
|
||||
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, transfer_nft))
|
||||
@@ -619,3 +691,68 @@ def nft_list_cmd(wallet_rpc_port: Optional[int], fingerprint: int, id: int) -> N
|
||||
|
||||
extra_params = {"wallet_id": id}
|
||||
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, list_nfts))
|
||||
|
||||
|
||||
@nft_cmd.command("set_did", short_help="Set a DID on 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("-di", "--did-id", help="DID Id to set on the NFT", type=str, required=True)
|
||||
@click.option("-ni", "--nft-coin-id", help="Id of the NFT coin to set the DID on", type=str, required=True)
|
||||
@click.option(
|
||||
"-m",
|
||||
"--fee",
|
||||
help="Set the fees per transaction, in XCH.",
|
||||
type=str,
|
||||
default="0",
|
||||
show_default=True,
|
||||
callback=validate_fee,
|
||||
)
|
||||
def nft_set_did_cmd(
|
||||
wallet_rpc_port: Optional[int],
|
||||
fingerprint: int,
|
||||
id: int,
|
||||
did_id: str,
|
||||
nft_coin_id: str,
|
||||
fee: str,
|
||||
) -> None:
|
||||
import asyncio
|
||||
from .wallet_funcs import execute_with_wallet, set_nft_did
|
||||
|
||||
extra_params = {
|
||||
"wallet_id": id,
|
||||
"did_id": did_id,
|
||||
"nft_coin_id": nft_coin_id,
|
||||
"fee": fee,
|
||||
}
|
||||
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, set_nft_did))
|
||||
|
||||
|
||||
@nft_cmd.command("get_info", short_help="Get NFT information")
|
||||
@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("-ni", "--nft-coin-id", help="Id of the NFT coin to get information on", type=str, required=True)
|
||||
def nft_get_info_cmd(
|
||||
wallet_rpc_port: Optional[int],
|
||||
fingerprint: int,
|
||||
nft_coin_id: str,
|
||||
) -> None:
|
||||
import asyncio
|
||||
from .wallet_funcs import execute_with_wallet, get_nft_info
|
||||
|
||||
extra_params = {
|
||||
"nft_coin_id": nft_coin_id,
|
||||
}
|
||||
asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, get_nft_info))
|
||||
|
||||
+218
-39
@@ -14,11 +14,13 @@ from chia.rpc.wallet_rpc_client import WalletRpcClient
|
||||
from chia.server.outbound_message import NodeType
|
||||
from chia.server.start_wallet import SERVICE_NAME
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.util.bech32m import encode_puzzle_hash
|
||||
from chia.util.bech32m import bech32_decode, decode_puzzle_hash, encode_puzzle_hash
|
||||
from chia.util.config import load_config
|
||||
from chia.util.default_root import DEFAULT_ROOT_PATH
|
||||
from chia.util.ints import uint16, uint32, uint64
|
||||
from chia.cmds.cmds_util import transaction_submitted_msg, transaction_status_msg
|
||||
from chia.wallet.did_wallet.did_info import DID_HRP
|
||||
from chia.wallet.nft_wallet.nft_info import NFT_HRP, NFTInfo
|
||||
from chia.wallet.trade_record import TradeRecord
|
||||
from chia.wallet.trading.offer import Offer
|
||||
from chia.wallet.trading.trade_status import TradeStatus
|
||||
@@ -282,22 +284,65 @@ async def make_offer(args: dict, wallet_client: WalletRpcClient, fingerprint: in
|
||||
print("Not creating offer: Must be offering and requesting at least one asset")
|
||||
else:
|
||||
offer_dict: Dict[Union[uint32, str], int] = {}
|
||||
driver_dict: Dict[str, Any] = {}
|
||||
printable_dict: Dict[str, Tuple[str, int, int]] = {} # Dict[asset_name, Tuple[amount, unit, multiplier]]
|
||||
nft_warning: bool = False
|
||||
for item in [*offers, *requests]:
|
||||
wallet_id, amount = tuple(item.split(":")[0:2])
|
||||
if int(wallet_id) == 1:
|
||||
name: str = "XCH"
|
||||
unit: int = units["chia"]
|
||||
else:
|
||||
name = await wallet_client.get_cat_name(wallet_id)
|
||||
unit = units["cat"]
|
||||
name, amount = tuple(item.split(":")[0:2])
|
||||
try:
|
||||
id: Union[uint32, str] = bytes32.from_hexstr(name).hex()
|
||||
unit = 1
|
||||
except ValueError:
|
||||
try:
|
||||
hrp, _ = bech32_decode(name)
|
||||
if hrp == "nft":
|
||||
coin_id = decode_puzzle_hash(name)
|
||||
unit = 1
|
||||
info = NFTInfo.from_json_dict((await wallet_client.get_nft_info(coin_id.hex()))["nft_info"])
|
||||
nft_warning = True
|
||||
id = info.launcher_id.hex()
|
||||
assert isinstance(id, str)
|
||||
if item in requests:
|
||||
driver_dict[id] = {
|
||||
"type": "singleton",
|
||||
"launcher_id": "0x" + id,
|
||||
"launcher_ph": "0x" + info.launcher_puzhash.hex(),
|
||||
"also": {
|
||||
"type": "metadata",
|
||||
"metadata": info.chain_info,
|
||||
"updater_hash": "0x" + info.updater_puzhash.hex(),
|
||||
},
|
||||
}
|
||||
if info.supports_did:
|
||||
driver_dict[id]["also"]["also"] = {
|
||||
"type": "ownership",
|
||||
"owner": "()",
|
||||
"transfer_program": {
|
||||
"type": "royalty transfer program",
|
||||
"launcher_id": "0x" + info.launcher_id.hex(),
|
||||
"royalty_address": "0x" + info.royalty_puzzle_hash.hex(),
|
||||
"royalty_percentage": str(info.royalty_percentage),
|
||||
},
|
||||
}
|
||||
else:
|
||||
id = decode_puzzle_hash(name).hex()
|
||||
assert hrp is not None
|
||||
unit = units[hrp]
|
||||
except ValueError:
|
||||
id = uint32(int(name))
|
||||
if id == 1:
|
||||
name = "XCH"
|
||||
unit = units["chia"]
|
||||
else:
|
||||
name = await wallet_client.get_cat_name(str(id))
|
||||
unit = units["cat"]
|
||||
multiplier: int = -1 if item in offers else 1
|
||||
printable_dict[name] = (amount, unit, multiplier)
|
||||
if uint32(int(wallet_id)) in offer_dict:
|
||||
if id in offer_dict:
|
||||
print("Not creating offer: Cannot offer and request the same asset in a trade")
|
||||
break
|
||||
else:
|
||||
offer_dict[uint32(int(wallet_id))] = int(Decimal(amount) * unit) * multiplier
|
||||
offer_dict[id] = int(Decimal(amount) * unit) * multiplier
|
||||
else:
|
||||
print("Creating Offer")
|
||||
print("--------------")
|
||||
@@ -313,11 +358,22 @@ async def make_offer(args: dict, wallet_client: WalletRpcClient, fingerprint: in
|
||||
if multiplier > 0:
|
||||
print(f" - {amount} {name} ({int(Decimal(amount) * unit)} mojos)")
|
||||
|
||||
if nft_warning:
|
||||
nft_confirmation = input(
|
||||
"Offers for NFTs will have royalties automatically added. "
|
||||
+ "Are you sure you would like to continue? (y/n): "
|
||||
)
|
||||
if nft_confirmation not in ["y", "yes"]:
|
||||
print("Not creating offer...")
|
||||
return
|
||||
|
||||
confirmation = input("Confirm (y/n): ")
|
||||
if confirmation not in ["y", "yes"]:
|
||||
print("Not creating offer...")
|
||||
else:
|
||||
offer, trade_record = await wallet_client.create_offer_for_ids(offer_dict, fee=fee)
|
||||
offer, trade_record = await wallet_client.create_offer_for_ids(
|
||||
offer_dict, driver_dict=driver_dict, fee=fee
|
||||
)
|
||||
if offer is not None:
|
||||
with open(pathlib.Path(filepath), "w") as file:
|
||||
file.write(offer.to_bech32())
|
||||
@@ -539,13 +595,23 @@ async def print_balances(args: dict, wallet_client: WalletRpcClient, fingerprint
|
||||
balances["unconfirmed_wallet_balance"], scale, address_prefix
|
||||
)
|
||||
spendable_balance: str = print_balance(balances["spendable_balance"], scale, address_prefix)
|
||||
my_did: Optional[str] = None
|
||||
print()
|
||||
print(f"{summary['name']}:")
|
||||
print(f"{indent}{'-Total Balance:'.ljust(23)} {total_balance}")
|
||||
print(f"{indent}{'-Pending Total Balance:'.ljust(23)} " f"{unconfirmed_wallet_balance}")
|
||||
print(f"{indent}{'-Spendable:'.ljust(23)} {spendable_balance}")
|
||||
print(f"{indent}{'-Type:'.ljust(23)} {typ.name}")
|
||||
if len(asset_id) > 0:
|
||||
if typ == WalletType.DECENTRALIZED_ID:
|
||||
get_did_response = await wallet_client.get_did_id(wallet_id)
|
||||
my_did = get_did_response["my_did"]
|
||||
print(f"{indent}{'-DID ID:'.ljust(23)} {my_did}")
|
||||
elif typ == WalletType.NFT:
|
||||
get_did_response = await wallet_client.get_nft_wallet_did(wallet_id)
|
||||
my_did = get_did_response["did_id"]
|
||||
if my_did is not None and len(my_did) > 0:
|
||||
print(f"{indent}{'-DID ID:'.ljust(23)} {my_did}")
|
||||
elif len(asset_id) > 0:
|
||||
print(f"{indent}{'-Asset ID:'.ljust(23)} {asset_id}")
|
||||
print(f"{indent}{'-Wallet ID:'.ljust(23)} {wallet_id}")
|
||||
|
||||
@@ -647,7 +713,7 @@ async def execute_with_wallet(
|
||||
|
||||
async def create_did_wallet(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) -> None:
|
||||
amount = args["amount"]
|
||||
fee = args["fee"]
|
||||
fee: int = int(Decimal(args["fee"]) * units["chia"])
|
||||
name = args["name"]
|
||||
try:
|
||||
response = await wallet_client.create_new_did_wallet(amount, fee, name)
|
||||
@@ -669,9 +735,23 @@ async def did_set_wallet_name(args: Dict, wallet_client: WalletRpcClient, finger
|
||||
print(f"Failed to set DID wallet name: {e}")
|
||||
|
||||
|
||||
async def create_nft_wallet(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) -> None:
|
||||
async def get_did(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) -> None:
|
||||
did_wallet_id: int = args["did_wallet_id"]
|
||||
try:
|
||||
response = await wallet_client.create_new_nft_wallet(None)
|
||||
response = await wallet_client.get_did_id(did_wallet_id)
|
||||
my_did = response["my_did"]
|
||||
coin_id = response["coin_id"]
|
||||
print(f"{'DID:'.ljust(23)} {my_did}")
|
||||
print(f"{'Coin ID:'.ljust(23)} {coin_id}")
|
||||
except Exception as e:
|
||||
print(f"Failed to get DID: {e}")
|
||||
|
||||
|
||||
async def create_nft_wallet(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) -> None:
|
||||
did_id = args["did_id"]
|
||||
name = args["name"]
|
||||
try:
|
||||
response = await wallet_client.create_new_nft_wallet(did_id, name)
|
||||
wallet_id = response["wallet_id"]
|
||||
print(f"Successfully created an NFT wallet with id {wallet_id} on key {fingerprint}")
|
||||
except Exception as e:
|
||||
@@ -679,13 +759,51 @@ async def create_nft_wallet(args: Dict, wallet_client: WalletRpcClient, fingerpr
|
||||
|
||||
|
||||
async def mint_nft(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) -> None:
|
||||
wallet_id = args["wallet_id"]
|
||||
royalty_address = args["royalty_address"]
|
||||
target_address = args["target_address"]
|
||||
no_did_ownership = args["no_did_ownership"]
|
||||
hash = args["hash"]
|
||||
uris = args["uris"]
|
||||
metadata_hash = args["metadata_hash"]
|
||||
metadata_uris = args["metadata_uris"]
|
||||
license_hash = args["license_hash"]
|
||||
license_uris = args["license_uris"]
|
||||
series_total = args["series_total"]
|
||||
series_number = args["series_number"]
|
||||
fee: int = int(Decimal(args["fee"]) * units["chia"])
|
||||
royalty_percentage = args["royalty_percentage"]
|
||||
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)
|
||||
response = await wallet_client.get_nft_wallet_did(wallet_id)
|
||||
wallet_did = response["did_id"]
|
||||
wallet_has_did = wallet_did is not None
|
||||
did_id: Optional[str] = wallet_did
|
||||
# Handle the case when the user wants to disable DID ownership
|
||||
if no_did_ownership:
|
||||
if wallet_has_did:
|
||||
raise ValueError("Disabling DID ownership is not supported for this NFT wallet, it does have a DID")
|
||||
else:
|
||||
did_id = None
|
||||
else:
|
||||
if not wallet_has_did:
|
||||
did_id = ""
|
||||
|
||||
response = await wallet_client.mint_nft(
|
||||
wallet_id,
|
||||
royalty_address,
|
||||
target_address,
|
||||
hash,
|
||||
uris,
|
||||
metadata_hash,
|
||||
metadata_uris,
|
||||
license_hash,
|
||||
license_uris,
|
||||
series_total,
|
||||
series_number,
|
||||
fee,
|
||||
royalty_percentage,
|
||||
did_id,
|
||||
)
|
||||
spend_bundle = response["spend_bundle"]
|
||||
print(f"NFT minted Successfully with spend bundle: {spend_bundle}")
|
||||
except Exception as e:
|
||||
@@ -697,8 +815,23 @@ async def add_uri_to_nft(args: Dict, wallet_client: WalletRpcClient, fingerprint
|
||||
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)
|
||||
metadata_uri = args["metadata_uri"]
|
||||
license_uri = args["license_uri"]
|
||||
if len([x for x in (uri, metadata_uri, license_uri) if x is not None]) > 1:
|
||||
raise ValueError("You must provide only one of the URI flags")
|
||||
if uri is not None and len(uri) > 0:
|
||||
key = "u"
|
||||
uri_value = uri
|
||||
elif metadata_uri is not None and len(metadata_uri) > 0:
|
||||
key = "mu"
|
||||
uri_value = metadata_uri
|
||||
elif license_uri is not None and len(license_uri) > 0:
|
||||
key = "lu"
|
||||
uri_value = license_uri
|
||||
else:
|
||||
raise ValueError("You must provide at least one of the URI flags")
|
||||
fee: int = int(Decimal(args["fee"]) * units["chia"])
|
||||
response = await wallet_client.add_uri_to_nft(wallet_id, nft_coin_id, key, uri_value, fee)
|
||||
spend_bundle = response["spend_bundle"]
|
||||
print(f"URI added successfully with spend bundle: {spend_bundle}")
|
||||
except Exception as e:
|
||||
@@ -709,37 +842,83 @@ async def transfer_nft(args: Dict, wallet_client: WalletRpcClient, fingerprint:
|
||||
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)
|
||||
target_address = args["target_address"]
|
||||
fee: int = int(Decimal(args["fee"]) * units["chia"])
|
||||
response = await wallet_client.transfer_nft(wallet_id, nft_coin_id, target_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}")
|
||||
|
||||
|
||||
def print_nft_info(nft: NFTInfo) -> None:
|
||||
indent: str = " "
|
||||
owner_did = None if nft.owner_did is None else encode_puzzle_hash(nft.owner_did, DID_HRP)
|
||||
print()
|
||||
print(f"{'NFT identifier:'.ljust(26)} {encode_puzzle_hash(nft.launcher_id, NFT_HRP)}")
|
||||
print(f"{'Launcher coin ID:'.ljust(26)} {nft.launcher_id}")
|
||||
print(f"{'Launcher puzhash:'.ljust(26)} {nft.launcher_puzhash}")
|
||||
print(f"{'Current NFT coin ID:'.ljust(26)} {nft.nft_coin_id}")
|
||||
print(f"{'On-chain data/info:'.ljust(26)} {nft.chain_info}")
|
||||
print(f"{'Owner DID:'.ljust(26)} {owner_did}")
|
||||
print(f"{'Royalty percentage:'.ljust(26)} {nft.royalty_percentage}")
|
||||
print(f"{'Royalty puzhash:'.ljust(26)} {nft.royalty_puzzle_hash}")
|
||||
print(f"{'NFT content hash:'.ljust(26)} {nft.data_hash.hex()}")
|
||||
print(f"{'Metadata hash:'.ljust(26)} {nft.metadata_hash.hex()}")
|
||||
print(f"{'License hash:'.ljust(26)} {nft.license_hash.hex()}")
|
||||
print(f"{'NFT series total:'.ljust(26)} {nft.series_total}")
|
||||
print(f"{'Current NFT number in the series:'.ljust(26)} {nft.series_number}")
|
||||
print(f"{'Metadata updater puzhash:'.ljust(26)} {nft.updater_puzhash}")
|
||||
print(f"{'NFT minting block height:'.ljust(26)} {nft.mint_height}")
|
||||
print(f"{'Inner puzzle supports DID:'.ljust(26)} {nft.supports_did}")
|
||||
print(f"{'NFT is pending for a transaction:'.ljust(26)} {nft.pending_transaction}")
|
||||
print()
|
||||
print("URIs:")
|
||||
for uri in nft.data_uris:
|
||||
print(f"{indent}{uri}")
|
||||
print()
|
||||
print("Metadata URIs:")
|
||||
for metadata_uri in nft.metadata_uris:
|
||||
print(f"{indent}{metadata_uri}")
|
||||
print()
|
||||
print("License URIs:")
|
||||
for license_uri in nft.license_uris:
|
||||
print(f"{indent}{license_uri}")
|
||||
|
||||
|
||||
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}")
|
||||
print_nft_info(nft)
|
||||
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}")
|
||||
|
||||
|
||||
async def set_nft_did(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) -> None:
|
||||
wallet_id = args["wallet_id"]
|
||||
did_id = args["did_id"]
|
||||
nft_coin_id = args["nft_coin_id"]
|
||||
fee: int = int(Decimal(args["fee"]) * units["chia"])
|
||||
try:
|
||||
response = await wallet_client.set_nft_did(wallet_id, did_id, nft_coin_id, fee)
|
||||
spend_bundle = response["spend_bundle"]
|
||||
print(f"Transaction to set DID on NFT has been initiated with: {spend_bundle}")
|
||||
except Exception as e:
|
||||
print(f"Failed to set DID on NFT: {e}")
|
||||
|
||||
|
||||
async def get_nft_info(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) -> None:
|
||||
nft_coin_id = args["nft_coin_id"]
|
||||
try:
|
||||
response = await wallet_client.get_nft_info(nft_coin_id)
|
||||
nft_info = NFTInfo.from_json_dict(response["nft_info"])
|
||||
print_nft_info(nft_info)
|
||||
except Exception as e:
|
||||
print(f"Failed to get NFT info: {e}")
|
||||
|
||||
@@ -15,6 +15,7 @@ from chia.types.peer_info import PeerInfo
|
||||
from chia.util.byte_types import hexstr_to_bytes
|
||||
from chia.util.ints import uint16
|
||||
from chia.util.json_util import dict_to_json_str
|
||||
from chia.util.network import select_port
|
||||
from chia.util.ws_message import create_payload, create_payload_dict, format_response, pong
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -336,7 +337,12 @@ async def start_rpc_server(
|
||||
|
||||
site = web.TCPSite(runner, self_hostname, int(rpc_port), ssl_context=rpc_server.ssl_context)
|
||||
await site.start()
|
||||
rpc_port = runner.addresses[0][1]
|
||||
#
|
||||
# On a dual-stack system, we want to get the (first) IPv4 port unless
|
||||
# prefer_ipv6 is set in which case we use the IPv6 port
|
||||
#
|
||||
if rpc_port == 0:
|
||||
rpc_port = select_port(root_path, runner.addresses)
|
||||
|
||||
async def cleanup():
|
||||
await rpc_server.stop()
|
||||
|
||||
+337
-73
@@ -12,17 +12,19 @@ from chia.data_layer.data_layer_wallet import DataLayerWallet
|
||||
from chia.pools.pool_wallet import PoolWallet
|
||||
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.protocols.wallet_protocol import CoinState
|
||||
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.coin_spend import CoinSpend
|
||||
from chia.types.spend_bundle import SpendBundle
|
||||
from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash
|
||||
from chia.util.byte_types import hexstr_to_bytes
|
||||
from chia.util.config import load_config
|
||||
from chia.util.ints import uint8, uint32, uint64
|
||||
from chia.util.ints import uint8, uint32, uint64, uint16
|
||||
from chia.util.keychain import KeyringIsLocked, bytes_to_mnemonic, generate_mnemonic
|
||||
from chia.util.path import path_from_root
|
||||
from chia.util.ws_message import WsRpcMessage, create_payload_dict
|
||||
@@ -35,9 +37,13 @@ from chia.wallet.derive_keys import (
|
||||
master_sk_to_singleton_owner_sk,
|
||||
match_address_to_sk,
|
||||
)
|
||||
from chia.wallet.did_wallet.did_info import DID_HRP
|
||||
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.nft_wallet import nft_puzzles
|
||||
from chia.wallet.nft_wallet.nft_info import NFT_HRP, NFTInfo
|
||||
from chia.wallet.nft_wallet.nft_puzzles import get_metadata_and_phs
|
||||
from chia.wallet.nft_wallet.nft_wallet import NFTWallet, NFTCoinInfo
|
||||
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.rl_wallet.rl_wallet import RLWallet
|
||||
@@ -132,6 +138,12 @@ class WalletRpcApi:
|
||||
# NFT Wallet
|
||||
"/nft_mint_nft": self.nft_mint_nft,
|
||||
"/nft_get_nfts": self.nft_get_nfts,
|
||||
"/nft_get_by_did": self.nft_get_by_did,
|
||||
"/nft_set_nft_did": self.nft_set_nft_did,
|
||||
"/nft_set_nft_status": self.nft_set_nft_status,
|
||||
"/nft_get_wallet_did": self.nft_get_wallet_did,
|
||||
"/nft_get_wallets_with_dids": self.nft_get_wallets_with_dids,
|
||||
"/nft_get_info": self.nft_get_info,
|
||||
"/nft_transfer_nft": self.nft_transfer_nft,
|
||||
"/nft_add_uri": self.nft_add_uri,
|
||||
# RL wallet
|
||||
@@ -438,13 +450,17 @@ class WalletRpcApi:
|
||||
|
||||
async def get_wallets(self, request: Dict):
|
||||
assert self.service.wallet_state_manager is not None
|
||||
|
||||
include_data: bool = request.get("include_data", True)
|
||||
wallet_type: Optional[WalletType] = None
|
||||
if "type" in request:
|
||||
wallet_type = WalletType(request["type"])
|
||||
|
||||
wallets: List[WalletInfo] = await self.service.wallet_state_manager.get_all_wallet_info_entries(wallet_type)
|
||||
|
||||
if not include_data:
|
||||
result: List[WalletInfo] = []
|
||||
for wallet in wallets:
|
||||
result.append(WalletInfo(wallet.id, wallet.name, wallet.type, ""))
|
||||
wallets = result
|
||||
return {"wallets": wallets}
|
||||
|
||||
async def create_new_wallet(self, request: Dict):
|
||||
@@ -523,7 +539,7 @@ class WalletRpcApi:
|
||||
backup_dids = []
|
||||
num_needed = 0
|
||||
for d in request["backup_dids"]:
|
||||
backup_dids.append(hexstr_to_bytes(d))
|
||||
backup_dids.append(decode_puzzle_hash(d))
|
||||
if len(backup_dids) > 0:
|
||||
num_needed = uint64(request["num_of_backup_ids_needed"])
|
||||
metadata: Dict[str, str] = {}
|
||||
@@ -543,7 +559,7 @@ class WalletRpcApi:
|
||||
uint64(request.get("fee", 0)),
|
||||
)
|
||||
|
||||
my_did = did_wallet.get_my_DID()
|
||||
my_did = encode_puzzle_hash(bytes32.fromhex(did_wallet.get_my_DID()), DID_HRP)
|
||||
return {
|
||||
"success": True,
|
||||
"type": did_wallet.type(),
|
||||
@@ -580,18 +596,20 @@ class WalletRpcApi:
|
||||
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
|
||||
did_id: Optional[bytes32] = None
|
||||
if "did_id" in request and request["did_id"] is not None:
|
||||
did_id = decode_puzzle_hash(request["did_id"])
|
||||
if wallet.type() == WalletType.NFT and wallet.get_did() == did_id:
|
||||
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,
|
||||
wallet_state_manager, main_wallet, did_id, request.get("name", None)
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
@@ -979,13 +997,14 @@ class WalletRpcApi:
|
||||
# This driver_dict construction is to maintain backward compatibility where everything is assumed to be a CAT
|
||||
driver_dict: Dict[bytes32, PuzzleInfo] = {}
|
||||
if driver_dict_str is None:
|
||||
for key in offer:
|
||||
try:
|
||||
driver_dict[bytes32.from_hexstr(key)] = PuzzleInfo(
|
||||
{"type": AssetType.CAT.value, "tail": "0x" + key}
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
for key, amount in offer.items():
|
||||
if amount > 0:
|
||||
try:
|
||||
driver_dict[bytes32.from_hexstr(key)] = PuzzleInfo(
|
||||
{"type": AssetType.CAT.value, "tail": "0x" + key}
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
for key, value in driver_dict_str.items():
|
||||
driver_dict[bytes32.from_hexstr(key)] = PuzzleInfo(value)
|
||||
@@ -1121,9 +1140,9 @@ class WalletRpcApi:
|
||||
|
||||
async def did_set_wallet_name(self, request):
|
||||
assert self.service.wallet_state_manager is not None
|
||||
wallet_id = int(request["wallet_id"])
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id]
|
||||
if wallet.type() == WalletType.DISTRIBUTED_ID:
|
||||
if wallet.type() == WalletType.DECENTRALIZED_ID:
|
||||
await wallet.set_name(str(request["name"]))
|
||||
return {"success": True, "wallet_id": wallet_id}
|
||||
else:
|
||||
@@ -1131,18 +1150,18 @@ class WalletRpcApi:
|
||||
|
||||
async def did_get_wallet_name(self, request):
|
||||
assert self.service.wallet_state_manager is not None
|
||||
wallet_id = int(request["wallet_id"])
|
||||
wallet_id = uint32(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_id = uint32(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(_))
|
||||
recovery_list.append(decode_puzzle_hash(_))
|
||||
if "num_verifications_required" in request:
|
||||
new_amount_verifications_required = uint64(request["num_verifications_required"])
|
||||
else:
|
||||
@@ -1157,26 +1176,29 @@ class WalletRpcApi:
|
||||
return {"success": success}
|
||||
|
||||
async def did_update_metadata(self, request):
|
||||
wallet_id = int(request["wallet_id"])
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id]
|
||||
if wallet.type() != WalletType.DECENTRALIZED_ID.value:
|
||||
return {"success": False, "error": f"Wallet with id {wallet_id} is not a DID one"}
|
||||
metadata: Dict[str, str] = {}
|
||||
success: bool = False
|
||||
if "metadata" in request:
|
||||
if type(request["metadata"]) is dict:
|
||||
metadata = request["metadata"]
|
||||
if "metadata" in request and 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()
|
||||
spend_bundle = await wallet.create_update_spend(uint64(request.get("fee", 0)))
|
||||
if spend_bundle is not None:
|
||||
success = True
|
||||
return {"success": success}
|
||||
return {"wallet_id": wallet_id, "success": True, "spend_bundle": spend_bundle}
|
||||
else:
|
||||
return {"success": False, "error": "Couldn't create an update spend bundle."}
|
||||
else:
|
||||
return {"success": False, "error": f"Couldn't update metadata with input: {metadata}"}
|
||||
|
||||
async def did_get_did(self, request):
|
||||
wallet_id = int(request["wallet_id"])
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id]
|
||||
my_did: str = wallet.get_my_DID()
|
||||
my_did: str = encode_puzzle_hash(bytes32.fromhex(wallet.get_my_DID()), DID_HRP)
|
||||
async with self.service.wallet_state_manager.lock:
|
||||
coins = await wallet.select_coins(1)
|
||||
if coins is None or coins == set():
|
||||
@@ -1186,21 +1208,21 @@ class WalletRpcApi:
|
||||
return {"success": True, "wallet_id": wallet_id, "my_did": my_did, "coin_id": coin.name()}
|
||||
|
||||
async def did_get_recovery_list(self, request):
|
||||
wallet_id = int(request["wallet_id"])
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id]
|
||||
recovery_list = wallet.did_info.backup_ids
|
||||
recover_hex_list = []
|
||||
for _ in recovery_list:
|
||||
recover_hex_list.append(_.hex())
|
||||
recovery_dids = []
|
||||
for backup_id in recovery_list:
|
||||
recovery_dids.append(encode_puzzle_hash(backup_id, DID_HRP))
|
||||
return {
|
||||
"success": True,
|
||||
"wallet_id": wallet_id,
|
||||
"recovery_list": recover_hex_list,
|
||||
"recovery_list": recovery_dids,
|
||||
"num_required": wallet.did_info.num_of_backup_ids_needed,
|
||||
}
|
||||
|
||||
async def did_get_metadata(self, request):
|
||||
wallet_id = int(request["wallet_id"])
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id]
|
||||
metadata = json.loads(wallet.did_info.metadata)
|
||||
return {
|
||||
@@ -1210,11 +1232,11 @@ class WalletRpcApi:
|
||||
}
|
||||
|
||||
async def did_recovery_spend(self, request):
|
||||
wallet_id = int(request["wallet_id"])
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id]
|
||||
if len(request["attest_data"]) < wallet.did_info.num_of_backup_ids_needed:
|
||||
return {"success": False, "reason": "insufficient messages"}
|
||||
|
||||
spend_bundle = None
|
||||
async with self.service.wallet_state_manager.lock:
|
||||
(
|
||||
info_list,
|
||||
@@ -1233,31 +1255,34 @@ class WalletRpcApi:
|
||||
assert wallet.did_info.temp_puzhash is not None
|
||||
puzhash = wallet.did_info.temp_puzhash
|
||||
|
||||
success = await wallet.recovery_spend(
|
||||
spend_bundle = await wallet.recovery_spend(
|
||||
wallet.did_info.temp_coin,
|
||||
puzhash,
|
||||
info_list,
|
||||
pubkey,
|
||||
message_spend_bundle,
|
||||
)
|
||||
return {"success": success}
|
||||
if spend_bundle:
|
||||
return {"success": True, "spend_bundle": spend_bundle}
|
||||
else:
|
||||
return {"success": False}
|
||||
|
||||
async def did_get_pubkey(self, request):
|
||||
wallet_id = int(request["wallet_id"])
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id]
|
||||
pubkey = bytes((await wallet.wallet_state_manager.get_unused_derivation_record(wallet_id)).pubkey).hex()
|
||||
return {"success": True, "pubkey": pubkey}
|
||||
|
||||
async def did_create_attest(self, request):
|
||||
wallet_id = int(request["wallet_id"])
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id]
|
||||
async with self.service.wallet_state_manager.lock:
|
||||
info = await wallet.get_info_for_recovery()
|
||||
coin = hexstr_to_bytes(request["coin_name"])
|
||||
coin = bytes32.from_hexstr(request["coin_name"])
|
||||
pubkey = G1Element.from_bytes(hexstr_to_bytes(request["pubkey"]))
|
||||
spend_bundle, attest_data = await wallet.create_attestment(
|
||||
coin,
|
||||
hexstr_to_bytes(request["puzhash"]),
|
||||
bytes32.from_hexstr(request["puzhash"]),
|
||||
pubkey,
|
||||
)
|
||||
if info is not None and spend_bundle is not None:
|
||||
@@ -1271,9 +1296,9 @@ class WalletRpcApi:
|
||||
return {"success": False}
|
||||
|
||||
async def did_get_information_needed_for_recovery(self, request):
|
||||
wallet_id = int(request["wallet_id"])
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
did_wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id]
|
||||
my_did = did_wallet.get_my_DID()
|
||||
my_did = encode_puzzle_hash(bytes32.from_hexstr(did_wallet.get_my_DID()), DID_HRP)
|
||||
coin_name = did_wallet.did_info.temp_coin.name().hex()
|
||||
return {
|
||||
"success": True,
|
||||
@@ -1286,9 +1311,9 @@ class WalletRpcApi:
|
||||
}
|
||||
|
||||
async def did_get_current_coin_info(self, request):
|
||||
wallet_id = int(request["wallet_id"])
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
did_wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id]
|
||||
my_did = did_wallet.get_my_DID()
|
||||
my_did = encode_puzzle_hash(bytes32.from_hexstr(did_wallet.get_my_DID()), DID_HRP)
|
||||
did_coin_threeple = await did_wallet.get_info_for_recovery()
|
||||
assert my_did is not None
|
||||
assert did_coin_threeple is not None
|
||||
@@ -1302,7 +1327,7 @@ class WalletRpcApi:
|
||||
}
|
||||
|
||||
async def did_create_backup_file(self, request):
|
||||
wallet_id = int(request["wallet_id"])
|
||||
wallet_id = uint32(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()}
|
||||
|
||||
@@ -1310,7 +1335,7 @@ class WalletRpcApi:
|
||||
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"])
|
||||
wallet_id = uint32(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:
|
||||
@@ -1333,22 +1358,58 @@ class WalletRpcApi:
|
||||
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()
|
||||
if nft_wallet.type() != WalletType.NFT.value:
|
||||
return {"success": False, "error": f"Wallet with id {wallet_id} is not an NFT one"}
|
||||
royalty_address = request.get("royalty_address")
|
||||
if isinstance(royalty_address, str):
|
||||
royalty_puzhash = decode_puzzle_hash(royalty_address)
|
||||
elif royalty_address is None:
|
||||
royalty_puzhash = await nft_wallet.standard_wallet.get_new_puzzlehash()
|
||||
else:
|
||||
puzzle_hash = address
|
||||
metadata = Program.to(
|
||||
[
|
||||
("u", request["uris"]),
|
||||
("h", hexstr_to_bytes(request["hash"])),
|
||||
]
|
||||
)
|
||||
royalty_puzhash = royalty_address
|
||||
target_address = request.get("target_address")
|
||||
if isinstance(target_address, str):
|
||||
target_puzhash = decode_puzzle_hash(target_address)
|
||||
elif target_address is None:
|
||||
target_puzhash = await nft_wallet.standard_wallet.get_new_puzzlehash()
|
||||
else:
|
||||
target_puzhash = target_address
|
||||
if "uris" not in request:
|
||||
return {"success": False, "error": "Data URIs is required"}
|
||||
if not isinstance(request["uris"], list):
|
||||
return {"success": False, "error": "Data URIs must be a list"}
|
||||
if not isinstance(request.get("meta_uris", []), list):
|
||||
return {"success": False, "error": "Metadata URIs must be a list"}
|
||||
if not isinstance(request.get("license_uris", []), list):
|
||||
return {"success": False, "error": "License URIs must be a list"}
|
||||
metadata_list = [
|
||||
("u", request["uris"]),
|
||||
("h", hexstr_to_bytes(request["hash"])),
|
||||
("mu", request.get("meta_uris", [])),
|
||||
("lu", request.get("license_uris", [])),
|
||||
("sn", uint64(request.get("series_number", 1))),
|
||||
("st", uint64(request.get("series_total", 1))),
|
||||
]
|
||||
if "meta_hash" in request and len(request["meta_hash"]) > 0:
|
||||
metadata_list.append(("mh", hexstr_to_bytes(request["meta_hash"])))
|
||||
if "license_hash" in request and len(request["license_hash"]) > 0:
|
||||
metadata_list.append(("lh", hexstr_to_bytes(request["license_hash"])))
|
||||
metadata = Program.to(metadata_list)
|
||||
fee = uint64(request.get("fee", 0))
|
||||
spend_bundle = await nft_wallet.generate_new_nft(metadata, puzzle_hash, fee=fee)
|
||||
did_id = request.get("did_id", None)
|
||||
if did_id is not None:
|
||||
if did_id == "":
|
||||
did_id = bytes()
|
||||
else:
|
||||
did_id = decode_puzzle_hash(did_id)
|
||||
spend_bundle = await nft_wallet.generate_new_nft(
|
||||
metadata,
|
||||
target_puzhash,
|
||||
royalty_puzhash,
|
||||
uint16(request.get("royalty_percentage", 0)),
|
||||
did_id,
|
||||
fee,
|
||||
)
|
||||
return {"wallet_id": wallet_id, "success": True, "spend_bundle": spend_bundle}
|
||||
|
||||
async def nft_get_nfts(self, request) -> Dict:
|
||||
@@ -1358,9 +1419,93 @@ class WalletRpcApi:
|
||||
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))
|
||||
nft_info_list.append(nft_puzzles.get_nft_info_from_puzzle(nft))
|
||||
return {"wallet_id": wallet_id, "success": True, "nft_list": nft_info_list}
|
||||
|
||||
async def nft_set_nft_did(self, request):
|
||||
try:
|
||||
assert self.service.wallet_state_manager is not None
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
nft_wallet: NFTWallet = self.service.wallet_state_manager.wallets[wallet_id]
|
||||
did_id = request.get("did_id", "")
|
||||
if did_id == "":
|
||||
did_id = b""
|
||||
else:
|
||||
did_id = decode_puzzle_hash(did_id)
|
||||
nft_coin_info = nft_wallet.get_nft_coin_by_id(bytes32.from_hexstr(request["nft_coin_id"]))
|
||||
if not nft_puzzles.get_nft_info_from_puzzle(nft_coin_info).supports_did:
|
||||
return {"success": False, "error": "The NFT doesn't support setting a DID."}
|
||||
fee = uint64(request.get("fee", 0))
|
||||
spend_bundle = await nft_wallet.set_nft_did(nft_coin_info, did_id, fee=fee)
|
||||
return {"wallet_id": wallet_id, "success": True, "spend_bundle": spend_bundle}
|
||||
except Exception as e:
|
||||
log.exception(f"Failed to set DID on NFT: {e}")
|
||||
return {"success": False, "error": f"Failed to set DID on NFT: {e}"}
|
||||
|
||||
async def nft_get_by_did(self, request) -> Dict:
|
||||
did_id: Optional[bytes32] = None
|
||||
if "did_id" in request:
|
||||
did_id = decode_puzzle_hash(request["did_id"])
|
||||
assert self.service.wallet_state_manager is not None
|
||||
for wallet in self.service.wallet_state_manager.wallets.values():
|
||||
if isinstance(wallet, NFTWallet) and wallet.get_did() == did_id:
|
||||
return {"wallet_id": wallet.wallet_id, "success": True}
|
||||
return {"error": f"Cannot find a NFT wallet DID = {did_id}", "success": False}
|
||||
|
||||
async def nft_get_wallet_did(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]
|
||||
if nft_wallet is not None:
|
||||
if nft_wallet.type() != WalletType.NFT.value:
|
||||
return {"success": False, "error": f"Wallet {wallet_id} is not an NFT wallet"}
|
||||
did_bytes: Optional[bytes32] = nft_wallet.get_did()
|
||||
did_id = ""
|
||||
if did_bytes is not None:
|
||||
did_id = encode_puzzle_hash(did_bytes, DID_HRP)
|
||||
return {"success": True, "did_id": None if len(did_id) == 0 else did_id}
|
||||
return {"success": False, "error": f"Wallet {wallet_id} not found"}
|
||||
|
||||
async def nft_get_wallets_with_dids(self, request) -> Dict:
|
||||
assert self.service.wallet_state_manager is not None
|
||||
all_wallets = self.service.wallet_state_manager.wallets.values()
|
||||
did_wallets_by_did_id: Dict[bytes32, uint32] = {
|
||||
wallet.did_info.origin_coin.name(): wallet.id()
|
||||
for wallet in all_wallets
|
||||
if wallet.type() == uint8(WalletType.DECENTRALIZED_ID) and wallet.did_info.origin_coin is not None
|
||||
}
|
||||
did_nft_wallets: List[Dict] = []
|
||||
for wallet in all_wallets:
|
||||
if isinstance(wallet, NFTWallet):
|
||||
nft_wallet_did: Optional[bytes32] = wallet.get_did()
|
||||
if nft_wallet_did is not None:
|
||||
did_wallet_id: uint32 = did_wallets_by_did_id.get(nft_wallet_did, uint32(0))
|
||||
if did_wallet_id == 0:
|
||||
log.warning(f"NFT wallet {wallet.id()} has DID {nft_wallet_did.hex()} but no DID wallet")
|
||||
else:
|
||||
did_nft_wallets.append(
|
||||
{
|
||||
"wallet_id": wallet.id(),
|
||||
"did_id": encode_puzzle_hash(nft_wallet_did, DID_HRP),
|
||||
"did_wallet_id": did_wallet_id,
|
||||
}
|
||||
)
|
||||
return {"success": True, "nft_wallets": did_nft_wallets}
|
||||
|
||||
async def nft_set_nft_status(self, request) -> Dict:
|
||||
try:
|
||||
wallet_id: uint32 = uint32(request["wallet_id"])
|
||||
coin_id: bytes32 = bytes32.from_hexstr(request["coin_id"])
|
||||
status: bool = request["in_transaction"]
|
||||
assert self.service.wallet_state_manager is not None
|
||||
nft_wallet: NFTWallet = self.service.wallet_state_manager.wallets[wallet_id]
|
||||
if nft_wallet is not None:
|
||||
await nft_wallet.update_coin_status(coin_id, status)
|
||||
return {"success": True}
|
||||
return {"success": False, "error": "NFT wallet doesn't exist."}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"Cannot change the status of the NFT.{e}"}
|
||||
|
||||
async def nft_transfer_nft(self, request):
|
||||
assert self.service.wallet_state_manager is not None
|
||||
wallet_id = uint32(request["wallet_id"])
|
||||
@@ -1371,23 +1516,142 @@ class WalletRpcApi:
|
||||
return dict(success=False, error="target_address parameter missing")
|
||||
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"]))
|
||||
nft_coin_id = request["nft_coin_id"]
|
||||
if nft_coin_id.startswith(NFT_HRP):
|
||||
nft_coin_id = decode_puzzle_hash(nft_coin_id)
|
||||
else:
|
||||
nft_coin_id = bytes32.from_hexstr(nft_coin_id)
|
||||
nft_coin_info = nft_wallet.get_nft_coin_by_id(nft_coin_id)
|
||||
fee = uint64(request.get("fee", 0))
|
||||
spend_bundle = await nft_wallet.transfer_nft(nft_coin_info, puzzle_hash, fee=fee)
|
||||
txs = await nft_wallet.generate_signed_transaction(
|
||||
[nft_coin_info.coin.amount],
|
||||
[puzzle_hash],
|
||||
coins={nft_coin_info.coin},
|
||||
fee=fee,
|
||||
new_owner=b"",
|
||||
new_did_inner_hash=b"",
|
||||
)
|
||||
spend_bundle: Optional[SpendBundle] = None
|
||||
for tx in txs:
|
||||
if tx.spend_bundle is not None:
|
||||
spend_bundle = tx.spend_bundle
|
||||
await self.service.wallet_state_manager.add_pending_transaction(tx)
|
||||
await nft_wallet.update_coin_status(nft_coin_info.coin.name(), True)
|
||||
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_get_info(self, request: Dict):
|
||||
assert self.service.wallet_state_manager is not None
|
||||
if "coin_id" not in request:
|
||||
return {"success": False, "error": "Coin ID is required."}
|
||||
coin_id = request["coin_id"]
|
||||
if coin_id.startswith(NFT_HRP):
|
||||
coin_id = decode_puzzle_hash(coin_id)
|
||||
else:
|
||||
coin_id = bytes32.from_hexstr(coin_id)
|
||||
peer = self.service.wallet_state_manager.wallet_node.get_full_node_peer()
|
||||
if peer is None:
|
||||
return {"success": False, "error": "Cannot find a full node peer."}
|
||||
# Get coin state
|
||||
coin_state_list: List[CoinState] = await self.service.wallet_state_manager.wallet_node.get_coin_state(
|
||||
[coin_id], peer=peer
|
||||
)
|
||||
if coin_state_list is None or len(coin_state_list) < 1:
|
||||
return {"success": False, "error": f"Coin record 0x{coin_id.hex()} not found"}
|
||||
coin_state: CoinState = coin_state_list[0]
|
||||
if request.get("latest", True):
|
||||
# Find the unspent coin
|
||||
while coin_state.spent_height is not None:
|
||||
coin_state_list = await self.service.wallet_state_manager.wallet_node.fetch_children(
|
||||
peer, coin_state.coin.name()
|
||||
)
|
||||
odd_coin = 0
|
||||
for coin in coin_state_list:
|
||||
if coin.coin.amount % 2 == 1:
|
||||
odd_coin += 1
|
||||
if odd_coin > 1:
|
||||
return {"success": False, "error": "This is not a singleton, multiple children coins found."}
|
||||
if odd_coin == 0:
|
||||
return {"success": False, "error": "Cannot find child coin, please wait then retry."}
|
||||
coin_state = coin_state_list[0]
|
||||
# Get parent coin
|
||||
parent_coin_state_list: List[CoinState] = await self.service.wallet_state_manager.wallet_node.get_coin_state(
|
||||
[coin_state.coin.parent_coin_info], peer=peer
|
||||
)
|
||||
if parent_coin_state_list is None or len(parent_coin_state_list) < 1:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Parent coin record 0x{coin_state.coin.parent_coin_info.hex()} not found",
|
||||
}
|
||||
parent_coin_state: CoinState = parent_coin_state_list[0]
|
||||
coin_spend: CoinSpend = await self.service.wallet_state_manager.wallet_node.fetch_puzzle_solution(
|
||||
peer, parent_coin_state.spent_height, parent_coin_state.coin
|
||||
)
|
||||
# convert to NFTInfo
|
||||
try:
|
||||
# Check if the metadata is updated
|
||||
full_puzzle: Program = Program.from_bytes(bytes(coin_spend.puzzle_reveal))
|
||||
|
||||
uncurried_nft: UncurriedNFT = UncurriedNFT.uncurry(full_puzzle)
|
||||
metadata, p2_puzzle_hash = get_metadata_and_phs(uncurried_nft, coin_spend.solution)
|
||||
# Note: This is not the actual unspent NFT full puzzle.
|
||||
# There is no way to rebuild the full puzzle in a different wallet.
|
||||
# But it shouldn't have impact on generating the NFTInfo, since inner_puzzle is not used there.
|
||||
if uncurried_nft.supports_did:
|
||||
inner_puzzle = nft_puzzles.recurry_nft_puzzle(
|
||||
uncurried_nft, coin_spend.solution.to_program(), uncurried_nft.p2_puzzle
|
||||
)
|
||||
else:
|
||||
inner_puzzle = uncurried_nft.p2_puzzle
|
||||
full_puzzle = nft_puzzles.create_full_puzzle(
|
||||
uncurried_nft.singleton_launcher_id,
|
||||
metadata,
|
||||
uncurried_nft.metadata_updater_hash,
|
||||
inner_puzzle,
|
||||
)
|
||||
|
||||
# Get launcher coin
|
||||
launcher_coin: List[CoinState] = await self.service.wallet_state_manager.wallet_node.get_coin_state(
|
||||
[uncurried_nft.singleton_launcher_id], peer=peer
|
||||
)
|
||||
if launcher_coin is None or len(launcher_coin) < 1 or launcher_coin[0].spent_height is None:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Launcher coin record 0x{uncurried_nft.singleton_launcher_id.hex()} not found",
|
||||
}
|
||||
nft_info: NFTInfo = nft_puzzles.get_nft_info_from_puzzle(
|
||||
NFTCoinInfo(
|
||||
uncurried_nft.singleton_launcher_id,
|
||||
coin_state.coin,
|
||||
None,
|
||||
full_puzzle,
|
||||
launcher_coin[0].spent_height,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"The coin is not a NFT. {e}"}
|
||||
else:
|
||||
return {"success": True, "nft_info": nft_info}
|
||||
|
||||
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"]
|
||||
# Note metadata updater can only add one uri for one field per spend.
|
||||
# If you want to add multiple uris for one field, you need to spend multiple times.
|
||||
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"]))
|
||||
uri = request["uri"]
|
||||
key = request["key"]
|
||||
nft_coin_id = request["nft_coin_id"]
|
||||
if nft_coin_id.startswith(NFT_HRP):
|
||||
nft_coin_id = decode_puzzle_hash(nft_coin_id)
|
||||
else:
|
||||
nft_coin_id = bytes32.from_hexstr(nft_coin_id)
|
||||
nft_coin_info = nft_wallet.get_nft_coin_by_id(nft_coin_id)
|
||||
fee = uint64(request.get("fee", 0))
|
||||
spend_bundle = await nft_wallet.update_metadata(nft_coin_info, uri, fee=fee)
|
||||
spend_bundle = await nft_wallet.update_metadata(nft_coin_info, key, 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}")
|
||||
|
||||
@@ -576,35 +576,72 @@ class WalletRpcClient(RpcClient):
|
||||
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):
|
||||
async def create_new_nft_wallet(self, did_id, name=None):
|
||||
request: Dict[str, Any] = {
|
||||
"wallet_type": "nft_wallet",
|
||||
"did_wallet_id": did_wallet_id,
|
||||
"did_id": did_id,
|
||||
"name": name,
|
||||
}
|
||||
response = await self.fetch("create_new_wallet", request)
|
||||
return response
|
||||
|
||||
async def mint_nft(self, wallet_id, artist_address, hash, uris, fee):
|
||||
async def mint_nft(
|
||||
self,
|
||||
wallet_id,
|
||||
royalty_address,
|
||||
target_address,
|
||||
hash,
|
||||
uris,
|
||||
meta_hash="",
|
||||
meta_uris=[],
|
||||
license_hash="",
|
||||
license_uris=[],
|
||||
series_total=1,
|
||||
series_number=1,
|
||||
fee=0,
|
||||
royalty_percentage=0,
|
||||
did_id=None,
|
||||
):
|
||||
request: Dict[str, Any] = {
|
||||
"wallet_id": wallet_id,
|
||||
"artist_address": artist_address,
|
||||
"royalty_address": royalty_address,
|
||||
"target_address": target_address,
|
||||
"hash": hash,
|
||||
"uris": uris,
|
||||
"meta_hash": meta_hash,
|
||||
"meta_uris": meta_uris,
|
||||
"license_hash": license_hash,
|
||||
"license_uris": license_uris,
|
||||
"series_number": series_number,
|
||||
"series_total": series_total,
|
||||
"royalty_percentage": royalty_percentage,
|
||||
"did_id": did_id,
|
||||
"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):
|
||||
async def add_uri_to_nft(self, wallet_id, nft_coin_id, key, uri, fee):
|
||||
request: Dict[str, Any] = {
|
||||
"wallet_id": wallet_id,
|
||||
"nft_coin_id": nft_coin_id,
|
||||
"target_address": artist_address,
|
||||
"uri": uri,
|
||||
"key": key,
|
||||
"fee": fee,
|
||||
}
|
||||
response = await self.fetch("nft_add_uri", request)
|
||||
return response
|
||||
|
||||
async def get_nft_info(self, coin_id: str, latest: bool = True):
|
||||
request: Dict[str, Any] = {"coin_id": coin_id, "latest": latest}
|
||||
response = await self.fetch("nft_get_info", request)
|
||||
return response
|
||||
|
||||
async def transfer_nft(self, wallet_id, nft_coin_id, target_address, fee):
|
||||
request: Dict[str, Any] = {
|
||||
"wallet_id": wallet_id,
|
||||
"nft_coin_id": nft_coin_id,
|
||||
"target_address": target_address,
|
||||
"fee": fee,
|
||||
}
|
||||
response = await self.fetch("nft_transfer_nft", request)
|
||||
@@ -615,6 +652,16 @@ class WalletRpcClient(RpcClient):
|
||||
response = await self.fetch("nft_get_nfts", request)
|
||||
return response
|
||||
|
||||
async def set_nft_did(self, wallet_id, did_id, nft_coin_id, fee):
|
||||
request: Dict[str, Any] = {"wallet_id": wallet_id, "did_id": did_id, "nft_coin_id": nft_coin_id, "fee": fee}
|
||||
response = await self.fetch("nft_set_nft_did", request)
|
||||
return response
|
||||
|
||||
async def get_nft_wallet_did(self, wallet_id):
|
||||
request: Dict[str, Any] = {"wallet_id": wallet_id}
|
||||
response = await self.fetch("nft_get_wallet_did", request)
|
||||
return response
|
||||
|
||||
# DataLayer
|
||||
async def create_new_dl(self, root: bytes32, fee: uint64) -> Tuple[List[TransactionRecord], bytes32]:
|
||||
request = {"root": root.hex(), "fee": fee}
|
||||
|
||||
@@ -30,7 +30,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.types.peer_info import PeerInfo
|
||||
from chia.util.errors import Err, ProtocolError
|
||||
from chia.util.ints import uint16
|
||||
from chia.util.network import is_in_network, is_localhost
|
||||
from chia.util.network import is_in_network, is_localhost, select_port
|
||||
from chia.util.ssl_check import verify_ssl_certs_and_keys
|
||||
|
||||
max_message_size = 50 * 1024 * 1024 # 50MB
|
||||
@@ -268,13 +268,19 @@ class ChiaServer:
|
||||
# this port from the socket itself and update self._port.
|
||||
self.site = web.TCPSite(
|
||||
self.runner,
|
||||
host="0.0.0.0",
|
||||
host="", # should listen to both IPv4 and IPv6 on a dual-stack system
|
||||
port=int(self._port),
|
||||
shutdown_timeout=3,
|
||||
ssl_context=ssl_context,
|
||||
)
|
||||
await self.site.start()
|
||||
self._port = self.runner.addresses[0][1]
|
||||
#
|
||||
# On a dual-stack system, we want to get the (first) IPv4 port unless
|
||||
# prefer_ipv6 is set in which case we use the IPv6 port
|
||||
#
|
||||
if self._port == 0:
|
||||
self._port = select_port(self.root_path, self.runner.addresses)
|
||||
|
||||
self.log.info(f"Started listening on port: {self._port}")
|
||||
|
||||
async def incoming_connection(self, request):
|
||||
|
||||
+3
-2
@@ -3,10 +3,11 @@ import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from aiofiles import tempfile # type: ignore
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
from aiofiles import tempfile
|
||||
from typing_extensions import Literal
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -66,7 +67,7 @@ async def write_file_async(file_path: Path, data: Union[str, bytes], *, file_mod
|
||||
# Create the parent directory if necessary
|
||||
os.makedirs(file_path.parent, mode=dir_mode, exist_ok=True)
|
||||
|
||||
mode: str = "w+" if type(data) == str else "w+b"
|
||||
mode: Literal["w+", "w+b"] = "w+" if type(data) == str else "w+b"
|
||||
temp_file_path: Path
|
||||
async with tempfile.NamedTemporaryFile(dir=file_path.parent, mode=mode, delete=False) as f:
|
||||
temp_file_path = f.name
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import socket
|
||||
from pathlib import Path
|
||||
from ipaddress import ip_address, IPv4Network, IPv6Network
|
||||
from typing import Iterable, List, Tuple, Union, Any, Optional, Dict
|
||||
from chia.server.outbound_message import NodeType
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.types.peer_info import PeerInfo
|
||||
from chia.util.config import load_config
|
||||
from chia.util.ints import uint16
|
||||
|
||||
|
||||
@@ -84,3 +86,21 @@ def is_trusted_inner(peer_host: str, peer_node_id: bytes32, trusted_peers: Dict,
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def select_port(root_path: Path, addresses: List[Any]) -> uint16:
|
||||
global_config = load_config(root_path, "config.yaml")
|
||||
prefer_ipv6 = global_config.get("prefer_ipv6", False)
|
||||
selected_port: uint16
|
||||
for address_string, port, *_ in addresses:
|
||||
address = ip_address(address_string)
|
||||
if address.version == 6 and prefer_ipv6:
|
||||
selected_port = port
|
||||
break
|
||||
elif address.version == 4 and not prefer_ipv6:
|
||||
selected_port = port
|
||||
break
|
||||
else:
|
||||
selected_port = addresses[0][1] # no matches, just use the first one in the list
|
||||
|
||||
return selected_port
|
||||
|
||||
@@ -23,6 +23,8 @@ class CATOuterPuzzle:
|
||||
_asset_id: Any
|
||||
_construct: Any
|
||||
_solve: Any
|
||||
_get_inner_puzzle: Any
|
||||
_get_inner_solution: Any
|
||||
|
||||
def match(self, puzzle: Program) -> Optional[PuzzleInfo]:
|
||||
matched, curried_args = match_cat_puzzle(puzzle)
|
||||
@@ -39,6 +41,26 @@ class CATOuterPuzzle:
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_inner_puzzle(self, constructor: PuzzleInfo, puzzle_reveal: Program) -> Optional[Program]:
|
||||
matched, curried_args = match_cat_puzzle(puzzle_reveal)
|
||||
if matched:
|
||||
_, _, inner_puzzle = curried_args
|
||||
if constructor.also() is not None:
|
||||
deep_inner_puzzle: Optional[Program] = self._get_inner_puzzle(constructor.also(), inner_puzzle)
|
||||
return deep_inner_puzzle
|
||||
else:
|
||||
return inner_puzzle
|
||||
else:
|
||||
raise ValueError("This driver is not for the specified puzzle reveal")
|
||||
|
||||
def get_inner_solution(self, constructor: PuzzleInfo, solution: Program) -> Optional[Program]:
|
||||
my_inner_solution: Program = solution.first()
|
||||
if constructor.also():
|
||||
deep_inner_solution: Optional[Program] = self._get_inner_solution(constructor.also(), my_inner_solution)
|
||||
return deep_inner_solution
|
||||
else:
|
||||
return my_inner_solution
|
||||
|
||||
def asset_id(self, constructor: PuzzleInfo) -> Optional[bytes32]:
|
||||
return bytes32(constructor["tail"])
|
||||
|
||||
@@ -73,7 +95,7 @@ class CATOuterPuzzle:
|
||||
parent_coin: Coin = parent_spend.coin
|
||||
if constructor.also() is not None:
|
||||
puzzle = self._construct(constructor.also(), puzzle)
|
||||
solution = self._solve(constructor.also(), solver, puzzle, solution)
|
||||
solution = self._solve(constructor.also(), solver, inner_puzzle, inner_solution)
|
||||
matched, curried_args = match_cat_puzzle(parent_spend.puzzle_reveal.to_program())
|
||||
assert matched
|
||||
_, _, parent_inner_puzzle = curried_args
|
||||
|
||||
@@ -8,6 +8,8 @@ from chia.wallet.lineage_proof import LineageProof
|
||||
from chia.types.blockchain_format.program import Program
|
||||
from chia.types.blockchain_format.coin import Coin
|
||||
|
||||
DID_HRP = "did:chia:"
|
||||
|
||||
|
||||
@streamable
|
||||
@dataclass(frozen=True)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import dataclasses
|
||||
import logging
|
||||
import time
|
||||
import json
|
||||
@@ -94,7 +95,7 @@ class DIDWallet:
|
||||
)
|
||||
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
|
||||
name, WalletType.DECENTRALIZED_ID.value, info_as_string
|
||||
)
|
||||
if self.wallet_info is None:
|
||||
raise ValueError("Internal Error")
|
||||
@@ -105,7 +106,7 @@ class DIDWallet:
|
||||
raise ValueError("Not enough balance")
|
||||
|
||||
try:
|
||||
spend_bundle = await self.generate_new_decentralised_id(uint64(amount))
|
||||
spend_bundle = await self.generate_new_decentralised_id(amount, fee)
|
||||
except Exception:
|
||||
await wallet_state_manager.user_store.delete_wallet(self.id(), False)
|
||||
raise
|
||||
@@ -114,50 +115,7 @@ class DIDWallet:
|
||||
await wallet_state_manager.user_store.delete_wallet(self.id(), False)
|
||||
raise ValueError("Failed to create spend.")
|
||||
await self.wallet_state_manager.add_new_wallet(self, self.wallet_info.id)
|
||||
assert self.did_info.origin_coin is not None
|
||||
assert self.did_info.current_inner is not None
|
||||
did_puzzle_hash = did_wallet_puzzles.create_fullpuz(
|
||||
self.did_info.current_inner, self.did_info.origin_coin.name()
|
||||
).get_tree_hash()
|
||||
|
||||
did_record = TransactionRecord(
|
||||
confirmed_at_height=uint32(0),
|
||||
created_at_time=uint64(int(time.time())),
|
||||
to_puzzle_hash=did_puzzle_hash,
|
||||
amount=uint64(amount),
|
||||
fee_amount=fee,
|
||||
confirmed=False,
|
||||
sent=uint32(10),
|
||||
spend_bundle=None,
|
||||
additions=spend_bundle.additions(),
|
||||
removals=spend_bundle.removals(),
|
||||
wallet_id=self.id(),
|
||||
sent_to=[],
|
||||
trade_id=None,
|
||||
type=uint32(TransactionType.INCOMING_TX.value),
|
||||
name=bytes32(token_bytes()),
|
||||
memos=[],
|
||||
)
|
||||
regular_record = TransactionRecord(
|
||||
confirmed_at_height=uint32(0),
|
||||
created_at_time=uint64(int(time.time())),
|
||||
to_puzzle_hash=did_puzzle_hash,
|
||||
amount=uint64(amount),
|
||||
fee_amount=fee,
|
||||
confirmed=False,
|
||||
sent=uint32(0),
|
||||
spend_bundle=spend_bundle,
|
||||
additions=spend_bundle.additions(),
|
||||
removals=spend_bundle.removals(),
|
||||
wallet_id=self.wallet_state_manager.main_wallet.id(),
|
||||
sent_to=[],
|
||||
trade_id=None,
|
||||
type=uint32(TransactionType.OUTGOING_TX.value),
|
||||
name=bytes32(token_bytes()),
|
||||
memos=list(compute_memos(spend_bundle).items()),
|
||||
)
|
||||
await self.standard_wallet.push_transaction(regular_record)
|
||||
await self.standard_wallet.push_transaction(did_record)
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
@@ -189,7 +147,7 @@ class DIDWallet:
|
||||
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
|
||||
name, WalletType.DECENTRALIZED_ID.value, info_as_string
|
||||
)
|
||||
await self.wallet_state_manager.add_new_wallet(self, self.wallet_info.id)
|
||||
await self.save_info(self.did_info, False)
|
||||
@@ -258,11 +216,10 @@ class DIDWallet:
|
||||
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
|
||||
name, WalletType.DECENTRALIZED_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.wallet_state_manager.add_new_wallet(self, self.wallet_info.id, in_transaction=True)
|
||||
await self.wallet_state_manager.update_wallet_puzzle_hashes(self.wallet_info.id, in_transaction=True)
|
||||
await self.load_parent(self.did_info)
|
||||
self.log.info(f"New DID wallet created {info_as_string}.")
|
||||
if self.wallet_info is None:
|
||||
@@ -299,9 +256,9 @@ class DIDWallet:
|
||||
|
||||
@classmethod
|
||||
def type(cls) -> uint8:
|
||||
return uint8(WalletType.DISTRIBUTED_ID)
|
||||
return uint8(WalletType.DECENTRALIZED_ID)
|
||||
|
||||
def id(self):
|
||||
def id(self) -> uint32:
|
||||
return self.wallet_info.id
|
||||
|
||||
async def get_confirmed_balance(self, record_list=None) -> uint128:
|
||||
@@ -388,6 +345,7 @@ class DIDWallet:
|
||||
# This will be used in the recovery case where we don't have the parent info already
|
||||
async def coin_added(self, coin: Coin, _: uint32):
|
||||
"""Notification from wallet state manager that wallet has been received."""
|
||||
|
||||
self.log.info(f"DID wallet has been notified that coin was added: {coin.name()}:{coin}")
|
||||
inner_puzzle = await self.inner_puzzle_for_did_puzzle(coin.puzzle_hash)
|
||||
if self.did_info.temp_coin is not None:
|
||||
@@ -461,11 +419,23 @@ class DIDWallet:
|
||||
"""
|
||||
# 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)
|
||||
|
||||
new_pubkey = bytes(
|
||||
(
|
||||
await self.wallet_state_manager.get_unused_derivation_record(self.wallet_info.id, in_transaction=True)
|
||||
).pubkey
|
||||
)
|
||||
new_puzhash = puzzle_for_pk(new_pubkey).get_tree_hash()
|
||||
parent_info = None
|
||||
assert did_info.origin_coin is not None
|
||||
assert did_info.current_inner is not None
|
||||
new_did_inner_puzhash = did_wallet_puzzles.get_inner_puzhash_by_p2(
|
||||
new_puzhash,
|
||||
did_info.backup_ids,
|
||||
did_info.num_of_backup_ids_needed,
|
||||
did_info.origin_coin.name(),
|
||||
did_wallet_puzzles.metadata_to_program(json.loads(self.did_info.metadata)),
|
||||
)
|
||||
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:
|
||||
@@ -481,8 +451,7 @@ class DIDWallet:
|
||||
did_info.current_inner.get_tree_hash(),
|
||||
coin.amount,
|
||||
)
|
||||
|
||||
await self.add_parent(coin.name(), future_parent, False)
|
||||
await self.add_parent(coin.name(), future_parent, True)
|
||||
if children_state.spent_height != children_state.created_height:
|
||||
did_info = DIDInfo(
|
||||
did_info.origin_coin,
|
||||
@@ -491,12 +460,13 @@ class DIDWallet:
|
||||
self.did_info.parent_info,
|
||||
did_info.current_inner,
|
||||
coin,
|
||||
new_puzhash,
|
||||
new_did_inner_puzhash,
|
||||
new_pubkey,
|
||||
False,
|
||||
did_info.metadata,
|
||||
)
|
||||
await self.save_info(did_info, False)
|
||||
|
||||
await self.save_info(did_info, True)
|
||||
assert children_state.created_height
|
||||
puzzle_solution_request = wallet_protocol.RequestPuzzleSolution(
|
||||
coin.parent_coin_info, children_state.created_height
|
||||
@@ -514,9 +484,23 @@ class DIDWallet:
|
||||
parent_innerpuz.get_tree_hash(),
|
||||
parent_state.coin.amount,
|
||||
)
|
||||
await self.add_parent(coin.parent_coin_info, parent_info, False)
|
||||
await self.add_parent(coin.parent_coin_info, parent_info, True)
|
||||
assert parent_info is not None
|
||||
|
||||
async def create_tandem_xch_tx(
|
||||
self, fee: uint64, announcement_to_assert: Optional[Announcement] = None
|
||||
) -> TransactionRecord:
|
||||
chia_coins = await self.standard_wallet.select_coins(fee)
|
||||
chia_tx = await self.standard_wallet.generate_signed_transaction(
|
||||
uint64(0),
|
||||
(await self.standard_wallet.get_new_puzzlehash()),
|
||||
fee=fee,
|
||||
coins=chia_coins,
|
||||
coin_announcements_to_consume={announcement_to_assert} if announcement_to_assert is not None else None,
|
||||
)
|
||||
assert chia_tx.spend_bundle is not None
|
||||
return chia_tx
|
||||
|
||||
def puzzle_for_pk(self, pubkey: G1Element) -> Program:
|
||||
if self.did_info.origin_coin is not None:
|
||||
innerpuz = did_wallet_puzzles.create_innerpuz(
|
||||
@@ -552,7 +536,7 @@ class DIDWallet:
|
||||
async def get_name(self):
|
||||
return self.wallet_info.name
|
||||
|
||||
async def create_update_spend(self):
|
||||
async def create_update_spend(self, fee: uint64 = uint64(0)):
|
||||
assert self.did_info.current_inner is not None
|
||||
assert self.did_info.origin_coin is not None
|
||||
coins = await self.select_coins(1)
|
||||
@@ -561,7 +545,8 @@ class DIDWallet:
|
||||
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]}]
|
||||
primaries=[{"puzzlehash": new_puzhash, "amount": uint64(coin.amount), "memos": [new_puzhash]}],
|
||||
coin_announcements={coin.name()},
|
||||
)
|
||||
innersol: Program = Program.to([1, p2_solution])
|
||||
# full solution is (corehash parent_info my_amount innerpuz_reveal solution)
|
||||
@@ -587,7 +572,16 @@ class DIDWallet:
|
||||
list_of_coinspends = [CoinSpend(coin, full_puzzle, fullsol)]
|
||||
unsigned_spend_bundle = SpendBundle(list_of_coinspends, G2Element())
|
||||
spend_bundle = await self.sign(unsigned_spend_bundle)
|
||||
|
||||
if fee > 0:
|
||||
announcement_to_make = coin.name()
|
||||
chia_tx = await self.create_tandem_xch_tx(fee, Announcement(coin.name(), announcement_to_make))
|
||||
else:
|
||||
announcement_to_make = None
|
||||
chia_tx = None
|
||||
if chia_tx is not None and chia_tx.spend_bundle is not None:
|
||||
spend_bundle = SpendBundle.aggregate([spend_bundle, chia_tx.spend_bundle])
|
||||
chia_tx = dataclasses.replace(chia_tx, spend_bundle=None)
|
||||
await self.wallet_state_manager.add_pending_transaction(chia_tx)
|
||||
did_record = TransactionRecord(
|
||||
confirmed_at_height=uint32(0),
|
||||
created_at_time=uint64(int(time.time())),
|
||||
@@ -606,7 +600,8 @@ class DIDWallet:
|
||||
name=bytes32(token_bytes()),
|
||||
memos=list(compute_memos(spend_bundle).items()),
|
||||
)
|
||||
await self.standard_wallet.push_transaction(did_record)
|
||||
await self.wallet_state_manager.add_pending_transaction(did_record)
|
||||
|
||||
return spend_bundle
|
||||
|
||||
async def transfer_did(self, new_puzhash: bytes32, fee: uint64, with_recovery: bool) -> TransactionRecord:
|
||||
@@ -641,7 +636,8 @@ class DIDWallet:
|
||||
"amount": uint64(coin.amount),
|
||||
"memos": [new_puzhash],
|
||||
}
|
||||
]
|
||||
],
|
||||
coin_announcements={coin.name()},
|
||||
)
|
||||
# Need to include backup list reveal here, even we are don't recover
|
||||
# innerpuz solution is
|
||||
@@ -671,7 +667,15 @@ class DIDWallet:
|
||||
list_of_coinspends = [CoinSpend(coin, full_puzzle, fullsol)]
|
||||
unsigned_spend_bundle = SpendBundle(list_of_coinspends, G2Element())
|
||||
spend_bundle = await self.sign(unsigned_spend_bundle)
|
||||
|
||||
if fee > 0:
|
||||
announcement_to_make = coin.name()
|
||||
chia_tx = await self.create_tandem_xch_tx(fee, Announcement(coin.name(), announcement_to_make))
|
||||
else:
|
||||
chia_tx = None
|
||||
if chia_tx is not None and chia_tx.spend_bundle is not None:
|
||||
spend_bundle = SpendBundle.aggregate([spend_bundle, chia_tx.spend_bundle])
|
||||
chia_tx = dataclasses.replace(chia_tx, spend_bundle=None)
|
||||
await self.wallet_state_manager.add_pending_transaction(chia_tx)
|
||||
did_record = TransactionRecord(
|
||||
confirmed_at_height=uint32(0),
|
||||
created_at_time=uint64(int(time.time())),
|
||||
@@ -690,7 +694,7 @@ class DIDWallet:
|
||||
name=bytes32(token_bytes()),
|
||||
memos=list(compute_memos(spend_bundle).items()),
|
||||
)
|
||||
await self.standard_wallet.push_transaction(did_record)
|
||||
await self.wallet_state_manager.add_pending_transaction(did_record)
|
||||
return did_record
|
||||
|
||||
# The message spend can tests\wallet\rpc\test_wallet_rpc.py send messages and also change your innerpuz
|
||||
@@ -793,7 +797,7 @@ class DIDWallet:
|
||||
name=bytes32(token_bytes()),
|
||||
memos=list(compute_memos(spend_bundle).items()),
|
||||
)
|
||||
await self.standard_wallet.push_transaction(did_record)
|
||||
await self.wallet_state_manager.add_pending_transaction(did_record)
|
||||
return spend_bundle
|
||||
|
||||
# Pushes a SpendBundle to create a message coin on the blockchain
|
||||
@@ -869,7 +873,7 @@ class DIDWallet:
|
||||
)
|
||||
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)
|
||||
await self.wallet_state_manager.add_pending_transaction(did_record)
|
||||
return message_spend_bundle, attest_str
|
||||
|
||||
async def get_info_for_recovery(self) -> Optional[Tuple[bytes32, bytes32, uint64]]:
|
||||
@@ -995,7 +999,7 @@ class DIDWallet:
|
||||
name=bytes32(token_bytes()),
|
||||
memos=list(compute_memos(spend_bundle).items()),
|
||||
)
|
||||
await self.standard_wallet.push_transaction(did_record)
|
||||
await self.wallet_state_manager.add_pending_transaction(did_record)
|
||||
new_did_info = DIDInfo(
|
||||
self.did_info.origin_coin,
|
||||
self.did_info.backup_ids,
|
||||
@@ -1112,12 +1116,12 @@ class DIDWallet:
|
||||
agg_sig = AugSchemeMPL.aggregate(sigs)
|
||||
return SpendBundle.aggregate([spend_bundle, SpendBundle([], agg_sig)])
|
||||
|
||||
async def generate_new_decentralised_id(self, amount: uint64) -> Optional[SpendBundle]:
|
||||
async def generate_new_decentralised_id(self, amount: uint64, fee: uint64 = uint64(0)) -> Optional[SpendBundle]:
|
||||
"""
|
||||
This must be called under the wallet state manager lock
|
||||
"""
|
||||
|
||||
coins = await self.standard_wallet.select_coins(amount)
|
||||
coins = await self.standard_wallet.select_coins(uint64(amount + fee))
|
||||
if coins is None:
|
||||
return None
|
||||
|
||||
@@ -1135,7 +1139,7 @@ class DIDWallet:
|
||||
announcement_set.add(Announcement(launcher_coin.name(), announcement_message))
|
||||
|
||||
tx_record: Optional[TransactionRecord] = await self.standard_wallet.generate_signed_transaction(
|
||||
amount, genesis_launcher_puz.get_tree_hash(), uint64(0), origin.name(), coins, None, False, announcement_set
|
||||
amount, genesis_launcher_puz.get_tree_hash(), fee, origin.name(), coins, None, False, announcement_set
|
||||
)
|
||||
|
||||
genesis_launcher_solution = Program.to([did_puzzle_hash, amount, bytes(0x80)])
|
||||
@@ -1175,17 +1179,47 @@ class DIDWallet:
|
||||
await self.save_info(did_info, False)
|
||||
eve_spend = await self.generate_eve_spend(eve_coin, did_full_puz, did_inner)
|
||||
full_spend = SpendBundle.aggregate([tx_record.spend_bundle, eve_spend, launcher_sb])
|
||||
assert self.did_info.origin_coin is not None
|
||||
assert self.did_info.current_inner is not None
|
||||
did_puzzle_hash = did_wallet_puzzles.create_fullpuz(
|
||||
self.did_info.current_inner, self.did_info.origin_coin.name()
|
||||
).get_tree_hash()
|
||||
|
||||
did_record = TransactionRecord(
|
||||
confirmed_at_height=uint32(0),
|
||||
created_at_time=uint64(int(time.time())),
|
||||
to_puzzle_hash=did_puzzle_hash,
|
||||
amount=uint64(amount),
|
||||
fee_amount=fee,
|
||||
confirmed=False,
|
||||
sent=uint32(10),
|
||||
spend_bundle=full_spend,
|
||||
additions=full_spend.additions(),
|
||||
removals=full_spend.removals(),
|
||||
wallet_id=self.id(),
|
||||
sent_to=[],
|
||||
trade_id=None,
|
||||
type=uint32(TransactionType.INCOMING_TX.value),
|
||||
name=bytes32(token_bytes()),
|
||||
memos=[],
|
||||
)
|
||||
regular_record = dataclasses.replace(tx_record, spend_bundle=None)
|
||||
await self.wallet_state_manager.add_pending_transaction(regular_record)
|
||||
await self.wallet_state_manager.add_pending_transaction(did_record)
|
||||
return full_spend
|
||||
|
||||
async def generate_eve_spend(self, coin: Coin, full_puzzle: Program, innerpuz: Program):
|
||||
assert self.did_info.origin_coin is not None
|
||||
uncurried = did_wallet_puzzles.uncurry_innerpuz(innerpuz)
|
||||
assert uncurried is not None
|
||||
p2_puzzle = uncurried[0]
|
||||
# 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()],
|
||||
"memos": [p2_puzzle.get_tree_hash()],
|
||||
}
|
||||
]
|
||||
)
|
||||
@@ -1285,7 +1319,7 @@ class DIDWallet:
|
||||
"""
|
||||
max_num = 0
|
||||
for wallet in self.wallet_state_manager.wallets.values():
|
||||
if wallet.type() == WalletType.DISTRIBUTED_ID:
|
||||
if wallet.type() == WalletType.DECENTRALIZED_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))
|
||||
@@ -1298,7 +1332,7 @@ class DIDWallet:
|
||||
"""
|
||||
for wallet in self.wallet_state_manager.wallets.values():
|
||||
if (
|
||||
wallet.type() == WalletType.DISTRIBUTED_ID
|
||||
wallet.type() == WalletType.DECENTRALIZED_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.")
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
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, Iterator, Dict
|
||||
@@ -205,7 +204,7 @@ def metadata_to_program(metadata: Dict) -> Program:
|
||||
"""
|
||||
kv_list = []
|
||||
for key, value in metadata.items():
|
||||
kv_list.append((assemble(key), assemble(value)))
|
||||
kv_list.append((key, value))
|
||||
return Program.to(kv_list)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
from clvm_tools.binutils import disassemble
|
||||
|
||||
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.puzzle_drivers import PuzzleInfo, Solver
|
||||
from chia.wallet.puzzles.load_clvm import load_clvm
|
||||
|
||||
NFT_STATE_LAYER_MOD = load_clvm("nft_state_layer.clvm")
|
||||
NFT_STATE_LAYER_MOD_HASH = NFT_STATE_LAYER_MOD.get_tree_hash()
|
||||
|
||||
|
||||
def match_metadata_layer_puzzle(puzzle: Program) -> Tuple[bool, List[Program]]:
|
||||
mod, meta_args = puzzle.uncurry()
|
||||
if mod == NFT_STATE_LAYER_MOD:
|
||||
return True, list(meta_args.as_iter())
|
||||
return False, []
|
||||
|
||||
|
||||
def puzzle_for_metadata_layer(metadata: Program, updater_hash: bytes32, inner_puzzle: Program) -> Program:
|
||||
return NFT_STATE_LAYER_MOD.curry(NFT_STATE_LAYER_MOD_HASH, metadata, updater_hash, inner_puzzle)
|
||||
|
||||
|
||||
def solution_for_metadata_layer(amount: uint64, inner_solution: Program) -> Program:
|
||||
return Program.to([inner_solution, amount]) # type: ignore
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MetadataOuterPuzzle:
|
||||
_match: Any
|
||||
_asset_id: Any
|
||||
_construct: Any
|
||||
_solve: Any
|
||||
_get_inner_puzzle: Any
|
||||
_get_inner_solution: Any
|
||||
|
||||
def match(self, puzzle: Program) -> Optional[PuzzleInfo]:
|
||||
matched, curried_args = match_metadata_layer_puzzle(puzzle)
|
||||
if matched:
|
||||
_, metadata, updater_hash, inner_puzzle = curried_args
|
||||
constructor_dict = {
|
||||
"type": "metadata",
|
||||
"metadata": disassemble(metadata), # type: ignore
|
||||
"updater_hash": "0x" + updater_hash.as_python().hex(),
|
||||
}
|
||||
next_constructor = self._match(inner_puzzle)
|
||||
if next_constructor is not None:
|
||||
constructor_dict["also"] = next_constructor.info
|
||||
return PuzzleInfo(constructor_dict)
|
||||
else:
|
||||
return None
|
||||
return None # Uncomment above when match_metadata_layer_puzzle works
|
||||
|
||||
def asset_id(self, constructor: PuzzleInfo) -> Optional[bytes32]:
|
||||
return bytes32(constructor["updater_hash"])
|
||||
|
||||
def construct(self, constructor: PuzzleInfo, inner_puzzle: Program) -> Program:
|
||||
if constructor.also() is not None:
|
||||
inner_puzzle = self._construct(constructor.also(), inner_puzzle)
|
||||
return puzzle_for_metadata_layer(constructor["metadata"], constructor["updater_hash"], inner_puzzle)
|
||||
|
||||
def get_inner_puzzle(self, constructor: PuzzleInfo, puzzle_reveal: Program) -> Optional[Program]:
|
||||
matched, curried_args = match_metadata_layer_puzzle(puzzle_reveal)
|
||||
if matched:
|
||||
_, _, _, inner_puzzle = curried_args
|
||||
if constructor.also() is not None:
|
||||
deep_inner_puzzle: Optional[Program] = self._get_inner_puzzle(constructor.also(), inner_puzzle)
|
||||
return deep_inner_puzzle
|
||||
else:
|
||||
return inner_puzzle
|
||||
else:
|
||||
raise ValueError("This driver is not for the specified puzzle reveal")
|
||||
|
||||
def get_inner_solution(self, constructor: PuzzleInfo, solution: Program) -> Optional[Program]:
|
||||
my_inner_solution: Program = solution.first()
|
||||
if constructor.also():
|
||||
deep_inner_solution: Optional[Program] = self._get_inner_solution(constructor.also(), my_inner_solution)
|
||||
return deep_inner_solution
|
||||
else:
|
||||
return my_inner_solution
|
||||
|
||||
def solve(self, constructor: PuzzleInfo, solver: Solver, inner_puzzle: Program, inner_solution: Program) -> Program:
|
||||
coin_bytes: bytes = solver["coin"]
|
||||
coin: Coin = Coin(bytes32(coin_bytes[0:32]), bytes32(coin_bytes[32:64]), uint64.from_bytes(coin_bytes[64:72]))
|
||||
if constructor.also() is not None:
|
||||
inner_solution = self._solve(constructor.also(), solver, inner_puzzle, inner_solution)
|
||||
return solution_for_metadata_layer(
|
||||
coin.amount,
|
||||
inner_solution,
|
||||
)
|
||||
@@ -1,8 +1,19 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from chia.util.ints import uint64
|
||||
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 uint16, uint32, uint64
|
||||
from chia.util.streamable import Streamable, streamable
|
||||
from chia.wallet.lineage_proof import LineageProof
|
||||
from chia.wallet.puzzles.load_clvm import load_clvm
|
||||
|
||||
LAUNCHER_PUZZLE = load_clvm("singleton_launcher.clvm")
|
||||
IN_TRANSACTION_STATUS = "IN_TRANSACTION"
|
||||
DEFAULT_STATUS = "DEFAULT"
|
||||
|
||||
NFT_HRP = "nft"
|
||||
|
||||
|
||||
@streamable
|
||||
@@ -10,41 +21,75 @@ from chia.util.streamable import Streamable, streamable
|
||||
class NFTInfo(Streamable):
|
||||
"""NFT Info for displaying NFT on the UI"""
|
||||
|
||||
launcher_id: str
|
||||
launcher_id: bytes32
|
||||
"""Launcher coin ID"""
|
||||
|
||||
nft_coin_id: str
|
||||
nft_coin_id: bytes32
|
||||
"""Current NFT coin ID"""
|
||||
|
||||
did_owner: str
|
||||
owner_did: Optional[bytes32]
|
||||
"""Owner DID"""
|
||||
|
||||
royalty: uint64
|
||||
royalty_percentage: Optional[uint16]
|
||||
"""Percentage of the transaction fee paid to the author, e.g. 1000 = 1%"""
|
||||
|
||||
royalty_puzzle_hash: Optional[bytes32]
|
||||
"""Puzzle hash where royalty will be sent to"""
|
||||
data_uris: List[str]
|
||||
""" A list of content URIs"""
|
||||
|
||||
data_hash: str
|
||||
data_hash: bytes
|
||||
"""Hash of the content"""
|
||||
|
||||
metadata_uris: List[str]
|
||||
"""A list of metadata URIs"""
|
||||
|
||||
metadata_hash: str
|
||||
metadata_hash: bytes
|
||||
"""Hash of the metadata"""
|
||||
|
||||
license_uris: List[str]
|
||||
"""A list of license URIs"""
|
||||
|
||||
license_hash: str
|
||||
license_hash: bytes
|
||||
"""Hash of the license"""
|
||||
|
||||
version: str
|
||||
"""Current NFT version"""
|
||||
|
||||
edition_count: uint64
|
||||
series_total: uint64
|
||||
"""How many NFTs in the current series"""
|
||||
|
||||
edition_number: uint64
|
||||
series_number: uint64
|
||||
"""Number of the current NFT in the series"""
|
||||
|
||||
updater_puzhash: bytes32
|
||||
"""Puzzle hash of the metadata updater in hex"""
|
||||
|
||||
chain_info: str
|
||||
"""Information saved on the chain in hex"""
|
||||
|
||||
mint_height: uint32
|
||||
"""Block height of the NFT minting"""
|
||||
|
||||
supports_did: bool
|
||||
"""If the inner puzzle supports DID"""
|
||||
|
||||
pending_transaction: bool = False
|
||||
"""Indicate if the NFT is pending for a transaction"""
|
||||
|
||||
launcher_puzhash: bytes32 = LAUNCHER_PUZZLE.get_tree_hash()
|
||||
"""Puzzle hash of the singleton launcher in hex"""
|
||||
|
||||
|
||||
@streamable
|
||||
@dataclass(frozen=True)
|
||||
class NFTCoinInfo(Streamable):
|
||||
nft_id: bytes32
|
||||
coin: Coin
|
||||
lineage_proof: Optional[LineageProof]
|
||||
full_puzzle: Program
|
||||
mint_height: uint32
|
||||
pending_transaction: bool = False
|
||||
|
||||
|
||||
@streamable
|
||||
@dataclass(frozen=True)
|
||||
class NFTWalletInfo(Streamable):
|
||||
did_id: Optional[bytes32] = None
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from chia.types.blockchain_format.coin import Coin
|
||||
from chia.types.blockchain_format.program import Program
|
||||
from clvm.casts import int_from_bytes
|
||||
from clvm_tools.binutils import disassemble
|
||||
|
||||
from chia.types.blockchain_format.program import Program, SerializedProgram
|
||||
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.util.ints import uint16, uint64
|
||||
from chia.wallet.nft_wallet.nft_info import NFTCoinInfo, NFTInfo
|
||||
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 solution_for_conditions
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
SINGLETON_TOP_LAYER_MOD = load_clvm("singleton_top_layer_v1_1.clvm")
|
||||
@@ -15,9 +19,10 @@ 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")
|
||||
NFT_OWNERSHIP_LAYER = load_clvm("nft_ownership_layer.clvm")
|
||||
NFT_TRANSFER_PROGRAM_DEFAULT = load_clvm("nft_ownership_transfer_program_one_way_claim_with_royalties.clvm")
|
||||
STANDARD_PUZZLE_MOD = load_clvm("p2_delegated_puzzle_or_hidden_puzzle.clvm")
|
||||
|
||||
|
||||
def create_nft_layer_puzzle_with_curry_params(
|
||||
@@ -36,6 +41,13 @@ def create_nft_layer_puzzle_with_curry_params(
|
||||
metadata,
|
||||
metadata_updater_hash,
|
||||
)
|
||||
log.debug(
|
||||
"Currying with: %s %s %s %s",
|
||||
NFT_STATE_LAYER_MOD_HASH,
|
||||
inner_puzzle.get_tree_hash(),
|
||||
metadata_updater_hash,
|
||||
metadata.get_tree_hash(),
|
||||
)
|
||||
return NFT_STATE_LAYER_MOD.curry(NFT_STATE_LAYER_MOD_HASH, metadata, metadata_updater_hash, inner_puzzle)
|
||||
|
||||
|
||||
@@ -70,32 +82,226 @@ def create_full_puzzle(
|
||||
return full_puzzle
|
||||
|
||||
|
||||
def get_nft_info_from_puzzle(puzzle: Program, nft_coin: Coin) -> NFTInfo:
|
||||
def get_nft_info_from_puzzle(nft_coin_info: NFTCoinInfo) -> NFTInfo:
|
||||
"""
|
||||
Extract NFT info from a full puzzle
|
||||
:param puzzle: NFT full puzzle
|
||||
:param nft_coin: NFT coin
|
||||
:param nft_coin_info NFTCoinInfo in local database
|
||||
:return: NFTInfo
|
||||
"""
|
||||
# TODO Update this method after the NFT code finalized
|
||||
uncurried_nft: UncurriedNFT = UncurriedNFT.uncurry(puzzle)
|
||||
data_uris = []
|
||||
uncurried_nft: UncurriedNFT = UncurriedNFT.uncurry(nft_coin_info.full_puzzle)
|
||||
data_uris: List[str] = []
|
||||
|
||||
for uri in uncurried_nft.data_uris.as_python():
|
||||
data_uris.append(str(uri, "utf-8"))
|
||||
meta_uris: List[str] = []
|
||||
for uri in uncurried_nft.meta_uris.as_python():
|
||||
meta_uris.append(str(uri, "utf-8"))
|
||||
license_uris: List[str] = []
|
||||
for uri in uncurried_nft.license_uris.as_python():
|
||||
license_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()),
|
||||
uncurried_nft.singleton_launcher_id,
|
||||
nft_coin_info.coin.name(),
|
||||
uncurried_nft.owner_did,
|
||||
uncurried_nft.trade_price_percentage,
|
||||
uncurried_nft.royalty_address,
|
||||
data_uris,
|
||||
uncurried_nft.data_hash.as_python().hex().upper(),
|
||||
[],
|
||||
"",
|
||||
[],
|
||||
"",
|
||||
"NFT0",
|
||||
uint64(1),
|
||||
uint64(1),
|
||||
uncurried_nft.data_hash.as_python(),
|
||||
meta_uris,
|
||||
uncurried_nft.meta_hash.as_python(),
|
||||
license_uris,
|
||||
uncurried_nft.license_hash.as_python(),
|
||||
uint64(uncurried_nft.series_total.as_int()),
|
||||
uint64(uncurried_nft.series_number.as_int()),
|
||||
uncurried_nft.metadata_updater_hash.as_python(),
|
||||
disassemble(uncurried_nft.metadata),
|
||||
nft_coin_info.mint_height,
|
||||
uncurried_nft.supports_did,
|
||||
nft_coin_info.pending_transaction,
|
||||
)
|
||||
return nft_info
|
||||
|
||||
|
||||
def metadata_to_program(metadata: Dict[bytes, Any]) -> 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((key, value))
|
||||
program: Program = Program.to(kv_list)
|
||||
return program
|
||||
|
||||
|
||||
def program_to_metadata(program: Program) -> Dict[bytes, Any]:
|
||||
"""
|
||||
Convert a program to a metadata dict
|
||||
:param program: Chialisp program contains the metadata
|
||||
:return: Metadata dict
|
||||
"""
|
||||
metadata = {}
|
||||
for kv_pair in program.as_iter():
|
||||
metadata[kv_pair.first().as_atom()] = kv_pair.rest().as_python()
|
||||
return metadata
|
||||
|
||||
|
||||
def prepend_value(key: bytes, value: Program, metadata: Dict[bytes, Any]) -> None:
|
||||
"""
|
||||
Prepend a value to a list in the metadata
|
||||
:param key: Key of the field
|
||||
:param value: Value want to add
|
||||
:param metadata: Metadata
|
||||
:return:
|
||||
"""
|
||||
|
||||
if value != Program.to(0):
|
||||
if metadata[key] == b"":
|
||||
metadata[key] = [value.as_python()]
|
||||
else:
|
||||
metadata[key].insert(0, value.as_python())
|
||||
|
||||
|
||||
def update_metadata(metadata: Program, update_condition: Program) -> Program:
|
||||
"""
|
||||
Apply conditions of metadata updater to the previous metadata
|
||||
:param metadata: Previous metadata
|
||||
:param update_condition: Update metadata conditions
|
||||
:return: Updated metadata
|
||||
"""
|
||||
new_metadata: Dict[bytes, Any] = program_to_metadata(metadata)
|
||||
uri: Program = update_condition.rest().rest().first()
|
||||
prepend_value(uri.first().as_python(), uri.rest(), new_metadata)
|
||||
return metadata_to_program(new_metadata)
|
||||
|
||||
|
||||
def construct_ownership_layer(
|
||||
current_owner: Optional[bytes32],
|
||||
transfer_program: Program,
|
||||
inner_puzzle: Program,
|
||||
) -> Program:
|
||||
return NFT_OWNERSHIP_LAYER.curry(
|
||||
NFT_OWNERSHIP_LAYER.get_tree_hash(),
|
||||
current_owner,
|
||||
transfer_program,
|
||||
inner_puzzle,
|
||||
)
|
||||
|
||||
|
||||
def create_ownership_layer_puzzle(
|
||||
nft_id: bytes32,
|
||||
did_id: bytes,
|
||||
p2_puzzle: Program,
|
||||
percentage: uint16,
|
||||
royalty_puzzle_hash: Optional[bytes32] = None,
|
||||
) -> Program:
|
||||
log.debug(
|
||||
"Creating ownership layer puzzle with NFT_ID: %s DID_ID: %s Royalty_Percentage: %d P2_puzzle: %s",
|
||||
nft_id.hex(),
|
||||
did_id,
|
||||
percentage,
|
||||
p2_puzzle,
|
||||
)
|
||||
singleton_struct = Program.to((SINGLETON_MOD_HASH, (nft_id, LAUNCHER_PUZZLE_HASH)))
|
||||
if not royalty_puzzle_hash:
|
||||
royalty_puzzle_hash = p2_puzzle.get_tree_hash()
|
||||
transfer_program = NFT_TRANSFER_PROGRAM_DEFAULT.curry(
|
||||
singleton_struct,
|
||||
royalty_puzzle_hash,
|
||||
percentage,
|
||||
)
|
||||
nft_inner_puzzle = p2_puzzle
|
||||
|
||||
nft_ownership_layer_puzzle = construct_ownership_layer(
|
||||
bytes32(did_id) if did_id else None, transfer_program, nft_inner_puzzle
|
||||
)
|
||||
return nft_ownership_layer_puzzle
|
||||
|
||||
|
||||
def create_ownership_layer_transfer_solution(
|
||||
new_did: bytes,
|
||||
new_did_inner_hash: bytes,
|
||||
trade_prices_list: List[List[int]],
|
||||
new_puzhash: bytes32,
|
||||
) -> Program:
|
||||
log.debug(
|
||||
"Creating a transfer solution with: DID:%s Inner_puzhash:%s trade_price:%s puzhash:%s",
|
||||
new_did.hex(),
|
||||
new_did_inner_hash.hex(),
|
||||
str(trade_prices_list),
|
||||
new_puzhash.hex(),
|
||||
)
|
||||
condition_list = [
|
||||
[
|
||||
51,
|
||||
new_puzhash,
|
||||
1,
|
||||
[new_puzhash],
|
||||
],
|
||||
[-10, new_did, trade_prices_list, new_did_inner_hash],
|
||||
]
|
||||
log.debug("Condition list raw: %r", condition_list)
|
||||
solution = Program.to(
|
||||
[
|
||||
[solution_for_conditions(condition_list)],
|
||||
]
|
||||
)
|
||||
log.debug("Generated transfer solution: %s", solution)
|
||||
return solution
|
||||
|
||||
|
||||
def get_metadata_and_phs(unft: UncurriedNFT, solution: SerializedProgram) -> Tuple[Program, bytes32]:
|
||||
conditions = unft.p2_puzzle.run(unft.get_innermost_solution(solution.to_program()))
|
||||
metadata = unft.metadata
|
||||
puzhash_for_derivation: Optional[bytes32] = None
|
||||
for condition in conditions.as_iter():
|
||||
if condition.list_len() < 2:
|
||||
# invalid condition
|
||||
continue
|
||||
condition_code = condition.first().as_int()
|
||||
log.debug("Checking condition code: %r", condition_code)
|
||||
if condition_code == -24:
|
||||
# metadata update
|
||||
metadata = update_metadata(metadata, condition)
|
||||
metadata = Program.to(metadata)
|
||||
elif condition_code == 51 and int_from_bytes(condition.rest().rest().first().atom) == 1:
|
||||
# destination puzhash
|
||||
if puzhash_for_derivation is not None:
|
||||
# ignore duplicated create coin conditions
|
||||
continue
|
||||
memo = bytes32(condition.as_python()[-1][0])
|
||||
puzhash_for_derivation = memo
|
||||
log.debug("Got back puzhash from solution: %s", puzhash_for_derivation)
|
||||
assert puzhash_for_derivation
|
||||
return metadata, puzhash_for_derivation
|
||||
|
||||
|
||||
def recurry_nft_puzzle(unft: UncurriedNFT, solution: Program, new_inner_puzzle: Program) -> Program:
|
||||
log.debug("Generating NFT puzzle with ownership support: %s", disassemble(solution))
|
||||
conditions = unft.p2_puzzle.run(unft.get_innermost_solution(solution))
|
||||
new_did_id = unft.owner_did
|
||||
new_puzhash = None
|
||||
for condition in conditions.as_iter():
|
||||
if condition.first().as_int() == -10:
|
||||
# this is the change owner magic condition
|
||||
new_did_id = condition.at("rf").atom
|
||||
elif condition.first().as_int() == 51:
|
||||
new_puzhash = condition.at("rf").atom
|
||||
# assert new_puzhash and new_did_id
|
||||
log.debug(f"Found NFT puzzle details: {new_did_id} {new_puzhash}")
|
||||
assert unft.transfer_program
|
||||
new_ownership_puzzle = construct_ownership_layer(new_did_id, unft.transfer_program, new_inner_puzzle)
|
||||
|
||||
return new_ownership_puzzle
|
||||
|
||||
|
||||
def get_new_owner_did(unft: UncurriedNFT, solution: Program) -> Optional[bytes32]:
|
||||
conditions = unft.p2_puzzle.run(unft.get_innermost_solution(solution))
|
||||
new_did_id = None
|
||||
for condition in conditions.as_iter():
|
||||
if condition.first().as_int() == -10:
|
||||
# this is the change owner magic condition
|
||||
new_did_id = condition.at("rf").atom
|
||||
return new_did_id
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional, Tuple, Union
|
||||
|
||||
from clvm_tools.binutils import disassemble
|
||||
|
||||
from chia.types.blockchain_format.program import Program
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.wallet.puzzle_drivers import PuzzleInfo, Solver
|
||||
from chia.wallet.puzzles.load_clvm import load_clvm
|
||||
|
||||
OWNERSHIP_LAYER_MOD = load_clvm("nft_ownership_layer.clvm")
|
||||
|
||||
|
||||
def match_ownership_layer_puzzle(puzzle: Program) -> Tuple[bool, List[Program]]:
|
||||
mod, args = puzzle.uncurry()
|
||||
if mod == OWNERSHIP_LAYER_MOD:
|
||||
return True, list(args.as_iter())
|
||||
return False, []
|
||||
|
||||
|
||||
def puzzle_for_ownership_layer(
|
||||
current_owner: Union[Program, bytes], transfer_program: Program, inner_puzzle: Program
|
||||
) -> Program:
|
||||
return OWNERSHIP_LAYER_MOD.curry(OWNERSHIP_LAYER_MOD.get_tree_hash(), current_owner, transfer_program, inner_puzzle)
|
||||
|
||||
|
||||
def solution_for_ownership_layer(inner_solution: Program) -> Program:
|
||||
return Program.to([inner_solution]) # type: ignore
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OwnershipOuterPuzzle:
|
||||
_match: Any
|
||||
_asset_id: Any
|
||||
_construct: Any
|
||||
_solve: Any
|
||||
_get_inner_puzzle: Any
|
||||
_get_inner_solution: Any
|
||||
|
||||
def match(self, puzzle: Program) -> Optional[PuzzleInfo]:
|
||||
matched, curried_args = match_ownership_layer_puzzle(puzzle)
|
||||
if matched:
|
||||
_, current_owner, transfer_program, inner_puzzle = curried_args
|
||||
owner_bytes: bytes = current_owner.as_python()
|
||||
tp_match: Optional[PuzzleInfo] = self._match(transfer_program)
|
||||
constructor_dict = {
|
||||
"type": "ownership",
|
||||
"owner": "()" if owner_bytes == b"" else "0x" + owner_bytes.hex(),
|
||||
"transfer_program": (
|
||||
disassemble(transfer_program) if tp_match is None else tp_match.info # type: ignore
|
||||
),
|
||||
}
|
||||
next_constructor = self._match(inner_puzzle)
|
||||
if next_constructor is not None:
|
||||
constructor_dict["also"] = next_constructor.info
|
||||
return PuzzleInfo(constructor_dict)
|
||||
else:
|
||||
return None
|
||||
|
||||
def asset_id(self, constructor: PuzzleInfo) -> Optional[bytes32]:
|
||||
return None
|
||||
|
||||
def construct(self, constructor: PuzzleInfo, inner_puzzle: Program) -> Program:
|
||||
if constructor.also() is not None:
|
||||
inner_puzzle = self._construct(constructor.also(), inner_puzzle)
|
||||
transfer_program_info: Union[PuzzleInfo, Program] = constructor["transfer_program"]
|
||||
if isinstance(transfer_program_info, Program):
|
||||
transfer_program: Program = transfer_program_info
|
||||
else:
|
||||
transfer_program = self._construct(transfer_program_info, inner_puzzle)
|
||||
return puzzle_for_ownership_layer(constructor["owner"], transfer_program, inner_puzzle)
|
||||
|
||||
def get_inner_puzzle(self, constructor: PuzzleInfo, puzzle_reveal: Program) -> Optional[Program]:
|
||||
matched, curried_args = match_ownership_layer_puzzle(puzzle_reveal)
|
||||
if matched:
|
||||
_, _, _, inner_puzzle = curried_args
|
||||
if constructor.also() is not None:
|
||||
deep_inner_puzzle: Optional[Program] = self._get_inner_puzzle(constructor.also(), inner_puzzle)
|
||||
return deep_inner_puzzle
|
||||
else:
|
||||
return inner_puzzle
|
||||
else:
|
||||
raise ValueError("This driver is not for the specified puzzle reveal")
|
||||
|
||||
def get_inner_solution(self, constructor: PuzzleInfo, solution: Program) -> Optional[Program]:
|
||||
my_inner_solution: Program = solution.first()
|
||||
if constructor.also():
|
||||
deep_inner_solution: Optional[Program] = self._get_inner_solution(constructor.also(), my_inner_solution)
|
||||
return deep_inner_solution
|
||||
else:
|
||||
return my_inner_solution
|
||||
|
||||
def solve(self, constructor: PuzzleInfo, solver: Solver, inner_puzzle: Program, inner_solution: Program) -> Program:
|
||||
if constructor.also() is not None:
|
||||
inner_solution = self._solve(constructor.also(), solver, inner_puzzle, inner_solution)
|
||||
return solution_for_ownership_layer(inner_solution)
|
||||
@@ -0,0 +1,87 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
|
||||
from chia.types.blockchain_format.coin import Coin
|
||||
from chia.types.blockchain_format.program import Program
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.types.coin_spend import CoinSpend
|
||||
from chia.util.ints import uint64
|
||||
from chia.wallet.lineage_proof import LineageProof
|
||||
from chia.wallet.puzzle_drivers import PuzzleInfo, Solver
|
||||
from chia.wallet.puzzles.singleton_top_layer_v1_1 import (
|
||||
SINGLETON_LAUNCHER_HASH,
|
||||
match_singleton_puzzle,
|
||||
puzzle_for_singleton,
|
||||
solution_for_singleton,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SingletonOuterPuzzle:
|
||||
_match: Any
|
||||
_asset_id: Any
|
||||
_construct: Any
|
||||
_solve: Any
|
||||
_get_inner_puzzle: Any
|
||||
_get_inner_solution: Any
|
||||
|
||||
def match(self, puzzle: Program) -> Optional[PuzzleInfo]:
|
||||
matched, curried_args = match_singleton_puzzle(puzzle)
|
||||
if matched:
|
||||
singleton_struct, inner_puzzle = curried_args
|
||||
constructor_dict = {
|
||||
"type": "singleton",
|
||||
"launcher_id": "0x" + singleton_struct.as_python()[1].hex(),
|
||||
"launcher_ph": "0x" + singleton_struct.as_python()[2].hex(),
|
||||
}
|
||||
next_constructor = self._match(inner_puzzle)
|
||||
if next_constructor is not None:
|
||||
constructor_dict["also"] = next_constructor.info
|
||||
return PuzzleInfo(constructor_dict)
|
||||
else:
|
||||
return None
|
||||
|
||||
def asset_id(self, constructor: PuzzleInfo) -> Optional[bytes32]:
|
||||
return bytes32(constructor["launcher_id"])
|
||||
|
||||
def construct(self, constructor: PuzzleInfo, inner_puzzle: Program) -> Program:
|
||||
if constructor.also() is not None:
|
||||
inner_puzzle = self._construct(constructor.also(), inner_puzzle)
|
||||
launcher_hash = constructor["launcher_ph"] if "launcher_ph" in constructor else SINGLETON_LAUNCHER_HASH
|
||||
return puzzle_for_singleton(constructor["launcher_id"], inner_puzzle, launcher_hash)
|
||||
|
||||
def get_inner_puzzle(self, constructor: PuzzleInfo, puzzle_reveal: Program) -> Optional[Program]:
|
||||
matched, curried_args = match_singleton_puzzle(puzzle_reveal)
|
||||
if matched:
|
||||
_, inner_puzzle = curried_args
|
||||
if constructor.also() is not None:
|
||||
deep_inner_puzzle: Optional[Program] = self._get_inner_puzzle(constructor.also(), inner_puzzle)
|
||||
return deep_inner_puzzle
|
||||
else:
|
||||
return inner_puzzle
|
||||
else:
|
||||
raise ValueError("This driver is not for the specified puzzle reveal")
|
||||
|
||||
def get_inner_solution(self, constructor: PuzzleInfo, solution: Program) -> Optional[Program]:
|
||||
my_inner_solution: Program = solution.at("rrf")
|
||||
if constructor.also():
|
||||
deep_inner_solution: Optional[Program] = self._get_inner_solution(constructor.also(), my_inner_solution)
|
||||
return deep_inner_solution
|
||||
else:
|
||||
return my_inner_solution
|
||||
|
||||
def solve(self, constructor: PuzzleInfo, solver: Solver, inner_puzzle: Program, inner_solution: Program) -> Program:
|
||||
coin_bytes: bytes = solver["coin"]
|
||||
coin: Coin = Coin(bytes32(coin_bytes[0:32]), bytes32(coin_bytes[32:64]), uint64.from_bytes(coin_bytes[64:72]))
|
||||
parent_spend: CoinSpend = CoinSpend.from_bytes(solver["parent_spend"])
|
||||
parent_coin: Coin = parent_spend.coin
|
||||
if constructor.also() is not None:
|
||||
inner_solution = self._solve(constructor.also(), solver, inner_puzzle, inner_solution)
|
||||
matched, curried_args = match_singleton_puzzle(parent_spend.puzzle_reveal.to_program())
|
||||
assert matched
|
||||
_, parent_inner_puzzle = curried_args
|
||||
return solution_for_singleton(
|
||||
LineageProof(parent_coin.parent_coin_info, parent_inner_puzzle.get_tree_hash(), parent_coin.amount),
|
||||
coin.amount,
|
||||
inner_solution,
|
||||
)
|
||||
@@ -0,0 +1,78 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
from chia.types.blockchain_format.program import Program
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.util.ints import uint16
|
||||
from chia.wallet.puzzle_drivers import PuzzleInfo, Solver
|
||||
from chia.wallet.puzzles.load_clvm import load_clvm
|
||||
from chia.wallet.puzzles.singleton_top_layer_v1_1 import SINGLETON_LAUNCHER_HASH, SINGLETON_MOD_HASH
|
||||
|
||||
TRANSFER_PROGRAM_MOD = load_clvm("nft_ownership_transfer_program_one_way_claim_with_royalties.clvm")
|
||||
|
||||
|
||||
def match_transfer_program_puzzle(puzzle: Program) -> Tuple[bool, List[Program]]:
|
||||
mod, args = puzzle.uncurry()
|
||||
if mod == TRANSFER_PROGRAM_MOD:
|
||||
return True, list(args.as_iter())
|
||||
return False, []
|
||||
|
||||
|
||||
def puzzle_for_transfer_program(launcher_id: bytes32, royalty_puzzle_hash: bytes32, percentage: uint16) -> Program:
|
||||
singleton_struct = Program.to((SINGLETON_MOD_HASH, (launcher_id, SINGLETON_LAUNCHER_HASH)))
|
||||
return TRANSFER_PROGRAM_MOD.curry(
|
||||
singleton_struct,
|
||||
royalty_puzzle_hash,
|
||||
percentage,
|
||||
)
|
||||
|
||||
|
||||
def solution_for_transfer_program(
|
||||
conditions: Program,
|
||||
current_owner: Optional[bytes32],
|
||||
new_did: bytes32,
|
||||
new_did_inner_hash: bytes32,
|
||||
trade_prices_list: Program,
|
||||
) -> Program:
|
||||
return Program.to([conditions, current_owner, [new_did, trade_prices_list, new_did_inner_hash]]) # type: ignore
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TransferProgramPuzzle:
|
||||
_match: Any
|
||||
_asset_id: Any
|
||||
_construct: Any
|
||||
_solve: Any
|
||||
_get_inner_puzzle: Any
|
||||
_get_inner_solution: Any
|
||||
|
||||
def match(self, puzzle: Program) -> Optional[PuzzleInfo]:
|
||||
matched, curried_args = match_transfer_program_puzzle(puzzle)
|
||||
if matched:
|
||||
singleton_struct, royalty_puzzle_hash, percentage = curried_args
|
||||
constructor_dict = {
|
||||
"type": "royalty transfer program",
|
||||
"launcher_id": "0x" + singleton_struct.rest().first().as_python().hex(),
|
||||
"royalty_address": "0x" + royalty_puzzle_hash.as_python().hex(),
|
||||
"royalty_percentage": str(percentage.as_int()),
|
||||
}
|
||||
return PuzzleInfo(constructor_dict)
|
||||
else:
|
||||
return None
|
||||
|
||||
def asset_id(self, constructor: PuzzleInfo) -> Optional[bytes32]:
|
||||
return None
|
||||
|
||||
def construct(self, constructor: PuzzleInfo, inner_puzzle: Program) -> Program:
|
||||
return puzzle_for_transfer_program(
|
||||
constructor["launcher_id"], constructor["royalty_address"], constructor["royalty_percentage"]
|
||||
)
|
||||
|
||||
def get_inner_puzzle(self, constructor: PuzzleInfo, puzzle_reveal: Program) -> Optional[Program]:
|
||||
return None
|
||||
|
||||
def get_inner_solution(self, constructor: PuzzleInfo, solution: Program) -> Optional[Program]:
|
||||
return None
|
||||
|
||||
def solve(self, constructor: PuzzleInfo, solver: Solver, inner_puzzle: Program, inner_solution: Program) -> Program:
|
||||
return Program.to(None) # type: ignore
|
||||
@@ -1,13 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Type, TypeVar
|
||||
from typing import Optional, Type, TypeVar
|
||||
|
||||
from chia.types.blockchain_format.program import Program
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.util.ints import uint16
|
||||
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")
|
||||
NFT_MOD = load_clvm("nft_state_layer.clvm")
|
||||
NFT_OWNERSHIP_LAYER = load_clvm("nft_ownership_layer.clvm")
|
||||
|
||||
_T_UncurriedNFT = TypeVar("_T_UncurriedNFT", bound="UncurriedNFT")
|
||||
|
||||
@@ -20,7 +25,7 @@ class UncurriedNFT:
|
||||
This is the only place you need to change after modified the Chialisp curried parameters.
|
||||
"""
|
||||
|
||||
nft_mod_hash: Program
|
||||
nft_mod_hash: bytes32
|
||||
"""NFT module hash"""
|
||||
|
||||
nft_state_layer: Program
|
||||
@@ -32,28 +37,12 @@ class UncurriedNFT:
|
||||
[singleton_mod_hash, singleton_launcher_id, launcher_puzhash]
|
||||
"""
|
||||
singleton_mod_hash: Program
|
||||
singleton_launcher_id: Program
|
||||
singleton_launcher_id: bytes32
|
||||
launcher_puzhash: Program
|
||||
|
||||
owner_did: Program
|
||||
"""Owner's DID"""
|
||||
|
||||
metdata_updater_hash: Program
|
||||
metadata_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
|
||||
@@ -61,10 +50,40 @@ class UncurriedNFT:
|
||||
"""
|
||||
data_uris: Program
|
||||
data_hash: Program
|
||||
meta_uris: Program
|
||||
meta_hash: Program
|
||||
license_uris: Program
|
||||
license_hash: Program
|
||||
series_number: Program
|
||||
series_total: Program
|
||||
|
||||
inner_puzzle: Program
|
||||
"""NFT state layer inner puzzle"""
|
||||
|
||||
p2_puzzle: Program
|
||||
"""p2 puzzle of the owner, either for ownership layer or standard"""
|
||||
|
||||
# ownership layer fields
|
||||
owner_did: Optional[bytes32]
|
||||
"""Owner's DID"""
|
||||
|
||||
supports_did: bool
|
||||
"""If the inner puzzle support the DID"""
|
||||
|
||||
nft_inner_puzzle_hash: Optional[bytes32]
|
||||
"""Puzzle hash of the ownership layer inner puzzle """
|
||||
|
||||
transfer_program: Optional[Program]
|
||||
"""Puzzle hash of the transfer program"""
|
||||
|
||||
transfer_program_curry_params: Optional[Program]
|
||||
"""
|
||||
Curried parameters of the transfer program
|
||||
[royalty_address, trade_price_percentage, settlement_mod_hash, cat_mod_hash]
|
||||
"""
|
||||
royalty_address: Optional[bytes32]
|
||||
trade_price_percentage: Optional[uint16]
|
||||
|
||||
@classmethod
|
||||
def uncurry(cls: Type[_T_UncurriedNFT], puzzle: Program) -> UncurriedNFT:
|
||||
"""
|
||||
@@ -89,14 +108,56 @@ class UncurriedNFT:
|
||||
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()
|
||||
|
||||
nft_mod_hash, metadata, metadata_updater_hash, inner_puzzle = curried_args.as_iter()
|
||||
data_uris = Program.to([])
|
||||
data_hash = Program.to(0)
|
||||
meta_uris = Program.to([])
|
||||
meta_hash = Program.to(0)
|
||||
license_uris = Program.to([])
|
||||
license_hash = Program.to(0)
|
||||
series_number = Program.to(1)
|
||||
series_total = Program.to(1)
|
||||
# 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()
|
||||
if kv_pair.first().as_atom() == b"mu":
|
||||
meta_uris = kv_pair.rest()
|
||||
if kv_pair.first().as_atom() == b"mh":
|
||||
meta_hash = kv_pair.rest()
|
||||
if kv_pair.first().as_atom() == b"lu":
|
||||
license_uris = kv_pair.rest()
|
||||
if kv_pair.first().as_atom() == b"lh":
|
||||
license_hash = kv_pair.rest()
|
||||
if kv_pair.first().as_atom() == b"sn":
|
||||
series_number = kv_pair.rest()
|
||||
if kv_pair.first().as_atom() == b"st":
|
||||
series_total = kv_pair.rest()
|
||||
current_did = None
|
||||
transfer_program = None
|
||||
transfer_program_args = None
|
||||
royalty_address = None
|
||||
royalty_percentage = None
|
||||
nft_inner_puzzle_mod = None
|
||||
mod, ol_args = inner_puzzle.uncurry()
|
||||
supports_did = False
|
||||
if mod == NFT_OWNERSHIP_LAYER:
|
||||
supports_did = True
|
||||
log.debug("Parsing ownership layer")
|
||||
_, current_did, transfer_program, p2_puzzle = ol_args.as_iter()
|
||||
transfer_program_mod, transfer_program_args = transfer_program.uncurry()
|
||||
_, royalty_address_p, royalty_percentage = transfer_program_args.as_iter()
|
||||
royalty_percentage = uint16(royalty_percentage.as_int())
|
||||
royalty_address = royalty_address_p.atom
|
||||
current_did = current_did.atom
|
||||
if current_did == b"":
|
||||
# For unassigned NFT, set owner DID to None
|
||||
current_did = None
|
||||
else:
|
||||
log.debug("Creating a standard NFT puzzle")
|
||||
p2_puzzle = inner_puzzle
|
||||
except Exception as e:
|
||||
raise ValueError(f"Cannot uncurry NFT state layer: Args {curried_args}") from e
|
||||
return cls(
|
||||
@@ -104,19 +165,32 @@ class UncurriedNFT:
|
||||
nft_state_layer=nft_state_layer,
|
||||
singleton_struct=singleton_struct,
|
||||
singleton_mod_hash=singleton_mod_hash,
|
||||
singleton_launcher_id=singleton_launcher_id,
|
||||
singleton_launcher_id=singleton_launcher_id.atom,
|
||||
launcher_puzhash=launcher_puzhash,
|
||||
metadata=metadata,
|
||||
data_uris=data_uris,
|
||||
data_hash=data_hash,
|
||||
metdata_updater_hash=metdata_updater_hash,
|
||||
p2_puzzle=p2_puzzle,
|
||||
metadata_updater_hash=metadata_updater_hash,
|
||||
meta_uris=meta_uris,
|
||||
meta_hash=meta_hash,
|
||||
license_uris=license_uris,
|
||||
license_hash=license_hash,
|
||||
series_number=series_number,
|
||||
series_total=series_total,
|
||||
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([]),
|
||||
owner_did=current_did,
|
||||
supports_did=supports_did,
|
||||
transfer_program=transfer_program,
|
||||
transfer_program_curry_params=transfer_program_args,
|
||||
royalty_address=royalty_address,
|
||||
trade_price_percentage=royalty_percentage,
|
||||
nft_inner_puzzle_hash=nft_inner_puzzle_mod,
|
||||
)
|
||||
|
||||
def get_innermost_solution(self, solution: Program) -> Program:
|
||||
state_layer_inner_solution: Program = solution.at("rrff")
|
||||
if self.supports_did:
|
||||
return state_layer_inner_solution.first() # type: ignore
|
||||
else:
|
||||
return state_layer_inner_solution
|
||||
|
||||
@@ -4,6 +4,10 @@ from typing import Any, Dict, Optional
|
||||
from chia.types.blockchain_format.program import Program
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.wallet.cat_wallet.cat_outer_puzzle import CATOuterPuzzle
|
||||
from chia.wallet.nft_wallet.metadata_outer_puzzle import MetadataOuterPuzzle
|
||||
from chia.wallet.nft_wallet.ownership_outer_puzzle import OwnershipOuterPuzzle
|
||||
from chia.wallet.nft_wallet.singleton_outer_puzzle import SingletonOuterPuzzle
|
||||
from chia.wallet.nft_wallet.transfer_program_puzzle import TransferProgramPuzzle
|
||||
from chia.wallet.puzzle_drivers import PuzzleInfo, Solver
|
||||
|
||||
"""
|
||||
@@ -12,6 +16,8 @@ This file provides a central location for acquiring drivers for outer puzzles li
|
||||
A driver for a puzzle must include the following functions:
|
||||
- match(self, puzzle: Program) -> Optional[PuzzleInfo]
|
||||
- Given a puzzle reveal, return a PuzzleInfo object that can be used to reconstruct it later
|
||||
- get_inner_puzzle(self, constructor: PuzzleInfo, puzzle_reveal: Program) -> Optional[Program]:
|
||||
- Given a PuzzleInfo object and a puzzle reveal, pull out this outer puzzle's inner puzzle
|
||||
- asset_id(self, constructor: PuzzleInfo) -> Optional[bytes32]
|
||||
- Given a PuzzleInfo object, generate a 32 byte ID for use in dictionaries, etc.
|
||||
- construct(self, constructor: PuzzleInfo, inner_puzzle: Program) -> Program
|
||||
@@ -26,6 +32,10 @@ A driver for a puzzle must include the following functions:
|
||||
|
||||
class AssetType(Enum):
|
||||
CAT = "CAT"
|
||||
SINGLETON = "singleton"
|
||||
METADATA = "metadata"
|
||||
OWNERSHIP = "ownership"
|
||||
ROYALTY_TRANSFER_PROGRAM = "royalty transfer program"
|
||||
|
||||
|
||||
def match_puzzle(puzzle: Program) -> Optional[PuzzleInfo]:
|
||||
@@ -46,12 +56,24 @@ def solve_puzzle(constructor: PuzzleInfo, solver: Solver, inner_puzzle: Program,
|
||||
)
|
||||
|
||||
|
||||
def get_inner_puzzle(constructor: PuzzleInfo, puzzle_reveal: Program) -> Optional[Program]:
|
||||
return driver_lookup[AssetType(constructor.type())].get_inner_puzzle(constructor, puzzle_reveal) # type: ignore
|
||||
|
||||
|
||||
def get_inner_solution(constructor: PuzzleInfo, solution: Program) -> Optional[Program]:
|
||||
return driver_lookup[AssetType(constructor.type())].get_inner_solution(constructor, solution) # type: ignore
|
||||
|
||||
|
||||
def create_asset_id(constructor: PuzzleInfo) -> bytes32:
|
||||
return driver_lookup[AssetType(constructor.type())].asset_id(constructor) # type: ignore
|
||||
|
||||
|
||||
function_args = [match_puzzle, construct_puzzle, solve_puzzle, create_asset_id]
|
||||
function_args = [match_puzzle, create_asset_id, construct_puzzle, solve_puzzle, get_inner_puzzle, get_inner_solution]
|
||||
|
||||
driver_lookup: Dict[AssetType, Any] = {
|
||||
AssetType.CAT: CATOuterPuzzle(*function_args),
|
||||
AssetType.SINGLETON: SingletonOuterPuzzle(*function_args),
|
||||
AssetType.METADATA: MetadataOuterPuzzle(*function_args),
|
||||
AssetType.OWNERSHIP: OwnershipOuterPuzzle(*function_args),
|
||||
AssetType.ROYALTY_TRANSFER_PROGRAM: TransferProgramPuzzle(*function_args),
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from clvm.casts import int_from_bytes
|
||||
from clvm.SExp import SExp
|
||||
@@ -42,6 +42,12 @@ class PuzzleInfo:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __contains__(self, item: str) -> bool:
|
||||
if item in self.info:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def type(self) -> str:
|
||||
return str(self.info["type"])
|
||||
|
||||
@@ -51,6 +57,22 @@ class PuzzleInfo:
|
||||
else:
|
||||
return None
|
||||
|
||||
def check_type(self, types: List[str]) -> bool:
|
||||
if types == []:
|
||||
if self.also() is None:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
if self.type() == types[0]:
|
||||
types.pop(0)
|
||||
if self.also():
|
||||
return self.also().check_type(types) # type: ignore
|
||||
else:
|
||||
return self.check_type(types)
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Solver:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ff02ffff01ff04ffff04ff38ffff04ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff2effff04ff02ffff04ff17ffff04ffff02ff26ffff04ff02ffff04ff2fff80808080ff8080808080ff8080808080ffff04ff27ff80808080ffff02ff32ffff04ff02ffff04ff0bffff04ff2fffff01ff01808080808080ffff04ffff01ffffff32ff0233ffff0401ff0102ffffffff02ffff03ff05ffff01ff02ff22ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff0bffff01ff04ffff04ff38ffff04ffff02ff2affff04ff02ffff04ff05ffff04ffff04ffff04ff17ff1380ff8080ff8080808080ffff01ff80808080ffff04ffff04ff10ffff04ff23ffff04ffff02ff3effff04ff02ffff04ffff04ff13ffff04ff17ff808080ff80808080ff80808080ffff02ff32ffff04ff02ffff04ff05ffff04ff1bffff04ffff10ff17ffff010180ff8080808080808080ff8080ff0180ffff02ff36ffff04ff02ffff04ff05ffff04ffff02ff3effff04ff02ffff04ff0bff80808080ff8080808080ff02ff36ffff04ff02ffff04ff05ffff04ffff02ff3effff04ff02ffff04ff0bff80808080ffff04ffff02ff3effff04ff02ffff04ff05ff80808080ff808080808080ffffff02ffff03ffff07ff0580ffff01ff04ff29ffff02ff26ffff04ff02ffff04ff0dff8080808080ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff22ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0b80ffff01ff10ff13ffff02ff2effff04ff02ffff04ff05ffff04ff1bff808080808080ffff011580ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080
|
||||
@@ -69,4 +69,24 @@
|
||||
(tree-hash-of-apply function-hash
|
||||
(build-curry-list reversed-curry-parameter-hashes (sha256 ONE ONE)))
|
||||
)
|
||||
|
||||
(defconstant b32 32)
|
||||
|
||||
(defun-inline size_b32 (var)
|
||||
(= (strlen var) b32)
|
||||
)
|
||||
|
||||
(defun calculate_coin_id (parent puzzlehash amount)
|
||||
(if (all (size_b32 parent) (size_b32 puzzlehash) (> amount -1))
|
||||
(sha256 parent puzzlehash amount)
|
||||
(x)
|
||||
)
|
||||
)
|
||||
|
||||
; 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 1 TREE)))
|
||||
|
||||
)
|
||||
|
||||
@@ -17,14 +17,6 @@
|
||||
(include condition_codes.clvm)
|
||||
(include curry-and-treehash.clinc)
|
||||
|
||||
(defun sha256tree
|
||||
(TREE)
|
||||
(if (l TREE)
|
||||
(sha256 2 (sha256tree (f TREE)) (sha256tree (r TREE)))
|
||||
(sha256 1 TREE)
|
||||
)
|
||||
)
|
||||
|
||||
(defun-inline tree_branch_hash (left right) (sha256 2 left right))
|
||||
|
||||
(defun calculate_merkle_root (path current_hash additional_steps)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ff02ffff01ff04ffff04ffff013effff04ffff02ff02ffff04ff02ffff04ff05ff80808080ff808080ff8080ffff04ffff01ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff02ffff04ff02ffff04ff09ff80808080ffff02ff02ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080
|
||||
@@ -1,85 +0,0 @@
|
||||
(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)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
)
|
||||
@@ -1 +0,0 @@
|
||||
ff02ffff01ff04ffff04ff10ffff04ff12ff808080ffff02ffff03ff8202ffffff01ff02ffff03ffff09ffff02ff3effff04ff02ffff04ff820bffff80808080ff2f80ffff01ff04ffff04ff28ffff04ffff0bffff02ff2effff04ff02ffff04ff13ffff04ff82017fffff04ffff02ff3effff04ff02ffff04ffff04ff13ffff04ff17ff3b8080ff80808080ff808080808080ffff02ff3effff04ff02ffff04ffff04ff8217ffffff04ff8202ffff808080ff8080808080ff808080ffff02ff36ffff04ff02ffff04ff05ffff04ff0bffff04ff2fffff04ff5fffff04ff81bfffff04ff8202ffffff04ffff02ff820bffffff04ff81bfffff04ff5fffff04ff0bffff04ff17ffff04ff82017fffff04ff8202ffffff04ff8205ffffff04ff8217ffff80808080808080808080ffff01ff808080808080808080808080ffff01ff088080ff0180ffff01ff04ffff04ff14ffff04ffff02ff2effff04ff02ffff04ff05ffff04ffff02ff3effff04ff02ffff04ff81bfff80808080ffff04ffff02ff3effff04ff02ffff04ff5fff80808080ffff04ffff0bff12ff2f80ffff04ffff0bff12ff1780ffff04ffff02ff3effff04ff02ffff04ff0bff80808080ffff04ffff0bff12ff0580ff80808080808080808080ffff04ff12ffff04ffff04ff17ff8080ff8080808080ffff04ffff04ff28ffff04ffff0bffff02ff2effff04ff02ffff04ff13ffff04ff82017fffff04ffff02ff3effff04ff02ffff04ffff04ff13ffff04ff17ff3b8080ff80808080ff808080808080ffff016180ff808080ffff04ffff04ff2cffff04ff17ff808080ff8080808080ff018080ffff04ffff01ffffff49ff3f02ff33ff3e04ffff01ff0102ffffff02ffff03ff05ffff01ff02ff26ffff04ff02ffff04ff0dffff04ffff0bff3affff0bff12ff3c80ffff0bff3affff0bff3affff0bff12ff2a80ff0980ffff0bff3aff0bffff0bff12ff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff82017fffff01ff02ffff03ffff09ff82047fffff0181ea80ffff01ff04ffff04ff14ffff04ffff02ff2effff04ff02ffff04ff05ffff04ffff02ff3effff04ff02ffff04ff820a7fff80808080ffff04ffff02ff3effff04ff02ffff04ff82167fff80808080ffff04ffff0bff12ff1780ffff04ffff0bff12ff81bf80ffff04ffff02ff3effff04ff02ffff04ff0bff80808080ffff04ffff0bff12ff0580ff80808080808080808080ffff04ff12ffff04ffff04ff81bfff8080ff8080808080ffff02ff36ffff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82037fffff01ff018080808080808080808080ffff01ff04ff82027fffff02ff36ffff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82037fffff04ff8202ffff80808080808080808080808080ff0180ffff01ff02ffff03ff8202ffff80ffff01ff04ffff04ff14ffff04ffff02ff2effff04ff02ffff04ff05ffff04ffff02ff3effff04ff02ffff04ff5fff80808080ffff04ffff02ff3effff04ff02ffff04ff2fff80808080ffff04ffff0bff12ff1780ffff04ffff0bff12ff81bf80ffff04ffff02ff3effff04ff02ffff04ff0bff80808080ffff04ffff0bff12ff0580ff80808080808080808080ffff04ff12ffff04ffff04ff81bfff8080ff8080808080ff808080ff018080ff0180ffff0bff3affff0bff12ff3880ffff0bff3affff0bff3affff0bff12ff2a80ff0580ffff0bff3affff02ff26ffff04ff02ffff04ff07ffff04ffff0bff12ff1280ff8080808080ffff0bff12ff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bff12ff058080ff0180ff018080
|
||||
@@ -1 +0,0 @@
|
||||
ae8ee2bf9e0bcc189d4d8efa3c8281235981abd75ade592ba71930aa6881f820
|
||||
@@ -1,20 +0,0 @@
|
||||
(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)
|
||||
)
|
||||
)
|
||||
@@ -1 +0,0 @@
|
||||
ff02ffff01ff02ffff03ff0bffff01ff04ffff02ff02ffff04ff02ffff04ff05ffff04ff0bff8080808080ffff01ff808080ffff01ff04ff05ffff01ff80808080ff0180ffff04ffff01ff02ffff03ff05ffff01ff02ffff03ffff09ff11ffff017580ffff01ff04ffff04ffff0175ffff04ff0bff198080ff0d80ffff01ff04ff09ffff02ff02ffff04ff02ffff04ff0dffff04ff0bff80808080808080ff0180ff8080ff0180ff018080
|
||||
@@ -1,21 +1,30 @@
|
||||
(mod (CURRENT_METADATA METADATA_UPDATER_PUZZLE_HASH solution)
|
||||
(mod (CURRENT_METADATA METADATA_UPDATER_PUZZLE_HASH (key . new_url))
|
||||
|
||||
; 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)
|
||||
; Add uri to a field
|
||||
(defun add_url (METADATA key 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))
|
||||
(if (= (f (f METADATA)) key)
|
||||
(c (c key (c new_url (r (f METADATA)))) (r METADATA))
|
||||
(c (f METADATA) (add_url (r METADATA) key 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)
|
||||
(list
|
||||
(list
|
||||
(if (all key new_url)
|
||||
(if (any (= key "mu") (= key "lu") (= key "u"))
|
||||
(add_url CURRENT_METADATA key new_url)
|
||||
CURRENT_METADATA
|
||||
)
|
||||
CURRENT_METADATA
|
||||
)
|
||||
METADATA_UPDATER_PUZZLE_HASH)
|
||||
0
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1 +1 @@
|
||||
ff02ffff01ff04ffff04ffff02ffff03ff17ffff01ff02ff02ffff04ff02ffff04ff05ffff04ff17ff8080808080ffff010580ff0180ffff04ff0bff808080ffff01ff808080ffff04ffff01ff02ffff03ff05ffff01ff02ffff03ffff09ff11ffff017580ffff01ff04ffff04ffff0175ffff04ff0bff198080ff0d80ffff01ff04ff09ffff02ff02ffff04ff02ffff04ff0dffff04ff0bff80808080808080ff0180ff8080ff0180ff018080
|
||||
ff02ffff01ff04ffff04ffff02ffff03ffff22ff27ff3780ffff01ff02ffff03ffff21ffff09ff27ffff01826d7580ffff09ff27ffff01826c7580ffff09ff27ffff01758080ffff01ff02ff02ffff04ff02ffff04ff05ffff04ff27ffff04ff37ff808080808080ffff010580ff0180ffff010580ff0180ffff04ff0bff808080ffff01ff808080ffff04ffff01ff02ffff03ff05ffff01ff02ffff03ffff09ff11ff0b80ffff01ff04ffff04ff0bffff04ff17ff198080ff0d80ffff01ff04ff09ffff02ff02ffff04ff02ffff04ff0dffff04ff0bffff04ff17ff8080808080808080ff0180ff8080ff0180ff018080
|
||||
|
||||
@@ -1 +1 @@
|
||||
81970d352e6a39a241eaf8ca510a0e669e40d778ba612621c60a50ef6cf29c7b
|
||||
fe8a4b4e27a2e29a4d3fc7ce9d527adbcaccbab6ada3903ccf3ba9a769d2d78b
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
(mod (CURRENT_METADATA METADATA_UPDATER_PUZZLE_HASH solution)
|
||||
|
||||
; solution is (new_url new_metadata_updater_puzhash)
|
||||
|
||||
; 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)
|
||||
|
||||
; NOTE THIS PROGRAM IS FOR TESTING ONLY - USE IN DEPLOYMENT AT YOUR OWN PERIL
|
||||
|
||||
; 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-inline assert_bytes32 (value)
|
||||
(= (strlen value) 32)
|
||||
)
|
||||
|
||||
; main
|
||||
; returns ((new_metadata new_metadata_updater_puzhash) conditions)
|
||||
(list (list (if (f solution) (add_url CURRENT_METADATA (f solution)) CURRENT_METADATA) (if (assert_bytes32 (f (r solution))) (f (r solution)) METADATA_UPDATER_PUZZLE_HASH)) 0)
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
ff02ffff01ff04ffff04ffff02ffff03ff27ffff01ff02ff02ffff04ff02ffff04ff05ffff04ff27ff8080808080ffff010580ff0180ffff04ffff02ffff03ffff09ffff0dff5780ffff012080ffff0157ffff010b80ff0180ff808080ffff01ff808080ffff04ffff01ff02ffff03ff05ffff01ff02ffff03ffff09ff11ffff017580ffff01ff04ffff04ffff0175ffff04ff0bff198080ff0d80ffff01ff04ff09ffff02ff02ffff04ff02ffff04ff0dffff04ff0bff80808080808080ff0180ff8080ff0180ff018080
|
||||
@@ -0,0 +1 @@
|
||||
0b1ffba1601777c06b78ab38636e9624f2f8da73be9b36e0ce17c8d8ef3bad9f
|
||||
@@ -0,0 +1,100 @@
|
||||
(mod (
|
||||
NFT_OWNERSHIP_LAYER_MOD_HASH
|
||||
CURRENT_OWNER
|
||||
TRANSFER_PROGRAM
|
||||
INNER_PUZZLE
|
||||
inner_solution
|
||||
)
|
||||
|
||||
(include condition_codes.clvm)
|
||||
(include curry-and-treehash.clinc)
|
||||
(include utility_macros.clib)
|
||||
|
||||
(defconstant NEW_OWNER_CONDITION -10)
|
||||
(defconstant ANNOUNCEMENT_PREFIX 0xad4c) ; first 2 bytes of (sha256 "Ownership Layer")
|
||||
|
||||
(defun-inline nft_ownership_layer_puzzle_hash (NFT_OWNERSHIP_LAYER_MOD_HASH new_owner TRANSFER_PROGRAM inner_puzzle_hash)
|
||||
(puzzle-hash-of-curried-function NFT_OWNERSHIP_LAYER_MOD_HASH
|
||||
inner_puzzle_hash
|
||||
(sha256tree TRANSFER_PROGRAM)
|
||||
(sha256 ONE new_owner)
|
||||
(sha256 ONE NFT_OWNERSHIP_LAYER_MOD_HASH)
|
||||
)
|
||||
)
|
||||
|
||||
(defun construct_end_conditions (NFT_OWNERSHIP_LAYER_MOD_HASH TRANSFER_PROGRAM odd_args (new_owner new_tp conditions))
|
||||
(c
|
||||
(c
|
||||
CREATE_COIN
|
||||
(c
|
||||
(nft_ownership_layer_puzzle_hash NFT_OWNERSHIP_LAYER_MOD_HASH new_owner (if new_tp new_tp TRANSFER_PROGRAM) (f odd_args))
|
||||
(r odd_args)
|
||||
)
|
||||
)
|
||||
conditions
|
||||
)
|
||||
)
|
||||
|
||||
(defun wrap_odd_create_coins (NFT_OWNERSHIP_LAYER_MOD_HASH TRANSFER_PROGRAM CURRENT_OWNER all_conditions conditions odd_args tp_output)
|
||||
(if conditions
|
||||
(if (= (f (f conditions)) CREATE_COIN)
|
||||
(if (= (logand (f (r (r (f conditions))))) ONE)
|
||||
(assert (not odd_args)
|
||||
; then
|
||||
(wrap_odd_create_coins NFT_OWNERSHIP_LAYER_MOD_HASH TRANSFER_PROGRAM CURRENT_OWNER all_conditions (r conditions) (r (f conditions)) tp_output)
|
||||
)
|
||||
(c (f conditions) (wrap_odd_create_coins NFT_OWNERSHIP_LAYER_MOD_HASH TRANSFER_PROGRAM CURRENT_OWNER all_conditions (r conditions) odd_args tp_output))
|
||||
)
|
||||
(if (= (f (f conditions)) NEW_OWNER_CONDITION)
|
||||
(assert (not tp_output)
|
||||
(c
|
||||
(list CREATE_PUZZLE_ANNOUNCEMENT (concat ANNOUNCEMENT_PREFIX (sha256tree (r (f conditions)))))
|
||||
(wrap_odd_create_coins NFT_OWNERSHIP_LAYER_MOD_HASH TRANSFER_PROGRAM CURRENT_OWNER all_conditions (r conditions) odd_args (a TRANSFER_PROGRAM (list CURRENT_OWNER all_conditions (r (f conditions)))))
|
||||
)
|
||||
)
|
||||
(if (= (f (f conditions)) CREATE_PUZZLE_ANNOUNCEMENT)
|
||||
(assert (not (and
|
||||
(= 34 (strlen (f (r (f conditions)))))
|
||||
(= (substr (f (r (f conditions))) 0 2) ANNOUNCEMENT_PREFIX) ; lazy eval
|
||||
))
|
||||
; then
|
||||
(c (f conditions) (wrap_odd_create_coins NFT_OWNERSHIP_LAYER_MOD_HASH TRANSFER_PROGRAM CURRENT_OWNER all_conditions (r conditions) odd_args tp_output))
|
||||
)
|
||||
(c (f conditions) (wrap_odd_create_coins NFT_OWNERSHIP_LAYER_MOD_HASH TRANSFER_PROGRAM CURRENT_OWNER all_conditions (r conditions) odd_args tp_output))
|
||||
)
|
||||
)
|
||||
)
|
||||
; odd_args is guaranteed to not be nil or else we'll have a path into atom error
|
||||
(construct_end_conditions NFT_OWNERSHIP_LAYER_MOD_HASH TRANSFER_PROGRAM odd_args
|
||||
(if tp_output
|
||||
tp_output
|
||||
(a TRANSFER_PROGRAM (list CURRENT_OWNER all_conditions ()))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
(defun main (
|
||||
NFT_OWNERSHIP_LAYER_MOD_HASH
|
||||
TRANSFER_PROGRAM
|
||||
CURRENT_OWNER
|
||||
conditions
|
||||
)
|
||||
(wrap_odd_create_coins
|
||||
NFT_OWNERSHIP_LAYER_MOD_HASH
|
||||
TRANSFER_PROGRAM
|
||||
CURRENT_OWNER
|
||||
conditions
|
||||
conditions
|
||||
() ()
|
||||
)
|
||||
)
|
||||
|
||||
; main
|
||||
(main
|
||||
NFT_OWNERSHIP_LAYER_MOD_HASH
|
||||
TRANSFER_PROGRAM
|
||||
CURRENT_OWNER
|
||||
(a INNER_PUZZLE inner_solution)
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
ff02ffff01ff02ff26ffff04ff02ffff04ff05ffff04ff17ffff04ff0bffff04ffff02ff2fff5f80ff80808080808080ffff04ffff01ffffff82ad4cff0233ffff3e04ff81f601ffffff0102ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff32ffff0bff3cff3480ffff0bff32ffff0bff32ffff0bff3cff2280ff0980ffff0bff32ff0bffff0bff3cff8080808080ff8080808080ffff010b80ff0180ff04ffff04ff38ffff04ffff02ff36ffff04ff02ffff04ff05ffff04ff27ffff04ffff02ff2effff04ff02ffff04ffff02ffff03ff81afffff0181afffff010b80ff0180ff80808080ffff04ffff0bff3cff4f80ffff04ffff0bff3cff0580ff8080808080808080ff378080ff82016f80ffffff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff2fffff01ff80ff808080808080808080ff0bff32ffff0bff3cff2880ffff0bff32ffff0bff32ffff0bff3cff2280ff0580ffff0bff32ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff3cff3c80ff8080808080ffff0bff3cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff5fffff01ff02ffff03ffff09ff82011fff3880ffff01ff02ffff03ffff09ffff18ff82059f80ff3c80ffff01ff02ffff03ffff20ff81bf80ffff01ff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff82019fffff04ff82017fff80808080808080808080ffff01ff088080ff0180ffff01ff04ff819fffff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff81bfffff04ff82017fff808080808080808080808080ff0180ffff01ff02ffff03ffff09ff82011fff2c80ffff01ff02ffff03ffff20ff82017f80ffff01ff04ffff04ff24ffff04ffff0eff10ffff02ff2effff04ff02ffff04ff82019fff8080808080ff808080ffff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff81bfffff04ffff02ff0bffff04ff17ffff04ff2fffff04ff82019fff8080808080ff8080808080808080808080ffff01ff088080ff0180ffff01ff02ffff03ffff09ff82011fff2480ffff01ff02ffff03ffff20ffff02ffff03ffff09ffff0122ffff0dff82029f8080ffff01ff02ffff03ffff09ffff0cff82029fff80ffff010280ff1080ffff01ff0101ff8080ff0180ff8080ff018080ffff01ff04ff819fffff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff81bfffff04ff82017fff8080808080808080808080ffff01ff088080ff0180ffff01ff04ff819fffff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff81dfffff04ff81bfffff04ff82017fff808080808080808080808080ff018080ff018080ff0180ffff01ff02ff3affff04ff02ffff04ff05ffff04ff0bffff04ff81bfffff04ffff02ffff03ff82017fffff0182017fffff01ff02ff0bffff04ff17ffff04ff2fffff01ff808080808080ff0180ff8080808080808080ff0180ff018080
|
||||
@@ -0,0 +1 @@
|
||||
c5abea79afaa001b5427dfa0c8cf42ca6f38f5841b78f9b3c252733eb2de2726
|
||||
@@ -1,83 +0,0 @@
|
||||
(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)))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -1 +0,0 @@
|
||||
ff02ffff01ff04ffff04ffff04ff38ffff04ff81bfff808080ffff04ffff04ff10ffff04ffff0bffff02ff36ffff04ff02ffff04ff09ffff04ff5fffff04ffff02ff3effff04ff02ffff04ffff04ff09ffff04ff17ff1d8080ff80808080ff808080808080ff15ffff02ff3effff04ff02ffff01ff95726f79616c74795f70726f675f736f6c7574696f6e808080ff81bf80ff808080ffff04ffff04ff10ffff04ffff0bffff02ff36ffff04ff02ffff04ff09ffff04ff82017fffff04ffff02ff3effff04ff02ffff04ffff04ff09ffff04ff81bfff1d8080ff80808080ff808080808080ff15ffff02ff3effff04ff02ffff01ff95726f79616c74795f70726f675f736f6c7574696f6e80808080ff808080ffff02ff26ffff04ff02ffff04ff0bffff04ff8202ffffff04ff15ff808080808080808080ff8080ffff04ffff01ffffff3fff0281eaffff3304ff0101ffff822710ff02ff02ffff03ff05ffff01ff02ff3affff04ff02ffff04ff0dffff04ffff0bff2affff0bff2cff3480ffff0bff2affff0bff2affff0bff2cff3c80ff0980ffff0bff2aff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffffff02ffff03ff0bffff01ff04ffff02ffff03ff33ffff01ff04ff10ffff04ffff0bffff02ff36ffff04ff02ffff04ff5dffff04ff2dffff04ffff0bffff0101ff5380ffff04ffff0bffff0101ff5d80ff80808080808080ffff02ff3effff04ff02ffff04ffff04ff17ffff04ffff04ff09ffff04ffff05ffff14ffff12ff23ff1580ff128080ff808080ff808080ff8080808080ff808080ffff01ff04ff24ffff04ff09ffff04ffff02ff2effff04ff02ffff04ffff05ffff14ffff12ff23ff1580ff128080ff80808080ff8080808080ff0180ffff02ff26ffff04ff02ffff04ffff04ff09ffff04ff15ffff04ff2dffff04ff5dff8080808080ffff04ff1bffff04ff17ff80808080808080ff8080ff0180ff0bff2affff0bff2cff2880ffff0bff2affff0bff2affff0bff2cff3c80ff0580ffff0bff2affff02ff3affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff18ff05ffff010180ffff01ff11ff05ffff010180ffff010580ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080
|
||||
@@ -1 +0,0 @@
|
||||
03557552e0bf8b200d8be8089f54786195ab8c07e3e07c39641f2f4f1fc2e48e
|
||||
@@ -0,0 +1,78 @@
|
||||
(mod
|
||||
(
|
||||
SINGLETON_STRUCT
|
||||
ROYALTY_ADDRESS
|
||||
TRADE_PRICE_PERCENTAGE
|
||||
Current_Owner ; Truth
|
||||
conditions ; Truth
|
||||
solution ; created from the NFT's inner puzzle - solution is (new_owner trade_prices_list new_did_inner_hash)
|
||||
)
|
||||
|
||||
; This is a transfer program - which must return (new_owner, Optional[new_transfer_program], conditions)
|
||||
|
||||
(include condition_codes.clvm)
|
||||
(include curry-and-treehash.clinc)
|
||||
|
||||
(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 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 trade_prices_list my_nft_id)
|
||||
(if trade_prices_list
|
||||
(c
|
||||
(list
|
||||
ASSERT_PUZZLE_ANNOUNCEMENT
|
||||
(sha256
|
||||
(f (r (f trade_prices_list)))
|
||||
(sha256tree (c my_nft_id (list (list ROYALTY_ADDRESS (calculate_percentage (f (f trade_prices_list)) TRADE_PRICE_PERCENTAGE) (list ROYALTY_ADDRESS)))))
|
||||
)
|
||||
)
|
||||
(parse_trade_prices_list ROYALTY_ADDRESS TRADE_PRICE_PERCENTAGE (r trade_prices_list) my_nft_id)
|
||||
)
|
||||
()
|
||||
)
|
||||
)
|
||||
|
||||
; main
|
||||
; Returning (new_owner new_transfer_program conditions)
|
||||
; solution is (new_owner trade_prices_list new_did_inner_hash)
|
||||
(if solution
|
||||
(list
|
||||
(f solution)
|
||||
0
|
||||
(if (all (f solution) (not (= (f solution) Current_Owner)))
|
||||
(c
|
||||
(list
|
||||
ASSERT_PUZZLE_ANNOUNCEMENT
|
||||
(sha256
|
||||
(calculate_full_puzzle_hash (get_singleton_struct SINGLETON_STRUCT (f solution)) (f (r (r solution))))
|
||||
(f (r SINGLETON_STRUCT))
|
||||
)
|
||||
)
|
||||
(parse_trade_prices_list ROYALTY_ADDRESS TRADE_PRICE_PERCENTAGE (f (r solution)) (f (r SINGLETON_STRUCT)))
|
||||
)
|
||||
(parse_trade_prices_list ROYALTY_ADDRESS TRADE_PRICE_PERCENTAGE (f (r solution)) (f (r SINGLETON_STRUCT)))
|
||||
)
|
||||
)
|
||||
(list Current_Owner () ())
|
||||
)
|
||||
|
||||
|
||||
)
|
||||
+1
@@ -0,0 +1 @@
|
||||
ff02ffff01ff02ffff03ff81bfffff01ff04ff82013fffff04ff80ffff04ffff02ffff03ffff22ff82013fffff20ffff09ff82013fff2f808080ffff01ff04ffff04ff10ffff04ffff0bffff02ff2effff04ff02ffff04ff09ffff04ff8205bfffff04ffff02ff3effff04ff02ffff04ffff04ff09ffff04ff82013fff1d8080ff80808080ff808080808080ff1580ff808080ffff02ff16ffff04ff02ffff04ff0bffff04ff17ffff04ff8202bfffff04ff15ff8080808080808080ffff01ff02ff16ffff04ff02ffff04ff0bffff04ff17ffff04ff8202bfffff04ff15ff8080808080808080ff0180ff80808080ffff01ff04ff2fffff01ff80ff80808080ff0180ffff04ffff01ffffff3f02ff04ff0101ffff822710ff02ff02ffff03ff05ffff01ff02ff3affff04ff02ffff04ff0dffff04ffff0bff2affff0bff2cff1480ffff0bff2affff0bff2affff0bff2cff3c80ff0980ffff0bff2aff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ff17ffff01ff04ffff04ff10ffff04ffff0bff81a7ffff02ff3effff04ff02ffff04ffff04ff2fffff04ffff04ff05ffff04ffff05ffff14ffff12ff47ff0b80ff128080ffff04ffff04ff05ff8080ff80808080ff808080ff8080808080ff808080ffff02ff16ffff04ff02ffff04ff05ffff04ff0bffff04ff37ffff04ff2fff8080808080808080ff8080ff0180ffff0bff2affff0bff2cff1880ffff0bff2affff0bff2affff0bff2cff3c80ff0580ffff0bff2affff02ff3affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080
|
||||
+1
@@ -0,0 +1 @@
|
||||
025dee0fb1e9fa110302a7e9bfb6e381ca09618e2778b0184fa5c6b275cfce1f
|
||||
@@ -3,97 +3,72 @@
|
||||
METADATA
|
||||
METADATA_UPDATER_PUZZLE_HASH
|
||||
INNER_PUZZLE
|
||||
solution ; either to inner puzzle or metadata updater
|
||||
my_amount
|
||||
inner_solution
|
||||
)
|
||||
|
||||
(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)
|
||||
)
|
||||
)
|
||||
(include utility_macros.clib)
|
||||
|
||||
(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)
|
||||
(sha256tree 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)
|
||||
|
||||
; this function does two things - it wraps the odd value create coins, and it also filters out all negative conditions
|
||||
; odd_coin_params is (puzhash amount ...)
|
||||
; new_metadata_info is ((METADATA METADATA_UPDATER_PUZZLE_HASH) conditions)
|
||||
(defun wrap_odd_create_coins (NFT_STATE_LAYER_MOD_HASH conditions odd_coin_params new_metadata_info metadata_seen)
|
||||
(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)))
|
||||
)
|
||||
(if (logand (f (r (r (f conditions)))) ONE)
|
||||
(assert (not odd_coin_params)
|
||||
(wrap_odd_create_coins NFT_STATE_LAYER_MOD_HASH (r conditions) (r (f conditions)) new_metadata_info metadata_seen)
|
||||
)
|
||||
(c (f conditions) (wrap_odd_create_coins NFT_STATE_LAYER_MOD_HASH (r conditions) odd_coin_params new_metadata_info metadata_seen))
|
||||
)
|
||||
(if (= (f (f conditions)) -24)
|
||||
(wrap_odd_create_coins NFT_STATE_LAYER_MOD_HASH (r conditions) odd_coin_params
|
||||
(assert (all
|
||||
(= (sha256tree (f (r (f conditions)))) (f (r (f new_metadata_info))))
|
||||
(not metadata_seen)
|
||||
)
|
||||
; then
|
||||
(a (f (r (f conditions))) (list (f (f new_metadata_info)) (f (r (f new_metadata_info))) (f (r (r (f conditions))))))
|
||||
)
|
||||
ONE ; the metadata update has been seen now
|
||||
)
|
||||
(c (f conditions) (wrap_odd_create_coins NFT_STATE_LAYER_MOD_HASH (r conditions) odd_coin_params new_metadata_info metadata_seen))
|
||||
)
|
||||
))
|
||||
(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)
|
||||
(c
|
||||
(c CREATE_COIN
|
||||
(c
|
||||
(nft_state_layer_puzzle_hash
|
||||
NFT_STATE_LAYER_MOD_HASH
|
||||
(f (f new_metadata_info))
|
||||
(f (r (f new_metadata_info)))
|
||||
(f odd_coin_params) ; metadata updater solution
|
||||
)
|
||||
(r odd_coin_params)
|
||||
)
|
||||
)
|
||||
(check_for_metadata_prog_reveal METADATA_UPDATER_PUZZLE_HASH CURRENT_METADATA (r conditions_loop))
|
||||
(f (r new_metadata_info)) ; metadata_updater conditions
|
||||
)
|
||||
(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
|
||||
)
|
||||
(wrap_odd_create_coins
|
||||
NFT_STATE_LAYER_MOD_HASH
|
||||
(a INNER_PUZZLE inner_solution)
|
||||
()
|
||||
(list (list METADATA METADATA_UPDATER_PUZZLE_HASH) 0) ; if the magic condition is never seen, this is the information we us to recurry
|
||||
()
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1 +1 @@
|
||||
ff02ffff01ff04ffff04ff10ffff04ff81bfff808080ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff3affff04ff02ffff04ff17ffff04ff0bffff04ffff02ff2fff5f80ff808080808080ffff04ff81bfffff01ff8080808080808080ffff04ffff01ffffff49ff0233ffff0401ff0102ffffffff02ffff03ff05ffff01ff02ff22ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff17ffff01ff02ffff03ffff09ff47ffff0181e880ffff01ff02ffff03ffff09ffff02ff2effff04ff02ffff04ff81a7ff80808080ff0580ffff01ff02ff81a7ffff04ff0bffff04ff05ffff04ff820167ff8080808080ffff01ff088080ff0180ffff01ff02ff32ffff04ff02ffff04ff05ffff04ff0bffff04ff37ff80808080808080ff0180ffff01ff04ffff04ff0bffff04ff05ff808080ffff01ff80808080ff0180ffff02ffff03ff05ffff01ff04ff09ffff02ff2affff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ff02ff26ffff04ff02ffff04ffff02ff32ffff04ff02ffff04ff05ffff04ff0bffff04ff17ff808080808080ffff04ff17ff8080808080ffffff04ff09ffff04ffff02ff2affff04ff02ffff04ff15ffff04ff0bff8080808080ff808080ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff22ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff2bffff01ff02ffff03ffff09ff818bff3880ffff01ff02ffff03ffff18ff8202cbff3480ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff04ffff04ff23ffff04ff53ff808080ffff04ff6bff808080ffff04ff17ffff04ff4bff80808080808080ffff01ff04ff4bffff02ff3effff04ff02ffff04ff05ffff04ffff04ffff04ff23ffff04ff53ff808080ffff04ff6bff808080ffff04ff17ffff04ff2fff808080808080808080ff0180ffff01ff02ffff03ffff15ff818bff8080ffff01ff04ff4bffff02ff3effff04ff02ffff04ff05ffff04ffff04ffff04ff23ffff04ff53ff808080ffff04ff6bff808080ffff04ff17ffff04ff2fff8080808080808080ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff04ffff04ff23ffff04ff53ff808080ffff04ff6bff808080ffff04ff17ffff04ff2fff8080808080808080ff018080ff0180ffff01ff02ffff03ff2fffff01ff04ffff04ff38ffff04ffff02ff36ffff04ff02ffff04ff05ffff04ff81afffff04ffff0bff34ff5380ffff04ffff02ff2effff04ff02ffff04ff23ff80808080ffff04ffff0bff34ff0580ff8080808080808080ffff04ff17ff8201ef808080ff8080ffff01ff088080ff018080ff0180ff018080
|
||||
ff02ffff01ff02ff3effff04ff02ffff04ff05ffff04ffff02ff2fff5f80ffff04ff80ffff04ffff04ffff04ff0bffff04ff17ff808080ffff01ff808080ffff01ff8080808080808080ffff04ffff01ffffff0233ff04ff0101ffff02ff02ffff03ff05ffff01ff02ff1affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff1affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff23ff1880ffff01ff02ffff03ffff18ff81b3ff2c80ffff01ff02ffff03ffff20ff1780ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff33ffff04ff2fffff04ff5fff8080808080808080ffff01ff088080ff0180ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff0180ffff01ff02ffff03ffff09ff23ffff0181e880ffff01ff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ffff02ffff03ffff22ffff09ffff02ff2effff04ff02ffff04ff53ff80808080ff82014f80ffff20ff5f8080ffff01ff02ff53ffff04ff818fffff04ff82014fffff04ff81b3ff8080808080ffff01ff088080ff0180ffff04ff2cff8080808080808080ffff01ff04ff13ffff02ff3effff04ff02ffff04ff05ffff04ff1bffff04ff17ffff04ff2fffff04ff5fff80808080808080808080ff018080ff0180ffff01ff04ffff04ff18ffff04ffff02ff16ffff04ff02ffff04ff05ffff04ff27ffff04ffff0bff2cff82014f80ffff04ffff02ff2effff04ff02ffff04ff818fff80808080ffff04ffff0bff2cff0580ff8080808080808080ff378080ff81af8080ff0180ff018080
|
||||
@@ -1 +1 @@
|
||||
dd8135d546e291df295b376aa89fc409c8c50d7f655d1ff4e845637901bc2f8f
|
||||
a04d9f57764f54a43e4030befb4d80026e870519aaa66334aef8304f5d0393c2
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
(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)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -1 +0,0 @@
|
||||
ff02ffff01ff02ff2affff04ff02ffff04ff05ffff04ff0bffff04ff820affffff04ffff02ff3affff04ff02ffff04ff17ffff04ff2fffff04ff0bffff04ff5fffff04ff81bfffff04ff82017fffff04ff8204ffff80808080808080808080ff80808080808080ffff04ffff01ffffffff3f02ff333effff0401ff01ff82271002ffffffff02ffff03ff05ffff01ff02ffff03ffff09ff11ffff017580ffff01ff04ffff04ffff0175ffff04ff0bff198080ff0d80ffff01ff04ff09ffff02ff22ffff04ff02ffff04ff0dffff04ff0bff80808080808080ff0180ff8080ff0180ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff7cffff0bff34ff2480ffff0bff7cffff0bff7cffff0bff34ff2c80ff0980ffff0bff7cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ff17ffff01ff04ffff04ffff0181eaffff04ffff02ff22ffff04ff02ffff04ff05ffff04ff17ff8080808080ffff04ff0bff80808080ff2f80ffff012f80ff0180ff02ffff03ff82017fffff01ff02ff26ffff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fffff04ffff02ff7effff04ff02ffff04ff82017fff80808080ff8080808080808080808080ffff01ff04ffff04ff38ffff01ff808080ff808080ff0180ffffff04ffff04ff38ffff04ff8202ffff808080ffff04ffff04ff20ffff04ffff0bffff02ff2effff04ff02ffff04ff09ffff04ff81bfffff04ffff02ff7effff04ff02ffff04ffff04ff09ffff04ff5fff1d8080ff80808080ff808080808080ff8202ffff1580ff808080ffff02ff36ffff04ff02ffff04ff17ffff04ff82017fffff04ff15ff8080808080808080ff02ffff03ff0bffff01ff04ffff02ffff03ff33ffff01ff04ff20ffff04ffff0bffff02ff2effff04ff02ffff04ff5dffff04ff2dffff04ffff0bff34ff5380ffff04ffff0bff34ff5d80ff80808080808080ffff02ff7effff04ff02ffff04ffff04ff17ffff04ffff04ff09ffff04ffff05ffff14ffff12ff23ff1580ff5c8080ff808080ff808080ff8080808080ff808080ffff01ff04ff28ffff04ff09ffff04ffff02ff5effff04ff02ffff04ffff05ffff14ffff12ff23ff1580ff5c8080ff80808080ff8080808080ff0180ffff02ff36ffff04ff02ffff04ffff04ff09ffff04ff15ffff04ff2dffff04ff5dff8080808080ffff04ff1bffff04ff17ff80808080808080ff8080ff0180ffff0bff7cffff0bff34ff3080ffff0bff7cffff0bff7cffff0bff34ff2c80ff0580ffff0bff7cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff18ff05ff3480ffff01ff11ff05ff3480ffff010580ff0180ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff7effff04ff02ffff04ff09ff80808080ffff02ff7effff04ff02ffff04ff0dff8080808080ffff01ff0bff34ff058080ff0180ff018080
|
||||
@@ -1 +0,0 @@
|
||||
519b806c31cce5280fa37695149521a301f13f5696d1fd1eeee1856c2d14965c
|
||||
@@ -9,14 +9,6 @@
|
||||
(include condition_codes.clvm)
|
||||
(include curry-and-treehash.clinc)
|
||||
|
||||
; 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 1 TREE)
|
||||
)
|
||||
)
|
||||
|
||||
;; 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_MOD_HASH LAUNCHER_ID LAUNCHER_PUZZLE_HASH inner_puzzle_hash)
|
||||
|
||||
@@ -16,14 +16,6 @@
|
||||
(include condition_codes.clvm)
|
||||
(include curry-and-treehash.clinc)
|
||||
|
||||
; 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 1 TREE)
|
||||
)
|
||||
)
|
||||
|
||||
;; 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 delayed_spend (SECONDS_DELAY DELAYED_PUZZLE_HASH amount)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import List, Tuple, Optional
|
||||
from typing import Iterator, List, Tuple, Optional
|
||||
|
||||
from chia.types.blockchain_format.coin import Coin
|
||||
from chia.types.blockchain_format.program import Program
|
||||
@@ -157,6 +157,14 @@ MELT_CONDITION = [ConditionOpcode.CREATE_COIN, 0, ESCAPE_VALUE]
|
||||
#
|
||||
|
||||
|
||||
def match_singleton_puzzle(puzzle: Program) -> Tuple[bool, Iterator[Program]]:
|
||||
mod, curried_args = puzzle.uncurry()
|
||||
if mod == SINGLETON_MOD:
|
||||
return True, curried_args.as_iter()
|
||||
else:
|
||||
return False, iter(())
|
||||
|
||||
|
||||
# Given the parent and amount of the launcher coin, return the launcher coin
|
||||
def generate_launcher_coin(coin: Coin, amount: uint64) -> Coin:
|
||||
return Coin(coin.name(), SINGLETON_LAUNCHER_HASH, amount)
|
||||
@@ -249,9 +257,11 @@ def lineage_proof_for_coinsol(coin_spend: CoinSpend) -> LineageProof:
|
||||
|
||||
|
||||
# Return the puzzle reveal of a singleton with specific ID and innerpuz
|
||||
def puzzle_for_singleton(launcher_id: bytes32, inner_puz: Program) -> Program:
|
||||
def puzzle_for_singleton(
|
||||
launcher_id: bytes32, inner_puz: Program, launcher_hash: bytes32 = SINGLETON_LAUNCHER_HASH
|
||||
) -> Program:
|
||||
return SINGLETON_MOD.curry(
|
||||
(SINGLETON_MOD_HASH, (launcher_id, SINGLETON_LAUNCHER_HASH)),
|
||||
(SINGLETON_MOD_HASH, (launcher_id, launcher_hash)),
|
||||
inner_puz,
|
||||
)
|
||||
|
||||
|
||||
@@ -13,28 +13,7 @@
|
||||
(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)))
|
||||
(include utility_macros.clib)
|
||||
|
||||
(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)))
|
||||
@@ -53,6 +32,10 @@
|
||||
(c (f condition) (c (calculate_full_puzzle_hash SINGLETON_STRUCT (f (r condition))) (r (r condition))))
|
||||
)
|
||||
|
||||
(defun is_odd_create_coin (condition)
|
||||
(and (= (f condition) CREATE_COIN) (logand (f (r (r condition))) 1))
|
||||
)
|
||||
|
||||
; 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
|
||||
@@ -65,7 +48,7 @@
|
||||
(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))
|
||||
(if (is_odd_create_coin (f conditions))
|
||||
; check that we haven't already found one
|
||||
(assert (not has_odd_output_been_found)
|
||||
; then
|
||||
@@ -103,7 +86,7 @@
|
||||
(verify_lineage_proof
|
||||
SINGLETON_STRUCT
|
||||
; calculate our parent's ID
|
||||
(sha256
|
||||
(calculate_coin_id
|
||||
(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
|
||||
|
||||
@@ -1 +1 @@
|
||||
ff02ffff01ff02ffff03ffff18ff2fff3c80ffff01ff04ffff04ff10ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff0bff27ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff02ffff03ff77ffff0181b7ffff015780ff018080ffff04ff77ff808080808080ffff02ff26ffff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffff49ff4702ff33ff0401ffff01ff02ff02ffff03ff05ffff01ff02ff3affff04ff02ffff04ff0dffff04ffff0bff2affff0bff3cff2c80ffff0bff2affff0bff2affff0bff3cff1280ff0980ffff0bff2aff0bffff0bff3cff8080808080ff8080808080ffff010b80ff0180ffffff02ffff03ff0bffff01ff02ffff03ffff02ffff03ffff09ff23ff1480ffff01ff02ffff03ffff18ff81b3ff3c80ffff01ff0101ff8080ff0180ff8080ff0180ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff26ffff04ff02ffff04ff05ffff04ff1bffff04ff3cff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff26ffff04ff02ffff04ff05ffff04ff1bffff04ff3cff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff26ffff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ff0bff2affff0bff3cff3880ffff0bff2affff0bff2affff0bff3cff1280ff0580ffff0bff2affff02ff3affff04ff02ffff04ff07ffff04ffff0bff3cff3c80ff8080808080ffff0bff3cff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bff3cff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff28ffff04ff0bff808080ffff01ff088080ff0180ff018080
|
||||
ff02ffff01ff02ffff03ffff18ff2fff3480ffff01ff04ffff04ff20ffff04ff2fff808080ffff04ffff02ff3effff04ff02ffff04ff05ffff04ffff02ff2affff04ff02ffff04ff27ffff04ffff02ffff03ff77ffff01ff02ff36ffff04ff02ffff04ff09ffff04ff57ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ffff011d80ff0180ffff04ffff02ffff03ff77ffff0181b7ffff015780ff0180ff808080808080ffff04ff77ff808080808080ffff02ff3affff04ff02ffff04ff05ffff04ffff02ff0bff5f80ffff01ff8080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4947ff0233ffff0401ff0102ffffff20ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff3cffff0bff34ff2480ffff0bff3cffff0bff3cffff0bff34ff2c80ff0980ffff0bff3cff0bffff0bff34ff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff22ffff09ffff0dff0580ff2280ffff09ffff0dff0b80ff2280ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ff02ffff03ff0bffff01ff02ffff03ffff02ff26ffff04ff02ffff04ff13ff80808080ffff01ff02ffff03ffff20ff1780ffff01ff02ffff03ffff09ff81b3ffff01818f80ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff808080808080ffff01ff04ffff04ff23ffff04ffff02ff36ffff04ff02ffff04ff09ffff04ff53ffff04ffff02ff2effff04ff02ffff04ff05ff80808080ff808080808080ff738080ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff34ff8080808080808080ff0180ffff01ff088080ff0180ffff01ff04ff13ffff02ff3affff04ff02ffff04ff05ffff04ff1bffff04ff17ff8080808080808080ff0180ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffff02ffff03ffff09ff09ff3880ffff01ff02ffff03ffff18ff2dffff010180ffff01ff0101ff8080ff0180ff8080ff0180ff0bff3cffff0bff34ff2880ffff0bff3cffff0bff3cffff0bff34ff2c80ff0580ffff0bff3cffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff34ff3480ff8080808080ffff0bff34ff8080808080ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff2effff04ff02ffff04ff09ff80808080ffff02ff2effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ffff03ffff21ff17ffff09ff0bff158080ffff01ff04ff30ffff04ff0bff808080ffff01ff088080ff0180ff018080
|
||||
@@ -1 +1 @@
|
||||
f1e8350cec62f8204aaf867cc3c12cae369f619258206616108c6cfd7be760b3
|
||||
7faa3253bfddd1e0decb0906b2dc6247bbc4cf608f58345d173adb63e8b47c9f
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
from typing import Iterator, List, Optional, Tuple
|
||||
|
||||
from chia.types.blockchain_format.coin import Coin
|
||||
from chia.types.blockchain_format.program import Program
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.types.coin_spend import CoinSpend
|
||||
from chia.types.condition_opcodes import ConditionOpcode
|
||||
from chia.util.hash import std_hash
|
||||
from chia.util.ints import uint64
|
||||
from chia.wallet.lineage_proof import LineageProof
|
||||
from chia.wallet.puzzles.load_clvm import load_clvm
|
||||
|
||||
SINGLETON_MOD = load_clvm("singleton_top_layer_v1_1.clvm")
|
||||
SINGLETON_MOD_HASH = SINGLETON_MOD.get_tree_hash()
|
||||
P2_SINGLETON_MOD = load_clvm("p2_singleton.clvm")
|
||||
P2_SINGLETON_OR_DELAYED_MOD = load_clvm("p2_singleton_or_delayed_puzhash.clvm")
|
||||
SINGLETON_LAUNCHER = load_clvm("singleton_launcher.clvm")
|
||||
SINGLETON_LAUNCHER_HASH = SINGLETON_LAUNCHER.get_tree_hash()
|
||||
ESCAPE_VALUE = -113
|
||||
MELT_CONDITION = [ConditionOpcode.CREATE_COIN, 0, ESCAPE_VALUE]
|
||||
|
||||
|
||||
#
|
||||
# An explanation of how this functions from a user's perspective
|
||||
#
|
||||
# Consider that you have some coin A that you want to create a singleton
|
||||
# containing some inner puzzle I from with amount T. We'll call the Launcher
|
||||
# coin, which is created from A "Launcher" and the first iteration of the
|
||||
# singleton, called the "Eve" spend, Eve. When spent, I yields a coin
|
||||
# running I' and so on in a singleton specific way described below.
|
||||
#
|
||||
# The structure of this on the blockchain when done looks like this
|
||||
#
|
||||
# ,------------.
|
||||
# | Coin A |
|
||||
# `------------'
|
||||
# |
|
||||
# ------------------ Atomic Transaction 1 -----------------
|
||||
# v
|
||||
# .------------. .-------------------------------.
|
||||
# | Launcher |------>| Eve Coin Containing Program I |
|
||||
# `------------' `-------------------------------'
|
||||
# |
|
||||
# -------------------- End Transaction 1 ------------------\
|
||||
# | > The Eve coin
|
||||
# --------------- (2) Transaction With I ------------------/ may also be
|
||||
# | spent
|
||||
# v simultaneously
|
||||
# .-----------------------------------.
|
||||
# | Running Singleton With Program I' |
|
||||
# `-----------------------------------'
|
||||
# |
|
||||
# --------------------- End Transaction 2 ------------------
|
||||
# |
|
||||
# --------------- (3) Transaction With I' ------------------
|
||||
# ...
|
||||
#
|
||||
#
|
||||
# == Practial use of singleton_top_layer.py ==
|
||||
#
|
||||
# 1) Designate some coin as coin A
|
||||
#
|
||||
# 2) call puzzle_for_singleton with that coin's name (it is the Parent of the
|
||||
# Launch coin), and the initial inner puzzle I, curried as appropriate for
|
||||
# its own purpose. Adaptations of the program I and its descendants are
|
||||
# required as below.
|
||||
#
|
||||
# 3) call launch_conditions_and_coinsol to get a set of "launch_conditions",
|
||||
# which will be used to spend standard coin A, and a "spend", which spends
|
||||
# the Launcher created by the application of "launch_conditions" to A in a
|
||||
# spend. These actions must be done in the same spend bundle.
|
||||
#
|
||||
# One can create a SpendBundle containing the spend of A giving it the
|
||||
# argument list (() (q . launch_conditions) ()) and then append "spend" onto
|
||||
# its .coin_spends to create a combined spend bundle.
|
||||
#
|
||||
# 4) submit the combine spend bundle.
|
||||
#
|
||||
# 5) Remember the identity of the Launcher coin:
|
||||
#
|
||||
# Coin(A.name(), SINGLETON_LAUNCHER_HASH, amount)
|
||||
#
|
||||
# A singleton has been created like this:
|
||||
#
|
||||
# Coin(Launcher.name(), puzzle_for_singleton(Launcher.name(), I), amount)
|
||||
#
|
||||
#
|
||||
# == To spend the singleton requires some host side setup ==
|
||||
#
|
||||
# The singleton adds an ASSERT_MY_COIN_ID to constrain it to the coin that
|
||||
# matches its own conception of itself. It consumes a "LineageProof" object
|
||||
# when spent that must be constructed so. We'll call the singleton we intend
|
||||
# to spend "S".
|
||||
#
|
||||
# Specifically, the required puzzle is the Inner puzzle I for the parent of S
|
||||
# unless S is the Eve coin, in which case it is None.
|
||||
# So to spend S', the second singleton, I is used, and to spend S'', I' is used.
|
||||
# We'll call this puzzle hash (or None) PH.
|
||||
#
|
||||
# If this is the Eve singleton:
|
||||
#
|
||||
# PH = None
|
||||
# L = LineageProof(Launcher, PH, amount)
|
||||
#
|
||||
# - Note: the Eve singleton's .parent_coin_info should match Launcher here.
|
||||
#
|
||||
# Otherwise
|
||||
#
|
||||
# PH = ParentOf(S).inner_puzzle_hash
|
||||
# L = LineageProof(ParentOf(S).name(), PH, amount)
|
||||
#
|
||||
# - Note: ParentOf(S).name is the .parent_coin_info member of the
|
||||
# coin record for S.
|
||||
#
|
||||
# Now the coin S can be spent.
|
||||
# The puzzle to use in the spend is given by
|
||||
#
|
||||
# puzzle_for_singleton(S.name(), I'.puzzle_hash())
|
||||
#
|
||||
# and the arguments are given by (with the argument list to I designated AI)
|
||||
#
|
||||
# solution_for_singleton(L, amount, AI)
|
||||
#
|
||||
# Note that AI contains dynamic arguments to puzzle I _after_ the singleton
|
||||
# truths.
|
||||
#
|
||||
#
|
||||
# Adapting puzzles to the singleton
|
||||
#
|
||||
# 1) For the puzzle to create a coin from inside the singleton it will need the
|
||||
# following values to be added to its curried in arguments:
|
||||
#
|
||||
# - A way to compute its own puzzle has for each of I' and so on. This can
|
||||
# be accomplished by giving it its uncurried puzzle hash and using
|
||||
# puzzle-hash-of-curried-function to compute it. Although full_puzzle_hash
|
||||
# is used for some arguments, the inputs to all singleton_top_layer
|
||||
# functions is the inner puzzle.
|
||||
#
|
||||
# - the name() of the Launcher coin (which you can compute from a Coin
|
||||
# object) if you're not already using it in I puzzle for some other
|
||||
# reason.
|
||||
#
|
||||
# 2) A non-curried argument called "singleton_truths" will be passed to your
|
||||
# program. It is not required to use anything inside.
|
||||
#
|
||||
# There is little value in not receiving this argument via the adaptations
|
||||
# below as a standard puzzle can't be used anyway. To work the result must
|
||||
# be itself a singleton, and the singleton does not change the puzzle hash
|
||||
# in an outgoing CREATE_COIN to cause it to be one.
|
||||
#
|
||||
# With this modification of the program I done, I and descendants will
|
||||
# continue to produce I', I'' etc.
|
||||
#
|
||||
# The actual CREATE_COIN puzzle hash will be the result of
|
||||
# this. The Launcher ID referred to here is the name() of
|
||||
# the Launcher coin as above.
|
||||
#
|
||||
|
||||
|
||||
def match_singleton_puzzle(puzzle: Program) -> Tuple[bool, Iterator[Program]]:
|
||||
mod, curried_args = puzzle.uncurry()
|
||||
if mod == SINGLETON_MOD:
|
||||
return True, curried_args.as_iter()
|
||||
else:
|
||||
return False, iter(())
|
||||
|
||||
|
||||
# Given the parent and amount of the launcher coin, return the launcher coin
|
||||
def generate_launcher_coin(coin: Coin, amount: uint64) -> Coin:
|
||||
return Coin(coin.name(), SINGLETON_LAUNCHER_HASH, amount)
|
||||
|
||||
|
||||
def remove_singleton_truth_wrapper(puzzle: Program) -> Program:
|
||||
inner_puzzle: Program = puzzle.rest().first().rest()
|
||||
return inner_puzzle
|
||||
|
||||
|
||||
# Take standard coin and amount -> launch conditions & launcher coin solution
|
||||
def launch_conditions_and_coinsol(
|
||||
coin: Coin,
|
||||
inner_puzzle: Program,
|
||||
comment: List[Tuple[str, str]],
|
||||
amount: uint64,
|
||||
) -> Tuple[List[Program], CoinSpend]:
|
||||
if (amount % 2) == 0:
|
||||
raise ValueError("Coin amount cannot be even. Subtract one mojo.")
|
||||
|
||||
launcher_coin: Coin = generate_launcher_coin(coin, amount)
|
||||
curried_singleton: Program = SINGLETON_MOD.curry(
|
||||
(SINGLETON_MOD_HASH, (launcher_coin.name(), SINGLETON_LAUNCHER_HASH)),
|
||||
inner_puzzle,
|
||||
)
|
||||
|
||||
launcher_solution = Program.to(
|
||||
[
|
||||
curried_singleton.get_tree_hash(),
|
||||
amount,
|
||||
comment,
|
||||
]
|
||||
)
|
||||
create_launcher = Program.to(
|
||||
[
|
||||
ConditionOpcode.CREATE_COIN,
|
||||
SINGLETON_LAUNCHER_HASH,
|
||||
amount,
|
||||
],
|
||||
)
|
||||
assert_launcher_announcement = Program.to(
|
||||
[
|
||||
ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT,
|
||||
std_hash(launcher_coin.name() + launcher_solution.get_tree_hash()),
|
||||
],
|
||||
)
|
||||
|
||||
conditions = [create_launcher, assert_launcher_announcement]
|
||||
|
||||
launcher_coin_spend = CoinSpend(
|
||||
launcher_coin,
|
||||
SINGLETON_LAUNCHER,
|
||||
launcher_solution,
|
||||
)
|
||||
|
||||
return conditions, launcher_coin_spend
|
||||
|
||||
|
||||
# Take a coin solution, return a lineage proof for their child to use in spends
|
||||
def lineage_proof_for_coinsol(coin_spend: CoinSpend) -> LineageProof:
|
||||
parent_name: bytes32 = coin_spend.coin.parent_coin_info
|
||||
|
||||
inner_puzzle_hash: Optional[bytes32] = None
|
||||
if coin_spend.coin.puzzle_hash != SINGLETON_LAUNCHER_HASH:
|
||||
full_puzzle = Program.from_bytes(bytes(coin_spend.puzzle_reveal))
|
||||
r = full_puzzle.uncurry()
|
||||
if r is not None:
|
||||
_, args = r
|
||||
_, inner_puzzle = list(args.as_iter())
|
||||
inner_puzzle_hash = inner_puzzle.get_tree_hash()
|
||||
|
||||
amount: uint64 = coin_spend.coin.amount
|
||||
|
||||
return LineageProof(
|
||||
parent_name,
|
||||
inner_puzzle_hash,
|
||||
amount,
|
||||
)
|
||||
|
||||
|
||||
# Return the puzzle reveal of a singleton with specific ID and innerpuz
|
||||
def puzzle_for_singleton(
|
||||
launcher_id: bytes32, inner_puz: Program, launcher_hash: bytes32 = SINGLETON_LAUNCHER_HASH
|
||||
) -> Program:
|
||||
return SINGLETON_MOD.curry(
|
||||
(SINGLETON_MOD_HASH, (launcher_id, launcher_hash)),
|
||||
inner_puz,
|
||||
)
|
||||
|
||||
|
||||
# Return a solution to spend a singleton
|
||||
def solution_for_singleton(
|
||||
lineage_proof: LineageProof,
|
||||
amount: uint64,
|
||||
inner_solution: Program,
|
||||
) -> Program:
|
||||
if lineage_proof.inner_puzzle_hash is None:
|
||||
parent_info = [
|
||||
lineage_proof.parent_name,
|
||||
lineage_proof.amount,
|
||||
]
|
||||
else:
|
||||
parent_info = [
|
||||
lineage_proof.parent_name,
|
||||
lineage_proof.inner_puzzle_hash,
|
||||
lineage_proof.amount,
|
||||
]
|
||||
|
||||
solution: Program = Program.to([parent_info, amount, inner_solution])
|
||||
return solution
|
||||
|
||||
|
||||
# Create a coin that a singleton can claim
|
||||
def pay_to_singleton_puzzle(launcher_id: bytes32) -> Program:
|
||||
return P2_SINGLETON_MOD.curry(SINGLETON_MOD_HASH, launcher_id, SINGLETON_LAUNCHER_HASH)
|
||||
|
||||
|
||||
# Create a coin that a singleton can claim or that can be sent to another puzzle after a specified time
|
||||
def pay_to_singleton_or_delay_puzzle(launcher_id: bytes32, delay_time: uint64, delay_ph: bytes32) -> Program:
|
||||
return P2_SINGLETON_OR_DELAYED_MOD.curry(
|
||||
SINGLETON_MOD_HASH,
|
||||
launcher_id,
|
||||
SINGLETON_LAUNCHER_HASH,
|
||||
delay_time,
|
||||
delay_ph,
|
||||
)
|
||||
|
||||
|
||||
# Solution for EITHER p2_singleton or the claiming spend case for p2_singleton_or_delayed_puzhash
|
||||
def solution_for_p2_singleton(p2_singleton_coin: Coin, singleton_inner_puzhash: bytes32) -> Program:
|
||||
solution: Program = Program.to([singleton_inner_puzhash, p2_singleton_coin.name()])
|
||||
return solution
|
||||
|
||||
|
||||
# Solution for the delayed spend case for p2_singleton_or_delayed_puzhash
|
||||
def solution_for_p2_delayed_puzzle(output_amount: uint64) -> Program:
|
||||
solution: Program = Program.to([output_amount, []])
|
||||
return solution
|
||||
|
||||
|
||||
# Get announcement conditions for singleton solution and full CoinSpend for the claimed coin
|
||||
def claim_p2_singleton(
|
||||
p2_singleton_coin: Coin,
|
||||
singleton_inner_puzhash: bytes32,
|
||||
launcher_id: bytes32,
|
||||
delay_time: Optional[uint64] = None,
|
||||
delay_ph: Optional[bytes32] = None,
|
||||
) -> Tuple[Program, Program, CoinSpend]:
|
||||
assertion = Program.to([ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, std_hash(p2_singleton_coin.name() + b"$")])
|
||||
announcement = Program.to([ConditionOpcode.CREATE_PUZZLE_ANNOUNCEMENT, p2_singleton_coin.name()])
|
||||
if delay_time is None or delay_ph is None:
|
||||
puzzle: Program = pay_to_singleton_puzzle(launcher_id)
|
||||
else:
|
||||
puzzle = pay_to_singleton_or_delay_puzzle(
|
||||
launcher_id,
|
||||
delay_time,
|
||||
delay_ph,
|
||||
)
|
||||
claim_coinsol = CoinSpend(
|
||||
p2_singleton_coin,
|
||||
puzzle,
|
||||
solution_for_p2_singleton(p2_singleton_coin, singleton_inner_puzhash),
|
||||
)
|
||||
return assertion, announcement, claim_coinsol
|
||||
|
||||
|
||||
# Get the CoinSpend for spending to a delayed puzzle
|
||||
def spend_to_delayed_puzzle(
|
||||
p2_singleton_coin: Coin,
|
||||
output_amount: uint64,
|
||||
launcher_id: bytes32,
|
||||
delay_time: uint64,
|
||||
delay_ph: bytes32,
|
||||
) -> CoinSpend:
|
||||
claim_coinsol = CoinSpend(
|
||||
p2_singleton_coin,
|
||||
pay_to_singleton_or_delay_puzzle(launcher_id, delay_time, delay_ph),
|
||||
solution_for_p2_delayed_puzzle(output_amount),
|
||||
)
|
||||
return claim_coinsol
|
||||
@@ -0,0 +1,17 @@
|
||||
(
|
||||
(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)
|
||||
)
|
||||
)
|
||||
+116
-30
@@ -12,6 +12,8 @@ from chia.types.spend_bundle import SpendBundle
|
||||
from chia.util.db_wrapper import DBWrapper
|
||||
from chia.util.hash import std_hash
|
||||
from chia.util.ints import uint32, uint64
|
||||
from chia.wallet.nft_wallet.nft_wallet import NFTWallet
|
||||
from chia.wallet.outer_puzzles import AssetType
|
||||
from chia.wallet.payment import Payment
|
||||
from chia.wallet.puzzle_drivers import PuzzleInfo
|
||||
from chia.wallet.trade_record import TradeRecord
|
||||
@@ -23,6 +25,9 @@ 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_coin_record import WalletCoinRecord
|
||||
from chia.wallet.puzzles.load_clvm import load_clvm
|
||||
|
||||
OFFER_MOD = load_clvm("settlement_payments.clvm")
|
||||
|
||||
|
||||
class TradeManager:
|
||||
@@ -58,7 +63,7 @@ class TradeManager:
|
||||
- get_asset_id() -> bytes32
|
||||
- Finally, you must make sure that your wallet will respond appropriately when these WSM methods are called:
|
||||
- get_wallet_for_puzzle_info(puzzle_info: PuzzleInfo) -> <Your wallet>
|
||||
- create_wallet_for_puzzle_info(puzzle_info: PuzzleInfo) -> <Your wallet>
|
||||
- create_wallet_for_puzzle_info(..., puzzle_info: PuzzleInfo) -> <Your wallet> (See cat_wallet.py for full API)
|
||||
- get_wallet_for_asset_id(asset_id: bytes32) -> <Your wallet>
|
||||
"""
|
||||
|
||||
@@ -222,15 +227,13 @@ class TradeManager:
|
||||
|
||||
if wallet is None:
|
||||
continue
|
||||
new_ph = await wallet.get_new_puzzlehash()
|
||||
# This should probably not switch on whether or not we're spending a CAT but it has to for now
|
||||
# ATTENTION: new_wallets
|
||||
if wallet.type() == WalletType.CAT:
|
||||
txs = await wallet.generate_signed_transaction(
|
||||
[coin.amount], [new_ph], fee=fee_to_pay, coins={coin}, ignore_max_send_amount=True
|
||||
)
|
||||
all_txs.extend(txs)
|
||||
|
||||
if wallet.type() == WalletType.NFT:
|
||||
new_ph = await wallet.wallet_state_manager.main_wallet.get_new_puzzlehash()
|
||||
else:
|
||||
new_ph = await wallet.get_new_puzzlehash()
|
||||
# This should probably not switch on whether or not we're spending a XCH but it has to for now
|
||||
if wallet.type() == WalletType.STANDARD_WALLET:
|
||||
if fee_to_pay > coin.amount:
|
||||
selected_coins: Set[Coin] = await wallet.select_coins(
|
||||
uint64(fee_to_pay - coin.amount),
|
||||
@@ -247,6 +250,12 @@ class TradeManager:
|
||||
ignore_max_send_amount=True,
|
||||
)
|
||||
all_txs.append(tx)
|
||||
else:
|
||||
# ATTENTION: new_wallets
|
||||
txs = await wallet.generate_signed_transaction(
|
||||
[coin.amount], [new_ph], fee=fee_to_pay, coins={coin}, ignore_max_send_amount=True
|
||||
)
|
||||
all_txs.extend(txs)
|
||||
fee_to_pay = uint64(0)
|
||||
|
||||
cancellation_addition = Coin(coin.name(), new_ph, coin.amount)
|
||||
@@ -329,6 +338,7 @@ class TradeManager:
|
||||
try:
|
||||
coins_to_offer: Dict[Union[int, bytes32], List[Coin]] = {}
|
||||
requested_payments: Dict[Optional[bytes32], List[Payment]] = {}
|
||||
offer_dict_no_ints: Dict[Optional[bytes32], int] = {}
|
||||
for id, amount in offer_dict.items():
|
||||
if amount > 0:
|
||||
if isinstance(id, int):
|
||||
@@ -372,18 +382,33 @@ class TradeManager:
|
||||
elif amount == 0:
|
||||
raise ValueError("You cannot offer nor request 0 amount of something")
|
||||
|
||||
offer_dict_no_ints[asset_id] = amount
|
||||
|
||||
if asset_id is not None and wallet is not None:
|
||||
if callable(getattr(wallet, "get_puzzle_info", None)):
|
||||
puzzle_driver: PuzzleInfo = wallet.get_puzzle_info(asset_id)
|
||||
if asset_id in driver_dict and driver_dict[asset_id] != puzzle_driver:
|
||||
raise ValueError(
|
||||
f"driver_dict specified {driver_dict[asset_id]}, was expecting {puzzle_driver}"
|
||||
)
|
||||
# ignore the case if we're an nft transfering the did owner
|
||||
if self.check_for_owner_change_in_drivers(puzzle_driver, driver_dict[asset_id]):
|
||||
driver_dict[asset_id] = puzzle_driver
|
||||
else:
|
||||
raise ValueError(
|
||||
f"driver_dict specified {driver_dict[asset_id]}, was expecting {puzzle_driver}"
|
||||
)
|
||||
else:
|
||||
driver_dict[asset_id] = puzzle_driver
|
||||
else:
|
||||
raise ValueError(f"Wallet for asset id {asset_id} is not properly integrated with TradeManager")
|
||||
|
||||
potential_special_offer: Optional[Offer] = await self.check_for_special_offer_making(
|
||||
offer_dict_no_ints,
|
||||
driver_dict,
|
||||
fee,
|
||||
)
|
||||
|
||||
if potential_special_offer is not None:
|
||||
return True, potential_special_offer, None
|
||||
|
||||
all_coins: List[Coin] = [c for coins in coins_to_offer.values() for c in coins]
|
||||
notarized_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = Offer.notarize_payments(
|
||||
requested_payments, all_coins
|
||||
@@ -397,18 +422,8 @@ class TradeManager:
|
||||
wallet = self.wallet_state_manager.wallets[id]
|
||||
else:
|
||||
wallet = await self.wallet_state_manager.get_wallet_for_asset_id(id.hex())
|
||||
# This should probably not switch on whether or not we're spending a CAT but it has to for now
|
||||
# ATTENTION: new_wallets
|
||||
if wallet.type() == WalletType.CAT:
|
||||
txs = await wallet.generate_signed_transaction(
|
||||
[abs(offer_dict[id])],
|
||||
[Offer.ph()],
|
||||
fee=fee_left_to_pay,
|
||||
coins=set(selected_coins),
|
||||
puzzle_announcements_to_consume=announcements_to_assert,
|
||||
)
|
||||
all_transactions.extend(txs)
|
||||
else:
|
||||
# This should probably not switch on whether or not we're spending XCH but it has to for now
|
||||
if wallet.type() == WalletType.STANDARD_WALLET:
|
||||
tx = await wallet.generate_signed_transaction(
|
||||
abs(offer_dict[id]),
|
||||
Offer.ph(),
|
||||
@@ -417,11 +432,36 @@ class TradeManager:
|
||||
puzzle_announcements_to_consume=announcements_to_assert,
|
||||
)
|
||||
all_transactions.append(tx)
|
||||
elif wallet.type() == WalletType.NFT:
|
||||
# This is to generate the tx for specific nft assets, i.e. not using
|
||||
# wallet_id as the selector which would select any coins from nft_wallet
|
||||
amounts = [coin.amount for coin in selected_coins]
|
||||
txs = await wallet.generate_signed_transaction(
|
||||
# [abs(offer_dict[id])],
|
||||
amounts,
|
||||
[Offer.ph()],
|
||||
fee=fee_left_to_pay,
|
||||
coins=set(selected_coins),
|
||||
puzzle_announcements_to_consume=announcements_to_assert,
|
||||
)
|
||||
all_transactions.extend(txs)
|
||||
else:
|
||||
# ATTENTION: new_wallets
|
||||
txs = await wallet.generate_signed_transaction(
|
||||
[abs(offer_dict[id])],
|
||||
[Offer.ph()],
|
||||
fee=fee_left_to_pay,
|
||||
coins=set(selected_coins),
|
||||
puzzle_announcements_to_consume=announcements_to_assert,
|
||||
)
|
||||
all_transactions.extend(txs)
|
||||
|
||||
fee_left_to_pay = uint64(0)
|
||||
|
||||
transaction_bundles: List[Optional[SpendBundle]] = [tx.spend_bundle for tx in all_transactions]
|
||||
total_spend_bundle = SpendBundle.aggregate(list(filter(lambda b: b is not None, transaction_bundles)))
|
||||
total_spend_bundle = SpendBundle.aggregate(
|
||||
[x.spend_bundle for x in all_transactions if x.spend_bundle is not None]
|
||||
)
|
||||
|
||||
offer = Offer(notarized_payments, total_spend_bundle, driver_dict)
|
||||
return True, offer, None
|
||||
|
||||
@@ -507,13 +547,16 @@ class TradeManager:
|
||||
removal_dict.setdefault(wallet_id, [])
|
||||
removal_dict[wallet_id].append(removal)
|
||||
|
||||
all_removals: List[bytes32] = [r.name() for removals in removal_dict.values() for r in removals]
|
||||
|
||||
for wid, grouped_removals in removal_dict.items():
|
||||
wallet = self.wallet_state_manager.wallets[wid]
|
||||
to_puzzle_hash = bytes32([1] * 32) # We use all zeros to be clear not to send here
|
||||
removal_tree_hash = Program.to([coin_as_list(rem) for rem in grouped_removals]).get_tree_hash()
|
||||
# We also need to calculate the sent amount
|
||||
removed: int = sum(c.amount for c in grouped_removals)
|
||||
change_coins: List[Coin] = addition_dict[wid] if wid in addition_dict else []
|
||||
potential_change_coins: List[Coin] = addition_dict[wid] if wid in addition_dict else []
|
||||
change_coins: List[Coin] = [c for c in potential_change_coins if c.parent_coin_info in all_removals]
|
||||
change_amount: int = sum(c.amount for c in change_coins)
|
||||
sent_amount: int = removed - change_amount
|
||||
txs.append(
|
||||
@@ -542,6 +585,7 @@ class TradeManager:
|
||||
async def respond_to_offer(self, offer: Offer, fee=uint64(0)) -> Tuple[bool, Optional[TradeRecord], Optional[str]]:
|
||||
take_offer_dict: Dict[Union[bytes32, int], int] = {}
|
||||
arbitrage: Dict[Optional[bytes32], int] = offer.arbitrage()
|
||||
|
||||
for asset_id, amount in arbitrage.items():
|
||||
if asset_id is None:
|
||||
wallet = self.wallet_state_manager.main_wallet
|
||||
@@ -551,7 +595,7 @@ class TradeManager:
|
||||
wallet = await self.wallet_state_manager.get_wallet_for_asset_id(asset_id.hex())
|
||||
if wallet is None and amount < 0:
|
||||
return False, None, f"Do not have a wallet for asset ID: {asset_id} to fulfill offer"
|
||||
elif wallet is None:
|
||||
elif wallet is None or wallet.type() == WalletType.NFT:
|
||||
key = asset_id
|
||||
else:
|
||||
key = int(wallet.id())
|
||||
@@ -561,15 +605,14 @@ class TradeManager:
|
||||
valid: bool = await self.check_offer_validity(offer)
|
||||
if not valid:
|
||||
return False, None, "This offer is no longer valid"
|
||||
|
||||
success, take_offer, error = await self._create_offer_for_ids(take_offer_dict, offer.driver_dict, fee=fee)
|
||||
if not success or take_offer is None:
|
||||
return False, None, error
|
||||
|
||||
complete_offer = Offer.aggregate([offer, take_offer])
|
||||
assert complete_offer.is_valid()
|
||||
final_spend_bundle: SpendBundle = complete_offer.to_valid_spend()
|
||||
|
||||
final_spend_bundle: SpendBundle = complete_offer.to_valid_spend()
|
||||
await self.maybe_create_wallets_for_offer(complete_offer)
|
||||
|
||||
tx_records: List[TransactionRecord] = await self.calculate_tx_records_for_offer(complete_offer, True)
|
||||
@@ -614,3 +657,46 @@ class TradeManager:
|
||||
await self.wallet_state_manager.add_transaction(tx)
|
||||
|
||||
return True, trade_record, None
|
||||
|
||||
async def check_for_special_offer_making(
|
||||
self,
|
||||
offer_dict: Dict[Optional[bytes32], int],
|
||||
driver_dict: Dict[bytes32, PuzzleInfo],
|
||||
fee: uint64 = uint64(0),
|
||||
) -> Optional[Offer]:
|
||||
|
||||
for puzzle_info in driver_dict.values():
|
||||
if (
|
||||
puzzle_info.check_type(
|
||||
[
|
||||
AssetType.SINGLETON.value,
|
||||
AssetType.METADATA.value,
|
||||
AssetType.OWNERSHIP.value,
|
||||
]
|
||||
)
|
||||
and isinstance(puzzle_info.also().also()["transfer_program"], PuzzleInfo) # type: ignore
|
||||
and puzzle_info.also().also()["transfer_program"].type() # type: ignore
|
||||
== AssetType.ROYALTY_TRANSFER_PROGRAM.value
|
||||
):
|
||||
return await NFTWallet.make_nft1_offer(self.wallet_state_manager, offer_dict, driver_dict, fee)
|
||||
return None
|
||||
|
||||
def check_for_owner_change_in_drivers(self, puzzle_info: PuzzleInfo, driver_info: PuzzleInfo) -> bool:
|
||||
if puzzle_info.check_type(
|
||||
[
|
||||
AssetType.SINGLETON.value,
|
||||
AssetType.METADATA.value,
|
||||
AssetType.OWNERSHIP.value,
|
||||
]
|
||||
) and driver_info.check_type(
|
||||
[
|
||||
AssetType.SINGLETON.value,
|
||||
AssetType.METADATA.value,
|
||||
AssetType.OWNERSHIP.value,
|
||||
]
|
||||
):
|
||||
old_owner = driver_info.also().also().info["owner"] # type: ignore
|
||||
puzzle_info.also().also().info["owner"] = old_owner # type: ignore
|
||||
if driver_info == puzzle_info:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -12,7 +12,14 @@ from chia.types.coin_spend import CoinSpend
|
||||
from chia.types.spend_bundle import SpendBundle
|
||||
from chia.util.bech32m import bech32_decode, bech32_encode, convertbits
|
||||
from chia.util.ints import uint64
|
||||
from chia.wallet.outer_puzzles import construct_puzzle, create_asset_id, match_puzzle, solve_puzzle
|
||||
from chia.wallet.outer_puzzles import (
|
||||
construct_puzzle,
|
||||
create_asset_id,
|
||||
match_puzzle,
|
||||
solve_puzzle,
|
||||
get_inner_puzzle,
|
||||
get_inner_solution,
|
||||
)
|
||||
from chia.wallet.payment import Payment
|
||||
from chia.wallet.puzzle_drivers import PuzzleInfo, Solver
|
||||
from chia.wallet.puzzles.load_clvm import load_clvm
|
||||
@@ -107,31 +114,70 @@ class Offer:
|
||||
if asset_id is not None and asset_id not in self.driver_dict:
|
||||
raise ValueError("Offer does not have enough driver information about the requested payments")
|
||||
|
||||
def additions(self) -> List[Coin]:
|
||||
final_list: List[Coin] = []
|
||||
for cs in self.bundle.coin_spends:
|
||||
try:
|
||||
final_list.extend(cs.additions())
|
||||
except Exception:
|
||||
pass
|
||||
return final_list
|
||||
|
||||
def removals(self) -> List[Coin]:
|
||||
return self.bundle.removals()
|
||||
|
||||
def incomplete_spends(self) -> List[CoinSpend]:
|
||||
final_list: List[CoinSpend] = []
|
||||
for cs in self.bundle.coin_spends:
|
||||
try:
|
||||
cs.additions()
|
||||
except Exception:
|
||||
final_list.append(cs)
|
||||
return final_list
|
||||
|
||||
# This method does not get every coin that is being offered, only the `settlement_payment` children
|
||||
# It's also a little heuristic, but it should get most things
|
||||
def get_offered_coins(self) -> Dict[Optional[bytes32], List[Coin]]:
|
||||
offered_coins: Dict[Optional[bytes32], List[Coin]] = {}
|
||||
|
||||
for addition in self.bundle.additions():
|
||||
# Get the parent puzzle
|
||||
parent_puzzle: Program = list(
|
||||
filter(lambda cs: cs.coin.name() == addition.parent_coin_info, self.bundle.coin_spends)
|
||||
)[0].puzzle_reveal.to_program()
|
||||
OFFER_HASH: bytes32 = OFFER_MOD.get_tree_hash()
|
||||
for parent_spend in self.bundle.coin_spends:
|
||||
coins_for_this_spend: List[Coin] = []
|
||||
|
||||
parent_puzzle: Program = parent_spend.puzzle_reveal.to_program()
|
||||
parent_solution: Program = parent_spend.solution.to_program()
|
||||
additions: List[Coin] = [a for a in parent_spend.additions() if a not in self.bundle.removals()]
|
||||
|
||||
# Determine it's TAIL (or lack of)
|
||||
puzzle_driver = match_puzzle(parent_puzzle)
|
||||
if puzzle_driver is not None:
|
||||
asset_id = create_asset_id(puzzle_driver)
|
||||
offer_ph: bytes32 = construct_puzzle(self.driver_dict[asset_id], OFFER_MOD).get_tree_hash()
|
||||
inner_puzzle: Optional[Program] = get_inner_puzzle(puzzle_driver, parent_puzzle)
|
||||
inner_solution: Optional[Program] = get_inner_solution(puzzle_driver, parent_solution)
|
||||
assert inner_puzzle is not None and inner_solution is not None
|
||||
conditions: Program = inner_puzzle.run(inner_solution)
|
||||
for condition in conditions.as_iter():
|
||||
if condition.first() == 51 and condition.rest().first() == OFFER_HASH:
|
||||
additions_w_amount: List[Coin] = [
|
||||
a for a in additions if a.amount == condition.rest().rest().first().as_int()
|
||||
]
|
||||
if len(additions_w_amount) == 1:
|
||||
coins_for_this_spend.append(additions_w_amount[0])
|
||||
else:
|
||||
additions_w_amount_and_puzhash: List[Coin] = [
|
||||
a
|
||||
for a in additions_w_amount
|
||||
if a.puzzle_hash
|
||||
== construct_puzzle(puzzle_driver, OFFER_HASH).get_tree_hash(OFFER_HASH) # type: ignore
|
||||
]
|
||||
if len(additions_w_amount_and_puzhash) == 1:
|
||||
coins_for_this_spend.append(additions_w_amount_and_puzhash[0])
|
||||
else:
|
||||
asset_id = None
|
||||
offer_ph = OFFER_MOD.get_tree_hash()
|
||||
coins_for_this_spend.extend([a for a in additions if a.puzzle_hash == OFFER_HASH])
|
||||
|
||||
# Check if the puzzle_hash matches the hypothetical `settlement_payments` puzzle hash
|
||||
if addition.puzzle_hash == offer_ph:
|
||||
if asset_id in offered_coins:
|
||||
offered_coins[asset_id].append(addition)
|
||||
else:
|
||||
offered_coins[asset_id] = [addition]
|
||||
if coins_for_this_spend != []:
|
||||
offered_coins.setdefault(asset_id, [])
|
||||
offered_coins[asset_id].extend(coins_for_this_spend)
|
||||
|
||||
return offered_coins
|
||||
|
||||
@@ -166,8 +212,8 @@ class Offer:
|
||||
offered_amounts: Dict[Optional[bytes32], int] = self.get_offered_amounts()
|
||||
requested_amounts: Dict[Optional[bytes32], int] = self.get_requested_amounts()
|
||||
|
||||
def keys_to_strings(dic: Dict[Optional[bytes32], int]) -> Dict[str, int]:
|
||||
new_dic: Dict[str, int] = {}
|
||||
def keys_to_strings(dic: Dict[Optional[bytes32], Any]) -> Dict[str, Any]:
|
||||
new_dic: Dict[str, Any] = {}
|
||||
for key in dic:
|
||||
if key is None:
|
||||
new_dic["xch"] = dic[key]
|
||||
@@ -184,8 +230,8 @@ class Offer:
|
||||
# Also mostly for the UI, returns a dictionary of assets and how much of them is pended for this offer
|
||||
# This method is also imperfect for sufficiently complex spends
|
||||
def get_pending_amounts(self) -> Dict[str, int]:
|
||||
all_additions: List[Coin] = self.bundle.additions()
|
||||
all_removals: List[Coin] = self.bundle.removals()
|
||||
all_additions: List[Coin] = self.additions()
|
||||
all_removals: List[Coin] = self.removals()
|
||||
non_ephemeral_removals: List[Coin] = list(filter(lambda c: c not in all_additions, all_removals))
|
||||
|
||||
pending_dict: Dict[str, int] = {}
|
||||
@@ -209,13 +255,13 @@ class Offer:
|
||||
|
||||
# This method returns all of the coins that are being used in the offer (without which it would be invalid)
|
||||
def get_involved_coins(self) -> List[Coin]:
|
||||
additions = self.bundle.additions()
|
||||
return list(filter(lambda c: c not in additions, self.bundle.removals()))
|
||||
additions = self.additions()
|
||||
return list(filter(lambda c: c not in additions, self.removals()))
|
||||
|
||||
# This returns the non-ephemeral removal that is an ancestor of the specified coin
|
||||
# This should maybe move to the SpendBundle object at some point
|
||||
def get_root_removal(self, coin: Coin) -> Coin:
|
||||
all_removals: Set[Coin] = set(self.bundle.removals())
|
||||
all_removals: Set[Coin] = set(self.removals())
|
||||
all_removal_ids: Set[bytes32] = {c.name() for c in all_removals}
|
||||
non_ephemeral_removals: Set[Coin] = {
|
||||
c for c in all_removals if c.parent_coin_info not in {r.name() for r in all_removals}
|
||||
|
||||
@@ -5,12 +5,24 @@ from typing import List
|
||||
from chia.wallet.puzzles.load_clvm import load_clvm
|
||||
from chia.wallet.puzzles import p2_delegated_puzzle_or_hidden_puzzle as standard_puzzle
|
||||
from chia.wallet.puzzles.cat_loader import CAT_MOD
|
||||
from chia.wallet.nft_wallet.nft_puzzles import (
|
||||
SINGLETON_TOP_LAYER_MOD,
|
||||
NFT_STATE_LAYER_MOD,
|
||||
NFT_OWNERSHIP_LAYER,
|
||||
NFT_METADATA_UPDATER,
|
||||
NFT_TRANSFER_PROGRAM_DEFAULT,
|
||||
)
|
||||
|
||||
OFFER_MOD = load_clvm("settlement_payments.clvm")
|
||||
|
||||
ZDICT = [
|
||||
bytes(standard_puzzle.MOD) + bytes(CAT_MOD),
|
||||
bytes(OFFER_MOD),
|
||||
bytes(SINGLETON_TOP_LAYER_MOD)
|
||||
+ bytes(NFT_STATE_LAYER_MOD)
|
||||
+ bytes(NFT_OWNERSHIP_LAYER)
|
||||
+ bytes(NFT_METADATA_UPDATER)
|
||||
+ bytes(NFT_TRANSFER_PROGRAM_DEFAULT),
|
||||
# more dictionaries go here
|
||||
]
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class WalletType(IntEnum):
|
||||
CUSTODY = 5
|
||||
CAT = 6
|
||||
RECOVERABLE = 7
|
||||
DISTRIBUTED_ID = 8
|
||||
DECENTRALIZED_ID = 8
|
||||
POOLING_WALLET = 9
|
||||
NFT = 10
|
||||
DATA_LAYER = 11
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import json
|
||||
from typing import List, Optional, Type, TypeVar
|
||||
|
||||
import aiosqlite
|
||||
|
||||
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.db_wrapper import DBWrapper
|
||||
from chia.util.ints import uint32
|
||||
from chia.wallet.lineage_proof import LineageProof
|
||||
from chia.wallet.nft_wallet.nft_info import DEFAULT_STATUS, IN_TRANSACTION_STATUS, NFTCoinInfo
|
||||
|
||||
_T_WalletNftStore = TypeVar("_T_WalletNftStore", bound="WalletNftStore")
|
||||
|
||||
|
||||
class WalletNftStore:
|
||||
"""
|
||||
WalletNftStore keeps track of all user created NFTs and necessary smart-contract data
|
||||
"""
|
||||
|
||||
db_connection: aiosqlite.Connection
|
||||
db_wrapper: DBWrapper
|
||||
|
||||
@classmethod
|
||||
async def create(cls: Type[_T_WalletNftStore], db_wrapper: DBWrapper) -> _T_WalletNftStore:
|
||||
self = cls()
|
||||
self.db_wrapper = db_wrapper
|
||||
self.db_connection = db_wrapper.db
|
||||
await self.db_connection.execute(
|
||||
(
|
||||
"CREATE TABLE IF NOT EXISTS users_nfts("
|
||||
" nft_id text PRIMARY KEY,"
|
||||
" nft_coin_id text,"
|
||||
" wallet_id int,"
|
||||
" did_id text,"
|
||||
" coin text,"
|
||||
" lineage_proof text,"
|
||||
" mint_height bigint,"
|
||||
" status text,"
|
||||
" full_puzzle blob)"
|
||||
)
|
||||
)
|
||||
await self.db_connection.execute("CREATE INDEX IF NOT EXISTS nft_coin_id on users_nfts(nft_coin_id)")
|
||||
await self.db_connection.execute("CREATE INDEX IF NOT EXISTS nft_wallet_id on users_nfts(wallet_id)")
|
||||
await self.db_connection.execute("CREATE INDEX IF NOT EXISTS nft_did_id on users_nfts(did_id)")
|
||||
await self.db_connection.commit()
|
||||
return self
|
||||
|
||||
async def _clear_database(self) -> None:
|
||||
cursor = await self.db_connection.execute("DELETE FROM users_nfts")
|
||||
await cursor.close()
|
||||
await self.db_connection.commit()
|
||||
|
||||
async def delete_nft(self, nft_id: bytes32, in_transaction: bool = False) -> None:
|
||||
if not in_transaction:
|
||||
await self.db_wrapper.lock.acquire()
|
||||
try:
|
||||
cursor = await self.db_connection.execute(f"DELETE FROM users_nfts where nft_id='{nft_id.hex()}'")
|
||||
await cursor.close()
|
||||
finally:
|
||||
if not in_transaction:
|
||||
await self.db_connection.commit()
|
||||
self.db_wrapper.lock.release()
|
||||
|
||||
async def save_nft(
|
||||
self, wallet_id: uint32, did_id: Optional[bytes32], nft_coin_info: NFTCoinInfo, in_transaction: bool = False
|
||||
) -> None:
|
||||
if not in_transaction:
|
||||
await self.db_wrapper.lock.acquire()
|
||||
try:
|
||||
cursor = await self.db_connection.execute(
|
||||
"INSERT or REPLACE INTO users_nfts VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
nft_coin_info.nft_id.hex(),
|
||||
nft_coin_info.coin.name().hex(),
|
||||
int(wallet_id),
|
||||
did_id.hex() if did_id else None,
|
||||
json.dumps(nft_coin_info.coin.to_json_dict()),
|
||||
json.dumps(nft_coin_info.lineage_proof.to_json_dict())
|
||||
if nft_coin_info.lineage_proof is not None
|
||||
else None,
|
||||
int(nft_coin_info.mint_height),
|
||||
IN_TRANSACTION_STATUS if nft_coin_info.pending_transaction else DEFAULT_STATUS,
|
||||
bytes(nft_coin_info.full_puzzle),
|
||||
),
|
||||
)
|
||||
await cursor.close()
|
||||
finally:
|
||||
if not in_transaction:
|
||||
await self.db_connection.commit()
|
||||
self.db_wrapper.lock.release()
|
||||
|
||||
async def get_nft_list(
|
||||
self, wallet_id: Optional[uint32] = None, did_id: Optional[bytes32] = None
|
||||
) -> List[NFTCoinInfo]:
|
||||
sql: str = "SELECT nft_id, coin, lineage_proof, mint_height, status, full_puzzle from users_nfts"
|
||||
if wallet_id is not None and did_id is None:
|
||||
sql += f" where wallet_id={wallet_id}"
|
||||
if wallet_id is None and did_id is not None:
|
||||
sql += f" where did_id='{did_id.hex()}'"
|
||||
if wallet_id is not None and did_id is not None:
|
||||
sql += f" where did_id='{did_id.hex()}' and wallet_id={wallet_id}"
|
||||
cursor = await self.db_connection.execute(sql)
|
||||
rows = await cursor.fetchall()
|
||||
await cursor.close()
|
||||
result = []
|
||||
|
||||
for row in rows:
|
||||
result.append(
|
||||
NFTCoinInfo(
|
||||
bytes32.from_hexstr(row[0]),
|
||||
Coin.from_json_dict(json.loads(row[1])),
|
||||
None if row[2] is None else LineageProof.from_json_dict(json.loads(row[2])),
|
||||
Program.from_bytes(row[5]),
|
||||
uint32(row[3]),
|
||||
row[4] == IN_TRANSACTION_STATUS,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
async def get_nft_by_id(self, nft_id: bytes32) -> Optional[NFTCoinInfo]:
|
||||
cursor = await self.db_connection.execute(
|
||||
"SELECT nft_id, coin, lineage_proof, mint_height, status, full_puzzle from users_nfts WHERE nft_id=?",
|
||||
(nft_id.hex(),),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
await cursor.close()
|
||||
|
||||
if row is None:
|
||||
return None
|
||||
|
||||
return NFTCoinInfo(
|
||||
bytes32.from_hexstr(row[0]),
|
||||
Coin.from_json_dict(json.loads(row[1])),
|
||||
None if row[2] is None else LineageProof.from_json_dict(json.loads(row[2])),
|
||||
Program.from_bytes(row[5]),
|
||||
uint32(row[3]),
|
||||
row[4] == IN_TRANSACTION_STATUS,
|
||||
)
|
||||
|
||||
async def close(self) -> None:
|
||||
await self.db_connection.close()
|
||||
@@ -1457,13 +1457,17 @@ class WalletNode:
|
||||
if len(all_nodes.keys()) == 0:
|
||||
raise ValueError("Not connected to the full node")
|
||||
# Use supplied if provided, prioritize trusted otherwise
|
||||
synced_peers = [node for node in all_nodes.values() if node.peer_node_id in self.synced_peers]
|
||||
if peer is None:
|
||||
for node in list(all_nodes.values()):
|
||||
for node in synced_peers:
|
||||
if self.is_trusted(node):
|
||||
peer = node
|
||||
break
|
||||
if peer is None:
|
||||
peer = list(all_nodes.values())[0]
|
||||
if len(synced_peers) > 0:
|
||||
peer = synced_peers[0]
|
||||
else:
|
||||
peer = list(all_nodes.values())[0]
|
||||
|
||||
assert peer is not None
|
||||
msg = wallet_protocol.RegisterForCoinUpdates(coin_names, uint32(0))
|
||||
|
||||
@@ -28,6 +28,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.types.coin_spend import CoinSpend
|
||||
from chia.types.full_block import FullBlock
|
||||
from chia.types.mempool_inclusion_status import MempoolInclusionStatus
|
||||
from chia.util.bech32m import encode_puzzle_hash
|
||||
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
|
||||
@@ -39,10 +40,13 @@ from chia.wallet.cat_wallet.cat_utils import construct_cat_puzzle, match_cat_puz
|
||||
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_info import DID_HRP
|
||||
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.nft_info import NFTWalletInfo
|
||||
from chia.wallet.nft_wallet.nft_puzzles import get_metadata_and_phs, get_new_owner_did
|
||||
from chia.wallet.nft_wallet.nft_wallet import NFTWallet
|
||||
from chia.wallet.nft_wallet.uncurry_nft import UncurriedNFT
|
||||
from chia.wallet.outer_puzzles import AssetType
|
||||
from chia.wallet.puzzle_drivers import PuzzleInfo
|
||||
@@ -63,6 +67,7 @@ from chia.wallet.wallet_coin_record import WalletCoinRecord
|
||||
from chia.wallet.wallet_coin_store import WalletCoinStore
|
||||
from chia.wallet.wallet_info import WalletInfo
|
||||
from chia.wallet.wallet_interested_store import WalletInterestedStore
|
||||
from chia.wallet.wallet_nft_store import WalletNftStore
|
||||
from chia.wallet.wallet_pool_store import WalletPoolStore
|
||||
from chia.wallet.wallet_puzzle_store import WalletPuzzleStore
|
||||
from chia.wallet.wallet_sync_store import WalletSyncStore
|
||||
@@ -77,6 +82,7 @@ class WalletStateManager:
|
||||
tx_store: WalletTransactionStore
|
||||
puzzle_store: WalletPuzzleStore
|
||||
user_store: WalletUserStore
|
||||
nft_store: WalletNftStore
|
||||
action_store: WalletActionStore
|
||||
basic_store: KeyValStore
|
||||
|
||||
@@ -153,6 +159,7 @@ class WalletStateManager:
|
||||
self.tx_store = await WalletTransactionStore.create(self.db_wrapper)
|
||||
self.puzzle_store = await WalletPuzzleStore.create(self.db_wrapper)
|
||||
self.user_store = await WalletUserStore.create(self.db_wrapper)
|
||||
self.nft_store = await WalletNftStore.create(self.db_wrapper)
|
||||
self.action_store = await WalletActionStore.create(self.db_wrapper)
|
||||
self.basic_store = await KeyValStore.create(self.db_wrapper)
|
||||
self.trade_manager = await TradeManager.create(self, self.db_wrapper)
|
||||
@@ -204,7 +211,7 @@ class WalletStateManager:
|
||||
)
|
||||
elif wallet_info.type == WalletType.RATE_LIMITED:
|
||||
wallet = await RLWallet.create(self, wallet_info)
|
||||
elif wallet_info.type == WalletType.DISTRIBUTED_ID:
|
||||
elif wallet_info.type == WalletType.DECENTRALIZED_ID:
|
||||
wallet = await DIDWallet.create(
|
||||
self,
|
||||
self.main_wallet,
|
||||
@@ -271,9 +278,13 @@ class WalletStateManager:
|
||||
|
||||
for wallet_id in targets:
|
||||
target_wallet = self.wallets[wallet_id]
|
||||
|
||||
if not hasattr(target_wallet, "puzzle_for_pk"):
|
||||
self.log.debug("Skipping wallet %s as no derivation paths required", wallet_id)
|
||||
continue
|
||||
last: Optional[uint32] = await self.puzzle_store.get_last_derivation_path_for_wallet(wallet_id)
|
||||
|
||||
self.log.debug(
|
||||
"Fetched last record for wallet %r: %s (from_zero=%r, unused=%r)", wallet_id, last, from_zero, unused
|
||||
)
|
||||
start_index = 0
|
||||
derivation_paths: List[DerivationRecord] = []
|
||||
|
||||
@@ -333,10 +344,11 @@ class WalletStateManager:
|
||||
[record.wallet_id for record in derivation_paths],
|
||||
in_transaction,
|
||||
)
|
||||
|
||||
if unused > 0:
|
||||
await self.puzzle_store.set_used_up_to(uint32(unused - 1), in_transaction)
|
||||
|
||||
async def update_wallet_puzzle_hashes(self, wallet_id) -> None:
|
||||
async def update_wallet_puzzle_hashes(self, wallet_id, in_transaction=False) -> None:
|
||||
derivation_paths: List[DerivationRecord] = []
|
||||
target_wallet = self.wallets[wallet_id]
|
||||
last: Optional[uint32] = await self.puzzle_store.get_last_derivation_path_for_wallet(wallet_id)
|
||||
@@ -364,7 +376,7 @@ class WalletStateManager:
|
||||
False,
|
||||
)
|
||||
)
|
||||
await self.puzzle_store.add_derivation_paths(derivation_paths)
|
||||
await self.puzzle_store.add_derivation_paths(derivation_paths, in_transaction=in_transaction)
|
||||
|
||||
async def get_unused_derivation_record(
|
||||
self, wallet_id: uint32, in_transaction=False, hardened=False
|
||||
@@ -449,6 +461,9 @@ class WalletStateManager:
|
||||
self.pending_tx_callback()
|
||||
|
||||
async def synced(self):
|
||||
if len(self.server.get_full_node_connections()) == 0:
|
||||
return False
|
||||
|
||||
latest = await self.blockchain.get_peak_block()
|
||||
if latest is None:
|
||||
return False
|
||||
@@ -707,8 +722,21 @@ class WalletStateManager:
|
||||
self.log.warning(f"Could not find the launch coin with ID: {launch_id}")
|
||||
return None, None
|
||||
launch_coin: CoinState = response[0]
|
||||
origin_coin = launch_coin.coin
|
||||
|
||||
for wallet in self.wallets.values():
|
||||
if (
|
||||
wallet.type() == WalletType.DECENTRALIZED_ID
|
||||
and origin_coin.name() == wallet.did_info.origin_coin.name()
|
||||
):
|
||||
return wallet.id(), wallet.type()
|
||||
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()}"
|
||||
self,
|
||||
self.main_wallet,
|
||||
launch_coin.coin,
|
||||
did_puzzle,
|
||||
coin_spend,
|
||||
f"DID {encode_puzzle_hash(launch_id, DID_HRP)}",
|
||||
)
|
||||
wallet_id = did_wallet.id()
|
||||
wallet_type = WalletType(did_wallet.type())
|
||||
@@ -726,30 +754,74 @@ class WalletStateManager:
|
||||
"""
|
||||
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
|
||||
# DID ID determines which NFT wallet should process the NFT
|
||||
new_did_id = None
|
||||
old_did_id = None
|
||||
# P2 puzzle hash determines if we should ignore the NFT
|
||||
old_p2_puzhash = uncurried_nft.p2_puzzle.get_tree_hash()
|
||||
metadata, new_p2_puzhash = get_metadata_and_phs(
|
||||
uncurried_nft,
|
||||
coin_spend.solution,
|
||||
)
|
||||
if uncurried_nft.supports_did:
|
||||
new_did_id = get_new_owner_did(uncurried_nft, coin_spend.solution.to_program())
|
||||
old_did_id = uncurried_nft.owner_did
|
||||
if new_did_id is None:
|
||||
new_did_id = old_did_id
|
||||
if new_did_id == b"":
|
||||
new_did_id = None
|
||||
self.log.debug(
|
||||
"Handling NFT: %s, old DID:%s, new DID:%s, old P2:%s, new P2:%s",
|
||||
coin_spend,
|
||||
old_did_id,
|
||||
new_did_id,
|
||||
old_p2_puzhash,
|
||||
new_p2_puzhash,
|
||||
)
|
||||
new_derivation_record: Optional[
|
||||
DerivationRecord
|
||||
] = await self.puzzle_store.get_derivation_record_for_puzzle_hash(new_p2_puzhash)
|
||||
old_derivation_record: Optional[
|
||||
DerivationRecord
|
||||
] = await self.puzzle_store.get_derivation_record_for_puzzle_hash(old_p2_puzhash)
|
||||
if new_derivation_record is None and old_derivation_record is None:
|
||||
self.log.debug(
|
||||
"Cannot find a P2 puzzle hash for NFT:%s, this NFT belongs to others.",
|
||||
uncurried_nft.singleton_launcher_id.hex(),
|
||||
)
|
||||
wallet_id = uint32(nft_wallet.wallet_id)
|
||||
wallet_type = WalletType.NFT
|
||||
return wallet_id, wallet_type
|
||||
|
||||
for wallet_info in await self.get_all_wallet_info_entries(wallet_type=WalletType.NFT):
|
||||
nft_wallet_info: NFTWalletInfo = NFTWalletInfo.from_json_dict(json.loads(wallet_info.data))
|
||||
if nft_wallet_info.did_id == old_did_id:
|
||||
self.log.info(
|
||||
"Removing old NFT, NFT_ID:%s, DID_ID:%s",
|
||||
uncurried_nft.singleton_launcher_id.hex(),
|
||||
old_did_id,
|
||||
)
|
||||
nft_wallet: NFTWallet = self.wallets[wallet_info.id]
|
||||
await nft_wallet.remove_coin(coin_spend.coin, in_transaction=True)
|
||||
if nft_wallet_info.did_id == new_did_id:
|
||||
self.log.info(
|
||||
"Adding new NFT, NFT_ID:%s, DID_ID:%s",
|
||||
uncurried_nft.singleton_launcher_id.hex(),
|
||||
new_did_id,
|
||||
)
|
||||
wallet_id = wallet_info.id
|
||||
wallet_type = WalletType.NFT
|
||||
|
||||
if wallet_id is None and new_derivation_record:
|
||||
# Cannot find an existed NFT wallet for the new NFT
|
||||
self.log.info(
|
||||
"Cannot find a NFT wallet for NFT_ID: %s DID_ID: %s, creating a new one.",
|
||||
uncurried_nft.singleton_launcher_id,
|
||||
new_did_id,
|
||||
)
|
||||
new_nft_wallet: NFTWallet = await NFTWallet.create_new_nft_wallet(
|
||||
self, self.main_wallet, did_id=new_did_id, name="NFT Wallet", in_transaction=True
|
||||
)
|
||||
wallet_id = uint32(new_nft_wallet.wallet_id)
|
||||
wallet_type = WalletType.NFT
|
||||
return wallet_id, wallet_type
|
||||
|
||||
async def new_coin_state(
|
||||
@@ -760,7 +832,6 @@ class WalletStateManager:
|
||||
in_transaction: bool = False,
|
||||
) -> None:
|
||||
# TODO: add comment about what this method does
|
||||
|
||||
# Input states should already be sorted by cs_height, with reorgs at the beginning
|
||||
curr_h = -1
|
||||
for c_state in coin_states:
|
||||
@@ -1224,7 +1295,7 @@ class WalletStateManager:
|
||||
)
|
||||
await self.coin_store.add_coin_record(coin_record_1)
|
||||
|
||||
if wallet_type == WalletType.CAT or wallet_type == WalletType.DISTRIBUTED_ID:
|
||||
if wallet_type == WalletType.CAT or wallet_type == WalletType.DECENTRALIZED_ID:
|
||||
wallet = self.wallets[wallet_id]
|
||||
await wallet.coin_added(coin, height)
|
||||
|
||||
@@ -1351,6 +1422,10 @@ class WalletStateManager:
|
||||
if wallet.type() == WalletType.CAT:
|
||||
if bytes(wallet.cat_info.limitations_program_hash).hex() == asset_id:
|
||||
return wallet
|
||||
elif wallet.type() == WalletType.NFT:
|
||||
for nft_coin in wallet.my_nft_coins:
|
||||
if nft_coin.nft_id.hex() == asset_id:
|
||||
return wallet
|
||||
return None
|
||||
|
||||
async def get_wallet_for_puzzle_info(self, puzzle_driver: PuzzleInfo):
|
||||
@@ -1363,14 +1438,13 @@ class WalletStateManager:
|
||||
|
||||
async def create_wallet_for_puzzle_info(self, puzzle_driver: PuzzleInfo, name=None, in_transaction=False):
|
||||
if AssetType(puzzle_driver.type()) in self.asset_to_wallet_map:
|
||||
async with self.lock:
|
||||
await self.asset_to_wallet_map[AssetType(puzzle_driver.type())].create_from_puzzle_info(
|
||||
self,
|
||||
self.main_wallet,
|
||||
puzzle_driver,
|
||||
name,
|
||||
in_transaction,
|
||||
)
|
||||
await self.asset_to_wallet_map[AssetType(puzzle_driver.type())].create_from_puzzle_info(
|
||||
self,
|
||||
self.main_wallet,
|
||||
puzzle_driver,
|
||||
name,
|
||||
in_transaction,
|
||||
)
|
||||
|
||||
async def add_new_wallet(self, wallet: Any, wallet_id: int, create_puzzle_hashes=True, in_transaction=False):
|
||||
self.wallets[uint32(wallet_id)] = wallet
|
||||
|
||||
@@ -180,10 +180,10 @@ class WalletWeightProofHandler:
|
||||
|
||||
overflow = 0
|
||||
count = 0
|
||||
for idx, new_ses in enumerate(new_wp.sub_epochs):
|
||||
for new_ses in new_wp.sub_epochs:
|
||||
if new_ses.reward_chain_hash in old_ses:
|
||||
count += 1
|
||||
overflow += new_ses.num_blocks_overflow
|
||||
overflow = new_ses.num_blocks_overflow
|
||||
continue
|
||||
else:
|
||||
break
|
||||
|
||||
@@ -45,12 +45,11 @@ wallet_program_files = set(
|
||||
"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_metadata_updater_updateable.clvm",
|
||||
"chia/wallet/puzzles/nft_state_layer.clvm",
|
||||
"chia/wallet/puzzles/nft_ownership_layer.clvm",
|
||||
"chia/wallet/puzzles/nft_ownership_transfer_program_one_way_claim_with_royalties.clvm",
|
||||
"chia/wallet/puzzles/database_offer.clvm",
|
||||
"chia/wallet/puzzles/database_layer.clvm",
|
||||
"chia/wallet/puzzles/singleton_top_layer_atari_only.clvm",
|
||||
@@ -168,7 +167,7 @@ class TestClvmCompilation(TestCase):
|
||||
self.assertEqual(
|
||||
s.get_tree_hash().hex(),
|
||||
existing_sha,
|
||||
msg=f"Checked-in shatree hash file does not match shatree hash of loaded SerializedProgram: {prog_path}", # noqa
|
||||
msg=f"Checked-in shatree hash file does not match hash of loaded SerializedProgram: {prog_path}",
|
||||
)
|
||||
self.assertEqual(
|
||||
p.get_tree_hash().hex(),
|
||||
|
||||
@@ -14,11 +14,7 @@ from chia.util.errors import Err
|
||||
from chia.util.ints import uint64
|
||||
from chia.consensus.default_constants import DEFAULT_CONSTANTS
|
||||
from chia.wallet.lineage_proof import LineageProof
|
||||
from chia.wallet.puzzles import (
|
||||
p2_conditions,
|
||||
p2_delegated_puzzle_or_hidden_puzzle,
|
||||
singleton_top_layer,
|
||||
)
|
||||
from chia.wallet.puzzles import p2_conditions, p2_delegated_puzzle_or_hidden_puzzle
|
||||
from tests.util.key_tool import KeyTool
|
||||
from tests.clvm.test_puzzles import (
|
||||
public_key_for_index,
|
||||
@@ -31,6 +27,8 @@ from chia.clvm.spend_sim import SpendSim, SimClient
|
||||
This test suite aims to test:
|
||||
- chia.wallet.puzzles.singleton_top_layer.py
|
||||
- chia.wallet.puzzles.singleton_top_layer.clvm
|
||||
- chia.wallet.puzzles.singleton_top_layer_v1_1.py
|
||||
- chia.wallet.puzzles.singleton_top_layer_v1_1.clvm
|
||||
- chia.wallet.puzzles.p2_singleton.clvm
|
||||
- chia.wallet.puzzles.p2_singleton_or_delayed_puzhash.clvm
|
||||
"""
|
||||
@@ -83,14 +81,23 @@ class TestSingleton:
|
||||
raise AssertionError(fail_msg)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_singleton_top_layer(self):
|
||||
@pytest.mark.parametrize("version", [0, 1])
|
||||
async def test_singleton_top_layer(self, version):
|
||||
try:
|
||||
# START TESTS
|
||||
# Generate starting info
|
||||
key_lookup = KeyTool()
|
||||
pk: G1Element = public_key_for_index(1, key_lookup)
|
||||
starting_puzzle: Program = p2_delegated_puzzle_or_hidden_puzzle.puzzle_for_pk(pk) # noqa
|
||||
adapted_puzzle: Program = singleton_top_layer.adapt_inner_to_singleton(starting_puzzle) # noqa
|
||||
|
||||
if version == 0:
|
||||
from chia.wallet.puzzles import singleton_top_layer
|
||||
|
||||
adapted_puzzle: Program = singleton_top_layer.adapt_inner_to_singleton(starting_puzzle) # noqa
|
||||
else:
|
||||
from chia.wallet.puzzles import singleton_top_layer_v1_1 as singleton_top_layer
|
||||
|
||||
adapted_puzzle = starting_puzzle
|
||||
adapted_puzzle_hash: bytes32 = adapted_puzzle.get_tree_hash()
|
||||
|
||||
# Get our starting standard coin created
|
||||
@@ -436,7 +443,7 @@ class TestSingleton:
|
||||
)
|
||||
|
||||
# Now try a perfectly innocent spend
|
||||
evil_coin: Coin = (await sim.all_non_reward_coins())[0]
|
||||
evil_coin: Coin = next(filter(lambda c: c.amount == 2, (await sim.all_non_reward_coins())))
|
||||
delegated_puzzle: Program = Program.to(
|
||||
(
|
||||
1,
|
||||
@@ -473,7 +480,7 @@ class TestSingleton:
|
||||
evil_coin,
|
||||
delegated_puzzle,
|
||||
[evil_coinsol],
|
||||
ex_error=Err.ASSERT_MY_COIN_ID_FAILED,
|
||||
ex_error=Err.ASSERT_MY_COIN_ID_FAILED if version == 0 else Err.ASSERT_MY_AMOUNT_FAILED,
|
||||
fail_msg="This coin is even!",
|
||||
)
|
||||
|
||||
|
||||
@@ -20,10 +20,14 @@ from tests.time_out_assert import time_out_assert
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def disconnect_all_and_reconnect(server: ChiaServer, reconnect_to: ChiaServer, self_hostname: str) -> bool:
|
||||
async def disconnect_all(server: ChiaServer) -> None:
|
||||
cons = list(server.all_connections.values())[:]
|
||||
for con in cons:
|
||||
await con.close()
|
||||
|
||||
|
||||
async def disconnect_all_and_reconnect(server: ChiaServer, reconnect_to: ChiaServer, self_hostname: str) -> bool:
|
||||
await disconnect_all(server)
|
||||
return await server.start_client(PeerInfo(self_hostname, uint16(reconnect_to._port)), None)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
from typing import Optional
|
||||
|
||||
from clvm_tools.binutils import disassemble
|
||||
|
||||
from chia.types.blockchain_format.coin import Coin
|
||||
from chia.types.blockchain_format.program import Program
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.types.coin_spend import CoinSpend
|
||||
from chia.util.ints import uint64
|
||||
from chia.wallet.cat_wallet.cat_utils import CAT_MOD, construct_cat_puzzle
|
||||
from chia.wallet.outer_puzzles import (
|
||||
construct_puzzle,
|
||||
create_asset_id,
|
||||
get_inner_puzzle,
|
||||
get_inner_solution,
|
||||
match_puzzle,
|
||||
solve_puzzle,
|
||||
)
|
||||
from chia.wallet.puzzle_drivers import PuzzleInfo, Solver
|
||||
|
||||
|
||||
def test_cat_outer_puzzle() -> None:
|
||||
ACS = Program.to(1)
|
||||
tail = bytes32([0] * 32)
|
||||
cat_puzzle: Program = construct_cat_puzzle(CAT_MOD, tail, ACS)
|
||||
double_cat_puzzle: Program = construct_cat_puzzle(CAT_MOD, tail, cat_puzzle)
|
||||
cat_driver: Optional[PuzzleInfo] = match_puzzle(double_cat_puzzle)
|
||||
|
||||
assert cat_driver is not None
|
||||
assert cat_driver.type() == "CAT"
|
||||
assert cat_driver["tail"] == tail
|
||||
inside_cat_driver: Optional[PuzzleInfo] = cat_driver.also()
|
||||
assert inside_cat_driver is not None
|
||||
assert inside_cat_driver.type() == "CAT"
|
||||
assert inside_cat_driver["tail"] == tail
|
||||
assert construct_puzzle(cat_driver, ACS) == double_cat_puzzle
|
||||
assert get_inner_puzzle(cat_driver, double_cat_puzzle) == ACS
|
||||
assert create_asset_id(cat_driver) == tail
|
||||
|
||||
# Set up for solve
|
||||
parent_coin = Coin(tail, double_cat_puzzle.get_tree_hash(), uint64(100))
|
||||
child_coin = Coin(parent_coin.name(), double_cat_puzzle.get_tree_hash(), uint64(100))
|
||||
parent_spend = CoinSpend(parent_coin, double_cat_puzzle.to_serialized_program(), Program.to([]))
|
||||
child_coin_as_hex: str = (
|
||||
"0x" + child_coin.parent_coin_info.hex() + child_coin.puzzle_hash.hex() + bytes(child_coin.amount).hex()
|
||||
)
|
||||
parent_spend_as_hex: str = "0x" + bytes(parent_spend).hex()
|
||||
inner_solution = Program.to([[51, ACS.get_tree_hash(), 100]])
|
||||
|
||||
solution: Program = solve_puzzle(
|
||||
cat_driver,
|
||||
Solver(
|
||||
{
|
||||
"coin": child_coin_as_hex,
|
||||
"parent_spend": parent_spend_as_hex,
|
||||
"siblings": "(" + child_coin_as_hex + ")",
|
||||
"sibling_spends": "(" + parent_spend_as_hex + ")",
|
||||
"sibling_puzzles": "(" + disassemble(ACS) + ")", # type: ignore
|
||||
"sibling_solutions": "(" + disassemble(inner_solution) + ")", # type: ignore
|
||||
}
|
||||
),
|
||||
ACS,
|
||||
inner_solution,
|
||||
)
|
||||
double_cat_puzzle.run(solution)
|
||||
assert get_inner_solution(cat_driver, solution) == inner_solution
|
||||
@@ -23,8 +23,12 @@ async def get_wallet_num(wallet_manager):
|
||||
|
||||
|
||||
class TestDIDWallet:
|
||||
@pytest.mark.parametrize(
|
||||
"trusted",
|
||||
[True, False],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_creation_from_backup_file(self, self_hostname, three_wallet_nodes):
|
||||
async def test_creation_from_backup_file(self, self_hostname, three_wallet_nodes, trusted):
|
||||
num_blocks = 5
|
||||
full_nodes, wallets = three_wallet_nodes
|
||||
full_node_api = full_nodes[0]
|
||||
@@ -39,7 +43,20 @@ class TestDIDWallet:
|
||||
ph = await wallet_0.get_new_puzzlehash()
|
||||
ph1 = await wallet_1.get_new_puzzlehash()
|
||||
ph2 = await wallet_2.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()
|
||||
}
|
||||
wallet_node_2.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"] = {}
|
||||
wallet_node_2.config["trusted_peers"] = {}
|
||||
await server_0.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None)
|
||||
await server_1.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None)
|
||||
await server_2.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None)
|
||||
@@ -67,7 +84,9 @@ class TestDIDWallet:
|
||||
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_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())
|
||||
@@ -86,7 +105,9 @@ class TestDIDWallet:
|
||||
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_list = await wallet_node_1.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())
|
||||
@@ -121,7 +142,6 @@ class TestDIDWallet:
|
||||
|
||||
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):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
|
||||
@@ -139,7 +159,6 @@ class TestDIDWallet:
|
||||
pubkey,
|
||||
test_message_spend_bundle,
|
||||
)
|
||||
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())
|
||||
|
||||
@@ -172,8 +191,12 @@ class TestDIDWallet:
|
||||
await time_out_assert(45, did_wallet_2.get_confirmed_balance, 0)
|
||||
await time_out_assert(45, did_wallet_2.get_unconfirmed_balance, 0)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trusted",
|
||||
[True, False],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_did_recovery_with_multiple_backup_dids(self, self_hostname, two_wallet_nodes):
|
||||
async def test_did_recovery_with_multiple_backup_dids(self, self_hostname, two_wallet_nodes, trusted):
|
||||
num_blocks = 5
|
||||
full_nodes, wallets = two_wallet_nodes
|
||||
full_node_api = full_nodes[0]
|
||||
@@ -184,7 +207,16 @@ class TestDIDWallet:
|
||||
wallet2 = wallet_node_2.wallet_state_manager.main_wallet
|
||||
|
||||
ph = await wallet.get_new_puzzlehash()
|
||||
|
||||
if trusted:
|
||||
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()
|
||||
}
|
||||
else:
|
||||
wallet_node.config["trusted_peers"] = {}
|
||||
wallet_node_2.config["trusted_peers"] = {}
|
||||
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)
|
||||
|
||||
@@ -205,7 +237,7 @@ class TestDIDWallet:
|
||||
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_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())
|
||||
@@ -224,7 +256,9 @@ class TestDIDWallet:
|
||||
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_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())
|
||||
@@ -244,7 +278,9 @@ class TestDIDWallet:
|
||||
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_list = await wallet_node_2.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())
|
||||
@@ -268,10 +304,11 @@ class TestDIDWallet:
|
||||
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_did_inner_hash()
|
||||
new_ph = did_wallet_4.did_info.temp_puzhash
|
||||
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())
|
||||
|
||||
@@ -294,7 +331,8 @@ class TestDIDWallet:
|
||||
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph2))
|
||||
|
||||
await time_out_assert(15, did_wallet_4.get_confirmed_balance, 0)
|
||||
await time_out_assert(15, did_wallet_4.get_unconfirmed_balance, 0)
|
||||
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()
|
||||
@@ -311,8 +349,12 @@ class TestDIDWallet:
|
||||
await time_out_assert(15, did_wallet_3.get_confirmed_balance, 0)
|
||||
await time_out_assert(15, did_wallet_3.get_unconfirmed_balance, 0)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trusted",
|
||||
[True, False],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_did_recovery_with_empty_set(self, self_hostname, two_wallet_nodes):
|
||||
async def test_did_recovery_with_empty_set(self, self_hostname, two_wallet_nodes, trusted):
|
||||
num_blocks = 5
|
||||
full_nodes, wallets = two_wallet_nodes
|
||||
full_node_api = full_nodes[0]
|
||||
@@ -322,7 +364,16 @@ class TestDIDWallet:
|
||||
wallet = wallet_node.wallet_state_manager.main_wallet
|
||||
|
||||
ph = await wallet.get_new_puzzlehash()
|
||||
|
||||
if trusted:
|
||||
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()
|
||||
}
|
||||
else:
|
||||
wallet_node.config["trusted_peers"] = {}
|
||||
wallet_node_2.config["trusted_peers"] = {}
|
||||
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)
|
||||
|
||||
@@ -343,7 +394,7 @@ class TestDIDWallet:
|
||||
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_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())
|
||||
@@ -355,7 +406,6 @@ class TestDIDWallet:
|
||||
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
|
||||
try:
|
||||
@@ -368,8 +418,12 @@ class TestDIDWallet:
|
||||
else:
|
||||
assert False
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trusted",
|
||||
[True, False],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes):
|
||||
async def test_did_attest_after_recovery(self, self_hostname, two_wallet_nodes, trusted):
|
||||
num_blocks = 5
|
||||
full_nodes, wallets = two_wallet_nodes
|
||||
full_node_api = full_nodes[0]
|
||||
@@ -379,7 +433,16 @@ class TestDIDWallet:
|
||||
wallet = wallet_node.wallet_state_manager.main_wallet
|
||||
wallet2 = wallet_node_2.wallet_state_manager.main_wallet
|
||||
ph = await wallet.get_new_puzzlehash()
|
||||
|
||||
if trusted:
|
||||
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()
|
||||
}
|
||||
else:
|
||||
wallet_node.config["trusted_peers"] = {}
|
||||
wallet_node_2.config["trusted_peers"] = {}
|
||||
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):
|
||||
@@ -398,7 +461,7 @@ class TestDIDWallet:
|
||||
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_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())
|
||||
@@ -414,7 +477,9 @@ class TestDIDWallet:
|
||||
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_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())
|
||||
@@ -457,6 +522,7 @@ class TestDIDWallet:
|
||||
pubkey = (
|
||||
await did_wallet_3.wallet_state_manager.get_unused_derivation_record(did_wallet_3.wallet_info.id)
|
||||
).pubkey
|
||||
await time_out_assert(15, did_wallet.get_confirmed_balance, 101)
|
||||
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())
|
||||
|
||||
@@ -533,9 +599,14 @@ class TestDIDWallet:
|
||||
"with_recovery",
|
||||
[True, False],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"trusted",
|
||||
[True, False],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_did_transfer(self, two_wallet_nodes, with_recovery):
|
||||
async def test_did_transfer(self, two_wallet_nodes, with_recovery, trusted):
|
||||
num_blocks = 5
|
||||
fee = uint64(1000)
|
||||
full_nodes, wallets = two_wallet_nodes
|
||||
full_node_api = full_nodes[0]
|
||||
server_1 = full_node_api.server
|
||||
@@ -545,13 +616,16 @@ class TestDIDWallet:
|
||||
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()
|
||||
}
|
||||
if trusted:
|
||||
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()
|
||||
}
|
||||
else:
|
||||
wallet_node.config["trusted_peers"] = {}
|
||||
wallet_node_2.config["trusted_peers"] = {}
|
||||
|
||||
await server_2.start_client(PeerInfo("localhost", uint16(server_1._port)), None)
|
||||
await server_3.start_client(PeerInfo("localhost", uint16(server_1._port)), None)
|
||||
@@ -575,20 +649,9 @@ class TestDIDWallet:
|
||||
[bytes(ph)],
|
||||
uint64(1),
|
||||
{"Twitter": "Test", "GitHub": "测试"},
|
||||
fee=fee,
|
||||
)
|
||||
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()
|
||||
)
|
||||
@@ -597,12 +660,29 @@ class TestDIDWallet:
|
||||
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)
|
||||
await time_out_assert(15, wallet.get_confirmed_balance, 7999999998899)
|
||||
await time_out_assert(15, wallet.get_unconfirmed_balance, 7999999998899)
|
||||
# Transfer DID
|
||||
new_puzhash = await wallet2.get_new_puzzlehash()
|
||||
await did_wallet_1.transfer_did(new_puzhash, fee, with_recovery)
|
||||
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))
|
||||
await time_out_assert(15, wallet.get_confirmed_balance, 7999999997899)
|
||||
await time_out_assert(15, wallet.get_unconfirmed_balance, 7999999997899)
|
||||
# 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),
|
||||
lambda w: (w.type == WalletType.DECENTRALIZED_ID),
|
||||
await wallet_node_2.wallet_state_manager.get_all_wallet_info_entries(),
|
||||
)
|
||||
)
|
||||
@@ -611,12 +691,16 @@ class TestDIDWallet:
|
||||
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)
|
||||
metadata = json.loads(did_wallet_2.did_info.metadata)
|
||||
assert metadata["Twitter"] == "Test"
|
||||
assert metadata["GitHub"] == "测试"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trusted",
|
||||
[True, False],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_recovery_list(self, two_wallet_nodes):
|
||||
async def test_update_recovery_list(self, two_wallet_nodes, trusted):
|
||||
num_blocks = 5
|
||||
full_nodes, wallets = two_wallet_nodes
|
||||
full_node_api = full_nodes[0]
|
||||
@@ -626,13 +710,16 @@ class TestDIDWallet:
|
||||
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()
|
||||
}
|
||||
if trusted:
|
||||
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()
|
||||
}
|
||||
else:
|
||||
wallet_node.config["trusted_peers"] = {}
|
||||
wallet_node_2.config["trusted_peers"] = {}
|
||||
|
||||
await server_2.start_client(PeerInfo("localhost", uint16(server_1._port)), None)
|
||||
await server_3.start_client(PeerInfo("localhost", uint16(server_1._port)), None)
|
||||
@@ -652,7 +739,9 @@ class TestDIDWallet:
|
||||
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_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 wallet.get_new_puzzlehash()
|
||||
@@ -670,24 +759,33 @@ class TestDIDWallet:
|
||||
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.parametrize(
|
||||
"trusted",
|
||||
[True, False],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_metadata(self, two_wallet_nodes):
|
||||
async def test_update_metadata(self, two_wallet_nodes, trusted):
|
||||
num_blocks = 5
|
||||
fee = uint64(1000)
|
||||
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
|
||||
wallet1 = 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()
|
||||
}
|
||||
ph1 = await wallet1.get_new_puzzlehash()
|
||||
if trusted:
|
||||
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()
|
||||
}
|
||||
else:
|
||||
wallet_node.config["trusted_peers"] = {}
|
||||
wallet_node_2.config["trusted_peers"] = {}
|
||||
|
||||
await server_2.start_client(PeerInfo("localhost", uint16(server_1._port)), None)
|
||||
await server_3.start_client(PeerInfo("localhost", uint16(server_1._port)), None)
|
||||
@@ -705,23 +803,28 @@ class TestDIDWallet:
|
||||
|
||||
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), []
|
||||
wallet_node.wallet_state_manager, wallet, uint64(101), [], fee=fee
|
||||
)
|
||||
spend_bundle_list = await wallet_node.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(wallet.id())
|
||||
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 wallet.get_new_puzzlehash()
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph2))
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1))
|
||||
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 time_out_assert(15, wallet.get_confirmed_balance, 7999999998899)
|
||||
await time_out_assert(15, wallet.get_unconfirmed_balance, 7999999998899)
|
||||
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()
|
||||
await did_wallet_1.create_update_spend(fee)
|
||||
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph2))
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1))
|
||||
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 time_out_assert(15, wallet.get_confirmed_balance, 7999999997899)
|
||||
await time_out_assert(15, wallet.get_unconfirmed_balance, 7999999997899)
|
||||
assert did_wallet_1.did_info.metadata.find("Twitter") > 0
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,93 +0,0 @@
|
||||
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()
|
||||
)
|
||||
@@ -0,0 +1,365 @@
|
||||
import itertools
|
||||
from typing import List, Tuple
|
||||
|
||||
import pytest
|
||||
from blspy import G2Element
|
||||
|
||||
from chia.clvm.spend_sim import SimClient, SpendSim
|
||||
from chia.types.announcement import Announcement
|
||||
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.mempool_inclusion_status import MempoolInclusionStatus
|
||||
from chia.types.spend_bundle import SpendBundle
|
||||
from chia.util.errors import Err
|
||||
from chia.wallet.nft_wallet.nft_puzzles import (
|
||||
NFT_METADATA_UPDATER,
|
||||
NFT_TRANSFER_PROGRAM_DEFAULT,
|
||||
construct_ownership_layer,
|
||||
create_nft_layer_puzzle_with_curry_params,
|
||||
metadata_to_program,
|
||||
)
|
||||
|
||||
ACS = Program.to(1)
|
||||
ACS_PH = ACS.get_tree_hash()
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
@pytest.mark.parametrize("metadata_updater", ["default"])
|
||||
async def test_state_layer(setup_sim: Tuple[SpendSim, SimClient], metadata_updater: str) -> None:
|
||||
sim, sim_client = setup_sim
|
||||
|
||||
try:
|
||||
if metadata_updater == "default":
|
||||
METADATA: Program = metadata_to_program(
|
||||
{
|
||||
b"u": ["hey hey"],
|
||||
b"lu": ["You have no permissions grr"],
|
||||
b"mu": ["This but off chain"],
|
||||
b"foo": ["Can't update this"],
|
||||
}
|
||||
)
|
||||
METADATA_UPDATER: Program = NFT_METADATA_UPDATER
|
||||
else:
|
||||
# TODO: Add test for updateable
|
||||
return
|
||||
METADATA_UPDATER_PUZZLE_HASH: bytes32 = METADATA_UPDATER.get_tree_hash()
|
||||
|
||||
state_layer_puzzle: Program = create_nft_layer_puzzle_with_curry_params(
|
||||
METADATA, METADATA_UPDATER_PUZZLE_HASH, ACS
|
||||
)
|
||||
state_layer_ph: bytes32 = state_layer_puzzle.get_tree_hash()
|
||||
await sim.farm_block(state_layer_ph)
|
||||
state_layer_coin = (
|
||||
await sim_client.get_coin_records_by_puzzle_hash(state_layer_ph, include_spent_coins=False)
|
||||
)[0].coin
|
||||
|
||||
generic_spend = CoinSpend(
|
||||
state_layer_coin,
|
||||
state_layer_puzzle,
|
||||
Program.to([[[51, ACS_PH, 1]]]),
|
||||
)
|
||||
generic_bundle = SpendBundle([generic_spend], G2Element())
|
||||
|
||||
result = await sim_client.push_tx(generic_bundle)
|
||||
assert result == (MempoolInclusionStatus.SUCCESS, None)
|
||||
await sim.farm_block()
|
||||
|
||||
if metadata_updater == "default":
|
||||
metadata_updater_solutions: List[Program] = [
|
||||
Program.to((b"u", "update")),
|
||||
Program.to((b"lu", "update")),
|
||||
Program.to((b"mu", "update")),
|
||||
Program.to((b"foo", "update")),
|
||||
]
|
||||
expected_metadatas: List[Program] = [
|
||||
metadata_to_program(
|
||||
{
|
||||
b"u": ["update", "hey hey"],
|
||||
b"lu": ["You have no permissions grr"],
|
||||
b"mu": ["This but off chain"],
|
||||
b"foo": ["Can't update this"],
|
||||
}
|
||||
),
|
||||
metadata_to_program(
|
||||
{
|
||||
b"u": ["update", "hey hey"],
|
||||
b"lu": ["update", "You have no permissions grr"],
|
||||
b"mu": ["This but off chain"],
|
||||
b"foo": ["Can't update this"],
|
||||
}
|
||||
),
|
||||
metadata_to_program(
|
||||
{
|
||||
b"u": ["update", "hey hey"],
|
||||
b"lu": ["update", "You have no permissions grr"],
|
||||
b"mu": ["update", "This but off chain"],
|
||||
b"foo": ["Can't update this"],
|
||||
}
|
||||
),
|
||||
metadata_to_program(
|
||||
{ # no change
|
||||
b"u": ["update", "hey hey"],
|
||||
b"lu": ["update", "You have no permissions grr"],
|
||||
b"mu": ["update", "This but off chain"],
|
||||
b"foo": ["Can't update this"],
|
||||
}
|
||||
),
|
||||
]
|
||||
else:
|
||||
return
|
||||
|
||||
for solution, metadata in zip(metadata_updater_solutions, expected_metadatas):
|
||||
state_layer_coin = (
|
||||
await sim_client.get_coin_records_by_parent_ids([state_layer_coin.name()], include_spent_coins=False)
|
||||
)[0].coin
|
||||
update_spend = CoinSpend(
|
||||
state_layer_coin,
|
||||
state_layer_puzzle,
|
||||
Program.to(
|
||||
[
|
||||
[
|
||||
[51, ACS_PH, 1],
|
||||
[-24, METADATA_UPDATER, solution],
|
||||
]
|
||||
]
|
||||
),
|
||||
)
|
||||
update_bundle = SpendBundle([update_spend], G2Element())
|
||||
result = await sim_client.push_tx(update_bundle)
|
||||
assert result == (MempoolInclusionStatus.SUCCESS, None)
|
||||
await sim.farm_block()
|
||||
state_layer_puzzle = create_nft_layer_puzzle_with_curry_params(metadata, METADATA_UPDATER_PUZZLE_HASH, ACS)
|
||||
finally:
|
||||
await sim.close() # type: ignore
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_ownership_layer(setup_sim: Tuple[SpendSim, SimClient]) -> None:
|
||||
sim, sim_client = setup_sim
|
||||
|
||||
try:
|
||||
TARGET_OWNER = bytes32([0] * 32)
|
||||
TARGET_TP = Program.to([8]) # (x)
|
||||
# (a (i 11 (q 4 19 (c 43 (q ()))) (q 8)) 1) or
|
||||
# (mod (_ _ solution) (if solution (list (f solution) (f (r solution)) ()) (x)))
|
||||
transfer_program = Program.to([2, [3, 11, [1, 4, 19, [4, 43, [1, []]]], [1, 8]], 1])
|
||||
|
||||
ownership_puzzle: Program = construct_ownership_layer(
|
||||
None,
|
||||
transfer_program,
|
||||
ACS,
|
||||
)
|
||||
ownership_ph: bytes32 = ownership_puzzle.get_tree_hash()
|
||||
await sim.farm_block(ownership_ph)
|
||||
ownership_coin = (await sim_client.get_coin_records_by_puzzle_hash(ownership_ph, include_spent_coins=False))[
|
||||
0
|
||||
].coin
|
||||
|
||||
generic_spend = CoinSpend(
|
||||
ownership_coin,
|
||||
ownership_puzzle,
|
||||
Program.to([[[51, ACS_PH, 1], [-10, [], []]]]),
|
||||
)
|
||||
generic_bundle = SpendBundle([generic_spend], G2Element())
|
||||
result = await sim_client.push_tx(generic_bundle)
|
||||
assert result == (MempoolInclusionStatus.SUCCESS, None)
|
||||
await sim.farm_block()
|
||||
ownership_coin = (await sim_client.get_coin_records_by_puzzle_hash(ownership_ph, include_spent_coins=False))[
|
||||
0
|
||||
].coin
|
||||
|
||||
skip_tp_spend = CoinSpend(
|
||||
ownership_coin,
|
||||
ownership_puzzle,
|
||||
Program.to([[[51, ACS_PH, 1]]]),
|
||||
)
|
||||
skip_tp_bundle = SpendBundle([skip_tp_spend], G2Element())
|
||||
|
||||
result = await sim_client.push_tx(skip_tp_bundle)
|
||||
assert result == (MempoolInclusionStatus.FAILED, Err.GENERATOR_RUNTIME_ERROR)
|
||||
with pytest.raises(ValueError, match="clvm raise"):
|
||||
skip_tp_spend.puzzle_reveal.to_program().run(skip_tp_spend.solution.to_program())
|
||||
|
||||
make_bad_announcement_spend = CoinSpend(
|
||||
ownership_coin,
|
||||
ownership_puzzle,
|
||||
Program.to(
|
||||
[
|
||||
[
|
||||
[51, ACS_PH, 1],
|
||||
[-10, TARGET_OWNER, TARGET_TP],
|
||||
[62, b"\xad\x4c" + bytes32([0] * 32)],
|
||||
]
|
||||
]
|
||||
),
|
||||
)
|
||||
make_bad_announcement_bundle = SpendBundle([make_bad_announcement_spend], G2Element())
|
||||
|
||||
result = await sim_client.push_tx(make_bad_announcement_bundle)
|
||||
assert result == (MempoolInclusionStatus.FAILED, Err.GENERATOR_RUNTIME_ERROR)
|
||||
with pytest.raises(ValueError, match="clvm raise"):
|
||||
make_bad_announcement_spend.puzzle_reveal.to_program().run(
|
||||
make_bad_announcement_spend.solution.to_program()
|
||||
)
|
||||
|
||||
expected_announcement = Announcement(
|
||||
ownership_puzzle.get_tree_hash(),
|
||||
b"\xad\x4c" + Program.to([TARGET_OWNER, TARGET_TP]).get_tree_hash(),
|
||||
)
|
||||
harmless_announcement = Announcement(
|
||||
ownership_puzzle.get_tree_hash(),
|
||||
b"oy",
|
||||
)
|
||||
update_everything_spend = CoinSpend(
|
||||
ownership_coin,
|
||||
ownership_puzzle,
|
||||
Program.to(
|
||||
[
|
||||
[
|
||||
[51, ACS_PH, 1],
|
||||
[-10, TARGET_OWNER, TARGET_TP],
|
||||
[62, harmless_announcement.message], # create a harmless puzzle announcement
|
||||
[63, expected_announcement.name()],
|
||||
[63, harmless_announcement.name()],
|
||||
]
|
||||
]
|
||||
),
|
||||
)
|
||||
update_everything_bundle = SpendBundle([update_everything_spend], G2Element())
|
||||
result = await sim_client.push_tx(update_everything_bundle)
|
||||
assert result == (MempoolInclusionStatus.SUCCESS, None)
|
||||
await sim.farm_block()
|
||||
assert (await sim_client.get_coin_records_by_parent_ids([ownership_coin.name()], include_spent_coins=False))[
|
||||
0
|
||||
].coin.puzzle_hash == construct_ownership_layer(
|
||||
TARGET_OWNER,
|
||||
TARGET_TP,
|
||||
ACS,
|
||||
).get_tree_hash()
|
||||
finally:
|
||||
await sim.close() # type: ignore
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_default_transfer_program(setup_sim: Tuple[SpendSim, SimClient]) -> None:
|
||||
sim, sim_client = setup_sim
|
||||
|
||||
try:
|
||||
# Now make the ownership coin
|
||||
FAKE_SINGLETON_MOD = Program.to([2, 5, 11]) # (a 5 11) | (mod (_ INNER_PUZ inner_sol) (a INNER_PUZ inner_sol))
|
||||
FAKE_CAT_MOD = Program.to([2, 11, 23]) # (a 11 23) or (mod (_ _ INNER_PUZ inner_sol) (a INNER_PUZ inner_sol))
|
||||
FAKE_LAUNCHER_ID = bytes32([0] * 32)
|
||||
FAKE_TAIL = bytes32([2] * 32)
|
||||
FAKE_SINGLETON_STRUCT = Program.to((FAKE_SINGLETON_MOD.get_tree_hash(), (FAKE_LAUNCHER_ID, FAKE_LAUNCHER_ID)))
|
||||
FAKE_SINGLETON = FAKE_SINGLETON_MOD.curry(FAKE_SINGLETON_STRUCT, ACS)
|
||||
FAKE_CAT = FAKE_CAT_MOD.curry(FAKE_CAT_MOD.get_tree_hash(), FAKE_TAIL, ACS)
|
||||
|
||||
ROYALTY_ADDRESS = bytes32([1] * 32)
|
||||
TRADE_PRICE_PERCENTAGE = 5000 # 50%
|
||||
transfer_program: Program = NFT_TRANSFER_PROGRAM_DEFAULT.curry(
|
||||
FAKE_SINGLETON_STRUCT,
|
||||
ROYALTY_ADDRESS,
|
||||
TRADE_PRICE_PERCENTAGE,
|
||||
)
|
||||
ownership_puzzle: Program = construct_ownership_layer(
|
||||
None,
|
||||
transfer_program,
|
||||
ACS,
|
||||
)
|
||||
ownership_ph: bytes32 = ownership_puzzle.get_tree_hash()
|
||||
await sim.farm_block(ownership_ph)
|
||||
ownership_coin = (await sim_client.get_coin_records_by_puzzle_hash(ownership_ph, include_spent_coins=False))[
|
||||
0
|
||||
].coin
|
||||
|
||||
BLOCK_HEIGHT = sim.block_height
|
||||
|
||||
# Try a spend, no royalties, no owner update
|
||||
generic_spend = CoinSpend(
|
||||
ownership_coin,
|
||||
ownership_puzzle,
|
||||
Program.to([[[51, ACS_PH, 1]]]),
|
||||
)
|
||||
generic_bundle = SpendBundle([generic_spend], G2Element())
|
||||
result = await sim_client.push_tx(generic_bundle)
|
||||
assert result == (MempoolInclusionStatus.SUCCESS, None)
|
||||
await sim.farm_block()
|
||||
assert len(await sim_client.get_coin_records_by_puzzle_hash(ownership_ph, include_spent_coins=False)) > 0
|
||||
await sim.rewind(BLOCK_HEIGHT)
|
||||
|
||||
# Now try an owner update plus royalties
|
||||
await sim.farm_block(FAKE_SINGLETON.get_tree_hash())
|
||||
await sim.farm_block(FAKE_CAT.get_tree_hash())
|
||||
await sim.farm_block(ACS_PH)
|
||||
singleton_coin = (
|
||||
await sim_client.get_coin_records_by_puzzle_hash(FAKE_SINGLETON.get_tree_hash(), include_spent_coins=False)
|
||||
)[0].coin
|
||||
cat_coin = (
|
||||
await sim_client.get_coin_records_by_puzzle_hash(FAKE_CAT.get_tree_hash(), include_spent_coins=False)
|
||||
)[0].coin
|
||||
xch_coin = (await sim_client.get_coin_records_by_puzzle_hash(ACS_PH, include_spent_coins=False))[0].coin
|
||||
|
||||
ownership_spend = CoinSpend(
|
||||
ownership_coin,
|
||||
ownership_puzzle,
|
||||
Program.to(
|
||||
[[[51, ACS_PH, 1], [-10, FAKE_LAUNCHER_ID, [[100, ACS_PH], [100, FAKE_CAT.get_tree_hash()]], ACS_PH]]]
|
||||
),
|
||||
)
|
||||
|
||||
did_announcement_spend = CoinSpend(
|
||||
singleton_coin,
|
||||
FAKE_SINGLETON,
|
||||
Program.to([[[62, FAKE_LAUNCHER_ID]]]),
|
||||
)
|
||||
|
||||
expected_announcement_data = Program.to(
|
||||
(FAKE_LAUNCHER_ID, [[ROYALTY_ADDRESS, 50, [ROYALTY_ADDRESS]]])
|
||||
).get_tree_hash()
|
||||
xch_announcement_spend = CoinSpend(
|
||||
xch_coin,
|
||||
ACS,
|
||||
Program.to([[62, expected_announcement_data]]),
|
||||
)
|
||||
|
||||
cat_announcement_spend = CoinSpend(cat_coin, FAKE_CAT, Program.to([[[62, expected_announcement_data]]]))
|
||||
|
||||
# Make sure every combo except all of them work
|
||||
for i in range(1, 3):
|
||||
for announcement_combo in itertools.combinations(
|
||||
[did_announcement_spend, xch_announcement_spend, cat_announcement_spend], i
|
||||
):
|
||||
result = await sim_client.push_tx(SpendBundle([ownership_spend, *announcement_combo], G2Element()))
|
||||
assert result == (MempoolInclusionStatus.FAILED, Err.ASSERT_ANNOUNCE_CONSUMED_FAILED)
|
||||
|
||||
# Make sure all of them together pass
|
||||
full_bundle = SpendBundle(
|
||||
[ownership_spend, did_announcement_spend, xch_announcement_spend, cat_announcement_spend], G2Element()
|
||||
)
|
||||
result = await sim_client.push_tx(full_bundle)
|
||||
assert result == (MempoolInclusionStatus.SUCCESS, None)
|
||||
|
||||
# Finally, make sure we can just clear the DID label off
|
||||
new_ownership_puzzle: Program = construct_ownership_layer(
|
||||
FAKE_LAUNCHER_ID,
|
||||
transfer_program,
|
||||
ACS,
|
||||
)
|
||||
new_ownership_ph: bytes32 = new_ownership_puzzle.get_tree_hash()
|
||||
await sim.farm_block(new_ownership_ph)
|
||||
new_ownership_coin = (
|
||||
await sim_client.get_coin_records_by_puzzle_hash(new_ownership_ph, include_spent_coins=False)
|
||||
)[0].coin
|
||||
|
||||
empty_spend = CoinSpend(
|
||||
new_ownership_coin,
|
||||
new_ownership_puzzle,
|
||||
Program.to([[[51, ACS_PH, 1], [-10, [], [], []]]]),
|
||||
)
|
||||
empty_bundle = SpendBundle([empty_spend], G2Element())
|
||||
result = await sim_client.push_tx(empty_bundle)
|
||||
assert result == (MempoolInclusionStatus.SUCCESS, None)
|
||||
await sim.farm_block()
|
||||
finally:
|
||||
await sim.close() # type: ignore
|
||||
@@ -0,0 +1,846 @@
|
||||
import asyncio
|
||||
from secrets import token_bytes
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
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.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, uint64
|
||||
from chia.wallet.cat_wallet.cat_wallet import CATWallet
|
||||
from chia.wallet.nft_wallet.nft_wallet import NFTWallet
|
||||
from chia.wallet.outer_puzzles import create_asset_id, match_puzzle
|
||||
from chia.wallet.puzzle_drivers import PuzzleInfo
|
||||
from chia.wallet.trading.offer import Offer
|
||||
from chia.wallet.trading.trade_status import TradeStatus
|
||||
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
|
||||
|
||||
|
||||
async def get_trade_and_status(trade_manager, trade) -> TradeStatus: # type: ignore
|
||||
trade_rec = await trade_manager.get_trade_by_id(trade.trade_id)
|
||||
return TradeStatus(trade_rec.status)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trusted",
|
||||
[True],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_nft_offer_with_fee(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_maker = wallet_node_0.wallet_state_manager.main_wallet
|
||||
wallet_taker = wallet_node_1.wallet_state_manager.main_wallet
|
||||
|
||||
maker_ph = await wallet_maker.get_new_puzzlehash()
|
||||
taker_ph = await wallet_taker.get_new_puzzlehash()
|
||||
token_ph = bytes32(token_bytes())
|
||||
|
||||
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(maker_ph))
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(taker_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
funds = sum(
|
||||
[calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) for i in range(1, num_blocks)]
|
||||
)
|
||||
|
||||
await time_out_assert(15, wallet_maker.get_unconfirmed_balance, funds)
|
||||
await time_out_assert(15, wallet_maker.get_confirmed_balance, funds)
|
||||
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
nft_wallet_maker = await NFTWallet.create_new_nft_wallet(
|
||||
wallet_node_0.wallet_state_manager, wallet_maker, name="NFT WALLET 1"
|
||||
)
|
||||
|
||||
nft_wallet_taker = await NFTWallet.create_new_nft_wallet(
|
||||
wallet_node_1.wallet_state_manager, wallet_taker, name="NFT WALLET 2"
|
||||
)
|
||||
|
||||
trade_manager_maker = wallet_maker.wallet_state_manager.trade_manager
|
||||
trade_manager_taker = wallet_taker.wallet_state_manager.trade_manager
|
||||
|
||||
metadata = Program.to(
|
||||
[
|
||||
("u", ["https://www.chia.net/img/branding/chia-logo.svg"]),
|
||||
("h", "0xD4584AD463139FA8C0D9F68F4B59F185"),
|
||||
]
|
||||
)
|
||||
|
||||
sb = await nft_wallet_maker.generate_new_nft(metadata)
|
||||
assert sb
|
||||
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(token_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
coins_maker = nft_wallet_maker.my_nft_coins
|
||||
assert len(coins_maker) == 1
|
||||
coins_taker = nft_wallet_taker.my_nft_coins
|
||||
assert len(coins_taker) == 0
|
||||
|
||||
# MAKE FIRST TRADE: 1 NFT for 100 xch
|
||||
maker_balance_pre = await wallet_maker.get_confirmed_balance()
|
||||
taker_balance_pre = await wallet_taker.get_confirmed_balance()
|
||||
|
||||
nft_to_offer = coins_maker[0]
|
||||
nft_info: Optional[PuzzleInfo] = match_puzzle(nft_to_offer.full_puzzle)
|
||||
nft_asset_id: bytes32 = create_asset_id(nft_info) # type: ignore
|
||||
driver_dict: Dict[bytes32, Optional[PuzzleInfo]] = {nft_asset_id: nft_info}
|
||||
|
||||
xch_request = 100
|
||||
maker_fee = uint64(10)
|
||||
offer_nft_for_xch = {wallet_maker.id(): xch_request, nft_asset_id: -1}
|
||||
|
||||
success, trade_make, error = await trade_manager_maker.create_offer_for_ids(
|
||||
offer_nft_for_xch, driver_dict, fee=maker_fee
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
assert success is True
|
||||
assert error is None
|
||||
assert trade_make is not None
|
||||
|
||||
taker_fee = uint64(1)
|
||||
success, trade_take, error = await trade_manager_taker.respond_to_offer(
|
||||
Offer.from_bytes(trade_make.offer), fee=taker_fee
|
||||
)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
assert success
|
||||
assert error is None
|
||||
assert trade_take is not None
|
||||
|
||||
for i in range(0, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_maker, trade_make)
|
||||
await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_taker, trade_take)
|
||||
await time_out_assert(15, wallet_maker.get_confirmed_balance, maker_balance_pre + xch_request - maker_fee)
|
||||
await time_out_assert(15, wallet_taker.get_confirmed_balance, taker_balance_pre - xch_request - taker_fee)
|
||||
|
||||
coins_maker = nft_wallet_maker.my_nft_coins
|
||||
coins_taker = nft_wallet_taker.my_nft_coins
|
||||
assert len(coins_maker) == 0
|
||||
assert len(coins_taker) == 1
|
||||
|
||||
# MAKE SECOND TRADE: 100 xch for 1 NFT
|
||||
|
||||
maker_balance_pre = await wallet_maker.get_confirmed_balance()
|
||||
taker_balance_pre = await wallet_taker.get_confirmed_balance()
|
||||
|
||||
nft_to_buy = coins_taker[0]
|
||||
nft_to_buy_info: Optional[PuzzleInfo] = match_puzzle(nft_to_buy.full_puzzle)
|
||||
nft_to_buy_asset_id: bytes32 = create_asset_id(nft_to_buy_info) # type: ignore
|
||||
driver_dict_to_buy: Dict[bytes32, Optional[PuzzleInfo]] = {nft_to_buy_asset_id: nft_to_buy_info}
|
||||
|
||||
xch_offered = 1000
|
||||
maker_fee = uint64(10)
|
||||
offer_xch_for_nft = {wallet_maker.id(): -xch_offered, nft_to_buy_asset_id: 1}
|
||||
success, trade_make, error = await trade_manager_maker.create_offer_for_ids(
|
||||
offer_xch_for_nft, driver_dict_to_buy, fee=maker_fee
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
assert success is True
|
||||
assert error is None
|
||||
assert trade_make is not None
|
||||
|
||||
taker_fee = uint64(1)
|
||||
success, trade_take, error = await trade_manager_taker.respond_to_offer(
|
||||
Offer.from_bytes(trade_make.offer), fee=taker_fee
|
||||
)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
assert success
|
||||
assert error is None
|
||||
assert trade_take is not None
|
||||
|
||||
for i in range(0, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_maker, trade_make)
|
||||
await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_taker, trade_take)
|
||||
await time_out_assert(15, wallet_maker.get_confirmed_balance, maker_balance_pre - xch_offered - maker_fee)
|
||||
await time_out_assert(15, wallet_taker.get_confirmed_balance, taker_balance_pre + xch_offered - taker_fee)
|
||||
|
||||
coins_maker = nft_wallet_maker.my_nft_coins
|
||||
coins_taker = nft_wallet_taker.my_nft_coins
|
||||
assert len(coins_maker) == 1
|
||||
assert len(coins_taker) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trusted",
|
||||
[True],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_nft_offer_cancellations(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_maker = wallet_node_0.wallet_state_manager.main_wallet
|
||||
wallet_taker = wallet_node_1.wallet_state_manager.main_wallet
|
||||
|
||||
maker_ph = await wallet_maker.get_new_puzzlehash()
|
||||
taker_ph = await wallet_taker.get_new_puzzlehash()
|
||||
token_ph = bytes32(token_bytes())
|
||||
|
||||
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(maker_ph))
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(taker_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
funds = sum(
|
||||
[calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) for i in range(1, num_blocks)]
|
||||
)
|
||||
|
||||
await time_out_assert(15, wallet_maker.get_unconfirmed_balance, funds)
|
||||
await time_out_assert(15, wallet_maker.get_confirmed_balance, funds)
|
||||
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
nft_wallet_maker = await NFTWallet.create_new_nft_wallet(
|
||||
wallet_node_0.wallet_state_manager, wallet_maker, name="NFT WALLET 1"
|
||||
)
|
||||
|
||||
nft_wallet_taker = await NFTWallet.create_new_nft_wallet(
|
||||
wallet_node_1.wallet_state_manager, wallet_taker, name="NFT WALLET 2"
|
||||
)
|
||||
|
||||
trade_manager_maker = wallet_maker.wallet_state_manager.trade_manager
|
||||
# trade_manager_taker = wallet_taker.wallet_state_manager.trade_manager
|
||||
|
||||
metadata = Program.to(
|
||||
[
|
||||
("u", ["https://www.chia.net/img/branding/chia-logo.svg"]),
|
||||
("h", "0xD4584AD463139FA8C0D9F68F4B59F185"),
|
||||
]
|
||||
)
|
||||
|
||||
sb = await nft_wallet_maker.generate_new_nft(metadata)
|
||||
assert sb
|
||||
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(token_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
coins_maker = nft_wallet_maker.my_nft_coins
|
||||
assert len(coins_maker) == 1
|
||||
coins_taker = nft_wallet_taker.my_nft_coins
|
||||
assert len(coins_taker) == 0
|
||||
|
||||
# maker creates offer and cancels
|
||||
maker_balance_pre = await wallet_maker.get_confirmed_balance()
|
||||
# taker_balance_pre = await wallet_taker.get_confirmed_balance()
|
||||
|
||||
nft_to_offer = coins_maker[0]
|
||||
nft_info: Optional[PuzzleInfo] = match_puzzle(nft_to_offer.full_puzzle)
|
||||
nft_asset_id: bytes32 = create_asset_id(nft_info) # type: ignore
|
||||
driver_dict: Dict[bytes32, Optional[PuzzleInfo]] = {nft_asset_id: nft_info}
|
||||
|
||||
xch_request = 100
|
||||
maker_fee = uint64(10)
|
||||
offer_nft_for_xch = {wallet_maker.id(): xch_request, nft_asset_id: -1}
|
||||
|
||||
success, trade_make, error = await trade_manager_maker.create_offer_for_ids(
|
||||
offer_nft_for_xch, driver_dict, fee=maker_fee
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
assert success is True
|
||||
assert error is None
|
||||
assert trade_make is not None
|
||||
|
||||
# await trade_manager_maker.cancel_pending_offer(trade_make.trade_id)
|
||||
# await time_out_assert(15, get_trade_and_status, TradeStatus.CANCELLED, trade_manager_maker, trade_make)
|
||||
|
||||
cancel_fee = uint64(10)
|
||||
|
||||
txs = await trade_manager_maker.cancel_pending_offer_safely(trade_make.trade_id, fee=cancel_fee)
|
||||
|
||||
await time_out_assert(15, get_trade_and_status, TradeStatus.PENDING_CANCEL, trade_manager_maker, trade_make)
|
||||
for tx in txs:
|
||||
if tx.spend_bundle is not None:
|
||||
await time_out_assert(15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx.spend_bundle.name())
|
||||
|
||||
for i in range(0, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
await time_out_assert(15, get_trade_and_status, TradeStatus.CANCELLED, trade_manager_maker, trade_make)
|
||||
|
||||
maker_balance = await wallet_maker.get_confirmed_balance()
|
||||
assert maker_balance == maker_balance_pre - cancel_fee
|
||||
coins_maker = nft_wallet_maker.my_nft_coins
|
||||
assert len(coins_maker) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trusted",
|
||||
[True],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_nft_offer_with_metadata_update(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_maker = wallet_node_0.wallet_state_manager.main_wallet
|
||||
wallet_taker = wallet_node_1.wallet_state_manager.main_wallet
|
||||
|
||||
maker_ph = await wallet_maker.get_new_puzzlehash()
|
||||
taker_ph = await wallet_taker.get_new_puzzlehash()
|
||||
token_ph = bytes32(token_bytes())
|
||||
|
||||
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(maker_ph))
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(taker_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
funds = sum(
|
||||
[calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) for i in range(1, num_blocks)]
|
||||
)
|
||||
|
||||
await time_out_assert(15, wallet_maker.get_unconfirmed_balance, funds)
|
||||
await time_out_assert(15, wallet_maker.get_confirmed_balance, funds)
|
||||
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
nft_wallet_maker = await NFTWallet.create_new_nft_wallet(
|
||||
wallet_node_0.wallet_state_manager, wallet_maker, name="NFT WALLET 1"
|
||||
)
|
||||
|
||||
nft_wallet_taker = await NFTWallet.create_new_nft_wallet(
|
||||
wallet_node_1.wallet_state_manager, wallet_taker, name="NFT WALLET 2"
|
||||
)
|
||||
|
||||
trade_manager_maker = wallet_maker.wallet_state_manager.trade_manager
|
||||
trade_manager_taker = wallet_taker.wallet_state_manager.trade_manager
|
||||
|
||||
metadata = Program.to(
|
||||
[
|
||||
("u", ["https://www.chia.net/img/branding/chia-logo.svg"]),
|
||||
("h", "0xD4584AD463139FA8C0D9F68F4B59F185"),
|
||||
("mu", []),
|
||||
("lu", []),
|
||||
("sn", uint64(1)),
|
||||
("st", uint64(1)),
|
||||
]
|
||||
)
|
||||
|
||||
sb = await nft_wallet_maker.generate_new_nft(metadata)
|
||||
assert sb
|
||||
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(token_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
coins_maker = nft_wallet_maker.my_nft_coins
|
||||
assert len(coins_maker) == 1
|
||||
coins_taker = nft_wallet_taker.my_nft_coins
|
||||
assert len(coins_taker) == 0
|
||||
|
||||
# Maker updates metadata:
|
||||
nft_to_update = coins_maker[0]
|
||||
url_to_add = "https://new_url.com"
|
||||
key = "mu"
|
||||
fee_for_update = uint64(10)
|
||||
update_sb = await nft_wallet_maker.update_metadata(nft_to_update, key, url_to_add, fee=fee_for_update)
|
||||
mempool_mgr = full_node_api.full_node.mempool_manager
|
||||
await time_out_assert_not_none(5, mempool_mgr.get_spendbundle, update_sb.name()) # type: ignore
|
||||
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
coins_maker = nft_wallet_maker.my_nft_coins
|
||||
updated_nft = coins_maker[0]
|
||||
updated_nft_info = match_puzzle(updated_nft.full_puzzle)
|
||||
|
||||
assert url_to_add in updated_nft_info.also().info["metadata"] # type: ignore
|
||||
|
||||
# MAKE FIRST TRADE: 1 NFT for 100 xch
|
||||
maker_balance_pre = await wallet_maker.get_confirmed_balance()
|
||||
taker_balance_pre = await wallet_taker.get_confirmed_balance()
|
||||
|
||||
nft_to_offer = coins_maker[0]
|
||||
nft_info: Optional[PuzzleInfo] = match_puzzle(nft_to_offer.full_puzzle)
|
||||
nft_asset_id: bytes32 = create_asset_id(nft_info) # type: ignore
|
||||
driver_dict: Dict[bytes32, Optional[PuzzleInfo]] = {nft_asset_id: nft_info}
|
||||
|
||||
xch_request = 100
|
||||
maker_fee = uint64(10)
|
||||
offer_nft_for_xch = {wallet_maker.id(): xch_request, nft_asset_id: -1}
|
||||
|
||||
success, trade_make, error = await trade_manager_maker.create_offer_for_ids(
|
||||
offer_nft_for_xch, driver_dict, fee=maker_fee
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
assert success is True
|
||||
assert error is None
|
||||
assert trade_make is not None
|
||||
|
||||
taker_fee = uint64(1)
|
||||
success, trade_take, error = await trade_manager_taker.respond_to_offer(
|
||||
Offer.from_bytes(trade_make.offer), fee=taker_fee
|
||||
)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
assert success
|
||||
assert error is None
|
||||
assert trade_take is not None
|
||||
|
||||
for i in range(0, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_maker, trade_make)
|
||||
await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_taker, trade_take)
|
||||
await time_out_assert(15, wallet_maker.get_confirmed_balance, maker_balance_pre + xch_request - maker_fee)
|
||||
await time_out_assert(15, wallet_taker.get_confirmed_balance, taker_balance_pre - xch_request - taker_fee)
|
||||
|
||||
coins_maker = nft_wallet_maker.my_nft_coins
|
||||
coins_taker = nft_wallet_taker.my_nft_coins
|
||||
assert len(coins_maker) == 0
|
||||
assert len(coins_taker) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trusted",
|
||||
[True],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_nft_offer_nft_for_cat(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_maker = wallet_node_0.wallet_state_manager.main_wallet
|
||||
wallet_taker = wallet_node_1.wallet_state_manager.main_wallet
|
||||
|
||||
maker_ph = await wallet_maker.get_new_puzzlehash()
|
||||
taker_ph = await wallet_taker.get_new_puzzlehash()
|
||||
token_ph = bytes32(token_bytes())
|
||||
|
||||
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(maker_ph))
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(taker_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
funds = sum(
|
||||
[calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) for i in range(1, num_blocks)]
|
||||
)
|
||||
|
||||
await time_out_assert(15, wallet_maker.get_unconfirmed_balance, funds)
|
||||
await time_out_assert(15, wallet_maker.get_confirmed_balance, funds)
|
||||
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
# Create NFT wallets and nfts for maker and taker
|
||||
nft_wallet_maker = await NFTWallet.create_new_nft_wallet(
|
||||
wallet_node_0.wallet_state_manager, wallet_maker, name="NFT WALLET 1"
|
||||
)
|
||||
|
||||
nft_wallet_taker = await NFTWallet.create_new_nft_wallet(
|
||||
wallet_node_1.wallet_state_manager, wallet_taker, name="NFT WALLET 2"
|
||||
)
|
||||
|
||||
trade_manager_maker = wallet_maker.wallet_state_manager.trade_manager
|
||||
trade_manager_taker = wallet_taker.wallet_state_manager.trade_manager
|
||||
|
||||
metadata = Program.to(
|
||||
[
|
||||
("u", ["https://www.chia.net/img/branding/chia-logo.svg"]),
|
||||
("h", "0xD4584AD463139FA8C0D9F68F4B59F185"),
|
||||
]
|
||||
)
|
||||
|
||||
sb = await nft_wallet_maker.generate_new_nft(metadata)
|
||||
assert sb
|
||||
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(token_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
coins_maker = nft_wallet_maker.my_nft_coins
|
||||
assert len(coins_maker) == 1
|
||||
coins_taker = nft_wallet_taker.my_nft_coins
|
||||
assert len(coins_taker) == 0
|
||||
|
||||
# Create two new CATs and wallets for maker and taker
|
||||
cats_to_mint = 10000
|
||||
async with wallet_node_0.wallet_state_manager.lock:
|
||||
cat_wallet_maker: CATWallet = await CATWallet.create_new_cat_wallet(
|
||||
wallet_node_0.wallet_state_manager, wallet_maker, {"identifier": "genesis_by_id"}, uint64(cats_to_mint)
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async with wallet_node_1.wallet_state_manager.lock:
|
||||
cat_wallet_taker: CATWallet = await CATWallet.create_new_cat_wallet(
|
||||
wallet_node_1.wallet_state_manager, wallet_taker, {"identifier": "genesis_by_id"}, uint64(cats_to_mint)
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
for i in range(0, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
await time_out_assert(15, cat_wallet_maker.get_confirmed_balance, cats_to_mint)
|
||||
await time_out_assert(15, cat_wallet_maker.get_unconfirmed_balance, cats_to_mint)
|
||||
await time_out_assert(15, cat_wallet_taker.get_confirmed_balance, cats_to_mint)
|
||||
await time_out_assert(15, cat_wallet_taker.get_unconfirmed_balance, cats_to_mint)
|
||||
|
||||
wallet_maker_for_taker_cat: CATWallet = await CATWallet.create_wallet_for_cat(
|
||||
wallet_node_0.wallet_state_manager, wallet_maker, cat_wallet_taker.get_asset_id()
|
||||
)
|
||||
|
||||
wallet_taker_for_maker_cat: CATWallet = await CATWallet.create_wallet_for_cat(
|
||||
wallet_node_1.wallet_state_manager, wallet_taker, cat_wallet_maker.get_asset_id()
|
||||
)
|
||||
|
||||
assert wallet_taker_for_maker_cat
|
||||
# MAKE FIRST TRADE: 1 NFT for 10 taker cats
|
||||
maker_balance_pre = await wallet_maker.get_confirmed_balance()
|
||||
taker_balance_pre = await wallet_taker.get_confirmed_balance()
|
||||
taker_cat_maker_balance_pre = await wallet_maker_for_taker_cat.get_confirmed_balance()
|
||||
taker_cat_taker_balance_pre = await cat_wallet_taker.get_confirmed_balance()
|
||||
|
||||
nft_to_offer = coins_maker[0]
|
||||
nft_info: Optional[PuzzleInfo] = match_puzzle(nft_to_offer.full_puzzle)
|
||||
nft_asset_id: bytes32 = create_asset_id(nft_info) # type: ignore
|
||||
driver_dict: Dict[bytes32, Optional[PuzzleInfo]] = {nft_asset_id: nft_info}
|
||||
|
||||
maker_fee = uint64(10)
|
||||
taker_cat_offered = 2500
|
||||
offer_nft_for_cat = {nft_asset_id: -1, wallet_maker_for_taker_cat.id(): taker_cat_offered}
|
||||
|
||||
success, trade_make, error = await trade_manager_maker.create_offer_for_ids(
|
||||
offer_nft_for_cat, driver_dict, fee=maker_fee
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
assert success is True
|
||||
assert error is None
|
||||
assert trade_make is not None
|
||||
|
||||
taker_fee = uint64(1)
|
||||
success, trade_take, error = await trade_manager_taker.respond_to_offer(
|
||||
Offer.from_bytes(trade_make.offer), fee=taker_fee
|
||||
)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
assert success
|
||||
assert error is None
|
||||
assert trade_take is not None
|
||||
|
||||
for i in range(0, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_maker, trade_make)
|
||||
await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_taker, trade_take)
|
||||
|
||||
taker_cat_maker_balance_post = await wallet_maker_for_taker_cat.get_confirmed_balance()
|
||||
taker_cat_taker_balance_post = await cat_wallet_taker.get_confirmed_balance()
|
||||
assert taker_cat_maker_balance_post == taker_cat_maker_balance_pre + taker_cat_offered
|
||||
assert taker_cat_taker_balance_post == taker_cat_taker_balance_pre - taker_cat_offered
|
||||
maker_balance_post = await wallet_maker.get_confirmed_balance()
|
||||
taker_balance_post = await wallet_taker.get_confirmed_balance()
|
||||
assert maker_balance_post == maker_balance_pre - maker_fee
|
||||
assert taker_balance_post == taker_balance_pre - taker_fee
|
||||
coins_maker = nft_wallet_maker.my_nft_coins
|
||||
coins_taker = nft_wallet_taker.my_nft_coins
|
||||
assert len(coins_maker) == 0
|
||||
assert len(coins_taker) == 1
|
||||
|
||||
# Make an offer for taker NFT for multiple cats
|
||||
maker_cat_amount = 400
|
||||
taker_cat_amount = 500
|
||||
|
||||
nft_to_buy = coins_taker[0]
|
||||
nft_to_buy_info: Optional[PuzzleInfo] = match_puzzle(nft_to_buy.full_puzzle)
|
||||
nft_to_buy_asset_id: bytes32 = create_asset_id(nft_to_buy_info) # type: ignore
|
||||
|
||||
driver_dict_to_buy: Dict[bytes32, Optional[PuzzleInfo]] = {
|
||||
nft_to_buy_asset_id: nft_to_buy_info,
|
||||
}
|
||||
|
||||
maker_fee = uint64(10)
|
||||
offer_multi_cats_for_nft = {
|
||||
nft_to_buy_asset_id: 1,
|
||||
wallet_maker_for_taker_cat.id(): -taker_cat_amount,
|
||||
cat_wallet_maker.id(): -maker_cat_amount,
|
||||
}
|
||||
|
||||
success, trade_make, error = await trade_manager_maker.create_offer_for_ids(
|
||||
offer_multi_cats_for_nft, driver_dict_to_buy, fee=maker_fee
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
assert success is True
|
||||
assert error is None
|
||||
assert trade_make is not None
|
||||
|
||||
taker_fee = uint64(1)
|
||||
success, trade_take, error = await trade_manager_taker.respond_to_offer(
|
||||
Offer.from_bytes(trade_make.offer), fee=taker_fee
|
||||
)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
assert success
|
||||
assert error is None
|
||||
assert trade_take is not None
|
||||
|
||||
# check balances: taker wallet down an NFT, up cats
|
||||
for i in range(0, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_maker, trade_make)
|
||||
await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_taker, trade_take)
|
||||
|
||||
taker_cat_maker_balance_post_2 = await wallet_maker_for_taker_cat.get_confirmed_balance()
|
||||
taker_cat_taker_balance_post_2 = await cat_wallet_taker.get_confirmed_balance()
|
||||
assert taker_cat_maker_balance_post_2 == taker_cat_maker_balance_post - taker_cat_amount
|
||||
assert taker_cat_taker_balance_post_2 == taker_cat_taker_balance_post + taker_cat_amount
|
||||
maker_balance_post_2 = await wallet_maker.get_confirmed_balance()
|
||||
taker_balance_post_2 = await wallet_taker.get_confirmed_balance()
|
||||
assert maker_balance_post_2 == maker_balance_post - maker_fee
|
||||
assert taker_balance_post_2 == taker_balance_post - taker_fee
|
||||
coins_maker = nft_wallet_maker.my_nft_coins
|
||||
coins_taker = nft_wallet_taker.my_nft_coins
|
||||
assert len(coins_maker) == 1
|
||||
assert len(coins_taker) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trusted",
|
||||
[True],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_nft_offer_nft_for_nft(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_maker = wallet_node_0.wallet_state_manager.main_wallet
|
||||
wallet_taker = wallet_node_1.wallet_state_manager.main_wallet
|
||||
|
||||
maker_ph = await wallet_maker.get_new_puzzlehash()
|
||||
taker_ph = await wallet_taker.get_new_puzzlehash()
|
||||
token_ph = bytes32(token_bytes())
|
||||
|
||||
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(maker_ph))
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(taker_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
funds = sum(
|
||||
[calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) for i in range(1, num_blocks)]
|
||||
)
|
||||
|
||||
await time_out_assert(15, wallet_maker.get_unconfirmed_balance, funds)
|
||||
await time_out_assert(15, wallet_maker.get_confirmed_balance, funds)
|
||||
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
# Create NFT wallets and nfts for maker and taker
|
||||
nft_wallet_maker = await NFTWallet.create_new_nft_wallet(
|
||||
wallet_node_0.wallet_state_manager, wallet_maker, name="NFT WALLET 1"
|
||||
)
|
||||
|
||||
nft_wallet_taker = await NFTWallet.create_new_nft_wallet(
|
||||
wallet_node_1.wallet_state_manager, wallet_taker, name="NFT WALLET 2"
|
||||
)
|
||||
|
||||
trade_manager_maker = wallet_maker.wallet_state_manager.trade_manager
|
||||
trade_manager_taker = wallet_taker.wallet_state_manager.trade_manager
|
||||
|
||||
metadata = Program.to(
|
||||
[
|
||||
("u", ["https://www.chia.net/img/branding/chia-logo.svg"]),
|
||||
("h", "0xD4584AD463139FA8C0D9F68F4B59F185"),
|
||||
]
|
||||
)
|
||||
|
||||
sb = await nft_wallet_maker.generate_new_nft(metadata)
|
||||
assert sb
|
||||
await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name())
|
||||
|
||||
metadata_2 = Program.to(
|
||||
[
|
||||
("u", ["https://www.chia.net/image2.html"]),
|
||||
("h", "0xD4584AD463139FA8C0D9F68F4B59F183"),
|
||||
]
|
||||
)
|
||||
sb_2 = await nft_wallet_taker.generate_new_nft(metadata_2)
|
||||
assert sb_2
|
||||
await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, sb_2.name())
|
||||
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
coins_maker = nft_wallet_maker.my_nft_coins
|
||||
assert len(coins_maker) == 1
|
||||
coins_taker = nft_wallet_taker.my_nft_coins
|
||||
assert len(coins_taker) == 1
|
||||
|
||||
maker_balance_pre = await wallet_maker.get_confirmed_balance()
|
||||
taker_balance_pre = await wallet_taker.get_confirmed_balance()
|
||||
|
||||
nft_to_offer = coins_maker[0]
|
||||
nft_to_offer_info: Optional[PuzzleInfo] = match_puzzle(nft_to_offer.full_puzzle)
|
||||
nft_to_offer_asset_id: bytes32 = create_asset_id(nft_to_offer_info) # type: ignore
|
||||
|
||||
nft_to_take = coins_taker[0]
|
||||
nft_to_take_info: Optional[PuzzleInfo] = match_puzzle(nft_to_take.full_puzzle)
|
||||
nft_to_take_asset_id: bytes32 = create_asset_id(nft_to_take_info) # type: ignore
|
||||
|
||||
driver_dict: Dict[bytes32, Optional[PuzzleInfo]] = {
|
||||
nft_to_offer_asset_id: nft_to_offer_info,
|
||||
nft_to_take_asset_id: nft_to_take_info,
|
||||
}
|
||||
|
||||
maker_fee = uint64(10)
|
||||
offer_nft_for_nft = {nft_to_take_asset_id: 1, nft_to_offer_asset_id: -1}
|
||||
|
||||
success, trade_make, error = await trade_manager_maker.create_offer_for_ids(
|
||||
offer_nft_for_nft, driver_dict, fee=maker_fee
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
assert success is True
|
||||
assert error is None
|
||||
assert trade_make is not None
|
||||
|
||||
taker_fee = uint64(1)
|
||||
success, trade_take, error = await trade_manager_taker.respond_to_offer(
|
||||
Offer.from_bytes(trade_make.offer), fee=taker_fee
|
||||
)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
assert success
|
||||
assert error is None
|
||||
assert trade_take is not None
|
||||
|
||||
for i in range(0, num_blocks):
|
||||
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(token_ph))
|
||||
await asyncio.sleep(5)
|
||||
|
||||
await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_maker, trade_make)
|
||||
await time_out_assert(15, get_trade_and_status, TradeStatus.CONFIRMED, trade_manager_taker, trade_take)
|
||||
await time_out_assert(15, wallet_maker.get_confirmed_balance, maker_balance_pre - maker_fee)
|
||||
await time_out_assert(15, wallet_taker.get_confirmed_balance, taker_balance_pre - taker_fee)
|
||||
|
||||
coins_maker = nft_wallet_maker.my_nft_coins
|
||||
coins_taker = nft_wallet_taker.my_nft_coins
|
||||
assert len(coins_maker) == 1
|
||||
assert len(coins_taker) == 1
|
||||
@@ -0,0 +1,193 @@
|
||||
from secrets import token_bytes
|
||||
from typing import Tuple
|
||||
|
||||
from clvm.casts import int_from_bytes
|
||||
|
||||
from chia.types.blockchain_format.program import Program
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.wallet.nft_wallet import uncurry_nft
|
||||
from chia.wallet.nft_wallet.nft_puzzles import (
|
||||
construct_ownership_layer,
|
||||
create_full_puzzle,
|
||||
create_nft_layer_puzzle_with_curry_params,
|
||||
recurry_nft_puzzle,
|
||||
)
|
||||
from chia.wallet.outer_puzzles import match_puzzle
|
||||
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 tests.core.make_block_generator import int_to_public_key
|
||||
|
||||
SINGLETON_MOD = load_clvm("singleton_top_layer_v1_1.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")
|
||||
NFT_OWNERSHIP_LAYER = load_clvm("nft_ownership_layer.clvm")
|
||||
NFT_TRANSFER_PROGRAM_DEFAULT = load_clvm("nft_ownership_transfer_program_one_way_claim_with_royalties.clvm")
|
||||
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()
|
||||
OFFER_MOD = load_clvm("settlement_payments.clvm")
|
||||
|
||||
LAUNCHER_ID = Program.to(b"launcher-id").get_tree_hash()
|
||||
NFT_METADATA_UPDATER_DEFAULT = load_clvm("nft_metadata_updater_default.clvm")
|
||||
|
||||
|
||||
def test_nft_transfer_puzzle_hashes():
|
||||
maker_pk = int_to_public_key(111)
|
||||
maker_p2_puz = puzzle_for_pk(maker_pk)
|
||||
maker_p2_ph = maker_p2_puz.get_tree_hash()
|
||||
maker_did = Program.to("maker did").get_tree_hash()
|
||||
# maker_did_inner_hash = Program.to("maker did inner hash").get_tree_hash()
|
||||
metadata = [
|
||||
("u", ["https://www.chia.net/img/branding/chia-logo.svg"]),
|
||||
("h", 0xD4584AD463139FA8C0D9F68F4B59F185),
|
||||
]
|
||||
metadata_updater_hash = NFT_METADATA_UPDATER_DEFAULT.get_tree_hash()
|
||||
# royalty_addr = maker_p2_ph
|
||||
royalty_pc = 2000 # basis pts
|
||||
nft_id = Program.to("nft id").get_tree_hash()
|
||||
SINGLETON_STRUCT = Program.to((SINGLETON_MOD_HASH, (nft_id, LAUNCHER_PUZZLE_HASH)))
|
||||
|
||||
transfer_puz = NFT_TRANSFER_PROGRAM_DEFAULT.curry(
|
||||
SINGLETON_STRUCT,
|
||||
maker_p2_ph,
|
||||
royalty_pc,
|
||||
)
|
||||
|
||||
ownership_puz = NFT_OWNERSHIP_LAYER.curry(
|
||||
NFT_OWNERSHIP_LAYER.get_tree_hash(), maker_did, transfer_puz, maker_p2_puz
|
||||
)
|
||||
|
||||
metadata_puz = NFT_STATE_LAYER_MOD.curry(
|
||||
NFT_STATE_LAYER_MOD.get_tree_hash(), metadata, metadata_updater_hash, ownership_puz
|
||||
)
|
||||
|
||||
nft_puz = SINGLETON_MOD.curry(SINGLETON_STRUCT, metadata_puz)
|
||||
|
||||
nft_info = match_puzzle(nft_puz)
|
||||
assert nft_info.also().also() is not None
|
||||
|
||||
unft = uncurry_nft.UncurriedNFT.uncurry(nft_puz)
|
||||
assert unft.supports_did
|
||||
|
||||
# setup transfer
|
||||
taker_pk = int_to_public_key(222)
|
||||
taker_p2_puz = puzzle_for_pk(taker_pk)
|
||||
taker_p2_ph = taker_p2_puz.get_tree_hash()
|
||||
|
||||
# make nft solution
|
||||
fake_lineage_proof = Program.to([token_bytes(32), maker_p2_ph, 1])
|
||||
transfer_conditions = Program.to([[51, taker_p2_ph, 1, [taker_p2_ph]], [-10, [], [], []]])
|
||||
|
||||
ownership_sol = Program.to([solution_for_conditions(transfer_conditions)])
|
||||
|
||||
metadata_sol = Program.to([ownership_sol])
|
||||
nft_sol = Program.to([fake_lineage_proof, 1, metadata_sol])
|
||||
|
||||
conds = nft_puz.run(nft_sol)
|
||||
|
||||
# get the new NFT puzhash
|
||||
for cond in conds.as_iter():
|
||||
if cond.first().as_int() == 51:
|
||||
expected_ph = bytes32(cond.at("rf").atom)
|
||||
|
||||
# recreate the puzzle for new_puzhash
|
||||
new_ownership_puz = NFT_OWNERSHIP_LAYER.curry(NFT_OWNERSHIP_LAYER.get_tree_hash(), None, transfer_puz, taker_p2_puz)
|
||||
new_metadata_puz = NFT_STATE_LAYER_MOD.curry(
|
||||
NFT_STATE_LAYER_MOD.get_tree_hash(), metadata, metadata_updater_hash, new_ownership_puz
|
||||
)
|
||||
new_nft_puz = SINGLETON_MOD.curry(SINGLETON_STRUCT, new_metadata_puz)
|
||||
calculated_ph = new_nft_puz.get_tree_hash()
|
||||
|
||||
assert expected_ph == calculated_ph
|
||||
|
||||
|
||||
def make_a_new_solution() -> Tuple[Program, Program]:
|
||||
destination = int_to_public_key(2)
|
||||
p2_puzzle = puzzle_for_pk(destination)
|
||||
puzhash = p2_puzzle.get_tree_hash()
|
||||
new_did = Program.to("test").get_tree_hash()
|
||||
new_did_inner_hash = Program.to("fake").get_tree_hash()
|
||||
trade_prices_list = [[200, OFFER_MOD.get_tree_hash()]]
|
||||
|
||||
condition_list = [
|
||||
[
|
||||
51,
|
||||
puzhash,
|
||||
1,
|
||||
[puzhash],
|
||||
],
|
||||
[-10, new_did, trade_prices_list, new_did_inner_hash],
|
||||
]
|
||||
solution = Program.to(
|
||||
[
|
||||
[],
|
||||
[],
|
||||
[
|
||||
[solution_for_conditions(condition_list)],
|
||||
],
|
||||
],
|
||||
)
|
||||
return p2_puzzle, solution
|
||||
|
||||
|
||||
def make_a_new_ownership_layer_puzzle() -> Tuple[Program, Program]:
|
||||
pubkey = int_to_public_key(1)
|
||||
innerpuz = puzzle_for_pk(pubkey)
|
||||
old_did = Program.to("test_2").get_tree_hash()
|
||||
nft_id = Program.to("nft_id")
|
||||
SINGLETON_STRUCT = Program.to((SINGLETON_MOD_HASH, (nft_id, LAUNCHER_PUZZLE_HASH)))
|
||||
curried_tp = NFT_TRANSFER_PROGRAM_DEFAULT.curry(
|
||||
SINGLETON_STRUCT,
|
||||
innerpuz.get_tree_hash(),
|
||||
2000,
|
||||
)
|
||||
curried_inner = innerpuz
|
||||
curried_ownership_layer = construct_ownership_layer(old_did, curried_tp, curried_inner)
|
||||
return innerpuz, curried_ownership_layer
|
||||
|
||||
|
||||
def make_a_new_nft_puzzle(curried_ownership_layer: Program, metadata: Program) -> Program:
|
||||
curried_state_layer = NFT_STATE_LAYER_MOD.curry(
|
||||
NFT_STATE_LAYER_MOD_HASH, metadata, NFT_METADATA_UPDATER_DEFAULT.get_tree_hash(), curried_ownership_layer
|
||||
)
|
||||
return curried_state_layer
|
||||
|
||||
|
||||
def get_updated_nft_puzzle(puzzle: Program, solution: Program) -> bytes32:
|
||||
result = puzzle.run(solution)
|
||||
for condition in result.as_iter():
|
||||
code = int_from_bytes(condition.first().atom)
|
||||
if code == 51:
|
||||
if int_from_bytes(condition.rest().rest().first().atom) == 1:
|
||||
# this is our new puzzle hash
|
||||
return bytes32(condition.rest().first().atom)
|
||||
raise ValueError("No create coin condition found")
|
||||
|
||||
|
||||
def test_transfer_puzzle_builder() -> None:
|
||||
metadata = [
|
||||
("u", ["https://www.chia.net/img/branding/chia-logo.svg"]),
|
||||
("h", 0xD4584AD463139FA8C0D9F68F4B59F185),
|
||||
]
|
||||
sp2_puzzle, solution = make_a_new_solution()
|
||||
p2_puzzle, ownership_puzzle = make_a_new_ownership_layer_puzzle()
|
||||
clvm_nft_puzzle = create_nft_layer_puzzle_with_curry_params(
|
||||
Program.to(metadata), NFT_METADATA_UPDATER_DEFAULT.get_tree_hash(), ownership_puzzle
|
||||
)
|
||||
puzzle = create_full_puzzle(
|
||||
Program.to(["singleton_id"]).get_tree_hash(),
|
||||
Program.to(metadata),
|
||||
NFT_METADATA_UPDATER_DEFAULT.get_tree_hash(),
|
||||
ownership_puzzle,
|
||||
)
|
||||
clvm_puzzle_hash = get_updated_nft_puzzle(clvm_nft_puzzle, solution.at("rrf"))
|
||||
unft = uncurry_nft.UncurriedNFT.uncurry(puzzle)
|
||||
assert unft.nft_state_layer == clvm_nft_puzzle
|
||||
assert unft.inner_puzzle == ownership_puzzle
|
||||
assert unft.p2_puzzle == p2_puzzle
|
||||
ol_puzzle = recurry_nft_puzzle(unft, solution, sp2_puzzle)
|
||||
nft_puzzle = create_nft_layer_puzzle_with_curry_params(
|
||||
Program.to(metadata), NFT_METADATA_UPDATER_DEFAULT.get_tree_hash(), ol_puzzle
|
||||
)
|
||||
assert clvm_puzzle_hash == nft_puzzle.get_tree_hash()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,75 @@
|
||||
from typing import Optional
|
||||
|
||||
from clvm_tools.binutils import assemble
|
||||
|
||||
from chia.types.blockchain_format.program import Program
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.util.ints import uint16
|
||||
from chia.wallet.nft_wallet.ownership_outer_puzzle import puzzle_for_ownership_layer
|
||||
from chia.wallet.nft_wallet.transfer_program_puzzle import puzzle_for_transfer_program
|
||||
from chia.wallet.outer_puzzles import (
|
||||
construct_puzzle,
|
||||
create_asset_id,
|
||||
get_inner_puzzle,
|
||||
get_inner_solution,
|
||||
match_puzzle,
|
||||
solve_puzzle,
|
||||
)
|
||||
from chia.wallet.puzzle_drivers import PuzzleInfo, Solver
|
||||
|
||||
|
||||
def test_ownership_outer_puzzle() -> None:
|
||||
ACS = Program.to(1)
|
||||
NIL = Program.to([])
|
||||
owner = bytes32([0] * 32)
|
||||
# (mod (current_owner conditions solution)
|
||||
# (list current_owner () conditions)
|
||||
# )
|
||||
transfer_program = assemble( # type: ignore
|
||||
"""
|
||||
(c 2 (c () (c 5 ())))
|
||||
"""
|
||||
)
|
||||
transfer_program_default: Program = puzzle_for_transfer_program(bytes32([1] * 32), bytes32([2] * 32), uint16(5000))
|
||||
ownership_puzzle: Program = puzzle_for_ownership_layer(owner, transfer_program, ACS)
|
||||
ownership_puzzle_empty: Program = puzzle_for_ownership_layer(NIL, transfer_program, ACS)
|
||||
ownership_puzzle_default: Program = puzzle_for_ownership_layer(owner, transfer_program_default, ACS)
|
||||
ownership_driver: Optional[PuzzleInfo] = match_puzzle(ownership_puzzle)
|
||||
ownership_driver_empty: Optional[PuzzleInfo] = match_puzzle(ownership_puzzle_empty)
|
||||
ownership_driver_default: Optional[PuzzleInfo] = match_puzzle(ownership_puzzle_default)
|
||||
transfer_program_driver: Optional[PuzzleInfo] = match_puzzle(transfer_program_default)
|
||||
|
||||
assert ownership_driver is not None
|
||||
assert ownership_driver_empty is not None
|
||||
assert ownership_driver_default is not None
|
||||
assert transfer_program_driver is not None
|
||||
assert ownership_driver.type() == "ownership"
|
||||
assert ownership_driver["owner"] == owner
|
||||
assert ownership_driver_empty["owner"] == NIL
|
||||
assert ownership_driver["transfer_program"] == transfer_program
|
||||
assert ownership_driver_default["transfer_program"] == transfer_program_driver
|
||||
assert transfer_program_driver.type() == "royalty transfer program"
|
||||
assert transfer_program_driver["launcher_id"] == bytes32([1] * 32)
|
||||
assert transfer_program_driver["royalty_address"] == bytes32([2] * 32)
|
||||
assert transfer_program_driver["royalty_percentage"] == 5000
|
||||
assert construct_puzzle(ownership_driver, ACS) == ownership_puzzle
|
||||
assert construct_puzzle(ownership_driver_empty, ACS) == ownership_puzzle_empty
|
||||
assert construct_puzzle(ownership_driver_default, ACS) == ownership_puzzle_default
|
||||
assert get_inner_puzzle(ownership_driver, ownership_puzzle) == ACS
|
||||
assert create_asset_id(ownership_driver) is None
|
||||
|
||||
# Set up for solve
|
||||
inner_solution = Program.to(
|
||||
[
|
||||
[51, ACS.get_tree_hash(), 1],
|
||||
[-10],
|
||||
]
|
||||
)
|
||||
solution: Program = solve_puzzle(
|
||||
ownership_driver,
|
||||
Solver({}),
|
||||
ACS,
|
||||
inner_solution,
|
||||
)
|
||||
ownership_puzzle.run(solution)
|
||||
assert get_inner_solution(ownership_driver, solution) == inner_solution
|
||||
@@ -33,6 +33,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.nft_wallet.nft_wallet import NFTWallet
|
||||
from chia.wallet.trading.trade_status import TradeStatus
|
||||
from chia.wallet.transaction_record import TransactionRecord
|
||||
from chia.wallet.transaction_sorting import SortKey
|
||||
@@ -734,6 +735,8 @@ async def test_offer_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_did_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment):
|
||||
from chia.wallet.did_wallet.did_info import DID_HRP
|
||||
|
||||
env: WalletRpcTestEnvironment = wallet_rpc_environment
|
||||
|
||||
wallet_1: Wallet = env.wallet_1.wallet
|
||||
@@ -742,6 +745,7 @@ async def test_did_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment):
|
||||
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
|
||||
wallet_1_id = wallet_1.id()
|
||||
|
||||
await generate_funds(env.full_node.api, env.wallet_1, 5)
|
||||
|
||||
@@ -764,7 +768,7 @@ async def test_did_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment):
|
||||
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)
|
||||
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)
|
||||
@@ -788,6 +792,8 @@ async def test_did_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment):
|
||||
await farm_transaction_block(full_node_api, wallet_1_node)
|
||||
|
||||
# Update metadata
|
||||
with pytest.raises(ValueError, match="Wallet with id 1 is not a DID one"):
|
||||
await wallet_1_rpc.update_did_metadata(wallet_1_id, {"Twitter": "Https://test"})
|
||||
res = await wallet_1_rpc.update_did_metadata(did_wallet_id_0, {"Twitter": "Https://test"})
|
||||
assert res["success"]
|
||||
|
||||
@@ -812,16 +818,79 @@ async def test_did_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment):
|
||||
|
||||
did_wallets = list(
|
||||
filter(
|
||||
lambda w: (w.type == WalletType.DISTRIBUTED_ID),
|
||||
lambda w: (w.type == WalletType.DECENTRALIZED_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
|
||||
assert encode_puzzle_hash(bytes32.from_hexstr(did_wallet_2.get_my_DID()), DID_HRP) == did_id_0
|
||||
metadata = json.loads(did_wallet_2.did_info.metadata)
|
||||
assert metadata["Twitter"] == "Https://test"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_nft_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment):
|
||||
|
||||
from chia.wallet.nft_wallet.nft_info import NFT_HRP
|
||||
|
||||
env: WalletRpcTestEnvironment = wallet_rpc_environment
|
||||
wallet_1_node: WalletNode = env.wallet_1.node
|
||||
wallet_1_rpc: WalletRpcClient = env.wallet_1.rpc_client
|
||||
wallet_2: Wallet = env.wallet_2.wallet
|
||||
wallet_2_node: WalletNode = env.wallet_2.node
|
||||
wallet_2_rpc: WalletRpcClient = env.wallet_2.rpc_client
|
||||
full_node_api: FullNodeSimulator = env.full_node.api
|
||||
|
||||
await generate_funds(env.full_node.api, env.wallet_1, 5)
|
||||
|
||||
res = await wallet_1_rpc.create_new_nft_wallet(None)
|
||||
nft_wallet_id = res["wallet_id"]
|
||||
res = await wallet_1_rpc.mint_nft(
|
||||
nft_wallet_id,
|
||||
None,
|
||||
None,
|
||||
"0xD4584AD463139FA8C0D9F68F4B59F185",
|
||||
["https://www.chia.net/img/branding/chia-logo.svg"],
|
||||
)
|
||||
assert res["success"]
|
||||
|
||||
for _ in range(3):
|
||||
await farm_transaction_block(full_node_api, wallet_1_node)
|
||||
|
||||
assert wallet_1_node.wallet_state_manager is not None
|
||||
|
||||
nft_wallet: NFTWallet = wallet_1_node.wallet_state_manager.wallets[nft_wallet_id]
|
||||
# Test with the hex version of nft_id
|
||||
nft_id = nft_wallet.get_current_nfts()[0].coin.name().hex()
|
||||
nft_info = (await wallet_1_rpc.get_nft_info(nft_id))["nft_info"]
|
||||
assert nft_info["nft_coin_id"][2:] == nft_wallet.get_current_nfts()[0].coin.name().hex()
|
||||
# Test with the bech32m version of nft_id
|
||||
hmr_nft_id = encode_puzzle_hash(nft_wallet.get_current_nfts()[0].coin.name(), NFT_HRP)
|
||||
nft_info = (await wallet_1_rpc.get_nft_info(hmr_nft_id))["nft_info"]
|
||||
assert nft_info["nft_coin_id"][2:] == nft_wallet.get_current_nfts()[0].coin.name().hex()
|
||||
|
||||
addr = encode_puzzle_hash(await wallet_2.get_new_puzzlehash(), "txch")
|
||||
res = await wallet_1_rpc.transfer_nft(nft_wallet_id, nft_id, addr, 0)
|
||||
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
|
||||
|
||||
nft_wallet_id_1 = (
|
||||
await wallet_2_node.wallet_state_manager.get_all_wallet_info_entries(wallet_type=WalletType.NFT)
|
||||
)[0].id
|
||||
nft_wallet_1: NFTWallet = wallet_2_node.wallet_state_manager.wallets[nft_wallet_id_1]
|
||||
nft_info_1 = (await wallet_1_rpc.get_nft_info(nft_id, False))["nft_info"]
|
||||
assert nft_info_1 == nft_info
|
||||
nft_info_1 = (await wallet_1_rpc.get_nft_info(nft_id))["nft_info"]
|
||||
assert nft_info_1["nft_coin_id"][2:] == nft_wallet_1.get_current_nfts()[0].coin.name().hex()
|
||||
# Cross-check NFT
|
||||
nft_info_2 = (await wallet_2_rpc.list_nfts(nft_wallet_id_1))["nft_list"][0]
|
||||
assert nft_info_1 == nft_info_2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_key_and_address_endpoints(wallet_rpc_environment: WalletRpcTestEnvironment):
|
||||
env: WalletRpcTestEnvironment = wallet_rpc_environment
|
||||
|
||||
@@ -6,7 +6,7 @@ from chia.protocols import full_node_protocol
|
||||
from chia.simulator.simulator_protocol import FarmNewBlockProtocol
|
||||
from chia.types.peer_info import PeerInfo
|
||||
from chia.util.ints import uint16, uint32
|
||||
from tests.connection_utils import disconnect_all_and_reconnect
|
||||
from tests.connection_utils import disconnect_all, disconnect_all_and_reconnect
|
||||
from tests.pools.test_pool_rpc import wallet_is_synced
|
||||
from tests.setup_nodes import test_constants
|
||||
from tests.time_out_assert import time_out_assert
|
||||
@@ -46,7 +46,9 @@ class TestWalletSync:
|
||||
|
||||
# Tests a reorg with the wallet
|
||||
num_blocks = 30
|
||||
blocks_reorg = bt.get_consecutive_blocks(num_blocks, block_list_input=default_400_blocks[:-5])
|
||||
blocks_reorg = bt.get_consecutive_blocks(
|
||||
num_blocks, block_list_input=default_400_blocks[:-5], current_time=True
|
||||
)
|
||||
for i in range(1, len(blocks_reorg)):
|
||||
await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(blocks_reorg[i]))
|
||||
|
||||
@@ -57,6 +59,9 @@ class TestWalletSync:
|
||||
await time_out_assert(
|
||||
100, wallet_height_at_least, True, wallet_node, len(default_400_blocks) + num_blocks - 5 - 1
|
||||
)
|
||||
await time_out_assert(20, wallet_node.wallet_state_manager.synced)
|
||||
await disconnect_all(wallet_server)
|
||||
assert not (await wallet_node.wallet_state_manager.synced())
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_almost_recent(self, bt, two_wallet_nodes, default_400_blocks, self_hostname):
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
from pathlib import Path
|
||||
|
||||
import aiosqlite
|
||||
import pytest
|
||||
|
||||
from chia.types.blockchain_format.coin import Coin
|
||||
from chia.types.blockchain_format.program import Program
|
||||
from chia.types.blockchain_format.sized_bytes import bytes32
|
||||
from chia.util.db_wrapper import DBWrapper
|
||||
from chia.util.ints import uint32, uint64
|
||||
from chia.wallet.lineage_proof import LineageProof
|
||||
from chia.wallet.nft_wallet.nft_info import NFTCoinInfo
|
||||
from chia.wallet.wallet_nft_store import WalletNftStore
|
||||
|
||||
|
||||
class TestNftStore:
|
||||
@pytest.mark.asyncio
|
||||
async def test_nft_store(self) -> None:
|
||||
db_filename = Path("nft_store_test.db")
|
||||
|
||||
if db_filename.exists():
|
||||
db_filename.unlink()
|
||||
|
||||
con = await aiosqlite.connect(db_filename)
|
||||
wrapper = DBWrapper(con)
|
||||
db = await WalletNftStore.create(wrapper)
|
||||
try:
|
||||
a_bytes32 = bytes32.fromhex("09287c75377c63fd6a3a4d6658abed03e9a521e0436b1f83cdf4af99341ce8f1")
|
||||
puzzle = Program.to(["A Test puzzle"])
|
||||
nft = NFTCoinInfo(
|
||||
a_bytes32,
|
||||
Coin(a_bytes32, a_bytes32, uint64(1)),
|
||||
LineageProof(a_bytes32, a_bytes32, uint64(1)),
|
||||
puzzle,
|
||||
uint32(10),
|
||||
)
|
||||
# Test save
|
||||
await db.save_nft(uint32(1), a_bytes32, nft)
|
||||
# Test get nft
|
||||
assert nft == (await db.get_nft_list(wallet_id=uint32(1)))[0]
|
||||
assert nft == (await db.get_nft_list())[0]
|
||||
assert nft == (await db.get_nft_list(did_id=a_bytes32))[0]
|
||||
assert nft == (await db.get_nft_list(wallet_id=uint32(1), did_id=a_bytes32))[0]
|
||||
assert nft == await db.get_nft_by_id(a_bytes32)
|
||||
# Test delete
|
||||
await db.delete_nft(a_bytes32)
|
||||
assert await db.get_nft_by_id(a_bytes32) is None
|
||||
|
||||
except Exception as e:
|
||||
print(e, type(e))
|
||||
await db._clear_database()
|
||||
await db.close()
|
||||
db_filename.unlink()
|
||||
raise e
|
||||
|
||||
await db._clear_database()
|
||||
await db.close()
|
||||
db_filename.unlink()
|
||||
Reference in New Issue
Block a user