Skip to content

Commit cfb9f3e

Browse files
Source code for consolidated NFT tutorial application (#17)
* First draft of new tutorial NFT contract * First draft of application after parts 2 and 3 * Rename contract as pre-deployed to separate from the one the tutorial user creates * Add description * WIP part 5: Getting token info * Got this working * Not sure whether to use await or not for these * Change path to create-nfts * Complete contract * README
1 parent a82337a commit cfb9f3e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+15271
-1
lines changed

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,6 @@ dist
174174
# SvelteKit build / generate output
175175
.svelte-kit
176176

177-
# End of https://www.toptal.com/developers/gitignore/api/node,macos
177+
# End of https://www.toptal.com/developers/gitignore/api/node,macos
178+
179+
nft-consolidated/contract/fa2_nft

create-nfts/README.md

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Tutorial application: Create NFTs from a web application
2+
3+
This is the completed application from the tutorial [Create NFTs from a web application](https://docs.tezos.com/tutorials/create-nfts).
4+
5+
Follow these steps to run it:
6+
7+
1. Install a Tezos wallet.
8+
1. Clone this repository.
9+
1. Go into the directory for the part of the tutorial that you're working on.
10+
1. Run `npm install` to install the application's dependencies.
11+
1. Run `npm run dev` to start the application.
12+
1. Open a web browser to http://localhost:4000 to see the running application.

create-nfts/contract/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fa2_lib_nft
+263
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
import smartpy as sp
2+
from smartpy.templates import fa2_lib as fa2
3+
4+
# Main template for FA2 contracts
5+
main = fa2.main
6+
7+
8+
@sp.module
9+
def 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+
# Create token metadata
45+
# Adapted from fa2.make_metadata
46+
def create_metadata(symbol, name, decimals, displayUri, artifactUri, description, thumbnailUri):
47+
return sp.map(
48+
l={
49+
"name": sp.scenario_utils.bytes_of_string(name),
50+
"decimals": sp.scenario_utils.bytes_of_string("%d" % decimals),
51+
"symbol": sp.scenario_utils.bytes_of_string(symbol),
52+
"displayUri": sp.scenario_utils.bytes_of_string(displayUri),
53+
"artifactUri": sp.scenario_utils.bytes_of_string(artifactUri),
54+
"description": sp.scenario_utils.bytes_of_string(description),
55+
"thumbnailUri": sp.scenario_utils.bytes_of_string(thumbnailUri),
56+
}
57+
)
58+
59+
def _get_balance(fa2_contract, args):
60+
"""Utility function to call the contract's get_balance view to get an account's token balance."""
61+
return sp.View(fa2_contract, "get_balance")(args)
62+
63+
64+
def _total_supply(fa2_contract, args):
65+
"""Utility function to call the contract's total_supply view to get the total amount of tokens."""
66+
return sp.View(fa2_contract, "total_supply")(args)
67+
68+
69+
@sp.add_test()
70+
def test():
71+
# Create and configure the test scenario
72+
# Import the types from the FA2 library, the library itself, and the contract module, in that order.
73+
scenario = sp.test_scenario("fa2_lib_nft", my_module)
74+
scenario.h1("FA2 NFT contract test")
75+
76+
# Define test accounts
77+
# admin = sp.record(
78+
# address=sp.address("tz1QCVQinE8iVj1H2fckqx6oiM85CNJSK9Sx"),
79+
# public_key_hash=sp.key_hash("tz1QCVQinE8iVj1H2fckqx6oiM85CNJSK9Sx"),
80+
# public_key=sp.key("edpktnVRg5YeqR6yWXUFuDVTLMzDQrre1QD3FXmYxRzTVinSsJLpnk"),
81+
# private_key="edskRypCD2G7B1ym3MWuJig8LvpmG32soDsmXVSs7QzZKAH7ehWfZx5buxZ8vaHP2FHqu7yj2jrdUQyBd72EaTJ5iDTbDD9bvJ"
82+
# )
83+
admin = sp.address("tz1QCVQinE8iVj1H2fckqx6oiM85CNJSK9Sx")
84+
alice = sp.test_account("Alice")
85+
bob = sp.test_account("Bob")
86+
87+
# Precreated image on IPFS
88+
token_thumb_uri = "https://gateway.pinata.cloud/ipfs/QmRCp4Qc8afPrEqtM1YdRvNagWCsFGXHgGjbBYrmNsBkcE"
89+
90+
# Define initial token metadata and ownership
91+
tok0_md = create_metadata(
92+
"Tok0",
93+
"Token Zero",
94+
0,
95+
token_thumb_uri,
96+
token_thumb_uri,
97+
"My first token",
98+
token_thumb_uri,
99+
)
100+
tok1_md = create_metadata(
101+
"Tok1",
102+
"Token One",
103+
0,
104+
token_thumb_uri,
105+
token_thumb_uri,
106+
"My second token",
107+
token_thumb_uri,
108+
)
109+
tok2_md = create_metadata(
110+
"Tok2",
111+
"Token Two",
112+
0,
113+
token_thumb_uri,
114+
token_thumb_uri,
115+
"My third token",
116+
token_thumb_uri,
117+
)
118+
token_metadata = [tok0_md, tok1_md, tok2_md]
119+
ledger = {0: alice.address, 1: alice.address, 2: bob.address}
120+
121+
# Instantiate the FA2 NFT contract
122+
contract = my_module.MyNFTContract(
123+
admin, sp.big_map(), ledger, token_metadata
124+
)
125+
126+
# Build contract metadata content
127+
contract_metadata = sp.create_tzip16_metadata(
128+
name="My FA2 NFT contract",
129+
description="This is an FA2 NFT contract using SmartPy.",
130+
version="1.0.0",
131+
license_name="CC-BY-SA",
132+
license_details="Creative Commons Attribution Share Alike license 4.0 https://creativecommons.org/licenses/by/4.0/",
133+
interfaces=["TZIP-012", "TZIP-016"],
134+
authors=["SmartPy <https://smartpy.io/#contact>"],
135+
homepage="https://smartpy.io/ide?template=fa2_lib_nft.py",
136+
# Optionally, upload the source code to IPFS and add the URI here
137+
source_uri=None,
138+
offchain_views=contract.get_offchain_views(),
139+
)
140+
141+
# Add the info specific to FA2 permissions
142+
contract_metadata["permissions"] = {
143+
# The operator policy chosen:
144+
# owner-or-operator-transfer is the default.
145+
"operator": "owner-or-operator-transfer",
146+
# Those two options should always have these values.
147+
# It means that the contract doesn't use the hook mechanism.
148+
"receiver": "owner-no-hook",
149+
"sender": "owner-no-hook",
150+
}
151+
152+
# You must upload the contract metadata to IPFS and get its URI.
153+
# You can write the contract_metadata object to a JSON file with json.dumps() and upload it manually.
154+
# You can also use sp.pin_on_ipfs() to upload the object via pinata.cloud and get the IPFS URI:
155+
# metadata_uri = sp.pin_on_ipfs(contract_metadata, api_key=None, secret_key=None, name = "Metadata for my FA2 contract")
156+
157+
# This is a placeholder value. In production, replace it with your metadata URI.
158+
metadata_uri = "ipfs://example"
159+
160+
# Create the metadata big map based on the IPFS URI
161+
contract_metadata = sp.scenario_utils.metadata_of_url(metadata_uri)
162+
163+
# Update the scenario instance with the new metadata
164+
contract.data.metadata = contract_metadata
165+
166+
# Originate the contract in the test scenario
167+
scenario += contract
168+
169+
if scenario.simulation_mode() is sp.SimulationMode.MOCKUP:
170+
scenario.p("mockups - fix transfer based testing")
171+
return
172+
173+
# Run tests
174+
175+
scenario.h2("Verify the initial owners of the tokens")
176+
scenario.verify(
177+
_get_balance(contract, sp.record(owner=alice.address, token_id=0)) == 1
178+
)
179+
scenario.verify(
180+
_get_balance(contract, sp.record(owner=bob.address, token_id=0)) == 0
181+
)
182+
scenario.verify(
183+
_get_balance(contract, sp.record(owner=alice.address, token_id=1)) == 1
184+
)
185+
scenario.verify(
186+
_get_balance(contract, sp.record(owner=bob.address, token_id=1)) == 0
187+
)
188+
scenario.verify(
189+
_get_balance(contract, sp.record(owner=alice.address, token_id=2)) == 0
190+
)
191+
scenario.verify(
192+
_get_balance(contract, sp.record(owner=bob.address, token_id=2)) == 1
193+
)
194+
195+
# Verify the token supply
196+
scenario.verify(_total_supply(contract, sp.record(token_id=0)) == 1)
197+
scenario.verify(_total_supply(contract, sp.record(token_id=1)) == 1)
198+
scenario.verify(_total_supply(contract, sp.record(token_id=2)) == 1)
199+
200+
scenario.h2("Transfer a token")
201+
contract.transfer(
202+
[
203+
sp.record(
204+
from_=alice.address,
205+
txs=[sp.record(to_=bob.address, amount=1, token_id=0)],
206+
),
207+
],
208+
_sender=alice,
209+
)
210+
# Verify the result
211+
scenario.verify(
212+
_get_balance(contract, sp.record(owner=alice.address, token_id=0)) == 0
213+
)
214+
scenario.verify(
215+
_get_balance(contract, sp.record(owner=bob.address, token_id=0)) == 1
216+
)
217+
# Transfer it back
218+
contract.transfer(
219+
[
220+
sp.record(
221+
from_=bob.address,
222+
txs=[sp.record(to_=alice.address, amount=1, token_id=0)],
223+
),
224+
],
225+
_sender=bob,
226+
)
227+
228+
scenario.h2("Mint a token")
229+
nft3_md = fa2.make_metadata(name="Token Three", decimals=1, symbol="Tok3")
230+
# Verify that only the admin can mint a token
231+
contract.mint(
232+
[
233+
sp.record(metadata=nft3_md, to_=bob.address),
234+
],
235+
_sender=bob,
236+
_valid=False,
237+
)
238+
# Mint a token
239+
contract.mint(
240+
[
241+
sp.record(metadata=nft3_md, to_=bob.address),
242+
],
243+
_sender=admin,
244+
)
245+
# Verify the result
246+
scenario.verify(_total_supply(contract, sp.record(token_id=3)) == 1)
247+
scenario.verify(
248+
_get_balance(contract, sp.record(owner=alice.address, token_id=3)) == 0
249+
)
250+
scenario.verify(
251+
_get_balance(contract, sp.record(owner=bob.address, token_id=3)) == 1
252+
)
253+
254+
scenario.h2("Burn a token")
255+
# Verify that you can't burn someone else's token
256+
contract.burn(
257+
[sp.record(token_id=3, from_=bob.address, amount=1)],
258+
_sender=alice,
259+
_valid=False,
260+
)
261+
262+
# Verify that you can burn your own token
263+
contract.burn([sp.record(token_id=3, from_=bob.address, amount=1)], _sender=bob)

0 commit comments

Comments
 (0)