├── ownable_singleton
├── __init__.py
├── clsp
│ ├── __init__.py
│ ├── p2_singleton_or_cancel.clsp.hex
│ ├── ownable_singleton_v1.clsp.hex
│ ├── ownable_singleton_v2.clsp.hex
│ ├── ownable_singleton_v1.clsp
│ ├── p2_singleton_or_cancel.clsp
│ └── ownable_singleton_v2.clsp
├── drivers
│ ├── __init__.py
│ └── ownable_singleton_driver.py
├── tests
│ ├── __init__.py
│ └── test_ownable_singleton.py
└── README.md
├── .gitignore
├── .github
└── singleton_offers.png
├── install.ps1
├── install.sh
├── setup.py
├── README.md
├── LICENSE
└── nft.py
/ownable_singleton/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ownable_singleton/clsp/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ownable_singleton/drivers/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ownable_singleton/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/main.sym
2 | venv
3 | .pytest_cache
4 | .idea
5 | **/__pycache__
6 | *.egg-info
7 | build*
8 | .eggs
--------------------------------------------------------------------------------
/.github/singleton_offers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mintgarden-io/nft-companion/HEAD/.github/singleton_offers.png
--------------------------------------------------------------------------------
/install.ps1:
--------------------------------------------------------------------------------
1 | python -m venv venv
2 | ./venv/Scripts/activate
3 | python -m pip install --upgrade pip
4 |
5 | pip3 install wheel
6 | pip3 install .
7 | pip3 install chia-dev-tools --no-deps
8 |
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | python3 -m venv venv
4 | . ./venv/bin/activate
5 |
6 | python3 -m pip install --upgrade pip
7 |
8 | pip3 install wheel && pip3 install .
9 | pip3 install chia-dev-tools --no-deps
10 |
11 | deactivate
--------------------------------------------------------------------------------
/ownable_singleton/README.md:
--------------------------------------------------------------------------------
1 | # Puzzles
2 |
3 | These puzzles are used to construct singletons with an owner and paid transfer capabilities.
4 |
5 | The following image shows an overview of a singleton transfer.
6 |
7 | 
8 |
9 | ## ownable_singleton
10 |
11 | This is an inner puzzle for a singleton_top_layer coin. It allows the singleton to be owned by a public key.
12 |
13 | Furthermore, the ownership can be transferred to a new owner, optionally by paying for it in XCH.
14 | The payment is done via a special p2_singleton_or_cancel coin.
15 |
16 | ## p2_singleton_or_cancel
17 |
18 | This coin can be spent in order to pay for a singleton ownership transfer.
19 | It also has a cancel functionality, in case an offer should be cancelled.
20 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from setuptools import setup
4 |
5 | with open("README.md", "rt") as fh:
6 | long_description = fh.read()
7 |
8 | dependencies = [
9 | "chia-blockchain@git+https://github.com/Chia-Network/chia-blockchain.git@protocol_and_cats_rebased#23d571d9bb6b5003b49dee7ee31c1799358c5349",
10 | "requests"
11 | ]
12 |
13 | dev_dependencies = [
14 | "black",
15 | ]
16 |
17 | setup(
18 | name="singleton-utils",
19 | version="0.0.1",
20 | author="xch-gallery",
21 | setup_requires=["setuptools_scm"],
22 | install_requires=dependencies,
23 | extras_require=dict(
24 | dev=dev_dependencies,
25 | ),
26 | project_urls={
27 | "Bug Reports": "https://github.com/xch-gallery/singleton-utils",
28 | "Source": "https://github.com/xch-gallery/singleton-utils",
29 | },
30 | )
31 |
--------------------------------------------------------------------------------
/ownable_singleton/clsp/p2_singleton_or_cancel.clsp.hex:
--------------------------------------------------------------------------------
1 | ff02ffff01ff02ffff03ff81bfffff01ff04ffff04ff38ffff04ffff0bffff02ff2effff04ff02ffff04ff05ffff04ff5fffff04ffff02ff3effff04ff02ffff04ffff04ff05ffff04ff0bff178080ff80808080ff808080808080ff81bf80ff808080ffff04ffff04ff3cffff04ff82017fff808080ffff04ffff04ff28ffff04ff81bfff808080ff80808080ffff01ff04ffff04ff2cffff04ff2fffff04ff5fff80808080ffff04ffff04ff10ffff04ff5fff808080ff80808080ff0180ffff04ffff01ffffff49ff463fff02ff333cffff04ff0101ffff02ff02ffff03ff05ffff01ff02ff36ffff04ff02ffff04ff0dffff04ffff0bff26ffff0bff2aff1280ffff0bff26ffff0bff26ffff0bff2aff3a80ff0980ffff0bff26ff0bffff0bff2aff8080808080ff8080808080ffff010b80ff0180ffff0bff26ffff0bff2aff1480ffff0bff26ffff0bff26ffff0bff2aff3a80ff0580ffff0bff26ffff02ff36ffff04ff02ffff04ff07ffff04ffff0bff2aff2a80ff8080808080ffff0bff2aff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080
--------------------------------------------------------------------------------
/ownable_singleton/clsp/ownable_singleton_v1.clsp.hex:
--------------------------------------------------------------------------------
1 | ff02ffff01ff02ffff03ff5fffff01ff02ffff03ffff15ff82017fff8080ffff01ff02ff2effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff8201afffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffff8080808080808080808080ffff01ff02ff36ffff04ff02ffff04ff05ffff04ff17ffff04ff8201afffff04ff5fffff04ff81bfff808080808080808080ff0180ffff01ff088080ff0180ffff04ffff01ffffff32ff3d02ff33ff3e04ffff01ff0102ffffff02ffff03ff05ffff01ff02ff26ffff04ff02ffff04ff0dffff04ffff0bff3affff0bff12ff3c80ffff0bff3affff0bff3affff0bff12ff2a80ff0980ffff0bff3aff0bffff0bff12ff8080808080ff8080808080ffff010b80ff0180ff04ffff04ff14ffff04ffff02ff3effff04ff02ffff04ff0bffff04ffff0bffff0101ff0b80ffff04ffff0bffff0101ff5f80ffff04ffff0bffff0101ff2f80ff80808080808080ffff04ff17ff80808080ffff04ffff04ff10ffff04ff2fffff04ff5fff80808080ffff04ffff04ff10ffff04ff05ffff04ff2fff80808080ff80808080ffff04ffff04ff14ffff04ffff02ff3effff04ff02ffff04ff17ffff04ffff0bffff0101ff1780ffff04ffff0bffff0101ff81bf80ffff04ffff0bffff0101ff5f80ff80808080808080ffff04ff2fff80808080ffff04ffff04ff14ffff04ff0bffff04ff82017fff80808080ffff04ffff04ff2cffff04ff8202ffff808080ffff04ffff04ff28ffff04ffff0bff8202ffff5f80ff808080ffff04ffff04ff10ffff04ff5fffff04ff81bfff80808080ffff04ffff04ff10ffff04ff05ffff04ff82017fff80808080ff80808080808080ff0bff3affff0bff12ff3880ffff0bff3affff0bff3affff0bff12ff2a80ff0580ffff0bff3affff02ff26ffff04ff02ffff04ff07ffff04ffff0bff12ff1280ff8080808080ffff0bff12ff8080808080ff018080
--------------------------------------------------------------------------------
/ownable_singleton/clsp/ownable_singleton_v2.clsp.hex:
--------------------------------------------------------------------------------
1 | ff02ffff01ff02ffff03ff5fffff01ff02ffff03ff81bfffff01ff02ff3affff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff8201afffff04ff5fffff04ff81bfff808080808080808080ffff01ff02ff2affff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff8201afffff04ff5fff808080808080808080ff0180ffff01ff088080ff0180ffff04ffff01ffffffff323dff0233ffff3e04ff0101ffffff02ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff22ffff0bff2cff3480ffff0bff22ffff0bff22ffff0bff2cff3c80ff0980ffff0bff22ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff04ffff04ff38ffff04ffff02ff2effff04ff02ffff04ff17ffff04ffff0bffff0101ff1780ffff04ffff02ff3effff04ff02ffff04ff0bff80808080ffff04ffff02ff3effff04ff02ffff04ff5fff80808080ff80808080808080ffff04ff2fff80808080ffff04ffff04ff20ffff04ff819fffff04ff82015fff80808080ffff04ffff04ff20ffff04ff09ffff04ff819fff80808080ff80808080ff02ff36ffff04ff02ffff04ffff02ffff03ff0bffff01ff04ffff04ff38ffff04ff15ffff04ffff02ff26ffff04ff02ffff04ffff11ff82013fffff02ffff03ff2bffff01ff05ffff14ffff12ff82013fff2b80ffff01648080ff8080ff018080ff80808080ff80808080ffff04ffff04ff38ffff04ff13ffff04ffff02ff26ffff04ff02ffff04ffff02ffff03ff2bffff01ff05ffff14ffff12ff82013fff2b80ffff01648080ff8080ff0180ff80808080ff80808080ff808080ffff01ff04ffff04ff38ffff04ff15ffff04ff82013fff80808080ff808080ff0180ffff04ffff04ffff04ff38ffff04ffff02ff2effff04ff02ffff04ff17ffff04ffff0bffff0101ff1780ffff04ffff02ff3effff04ff02ffff04ff0bff80808080ffff04ffff02ff3effff04ff02ffff04ff5fff80808080ff80808080808080ffff04ff2fff80808080ffff04ffff04ff24ffff04ff8202bfff808080ffff04ffff04ff30ffff04ffff0bff8202bfff819f80ff808080ffff04ffff04ff20ffff04ff819fffff04ff82015fff80808080ffff04ffff04ff20ffff04ff09ffff04ff82013fff80808080ff808080808080ff8080808080ffffff02ffff03ffff06ffff14ff05ffff01028080ffff01ff11ff05ffff010180ffff010580ff0180ff02ffff03ff05ffff01ff04ff09ffff02ff36ffff04ff02ffff04ff0dffff04ff0bff808080808080ffff010b80ff0180ffff0bff22ffff0bff2cff2880ffff0bff22ffff0bff22ffff0bff2cff3c80ff0580ffff0bff22ffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080
--------------------------------------------------------------------------------
/ownable_singleton/clsp/ownable_singleton_v1.clsp:
--------------------------------------------------------------------------------
1 | (mod (OWNER_PUBKEY
2 | OWNER_PUZZLE_HASH
3 | INNER_PUZZLE_HASH
4 | Truths
5 | new_owner_pubkey
6 | new_owner_puzhash
7 | payment_amount ; insert 0 for transfer without payment
8 | payment_id
9 | )
10 |
11 | (include condition_codes.clib)
12 | (include singleton_truths.clib)
13 | (include curry_and_treehash.clib)
14 |
15 | (defun-inline inner_puzzle_hash_for_new_owner (INNER_PUZZLE_HASH new_owner_pubkey new_owner_puzhash)
16 | (puzzle-hash-of-curried-function INNER_PUZZLE_HASH (sha256 1 INNER_PUZZLE_HASH) (sha256 1 new_owner_puzhash) (sha256 1 new_owner_pubkey))
17 | )
18 |
19 | (defun change_owner (OWNER_PUBKEY INNER_PUZZLE_HASH my_amount new_owner_pubkey new_owner_puzhash)
20 | (list (list CREATE_COIN (inner_puzzle_hash_for_new_owner INNER_PUZZLE_HASH new_owner_pubkey new_owner_puzhash) my_amount) ; Transfer the singleton to the new owner
21 | (list AGG_SIG_ME new_owner_pubkey new_owner_puzhash) ; Ensure that the puzhash matches the public key
22 | (list AGG_SIG_ME OWNER_PUBKEY new_owner_pubkey) ; Owner asserts the new owner
23 | )
24 | )
25 |
26 | (defun change_owner_and_transfer_payment (OWNER_PUBKEY OWNER_PUZZLE_HASH INNER_PUZZLE_HASH my_amount new_owner_pubkey new_owner_puzhash payment_amount payment_id)
27 | (list (list CREATE_COIN (inner_puzzle_hash_for_new_owner INNER_PUZZLE_HASH new_owner_pubkey new_owner_puzhash) my_amount) ; Transfer the singleton to the new owner
28 | (list CREATE_COIN OWNER_PUZZLE_HASH payment_amount) ; Pay the current owner
29 | (list CREATE_PUZZLE_ANNOUNCEMENT payment_id) ; Inform the payment coin of this spend
30 | (list ASSERT_COIN_ANNOUNCEMENT (sha256 payment_id new_owner_pubkey)) ; Assert that the payment coin is being spent and validate the owner
31 | (list AGG_SIG_ME new_owner_pubkey new_owner_puzhash) ; Ensure that the puzhash matches the public key
32 | (list AGG_SIG_ME OWNER_PUBKEY payment_amount) ; Owner asserts the price
33 | )
34 | )
35 |
36 | ; main
37 | (if new_owner_pubkey
38 | (if (> payment_amount 0)
39 | (change_owner_and_transfer_payment OWNER_PUBKEY OWNER_PUZZLE_HASH INNER_PUZZLE_HASH (my_amount_truth Truths) new_owner_pubkey new_owner_puzhash payment_amount payment_id)
40 | (change_owner OWNER_PUBKEY INNER_PUZZLE_HASH (my_amount_truth Truths) new_owner_pubkey new_owner_puzhash)
41 | )
42 | (x)
43 | )
44 | )
45 |
--------------------------------------------------------------------------------
/ownable_singleton/clsp/p2_singleton_or_cancel.clsp:
--------------------------------------------------------------------------------
1 | (mod (SINGLETON_MOD_HASH LAUNCHER_ID LAUNCHER_PUZZLE_HASH CANCEL_PUZZLE_HASH p1 my_id new_owner_pubkey)
2 |
3 | ;; This puzzle has two escape conditions: the regular "claim via singleton", and the
4 | ;; cancel "claim via puzzle hash".
5 |
6 | ; SINGLETON_MOD_HASH is the mod-hash for the singleton_top_layer puzzle
7 | ; LAUNCHER_ID is the ID of the singleton we are commited to paying to
8 | ; LAUNCHER_PUZZLE_HASH is the puzzle hash of the launcher
9 | ; CANCEL_PUZZLE_HASH is the puzzle hash of the cancel puzzle
10 | ; if my_id is passed in as () then this signals that we are trying to do a cancel spend case
11 | ; p1's meaning changes depending upon which case we're using
12 | ; if we are paying to singleton then p1 is singleton_inner_puzzle_hash
13 | ; if we are running the cancel case then p1 is the amount to output
14 |
15 | (include condition_codes.clib)
16 | (include curry_and_treehash.clib)
17 |
18 | ; takes a lisp tree and returns the hash of it
19 | (defun sha256tree (TREE)
20 | (if (l TREE)
21 | (sha256 2 (sha256tree (f TREE)) (sha256tree (r TREE)))
22 | (sha256 1 TREE)
23 | )
24 | )
25 |
26 | (defun-inline cancel_spend (CANCEL_PUZZLE_HASH amount)
27 | (list
28 | (list CREATE_COIN CANCEL_PUZZLE_HASH amount)
29 | (list ASSERT_MY_AMOUNT amount)
30 | )
31 | )
32 |
33 | ;; return the full puzzlehash for a singleton with the innerpuzzle curried in
34 | ; puzzle-hash-of-curried-function is imported from curry-and-treehash.clinc
35 | (defun-inline calculate_full_puzzle_hash (SINGLETON_MOD_HASH LAUNCHER_ID LAUNCHER_PUZZLE_HASH inner_puzzle_hash)
36 | (puzzle-hash-of-curried-function SINGLETON_MOD_HASH
37 | inner_puzzle_hash
38 | (sha256tree (c SINGLETON_MOD_HASH (c LAUNCHER_ID LAUNCHER_PUZZLE_HASH)))
39 | )
40 | )
41 |
42 | (defun-inline claim_payment (SINGLETON_MOD_HASH LAUNCHER_ID LAUNCHER_PUZZLE_HASH singleton_inner_puzzle_hash my_id)
43 | (list
44 | (list ASSERT_PUZZLE_ANNOUNCEMENT (sha256 (calculate_full_puzzle_hash SINGLETON_MOD_HASH LAUNCHER_ID LAUNCHER_PUZZLE_HASH singleton_inner_puzzle_hash) my_id))
45 | (list CREATE_COIN_ANNOUNCEMENT new_owner_pubkey) ; Announce the new owner of the singleton
46 | (list ASSERT_MY_COIN_ID my_id))
47 | )
48 |
49 | ; main
50 | (if my_id
51 | (claim_payment SINGLETON_MOD_HASH LAUNCHER_ID LAUNCHER_PUZZLE_HASH p1 my_id)
52 | (cancel_spend CANCEL_PUZZLE_HASH p1)
53 | )
54 | )
55 |
--------------------------------------------------------------------------------
/ownable_singleton/clsp/ownable_singleton_v2.clsp:
--------------------------------------------------------------------------------
1 | (mod (OWNER
2 | ROYALTY
3 | INNER_PUZZLE_HASH
4 | Truths
5 | new_owner
6 | payment
7 | )
8 |
9 | (defun-inline pubkey_of (owner) (f owner))
10 | (defun-inline puzhash_of (owner) (f (r owner)))
11 |
12 | (defun-inline creator_puzhash_of (royalty) (f royalty))
13 | (defun-inline percentage_of (royalty) (f (r royalty)))
14 |
15 | (defun-inline amount_of (payment) (f payment))
16 | (defun-inline id_of (payment) (f (r payment)))
17 |
18 | (include condition_codes.clib)
19 | (include sha256tree.clib)
20 | (include singleton_truths.clib)
21 | (include curry_and_treehash.clib)
22 |
23 | (defun merge_list (list_a list_b)
24 | (if list_a
25 | (c (f list_a) (merge_list (r list_a) list_b))
26 | list_b
27 | )
28 | )
29 |
30 | (defun make_even (amt)
31 | (if (r (divmod amt 2))
32 | (- amt 1)
33 | amt
34 | )
35 | )
36 |
37 | (defun-inline creator_amt (ROYALTY amount)
38 | (if (percentage_of ROYALTY)
39 | (f (divmod (* amount (percentage_of ROYALTY)) 100))
40 | 0
41 | )
42 | )
43 |
44 | (defun-inline inner_puzzle_hash_for_new_owner (INNER_PUZZLE_HASH ROYALTY new_owner)
45 | (puzzle-hash-of-curried-function INNER_PUZZLE_HASH (sha256 1 INNER_PUZZLE_HASH) (sha256tree ROYALTY) (sha256tree new_owner))
46 | )
47 |
48 | (defun change_owner (OWNER ROYALTY INNER_PUZZLE_HASH my_amount new_owner)
49 | (list (list CREATE_COIN (inner_puzzle_hash_for_new_owner INNER_PUZZLE_HASH ROYALTY new_owner) my_amount) ; Transfer the singleton to the new owner
50 | (list AGG_SIG_ME (pubkey_of new_owner) (puzhash_of new_owner)) ; Ensure that the puzhash matches the public key
51 | (list AGG_SIG_ME (pubkey_of OWNER) (pubkey_of new_owner)) ; Owner asserts the new owner
52 | )
53 | )
54 |
55 | ; TODO Allow no royalty
56 | (defun change_owner_and_transfer_payment (OWNER ROYALTY INNER_PUZZLE_HASH my_amount new_owner payment)
57 | (merge_list
58 | (if ROYALTY
59 | (list (list CREATE_COIN (puzhash_of OWNER) (make_even (- (amount_of payment) (creator_amt ROYALTY (amount_of payment))))) ; Pay the current owner
60 | (list CREATE_COIN (creator_puzhash_of ROYALTY) (make_even (creator_amt ROYALTY (amount_of payment)))) ; Pay the creator
61 | )
62 | (list (list CREATE_COIN (puzhash_of OWNER) (amount_of payment))) ; Pay the current owner
63 | )
64 | (list (list CREATE_COIN (inner_puzzle_hash_for_new_owner INNER_PUZZLE_HASH ROYALTY new_owner) my_amount) ; Transfer the singleton to the new owner
65 | (list CREATE_PUZZLE_ANNOUNCEMENT (id_of payment)) ; Inform the payment coin of this spend
66 | (list ASSERT_COIN_ANNOUNCEMENT (sha256 (id_of payment) (pubkey_of new_owner))) ; Assert that the payment coin is being spent and validate the owner
67 | (list AGG_SIG_ME (pubkey_of new_owner) (puzhash_of new_owner)) ; Ensure that the puzhash matches the public key
68 | (list AGG_SIG_ME (pubkey_of OWNER) (amount_of payment)) ; Owner asserts the price
69 | )
70 | )
71 | )
72 |
73 | ; main
74 | (if new_owner
75 | (if payment
76 | (change_owner_and_transfer_payment OWNER ROYALTY INNER_PUZZLE_HASH (my_amount_truth Truths) new_owner payment)
77 | (change_owner OWNER ROYALTY INNER_PUZZLE_HASH (my_amount_truth Truths) new_owner)
78 | )
79 | (x)
80 | )
81 | )
82 |
--------------------------------------------------------------------------------
/ownable_singleton/drivers/ownable_singleton_driver.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from typing import Tuple, List, Optional
3 |
4 | import cdv.clibs as std_lib
5 | from blspy import G1Element
6 | from cdv.util.load_clvm import load_clvm
7 | from clvm.casts import int_from_bytes
8 |
9 | from chia.types.blockchain_format.coin import Coin
10 | from chia.types.blockchain_format.program import Program
11 | from chia.types.blockchain_format.sized_bytes import bytes32
12 | from chia.types.coin_spend import CoinSpend
13 | from chia.util.ints import uint64
14 | from chia.wallet.lineage_proof import LineageProof
15 | from chia.wallet.puzzles import (
16 | p2_conditions,
17 | p2_delegated_puzzle_or_hidden_puzzle,
18 | singleton_top_layer,
19 | )
20 | from chia.wallet.puzzles.singleton_top_layer import (
21 | SINGLETON_MOD_HASH,
22 | SINGLETON_LAUNCHER_HASH,
23 | )
24 |
25 | clibs_path: Path = Path(std_lib.__file__).parent
26 |
27 | OWNABLE_SINGLETON_MOD_V1: Program = load_clvm(
28 | "ownable_singleton_v1.clsp", "ownable_singleton.clsp", search_paths=[clibs_path]
29 | )
30 | OWNABLE_SINGLETON_MOD_V2: Program = load_clvm(
31 | "ownable_singleton_v2.clsp", "ownable_singleton.clsp", search_paths=[clibs_path]
32 | )
33 | P2_SINGLETON_OR_CANCEL_MOD: Program = load_clvm(
34 | "p2_singleton_or_cancel.clsp", "ownable_singleton.clsp", search_paths=[clibs_path]
35 | )
36 | SINGLETON_AMOUNT: uint64 = 1023
37 |
38 |
39 | class Owner:
40 | def __init__(self, public_key: G1Element, puzzle_hash: bytes32):
41 | self.public_key = public_key
42 | self.puzzle_hash = puzzle_hash
43 |
44 | @staticmethod
45 | def from_bytes_list(owner_array: List[bytes32]):
46 | return Owner(G1Element.from_bytes(owner_array[0]), owner_array[1])
47 |
48 |
49 | class Royalty:
50 | def __init__(self, creator_puzhash: bytes32, percentage: int):
51 | self.creator_puzhash = creator_puzhash
52 | self.percentage = percentage
53 |
54 | @staticmethod
55 | def from_bytes_list(royalty_list: List[bytes32]):
56 | return Royalty(royalty_list[0], int_from_bytes(royalty_list[1]))
57 |
58 |
59 | def pay_to_singleton_puzzle(launcher_id: bytes32, cancel_puzhash: bytes32) -> Program:
60 | return P2_SINGLETON_OR_CANCEL_MOD.curry(
61 | SINGLETON_MOD_HASH, launcher_id, SINGLETON_LAUNCHER_HASH, cancel_puzhash
62 | )
63 |
64 |
65 | def create_inner_puzzle(version: int, owner: Owner, royalty: Optional[Royalty] = None):
66 | if version == 1:
67 | if royalty is not None:
68 | raise f"Version 1 does not support royalties"
69 |
70 | return OWNABLE_SINGLETON_MOD_V1.curry(
71 | owner.public_key,
72 | owner.puzzle_hash,
73 | OWNABLE_SINGLETON_MOD_V1.get_tree_hash(),
74 | )
75 | elif version == 2:
76 | return OWNABLE_SINGLETON_MOD_V2.curry(
77 | [
78 | owner.public_key,
79 | owner.puzzle_hash,
80 | ],
81 | [royalty.creator_puzhash, royalty.percentage] if royalty else [],
82 | OWNABLE_SINGLETON_MOD_V2.get_tree_hash(),
83 | )
84 | else:
85 | raise f"Unsupported version: {version}"
86 |
87 |
88 | def create_inner_solution(
89 | version: int, new_owner: Owner, payment_amount: int, payment_id: bytes32
90 | ) -> Program:
91 | if version == 1:
92 | return Program.to(
93 | [
94 | new_owner.public_key,
95 | new_owner.puzzle_hash,
96 | payment_amount,
97 | payment_id,
98 | ]
99 | )
100 | elif version == 2:
101 | return Program.to(
102 | [
103 | [new_owner.public_key, new_owner.puzzle_hash],
104 | [payment_amount, payment_id],
105 | ]
106 | )
107 |
108 |
109 | def create_unsigned_ownable_singleton(
110 | genesis_coin: Coin,
111 | genesis_coin_puzzle: Program,
112 | creator: Owner,
113 | uri: str,
114 | name: str,
115 | version=1,
116 | royalty: Optional[Royalty] = None,
117 | ) -> Tuple[List[CoinSpend], Program]:
118 | comment = [
119 | ("uri", uri),
120 | ("name", name),
121 | ("creator", [creator.public_key, creator.puzzle_hash] if version == 2 else creator.public_key),
122 | ("version", version),
123 | ]
124 |
125 | inner_puzzle = create_inner_puzzle(version, creator, royalty)
126 | if royalty:
127 | comment.append(
128 | (
129 | "royalty",
130 | [royalty.creator_puzhash, royalty.percentage],
131 | )
132 | )
133 |
134 | assert genesis_coin.amount == SINGLETON_AMOUNT
135 |
136 | conditions, launcher_coinsol = singleton_top_layer.launch_conditions_and_coinsol(
137 | genesis_coin, inner_puzzle, comment, SINGLETON_AMOUNT
138 | )
139 |
140 | delegated_puzzle: Program = p2_conditions.puzzle_for_conditions(conditions)
141 | full_solution: Program = (
142 | p2_delegated_puzzle_or_hidden_puzzle.solution_for_conditions(conditions)
143 | )
144 |
145 | starting_coinsol: CoinSpend = CoinSpend(
146 | genesis_coin,
147 | genesis_coin_puzzle,
148 | full_solution,
149 | )
150 |
151 | return [launcher_coinsol, starting_coinsol], delegated_puzzle
152 |
153 |
154 | def create_buy_offer(
155 | p2_singleton_coin: Coin,
156 | p2_singleton_puzzle: Program,
157 | launcher_id: bytes32,
158 | lineage_proof: LineageProof,
159 | singleton_coin: Coin,
160 | current_owner: Owner,
161 | new_owner: Owner,
162 | payment_amount: uint64,
163 | version=1,
164 | royalty: Optional[Royalty] = None,
165 | ) -> List[CoinSpend]:
166 | singleton_inner_puzzle = create_inner_puzzle(version, current_owner, royalty)
167 |
168 | p2_singleton_solution = Program.to(
169 | [
170 | singleton_inner_puzzle.get_tree_hash(),
171 | p2_singleton_coin.name(),
172 | new_owner.public_key,
173 | ]
174 | )
175 |
176 | p2_singleton_coinsol: CoinSpend = CoinSpend(
177 | p2_singleton_coin, p2_singleton_puzzle, p2_singleton_solution
178 | )
179 |
180 | singleton_puzzle = singleton_top_layer.puzzle_for_singleton(
181 | launcher_id, singleton_inner_puzzle
182 | )
183 |
184 | inner_solution = create_inner_solution(
185 | version, new_owner, payment_amount, p2_singleton_coin.name()
186 | )
187 |
188 | singleton_solution: Program = singleton_top_layer.solution_for_singleton(
189 | lineage_proof,
190 | singleton_coin.amount,
191 | inner_solution,
192 | )
193 |
194 | singleton_coinsol: CoinSpend = CoinSpend(
195 | singleton_coin, singleton_puzzle, singleton_solution
196 | )
197 |
198 | return [p2_singleton_coinsol, singleton_coinsol]
199 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | MintGarden NFT Companion
8 |
9 |
12 |
13 | ## Installation
14 |
15 | 1. Clone the repository
16 | ```shell
17 | git clone https://github.com/mintgarden-io/nft-companion.git
18 | cd nft-companion
19 | ```
20 |
21 | 2.LINUX install: Run the install script and activate the virtual environment in Linux
22 | ```shell
23 | sh install.sh
24 | . ./venv/bin/activate
25 | ```
26 |
27 |
28 | 2.WIN10 install: Run the install script and activate the virtual environment in Win10
29 | ```
30 | git clone https://github.com/mintgarden-io/nft-companion
31 | cd nft-companion
32 | ./install.ps1
33 |
34 | ./venv/Scripts/activate
35 | ```
36 |
37 | ## Create a new NFT singleton
38 |
39 | The `create` command can be used to create a new NFT singleton using the Chia light wallet.
40 | It requires a running wallet on your computer.
41 |
42 | ```shell
43 | $ python3 nft.py create --help
44 | Usage: nft.py create [OPTIONS]
45 |
46 | Options:
47 | --name TEXT The name of the NFT
48 | --uri TEXT The uri of the main NFT image
49 | --royalty INTEGER The royalty percentage [default: 0]
50 | --fingerprint INTEGER The fingerprint of the key to use [optional]
51 | --fee INTEGER The XCH fee to use for this transaction [default: 0]
52 | --help Show this message and exit.
53 | ```
54 |
55 | The following example shows the creation of an example NFT singleton.
56 |
57 | ```shell
58 | $ python3 nft.py create --name "Curly Nonchalant Marmot" --uri "https://example.com/curly-nonchalant-marmot.png"
59 | The transaction seems valid. Do you want to submit it? [y/N]: y
60 | The NFT has been submitted successfully!
61 | Please wait a few minutes for the NFT to be finalized.
62 | You can inspect your NFT using the following link: https://testnet.mintgarden.io/singletons/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
63 |
64 |
65 | Accept a offer:
66 | $python3 nft.py accept-offer --offer-id 8 --launcher-id "4e4d4bf47b26e233de96da85d132617e5aac4d8087cf61e0f17a2a7d92a1d51e" --fingerprint "3405833834"
67 |
68 | ```
69 |
70 | ## Make a buy offer for a NFT singleton
71 |
72 | The `offer` command can be used to make an offer to buy a NFT singleton.
73 | It requires a running wallet on your computer.
74 |
75 | ```shell
76 | $ python3 nft.py offer --help
77 | Usage: nft.py offer [OPTIONS]
78 |
79 | Options:
80 | --launcher-id TEXT The ID of the NFT
81 | --price FLOAT The price (in XCH) you want to offer for this NFT singleton
82 | --fingerprint INTEGER The fingerprint of the key to use [optional]
83 | --fee INTEGER The XCH fee to use for this transaction [default: 0]
84 | --help Show this message and exit.
85 | ```
86 |
87 | Here is an example of making a buy offer.
88 |
89 | ```shell
90 | $ python3 nft.py offer --price 0.11 --launcher-id "356eb19da1fac4490c8f83e39788d5989cc0db5a2eaf8285a58cd7f4ebe07501"
91 | You are offering 0.11 XCH for 'The fox'. Do you want to submit it? [y/N]: y
92 | Your offer has been submitted successfully!
93 | You can inspect it using the following link: https://testnet.mintgarden.io/singletons/356eb19da1fac4490c8f83e39788d5989cc0db5a2eaf8285a58cd7f4ebe07501
94 | ```
95 |
96 | ## Accept a buy offer for a NFT singleton
97 |
98 | The `accept-offer` command can be used to accept a buy offer.
99 | It requires a running wallet on your computer.
100 |
101 | ```shell
102 | $ python3 nft.py accept-offer --help
103 | Usage: nft.py accept-offer [OPTIONS]
104 |
105 | Options:
106 | --launcher-id TEXT The ID of the NFT
107 | --offer-id TEXT The ID of the offer you want to accept
108 | --fingerprint INTEGER The fingerprint of the key to use [optional]
109 | --help Show this message and exit.
110 | ```
111 |
112 | Here is an example of accepting a buy offer.
113 |
114 | ```shell
115 | $ python3 nft.py accept-offer --offer-id 16 --launcher-id "356eb19da1fac4490c8f83e39788d5989cc0db5a2eaf8285a58cd7f4ebe07501"
116 | You are accepting 0.11 XCH for 'The fox'. Do you want to submit it? [y/N]: y
117 | You accepted the offer!
118 | The payment is being sent to your singleton wallet address.
119 | ```
120 |
121 | ## Cancel a buy offer for a NFT singleton
122 |
123 | The `cancel-offer` command can be used to cancel one of your buy offers.
124 | It requires a running wallet on your computer.
125 |
126 | ```shell
127 | $ python3 nft.py cancel-offer --help
128 | Usage: nft.py cancel-offer [OPTIONS]
129 |
130 | Options:
131 | --launcher-id TEXT The ID of the NFT
132 | --offer-id TEXT The ID of the offer you want to cancel
133 | --fingerprint INTEGER The fingerprint of the key to use [optional]
134 | --help Show this message and exit.
135 | ```
136 |
137 | Here is an example of canceling a buy offer.
138 |
139 | ```shell
140 | $ python3 nft.py cancel-offer --offer-id 16 --launcher-id "356eb19da1fac4490c8f83e39788d5989cc0db5a2eaf8285a58cd7f4ebe07501"
141 | Do you want to cancel your offer of 0.11 XCH for 'The fox'? [y/N]: y
142 | You cancelled the offer.
143 | ```
144 |
145 | ## Showing your profile
146 |
147 | The `profile` command can be used to show the singleton profile for a given wallet.
148 | It requires a running wallet on your computer.
149 |
150 | ```shell
151 | $ python3 nft.py profile --help
152 | Usage: nft.py profile [OPTIONS]
153 |
154 | Options:
155 | --fingerprint INTEGER The fingerprint of the key to use [optional]
156 | --help Show this message and exit.
157 | ```
158 |
159 | Here is an example.
160 |
161 | ```shell
162 | $ python3 nft.py profile
163 | Choose wallet key:
164 | 1) 1105740000
165 | 2) 2244950000
166 | Enter a number to pick or q to quit: 1
167 | Your singleton profile is https://testnet.mintgarden.io/profile/991053e52414463d68cb9f8901f1bf1d7301acf2d1203b4fb28e2ea93c48f10b336a56077ac4fd9a591ce514e72beb00
168 | ```
169 |
170 |
171 | ## Updating your profile
172 |
173 | The `update-profile` command can be used to show the singleton profile for a given wallet.
174 | It requires a running wallet on your computer.
175 |
176 | ```shell
177 | python3 nft.py update-profile --help
178 | Usage: nft.py update-profile [OPTIONS]
179 |
180 | Options:
181 | --name TEXT Your profile name
182 | --fingerprint INTEGER The fingerprint of the key to use
183 | --help Show this message and exit.
184 | ```
185 |
186 | Here is an example.
187 |
188 | ```shell
189 | $ python3 nft.py update-profile
190 | Name: Acevail
191 | Do you want to set your profile name to Acevail? [y/N]: y
192 | Your profile has been updated!
193 | You can inspect it using the following link: https://testnet.mintgarden.io/profile/b3035d8ca2d572dec7843cc134277eec13e56c84afb2bd41ba78cb5a1b080033177433cfa8973bb5bd583ff55e96f4b4
194 | ```
195 |
196 | ## Attribution
197 |
198 | The puzzles in this repository build on puzzles included in the [chia-blockchain](https://github.com/Chia-Network/chia-blockchain) project, which is licensed under Apache 2.0.
199 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2021 Andreas Greimel
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/ownable_singleton/tests/test_ownable_singleton.py:
--------------------------------------------------------------------------------
1 | from typing import List, Optional
2 |
3 | import pytest
4 | from blspy import AugSchemeMPL, G2Element, PrivateKey, G1Element
5 | from cdv.test import CoinWrapper, Wallet
6 | from cdv.test import setup as setup_test
7 | from clvm.casts import int_to_bytes
8 |
9 | from chia.consensus.default_constants import DEFAULT_CONSTANTS
10 | from chia.types.blockchain_format.coin import Coin
11 | from chia.types.condition_opcodes import ConditionOpcode
12 | from chia.types.spend_bundle import SpendBundle
13 | from chia.util.ints import uint64, uint32
14 | from chia.wallet.derive_keys import master_sk_to_singleton_owner_sk
15 | from chia.wallet.puzzles import (
16 | singleton_top_layer,
17 | p2_delegated_puzzle_or_hidden_puzzle,
18 | )
19 | from ownable_singleton.drivers.ownable_singleton_driver import (
20 | create_unsigned_ownable_singleton,
21 | create_inner_puzzle,
22 | create_buy_offer,
23 | pay_to_singleton_puzzle,
24 | Owner,
25 | Royalty,
26 | )
27 |
28 | SINGLETON_AMOUNT: uint64 = 1023
29 |
30 |
31 | def wallet_to_owner(wallet: Wallet) -> Owner:
32 | wallet_singleton_sk = master_sk_to_singleton_owner_sk(wallet.sk_, uint32(0))
33 | singleton_wallet_puzzle = p2_delegated_puzzle_or_hidden_puzzle.puzzle_for_pk(
34 | wallet_singleton_sk.get_g1()
35 | )
36 |
37 | return Owner(wallet_singleton_sk.get_g1(), singleton_wallet_puzzle.get_tree_hash())
38 |
39 |
40 | async def create_singleton_spend_bundle(
41 | contribution_coin: Coin, creator_wallet, version, royalty: Royalty
42 | ):
43 | creator = wallet_to_owner(creator_wallet)
44 |
45 | genesis_create_spend = await creator_wallet.spend_coin(
46 | contribution_coin,
47 | pushtx=False,
48 | amt=SINGLETON_AMOUNT,
49 | remain=creator_wallet,
50 | custom_conditions=[
51 | [
52 | ConditionOpcode.CREATE_COIN,
53 | creator.puzzle_hash,
54 | SINGLETON_AMOUNT,
55 | ]
56 | ],
57 | )
58 | genesis_coin = Coin(
59 | parent_coin_info=contribution_coin.as_coin().name(),
60 | puzzle_hash=creator.puzzle_hash,
61 | amount=SINGLETON_AMOUNT,
62 | )
63 | genesis_coin_puzzle = p2_delegated_puzzle_or_hidden_puzzle.puzzle_for_pk(
64 | creator.public_key
65 | )
66 | name = "Curly Nonchalant Marmot"
67 | uri = "https://example.com/curly-nonchalant-marmot.png"
68 | (coin_spends, delegated_puzzle) = create_unsigned_ownable_singleton(
69 | genesis_coin,
70 | genesis_coin_puzzle,
71 | creator,
72 | uri,
73 | name,
74 | version,
75 | royalty,
76 | )
77 |
78 | creator_singleton_sk = master_sk_to_singleton_owner_sk(
79 | creator_wallet.sk_, uint32(0)
80 | )
81 | synthetic_secret_key: PrivateKey = (
82 | p2_delegated_puzzle_or_hidden_puzzle.calculate_synthetic_secret_key(
83 | creator_singleton_sk,
84 | p2_delegated_puzzle_or_hidden_puzzle.DEFAULT_HIDDEN_PUZZLE_HASH,
85 | )
86 | )
87 | signature = AugSchemeMPL.sign(
88 | synthetic_secret_key,
89 | (
90 | delegated_puzzle.get_tree_hash()
91 | + genesis_coin.name()
92 | + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA
93 | ),
94 | )
95 | singleton_spend = SpendBundle(coin_spends, signature)
96 | combined_spend = SpendBundle.aggregate([genesis_create_spend, singleton_spend])
97 | return combined_spend, genesis_coin, singleton_spend.coin_spends[0]
98 |
99 |
100 | async def get_singleton_puzzle_owned_by_user(
101 | result, launcher_id, user, version, royalty
102 | ):
103 | owner = wallet_to_owner(user)
104 |
105 | inner_puzzle = create_inner_puzzle(version, owner, royalty)
106 | user_singleton_puzzle = singleton_top_layer.puzzle_for_singleton(
107 | launcher_id, inner_puzzle
108 | )
109 | # singleton coin is added and owned by user
110 | filtered_result: List[Coin] = list(
111 | filter(
112 | lambda addition: (addition.amount == SINGLETON_AMOUNT)
113 | and (addition.puzzle_hash == user_singleton_puzzle.get_tree_hash()),
114 | result["additions"],
115 | )
116 | )
117 | assert len(filtered_result) == 1
118 | return user_singleton_puzzle
119 |
120 |
121 | async def create_buy_offer_for_user(
122 | current_owner_wallet: Wallet,
123 | buyer_wallet: Wallet,
124 | launcher_coinsol,
125 | launcher_id,
126 | payment_amount,
127 | payment_coin,
128 | singleton_coin,
129 | version,
130 | royalty,
131 | ) -> SpendBundle:
132 | current_owner = wallet_to_owner(current_owner_wallet)
133 | buyer = wallet_to_owner(buyer_wallet)
134 |
135 | buyer_singleton_sk = master_sk_to_singleton_owner_sk(buyer_wallet.sk_, uint32(0))
136 |
137 | lineage_proof = singleton_top_layer.lineage_proof_for_coinsol(launcher_coinsol)
138 | p2_singleton_puzzle = pay_to_singleton_puzzle(launcher_id, payment_coin.puzzle_hash)
139 | payment_coin_spend = await buyer_wallet.spend_coin(
140 | payment_coin,
141 | pushtx=False,
142 | custom_conditions=[
143 | [
144 | ConditionOpcode.CREATE_COIN,
145 | p2_singleton_puzzle.get_tree_hash(),
146 | payment_amount,
147 | ]
148 | ],
149 | )
150 | p2_singleton_coin = Coin(
151 | parent_coin_info=payment_coin.as_coin().name(),
152 | puzzle_hash=p2_singleton_puzzle.get_tree_hash(),
153 | amount=payment_amount,
154 | )
155 | # Bob prepares payment bundle
156 | coin_spends = create_buy_offer(
157 | p2_singleton_coin,
158 | p2_singleton_puzzle,
159 | launcher_id,
160 | lineage_proof,
161 | singleton_coin,
162 | current_owner,
163 | buyer,
164 | payment_amount,
165 | version,
166 | royalty,
167 | )
168 | signatures: List[G2Element] = [
169 | AugSchemeMPL.sign(
170 | buyer_singleton_sk,
171 | buyer.puzzle_hash
172 | + singleton_coin.name()
173 | + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA,
174 | ),
175 | ]
176 | aggregated_signature = AugSchemeMPL.aggregate(signatures)
177 | assert AugSchemeMPL.aggregate_verify(
178 | [buyer.public_key],
179 | [
180 | buyer.puzzle_hash
181 | + singleton_coin.name()
182 | + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA
183 | ],
184 | aggregated_signature,
185 | )
186 | buy_offer = SpendBundle.aggregate(
187 | [
188 | payment_coin_spend,
189 | SpendBundle(
190 | coin_spends,
191 | aggregated_signature,
192 | ),
193 | ]
194 | )
195 | return buy_offer
196 |
197 |
198 | async def accept_buy_offer(singleton_coin, buy_offer, seller, payment_amount):
199 | seller_singleton_sk = master_sk_to_singleton_owner_sk(seller.sk_, uint32(0))
200 |
201 | # ALice signs the buy offer
202 | seller_signature = AugSchemeMPL.sign(
203 | seller_singleton_sk,
204 | int_to_bytes(payment_amount)
205 | + singleton_coin.name()
206 | + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA,
207 | )
208 | aggregated_signature = AugSchemeMPL.aggregate(
209 | [buy_offer.aggregated_signature, seller_signature]
210 | )
211 | owner_change_spend = SpendBundle(
212 | buy_offer.coin_spends,
213 | aggregated_signature,
214 | )
215 | return owner_change_spend
216 |
217 |
218 | testdata = [
219 | [1, 0],
220 | [2, 0],
221 | [2, 10],
222 | ]
223 |
224 |
225 | class TestOwnableSingleton:
226 | @pytest.fixture(scope="function")
227 | async def setup(self):
228 | network, alice, bob = await setup_test()
229 | await network.farm_block()
230 | yield network, alice, bob
231 |
232 | @pytest.mark.asyncio
233 | @pytest.mark.parametrize("version,royalty_percentage", testdata)
234 | async def test_singleton_creation(self, setup, version, royalty_percentage):
235 | network, alice, bob = setup
236 | try:
237 | await network.farm_block(farmer=alice)
238 |
239 | contribution_coin: Optional[CoinWrapper] = await alice.choose_coin(
240 | SINGLETON_AMOUNT
241 | )
242 | royalty = (
243 | Royalty(alice.puzzle_hash, royalty_percentage)
244 | if royalty_percentage
245 | else None
246 | )
247 |
248 | (
249 | combined_spend,
250 | genesis_coin,
251 | launcher_coinsol,
252 | ) = await create_singleton_spend_bundle(
253 | contribution_coin, alice, version, royalty
254 | )
255 |
256 | result = await network.push_tx(combined_spend)
257 |
258 | assert "error" not in result
259 |
260 | # Make sure there is a singleton owned by alice
261 | launcher_coin: Coin = singleton_top_layer.generate_launcher_coin(
262 | genesis_coin,
263 | SINGLETON_AMOUNT,
264 | )
265 | launcher_id = launcher_coin.name()
266 |
267 | await get_singleton_puzzle_owned_by_user(
268 | result, launcher_id, alice, version, royalty
269 | )
270 |
271 | finally:
272 | await network.close()
273 |
274 | @pytest.mark.asyncio
275 | @pytest.mark.parametrize("version,royalty_percentage", testdata)
276 | async def test_singleton_buy_offer(self, setup, version, royalty_percentage):
277 | network, alice, bob = setup
278 | try:
279 | await network.farm_block(farmer=alice)
280 | await network.farm_block(farmer=bob)
281 |
282 | contribution_coin: Optional[CoinWrapper] = await alice.choose_coin(
283 | SINGLETON_AMOUNT
284 | )
285 | royalty = (
286 | Royalty(alice.puzzle_hash, royalty_percentage)
287 | if royalty_percentage
288 | else None
289 | )
290 | alice_initial_balance = alice.balance()
291 |
292 | (
293 | combined_spend,
294 | genesis_coin,
295 | launcher_coinsol,
296 | ) = await create_singleton_spend_bundle(
297 | contribution_coin, alice, version, royalty
298 | )
299 |
300 | result = await network.push_tx(combined_spend)
301 |
302 | assert "error" not in result
303 |
304 | # Make sure there is a singleton owned by alice
305 | launcher_coin: Coin = singleton_top_layer.generate_launcher_coin(
306 | genesis_coin,
307 | SINGLETON_AMOUNT,
308 | )
309 | launcher_id = launcher_coin.name()
310 |
311 | alice_singleton_puzzle = await get_singleton_puzzle_owned_by_user(
312 | result, launcher_id, alice, version, royalty
313 | )
314 |
315 | assert alice.balance() == alice_initial_balance - SINGLETON_AMOUNT
316 |
317 | # Eve Spend
318 | singleton_coin: Coin = next(
319 | x
320 | for x in result["additions"]
321 | if x.puzzle_hash == alice_singleton_puzzle.get_tree_hash()
322 | )
323 |
324 | payment_amount = 10000
325 |
326 | payment_coin: Optional[CoinWrapper] = await bob.choose_coin(payment_amount)
327 |
328 | buy_offer = await create_buy_offer_for_user(
329 | alice,
330 | bob,
331 | launcher_coinsol,
332 | launcher_id,
333 | payment_amount,
334 | payment_coin,
335 | singleton_coin,
336 | version,
337 | royalty,
338 | )
339 |
340 | accepted_buy_offer = await accept_buy_offer(
341 | singleton_coin, buy_offer, alice, payment_amount
342 | )
343 |
344 | result = await network.push_tx(accepted_buy_offer)
345 |
346 | assert "error" not in result
347 |
348 | owner = wallet_to_owner(bob)
349 |
350 | inner_puzzle = create_inner_puzzle(version, owner, royalty)
351 | bob_singleton_puzzle = singleton_top_layer.puzzle_for_singleton(
352 | launcher_id, inner_puzzle
353 | )
354 | # singleton coin is added and owned by user
355 | filtered_result: List[Coin] = list(
356 | filter(
357 | lambda addition: (addition.amount == SINGLETON_AMOUNT)
358 | and (addition.puzzle_hash == bob_singleton_puzzle.get_tree_hash()),
359 | result["additions"],
360 | )
361 | )
362 | assert len(filtered_result) == 1
363 |
364 | assert alice.balance() == alice_initial_balance - SINGLETON_AMOUNT + (
365 | payment_amount * royalty_percentage / 100
366 | )
367 | print(alice.usable_coins)
368 |
369 | finally:
370 | await network.close()
371 |
--------------------------------------------------------------------------------
/nft.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import asyncio
3 | from typing import Optional, Tuple
4 |
5 | import aiohttp
6 | import click
7 | import requests
8 | from blspy import PrivateKey, AugSchemeMPL, G2Element
9 | from click import FLOAT, INT
10 | from clvm.casts import int_to_bytes
11 |
12 | from chia.cmds.units import units
13 | from chia.cmds.wallet_funcs import get_wallet
14 | from chia.rpc.wallet_rpc_client import WalletRpcClient
15 | from chia.types.blockchain_format.coin import Coin
16 | from chia.types.blockchain_format.program import Program
17 | from chia.types.blockchain_format.sized_bytes import bytes32
18 | from chia.types.spend_bundle import SpendBundle
19 | from chia.util.config import load_config
20 | from chia.util.default_root import DEFAULT_ROOT_PATH
21 | from chia.util.ints import uint16, uint32
22 | from chia.wallet.derive_keys import (
23 | master_sk_to_singleton_owner_sk,
24 | master_sk_to_wallet_sk,
25 | )
26 | from chia.wallet.puzzles import p2_delegated_puzzle_or_hidden_puzzle
27 | from chia.wallet.puzzles.singleton_top_layer import SINGLETON_LAUNCHER_HASH
28 | from chia.wallet.transaction_record import TransactionRecord
29 | from ownable_singleton.drivers.ownable_singleton_driver import (
30 | SINGLETON_AMOUNT,
31 | create_unsigned_ownable_singleton,
32 | pay_to_singleton_puzzle,
33 | Owner,
34 | Royalty,
35 | )
36 |
37 | AGG_SIG_ME_ADDITIONAL_DATA_TESTNET10 = bytes.fromhex(
38 | "ae83525ba8d1dd3f09b277de18ca3e43fc0af20d20c4b3e92ef2a48bd291ccb2"
39 | )
40 |
41 | SINGLETON_GALLERY_API = "https://testnet.mintgarden.io/api"
42 | SINGLETON_GALLERY_FRONTEND = "https://testnet.mintgarden.io"
43 |
44 |
45 | # Loading the client requires the standard chia root directory configuration that all of the chia commands rely on
46 | async def get_client() -> Optional[WalletRpcClient]:
47 | config = load_config(DEFAULT_ROOT_PATH, "config.yaml")
48 | self_hostname = config["self_hostname"]
49 | wallet_rpc_port = config["wallet"]["rpc_port"]
50 |
51 | try:
52 | wallet_client = await WalletRpcClient.create(
53 | self_hostname, uint16(wallet_rpc_port), DEFAULT_ROOT_PATH, config
54 | )
55 | return wallet_client
56 | except Exception as e:
57 | if isinstance(e, aiohttp.ClientConnectorError):
58 | print(f"Connection error. Check if wallet is running at {wallet_rpc_port}")
59 | else:
60 | print(f"Exception from 'wallets' {e}")
61 | return None
62 |
63 |
64 | def master_sk_to_wallet_puzhash(master_sk: PrivateKey) -> bytes32:
65 | wallet_sk = master_sk_to_wallet_sk(master_sk, uint32(0))
66 | wallet_puzzle = p2_delegated_puzzle_or_hidden_puzzle.puzzle_for_pk(
67 | wallet_sk.get_g1()
68 | )
69 | return wallet_puzzle.get_tree_hash()
70 |
71 |
72 | async def get_singleton_wallet(fingerprint: int) -> Tuple[PrivateKey, int]:
73 | try:
74 | wallet_client: WalletRpcClient = await get_client()
75 | wallet_client_f, fingerprint = await get_wallet(wallet_client, fingerprint)
76 |
77 | private_key = await wallet_client.get_private_key(fingerprint)
78 | master_sk = PrivateKey.from_bytes(bytes.fromhex(private_key["sk"]))
79 | singleton_sk = master_sk_to_singleton_owner_sk(master_sk, uint32(0))
80 |
81 | return singleton_sk, fingerprint
82 | finally:
83 | wallet_client.close()
84 | await wallet_client.await_closed()
85 |
86 |
87 | async def create_genesis_coin(
88 | fingerprint, amt, fee
89 | ) -> [TransactionRecord, PrivateKey, bytes32]:
90 | try:
91 | wallet_client: WalletRpcClient = await get_client()
92 | wallet_client_f, fingerprint = await get_wallet(wallet_client, fingerprint)
93 |
94 | private_key = await wallet_client.get_private_key(fingerprint)
95 | master_sk = PrivateKey.from_bytes(bytes.fromhex(private_key["sk"]))
96 | singleton_sk = master_sk_to_singleton_owner_sk(master_sk, uint32(0))
97 |
98 | singleton_wallet_puzhash = p2_delegated_puzzle_or_hidden_puzzle.puzzle_for_pk(
99 | singleton_sk.get_g1()
100 | ).get_tree_hash()
101 |
102 | signed_tx = await wallet_client.create_signed_transaction(
103 | [{"puzzle_hash": singleton_wallet_puzhash, "amount": amt}], fee=fee
104 | )
105 | return signed_tx, singleton_sk, master_sk_to_wallet_puzhash(master_sk)
106 | finally:
107 | wallet_client.close()
108 | await wallet_client.await_closed()
109 |
110 |
111 | async def create_p2_singleton_coin(
112 | fingerprint: Optional[int], launcher_id: str, amt: int, fee: int
113 | ) -> [TransactionRecord, Program, PrivateKey, bytes32]:
114 | try:
115 | wallet_client: WalletRpcClient = await get_client()
116 | wallet_client_f, fingerprint = await get_wallet(wallet_client, fingerprint)
117 |
118 | private_key = await wallet_client.get_private_key(fingerprint)
119 | master_sk = PrivateKey.from_bytes(bytes.fromhex(private_key["sk"]))
120 | singleton_sk = master_sk_to_singleton_owner_sk(master_sk, uint32(0))
121 |
122 | dummy_p2_singleton_puzzle = pay_to_singleton_puzzle(launcher_id, (b"0" * 32))
123 |
124 | signed_tx = await wallet_client.create_signed_transaction(
125 | [{"puzzle_hash": dummy_p2_singleton_puzzle.get_tree_hash(), "amount": amt}],
126 | fee=fee,
127 | )
128 | spent_coin = signed_tx.removals[0]
129 |
130 | p2_singleton_puzzle = pay_to_singleton_puzzle(
131 | bytes.fromhex(launcher_id), spent_coin.puzzle_hash
132 | )
133 | signed_tx = await wallet_client.create_signed_transaction(
134 | [{"puzzle_hash": p2_singleton_puzzle.get_tree_hash(), "amount": amt}],
135 | fee=fee,
136 | coins=signed_tx.removals,
137 | )
138 |
139 | return (
140 | signed_tx,
141 | p2_singleton_puzzle,
142 | singleton_sk,
143 | master_sk_to_wallet_puzhash(master_sk),
144 | )
145 | finally:
146 | wallet_client.close()
147 | await wallet_client.await_closed()
148 |
149 |
150 | async def sign_offer(
151 | fingerprint: Optional[int], price: int, singleton_id: str
152 | ) -> [TransactionRecord, Program, PrivateKey]:
153 | try:
154 | wallet_client: WalletRpcClient = await get_client()
155 | wallet_client_f, fingerprint = await get_wallet(wallet_client, fingerprint)
156 |
157 | private_key = await wallet_client.get_private_key(fingerprint)
158 | master_sk = PrivateKey.from_bytes(bytes.fromhex(private_key["sk"]))
159 | singleton_sk = master_sk_to_singleton_owner_sk(master_sk, uint32(0))
160 |
161 | return AugSchemeMPL.sign(
162 | singleton_sk,
163 | int_to_bytes(price)
164 | + bytes.fromhex(singleton_id)
165 | + AGG_SIG_ME_ADDITIONAL_DATA_TESTNET10,
166 | )
167 | finally:
168 | wallet_client.close()
169 | await wallet_client.await_closed()
170 |
171 |
172 | @click.group()
173 | def cli():
174 | pass
175 |
176 |
177 | @cli.command()
178 | @click.option("--fingerprint", type=int, help="The fingerprint of the key to use")
179 | def profile(fingerprint: int):
180 | singleton_sk: PrivateKey
181 | singleton_sk, _ = asyncio.get_event_loop().run_until_complete(
182 | get_singleton_wallet(fingerprint)
183 | )
184 |
185 | click.echo(
186 | f"Your singleton profile is {SINGLETON_GALLERY_FRONTEND}/profile/{bytes(singleton_sk.get_g1()).hex()}"
187 | )
188 |
189 |
190 | @cli.command()
191 | @click.option("--name", prompt=True, help="Your profile name")
192 | @click.option("--fingerprint", type=int, help="The fingerprint of the key to use")
193 | def update_profile(name: str, fingerprint: int):
194 | singleton_sk: PrivateKey
195 | singleton_sk, _ = asyncio.get_event_loop().run_until_complete(
196 | get_singleton_wallet(fingerprint)
197 | )
198 |
199 | public_key = singleton_sk.get_g1()
200 | signature = AugSchemeMPL.sign(
201 | singleton_sk,
202 | bytes(public_key) + bytes(name, "utf-8"),
203 | )
204 |
205 | if click.confirm(f"Do you want to set your profile name to {name}?"):
206 | response = requests.patch(
207 | f"{SINGLETON_GALLERY_API}/profile/{public_key}",
208 | json={"signature": bytes(signature).hex(), "name": name},
209 | )
210 | if response.status_code != 200:
211 | click.secho("Failed to update profile:", err=True, fg="red")
212 | click.secho(response.text, err=True, fg="red")
213 | else:
214 | click.secho("Your profile has been updated!", fg="green")
215 | click.echo(
216 | f"You can inspect it using the following link: {SINGLETON_GALLERY_FRONTEND}/profile/{public_key}"
217 | )
218 |
219 |
220 | @cli.command()
221 | @click.option("--name", prompt=True, help="The name of the NFT")
222 | @click.option("--uri", prompt=True, help="The uri of the main NFT image")
223 | @click.option(
224 | "-r",
225 | "--royalty",
226 | "royalty_percentage",
227 | type=INT,
228 | prompt=True,
229 | help="The percentage of each sale you want to receive as royalty.",
230 | default=0,
231 | show_default=True,
232 | )
233 | @click.option("--fingerprint", type=int, help="The fingerprint of the key to use")
234 | @click.option(
235 | "--fee",
236 | type=FLOAT,
237 | required=True,
238 | default=0,
239 | show_default=True,
240 | help="The XCH fee to use for this transaction",
241 | )
242 | def create(name: str, uri: str, fingerprint: int, royalty_percentage: int, fee: int):
243 | if royalty_percentage > 99 or royalty_percentage < 0:
244 | click.secho(
245 | f"Royalty percentage has to be between 1 and 99.", err=True, fg="red"
246 | )
247 | return
248 |
249 | signed_tx: TransactionRecord
250 | owner_sk: PrivateKey
251 | wallet_puzzle_hash: bytes32
252 | (
253 | signed_tx,
254 | owner_sk,
255 | wallet_puzzle_hash,
256 | ) = asyncio.get_event_loop().run_until_complete(
257 | create_genesis_coin(fingerprint, SINGLETON_AMOUNT, fee)
258 | )
259 | genesis_coin: Coin = next(
260 | coin for coin in signed_tx.additions if coin.amount == SINGLETON_AMOUNT
261 | )
262 | genesis_puzzle = p2_delegated_puzzle_or_hidden_puzzle.puzzle_for_pk(
263 | owner_sk.get_g1()
264 | )
265 | creator = Owner(owner_sk.get_g1(), wallet_puzzle_hash)
266 | royalty = (
267 | Royalty(creator.puzzle_hash, royalty_percentage)
268 | if royalty_percentage > 0
269 | else None
270 | )
271 |
272 | coin_spends, delegated_puzzle = create_unsigned_ownable_singleton(
273 | genesis_coin, genesis_puzzle, creator, uri, name, version=2, royalty=royalty
274 | )
275 |
276 | synthetic_secret_key: PrivateKey = (
277 | p2_delegated_puzzle_or_hidden_puzzle.calculate_synthetic_secret_key(
278 | owner_sk,
279 | p2_delegated_puzzle_or_hidden_puzzle.DEFAULT_HIDDEN_PUZZLE_HASH,
280 | )
281 | )
282 | signature = AugSchemeMPL.sign(
283 | synthetic_secret_key,
284 | (
285 | delegated_puzzle.get_tree_hash()
286 | + genesis_coin.name()
287 | + AGG_SIG_ME_ADDITIONAL_DATA_TESTNET10
288 | ),
289 | )
290 |
291 | combined_spend_bundle: SpendBundle = SpendBundle.aggregate(
292 | [signed_tx.spend_bundle, SpendBundle(coin_spends, signature)]
293 | )
294 |
295 | if click.confirm("The transaction seems valid. Do you want to submit it?"):
296 | response = requests.post(
297 | f"{SINGLETON_GALLERY_API}/singletons/submit",
298 | json=combined_spend_bundle.to_json_dict(
299 | include_legacy_keys=False, exclude_modern_keys=False
300 | ),
301 | )
302 | if response.status_code != 200:
303 | click.secho("Failed to submit NFT:", err=True, fg="red")
304 | click.secho(response.text, err=True, fg="red")
305 | else:
306 | launcher_coin_record = next(
307 | coin
308 | for coin in combined_spend_bundle.coin_spends
309 | if coin.coin.puzzle_hash == SINGLETON_LAUNCHER_HASH
310 | )
311 | click.secho("Your NFT has been submitted successfully!", fg="green")
312 | click.echo(
313 | "Please wait a few minutes until the NFT has been added to the blockchain."
314 | )
315 | click.echo(
316 | f"You can inspect your NFT using the following link: {SINGLETON_GALLERY_FRONTEND}/singletons/{launcher_coin_record.coin.name()}?pending=1"
317 | )
318 |
319 |
320 | @cli.command()
321 | @click.option("--launcher-id", prompt=True, help="The ID of the NFT")
322 | @click.option(
323 | "--price",
324 | type=float,
325 | prompt=True,
326 | help="The price (in XCH) you want to offer for this NFT singleton",
327 | )
328 | @click.option("--fingerprint", type=int, help="The fingerprint of the key to use")
329 | @click.option(
330 | "--fee",
331 | required=True,
332 | default=0,
333 | show_default=True,
334 | help="The XCH fee to use for this transaction",
335 | )
336 | def offer(launcher_id: str, price: float, fingerprint: Optional[int], fee: int):
337 | response = requests.get(f"{SINGLETON_GALLERY_API}/singletons/{launcher_id}")
338 | if response.status_code != 200:
339 | click.secho(
340 | f"Could not find an NFT with ID '{launcher_id}'", err=True, fg="red"
341 | )
342 | return
343 | singleton = response.json()
344 | name = singleton["name"]
345 | owner = singleton["owner"]
346 |
347 | price_in_mojo = int(price * units["chia"])
348 |
349 | try:
350 | signed_tx: TransactionRecord
351 | p2_singleton_puzzle: Program
352 | owner_sk: PrivateKey
353 | wallet_puzzle_hash: bytes32
354 | (
355 | signed_tx,
356 | p2_singleton_puzzle,
357 | owner_sk,
358 | wallet_puzzle_hash,
359 | ) = asyncio.get_event_loop().run_until_complete(
360 | create_p2_singleton_coin(fingerprint, launcher_id, price_in_mojo, fee)
361 | )
362 | p2_singleton_coin: Coin = next(
363 | coin
364 | for coin in signed_tx.additions
365 | if coin.puzzle_hash == p2_singleton_puzzle.get_tree_hash()
366 | )
367 | except TypeError:
368 | return
369 |
370 | new_owner_pubkey = owner_sk.get_g1()
371 | if owner == bytes(new_owner_pubkey).hex():
372 | click.secho(
373 | "This is your singleton, you can't create an offer for it.", fg="yellow"
374 | )
375 | return
376 |
377 | singleton_signature = AugSchemeMPL.sign(
378 | owner_sk,
379 | wallet_puzzle_hash
380 | + bytes.fromhex(singleton["singleton_id"])
381 | + AGG_SIG_ME_ADDITIONAL_DATA_TESTNET10,
382 | )
383 | payment_spend_bundle = SpendBundle.aggregate(
384 | [signed_tx.spend_bundle, SpendBundle([], singleton_signature)]
385 | )
386 |
387 | if click.confirm(
388 | f"You are offering {price} XCH for '{name}'. Do you want to submit it?"
389 | ):
390 | response = requests.post(
391 | f"{SINGLETON_GALLERY_API}/singletons/{launcher_id}/offers/submit",
392 | json={
393 | "payment_spend_bundle": payment_spend_bundle.to_json_dict(
394 | include_legacy_keys=False, exclude_modern_keys=False
395 | ),
396 | "p2_singleton_coin": p2_singleton_coin.to_json_dict(),
397 | "p2_singleton_puzzle": bytes(p2_singleton_puzzle).hex(),
398 | "new_owner_pubkey": bytes(new_owner_pubkey).hex(),
399 | "new_owner_puzhash": wallet_puzzle_hash.hex(),
400 | "price": price_in_mojo,
401 | },
402 | )
403 | if response.status_code != 200:
404 | click.secho("Failed to submit offer:", err=True, fg="red")
405 | click.secho(response.text, err=True, fg="red")
406 | else:
407 | click.secho("Your offer has been submitted successfully!", fg="green")
408 | click.echo(
409 | f"You can inspect it using the following link: {SINGLETON_GALLERY_FRONTEND}/singletons/{launcher_id}"
410 | )
411 |
412 |
413 | @cli.command()
414 | @click.option("--launcher-id", prompt=True, help="The ID of the NFT")
415 | @click.option("--offer-id", prompt=True, help="The ID of the offer you want to accept")
416 | @click.option("--fingerprint", type=int, help="The fingerprint of the key to use")
417 | def accept_offer(launcher_id: str, offer_id: str, fingerprint: Optional[int]):
418 | singleton_response = requests.get(
419 | f"{SINGLETON_GALLERY_API}/singletons/{launcher_id}"
420 | )
421 | if singleton_response.status_code != 200:
422 | click.secho(
423 | f"Could not find an NFT with ID '{launcher_id}'", err=True, fg="red"
424 | )
425 | return
426 | name = singleton_response.json()["name"]
427 | royalty_percentage = singleton_response.json()["royalty_percentage"]
428 |
429 | offer_response = requests.get(
430 | f"{SINGLETON_GALLERY_API}/singletons/{launcher_id}/offers/{offer_id}"
431 | )
432 | if offer_response.status_code != 200:
433 | click.secho(
434 | f"Could not find an offer with ID '{offer_id}' for NFT '{name}'.",
435 | err=True,
436 | fg="yellow",
437 | )
438 | return
439 | offer = offer_response.json()
440 | price = offer["price"]
441 | price_in_chia = price / units["chia"]
442 |
443 | price_signature: G2Element = asyncio.get_event_loop().run_until_complete(
444 | sign_offer(fingerprint, price, offer["singleton_id"])
445 | )
446 |
447 | royalty_text = (
448 | f" A share of {royalty_percentage}% of that price is sent to its creator."
449 | if royalty_percentage > 0
450 | else ""
451 | )
452 | if click.confirm(
453 | f"You are accepting {price_in_chia} XCH for '{name}'.{royalty_text} Do you want to submit it?"
454 | ):
455 | response = requests.post(
456 | f"{SINGLETON_GALLERY_API}/singletons/{launcher_id}/offers/{offer_id}/accept",
457 | json={
458 | "price_signature": bytes(price_signature).hex(),
459 | },
460 | )
461 | if response.status_code != 200:
462 | click.secho("Failed to accept offer:", err=True, fg="red")
463 | click.secho(response.text, err=True, fg="red")
464 | else:
465 | click.secho("You accepted the offer!", fg="green")
466 | click.echo(f"The payment is being sent to your wallet address.")
467 |
468 |
469 | @cli.command()
470 | @click.option("--launcher-id", prompt=True, help="The ID of the NFT")
471 | @click.option("--offer-id", prompt=True, help="The ID of the offer you want to cancel")
472 | @click.option("--fingerprint", type=int, help="The fingerprint of the key to use")
473 | def cancel_offer(launcher_id: str, offer_id: str, fingerprint: Optional[int]):
474 | singleton_response = requests.get(
475 | f"{SINGLETON_GALLERY_API}/singletons/{launcher_id}"
476 | )
477 | if singleton_response.status_code != 200:
478 | click.secho(
479 | f"Could not find an NFT with ID '{launcher_id}'", err=True, fg="red"
480 | )
481 | return
482 | name = singleton_response.json()["name"]
483 |
484 | offer_response = requests.get(
485 | f"{SINGLETON_GALLERY_API}/singletons/{launcher_id}/offers/{offer_id}"
486 | )
487 | if offer_response.status_code != 200:
488 | click.secho(
489 | f"Could not find an offer with ID '{offer_id}' for NFT '{name}'.",
490 | err=True,
491 | fg="yellow",
492 | )
493 | return
494 | offer = offer_response.json()
495 | price = offer["price"]
496 | price_in_chia = price / units["chia"]
497 |
498 | singleton_sk: PrivateKey
499 | (singleton_sk, fingerprint) = asyncio.get_event_loop().run_until_complete(
500 | get_singleton_wallet(fingerprint)
501 | )
502 | if offer["new_owner_public_key"] != bytes(singleton_sk.get_g1()).hex():
503 | click.secho(f"This is not your offer.", err=True, fg="red")
504 | return
505 |
506 | price_signature: G2Element = asyncio.get_event_loop().run_until_complete(
507 | sign_offer(fingerprint, price, offer["singleton_id"])
508 | )
509 |
510 | if click.confirm(
511 | f"Do you want to cancel your offer of {price_in_chia} XCH for '{name}'?"
512 | ):
513 | response = requests.delete(
514 | f"{SINGLETON_GALLERY_API}/singletons/{launcher_id}/offers/{offer_id}",
515 | json={
516 | "price_signature": bytes(price_signature).hex(),
517 | },
518 | )
519 | if response.status_code != 200:
520 | click.secho("Failed to cancel offer:", err=True, fg="red")
521 | click.secho(response.text, err=True, fg="red")
522 | else:
523 | click.secho("You cancelled the offer.", fg="green")
524 |
525 |
526 | if __name__ == "__main__":
527 | cli()
528 |
--------------------------------------------------------------------------------