├── .gitattributes ├── workshop_slides ├── Introduction to Sui Move.pdf └── Intro to Sui Objects Workshop.pdf ├── Move.toml ├── sources ├── test.move ├── sui_dinos.move_basic ├── sui_dinos.move_childobject └── sui_dinos.move ├── .gitignore ├── LICENSE ├── demo-commands.md ├── README.md └── breakout_session_solutions ├── challenge_1_solution.move ├── challenge_2_solution.move └── combined_solution.move /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /workshop_slides/Introduction to Sui Move.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyd628/sui_intro_workshop/HEAD/workshop_slides/Introduction to Sui Move.pdf -------------------------------------------------------------------------------- /workshop_slides/Intro to Sui Objects Workshop.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyd628/sui_intro_workshop/HEAD/workshop_slides/Intro to Sui Objects Workshop.pdf -------------------------------------------------------------------------------- /Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sui_intro_workshop" 3 | version = "0.0.1" 4 | 5 | [dependencies] 6 | Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework", rev = "devnet" } 7 | 8 | [addresses] 9 | sui_intro_workshop = "0x0" 10 | sui = "0x2" 11 | -------------------------------------------------------------------------------- /sources/test.move: -------------------------------------------------------------------------------- 1 | module sui_intro_workshop::haha { 2 | public fun plus(a: u32): u32 { 3 | a + 1 4 | } 5 | } 6 | 7 | module sui_intro_workshop::hahatest { 8 | use sui_intro_workshop::haha; 9 | #[test] 10 | fun test_neg_111() { 11 | //assert!() 12 | haha::plus(1u32); 13 | } 14 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | build/ 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Henry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo-commands.md: -------------------------------------------------------------------------------- 1 | ## Demo Sui CLI commands 2 | 3 | ### Basic Version: 4 | 5 | sui client publish --path /Users/henryworkmac/Documents/workspace/sui_intro_workshop --gas-budget 30000 6 | 7 | sui client call --function mint --module dino_nft --package 0x8cc2d744c9415975ae384f54142cefd96d32d504 --args "Sui Dino NFT" "A Dino NFT minted on Sui" "https://www.ikea.com/sg/en/images/products/jaettelik-soft-toy-dinosaur-dinosaur-stegosaurus__0804790_pe769331_s5.jpg" --gas-budget 1000 8 | 9 | ### Child Parent Version: 10 | 11 | sui client publish --path /Users/henryworkmac/Documents/workspace/sui_intro_workshop --gas-budget 30000 12 | 13 | sui client call --function mint_to_account --module dino_nft --package 0x88803877c7c0f7465db972b3db472f156a663def --args "Sui Dino NFT" "A Dino NFT minted on Sui" "https://www.ikea.com/sg/en/images/products/jaettelik-soft-toy-dinosaur-dinosaur-stegosaurus__0804790_pe769331_s5.jpg" --gas-budget 1000 14 | 15 | sui client call --function mint_to_object --module dino_nft --package 0x88803877c7c0f7465db972b3db472f156a663def --args "Sui Dino Egg NFT" "A Dino Egg NFT minted on Sui" "https://pisces.bbystatic.com/image2/BestBuy_US/images/products/6366/6366914cv12d.jpg" "0xcb6d9cca5f6f4d7a309b06e766d68f80f2d0ad28" --gas-budget 1000 16 | 17 | sui client call --function hatch_egg --module dino_nft --package 0x88803877c7c0f7465db972b3db472f156a663def --args "Baby Dino NFT" "Hatched Dino NFT" "https://i5.walmartimages.com/asr/f9653453-7d31-45bc-af26-51a027f16d23_1.76b701b2bf1fddb47a3f823807c1999f.jpeg" "0xb5cfa753723d6305430b96c1f6f9b859b579dbff" "0xcb6d9cca5f6f4d7a309b06e766d68f80f2d0ad28" --gas-budget 1000 18 | 19 | sui client call --function retrieve_child_dino --module dino_nft --package 0x88803877c7c0f7465db972b3db472f156a663def --args "0xb5cfa753723d6305430b96c1f6f9b859b579dbff" "0xcb6d9cca5f6f4d7a309b06e766d68f80f2d0ad28" --gas-budget 1000 20 | 21 | ### Access Control Version: 22 | 23 | sui client publish --path /Users/henryworkmac/Documents/workspace/sui_intro_workshop --gas-budget 30000 24 | 25 | sui client call --function owner_mint_to_account --module dino_nft --package 0x063fc7eafe8dea7a4a984bc8ff476c344f0b2f59 --args 0x7f2e5947ae28fb8400afab56eee94113ca796572 "Sui Dino NFT" "A Dino NFT minted on Sui" "https://www.ikea.com/sg/en/images/products/jaettelik-soft-toy-dinosaur-dinosaur-stegosaurus__0804790_pe769331_s5.jpg" --gas-budget 1000 26 | 27 | sui client addresses 28 | 29 | sui client switch --address 0x33b31cad232ee4afa34174ed23107b567e0b7439 30 | 31 | sui client call --function owner_mint_to_account --module dino_nft --package 0x063fc7eafe8dea7a4a984bc8ff476c344f0b2f59 --args 0x7f2e5947ae28fb8400afab56eee94113ca796572 "Sui Dino NFT" "A Dino NFT minted on Sui" "https://www.ikea.com/sg/en/images/products/jaettelik-soft-toy-dinosaur-dinosaur-stegosaurus__0804790_pe769331_s5.jpg" --gas-budget 1000 32 | 33 | (paid mint) 34 | 35 | sui client call --function mint_to_account --module dino_nft --package 0x8faafda44b58a56d5f3cf22bbcbee60d308baa94 --args 0xd80764d8663056354861d5274c73b8df77892aa1 "Sui Dino NFT" "A Dino NFT minted on Sui" "https://www.ikea.com/sg/en/images/products/jaettelik-soft-toy-dinosaur-dinosaur-stegosaurus__0804790_pe769331_s5.jpg" 0x33a34d986f6fa38e76304fbeee1ac36da4734299 --gas-budget 1000 36 | 37 | (withdrawal) 38 | 39 | sui client call --function owner_withdraw --module dino_nft --package 0xce92852eee84d0fb483878dfcea051ad6b9b39af --args 0x1026b22b00ae47553e8206258d29cda47a2b0f80 0x64bf57b21b50215c34297fce647669f5aae3a012 --gas-budget 1000 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sui_intro_workshop 2 | 3 | Intro to Sui Objects Workshop Repo 4 | 5 | ## Installing Sui Binaries 6 | 7 | Install Rust and other dependencies: https://docs.sui.io/build/install#prerequisites 8 | 9 | `git clone https://github.com/MystenLabs/sui.git --branch devnet` 10 | 11 | `git fetch upstream` 12 | 13 | `git checkout devnet-0.12.2` 14 | 15 | `cargo install --locked --git https://github.com/MystenLabs/sui.git --branch "devnet" sui sui-gateway` 16 | 17 | `sui client (use default options to connect to devnet)` 18 | 19 | ## Acquire Devnet Sui Tokens 20 | 21 | - Join Sui Discord: https://discord.com/invite/GcFNX4WMrB 22 | - Get your current wallet address with the command: sui client active-address 23 | - Go to #devnet-faucet channel and type "!faucet [your wallet address]" 24 | 25 | ## Build and Publish the Module 26 | 27 | `git clone https://github.com/hyd628/sui_intro_workshop.git` 28 | 29 | `sui move build` 30 | 31 | `sui client publish --path [local file path to the sui intro workshop module] --gas-budget 30000` 32 | 33 | ## Breakout Session Instructions 34 | 35 | Set up the environment, git clone this repo, and acquire some DevNet Sui tokens per the instructions in this README. 36 | 37 | Familiarize yourself with the sui_dinos.move contract. You will use that as the starting point for the following two tasks. 38 | 39 | The two challenges are independent. You can work on either one, but doing the first one will give you some familiarity with the `sui::coin` module, which is also used in challenge two. 40 | 41 | ### Challenge One: Working with Sui Coin Objects 42 | 43 | Currently, we are aborting the transaction if the coin object sent in during paid minting is not the exact same amount as the minting fee. 44 | 45 | Make the changes necessary so that we check if the coin object sent in is enough to cover the minting fee and send back any extra left as change, instead of only accepting the exact amount. 46 | 47 | *Hints:* 48 | - *You will need to utilize functions in [`sui::coin`](https://github.com/MystenLabs/sui/blob/fe1db4b50425c28693a34564bd8b54be8a68ad89/crates/sui-framework/docs/coin.md).* 49 | - *It might be useful to redefine the existing error type for not sending in the exact amount.* 50 | 51 | ### Challenge Two: Access Based Withdrawal 52 | 53 | Create a new method in the `sui_intro_workshop::dino_nft` module for the owner of the NFT contract to withdraw the balance collected in `MintingTreasury` to their own account. 54 | 55 | *Hints:* 56 | 57 | - *The contract already has most of the infrastructure needed to do this. What are we using to mark the owner or minter account of the NFT contract?* 58 | - *You will need to use functions from [`sui::coin`](https://github.com/MystenLabs/sui/blob/fe1db4b50425c28693a34564bd8b54be8a68ad89/crates/sui-framework/docs/coin.md) and [`sui::balance`](https://github.com/MystenLabs/sui/blob/fe1db4b50425c28693a34564bd8b54be8a68ad89/crates/sui-framework/docs/balance.md).* 59 | - *It might make sense to create a new error type for an edge case here.* 60 | 61 | ### Resources 62 | 63 | - [Sui Framework Docs](https://github.com/MystenLabs/sui/tree/fe1db4b50425c28693a34564bd8b54be8a68ad89/crates/sui-framework/docs) 64 | - [Sui Developer Documentation](https://docs.sui.io/) 65 | - [Sui Explorer](https://explorer.devnet.sui.io/) 66 | - [Sui CLI Tutorial](https://docs.sui.io/build/cli-client) 67 | - [Move Programming Language Book](https://move-book.com/index.html) 68 | - [Sui by Example](https://examples.sui.io/index.html) 69 | 70 | ### Check Solutions 71 | 72 | When you are ready to see the solutions: 73 | 74 | - [Solution to challenge one](https://github.com/hyd628/sui_intro_workshop/blob/main/breakout_session_solutions/challenge_1_solution.move) 75 | - [Solution to challenge two](https://github.com/hyd628/sui_intro_workshop/blob/main/breakout_session_solutions/challenge_2_solution.move) 76 | - [Combined solution to both challenges](https://github.com/hyd628/sui_intro_workshop/blob/main/breakout_session_solutions/combined_solution.move) 77 | 78 | -------------------------------------------------------------------------------- /sources/sui_dinos.move_basic: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /// A basic NFT example to demonstrate how to create and interact with a Dino NFT 5 | /// on Sui. 6 | /// 7 | /// Based on https://github.com/MystenLabs/sui/blob/94e5bca0f38def733d2a3bb45683834126069c43/crates/sui-framework/sources/devnet_nft.move 8 | /// 9 | module sui_intro_workshop::dino_nft { 10 | use sui::url::{Self, Url}; 11 | use std::string; 12 | use sui::object::{Self, ID, UID}; 13 | use sui::event; 14 | use sui::transfer; 15 | use sui::tx_context::{Self, TxContext}; 16 | 17 | /// An example NFT that can be minted by anybody 18 | struct DinoNFT has key, store { 19 | id: UID, 20 | /// Name for the token 21 | name: string::String, 22 | /// Description of the token 23 | description: string::String, 24 | /// URL for the token 25 | url: Url 26 | } 27 | 28 | struct MintNFTEvent has copy, drop { 29 | // The Object ID of the NFT 30 | object_id: ID, 31 | // The creator of the NFT 32 | creator: address, 33 | // The name of the NFT 34 | name: string::String, 35 | } 36 | 37 | /// Create a new devnet_nft 38 | public entry fun mint( 39 | name: vector, 40 | description: vector, 41 | url: vector, 42 | ctx: &mut TxContext 43 | ) { 44 | let nft = DinoNFT { 45 | id: object::new(ctx), 46 | name: string::utf8(name), 47 | description: string::utf8(description), 48 | url: url::new_unsafe_from_bytes(url) 49 | }; 50 | let sender = tx_context::sender(ctx); 51 | event::emit(MintNFTEvent { 52 | object_id: object::uid_to_inner(&nft.id), 53 | creator: sender, 54 | name: nft.name, 55 | }); 56 | transfer::transfer(nft, sender); 57 | } 58 | 59 | /// Update the `description` of `nft` to `new_description` 60 | public entry fun update_description( 61 | nft: &mut DinoNFT, 62 | new_description: vector, 63 | _: &mut TxContext 64 | ) { 65 | nft.description = string::utf8(new_description) 66 | } 67 | 68 | /// Permanently delete `nft` 69 | public entry fun burn(nft: DinoNFT, _: &mut TxContext) { 70 | let DinoNFT { id, name: _, description: _, url: _ } = nft; 71 | object::delete(id) 72 | } 73 | 74 | /// Get the NFT's `name` 75 | public fun name(nft: &DinoNFT): &string::String { 76 | &nft.name 77 | } 78 | 79 | /// Get the NFT's `description` 80 | public fun description(nft: &DinoNFT): &string::String { 81 | &nft.description 82 | } 83 | 84 | /// Get the NFT's `url` 85 | public fun url(nft: &DinoNFT): &Url { 86 | &nft.url 87 | } 88 | } 89 | 90 | #[test_only] 91 | module sui_intro_workshop::devnet_nftTests { 92 | use sui_intro_workshop::dino_nft::{Self, DinoNFT}; 93 | use sui::test_scenario; 94 | use sui::transfer; 95 | use std::string; 96 | 97 | #[test] 98 | fun mint_transfer_update() { 99 | let addr1 = @0xA; 100 | let addr2 = @0xB; 101 | // create the NFT 102 | let scenario = test_scenario::begin(&addr1); 103 | { 104 | dino_nft::mint(b"test", b"a test", b"https://www.sui.io", test_scenario::ctx(&mut scenario)) 105 | }; 106 | // send it from A to B 107 | test_scenario::next_tx(&mut scenario, &addr1); 108 | { 109 | let nft = test_scenario::take_owned(&mut scenario); 110 | transfer::transfer(nft, addr2); 111 | }; 112 | // update its description 113 | test_scenario::next_tx(&mut scenario, &addr2); 114 | { 115 | let nft = test_scenario::take_owned(&mut scenario); 116 | dino_nft::update_description(&mut nft, b"a new description", test_scenario::ctx(&mut scenario)) ; 117 | assert!(*string::bytes(dino_nft::description(&nft)) == b"a new description", 0); 118 | test_scenario::return_owned(&mut scenario, nft); 119 | }; 120 | // burn it 121 | test_scenario::next_tx(&mut scenario, &addr2); 122 | { 123 | let nft = test_scenario::take_owned(&mut scenario); 124 | dino_nft::burn(nft, test_scenario::ctx(&mut scenario)) 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /sources/sui_dinos.move_childobject: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /// A basic NFT example to demonstrate how to create and interact with a Dino NFT 5 | /// on Sui that can own and a child Dino NFT, illustrating basic parent-child object 6 | /// relationships in Sui Move. 7 | /// 8 | /// Based on https://github.com/MystenLabs/sui/blob/94e5bca0f38def733d2a3bb45683834126069c43/crates/sui-framework/sources/devnet_nft.move 9 | /// 10 | module sui_intro_workshop::dino_nft { 11 | use sui::url::{Self, Url}; 12 | use std::string; 13 | use std::option::{Self, Option}; 14 | use sui::object::{Self, ID, UID}; 15 | use sui::event; 16 | use sui::transfer; 17 | use sui::tx_context::{Self, TxContext}; 18 | 19 | /// An example NFT that can be minted by anybody 20 | struct DinoNFT has key, store { 21 | id: UID, 22 | /// Name for the token 23 | name: string::String, 24 | /// Description of the token 25 | description: string::String, 26 | /// URL for the token 27 | url: Url, 28 | /// Dino egg optional field 29 | dino_egg: Option, 30 | } 31 | 32 | struct MintNFTEvent has copy, drop { 33 | // The Object ID of the NFT 34 | object_id: ID, 35 | // The creator of the NFT 36 | creator: address, 37 | // The name of the NFT 38 | name: string::String, 39 | } 40 | 41 | /// Private function that creates and returns a new DinoNFT 42 | fun mint( 43 | name: vector, 44 | description: vector, 45 | url: vector, 46 | ctx: &mut TxContext 47 | ): DinoNFT { 48 | let nft = DinoNFT { 49 | id: object::new(ctx), 50 | name: string::utf8(name), 51 | description: string::utf8(description), 52 | url: url::new_unsafe_from_bytes(url), 53 | // Set the dino_egg field to None 54 | dino_egg: option::none() 55 | }; 56 | let sender = tx_context::sender(ctx); 57 | event::emit(MintNFTEvent { 58 | object_id: object::uid_to_inner(&nft.id), 59 | creator: sender, 60 | name: nft.name, 61 | }); 62 | nft 63 | } 64 | 65 | /// Mint a DinoNFT to an account 66 | public entry fun mint_to_account( 67 | name: vector, 68 | description: vector, 69 | url: vector, 70 | ctx: &mut TxContext 71 | ) { 72 | let nft = mint(name, description, url, ctx); 73 | transfer::transfer(nft, tx_context::sender(ctx)); 74 | } 75 | 76 | /// Mint a DinoNFT and set it as a child to a parent object 77 | public entry fun mint_to_object( 78 | name: vector, 79 | description: vector, 80 | url: vector, 81 | dino_parent: &mut DinoNFT, 82 | ctx: &mut TxContext 83 | ) { 84 | let nft = mint(name, description, url, ctx); 85 | option::fill(&mut dino_parent.dino_egg, object::id(&nft)); 86 | transfer::transfer_to_object(nft, dino_parent); 87 | } 88 | 89 | /// Update the `description` of `nft` to `new_description` 90 | public entry fun update_description( 91 | nft: &mut DinoNFT, 92 | new_description: vector, 93 | _: &mut TxContext 94 | ) { 95 | nft.description = string::utf8(new_description) 96 | } 97 | 98 | /// Hatch the egg by changing the NFT's description, name and URL 99 | public entry fun hatch_egg( 100 | name: vector, 101 | description: vector, 102 | url: vector, 103 | egg_nft: &mut DinoNFT, 104 | _parent_nft: &mut DinoNFT, 105 | _: &mut TxContext 106 | ) { 107 | egg_nft.name = string::utf8(name); 108 | egg_nft.description = string::utf8(description); 109 | egg_nft.url = url::new_unsafe_from_bytes(url) 110 | } 111 | 112 | /// Retrieve the child NFT and send it to the message sender 113 | public entry fun retrieve_child_dino( 114 | child_nft: DinoNFT, 115 | parent_nft: &mut DinoNFT, 116 | ctx: &mut TxContext 117 | ) { 118 | option::extract(&mut parent_nft.dino_egg); 119 | transfer::transfer(child_nft, tx_context::sender(ctx)); 120 | } 121 | 122 | /// Permanently delete `nft` 123 | public entry fun burn(nft: DinoNFT, _: &mut TxContext) { 124 | let DinoNFT { id, name: _, description: _, url: _, dino_egg: _} = nft; 125 | object::delete(id) 126 | } 127 | 128 | /// Get the NFT's `name` 129 | public fun name(nft: &DinoNFT): &string::String { 130 | &nft.name 131 | } 132 | 133 | /// Get the NFT's `description` 134 | public fun description(nft: &DinoNFT): &string::String { 135 | &nft.description 136 | } 137 | 138 | /// Get the NFT's `url` 139 | public fun url(nft: &DinoNFT): &Url { 140 | &nft.url 141 | } 142 | } 143 | 144 | #[test_only] 145 | module sui_intro_workshop::dino_nftTests { 146 | use sui_intro_workshop::dino_nft::{Self, DinoNFT}; 147 | use sui::test_scenario; 148 | use sui::transfer; 149 | use std::string; 150 | 151 | #[test] 152 | fun mint_transfer_update() { 153 | let addr1 = @0xA; 154 | let addr2 = @0xB; 155 | // create the NFT 156 | let scenario = test_scenario::begin(&addr1); 157 | { 158 | dino_nft::mint_to_account(b"test", b"a test", b"https://www.sui.io", test_scenario::ctx(&mut scenario)) 159 | }; 160 | // send it from A to B 161 | test_scenario::next_tx(&mut scenario, &addr1); 162 | { 163 | let nft = test_scenario::take_owned(&mut scenario); 164 | transfer::transfer(nft, addr2); 165 | }; 166 | // update its description 167 | test_scenario::next_tx(&mut scenario, &addr2); 168 | { 169 | let nft = test_scenario::take_owned(&mut scenario); 170 | dino_nft::update_description(&mut nft, b"a new description", test_scenario::ctx(&mut scenario)) ; 171 | assert!(*string::bytes(dino_nft::description(&nft)) == b"a new description", 0); 172 | test_scenario::return_owned(&mut scenario, nft); 173 | }; 174 | // burn it 175 | test_scenario::next_tx(&mut scenario, &addr2); 176 | { 177 | let nft = test_scenario::take_owned(&mut scenario); 178 | dino_nft::burn(nft, test_scenario::ctx(&mut scenario)) 179 | } 180 | } 181 | } -------------------------------------------------------------------------------- /sources/sui_dinos.move: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /// A basic NFT example to demonstrate how to create and interact with a Dino NFT 5 | /// on Sui that can own and a child Dino NFT, illustrating basic parent-child object 6 | /// relationships and access control in Sui Move. 7 | /// 8 | /// Based on https://github.com/MystenLabs/sui/blob/94e5bca0f38def733d2a3bb45683834126069c43/crates/sui-framework/sources/devnet_nft.move 9 | /// 10 | module sui_intro_workshop::dino_nft { 11 | use sui::url::{Self, Url}; 12 | use std::string; 13 | use std::option::{Self, Option}; 14 | use sui::object::{Self, ID, UID}; 15 | use sui::coin::{Self, Coin}; 16 | use sui::balance::{Self, Balance}; 17 | use sui::event; 18 | use sui::transfer; 19 | use sui::tx_context::{Self, TxContext}; 20 | use sui::sui::SUI; 21 | 22 | /// For when paid amount is not equal to the minting price. 23 | const EWrongAmount: u64 = 0; 24 | 25 | /// An example NFT that can be minted by anybody 26 | struct DinoNFT has key, store { 27 | id: UID, 28 | /// Name for the token 29 | name: string::String, 30 | /// Description of the token 31 | description: string::String, 32 | /// URL for the token 33 | url: Url, 34 | /// Dino egg optional field 35 | dino_egg: Option, 36 | } 37 | 38 | /// Type that marks the capability to mint new DinoNFT's. 39 | struct MinterCap has key { id: UID } 40 | 41 | /// Event marking when a DinoNFT has been minted 42 | struct MintNFTEvent has copy, drop { 43 | // The Object ID of the NFT 44 | object_id: ID, 45 | // The creator of the NFT 46 | creator: address, 47 | // The name of the NFT 48 | name: string::String, 49 | } 50 | 51 | struct MintingTreasury has key { 52 | id: UID, 53 | balance: Balance, 54 | mintingfee: u64 55 | } 56 | 57 | /// Module initializer is called once on module publish. 58 | /// Here we create only one instance of `MinterCap` and `MintingTreausury` and send it to the deployer account. 59 | fun init(ctx: &mut TxContext) { 60 | 61 | transfer::transfer(MinterCap { 62 | id: object::new(ctx) 63 | }, tx_context::sender(ctx)); 64 | 65 | transfer::share_object(MintingTreasury { 66 | id: object::new(ctx), 67 | balance: balance::zero(), 68 | mintingfee: 5000000 69 | }) 70 | } 71 | 72 | /// Private function that creates and returns a new DinoNFT 73 | fun mint( 74 | name: vector, 75 | description: vector, 76 | url: vector, 77 | ctx: &mut TxContext 78 | ): DinoNFT { 79 | let nft = DinoNFT { 80 | id: object::new(ctx), 81 | name: string::utf8(name), 82 | description: string::utf8(description), 83 | url: url::new_unsafe_from_bytes(url), 84 | // Set the dino_egg field to None 85 | dino_egg: option::none() 86 | }; 87 | let sender = tx_context::sender(ctx); 88 | event::emit(MintNFTEvent { 89 | object_id: object::uid_to_inner(&nft.id), 90 | creator: sender, 91 | name: nft.name, 92 | }); 93 | nft 94 | } 95 | 96 | /// Paid public mint to an account 97 | public entry fun mint_to_account( 98 | mintingtreasury: &mut MintingTreasury, 99 | name: vector, 100 | description: vector, 101 | url: vector, 102 | fee: Coin, 103 | ctx: &mut TxContext 104 | ) { 105 | assert!(coin::value(&fee) == mintingtreasury.mintingfee, EWrongAmount); 106 | // add a payment to the minting treasury balance 107 | balance::join(&mut mintingtreasury.balance, coin::into_balance(fee)); 108 | 109 | //mint the NFT and transfer to sender 110 | let nft = mint(name, description, url, ctx); 111 | transfer::transfer(nft, tx_context::sender(ctx)); 112 | } 113 | 114 | /// Paid public mint function for minting a DinoNFT and set it as a child to a parent object 115 | public entry fun mint_to_object( 116 | mintingtreasury: &mut MintingTreasury, 117 | name: vector, 118 | description: vector, 119 | url: vector, 120 | dino_parent: &mut DinoNFT, 121 | fee: Coin, 122 | ctx: &mut TxContext 123 | ) { 124 | assert!(coin::value(&fee) == mintingtreasury.mintingfee, EWrongAmount); 125 | // add a payment to the minting treasury balance 126 | balance::join(&mut mintingtreasury.balance, coin::into_balance(fee)); 127 | 128 | 129 | let nft = mint(name, description, url, ctx); 130 | option::fill(&mut dino_parent.dino_egg, object::id(&nft)); 131 | transfer::transfer_to_object(nft, dino_parent); 132 | } 133 | 134 | /// Privileged mint a DinoNFT to an account 135 | public entry fun owner_mint_to_account( 136 | _: &MinterCap, 137 | name: vector, 138 | description: vector, 139 | url: vector, 140 | ctx: &mut TxContext 141 | ) { 142 | let nft = mint(name, description, url, ctx); 143 | transfer::transfer(nft, tx_context::sender(ctx)); 144 | } 145 | 146 | /// Privileged mint a DinoNFT and set it as a child to a parent object 147 | public entry fun owner_mint_to_object( 148 | _: &MinterCap, 149 | name: vector, 150 | description: vector, 151 | url: vector, 152 | dino_parent: &mut DinoNFT, 153 | ctx: &mut TxContext 154 | ) { 155 | let nft = mint(name, description, url, ctx); 156 | option::fill(&mut dino_parent.dino_egg, object::id(&nft)); 157 | transfer::transfer_to_object(nft, dino_parent); 158 | } 159 | 160 | /// Update the `description` of `nft` to `new_description` 161 | public entry fun update_description( 162 | nft: &mut DinoNFT, 163 | new_description: vector, 164 | _: &mut TxContext 165 | ) { 166 | nft.description = string::utf8(new_description) 167 | } 168 | 169 | /// Hatch the egg by changing the NFT's description, name and URL 170 | public entry fun hatch_egg( 171 | name: vector, 172 | description: vector, 173 | url: vector, 174 | egg_nft: &mut DinoNFT, 175 | _parent_nft: &mut DinoNFT, 176 | _: &mut TxContext 177 | ) { 178 | egg_nft.name = string::utf8(name); 179 | egg_nft.description = string::utf8(description); 180 | egg_nft.url = url::new_unsafe_from_bytes(url) 181 | } 182 | 183 | /// Retrieve the child NFT and send it to the message sender 184 | public entry fun retrieve_child_dino( 185 | child_nft: DinoNFT, 186 | parent_nft: &mut DinoNFT, 187 | ctx: &mut TxContext 188 | ) { 189 | option::extract(&mut parent_nft.dino_egg); 190 | transfer::transfer(child_nft, tx_context::sender(ctx)); 191 | } 192 | 193 | /// Permanently delete `nft` 194 | public entry fun burn(nft: DinoNFT, _: &mut TxContext) { 195 | let DinoNFT { id, name: _, description: _, url: _, dino_egg: _} = nft; 196 | object::delete(id) 197 | } 198 | 199 | /// Permanently delete `minterCap` 200 | public entry fun burn_cap(cap: MinterCap, _: &mut TxContext) { 201 | let MinterCap { id } = cap; 202 | object::delete(id) 203 | } 204 | 205 | /// Get the NFT's `name` 206 | public fun name(nft: &DinoNFT): &string::String { 207 | &nft.name 208 | } 209 | 210 | /// Get the NFT's `description` 211 | public fun description(nft: &DinoNFT): &string::String { 212 | &nft.description 213 | } 214 | 215 | /// Get the NFT's `url` 216 | public fun url(nft: &DinoNFT): &Url { 217 | &nft.url 218 | } 219 | 220 | #[test_only] public fun init_for_testing(ctx: &mut TxContext) { init(ctx) } 221 | } 222 | 223 | 224 | #[test_only] 225 | module sui_intro_workshop::dino_nftTests { 226 | use sui_intro_workshop::dino_nft::{Self, DinoNFT, MinterCap}; 227 | use sui::test_scenario::{Self, next_tx, ctx}; 228 | use sui::transfer; 229 | use std::string; 230 | 231 | #[test] 232 | fun mint_transfer_update() { 233 | let addr1 = @0xA; 234 | let addr2 = @0xB; 235 | // create the NFT 236 | let scenario = test_scenario::begin(&addr1); 237 | 238 | next_tx(&mut scenario, &addr1); 239 | { 240 | dino_nft::init_for_testing(ctx(&mut scenario)) 241 | }; 242 | next_tx(&mut scenario, &addr1); 243 | { 244 | let mintercap = test_scenario::take_owned(&mut scenario); 245 | dino_nft::owner_mint_to_account(&mintercap, b"test", b"a test", b"https://www.sui.io", test_scenario::ctx(&mut scenario)); 246 | dino_nft::burn_cap(mintercap, test_scenario::ctx(&mut scenario)) 247 | }; 248 | // send it from A to B 249 | test_scenario::next_tx(&mut scenario, &addr1); 250 | { 251 | let nft = test_scenario::take_owned(&mut scenario); 252 | transfer::transfer(nft, addr2); 253 | }; 254 | // update its description 255 | test_scenario::next_tx(&mut scenario, &addr2); 256 | { 257 | let nft = test_scenario::take_owned(&mut scenario); 258 | dino_nft::update_description(&mut nft, b"a new description", test_scenario::ctx(&mut scenario)) ; 259 | assert!(*string::bytes(dino_nft::description(&nft)) == b"a new description", 0); 260 | test_scenario::return_owned(&mut scenario, nft); 261 | }; 262 | // burn it 263 | test_scenario::next_tx(&mut scenario, &addr2); 264 | { 265 | let nft = test_scenario::take_owned(&mut scenario); 266 | dino_nft::burn(nft, test_scenario::ctx(&mut scenario)); 267 | } 268 | } 269 | } -------------------------------------------------------------------------------- /breakout_session_solutions/challenge_1_solution.move: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /// A basic NFT example to demonstrate how to create and interact with a Dino NFT 5 | /// on Sui that can own and a child Dino NFT, illustrating basic parent-child object 6 | /// relationships and access control in Sui Move. 7 | /// 8 | /// Based on https://github.com/MystenLabs/sui/blob/94e5bca0f38def733d2a3bb45683834126069c43/crates/sui-framework/sources/devnet_nft.move 9 | /// 10 | /// For challenge 1 solution, see methods mint_to_account and mint_to_object. 11 | module sui_intro_workshop::dino_nft { 12 | use sui::url::{Self, Url}; 13 | use std::string; 14 | use std::option::{Self, Option}; 15 | use sui::object::{Self, ID, UID}; 16 | use sui::coin::{Self, Coin}; 17 | use sui::balance::{Self, Balance}; 18 | use sui::event; 19 | use sui::transfer; 20 | use sui::tx_context::{Self, TxContext}; 21 | use sui::sui::SUI; 22 | 23 | /// For when paid amount is lower than the minting price. 24 | const ENotEnoughCoinPaid: u64 = 0; 25 | 26 | /// An example NFT that can be minted by anybody 27 | struct DinoNFT has key, store { 28 | id: UID, 29 | /// Name for the token 30 | name: string::String, 31 | /// Description of the token 32 | description: string::String, 33 | /// URL for the token 34 | url: Url, 35 | /// Dino egg optional field 36 | dino_egg: Option, 37 | } 38 | 39 | /// Type that marks the capability to mint new DinoNFT's. 40 | struct MinterCap has key { id: UID } 41 | 42 | /// Event marking when a DinoNFT has been minted 43 | struct MintNFTEvent has copy, drop { 44 | // The Object ID of the NFT 45 | object_id: ID, 46 | // The creator of the NFT 47 | creator: address, 48 | // The name of the NFT 49 | name: string::String, 50 | } 51 | 52 | struct MintingTreasury has key { 53 | id: UID, 54 | balance: Balance, 55 | mintingfee: u64 56 | } 57 | 58 | /// Module initializer is called once on module publish. 59 | /// Here we create only one instance of `MinterCap` and `MintingTreausury` and send it to the deployer account. 60 | fun init(ctx: &mut TxContext) { 61 | 62 | transfer::transfer(MinterCap { 63 | id: object::new(ctx) 64 | }, tx_context::sender(ctx)); 65 | 66 | transfer::share_object(MintingTreasury { 67 | id: object::new(ctx), 68 | balance: balance::zero(), 69 | mintingfee: 5000000 70 | }) 71 | } 72 | 73 | /// Private function that creates and returns a new DinoNFT 74 | fun mint( 75 | name: vector, 76 | description: vector, 77 | url: vector, 78 | ctx: &mut TxContext 79 | ): DinoNFT { 80 | let nft = DinoNFT { 81 | id: object::new(ctx), 82 | name: string::utf8(name), 83 | description: string::utf8(description), 84 | url: url::new_unsafe_from_bytes(url), 85 | // Set the dino_egg field to None 86 | dino_egg: option::none() 87 | }; 88 | let sender = tx_context::sender(ctx); 89 | event::emit(MintNFTEvent { 90 | object_id: object::uid_to_inner(&nft.id), 91 | creator: sender, 92 | name: nft.name, 93 | }); 94 | nft 95 | } 96 | 97 | /// Paid public mint to an account 98 | public entry fun mint_to_account( 99 | mintingtreasury: &mut MintingTreasury, 100 | name: vector, 101 | description: vector, 102 | url: vector, 103 | fee: Coin, 104 | ctx: &mut TxContext 105 | ) { 106 | 107 | let paid_value = coin::value(&fee); 108 | assert!(paid_value >= mintingtreasury.mintingfee, ENotEnoughCoinPaid); 109 | // Transfer back any extra 110 | if (paid_value > mintingtreasury.mintingfee) { 111 | coin::split(&mut fee, paid_value - mintingtreasury.mintingfee, ctx); 112 | }; 113 | // Add a payment to the minting treasury balance 114 | balance::join(&mut mintingtreasury.balance, coin::into_balance(fee)); 115 | // Mint the NFT and transfer to sender 116 | let nft = mint(name, description, url, ctx); 117 | transfer::transfer(nft, tx_context::sender(ctx)); 118 | } 119 | 120 | /// Paid public mint function for minting a DinoNFT and set it as a child to a parent object 121 | public entry fun mint_to_object( 122 | mintingtreasury: &mut MintingTreasury, 123 | name: vector, 124 | description: vector, 125 | url: vector, 126 | dino_parent: &mut DinoNFT, 127 | fee: Coin, 128 | ctx: &mut TxContext 129 | ) { 130 | let paid_value = coin::value(&fee); 131 | assert!(paid_value >= mintingtreasury.mintingfee, ENotEnoughCoinPaid); 132 | // Transfer back any extra 133 | if (paid_value > mintingtreasury.mintingfee) { 134 | coin::split(&mut fee, paid_value - mintingtreasury.mintingfee, ctx); 135 | }; 136 | // add a payment to the minting treasury balance 137 | balance::join(&mut mintingtreasury.balance, coin::into_balance(fee)); 138 | // Mint the NFT and transfer to parent object 139 | let nft = mint(name, description, url, ctx); 140 | option::fill(&mut dino_parent.dino_egg, object::id(&nft)); 141 | transfer::transfer_to_object(nft, dino_parent); 142 | } 143 | 144 | /// Privileged mint a DinoNFT to an account 145 | public entry fun owner_mint_to_account( 146 | _: &MinterCap, 147 | name: vector, 148 | description: vector, 149 | url: vector, 150 | ctx: &mut TxContext 151 | ) { 152 | let nft = mint(name, description, url, ctx); 153 | transfer::transfer(nft, tx_context::sender(ctx)); 154 | } 155 | 156 | /// Privileged mint a DinoNFT and set it as a child to a parent object 157 | public entry fun owner_mint_to_object( 158 | _: &MinterCap, 159 | name: vector, 160 | description: vector, 161 | url: vector, 162 | dino_parent: &mut DinoNFT, 163 | ctx: &mut TxContext 164 | ) { 165 | let nft = mint(name, description, url, ctx); 166 | option::fill(&mut dino_parent.dino_egg, object::id(&nft)); 167 | transfer::transfer_to_object(nft, dino_parent); 168 | } 169 | 170 | /// Update the `description` of `nft` to `new_description` 171 | public entry fun update_description( 172 | nft: &mut DinoNFT, 173 | new_description: vector, 174 | _: &mut TxContext 175 | ) { 176 | nft.description = string::utf8(new_description) 177 | } 178 | 179 | /// Hatch the egg by changing the NFT's description, name and URL 180 | public entry fun hatch_egg( 181 | name: vector, 182 | description: vector, 183 | url: vector, 184 | egg_nft: &mut DinoNFT, 185 | _parent_nft: &mut DinoNFT, 186 | _: &mut TxContext 187 | ) { 188 | egg_nft.name = string::utf8(name); 189 | egg_nft.description = string::utf8(description); 190 | egg_nft.url = url::new_unsafe_from_bytes(url) 191 | } 192 | 193 | /// Retrieve the child NFT and send it to the message sender 194 | public entry fun retrieve_child_dino( 195 | child_nft: DinoNFT, 196 | parent_nft: &mut DinoNFT, 197 | ctx: &mut TxContext 198 | ) { 199 | option::extract(&mut parent_nft.dino_egg); 200 | transfer::transfer(child_nft, tx_context::sender(ctx)); 201 | } 202 | 203 | /// Permanently delete `nft` 204 | public entry fun burn(nft: DinoNFT, _: &mut TxContext) { 205 | let DinoNFT { id, name: _, description: _, url: _, dino_egg: _} = nft; 206 | object::delete(id) 207 | } 208 | 209 | /// Permanently delete `minterCap` 210 | public entry fun burn_cap(cap: MinterCap, _: &mut TxContext) { 211 | let MinterCap { id } = cap; 212 | object::delete(id) 213 | } 214 | 215 | /// Get the NFT's `name` 216 | public fun name(nft: &DinoNFT): &string::String { 217 | &nft.name 218 | } 219 | 220 | /// Get the NFT's `description` 221 | public fun description(nft: &DinoNFT): &string::String { 222 | &nft.description 223 | } 224 | 225 | /// Get the NFT's `url` 226 | public fun url(nft: &DinoNFT): &Url { 227 | &nft.url 228 | } 229 | 230 | #[test_only] public fun init_for_testing(ctx: &mut TxContext) { init(ctx) } 231 | } 232 | 233 | 234 | #[test_only] 235 | module sui_intro_workshop::dino_nftTests { 236 | use sui_intro_workshop::dino_nft::{Self, DinoNFT, MinterCap}; 237 | use sui::test_scenario::{Self, next_tx, ctx}; 238 | use sui::transfer; 239 | use std::string; 240 | 241 | #[test] 242 | fun mint_transfer_update() { 243 | let addr1 = @0xA; 244 | let addr2 = @0xB; 245 | // create the NFT 246 | let scenario = test_scenario::begin(&addr1); 247 | 248 | next_tx(&mut scenario, &addr1); 249 | { 250 | dino_nft::init_for_testing(ctx(&mut scenario)) 251 | }; 252 | next_tx(&mut scenario, &addr1); 253 | { 254 | let mintercap = test_scenario::take_owned(&mut scenario); 255 | dino_nft::owner_mint_to_account(&mintercap, b"test", b"a test", b"https://www.sui.io", test_scenario::ctx(&mut scenario)); 256 | dino_nft::burn_cap(mintercap, test_scenario::ctx(&mut scenario)) 257 | }; 258 | // send it from A to B 259 | test_scenario::next_tx(&mut scenario, &addr1); 260 | { 261 | let nft = test_scenario::take_owned(&mut scenario); 262 | transfer::transfer(nft, addr2); 263 | }; 264 | // update its description 265 | test_scenario::next_tx(&mut scenario, &addr2); 266 | { 267 | let nft = test_scenario::take_owned(&mut scenario); 268 | dino_nft::update_description(&mut nft, b"a new description", test_scenario::ctx(&mut scenario)) ; 269 | assert!(*string::bytes(dino_nft::description(&nft)) == b"a new description", 0); 270 | test_scenario::return_owned(&mut scenario, nft); 271 | }; 272 | // burn it 273 | test_scenario::next_tx(&mut scenario, &addr2); 274 | { 275 | let nft = test_scenario::take_owned(&mut scenario); 276 | dino_nft::burn(nft, test_scenario::ctx(&mut scenario)); 277 | } 278 | } 279 | } -------------------------------------------------------------------------------- /breakout_session_solutions/challenge_2_solution.move: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /// A basic NFT example to demonstrate how to create and interact with a Dino NFT 5 | /// on Sui that can own and a child Dino NFT, illustrating basic parent-child object 6 | /// relationships and access control in Sui Move. 7 | /// 8 | /// Based on https://github.com/MystenLabs/sui/blob/94e5bca0f38def733d2a3bb45683834126069c43/crates/sui-framework/sources/devnet_nft.move 9 | /// 10 | /// Challenge 2 solution is the owner_withdraw function under MintingTreasury 11 | module sui_intro_workshop::dino_nft { 12 | use sui::url::{Self, Url}; 13 | use std::string; 14 | use std::option::{Self, Option}; 15 | use sui::object::{Self, ID, UID}; 16 | use sui::coin::{Self, Coin}; 17 | use sui::balance::{Self, Balance}; 18 | use sui::event; 19 | use sui::transfer; 20 | use sui::tx_context::{Self, TxContext}; 21 | use sui::sui::SUI; 22 | 23 | /// For when paid amount is not equal to the minting price. 24 | const EWrongAmount: u64 = 0; 25 | /// Nothing to withdraw error. 26 | const ENoProfits: u64 = 1; 27 | 28 | /// An example NFT that can be minted by anybody 29 | struct DinoNFT has key, store { 30 | id: UID, 31 | /// Name for the token 32 | name: string::String, 33 | /// Description of the token 34 | description: string::String, 35 | /// URL for the token 36 | url: Url, 37 | /// Dino egg optional field 38 | dino_egg: Option, 39 | } 40 | 41 | /// Type that marks the capability to mint new DinoNFT's. 42 | struct MinterCap has key { id: UID } 43 | 44 | /// Event marking when a DinoNFT has been minted 45 | struct MintNFTEvent has copy, drop { 46 | // The Object ID of the NFT 47 | object_id: ID, 48 | // The creator of the NFT 49 | creator: address, 50 | // The name of the NFT 51 | name: string::String, 52 | } 53 | 54 | struct MintingTreasury has key { 55 | id: UID, 56 | balance: Balance, 57 | mintingfee: u64 58 | } 59 | 60 | /// Withdraw the balance in Minting Treasury to the owner of MinterCap 61 | public entry fun owner_withdraw( 62 | _: &MinterCap, 63 | mintingtreasury: &mut MintingTreasury, 64 | ctx: &mut TxContext 65 | ) { 66 | let amount = balance::value(&mintingtreasury.balance); 67 | assert!(amount > 0, ENoProfits); 68 | // Take a transferable `Coin` from a `Balance` 69 | let coin = coin::take(&mut mintingtreasury.balance, amount, ctx); 70 | transfer::transfer(coin, tx_context::sender(ctx)); 71 | } 72 | 73 | /// Module initializer is called once on module publish. 74 | /// Here we create only one instance of `MinterCap` and `MintingTreausury` and send it to the deployer account. 75 | fun init(ctx: &mut TxContext) { 76 | 77 | transfer::transfer(MinterCap { 78 | id: object::new(ctx) 79 | }, tx_context::sender(ctx)); 80 | 81 | transfer::share_object(MintingTreasury { 82 | id: object::new(ctx), 83 | balance: balance::zero(), 84 | mintingfee: 5000000 85 | }) 86 | } 87 | 88 | /// Private function that creates and returns a new DinoNFT 89 | fun mint( 90 | name: vector, 91 | description: vector, 92 | url: vector, 93 | ctx: &mut TxContext 94 | ): DinoNFT { 95 | let nft = DinoNFT { 96 | id: object::new(ctx), 97 | name: string::utf8(name), 98 | description: string::utf8(description), 99 | url: url::new_unsafe_from_bytes(url), 100 | // Set the dino_egg field to None 101 | dino_egg: option::none() 102 | }; 103 | let sender = tx_context::sender(ctx); 104 | event::emit(MintNFTEvent { 105 | object_id: object::uid_to_inner(&nft.id), 106 | creator: sender, 107 | name: nft.name, 108 | }); 109 | nft 110 | } 111 | 112 | /// Paid public mint to an account 113 | public entry fun mint_to_account( 114 | mintingtreasury: &mut MintingTreasury, 115 | name: vector, 116 | description: vector, 117 | url: vector, 118 | fee: Coin, 119 | ctx: &mut TxContext 120 | ) { 121 | assert!(coin::value(&fee) == mintingtreasury.mintingfee, EWrongAmount); 122 | // add a payment to the minting treasury balance 123 | balance::join(&mut mintingtreasury.balance, coin::into_balance(fee)); 124 | 125 | //mint the NFT and transfer to sender 126 | let nft = mint(name, description, url, ctx); 127 | transfer::transfer(nft, tx_context::sender(ctx)); 128 | } 129 | 130 | /// Paid public mint function for minting a DinoNFT and set it as a child to a parent object 131 | public entry fun mint_to_object( 132 | mintingtreasury: &mut MintingTreasury, 133 | name: vector, 134 | description: vector, 135 | url: vector, 136 | dino_parent: &mut DinoNFT, 137 | fee: Coin, 138 | ctx: &mut TxContext 139 | ) { 140 | assert!(coin::value(&fee) == mintingtreasury.mintingfee, EWrongAmount); 141 | // add a payment to the minting treasury balance 142 | balance::join(&mut mintingtreasury.balance, coin::into_balance(fee)); 143 | 144 | 145 | let nft = mint(name, description, url, ctx); 146 | option::fill(&mut dino_parent.dino_egg, object::id(&nft)); 147 | transfer::transfer_to_object(nft, dino_parent); 148 | } 149 | 150 | /// Privileged mint a DinoNFT to an account 151 | public entry fun owner_mint_to_account( 152 | _: &MinterCap, 153 | name: vector, 154 | description: vector, 155 | url: vector, 156 | ctx: &mut TxContext 157 | ) { 158 | let nft = mint(name, description, url, ctx); 159 | transfer::transfer(nft, tx_context::sender(ctx)); 160 | } 161 | 162 | /// Privileged mint a DinoNFT and set it as a child to a parent object 163 | public entry fun owner_mint_to_object( 164 | _: &MinterCap, 165 | name: vector, 166 | description: vector, 167 | url: vector, 168 | dino_parent: &mut DinoNFT, 169 | ctx: &mut TxContext 170 | ) { 171 | let nft = mint(name, description, url, ctx); 172 | option::fill(&mut dino_parent.dino_egg, object::id(&nft)); 173 | transfer::transfer_to_object(nft, dino_parent); 174 | } 175 | 176 | /// Update the `description` of `nft` to `new_description` 177 | public entry fun update_description( 178 | nft: &mut DinoNFT, 179 | new_description: vector, 180 | _: &mut TxContext 181 | ) { 182 | nft.description = string::utf8(new_description) 183 | } 184 | 185 | /// Hatch the egg by changing the NFT's description, name and URL 186 | public entry fun hatch_egg( 187 | name: vector, 188 | description: vector, 189 | url: vector, 190 | egg_nft: &mut DinoNFT, 191 | _parent_nft: &mut DinoNFT, 192 | _: &mut TxContext 193 | ) { 194 | egg_nft.name = string::utf8(name); 195 | egg_nft.description = string::utf8(description); 196 | egg_nft.url = url::new_unsafe_from_bytes(url) 197 | } 198 | 199 | /// Retrieve the child NFT and send it to the message sender 200 | public entry fun retrieve_child_dino( 201 | child_nft: DinoNFT, 202 | parent_nft: &mut DinoNFT, 203 | ctx: &mut TxContext 204 | ) { 205 | option::extract(&mut parent_nft.dino_egg); 206 | transfer::transfer(child_nft, tx_context::sender(ctx)); 207 | } 208 | 209 | /// Permanently delete `nft` 210 | public entry fun burn(nft: DinoNFT, _: &mut TxContext) { 211 | let DinoNFT { id, name: _, description: _, url: _, dino_egg: _} = nft; 212 | object::delete(id) 213 | } 214 | 215 | /// Permanently delete `minterCap` 216 | public entry fun burn_cap(cap: MinterCap, _: &mut TxContext) { 217 | let MinterCap { id } = cap; 218 | object::delete(id) 219 | } 220 | 221 | /// Get the NFT's `name` 222 | public fun name(nft: &DinoNFT): &string::String { 223 | &nft.name 224 | } 225 | 226 | /// Get the NFT's `description` 227 | public fun description(nft: &DinoNFT): &string::String { 228 | &nft.description 229 | } 230 | 231 | /// Get the NFT's `url` 232 | public fun url(nft: &DinoNFT): &Url { 233 | &nft.url 234 | } 235 | 236 | #[test_only] public fun init_for_testing(ctx: &mut TxContext) { init(ctx) } 237 | } 238 | 239 | 240 | #[test_only] 241 | module sui_intro_workshop::dino_nftTests { 242 | use sui_intro_workshop::dino_nft::{Self, DinoNFT, MinterCap}; 243 | use sui::test_scenario::{Self, next_tx, ctx}; 244 | use sui::transfer; 245 | use std::string; 246 | 247 | #[test] 248 | fun mint_transfer_update() { 249 | let addr1 = @0xA; 250 | let addr2 = @0xB; 251 | // create the NFT 252 | let scenario = test_scenario::begin(&addr1); 253 | 254 | next_tx(&mut scenario, &addr1); 255 | { 256 | dino_nft::init_for_testing(ctx(&mut scenario)) 257 | }; 258 | next_tx(&mut scenario, &addr1); 259 | { 260 | let mintercap = test_scenario::take_owned(&mut scenario); 261 | dino_nft::owner_mint_to_account(&mintercap, b"test", b"a test", b"https://www.sui.io", test_scenario::ctx(&mut scenario)); 262 | dino_nft::burn_cap(mintercap, test_scenario::ctx(&mut scenario)) 263 | }; 264 | // send it from A to B 265 | test_scenario::next_tx(&mut scenario, &addr1); 266 | { 267 | let nft = test_scenario::take_owned(&mut scenario); 268 | transfer::transfer(nft, addr2); 269 | }; 270 | // update its description 271 | test_scenario::next_tx(&mut scenario, &addr2); 272 | { 273 | let nft = test_scenario::take_owned(&mut scenario); 274 | dino_nft::update_description(&mut nft, b"a new description", test_scenario::ctx(&mut scenario)) ; 275 | assert!(*string::bytes(dino_nft::description(&nft)) == b"a new description", 0); 276 | test_scenario::return_owned(&mut scenario, nft); 277 | }; 278 | // burn it 279 | test_scenario::next_tx(&mut scenario, &addr2); 280 | { 281 | let nft = test_scenario::take_owned(&mut scenario); 282 | dino_nft::burn(nft, test_scenario::ctx(&mut scenario)); 283 | } 284 | } 285 | } -------------------------------------------------------------------------------- /breakout_session_solutions/combined_solution.move: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /// A basic NFT example to demonstrate how to create and interact with a Dino NFT 5 | /// on Sui that can own and a child Dino NFT, illustrating basic parent-child object 6 | /// relationships and access control in Sui Move. 7 | /// 8 | /// Based on https://github.com/MystenLabs/sui/blob/94e5bca0f38def733d2a3bb45683834126069c43/crates/sui-framework/sources/devnet_nft.move 9 | /// 10 | module sui_intro_workshop::dino_nft { 11 | use sui::url::{Self, Url}; 12 | use std::string; 13 | use std::option::{Self, Option}; 14 | use sui::object::{Self, ID, UID}; 15 | use sui::coin::{Self, Coin}; 16 | use sui::balance::{Self, Balance}; 17 | use sui::event; 18 | use sui::transfer; 19 | use sui::tx_context::{Self, TxContext}; 20 | use sui::sui::SUI; 21 | 22 | /// For when paid amount is lower than the minting price. 23 | const ENotEnoughCoinPaid: u64 = 0; 24 | /// Nothing to withdraw error. 25 | const ENoProfits: u64 = 1; 26 | 27 | /// An example NFT that can be minted by anybody 28 | struct DinoNFT has key, store { 29 | id: UID, 30 | /// Name for the token 31 | name: string::String, 32 | /// Description of the token 33 | description: string::String, 34 | /// URL for the token 35 | url: Url, 36 | /// Dino egg optional field 37 | dino_egg: Option, 38 | } 39 | 40 | /// Type that marks the capability to mint new DinoNFT's. 41 | struct MinterCap has key { id: UID } 42 | 43 | /// Event marking when a DinoNFT has been minted 44 | struct MintNFTEvent has copy, drop { 45 | // The Object ID of the NFT 46 | object_id: ID, 47 | // The creator of the NFT 48 | creator: address, 49 | // The name of the NFT 50 | name: string::String, 51 | } 52 | 53 | struct MintingTreasury has key { 54 | id: UID, 55 | balance: Balance, 56 | mintingfee: u64 57 | } 58 | 59 | /// Module initializer is called once on module publish. 60 | /// Here we create only one instance of `MinterCap` and `MintingTreausury` and send it to the deployer account. 61 | fun init(ctx: &mut TxContext) { 62 | 63 | transfer::transfer(MinterCap { 64 | id: object::new(ctx) 65 | }, tx_context::sender(ctx)); 66 | 67 | transfer::share_object(MintingTreasury { 68 | id: object::new(ctx), 69 | balance: balance::zero(), 70 | mintingfee: 5000000 71 | }) 72 | } 73 | 74 | /// Private function that creates and returns a new DinoNFT 75 | fun mint( 76 | name: vector, 77 | description: vector, 78 | url: vector, 79 | ctx: &mut TxContext 80 | ): DinoNFT { 81 | let nft = DinoNFT { 82 | id: object::new(ctx), 83 | name: string::utf8(name), 84 | description: string::utf8(description), 85 | url: url::new_unsafe_from_bytes(url), 86 | // Set the dino_egg field to None 87 | dino_egg: option::none() 88 | }; 89 | let sender = tx_context::sender(ctx); 90 | event::emit(MintNFTEvent { 91 | object_id: object::uid_to_inner(&nft.id), 92 | creator: sender, 93 | name: nft.name, 94 | }); 95 | nft 96 | } 97 | 98 | /// Paid public mint to an account 99 | public entry fun mint_to_account( 100 | mintingtreasury: &mut MintingTreasury, 101 | name: vector, 102 | description: vector, 103 | url: vector, 104 | fee: Coin, 105 | ctx: &mut TxContext 106 | ) { 107 | 108 | let paid_value = coin::value(&fee); 109 | assert!(paid_value >= mintingtreasury.mintingfee, ENotEnoughCoinPaid); 110 | // Transfer back any extra 111 | if (paid_value > mintingtreasury.mintingfee) { 112 | coin::split(&mut fee, paid_value - mintingtreasury.mintingfee, ctx); 113 | }; 114 | // Add a payment to the minting treasury balance 115 | balance::join(&mut mintingtreasury.balance, coin::into_balance(fee)); 116 | // Mint the NFT and transfer to sender 117 | let nft = mint(name, description, url, ctx); 118 | transfer::transfer(nft, tx_context::sender(ctx)); 119 | } 120 | 121 | /// Paid public mint function for minting a DinoNFT and set it as a child to a parent object 122 | public entry fun mint_to_object( 123 | mintingtreasury: &mut MintingTreasury, 124 | name: vector, 125 | description: vector, 126 | url: vector, 127 | dino_parent: &mut DinoNFT, 128 | fee: Coin, 129 | ctx: &mut TxContext 130 | ) { 131 | let paid_value = coin::value(&fee); 132 | assert!(paid_value >= mintingtreasury.mintingfee, ENotEnoughCoinPaid); 133 | // Transfer back any extra 134 | if (paid_value > mintingtreasury.mintingfee) { 135 | coin::split(&mut fee, paid_value - mintingtreasury.mintingfee, ctx); 136 | }; 137 | // add a payment to the minting treasury balance 138 | balance::join(&mut mintingtreasury.balance, coin::into_balance(fee)); 139 | // Mint the NFT and transfer to parent object 140 | let nft = mint(name, description, url, ctx); 141 | option::fill(&mut dino_parent.dino_egg, object::id(&nft)); 142 | transfer::transfer_to_object(nft, dino_parent); 143 | } 144 | 145 | /// Privileged mint a DinoNFT to an account 146 | public entry fun owner_mint_to_account( 147 | _: &MinterCap, 148 | name: vector, 149 | description: vector, 150 | url: vector, 151 | ctx: &mut TxContext 152 | ) { 153 | let nft = mint(name, description, url, ctx); 154 | transfer::transfer(nft, tx_context::sender(ctx)); 155 | } 156 | 157 | /// Privileged mint a DinoNFT and set it as a child to a parent object 158 | public entry fun owner_mint_to_object( 159 | _: &MinterCap, 160 | name: vector, 161 | description: vector, 162 | url: vector, 163 | dino_parent: &mut DinoNFT, 164 | ctx: &mut TxContext 165 | ) { 166 | let nft = mint(name, description, url, ctx); 167 | option::fill(&mut dino_parent.dino_egg, object::id(&nft)); 168 | transfer::transfer_to_object(nft, dino_parent); 169 | } 170 | 171 | /// Withdraw the balance in Minting Treasury to the owner of MinterCap 172 | public entry fun owner_withdraw( 173 | _: &MinterCap, 174 | mintingtreasury: &mut MintingTreasury, 175 | ctx: &mut TxContext 176 | ) { 177 | let amount = balance::value(&mintingtreasury.balance); 178 | assert!(amount > 0, ENoProfits); 179 | // Take a transferable `Coin` from a `Balance` 180 | let coin = coin::take(&mut mintingtreasury.balance, amount, ctx); 181 | transfer::transfer(coin, tx_context::sender(ctx)); 182 | } 183 | 184 | /// Update the `description` of `nft` to `new_description` 185 | public entry fun update_description( 186 | nft: &mut DinoNFT, 187 | new_description: vector, 188 | _: &mut TxContext 189 | ) { 190 | nft.description = string::utf8(new_description) 191 | } 192 | 193 | /// Hatch the egg by changing the NFT's description, name and URL 194 | public entry fun hatch_egg( 195 | name: vector, 196 | description: vector, 197 | url: vector, 198 | egg_nft: &mut DinoNFT, 199 | _parent_nft: &mut DinoNFT, 200 | _: &mut TxContext 201 | ) { 202 | egg_nft.name = string::utf8(name); 203 | egg_nft.description = string::utf8(description); 204 | egg_nft.url = url::new_unsafe_from_bytes(url) 205 | } 206 | 207 | /// Retrieve the child NFT and send it to the message sender 208 | public entry fun retrieve_child_dino( 209 | child_nft: DinoNFT, 210 | parent_nft: &mut DinoNFT, 211 | ctx: &mut TxContext 212 | ) { 213 | option::extract(&mut parent_nft.dino_egg); 214 | transfer::transfer(child_nft, tx_context::sender(ctx)); 215 | } 216 | 217 | /// Permanently delete `nft` 218 | public entry fun burn(nft: DinoNFT, _: &mut TxContext) { 219 | let DinoNFT { id, name: _, description: _, url: _, dino_egg: _} = nft; 220 | object::delete(id) 221 | } 222 | 223 | /// Permanently delete `minterCap` 224 | public entry fun burn_cap(cap: MinterCap, _: &mut TxContext) { 225 | let MinterCap { id } = cap; 226 | object::delete(id) 227 | } 228 | 229 | /// Get the NFT's `name` 230 | public fun name(nft: &DinoNFT): &string::String { 231 | &nft.name 232 | } 233 | 234 | /// Get the NFT's `description` 235 | public fun description(nft: &DinoNFT): &string::String { 236 | &nft.description 237 | } 238 | 239 | /// Get the NFT's `url` 240 | public fun url(nft: &DinoNFT): &Url { 241 | &nft.url 242 | } 243 | 244 | #[test_only] public fun init_for_testing(ctx: &mut TxContext) { init(ctx) } 245 | } 246 | 247 | 248 | #[test_only] 249 | module sui_intro_workshop::dino_nftTests { 250 | use sui_intro_workshop::dino_nft::{Self, DinoNFT, MinterCap}; 251 | use sui::test_scenario::{Self, next_tx, ctx}; 252 | use sui::transfer; 253 | use std::string; 254 | 255 | #[test] 256 | fun mint_transfer_update() { 257 | let addr1 = @0xA; 258 | let addr2 = @0xB; 259 | // create the NFT 260 | let scenario = test_scenario::begin(&addr1); 261 | 262 | next_tx(&mut scenario, &addr1); 263 | { 264 | dino_nft::init_for_testing(ctx(&mut scenario)) 265 | }; 266 | next_tx(&mut scenario, &addr1); 267 | { 268 | let mintercap = test_scenario::take_owned(&mut scenario); 269 | dino_nft::owner_mint_to_account(&mintercap, b"test", b"a test", b"https://www.sui.io", test_scenario::ctx(&mut scenario)); 270 | dino_nft::burn_cap(mintercap, test_scenario::ctx(&mut scenario)) 271 | }; 272 | // send it from A to B 273 | test_scenario::next_tx(&mut scenario, &addr1); 274 | { 275 | let nft = test_scenario::take_owned(&mut scenario); 276 | transfer::transfer(nft, addr2); 277 | }; 278 | // update its description 279 | test_scenario::next_tx(&mut scenario, &addr2); 280 | { 281 | let nft = test_scenario::take_owned(&mut scenario); 282 | dino_nft::update_description(&mut nft, b"a new description", test_scenario::ctx(&mut scenario)) ; 283 | assert!(*string::bytes(dino_nft::description(&nft)) == b"a new description", 0); 284 | test_scenario::return_owned(&mut scenario, nft); 285 | }; 286 | // burn it 287 | test_scenario::next_tx(&mut scenario, &addr2); 288 | { 289 | let nft = test_scenario::take_owned(&mut scenario); 290 | dino_nft::burn(nft, test_scenario::ctx(&mut scenario)); 291 | } 292 | } 293 | } --------------------------------------------------------------------------------