Monorepo for Aesthetic.Computer
aesthetic.computer
1import smartpy as sp
2from smartpy.templates import fa2_lib as fa2
3
4# Main template for FA2 contracts
5main = fa2.main
6
7
8@sp.module
9def my_module():
10 import main
11
12 # Order of inheritance: [Admin], [<policy>], <base class>, [<other mixins>].
13 class MyNFTContract(
14 main.Admin,
15 main.Nft,
16 main.MintNft,
17 main.BurnNft,
18 main.OnchainviewBalanceOf,
19 ):
20 def __init__(self, admin_address, contract_metadata, ledger, token_metadata):
21 """Initializes the contract with administrative permissions and NFT functionalities.
22 The base class is required; all mixins are optional.
23 The initialization must follow this order:
24
25 - Other mixins such as OnchainviewBalanceOf, MintNFT, and BurnNFT
26 - Base class: NFT
27 - Transfer policy
28 - Admin
29 """
30
31 # Initialize on-chain balance view
32 main.OnchainviewBalanceOf.__init__(self)
33
34 # Initialize the NFT-specific entrypoints
35 main.BurnNft.__init__(self)
36 main.MintNft.__init__(self)
37
38 # Initialize the NFT base class
39 main.Nft.__init__(self, contract_metadata, ledger, token_metadata)
40
41 # Initialize administrative permissions
42 main.Admin.__init__(self, admin_address)
43
44
45def _get_balance(fa2_contract, args):
46 """Utility function to call the contract's get_balance view to get an account's token balance."""
47 return sp.View(fa2_contract, "get_balance")(args)
48
49
50def _total_supply(fa2_contract, args):
51 """Utility function to call the contract's total_supply view to get the total amount of tokens."""
52 return sp.View(fa2_contract, "total_supply")(args)
53
54
55@sp.add_test()
56def test():
57 # Create and configure the test scenario
58 # Import the types from the FA2 library, the library itself, and the contract module, in that order.
59 scenario = sp.test_scenario("fa2_lib_nft")
60 scenario.h1("FA2 NFT contract test")
61
62 # Define test accounts
63 admin = sp.test_account("Admin")
64 alice = sp.test_account("Alice")
65 bob = sp.test_account("Bob")
66
67 # Define initial token metadata and ownership
68 tok0_md = fa2.make_metadata(name="Token Zero", decimals=1, symbol="Tok0")
69 tok1_md = fa2.make_metadata(name="Token One", decimals=1, symbol="Tok1")
70 tok2_md = fa2.make_metadata(name="Token Two", decimals=1, symbol="Tok2")
71 token_metadata = [tok0_md, tok1_md, tok2_md]
72 ledger = {0: alice.address, 1: alice.address, 2: bob.address}
73
74 # Instantiate the FA2 NFT contract
75 contract = my_module.MyNFTContract(
76 admin.address, sp.big_map(), ledger, token_metadata
77 )
78
79 # Build contract metadata content
80 contract_metadata = sp.create_tzip16_metadata(
81 name="My FA2 NFT contract",
82 description="This is an FA2 NFT contract using SmartPy.",
83 version="1.0.0",
84 license_name="CC-BY-SA",
85 license_details="Creative Commons Attribution Share Alike license 4.0 https://creativecommons.org/licenses/by/4.0/",
86 interfaces=["TZIP-012", "TZIP-016"],
87 authors=["SmartPy <https://smartpy.io/#contact>"],
88 homepage="https://smartpy.io/ide?template=fa2_lib_nft.py",
89 # Optionally, upload the source code to IPFS and add the URI here
90 source_uri=None,
91 offchain_views=contract.get_offchain_views(),
92 )
93
94 # Add the info specific to FA2 permissions
95 contract_metadata["permissions"] = {
96 # The operator policy chosen:
97 # owner-or-operator-transfer is the default.
98 "operator": "owner-or-operator-transfer",
99 # Those two options should always have these values.
100 # It means that the contract doesn't use the hook mechanism.
101 "receiver": "owner-no-hook",
102 "sender": "owner-no-hook",
103 }
104
105 # You must upload the contract metadata to IPFS and get its URI.
106 # You can write the contract_metadata object to a JSON file with json.dumps() and upload it manually.
107 # You can also use sp.pin_on_ipfs() to upload the object via pinata.cloud and get the IPFS URI:
108 # metadata_uri = sp.pin_on_ipfs(contract_metadata, api_key=None, secret_key=None, name = "Metadata for my FA2 contract")
109
110 # This is a placeholder value. In production, replace it with your metadata URI.
111 metadata_uri = "ipfs://example"
112
113 # Create the metadata big map based on the IPFS URI
114 contract_metadata = sp.scenario_utils.metadata_of_url(metadata_uri)
115
116 # Update the scenario instance with the new metadata
117 contract.data.metadata = contract_metadata
118
119 # Originate the contract in the test scenario
120 scenario += contract
121
122 if scenario.simulation_mode() is sp.SimulationMode.MOCKUP:
123 scenario.p("mockups - fix transfer based testing")
124 return
125
126 # Run tests
127
128 scenario.h2("Verify the initial owners of the tokens")
129 scenario.verify(
130 _get_balance(contract, sp.record(owner=alice.address, token_id=0)) == 1
131 )
132 scenario.verify(
133 _get_balance(contract, sp.record(owner=bob.address, token_id=0)) == 0
134 )
135 scenario.verify(
136 _get_balance(contract, sp.record(owner=alice.address, token_id=1)) == 1
137 )
138 scenario.verify(
139 _get_balance(contract, sp.record(owner=bob.address, token_id=1)) == 0
140 )
141 scenario.verify(
142 _get_balance(contract, sp.record(owner=alice.address, token_id=2)) == 0
143 )
144 scenario.verify(
145 _get_balance(contract, sp.record(owner=bob.address, token_id=2)) == 1
146 )
147
148 # Verify the token supply
149 scenario.verify(_total_supply(contract, sp.record(token_id=0)) == 1)
150 scenario.verify(_total_supply(contract, sp.record(token_id=1)) == 1)
151 scenario.verify(_total_supply(contract, sp.record(token_id=2)) == 1)
152
153 scenario.h2("Transfer a token")
154 contract.transfer(
155 [
156 sp.record(
157 from_=alice.address,
158 txs=[sp.record(to_=bob.address, amount=1, token_id=0)],
159 ),
160 ],
161 _sender=alice,
162 )
163 # Verify the result
164 scenario.verify(
165 _get_balance(contract, sp.record(owner=alice.address, token_id=0)) == 0
166 )
167 scenario.verify(
168 _get_balance(contract, sp.record(owner=bob.address, token_id=0)) == 1
169 )
170 # Transfer it back
171 contract.transfer(
172 [
173 sp.record(
174 from_=bob.address,
175 txs=[sp.record(to_=alice.address, amount=1, token_id=0)],
176 ),
177 ],
178 _sender=bob,
179 )
180
181 scenario.h2("Mint a token")
182 nft3_md = fa2.make_metadata(name="Token Three", decimals=1, symbol="Tok3")
183 # Verify that only the admin can mint a token
184 contract.mint(
185 [
186 sp.record(metadata=nft3_md, to_=bob.address),
187 ],
188 _sender=bob,
189 _valid=False,
190 )
191 # Mint a token
192 contract.mint(
193 [
194 sp.record(metadata=nft3_md, to_=bob.address),
195 ],
196 _sender=admin,
197 )
198 # Verify the result
199 scenario.verify(_total_supply(contract, sp.record(token_id=3)) == 1)
200 scenario.verify(
201 _get_balance(contract, sp.record(owner=alice.address, token_id=3)) == 0
202 )
203 scenario.verify(
204 _get_balance(contract, sp.record(owner=bob.address, token_id=3)) == 1
205 )
206
207 scenario.h2("Burn a token")
208 # Verify that you can't burn someone else's token
209 contract.burn(
210 [sp.record(token_id=3, from_=bob.address, amount=1)],
211 _sender=alice,
212 _valid=False,
213 )
214
215 # Verify that you can burn your own token
216 contract.burn([sp.record(token_id=3, from_=bob.address, amount=1)], _sender=bob)